diff options
79 files changed, 1472 insertions, 1142 deletions
@@ -1,15 +1,7 @@ -# build output -bin/ +# everything built or generated lives in build/ (make clean removes it) +/build/ + +# stray objects and executables from building by hand *.o *.obj *.exe - -# built programs -/bin/ -/build/ -/src/web/zc.wasm - -# the editor's packaged server: layout, vm and language-server image, plus the -# wasm server. `make vsix` stages it into editors/vscode/server/ only while it -# packages, so nothing built is kept in the tree. -/editors/vscode/server/ @@ -1,27 +1,30 @@ # tinyvm and zc. `make` builds the tools, the interpreter, the microcore and the -# language server into bin/. nothing built is kept in the tree. +# language server into build/bin/. everything built or generated goes under +# build/; the tree holds sources only, and `make clean` (rm -rf build) returns it +# to a fresh checkout. # # make everything below that is a program # make test the whole suite (tests/test.sh) # make check the minimal suite, for iterating (tests/test.sh -q) # make test JOBS=4 the same, four sections at a time -# make genome rebuild build/core/genome.h from src/ and check the fixpoint -# make lsp bin/zclsp, the language server -# make web src/web/zc.wasm (needs clang with the wasm32 target) -# make js build/js/genome.json and the vm, for the javascript vm +# make genome build/core/genome.bin from src/, with the fixpoint check +# 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 # make run PROG=examples/hello.zc # compile one program into a run-only image on a memory # map sized to it (src/tools/automap.sh) -> build/run/zymr # make vsix the vscode extension, server and all -# make clean remove bin/ and build/ and the web build +# make clean remove build/ # -# nothing built is kept in the tree: the genome, the javascript vm and its -# layout, and the packed editor server all land in build/ (the web build keeps -# its historical src/web/zc.wasm path because the page loads it by name). -# -# CC picks the compiler (cc by default; clang works too). +# the cores take their genome in with c23 #embed (src/core/genome.h), so CC has +# to be clang 19 or gcc 15 or later. src/tools/findcc.sh picks one when CC is +# not given on the command line or in the environment. -CC ?= cc +ifeq ($(origin CC),default) + CC := $(shell sh src/tools/findcc.sh || echo cc) +endif +export CC CFLAGS ?= -O2 -w -fwrapv ifeq ($(OS),Windows_NT) EXE := .exe @@ -35,99 +38,105 @@ else MICRO_THREADS := -DZY_THREADS endif -BIN := bin B := build +BIN := $(B)/bin JS := $(B)/js -# the hand-written core headers, then the generated genome beside them -COREINC := -Isrc/core -I$(B)/core -# the sources the compiler genome is built from +WEB := $(B)/web +# the default memory map, generated from the const blocks in the zc sources +MAP := $(B)/core/map +# hand-written core headers, the generated map, and where #embed finds the genome image +COREINC := -Isrc/core -I$(MAP) --embed-dir=$(B)/core +CORE_H := src/core/genome.h src/core/fops.h src/core/bops.h src/core/stack.h $(MAP)/memmap.h +# the sources the compiler genome is built from, and the files it embeds ZC_SRC := src/runtime/rt.zc src/core/core.zc src/compiler/compiler.zc +ZC_DEPS := $(ZC_SRC) src/lib/std.zc src/lib/icon.zc src/tools/build.zc TOOLS := $(BIN)/zc0$(EXE) $(BIN)/mkgenome$(EXE) $(BIN)/genmap$(EXE) PROGS := $(BIN)/zy$(EXE) $(BIN)/zym$(EXE) $(BIN)/zclsp$(EXE) all: $(TOOLS) $(PROGS) -$(BIN) $(B): +$(BIN): mkdir -p $@ -$(BIN)/zc0$(EXE): src/tools/zc0.c src/core/memmap.h src/core/fops.h src/core/bops.h | $(BIN) - $(CC) -O1 -w -fwrapv src/tools/zc0.c -Isrc/core -o $@ -$(BIN)/mkgenome$(EXE): src/tools/mkgenome.c src/core/memmap.h | $(BIN) - $(CC) -O1 -w -fwrapv src/tools/mkgenome.c -Isrc/core -o $@ +# genmap needs no map of its own; everything else is built against the one it writes $(BIN)/genmap$(EXE): src/tools/genmap.c | $(BIN) - $(CC) $(CFLAGS) src/tools/genmap.c -Isrc/core -o $@ + $(CC) $(CFLAGS) src/tools/genmap.c -o $@ +$(MAP)/memmap.h: $(BIN)/genmap$(EXE) src/runtime/rt.zc src/compiler/compiler.zc + mkdir -p $(B)/core + $(BIN)/genmap$(EXE) --preset default --check -o $(MAP) >$(B)/core/genmap.log 2>&1 || { cat $(B)/core/genmap.log; exit 1; } +$(BIN)/zc0$(EXE): src/tools/zc0.c src/core/fops.h src/core/bops.h $(MAP)/memmap.h | $(BIN) + $(CC) -O1 -w -fwrapv src/tools/zc0.c -Isrc/core -I$(MAP) -o $@ +$(BIN)/mkgenome$(EXE): src/tools/mkgenome.c $(MAP)/memmap.h | $(BIN) + $(CC) -O1 -w -fwrapv src/tools/mkgenome.c -I$(MAP) -o $@ + +# bootstrap: zc0 compiles the compiler to g0, g0 compiles it to g1, g1 to g2. +# g1 and g2 must be identical (the fixpoint), and g1 becomes build/core/genome.bin. +# each stage's core embeds the image beside it, so no stage reads another's. +genome: $(B)/core/genome.bin +$(B)/core/genome.bin: $(ZC_DEPS) $(BIN)/zc0$(EXE) $(BIN)/mkgenome$(EXE) src/core/core.c $(IO) $(CORE_H) + mkdir -p $(B)/s0 $(B)/s1 $(B)/core + cat $(ZC_SRC) | $(BIN)/zc0$(EXE) > $(B)/s0/g0.bin + $(BIN)/mkgenome$(EXE) < $(B)/s0/g0.bin > $(B)/s0/genome.bin + $(CC) $(CFLAGS) -DJTYPE=int src/core/core.c $(IO) -Isrc/core -I$(MAP) --embed-dir=$(B)/s0 -o $(B)/s0/zy0$(EXE) + (cat src/tools/build.zc; printf '`'; cat $(ZC_SRC)) | $(B)/s0/zy0$(EXE) > $(B)/s1/g1.bin + $(BIN)/mkgenome$(EXE) < $(B)/s1/g1.bin > $(B)/s1/genome.bin + $(CC) $(CFLAGS) -DJTYPE=int src/core/core.c $(IO) -Isrc/core -I$(MAP) --embed-dir=$(B)/s1 -o $(B)/s1/zy1$(EXE) + (cat src/tools/build.zc; printf '`'; cat $(ZC_SRC)) | $(B)/s1/zy1$(EXE) > $(B)/s1/g2.bin + cmp $(B)/s1/g1.bin $(B)/s1/g2.bin + cp $(B)/s1/genome.bin $@ + @echo "fixpoint: $$(( $$(wc -c < $@) / 8 )) words, $@ updated" # the interpreter: the vm core with the compiler genome inside -$(BIN)/zy$(EXE): src/core/core.c $(IO) $(B)/core/genome.h src/core/memmap.h src/core/fops.h src/core/bops.h src/core/stack.h | $(BIN) +$(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 portable microcore -$(BIN)/zym$(EXE): src/micro/micro.c src/micro/micro_port.h $(PORT) $(B)/core/genome.h src/core/memmap.h src/core/fops.h src/core/bops.h src/core/stack.h | $(BIN) +$(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 $@ # the language server is the compiler in check mode plus src/lsp/server.zc, on a map # sized to it +LSP_DEPS := src/core/core.c src/core/io_linux.c $(filter-out $(MAP)/memmap.h,$(CORE_H)) $(ZC_DEPS) src/lsp/server.zc \ + src/tools/zc0.c src/tools/genmap.c src/tools/mkgenome.c src/tools/automap.sh src/tools/mklsp.sh lsp: $(BIN)/zclsp$(EXE) -$(BIN)/zclsp$(EXE): src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h src/core/memmap.h src/runtime/rt.zc src/core/core.zc src/compiler/compiler.zc src/lsp/server.zc src/lib/std.zc src/lib/icon.zc src/tools/embed.sh src/tools/zc0.c src/tools/genmap.c src/tools/automap.sh src/tools/mklsp.sh | $(BIN) - CC="$(CC)" bash src/tools/mklsp.sh $(B)/lsp +$(BIN)/zclsp$(EXE): $(LSP_DEPS) | $(BIN) + bash src/tools/mklsp.sh $(B)/lsp cp $(B)/lsp/zclsp$(EXE) $@ -src/compiler/compiler.zc: src/lib/std.zc src/lib/icon.zc src/tools/embed.sh - bash src/tools/embed.sh all - -# bootstrap: zc0 compiles the compiler to g0, g0 compiles it to g1, g1 to g2. -# g1 and g2 must be identical (the fixpoint), and g1 becomes build/core/genome.h. -# run this by hand after editing a .zc source; `make check` reports a stale genome. -genome: $(BIN)/zc0$(EXE) $(BIN)/mkgenome$(EXE) src/compiler/compiler.zc | $(B) - mkdir -p $(B)/s0 $(B)/s1 $(B)/core - cp src/core/core.c $(IO) src/core/memmap.h src/core/fops.h src/core/bops.h src/core/stack.h $(B)/s0/ - cp src/core/core.c $(IO) src/core/memmap.h src/core/fops.h src/core/bops.h src/core/stack.h $(B)/s1/ - cat $(ZC_SRC) | $(BIN)/zc0$(EXE) > $(B)/s0/genome.h - $(CC) $(CFLAGS) -DJTYPE=int $(B)/s0/core.c $(B)/s0/$(notdir $(IO)) -o $(B)/zy0$(EXE) - (cat src/tools/build.zc; printf '`'; cat $(ZC_SRC)) | $(B)/zy0$(EXE) > $(B)/g1.bin - $(BIN)/mkgenome$(EXE) < $(B)/g1.bin > $(B)/s1/genome.h - $(CC) $(CFLAGS) -DJTYPE=int $(B)/s1/core.c $(B)/s1/$(notdir $(IO)) -o $(B)/zy1$(EXE) - (cat src/tools/build.zc; printf '`'; cat $(ZC_SRC)) | $(B)/zy1$(EXE) > $(B)/g2.bin - cmp $(B)/g1.bin $(B)/g2.bin - cp $(B)/s1/genome.h $(B)/core/genome.h - @echo "fixpoint: $$(( $$(wc -c < $(B)/g1.bin) / 8 )) words, $(B)/core/genome.h updated" - -# a fresh clone has no genome, so bootstrap one on demand. it is deliberately -# not rebuilt from source dependencies: the fixpoint run is expensive, and -# `make genome` is the explicit way to refresh it after editing a .zc file. -$(B)/core/genome.h: - $(MAKE) genome - -js: $(JS)/genome.json $(JS)/layout.js $(JS)/tinyvm.js +# the javascript vm: tinyvm.js loads genome.bin from beside itself +js: $(JS)/tinyvm.js $(JS)/layout.js $(JS)/genome.bin $(JS): mkdir -p $@ -$(JS)/genome.json: $(B)/core/genome.h src/core/memmap.h src/js/extract_genome.js src/js/mapvalues.js | $(JS) - node src/js/extract_genome.js -$(JS)/layout.ts $(JS)/layout.js: $(JS)/genome.json - @test -f $@ || node src/js/extract_genome.js +$(JS)/genome.bin: $(B)/core/genome.bin | $(JS) + cp $< $@ +$(JS)/layout.js: $(MAP)/memmap.h src/js/mklayout.js src/js/mapvalues.js | $(JS) + node src/js/mklayout.js --map $(MAP)/memmap.h --out $(JS) $(JS)/tinyvm.js: src/js/tinyvm.ts src/js/layout.d.ts | $(JS) tsc src/js/tinyvm.ts --target es2020 --lib es2020,dom --module commonjs --outDir $(JS) # the language server image for the javascript vm, and the wasm server -lspimage: $(JS)/lsp.json -$(JS)/lsp.json: $(BIN)/zclsp$(EXE) src/js/extract_lsp.js src/js/mapvalues.js | $(JS) +lspimage: $(JS)/lsp.bin +$(JS)/lsp.bin: $(BIN)/zclsp$(EXE) src/js/extract_lsp.js src/js/mapvalues.js | $(JS) node src/js/extract_lsp.js # the vscode extension, with the server and its image inside it -vsix: | $(B) +vsix: bash src/tools/mkvsix.sh -web: src/web/zc.wasm -src/web/zc.wasm: src/micro/micro.c src/micro/micro_port.h src/web/port_wasm.c src/web/build.sh $(B)/core/genome.h src/core/memmap.h src/core/fops.h src/core/bops.h src/core/stack.h +# the browser compiler: the page and its loader beside the wasm build +web: $(WEB)/zc.wasm $(WEB)/index.html $(WEB)/zc.js +$(WEB)/zc.wasm: src/micro/micro.c src/micro/micro_port.h src/web/port_wasm.c src/web/build.sh $(B)/core/genome.bin $(CORE_H) bash src/web/build.sh $@ +$(WEB)/index.html $(WEB)/zc.js: $(WEB)/%: src/web/% + mkdir -p $(WEB) + cp $< $@ # one program as a run-only image: the runtime, the program, and a map exactly # big enough for them -run: | $(B) +run: @test -n "$(PROG)" || { echo "make run PROG=examples/hello.zc"; exit 2; } bash src/tools/automap.sh run $(B)/run src/runtime/run.zc $(PROG) - cp src/micro/micro.c src/micro/micro_port.h $(PORT) src/core/fops.h src/core/bops.h src/core/stack.h $(B)/run/ - $(CC) $(CFLAGS) $(MICRO_THREADS) $(B)/run/micro.c $(B)/run/$(notdir $(PORT)) -I$(B)/run -o $(B)/run/zymr$(EXE) + $(CC) $(CFLAGS) $(MICRO_THREADS) src/micro/micro.c $(PORT) -Isrc/core -Isrc/micro -I$(B)/run --embed-dir=$(B)/run -o $(B)/run/zymr$(EXE) @echo "built $(B)/run/zymr$(EXE)" # JOBS sets how many sections run at once: make test JOBS=4 @@ -139,6 +148,6 @@ check: bash tests/test.sh -q -j $(or $(JOBS),2) clean: - rm -rf $(BIN) $(B) src/web/zc.wasm + rm -rf $(B) .PHONY: all lsp genome js lspimage vsix web run test check clean diff --git a/editors/vscode/client/extension.js b/editors/vscode/client/extension.js index 3846eca..cd497d8 100644 --- a/editors/vscode/client/extension.js +++ b/editors/vscode/client/extension.js @@ -185,7 +185,7 @@ function shortKey(key) { function serverStampFor(bin) { if (bin) return fileStamp(bin); const dir = path.join(__dirname, '..', 'server'); - return sha256(['lsp.wasm', 'lsp.json', 'tinyvm.js'].map(f => fileStamp(path.join(dir, f))).join(',')); + return sha256(['lsp.wasm', 'lsp.json', 'lsp.bin', 'tinyvm.js'].map(f => fileStamp(path.join(dir, f))).join(',')); } // the key of a document as it is now, and what it reads (for checking it again when one of those changes) diff --git a/editors/vscode/client/vmproc.js b/editors/vscode/client/vmproc.js index 8ce8c52..6b57554 100644 --- a/editors/vscode/client/vmproc.js +++ b/editors/vscode/client/vmproc.js @@ -16,6 +16,7 @@ function vmProcess(dir, opts = {}) { const log = opts.log || (() => {}); const { TinyVM } = require(path.join(dir, 'tinyvm.js')); const image = require(path.join(dir, 'lsp.json')); + const words = require('fs').readFileSync(path.join(dir, 'lsp.bin')); const proc = new EventEmitter(); const out = new EventEmitter(); const err = new EventEmitter(); @@ -33,8 +34,8 @@ function vmProcess(dir, opts = {}) { err.emit('data', Buffer.from(m, 'utf8')); }, }); - vm.loadGenome(image.words, image.gorg); - log('message', 'loaded the image: ' + image.words.length + ' words, code at ' + image.gorg); + const loaded = vm.loadImage(words, image.gorg); + log('message', 'loaded the image: ' + loaded + ' words, code at ' + image.gorg); const flush = () => { if (pending.length) { diff --git a/examples/quine.zc b/examples/quine.zc index 58a5351..3fb563b 100644 --- a/examples/quine.zc +++ b/examples/quine.zc @@ -1,4 +1 @@ -namespace; -include rt; - -let s="let s=#;void main(){let i=0;while(s[i]){if(s[i]==35){put_c(34);put_s(s);put_c(34);}else put_c(s[i]);i++;}put_c(10);return;}";void main(){let i=0;while(s[i]){if(s[i]==35){rt.put_c(34);rt.put_s(s);rt.put_c(34);}else rt.put_c(s[i]);i++;}rt.put_c(10);return;} +namespace;include rt;let s="namespace;include rt;let s=#;void main(){let i=0;while(s[i]){if(s[i]==35){rt.put_c(34);rt.put_s(s);rt.put_c(34);}else rt.put_c(s[i]);i++;}rt.put_c(10);return;}";void main(){let i=0;while(s[i]){if(s[i]==35){rt.put_c(34);rt.put_s(s);rt.put_c(34);}else rt.put_c(s[i]);i++;}rt.put_c(10);return;} diff --git a/src/baremetal/run.sh b/src/baremetal/run.sh index 467e86e..43f8a4c 100755 --- a/src/baremetal/run.sh +++ b/src/baremetal/run.sh @@ -6,11 +6,12 @@ # src/baremetal/run.sh riscv|aarch64 compile examples/hello.zc # run: the program is compiled into the image. nothing is read. # io: the same image, but stdin is the program's input and 0x04 ends it. -# compile: the default 1,048,576-word genome. the source, a backtick, and +# compile: the default compiler genome (build/core/genome.bin, from make genome). the source, a backtick, and # 0x04 are written to the UART. the backtick ends the unit; the # 0x04 is what eof() waits for after that. # Prints the program's UART output and nothing else. Exits 0 on a clean -# power-off. A missing toolchain exits 0 from `ready` and 1 otherwise. +# power-off. A missing toolchain exits 0 from `ready` and 1 otherwise. the cross +# compilers need c23 #embed (gcc 15 or later) to take in the genome. set -euo pipefail case "${1:-}" in --help|-h) printf '%s\n' 'usage: src/baremetal/run.sh ready riscv|aarch64' ' src/baremetal/run.sh riscv|aarch64 run|io|compile FILE.zc' 'builds a freestanding microcore and boots it in qemu-system.' 'run compiles the program into the image; io also forwards program input.' 'compile sends source to the resident compiler over the uart.' 'ready checks for the cross compiler and emulator.'; exit 0 ;; @@ -66,9 +67,9 @@ common="$cflags -ffreestanding -fno-pic -fno-pie -fno-builtin -fno-unwind-tables if [ "$mode" = run ] || [ "$mode" = io ]; then # the map is sized to the program, so a bigger runtime or program just gets a bigger board - gcc -O2 -w src/tools/genmap.c -o "$D/genmap" + ${CC:-$(sh src/tools/findcc.sh)} -O2 -w src/tools/genmap.c -o "$D/genmap" GENMAP="$D/genmap" bash src/tools/automap.sh run "$D/img" src/runtime/run.zc "$src" >"$D/genmap.log" 2>&1 || { cat "$D/genmap.log" >&2; exit 1; } - cp src/micro/micro.c src/micro/micro_port.h src/core/fops.h src/core/bops.h src/core/stack.h "$D/img/" + cp src/micro/micro.c src/micro/micro_port.h src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h "$D/img/" "$cc" $common $def -I"$D/img" -c "$D/img/micro.c" -o "$D/micro.o" if [ "$mode" = io ]; then "$cc" $common $def -DBARE_INPUT=1 -I"$D/img" -c src/baremetal/port.c -o "$D/port.o" @@ -78,7 +79,8 @@ if [ "$mode" = run ] || [ "$mode" = io ]; then timeout=120 fi elif [ "$mode" = compile ]; then - cp src/micro/micro.c src/micro/micro_port.h src/core/memmap.h build/core/genome.h src/core/fops.h src/core/bops.h src/core/stack.h "$D/" + [ -f build/core/genome.bin ] || { echo "baremetal: no build/core/genome.bin; run make genome first" >&2; exit 1; } + cp src/micro/micro.c src/micro/micro_port.h build/core/map/memmap.h build/core/genome.bin src/core/genome.h src/core/fops.h src/core/bops.h src/core/stack.h "$D/" "$cc" $common $def -I"$D" -c "$D/micro.c" -o "$D/micro.o" "$cc" $common $def -DBARE_INPUT=1 -I"$D" -c src/baremetal/port.c -o "$D/port.o" timeout=90 diff --git a/src/compiler/compiler.zc b/src/compiler/compiler.zc index c33005d..244efc5 100644 --- a/src/compiler/compiler.zc +++ b/src/compiler/compiler.zc @@ -256,6 +256,7 @@ int s_star, s_slash, s_amp, s_colon, s_hash, s_minus, s_andand, s_oror; int s_dot, s_fn, s_var, s_const, s_else, s_as, s_unchecked, s_immediate, s_m, s_d, s_u; int s_q, s_case, s_default, s_fovf, s_start, s_main, s_free, s_recycle, s_own, s_qnew, s_enhance, s_micro, s_vm; int s_frame_floor, s_frame_top, s_frame_node, s_frame_busy, s_frame_guard, s_frame_grow, s_frame_leave; +int s_copy_w; int s_do, s_while, s_enum, s_include, s_memdebug, s_scrub, s_alloc, s_check, s_memset, s_static; //: ---- types: kinds, tables and parser state (see the types section) ---- @@ -1067,6 +1068,21 @@ int module_imported(char* name) { while (p) { if (rt.eq_w(p[1], name)) return 1; p = p[0]; } return 0; } +// modules reported missing in this unit, so their names are not reported again as undefined +word missing_imports; +// true when name is qualified by a module this unit was already told to include. +int missing_import_name(char* name) { + int i = 0; word p = missing_imports; + while (name[i] && name[i] != '.') i++; + if (!name[i]) return 0; + while (p) { + char* module = p[1]; int k = 0; + while (k < i && module[k] == name[k]) k++; + if (k == i && !module[k]) return 1; + p = p[0]; + } + return 0; +} // source references require their module's explicit import; compiler-generated calls use lookup directly. int source_lookup(char* name) { int i = 0, s; char* owner; @@ -1074,7 +1090,9 @@ int source_lookup(char* name) { if (name[i]) { owner = chk_resize(0, 0, i + 1); rt.copy_w(owner, name, i); owner[i] = 0; if ((module_known(owner) || module_loaded(owner)) && !module_imported(owner)) { - errat(); rt.put_s("module access requires `include "); rt.put_s(owner); rt.put_s(";`"); errend(); return -1; + errat(); rt.put_s("module access requires `include "); rt.put_s(owner); rt.put_s(";`"); errend(); + word link = chk_resize(0, 0, 2); link[0] = missing_imports; link[1] = owner; missing_imports = link; + // the unit already fails; resolving the name anyway keeps later diagnostics about the real code } } if (module_name) { s = lookup_raw(module_symbol(name)); if (s >= 0) return s; } @@ -4532,37 +4550,22 @@ int cover_warn(int e, int cov, int sl) { return 0; } //: ---- include ---- -//: the text of the embedded std library, one character to a word and ending in 0, as readfile gives a file. -//: it is built from the pieces src/tools/embed.sh put at the end of this file, the first time it is asked for +//: the text of an embedded library, one character to a word and ending in 0, as readfile gives a file. +//: it is unpacked from the byte string the first time it is asked for. +char* bstr_text(bstr p) { + int n = rt.len_s(p), i = 0; char* b = m*rt.new(n + 2); + while (i < n) { b[i] = rt.at_s(p, i); i++; } + b[n] = 0; + return b; +} +// the embedded std module. char* std_text() { - int n = 0, i = 0, k = 0, j; bstr p; char* b; - if (std_buf) return std_buf; - while (1) { p = std_part(i); if (!p) break; n = n + rt.len_s(p); i++; } - b = rt.new(n + 2); - i = 0; - while (1) { - p = std_part(i); if (!p) break; - j = 0; while (j < rt.len_s(p)) { b[k] = rt.at_s(p, j); k++; j++; } - i++; - } - b[k] = 0; - std_buf = b; + if (!std_buf) std_buf = bstr_text(std_source()); return std_buf; } -// the embedded icon module, assembled from its source pieces on first use. +// the embedded icon module. char* icon_text() { - int n = 0, i = 0, k = 0, j; bstr p; char* b; - if (icon_buf) return icon_buf; - while (1) { p = icon_part(i); if (!p) break; n = n + rt.len_s(p); i++; } - b = rt.new(n + 2); - i = 0; - while (1) { - p = icon_part(i); if (!p) break; - j = 0; while (j < rt.len_s(p)) { b[k] = rt.at_s(p, j); k++; j++; } - i++; - } - b[k] = 0; - icon_buf = b; + if (!icon_buf) icon_buf = bstr_text(icon_source()); return icon_buf; } // a path with its . and .. segments and doubled slashes taken out: /a/b/../c/./d.zc is /a/c/d.zc @@ -4714,6 +4717,120 @@ int kw_include() { inc_compile(buf, incres, inc_dir(incres)); return 0; } +// embed "path" as [type] name; declares name with the bytes of the file at path, read +// when the unit is compiled. the path is found the way an include's is. with no type, or +// bstr, name is a byte string of the file; char* gives one character to a word; a number +// type takes the bytes little-endian (a float or f32 needs exactly its size); T[] and +// T[n] lay the bytes out as the array's elements, packed for the sized types, one byte +// to an element for char[]. at the top level name is a global, in a function a local. +int kw_embed() { + int pp, off, n, t = 0, i, w, count, k, a, j, v, sz, skip = -1; char* path; word data; + stmtonly(); + next(); + pp = tpack(); + if (tk != T_STR) { die("embed takes a path in quotes: embed \"file\" as name;"); return 0; } + path = tv; next(); + if (!is_id(s_as)) { die("expected as: embed \"file\" as [type] name;"); return 0; } + next(); + peekat(0); + if (!(tk == T_ID && pk_tk == T_OP && pk_tv == s_semi)) t = ptype(); + off = toff; n = name(); + expect(s_semi); + data = m*embed_open(path); + if (!data) { int sv = tpack(); tunpack(pp); serr("can't read the file named by embed"); tunpack(sv); data = chk_resize(0, 0, 1); } + sz = data[0]; + if (!t) t = tbstr; + // in a function the data sits in the code stream, so the code jumps over it + if (infn && (t == tbstr || t == tptr(tchar) || t[0] == TY_ARR)) { reserve_code(2); skip = hole(); op(O_JMP); } + if (t == tbstr || t == tptr(tchar)) { + if (t == tbstr) { a = embed_bstr(data); } + else { reserve_code(sz + 1); a = here; i = 0; while (i < sz) { emit(data[1 + i]); i++; } emit(0); } + if (skip >= 0) setlit(skip, here); + return embed_bind(n, off, t, a, 0); + } + if (t[0] == TY_ARR) { + w = embed_width(t[4]); + if (!w) { serr("embed declares a bstr, a char*, a number, or an array of numbers"); if (skip >= 0) setlit(skip, here); return 0; } + if (sz % w) { serr("the embedded file's size is not a whole number of array elements"); if (skip >= 0) setlit(skip, here); return 0; } + count = sz / w; + if (t[3] >= 0 && count > t[3]) { serr("the embedded file is larger than the array it is declared as"); if (skip >= 0) setlit(skip, here); return 0; } + if (t[3] < 0) t = tarr(t[4], count); + k = tsz(t); reserve_code(k); a = here; i = 0; + if (t[4][0] == TY_CHAR) { while (i < k) { if (i < sz) emit(data[1 + i]); else emit(0); i++; } } + else { + while (i < k) { + v = 0; j = 0; + while (j < 8) { if (i * 8 + j < sz) v = v | (data[1 + i * 8 + j] << (j * 8)); j++; } + emit(v); i++; + } + } + if (skip >= 0) setlit(skip, here); + return embed_bind(n, off, t, a, 1); + } + w = embed_width(t); + if (!w) { serr("embed declares a bstr, a char*, a number, or an array of numbers"); return 0; } + if (sz > w) { serr("the embedded file is larger than the type it is declared as"); return 0; } + if (isfl(t) && sz != w) { serr("an embedded float takes exactly as many bytes as its type"); return 0; } + v = 0; i = 0; + while (i < sz) { v = v | (data[1 + i] << (i * 8)); i++; } + if (isfl(t) && w == 4) v = rt.s2f(v); + else v = cnorm(t, v); + return embed_bind(n, off, t, v, 2); +} +// the bytes one element of type t takes in an embedded file: the packed size of a +// sized type or f32, 1 for char, 8 for the other number types, 0 when t is not a number. +int embed_width(word t) { + if (tbytes(t)) return tbytes(t); + if (t[0] == TY_CHAR) return 1; + if (t[0] == TY_INT || t[0] == TY_WORD || t[0] == TY_BOOL || t[0] == TY_FLOAT) return 8; + return 0; +} +// the bytes of block data ([n, b0, b1, ...]) as byte string storage in the code stream. +int embed_bstr(word data) { + int n = data[0], a, i = 0, w, k; + reserve_code((n >> 3) + 2); a = here; + emit(n); + while (i < n) { + w = 0; k = 0; + while (k < 8 && i < n) { w = w | (data[1 + i] << (k * 8)); k++; i++; } + emit(w); + } + if (!(n & 7)) emit(0); + return a; +} +// declare name of type t. kind 0: a is the address of the data, which becomes the value; +// kind 1: a is an array's storage; kind 2: a is the value itself. a global keeps the +// value in its slot; a local is initialized from it, and a local array copies the storage. +int embed_bind(int n, int off, word t, int a, int kind) { + int s, i; + if (!infn) { + if (kind == 1) { s = addsym(n, K_GARR, a); } + else { reserve_code(1); s = addsym(n, K_GVAR, here); emit(a); } + rs_put(off, RS_SYM, rs_sym(s), s); stype[s] = t; + lt_note(off, t); + return 0; + } + i = newloc(n); lrsoff[i] = off; rs_note(RS_LOCAL, off); + ltag[i] = LDECL; lpos[i] = tpack(); lnsz[i] = 0; lleaf[i] = 0; ltype[i] = t; + if (kind == 1) { + larr[i] = 1; fslots = fslots + tsz(t); capture_cell(i, 0); + vaddr(i); lit(a); lit(tsz(t)); callname(s_copy_w); op(O_DROP); + } else { + fslots++; if (isfl(t)) setflt(i, 1); capture_cell(i, 0); + lit(a); vaddr(i); op(O_ST); + setstr(i, t == tbstr); + } + lt_note(off, t); + return 0; +} +// the bytes of an embedded file as [n, b0, b1, ...], found next to the including file, +// then in the workspace folder, then as written, like an include. 0 when none opens. +word.u embed_open(char*.m path) { + if (path[0] != '/' && incdir) { char* combined = inc_cat(incdir, path); char* c = inc_norm(combined); word b = rt.readbytes(c); if (b) return b; } + if (path[0] != '/' && increoot) { char* combined = inc_cat(increoot, path); char* c = inc_norm(combined); word b = rt.readbytes(c); if (b) return b; } + word b = rt.readbytes(path); + return b; +} // file namespace and imports are restored when an included source ends. int kw_namespace() { next(); @@ -9201,8 +9318,9 @@ int fn_remember(int s, int name) { } if (depth) return 0; close = j; - q = rt.new(1 + count * 3); q[0] = count; rt.copy_w(q + 1, gbuf + 1, count * 3); - meta = rt.new(6); meta[0] = q; meta[1] = module_name; meta[2] = module_imports; + // compiler storage, not program objects: internal, so live_objects and leak reports skip it + q = chk_resize(0, 0, 1 + count * 3); q[0] = count; rt.copy_w(q + 1, gbuf + 1, count * 3); + meta = chk_resize(0, 0, 6); meta[0] = q; meta[1] = module_name; meta[2] = module_imports; meta[3] = namepos; meta[4] = open; meta[5] = close; fnmeta[s] = meta; return 1; } @@ -9407,7 +9525,7 @@ int unit_end(int from) { while (i < nsym) { // a bodyless declaration (fn f();) promises a definition elsewhere. in check mode that // elsewhere is another file of the same build, which this document can't see - if (skind[i] == K_FN && !(sflag[i] & F_DEF) && sfix[i] && !(cmode && (sflag[i] & F_DECL))) { + if (skind[i] == K_FN && !(sflag[i] & F_DEF) && sfix[i] && !(cmode && (sflag[i] & F_DECL)) && !missing_import_name(sname[i])) { tunpack(sval[i]); best = similar(sname[i]); if (!dry) { @@ -9419,6 +9537,7 @@ int unit_end(int from) { } i++; } + missing_imports = 0; if (nerr) giveup(); return 0; } @@ -9657,7 +9776,7 @@ int zc_init() { s_start = intern("run.start"); s_main = intern("main"); s_free = intern("rt.free"); - s_recycle = intern("rt.recycle"); + s_recycle = intern("rt.recycle"); s_copy_w = intern("rt.copy_w"); s_do = intern("do"); s_while = intern("while"); s_enum = intern("enum"); @@ -9706,6 +9825,7 @@ int zc_tables() { add_kw("do", kw_do, F_STMT); add_kw("enum", kw_enum, F_STMT | F_TOP); add_kw("include", kw_include, F_TOP | F_STMT); + add_kw("embed", kw_embed, F_TOP | F_STMT); add_kw("run.method", kw_method, F_TOP | F_STMT); add_kw("run.share_value", kw_share_value, F_EXPR); add_kw("run.syntax", kw_syntax, F_TOP | F_STMT); @@ -9903,31 +10023,9 @@ int hl_end() { pq_n = 0; return 0; } -// ---- embedded std library (generated by src/tools/embed.sh from src/lib/std.zc; do not edit) ---- -bstr std_part(int i) { - if (i == 0) return b"namespace std;\ninclude rt, run;\n\n// small helpers for strings, files, the clock and byte buffers. not part of the\n// genome as declarations; its source is embedded. include it with: include std;\n// syscall numbers (SYS_OPEN and the rest) live in the runtime.\n// a character string has type char*, one character per word and a 0 at the end.\n// the Bytes group takes packed byte strings (b\"...\"), whose\n// type is bstr.\n// the helpers are grouped in structs, and each is a static function, so it is\n// called as std.Group.name(args): std.Text.itoa(n, buf), std.Host.now(),\n// std.Buf.get(b, 0).\n// the constructors arena(n) and bnew(n) and the argv helpers are plain\n// functions, because an owned result has to carry its .u tag.\n\n//: ---- the command line and the environment ----\n\n//: argc and argv, envc and envp, as passed to main by c. a zc string is\n//: one character per word, so an array of them is char*[?]. libinit() fills\n//: these and the runtime calls it before main, if the program has one (an\n//: image built by build_image, or a check-mode document, does not).\n\nint argc;\nchar*[?] argv;\nint envc;\nchar*[?] envp;\n\n// one entry of a vector syscall, as a fresh string, or 0 past the end.\n// op: the syscall that fetches an entry (SYS_ARG or SYS_ENV)\n// i: which entry\n// returns: the entry, owned by the caller; 0 when there is no entry i\nchar*.u argstr(int op, int i) {\n\tint n = rt.syscall(op, i, 0, 0);\n\tchar* s;\n\tif (n < 0) return 0;\n\ts = rt.new(n + 1);\n\trt.syscall(op, i, s, n + 1);\n\treturn s;\n}\n\n// every entry of a vector syscall, in order.\n// cntop: the syscall that counts the entries (SYS_ARGC or SYS_ENVC)\n// stop: the syscall that fetches one (SYS_ARG or SYS_ENV)\n// returns: a new array of the strings, owned by the caller\nchar*[?].u argsvec(int cntop, int stop) {\n\tint n = rt.syscall(cntop, 0, 0, 0), i = 0;\n\tchar*[?] a = rt.new(char*[?], 0);\n\twhile (i < n) { push(a, argstr(stop, i)); i++; }\n\treturn a;\n}\n\n// reads the command line and the environment into argc, argv, envc and envp.\n// it stays a plain function: the compiler looks it up by name.\n// returns: 0\nint libinit() {\n\targv = argsvec(rt.SYS_ARGC, rt.SYS_ARG);\n\tenvp = argsvec(rt.SYS_ENVC, rt.SYS_ENV);\n\targc = len(argv); envc = len(envp);\n\treturn 0;\n}\n\n//: ---- capturing output ----\n\n// capture.start() redirects everything the program writes into a string and\n// returns it. writes go to the buffer instead of the terminal until end()\n// stops it, so the buffer is a string once end() has run:\n// let buf = capture.start();\n// put_s(\"hello world!\"); // nothing on the terminal\n// capture.end(buf);\n// put_s(buf); // hello world\n// one capture at a time, and the buffer belongs to whoever holds it.\n\nstruct capture {\n\t// redirects program output into a collector-managed character buffer.\n\t// returns the buffer; call end(buffer) before reading its terminator.\n\t// returns: the buffer used for this capture\n\tstatic char* start() {\n\t\tint cap = 256;\n\t\trt.outmode = 2; rt.outn ="; - if (i == 1) return b" 0; rt.outcap = cap;\n\t\trt.outbuf = rt.new(cap);\n\t\treturn rt.outbuf;\n\t}\n\t// stops output capture, terminates the buffer, and returns the collected text.\n\t// buffer: the buffer returned by start\n\t// returns: the same buffer, now terminated\n\tstatic char* end(char* buffer) {\n\t\trt.outbuf[rt.outn] = 0;\n\t\trt.outmode = 0; rt.outbuf = 0; rt.outn = 0; rt.outcap = 0;\n\t\treturn buffer;\n\t}\n}\n\n//: ---- character strings ----\n\nstruct Text {\n\t// the number written in decimal at the start of s. a leading '-' makes it\n\t// negative and the first character that is not a digit ends it.\n\t// s: the string to read\n\t// returns: the number, 0 when s starts with no digit\n\tstatic int atoi(char* s) {\n\t\tint n = 0, i = 0, sign = 1;\n\t\tif (s[0] == '-') { sign = 0 - 1; i = 1; }\n\t\twhile (s[i] >= '0' && s[i] <= '9') { n = n * 10 + s[i] - '0'; i++; }\n\t\treturn n * sign;\n\t}\n\n\t// writes n in decimal into dst, with a 0 after it.\n\t// n: the number\n\t// dst: where the text goes; 24 words is always enough\n\t// returns: how many characters were written, not counting the 0\n\tstatic int itoa(int n, char* dst) {\n\t\tint b[24], i = 0, j = 0, neg = 0;\n\t\tif (!n) { dst[0] = '0'; dst[1] = 0; return 1; }\n\t\tif (n < 0) { neg = 1; n = 0 - n; }\n\t\tif (n < 0) { rt.copy_w(dst, \"-9223372036854775808\", 21); return 20; }\n\t\twhile (n) { b[i] = '0' + n % 10; i++; n = n / 10; }\n\t\tif (neg) { dst[0] = '-'; j = 1; }\n\t\twhile (i) { i--; dst[j] = b[i]; j++; }\n\t\tdst[j] = 0;\n\t\treturn j;\n\t}\n\n\t// joins two strings.\n\t// dst: where the result goes, with room for both and a 0; not a or b\n\t// a: the first string\n\t// b: the second string\n\t// returns: the length of the result\n\tstatic int cat(char* dst, char* a, char* b) {\n\t\tint i = 0, j = 0;\n\t\twhile (a[i]) { dst[j] = a[i]; i++; j++; }\n\t\ti = 0;\n\t\twhile (b[i]) { dst[j] = b[i]; i++; j++; }\n\t\tdst[j] = 0;\n\t\treturn j;\n\t}\n\n\t// does s begin with pfx?\n\t// s: the string to test\n\t// pfx: the beginning to look for\n\t// returns: 1 if it does (always, for an empty pfx), 0 if not\n\tstatic int starts(char* s, char* pfx) {\n\t\tint i = 0;\n\t\twhile (pfx[i]) { if (s[i] != pfx[i]) return 0; i++; }\n\t\treturn 1;\n\t}\n\n\t// does s end with suffix? an empty suffix always matches.\n\t// s: the string to test\n\t// suffix: the ending to look for\n\t// returns: 1 when it matches, 0 otherwise\n\tstatic int ends(char* s, char* suffix) {\n\t\tint n = 0, m = 0, i = 0;\n\t\twhile (s[n]) n++;\n\t\twhile (suffix[m]) m++;\n\t\tif (m > n) return 0;\n\t\twhile (i < m) { if (s[n - m + i] != suffix[i]) return 0; i++; }\n\t\treturn 1;\n\t}\n\n\t// how many characters come before the 0.\n\t// s: the string\n\t// returns: the length, not counting the 0\n\tstatic int len(char* s) {\n\t\tint n = 0;\n\t\twhile (s[n]) n++;\n\t\treturn n;\n\t}\n\n\t// are the two strings the same, character for character?\n\t// a, b: the strings to compare\n\t// returns: 1 if they are equal, 0 if not\n\tstatic int eq(char* a, char* b) {\n\t\tint i = 0;\n\t\twhile (a[i] && a[i] == b[i]) i++;\n\t\treturn a[i] == b[i];\n\t}\n\n\t// copies s into dst, with a 0 after it.\n\t// dst: where th"; - if (i == 2) return b"e copy goes, with room for s and the 0\n\t// s: the string to copy\n\t// returns: the length of the copy\n\tstatic int copy(char* dst, char* s) {\n\t\tint i = 0;\n\t\twhile (s[i]) { dst[i] = s[i]; i++; }\n\t\tdst[i] = 0;\n\t\treturn i;\n\t}\n\n\t// where pat first appears in s.\n\t// s: the string to search\n\t// pat: the string to look for; an empty pat is found at 0\n\t// returns: the index of the first match, or -1 when there is none\n\tstatic int find(char* s, char* pat) {\n\t\tint i = 0, j;\n\t\tif (!pat[0]) return 0;\n\t\twhile (s[i]) {\n\t\t\tj = 0;\n\t\t\twhile (pat[j] && s[i + j] == pat[j]) j++;\n\t\t\tif (!pat[j]) return i;\n\t\t\ti++;\n\t\t}\n\t\treturn 0 - 1;\n\t}\n\n\t// changes the ASCII letters of s to lower case, in place.\n\t// s: the string to change\n\t// returns: the length of s\n\tstatic int lower(char* s) {\n\t\tint i = 0;\n\t\twhile (s[i]) {\n\t\t\tif (s[i] >= 'A' && s[i] <= 'Z') s[i] = s[i] + 32;\n\t\t\ti++;\n\t\t}\n\t\treturn i;\n\t}\n\n\t// changes the ASCII letters of s to upper case, in place.\n\t// s: the string to change\n\t// returns: the length of s\n\tstatic int upper(char* s) {\n\t\tint i = 0;\n\t\twhile (s[i]) {\n\t\t\tif (s[i] >= 'a' && s[i] <= 'z') s[i] = s[i] - 32;\n\t\t\ti++;\n\t\t}\n\t\treturn i;\n\t}\n\n\t// removes the spaces, tabs and line breaks from both ends of s, in place.\n\t// s: the string to change\n\t// returns: the length of what is left\n\tstatic int trim(char* s) {\n\t\tint n = 0, a = 0, b, i = 0, c;\n\t\twhile (s[n]) n++;\n\t\twhile (a < n && (s[a] == ' ' || s[a] == '\\t' || s[a] == '\\n' || s[a] == '\\r')) a++;\n\t\tb = n;\n\t\twhile (b > a) {\n\t\t\tc = s[b - 1];\n\t\t\tif (!(c == ' ' || c == '\\t' || c == '\\n' || c == '\\r')) break;\n\t\t\tb--;\n\t\t}\n\t\tn = b - a;\n\t\twhile (i < n) { s[i] = s[i + a]; i++; }\n\t\ts[n] = 0;\n\t\treturn n;\n\t}\n}\n\n//: ---- numbers ----\n\n// plain integer helpers. none of them can fail: an out of range result wraps\n// the same way the arithmetic does.\n\nstruct Num {\n\t// the absolute value of n, using wrapping word arithmetic. the minimum\n\t// word remains negative because its positive value cannot fit in a word.\n\t// n: the value\n\t// returns: its absolute value, or the minimum word\n\tstatic int abs(int n) {\n\t\tif (n < 0) return 0 - n;\n\t\treturn n;\n\t}\n\n\t// the smaller of a and b.\n\t// a, b: the values to compare\n\t// returns: the smaller value (a when they are equal)\n\tstatic int min(int a, int b) {\n\t\tif (a < b) return a;\n\t\treturn b;\n\t}\n\n\t// the larger of a and b.\n\t// a, b: the values to compare\n\t// returns: the larger value (a when they are equal)\n\tstatic int max(int a, int b) {\n\t\tif (a > b) return a;\n\t\treturn b;\n\t}\n\n\t// v, kept between lo and hi.\n\t// v: the value\n\t// lo, hi: the bounds, lo first; lo must not exceed hi\n\t// returns: v clamped to the bounds\n\tstatic int clamp(int v, int lo, int hi) {\n\t\tif (v < lo) return lo;\n\t\tif (v > hi) return hi;\n\t\treturn v;\n\t}\n\n\t// the greatest common divisor of a and b, computed with word arithmetic.\n\t// a, b: the values\n\t// returns: their greatest common divisor\n\tstatic int gcd(int a, int b) {\n\t\tif (a < 0) a = 0 - a;\n\t\tif (b < 0) b = 0 - b;\n\t\tint t;\n\t\twhile (b) { t "; - if (i == 3) return b"= a % b; a = b; b = t; }\n\t\treturn a;\n\t}\n\n\t// b raised to the power e. e below 0 gives 1; the result wraps as a word.\n\t// b: the base\n\t// e: the exponent\n\t// returns: b to the power e, or 1 when e is negative\n\tstatic int pow(int b, int e) {\n\t\tint r = 1;\n\t\twhile (e > 0) { r = r * b; e--; }\n\t\treturn r;\n\t}\n\n\t// the whole square root of n, rounded down. n below 0 gives 0.\n\t// n: the value\n\t// returns: the greatest integer whose square is at most n, or 0 for n <= 0\n\tstatic int isqrt(int n) {\n\t\tint lo = 0, hi = 3037000499, mid;\n\t\tif (n <= 0) return 0;\n\t\twhile (lo < hi) {\n\t\t\tmid = lo + (hi - lo + 1) / 2;\n\t\t\tif (mid <= n / mid) lo = mid; else hi = mid - 1;\n\t\t}\n\t\treturn lo;\n\t}\n\n\t// -1, 0 or 1 according to the sign of n.\n\t// n: the value\n\t// returns: -1 when n is negative, 0 when it is zero, and 1 when it is positive\n\tstatic int sign(int n) {\n\t\tif (n < 0) return 0 - 1;\n\t\tif (n > 0) return 1;\n\t\treturn 0;\n\t}\n}\n\n//: ---- the host ----\n\n//: the argument block for Host.raw: the number, then its six arguments.\nint rawblk[7];\n\nstruct Host {\n\t// calls any linux system call on the host. this is the escape hatch below\n\t// the named helpers: the number is the kernel's, so the meaning of each\n\t// argument depends on the call. a pointer argument is an address in the\n\t// program's memory, and only the host reads it, so a string or buffer has\n\t// to be passed as its address.\n\t// it runs the call on the real machine, not inside the vm, so a program\n\t// that uses it can do whatever the process can do. only use it where that\n\t// is wanted.\n\t// nr: the syscall number (x86-64 and arm64 numbers differ, so check the host)\n\t// a..f: the six arguments, unused ones 0\n\t// returns: the kernel's result, -errno on failure, or -1 on unsupported hosts\n\tstatic int raw(int nr, int a, int b, int c, int d, int e, int f) {\n\t\trawblk[0] = nr; rawblk[1] = a; rawblk[2] = b; rawblk[3] = c;\n\t\trawblk[4] = d; rawblk[5] = e; rawblk[6] = f;\n\t\treturn rt.syscall(rt.SYS_RAW, rawblk, 0, 0);\n\t}\n\n\t// the value of an environment variable.\n\t// name: the variable's name\n\t// dst: where the value goes, a zero-terminated character string\n\t// n: how many words dst has\n\t// returns: the length of the value, -1 if the variable is not set\n\tstatic int env(char* name, char* dst, int n) { return rt.syscall(rt.SYS_GETENV, name, dst, n); }\n\n\t// the clock.\n\t// returns: the time in seconds since 1970\n\tstatic int now() { return rt.syscall(rt.SYS_TIME, 0, 0, 0); }\n\n\t// writes a string to a file, replacing what was there.\n\t// path: the file's name\n\t// s: the text to write\n\t// returns: how many bytes were written, -1 if the file could not be opened\n\tstatic int writefile(char* path, char* s) {\n\t\tint fd = rt.syscall(rt.SYS_OPEN, path, 1, 0), n;\n\t\tif (fd < 0) return -1;\n\t\tn = rt.syscall(rt.SYS_WRITE, fd, s, rt.len_w(s));\n\t\trt.syscall(rt.SYS_CLOSE, fd, 0, 0);\n\t\treturn n;\n\t}\n}\n\n//: ---- byte strings ----\n\n// the same helpers for packed byte strings. character-string helpers above are unchanged.\n// a"; - if (i == 4) return b" packed byte string has type bstr; len_s reads its stored byte length.\n\nstruct Bytes {\n\t// reads a decimal number at the start of s; a leading '-' makes it\n\t// negative, and the first non-digit ends the number.\n\t// s: the byte string\n\t// returns: the number, or 0 when s starts with no digit\n\tstatic int atoi(bstr s) { return rt.s_to_n(s); }\n\n\t// like Text.itoa, but the text is made fresh.\n\t// n: the number\n\t// returns: a new byte string\n\tstatic bstr itoa(int n) { return rt.n_to_s(n); }\n\n\t// like Text.starts, for byte strings.\n\t// s: the byte string to test\n\t// pfx: the beginning to look for\n\t// returns: 1 if s begins with pfx, 0 if not\n\tstatic int starts(bstr s, bstr pfx) {\n\t\tint i = 0, n = rt.len_s(s), m = rt.len_s(pfx);\n\t\tif (m > n) return 0;\n\t\twhile (i < m) { if (rt.at_s(s, i) != rt.at_s(pfx, i)) return 0; i++; }\n\t\treturn 1;\n\t}\n\n\t// the number of bytes in a packed byte string.\n\t// s: the byte string\n\t// returns: its stored length\n\tstatic int len(bstr s) { return rt.len_s(s); }\n\n\t// are two packed byte strings equal, byte for byte.\n\t// a, b: the byte strings to compare\n\t// returns: 1 if equal, 0 otherwise\n\tstatic int eq(bstr a, bstr b) {\n\t\tint i = 0;\n\t\tif (rt.len_s(a) != rt.len_s(b)) return 0;\n\t\twhile (i < rt.len_s(a)) { if (rt.at_s(a, i) != rt.at_s(b, i)) return 0; i++; }\n\t\treturn 1;\n\t}\n\n\t// where pat first appears in s.\n\t// s: the byte string to search\n\t// pat: the byte string to find; an empty pat is found at 0\n\t// returns: its byte index, or -1 when absent\n\tstatic int find(bstr s, bstr pat) {\n\t\tint i = 0, j, n = rt.len_s(s), m = rt.len_s(pat);\n\t\tif (!m) return 0;\n\t\twhile (i + m <= n) {\n\t\t\tj = 0;\n\t\t\twhile (j < m && rt.at_s(s, i + j) == rt.at_s(pat, j)) j++;\n\t\t\tif (j == m) return i;\n\t\t\ti++;\n\t\t}\n\t\treturn 0 - 1;\n\t}\n\n\t// does s end with suffix? an empty suffix always matches.\n\t// s: the byte string to test\n\t// suffix: the ending to look for\n\t// returns: 1 when it matches, 0 otherwise\n\tstatic int ends(bstr s, bstr suffix) {\n\t\tint n = rt.len_s(s), m = rt.len_s(suffix), i = 0;\n\t\tif (m > n) return 0;\n\t\twhile (i < m) { if (rt.at_s(s, n - m + i) != rt.at_s(suffix, i)) return 0; i++; }\n\t\treturn 1;\n\t}\n\n\t// like Host.env, for byte strings.\n\t// name: the variable's name, a byte string\n\t// dst: the packed byte string whose payload receives the value\n\t// n: the maximum number of bytes to write\n\t// returns: the length of the value, -1 if the variable is not set\n\tstatic int env(bstr name, bstr dst, int n) { return rt.bgetenv(name, dst, n); }\n\n\t// like Host.writefile, for a byte string.\n\t// path: the file's name, a byte string\n\t// s: the bytes to write\n\t// returns: how many bytes were written, -1 if the file could not be opened\n\tstatic int writefile(bstr path, bstr s) {\n\t\tint fd = rt.bopen(path, 1), n;\n\t\tif (fd < 0) return -1;\n\t\tn = rt.bwrite(fd, s, rt.len_s(s));\n\t\trt.syscall(rt.SYS_CLOSE, fd, 0, 0);\n\t\treturn n;\n\t}\n}\n\n//: ---- arenas ----\n\n// a region that is freed all at once.\n// n: its capacity in words; value"; - if (i == 5) return b"s below 1 allocate one word\n// returns: the arena, owned by the caller\nint .u arena(int n) { return rt.arena_new(n); }\n\nstruct Arena {\n\t// takes n zeroed words from an arena.\n\t// a: the arena\n\t// n: how many words\n\t// returns: the first word, or 0 when n words do not fit\n\tstatic int alloc(word a, int n) { return rt.arena_alloc(a, n); }\n\n\t// gives every block of an arena back at once. pointers returned by alloc\n\t// before the reset are no longer valid.\n\t// a: the arena\n\t// returns: 0\n\tstatic int reset(word a) { return rt.arena_reset(a); }\n}\n\n//: ---- growable byte buffers ----\n\n// a byte buffer stores its length and capacity in two words, followed by the\n// bytes, packed.\n// n: the capacity in bytes; a negative value is treated as 0\n// returns: an empty buffer with that capacity, owned by the caller\nword .u bnew(int n) {\n\tint words;\n\tword p;\n\tif (n < 0) n = 0;\n\twords = (n + 7) >> 3;\n\tp = rt.new(2 + words);\n\tp[0] = 0;\n\tp[1] = n;\n\treturn u*p;\n}\n\nstruct Buf {\n\t// how many bytes a buffer holds.\n\t// b: the buffer\n\t// returns: its length\n\tstatic int length(word b) { return b[0]; }\n\n\t// how many bytes a buffer can hold.\n\t// b: the buffer\n\t// returns: its capacity\n\tstatic int capacity(word b) { return b[1]; }\n\n\t// one byte of a buffer.\n\t// b: the buffer\n\t// i: the position\n\t// returns: the byte, 0 when i is outside the buffer\n\tstatic int get(word b, int i) { if (i < 0 || i >= b[0]) return 0; return rt.ld8(rt.addr_b(b + 2) + i); }\n\n\t// sets one byte of a buffer. setting the byte at the current length makes the\n\t// buffer one longer.\n\t// b: the buffer\n\t// i: the position, below the capacity\n\t// v: the value; only its low 8 bits are kept\n\t// returns: the byte stored, 0 when i is outside the capacity\n\tstatic int set(word b, int i, int v) {\n\t\tif (i < 0 || i >= b[1]) return 0;\n\t\trt.st8(rt.addr_b(b + 2) + i, v & 255);\n\t\tif (i >= b[0]) b[0] = i + 1;\n\t\treturn v & 255;\n\t}\n}\n\n//: array algorithms borrow their input and visit elements in index order.\n//: map and filter return independent handles. reference elements are shared\n//: with the collector, so freeing the input does not invalidate the result.\n\n// the first index equal to value, or -1 when no element matches.\nint array_find<T>(fixed T[?].m source, T value) {\n\tint length = len(source);\n\tfor (int i = 0; i < length; i++) if (source[i] == value) return i;\n\treturn -1;\n}\n// true when an element equals value.\nint array_contains<T>(fixed T[?].m source, T value) { return array_find(source, value) >= 0; }\n// the first index whose predicate is true, or -1 when none matches.\nint array_find_if<T>(fixed T[?].m source, int(T).m predicate) {\n\tint length = len(source);\n\tfor (int i = 0; i < length; i++) if (predicate(source[i])) return i;\n\treturn -1;\n}\n// true when any element satisfies predicate; false for an empty array.\nint array_any<T>(fixed T[?].m source, int(T).m predicate) { return array_find_if(source, predicate) >= 0; }\n// true when every element satisfies predicate, including an "; - if (i == 6) return b"empty array.\nint array_all<T>(fixed T[?].m source, int(T).m predicate) {\n\tint length = len(source);\n\tfor (int i = 0; i < length; i++) if (!predicate(source[i])) return 0;\n\treturn 1;\n}\n// the number of elements satisfying predicate.\nint array_count<T>(fixed T[?].m source, int(T).m predicate) {\n\tint length = len(source);\n\tint count = 0;\n\tfor (int i = 0; i < length; i++) if (predicate(source[i])) count++;\n\treturn count;\n}\n// apply transform once to each element and return the transformed array.\n// the result element type follows the callback's return type.\nU[?].u array_map<T,U>(fixed T[?].m source, U(T).m transform) {\n\tint length = len(source);\n\tU[?] result = rt.new(U[?]);\n\tfor (int i = 0; i < length; i++) push(result, run.share_value(transform(source[i])));\n\treturn result;\n}\n// copy elements satisfying predicate into a new array, preserving order.\nT[?].u array_filter<T>(fixed T[?].m source, int(T).m predicate) {\n\tint length = len(source);\n\tT[?] result = rt.new(T[?]);\n\tfor (int i = 0; i < length; i++) if (predicate(source[i])) push(result, run.share_value(source[i]));\n\treturn result;\n}\n// fold elements from left to right, starting with initial; an empty array\n// returns initial without calling combine.\nA array_reduce<T,A>(fixed T[?].m source, A initial, A(A,T).m combine) {\n\tint length = len(source);\n\tfor (int i = 0; i < length; i++) initial = combine(initial, source[i]);\n\treturn initial;\n}\n\nrun.method(\"find\", array_find);\nrun.method(\"contains\", array_contains);\nrun.method(\"find_if\", array_find_if);\nrun.method(\"any\", array_any);\nrun.method(\"all\", array_all);\nrun.method(\"count\", array_count);\nrun.method(\"map\", array_map);\nrun.method(\"filter\", array_filter);\nrun.method(\"reduce\", array_reduce);\n"; - return 0; -} -// ---- end embedded std library ---- -// ---- embedded icon library (generated by src/tools/embed.sh from src/lib/icon.zc; do not edit) ---- -bstr icon_part(int i) { - if (i == 0) return b"namespace icon;\ninclude rt, run;\n\n// structured runtime transformations of compiler token lists.\n// token storage follows run.tokens: count, then kind, value and source line.\n// edits create independent lists; publication recompiles a function only after\n// delimiter validation. captures match complete balanced token ranges.\n\nint icon_error;\n\nint icon_eq(word* a, int ai, word* b, int bi, int n) {\n\tint i = 0;\n\twhile (i < n) {\n\t\tif (a[1 + (ai + i) * 3] != b[1 + (bi + i) * 3] || a[2 + (ai + i) * 3] != b[2 + (bi + i) * 3]) return 0;\n\t\ti++;\n\t}\n\treturn 1;\n}\nint icon_value(word* q, int i) { if (i < 0 || i >= q[0]) return 0; return q[2 + i * 3]; }\nint icon_kind(word* q, int i) { if (i < 0 || i >= q[0]) return 0; return q[1 + i * 3]; }\nint icon_is(word* q, int i, char* s) {\n\tint kind = run.T_OP; if (s[0] >= 'a' && s[0] <= 'z') kind = run.T_ID;\n\treturn icon_kind(q, i) == kind && icon_value(q, i) == run.intern(s);\n}\nint icon_open(int v) { return v == run.intern(\"(\") || v == run.intern(\"[\") || v == run.intern(\"{\"); }\nint icon_close(int v) { return v == run.intern(\")\") || v == run.intern(\"]\") || v == run.intern(\"}\"); }\nint icon_pair(int a, int b) {\n\treturn (a == run.intern(\"(\") && b == run.intern(\")\")) || (a == run.intern(\"[\") && b == run.intern(\"]\")) || (a == run.intern(\"{\") && b == run.intern(\"}\"));\n}\nint icon_declaration(word* q, int start, int binding) {\n\tint first = icon_value(q, start), i = start;\n\tif (icon_kind(q, start) != run.T_ID || (first != run.intern(\"let\") && first != run.intern(\"const\") && !run.istypename(first))) return 0;\n\twhile (i < binding) { if (icon_is(q, i, \"(\") || icon_is(q, i, \"=\")) return 0; i++; }\n\treturn binding > start;\n}\n// locate a simple declaration at a statement boundary, including nested blocks.\n// result records [type start, binding position, semicolon, statement start].\nint icon_locate_declaration(word* q, int id, word* result) {\n\tint start = 0, i, depth, v, binding, comma, type_start;\n\twhile (start < q[0]) {\n\t\tif (start && !icon_is(q, start - 1, \";\") && !icon_is(q, start - 1, \"{\") && !icon_is(q, start - 1, \"}\")) { start++; continue; }\n\t\ttype_start = start; if (icon_is(q, type_start, \"const\")) type_start++;\n\t\tif (icon_kind(q, type_start) != run.T_ID || (!icon_is(q, type_start, \"let\") && !run.istypename(icon_value(q, type_start)))) { start++; continue; }\n\t\ti = type_start; depth = 0; binding = -1; comma = 0;\n\t\twhile (i < q[0]) {\n\t\t\tv = icon_value(q, i);\n\t\t\tif (!depth && v == id && icon_kind(q, i) == run.T_ID && binding < 0 && icon_declaration(q, type_start, i) && (icon_is(q, i + 1, \"=\") || icon_is(q, i + 1, \";\") || icon_is(q, i + 1, \",\") || icon_is(q, i + 1, \"(\"))) binding = i;\n\t\t\tif (icon_kind(q, i) == run.T_OP) {\n\t\t\t\tif (icon_open(v)) depth++;\n\t\t\t\telse if (icon_close(v)) { if (!depth) break; depth--; }\n\t\t\t\tif (!depth && v == run.intern(\",\")) comma = 1;\n\t\t\t\tif (!depth && v == run.intern(\";\")) {\n\t\t\t\t\tif (binding >= 0) {\n\t\t\t\t\t\tif (comma || icon_is(q, binding + 1, \"(\")) return 0;\n\t\t\t\t\t\tresult[0] = type_start; result[1] = bindin"; - if (i == 1) return b"g; result[2] = i; result[3] = start; return 1;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\ti++;\n\t\t}\n\t\tstart++;\n\t}\n\treturn 0;\n}\nint icon_balanced(word* q, int a, int b) {\n\tword* stack = rt.new(b - a + 1); int n = 0, i = a, v, valid = 1;\n\twhile (i < b) {\n\t\tv = icon_value(q, i);\n\t\tif (icon_kind(q, i) == run.T_OP) {\n\t\t\tif (icon_open(v)) { stack[n] = v; n++; }\n\t\t\telse if (icon_close(v)) { if (!n || !icon_pair(stack[n - 1], v)) { valid = 0; break; } n--; }\n\t\t}\n\t\ti++;\n\t}\n\tif (n) valid = 0;\n\trt.free(stack);\n\treturn valid;\n}\nword* .u icon_slice(word* q, int a, int b) {\n\tword* r = rt.new(1 + (b - a) * 3); int i = 0; r[0] = b - a;\n\twhile (i < (b - a) * 3) { r[1 + i] = q[1 + a * 3 + i]; i++; }\n\treturn r;\n}\nword* .u icon_splice(word* q, int a, int b, word* insert) {\n\tint n = q[0] - (b - a) + insert[0], i = 0, j = 0;\n\tword* r = rt.new(1 + n * 3); r[0] = n;\n\twhile (i < a * 3) { r[1 + j] = q[1 + i]; i++; j++; }\n\ti = 0; while (i < insert[0] * 3) { r[1 + j] = insert[1 + i]; i++; j++; }\n\ti = b * 3; while (i < q[0] * 3) { r[1 + j] = q[1 + i]; i++; j++; }\n\treturn r;\n}\n// capture classes use integer literals, identifiers, expressions, statements\n// or blocks. a named capture records its exact token range for substitution.\nint icon_class(int name) {\n\tif (name == run.intern(\"int\")) return 1;\n\tif (name == run.intern(\"name\")) return 2;\n\tif (name == run.intern(\"expr\")) return 3;\n\tif (name == run.intern(\"stmt\")) return 4;\n\tif (name == run.intern(\"block\")) return 5;\n\treturn 0;\n}\nint icon_pattern(word* q) {\n\tint i = 0;\n\tif (!q || q[0] <= 0 || !icon_balanced(q, 0, q[0])) return 0;\n\twhile (i < q[0]) {\n\t\tif (icon_is(q, i, \"$\")) {\n\t\t\tif (icon_kind(q, i + 1) != run.T_ID || !icon_class(icon_value(q, i + 1))) return 0;\n\t\t\ti += 2;\n\t\t\tif (icon_is(q, i, \":\")) { if (icon_kind(q, i + 1) != run.T_ID || icon_class(icon_value(q, i + 1))) return 0; i += 2; }\n\t\t} else i++;\n\t}\n\treturn 1;\n}\nint icon_precedence(int name) {\n\tif (name == run.intern(\"||\")) return 1;\n\tif (name == run.intern(\"&&\")) return 2;\n\tif (name == run.intern(\"|\")) return 3;\n\tif (name == run.intern(\"^\")) return 4;\n\tif (name == run.intern(\"&\")) return 5;\n\tif (name == run.intern(\"==\") || name == run.intern(\"!=\")) return 6;\n\tif (name == run.intern(\"<\") || name == run.intern(\"<=\") || name == run.intern(\">\") || name == run.intern(\">=\")) return 7;\n\tif (name == run.intern(\"<<\") || name == run.intern(\">>\")) return 8;\n\tif (name == run.intern(\"+\") || name == run.intern(\"-\")) return 9;\n\tif (name == run.intern(\"*\") || name == run.intern(\"/\") || name == run.intern(\"%\")) return 10;\n\treturn 0;\n}\n// recognize expression structure before matching a variable-length capture.\n// this excludes statement keywords and incomplete operator sequences.\nint icon_expression(word* q, int start, int end, int minimum) {\n\tint i = start, v, kind, priority, next;\n\tif (i >= end) return -1;\n\tv = icon_value(q, i); kind = icon_kind(q, i);\n\tif (kind == run.T_OP && (v == run.intern(\"!\") || v == run.intern(\"~\") || v == run.intern(\"+\") || v == run.intern(\"-\") || v == run.intern(\"*"; - if (i == 2) return b"\") || v == run.intern(\"&\"))) {\n\t\ti = icon_expression(q, i + 1, end, 11); if (i < 0) return -1;\n\t} else if (icon_is(q, i, \"(\")) {\n\t\ti = icon_expression(q, i + 1, end, 1); if (i < 0 || !icon_is(q, i, \")\")) return -1; i++;\n\t} else if (kind == run.T_NUM || kind == run.T_FLT || kind == run.T_STR || kind == run.T_BSTR || kind == run.T_ID) {\n\t\tif (kind == run.T_ID && (v == run.intern(\"return\") || v == run.intern(\"let\") || v == run.intern(\"if\") || v == run.intern(\"while\") || v == run.intern(\"for\") || v == run.intern(\"else\") || v == run.intern(\"break\") || v == run.intern(\"continue\") || v == run.intern(\"do\") || v == run.intern(\"switch\") || v == run.intern(\"const\"))) return -1;\n\t\ti++;\n\t} else return -1;\n\twhile (i < end) {\n\t\tif (icon_is(q, i, \"(\") || icon_is(q, i, \"[\")) {\n\t\t\tv = icon_value(q, i); i++;\n\t\t\tif (v != run.intern(\"(\") || !icon_is(q, i, \")\")) {\n\t\t\t\twhile (1) {\n\t\t\t\t\ti = icon_expression(q, i, end, 1); if (i < 0) return -1;\n\t\t\t\t\tif (v == run.intern(\"(\") && icon_is(q, i, \",\")) { i++; continue; } break;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (i >= end || icon_kind(q, i) != run.T_OP || !icon_pair(v, icon_value(q, i))) return -1; i++; continue;\n\t\t}\n\t\tif (icon_is(q, i, \".\")) { if (i + 1 >= end || icon_kind(q, i + 1) != run.T_ID) return -1; i += 2; continue; }\n\t\tif (icon_is(q, i, \"++\") || icon_is(q, i, \"--\")) { i++; continue; }\n\t\tif (minimum == 1 && icon_is(q, i, \"?\")) {\n\t\t\ti = icon_expression(q, i + 1, end, 1); if (i < 0 || !icon_is(q, i, \":\")) return -1;\n\t\t\ti = icon_expression(q, i + 1, end, 1); if (i < 0) return -1; continue;\n\t\t}\n\t\tpriority = 0; if (icon_kind(q, i) == run.T_OP) priority = icon_precedence(icon_value(q, i)); if (!priority || priority < minimum) break;\n\t\tnext = icon_expression(q, i + 1, end, priority + 1); if (next < 0) return -1; i = next;\n\t}\n\treturn i;\n}\nint icon_range(word* q, int a, int b, int kind) {\n\tif (a < 0 || a >= b || b > q[0]) return 0;\n\tif (kind == 1) return b == a + 1 && icon_kind(q, a) == run.T_NUM;\n\tif (kind == 2) return b == a + 1 && icon_kind(q, a) == run.T_ID;\n\tif (kind == 3) return icon_expression(q, a, b, 1) == b;\n\tif (kind == 5) return icon_is(q, a, \"{\") && icon_mate(q, a) == b - 1;\n\tif (kind == 4) return icon_statement_end(q, a) == b;\n\treturn icon_expression(q, a, b, 1) == b;\n}\n// recursively match a pattern with backtracking at balanced capture boundaries.\n// captures contain count followed by [name, start, end, class] records.\nint icon_match(word* q, int at, word* p, int pi, word* caps) {\n\tint kind, name, next, end, old, i;\n\tif (pi == p[0]) return at + 1;\n\tif (at >= q[0]) return 0;\n\tif (!icon_is(p, pi, \"$\")) {\n\t\tif (!icon_eq(q, at, p, pi, 1)) return 0;\n\t\treturn icon_match(q, at + 1, p, pi + 1, caps);\n\t}\n\tif (pi + 1 >= p[0]) return 0;\n\tname = icon_value(p, pi + 1); kind = icon_class(name); next = pi + 2;\n\tif (!kind) return 0;\n\tif (icon_is(p, next, \":\")) { name = icon_value(p, next + 1); next += 2; if (!name) return 0; }\n\telse name = 0;\n\told = caps[0]; end = at + 1;\n\tif (kind == 4) end = icon_statement_end(q, at);\n\tif (kind == 5) { if (!ic"; - if (i == 3) return b"on_is(q, at, \"{\")) return 0; end = icon_mate(q, at) + 1; }\n\tif (end <= at) return 0;\n\twhile (end <= q[0]) {\n\t\tif (icon_range(q, at, end, kind)) {\n\t\t\ti = 0;\n\t\t\twhile (i < old) {\n\t\t\t\tif (name && caps[1 + i * 4] == name) {\n\t\t\t\t\tif (end - at != caps[3 + i * 4] - caps[2 + i * 4] || !icon_eq(q, at, q, caps[2 + i * 4], end - at)) break;\n\t\t\t\t}\n\t\t\t\ti++;\n\t\t\t}\n\t\t\tif (i == old) {\n\t\t\t\tcaps[0] = old + 1; caps[1 + old * 4] = name; caps[2 + old * 4] = at; caps[3 + old * 4] = end; caps[4 + old * 4] = kind;\n\t\t\t\ti = icon_match(q, end, p, next, caps);\n\t\t\t\tif (i) return i;\n\t\t\t\tcaps[0] = old;\n\t\t\t}\n\t\t}\n\t\tif (kind != 3) break;\n\t\tend++;\n\t}\n\treturn 0;\n}\nint icon_capture(word* caps, int name, int ordinal) {\n\tint c = 0, kind = icon_class(name), k = 0;\n\twhile (c < caps[0]) {\n\t\tif (!kind) { if (caps[1 + c * 4] == name) return c + 1; }\n\t\telse if (caps[4 + c * 4] == kind) { if (k == ordinal) return c + 1; k++; }\n\t\tc++;\n\t}\n\treturn 0;\n}\nword* .u icon_expand(word* q, word* replacement, word* caps) {\n\tint n = 0, i = 0, j = 0, c, name, kind, k, seen[6], a, b;\n\tk = 0; while (k < 6) { seen[k] = 0; k++; }\n\twhile (i < replacement[0]) {\n\t\tif (icon_is(replacement, i, \"$\")) {\n\t\t\tif (icon_kind(replacement, i + 1) != run.T_ID) { icon_error = 3; return 0; }\n\t\t\tname = icon_value(replacement, i + 1); kind = icon_class(name); c = icon_capture(caps, name, seen[kind]);\n\t\t\tif (!c) { icon_error = 3; return 0; } c--;\n\t\t\tn += caps[3 + c * 4] - caps[2 + c * 4]; if (kind) seen[kind]++; i += 2;\n\t\t} else { n++; i++; }\n\t}\n\tword* r = rt.new(1 + n * 3); i = 0;\n\tk = 0; while (k < 6) { seen[k] = 0; k++; }\n\twhile (i < replacement[0]) {\n\t\tif (icon_is(replacement, i, \"$\")) {\n\t\t\tname = icon_value(replacement, i + 1); kind = icon_class(name); c = icon_capture(caps, name, seen[kind]) - 1;\n\t\t\tif (kind) seen[kind]++;\n\t\t\ta = caps[2 + c * 4] * 3; b = caps[3 + c * 4] * 3;\n\t\t\twhile (a < b) { r[1 + j] = q[1 + a]; a++; j++; }\n\t\t\ti += 2;\n\t\t} else {\n\t\t\tk = 0; while (k < 3) { r[1 + j] = replacement[1 + i * 3 + k]; j++; k++; } i++;\n\t\t}\n\t}\n\tr[0] = j / 3;\n\tif (!icon_balanced(r, 0, r[0])) { rt.free(r); icon_error = 1; return 0; }\n\treturn r;\n}\n\n// resumable frames retain their own token list and explicit binding maps.\n// [tokens, position, state, result, error, locals, globals, blocks, steps].\n\n// locate a binding in a frame map by its interned name.\nint icon_binding(word map, int name) {\n\twhile (map) { if (map[0] == name) return map; map = map[2]; }\n\treturn 0;\n}\nint icon_read(word binding) { if (binding[3]) return *binding[3]; return binding[1]; }\nint icon_store(word*.m frame, int column, int name, int value) {\n\tword entry = icon_binding(frame[column], name);\n\tif (!entry) {\n\t\tword* node = rt.new(4); node[0] = name; node[1] = value; node[2] = frame[column]; node[3] = 0; frame[column] = node; return value;\n\t}\n\tentry[1] = value; if (entry[3]) *entry[3] = value; return value;\n}\nint icon_declare(word*.m frame, int name, int value) {\n\tword* node = rt.new(4); node[0] = name; node[1] = value; node[2] = frame[5]; node[3] = 0; frame[5] = no"; - if (i == 4) return b"de; return value;\n}\nint icon_fail(word frame) { frame[2] = 2; frame[4] = 5; icon_error = 5; return 0; }\nint icon_end(word* q, int start) {\n\tint depth = 0, i = start, v;\n\twhile (i < q[0]) {\n\t\tv = icon_value(q, i);\n\t\tif (icon_kind(q, i) == run.T_OP) {\n\t\t\tif (icon_open(v)) depth++;\n\t\t\telse if (icon_close(v)) { if (!depth) return i; depth--; }\n\t\t\tif (!depth && v == run.intern(\";\")) return i;\n\t\t}\n\t\ti++;\n\t}\n\treturn i;\n}\nint icon_mate(word* q, int start) {\n\tint depth = 1, i = start + 1, v;\n\twhile (i < q[0]) {\n\t\tv = icon_value(q, i);\n\t\tif (icon_kind(q, i) == run.T_OP) {\n\t\t\tif (icon_open(v)) depth++; else if (icon_close(v)) depth--;\n\t\t\tif (!depth) return i;\n\t\t}\n\t\ti++;\n\t}\n\treturn -1;\n}\n// find one statement boundary; control-flow captures require braced bodies.\nint icon_statement_end(word* q, int start) {\n\tint close, end;\n\tif (icon_is(q, start, \"{\")) { close = icon_mate(q, start); if (close < 0) return -1; return close + 1; }\n\tif (icon_is(q, start, \"if\") || icon_is(q, start, \"while\")) {\n\t\tif (!icon_is(q, start + 1, \"(\")) return -1;\n\t\tclose = icon_mate(q, start + 1);\n\t\tif (close < 0 || !icon_is(q, close + 1, \"{\")) return -1;\n\t\tend = icon_mate(q, close + 1); if (end < 0) return -1; end++;\n\t\tif (icon_is(q, start, \"if\") && icon_is(q, end, \"else\")) {\n\t\t\tif (!icon_is(q, end + 1, \"{\")) return -1;\n\t\t\tclose = icon_mate(q, end + 1); if (close < 0) return -1; return close + 1;\n\t\t}\n\t\treturn end;\n\t}\n\tend = icon_end(q, start);\n\tif (!icon_is(q, end, \";\")) return -1;\n\treturn end + 1;\n}\n// construct a compiler return body in one allocation, retaining source lines.\nword* .u icon_eval_tokens(word* q, int start, int end) {\n\tint count = end - start;\n\tword* expr = rt.new(7 + count * 3); expr[0] = count + 2;\n\texpr[1] = run.T_ID; expr[2] = run.intern(\"return\"); expr[3] = 1;\n\tif (count) rt.copy_w(expr + 4, q + 1 + start * 3, count * 3);\n\texpr[4 + count * 3] = run.T_OP; expr[5 + count * 3] = run.intern(\";\"); expr[6 + count * 3] = 1;\n\treturn expr;\n}\n// scalar frame substitution cannot preserve lvalue operations or implicit\n// program-global reads. reject these before compiling or executing a range.\nint icon_frame_expression(word frame, int start, int end) {\n\tword* q = frame[0]; int i = start, s; word binding;\n\tif (!icon_range(q, start, end, 3)) return 0;\n\twhile (i < end) {\n\t\tif (icon_is(q, i, \"++\") || icon_is(q, i, \"--\")) return 0;\n\t\tif (icon_kind(q, i) == run.T_ID && !icon_is(q, i - 1, \".\")) {\n\t\t\tbinding = icon_binding(frame[5], icon_value(q, i));\n\t\t\tif (!binding) binding = icon_binding(frame[6], icon_value(q, i));\n\t\t\tif (!binding) {\n\t\t\t\ts = run.lookups(icon_value(q, i));\n\t\t\t\tif (s < 0 || (run.skind[s] != run.K_FN && run.skind[s] != run.K_CONST)) return 0;\n\t\t\t}\n\t\t}\n\t\ti++;\n\t}\n\treturn 1;\n}\nint icon_frame_eval(word frame, int start, int end) {\n\tword* expr = icon_eval_tokens(frame[0], start, end);\n\tint i = 1; word binding;\n\twhile (i < expr[0] - 1) {\n\t\tif (icon_kind(expr, i) == run.T_ID && !icon_is(expr, i - 1, \".\")) {\n\t\t\tbinding = icon_binding(frame[5], icon_value(expr, i));\n\t\t\tif (!bi"; - if (i == 5) return b"nding) binding = icon_binding(frame[6], icon_value(expr, i));\n\t\t\tif (binding) { expr[1 + i * 3] = run.T_NUM; expr[2 + i * 3] = icon_read(binding); }\n\t\t}\n\t\ti++;\n\t}\n\tint result = run.eval(expr); rt.free(expr); return result;\n}\nint icon_block(word*.m frame, int open, int resume) {\n\tint close = icon_mate(frame[0], open);\n\tif (close < 0) return icon_fail(frame);\n\tword block = rt.new(4); block[0] = close; block[1] = resume; block[2] = frame[7]; block[3] = frame[5]; frame[7] = block; frame[1] = open + 1; return 1;\n}\n\nstruct icon {\n\t// the last transformation status: 0 success, 1 invalid delimiters,\n\t// 2 invalid range or declaration, 3 unresolved replacement capture,\n\t// 4 unavailable function metadata, 5 unsupported stepping syntax,\n\t// 6 rejected function publication.\n\tstatic int error() { return icon_error; }\n\t// check token delimiters without compiling or executing the list.\n\tstatic int validate(word* q) { icon_error = 0; if (!q || q[0] < 0 || !icon_balanced(q, 0, q[0])) { icon_error = 1; return 0; } return 1; }\n\t// return an independent token list with [start, end) replaced by insert.\n\tstatic word* .u splice(word* q, int start, int end, word* insert) {\n\t\ticon_error = 0;\n\t\tif (!q || !insert || q[0] < 0 || insert[0] < 0 || start < 0 || end < start || end > q[0]) { icon_error = 2; return 0; }\n\t\tword* r = icon_splice(q, start, end, insert);\n\t\tif (!icon_balanced(r, 0, r[0])) { rt.free(r); icon_error = 1; return 0; }\n\t\treturn r;\n\t}\n\t// replace each nonoverlapping pattern match. inserted tokens are not\n\t// searched again. the input and templates remain unchanged.\n\tstatic word* .u rewrite(word* q, word* pattern, word* replacement) {\n\t\ticon_error = 0;\n\t\tif (!q || q[0] < 0 || !replacement || replacement[0] < 0 || !icon_pattern(pattern) || !icon_balanced(q, 0, q[0])) { icon_error = 1; return 0; }\n\t\tword* result = icon_slice(q, 0, q[0]); word* caps = rt.new(1 + pattern[0] * 4); word* insert, next;\n\t\tint at = 0, end;\n\t\twhile (at < result[0]) {\n\t\t\tcaps[0] = 0; end = icon_match(result, at, pattern, 0, caps);\n\t\t\tif (!end) { at++; continue; }\n\t\t\tinsert = icon_expand(result, replacement, caps);\n\t\t\tif (!insert) { rt.free(result); rt.free(caps); return 0; }\n\t\t\tnext = icon_splice(result, at, end - 1, insert); at += insert[0];\n\t\t\trt.free(insert); rt.free(result); result = next;\n\t\t}\n\t\trt.free(caps); return result;\n\t}\n\t// transform retained function tokens and publish the compiled replacement.\n\t// references to the original entry continue to use the published version.\n\tstatic int find_replace(word function, word* pattern, word* replacement) {\n\t\ticon_error = 0;\n\t\tif (!replacement || replacement[0] < 0 || !icon_pattern(pattern)) { icon_error = 1; return 0; }\n\t\tword* q = run.function_tokens(function);\n\t\tif (!q) { icon_error = 4; return 0; }\n\t\tword* caps = rt.new(1 + pattern[0] * 4); int i = 0, end, count = 0;\n\t\twhile (i < q[0]) { caps[0] = 0; end = icon_match(q, i, pattern, 0, caps); if (end) { count++; i = end - 1; } else i++; }\n\t\trt.free(caps);\n\t\tif (!count) { rt.free(q); "; - if (i == 6) return b"return 0; }\n\t\tword* changed = icon.rewrite(q, pattern, replacement); rt.free(q);\n\t\tif (!changed) return 0;\n\t\tint published = run.replace_function(function, changed); rt.free(changed);\n\t\tif (!published) { icon_error = 6; return 0; }\n\t\treturn count;\n\t}\n\t// remove a complete declaration statement containing the requested name.\n\t// declarations containing multiple bindings are rejected to preserve them.\n\tstatic word* .u undeclare(word* q, char* name) {\n\t\ticon_error = 0;\n\t\tint declaration[4];\n\t\tif (!q || q[0] < 0 || !name || !icon_locate_declaration(q, run.intern(name), declaration)) { icon_error = 2; return 0; }\n\t\tword* empty = rt.new(1); empty[0] = 0;\n\t\tword* result = icon.splice(q, declaration[3], declaration[2] + 1, empty); rt.free(empty); return result;\n\t}\n\t// change the type of the first simple declaration of name.\n\tstatic word* .u change_type(word* q, char* name, word* type) {\n\t\ticon_error = 0;\n\t\tint declaration[4];\n\t\tif (!q || q[0] < 0 || !name || !icon_locate_declaration(q, run.intern(name), declaration)) { icon_error = 2; return 0; }\n\t\treturn icon.splice(q, declaration[0], declaration[1], type);\n\t}\n\t// insert a scoped statement block at the given token boundary.\n\tstatic word* .u insert_block(word* q, int at, word* body) {\n\t\ticon_error = 0;\n\t\tif (!q || !body || body[0] < 0 || at < 0 || at > q[0]) { icon_error = 2; return 0; }\n\t\tword* block = rt.new(7 + body[0] * 3); int i = 0; block[0] = body[0] + 2;\n\t\tblock[1] = run.T_OP; block[2] = run.intern(\"{\"); block[3] = 1;\n\t\twhile (i < body[0] * 3) { block[4 + i] = body[1 + i]; i++; }\n\t\tblock[4 + i] = run.T_OP; block[5 + i] = run.intern(\"}\"); block[6 + i] = 1;\n\t\tword* result = icon.splice(q, at, at, block); rt.free(block); return result;\n\t}\n\t// evaluate a selected expression using the compiler rather than a text\n\t// evaluator. the range must contain one complete expression.\n\tstatic int calculate(word* q, int start, int end) {\n\t\ticon_error = 0;\n\t\tif (!q || start < 0 || end > q[0] || !icon_range(q, start, end, 3)) { icon_error = 2; return 0; }\n\t\tword* expr = icon_eval_tokens(q, start, end);\n\t\tint value = run.eval(expr); rt.free(expr); return value;\n\t}\n\t// start a resumable evaluation with independent tokens and binding storage.\n\t// stepping executes statements through the compiler; no native frame is\n\t// suspended or inspected. scalar bindings are visible between statements.\n\tstatic word* .u begin(word* q) {\n\t\tif (!icon.validate(q)) { icon_error = 1; return 0; }\n\t\tword* frame = rt.new(9); int i = 0; while (i < 9) { frame[i] = 0; i++; } frame[0] = icon_slice(q, 0, q[0]); return frame;\n\t}\n\t// create an explicit frame from a function's retained body. argument values\n\t// are supplied through set_local before stepping starts.\n\tstatic word* .u begin_function(int function) {\n\t\tword* q = run.function_tokens(function);\n\t\tif (!q) { icon_error = 4; return 0; }\n\t\tword* frame = icon.begin(q); rt.free(q); return frame;\n\t}\n\t// set an isolated global binding, or update storage explicitly bound to it.\n\tstatic int set_gl"; - if (i == 7) return b"obal(word frame, char* name, int value) { icon_error = 0; return icon_store(frame, 6, run.intern(name), value); }\n\t// update the innermost local binding, creating a binding when absent.\n\tstatic int set_local(word frame, char* name, int value) { icon_error = 0; return icon_store(frame, 5, run.intern(name), value); }\n\t// bind one existing scalar global explicitly. reads observe its current\n\t// value, and subsequent frame assignments update its program storage.\n\tstatic int bind_global(word frame, char* name) {\n\t\tint s = run.lookups(name); icon_error = 0;\n\t\tif (s < 0 || run.skind[s] != run.K_GVAR) { icon_error = 2; return 0; }\n\t\tword type = run.stype[s];\n\t\tif (type && type != run.tint && type != run.tword) { icon_error = 2; return 0; }\n\t\ticon_store(frame, 6, run.intern(name), *run.sval[s]);\n\t\tword binding = icon_binding(frame[6], run.intern(name)); binding[3] = run.sval[s]; return 1;\n\t}\n\t// read the innermost local binding; a missing binding sets status 2.\n\tstatic int local(word frame, char* name) { icon_error = 0; word b = icon_binding(frame[5], run.intern(name)); if (!b) { icon_error = 2; return 0; } return icon_read(b); }\n\t// read an isolated or explicitly connected global binding.\n\tstatic int global(word frame, char* name) { icon_error = 0; word b = icon_binding(frame[6], run.intern(name)); if (!b) { icon_error = 2; return 0; } return icon_read(b); }\n\t// frame state: 0 resumable, 1 complete, 2 unsupported statement.\n\tstatic int state(word frame) { return frame[2]; }\n\t// the returned value after completion; zero before a return is executed.\n\tstatic int result(word frame) { return frame[3]; }\n\t// token index of the next statement or control-flow decision.\n\tstatic int position(word frame) { return frame[1]; }\n\t// number of executed statements and control-flow decisions.\n\tstatic int steps(word frame) { return frame[8]; }\n\t// number of retained local binding records, including active shadowed records.\n\tstatic int local_count(word frame) { word b = frame[5]; int n = 0; while (b) { n++; b = b[2]; } return n; }\n\t// interned name of a local record; zero when index is outside the map.\n\tstatic char* local_name(word frame, int index) { word b = frame[5]; while (b && index > 0) { index--; b = b[2]; } if (!b || index < 0) return 0; return b[0]; }\n\t// execute one statement or control-flow decision. returns 1 after progress,\n\t// 0 after completion or an unsupported statement. blocks, if and while\n\t// require braces; declarations contain one scalar binding.\n\tstatic int step(word frame) {\n\t\tif (!frame || frame[2]) return 0;\n\t\ticon_error = 0;\n\t\tword* q = frame[0]; int at = frame[1], end, name, value, column = 5, v, close, open, alternate;\n\t\tword block, binding;\n\t\twhile (frame[7] && at == frame[7][0]) {\n\t\t\tblock = frame[7];\n\t\t\twhile (frame[5] != block[3]) { binding = frame[5]; frame[5] = binding[2]; rt.free(binding); }\n\t\t\tat = block[1]; frame[7] = block[2]; rt.free(block); frame[1] = at;\n\t\t}\n\t\tif (at >= q[0]) { frame[2] = 1; return 0; }\n\t\tframe[8]++;\n\t\tif (icon_is(q"; - if (i == 8) return b", at, \";\")) { frame[1] = at + 1; return 1; }\n\t\tif (icon_is(q, at, \"{\")) { close = icon_mate(q, at); return icon_block(frame, at, close + 1); }\n\t\tif (icon_is(q, at, \"if\") || icon_is(q, at, \"while\")) {\n\t\t\tif (!icon_is(q, at + 1, \"(\")) return icon_fail(frame);\n\t\t\tclose = icon_mate(q, at + 1); open = close + 1;\n\t\t\tif (close < 0 || !icon_is(q, open, \"{\")) return icon_fail(frame);\n\t\t\tend = icon_mate(q, open); if (end < 0) return icon_fail(frame);\n\t\t\talternate = end + 1;\n\t\t\tif (icon_is(q, at, \"if\") && icon_is(q, alternate, \"else\")) {\n\t\t\t\tif (!icon_is(q, alternate + 1, \"{\")) return icon_fail(frame);\n\t\t\t}\n\t\t\tif (!icon_frame_expression(frame, at + 2, close)) return icon_fail(frame);\n\t\t\tvalue = icon_frame_eval(frame, at + 2, close);\n\t\t\tif (icon_is(q, at, \"while\")) { if (value) return icon_block(frame, open, at); frame[1] = end + 1; return 1; }\n\t\t\tif (icon_is(q, alternate, \"else\")) {\n\t\t\t\tclose = icon_mate(q, alternate + 1);\n\t\t\t\tif (value) return icon_block(frame, open, close + 1);\n\t\t\t\treturn icon_block(frame, alternate + 1, close + 1);\n\t\t\t}\n\t\t\tif (value) return icon_block(frame, open, end + 1);\n\t\t\tframe[1] = end + 1; return 1;\n\t\t}\n\t\tend = icon_end(q, at);\n\t\tif (end >= q[0] || !icon_is(q, end, \";\")) return icon_fail(frame);\n\t\tif (icon_is(q, at, \"return\")) {\n\t\t\tframe[3] = 0;\n\t\t\tif (end > at + 1) { if (!icon_frame_expression(frame, at + 1, end)) return icon_fail(frame); frame[3] = icon_frame_eval(frame, at + 1, end); }\n\t\t\tframe[1] = end + 1; frame[2] = 1; return 1;\n\t\t}\n\t\tif (icon_is(q, at, \"let\") || icon_is(q, at, \"int\") || icon_is(q, at, \"word\")) {\n\t\t\tif (icon_kind(q, at + 1) != run.T_ID) return icon_fail(frame);\n\t\t\tname = icon_value(q, at + 1); value = 0;\n\t\t\tif (end > at + 2) { if (!icon_is(q, at + 2, \"=\") || !icon_frame_expression(frame, at + 3, end)) return icon_fail(frame); value = icon_frame_eval(frame, at + 3, end); }\n\t\t\ticon_declare(frame, name, value); frame[1] = end + 1; return 1;\n\t\t}\n\t\tname = icon_value(q, at); binding = icon_binding(frame[5], name);\n\t\tif (!binding) { binding = icon_binding(frame[6], name); column = 6; }\n\t\tif (binding && icon_kind(q, at) == run.T_ID) {\n\t\t\tv = icon_value(q, at + 1);\n\t\t\tif (end == at + 2 && (v == run.intern(\"++\") || v == run.intern(\"--\"))) {\n\t\t\t\tvalue = icon_read(binding) + 1; if (v == run.intern(\"--\")) value = icon_read(binding) - 1;\n\t\t\t} else {\n\t\t\t\tif (v != run.intern(\"=\") && v != run.intern(\"+=\") && v != run.intern(\"-=\") && v != run.intern(\"*=\") && v != run.intern(\"/=\")) return icon_fail(frame);\n\t\t\t\tif (!icon_frame_expression(frame, at + 2, end)) return icon_fail(frame);\n\t\t\t\tvalue = icon_frame_eval(frame, at + 2, end);\n\t\t\t\tif (v == run.intern(\"+=\")) value = icon_read(binding) + value;\n\t\t\t\tif (v == run.intern(\"-=\")) value = icon_read(binding) - value;\n\t\t\t\tif (v == run.intern(\"*=\")) value = icon_read(binding) * value;\n\t\t\t\tif (v == run.intern(\"/=\")) value = icon_read(binding) / value;\n\t\t\t}\n\t\t\ticon_store(frame, column, name, value); frame[1] = end + 1; return 1;\n\t\t}\n\t\treturn icon_fail(frame);\n\t}\n\t// execute at most budg"; - if (i == 9) return b"et statements. returning 0 leaves the frame resumable;\n\t// callers can inspect bindings, edit them, then continue with another budget.\n\tstatic int resume(word frame, int budget) { while (budget > 0 && !frame[2]) { icon.step(frame); budget--; } return frame[2]; }\n\t// release a frame and all storage it owns.\n\tstatic int close(.d frame) {\n\t\tword b, next; int column = 5;\n\t\twhile (column <= 7) { b = frame[column]; while (b) { next = b[2]; rt.free(b); b = next; } column++; }\n\t\trt.free(frame[0]); rt.free(frame); return 0;\n\t}\n}\n\n// public entry points for the icon namespace.\n\n// status of the most recent transformation or binding operation; see docs/ICON.md.\nint error() { return icon.icon.error(); }\n// check delimiter structure only; does not compile or execute tokens.\nint validate(word* q) { return icon.icon.validate(q); }\n// return a new token list with [start, end) replaced; caller owns the result.\nword* .u splice(word* q, int start, int end, word* insert) { return icon.icon.splice(q, start, end, insert); }\n// return a new list with nonoverlapping matches replaced; inputs remain unchanged.\nword* .u rewrite(word* q, word* pattern, word* replacement) { return icon.icon.rewrite(q, pattern, replacement); }\n// publish a transformed function body; returns matches, or 0 on no match or failure.\nint find_replace(word function, word* pattern, word* replacement) { return icon.icon.find_replace(function, pattern, replacement); }\n// remove the first complete single-binding declaration named name; caller owns the result.\nword* .u undeclare(word* q, char* name) { return icon.icon.undeclare(q, name); }\n// replace the type prefix of the first simple declaration named name; caller owns the result.\nword* .u change_type(word* q, char* name, word* type) { return icon.icon.change_type(q, name, type); }\n// insert a scoped block containing body at token boundary at; caller owns the result.\nword* .u insert_block(word* q, int at, word* body) { return icon.icon.insert_block(q, at, body); }\n// compile and evaluate the expression in [start, end); expression calls retain their effects.\nint calculate(word* q, int start, int end) { return icon.icon.calculate(q, start, end); }\n// create a frame with copied tokens and isolated bindings; caller closes the frame.\nword* .u begin(word* q) { return icon.icon.begin(q); }\n// create a frame from retained function tokens; set arguments as locals before stepping.\nword* .u begin_function(int function) { return icon.icon.begin_function(function); }\n// set an isolated global binding, creating it when absent; returns the stored value.\nint set_global(word frame, char* name, int value) { return icon.icon.set_global(frame, name, value); }\n// set the innermost local binding, creating it when absent; returns the stored value.\nint set_local(word frame, char* name, int value) { return icon.icon.set_local(frame, name, value); }\n// connect an int or word program global to the frame; later frame writes update it.\nint bind_global(word frame, char* name) { ret"; - if (i == 10) return b"urn icon.icon.bind_global(frame, name); }\n// read a local binding; missing names return 0 and set error status 2.\nint local(word frame, char* name) { return icon.icon.local(frame, name); }\n// read a frame global binding; missing names return 0 and set error status 2.\nint global(word frame, char* name) { return icon.icon.global(frame, name); }\n// return frame state: 0 resumable, 1 complete, or 2 unsupported statement.\nint state(word frame) { return icon.icon.state(frame); }\n// return the value produced by return, or 0 before a return is executed.\nint result(word frame) { return icon.icon.result(frame); }\n// return the token index of the next statement or control-flow decision.\nint position(word frame) { return icon.icon.position(frame); }\n// return the number of statements and control-flow decisions executed.\nint steps(word frame) { return icon.icon.steps(frame); }\n// count retained local records, including shadowed records in active blocks.\nint local_count(word frame) { return icon.icon.local_count(frame); }\n// return the zero-based local record name, or 0 when index is out of range.\nchar* local_name(word frame, int index) { return icon.icon.local_name(frame, index); }\n// execute scalar declarations and assignments, standalone ++/--, return, blocks,\n// or braced if/else/while; direct program globals need bind_global first.\n// unsupported syntax sets error status 5.\nint step(word frame) { return icon.icon.step(frame); }\n// execute at most budget steps and return the frame state; frame bindings are scalar int/word values.\nint resume(word frame, int budget) { return icon.icon.resume(frame, budget); }\n// release the frame and owned storage; consumes frame and returns 0.\nint close(.d frame) { return icon.icon.close(d*frame); }\n"; - return 0; -} -// ---- end embedded icon library ---- +// the source of the std and icon libraries, embedded when the compiler is built, so +// `include std;` and `include icon;` need no file at run time. +embed "src/lib/std.zc" as bstr std_embedded; +embed "src/lib/icon.zc" as bstr icon_embedded; +bstr std_source() { return std_embedded; } +bstr icon_source() { return icon_embedded; } diff --git a/src/core/core.c b/src/core/core.c index 3a1d0ca..dc50644 100644 --- a/src/core/core.c +++ b/src/core/core.c @@ -58,13 +58,7 @@ #include <sys/mman.h> #include <ucontext.h> #endif -#ifdef USE_LSP_GENOME -#include "lsp_genome.h" -#elif defined(GENOME_H) -#include GENOME_H -#else #include "genome.h" -#endif #include "memmap.h" #include "fops.h" #include "bops.h" @@ -6208,7 +6202,7 @@ int main(int argc, char **argv) fprintf(stderr, "zy: genome does not fit\n"); return 1; } - memcpy(m + GORG, genome, sizeof genome); + for (int g = 0; g < GLEN; g++) m[GORG + g] = genome_word(g); m[ZFP + 6] = FBOT; m[ZFP + 7] = FTOP; m[ZFP + 8] = 1024; #ifdef ZYJIT { diff --git a/src/core/genome.h b/src/core/genome.h new file mode 100644 index 0000000..9d0ff96 --- /dev/null +++ b/src/core/genome.h @@ -0,0 +1,35 @@ +/* the genome: the image a core boots, embedded into the core when it is + compiled. an image is a file of little-endian 64-bit words placed at GORG, + as written by zc0 or by src/tools/build.zc and checked by mkgenome. the + build keeps every image in build/, never in the tree. + + GENOME_FILE names the image. it defaults to "genome.bin" (or + "lsp_genome.bin" with USE_LSP_GENOME) and is found next to this header, + then in the directories given with --embed-dir, so that option selects + which image a core carries. the file needs a compiler with c23 + #embed (clang 19 or gcc 15 and later). + + no libc here: the microcore includes this on bare metal. */ +#ifndef GENOME_H_INCLUDED +#define GENOME_H_INCLUDED +#include <stdint.h> +#ifndef GENOME_FILE +#ifdef USE_LSP_GENOME +#define GENOME_FILE "lsp_genome.bin" +#else +#define GENOME_FILE "genome.bin" +#endif +#endif +static const unsigned char genome_image[] = { +#embed GENOME_FILE +}; +/* the image's length in words */ +#define GLEN ((int)(sizeof genome_image / 8)) +/* word i of the image, independent of the host's byte order */ +static int64_t genome_word(int i) +{ + const unsigned char *b = genome_image + (long)i * 8; + return (int64_t)((uint64_t)b[0] | (uint64_t)b[1] << 8 | (uint64_t)b[2] << 16 | (uint64_t)b[3] << 24 + | (uint64_t)b[4] << 32 | (uint64_t)b[5] << 40 | (uint64_t)b[6] << 48 | (uint64_t)b[7] << 56); +} +#endif diff --git a/src/core/memmap.h b/src/core/memmap.h deleted file mode 100644 index cc768d4..0000000 --- a/src/core/memmap.h +++ /dev/null @@ -1,34 +0,0 @@ -/* generated by src/tools/genmap for the default profile. do not edit. */ -#ifndef MEMMAP_H -#define MEMMAP_H -#define PW 0x1D5000 -#define CODE_END 0x80000 -#define HEAP_LO 0x1D5000 -#define HEAP_HI 0x235000 -#define HEAP_GROW 1 -#ifndef MW -#define MW 0x8000000 -#endif -#if MW < HEAP_HI -#error MW is below the end of the initial heap -#endif -#define ZFP 0x90000 -#ifdef GORG -#if GORG != 0x114100 -#error genome GORG does not match this map -#endif -#else -#define GORG 0x114100 -#endif -#define FBOT 0x174100 -#define FTOP 0x1B4100 -#define RS0 0x1B4100 -#define MARK0 0x1C4100 -#define MARKN 0x8000 -#define DSLO 0x1CC100 -#define DSHI 0x1D4FF0 -#define DS0 DSHI -#define SYSGATE (ZFP + 2) -#define DSLIM (DSLO + 1100) -#define RSLIM (MARK0 - 8) -#endif diff --git a/src/js/extract_genome.js b/src/js/extract_genome.js deleted file mode 100644 index 4ef2c43..0000000 --- a/src/js/extract_genome.js +++ /dev/null @@ -1,93 +0,0 @@ -// converts the compiler genome and the memory map into the javascript vm's -// layout files. everything it writes is generated, so it goes to build/js/ and -// nothing is left in the tree. -// -// node src/js/extract_genome.js [--genome PATH] [--map PATH] [--out DIR] -// -// the genome is the one make genome produced in build/core/genome.h, and the -// map is src/core/memmap.h. outputs, in the out directory: -// genome.json the genome words, for the vm that has no compiler inside it -// layout.ts the map as typescript constants -// layout.js the map as commonjs constants -const fs = require('fs'); -const path = require('path'); -const root = path.join(__dirname, '..', '..'); - -const args = process.argv.slice(2); -const opt = (name) => { - const i = args.indexOf(name); - return i >= 0 && args[i + 1] ? args[i + 1] : null; -}; -if (args.includes('--help') || args.includes('-h')) { - console.log([ - 'usage: node src/js/extract_genome.js [--genome PATH] [--map PATH] [--out DIR]', - 'converts the compiler genome and memory map into the javascript layout files.', - 'options:', - ' --genome PATH the genome header to read; defaults to build/core/genome.h', - ' --map PATH the memory map header to read; defaults to src/core/memmap.h', - ' --out DIR where to write; defaults to build/js', - ' --help, -h show this help and exit', - ].join('\n')); - process.exit(0); -} -const unknown = args.filter((a) => !['--genome', '--map', '--out'].includes(a)); -if (unknown.length) { - console.error('extract_genome: unexpected arguments: ' + unknown.join(' ') + '; use --help for usage'); - process.exit(2); -} - -const genomePath = opt('--genome') || path.join(root, 'build', 'core', 'genome.h'); -const mapPath = opt('--map') || path.join(__dirname, '..', 'core', 'memmap.h'); -const outDir = opt('--out') || path.join(root, 'build', 'js'); - -if (!fs.existsSync(genomePath)) { - console.error('extract_genome: no genome at ' + genomePath + '; run `make genome` first'); - process.exit(1); -} -fs.mkdirSync(outDir, { recursive: true }); - -const text = fs.readFileSync(genomePath, 'utf8'); -const gorgMatch = text.match(/#define\s+GORG\s+(\d+)/); -const glenMatch = text.match(/#define\s+GLEN\s+(\d+)/); - -const gorg = parseInt(gorgMatch[1], 10); -const glen = parseInt(glenMatch[1], 10); - -const arrMatch = text.match(/genome\[\]\s*=\s*\{([\s\S]*?)\};/); -if (!arrMatch) { - console.error('extract_genome: could not find the genome array in ' + genomePath); - process.exit(1); -} - -const words = arrMatch[1] - .split(',') - .map(s => s.trim()) - .filter(s => s.length > 0); - -console.log(`Parsed genome: GORG=${gorg}, GLEN=${words.length}`); - -if (words.length !== glen) throw new Error('genome length does not match GLEN'); - -// the compiler's map, as typescript constants: the js vm reads them instead of -// its own copies. the values are computed here, from the map. -const { mapValues } = require('./mapvalues.js'); -const names = ['MW', 'PW', 'ZFP', 'GORG', 'DS0', 'DSLIM', 'RS0', 'RSLIM', 'SYSGATE', 'HEAP_LO', 'HEAP_HI']; -const map = mapValues(mapPath, names); -if (map.GORG !== gorg) throw new Error('genome GORG ' + gorg + ' is not the map GORG ' + map.GORG); - -const genomeJson = path.join(outDir, 'genome.json'); -fs.writeFileSync(genomeJson, JSON.stringify({ gorg, glen, words })); -console.log('saved genome to ' + genomeJson); - -const lines = names.map(k => ` ${k}: ${map[k]},`).join('\n'); -const layoutTs = path.join(outDir, 'layout.ts'); -fs.writeFileSync(layoutTs, - '// generated by src/js/extract_genome.js from ' + path.relative(root, mapPath) + '. do not edit.\n' + - 'export const MAP = {\n' + lines + '\n} as const;\n'); -console.log('saved the map to ' + layoutTs); - -const layoutJs = path.join(outDir, 'layout.js'); -fs.writeFileSync(layoutJs, - '// generated by src/js/extract_genome.js from ' + path.relative(root, mapPath) + '. do not edit.\n' + - 'exports.MAP = ' + JSON.stringify(map, null, 4) + ';\n'); -console.log('saved the map to ' + layoutJs);
\ No newline at end of file diff --git a/src/js/extract_lsp.js b/src/js/extract_lsp.js index 14ee279..bdbcb3c 100644 --- a/src/js/extract_lsp.js +++ b/src/js/extract_lsp.js @@ -1,13 +1,12 @@ -// writes the language server's javascript image: the genome build/lsp/genome.h, -// made by src/tools/mklsp.sh, and the map it was built for, read from -// build/lsp/map/memmap.h. the vm needs the layout, because the server's map is -// not the compiler's. the image is generated, so it goes to build/js/ and -// nothing is left in the tree. +// writes the language server's javascript image: build/js/lsp.bin, the server's +// genome (build/lsp/lsp_genome.bin, made by src/tools/mklsp.sh), and +// build/js/lsp.json, the map it was built for, read from build/lsp/memmap.h. the +// vm needs the layout, because the server's map is not the compiler's. the image +// is generated, so it goes to build/js/ and nothing is left in the tree. // -// node src/js/extract_lsp.js [--out PATH] +// node src/js/extract_lsp.js [--lsp DIR] [--out DIR] // -// build the lsp first (`make lsp`, or src/tools/mklsp.sh); it needs -// build/lsp/genome.h. +// build the lsp first (`make lsp`, or src/tools/mklsp.sh). const fs = require('fs'); const path = require('path'); const root = path.join(__dirname, '..', '..'); @@ -15,42 +14,39 @@ const root = path.join(__dirname, '..', '..'); const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { console.log([ - 'usage: node src/js/extract_lsp.js [--out PATH]', - 'converts the language server genome and its memory map into a javascript image.', + 'usage: node src/js/extract_lsp.js [--lsp DIR] [--out DIR]', + 'copies the language server genome beside a json description of its memory map.', 'options:', - ' --out PATH where to write; defaults to build/js/lsp.json', + ' --lsp DIR the language server build to read; defaults to build/lsp', + ' --out DIR where to write lsp.bin and lsp.json; defaults to build/js', ' --help, -h show this help and exit', ].join('\n')); process.exit(0); } -let out = path.join(root, 'build', 'js', 'lsp.json'); +let out = path.join(root, 'build', 'js'), lsp = path.join(root, 'build', 'lsp'); for (let i = 0; i < args.length; i++) { - if (args[i] === '--out' && args[i + 1]) out = args[i + 1]; + if (args[i] === '--out' && args[i + 1]) out = args[++i]; + else if (args[i] === '--lsp' && args[i + 1]) lsp = args[++i]; else { console.error('extract_lsp: unexpected arguments; use --help for usage'); process.exit(2); } } -const genomePath = path.join(root, 'build', 'lsp', 'genome.h'); -const mapPath = path.join(root, 'build', 'lsp', 'map', 'memmap.h'); +const genomePath = path.join(lsp, 'lsp_genome.bin'); +const mapPath = path.join(lsp, 'memmap.h'); if (!fs.existsSync(genomePath)) { console.error('extract_lsp: no language server image at ' + genomePath + '; run `make lsp` first'); process.exit(1); } -const genome = fs.readFileSync(genomePath, 'utf8'); - const { mapValues } = require('./mapvalues.js'); const layout = mapValues(mapPath, ['GORG', 'DS0', 'DSLIM', 'RS0', 'RSLIM', 'SYSGATE', 'ZFP', 'HEAP_LO', 'HEAP_HI']); +const image = fs.readFileSync(genomePath); +if (image.length % 8) throw new Error(genomePath + ' is not a whole number of words'); +const glen = image.length / 8; -const gorg = parseInt(genome.match(/#define\s+GORG\s+(\d+)/)[1], 10); -const glen = parseInt(genome.match(/#define\s+GLEN\s+(\d+)/)[1], 10); -const arr = genome.match(/genome\[\]\s*=\s*\{([\s\S]*?)\};/); -const words = arr[1].split(',').map(s => s.trim()).filter(s => s.length > 0); -if (words.length !== glen) throw new Error('genome has ' + words.length + ' words, GLEN says ' + glen); -if (gorg !== layout.GORG) throw new Error('genome GORG ' + gorg + ' is not the map GORG ' + layout.GORG); - -fs.mkdirSync(path.dirname(out), { recursive: true }); -fs.writeFileSync(out, JSON.stringify({ gorg, glen, layout, words })); -console.log('wrote ' + path.relative(root, out) + ': genome ' + glen + ' words at ' + gorg + - ', heap ' + layout.HEAP_LO + '..' + layout.HEAP_HI);
\ No newline at end of file +fs.mkdirSync(out, { recursive: true }); +fs.copyFileSync(genomePath, path.join(out, 'lsp.bin')); +fs.writeFileSync(path.join(out, 'lsp.json'), JSON.stringify({ gorg: layout.GORG, glen, layout })); +console.log('wrote ' + path.relative(root, path.join(out, 'lsp.bin')) + ': genome ' + glen + ' words at ' + layout.GORG + + ', heap ' + layout.HEAP_LO + '..' + layout.HEAP_HI); diff --git a/src/js/layout.d.ts b/src/js/layout.d.ts index 1e8c004..9a531eb 100644 --- a/src/js/layout.d.ts +++ b/src/js/layout.d.ts @@ -1,5 +1,5 @@ // types for the map the javascript vm reads at runtime. the values are -// generated by src/js/extract_genome.js into build/js/layout.js, which sits +// generated by src/js/mklayout.js into build/js/layout.js, which sits // beside the compiled vm; this declaration is here so tinyvm.ts typechecks // without a generated file in the tree. export declare const MAP: { diff --git a/src/js/mapvalues.js b/src/js/mapvalues.js index f701ba2..bb6fc86 100644 --- a/src/js/mapvalues.js +++ b/src/js/mapvalues.js @@ -1,6 +1,6 @@ // reads the values of a machine map from its memmap.h. the #define lines may use // other names and small sums (ZFP + 2, DSLO + 1100), so each value is evaluated -// with its names substituted. shared by extract_genome.js and extract_lsp.js. +// with its names substituted. shared by mklayout.js and extract_lsp.js. const fs = require('fs'); function mapValues(mapPath, names) { diff --git a/src/js/mklayout.js b/src/js/mklayout.js new file mode 100644 index 0000000..cf95091 --- /dev/null +++ b/src/js/mklayout.js @@ -0,0 +1,61 @@ +// writes the javascript vm's view of a memory map: the map's constants as +// typescript and commonjs modules. the vm reads its genome image itself, from +// genome.bin beside tinyvm.js, so only the layout is generated here. everything +// it writes is generated, so it goes to build/js/ and nothing is left in the tree. +// +// node src/js/mklayout.js [--map PATH] [--out DIR] +// +// the map is the default one `make` generates in build/core/map/memmap.h. +// outputs, in the out directory: +// layout.ts the map as typescript constants +// layout.js the map as commonjs constants +const fs = require('fs'); +const path = require('path'); +const root = path.join(__dirname, '..', '..'); + +const args = process.argv.slice(2); +const opt = (name) => { + const i = args.indexOf(name); + return i >= 0 && args[i + 1] ? args[i + 1] : null; +}; +if (args.includes('--help') || args.includes('-h')) { + console.log([ + 'usage: node src/js/mklayout.js [--map PATH] [--out DIR]', + 'converts a memory map header into the javascript vm layout files.', + 'options:', + ' --map PATH the memory map header to read; defaults to build/core/map/memmap.h', + ' --out DIR where to write; defaults to build/js', + ' --help, -h show this help and exit', + ].join('\n')); + process.exit(0); +} +for (let i = 0; i < args.length; i += 2) { + if (!['--map', '--out'].includes(args[i]) || args[i + 1] === undefined) { + console.error('mklayout: unexpected arguments: ' + args.join(' ') + '; use --help for usage'); + process.exit(2); + } +} + +const mapPath = opt('--map') || path.join(root, 'build', 'core', 'map', 'memmap.h'); +const outDir = opt('--out') || path.join(root, 'build', 'js'); +if (!fs.existsSync(mapPath)) { + console.error('mklayout: no memory map at ' + mapPath + '; run `make` first'); + process.exit(1); +} +fs.mkdirSync(outDir, { recursive: true }); + +// the compiler's map, as constants: the js vm reads them instead of its own copies. +const { mapValues } = require('./mapvalues.js'); +const names = ['MW', 'PW', 'ZFP', 'GORG', 'DS0', 'DSLIM', 'RS0', 'RSLIM', 'SYSGATE', 'HEAP_LO', 'HEAP_HI']; +const map = mapValues(mapPath, names); + +const lines = names.map(k => ` ${k}: ${map[k]},`).join('\n'); +const layoutTs = path.join(outDir, 'layout.ts'); +fs.writeFileSync(layoutTs, + '// generated by src/js/mklayout.js from ' + path.relative(root, mapPath) + '. do not edit.\n' + + 'export const MAP = {\n' + lines + '\n} as const;\n'); +const layoutJs = path.join(outDir, 'layout.js'); +fs.writeFileSync(layoutJs, + '// generated by src/js/mklayout.js from ' + path.relative(root, mapPath) + '. do not edit.\n' + + 'exports.MAP = ' + JSON.stringify(map, null, 4) + ';\n'); +console.log('wrote ' + path.relative(root, layoutJs) + ' and layout.ts'); diff --git a/src/js/tinyvm.ts b/src/js/tinyvm.ts index 4b4bc39..56fab70 100644 --- a/src/js/tinyvm.ts +++ b/src/js/tinyvm.ts @@ -10,7 +10,7 @@ declare const process: any, require: any, module: any, __dirname: string, Buffer const fs: any = typeof require === 'function' ? require('fs') : undefined; const path: any = typeof require === 'function' ? require('path') : undefined; -// the compiler's map, computed from src/core/memmap.h at build time (build/js/layout.js) +// the compiler's map, computed from build/core/map/memmap.h at build time (build/js/layout.js) import { MAP } from './layout'; // float bits stay in vm words; arithmetic uses javascript's binary64 numbers. @@ -257,6 +257,16 @@ export class TinyVM { } } + /** load an image file's bytes (little-endian 64-bit words, the format the c cores + #embed) at org. returns the number of words. */ + public loadImage(bytes: Uint8Array, org: number = this.L.GORG): number { + if (bytes.length % 8) throw new RangeError('an image is a whole number of 64-bit words'); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length); + const n = bytes.length / 8; + for (let i = 0; i < n; i++) this.mem[org + i] = view.getBigInt64(i * 8, true); + return n; + } + public getOutput(): string { return new TextDecoder().decode(new Uint8Array(this.outputBuffer)); } @@ -931,13 +941,13 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma process.stderr.write('tinyvm.js: unknown option ' + option + '; use --help for usage\n'); process.exit(2); } - const genomePath = path.join(__dirname, 'genome.json'); + const genomePath = path.join(__dirname, 'genome.bin'); if (!fs.existsSync(genomePath)) { - console.error('Error: genome.json not found. Run extract_genome.js first.'); + console.error('Error: genome.bin not found beside tinyvm.js. Run make js first.'); process.exit(1); } - const genomeData = JSON.parse(fs.readFileSync(genomePath, 'utf8')); + const genome: Uint8Array = fs.readFileSync(genomePath); let inputSource = ''; if (process.argv.length > 2) { @@ -970,9 +980,9 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma onMessage: (msg: string) => process.stderr.write(msg) }); - vm.loadGenome(genomeData.words, genomeData.gorg); + vm.loadImage(genome, GORG); vm.setInput(inputSource); - const code = vm.run(genomeData.gorg); + const code = vm.run(GORG); process.exit(code); } diff --git a/src/lsp/server.zc b/src/lsp/server.zc index a07e4d4..48fdf90 100644 --- a/src/lsp/server.zc +++ b/src/lsp/server.zc @@ -1057,8 +1057,15 @@ int doc_gap(int base, int from, int to) { return 1; } // remove comment delimiters and optional line-leading stars; retain markdown. +// the line comment emitted last: consecutive // lines are one paragraph, joined by a soft break +int hct_doc = 0 - 1, hct_end, hct_hn; +int hover_comment_adjacent(int base, int from, int to) { + int lines = 0, c; + while (from < to) { c = dc(base, from); if (c == 10) lines++; else if (c != 32 && c != 9 && c != 13) return 0; from++; } + return lines <= 1; +} int hover_comment_text(int i, int index) { - int base = doff[i], o = hc_start[index] + 2, end = hc_end[index], bol = 1, c, before = hn; + int base = doff[i], o = hc_start[index] + 2, end = hc_end[index], bol = 1, c, before = hn, joined; if (hc_kind[index] == 3) { if (end >= o + 2 && dc(base, end - 2) == '*' && dc(base, end - 1) == '/') end = end - 2; if (o < end && dc(base, o) == '*') o++; @@ -1066,7 +1073,8 @@ int hover_comment_text(int i, int index) { while (o < end && (dc(base, o) == 32 || dc(base, o) == 9 || dc(base, o) == 10 || dc(base, o) == 13)) o++; while (end > o && (dc(base, end - 1) == 32 || dc(base, end - 1) == 9 || dc(base, end - 1) == 10 || dc(base, end - 1) == 13)) end--; if (o == end) return 0; - hov_text("\n\n"); + joined = hc_kind[index] != 3 && hct_doc == i && hct_hn == hn && hn > 0 && hover_comment_adjacent(base, hct_end, hc_start[index]); + if (joined) hov_put(10); else hov_text("\n\n"); while (o < end) { if (bol && hc_kind[index] == 3) { while (o < end && (dc(base, o) == 32 || dc(base, o) == 9)) o++; @@ -1078,6 +1086,7 @@ int hover_comment_text(int i, int index) { if (c != 13) hov_put(c); if (c == 10) bol = 1; } + if (hc_kind[index] != 3) { hct_doc = i; hct_end = hc_end[index]; hct_hn = hn; } else hct_doc = 0 - 1; return hn - before; } // a trailing declaration comment belongs to its last name, not the next line. @@ -2085,18 +2094,21 @@ int do_nav(int i, int refs) { } //: ---- the sources of this image ---- -//: the image carries the text of rt.zc, compiler.zc, server.zc, std.zc, icon.zc and core.zc (the -//: pieces src/tools/embed.sh made), so the definition of a built-in has somewhere to go. each source is a document +//: the image carries the text of rt.zc, compiler.zc, server.zc, std.zc, icon.zc and core.zc, embedded +//: when the image is built, so the definition of a built-in has somewhere to go. each source is a document //: of its own at tinyvm:/NAME.zc, which the editor shows read only and can ask for with zc/source const int BS_COUNT = 6; -bstr rt_part(int i); bstr compiler_part(int i); bstr server_part(int i); bstr core_part(int i); -bstr bs_part(int k, int i) { - if (k == 0) return rt_part(i); - if (k == 1) return compiler_part(i); - if (k == 2) return server_part(i); - if (k == 3) return run.std_part(i); - if (k == 4) return run.icon_part(i); - return core_part(i); +embed "src/runtime/rt.zc" as bstr rt_embedded; +embed "src/compiler/compiler.zc" as bstr compiler_embedded; +embed "src/lsp/server.zc" as bstr server_embedded; +embed "src/core/core.zc" as bstr core_embedded; +bstr bs_text(int k) { + if (k == 0) return rt_embedded; + if (k == 1) return compiler_embedded; + if (k == 2) return server_embedded; + if (k == 3) return run.std_source(); + if (k == 4) return run.icon_source(); + return core_embedded; } char* bs_uri(int k) { if (k == 0) return "tinyvm:/rt.zc"; @@ -2108,21 +2120,15 @@ char* bs_uri(int k) { } // the document of built-in source k, loaded the first time it is asked for int bs_load(int k) { - int i, n = 0, j = 0, b; bstr p; word text; + int i, n = 0, j = 0; bstr p; word text; if (!bs_idx) bs_idx = lsp_grow(0, 0, BS_COUNT); if (bs_idx[k]) return bs_idx[k] - 1; - while (1) { p = bs_part(k, j); if (!p) break; n = n + rt.len_s(p); j++; } - text = lsp_grow(0, 0, (n >> 2) + 3); - n = 0; j = 0; - while (1) { - p = bs_part(k, j); if (!p) break; - b = 0; - while (b < rt.len_s(p)) { - if ((n & 3) == 0) text[n >> 2] = 0; - text[n >> 2] = text[n >> 2] | (rt.at_s(p, b) << ((n & 3) * 8)); - n++; b++; - } - j++; + p = bs_text(k); j = rt.len_s(p); + text = lsp_grow(0, 0, (j >> 2) + 3); + while (n < j) { + if ((n & 3) == 0) text[n >> 2] = 0; + text[n >> 2] = text[n >> 2] | (rt.at_s(p, n) << ((n & 3) * 8)); + n++; } i = docnew(bs_uri(k), rt.len_w(bs_uri(k))); doff[i] = text; dsize[i] = n; dver[i] = 1; dgen[i]++; @@ -2334,8 +2340,13 @@ int fm_symbol(int base, int o, int size) { return n; } // split document i into tokens, each with the spaces before it on its line (-1 for the first of a line) +// the document the token arrays describe: hover lookups lex the same documents again and +// again, so a lex of an unchanged document is reused +int fm_doc = 0 - 1, fm_gen, fm_size; word fm_base; int fm_lex(int i) { int base = doff[i], size = dsize[i], o = 0, ln = 0, gap = 0 - 1, c, s, q, e, el, n; + if (fm_doc == i && fm_gen == dgen[i] && fm_base == base && fm_size == size && fm_s) return 0; + fm_doc = i; fm_gen = dgen[i]; fm_base = base; fm_size = size; fm_nt = 0; if (!fm_s) { fm_tcap = 256; diff --git a/src/micro/micro.c b/src/micro/micro.c index 528382a..f598c80 100644 --- a/src/micro/micro.c +++ b/src/micro/micro.c @@ -30,13 +30,7 @@ #else #define VM_TLS #endif -#ifdef USE_LSP_GENOME -#include "lsp_genome.h" -#elif defined(GENOME_H) -#include GENOME_H -#else #include "genome.h" -#endif #include "micro_port.h" #include "fops.h" #include "bops.h" @@ -779,7 +773,7 @@ int micro_run(void) micro_msg("zy: genome does not fit\n"); return 1; } - for (i = 0; i < GLEN; i++) m[GORG + i] = genome[i]; + for (i = 0; i < GLEN; i++) m[GORG + i] = genome_word(i); jknown = 0; return run(GORG); } diff --git a/src/runtime/rt.zc b/src/runtime/rt.zc index ee3594d..a54d88c 100644 --- a/src/runtime/rt.zc +++ b/src/runtime/rt.zc @@ -760,7 +760,10 @@ word .u new_t(int n, int t) { if (dbg & 1) note_alloc(p, alloc_line); return p; } -// allocate n words without a type stamp; the caller owns the fresh object. +// new(n) allocates n zeroed words without a type stamp. new(T) and new(T, n) allocate +// a fresh heap object of type T (n of them for the second form) and stamp T in its +// header; the compiler rewrites those forms to new_t or new_k. the caller owns it, +// and the analyzer frees it when its owner's block ends. word .u new(int n) { return new_t(n, 0); } // the compiler works out the size class of a constant-size new at compile // time (k is the class of n + HDR words), so a repeated allocation of the same @@ -1380,3 +1383,28 @@ char* .u readfile(char* path) { syscall(SYS_CLOSE, fd, 0, 0); return buf; } +// reads a file's bytes into an owned block, one byte to a word after a length word: +// [n, b0, b1, ...]. unlike readfile, a 0 byte in the file is kept. returns null if +// opening fails. +word.u readbytes(char* path) { + int fd; word buf; int n = 0, got, cap; word nb; + fd = syscall(SYS_OPEN, path, 0, 0); + if (fd < 0) return 0; + cap = 256; + buf = new(cap); + while (1) { + if (n + 130 >= cap) { + nb = new(cap + cap); + copy_w(nb, buf, n + 1); + free(buf); + buf = nb; + cap = cap + cap; + } + got = syscall(SYS_READ, fd, buf + 1 + n, 128); + if (got <= 0) break; + n = n + got; + } + buf[0] = n; + syscall(SYS_CLOSE, fd, 0, 0); + return buf; +} diff --git a/src/tools/automap.sh b/src/tools/automap.sh index 69849ca..ff559ac 100755 --- a/src/tools/automap.sh +++ b/src/tools/automap.sh @@ -3,7 +3,8 @@ # src/tools/automap.sh PRESET OUTDIR FILE... # PRESET is run, small, default or lsp. DOCS=1 makes the genome with its doc comments. FILE... are appended after the map's own # rt.zc (and compiler.zc, for the presets that carry the compiler). the result -# is OUTDIR/memmap.h and OUTDIR/genome.h, and they agree with each other. +# is OUTDIR/memmap.h and OUTDIR/genome.bin, and they agree with each other. a core +# compiled with -IOUTDIR --embed-dir=OUTDIR embeds that image (src/core/genome.h). # # a genome's length doesn't depend on where it lives (every address is a one # word literal), so two passes are enough: compile once on the preset's map @@ -14,15 +15,14 @@ # GENMAP can name a genmap that is already built. set -e case "${1:-}" in - --help|-h) printf '%s\n' 'usage: src/tools/automap.sh run|small|default|lsp OUTDIR FILE...' 'builds a genome and memory map sized to the supplied source files.' 'output: OUTDIR/genome.h and OUTDIR/memmap.h' 'environment: CC selects the c compiler; DOCS=1 retains documentation; GENMAP selects genmap.'; exit 0 ;; + --help|-h) printf '%s\n' 'usage: src/tools/automap.sh run|small|default|lsp OUTDIR FILE...' 'builds a genome and memory map sized to the supplied source files.' 'output: OUTDIR/genome.bin and OUTDIR/memmap.h' 'environment: CC selects the c compiler; DOCS=1 retains documentation; GENMAP selects genmap.'; exit 0 ;; -*) echo "automap: unknown option $1; use --help for usage" >&2; exit 2 ;; esac cd "$(dirname "$0")/../.." [[ $# -ge 2 ]] || { echo 'usage: src/tools/automap.sh run|small|default|lsp OUTDIR FILE...' >&2; exit 2; } P=$1; O=$2; shift 2 [ -n "$P" ] && [ -n "$O" ] || { echo "usage: src/tools/automap.sh run|small|default|lsp OUTDIR FILE..." >&2; exit 2; } -CC=${CC:-gcc} -bash src/tools/embed.sh all --check || bash src/tools/embed.sh all +CC=${CC:-$(sh src/tools/findcc.sh)} mkdir -p "$O" ZC0=$O/.zc0 if [ -z "$GENMAP" ]; then @@ -33,32 +33,36 @@ lay() { # $1 = genome slot, or empty for the preset's own rm -rf "$O/map" "$GENMAP" --preset "$P" --grow ${1:+--genome $1} -o "$O/map" >/dev/null 2>&1 $CC -O1 -w -fwrapv src/tools/zc0.c -I"$O/map" -Isrc/core -o "$ZC0" + $CC -O1 -w -fwrapv src/tools/mkgenome.c -I"$O/map" -o "$O/.mkgenome" } build() { - if [ -f "$O/map/compiler.zc" ]; then cat "$O/map/rt.zc" src/core/core.zc "$O/map/compiler.zc" "$@"; else cat "$O/map/rt.zc" src/core/core.zc "$@"; fi | "$ZC0" + if [ -f "$O/map/compiler.zc" ]; then cat "$O/map/rt.zc" src/core/core.zc "$O/map/compiler.zc" "$@"; else cat "$O/map/rt.zc" src/core/core.zc "$@"; fi | "$ZC0" > "$O/image.bin" + "$O/.mkgenome" < "$O/image.bin" > "$O/genome.bin" } # with DOCS=1 the genome is made again with zc0 built for docs (ZC0DOCS), so the # image carries the doc comments of the sources and the program can answer # docofs(). the image is the same code either way; the docs are stored after it. docs() { $CC -O1 -w -fwrapv -DZC0DOCS src/tools/zc0.c -I"$O/map" -Isrc/core -o "$O/.zc0docs" - if [ -f "$O/map/compiler.zc" ]; then cat "$O/map/rt.zc" src/core/core.zc "$O/map/compiler.zc" "$@"; else cat "$O/map/rt.zc" src/core/core.zc "$@"; fi | "$O/.zc0docs" + if [ -f "$O/map/compiler.zc" ]; then cat "$O/map/rt.zc" src/core/core.zc "$O/map/compiler.zc" "$@"; else cat "$O/map/rt.zc" src/core/core.zc "$@"; fi | "$O/.zc0docs" > "$O/image.bin" + "$O/.mkgenome" < "$O/image.bin" > "$O/genome.bin" } -len() { grep -m1 '#define GLEN' "$1" | awk '{print $3}'; } +len() { echo $(( $(wc -c < "$1") / 8 )); } lay "" -build "$@" > "$O/genome.h" -glen=$(len "$O/genome.h") +build "$@" +glen=$(len "$O/genome.bin") slot=$(( (glen + 4095) / 4096 * 4096 )) lay "$slot" -build "$@" > "$O/genome.h" +build "$@" if [ -n "$DOCS" ]; then lay $((slot + 65536)) # room for the image with its docs while it is being made - docs "$@" > "$O/genome.h" - glen=$(len "$O/genome.h") + docs "$@" + glen=$(len "$O/genome.bin") slot=$(( (glen + 4095) / 4096 * 4096 )) lay "$slot" - docs "$@" > "$O/genome.h" + docs "$@" fi -[ "$(len "$O/genome.h")" = "$glen" ] || { echo "automap: the genome changed length with the map" >&2; exit 1; } +rm -f "$O/image.bin" +[ "$(len "$O/genome.bin")" = "$glen" ] || { echo "automap: the genome changed length with the map" >&2; exit 1; } cp "$O/map/memmap.h" "$O/map/map.txt" "$O/" echo "automap $P: genome $glen words in a slot of $slot, $(grep -m1 '^MW' "$O/map/map.txt")" diff --git a/src/tools/bootstrap-check.sh b/src/tools/bootstrap-check.sh index a5de341..7acd74c 100755 --- a/src/tools/bootstrap-check.sh +++ b/src/tools/bootstrap-check.sh @@ -2,19 +2,23 @@ # rebuild the bootstrap one stage and report what the compiler says about itself. # zc0 (stage 0) compiles rt+core+compiler into build/s0, s0 becomes zy0, and zy0 # runs build.zc over the same sources to produce the image. ownership diagnostics -# come back in the image, so print them instead of the word dump. +# come back in place of the image; mkgenome rejects them and prints them. set -e +case "${1:-}" in + --help|-h) printf '%s\n' 'usage: src/tools/bootstrap-check.sh' 'runs the first two bootstrap stages and prints the compiler diagnostics on its own source, or the image size.'; exit 0 ;; + -*) echo "bootstrap-check: unknown option $1; use --help for usage" >&2; exit 2 ;; +esac cd "$(dirname "$0")/../.." -mkdir -p bin build/s0 build/s1 build/core -cc -O1 -w -fwrapv src/tools/zc0.c -Isrc/core -o bin/zc0 -cc -O1 -w -fwrapv src/tools/mkgenome.c -Isrc/core -o bin/mkgenome -cp src/core/core.c src/core/io_linux.c src/core/memmap.h src/core/fops.h src/core/bops.h src/core/stack.h build/s0/ -cat src/runtime/rt.zc src/core/core.zc src/compiler/compiler.zc | bin/zc0 > build/s0/genome.h -cc -O2 -w -fwrapv -pthread -DJTYPE=int build/s0/core.c build/s0/io_linux.c -o build/zy0 -(cat src/tools/build.zc; printf '`'; cat src/runtime/rt.zc src/core/core.zc src/compiler/compiler.zc) | build/zy0 > build/g1.bin -if ! grep -q '^zc: ' build/g1.bin 2>/dev/null; then - echo "image ok: $(wc -c < build/g1.bin) bytes" +make -s build/bin/zc0 build/bin/mkgenome +CC=${CC:-$(sh src/tools/findcc.sh)} +SRC="src/runtime/rt.zc src/core/core.zc src/compiler/compiler.zc" +mkdir -p build/s0 build/s1 +cat $SRC | build/bin/zc0 > build/s0/g0.bin +build/bin/mkgenome < build/s0/g0.bin > build/s0/genome.bin +$CC -O2 -w -fwrapv -pthread -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core/map --embed-dir=build/s0 -o build/s0/zy0 +(cat src/tools/build.zc; printf '`'; cat $SRC) | build/s0/zy0 > build/s1/g1.bin +if build/bin/mkgenome < build/s1/g1.bin > build/s1/check.bin; then + echo "image ok: $(wc -c < build/s1/g1.bin) bytes" else - echo "--- compiler diagnostics on its own source ---" - cat -v build/g1.bin -fi
\ No newline at end of file + exit 1 +fi diff --git a/src/tools/embed.sh b/src/tools/embed.sh deleted file mode 100755 index 59a377f..0000000 --- a/src/tools/embed.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash -# embeds source text in the compiler, so the editor can show a built-in's definition and `include std;` needs -# no file. a text becomes a function that returns it in pieces of at most 3000 bytes (the bootstrap -# compiler's token buffer is 4 KB), one byte string per piece: -# bstr NAME_part(int i) { if (i == 0) return b"..."; if (i == 1) return b"..."; ... return 0; } -# -# src/tools/embed.sh all embeds src/lib modules in the compiler -# src/tools/embed.sh all --check exits 1 when an embedded module is out of date -# src/tools/embed.sh lsp OUT writes OUT: rt_part, compiler_part and server_part, for the language server's image -set -e -case "${1:-}" in - --help|-h) printf '%s\n' 'usage: src/tools/embed.sh std|icon|all [--check]' ' src/tools/embed.sh lsp OUT' 'embeds library sources in the compiler, or writes the lsp source providers to OUT.' 'options:' ' --check report stale embedded library sources without changing files' ' --help, -h show this help and exit'; exit 0 ;; - -*) echo "embed: unknown option $1; use --help for usage" >&2; exit 2 ;; -esac -cd "$(dirname "$0")/../.." -export LC_ALL=C -# parts NAME FILE: the NAME_part function for FILE -parts() { - echo "bstr $1_part(int i) {" - awk -v LIMIT=3000 ' - function flush() { if (cnt > 0) { printf("\tif (i == %d) return b\"%s\";\n", n, buf); n++; buf = ""; cnt = 0 } } - { - line = $0; sub(/\r$/, "", line); line = line "\n" - len = length(line) - for (j = 1; j <= len; j++) { - if (cnt >= LIMIT) flush() - c = substr(line, j, 1) - if (c == "\\") e = "\\\\"; else if (c == "\"") e = "\\\""; else if (c == "\t") e = "\\t"; else if (c == "\n") e = "\\n"; else e = c - buf = buf e; cnt++ - } - } - END { flush() }' "$2" - echo " return 0;" - echo "}" -} -embed() { - name=$1; file=$2; check=$3; C=src/compiler/compiler.zc; T=$(mktemp -d) - BEGIN_MARK="// ---- embedded $name library (generated by src/tools/embed.sh from $file; do not edit) ----" - END_MARK="// ---- end embedded $name library ----" - if grep -qF "$BEGIN_MARK" "$C"; then - awk -v b="$BEGIN_MARK" '$0 == b { exit } { print }' "$C" > "$T/head" - awk -v e="$END_MARK" 'found { print } $0 == e { found = 1; next }' "$C" > "$T/tail" - else - cp "$C" "$T/head" - : > "$T/tail" - fi - { - cat "$T/head" - echo "$BEGIN_MARK" - parts "$name" "$file" - echo "$END_MARK" - cat "$T/tail" - } > "$T/new" - if [ "$check" = "--check" ]; then - cmp -s "$T/new" "$C" && result=0 || result=1 - rm -rf "$T" - return "$result" - fi - cp "$T/new" "$C" - rm -rf "$T" -} -case "$1" in -std) - embed std src/lib/std.zc "$2" - ;; -icon) - embed icon src/lib/icon.zc "$2" - ;; -all) - if [ "$2" = "--check" ]; then - embed std src/lib/std.zc --check && embed icon src/lib/icon.zc --check - else - embed std src/lib/std.zc - embed icon src/lib/icon.zc - fi - ;; -lsp) - [ -n "$2" ] || { echo "usage: src/tools/embed.sh lsp OUT" >&2; exit 2; } - { - echo "// the sources of this image, embedded (generated by src/tools/embed.sh; do not edit)" - parts rt src/runtime/rt.zc - parts compiler src/compiler/compiler.zc - parts server src/lsp/server.zc - parts core src/core/core.zc - } > "$2" - ;; -*) - echo "usage: src/tools/embed.sh all [--check] | std | icon | lsp OUT" >&2; exit 2;; -esac diff --git a/src/tools/findcc.sh b/src/tools/findcc.sh new file mode 100755 index 0000000..caa3af3 --- /dev/null +++ b/src/tools/findcc.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# prints the first c compiler that accepts c23 #embed, which every core needs to +# take in its genome (src/core/genome.h). clang 19 and gcc 15 are the first +# releases with it, and both find #embed files through --embed-dir. a CC +# already in the environment is tried first. +# src/tools/findcc.sh a native compiler +# src/tools/findcc.sh --wasm a clang that also targets wasm32 +# exits 1, naming what was tried, when nothing fits. +case "${1:-}" in + --help|-h) printf '%s\n' 'usage: src/tools/findcc.sh [--wasm]' 'prints a c compiler that supports #embed; --wasm also requires the wasm32 target.' 'environment: CC is tried first.'; exit 0 ;; + --wasm|'') ;; + *) echo "findcc: unknown option $1; use --help for usage" >&2; exit 2 ;; +esac +wasm=${1:-} +dir=$(mktemp -d "${TMPDIR:-/tmp}/findcc.XXXXXX") || exit 1 +trap 'rm -rf "$dir"' EXIT +mkdir "$dir/data" +printf 'ab' > "$dir/data/probe.bin" +printf 'static const unsigned char probe[] = {\n#embed "probe.bin"\n};\nint main(void) { return sizeof probe == 2 ? 0 : 1; }\n' > "$dir/probe.c" +tried="" +for cc in ${CC:-} cc clang gcc clang-22 clang-21 clang-20 clang-19 gcc-16 gcc-15 /opt/homebrew/opt/llvm/bin/clang /usr/local/opt/llvm/bin/clang; do + command -v "$cc" >/dev/null 2>&1 || continue + tried="$tried $cc" + if [ -n "$wasm" ]; then + "$cc" --target=wasm32 -nostdlib -w --embed-dir="$dir/data" -c "$dir/probe.c" -o "$dir/probe.o" >/dev/null 2>&1 || continue + else + "$cc" -w --embed-dir="$dir/data" "$dir/probe.c" -o "$dir/probe" >/dev/null 2>&1 || continue + fi + echo "$cc" + exit 0 +done +echo "findcc: no c compiler with #embed${wasm:+ and wasm32} (tried:${tried:- nothing}); install clang 19 or gcc 15, or set CC" >&2 +exit 1 diff --git a/src/tools/genmap.c b/src/tools/genmap.c index 8a258e1..d063d36 100644 --- a/src/tools/genmap.c +++ b/src/tools/genmap.c @@ -609,9 +609,11 @@ static void expect_hex(const char *text, const char *where, const char *name, in static void check_sources(const Map *m) { char lfa_old[64]; + /* a memmap.h in the working directory is compared too, when there is one: the + default map's header is generated into build/core and never kept in the tree */ FILE *mf = fopen("memmap.h", "rb"); - const char *mm_path = mf ? (fclose(mf), "memmap.h") : "src/core/memmap.h"; - char *rt = slurp("src/runtime/rt.zc"), *cc = slurp("src/compiler/compiler.zc"), *mm = slurp(mm_path); + const char *mm_path = mf ? (fclose(mf), "memmap.h") : 0; + char *rt = slurp("src/runtime/rt.zc"), *cc = slurp("src/compiler/compiler.zc"), *mm = mm_path ? slurp(mm_path) : 0; char *rtb = between(rt, "// @map-rt", "// @end-map"); char *ccb = between(cc, "// @map-cc", "// @end-map"); static const char *rt_names[] = { @@ -644,7 +646,7 @@ static void check_sources(const Map *m) if (!strstr(rt, lfa_old)) die("src/runtime/rt.zc no longer has the default lfa immediate"); if (!strstr(cc, "ZFP - 16384")) die("src/compiler/compiler.zc lost its initial type-area formula"); for (i = 0; rt_names[i]; i++) expect_hex(rtb, "src/runtime/rt.zc", rt_names[i], rt_vals[i]); - for (i = 0; rt_names[i]; i++) { + for (i = 0; mm && rt_names[i]; i++) { if (!strcmp(rt_names[i], "MAGIC") || !strcmp(rt_names[i], "HEAP_GROW")) continue; expect_hex(mm, mm_path, !strcmp(rt_names[i], "MW") ? "PW" : rt_names[i], rt_vals[i]); } @@ -700,7 +702,7 @@ static void usage_status(int status) "--preset default has a growing heap above the stacks; fixed is the old 1,081,344-word map.\n" "--grow lets a map with a fixed size get bigger when its genome needs more room.\n" "--mode packs a compiler or a run-only image; spare words go to the data stack.\n" - "--check compares that layout to memmap.h and the checked-in const blocks.\n"); + "--check compares that layout to the checked-in const blocks (and to ./memmap.h if present).\n"); exit(status); } diff --git a/src/tools/mkgenome.c b/src/tools/mkgenome.c index 3bf1533..9720760 100644 --- a/src/tools/mkgenome.c +++ b/src/tools/mkgenome.c @@ -1,7 +1,12 @@ -/* turns a genome image (little-endian 64-bit words on stdin, as written by - src/tools/build.zc) into genome.h */ +/* checks a genome image (little-endian 64-bit words on stdin, as written by + zc0 or src/tools/build.zc) against the memory map it was built with, and + copies it to stdout unchanged. the cores #embed the result (src/core/genome.h), + so this is the gate that keeps a compiler's diagnostics out of a core: when + the input is not an image for this map, nothing is written and the input is + shown on stderr, since it is usually the compiler's error report. */ #include <stdio.h> #include <stdint.h> +#include <stdlib.h> #include <string.h> #ifdef _WIN32 #include <fcntl.h> @@ -11,8 +16,9 @@ int main(int argc, char **argv) { if (argc == 2 && (!strcmp(argv[1], "--help") || !strcmp(argv[1], "-h"))) { - fputs("usage: mkgenome [--help|-h] < image.bin > genome.h\n" - "converts little-endian 64-bit image words into a c genome header.\n" + fputs("usage: mkgenome [--help|-h] < image.bin > genome.bin\n" + "checks little-endian 64-bit image words against this memory map and copies them\n" + "to stdout. input that is not an image is reported on stderr and nothing is written.\n" "the image must match the memory layout selected when mkgenome is built.\n" "options:\n --help, -h show this help and exit\n", stdout); return 0; @@ -22,19 +28,29 @@ int main(int argc, char **argv) _setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdout), _O_BINARY); #endif - unsigned char b[8]; int n = 0; size_t got; - printf("#define GORG %d\nstatic const int64_t genome[] = {", GORG); - while ((got = fread(b, 1, 8, stdin)) == 8) { - uint64_t u = (uint64_t)b[0] | (uint64_t)b[1] << 8 | (uint64_t)b[2] << 16 | (uint64_t)b[3] << 24 - | (uint64_t)b[4] << 32 | (uint64_t)b[5] << 40 | (uint64_t)b[6] << 48 | (uint64_t)b[7] << 56; - if ((n == 0 && u != (uint64_t)FTOP * 2 + 1) || - (n == 1 && u != (uint64_t)ZFP * 2 + 1) || (n == 2 && u != 4)) { - fprintf(stderr, "mkgenome: input is not a genome for this map\n"); return 1; + size_t n = 0, cap = 1 << 20, got; + unsigned char *b = malloc(cap); + if (!b) { fputs("mkgenome: out of memory\n", stderr); return 1; } + while ((got = fread(b + n, 1, cap - n, stdin)) > 0) { + n += got; + if (n == cap) { + unsigned char *next = cap > ((size_t)1 << 30) ? 0 : realloc(b, cap * 2); + if (!next) { fputs("mkgenome: image too large\n", stderr); return 1; } + b = next; cap *= 2; } - printf("%s%lld", !n ? "\n" : n % 12 ? "," : ",\n", (long long)u); - n++; } - if (got || ferror(stdin) || n < 7) { fprintf(stderr, "mkgenome: incomplete genome\n"); return 1; } - printf("\n};\n#define GLEN %d\n", n); - return n ? 0 : 1; + if (ferror(stdin)) { fputs("mkgenome: read failed\n", stderr); return 1; } + int ok = n >= 8 * 7 && n % 8 == 0; + for (int i = 0; ok && i < 3; i++) { + uint64_t u = 0; + for (int k = 0; k < 8; k++) u |= (uint64_t)b[i * 8 + k] << (k * 8); + if (u != (i == 0 ? (uint64_t)FTOP * 2 + 1 : i == 1 ? (uint64_t)ZFP * 2 + 1 : 4)) ok = 0; + } + if (!ok) { + if (n >= 4 && !memcmp(b, "zc: ", 4)) fwrite(b, 1, n, stderr); + fprintf(stderr, n % 8 || n < 8 * 7 ? "mkgenome: incomplete genome\n" : "mkgenome: input is not a genome for this map\n"); + return 1; + } + if (fwrite(b, 1, n, stdout) != n || fflush(stdout)) { fputs("mkgenome: write failed\n", stderr); return 1; } + return 0; } diff --git a/src/tools/mklsp.sh b/src/tools/mklsp.sh index 9bab550..1c44998 100755 --- a/src/tools/mklsp.sh +++ b/src/tools/mklsp.sh @@ -2,21 +2,19 @@ # builds the language server: src/tools/mklsp.sh [outdir] leaves outdir/zclsp. # the server is the real compiler (src/compiler/compiler.zc) in check mode plus # src/lsp/server.zc. its memory map is sized to that program by src/tools/automap.sh, -# so the genome slot is as big as it needs to be and no bigger. +# so the genome slot is as big as it needs to be and no bigger. the image carries +# the doc comments (DOCS=1) and, through embed declarations in server.zc, its own +# sources, so hover and go to definition work on built-ins. the core takes the +# image in with #embed as lsp_genome.bin. set -e case "${1:-}" in - --help|-h) printf '%s\n' 'usage: src/tools/mklsp.sh [OUTDIR]' 'builds the native language server in OUTDIR/zclsp; defaults to build/lsp.' 'environment: CC selects the c compiler.'; exit 0 ;; + --help|-h) printf '%s\n' 'usage: src/tools/mklsp.sh [OUTDIR]' 'builds the native language server in OUTDIR/zclsp; defaults to build/lsp.' 'environment: CC selects the c compiler (one with #embed).'; exit 0 ;; -*) echo "mklsp: unknown option $1; use --help for usage" >&2; exit 2 ;; esac cd "$(dirname "$0")/../.." -O=${1:-build/lsp}; CC=${CC:-gcc} -# DOCS=1: the image carries the doc comments, so hover can show what a name is for +O=${1:-build/lsp}; CC=${CC:-$(sh src/tools/findcc.sh)} +export CC mkdir -p "$O" -bash src/tools/embed.sh all --check || bash src/tools/embed.sh all -# the image carries its own sources (for go to definition into a built-in) -bash src/tools/embed.sh lsp "$O/embed.zc" -DOCS=1 bash src/tools/automap.sh lsp "$O" src/lsp/server.zc "$O/embed.zc" >/dev/null -rm -rf "$O/src"; mkdir -p "$O/src" -cp src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h "$O/memmap.h" "$O/src/" -cp "$O/genome.h" "$O/src/lsp_genome.h" -$CC -O2 -w -fwrapv -pthread -DJTYPE=int -DZY_SANDBOX -DUSE_LSP_GENOME "$O/src/core.c" "$O/src/io_linux.c" -I"$O/src" -o "$O/zclsp" +DOCS=1 bash src/tools/automap.sh lsp "$O" src/lsp/server.zc >/dev/null +cp "$O/genome.bin" "$O/lsp_genome.bin" +$CC -O2 -w -fwrapv -pthread -DJTYPE=int -DZY_SANDBOX -DUSE_LSP_GENOME src/core/core.c src/core/io_linux.c -Isrc/core -I"$O" --embed-dir="$O" -o "$O/zclsp" diff --git a/src/tools/mklspwasm.sh b/src/tools/mklspwasm.sh index 4789eab..d05a5a7 100755 --- a/src/tools/mklspwasm.sh +++ b/src/tools/mklspwasm.sh @@ -3,21 +3,22 @@ # by default, or the path given as the first argument. the server is the compiler # and src/lsp/server.zc on the map and genome that src/tools/mklsp.sh made in build/lsp, # and the microcore (src/micro/micro.c) runs them. the host is client/wasmproc.js. -# needs clang with the wasm32 target and wasm-ld; run src/tools/mklsp.sh first. +# needs clang with the wasm32 target, #embed and wasm-ld; run src/tools/mklsp.sh first. # src/tools/mklspwasm.sh [outfile.wasm] set -e case "${1:-}" in - --help|-h) printf '%s\n' 'usage: src/tools/mklspwasm.sh [OUTFILE.wasm]' 'builds the wasm language server; defaults to build/editors/vscode/server/lsp.wasm.' 'requires clang with wasm32 support and wasm-ld.' 'environment: LSP_BUILD_DIR selects the native lsp build; LSP_REUSE_BUILD=1 reuses its image.'; exit 0 ;; + --help|-h) printf '%s\n' 'usage: src/tools/mklspwasm.sh [OUTFILE.wasm]' 'builds the wasm language server; defaults to build/editors/vscode/server/lsp.wasm.' 'requires clang with wasm32 and #embed support, and wasm-ld.' 'environment: WASM_CC selects the compiler; LSP_BUILD_DIR selects the native lsp build; LSP_REUSE_BUILD=1 reuses its image.'; exit 0 ;; -*) echo "mklspwasm: unknown option $1; use --help for usage" >&2; exit 2 ;; esac cd "$(dirname "$0")/../.." OUT=${1:-build/editors/vscode/server/lsp.wasm} LSP_BUILD_DIR=${LSP_BUILD_DIR:-build/lsp} +WASM_CC=${WASM_CC:-$(sh src/tools/findcc.sh --wasm)} if [ "${LSP_REUSE_BUILD:-0}" != 1 ]; then bash src/tools/mklsp.sh "$LSP_BUILD_DIR" >/dev/null; fi -[ -f "$LSP_BUILD_DIR/src/lsp_genome.h" ] || { echo "mklspwasm: no language server image in $LSP_BUILD_DIR" >&2; exit 1; } +[ -f "$LSP_BUILD_DIR/lsp_genome.bin" ] || { echo "mklspwasm: no language server image in $LSP_BUILD_DIR" >&2; exit 1; } mkdir -p "$(dirname "$OUT")" -clang --target=wasm32 -O2 -w -fwrapv -nostdlib -fno-builtin -mbulk-memory -DJTYPE=int -DUSE_LSP_GENOME \ - -I"$LSP_BUILD_DIR/src" -Isrc/micro src/micro/micro.c src/micro/port_wasm_stream.c \ +"$WASM_CC" --target=wasm32 -O2 -w -fwrapv -nostdlib -fno-builtin -mbulk-memory -DJTYPE=int -DUSE_LSP_GENOME \ + -Isrc/core -I"$LSP_BUILD_DIR" --embed-dir="$LSP_BUILD_DIR" -Isrc/micro src/micro/micro.c src/micro/port_wasm_stream.c \ -Wl,--no-entry -Wl,--export=wasm_chunk -Wl,--export=wasm_chunk_cap -Wl,--export=wasm_feed \ -Wl,--export=wasm_polling -Wl,--export=wasm_close -Wl,--export=wasm_start -Wl,--export=wasm_resume \ -Wl,--export=memory -Wl,--allow-undefined -Wl,--initial-memory=41943040 -Wl,-z,stack-size=65536 \ diff --git a/src/tools/mkvsix.sh b/src/tools/mkvsix.sh index df9e703..61fe9a8 100755 --- a/src/tools/mkvsix.sh +++ b/src/tools/mkvsix.sh @@ -1,13 +1,12 @@ #!/bin/bash # builds the vs code extension, with the language server inside it: the tinyvm # javascript vm (built from src/js/tinyvm.ts) and the server's image -# (build/js/lsp.json, from build/lsp/genome.h and its map), and the wasm server +# (build/js/lsp.bin and its map in lsp.json, from build/lsp), and the wasm server # (src/tools/mklspwasm.sh) when clang can build it. needs node and tsc; the # server build (src/tools/mklsp.sh) runs first if build/lsp is missing. # -# everything generated lands in build/. the packaged files are staged into -# editors/vscode/server/ only for as long as vsce is packing, then removed, so a -# fresh clone has no extension until it is built. +# everything generated lands in build/: the server files in +# build/editors/vscode/server, the packing copy of the extension in build/vsix. # src/tools/mkvsix.sh [outfile.vsix] set -e case "${1:-}" in @@ -20,31 +19,33 @@ case "$OUT" in /*) ;; *) OUT="$PWD/$OUT" ;; esac mkdir -p "$(dirname "$OUT")" JS=build/js STAGE=build/editors/vscode/server -PACKDIR=editors/vscode/server bash src/tools/mklsp.sh build/lsp >/dev/null # typescript errors stop packaging, so broken generated code is never shipped. command -v tsc >/dev/null || { echo "mkvsix: tsc is not installed" >&2; exit 1; } +make --no-print-directory build/js/layout.js >/dev/null tsc src/js/tinyvm.ts --target es2020 --lib es2020,dom --module commonjs --outDir "$JS" node src/js/extract_lsp.js mkdir -p "$STAGE" -cp "$JS/tinyvm.js" "$JS/layout.js" "$JS/lsp.json" "$STAGE/" +cp "$JS/tinyvm.js" "$JS/layout.js" "$JS/lsp.json" "$JS/lsp.bin" "$STAGE/" # the wasm server runs when there is one; without clang the vm server is packed alone -if command -v clang >/dev/null && clang --print-targets | grep -q wasm32; then +if sh src/tools/findcc.sh --wasm >/dev/null 2>&1; then LSP_BUILD_DIR=build/lsp LSP_REUSE_BUILD=1 bash src/tools/mklspwasm.sh "$STAGE/lsp.wasm" else rm -f "$STAGE/lsp.wasm" - echo "no clang with wasm32: packing the javascript vm server only" >&2 + echo "no clang with wasm32 and #embed: packing the javascript vm server only" >&2 fi -# vsce packs the extension directory, so the server has to sit inside it while -# it runs. stage it, package, and leave the tree as it was. -cleanup() { rm -rf "$PACKDIR"; } -trap cleanup EXIT INT TERM -rm -rf "$PACKDIR" -mkdir -p "$PACKDIR" -cp "$STAGE"/* "$PACKDIR"/ -cd editors/vscode +# vsce packs a whole extension directory, so the extension is copied into +# build/vsix/extension with the server beside its client, and packed there. the +# tree is never written to. +PACKROOT=build/vsix/extension +rm -rf "$PACKROOT" +mkdir -p "$PACKROOT" +cp -R editors/vscode/. "$PACKROOT/" +mkdir -p "$PACKROOT/server" +cp "$STAGE"/* "$PACKROOT/server/" +cd "$PACKROOT" if [ -n "${VSCE:-}" ]; then "$VSCE" package --allow-missing-repository --skip-license -o "$OUT" >/dev/null elif command -v vsce >/dev/null; then @@ -52,4 +53,4 @@ elif command -v vsce >/dev/null; then else npx -y @vscode/vsce package --allow-missing-repository --skip-license -o "$OUT" >/dev/null fi -echo "$OUT"
\ No newline at end of file +echo "$OUT" diff --git a/src/tools/zc0.c b/src/tools/zc0.c index 446557b..5f5b97d 100644 --- a/src/tools/zc0.c +++ b/src/tools/zc0.c @@ -1,7 +1,8 @@ /* zc0: stage 0 of the bootstrap. a small c compiler for the subset of zc that compiler.zc is written in. it reads zc source on stdin and writes a genome - header (vm words placed at GORG) on stdout, in the same image layout the - resident compiler's image mode produces. no lifetime analysis, no quote, no + image (vm words placed at GORG, as little-endian 64-bit words) on stdout, in + the same image layout the resident compiler's image mode produces. the cores + embed that file with #embed (src/core/genome.h). no lifetime analysis, no quote, no parse words, no block expressions: those only exist in compiler.zc. tags (m, d, -> u) and casts (as u, as d) are parsed and recorded in signatures but not checked. */ @@ -454,12 +455,13 @@ static void strlit(void) /* inline word string: jump over it, push its address * static void bstrlit(void) { int j = here, a, i = 0, k, n = (int)strlen(ts); + const char *b = ts; uint64_t w; lit(0); op(O_JMP); a = here; emit(n); while (i < n) { w = 0; k = 0; - while (k < 8 && i < n) { w |= (uint64_t)(unsigned char)ts[i] << (k * 8); k++; i++; } + while (k < 8 && i < n) { w |= (uint64_t)(unsigned char)b[i] << (k * 8); k++; i++; } emit((int64_t)w); } if ((n & 7) == 0) emit(0); @@ -497,7 +499,7 @@ static void primary(void) else if (((s->sig >> 4) & 15) != 1) die("multiple enhancement outputs need asm"); xs = 0; xf = 0; return; } - if (!strcmp(n, "put_s") && na == 1 && xs) fnref("put_bs"); + if ((!strcmp(n, "put_s") || !strcmp(n, "rt.put_s")) && na == 1 && xs) fnref(n[0] == 'r' ? "rt.put_bs" : "put_bs"); else if (!strcmp(n, "len")) fnref("len_s"); else fnref(n); op(O_CALL); @@ -794,6 +796,77 @@ static void enhdef(void) s = add(n, K_ENH, nenh++); s->sig = np + nout * 16 + d * 256; expect(";"); } +/* embed "path" as [type] name; at the top level: a global holding the file's bytes, + read when the image is built. the path is relative to the working directory. + bstr (the default) packs the bytes; char* keeps one character to a word; a + number takes them little-endian; T[] and T[n] lay them out as the array's + elements. the same rules as the resident compiler's kw_embed. */ +static int embed_width(const char *t, int star) +{ + if (star) return 0; + if (!strcmp(t, "char") || !strcmp(t, "u8") || !strcmp(t, "i8") || !strcmp(t, "byte")) return 1; + if (!strcmp(t, "u16") || !strcmp(t, "i16")) return 2; + if (!strcmp(t, "u32") || !strcmp(t, "i32") || !strcmp(t, "f32")) return 4; + if (!strcmp(t, "int") || !strcmp(t, "word") || !strcmp(t, "bool") || !strcmp(t, "float") || !strcmp(t, "f64")) return 8; + return 0; +} +static void embeddecl(void) +{ + char path[1024], ty[64] = "", *n; int star = 0, arr = -2, w, i, a; long len; unsigned char *buf; FILE *f; + if (tk != T_STR) die("embed takes a path in quotes: embed \"file\" as name;"); + snprintf(path, sizeof path, "%s", ts); next(); + expect("as"); + if (tk == T_ID) { + Lex L; lexsave(&L); next(); + int named = is(";"); lexrest(&L); + if (!named) { + snprintf(ty, sizeof ty, "%s", ts); next(); + while (accept("*")) star++; + if (accept("[")) { arr = -1; if (tk == T_NUM) { arr = (int)tv; next(); } expect("]"); } + } + } + n = ident(); expect(";"); + if (!(f = fopen(path, "rb"))) die("can't read the file named by embed"); + fseek(f, 0, SEEK_END); len = ftell(f); fseek(f, 0, SEEK_SET); + buf = malloc((size_t)len + 1); + if (!buf || fread(buf, 1, (size_t)len, f) != (size_t)len) die("can't read the file named by embed"); + fclose(f); + if (arr == -2 && (!ty[0] || (!strcmp(ty, "bstr") && !star))) { + a = here; add(n, K_GVAR, a); emit(0); img[a - GORG] = here; emit(len); + for (i = 0; i < len; i += 8) { uint64_t u = 0; for (w = 0; w < 8 && i + w < len; w++) u |= (uint64_t)buf[i + w] << (w * 8); emit((int64_t)u); } + if (!(len & 7)) emit(0); + } else if (arr == -2 && !strcmp(ty, "char") && star == 1) { + a = here; add(n, K_GVAR, a); emit(0); img[a - GORG] = here; + for (i = 0; i < len; i++) emit(buf[i]); + emit(0); + } else if (arr == -2) { + uint64_t u = 0; w = embed_width(ty, star); + if (!w) die("embed declares a bstr, a char*, a number or an array of numbers"); + if (len > w) die("the embedded file is larger than the type it is declared as"); + if (!strcmp(ty, "f32") && len != 4) die("an embedded f32 takes exactly 4 bytes"); + if ((!strcmp(ty, "float") || !strcmp(ty, "f64")) && len != 8) die("an embedded float takes exactly 8 bytes"); + for (i = 0; i < len; i++) u |= (uint64_t)buf[i] << (i * 8); + if (ty[0] == 'i' && w < 8 && (u >> (w * 8 - 1) & 1)) u |= ~(uint64_t)0 << (w * 8); + if (!strcmp(ty, "f32")) u = fop_s2f(u); + a = here; add(n, K_GVAR, a); emit((int64_t)u); + } else { + int count; w = embed_width(ty, star); + if (!w) die("embed declares a bstr, a char*, a number or an array of numbers"); + if (len % w) die("the embedded file's size is not a whole number of elements"); + count = (int)(len / w); + if (arr >= 0 && count > arr) die("the embedded file is larger than the array it is declared as"); + if (arr >= 0) count = arr; + a = here; + if (!strcmp(ty, "char")) { for (i = 0; i < count; i++) emit(i < len ? buf[i] : 0); } + else { + long bytes = (long)count * w; + for (i = 0; i < bytes; i += 8) { uint64_t u = 0; for (int k = 0; k < 8 && i + k < bytes; k++) u |= (uint64_t)(i + k < len ? buf[i + k] : 0) << (k * 8); emit((int64_t)u); } + if (!bytes) emit(0); + } + add(n, K_GARR, a); + } + free(buf); +} static void top1(void) { if (accept("namespace")) { @@ -802,6 +875,7 @@ static void top1(void) expect(";"); return; } if (accept("include")) { do { ident(); } while (accept(",")); expect(";"); return; } + if (accept("embed")) { embeddecl(); return; } { if (accept("unchecked")) { top1(); return; } if (accept("fn")) { fndef(0); return; } @@ -898,8 +972,9 @@ static void top(void) int main(int argc, char **argv) { if (argc == 2 && (!strcmp(argv[1], "--help") || !strcmp(argv[1], "-h"))) { - fputs("usage: zc0 [--help|-h] < source.zc > genome.h\n" - "compiles bootstrap source from stdin into a c genome header on stdout.\n" + fputs("usage: zc0 [--help|-h] < source.zc > genome.bin\n" + "compiles bootstrap source from stdin into a genome image on stdout:\n" + "little-endian 64-bit words, the format the cores #embed.\n" "source must define start(); memory layout is selected when zc0 is built.\n" "options:\n --help, -h show this help and exit\n", stdout); return 0; @@ -942,8 +1017,10 @@ int main(int argc, char **argv) } emit(0); img[6] = here; - printf("#define GORG %d\n#define GLEN %d\nstatic const int64_t genome[] = {", GORG, here - GORG); - for (i = 0; i < here - GORG; i++) printf("%s%lld", !i ? "\n" : i % 12 ? "," : ",\n", (long long)img[i]); - printf("\n};\n"); - return 0; + for (i = 0; i < here - GORG; i++) { + unsigned char b[8]; uint64_t u = (uint64_t)img[i]; + for (j = 0; j < 8; j++) b[j] = (unsigned char)(u >> (j * 8)); + if (fwrite(b, 1, 8, stdout) != 8) { fprintf(stderr, "zc0: write failed\n"); return 1; } + } + return fflush(stdout) ? 1 : 0; } diff --git a/src/tools/zcc b/src/tools/zcc index 3fb8f0c..ba38e97 100755 --- a/src/tools/zcc +++ b/src/tools/zcc @@ -8,7 +8,7 @@ # # -o OUT the binary to write (default: FILE without .zc) # -k keep the build directory and print where it is -# CC the C compiler (default cc). CFLAGS adds to the default -O2 +# CC the C compiler, one with c23 #embed (default: src/tools/findcc.sh). CFLAGS adds to -O2 # # the build is three steps: automap sizes the map to the program and writes the # genome (the compiled program image) for it; the micro core and its linux port @@ -16,13 +16,13 @@ # the same image without the static link. set -e here="$(cd "$(dirname "$0")/../.." && pwd)" -CC=${CC:-cc} +CC=${CC:-$(sh "$here/src/tools/findcc.sh")} out=""; keep=0 while [ $# -gt 0 ]; do case "$1" in -o) [ $# -ge 2 ] || { echo "zcc: -o needs a name" >&2; exit 2; }; out="$2"; shift 2 ;; -k) keep=1; shift ;; - -h|--help) printf '%s\n' 'usage: src/tools/zcc [-o OUT] [-k] FILE.zc' 'compiles a source file into a standalone binary; links statically where supported.' 'options:' ' -o OUT output binary path; defaults to the source basename without .zc' ' -k keep the temporary build directory' ' --help, -h show this help and exit' 'environment: CC selects the c compiler; CFLAGS adds compiler flags.'; exit 0 ;; + -h|--help) printf '%s\n' 'usage: src/tools/zcc [-o OUT] [-k] FILE.zc' 'compiles a source file into a standalone binary; links statically where supported.' 'options:' ' -o OUT output binary path; defaults to the source basename without .zc' ' -k keep the temporary build directory' ' --help, -h show this help and exit' 'environment: CC selects the c compiler (one with #embed); CFLAGS adds compiler flags.'; exit 0 ;; -*) echo "zcc: unknown option $1" >&2; exit 2 ;; *) break ;; esac @@ -41,7 +41,7 @@ trap cleanup EXIT # the map and the genome for this program only ( cd "$here" && bash src/tools/automap.sh run "$work/run" src/runtime/run.zc "$abs" ) >/dev/null cp "$here/src/micro/micro.c" "$here/src/micro/micro_port.h" "$here/src/micro/port_linux.c" \ - "$here/src/core/fops.h" "$here/src/core/bops.h" "$here/src/core/stack.h" "$work/run/" + "$here/src/core/fops.h" "$here/src/core/bops.h" "$here/src/core/stack.h" "$here/src/core/genome.h" "$work/run/" # static where the toolchain allows it; a plain link otherwise, with a note static=-static diff --git a/src/web/build.sh b/src/web/build.sh index 7443f46..1f2cfae 100755 --- a/src/web/build.sh +++ b/src/web/build.sh @@ -1,16 +1,23 @@ #!/bin/bash -# builds src/web/zc.wasm (or the path given as the first argument): the microcore, with the compiler genome inside, as -# WebAssembly. needs clang with the wasm32 target and wasm-ld. the page -# (src/web/index.html) loads it next to src/web/zc.js. +# builds the browser compiler: the microcore with the compiler genome inside, as +# WebAssembly, written to build/web/zc.wasm (or the path given as the first +# argument). the page (src/web/index.html) loads it next to zc.js; `make web` +# copies both beside it, so build/web/ is the whole site. needs clang with the +# wasm32 target, #embed and wasm-ld, and the genome from `make genome`. set -e case "${1:-}" in - --help|-h) printf '%s\n' 'usage: src/web/build.sh [OUTFILE.wasm]' 'builds the browser compiler vm; defaults to src/web/zc.wasm.' 'requires clang with wasm32 support and wasm-ld.'; exit 0 ;; + --help|-h) printf '%s\n' 'usage: src/web/build.sh [OUTFILE.wasm]' 'builds the browser compiler vm; defaults to build/web/zc.wasm.' 'requires clang with wasm32 and #embed support, and wasm-ld.' 'environment: WASM_CC selects the compiler; GENOME_DIR holds genome.bin (build/core); MAP_DIR holds its memmap.h (build/core/map).'; exit 0 ;; -*) echo "web build: unknown option $1; use --help for usage" >&2; exit 2 ;; esac cd "$(dirname "$0")/../.." -clang --target=wasm32 -O2 -w -fwrapv -nostdlib -fno-builtin -mbulk-memory -DJTYPE=int \ - -Isrc/core -Isrc/micro src/micro/micro.c src/web/port_wasm.c \ +OUT=${1:-build/web/zc.wasm} +WASM_CC=${WASM_CC:-$(sh src/tools/findcc.sh --wasm)} +GENOME_DIR=${GENOME_DIR:-build/core}; MAP_DIR=${MAP_DIR:-build/core/map} +[ -f "$GENOME_DIR/genome.bin" ] || { echo "web build: no $GENOME_DIR/genome.bin; run make genome first" >&2; exit 1; } +mkdir -p "$(dirname "$OUT")" +"$WASM_CC" --target=wasm32 -O2 -w -fwrapv -nostdlib -fno-builtin -mbulk-memory -DJTYPE=int \ + -Isrc/core -I"$MAP_DIR" --embed-dir="$GENOME_DIR" -Isrc/micro src/micro/micro.c src/web/port_wasm.c \ -Wl,--no-entry -Wl,--export=wasm_input -Wl,--export=wasm_input_cap -Wl,--export=wasm_set_input -Wl,--export=wasm_run \ -Wl,--export=memory -Wl,--allow-undefined -Wl,--initial-memory=33554432 -Wl,-z,stack-size=65536 \ - -o "${1:-src/web/zc.wasm}" -ls -l "${1:-src/web/zc.wasm}" + -o "$OUT" +ls -l "$OUT" diff --git a/tests/bench/bench.sh b/tests/bench/bench.sh index ab6b07d..65f455e 100644 --- a/tests/bench/bench.sh +++ b/tests/bench/bench.sh @@ -2,13 +2,15 @@ # o3 against plain code: each kernel in tests/programs/bench.zc on each backend, with # the time to start up and compile (the 'z' run) taken off. needs bc. cd "$(dirname "$0")/../.."; T=$(mktemp -d); trap 'rm -rf $T' EXIT -gcc -O2 -w -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -o $T/zy +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 vms=("$T/zy"); names=("interp") if command -v llvm-config >/dev/null; then - gcc -O2 -w -DJTYPE=int -DZYLLVM $(llvm-config --cflags) src/core/core.c src/core/io_linux.c -Isrc/core -o $T/zyl $(llvm-config --ldflags --libs) && { vms+=("$T/zyl"); names+=("llvm"); } + $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 if command -v aarch64-linux-gnu-gcc >/dev/null && command -v qemu-aarch64 >/dev/null; then - aarch64-linux-gnu-gcc -O2 -w -DJTYPE=int -static src/core/core.c src/core/io_linux.c -Isrc/core -o $T/zyj && { vms+=("qemu-aarch64 $T/zyj"); names+=("arm64 (qemu)"); } + aarch64-linux-gnu-gcc -O2 -w -DJTYPE=int -static src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core/map --embed-dir=build/core -o $T/zyj && { vms+=("qemu-aarch64 $T/zyj"); names+=("arm64 (qemu)"); } fi # seconds per run of kernel $2 on vm $1: the slope between $3 runs and 1 run, # best of three each, so starting up and compiling cancel out diff --git a/tests/bench/bench_bytes.sh b/tests/bench/bench_bytes.sh index acff074..775b71d 100644 --- a/tests/bench/bench_bytes.sh +++ b/tests/bench/bench_bytes.sh @@ -4,16 +4,18 @@ set -e # in the process. the slope between 1 repetition and N drops startup. # prints: backend ms/rep checksum cd "$(dirname "$0")/../.." +CC=${CC:-$(sh src/tools/findcc.sh)} || exit 1 +make -s build/core/genome.bin || exit 1 T=$(mktemp -d) trap 'rm -rf "$T"' EXIT -gcc -O2 -w src/tools/genmap.c -o "$T/genmap" || exit 1 +$CC -O2 -w src/tools/genmap.c -o "$T/genmap" || exit 1 GENMAP="$T/genmap" bash src/tools/automap.sh run "$T/run" src/runtime/run.zc tests/programs/bench_bytes.zc >/dev/null || exit 1 -cp src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h "$T/run/" -gcc -O2 -w -DJTYPE=int "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zy" || exit 1 -gcc -O2 -w "$T/run/micro.c" "$T/run/port_linux.c" -o "$T/zym" || exit 1 +cp src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h "$T/run/" +$CC -O2 -w -DJTYPE=int "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zy" || exit 1 +$CC -O2 -w "$T/run/micro.c" "$T/run/port_linux.c" -o "$T/zym" || exit 1 vms=("$T/zy" "$T/zym"); names=("interp" "micro") if command -v llvm-config >/dev/null; then - gcc -O2 -w -DJTYPE=int -DZYLLVM $(llvm-config --cflags) "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zyl" $(llvm-config --ldflags --libs) \ + $CC -O2 -w -DJTYPE=int -DZYLLVM $(llvm-config --cflags) "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zyl" $(llvm-config --ldflags --libs) \ && { vms+=("$T/zyl"); names+=("llvm"); } fi if command -v aarch64-linux-gnu-gcc >/dev/null && command -v qemu-aarch64 >/dev/null; then diff --git a/tests/bench/bench_float.sh b/tests/bench/bench_float.sh index 794e976..4ebfbe4 100644 --- a/tests/bench/bench_float.sh +++ b/tests/bench/bench_float.sh @@ -5,17 +5,19 @@ # this does not bootstrap the compiler. each kernel is poly() of 20000, # and (int) of one call is 59997. cd "$(dirname "$0")/../.." +CC=${CC:-$(sh src/tools/findcc.sh)} || exit 1 +make -s build/core/genome.bin || exit 1 T=$(mktemp -d) trap 'rm -rf "$T"' EXIT -gcc -O2 -w -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -o "$T/zy" || 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" || exit 1 vms=("$T/zy"); names=("interp") -gcc -O2 -w src/micro/micro.c src/micro/port_linux.c -Isrc/core -Isrc/micro -o "$T/zym" && { vms+=("$T/zym"); names+=("micro"); } +$CC -O2 -w src/micro/micro.c src/micro/port_linux.c -Isrc/core -Ibuild/core/map --embed-dir=build/core -Isrc/micro -o "$T/zym" && { vms+=("$T/zym"); names+=("micro"); } if command -v llvm-config >/dev/null; then - gcc -O2 -w -DJTYPE=int -DZYLLVM $(llvm-config --cflags) src/core/core.c src/core/io_linux.c -Isrc/core -o "$T/zyl" $(llvm-config --ldflags --libs) \ + $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 if command -v aarch64-linux-gnu-gcc >/dev/null && command -v qemu-aarch64 >/dev/null; then - aarch64-linux-gnu-gcc -O2 -w -DJTYPE=int -static src/core/core.c src/core/io_linux.c -Isrc/core -o "$T/zyj" \ + aarch64-linux-gnu-gcc -O2 -w -DJTYPE=int -static src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core/map --embed-dir=build/core -o "$T/zyj" \ && { vms+=("qemu-aarch64 $T/zyj"); names+=("arm64"); } fi src=$(cat tests/programs/fbench.zc) diff --git a/tests/bench/bench_share.sh b/tests/bench/bench_share.sh index b449e09..5cb1bfd 100644 --- a/tests/bench/bench_share.sh +++ b/tests/bench/bench_share.sh @@ -3,18 +3,19 @@ # resident compiler is not in the process. the slope between 1 repetition # and N drops startup. prints: backend ms/rep checksum cd "$(dirname "$0")/../.." +CC=${CC:-$(sh src/tools/findcc.sh)} || exit 1 T=$(mktemp -d) trap 'rm -rf "$T"' EXIT -gcc -O2 -w src/tools/genmap.c -o "$T/genmap" || exit 1 +$CC -O2 -w src/tools/genmap.c -o "$T/genmap" || exit 1 "$T/genmap" --preset run -o "$T/run" >/dev/null || exit 1 -cp src/tools/zc0.c src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h "$T/run/" -gcc -O1 -w "$T/run/zc0.c" -o "$T/zc0" || exit 1 -cat "$T/run/rt.zc" src/runtime/run.zc tests/programs/bench_share.zc | "$T/zc0" > "$T/run/genome.h" || exit 1 -gcc -O2 -w -DJTYPE=int "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zy" || exit 1 -gcc -O2 -w "$T/run/micro.c" "$T/run/port_linux.c" -o "$T/zym" || exit 1 +cp src/tools/zc0.c src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h "$T/run/" +$CC -O1 -w "$T/run/zc0.c" -o "$T/zc0" || exit 1 +cat "$T/run/rt.zc" src/core/core.zc src/runtime/run.zc tests/programs/bench_share.zc | "$T/zc0" > "$T/run/genome.bin" || exit 1 +$CC -O2 -w -DJTYPE=int "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zy" || exit 1 +$CC -O2 -w "$T/run/micro.c" "$T/run/port_linux.c" -o "$T/zym" || exit 1 vms=("$T/zy" "$T/zym"); names=("interp" "micro") if command -v llvm-config >/dev/null; then - gcc -O2 -w -DJTYPE=int -DZYLLVM $(llvm-config --cflags) "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zyl" $(llvm-config --ldflags --libs) \ + $CC -O2 -w -DJTYPE=int -DZYLLVM $(llvm-config --cflags) "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zyl" $(llvm-config --ldflags --libs) \ && { vms+=("$T/zyl"); names+=("llvm"); } fi if command -v aarch64-linux-gnu-gcc >/dev/null && command -v qemu-aarch64 >/dev/null; then diff --git a/tests/bench/bench_strings.sh b/tests/bench/bench_strings.sh index c353342..b14bf57 100644 --- a/tests/bench/bench_strings.sh +++ b/tests/bench/bench_strings.sh @@ -3,18 +3,19 @@ # resident compiler is not in the process. the slope between 1 repetition # and N drops startup. prints: backend ms/rep checksum cd "$(dirname "$0")/../.." +CC=${CC:-$(sh src/tools/findcc.sh)} || exit 1 T=$(mktemp -d) trap 'rm -rf "$T"' EXIT -gcc -O2 -w src/tools/genmap.c -o "$T/genmap" || exit 1 +$CC -O2 -w src/tools/genmap.c -o "$T/genmap" || exit 1 "$T/genmap" --preset run -o "$T/run" >/dev/null || exit 1 -cp src/tools/zc0.c src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h "$T/run/" -gcc -O1 -w "$T/run/zc0.c" -o "$T/zc0" || exit 1 -cat "$T/run/rt.zc" src/runtime/run.zc tests/programs/bench_strings.zc | "$T/zc0" > "$T/run/genome.h" || exit 1 -gcc -O2 -w -DJTYPE=int "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zy" || exit 1 -gcc -O2 -w "$T/run/micro.c" "$T/run/port_linux.c" -o "$T/zym" || exit 1 +cp src/tools/zc0.c src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h "$T/run/" +$CC -O1 -w "$T/run/zc0.c" -o "$T/zc0" || exit 1 +cat "$T/run/rt.zc" src/core/core.zc src/runtime/run.zc tests/programs/bench_strings.zc | "$T/zc0" > "$T/run/genome.bin" || exit 1 +$CC -O2 -w -DJTYPE=int "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zy" || exit 1 +$CC -O2 -w "$T/run/micro.c" "$T/run/port_linux.c" -o "$T/zym" || exit 1 vms=("$T/zy" "$T/zym"); names=("interp" "micro") if command -v llvm-config >/dev/null; then - gcc -O2 -w -DJTYPE=int -DZYLLVM $(llvm-config --cflags) "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zyl" $(llvm-config --ldflags --libs) \ + $CC -O2 -w -DJTYPE=int -DZYLLVM $(llvm-config --cflags) "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zyl" $(llvm-config --ldflags --libs) \ && { vms+=("$T/zyl"); names+=("llvm"); } fi if command -v aarch64-linux-gnu-gcc >/dev/null && command -v qemu-aarch64 >/dev/null; then diff --git a/tests/bench/run.py b/tests/bench/run.py index cb28b92..7f7ea6f 100755 --- a/tests/bench/run.py +++ b/tests/bench/run.py @@ -79,18 +79,21 @@ def machine_lines(): def build(td): td = Path(td) log("building vms") + need(["make", "-s", "build/core/genome.bin"], cwd=ROOT) + cc = os.environ.get("CC") or subprocess.check_output(["sh", "src/tools/findcc.sh"], cwd=ROOT, text=True).strip() + core = ["-Isrc/core", "-Ibuild/core/map", "--embed-dir=build/core"] zy = td / "zy" - need(["gcc", "-O3", "-w", "-DJTYPE=int", "src/core/core.c", "src/core/io_linux.c", "-Isrc/core", "-o", str(zy)], cwd=ROOT) + need([cc, "-O3", "-w", "-DJTYPE=int", "src/core/core.c", "src/core/io_linux.c", *core, "-o", str(zy)], cwd=ROOT) vms = {"interp": [str(zy)]} if shutil.which("llvm-config"): cflags = subprocess.check_output(["llvm-config", "--cflags"], text=True).split() ld = subprocess.check_output(["llvm-config", "--ldflags", "--libs"], text=True).split() zyl = td / "zyl" - need(["gcc", "-O3", "-w", "-DJTYPE=int", "-DZYLLVM", *cflags, "src/core/core.c", "src/core/io_linux.c", "-Isrc/core", "-o", str(zyl), *ld], cwd=ROOT) + need([cc, "-O3", "-w", "-DJTYPE=int", "-DZYLLVM", *cflags, "src/core/core.c", "src/core/io_linux.c", *core, "-o", str(zyl), *ld], cwd=ROOT) vms["llvm"] = [str(zyl)] if shutil.which("aarch64-linux-gnu-gcc") and shutil.which("qemu-aarch64"): zyj = td / "zyj" - need(["aarch64-linux-gnu-gcc", "-O3", "-w", "-DJTYPE=int", "-static", "src/core/core.c", "src/core/io_linux.c", "-Isrc/core", "-o", str(zyj)], cwd=ROOT) + need(["aarch64-linux-gnu-gcc", "-O3", "-w", "-DJTYPE=int", "-static", "src/core/core.c", "src/core/io_linux.c", *core, "-o", str(zyj)], cwd=ROOT) vms["arm64"] = ["qemu-aarch64", str(zyj)] log("building C") bins = {} diff --git a/tests/drivers/actor_image_test.py b/tests/drivers/actor_image_test.py index 9df8828..5b815f2 100644 --- a/tests/drivers/actor_image_test.py +++ b/tests/drivers/actor_image_test.py @@ -1,12 +1,16 @@ #!/usr/bin/env python3 # source syntax callbacks execute in the host while actor code is emitted separately. from pathlib import Path +import sys import argparse import subprocess import tempfile +sys.dont_write_bytecode = True # importing imagekit must not leave __pycache__ in the tree +from imagekit import micro_image parser = argparse.ArgumentParser() -parser.add_argument('--host', default='bin/zy') +parser.add_argument('--host', default='build/bin/zy') +parser.add_argument('--mkgenome', default='build/bin/mkgenome') parser.add_argument('--case', default='') args = parser.parse_args() prefix = b''.join(Path(f).read_bytes() + b'\n' for f in ('src/runtime/rt.zc', 'src/core/core.zc', 'src/runtime/run.zc')) @@ -18,11 +22,8 @@ for name, source, expected in cases: directory = Path(work) compiled = subprocess.run([args.host], input=Path('src/tools/build.zc').read_bytes() + b'`' + prefix + source.encode(), capture_output=True, timeout=30) assert compiled.returncode == 0 and len(compiled.stdout) >= 56 and not compiled.stdout.startswith(b'zc:'), (name, compiled.stdout[:2000], compiled.stderr) - header = subprocess.run(['bin/mkgenome'], input=compiled.stdout, capture_output=True, check=True) - (directory / 'genome.h').write_bytes(header.stdout) - for f in ('src/core/memmap.h', 'src/core/fops.h', 'src/core/bops.h', 'src/core/stack.h', 'src/micro/micro.c', 'src/micro/micro_port.h', 'src/micro/port_linux.c'): - (directory / Path(f).name).write_bytes(Path(f).read_bytes()) - subprocess.run(['cc', '-O2', '-w', '-fwrapv', '-pthread', '-DZY_THREADS', str(directory / 'micro.c'), str(directory / 'port_linux.c'), '-I' + work, '-o', str(directory / 'image')], check=True, timeout=30) - result = subprocess.run([str(directory / 'image')], capture_output=True, timeout=12) + image = subprocess.run([args.mkgenome], input=compiled.stdout, capture_output=True, check=True) + binary = micro_image(directory, image.stdout) + result = subprocess.run([str(binary)], capture_output=True, timeout=12) assert result.returncode == 0 and result.stdout == expected, (name, result.returncode, result.stdout, result.stderr) print('ok actor compiler-free image: ' + name) diff --git a/tests/drivers/actor_test.py b/tests/drivers/actor_test.py index 43a110f..0cfa51f 100644 --- a/tests/drivers/actor_test.py +++ b/tests/drivers/actor_test.py @@ -6,7 +6,7 @@ import subprocess import time parser = argparse.ArgumentParser() -parser.add_argument('vm', nargs='?', default='bin/zy') +parser.add_argument('vm', nargs='?', default='build/bin/zy') parser.add_argument('--case', default='') parser.add_argument('--library', default='') args = parser.parse_args() diff --git a/tests/drivers/analysis_cache_test.js b/tests/drivers/analysis_cache_test.js index 3ffde33..88bb4bd 100644 --- a/tests/drivers/analysis_cache_test.js +++ b/tests/drivers/analysis_cache_test.js @@ -1,4 +1,6 @@ // Repeated replies use cached results; edits, include updates and reopen invalidate them. +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'), cp = require('child_process'), path = require('path'); const uri = 'file:///cache/main.zc', text = 'include "dep.zc";\nfn main() { int n = 7; return n + value(); }'; const msgs = []; let id = 1; @@ -26,7 +28,7 @@ if (process.argv[2] === '--wasm' || process.argv[2] === '--vm') { const p = (wasm ? host.wasmProcess : host.vmProcess)(path.resolve(process.argv[3]), { log: () => {} }); const chunks = []; p.stdout.on('data', d => chunks.push(d)); msgs.forEach(m => p.stdin.write(frame(m))); out = Buffer.concat(chunks); } else { - const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 60000, maxBuffer: 16 << 20 }); + const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 60000 * TIMEOUT_SCALE, maxBuffer: 16 << 20 }); assert.strictEqual(r.status, 0, String(r.stderr)); out = r.stdout; } const replies = new Map(); diff --git a/tests/drivers/array_methods_test.py b/tests/drivers/array_methods_test.py index 82fef67..bf6e670 100644 --- a/tests/drivers/array_methods_test.py +++ b/tests/drivers/array_methods_test.py @@ -5,7 +5,7 @@ import re import subprocess parser = argparse.ArgumentParser() -parser.add_argument('host', nargs='?', default='bin/zy') +parser.add_argument('host', nargs='?', default='build/bin/zy') parser.add_argument('--case', default='') args = parser.parse_args() diff --git a/tests/drivers/cli_help_test.py b/tests/drivers/cli_help_test.py index 1b32fa4..97eebea 100644 --- a/tests/drivers/cli_help_test.py +++ b/tests/drivers/cli_help_test.py @@ -7,15 +7,16 @@ from pathlib import Path parser = argparse.ArgumentParser() parser.add_argument('--group', choices=['binaries', 'scripts', 'js', 'all'], default='all') -parser.add_argument('--bin-dir', default='bin') +parser.add_argument('--bin-dir', default='build/bin') +parser.add_argument('--js-dir', default='build/js') args = parser.parse_args() commands = [] if args.group in ('binaries', 'all'): commands += [[str(Path(args.bin_dir) / name)] for name in ['zy', 'zym', 'zclsp', 'zc0', 'mkgenome', 'genmap']] if args.group in ('scripts', 'all'): - commands += [['bash', name] for name in ['tinyvm.sh', 'src/tools/zcc', 'src/tools/automap.sh', 'src/tools/embed.sh', 'src/tools/mklsp.sh', 'src/tools/mklspwasm.sh', 'src/tools/mkvsix.sh', 'src/web/build.sh', 'src/baremetal/run.sh']] + commands += [['bash', name] for name in ['tinyvm.sh', 'src/tools/zcc', 'src/tools/automap.sh', 'src/tools/findcc.sh', 'src/tools/bootstrap-check.sh', 'src/tools/mklsp.sh', 'src/tools/mklspwasm.sh', 'src/tools/mkvsix.sh', 'src/web/build.sh', 'src/baremetal/run.sh']] if args.group in ('js', 'all'): - commands += [['node', name] for name in ['src/js/tinyvm.js', 'src/js/extract_genome.js', 'src/js/extract_lsp.js']] + commands += [['node', name] for name in [str(Path(args.js_dir) / 'tinyvm.js'), 'src/js/mklayout.js', 'src/js/extract_lsp.js']] def invoke(command, option): # keep stdin open: help and rejected options must finish without reading it. diff --git a/tests/drivers/completion_test.js b/tests/drivers/completion_test.js index 22c5fa5..f1ee5e6 100644 --- a/tests/drivers/completion_test.js +++ b/tests/drivers/completion_test.js @@ -1,5 +1,7 @@ // compiler-backed completion, including incomplete source and position-aware lifetimes. // usage: node tests/drivers/completion_test.js path/to/zclsp [server arguments] +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const path = require('path'); @@ -25,7 +27,7 @@ function complete(source, extra = []) { for (const m of msgs) proc.stdin.write(frame(m)); output = Buffer.concat(chunks); } else { - const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 30000, maxBuffer: 32 << 20 }); + const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 30000 * TIMEOUT_SCALE, maxBuffer: 32 << 20 }); assert.strictEqual(r.status, 0, String(r.stderr)); output = r.stdout; } let b = output; const replies = []; diff --git a/tests/drivers/core_lsp_test.js b/tests/drivers/core_lsp_test.js index 983f9aa..c3c01aa 100644 --- a/tests/drivers/core_lsp_test.js +++ b/tests/drivers/core_lsp_test.js @@ -1,4 +1,6 @@ // check maintained zc sources through the editor document protocol, one file at a time. +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const fs = require('fs'); const cp = require('child_process'); @@ -23,7 +25,7 @@ for (const file of files) { { jsonrpc: '2.0', id: 3, method: 'shutdown', params: null }, { jsonrpc: '2.0', method: 'exit' }, ]; - const result = cp.spawnSync(target, [], { input: Buffer.concat(messages.map(frame)), timeout: 30000, maxBuffer: 32 << 20 }); + const result = cp.spawnSync(target, [], { input: Buffer.concat(messages.map(frame)), timeout: 30000 * TIMEOUT_SCALE, maxBuffer: 32 << 20 }); assert.strictEqual(result.status, 0, file + ': ' + result.stderr); let data = result.stdout; const replies = []; diff --git a/tests/drivers/enhancement_backends_test.py b/tests/drivers/enhancement_backends_test.py index b7d553b..ddfee1b 100644 --- a/tests/drivers/enhancement_backends_test.py +++ b/tests/drivers/enhancement_backends_test.py @@ -9,7 +9,7 @@ source = (root / 'tests/programs/enhance.zc').read_text() source = source.replace('i < 5000', 'i < 200') source += '\n' reference = None -for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]: +for command in [['build/bin/zy'], ['build/bin/zym'], ['node', 'src/js/tinyvm.js']]: result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=25) assert result.returncode == 0 and result.stdout and 'zc:' not in result.stdout, (command, result.returncode, result.stdout, result.stderr) if reference is None: @@ -20,7 +20,7 @@ for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]: source = '''o3 int increment(int value) { return value + 1; } fn main() { put_n(increment(6)); put_c(32); put_n(17 / 5); put_c(32); put_n(17 % 5); put_c(10); return 0; } ''' -for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]: +for command in [['build/bin/zy'], ['build/bin/zym'], ['node', 'src/js/tinyvm.js']]: result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=15) assert result.returncode == 0 and result.stdout == '7 3 2\n', (command, result.stdout, result.stderr) print('ok optimizer descriptors preserve portable division slots: ' + command[0]) @@ -39,7 +39,7 @@ fn main() { put_n(installed); put_c(32); put_n(first); put_c(32); put_n(rejected); put_c(32); put_n(second); put_c(10); return 0; } ''' -for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]: +for command in [['build/bin/zy'], ['build/bin/zym'], ['node', 'src/js/tinyvm.js']]: result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=15) assert result.returncode == 0 and result.stdout == '1 22 1 22\n', (command, result.stdout, result.stderr) print('ok replacing a built-in slot and rejecting a bad recipe: ' + command[0]) diff --git a/tests/drivers/ext_cache_test.js b/tests/drivers/ext_cache_test.js index f389fcc..93caf94 100644 --- a/tests/drivers/ext_cache_test.js +++ b/tests/drivers/ext_cache_test.js @@ -5,14 +5,25 @@ // starts; a change to an included file is checked again; putting it back is // answered from the cache. // -// node tests/drivers/ext_cache_test.js +// node tests/drivers/ext_cache_test.js [server-dir] +// +// the server directory holds what the packaged extension carries in server/: +// lsp.wasm, or tinyvm.js, layout.js, lsp.json and lsp.bin (the default is +// build/editors/vscode/server, which src/tools/mkvsix.sh fills). the extension is +// copied with that server into a scratch directory, since the tree has none. const fs = require('fs'); const os = require('os'); const path = require('path'); const Module = require('module'); -const client = path.join(__dirname, '..', '..', 'editors', 'vscode', 'client'); +const serverDir = path.resolve(process.argv[2] || path.join(__dirname, '..', '..', 'build', 'editors', 'vscode', 'server')); +if (!fs.existsSync(serverDir)) { console.log('FAIL no server directory at ' + serverDir); process.exit(1); } +const staged = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-extension-')); +fs.cpSync(path.join(__dirname, '..', '..', 'editors', 'vscode'), staged, { recursive: true }); +fs.cpSync(serverDir, path.join(staged, 'server'), { recursive: true }); +process.on('exit', () => fs.rmSync(staged, { recursive: true, force: true })); +const client = path.join(staged, 'client'); // the vscode module, as far as the client uses it const world = { lines: [], sets: [], listeners: {}, watchers: [], textDocuments: [], saved: undefined, root: '' }; diff --git a/tests/drivers/float_backends_test.py b/tests/drivers/float_backends_test.py index cdcf48b..65901e2 100644 --- a/tests/drivers/float_backends_test.py +++ b/tests/drivers/float_backends_test.py @@ -4,7 +4,7 @@ import pathlib import subprocess root = pathlib.Path(__file__).resolve().parents[2] -commands = [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']] +commands = [['build/bin/zy'], ['build/bin/zym'], ['node', 'src/js/tinyvm.js']] sources = [(root / 'tests/programs/float.zc').read_text(), '''include rt; fn render(bits, precision, mode) { asm { bits @ precision @ mode @ #1236 } return 0; } fn convert(bits) { return asm { bits @ #1218 }; } fn main() { diff --git a/tests/drivers/fuzz_compiler.py b/tests/drivers/fuzz_compiler.py index 512ec26..0fe0d27 100644 --- a/tests/drivers/fuzz_compiler.py +++ b/tests/drivers/fuzz_compiler.py @@ -1,17 +1,23 @@ #!/usr/bin/env python3 """Deterministic grammar fuzzing for the parser, runtime, and ownership paths. -Usage: python3 tests/drivers/fuzz_compiler.py [path/to/zy] [path/to/zym] [--js] +Usage: python3 tests/drivers/fuzz_compiler.py [path/to/zy] [path/to/zym] [--js [path/to/tinyvm.js]] +(--js alone runs build/js/tinyvm.js, from make js) """ import random import subprocess import sys -args = [arg for arg in sys.argv[1:] if arg != "--js"] -js = "--js" in sys.argv[1:] -zy = args[0] if len(args) > 0 else "bin/zy" -zym = args[1] if len(args) > 1 else "bin/zym" +argv = sys.argv[1:] +js = None +if "--js" in argv: + k = argv.index("--js") + del argv[k] + js = argv.pop(k) if k < len(argv) and argv[k].endswith(".js") else "build/js/tinyvm.js" +args = argv +zy = args[0] if len(args) > 0 else "build/bin/zy" +zym = args[1] if len(args) > 1 else "build/bin/zym" rng = random.Random(0x5A17) values = [] library_values = [] @@ -33,26 +39,26 @@ def expr(depth): return f"({left} {op} {right})", value -lines = ["include std;", "fn churn(n) {", " int i = 0;", " while (i < n) {", " { int* p = new(int); *p = i; }", " i++;", " }", " return 0;", "}", "fn stable() { return 73; }", "fn main() {", " let base = live_objects();"] +lines = ["namespace;", "include rt, run, std;", "fn churn(n) {", " int i = 0;", " while (i < n) {", " { int* p = rt.new(int); *p = i; }", " i++;", " }", " return 0;", "}", "fn stable() { return 73; }", "fn main() {", " let base = rt.live_objects();"] for _ in range(128): source, value = expr(4) - lines.append(f" put_n({source}); put_c(32);") + lines.append(f" rt.put_n({source}); rt.put_c(32);") values.append(value) for _ in range(32): a = "".join(rng.choice("abc") for _ in range(rng.randrange(11))) b = "".join(rng.choice("abc") for _ in range(rng.randrange(6))) - lines.append(f' put_n(Text.len("{a}")); put_c(32); put_n(Text.eq("{a}", "{b}")); put_c(32);') - lines.append(f' put_n(Text.starts("{a}", "{b}")); put_c(32); put_n(Text.ends("{a}", "{b}")); put_c(32); put_n(Text.find("{a}", "{b}")); put_c(32);') - lines.append(f' put_n(Bytes.len(b"{a}")); put_c(32); put_n(Bytes.eq(b"{a}", b"{b}")); put_c(32);') - lines.append(f' put_n(Bytes.starts(b"{a}", b"{b}")); put_c(32); put_n(Bytes.ends(b"{a}", b"{b}")); put_c(32); put_n(Bytes.find(b"{a}", b"{b}")); put_c(32);') + lines.append(f' rt.put_n(std.Text.len("{a}")); rt.put_c(32); rt.put_n(std.Text.eq("{a}", "{b}")); rt.put_c(32);') + lines.append(f' rt.put_n(std.Text.starts("{a}", "{b}")); rt.put_c(32); rt.put_n(std.Text.ends("{a}", "{b}")); rt.put_c(32); rt.put_n(std.Text.find("{a}", "{b}")); rt.put_c(32);') + lines.append(f' rt.put_n(std.Bytes.len(b"{a}")); rt.put_c(32); rt.put_n(std.Bytes.eq(b"{a}", b"{b}")); rt.put_c(32);') + lines.append(f' rt.put_n(std.Bytes.starts(b"{a}", b"{b}")); rt.put_c(32); rt.put_n(std.Bytes.ends(b"{a}", b"{b}")); rt.put_c(32); rt.put_n(std.Bytes.find(b"{a}", b"{b}")); rt.put_c(32);') library_values += [len(a), int(a == b), int(a.startswith(b)), int(a.endswith(b)), a.find(b), len(a.encode()), int(a == b), int(a.startswith(b)), int(a.endswith(b)), a.find(b)] lines += [ - " churn(300); gc(); put_n(live_objects() - base); put_c(32);", - ' char* source = "fn f() { return 7; }"; let parsed = tokens(source);', - ' put_n(parsed[0] > 0); put_c(32); put_n(eq_w(source, "fn f() { return 7; }")); put_c(32);', + " churn(300); rt.gc(); rt.put_n(rt.live_objects() - base); rt.put_c(32);", + ' char* source = "fn f() { return 7; }"; let parsed = run.tokens(source);', + ' rt.put_n(parsed[0] > 0); rt.put_c(32); rt.put_n(rt.eq_w(source, "fn f() { return 7; }")); rt.put_c(32);', ' char* eval_source = "return 42;"; int e = 0;', - ' while (e < 16) { let q = tokens(eval_source); put_n(eval(q)); put_c(32); e++; }', - ' put_n(stable()); put_c(32); put_n(eq_w(eval_source, "return 42;")); put_c(10);', + ' while (e < 16) { let q = run.tokens(eval_source); rt.put_n(run.eval(q)); rt.put_c(32); e++; }', + ' rt.put_n(stable()); rt.put_c(32); rt.put_n(rt.eq_w(eval_source, "return 42;")); rt.put_c(10);', " return 0;", "}", ] @@ -78,7 +84,7 @@ def run(binary): hosts = [(zy, [zy]), (zym, [zym])] if js: - hosts.append(("javascript", ["node", "src/js/tinyvm.js"])) + hosts.append(("javascript", ["node", js])) outputs = [run(command) for _, command in hosts] if any(output != outputs[0] for output in outputs[1:]): raise SystemExit("compiler hosts disagreed on deterministic fuzz output") diff --git a/tests/drivers/fuzz_lsp.js b/tests/drivers/fuzz_lsp.js index e22a44b..f8a1d3b 100644 --- a/tests/drivers/fuzz_lsp.js +++ b/tests/drivers/fuzz_lsp.js @@ -1,7 +1,9 @@ // Deterministic source/protocol fuzzing for diagnostics, caching, and recovery. // Usage: node tests/drivers/fuzz_lsp.js path/to/zclsp +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const { spawnSync } = require('child_process'); -const server = process.argv[2] || 'bin/zclsp'; +const server = process.argv[2] || 'build/bin/zclsp'; let state = 0x5a17; function next() { state = (Math.imul(state, 1664525) + 1013904223) >>> 0; return state; } function frame(m) { @@ -35,7 +37,7 @@ for (let i = 0; i < 32; i++) { msgs.push({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version: 2 }, contentChanges: [{ text: fixed }] } }); } const stdUri = 'file:///fuzz-std.zc'; -msgs.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri: stdUri, languageId: 'zc', version: 1, text: 'include std;\nfn main() { return Text.starts("alpha", "al"); }\n' } } }); +msgs.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri: stdUri, languageId: 'zc', version: 1, text: 'include std;\nfn main() { return std.Text.starts("alpha", "al"); }\n' } } }); // Keep one server alive while documents are repeatedly opened, made malformed, // repaired, closed, and reopened at a new version. This exercises cache @@ -57,7 +59,7 @@ for (const [uri, text] of churn) { msgs.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: churnVersion++, text } } }); } msgs.push({ jsonrpc: '2.0', method: 'exit' }); -const run = spawnSync(server, [], { input: Buffer.concat(msgs.map(frame)), timeout: 60000, maxBuffer: 1 << 28 }); +const run = spawnSync(server, [], { input: Buffer.concat(msgs.map(frame)), timeout: 60000 * TIMEOUT_SCALE, maxBuffer: 1 << 28 }); if (run.error || run.status !== 0) throw run.error || new Error(`server exit ${run.status}: ${run.stderr}`); const out = parse(run.stdout); const diagnostics = new Map(); diff --git a/tests/drivers/fuzz_malformed.py b/tests/drivers/fuzz_malformed.py index 5bf97cf..42b8a0e 100644 --- a/tests/drivers/fuzz_malformed.py +++ b/tests/drivers/fuzz_malformed.py @@ -8,7 +8,7 @@ import subprocess import sys -bins = sys.argv[1:] or ["bin/zy", "bin/zym"] +bins = sys.argv[1:] or ["build/bin/zy", "build/bin/zym"] if len(bins) == 1: bins.append(bins[0]) rng = random.Random(0xC0DE) diff --git a/tests/drivers/fuzz_wasm.js b/tests/drivers/fuzz_wasm.js index b0982d4..22fa7d5 100644 --- a/tests/drivers/fuzz_wasm.js +++ b/tests/drivers/fuzz_wasm.js @@ -31,7 +31,7 @@ function quote(s) { return '"' + s + '"'; } lines.push(' churn(300); gc(); put_n(live_objects() - base); put_c(32);', ' put_n(stable()); put_c(10);', ' return 0;', '}'); expected.push(0, 73); const source = lines.join('\n') + '\n', want = expected.join(' ') + '\n'; - const zc = await ZC.load(fs.readFileSync(process.argv[2] || 'src/web/zc.wasm')); + const zc = await ZC.load(fs.readFileSync(process.argv[2] || 'build/web/zc.wasm')); const result = await zc.run(source); if (result.output !== want) throw new Error(`wasm fuzz mismatch\nexpected: ${want.slice(0, 180)}\nactual: ${result.output.slice(0, 180)}\nmessages: ${result.messages}`); console.log('ok wasm compiler/runtime/stdlib fuzz: 128 expressions, 300 scoped allocations and GC, and 32 Text/Bytes cases'); diff --git a/tests/drivers/genome_blob_test.py b/tests/drivers/genome_blob_test.py index 81533a7..fea055b 100644 --- a/tests/drivers/genome_blob_test.py +++ b/tests/drivers/genome_blob_test.py @@ -1,13 +1,17 @@ #!/usr/bin/env python3 # documentation precedes the symbol table; the boot header must skip those bytes. +# usage: python3 tests/drivers/genome_blob_test.py [build/lsp] (the documented language server image) import pathlib import re +import struct +import sys -source = pathlib.Path('src/core/genome.h').read_text() -org = int(re.search(r'#define GORG (\d+)', source)[1]) -length = int(re.search(r'#define GLEN (\d+)', source)[1]) -words = [int(word.strip()) for word in re.search(r'genome\[\]\s*=\s*\{([\s\S]*?)\};', source)[1].split(',') if word.strip()] -assert len(words) == length and words[6] == org + length +folder = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else 'build/lsp') +image = (folder / 'lsp_genome.bin').read_bytes() +org = int(re.search(r'#define GORG (?:0x)?([0-9A-Fa-f]+)', (folder / 'memmap.h').read_text())[1], 16) +words = list(struct.unpack('<%dq' % (len(image) // 8), image)) +length = len(words) +assert words[6] == org + length cursor = words[5] - org assert 7 <= cursor < length records = {} @@ -23,8 +27,8 @@ while words[cursor]: cursor += 1 records[name] = (kind, value, signature) assert cursor == length - 1 -assert len(records) > 1000 and records['start'][0] == 1 -assert records['reserve_code'][0] == 1 and records['build_image_grow'][0] == 1 +assert len(records) > 1000 and records['run.start'][0] == 1 +assert records['run.reserve_code'][0] == 1 and records['run.build_image'][0] == 1 assert any(name.startswith('doc ') for name in records), 'the fixture contains documentation bytes' for name, (kind, value, _) in records.items(): if kind == 8: diff --git a/tests/drivers/hints_test.js b/tests/drivers/hints_test.js index 8bb1934..b3598be 100644 --- a/tests/drivers/hints_test.js +++ b/tests/drivers/hints_test.js @@ -1,5 +1,7 @@ // hints are informational diagnostics (severity 3) and follow positional hints on / small / off directives. // usage: node tests/drivers/hints_test.js path/to/zclsp +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const server = process.argv[2]; @@ -10,7 +12,7 @@ function diagnostics(text) { { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }, { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } }, { jsonrpc: '2.0', method: 'exit' }].map(frame)); - const r = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000, maxBuffer: 4 << 20 }); + const r = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000 * TIMEOUT_SCALE, maxBuffer: 4 << 20 }); assert.strictEqual(r.status, 0, String(r.stderr)); let d = r.stdout, out = []; while (d.length) { diff --git a/tests/drivers/hover_features_test.js b/tests/drivers/hover_features_test.js index d6e1226..25f5965 100644 --- a/tests/drivers/hover_features_test.js +++ b/tests/drivers/hover_features_test.js @@ -1,5 +1,7 @@ // focused type, inference, and block-documentation hover regressions. // usage: node tests/drivers/hover_features_test.js path/to/zclsp +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const server = process.argv[2]; @@ -21,7 +23,7 @@ function hover(text, needle, nth = 0) { { jsonrpc: '2.0', id: 2, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line, character } } }, { jsonrpc: '2.0', method: 'exit' }, ]; - const result = cp.spawnSync(server, process.argv.slice(3), { input: Buffer.concat(messages.map(frame)), timeout: 10000, maxBuffer: 4 << 20 }); + const result = cp.spawnSync(server, process.argv.slice(3), { input: Buffer.concat(messages.map(frame)), timeout: 10000 * TIMEOUT_SCALE, maxBuffer: 4 << 20 }); assert.strictEqual(result.status, 0, String(result.stderr)); let data = result.stdout; while (data.length) { diff --git a/tests/drivers/hover_surface_test.js b/tests/drivers/hover_surface_test.js index 13a9a3f..688aca6 100644 --- a/tests/drivers/hover_surface_test.js +++ b/tests/drivers/hover_surface_test.js @@ -1,4 +1,6 @@ // hover declarations and instructions in the source-only documents opened by f12. +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const fs = require('fs'); const cp = require('child_process'); @@ -48,7 +50,7 @@ if (process.argv[2] === '--wasm' || process.argv[2] === '--vm') { for (const m of messages) p.stdin.write(frame({ jsonrpc: '2.0', ...m })); out = Buffer.concat(chunks); } else { - const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(messages.map(m => frame({ jsonrpc: '2.0', ...m }))), timeout: 60000, maxBuffer: 32 << 20 }); + const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(messages.map(m => frame({ jsonrpc: '2.0', ...m }))), timeout: 60000 * TIMEOUT_SCALE, maxBuffer: 32 << 20 }); assert.strictEqual(r.status, 0, String(r.stderr)); out = r.stdout; } const replies = new Map(); diff --git a/tests/drivers/hover_values_test.js b/tests/drivers/hover_values_test.js index badc2ed..0f09cfc 100644 --- a/tests/drivers/hover_values_test.js +++ b/tests/drivers/hover_values_test.js @@ -1,4 +1,6 @@ // value previews follow the compiler's bindings and the position of each use. +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const server = process.argv[2]; @@ -17,7 +19,7 @@ function hover(text, needle, nth = 0) { const body = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]); })); - const result = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000, maxBuffer: 8 << 20 }); + const result = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000 * TIMEOUT_SCALE, maxBuffer: 8 << 20 }); assert.strictEqual(result.status, 0, String(result.stderr)); let data = result.stdout; while (data.length) { diff --git a/tests/drivers/icon_test.py b/tests/drivers/icon_test.py index 560c622..72cd69b 100644 --- a/tests/drivers/icon_test.py +++ b/tests/drivers/icon_test.py @@ -5,7 +5,7 @@ import argparse import subprocess parser = argparse.ArgumentParser() -parser.add_argument('vm', nargs='?', default='bin/zy') +parser.add_argument('vm', nargs='?', default='build/bin/zy') parser.add_argument('--case', default='') parser.add_argument('--js', action='store_true') parser.add_argument('--source', action='store_true', help='test the working library source instead of the embedded module') diff --git a/tests/drivers/image_run.js b/tests/drivers/image_run.js index f657916..ea05f9f 100644 --- a/tests/drivers/image_run.js +++ b/tests/drivers/image_run.js @@ -2,12 +2,11 @@ const fs = require('fs'); const { TinyVM } = require('../../build/js/tinyvm.js'); const { mapValues } = require('../../src/js/mapvalues.js'); -const header = fs.readFileSync(process.argv[2], 'utf8'); -const org = Number(header.match(/#define\s+GORG\s+(\d+)/)[1]); -const words = header.match(/genome\[\]\s*=\s*\{([\s\S]*?)\};/)[1].split(',').map(s => s.trim()).filter(Boolean); +// usage: node tests/drivers/image_run.js genome.bin memmap.h [--stdin] +const image = fs.readFileSync(process.argv[2]); const layout = mapValues(process.argv[3], ['GORG', 'DS0', 'DSLIM', 'RS0', 'RSLIM', 'SYSGATE', 'ZFP', 'HEAP_LO', 'HEAP_HI']); const machine = new TinyVM({ layout, onMessage: message => process.stderr.write(message) }); -machine.loadGenome(words, org); +machine.loadImage(image, layout.GORG); if (process.argv[4] === '--stdin') machine.setInput(fs.readFileSync(0)); const status = machine.run(); process.stdout.write(machine.getOutput()); diff --git a/tests/drivers/imagekit.py b/tests/drivers/imagekit.py new file mode 100644 index 0000000..6cc083f --- /dev/null +++ b/tests/drivers/imagekit.py @@ -0,0 +1,34 @@ +"""shared steps for the drivers that build their own images: pick a c compiler with +#embed, and link a microcore around a genome image (little-endian 64-bit words) +for a given memory map. everything is written into the caller's directory.""" +import os +import shutil +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +DEFAULT_MAP = ROOT / 'build' / 'core' / 'map' / 'memmap.h' +MICRO_SOURCES = ['src/micro/micro.c', 'src/micro/micro_port.h', 'src/micro/port_linux.c', + 'src/core/fops.h', 'src/core/bops.h', 'src/core/stack.h', 'src/core/genome.h'] + + +def compiler(): + """the c compiler: CC, or the one src/tools/findcc.sh picks.""" + if os.environ.get('CC'): + return os.environ['CC'] + return subprocess.run(['sh', str(ROOT / 'src/tools/findcc.sh')], check=True, capture_output=True, text=True).stdout.strip() + + +def micro_image(directory, image, memmap=DEFAULT_MAP, threads=True, name='image', timeout=60): + """link a microcore in directory that boots image (bytes). memmap is the map header the + image was built for. returns the executable's path.""" + directory = Path(directory) + (directory / 'genome.bin').write_bytes(image) + shutil.copy(memmap, directory / 'memmap.h') + for f in MICRO_SOURCES: + shutil.copy(ROOT / f, directory) + flags = ['-pthread', '-DZY_THREADS'] if threads else [] + out = directory / name + subprocess.run([compiler(), '-O2', '-w', '-fwrapv', *flags, str(directory / 'micro.c'), str(directory / 'port_linux.c'), + '-I' + str(directory), '-o', str(out)], check=True, timeout=timeout) + return out diff --git a/tests/drivers/layout_config_test.py b/tests/drivers/layout_config_test.py index 469e7dd..2f0fa07 100644 --- a/tests/drivers/layout_config_test.py +++ b/tests/drivers/layout_config_test.py @@ -5,7 +5,7 @@ import subprocess import tempfile root = pathlib.Path(__file__).resolve().parents[2] -tool = root / 'bin/genmap' +tool = root / 'build/bin/genmap' result = subprocess.run([tool, '--help'], capture_output=True, text=True) assert result.returncode == 0 and '--generated-text' in result.stderr invalid = [ diff --git a/tests/drivers/lsp_growth_test.js b/tests/drivers/lsp_growth_test.js index b06230a..6d221fd 100644 --- a/tests/drivers/lsp_growth_test.js +++ b/tests/drivers/lsp_growth_test.js @@ -1,5 +1,7 @@ // focused regressions for growing document, uri, hover, and name-set storage. // usage: node tests/drivers/lsp_growth_test.js path/to/zclsp-or-lsp.wasm +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const { wasmProcess } = require('../../editors/vscode/client/wasmproc'); @@ -36,7 +38,7 @@ if (target.endsWith('.wasm')) { for (const message of messages) assert.ok(server.stdin.write(frame(message))); output = Buffer.concat(chunks); } else { - const result = cp.spawnSync(target, [], { input: Buffer.concat(messages.map(frame)), timeout: 30000, maxBuffer: 8 << 20 }); + const result = cp.spawnSync(target, [], { input: Buffer.concat(messages.map(frame)), timeout: 30000 * TIMEOUT_SCALE, maxBuffer: 8 << 20 }); assert.strictEqual(result.status, 0, String(result.stderr)); output = result.stdout; } diff --git a/tests/drivers/lsp_sandbox.js b/tests/drivers/lsp_sandbox.js index 14bc1e3..41523cc 100644 --- a/tests/drivers/lsp_sandbox.js +++ b/tests/drivers/lsp_sandbox.js @@ -2,10 +2,12 @@ // (immediate functions). the server must not let that code write files, so a file that // tries to leaves nothing behind and the server still answers. // usage: node tests/drivers/lsp_sandbox.js path/to/zclsp +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const cp = require('child_process'), fs = require('fs'), os = require('os'), path = require('path'); const target = path.join(os.tmpdir(), 'zc-sandbox-' + process.pid); try { fs.unlinkSync(target); } catch (e) {} -const src = 'immediate fn evil() { let fd = syscall(1, "' + target + '", 1, 0); syscall(4, fd, 65, 1); syscall(2, fd, 0, 0); return 0; }\nevil\nfn main() { return 0; }\n'; +const src = 'include rt, run;\nimmediate fn evil() { run.next(); let fd = rt.syscall(1, "' + target + '", 1, 0); rt.syscall(4, fd, 65, 1); rt.syscall(2, fd, 0, 0); return 0; }\nevil\nfn main() { return 0; }\n'; const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b]); }; const input = Buffer.concat([ frame({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } }), @@ -13,7 +15,7 @@ const input = Buffer.concat([ frame({ jsonrpc: '2.0', id: 3, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri: 'file:///evil.zc' } } }), frame({ jsonrpc: '2.0', method: 'exit' }), ]); -const r = cp.spawnSync(process.argv[2], [], { input, timeout: 20000 }); +const r = cp.spawnSync(process.argv[2], [], { input, timeout: 20000 * TIMEOUT_SCALE }); const wrote = fs.existsSync(target); if (wrote) fs.unlinkSync(target); const answered = r.stdout && r.stdout.includes('"id":3'); diff --git a/tests/drivers/lsp_test.js b/tests/drivers/lsp_test.js index ab43c41..18d8bf7 100644 --- a/tests/drivers/lsp_test.js +++ b/tests/drivers/lsp_test.js @@ -1,5 +1,7 @@ // protocol tests for the language server (node, no packages). // usage: node tests/drivers/lsp_test.js path/to/zclsp +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const { spawnSync } = require('child_process'); const fs = require('fs'); const server = process.argv[2]; @@ -24,7 +26,7 @@ function parse(buf) { return out; } function run(msgs) { - const r = spawnSync(server, [], { input: Buffer.concat(msgs.map(frame)), timeout: 30000, maxBuffer: 1 << 28 }); + const r = spawnSync(server, [], { input: Buffer.concat(msgs.map(frame)), timeout: 30000 * TIMEOUT_SCALE, maxBuffer: 1 << 28 }); return { code: r.status, msgs: parse(r.stdout) }; } const init = id => ({ jsonrpc: '2.0', id, method: 'initialize', params: {} }); @@ -173,7 +175,7 @@ check(hv && hv.includes('leaves the function with a value'), 'a keyword shows wh const builtinSource = 'include rt; fn main() {\n\trt.put_c(32);\n\treturn 0;\n}\n'; hv = hover(builtinSource, 1, at(builtinSource, 1, 'put_c')); check(hv && hv.includes('put_c('), 'a built-in function shows its signature', hv); -const kwsrc = 'include rt; fn main() {\n\tint* p = new(int);\n\trt.free(p);\n\treturn 0;\n}\n'; +const kwsrc = 'include rt; fn main() {\n\tint* p = rt.new(int);\n\trt.free(p);\n\treturn 0;\n}\n'; hv = hover(kwsrc, 1, at(kwsrc, 1, 'new')); check(hv && hv.includes('new(T)') && hv.includes('fresh heap object') && hv.includes('owns it'), 'hover on new shows its form and ownership behavior', hv); hv = hover(kwsrc, 2, at(kwsrc, 2, 'free')); @@ -209,7 +211,7 @@ check(r.code === 0 && r.msgs.some(m => m.id === 9 && m.result === null), 'shutdo // the corpus: every valid program checks clean, every bad one is rejected with its message function cli(file) { - const r = spawnSync(server, [], { input: fs.readFileSync(file), timeout: 30000 }); + const r = spawnSync(server, [], { input: fs.readFileSync(file), timeout: 30000 * TIMEOUT_SCALE }); return r.stdout.toString(); } let clean = 0, dirty = []; diff --git a/tests/drivers/memory_limits_test.py b/tests/drivers/memory_limits_test.py index a973480..808a239 100644 --- a/tests/drivers/memory_limits_test.py +++ b/tests/drivers/memory_limits_test.py @@ -13,7 +13,7 @@ source = '''fn main() { return 0; } ''' -commands = [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']] +commands = [['build/bin/zy'], ['build/bin/zym'], ['node', 'src/js/tinyvm.js']] environment = dict(os.environ, TINYVM_MAX_MEMORY_WORDS='16777216') for command in commands: result = subprocess.run(command, input=source, text=True, capture_output=True, env=environment, timeout=15) diff --git a/tests/drivers/nav_test.js b/tests/drivers/nav_test.js index 23afbf8..8f0d8fd 100644 --- a/tests/drivers/nav_test.js +++ b/tests/drivers/nav_test.js @@ -1,5 +1,7 @@ // go to definition and find references, in the document and across includes. // usage: node tests/drivers/nav_test.js path/to/zclsp (run from the repository root) +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); @@ -18,7 +20,7 @@ function request(text, needle, method, extra = {}) { { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } }, { jsonrpc: '2.0', id: 2, method, params: { textDocument: { uri }, position, ...extra } }, { jsonrpc: '2.0', method: 'exit' }].map(frame)); - const r = cp.spawnSync(server, process.argv.slice(3), { input, cwd: root, timeout: 30000, maxBuffer: 8 << 20 }); + const r = cp.spawnSync(server, process.argv.slice(3), { input, cwd: root, timeout: 30000 * TIMEOUT_SCALE, maxBuffer: 8 << 20 }); assert.strictEqual(r.status, 0, String(r.stderr)); let d = r.stdout, init; while (d.length) { diff --git a/tests/drivers/protocol_growth_test.js b/tests/drivers/protocol_growth_test.js index 64015d8..d8dff6c 100644 --- a/tests/drivers/protocol_growth_test.js +++ b/tests/drivers/protocol_growth_test.js @@ -1,4 +1,6 @@ // preserve long request identifiers and ignore deeply nested unknown json fields. +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const target = process.argv[2]; @@ -15,7 +17,7 @@ for (let i = 0; i < 24; i++) messages.push({ jsonrpc: '2.0', method: 'textDocume text: 'enum choice { item { int value; }, empty };\nfn main() { choice*.m p = choice.item(7); return p.item_value; }\n', } } }); messages.push({ jsonrpc: '2.0', id: 'finish', method: 'shutdown', params: null }, { jsonrpc: '2.0', method: 'exit' }); -const result = cp.spawnSync(target, [], { input: Buffer.concat(messages.map(frame)), timeout: 20000, maxBuffer: 8 << 20 }); +const result = cp.spawnSync(target, [], { input: Buffer.concat(messages.map(frame)), timeout: 20000 * TIMEOUT_SCALE, maxBuffer: 8 << 20 }); assert.strictEqual(result.status, 0, String(result.stderr)); const responses = []; let data = result.stdout; diff --git a/tests/drivers/runtime_image_test.py b/tests/drivers/runtime_image_test.py index c1f4b9c..d214613 100644 --- a/tests/drivers/runtime_image_test.py +++ b/tests/drivers/runtime_image_test.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 """self-hosted run-only images retain enhancements and generic method bodies.""" +import sys import argparse import subprocess import tempfile from pathlib import Path +sys.dont_write_bytecode = True # importing imagekit must not leave __pycache__ in the tree +from imagekit import micro_image parser = argparse.ArgumentParser() parser.add_argument('--case', default='') -parser.add_argument('--host', default='bin/zy') -parser.add_argument('--mkgenome', default='bin/mkgenome') +parser.add_argument('--host', default='build/bin/zy') +parser.add_argument('--mkgenome', default='build/bin/mkgenome') +parser.add_argument('--map', default='build/core/map/memmap.h') args = parser.parse_args() cases = [ ('enhancements', 'include rt; fn main() { rt.put_n(17 % 5); rt.put_c(32); rt.put_n(17 / 5); }', b'2 3'), @@ -23,12 +27,9 @@ for name, source, expected in cases: directory = Path(work) r = subprocess.run([args.host], input=Path('src/tools/build.zc').read_bytes() + b'`' + prefix + source.encode(), capture_output=True, timeout=30) assert r.returncode == 0 and len(r.stdout) >= 56 and not r.stdout.startswith(b'zc:'), (name, r.stdout[:1000], r.stderr) - header = subprocess.run([args.mkgenome], input=r.stdout, capture_output=True, timeout=5) - assert header.returncode == 0, (name, header.stderr) - (directory / 'genome.h').write_bytes(header.stdout) - for f in ['src/core/memmap.h', 'src/core/fops.h', 'src/core/bops.h', 'src/core/stack.h', 'src/micro/micro.c', 'src/micro/micro_port.h', 'src/micro/port_linux.c']: - (directory / Path(f).name).write_bytes(Path(f).read_bytes()) - subprocess.run(['cc', '-O2', '-w', '-fwrapv', '-pthread', '-DZY_THREADS', str(directory / 'micro.c'), str(directory / 'port_linux.c'), '-I' + work, '-o', str(directory / 'image')], check=True, timeout=30) - r = subprocess.run([str(directory / 'image')], capture_output=True, timeout=10) + image = subprocess.run([args.mkgenome], input=r.stdout, capture_output=True, timeout=5) + assert image.returncode == 0, (name, image.stderr) + binary = micro_image(directory, image.stdout, args.map) + r = subprocess.run([str(binary)], capture_output=True, timeout=10) assert r.returncode == 0 and r.stdout == expected, (name, r.returncode, r.stdout, r.stderr, expected) print('ok self-hosted run-only image: ' + name) diff --git a/tests/drivers/seed_growth_test.py b/tests/drivers/seed_growth_test.py index 426c970..b94445e 100644 --- a/tests/drivers/seed_growth_test.py +++ b/tests/drivers/seed_growth_test.py @@ -1,25 +1,24 @@ #!/usr/bin/env python3 # stage 0 accepts long source streams and grows a tiny initial image reservation. +import sys import pathlib import subprocess import tempfile +sys.dont_write_bytecode = True # importing imagekit must not leave __pycache__ in the tree +from imagekit import compiler, micro_image root = pathlib.Path(__file__).resolve().parents[2] +(root / 'build').mkdir(exist_ok=True) with tempfile.TemporaryDirectory(prefix='seed-growth-', dir=root / 'build') as directory: folder = pathlib.Path(directory) - subprocess.run(['bin/genmap', '--preset', 'run', '--grow', '--genome', '600000', '--heap', '1048576', '-o', directory], check=True, capture_output=True) - compiler = folder / 'zc0' - subprocess.run(['cc', '-O1', '-w', '-fwrapv', '-DMAXW=16', 'src/tools/zc0.c', '-I' + directory, '-Isrc/core', '-o', str(compiler)], check=True) + subprocess.run(['build/bin/genmap', '--preset', 'run', '--grow', '--genome', '600000', '--heap', '1048576', '-o', directory], check=True, capture_output=True) + zc0 = folder / 'zc0' + subprocess.run([compiler(), '-O1', '-w', '-fwrapv', '-DMAXW=16', 'src/tools/zc0.c', '-I' + directory, '-Isrc/core', '-o', str(zc0)], check=True) source = '/*' + 'x' * (5 << 20) + '*/\n' - source += (folder / 'rt.zc').read_text() + (root / 'src/runtime/run.zc').read_text() - source += 'let data[550000]; fn main() { data[549999] = 7; put_n(data[549999]); put_c(32); put_n(4611686018427387904); put_c(10); return 0; }\n' - with (folder / 'genome.h').open('w') as output: - subprocess.run([str(compiler)], input=source, text=True, stdout=output, check=True, timeout=30) - binary = folder / 'vm' - import shutil - for path in ['src/micro/micro.c', 'src/micro/micro_port.h', 'src/micro/port_linux.c', 'src/core/fops.h', 'src/core/bops.h', 'src/core/stack.h']: - shutil.copy(root / path, folder) - subprocess.run(['cc', '-O2', '-w', '-fwrapv', str(folder / 'micro.c'), str(folder / 'port_linux.c'), '-I' + directory, '-o', str(binary)], check=True) + source += (folder / 'rt.zc').read_text() + (root / 'src/core/core.zc').read_text() + (root / 'src/runtime/run.zc').read_text() + source += 'namespace; include rt; let data[550000]; fn main() { data[549999] = 7; rt.put_n(data[549999]); rt.put_c(32); rt.put_n(4611686018427387904); rt.put_c(10); return 0; }\n' + image = subprocess.run([str(zc0)], input=source.encode(), capture_output=True, check=True, timeout=30).stdout + binary = micro_image(folder / 'vm', image, folder / 'memmap.h', threads=False) if (folder / 'vm').mkdir() is None else None result = subprocess.run([str(binary)], text=True, capture_output=True, timeout=20) assert result.returncode == 0 and result.stdout == '7 4611686018427387904\n', (result.returncode, result.stdout, result.stderr) print('ok stage 0 reads beyond four megabytes and grows beyond its initial image capacity') diff --git a/tests/drivers/stack_growth_test.py b/tests/drivers/stack_growth_test.py index d4abe81..3c90305 100644 --- a/tests/drivers/stack_growth_test.py +++ b/tests/drivers/stack_growth_test.py @@ -4,6 +4,8 @@ import subprocess import sys import tempfile from pathlib import Path +sys.dont_write_bytecode = True # importing imagekit must not leave __pycache__ in the tree +from imagekit import compiler command = sys.argv[1:] @@ -12,47 +14,53 @@ def run(source, expected, compile_image=False): if compile_image and command[0] == 'node': # compile the execution stress cases once with stage 0, so a javascript # compiler's speed does not determine the execution-test timeout. + Path('build').mkdir(exist_ok=True) with tempfile.TemporaryDirectory(prefix='stack-image-', dir='build') as directory: target = Path(directory) - subprocess.run(['bin/genmap', '--preset', 'run', '--grow', '--heap', '1048576', '--genome', '131072', '--frame', '4096', '--return', '256', '--data', '3072', '-o', directory], check=True, capture_output=True) - compiler = target / 'zc0' - subprocess.run(['cc', '-O1', '-w', '-fwrapv', 'src/tools/zc0.c', '-I' + directory, '-Isrc/core', '-o', str(compiler)], check=True, capture_output=True) - full_source = (target / 'rt.zc').read_text() + Path('src/runtime/run.zc').read_text() + source - image = subprocess.run([str(compiler)], input=full_source, text=True, capture_output=True, check=True) - header = target / 'genome.h' - header.write_text(image.stdout) - result = subprocess.run(['node', 'tests/drivers/image_run.js', str(header), str(target / 'memmap.h')], text=True, capture_output=True, timeout=45) + subprocess.run(['build/bin/genmap', '--preset', 'run', '--grow', '--heap', '1048576', '--genome', '131072', '--frame', '4096', '--return', '256', '--data', '3072', '-o', directory], check=True, capture_output=True) + zc0 = target / 'zc0' + subprocess.run([compiler(), '-O1', '-w', '-fwrapv', 'src/tools/zc0.c', '-I' + directory, '-Isrc/core', '-o', str(zc0)], check=True, capture_output=True) + full_source = (target / 'rt.zc').read_text() + Path('src/core/core.zc').read_text() + Path('src/runtime/run.zc').read_text() + source + image = subprocess.run([str(zc0)], input=full_source.encode(), capture_output=True, check=True) + (target / 'genome.bin').write_bytes(image.stdout) + result = subprocess.run(['node', 'tests/drivers/image_run.js', str(target / 'genome.bin'), str(target / 'memmap.h')], text=True, capture_output=True, timeout=45) else: result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=45) assert result.returncode == 0 and result.stdout == expected, (command, result.returncode, result.stdout, result.stderr) -run('''fn main() { - int old_data = data_top - data_low, old_return = return_top - return_low; - put_n(reserve_data(old_data + 1) > old_data); put_c(32); - put_n(reserve_returns(old_return + 1) > old_return); put_c(32); - gc(); put_n(data_node != 0 && return_node != 0); put_c(10); return 0; +run('''namespace; +include rt; +fn main() { + int old_data = rt.data_top - rt.data_low, old_return = rt.return_top - rt.return_low; + rt.put_n(rt.reserve_data(old_data + 1) > old_data); rt.put_c(32); + rt.put_n(rt.reserve_returns(old_return + 1) > old_return); rt.put_c(32); + rt.gc(); rt.put_n(rt.data_node != 0 && rt.return_node != 0); rt.put_c(10); return 0; } ''', '1 1 1\n') -run('''fn main() { - put_n(reserve_data(-1) == -1); put_c(32); put_n(reserve_returns(-1) == -1); put_c(32); - put_n(syscall(9, 6, 0, 0) == -1); put_c(32); - int current = syscall(9, 0, 0, 0); syscall(9, 5, current, 0); - put_n(reserve_data(0x10000000) == -1); put_c(32); gc(); put_n(7); put_c(10); return 0; +run('''namespace; +include rt; +fn main() { + rt.put_n(rt.reserve_data(-1) == -1); rt.put_c(32); rt.put_n(rt.reserve_returns(-1) == -1); rt.put_c(32); + rt.put_n(rt.syscall(9, 6, 0, 0) == -1); rt.put_c(32); + int current = rt.syscall(9, 0, 0, 0); rt.syscall(9, 5, current, 0); + rt.put_n(rt.reserve_data(0x10000000) == -1); rt.put_c(32); rt.gc(); rt.put_n(7); rt.put_c(10); return 0; } ''', '1 1 1 1 7\n') count = 40000 -source = 'fn main() { int total = asm { ' + ' '.join(str(i + 1) for i in range(count)) -source += ' + ' * (count - 1) + ' }; gc(); put_n(total); put_c(32); ' -source += 'put_n(data_node != 0); put_c(10); return 0; }\n' +source = 'namespace; include rt; fn main() { int total = asm { ' + ' '.join(str(i + 1) for i in range(count)) +source += ' + ' * (count - 1) + ' }; rt.gc(); rt.put_n(total); rt.put_c(32); ' +source += 'rt.put_n(rt.data_node != 0); rt.put_c(10); return 0; }\n' run(source, '800020000 1\n', compile_image=True) -run('''unchecked int descend(int n) { - if (!n) { gc(); return 7; } +run('''namespace; +include rt; +unchecked int descend(int n) { + if (!n) { rt.gc(); return 7; } return descend(n - 1) + 1; } -fn main() { put_n(descend(70000)); put_c(32); put_n(return_node != 0); put_c(10); return 0; } +fn main() { rt.put_n(descend(70000)); rt.put_c(32); rt.put_n(rt.return_node != 0); rt.put_c(10); return 0; } ''', '70007 1\n', compile_image=True) print('ok explicit reservation, 40000 operands, and 70000 return addresses: ' + command[0]) diff --git a/tests/drivers/storage_analysis_test.js b/tests/drivers/storage_analysis_test.js index 774fa89..704a844 100644 --- a/tests/drivers/storage_analysis_test.js +++ b/tests/drivers/storage_analysis_test.js @@ -1,8 +1,12 @@ // definite storage findings use the compiler's parser and the editor protocol. +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const server = process.argv[2]; function check(text, needle) { + // the runtime is an explicit import; it shares the first line so line numbers stay as written + text = 'include rt; ' + text; const uri = 'file:///storage-analysis.zc'; const messages = [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }, { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } }]; @@ -13,7 +17,7 @@ function check(text, needle) { } messages.push({ jsonrpc: '2.0', method: 'exit' }); const input = Buffer.concat(messages.map(m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b]); })); - const r = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000, maxBuffer: 8 << 20 }); + const r = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000 * TIMEOUT_SCALE, maxBuffer: 8 << 20 }); assert.strictEqual(r.status, 0, String(r.stderr)); const replies = []; let data = r.stdout; while (data.length) { @@ -27,50 +31,50 @@ function check(text, needle) { } const prefix = 'unchecked fn main() {\n'; for (const [type, length] of [['int', 3], ['u8', 3], ['fixed int[?]', 3], ['u8[?]', 3]]) { - const r = check(prefix + ` let p = new(${type}, ${length});\n let alias = p;\n return alias[${length}];\n}\n`, 'alias['); + const r = check(prefix + ` let p = rt.new(${type}, ${length});\n let alias = p;\n return alias[${length}];\n}\n`, 'alias['); assert.strictEqual(r.warnings.length, 1, JSON.stringify(r.diagnostics)); assert.match(r.warnings[0].message, /out-of-bounds.*index 3.*length 3/); assert.match(r.hover, /size: 3 elements.*lifetime: heap storage; live here/); assert.strictEqual(r.warnings[0].range.start.line, 3); } -const negative = check(prefix + ' let p = new(3); return p[-1];\n}\n'); +const negative = check(prefix + ' let p = rt.new(3); return p[-1];\n}\n'); assert.match(negative.warnings[0]?.message || '', /index -1.*length 3/); -const freed = check(prefix + ' let p = new(int, 3);\n let q = p;\n free(p);\n return q[0];\n}\n', 'q['); +const freed = check(prefix + ' let p = rt.new(int, 3);\n let q = p;\n rt.free(p);\n return q[0];\n}\n', 'q['); assert.strictEqual(freed.warnings.length, 1, JSON.stringify(freed.diagnostics)); assert.match(freed.warnings[0].message, /use after free.*line 4/); assert.match(freed.hover, /size: 3 elements.*lifetime: ended on line 4/); -const dereference = check(prefix + ' let p = new(int); free(p); return *p;\n}\n'); +const dereference = check(prefix + ' let p = rt.new(int); rt.free(p); return *p;\n}\n'); assert.match(dereference.warnings[0]?.message || '', /use after free/); -const field = check('struct item { int value; };\n' + prefix + ' let p = new(item); free(p); return p.value;\n}\n'); +const field = check('struct item { int value; };\n' + prefix + ' let p = rt.new(item); rt.free(p); return p.value;\n}\n'); assert.match(field.warnings[0]?.message || '', /use after free/); const scope = check(prefix + ' int* p;\n { let a[3]; p = a; }\n return p[0];\n}\n', 'p['); assert.match(scope.warnings[0]?.message || '', /use after free.*line 3/); assert.match(scope.hover, /lifetime: ended on line 3/); const safe = [ - prefix + ' let p = new(int, 3); let q = p as u8*; return q[23]; }', - prefix + ' let p = new(int, 3); let q = (u8*)p; return q[23]; }', + prefix + ' let p = rt.new(int, 3); let q = p as u8*; return q[23]; }', + prefix + ' let p = rt.new(int, 3); let q = (u8*)p; return q[23]; }', prefix + ' int n = 2; let p = &n; let q = n; return q[2]; }', - prefix + ' let p = new(int, 3); let ignored = 0 && free(p); return p[0]; }', - prefix + ' let p = new(int, 3); let ignored = 1 || free(p); return p[0]; }', - prefix + ' let p = new(int, 50); return p[(u8)300]; }', - prefix + ' let p = new(int, 50); return p[300 as u8]; }', - prefix + ' let p = new(int, 3); let q = p; q[2] = 7; free(p); return 0; }', - prefix + ' let p = new(3); free(p); p = new(3); return p[0]; }', - 'unchecked fn unknown(int* p) {}\n' + prefix + ' let p = new(int, 3); unknown(p); return p[9]; }', - 'fn maybe(int n) { return n; }\n' + prefix + ' let p = new(int, 3); return p[maybe(9)]; }', - prefix + ' let p = new(int, 3); if (1) free(p); return p[0]; }', - prefix + ' let p = new(int, 3); while (0) free(p); return p[0]; }', - prefix + ' let p = new(int, 3); let f = fn() { free(p); return 0; }; return p[0]; }', - prefix + ' let p = new(int[?], 0); push(p, 9); return p[0]; }', + prefix + ' let p = rt.new(int, 3); let ignored = 0 && rt.free(p); return p[0]; }', + prefix + ' let p = rt.new(int, 3); let ignored = 1 || rt.free(p); return p[0]; }', + prefix + ' let p = rt.new(int, 50); return p[(u8)300]; }', + prefix + ' let p = rt.new(int, 50); return p[300 as u8]; }', + prefix + ' let p = rt.new(int, 3); let q = p; q[2] = 7; rt.free(p); return 0; }', + prefix + ' let p = rt.new(3); rt.free(p); p = rt.new(3); return p[0]; }', + 'unchecked fn unknown(int* p) {}\n' + prefix + ' let p = rt.new(int, 3); unknown(p); return p[9]; }', + 'fn maybe(int n) { return n; }\n' + prefix + ' let p = rt.new(int, 3); return p[maybe(9)]; }', + prefix + ' let p = rt.new(int, 3); if (1) rt.free(p); return p[0]; }', + prefix + ' let p = rt.new(int, 3); while (0) rt.free(p); return p[0]; }', + prefix + ' let p = rt.new(int, 3); let f = fn() { rt.free(p); return 0; }; return p[0]; }', + prefix + ' let p = rt.new(int[?], 0); push(p, 9); return p[0]; }', ]; for (const source of safe) assert.deepStrictEqual(check(source).warnings, [], source); -const preserved = check('int read(int* p) { return p[0]; }\n' + prefix + ' let p = new(int, 3); read(p); return p[9]; }'); +const preserved = check('int read(int* p) { return p[0]; }\n' + prefix + ' let p = rt.new(int, 3); read(p); return p[9]; }'); assert.match(preserved.warnings[0]?.message || '', /index 9.*length 3/); const unknown = check('int read(int* p) { return p[0]; }\nfn main() { int n = 1; return read(&n); }\n', 'p['); assert.match(unknown.hover, /size: unknown.*lifetime: unknown/); const stack = check('fn main() { let a[3]; return a[2]; }\n', 'a['); assert.match(stack.hover, /size: 3 elements.*lifetime: stack storage/); -const replacement = check('fn main() {\n let owner = new(1), replacement = new(2);\n free(owner); owner = replacement;\n return owner[0];\n}\n', 'replacement;'); +const replacement = check('fn main() {\n let owner = rt.new(1), replacement = rt.new(2);\n rt.free(owner); owner = replacement;\n return owner[0];\n}\n', 'replacement;'); assert.ok(!replacement.diagnostics.some(d => d.severity === 1), JSON.stringify(replacement.diagnostics)); assert.match(replacement.hover, /moved: replaces a released owner in `owner` on line 3/); console.log('ok definite bounds, free and scope lifetimes, alias hovers, and conservative unknown cases'); diff --git a/tests/drivers/thread_context_test.py b/tests/drivers/thread_context_test.py index fa3f144..b237acd 100644 --- a/tests/drivers/thread_context_test.py +++ b/tests/drivers/thread_context_test.py @@ -5,7 +5,7 @@ import argparse import subprocess parser = argparse.ArgumentParser() -parser.add_argument('vm', nargs='?', default='bin/zy') +parser.add_argument('vm', nargs='?', default='build/bin/zy') parser.add_argument('--case', default='') parser.add_argument('--library', default='') parser.add_argument('--backend', choices=('native', 'micro'), default='native') diff --git a/tests/drivers/toolchain_lsp_contract_test.js b/tests/drivers/toolchain_lsp_contract_test.js index 0d1b515..2872282 100644 --- a/tests/drivers/toolchain_lsp_contract_test.js +++ b/tests/drivers/toolchain_lsp_contract_test.js @@ -1,4 +1,6 @@ // pending requirements have explicit protocol oracles, selectable in small groups. +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); @@ -21,7 +23,7 @@ function run(text, requests = [], uri = 'file:///contracts.zc', files = []) { for (const message of messages) proc.stdin.write(frame(message)); output = Buffer.concat(chunks); } else { - const r = cp.spawnSync(host, process.argv.slice(3), { input: Buffer.concat(messages.map(frame)), timeout: 60000, maxBuffer: 32 << 20 }); + const r = cp.spawnSync(host, process.argv.slice(3), { input: Buffer.concat(messages.map(frame)), timeout: 60000 * TIMEOUT_SCALE, maxBuffer: 32 << 20 }); assert.strictEqual(r.status, 0, String(r.stderr)); output = r.stdout; } const replies = []; let data = output; diff --git a/tests/drivers/tools_contract_test.py b/tests/drivers/tools_contract_test.py index df02b2b..6a98678 100644 --- a/tests/drivers/tools_contract_test.py +++ b/tests/drivers/tools_contract_test.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """bounded accept/reject tests for standalone build tools and generated images.""" import argparse -import re import struct import subprocess import tempfile @@ -17,32 +16,33 @@ def command(argv, data=None): return r if args.group == 'image': - source = Path('src/core/genome.h').read_text() - body = source.split('{', 1)[1].split('}', 1)[0] - words = [int(x) for x in re.findall(r'-?\d+', body)] + image = Path('build/core/genome.bin').read_bytes() + words = list(struct.unpack('<%dq' % (len(image) // 8), image)) pack = lambda xs: b''.join(struct.pack('<q', x) for x in xs) - good = command(['bin/mkgenome'], pack(words)) - assert good.returncode == 0 and f'#define GLEN {len(words)}'.encode() in good.stdout - for data in [b'', b'x', pack(words[:6]), pack(words) + b'x']: - r = command(['bin/mkgenome'], data) - assert r.returncode != 0 and b'incomplete' in r.stderr + good = command(['build/bin/mkgenome'], image) + assert good.returncode == 0 and good.stdout == image + for data in [b'', b'x', pack(words[:6]), image + b'x']: + r = command(['build/bin/mkgenome'], data) + assert r.returncode != 0 and b'incomplete' in r.stderr and not r.stdout for index in range(3): changed = words[:]; changed[index] ^= 2 - r = command(['bin/mkgenome'], pack(changed)) - assert r.returncode != 0 and b'not a genome' in r.stderr - print('ok mkgenome roundtrip, truncation, alignment and map-header mutation contracts') + r = command(['build/bin/mkgenome'], pack(changed)) + assert r.returncode != 0 and b'not a genome' in r.stderr and not r.stdout + r = command(['build/bin/mkgenome'], b'zc: line 1: something is wrong\nzc: 1 error\n') + assert r.returncode != 0 and b'something is wrong' in r.stderr and not r.stdout + print('ok mkgenome roundtrip, truncation, alignment, map-header mutation and diagnostic contracts') elif args.group == 'map': for preset in ['default', 'small', 'run', 'lsp', 'fixed']: with tempfile.TemporaryDirectory(prefix='tinyvm preset ') as directory: - r = command(['bin/genmap', '--preset', preset, '-o', directory]) + r = command(['build/bin/genmap', '--preset', preset, '-o', directory]) assert r.returncode == 0, (preset, r.stderr) assert (Path(directory) / 'memmap.h').is_file() - assert command(['bin/genmap', '--preset', 'default', '--check']).returncode == 0 + assert command(['build/bin/genmap', '--preset', 'default', '--check']).returncode == 0 for options in [['--words', '-1'], ['--heap', '0'], ['--data', '0'], ['--symbols', '-1'], ['--total', '999999999999999999999'], ['--preset', 'imaginary']]: - r = command(['bin/genmap', *options, '--check']) + r = command(['build/bin/genmap', *options, '--check']) assert r.returncode != 0, options with tempfile.TemporaryDirectory(prefix='tinyvm map ') as directory: - r = command(['bin/genmap', '--preset', 'small', '--locals', '32', '--symbols', '64', '-o', directory]) + r = command(['build/bin/genmap', '--preset', 'small', '--locals', '32', '--symbols', '64', '-o', directory]) assert r.returncode == 0, r.stderr for name in ['memmap.h', 'rt.zc', 'compiler.zc', 'map.txt']: assert (Path(directory) / name).is_file(), name @@ -50,7 +50,7 @@ elif args.group == 'map': else: r = command(['bash', '-n', 'tinyvm.sh']) assert r.returncode == 0, r.stderr - for script in ['src/tools/automap.sh', 'src/tools/mklsp.sh', 'src/tools/mklspwasm.sh', 'src/tools/mkvsix.sh', 'src/tools/embed.sh']: + for script in ['src/tools/automap.sh', 'src/tools/mklsp.sh', 'src/tools/mklspwasm.sh', 'src/tools/mkvsix.sh', 'src/tools/findcc.sh', 'src/tools/bootstrap-check.sh']: assert command(['bash', '-n', script]).returncode == 0 assert command(['bash', 'tinyvm.sh', 'imaginary-command']).returncode != 0 assert Path('tinyvm.bat').is_file() diff --git a/tests/drivers/wasm_test.js b/tests/drivers/wasm_test.js index e4f7ecb..56f32a2 100644 --- a/tests/drivers/wasm_test.js +++ b/tests/drivers/wasm_test.js @@ -1,9 +1,9 @@ -// runs programs on the webassembly build (src/web/zc.wasm) and compares with the expected output. +// runs programs on the webassembly build (build/web/zc.wasm, from make web) and compares with the expected output. // usage: node tests/drivers/wasm_test.js [path/to/zc.wasm] const fs = require('fs'); const ZC = require('../../src/web/zc.js'); (async () => { - const zc = await ZC.load(fs.readFileSync(process.argv[2] || __dirname + '/../../src/web/zc.wasm')); + const zc = await ZC.load(fs.readFileSync(process.argv[2] || __dirname + '/../../build/web/zc.wasm')); const cases = [ ['examples/hello.zc', 'hello, world!'], ['examples/fact.zc', '1101110101111100000000\n3628800\n'], diff --git a/tests/drivers/word_literals_test.py b/tests/drivers/word_literals_test.py index a2d230d..cfac811 100644 --- a/tests/drivers/word_literals_test.py +++ b/tests/drivers/word_literals_test.py @@ -5,7 +5,7 @@ import subprocess values = [0, 1, (1 << 62) - 1, 1 << 62, (1 << 63) - 1, -(1 << 62), -(1 << 62) - 1, -(1 << 63), -1] source = 'fn main() {\n' + ''.join(f'put_n({value}); put_c(10);\n' for value in values) + 'return 0; }\n' expected = ''.join(str(value) + '\n' for value in values) -for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]: +for command in [['build/bin/zy'], ['build/bin/zym'], ['node', 'src/js/tinyvm.js']]: result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=20) assert result.returncode == 0 and result.stdout == expected, (command, result.stdout, result.stderr) print('ok full-width positive and negative word literals: ' + command[0]) @@ -13,7 +13,7 @@ source = '''immediate fn add_word_operator() { next(); add_binop("**", 11, "4611 add_word_operator fn main() { put_n(0 ** 0); put_c(10); return 0; } ''' -for command in [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]: +for command in [['build/bin/zy'], ['build/bin/zym'], ['node', 'src/js/tinyvm.js']]: result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=20) assert result.returncode == 0 and result.stdout == '4611686018427387904\n', (command, result.stdout, result.stderr) print('ok full-width word in a stored operator sequence: ' + command[0]) diff --git a/tests/test.sh b/tests/test.sh index 16aa1a9..fd2bcd3 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -1,9 +1,14 @@ #!/bin/bash # full check: bootstraps the compiler from zc0, verifies the fixpoint, then runs # the examples and tests on the portable interpreter (host), the microcore, the -# arm64 jit under qemu-user, and the llvm backend. needs gcc. the jit half -# needs aarch64-linux-gnu-gcc and qemu-aarch64 (linux-user, not qemu-system). -# the llvm half needs llvm-config and libLLVM. src/baremetal/run.sh boots the +# arm64 jit under qemu-user, and the llvm backend. needs a c compiler with c23 +# #embed (src/tools/findcc.sh picks one; CC overrides it). the jit half needs +# aarch64-linux-gnu-gcc and qemu-aarch64 (linux-user, not qemu-system). +# +# the suite reads nothing from build/: it lays out the default map, bootstraps +# the compiler and builds every core into its own scratch directory, so it runs +# the same from a clean checkout as after `make`. +# the llvm half needs llvm-config (LLVM_CONFIG names another) and libLLVM. src/baremetal/run.sh boots the # microcore on qemu-system-riscv64 and qemu-system-aarch64 when those cross # compilers and system emulators are installed. each of these is skipped # when its tools are missing. @@ -43,26 +48,36 @@ if [ -n "$SELECT" ]; then fi ROOT=$T -# built here rather than inside a section: interp runs the program list on -# it, and the run-only and bare-metal sections read it as the reference. -gcc -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core -o $T/zy || exit 1 -gcc -O2 -w -fwrapv src/tools/genmap.c -Isrc/core -Ibuild/core -o $T/genmap || exit 1 +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 +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 +$T/genmap --preset default --check -o $T/map >$T/genmap.log 2>&1 || { cat $T/genmap.log; echo "FAIL the default map does not match the checked-in sources"; exit 1; } +MAP=$T/map +$CC -O1 -w -fwrapv src/tools/zc0.c -Isrc/core -I$MAP -o $T/zc0 && $CC -O1 -w -fwrapv src/tools/mkgenome.c -I$MAP -o $T/mkg || exit 1 # the c helper for tests/programs/structval.zc: structs by value, loaded with cload -mkdir -p build && gcc -shared -fPIC -w tests/data/sbv.c -o build/sbv.so || exit 1 +mkdir -p build && $CC -shared -fPIC -w tests/data/sbv.c -o build/sbv.so || exit 1 selfc(){ (cat src/tools/build.zc; printf '`'; src) | $1 > $2; } -# bootstrap: zc0 (c) -> g0, g0 compiles compiler.zc -> g1, g1 compiles it -> g2 -gcc -O1 -w -fwrapv src/tools/zc0.c -Isrc/core -Ibuild/core -o $T/zc0 && gcc -O1 -w -fwrapv src/tools/mkgenome.c -Isrc/core -Ibuild/core -o $T/mkg || exit 1 +# bootstrap: zc0 (c) -> g0, g0 compiles compiler.zc -> g1, g1 compiles it -> g2. +# each stage's core embeds the image in its own directory mkdir $T/s0 $T/s1 -cp src/core/core.c src/core/io_linux.c src/core/memmap.h src/core/fops.h src/core/bops.h src/core/stack.h $T/s0/; cp src/core/core.c src/core/io_linux.c src/core/memmap.h src/core/fops.h src/core/bops.h src/core/stack.h $T/s1/ -src | $T/zc0 > $T/s0/genome.h || exit 1 -gcc -pthread -O2 -w -fwrapv -DJTYPE=int $T/s0/core.c $T/s0/io_linux.c -o $T/zy0 -selfc $T/zy0 $T/g1.bin; $T/mkg < $T/g1.bin > $T/s1/genome.h -gcc -pthread -O2 -w -fwrapv -DJTYPE=int $T/s1/core.c $T/s1/io_linux.c -o $T/zy1 +src | $T/zc0 > $T/s0/g0.bin && $T/mkg < $T/s0/g0.bin > $T/s0/genome.bin || exit 1 +$CC -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -I$MAP --embed-dir=$T/s0 -o $T/zy0 || exit 1 +selfc $T/zy0 $T/g1.bin; $T/mkg < $T/g1.bin > $T/s1/genome.bin || exit 1 +$CC -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -I$MAP --embed-dir=$T/s1 -o $T/zy1 || exit 1 selfc $T/zy1 $T/g2.bin cmp -s $T/g1.bin $T/g2.bin && chk 1 1 "fixpoint: the compiler compiles itself to the same $(($(wc -c < $T/g1.bin) / 8)) words" || chk 0 1 "fixpoint" -cmp -s $T/s1/genome.h build/core/genome.h && chk 1 1 "genome.h is up to date" || chk 0 1 "genome.h is stale (run make genome)" +# every core below carries the fixpoint genome: GI is the include and embed path for it +GI="-Isrc/core -I$MAP --embed-dir=$T/s1" +# built here rather than inside a section: interp runs the program list on +# it, and the run-only and bare-metal sections read it as the reference. +$CC -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c $GI -o $T/zy || exit 1 chk "$($T/zc0 < tests/programs/zc0typed.zc | md5sum)" "$($T/zc0 < tests/programs/zc0plain.zc | md5sum)" "zc0 reads types and throws them away: typed and plain sources give one image" for n in $SECS; do @@ -80,7 +95,7 @@ run(){ # $1 = vm command, $2 = label chk "$( (cat tests/programs/backtick.zc; printf '`xyz') | $vm)" "3 3 96 39 xyz" "$l a backtick in a comment, string or char literal does not end the source" chk "$($vm < examples/selfpatch.zc)" "ABC" "$l self-patching code" chk "$($vm < examples/lists.zc | tr '\n' '/')" "[0 1 4 9 16 25]/live: 7/after the block: 0/" "$l ownership frees everything" - chk "$( (cat examples/repl.zc; printf '`fn sq(x) { return x * x; }`put_n(sq(7)); put_c(10);`put_s("hi\\n");') | $vm | tr '\n' '/')" "49/hi/" "$l repl on the resident compiler" + chk "$( (cat examples/repl.zc; printf '`fn sq(x) { return x * x; }`rt.put_n(sq(7)); rt.put_c(10);`rt.put_s("hi\\n");') | $vm | tr '\n' '/')" "49/hi/" "$l repl on the resident compiler" chk "$($vm < examples/typed.zc | tr '\n' '/')" "last: 4/point is 2 words: x int, y int/vec<point*> is 3 words: len int, cap int, items point**/push is bool(vec<T>*, T.d)/the heap says v is a vec<point*>, and v.items[0] is a point/" "$l typed example" chk "$($vm < examples/wiring.zc)" "16 1234 25 4/9" "$l enhancement example" chk "$($vm < tests/programs/allocfast.zc)" "0 0 499500 0" "$l constant-size allocation fast path" @@ -111,7 +126,7 @@ run(){ # $1 = vm command, $2 = label chk "$($vm < tests/programs/enumscope.zc)" "0 5 6 blue" "$l enum members are reached as Enum.MEMBER, also in a case" chk "$($vm < tests/programs/enumtype.zc)" "6 5 1" "$l typed enums: members take their enum, an enum becomes an int" chk "$($vm < tests/programs/tagenum.zc)" "27 20 0 0 1 2" "$l tagged enums: members carry fields, constructors build them, a case binds the fields" - chk "$($vm < tests/programs/tagenum_warn.zc)" "$(printf 'zc: line 5: warning: this switch doesn'"'"'t cover rect\n0')" "$l a switch on a tagged enum that misses a member warns, on the switch's line" + chk "$($vm < tests/programs/tagenum_warn.zc)" "$(printf 'zc: line 8: warning: this switch doesn'"'"'t cover rect\n0')" "$l a switch on a tagged enum that misses a member warns, on the switch's line" chk "$($vm < tests/programs/tagenum_two.zc)" "3 8 12" "$l two tagged enums with the same member name, and a case in an o3 loop" chk "$($vm < tests/programs/o3push.zc | md5sum)" "$(md5sum < tests/programs/o3push.expected)" "$l push and pop of a growable array are inline in o3" chk "$($vm < tests/programs/o3copy.zc)" "520507 1005" "$l o3 copies of a loop variable read the source instead" @@ -136,11 +151,11 @@ hello world!42 1 0" "$l capture.start() collects output and end() hands back the string" chk "$($vm < tests/programs/sys.zc)" "2 zy time" "$l syscalls: write, read, time" - chk "$($vm < tests/programs/memleak.zc)" $'leak: line 5 3 words\n1' "$l memdebug names a live allocation" - chk "$($vm < tests/programs/uaf.zc)" $'zc: line 7: warning: use after free: this storage\'s lifetime ended on line 6\nzy: use after free from line 4' "$l memdebug: use after free" - chk "$($vm < tests/programs/uaf_in.zc)" "zy: use after free from line 4" "$l memdebug: use after free through an interior pointer" - chk "$($vm < tests/programs/doublefree.zc)" $'zy: double free from line 4\nok' "$l memdebug: double free is reported and does nothing" - chk "$($vm < tests/programs/scrub.zc)" $'zc: line 6: warning: use after free: this storage\'s lifetime ended on line 5\n57005' "$l memdebug scrub poisons a freed payload" + chk "$($vm < tests/programs/memleak.zc)" $'leak: line 8 3 words\n1' "$l memdebug names a live allocation" + chk "$($vm < tests/programs/uaf.zc)" $'zc: line 10: warning: use after free: this storage\'s lifetime ended on line 9\nzy: use after free from line 7' "$l memdebug: use after free" + chk "$($vm < tests/programs/uaf_in.zc)" "zy: use after free from line 7" "$l memdebug: use after free through an interior pointer" + chk "$($vm < tests/programs/doublefree.zc)" $'zy: double free from line 7\nok' "$l memdebug: double free is reported and does nothing" + chk "$($vm < tests/programs/scrub.zc)" $'zc: line 9: warning: use after free: this storage\'s lifetime ended on line 8\n57005' "$l memdebug scrub poisons a freed payload" chk "$($vm < tests/programs/interior.zc)" "0" "$l an interior pointer keeps a quarantined block" chk "$($vm < tests/programs/wide.zc)" "1099511627776 1 1 1 -1" "$l 64-bit shifts and equality" chk "$($vm < tests/programs/float.zc)" "4.000000 4.500000 3.000000 2.500000 2.500000 0.001500 0.100000 -0.125000 inf -inf nan 1 1 1 0 0 3 -3 3.000000 -2.000000 2" "$l floating point" @@ -169,7 +184,7 @@ ABC" "$l o3 lifts byte loads and stores" chk "$($vm < examples/bf.zc 2>&1 | md5sum | cut -c1-32)" "223570983752b17f7ece32e93d5e19e4" "$l example: brainfuck compiled at runtime" chk "$($vm < examples/dump.zc 2>&1 | md5sum | cut -c1-32)" "757774b00f6b3cd0bfbcba66448a8619" "$l example: struct printer through reflection" chk "$($vm < examples/tree.zc 2>&1 | md5sum | cut -c1-32)" "b86a2e4e9e31acc294a4ddf86be863f2" "$l example: owning tree frees itself" - chk "$($vm < examples/lang.zc 2>&1 | md5sum | cut -c1-32)" "6b234dc8a0a1aa0fb02eb55a6ff7ccc0" "$l example: pipe operator, unless, assert, times" + chk "$($vm < examples/lang.zc 2>&1 | md5sum | cut -c1-32)" "$(printf '22\none isn'"'"'t bigger than two\n***\nassertion failed on line 50\n' | md5sum | cut -c1-32)" "$l example: pipe operator, unless, assert, times" chk "$($vm < examples/staged.zc 2>&1 | md5sum | cut -c1-32)" "6f790a125c48b0cc7a897cee73ee5627" "$l example: staged code compiled while running" chk "$($vm < examples/quine.zc 2>&1 | md5sum | cut -c1-32)" "$(md5sum < examples/quine.zc | cut -c1-32)" "$l example: quine" chk "$($vm < examples/primes.zc 2>&1 | md5sum | cut -c1-32)" "814faffc70dad0ae520c0671c33e73d4" "$l example: o3 prime sieve" @@ -200,7 +215,7 @@ ABC" "$l o3 lifts byte loads and stores" } s_interp() { T=$ROOT/interp - gcc -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core -o $T/zy + $CC -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c $GI -o $T/zy run $T/zy "interp" python3 tests/drivers/array_methods_test.py $T/zy > $T/arrays.out 2>&1; r=$? grep -v '^ok' $T/arrays.out @@ -218,14 +233,14 @@ s_interp() { chk "$($T/zy < tests/programs/infer_return.zc)" "3" "an owned return with no .u is inferred from the body" chk "$($T/zy < tests/programs/discard_m.zc)" "7" "m* discards an unnamed u result" chk "$($T/zy < tests/programs/closure.zc | tr '\n' ' ')" "31 35 42 2 7 7 109 30 7 16 33 20 101 " "closures: calls through values, captures, nesting, returns" - chk "$($T/zy < tests/programs/decl_undefined.zc 2>&1 | head -1)" "zc: line 1: helper is used but never defined" "a declared function nothing defines fails a build" + chk "$($T/zy < tests/programs/decl_undefined.zc 2>&1 | head -1)" "zc: line 3: helper is used but never defined" "a declared function nothing defines fails a build" if [ "$(uname -s)" = Linux ]; then chk "$($T/zy < tests/programs/rawsys.zc)" "-38" "Host.raw: an unknown syscall number gives -ENOSYS"; else chk "$($T/zy < tests/programs/rawsys.zc)" "-1" "Host.raw: unavailable on this host"; fi if [ "$(uname -m)" = x86_64 ]; then chk "$($T/zy < tests/programs/rawpid.zc)" "1" "Host.raw: getpid (39 on x86-64) answers"; fi } s_micro() { T=$ROOT/micro - gcc -pthread -DZY_THREADS -O2 -w -fwrapv src/micro/micro.c src/micro/port_linux.c -Isrc/core -Isrc/micro -o $T/zym || exit 1 + $CC -pthread -DZY_THREADS -O2 -w -fwrapv src/micro/micro.c src/micro/port_linux.c $GI -Isrc/micro -o $T/zym || exit 1 run $T/zym "micro" python3 tests/drivers/array_methods_test.py $T/zym > $T/arrays.out 2>&1; r=$? grep -v '^ok' $T/arrays.out @@ -233,8 +248,9 @@ s_micro() { python3 tests/drivers/actor_test.py $T/zym > $T/actor.out 2>&1; r=$? grep -v '^ok' $T/actor.out chk $r 0 "micro isolated actor acceptance, rejection and regression fixtures" - if command -v node >/dev/null; then - python3 tests/drivers/fuzz_compiler.py $ROOT/zy $T/zym --js > $T/fuzz.out 2>&1; r=$? + if command -v node >/dev/null && command -v tsc >/dev/null && node src/js/mklayout.js --map $MAP --out $T/jsvm >/dev/null \ + && cp $ROOT/s1/genome.bin $T/jsvm/ && tsc src/js/tinyvm.ts --target es2020 --lib es2020,dom --module commonjs --outDir $T/jsvm; then + python3 tests/drivers/fuzz_compiler.py $ROOT/zy $T/zym --js $T/jsvm/tinyvm.js > $T/fuzz.out 2>&1; r=$? else python3 tests/drivers/fuzz_compiler.py $ROOT/zy $T/zym > $T/fuzz.out 2>&1; r=$? fi @@ -245,8 +261,8 @@ s_micro() { chk $r 0 "malformed compiler input fuzz and host agreement" selfc $T/zym $T/gm.bin cmp -s $T/g1.bin $T/gm.bin && chk 1 1 "microcore self-compile matches" || chk 0 1 "microcore self-compile matches" - gcc -ffreestanding -w -c src/micro/micro.c -Isrc/core -Isrc/micro -o $T/micro.o && gcc -ffreestanding -w -c src/micro/port_stub.c -Isrc/core -Isrc/micro -o $T/stub.o && chk 1 1 "microcore and the stub port compile freestanding" || chk 0 1 "microcore and the stub port compile freestanding" - gcc -pthread -O2 -w -fwrapv -DJTYPE=int -DZY_SOFTFLOAT -DZYINTERP src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core -o $T/zysoft || exit 1 + $CC -ffreestanding -w -c src/micro/micro.c $GI -Isrc/micro -o $T/micro.o && $CC -ffreestanding -w -c src/micro/port_stub.c $GI -Isrc/micro -o $T/stub.o && chk 1 1 "microcore and the stub port compile freestanding" || chk 0 1 "microcore and the stub port compile freestanding" + $CC -pthread -O2 -w -fwrapv -DJTYPE=int -DZY_SOFTFLOAT -DZYINTERP src/core/core.c src/core/io_linux.c $GI -o $T/zysoft || exit 1 chk "$($T/zysoft < tests/programs/float.zc)" "4.000000 4.500000 3.000000 2.500000 2.500000 0.001500 0.100000 -0.125000 inf -inf nan 1 1 1 0 0 3 -3 3.000000 -2.000000 2" "soft-float fallback matches the hardware line" chk "$($T/zysoft < tests/programs/ftruth.zc)" "00111000110107" "soft-float negative zero is false" chk "$($T/zysoft < tests/programs/fo3.zc)" "5997 5997 4.000000 nan 0 3.250000 1 0 0" "soft-float o3 float ops match" @@ -256,7 +272,7 @@ s_micro() { s_small() { T=$ROOT/small # smaller machines. addresses are literals, so each size is its own genome. - gcc -O2 -w -fwrapv src/tools/genmap.c -Isrc/core -Ibuild/core -o $T/genmap || exit 1 + $CC -O2 -w -fwrapv src/tools/genmap.c $GI -o $T/genmap || exit 1 $T/genmap --preset default --check >/dev/null && chk 1 1 "default map matches the checked-in sources" || chk 0 1 "default map matches the checked-in sources" $T/genmap --preset small -o $T/small || exit 1 $T/genmap --mode compiler --name small --heap-lo 8192 --heap 20480 --types 8192 \ @@ -265,13 +281,13 @@ s_small() { cmp -s $T/small/memmap.h $T/small-flags/memmap.h && cmp -s $T/small/rt.zc $T/small-flags/rt.zc \ && cmp -s $T/small/compiler.zc $T/small-flags/compiler.zc && cmp -s $T/small/map.txt $T/small-flags/map.txt \ && chk 1 1 "compiler flags reproduce the small map" || chk 0 1 "compiler flags reproduce the small map" - cp src/tools/zc0.c src/core/fops.h src/core/bops.h src/core/stack.h $T/small/ - gcc -O1 -w -fwrapv $T/small/zc0.c -o $T/zc0s || exit 1 - cat $T/small/rt.zc $T/small/compiler.zc | $T/zc0s > $T/small/genome.h || exit 1 - cp src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/micro/port_stub.c src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h $T/small/ - gcc -pthread -DZY_THREADS -O2 -w -fwrapv $T/small/micro.c $T/small/port_linux.c -o $T/zyms || exit 1 - gcc -pthread -O2 -w -fwrapv -DJTYPE=int $T/small/core.c $T/small/io_linux.c -o $T/zys || exit 1 - gcc -ffreestanding -w -c $T/small/micro.c -o $T/micro_s.o && gcc -ffreestanding -w -c $T/small/port_stub.c -o $T/stub_s.o && chk 1 1 "small microcore compiles freestanding" || chk 0 1 "small microcore compiles freestanding" + cp src/tools/zc0.c src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h $T/small/ + $CC -O1 -w -fwrapv $T/small/zc0.c -o $T/zc0s || exit 1 + cat $T/small/rt.zc src/core/core.zc $T/small/compiler.zc | $T/zc0s > $T/small/genome.bin || exit 1 + cp src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/micro/port_stub.c src/core/core.c src/core/io_linux.c src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h $T/small/ + $CC -pthread -DZY_THREADS -O2 -w -fwrapv $T/small/micro.c $T/small/port_linux.c -o $T/zyms || exit 1 + $CC -pthread -O2 -w -fwrapv -DJTYPE=int $T/small/core.c $T/small/io_linux.c -o $T/zys || exit 1 + $CC -ffreestanding -w -c $T/small/micro.c -o $T/micro_s.o && $CC -ffreestanding -w -c $T/small/port_stub.c -o $T/stub_s.o && chk 1 1 "small microcore compiles freestanding" || chk 0 1 "small microcore compiles freestanding" sameout() { # $1 = other vm, $2 = label, $3 = source file local a b a=$($T/zy < "$3" 2>&1 | tr '\n' '/') @@ -309,15 +325,15 @@ s_runonly() { # its own run map: the run-only image is built against this one, and the # small-machine section lays out its own rather than sharing this $T/genmap --preset run -o $T/run || exit 1 - cp src/tools/zc0.c src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/micro/port_stub.c src/core/fops.h src/core/bops.h src/core/stack.h $T/run/ - gcc -O1 -w -fwrapv $T/run/zc0.c -o $T/zc0r || exit 1 + cp src/tools/zc0.c src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/micro/port_stub.c src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h $T/run/ + $CC -O1 -w -fwrapv $T/run/zc0.c -o $T/zc0r || exit 1 runonly() { # $1 label, $2 source. the program is already in the image, so it reads no source. # the map is sized to the program: src/tools/automap.sh lays it out for the image's own length local a b d=$T/run-$1 input=$2 if [ "$1" = gc ]; then input=$T/collector-progress.zc; sed 's/f >= 990/f > 0/' "$2" > "$input"; fi GENMAP=$T/genmap bash src/tools/automap.sh run $d src/runtime/run.zc "$input" >/dev/null || { chk 0 1 "run $1 builds"; return; } - cp src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/micro/port_stub.c src/core/fops.h src/core/bops.h src/core/stack.h $d/ - gcc -pthread -DZY_THREADS -O2 -w -fwrapv $d/micro.c $d/port_linux.c -o $T/zymr || { chk 0 1 "run $1 links"; return; } + cp src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/micro/port_stub.c src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h $d/ + $CC -pthread -DZY_THREADS -O2 -w -fwrapv $d/micro.c $d/port_linux.c -o $T/zymr || { chk 0 1 "run $1 links"; return; } a=$($T/zy < "$input" 2>&1 | tr '\n' '/') b=$($T/zymr </dev/null 2>&1 | tr '\n' '/') chk "$b" "$a" "run $1" @@ -334,12 +350,12 @@ s_runonly() { runonly strings tests/programs/strings.zc runonly strings-ex examples/strings.zc runonly share tests/programs/share.zc - d=$T/run-echo; GENMAP=$T/genmap bash src/tools/automap.sh run $d src/runtime/run.zc examples/echo.zc >/dev/null && cp src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/micro/port_stub.c src/core/fops.h src/core/bops.h src/core/stack.h $d/ \ - && gcc -pthread -DZY_THREADS -O2 -w -fwrapv $d/micro.c $d/port_linux.c -o $T/zymr \ + d=$T/run-echo; GENMAP=$T/genmap bash src/tools/automap.sh run $d src/runtime/run.zc examples/echo.zc >/dev/null && cp src/micro/micro.c src/micro/micro_port.h src/micro/port_linux.c src/micro/port_stub.c src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h $d/ \ + && $CC -pthread -DZY_THREADS -O2 -w -fwrapv $d/micro.c $d/port_linux.c -o $T/zymr \ && chk "$(printf 'abc\n' | $T/zymr | tr '\n' '/')" "$( (cat examples/echo.zc; printf '`abc\n') | $T/zy | tr '\n' '/')" "run echo" \ || chk 0 1 "run echo" # zcc: a program becomes a static binary that runs without stdin and with a held-open pipe - if CC=gcc src/tools/zcc -o $T/zcc-hello examples/hello.zc >/dev/null 2>&1; then + if src/tools/zcc -o $T/zcc-hello examples/hello.zc >/dev/null 2>&1; then chk "$($T/zcc-hello < /dev/null)" "hello, world!" "zcc: hello, static" chk "$(sleep 2 | timeout 5 $T/zcc-hello; echo $?)" "hello, world! 0" "zcc: a program that never reads stdin does not wait for it" @@ -348,7 +364,7 @@ s_runonly() { printf 'fn main() {\n\tput_n(1\n\treturn 0;\n}\n' > $T/zcc-bad.zc src/tools/zcc -o $T/zcc-bad $T/zcc-bad.zc </dev/null >/dev/null 2>&1; r=$? chk "$r $( [ -e $T/zcc-bad ] && echo yes || echo no)" "1 no" "zcc: a bad program exits 1 and writes no binary" - gcc -ffreestanding -w -c $T/run-echo/micro.c -o $T/micro_r.o && gcc -ffreestanding -w -c $T/run-echo/port_stub.c -o $T/stub_r.o && chk 1 1 "run-only microcore compiles freestanding" || chk 0 1 "run-only microcore compiles freestanding" + $CC -ffreestanding -w -c $T/run-echo/micro.c -o $T/micro_r.o && $CC -ffreestanding -w -c $T/run-echo/port_stub.c -o $T/stub_r.o && chk 1 1 "run-only microcore compiles freestanding" || chk 0 1 "run-only microcore compiles freestanding" } s_bare() { @@ -427,13 +443,13 @@ s_bare() { s_jit() { T=$ROOT/jit if command -v aarch64-linux-gnu-gcc >/dev/null && command -v qemu-aarch64 >/dev/null; then - aarch64-linux-gnu-gcc -pthread -O2 -w -fwrapv -DJTYPE=int -static src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core -o $T/zyj || exit 1 + aarch64-linux-gnu-gcc -pthread -O2 -w -fwrapv -DJTYPE=int -static src/core/core.c src/core/io_linux.c $GI -o $T/zyj || exit 1 printf '#!/bin/sh\nexec qemu-aarch64 %s "$@"\n' $T/zyj > $T/jit; chmod +x $T/jit run $T/jit "jit" chk "$(ZYTRACE=1 $T/jit < tests/programs/o3.zc 2>&1 | grep -c 'bytes of arm64')" "8" "jit compiles every o3 region to arm64" selfc $T/jit $T/gj.bin; cmp -s $T/g1.bin $T/gj.bin && chk 1 1 "jit self-compile matches" || chk 0 1 "jit self-compile matches" # native calls need a dynamic binary (a static one has no dlsym), run against the aarch64 libc - if [ -d /usr/aarch64-linux-gnu/lib ] && aarch64-linux-gnu-gcc -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core -o $T/zyjd -ldl 2>/dev/null; then + if [ -d /usr/aarch64-linux-gnu/lib ] && aarch64-linux-gnu-gcc -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c $GI -o $T/zyjd -ldl 2>/dev/null; then chk "$(qemu-aarch64 -L /usr/aarch64-linux-gnu $T/zyjd < tests/programs/cabi.zc | md5sum)" "$(md5sum < tests/programs/cabi.expected)" "jit native calls and callbacks (extern, cfunc)" chk "$(qemu-aarch64 -L /usr/aarch64-linux-gnu $T/zyjd < tests/programs/cabi_o3.zc | md5sum)" "$(md5sum < tests/programs/cabi_o3.expected)" "jit native calls from o3 code, stack arguments" chk "$(qemu-aarch64 -L /usr/aarch64-linux-gnu $T/zyjd < tests/programs/cabi_sized.zc | md5sum)" "$(md5sum < tests/programs/cabi_sized.expected)" "jit native calls with f32 and the sized integers" @@ -449,7 +465,9 @@ s_llvm() { T=$ROOT/llvm # llvm-config can exist without the headers and static libs (a runtime-only install), so the gate is a real build llvm_ok=0 - if command -v llvm-config >/dev/null && gcc -pthread -O2 -w -fwrapv -DJTYPE=int -DZYLLVM $(llvm-config --cflags) src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core -o $T/zyl $(llvm-config --ldflags --libs) 2>/dev/null; then llvm_ok=1; fi + # LLVM_CONFIG picks the llvm build when the one on PATH has no headers (e.g. /usr/lib/llvm-20/bin/llvm-config) + local lc=${LLVM_CONFIG:-llvm-config} + if command -v $lc >/dev/null && $CC -pthread -O2 -w -fwrapv -DJTYPE=int -DZYLLVM $($lc --cflags) src/core/core.c src/core/io_linux.c $GI -o $T/zyl $($lc --ldflags --libs) 2>/dev/null; then llvm_ok=1; fi if [ $llvm_ok = 1 ]; then run $T/zyl "llvm" chk "$(ZYTRACE=1 $T/zyl < tests/programs/o3.zc 2>&1 | grep -c 'compiled by llvm')" "8" "llvm compiles every o3 region" @@ -558,11 +576,8 @@ s_lsp() { node tests/drivers/diagcache_test.js > $T/dcache.out 2>&1; r=$? grep -v '^ok' $T/dcache.out chk $r 0 "lsp: the diagnostics cache keys and store ($(grep -c '^ok' $T/dcache.out) checks)" - node tests/drivers/ext_cache_test.js > $T/ecache.out 2>&1; r=$? - grep -v '^ok' $T/ecache.out - chk $r 0 "lsp: the extension's cache, end to end ($(grep -c '^ok' $T/ecache.out) checks)" # the same server as WebAssembly, through the extension's host (needs clang) - if command -v clang >/dev/null && clang --print-targets 2>/dev/null | grep -q wasm32 && LSP_BUILD_DIR=$T/lsp LSP_REUSE_BUILD=1 bash src/tools/mklspwasm.sh $T/lsp/lsp.wasm >/dev/null 2>&1; then + if sh src/tools/findcc.sh --wasm >/dev/null 2>&1 && LSP_BUILD_DIR=$T/lsp LSP_REUSE_BUILD=1 bash src/tools/mklspwasm.sh $T/lsp/lsp.wasm >/dev/null 2>&1; then node tests/drivers/hover_surface_test.js --wasm $T/lsp/lsp.wasm > $T/hoversurfacewasm.out 2>&1; r=$? grep -v '^ok' $T/hoversurfacewasm.out chk $r 0 "lsp: embedded and asm hovers through the wasm host" @@ -575,7 +590,8 @@ s_lsp() { else echo "skip lsp wasm tests (no clang with wasm32)"; fi # the same server run by the javascript vm, the extension's fallback when wasm is not there. # the vm starts at the image's heap and grows on the heap's request, so a long document is the test - node src/js/extract_lsp.js >/dev/null && mkdir -p $T/vmserver && cp build/js/tinyvm.js build/js/layout.js build/js/lsp.json $T/vmserver/ + node src/js/extract_lsp.js --lsp $T/lsp --out $T/vmserver >/dev/null && node src/js/mklayout.js --map $MAP --out $T/vmserver >/dev/null \ + && tsc src/js/tinyvm.ts --target es2020 --lib es2020,dom --module commonjs --outDir $T/vmserver node tests/drivers/hover_surface_test.js --vm $T/vmserver > $T/hoversurfacevm.out 2>&1; r=$? grep -v '^ok' $T/hoversurfacevm.out chk $r 0 "lsp: embedded and asm hovers through the javascript host" @@ -590,6 +606,12 @@ s_lsp() { grep -v '^ok' $T/streamhost.out chk $r 0 "lsp: the wasm and vm servers stream and drop stale checks" fi + # the extension's client end to end, with the server files the package would carry + mkdir -p $T/extserver && cp $T/vmserver/tinyvm.js $T/vmserver/layout.js $T/vmserver/lsp.json $T/vmserver/lsp.bin $T/extserver/ + [ -f $T/lsp/lsp.wasm ] && cp $T/lsp/lsp.wasm $T/extserver/ + node tests/drivers/ext_cache_test.js $T/extserver > $T/ecache.out 2>&1; r=$? + grep -v '^ok' $T/ecache.out + chk $r 0 "lsp: the extension's cache, end to end ($(grep -c '^ok' $T/ecache.out) checks)" else echo "skip lsp protocol tests (no node)"; fi else chk 0 1 "lsp server builds"; fi } @@ -597,36 +619,39 @@ s_lsp() { s_js() { T=$ROOT/js # the javascript vm, and the microcore as webassembly. both run the same genome - if command -v node >/dev/null; then - chk "$(node -e "console.log(require('./build/js/genome.json').glen)")" "$(grep -m1 '#define GLEN' build/core/genome.h | awk '{print $3}')" "build/js/genome.json is up to date" - chk "$(node build/js/tinyvm.js examples/hello.zc)" "hello, world!" "js vm: hello" - chk "$(node build/js/tinyvm.js examples/fact.zc | tail -1)" "3628800" "js vm: factorial" - chk "$(node build/js/tinyvm.js examples/primes.zc | tail -1)" "widest gap: 72, after 31397" "js vm: primes" - chk "$(node build/js/tinyvm.js tests/programs/growone.zc)" "12 1" "js vm: a block past the initial heap grows the machine" - chk "$(node build/js/tinyvm.js tests/programs/replace_owner.zc)" "$(cat tests/programs/replace_owner.expected)" "js vm: replacement ownership and cleanup" - if command -v clang >/dev/null && command -v wasm-ld >/dev/null && clang --print-targets 2>/dev/null | grep -q wasm32; then - bash src/web/build.sh $T/zc.wasm >/dev/null && node tests/drivers/wasm_test.js $T/zc.wasm > $T/wasm.out 2>&1; r=$? + if command -v node >/dev/null && command -v tsc >/dev/null; then + # the vm as make js builds it, with the fixpoint genome beside it + node src/js/mklayout.js --map $MAP --out $T/jsvm >/dev/null && cp $ROOT/s1/genome.bin $T/jsvm/genome.bin \ + && tsc src/js/tinyvm.ts --target es2020 --lib es2020,dom --module commonjs --outDir $T/jsvm || chk 0 1 "js vm builds" + chk "$(node $T/jsvm/tinyvm.js examples/hello.zc)" "hello, world!" "js vm: hello" + chk "$(node $T/jsvm/tinyvm.js examples/fact.zc | tail -1)" "3628800" "js vm: factorial" + chk "$(node $T/jsvm/tinyvm.js examples/primes.zc | tail -1)" "widest gap: 72, after 31397" "js vm: primes" + chk "$(node $T/jsvm/tinyvm.js tests/programs/growone.zc)" "12 1" "js vm: a block past the initial heap grows the machine" + chk "$(node $T/jsvm/tinyvm.js tests/programs/replace_owner.zc)" "$(cat tests/programs/replace_owner.expected)" "js vm: replacement ownership and cleanup" + if sh src/tools/findcc.sh --wasm >/dev/null 2>&1 && command -v wasm-ld >/dev/null; then + GENOME_DIR=$ROOT/s1 MAP_DIR=$MAP bash src/web/build.sh $T/zc.wasm >/dev/null && node tests/drivers/wasm_test.js $T/zc.wasm > $T/wasm.out 2>&1; r=$? grep -v '^ok' $T/wasm.out chk $r 0 "wasm: the microcore in webassembly ($(grep -c '^ok' $T/wasm.out) checks)" node tests/drivers/fuzz_wasm.js $T/zc.wasm > $T/fuzzw.out 2>&1; r=$? grep -v '^ok' $T/fuzzw.out chk $r 0 "wasm: compiler/runtime/stdlib differential fuzz" - else echo "skip wasm (no clang with the wasm32 target, or no wasm-ld)"; fi - else echo "skip js and wasm (no node)"; fi + else echo "skip wasm (no clang with the wasm32 target and #embed, or no wasm-ld)"; fi + else echo "skip js and wasm (no node or tsc)"; fi } s_make() { T=$ROOT/make # the makefile builds the programs and the run-only image tool if command -v make >/dev/null; then - make -s BIN=$T/mbin B=$T/mbuild all >/dev/null 2>&1 && chk "$($T/mbin/zy < examples/hello.zc)" "hello, world!" "make: bin/zy runs a program" || chk 0 1 "make: all builds" - python3 tests/drivers/cli_help_test.py --group binaries --bin-dir $T/mbin > $T/help.out 2>&1; r=$? + # from an empty build directory, as a fresh checkout would be + make -s B=$T/mbuild all >/dev/null 2>&1 && chk "$($T/mbuild/bin/zy < examples/hello.zc)" "hello, world!" "make: build/bin/zy runs a program" || chk 0 1 "make: all builds" + python3 tests/drivers/cli_help_test.py --group binaries --bin-dir $T/mbuild/bin > $T/help.out 2>&1; r=$? grep -v '^ok' $T/help.out chk $r 0 "command-line help, invalid options and existing lsp source modes" python3 tests/drivers/cli_help_test.py --group scripts > $T/scripthelp.out 2>&1; r=$? grep -v '^ok' $T/scripthelp.out chk $r 0 "build-script help exits without reading input or starting a build" - make -s BIN=$T/mbin B=$T/mbuild run PROG=examples/hello.zc >/dev/null 2>&1 && chk "$($T/mbuild/run/zymr </dev/null)" "hello, world!" "make: run PROG= builds a run-only image" || chk 0 1 "make: run builds" + make -s B=$T/mbuild run PROG=examples/hello.zc >/dev/null 2>&1 && chk "$($T/mbuild/run/zymr </dev/null)" "hello, world!" "make: run PROG= builds a run-only image" || chk 0 1 "make: run builds" else echo "skip make (not installed)"; fi } @@ -634,7 +659,7 @@ s_make() { # every block in it is one of those examples (tests/drivers/learn.py) s_learn() { T=$ROOT/learn - gcc -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -Ibuild/core -o $T/zy + $CC -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c $GI -o $T/zy # learn.py prints its own ok and FAIL lines, which the tally below counts python3 tests/drivers/learn.py $T/zy; true } diff --git a/tests/test_win.ps1 b/tests/test_win.ps1 index 0edc86c..953e875 100644 --- a/tests/test_win.ps1 +++ b/tests/test_win.ps1 @@ -1,17 +1,29 @@ # TinyVM Windows test suite (PowerShell implementation of test.sh) $ErrorActionPreference = "Stop" -# detect compiler (prioritizing LLVM-MinGW with UCRT headers) +# the suite reads nothing from build\: it lays out the default map, bootstraps the +# compiler and builds every executable in its own temporary directory. the cores +# take their genome in with c23 #embed, so the compiler must be clang 19 or gcc 15. +$tempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "tinyvm_test_" + [System.Guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Path "$tempDir\s0", "$tempDir\s1", "$tempDir\probe" -Force | Out-Null +[IO.File]::WriteAllText("$tempDir\probe\probe.bin", "ab") +[IO.File]::WriteAllText("$tempDir\probe\probe.c", "static const unsigned char probe[] = {`n#embed `"probe.bin`"`n};`nint main(void) { return sizeof probe == 2 ? 0 : 1; }`n") +function Test-Embed($compiler) { + & $compiler -w "--embed-dir=$tempDir\probe" "$tempDir\probe\probe.c" -o "$tempDir\probe\probe.exe" 2>$null | Out-Null + return $LASTEXITCODE -eq 0 +} +# detect a compiler with #embed (CC first, then LLVM-MinGW with UCRT headers, then PATH) +$cc = $null +$candidates = @() +if ($env:CC) { $candidates += $env:CC } $llvmMinGW = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages\MartinStorsjo.LLVM-MinGW*\llvm-mingw*\bin" -ErrorAction SilentlyContinue -if ($llvmMinGW) { - $env:Path = "$($llvmMinGW[0].FullName);$env:Path" - $cc = "clang" -} elseif (Get-Command clang -ErrorAction SilentlyContinue) { - $cc = "clang" -} elseif (Get-Command gcc -ErrorAction SilentlyContinue) { - $cc = "gcc" -} else { - Write-Error "No C compiler (clang or gcc) found in PATH." +if ($llvmMinGW) { $candidates += "$($llvmMinGW[0].FullName)\clang.exe" } +$candidates += @("clang", "gcc", "$env:ProgramFiles\LLVM\bin\clang.exe") +foreach ($c in $candidates) { + if ((Get-Command $c -ErrorAction SilentlyContinue) -and (Test-Embed $c)) { $cc = $c; break } +} +if (-not $cc) { + Write-Error "No C compiler with #embed support (clang 19 or gcc 15 and later) was found." exit 1 } @@ -21,9 +33,6 @@ Write-Host "==================================================================== Write-Host "Compiler: $cc" Write-Host "" -$tempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "tinyvm_test_" + [System.Guid]::NewGuid().ToString("N")) -New-Item -ItemType Directory -Path "$tempDir\s0", "$tempDir\s1" -Force | Out-Null - $totalPassed = 0 $totalFailed = 0 @@ -40,41 +49,41 @@ function Report-Result($success, $name, $detail = "") { try { # --- 1. bootstrap & fixpoint --- Write-Host "=== 1. Bootstrap and Fixpoint Verification ===" -ForegroundColor Yellow - & $cc -fwrapv -O1 -w src\tools\zc0.c -Isrc/core -o "$tempDir\zc0.exe" - & $cc -fwrapv -O1 -w src\tools\mkgenome.c -Isrc/core -o "$tempDir\mkgenome.exe" + # the default map, as make writes it, and the tools built against it + & $cc -fwrapv -O2 -w src\tools\genmap.c -o "$tempDir\genmap.exe" + cmd.exe /c "`"$tempDir\genmap.exe`" --preset default --check -o `"$tempDir\map`" >nul 2>nul" + Report-Result ($LASTEXITCODE -eq 0) "genmap default preset matches the sources" + $map = "$tempDir\map" + & $cc -fwrapv -O1 -w src\tools\zc0.c -Isrc/core "-I$map" -o "$tempDir\zc0.exe" + & $cc -fwrapv -O1 -w src\tools\mkgenome.c "-I$map" -o "$tempDir\mkgenome.exe" - Copy-Item src\core\core.c, src\core\io_win.c, src\core\memmap.h, src\core\fops.h, src\core\bops.h, src\core\stack.h -Destination "$tempDir\s0" - Copy-Item src\core\core.c, src\core\io_win.c, src\core\memmap.h, src\core\fops.h, src\core\bops.h, src\core\stack.h -Destination "$tempDir\s1" - - cmd.exe /c "(type src\runtime\rt.zc & type src\compiler\compiler.zc) | $tempDir\zc0.exe > $tempDir\s0\genome.h" - & $cc -fwrapv -O2 -w -DJTYPE=int "$tempDir\s0\core.c" "$tempDir\s0\io_win.c" -o "$tempDir\zy0.exe" + cmd.exe /c "(type src\runtime\rt.zc & type src\core\core.zc & type src\compiler\compiler.zc) | `"$tempDir\zc0.exe`" > `"$tempDir\s0\g0.bin`"" + cmd.exe /c "`"$tempDir\mkgenome.exe`" < `"$tempDir\s0\g0.bin`" > `"$tempDir\s0\genome.bin`"" + & $cc -fwrapv -O2 -w -DJTYPE=int src\core\core.c src\core\io_win.c -Isrc/core "-I$map" "--embed-dir=$tempDir\s0" -o "$tempDir\zy0.exe" $bzc = [IO.File]::ReadAllText("src\tools\build.zc") $rt = [IO.File]::ReadAllText("src\runtime\rt.zc") + $core = [IO.File]::ReadAllText("src\core\core.zc") $cp = [IO.File]::ReadAllText("src\compiler\compiler.zc") - $combined = $bzc + [char]96 + $rt + $cp + $combined = $bzc + [char]96 + $rt + $core + $cp [IO.File]::WriteAllBytes("$tempDir\in.tmp", [Text.Encoding]::ASCII.GetBytes($combined)) $p1 = Start-Process -FilePath "$tempDir\zy0.exe" -NoNewWindow -PassThru -RedirectStandardInput "$tempDir\in.tmp" -RedirectStandardOutput "$tempDir\g1.bin" -Wait - cmd.exe /c "$tempDir\mkgenome.exe < $tempDir\g1.bin > $tempDir\s1\genome.h" + cmd.exe /c "`"$tempDir\mkgenome.exe`" < `"$tempDir\g1.bin`" > `"$tempDir\s1\genome.bin`"" + Report-Result ($LASTEXITCODE -eq 0) "the compiler compiles its own source" - & $cc -fwrapv -O2 -w -DJTYPE=int "$tempDir\s1\core.c" "$tempDir\s1\io_win.c" -o "$tempDir\zy1.exe" + & $cc -fwrapv -O2 -w -DJTYPE=int src\core\core.c src\core\io_win.c -Isrc/core "-I$map" "--embed-dir=$tempDir\s1" -o "$tempDir\zy1.exe" $p2 = Start-Process -FilePath "$tempDir\zy1.exe" -NoNewWindow -PassThru -RedirectStandardInput "$tempDir\in.tmp" -RedirectStandardOutput "$tempDir\g2.bin" -Wait $g1 = [IO.File]::ReadAllBytes("$tempDir\g1.bin") $g2 = [IO.File]::ReadAllBytes("$tempDir\g2.bin") $isFixpoint = [System.Linq.Enumerable]::SequenceEqual($g1, $g2) Report-Result $isFixpoint "Fixpoint: compiler compiles itself to identical $($g1.Length / 8) words" - - $ghRepo = (Get-FileHash "build\core\genome.h").Hash - $ghGen = (Get-FileHash "$tempDir\s1\genome.h").Hash - Report-Result ($ghRepo -eq $ghGen) "genome.h is up to date" - # --- 2. compile test executables --- + # --- 2. compile test executables, each carrying the fixpoint genome --- Write-Host "`n=== 2. Compiling Test Executables ===" -ForegroundColor Yellow - & $cc -fwrapv -O2 -w -DJTYPE=int src\core\core.c src\core\io_win.c -Isrc/core -o "$tempDir\zy.exe" - & $cc -fwrapv -O2 -w src\micro\micro.c src\micro\port_win.c -Isrc/core -Isrc/micro -o "$tempDir\zym.exe" - & $cc -fwrapv -O2 -w src\tools\genmap.c -Isrc/core -o "$tempDir\genmap.exe" + & $cc -fwrapv -O2 -w -DJTYPE=int src\core\core.c src\core\io_win.c -Isrc/core "-I$map" "--embed-dir=$tempDir\s1" -o "$tempDir\zy.exe" + & $cc -fwrapv -O2 -w src\micro\micro.c src\micro\port_win.c -Isrc/core "-I$map" "--embed-dir=$tempDir\s1" -Isrc/micro -o "$tempDir\zym.exe" # --- 3. feature tests on zy.exe --- Write-Host "`n=== 3. Running Feature Tests on VM Core (zy.exe) ===" -ForegroundColor Yellow @@ -158,7 +167,7 @@ try { $pass = 0 $fail = 0 foreach ($f in $files) { - $want = ((Get-Content $f.FullName -TotalCount 1) -replace '^// expect:\s*').Trim() + $want = ((Get-Content $f.FullName | Where-Object { $_ -match '^// expect:' } | Select-Object -First 1) -replace '^// expect:\s*').Trim() $got = (Get-Content $f.FullName -Raw | & "$tempDir\zy.exe" 2>&1 | Select-Object -First 1) if ($got -and $got.Contains($want)) { $pass++ @@ -172,9 +181,6 @@ try { # --- 6. tool verification --- Write-Host "`n=== 6. Memory Map Tool Verification (genmap.exe) ===" -ForegroundColor Yellow - cmd.exe /c "`"$tempDir\genmap.exe`" --preset default --check >nul 2>nul" - Report-Result ($LASTEXITCODE -eq 0) "genmap default preset check passed" - cmd.exe /c "`"$tempDir\genmap.exe`" --preset small -o `"$tempDir\small`" >nul 2>nul" Report-Result ($LASTEXITCODE -eq 0) "genmap small preset generated" @@ -182,21 +188,28 @@ try { Write-Host "`n=== 7. ZC Language Server (zclsp.exe) ===" -ForegroundColor Yellow # the server is the real compiler in check mode plus src\lsp\server.zc, on its own map $lspDir = "$tempDir\lsp" - New-Item -ItemType Directory -Force "$lspDir\src" | Out-Null - # two passes: compile once to learn the genome length, then lay the map out again for that length - $lspSrc = "(type `"$lspDir\map\rt.zc`" & type `"$lspDir\map\compiler.zc`" & type src\lsp\server.zc)" - cmd.exe /c "`"$tempDir\genmap.exe`" --preset lsp --grow -o `"$lspDir\map`" >nul 2>nul" - & $cc -fwrapv -O1 -w src\tools\zc0.c "-I$lspDir\map" -Isrc/core -o "$lspDir\zc0.exe" - cmd.exe /c "$lspSrc | `"$lspDir\zc0.exe`" > `"$lspDir\genome.h`"" - $glen = [int]((Select-String -Path "$lspDir\genome.h" -Pattern '#define\s+GLEN\s+(\d+)' | Select-Object -First 1).Matches[0].Groups[1].Value) + New-Item -ItemType Directory -Force "$lspDir" | Out-Null + # the steps of src/tools/automap.sh lsp with DOCS=1: compile once to learn the genome + # length, lay the map out for it, then again with the documentation the hovers read + function Build-LspImage($slot, $define, $out) { + if (Test-Path "$lspDir\map") { Remove-Item -Recurse -Force "$lspDir\map" } + $genomeOption = if ($slot) { "--genome $slot" } else { "" } + cmd.exe /c "`"$tempDir\genmap.exe`" --preset lsp --grow $genomeOption -o `"$lspDir\map`" >nul 2>nul" + & $cc -fwrapv -O1 -w $define src\tools\zc0.c "-I$lspDir\map" -Isrc/core -o "$lspDir\zc0.exe" + & $cc -fwrapv -O1 -w src\tools\mkgenome.c "-I$lspDir\map" -o "$lspDir\mkgenome.exe" + cmd.exe /c "(type `"$lspDir\map\rt.zc`" & type src\core\core.zc & type `"$lspDir\map\compiler.zc`" & type src\lsp\server.zc) | `"$lspDir\zc0.exe`" > `"$lspDir\raw.bin`"" + cmd.exe /c "`"$lspDir\mkgenome.exe`" < `"$lspDir\raw.bin`" > `"$lspDir\$out`"" + return [int]((Get-Item "$lspDir\$out").Length / 8) + } + # ZC0PLAIN defines nothing zc0 reads; it stands in for "no documentation" as one argument + $glen = Build-LspImage $null "-DZC0PLAIN" "image.bin" + $slot = [int]([math]::Ceiling($glen / 4096.0) * 4096) + $glen = Build-LspImage ($slot + 65536) "-DZC0DOCS" "image.bin" $slot = [int]([math]::Ceiling($glen / 4096.0) * 4096) - Remove-Item -Recurse -Force "$lspDir\map" - cmd.exe /c "`"$tempDir\genmap.exe`" --preset lsp --grow --genome $slot -o `"$lspDir\map`" >nul 2>nul" - & $cc -fwrapv -O1 -w src\tools\zc0.c "-I$lspDir\map" -Isrc/core -o "$lspDir\zc0.exe" - cmd.exe /c "$lspSrc | `"$lspDir\zc0.exe`" > `"$lspDir\src\lsp_genome.h`"" - foreach ($f in 'core.c','io_win.c','fops.h','bops.h','stack.h') { Copy-Item "src\core\$f" "$lspDir\src\" } - Copy-Item "$lspDir\map\memmap.h" "$lspDir\src\" - & $cc -fwrapv -O2 -w -DJTYPE=int -DUSE_LSP_GENOME "-I$lspDir\src" "$lspDir\src\core.c" "$lspDir\src\io_win.c" -o "$lspDir\zclsp.exe" + $final = Build-LspImage $slot "-DZC0DOCS" "lsp_genome.bin" + Report-Result ($final -eq $glen) "the LSP genome keeps its length on its sized map" + Copy-Item "$lspDir\map\memmap.h" "$lspDir\" + & $cc -fwrapv -O2 -w -DJTYPE=int -DZY_SANDBOX -DUSE_LSP_GENOME src\core\core.c src\core\io_win.c -Isrc/core "-I$lspDir" "--embed-dir=$lspDir" -o "$lspDir\zclsp.exe" Report-Result (Test-Path "$lspDir\zclsp.exe") "zclsp.exe compiled" # CLI check: a clean file @@ -209,7 +222,7 @@ try { # CLI check: the compiler's optimization hints $lspOpt = (cmd.exe /c "`"$lspDir\zclsp.exe`" < examples\opt_test.zc 2>&1") -join "`n" - $hints = @("is never read", "is never reassigned; make it a const", "is a shift; write << 3", "the number 1234 has no name", "new inside a loop", "stack buffer", "handed off once") + $hints = @("is never read", "is never reassigned; make it a const", "is a shift; write << 3", "unannotated numeric literal 1234", "new inside a loop", "stack buffer") foreach ($h in $hints) { Report-Result ($lspOpt.Contains($h)) "zclsp hint: $h" } # methods with this, and closures, check clean @@ -226,23 +239,28 @@ try { Write-Host " [SKIP] Node.js not detected in PATH, skipping lsp protocol tests." -ForegroundColor DarkYellow } - # --- 8. JavaScript / TypeScript vm runtime (build/js/tinyvm.js) --- - Write-Host "`n=== 8. JavaScript VM Runtime (node build/js/tinyvm.js) ===" -ForegroundColor Yellow + # --- 8. JavaScript / TypeScript vm runtime, built as make js builds it --- + Write-Host "`n=== 8. JavaScript VM Runtime (node tinyvm.js) ===" -ForegroundColor Yellow $nodeCmd = Get-Command node -ErrorAction SilentlyContinue - if ($nodeCmd) { - $jsHello = (node build\js\tinyvm.js examples\hello.zc 2>&1) -join "`n" + $tscCmd = Get-Command tsc -ErrorAction SilentlyContinue + if ($nodeCmd -and $tscCmd) { + $jsDir = "$tempDir\js" + node src\js\mklayout.js --map "$map\memmap.h" --out $jsDir | Out-Null + Copy-Item "$tempDir\s1\genome.bin" "$jsDir\genome.bin" + tsc src\js\tinyvm.ts --target es2020 --lib es2020,dom --module commonjs --outDir $jsDir + $jsHello = (node $jsDir\tinyvm.js examples\hello.zc 2>&1) -join "`n" Report-Result ($jsHello.Trim() -eq "hello, world!") "JS VM: hello.zc" - $jsFact = (node build\js\tinyvm.js examples\fact.zc 2>&1) -join "`n" + $jsFact = (node $jsDir\tinyvm.js examples\fact.zc 2>&1) -join "`n" Report-Result ($jsFact.Contains("3628800")) "JS VM: fact.zc" - $jsPrimes = (node build\js\tinyvm.js examples\primes.zc 2>&1) -join "`n" + $jsPrimes = (node $jsDir\tinyvm.js examples\primes.zc 2>&1) -join "`n" Report-Result ($jsPrimes.Contains("widest gap: 72")) "JS VM: primes.zc" - $jsMandel = (node build\js\tinyvm.js examples\mandel.zc 2>&1) -join "`n" + $jsMandel = (node $jsDir\tinyvm.js examples\mandel.zc 2>&1) -join "`n" Report-Result ($jsMandel.Contains("*") -or $jsMandel.Contains(".")) "JS VM: mandel.zc (custom fmul enhancement)" } else { - Write-Host " [SKIP] Node.js not detected in PATH, skipping JS tests." -ForegroundColor DarkYellow + Write-Host " [SKIP] Node.js or tsc not detected in PATH, skipping JS tests." -ForegroundColor DarkYellow } } finally { @@ -7,65 +7,83 @@ if /i "%~1"=="help" goto :show_help if /i "%~1"=="--help" goto :show_help if /i "%~1"=="-h" goto :show_help +REM everything built or generated goes under build\ (executables in build\bin); +REM the tree holds sources only. the cores take their genome in with c23 #embed, +REM so the compiler has to be clang 19 or gcc 15 or later. + REM ========================================================================= REM Compiler Detection & Environment Setup REM ========================================================================= set "CC_NAME=" - -REM preserve a compiler selected by the caller. +if not exist "build\probe" mkdir "build\probe" +> "build\probe\probe.bin" echo ab +> "build\probe\probe.c" echo static const unsigned char probe[] = { +>> "build\probe\probe.c" echo #embed "probe.bin" +>> "build\probe\probe.c" echo }; +>> "build\probe\probe.c" echo int main(void) { return sizeof probe ^> 0 ? 0 : 1; } + +REM preserve a compiler selected by the caller, if it can #embed. if defined CC ( - set "CC_NAME=Configured [%CC%]" - goto :has_compiler + call :probe_cc "%CC%" + if not errorlevel 1 ( + set "CC_NAME=Configured [%CC%]" + goto :has_compiler + ) + echo [ERROR] CC=%CC% does not support #embed; clang 19 or gcc 15 is needed. + exit /b 1 ) -REM 1. Search WinGet LLVM-MinGW portable packages first (self-contained with CRT headers) +REM 1. WinGet LLVM-MinGW portable packages (self-contained with CRT headers) for /d %%D in ("%LOCALAPPDATA%\Microsoft\WinGet\Packages\MartinStorsjo.LLVM-MinGW*") do ( for /d %%B in ("%%D\llvm-mingw*") do ( if exist "%%B\bin\clang.exe" ( - set "PATH=%%B\bin;!PATH!" - set "CC=clang" - set "CC_NAME=LLVM-MinGW Clang [WinGet]" - goto :compiler_found + call :probe_cc "%%B\bin\clang.exe" + if not errorlevel 1 ( + set "PATH=%%B\bin;!PATH!" + set "CC=clang" + set "CC_NAME=LLVM-MinGW Clang [WinGet]" + goto :has_compiler + ) ) ) ) -REM 2. Check if clang or gcc is already in PATH -where clang >nul 2>nul -if %errorlevel% equ 0 ( - set "CC=clang" - set "CC_NAME=Clang [in PATH]" - goto :compiler_found -) - -where gcc >nul 2>nul -if %errorlevel% equ 0 ( - set "CC=gcc" - set "CC_NAME=GCC [in PATH]" - goto :compiler_found +REM 2. clang or gcc already in PATH +for %%C in (clang gcc) do ( + where %%C >nul 2>nul + if !errorlevel! equ 0 ( + call :probe_cc %%C + if not errorlevel 1 ( + set "CC=%%C" + set "CC_NAME=%%C [in PATH]" + goto :has_compiler + ) + ) ) -REM 3. Search C:\Program Files\LLVM\bin +REM 3. C:\Program Files\LLVM\bin and the x86 variant if exist "%ProgramFiles%\LLVM\bin\clang.exe" ( - set "PATH=%ProgramFiles%\LLVM\bin;%PATH%" - set "CC=clang" - set "CC_NAME=LLVM Clang [Program Files]" - goto :compiler_found + call :probe_cc "%ProgramFiles%\LLVM\bin\clang.exe" + if not errorlevel 1 ( + set "PATH=%ProgramFiles%\LLVM\bin;%PATH%" + set "CC=clang" + set "CC_NAME=LLVM Clang [Program Files]" + goto :has_compiler + ) ) - set "PFX86=%ProgramFiles(x86)%" if defined PFX86 if exist "!PFX86!\LLVM\bin\clang.exe" ( - set "PATH=!PFX86!\LLVM\bin;%PATH%" - set "CC=clang" - set "CC_NAME=LLVM Clang [Program Files x86]" - goto :compiler_found + call :probe_cc "!PFX86!\LLVM\bin\clang.exe" + if not errorlevel 1 ( + set "PATH=!PFX86!\LLVM\bin;%PATH%" + set "CC=clang" + set "CC_NAME=LLVM Clang [Program Files x86]" + goto :has_compiler + ) ) -:compiler_found -if not "%CC%"=="" goto :has_compiler - -echo [ERROR] No suitable C compiler [clang or gcc] found on this system. +echo [ERROR] No C compiler with #embed support [clang 19 or gcc 15 and later] was found. echo. echo Please install LLVM / Clang: echo winget install MartinStorsjo.LLVM-MinGW.UCRT @@ -74,7 +92,14 @@ echo. pause exit /b 1 +REM :probe_cc compiler - errorlevel 0 when it compiles a file that uses #embed +:probe_cc +%1 -w --embed-dir=build\probe build\probe\probe.c -o build\probe\probe.exe >nul 2>nul +exit /b %errorlevel% + :has_compiler +REM the include and embed paths of a core that carries the compiler genome +set "COREINC=-Isrc/core -Ibuild/core/map --embed-dir=build/core" REM Handle CLI Arguments if provided if not "%~1"=="" goto :handle_cli @@ -92,29 +117,29 @@ echo Compiler : %CC_NAME% echo Working : %CD% echo ------------------------------------------------------------------------------- echo executable status: -if exist "bin\zy.exe" ( echo [X] bin\zy.exe - VM core interpreter ) else ( echo [ ] bin\zy.exe - not built ) -if exist "bin\zym.exe" ( echo [X] bin\zym.exe - portable microcore ) else ( echo [ ] bin\zym.exe - not built ) -if exist "bin\zc0.exe" ( echo [X] bin\zc0.exe - stage 0 bootstrap compiler ) else ( echo [ ] bin\zc0.exe - not built ) -if exist "bin\mkgenome.exe" ( echo [X] bin\mkgenome.exe - genome header generator ) else ( echo [ ] bin\mkgenome.exe - not built ) -if exist "bin\genmap.exe" ( echo [X] bin\genmap.exe - memory map generator ) else ( echo [ ] bin\genmap.exe - not built ) -if exist "bin\zclsp.exe" ( echo [X] bin\zclsp.exe - language server protocol ^& highlighter ) else ( echo [ ] bin\zclsp.exe - not built ) +if exist "build\bin\zy.exe" ( echo [X] build\bin\zy.exe - VM core interpreter ) else ( echo [ ] build\bin\zy.exe - not built ) +if exist "build\bin\zym.exe" ( echo [X] build\bin\zym.exe - portable microcore ) else ( echo [ ] build\bin\zym.exe - not built ) +if exist "build\bin\zc0.exe" ( echo [X] build\bin\zc0.exe - stage 0 bootstrap compiler ) else ( echo [ ] build\bin\zc0.exe - not built ) +if exist "build\bin\mkgenome.exe" ( echo [X] build\bin\mkgenome.exe - genome image checker ) else ( echo [ ] build\bin\mkgenome.exe - not built ) +if exist "build\bin\genmap.exe" ( echo [X] build\bin\genmap.exe - memory map generator ) else ( echo [ ] build\bin\genmap.exe - not built ) +if exist "build\bin\zclsp.exe" ( echo [X] build\bin\zclsp.exe - language server protocol ^& highlighter ) else ( echo [ ] build\bin\zclsp.exe - not built ) echo =============================================================================== -echo [1] build VM core (bin\zy.exe) -echo [2] build microcore (bin\zym.exe) -echo [3] build bootstrap tools (bin\zc0.exe, bin\mkgenome.exe, bin\genmap.exe) -echo [4] build LSP server ^& highlighter (bin\zclsp.exe) +echo [1] build VM core (build\bin\zy.exe) +echo [2] build microcore (build\bin\zym.exe) +echo [3] build bootstrap tools (zc0.exe, mkgenome.exe, genmap.exe) +echo [4] build LSP server ^& highlighter (build\bin\zclsp.exe) echo [5] build everything echo [6] run Windows test suite (native, LSP, JS, bootstrap) -echo [7] run an example on the native VM (bin\zy.exe) -echo [8] run an example in the JavaScript VM (node build/js/tinyvm.js) +echo [7] run an example on the native VM (build\bin\zy.exe) +echo [8] run an example in the JavaScript VM (node build\js\tinyvm.js) echo [9] run a custom .zc source file -echo [L] check/highlight .zc with LSP (bin\zclsp.exe) +echo [L] check/highlight .zc with LSP (build\bin\zclsp.exe) echo [R] launch the interactive REPL (resident compiler) -echo [B] bootstrap and run the full test suite -echo [C] clean generated build directories (bin, build) +echo [G] rebuild the compiler genome and check the fixpoint +echo [C] clean generated output (build) echo [Q] quit echo =============================================================================== -set /p "CHOICE=select an option [1-9, L, R, B, C, Q]: " +set /p "CHOICE=select an option [1-9, L, R, G, C, Q]: " if /i "%CHOICE%"=="1" goto :menu_build_core if /i "%CHOICE%"=="2" goto :menu_build_micro @@ -127,7 +152,7 @@ if /i "%CHOICE%"=="8" goto :menu_examples_js if /i "%CHOICE%"=="9" goto :menu_run_custom if /i "%CHOICE%"=="l" goto :menu_lsp_check if /i "%CHOICE%"=="r" goto :menu_repl -if /i "%CHOICE%"=="b" goto :menu_bootstrap +if /i "%CHOICE%"=="g" goto :menu_genome if /i "%CHOICE%"=="c" goto :menu_clean if /i "%CHOICE%"=="q" exit /b 0 @@ -141,7 +166,6 @@ REM ========================================================================= :menu_build_core echo. -echo [Building zy.exe]... call :build_core echo. pause @@ -149,7 +173,6 @@ goto :main_menu :menu_build_micro echo. -echo [Building zym.exe]... call :build_micro echo. pause @@ -157,7 +180,6 @@ goto :main_menu :menu_build_tools echo. -echo [Building tools: zc0.exe, mkgenome.exe, genmap.exe]... call :build_tools echo. pause @@ -165,7 +187,6 @@ goto :main_menu :menu_build_lsp echo. -echo [Building zclsp.exe (LSP Server ^& Highlighter)]... call :build_lsp echo. pause @@ -173,14 +194,7 @@ goto :main_menu :menu_build_all echo. -echo [Building all components]... -call :build_core -if errorlevel 1 goto :build_failed -call :build_micro -if errorlevel 1 goto :build_failed -call :build_tools -if errorlevel 1 goto :build_failed -call :build_lsp +call :build_all if errorlevel 1 goto :build_failed echo. echo [OK] Build complete. @@ -195,41 +209,23 @@ goto :main_menu :menu_test echo. -if not exist "bin\zy.exe" ( - call :build_core - if errorlevel 1 goto :build_failed -) -if not exist "bin\zym.exe" ( - call :build_micro - if errorlevel 1 goto :build_failed -) -echo [Running test suite]... -powershell -NoProfile -ExecutionPolicy Bypass -File "tests\test_win.ps1" +call :run_tests if errorlevel 1 echo [ERROR] Test suite failed. echo. pause goto :main_menu -:menu_bootstrap +:menu_genome echo. -echo [Running Bootstrap and Full Test Verification]... -call :build_tools +call :build_genome if errorlevel 1 goto :build_failed -if not exist "bin\zy.exe" ( - call :build_core - if errorlevel 1 goto :build_failed -) -powershell -NoProfile -ExecutionPolicy Bypass -File "tests\test_win.ps1" -if errorlevel 1 echo [ERROR] Test suite failed. echo. pause goto :main_menu :menu_clean echo. -echo [Cleaning build artifacts]... -rmdir /s /q build bin 2>nul -echo [OK] Generated bin and build directories removed. +call :clean echo. pause goto :main_menu @@ -245,18 +241,15 @@ if not exist "%CUSTOM_FILE%" ( pause goto :main_menu ) - -if not exist "bin\zy.exe" ( - echo bin\zy.exe not found, building first... +if not exist "build\bin\zy.exe" ( call :build_core if errorlevel 1 goto :build_failed ) - echo. echo =============================================================================== echo Running: %CUSTOM_FILE% echo =============================================================================== -bin\zy.exe < "%CUSTOM_FILE%" +build\bin\zy.exe < "%CUSTOM_FILE%" echo. echo =============================================================================== pause @@ -272,18 +265,15 @@ if not exist "%LSP_FILE%" ( pause goto :main_menu ) - -if not exist "bin\zclsp.exe" ( - echo bin\zclsp.exe not found, building first... +if not exist "build\bin\zclsp.exe" ( call :build_lsp if errorlevel 1 goto :build_failed ) - echo. echo =============================================================================== -echo Analyzing with bin\zclsp.exe: %LSP_FILE% +echo Analyzing with build\bin\zclsp.exe: %LSP_FILE% echo =============================================================================== -bin\zclsp.exe < "%LSP_FILE%" +build\bin\zclsp.exe < "%LSP_FILE%" echo. echo =============================================================================== pause @@ -291,8 +281,7 @@ goto :main_menu :menu_repl echo. -if not exist "bin\zy.exe" ( - echo bin\zy.exe not found, building first... +if not exist "build\bin\zy.exe" ( call :build_core if errorlevel 1 goto :build_failed ) @@ -302,7 +291,7 @@ echo Each input chunk separated by backtick (`) is compiled and run. echo Type expression or function, terminate with ` (or EOF / Ctrl+Z). echo Type 'quit;' then backtick or press Ctrl+C to exit. echo =============================================================================== -(type examples\repl.zc & echo ` & more) | bin\zy.exe +(type examples\repl.zc & echo ` & more) | build\bin\zy.exe echo. pause goto :main_menu @@ -316,47 +305,24 @@ cls echo =============================================================================== echo Run TinyVM Examples on Native VM (zy.exe) echo =============================================================================== -echo [1] hello.zc - Classic "hello, world!" -echo [2] fact.zc - Recursive & iterative factorial computation -echo [3] mandel.zc - Mandelbrot ASCII set using custom enhancement opcode -echo [4] primes.zc - Sieve of Eratosthenes with o3 region compilation -echo [5] life.zc - Conway's Game of Life on a typed torus board -echo [6] lists.zc - Dynamic array with ownership tracking (u, m, d tags) -echo [7] typed.zc - Structs, generic vectors, and reflection -echo [8] wiring.zc - Runtime custom enhancement opcodes demo -echo [9] bf.zc - Brainfuck JIT compiler running inside TinyVM -echo [B] Back to main menu -echo =============================================================================== +call :example_list set /p "EX_CHOICE=Select an example [1-9, B]: " - if /i "%EX_CHOICE%"=="b" goto :main_menu -set "EX_FILE=" -if "%EX_CHOICE%"=="1" set "EX_FILE=examples\hello.zc" -if "%EX_CHOICE%"=="2" set "EX_FILE=examples\fact.zc" -if "%EX_CHOICE%"=="3" set "EX_FILE=examples\mandel.zc" -if "%EX_CHOICE%"=="4" set "EX_FILE=examples\primes.zc" -if "%EX_CHOICE%"=="5" set "EX_FILE=examples\life.zc" -if "%EX_CHOICE%"=="6" set "EX_FILE=examples\lists.zc" -if "%EX_CHOICE%"=="7" set "EX_FILE=examples\typed.zc" -if "%EX_CHOICE%"=="8" set "EX_FILE=examples\wiring.zc" -if "%EX_CHOICE%"=="9" set "EX_FILE=examples\bf.zc" - +call :example_file "%EX_CHOICE%" if "%EX_FILE%"=="" ( echo Invalid choice. timeout /t 2 >nul goto :menu_examples ) - -if not exist "bin\zy.exe" ( +if not exist "build\bin\zy.exe" ( call :build_core if errorlevel 1 goto :build_failed ) - echo. echo =============================================================================== echo Running: %EX_FILE% echo =============================================================================== -bin\zy.exe < "%EX_FILE%" +build\bin\zy.exe < "%EX_FILE%" echo. echo =============================================================================== pause @@ -367,142 +333,216 @@ cls echo =============================================================================== echo Run TinyVM Examples in JavaScript VM (Node.js) echo =============================================================================== -echo [1] hello.zc - Classic "hello, world!" -echo [2] fact.zc - Recursive & iterative factorial computation -echo [3] mandel.zc - Mandelbrot ASCII set using custom enhancement opcode -echo [4] primes.zc - Sieve of Eratosthenes with o3 region compilation -echo [5] life.zc - Conway's Game of Life on a typed torus board -echo [6] lists.zc - Dynamic array with ownership tracking (u, m, d tags) -echo [7] typed.zc - Structs, generic vectors, and reflection -echo [8] wiring.zc - Runtime custom enhancement opcodes demo -echo [9] bf.zc - Brainfuck JIT compiler running inside TinyVM -echo [B] Back to main menu -echo =============================================================================== -set /p "JS_EX_CHOICE=Select an example [1-9, B]: " - -if /i "%JS_EX_CHOICE%"=="b" goto :main_menu -set "JS_EX_FILE=" -if "%JS_EX_CHOICE%"=="1" set "JS_EX_FILE=examples\hello.zc" -if "%JS_EX_CHOICE%"=="2" set "JS_EX_FILE=examples\fact.zc" -if "%JS_EX_CHOICE%"=="3" set "JS_EX_FILE=examples\mandel.zc" -if "%JS_EX_CHOICE%"=="4" set "JS_EX_FILE=examples\primes.zc" -if "%JS_EX_CHOICE%"=="5" set "JS_EX_FILE=examples\life.zc" -if "%JS_EX_CHOICE%"=="6" set "JS_EX_FILE=examples\lists.zc" -if "%JS_EX_CHOICE%"=="7" set "JS_EX_FILE=examples\typed.zc" -if "%JS_EX_CHOICE%"=="8" set "JS_EX_FILE=examples\wiring.zc" -if "%JS_EX_CHOICE%"=="9" set "JS_EX_FILE=examples\bf.zc" - -if "%JS_EX_FILE%"=="" ( +call :example_list +set /p "EX_CHOICE=Select an example [1-9, B]: " +if /i "%EX_CHOICE%"=="b" goto :main_menu +call :example_file "%EX_CHOICE%" +if "%EX_FILE%"=="" ( echo Invalid choice. timeout /t 2 >nul goto :menu_examples_js ) - +call :build_js +if errorlevel 1 goto :build_failed echo. echo =============================================================================== -echo Running in JS: %JS_EX_FILE% +echo Running in JS: %EX_FILE% echo =============================================================================== -node src\js\tinyvm.js "%JS_EX_FILE%" +node build\js\tinyvm.js "%EX_FILE%" echo. echo =============================================================================== pause goto :menu_examples_js +:example_list +echo [1] hello.zc - Classic "hello, world!" +echo [2] fact.zc - Recursive ^& iterative factorial computation +echo [3] mandel.zc - Mandelbrot ASCII set using custom enhancement opcode +echo [4] primes.zc - Sieve of Eratosthenes with o3 region compilation +echo [5] life.zc - Conway's Game of Life on a typed torus board +echo [6] lists.zc - Dynamic array with ownership tracking (u, m, d tags) +echo [7] typed.zc - Structs, generic vectors, and reflection +echo [8] wiring.zc - Runtime custom enhancement opcodes demo +echo [9] bf.zc - Brainfuck compiler running inside TinyVM +echo [B] Back to main menu +echo =============================================================================== +exit /b 0 + +:example_file +set "EX_FILE=" +if "%~1"=="1" set "EX_FILE=examples\hello.zc" +if "%~1"=="2" set "EX_FILE=examples\fact.zc" +if "%~1"=="3" set "EX_FILE=examples\mandel.zc" +if "%~1"=="4" set "EX_FILE=examples\primes.zc" +if "%~1"=="5" set "EX_FILE=examples\life.zc" +if "%~1"=="6" set "EX_FILE=examples\lists.zc" +if "%~1"=="7" set "EX_FILE=examples\typed.zc" +if "%~1"=="8" set "EX_FILE=examples\wiring.zc" +if "%~1"=="9" set "EX_FILE=examples\bf.zc" +exit /b 0 + REM ========================================================================= REM Build Routines REM ========================================================================= -:build_core -echo Compiling zy.exe (VM Core with JIT/Interpreter and Enhancements)... -if not exist "bin" mkdir bin -"%CC%" -O2 -w -fwrapv -DJTYPE=int src\core\core.c src\core\io_win.c -Isrc/core -o bin\zy.exe -if errorlevel 1 (echo [ERROR] Failed to build bin\zy.exe.& exit /b 1) -echo [OK] bin\zy.exe built successfully. +REM genmap needs no map; the default map comes from it and the zc sources' const blocks +:build_tools +if not exist "build\bin" mkdir build\bin +echo Compiling genmap.exe (memory map generator)... +"%CC%" -O2 -w -fwrapv src\tools\genmap.c -o build\bin\genmap.exe +if errorlevel 1 (echo [ERROR] Failed to build build\bin\genmap.exe.& exit /b 1) +if exist build\core\map rmdir /s /q build\core\map +if not exist build\core mkdir build\core +build\bin\genmap.exe --preset default --check -o build\core\map >nul +if errorlevel 1 (echo [ERROR] The default memory map does not match the sources.& exit /b 1) +echo Compiling zc0.exe (bootstrap compiler)... +"%CC%" -O1 -w -fwrapv src\tools\zc0.c -Isrc/core -Ibuild/core/map -o build\bin\zc0.exe +if errorlevel 1 (echo [ERROR] Failed to build build\bin\zc0.exe.& exit /b 1) +echo Compiling mkgenome.exe (genome image checker)... +"%CC%" -O1 -w -fwrapv src\tools\mkgenome.c -Ibuild/core/map -o build\bin\mkgenome.exe +if errorlevel 1 (echo [ERROR] Failed to build build\bin\mkgenome.exe.& exit /b 1) +echo [OK] Bootstrap tools built in build\bin. exit /b 0 -:build_micro -echo Compiling zym.exe (Portable Microcore)... -if not exist "bin" mkdir bin -"%CC%" -O2 -w -fwrapv src\micro\micro.c src\micro\port_win.c -Isrc/core -Isrc/micro -o bin\zym.exe -if errorlevel 1 (echo [ERROR] Failed to build bin\zym.exe.& exit /b 1) -echo [OK] bin\zym.exe built successfully. +REM zc0 compiles the compiler to g0, g0 compiles it to g1, g1 to g2. g1 and g2 +REM must be identical, and g1 becomes build\core\genome.bin. +:build_genome +call :build_tools +if errorlevel 1 exit /b 1 +echo Bootstrapping the compiler genome... +if not exist build\s0 mkdir build\s0 +if not exist build\s1 mkdir build\s1 +(type src\runtime\rt.zc & type src\core\core.zc & type src\compiler\compiler.zc) | build\bin\zc0.exe > build\s0\g0.bin +if errorlevel 1 (echo [ERROR] Stage 0 failed.& exit /b 1) +build\bin\mkgenome.exe < build\s0\g0.bin > build\s0\genome.bin +if errorlevel 1 (echo [ERROR] Stage 0 did not produce a genome.& exit /b 1) +"%CC%" -O2 -w -fwrapv -DJTYPE=int src\core\core.c src\core\io_win.c -Isrc/core -Ibuild/core/map --embed-dir=build/s0 -o build\s0\zy0.exe +if errorlevel 1 (echo [ERROR] Failed to build the stage 0 core.& exit /b 1) +(type src\tools\build.zc & echo ` & type src\runtime\rt.zc & type src\core\core.zc & type src\compiler\compiler.zc) | build\s0\zy0.exe > build\s1\g1.bin +build\bin\mkgenome.exe < build\s1\g1.bin > build\s1\genome.bin +if errorlevel 1 (echo [ERROR] The compiler did not compile itself; its diagnostics are above.& exit /b 1) +"%CC%" -O2 -w -fwrapv -DJTYPE=int src\core\core.c src\core\io_win.c -Isrc/core -Ibuild/core/map --embed-dir=build/s1 -o build\s1\zy1.exe +if errorlevel 1 (echo [ERROR] Failed to build the stage 1 core.& exit /b 1) +(type src\tools\build.zc & echo ` & type src\runtime\rt.zc & type src\core\core.zc & type src\compiler\compiler.zc) | build\s1\zy1.exe > build\s1\g2.bin +fc /b build\s1\g1.bin build\s1\g2.bin >nul +if errorlevel 1 (echo [ERROR] Fixpoint failed: the compiler does not compile itself to the same image.& exit /b 1) +copy /y build\s1\genome.bin build\core\genome.bin >nul +echo [OK] Fixpoint reached; build\core\genome.bin updated. exit /b 0 -:build_tools -if not exist "bin" mkdir bin -echo Compiling zc0.exe (Bootstrap compiler)... -"%CC%" -O1 -w -fwrapv src\tools\zc0.c -Isrc/core -o bin\zc0.exe -if errorlevel 1 (echo [ERROR] Failed to build bin\zc0.exe.& exit /b 1) -echo Compiling mkgenome.exe (Genome generator)... -"%CC%" -O1 -w -fwrapv src\tools\mkgenome.c -Isrc/core -o bin\mkgenome.exe -if errorlevel 1 (echo [ERROR] Failed to build bin\mkgenome.exe.& exit /b 1) -echo Compiling genmap.exe (Memory map generator)... -"%CC%" -O2 -w -fwrapv src\tools\genmap.c -Isrc/core -o bin\genmap.exe -if errorlevel 1 (echo [ERROR] Failed to build bin\genmap.exe.& exit /b 1) -echo [OK] Bootstrap tools built successfully in bin\. +:need_genome +if exist build\core\genome.bin if exist build\core\map\memmap.h exit /b 0 +call :build_genome +exit /b %errorlevel% + +:build_core +call :need_genome +if errorlevel 1 exit /b 1 +echo Compiling zy.exe (VM core with the compiler genome)... +"%CC%" -O2 -w -fwrapv -DJTYPE=int src\core\core.c src\core\io_win.c %COREINC% -o build\bin\zy.exe +if errorlevel 1 (echo [ERROR] Failed to build build\bin\zy.exe.& exit /b 1) +echo [OK] build\bin\zy.exe built. +exit /b 0 + +:build_micro +call :need_genome +if errorlevel 1 exit /b 1 +echo Compiling zym.exe (portable microcore)... +"%CC%" -O2 -w -fwrapv src\micro\micro.c src\micro\port_win.c %COREINC% -Isrc/micro -o build\bin\zym.exe +if errorlevel 1 (echo [ERROR] Failed to build build\bin\zym.exe.& exit /b 1) +echo [OK] build\bin\zym.exe built. exit /b 0 +REM the language server: the compiler in check mode plus src\lsp\server.zc, on a +REM map sized to the image (the steps of src/tools/automap.sh lsp with DOCS=1) :build_lsp -echo Compiling zclsp.exe (LSP Server ^& Semantic Highlighter in zc)... -if not exist "bin" mkdir bin -if not exist "bin\zc0.exe" ( - call :build_tools - if errorlevel 1 exit /b 1 -) -if not exist "bin\genmap.exe" ( +if not exist "build\bin\genmap.exe" ( call :build_tools if errorlevel 1 exit /b 1 ) +echo Compiling zclsp.exe (language server ^& semantic highlighter)... if exist build\lsp rmdir /s /q build\lsp -mkdir build\lsp\src -if errorlevel 1 (echo [ERROR] Could not create build\lsp.& exit /b 1) -bin\genmap.exe --preset lsp --grow -o build\lsp\map >nul 2>nul -if errorlevel 1 (echo [ERROR] Could not generate the initial LSP map.& exit /b 1) -"%CC%" -O1 -w -fwrapv src\tools\zc0.c -Ibuild\lsp\map -Isrc/core -o build\lsp\zc0.exe -if errorlevel 1 (echo [ERROR] Could not build the LSP sizing compiler.& exit /b 1) -(type build\lsp\map\rt.zc & type build\lsp\map\compiler.zc & type src\lsp\server.zc) | build\lsp\zc0.exe > build\lsp\plain.h -if errorlevel 1 (echo [ERROR] Could not size the LSP genome.& exit /b 1) -set "GLEN=" -for /f "tokens=3" %%G in ('findstr /b /c:"#define GLEN" build\lsp\plain.h') do set "GLEN=%%G" -if not defined GLEN (echo [ERROR] Could not read the LSP genome size.& exit /b 1) -set /a SLOT=(GLEN+4095)/4096*4096 +mkdir build\lsp +call :lsp_pass "" "" image.bin +if errorlevel 1 exit /b 1 +call :words build\lsp\image.bin +set /a SLOT=(WORDS+4095)/4096*4096 set /a DOCS_WORK=SLOT+65536 -rmdir /s /q build\lsp\map -bin\genmap.exe --preset lsp --grow --genome %DOCS_WORK% -o build\lsp\map >nul 2>nul -if errorlevel 1 (echo [ERROR] Could not generate the documentation build map.& exit /b 1) -"%CC%" -O1 -w -fwrapv -DZC0DOCS src\tools\zc0.c -Ibuild\lsp\map -Isrc/core -o build\lsp\zc0docs.exe -if errorlevel 1 (echo [ERROR] Could not build the documentation compiler.& exit /b 1) -(type build\lsp\map\rt.zc & type build\lsp\map\compiler.zc & type src\lsp\server.zc) | build\lsp\zc0docs.exe > build\lsp\docs-first.h -if errorlevel 1 (echo [ERROR] Could not build the first documentation genome.& exit /b 1) -set "DOCS_GLEN=" -for /f "tokens=3" %%G in ('findstr /b /c:"#define GLEN" build\lsp\docs-first.h') do set "DOCS_GLEN=%%G" -if not defined DOCS_GLEN (echo [ERROR] Could not read the documentation genome size.& exit /b 1) -set /a DOCS_SLOT=(DOCS_GLEN+4095)/4096*4096 -rmdir /s /q build\lsp\map -bin\genmap.exe --preset lsp --grow --genome %DOCS_SLOT% -o build\lsp\map >nul 2>nul -if errorlevel 1 (echo [ERROR] Could not generate the sized LSP map.& exit /b 1) -"%CC%" -O1 -w -fwrapv -DZC0DOCS src\tools\zc0.c -Ibuild\lsp\map -Isrc/core -o build\lsp\zc0docs.exe -if errorlevel 1 (echo [ERROR] Could not rebuild the documentation compiler.& exit /b 1) -(type build\lsp\map\rt.zc & type build\lsp\map\compiler.zc & type src\lsp\server.zc) | build\lsp\zc0docs.exe > build\lsp\src\lsp_genome.h -if errorlevel 1 (echo [ERROR] Could not build the sized documentation genome.& exit /b 1) -set "FINAL_GLEN=" -for /f "tokens=3" %%G in ('findstr /b /c:"#define GLEN" build\lsp\src\lsp_genome.h') do set "FINAL_GLEN=%%G" -if not "!FINAL_GLEN!"=="!DOCS_GLEN!" (echo [ERROR] LSP genome changed length after map sizing.& exit /b 1) -copy /y src\core\core.c build\lsp\src\ >nul +call :lsp_pass %DOCS_WORK% -DZC0DOCS image.bin +if errorlevel 1 exit /b 1 +call :words build\lsp\image.bin +set /a DOCS_SLOT=(WORDS+4095)/4096*4096 +set "DOCS_WORDS=!WORDS!" +call :lsp_pass %DOCS_SLOT% -DZC0DOCS lsp_genome.bin +if errorlevel 1 exit /b 1 +call :words build\lsp\lsp_genome.bin +if not "!WORDS!"=="!DOCS_WORDS!" (echo [ERROR] LSP genome changed length after map sizing.& exit /b 1) +copy /y build\lsp\map\memmap.h build\lsp\memmap.h >nul +"%CC%" -O2 -w -fwrapv -DJTYPE=int -DZY_SANDBOX -DUSE_LSP_GENOME src\core\core.c src\core\io_win.c -Isrc/core -Ibuild/lsp --embed-dir=build/lsp -o build\lsp\zclsp.exe +if errorlevel 1 (echo [ERROR] Failed to build zclsp.exe.& exit /b 1) +if not exist "build\bin" mkdir build\bin +copy /y build\lsp\zclsp.exe build\bin\zclsp.exe >nul +echo [OK] build\bin\zclsp.exe built with !WORDS! words. +exit /b 0 + +REM :lsp_pass genome-slot zc0-define output - lay out the lsp map (with that genome +REM slot, or the preset's own when it is empty) and compile the server image into it +:lsp_pass +if exist build\lsp\map rmdir /s /q build\lsp\map +if "%~1"=="" ( + build\bin\genmap.exe --preset lsp --grow -o build\lsp\map >nul 2>nul +) else ( + build\bin\genmap.exe --preset lsp --grow --genome %~1 -o build\lsp\map >nul 2>nul +) +if errorlevel 1 (echo [ERROR] Could not generate the LSP map.& exit /b 1) +"%CC%" -O1 -w -fwrapv %~2 src\tools\zc0.c -Ibuild/lsp/map -Isrc/core -o build\lsp\zc0.exe +if errorlevel 1 (echo [ERROR] Could not build the LSP stage 0 compiler.& exit /b 1) +"%CC%" -O1 -w -fwrapv src\tools\mkgenome.c -Ibuild/lsp/map -o build\lsp\mkgenome.exe +if errorlevel 1 (echo [ERROR] Could not build the LSP image checker.& exit /b 1) +(type build\lsp\map\rt.zc & type src\core\core.zc & type build\lsp\map\compiler.zc & type src\lsp\server.zc) | build\lsp\zc0.exe > build\lsp\raw.bin +if errorlevel 1 (echo [ERROR] Could not compile the LSP image.& exit /b 1) +build\lsp\mkgenome.exe < build\lsp\raw.bin > build\lsp\%~3 +if errorlevel 1 (echo [ERROR] The LSP image does not match its map.& exit /b 1) +exit /b 0 + +REM :words file - WORDS is the file's length in 64-bit words +:words +set /a WORDS=%~z1/8 +exit /b 0 + +REM the javascript vm: layout, genome and the compiled vm in build\js (needs node and tsc) +:build_js +call :need_genome if errorlevel 1 exit /b 1 -copy /y src\core\io_win.c build\lsp\src\ >nul +if not exist build\js mkdir build\js +node src\js\mklayout.js --map build\core\map\memmap.h --out build\js >nul +if errorlevel 1 (echo [ERROR] Could not write the javascript layout.& exit /b 1) +copy /y build\core\genome.bin build\js\genome.bin >nul +call tsc src\js\tinyvm.ts --target es2020 --lib es2020,dom --module commonjs --outDir build\js +if errorlevel 1 (echo [ERROR] Could not compile the javascript vm; tsc is needed.& exit /b 1) +exit /b 0 + +:build_all +call :build_genome if errorlevel 1 exit /b 1 -copy /y src\core\fops.h build\lsp\src\ >nul +call :build_core if errorlevel 1 exit /b 1 -copy /y src\core\bops.h build\lsp\src\ >nul +call :build_micro if errorlevel 1 exit /b 1 -copy /y src\core\stack.h build\lsp\src\ >nul +call :build_lsp +exit /b %errorlevel% + +:run_tests +call :build_core if errorlevel 1 exit /b 1 -copy /y build\lsp\map\memmap.h build\lsp\src\ >nul +call :build_micro if errorlevel 1 exit /b 1 -"%CC%" -O2 -w -fwrapv -DJTYPE=int -DZY_SANDBOX -DUSE_LSP_GENOME -Ibuild\lsp\src build\lsp\src\core.c build\lsp\src\io_win.c -o bin\zclsp.exe -if errorlevel 1 (echo [ERROR] Failed to build bin\zclsp.exe.& exit /b 1) -echo [OK] bin\zclsp.exe built successfully with %FINAL_GLEN% words. +powershell -NoProfile -ExecutionPolicy Bypass -File "tests\test_win.ps1" +exit /b %errorlevel% + +:clean +rmdir /s /q build 2>nul +echo [OK] Generated build directory removed. exit /b 0 REM ========================================================================= @@ -514,22 +554,24 @@ set "CMD=%~1" set "ARG2=%~2" if /i "%CMD%"=="build" ( - if "%ARG2%"=="" goto :cli_build_all - if /i "%ARG2%"=="all" goto :cli_build_all + if "%ARG2%"=="" ( call :build_all & exit /b !errorlevel! ) + if /i "%ARG2%"=="all" ( call :build_all & exit /b !errorlevel! ) if /i "%ARG2%"=="core" ( call :build_core & exit /b !errorlevel! ) if /i "%ARG2%"=="micro" ( call :build_micro & exit /b !errorlevel! ) if /i "%ARG2%"=="tools" ( call :build_tools & exit /b !errorlevel! ) if /i "%ARG2%"=="lsp" ( call :build_lsp & exit /b !errorlevel! ) + if /i "%ARG2%"=="js" ( call :build_js & exit /b !errorlevel! ) echo Unknown build target: %ARG2% exit /b 1 ) +if /i "%CMD%"=="genome" ( + call :build_genome + exit /b !errorlevel! +) + if /i "%CMD%"=="test" ( - call :build_core - if errorlevel 1 exit /b 1 - call :build_micro - if errorlevel 1 exit /b 1 - powershell -NoProfile -ExecutionPolicy Bypass -File "tests\test_win.ps1" + call :run_tests exit /b !errorlevel! ) @@ -539,11 +581,11 @@ if /i "%CMD%"=="run" ( echo Usage: tinyvm.bat run path\to\program.zc exit /b 1 ) - if not exist "bin\zy.exe" ( + if not exist "build\bin\zy.exe" ( call :build_core if errorlevel 1 exit /b 1 ) - bin\zy.exe < "%SOURCE_ARG%" + build\bin\zy.exe < "%SOURCE_ARG%" exit /b !errorlevel! ) @@ -553,11 +595,11 @@ if /i "%CMD%"=="run-micro" ( echo Usage: tinyvm.bat run-micro path\to\program.zc exit /b 1 ) - if not exist "bin\zym.exe" ( + if not exist "build\bin\zym.exe" ( call :build_micro if errorlevel 1 exit /b 1 ) - bin\zym.exe < "%SOURCE_ARG%" + build\bin\zym.exe < "%SOURCE_ARG%" exit /b !errorlevel! ) @@ -567,7 +609,9 @@ if /i "%CMD%"=="js" ( echo Usage: tinyvm.bat js path\to\program.zc exit /b 1 ) - node src\js\tinyvm.js "%SOURCE_ARG%" + call :build_js + if errorlevel 1 exit /b 1 + node build\js\tinyvm.js "%SOURCE_ARG%" exit /b !errorlevel! ) @@ -577,62 +621,43 @@ if /i "%CMD%"=="lsp" ( echo Usage: tinyvm.bat lsp path\to\program.zc exit /b 1 ) - if not exist "bin\zclsp.exe" ( + if not exist "build\bin\zclsp.exe" ( call :build_lsp if errorlevel 1 exit /b 1 ) - bin\zclsp.exe < "%SOURCE_ARG%" + build\bin\zclsp.exe < "%SOURCE_ARG%" exit /b !errorlevel! ) if /i "%CMD%"=="repl" ( - if not exist "bin\zy.exe" ( + if not exist "build\bin\zy.exe" ( call :build_core if errorlevel 1 exit /b 1 ) - (type examples\repl.zc & echo ` & more) | bin\zy.exe + (type examples\repl.zc & echo ` & more) | build\bin\zy.exe exit /b !errorlevel! ) if /i "%CMD%"=="clean" ( - rmdir /s /q build bin 2>nul - echo [OK] Generated bin and build directories removed. + call :clean exit /b 0 ) -if /i "%CMD%"=="help" ( - goto :show_help -) -if /i "%CMD%"=="-h" ( - goto :show_help -) -if /i "%CMD%"=="--help" ( - goto :show_help -) - echo Unknown command: %CMD% echo. exit /b 1 :show_help echo usage: tinyvm.bat [command] echo tinyvm.bat launch interactive build ^& run menu -echo tinyvm.bat build [all^|core^|micro^|tools^|lsp] -echo compile TinyVM executables +echo tinyvm.bat build [all^|core^|micro^|tools^|lsp^|js] +echo compile TinyVM executables into build\bin +echo tinyvm.bat genome rebuild build\core\genome.bin and check the fixpoint echo tinyvm.bat run ^<file.zc^> run a .zc program on VM core (zy.exe) echo tinyvm.bat run-micro ^<file.zc^> run a .zc program on microcore (zym.exe) echo tinyvm.bat js ^<file.zc^> run a .zc program on the JavaScript VM (node) echo tinyvm.bat lsp ^<file.zc^> check/highlight a .zc program with zclsp.exe -echo tinyvm.bat test run the full Windows test suite +echo tinyvm.bat test run the Windows test suite echo tinyvm.bat repl launch the resident compiler REPL -echo tinyvm.bat clean remove generated bin and build directories +echo tinyvm.bat clean remove the generated build directory +echo the compiler must support c23 #embed: clang 19 or gcc 15 and later. exit /b 0 - -:cli_build_all -call :build_core -if errorlevel 1 exit /b 1 -call :build_micro -if errorlevel 1 exit /b 1 -call :build_tools -if errorlevel 1 exit /b 1 -call :build_lsp -exit /b !errorlevel! @@ -27,9 +27,9 @@ HELP build() { case "${1:-all}" in all) make ;; - core) make bin/zy ;; - micro) make bin/zym ;; - tools) make bin/zc0 bin/mkgenome bin/genmap ;; + core) make build/bin/zy ;; + micro) make build/bin/zym ;; + tools) make build/bin/zc0 build/bin/mkgenome build/bin/genmap ;; lsp) make lsp ;; *) printf 'Unknown build target: %s\n' "$1" >&2; return 2 ;; esac @@ -47,9 +47,9 @@ run_file() { [[ $# == 2 ]] || { printf 'Usage: tinyvm.sh %s FILE.zc\n' "$mode" >&2; return 2; } input=$(source_path "$2") || return case $mode in - run) exe=bin/zy ;; - run-micro) exe=bin/zym ;; - lsp) exe=bin/zclsp ;; + run) exe=build/bin/zy ;; + run-micro) exe=build/bin/zym ;; + lsp) exe=build/bin/zclsp ;; js) command -v node >/dev/null || { echo 'node.js is required.' >&2; return 1; }; make js || return; node build/js/tinyvm.js "$input"; return ;; esac make "$exe" || return @@ -57,9 +57,9 @@ run_file() { } repl() { - make bin/zy || return + make build/bin/zy || return echo 'Enter statements or fn definitions, ending each chunk with a backtick. Ctrl-D exits.' - { cat examples/repl.zc; printf '`'; cat; } | bin/zy + { cat examples/repl.zc; printf '`'; cat; } | build/bin/zy } dispatch() { |
