aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
committerGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
commitecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch)
tree70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /tests
a
Diffstat (limited to 'tests')
-rw-r--r--tests/bench/bench.sh36
-rw-r--r--tests/bench/bench_bytes.sh33
-rw-r--r--tests/bench/bench_float.sh54
-rw-r--r--tests/bench/bench_share.sh32
-rw-r--r--tests/bench/bench_strings.sh32
-rwxr-xr-xtests/bench/run.py304
-rwxr-xr-xtests/bench/run.sh5
-rw-r--r--tests/bench/selfmod.zc19
-rw-r--r--tests/bench/sieve.c58
-rw-r--r--tests/bench/sieve.zc51
-rw-r--r--tests/bench/trees.c85
-rw-r--r--tests/bench/trees.zc92
-rw-r--r--tests/data/hi.txt1
-rw-r--r--tests/data/sbv.c18
-rw-r--r--tests/drivers/code_address_growth_test.py23
-rw-r--r--tests/drivers/code_emission_growth_test.py40
-rw-r--r--tests/drivers/compiler_growth_test.py82
-rw-r--r--tests/drivers/core_lsp_test.js43
-rw-r--r--tests/drivers/diagcache_test.js140
-rw-r--r--tests/drivers/diagnostic_revision_test.js47
-rw-r--r--tests/drivers/enhancement_backends_test.py45
-rw-r--r--tests/drivers/ext_cache_test.js154
-rw-r--r--tests/drivers/float_backends_test.py47
-rw-r--r--tests/drivers/frame_growth_test.py32
-rw-r--r--tests/drivers/genome_blob_test.py32
-rw-r--r--tests/drivers/hover_features_test.js85
-rw-r--r--tests/drivers/hover_test.js23
-rw-r--r--tests/drivers/image_growth_test.py25
-rw-r--r--tests/drivers/image_run.js14
-rw-r--r--tests/drivers/inference_growth_test.py34
-rw-r--r--tests/drivers/layout_config_test.py26
-rw-r--r--tests/drivers/learn.py64
-rw-r--r--tests/drivers/local_index_test.py23
-rw-r--r--tests/drivers/lsp_growth_test.js53
-rw-r--r--tests/drivers/lsp_live.js23
-rw-r--r--tests/drivers/lsp_sandbox.js22
-rw-r--r--tests/drivers/lsp_stream.js11
-rw-r--r--tests/drivers/lsp_test.js246
-rw-r--r--tests/drivers/lsp_vm.js112
-rw-r--r--tests/drivers/lsp_wasm.js112
-rw-r--r--tests/drivers/memory_limits_test.py23
-rw-r--r--tests/drivers/parser_growth_test.py48
-rw-r--r--tests/drivers/protocol_growth_test.js34
-rw-r--r--tests/drivers/seed_growth_test.py25
-rw-r--r--tests/drivers/stack_growth_test.py58
-rw-r--r--tests/drivers/storage_analysis_test.js71
-rw-r--r--tests/drivers/vscode_hover_test.js48
-rw-r--r--tests/drivers/wasm_test.js28
-rw-r--r--tests/drivers/word_literals_test.py19
-rw-r--r--tests/errors/alias_field.zc9
-rw-r--r--tests/errors/alias_index.zc9
-rw-r--r--tests/errors/alias_plus.zc2
-rw-r--r--tests/errors/alias_scope.zc6
-rw-r--r--tests/errors/back_to_m.zc2
-rw-r--r--tests/errors/borrow_escape.zc3
-rw-r--r--tests/errors/branch_mismatch.zc3
-rw-r--r--tests/errors/child_dies.zc3
-rw-r--r--tests/errors/child_recast.zc2
-rw-r--r--tests/errors/copy_owned.zc2
-rw-r--r--tests/errors/d_into_block.zc2
-rw-r--r--tests/errors/d_kept.zc2
-rw-r--r--tests/errors/did_you_mean.zc3
-rw-r--r--tests/errors/discard_u.zc3
-rw-r--r--tests/errors/found_token.zc2
-rw-r--r--tests/errors/give_m.zc3
-rw-r--r--tests/errors/global_store.zc3
-rw-r--r--tests/errors/leak_overwrite.zc2
-rw-r--r--tests/errors/lend_d.zc4
-rw-r--r--tests/errors/loop_move.zc3
-rw-r--r--tests/errors/plain_return.zc3
-rw-r--r--tests/errors/return_m.zc3
-rw-r--r--tests/errors/tag_mismatch.zc2
-rw-r--r--tests/errors/temporary_output.zc9
-rw-r--r--tests/errors/u_dropped.zc2
-rw-r--r--tests/errors/u_skip_level.zc2
-rw-r--r--tests/errors/unchecked_touch.zc2
-rw-r--r--tests/errors/unused.zc2
-rw-r--r--tests/errors/use_after_give.zc3
-rw-r--r--tests/langerrors/cfunc_name.zc2
-rw-r--r--tests/langerrors/const_index.zc2
-rw-r--r--tests/langerrors/extern_bigret.zc4
-rw-r--r--tests/langerrors/extern_struct.zc3
-rw-r--r--tests/langerrors/extern_type.zc3
-rw-r--r--tests/langerrors/fixed_nolen.zc2
-rw-r--r--tests/langerrors/fixed_push.zc2
-rw-r--r--tests/langerrors/fixed_to_dyn.zc4
-rw-r--r--tests/langerrors/gen_undeclared.zc3
-rw-r--r--tests/langerrors/o3_addr.zc2
-rw-r--r--tests/langerrors/o3_asm.zc2
-rw-r--r--tests/langerrors/o3_let.zc2
-rw-r--r--tests/langerrors/o3_param.zc3
-rw-r--r--tests/langerrors/parseword_noread.zc5
-rw-r--r--tests/langerrors/pk_addr.zc6
-rw-r--r--tests/langerrors/tagenum_args.zc3
-rw-r--r--tests/langerrors/tagenum_bind.zc10
-rw-r--r--tests/langerrors/tagenum_value.zc3
-rw-r--r--tests/programs/alias.zc21
-rw-r--r--tests/programs/allocfast.zc45
-rw-r--r--tests/programs/annotate.expected33
-rw-r--r--tests/programs/annotate.zc33
-rw-r--r--tests/programs/arena.zc16
-rw-r--r--tests/programs/args.zc16
-rw-r--r--tests/programs/arrays.zc23
-rw-r--r--tests/programs/backtick.zc9
-rw-r--r--tests/programs/bareexit.zc7
-rw-r--r--tests/programs/bareio.zc28
-rw-r--r--tests/programs/basics.zc21
-rw-r--r--tests/programs/bench.zc29
-rw-r--r--tests/programs/bench_bytes.zc19
-rw-r--r--tests/programs/bench_share.zc30
-rw-r--r--tests/programs/bench_strings.zc27
-rw-r--r--tests/programs/bitops.expected2
-rw-r--r--tests/programs/bitops.zc26
-rw-r--r--tests/programs/borrowmove.zc2
-rw-r--r--tests/programs/bounds.zc28
-rw-r--r--tests/programs/bounds_hi.zc8
-rw-r--r--tests/programs/bounds_lo.zc8
-rw-r--r--tests/programs/bstr_io.zc22
-rw-r--r--tests/programs/buddy.zc16
-rw-r--r--tests/programs/bytes.zc44
-rw-r--r--tests/programs/cabi.expected5
-rw-r--r--tests/programs/cabi.zc46
-rw-r--r--tests/programs/cabi_o3.expected2
-rw-r--r--tests/programs/cabi_o3.zc30
-rw-r--r--tests/programs/cabi_sized.expected1
-rw-r--r--tests/programs/cabi_sized.zc28
-rw-r--r--tests/programs/capture.zc31
-rw-r--r--tests/programs/closure.zc52
-rw-r--r--tests/programs/csyn.zc18
-rw-r--r--tests/programs/decl_undefined.zc2
-rw-r--r--tests/programs/discard_m.zc3
-rw-r--r--tests/programs/doublefree.zc9
-rw-r--r--tests/programs/enhance.zc28
-rw-r--r--tests/programs/enumscope.zc9
-rw-r--r--tests/programs/enumtype.zc12
-rw-r--r--tests/programs/f32.expected1
-rw-r--r--tests/programs/f32.zc37
-rw-r--r--tests/programs/f32o3.zc14
-rw-r--r--tests/programs/fbench.zc37
-rw-r--r--tests/programs/float.zc32
-rw-r--r--tests/programs/fo3.zc50
-rw-r--r--tests/programs/ftext.zc37
-rw-r--r--tests/programs/ftruth.zc22
-rw-r--r--tests/programs/ftype.zc47
-rw-r--r--tests/programs/gc.zc18
-rw-r--r--tests/programs/generics.expected2
-rw-r--r--tests/programs/generics.zc54
-rw-r--r--tests/programs/genmeth.expected1
-rw-r--r--tests/programs/genmeth.zc36
-rw-r--r--tests/programs/growheap.zc32
-rw-r--r--tests/programs/growone.zc9
-rw-r--r--tests/programs/infer_return.zc3
-rw-r--r--tests/programs/interior.zc17
-rw-r--r--tests/programs/joins.zc31
-rw-r--r--tests/programs/manyerrors.zc6
-rw-r--r--tests/programs/memleak.zc11
-rw-r--r--tests/programs/memory_bounds.zc13
-rw-r--r--tests/programs/meta.zc43
-rw-r--r--tests/programs/methodown.zc15
-rw-r--r--tests/programs/misc.zc27
-rw-r--r--tests/programs/names.zc9
-rw-r--r--tests/programs/o3.zc75
-rw-r--r--tests/programs/o3_bounds.zc2
-rw-r--r--tests/programs/o3bytes.zc52
-rw-r--r--tests/programs/o3call.zc29
-rw-r--r--tests/programs/o3copy.zc12
-rw-r--r--tests/programs/o3deep.zc3
-rw-r--r--tests/programs/o3deep_over.zc3
-rw-r--r--tests/programs/o3direct.expected2
-rw-r--r--tests/programs/o3direct.zc33
-rw-r--r--tests/programs/o3fat.zc20
-rw-r--r--tests/programs/o3licm.zc46
-rw-r--r--tests/programs/o3push.expected1
-rw-r--r--tests/programs/o3push.zc38
-rw-r--r--tests/programs/o3scopes.zc17
-rw-r--r--tests/programs/o3zero.zc8
-rw-r--r--tests/programs/overflow_data.zc1
-rw-r--r--tests/programs/overflow_frame.zc2
-rw-r--r--tests/programs/overflow_return.zc2
-rw-r--r--tests/programs/packedfields.expected2
-rw-r--r--tests/programs/packedfields.zc42
-rw-r--r--tests/programs/quarantine.zc25
-rw-r--r--tests/programs/rawpid.zc6
-rw-r--r--tests/programs/rawsys.zc5
-rw-r--r--tests/programs/recycle.zc23
-rw-r--r--tests/programs/roots.zc17
-rw-r--r--tests/programs/scrub.zc9
-rw-r--r--tests/programs/share.zc34
-rw-r--r--tests/programs/share_dbg.zc10
-rw-r--r--tests/programs/sized.expected5
-rw-r--r--tests/programs/sized.zc80
-rw-r--r--tests/programs/sizedvec.expected2
-rw-r--r--tests/programs/sizedvec.zc39
-rw-r--r--tests/programs/static.zc2
-rw-r--r--tests/programs/stdbuf.zc18
-rw-r--r--tests/programs/stdmore.zc32
-rw-r--r--tests/programs/strings.zc37
-rw-r--r--tests/programs/structret.zc24
-rw-r--r--tests/programs/structval.zc26
-rw-r--r--tests/programs/sys.zc11
-rw-r--r--tests/programs/tagenum.zc26
-rw-r--r--tests/programs/tagenum_two.zc20
-rw-r--r--tests/programs/tagenum_warn.zc17
-rw-r--r--tests/programs/typedmethods.zc16
-rw-r--r--tests/programs/typeoff.zc2
-rw-r--r--tests/programs/typeok.zc21
-rw-r--r--tests/programs/types.zc55
-rw-r--r--tests/programs/uaf.zc8
-rw-r--r--tests/programs/uaf_in.zc9
-rw-r--r--tests/programs/umd.zc50
-rw-r--r--tests/programs/undefined_call.zc1
-rw-r--r--tests/programs/use_std.zc9
-rw-r--r--tests/programs/wide.zc9
-rw-r--r--tests/programs/zc0plain.zc23
-rw-r--r--tests/programs/zc0typed.zc23
-rwxr-xr-xtests/runerr.sh10
-rwxr-xr-xtests/test.sh577
-rw-r--r--tests/test_win.ps1258
-rw-r--r--tests/typeerrors/arg.zc5
-rw-r--r--tests/typeerrors/arith.zc3
-rw-r--r--tests/typeerrors/assign.zc3
-rw-r--r--tests/typeerrors/call.zc3
-rw-r--r--tests/typeerrors/deref.zc3
-rw-r--r--tests/typeerrors/enum_int.zc4
-rw-r--r--tests/typeerrors/enum_other.zc5
-rw-r--r--tests/typeerrors/field.zc3
-rw-r--r--tests/typeerrors/generic.zc4
-rw-r--r--tests/typeerrors/notstruct.zc2
-rw-r--r--tests/typeerrors/ret.zc4
-rw-r--r--tests/typeerrors/voidret.zc4
230 files changed, 6254 insertions, 0 deletions
diff --git a/tests/bench/bench.sh b/tests/bench/bench.sh
new file mode 100644
index 0000000..ab6b07d
--- /dev/null
+++ b/tests/bench/bench.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+# 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
+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"); }
+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)"); }
+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
+t(){ local s e; s=$(date +%s.%N); (cat tests/programs/bench.zc; printf '`%s%s' $2 $3) | $1 >/dev/null; e=$(date +%s.%N); echo "$e - $s" | bc; }
+best(){ local b=999 x i; for i in 1 2 3; do x=$(t "$1" $2 $3); b=$(echo "if ($x < $b) $x else $b" | bc); done; echo $b; }
+# grows the run count until the difference is at least a second, so the noise
+# of starting up is small next to it
+per(){
+ local one n=4 many
+ one=$(best "$1" $2 1)
+ while :; do
+ many=$(best "$1" $2 $n)
+ if [ "$(echo "$many - $one > 1" | bc)" = 1 ] || [ $n -ge 4096 ]; then break; fi
+ n=$((n * 4))
+ done
+ echo "scale=6; ($many - $one) / ($n - 1)" | bc
+}
+printf "%-14s %-8s %10s %10s %8s\n" backend kernel "plain ms" "o3 ms" speedup
+for i in "${!vms[@]}"; do
+ for k in "collatz a b" "sieve c d" "digits e f" "fib g h"; do
+ set -- $k
+ p=$(per "${vms[$i]}" $2); o=$(per "${vms[$i]}" $3)
+ printf "%-14s %-8s %10.2f %10.2f %7.1fx\n" "${names[$i]}" $1 $(echo "$p * 1000" | bc) $(echo "$o * 1000" | bc) $(echo "scale=2; $p / $o" | bc)
+ done
+done
diff --git a/tests/bench/bench_bytes.sh b/tests/bench/bench_bytes.sh
new file mode 100644
index 0000000..3287a91
--- /dev/null
+++ b/tests/bench/bench_bytes.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# byte-load loop, timed on a run-only image so the resident compiler is not
+# in the process. the slope between 1 repetition and N drops startup.
+# prints: backend ms/rep checksum
+cd "$(dirname "$0")/../.."
+T=$(mktemp -d)
+trap 'rm -rf "$T"' EXIT
+gcc -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_bytes.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
+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) \
+ && { 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 "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zyj" \
+ && { vms+=("qemu-aarch64 $T/zyj"); names+=("arm64"); }
+fi
+# seconds for $2 repetitions on vm $1
+once(){ local s e; s=$(date +%s.%N); printf '%s' "$2" | $1 >/tmp/bench_bytes.out; e=$(date +%s.%N); python3 -c "print($e - $s)"; }
+printf "%-10s %12s %s\n" backend "ms/rep" checksum
+for i in "${!vms[@]}"; do
+ one=$(once "${vms[$i]}" 1)
+ many=$(once "${vms[$i]}" 40)
+ ms=$(python3 -c "print(round(($many - $one) / 39 * 1000, 3))")
+ sum=$(tr -d '\n' < /tmp/bench_bytes.out)
+ printf "%-10s %12s %s\n" "${names[$i]}" "$ms" "$sum"
+done
diff --git a/tests/bench/bench_float.sh b/tests/bench/bench_float.sh
new file mode 100644
index 0000000..794e976
--- /dev/null
+++ b/tests/bench/bench_float.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+# plain float loop against the same loop lifted by o3. the program is
+# tests/programs/fbench.zc: 'a' is plain, 'b' is o3, and the digits are repetitions.
+# the slope between 1 repetition and N drops startup and compile time, so
+# this does not bootstrap the compiler. each kernel is poly() of 20000,
+# and (int) of one call is 59997.
+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" || 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"); }
+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"); }
+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"); }
+fi
+src=$(cat tests/programs/fbench.zc)
+# milliseconds per call of kernel $2 (a or b) on vm $1
+per() {
+ python3 - "$1" "$2" "$src" << 'PY'
+import sys, time, subprocess
+vm, which, src = sys.argv[1], sys.argv[2], sys.argv[3].encode()
+def once(arg):
+ t0 = time.perf_counter()
+ p = subprocess.run(vm.split(), input=src + b"`" + arg.encode(), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
+ return time.perf_counter() - t0, p.returncode, p.stdout
+def best(arg):
+ b, out = 1e9, b""
+ for _ in range(3):
+ d, r, o = once(arg)
+ if r == 0 and d < b:
+ b, out = d, o
+ return b, out
+one, _ = best(which + "1")
+n = 4
+while True:
+ many, out = best("%s%d" % (which, n))
+ if many - one > 0.8 or n >= 1024:
+ break
+ n *= 4
+print("%.3f" % ((many - one) / (n - 1) * 1000))
+print(out.decode().strip(), file=sys.stderr)
+PY
+}
+printf "%-10s %12s %12s %8s\n" backend "plain ms" "o3 ms" speedup
+for i in "${!vms[@]}"; do
+ p=$(per "${vms[$i]}" a)
+ o=$(per "${vms[$i]}" b)
+ printf "%-10s %12s %12s %8s\n" "${names[$i]}" "$p" "$o" "$(python3 -c "print('%.2fx' % ($p / $o) if $o else 'n/a')")"
+done
diff --git a/tests/bench/bench_share.sh b/tests/bench/bench_share.sh
new file mode 100644
index 0000000..b449e09
--- /dev/null
+++ b/tests/bench/bench_share.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+# walk a shared cons spine from two parents, timed on a run-only image so the
+# resident compiler is not in the process. the slope between 1 repetition
+# and N drops startup. prints: backend ms/rep checksum
+cd "$(dirname "$0")/../.."
+T=$(mktemp -d)
+trap 'rm -rf "$T"' EXIT
+gcc -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
+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) \
+ && { 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 "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zyj" \
+ && { vms+=("qemu-aarch64 $T/zyj"); names+=("arm64"); }
+fi
+once(){ local s e; s=$(date +%s.%N); printf '%s' "$2" | $1 >/tmp/bench_share.out; e=$(date +%s.%N); python3 -c "print($e - $s)"; }
+printf "%-10s %12s %s\n" backend "ms/rep" checksum
+for i in "${!vms[@]}"; do
+ one=$(once "${vms[$i]}" 1)
+ many=$(once "${vms[$i]}" 40)
+ ms=$(python3 -c "print(round(($many - $one) / 39 * 1000, 3))")
+ sum=$(tr -d '\n' < /tmp/bench_share.out)
+ printf "%-10s %12s %s\n" "${names[$i]}" "$ms" "$sum"
+done
diff --git a/tests/bench/bench_strings.sh b/tests/bench/bench_strings.sh
new file mode 100644
index 0000000..c353342
--- /dev/null
+++ b/tests/bench/bench_strings.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+# byte-string scan, concat and slice, timed on a run-only image so the
+# resident compiler is not in the process. the slope between 1 repetition
+# and N drops startup. prints: backend ms/rep checksum
+cd "$(dirname "$0")/../.."
+T=$(mktemp -d)
+trap 'rm -rf "$T"' EXIT
+gcc -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
+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) \
+ && { 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 "$T/run/core.c" "$T/run/io_linux.c" -o "$T/zyj" \
+ && { vms+=("qemu-aarch64 $T/zyj"); names+=("arm64"); }
+fi
+once(){ local s e; s=$(date +%s.%N); printf '%s' "$2" | $1 >/tmp/bench_strings.out; e=$(date +%s.%N); python3 -c "print($e - $s)"; }
+printf "%-10s %12s %s\n" backend "ms/rep" checksum
+for i in "${!vms[@]}"; do
+ one=$(once "${vms[$i]}" 1)
+ many=$(once "${vms[$i]}" 40)
+ ms=$(python3 -c "print(round(($many - $one) / 39 * 1000, 3))")
+ sum=$(tr -d '\n' < /tmp/bench_strings.out)
+ printf "%-10s %12s %s\n" "${names[$i]}" "$ms" "$sum"
+done
diff --git a/tests/bench/run.py b/tests/bench/run.py
new file mode 100755
index 0000000..69cb943
--- /dev/null
+++ b/tests/bench/run.py
@@ -0,0 +1,304 @@
+#!/usr/bin/env python3
+# Time tests/bench/sieve and tests/bench/trees on C and on zc.
+# The number is the slope between 1 repetition and N: that drops process
+# startup and, for zc, the time spent compiling the benchmark. It does not
+# bootstrap the compiler. Each cell is the median of TRIALS slopes, with the
+# min and the max as the spread.
+#
+# python3 tests/bench/run.py # everything
+# python3 tests/bench/run.py check # checksums only
+# python3 tests/bench/run.py sieve # one program
+# TRIALS=5 GAP=0.8 python3 tests/bench/run.py
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent.parent
+BENCH = Path(__file__).resolve().parent
+TRIALS = int(os.environ.get("TRIALS", "5"))
+GAP = float(os.environ.get("GAP", "0.8"))
+NMAX = int(os.environ.get("NMAX", "200000"))
+SIEVE_N = b"9592" # primes below 100000
+TREE_N = b"8191" # nodes in a full tree of depth 12
+
+
+def log(msg):
+ print(msg, file=sys.stderr, flush=True)
+
+
+def run(cmd, stdin=None, env=None, cwd=None):
+ e = os.environ.copy()
+ if env:
+ e.update(env)
+ t0 = time.perf_counter()
+ p = subprocess.run(cmd, input=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=e, cwd=cwd)
+ return time.perf_counter() - t0, p.returncode, p.stdout, p.stderr
+
+
+def pin(cmd):
+ if shutil.which("taskset"):
+ return ["taskset", "-c", "0"] + list(cmd)
+ return list(cmd)
+
+
+def need(cmd, cwd=None):
+ p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
+ if p.returncode != 0:
+ sys.stderr.write(p.stderr.decode(errors="replace"))
+ raise SystemExit(f"command failed: {' '.join(cmd)}")
+ return p
+
+
+def machine_lines():
+ lines = []
+ u = subprocess.check_output(["uname", "-srm"], text=True).strip()
+ lines.append(u)
+ model = ""
+ if Path("/proc/cpuinfo").exists():
+ for line in Path("/proc/cpuinfo").read_text().splitlines():
+ if line.startswith("model name"):
+ model = line.split(":", 1)[1].strip()
+ break
+ if model:
+ lines.append(model)
+ lines.append("gcc " + subprocess.check_output(["gcc", "-dumpversion"], text=True).strip())
+ if shutil.which("clang"):
+ lines.append(subprocess.check_output(["clang", "--version"], text=True).splitlines()[0])
+ if shutil.which("qemu-aarch64"):
+ lines.append(subprocess.check_output(["qemu-aarch64", "--version"], text=True).splitlines()[0])
+ if shutil.which("llvm-config"):
+ lines.append("llvm " + subprocess.check_output(["llvm-config", "--version"], text=True).strip())
+ lines.append("pinned to cpu 0 with taskset" if shutil.which("taskset") else "not pinned")
+ return lines
+
+
+def build(td):
+ td = Path(td)
+ log("building vms")
+ 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)
+ 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)
+ 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)
+ vms["arm64"] = ["qemu-aarch64", str(zyj)]
+ log("building C")
+ bins = {}
+ for comp, tag in (("gcc", "gcc"), ("clang", "clang")):
+ if comp == "clang" and not shutil.which("clang"):
+ continue
+ for lto in (False, True):
+ name = tag + ("-lto" if lto else "")
+ flags = [comp, "-O3", "-march=native"] + (["-flto"] if lto else [])
+ sw = td / f"sieve-{name}"
+ tw = td / f"trees-{name}"
+ need(flags + ["-o", str(sw), str(BENCH / "sieve.c")])
+ need(flags + ["-o", str(tw), str(BENCH / "trees.c")])
+ bins[f"sieve-{name}"] = sw
+ bins[f"trees-{name}"] = tw
+ # PGO: train and use the same output path so the .gcda name matches.
+ if shutil.which("gcc"):
+ for src, key, arg in (("sieve.c", "sieve-gcc-pgo", ["word", "80"]), ("trees.c", "trees-gcc-pgo", ["malloc", "40"])):
+ out = td / key
+ prof = td / f"prof-{key}"
+ prof.mkdir()
+ base = ["gcc", "-O3", "-march=native", "-flto", "-o", str(out), str(BENCH / src)]
+ need(base + [f"-fprofile-generate={prof}"])
+ need([str(out), *arg])
+ p = subprocess.run(base + [f"-fprofile-use={prof}", "-fprofile-correction"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ if p.returncode != 0 or b"profile count data file not found" in p.stderr:
+ log(f"PGO skipped for {key}: {p.stderr.decode(errors='replace')[:200]}")
+ else:
+ bins[key] = out
+ log(f"PGO applied for {key}")
+ return vms, bins
+
+
+def slope(invoke, expected, trials=TRIALS):
+ def once(n):
+ cmd, stdin, env = invoke(n)
+ dt, rc, out, err = run(pin(cmd), stdin, env)
+ if rc != 0 or out.strip() != expected:
+ raise SystemExit(
+ f"bad run n={n} rc={rc}\ncmd={cmd}\nout={out[:200]!r}\nerr={err[:400]!r}"
+ )
+ return dt
+
+ dt1 = once(1)
+ n = 16
+ per = 0.0
+ while n <= NMAX:
+ dtn = once(n)
+ per = (dtn - dt1) / (n - 1)
+ gap = dtn - dt1
+ if per > 0 and gap >= GAP:
+ break
+ if per > 0 and gap >= 0.05:
+ need_n = int(GAP / per) + 1
+ need_n = min(NMAX, max(need_n, n))
+ if need_n != n:
+ dtn = once(need_n)
+ per = (dtn - dt1) / (need_n - 1)
+ n = need_n
+ # a noisy calibrating run can undershoot the gap. grow until
+ # the kernel time actually dominates, or we hit the cap.
+ while n < NMAX and per > 0 and (per * (n - 1)) < GAP:
+ n = min(NMAX, max(n + 1, int(GAP / per) + 1))
+ dtn = once(n)
+ per = (dtn - dt1) / (n - 1)
+ break
+ nxt = n * 4
+ if nxt > NMAX:
+ n = NMAX
+ break
+ n = nxt
+ samples = []
+ for i in range(trials):
+ a = once(1)
+ b = once(n)
+ ms = (b - a) / (n - 1) * 1000.0
+ samples.append(ms)
+ log(f" trial {i + 1}/{trials}: {ms:.4f} ms (n={n})")
+ samples.sort()
+ mid = samples[len(samples) // 2]
+ return {"median": mid, "min": samples[0], "max": samples[-1], "n": n, "samples": samples}
+
+
+def zc_call(vm, src, which, env=None):
+ def invoke(n):
+ return vm, src + b"`" + f"{which}{n}".encode(), env
+ return invoke
+
+
+def c_call(binary, mode):
+ def invoke(n):
+ return [str(binary), mode, str(n)], None, None
+ return invoke
+
+
+def show(title, rows):
+ print()
+ print(title)
+ print(f"{'path':<52} {'median ms':>10} {'min':>10} {'max':>10} {'reps':>8}")
+ for name, r in rows:
+ print(f"{name:<52} {r['median']:10.4f} {r['min']:10.4f} {r['max']:10.4f} {r['n']:8d}")
+ print("samples:")
+ for name, r in rows:
+ nums = " ".join(f"{s:.4f}" for s in r["samples"])
+ print(f" {name}: {nums}")
+ sys.stdout.flush()
+
+
+def main():
+ which = sys.argv[1] if len(sys.argv) > 1 else "all"
+ os.chdir(ROOT)
+ for line in machine_lines():
+ print(line)
+ print(f"trials {TRIALS}, gap {GAP}s, slope of 1 vs N, median of the trials")
+ print("arm64 is qemu-user on this host, not native arm64 hardware")
+ sys.stdout.flush()
+ td = tempfile.mkdtemp(prefix="zcbench-")
+ log(f"work dir {td}")
+ vms, bins = build(td)
+ sieve_src = (BENCH / "sieve.zc").read_bytes()
+ tree_src = (BENCH / "trees.zc").read_bytes()
+
+ log("checksums")
+ checks = [
+ (pin(vms["interp"]), sieve_src + b"`a1", SIEVE_N, None),
+ (pin(vms["interp"]), sieve_src + b"`b1", SIEVE_N, None),
+ (pin(vms["interp"]), sieve_src + b"`c1", SIEVE_N, None),
+ (pin(vms["interp"]), tree_src + b"`a2", TREE_N, None),
+ (pin(vms["interp"]), tree_src + b"`b1", TREE_N, None),
+ (pin(vms["interp"]), tree_src + b"`c1", TREE_N, None),
+ (pin([str(bins["sieve-gcc"]), "word", "1"]), None, SIEVE_N, None),
+ (pin([str(bins["sieve-gcc"]), "byte", "1"]), None, SIEVE_N, None),
+ (pin([str(bins["trees-gcc"]), "malloc", "1"]), None, TREE_N, None),
+ (pin([str(bins["trees-gcc"]), "arena", "1"]), None, TREE_N, None),
+ ]
+ if "llvm" in vms:
+ checks.append((pin(vms["llvm"]), sieve_src + b"`c1", SIEVE_N, {"ZYHOT": "1"}))
+ if "arm64" in vms:
+ checks.append((pin(vms["arm64"]), sieve_src + b"`c1", SIEVE_N, None))
+ for cmd, stdin, expect, env in checks:
+ dt, rc, out, err = run(cmd, stdin, env)
+ if rc != 0 or out.strip() != expect:
+ raise SystemExit(f"checksum failed {cmd} rc={rc} out={out!r} err={err[:300]!r}")
+ log(f" ok {expect.decode()} in {dt:.2f}s")
+ if which == "check":
+ print("checksums ok")
+ return
+
+ sieve_rows = []
+ tree_rows = []
+
+ def add(rows, name, invoke, expect):
+ log(f"timing {name}")
+ rows.append((name, slope(invoke, expect)))
+ r = rows[-1][1]
+ log(f" median {r['median']:.4f} ms spread {r['min']:.4f}..{r['max']:.4f}")
+
+ if which in ("all", "sieve"):
+ for key, label, mode in (
+ ("sieve-gcc", "gcc -O3 -march=native, word flags", "word"),
+ ("sieve-gcc-lto", "gcc -O3 -march=native -flto, word flags", "word"),
+ ("sieve-gcc-pgo", "gcc -O3 -march=native -flto PGO, word flags", "word"),
+ ("sieve-clang", "clang -O3 -march=native, word flags", "word"),
+ ("sieve-clang-lto", "clang -O3 -march=native -flto, word flags", "word"),
+ ("sieve-gcc-lto", "gcc -O3 -march=native -flto, byte flags", "byte"),
+ ):
+ if key not in bins:
+ continue
+ add(sieve_rows, label, c_call(bins[key], mode), SIEVE_N)
+ add(sieve_rows, "zc interp, plain, bounds-checked []int", zc_call(vms["interp"], sieve_src, "a"), SIEVE_N)
+ add(sieve_rows, "zc interp, o3, bounds-checked []int", zc_call(vms["interp"], sieve_src, "b"), SIEVE_N)
+ add(sieve_rows, "zc interp, o3, raw *int", zc_call(vms["interp"], sieve_src, "c"), SIEVE_N)
+ if "llvm" in vms:
+ hot = {"ZYHOT": "1"}
+ add(sieve_rows, "zc llvm O3, ZYHOT=1, o3 raw *int", zc_call(vms["llvm"], sieve_src, "c", hot), SIEVE_N)
+ add(sieve_rows, "zc llvm O3, ZYHOT=1, o3 bounds-checked", zc_call(vms["llvm"], sieve_src, "b", hot), SIEVE_N)
+ if "arm64" in vms:
+ add(sieve_rows, "zc arm64 jit, qemu-user, o3 raw *int", zc_call(vms["arm64"], sieve_src, "c"), SIEVE_N)
+ show("sieve: primes below 100000, checksum 9592", sieve_rows)
+
+ if which in ("all", "trees"):
+ for key, label, mode in (
+ ("trees-gcc", "gcc -O3 -march=native, malloc", "malloc"),
+ ("trees-gcc-lto", "gcc -O3 -march=native -flto, malloc", "malloc"),
+ ("trees-gcc-pgo", "gcc -O3 -march=native -flto PGO, malloc", "malloc"),
+ ("trees-clang", "clang -O3 -march=native, malloc", "malloc"),
+ ("trees-clang-lto", "clang -O3 -march=native -flto, malloc", "malloc"),
+ ("trees-gcc-lto", "gcc -O3 -march=native -flto, arena", "arena"),
+ ("trees-clang-lto", "clang -O3 -march=native -flto, arena", "arena"),
+ ):
+ if key not in bins:
+ continue
+ add(tree_rows, label, c_call(bins[key], mode), TREE_N)
+ add(tree_rows, "zc interp, plain new/free", zc_call(vms["interp"], tree_src, "a"), TREE_N)
+ add(tree_rows, "zc interp, o3 new/free", zc_call(vms["interp"], tree_src, "b"), TREE_N)
+ add(tree_rows, "zc interp, o3 arena", zc_call(vms["interp"], tree_src, "c"), TREE_N)
+ if "llvm" in vms:
+ hot = {"ZYHOT": "1"}
+ add(tree_rows, "zc llvm O3, ZYHOT=1, plain new/free", zc_call(vms["llvm"], tree_src, "a", hot), TREE_N)
+ add(tree_rows, "zc llvm O3, ZYHOT=1, o3 new/free", zc_call(vms["llvm"], tree_src, "b", hot), TREE_N)
+ add(tree_rows, "zc llvm O3, ZYHOT=1, o3 arena", zc_call(vms["llvm"], tree_src, "c", hot), TREE_N)
+ if "arm64" in vms:
+ add(tree_rows, "zc arm64 jit, qemu-user, plain new/free", zc_call(vms["arm64"], tree_src, "a"), TREE_N)
+ add(tree_rows, "zc arm64 jit, qemu-user, o3 arena", zc_call(vms["arm64"], tree_src, "c"), TREE_N)
+ show("trees: depth 12, checksum 8191", tree_rows)
+
+ print(f"\nwork dir left at {td}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/bench/run.sh b/tests/bench/run.sh
new file mode 100755
index 0000000..214d0a2
--- /dev/null
+++ b/tests/bench/run.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+# build the VMs and the C binaries, check the checksums, then time them.
+# see tests/bench/run.py for the slope, the trials, and the kernel letters.
+cd "$(dirname "$0")/../.."
+exec python3 tests/bench/run.py "$@"
diff --git a/tests/bench/selfmod.zc b/tests/bench/selfmod.zc
new file mode 100644
index 0000000..7e16fdb
--- /dev/null
+++ b/tests/bench/selfmod.zc
@@ -0,0 +1,19 @@
+// self-modifying code in a loop: each pass rewrites a literal inside g, so the
+// translation of g is dropped and made again, while a pile of other code stays
+// translated. prints the sum so the engines can be compared.
+int litw(int v) { return v + v + 1; }
+int g() { return 1000; }
+int work(int n) { int i = 0, s = 0; while (i < n) { s = s + i % 7; i++; } return s; }
+int a1() { return work(50); } int a2() { return work(60); } int a3() { return work(70); } int a4() { return a1() + a2(); }
+int main() {
+ word p = g; int i = 0, tot = 0;
+ while (*p != litw(1000)) p++;
+ a4(); a3();
+ while (i < 20000) {
+ *p = litw(i);
+ tot = tot + g() + a3();
+ i++;
+ }
+ put_n(tot); put_c(10);
+ return 0;
+}
diff --git a/tests/bench/sieve.c b/tests/bench/sieve.c
new file mode 100644
index 0000000..6ac503e
--- /dev/null
+++ b/tests/bench/sieve.c
@@ -0,0 +1,58 @@
+/* primes below N. the same loops as tests/bench/sieve.zc.
+ word: one int64 flag per number (what zc's fast path can do).
+ byte: one byte per number, the representation a C sieve would actually use.
+ the count is 9592 either way.
+ usage: sieve word|byte REPS */
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+enum { N = 100000, EXPECT = 9592 };
+
+static int sieve_word(int64_t *restrict f)
+{
+ int c = 0;
+ for (int i = 2; i < N; i++) f[i] = 1;
+ for (int i = 2; (long long)i * i < N; i++)
+ if (f[i])
+ for (int j = i * i; j < N; j += i) f[j] = 0;
+ for (int i = 2; i < N; i++) c += (int)f[i];
+ return c;
+}
+
+static int sieve_byte(unsigned char *restrict f)
+{
+ int c = 0;
+ for (int i = 2; i < N; i++) f[i] = 1;
+ for (int i = 2; (long long)i * i < N; i++)
+ if (f[i])
+ for (int j = i * i; j < N; j += i) f[j] = 0;
+ for (int i = 2; i < N; i++) c += f[i];
+ return c;
+}
+
+int main(int argc, char **argv)
+{
+ int reps, r = 0, k, byte;
+ int64_t *w = 0;
+ unsigned char *b = 0;
+ if (argc != 3) return 2;
+ byte = argv[1][0] == 'b';
+ reps = atoi(argv[2]);
+ if (byte) b = malloc(N);
+ else w = malloc((size_t)N * sizeof *w);
+ if (!w && !b) return 3;
+ for (k = 0; k < reps; k++) {
+ r = byte ? sieve_byte(b) : sieve_word(w);
+ /* the stores have to happen: the next pass must see a clobbered buffer */
+ __asm__ __volatile__("" : "+r"(r) : "r"(byte ? (void *)b : (void *)w) : "memory");
+ }
+ if (r != EXPECT) {
+ fprintf(stderr, "bad %d\n", r);
+ return 1;
+ }
+ printf("%d\n", r);
+ free(w);
+ free(b);
+ return 0;
+}
diff --git a/tests/bench/sieve.zc b/tests/bench/sieve.zc
new file mode 100644
index 0000000..1fb579b
--- /dev/null
+++ b/tests/bench/sieve.zc
@@ -0,0 +1,51 @@
+// primes below N. the same loops as tests/bench/sieve.c: one word per flag,
+// marks start at i*i. the count is 9592.
+// a fixed []int, bounds-checked, plain bytecode
+// b the same function lifted by o3
+// c o3 through a raw *int, no per-index check
+const int N = 100000;
+
+int sieve(fixed []int f) {
+ int n = len(f), c = 0;
+ for (int i = 2; i < n; i++) f[i] = 1;
+ for (int i = 2; i * i < n; i++)
+ if (f[i]) for (int j = i * i; j < n; j += i) f[j] = 0;
+ for (int i = 2; i < n; i++) c += f[i];
+ return c;
+}
+o3 int sieve3(fixed []int f) {
+ int n = len(f), c = 0;
+ for (int i = 2; i < n; i++) f[i] = 1;
+ for (int i = 2; i * i < n; i++)
+ if (f[i]) for (int j = i * i; j < n; j += i) f[j] = 0;
+ for (int i = 2; i < n; i++) c += f[i];
+ return c;
+}
+o3 int sieve_raw(int* f) {
+ int n = N, c = 0;
+ for (int i = 2; i < n; i++) f[i] = 1;
+ for (int i = 2; i * i < n; i++)
+ if (f[i]) for (int j = i * i; j < n; j += i) f[j] = 0;
+ for (int i = 2; i < n; i++) c += f[i];
+ return c;
+}
+int main() {
+ int which = get_c(), reps = 0, ch = get_c(), r = 0;
+ while (ch >= '0' && ch <= '9') { reps = reps * 10 + ch - '0'; ch = get_c(); }
+ if (which == 'c') {
+ int* raw = new(int, N);
+ while (reps > 0) { r = sieve_raw(raw); reps--; }
+ put_n(r); put_c(10);
+ free(d*raw);
+ return 0;
+ }
+ fixed []int f = new(fixed []int, N);
+ while (reps > 0) {
+ if (which == 'b') r = sieve3(f);
+ else r = sieve(f);
+ reps--;
+ }
+ put_n(r); put_c(10);
+ free(d*f);
+ return 0;
+}
diff --git a/tests/bench/trees.c b/tests/bench/trees.c
new file mode 100644
index 0000000..df46447
--- /dev/null
+++ b/tests/bench/trees.c
@@ -0,0 +1,85 @@
+/* a full binary tree of depth D. checksum is the node count, 8191.
+ malloc: one malloc per node, then a recursive free. same shape as new().
+ arena: one buffer, a bump pointer, no allocator inside the loop.
+ usage: trees malloc|arena REPS */
+#include <stdio.h>
+#include <stdlib.h>
+
+enum { D = 12, EXPECT = 8191 };
+
+typedef struct node {
+ struct node *left, *right;
+} node;
+
+static node *make(int d)
+{
+ node *n = (node *)malloc(sizeof *n);
+ if (d > 0) {
+ n->left = make(d - 1);
+ n->right = make(d - 1);
+ } else
+ n->left = n->right = 0;
+ return n;
+}
+
+static int check(const node *n)
+{
+ if (!n->left) return 1;
+ return 1 + check(n->left) + check(n->right);
+}
+
+static void destroy(node *n)
+{
+ if (!n) return;
+ destroy(n->left);
+ destroy(n->right);
+ free(n);
+}
+
+static node *bump;
+
+static node *make_at(int d)
+{
+ node *n = bump++;
+ if (d > 0) {
+ n->left = make_at(d - 1);
+ n->right = make_at(d - 1);
+ } else
+ n->left = n->right = 0;
+ return n;
+}
+
+int main(int argc, char **argv)
+{
+ int reps, r = 0, k, arena, nodes;
+ node *buf = 0;
+ if (argc != 3) return 2;
+ arena = argv[1][0] == 'a';
+ reps = atoi(argv[2]);
+ nodes = (1 << (D + 1)) - 1;
+ if (arena) {
+ buf = malloc((size_t)nodes * sizeof *buf);
+ if (!buf) return 3;
+ }
+ for (k = 0; k < reps; k++) {
+ if (arena) {
+ node *root;
+ bump = buf;
+ root = make_at(D);
+ r = check(root);
+ __asm__ __volatile__("" : "+r"(r) : "r"(root) : "memory");
+ } else {
+ node *root = make(D);
+ r = check(root);
+ __asm__ __volatile__("" : "+r"(r) : "r"(root) : "memory");
+ destroy(root);
+ }
+ }
+ if (r != EXPECT) {
+ fprintf(stderr, "bad %d\n", r);
+ return 1;
+ }
+ printf("%d\n", r);
+ free(buf);
+ return 0;
+}
diff --git a/tests/bench/trees.zc b/tests/bench/trees.zc
new file mode 100644
index 0000000..c8f46ef
--- /dev/null
+++ b/tests/bench/trees.zc
@@ -0,0 +1,92 @@
+// a full binary tree of depth D. checksum is the node count, 8191.
+// each node is two pointers. new() rounds that plus the 5-word header up
+// to 8 words (a 4-word block cannot hold the header). free of the owned
+// root recycles the tree onto the size-class lists.
+// a plain new / own / free
+// b the same, marked o3 (calls and new still leave the region)
+// c one array, used as an arena of pairs. no allocator in the loop.
+// the same shapes are tests/bench/trees.c.
+const int D = 12;
+const int WORDS = 16382; // two words for each of the 8191 nodes
+
+struct node { node* left, right; };
+int* heap;
+int used;
+
+node*.u make(int depth) {
+ node* n = new(node);
+ if (depth > 0) {
+ n.left = make(depth - 1);
+ n.right = make(depth - 1);
+ } else {
+ n.left = 0;
+ n.right = 0;
+ }
+ return u*n;
+}
+o3 node*.u make3(int depth) {
+ node* n = new(node);
+ if (depth > 0) {
+ n.left = make3(depth - 1);
+ n.right = make3(depth - 1);
+ } else {
+ n.left = 0;
+ n.right = 0;
+ }
+ return u*n;
+}
+int check(node* n) {
+ if (!n.left) return 1;
+ return 1 + check(n.left) + check(n.right);
+}
+o3 int check3(node* n) {
+ if (!n.left) return 1;
+ return 1 + check3(n.left) + check3(n.right);
+}
+
+// a node id is its word index plus one, so 0 can mean "no child"
+o3 int build(int depth) {
+ int i = used;
+ used = used + 2;
+ if (depth > 0) {
+ heap[i] = build(depth - 1);
+ heap[i + 1] = build(depth - 1);
+ } else {
+ heap[i] = 0;
+ heap[i + 1] = 0;
+ }
+ return i + 1;
+}
+o3 int ck(int id) {
+ int i, l;
+ if (id == 0) return 0;
+ i = id - 1;
+ l = heap[i];
+ if (l == 0) return 1;
+ return 1 + ck(l) + ck(heap[i + 1]);
+}
+
+int main() {
+ int which = get_c(), reps = 0, ch = get_c(), r = 0;
+ while (ch >= '0' && ch <= '9') { reps = reps * 10 + ch - '0'; ch = get_c(); }
+ if (which == 'c') {
+ heap = new(int, WORDS);
+ while (reps > 0) { used = 0; r = ck(build(D)); reps--; }
+ put_n(r); put_c(10);
+ return 0;
+ }
+ while (reps > 0) {
+ if (which == 'b') {
+ node* root = make3(D);
+ r = check3(root);
+ free(d*root);
+ } else {
+ node* root = make(D);
+ r = check(root);
+ free(d*root);
+ }
+ reps--;
+ }
+ put_n(r); put_c(10);
+ return 0;
+}
diff --git a/tests/data/hi.txt b/tests/data/hi.txt
new file mode 100644
index 0000000..ce01362
--- /dev/null
+++ b/tests/data/hi.txt
@@ -0,0 +1 @@
+hello
diff --git a/tests/data/sbv.c b/tests/data/sbv.c
new file mode 100644
index 0000000..7e12680
--- /dev/null
+++ b/tests/data/sbv.c
@@ -0,0 +1,18 @@
+/* the c side of tests/programs/structval.zc: functions that take structs by value. test.sh
+ builds it to build/sbv.so, and the test loads it with cload. */
+typedef struct { long a, b; } pair;
+typedef struct { double x, y; } vec;
+typedef struct { long a, b, c; } trio;
+long pair_sum(pair p) { return p.a * 10 + p.b; }
+long pair_sum2(pair p, pair q) { return p.a * 1000 + p.b * 100 + q.a * 10 + q.b; }
+long vec_scaled(vec v, long k) { return (long)(v.x * 2 + v.y * 3) * k; }
+long many(long a, long b, long c, long d, long e, long f, pair p) { return a + b + c + d + e + f + p.a * 100 + p.b; }
+long trio_sum(long a, long b, long c, long d, long e, long f, trio t) { return a + b + c + d + e + f + t.a * 100 + t.b * 10 + t.c; }
+long eight(long a, long b, long c, long d, long e, long f, long g, long h, pair p) { return a + b + c + d + e + f + g + h + p.a * 100 + p.b; }
+pair mkpair(long a, long b) { pair r; r.a = a; r.b = b; return r; }
+vec mkvec(double x, double y) { vec r; r.x = x + 1; r.y = y + 1; return r; }
+trio mktrio(long a, long b, long c) { trio r; r.a = a; r.b = b; r.c = c; return r; }
+typedef struct { long a; double x; } mixed;
+mixed mkmixed(long a, double x) { mixed r; r.a = a + 1; r.x = x * 2; return r; }
+typedef struct { double x, y, z; } v3;
+v3 mkv3(double x) { v3 r; r.x = x; r.y = x + 1; r.z = x + 2; return r; }
diff --git a/tests/drivers/code_address_growth_test.py b/tests/drivers/code_address_growth_test.py
new file mode 100644
index 0000000..a454406
--- /dev/null
+++ b/tests/drivers/code_address_growth_test.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# native address metadata grows without losing warm translations or rewrite tracking.
+import subprocess
+import sys
+
+source = '''o3 int warmed(int value) { return value + 1; }
+fn main() {
+ put_n(warmed(6)); put_c(32);
+ let code = new(6);
+ code[0] = code + 3; code[3] = 8; code[4] = 15; code[5] = 18;
+ put_n(syscall(9, 9, code + 6, 0) >= code + 6); put_c(32);
+ put_n(code()); put_c(32);
+ fill_w(code + 4, 19, 1); put_n(code()); put_c(32);
+ let replacement = new(1); replacement[0] = 23;
+ copy_w(code + 4, replacement, 1); put_n(code()); put_c(32);
+ gc(); put_n(warmed(8)); put_c(32);
+ put_n(syscall(9, 9, syscall(9, 0, 0, 0) + 1, 0) == -1); put_c(10);
+ return 0;
+}
+'''
+result = subprocess.run(sys.argv[1:], input=source, text=True, capture_output=True, timeout=20)
+assert result.returncode == 0 and result.stdout == '7 1 7 9 11 9 1\n', (result.returncode, result.stdout, result.stderr)
+print('ok new code addresses, warm calls, bulk rewrites, and denied metadata growth')
diff --git a/tests/drivers/code_emission_growth_test.py b/tests/drivers/code_emission_growth_test.py
new file mode 100644
index 0000000..f13c141
--- /dev/null
+++ b/tests/drivers/code_emission_growth_test.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+# emitted code and global data keep their addresses when the program area fills.
+import argparse
+import subprocess
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--length', type=int, default=600000)
+parser.add_argument('command', nargs=argparse.REMAINDER)
+options = parser.parse_args()
+source = 'int earlier() { return 7; }\n'
+source += f'let storage[{options.length}];\n'
+source += 'let text = "' + 's' * 3500 + '";\n'
+source += 'int later(int n) { return earlier() + n; }\n'
+source += '''fn main() {
+ storage[len(storage) - 1] = 9;
+ gc(); put_n(later(3)); put_c(32); put_n(storage[len(storage) - 1]); put_c(32);
+ put_n(len_w(text)); put_c(32); put_n(code_blocks != 0); put_c(32);
+ let quoted = quote { return 5; };
+ put_n(eval(quoted)); put_c(32);
+ let f = fn(float x) { return x + 1.5; }; put_n((f(2.0) as word) == 0x400c000000000000); put_c(10);
+ return 0;
+}
+'''
+result = subprocess.run(options.command, input=source, text=True, capture_output=True, timeout=40)
+assert result.returncode == 0 and result.stdout == '10 9 3500 1 5 1\n', (result.returncode, result.stdout, result.stderr)
+print('ok linked code segments preserve functions, globals, strings, quotes, and float payloads')
+
+# force the next float opcode and its payload to need a new segment.
+source = 'immediate fn force_edge() { next(); hlimit = here + 3; return 0; }\n'
+source += 'fn main() { force_edge let value = 1.5; gc(); put_n((value as word) == 0x3ff8000000000000); put_c(10); return 0; }\n'
+result = subprocess.run(options.command, input=source, text=True, capture_output=True, timeout=20)
+assert result.returncode == 0 and result.stdout == '1\n', (result.returncode, result.stdout, result.stderr)
+print('ok a float opcode and its payload remain adjacent at a segment boundary')
+source = 'immediate fn force_edge() { next(); hlimit = here + 3; return 0; }\n'
+source += 'force_edge\nenhance cube(x) vm { 0 $ 0 $ * * };\n'
+source += 'enhance sum(a, b) micro "a = add a b";\n'
+source += 'fn main() { gc(); put_n(cube(7)); put_c(32); put_n(sum(3, 4)); put_c(10); return 0; }\n'
+result = subprocess.run(options.command, input=source, text=True, capture_output=True, timeout=20)
+assert result.returncode == 0 and result.stdout == '343 7\n', (result.returncode, result.stdout, result.stderr)
+print('ok vm and micro recipes remain contiguous after a code reservation')
diff --git a/tests/drivers/compiler_growth_test.py b/tests/drivers/compiler_growth_test.py
new file mode 100644
index 0000000..982611c
--- /dev/null
+++ b/tests/drivers/compiler_growth_test.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+# focused growth checks for compiler columns, stable names, types, and scratch stacks.
+import argparse
+import subprocess
+import sys
+
+parser = argparse.ArgumentParser()
+parser.add_argument('server')
+parser.add_argument('--names', type=int, default=18000)
+options = parser.parse_args()
+server = options.server
+assert options.names > 0
+
+
+def run(source, expected):
+ result = subprocess.run([server], input=source, text=True, capture_output=True, timeout=30)
+ assert result.returncode == 0, result.stderr
+ assert result.stdout == expected, result.stdout[:1000]
+
+
+locals_source = 'fn main() {\n'
+locals_source += ''.join(f'int local_{i} = {i};\n' for i in range(900))
+locals_source += 'put_n(local_899); put_c(10); return 0; }\n'
+run(locals_source, '899\n')
+
+types_source = ''.join(f'type array_{i} = [{i + 1}]int;\n' for i in range(1500))
+types_source += 'fn main() { put_n(ntl > 1500); put_c(10); gc(); put_n(tsz(tarr(tint, 1500))); put_c(10); return 0; }\n'
+run(types_source, '1\n1500\n')
+
+names_source = ''.join(f'const int repeated_long_identifier_for_pool_and_hash_growth_{i} = {i};\n' for i in range(options.names))
+names_source += f'''fn main() {{
+ word saved = intern("a name that must survive growth");
+ reserve_names(hashcap * 4); reserve_symbols(symcap * 2); reserve_locals(loccap * 2);
+ reserve_fixes(fixcap * 2); reserve_R_SRC(4096); reserve_R_CTX(4096);
+ reserve_R_LOOP(4096); reserve_R_SNAP(32768); reserve_R_QBUF(32768); reserve_R_EXIT(8192);
+ gc();
+ put_n(saved == intern("a name that must survive growth")); put_c(10);
+ put_n(repeated_long_identifier_for_pool_and_hash_growth_{options.names - 1}); put_c(10);
+ put_n(poolblocks != 0 && symcap > NS && hashcap > IHN); put_c(10);
+ return 0;
+}}
+'''
+run(names_source, f'1\n{options.names - 1}\n1\n')
+
+long_name = 'identifier_' + 'x' * 1500
+run(f'const int {long_name} = 7; fn main() {{ put_n({long_name}); put_c(10); return 0; }}\n', '7\n')
+
+mark_source = '''let roots[60000];
+unchecked fn main() {
+ int i = 0;
+ while (i < 60000) { roots[i] = new(1); i++; }
+ gc(); put_n(markcap > MARKN); put_c(32); put_n(live_objects() == 60000); put_c(32);
+ fill_w(roots, 0, 60000); scrub(); gc();
+ // conservative roots can retain incidental addresses in code or untyped words.
+ put_n(live_objects() < 1000); put_c(10); return 0;
+}
+'''
+run(mark_source, '1 1 1\n')
+
+generics_source = ''.join(f'int generic_{i}<T>() {{ return sizeof(T); }}\n' for i in range(90))
+generics_source += 'int size_of<T>() { return sizeof(T); }\n'
+generics_source += 'fn main() { int total = 0;\n'
+generics_source += ''.join(f'total += generic_{i}<int>();\n' for i in range(90))
+generics_source += ''.join(f'total += size_of<[{i + 1}]int>();\n' for i in range(150))
+generics_source += 'gc(); put_n(total); put_c(10); return 0; }\n'
+run(generics_source, '11415\n')
+
+captures_source = 'fn main() {\n' + ''.join(f'int captured_{i} = {i};\n' for i in range(70))
+captures_source += 'let f = fn() { return ' + ' + '.join(f'captured_{i}' for i in range(70)) + '; };\n'
+captures_source += 'gc(); put_n(f()); put_c(10); return 0; }\n'
+run(captures_source, '2415\n')
+
+nested_source = 'fn main() { int captured = 7;\n'
+nested_source += ''.join(f'let nested_{i} = fn() {{\n' for i in range(12))
+nested_source += 'return captured;\n'
+nested_source += ''.join(f'}}; return nested_{i}();\n' for i in reversed(range(1, 12)))
+nested_source += '}; gc(); put_n(nested_0()); put_c(10); return 0; }\n'
+run(nested_source, '7\n')
+assembly_source = 'fn main() { int value = asm { ' + 'future _ ' * 300
+assembly_source += ''.join(f':label_{i} ' for i in range(200)) + ':future 7 }; gc(); put_n(value); put_c(10); return 0; }\n'
+run(assembly_source, '7\n')
+print('ok compiler tables, long identifiers, and mark work grow without losing references')
diff --git a/tests/drivers/core_lsp_test.js b/tests/drivers/core_lsp_test.js
new file mode 100644
index 0000000..41d0095
--- /dev/null
+++ b/tests/drivers/core_lsp_test.js
@@ -0,0 +1,43 @@
+// check maintained zc sources through the editor document protocol, one file at a time.
+const assert = require('assert');
+const fs = require('fs');
+const cp = require('child_process');
+const path = require('path');
+const target = process.argv[2];
+const files = ['src/runtime/rt.zc', 'src/compiler/compiler.zc', 'src/runtime/run.zc', 'src/lib/std.zc', 'src/lsp/server.zc', 'src/tools/build.zc'];
+const frame = message => {
+ const body = Buffer.from(JSON.stringify(message));
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]);
+};
+for (const file of files) {
+ const uri = 'file://' + path.resolve(file);
+ const text = fs.readFileSync(file, 'utf8');
+ const messages = [
+ { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
+ { jsonrpc: '2.0', method: 'initialized', params: {} },
+ { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } },
+ { jsonrpc: '2.0', id: 2, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } },
+ { jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version: 2 }, contentChanges: [{ text: text + '\n' }] } },
+ { jsonrpc: '2.0', method: 'textDocument/didClose', params: { textDocument: { uri } } },
+ { 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 });
+ assert.strictEqual(result.status, 0, file + ': ' + result.stderr);
+ let data = result.stdout;
+ const replies = [];
+ while (data.length) {
+ const end = data.indexOf('\r\n\r\n');
+ assert.ok(end >= 0, 'complete protocol header');
+ const length = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
+ replies.push(JSON.parse(data.subarray(end + 4, end + 4 + length)));
+ data = data.subarray(end + 4 + length);
+ }
+ const diagnostics = replies.filter(m => m.method === 'textDocument/publishDiagnostics');
+ assert.strictEqual(diagnostics.length, 3, file + ': open, change, and close publish diagnostics');
+ const errors = diagnostics.flatMap(m => m.params.diagnostics).filter(d => d.severity === 1);
+ assert.deepStrictEqual(errors, [], file + ': ' + errors.map(d => (d.range.start.line + 1) + ': ' + d.message).join('\n'));
+ assert.ok(replies.find(m => m.id === 2).result.data.length, file + ': semantic tokens are available');
+ assert.deepStrictEqual(diagnostics[2].params.diagnostics, [], 'close clears diagnostics');
+ console.log('ok editor open, highlight, change, and close: ' + file);
+}
diff --git a/tests/drivers/diagcache_test.js b/tests/drivers/diagcache_test.js
new file mode 100644
index 0000000..43966cf
--- /dev/null
+++ b/tests/drivers/diagcache_test.js
@@ -0,0 +1,140 @@
+// the rules of the diagnostics cache (editors/vscode/client/diagcache.js), with
+// plain node and no vscode: a key covers the server build, the text, and every
+// file the text reads through include, transitively.
+//
+// node tests/drivers/diagcache_test.js
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const assert = require('assert');
+const dc = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'diagcache.js'));
+
+let passed = 0, failed = 0;
+function check(name, fn) {
+ try { fn(); passed++; console.log('ok ' + name); }
+ catch (e) { failed++; console.log('FAIL ' + name + ': ' + e.message); }
+}
+
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-diagcache-'));
+const write = (rel, text) => { const p = path.join(root, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, text); return p; };
+const A = write('a.zc', 'include "b.zc";\nfn main() { return helper(); }\n');
+const B = write('b.zc', 'include "c.zc";\n');
+const C = write('c.zc', 'fn helper() { return 1; }\n');
+const U = write('unrelated.zc', 'fn other() { return 2; }\n');
+const aText = fs.readFileSync(A, 'utf8');
+
+const keyA = (stamp = 'S1', text = aText, p = A) => dc.diagKey(stamp, text, p, root).key;
+
+check('includeNames reads every include, however it is spaced', () => {
+ assert.deepStrictEqual(dc.includeNames('include "a.zc";\ninclude"b/c.zc";\nfn f() {}'), ['a.zc', 'b/c.zc']);
+ assert.deepStrictEqual(dc.includeNames('fn f() { return 1; }'), []);
+});
+
+check('includeNames ignores comments and literals while allowing comments between tokens', () => {
+ assert.deepStrictEqual(dc.includeNames('// include "fake.zc"\n/* include "also-fake.zc" */\nlet s = "include \\"string.zc\\"";\ninclude /* dependency */ "real.zc";'), ['real.zc']);
+});
+
+check('the same tree gives the same key', () => {
+ assert.strictEqual(keyA(), keyA());
+ assert.match(keyA(), /^[0-9a-f]{64}$/);
+});
+
+check('a change to the document text changes the key', () => {
+ assert.notStrictEqual(keyA('S1', aText + ' '), keyA());
+});
+
+check('a change to the server build changes the key', () => {
+ assert.notStrictEqual(keyA('S2'), keyA('S1'));
+});
+
+check('a change to a file the document includes, two steps down, changes the key', () => {
+ const before = keyA();
+ fs.writeFileSync(C, 'fn helper() { return 3; }\n');
+ const after = keyA();
+ assert.notStrictEqual(before, after);
+ fs.writeFileSync(C, 'fn helper() { return 1; }\n');
+ assert.strictEqual(keyA(), before, 'putting it back gives the old key again');
+});
+
+check('a file nothing includes does not change the key', () => {
+ const before = keyA();
+ fs.writeFileSync(U, 'fn other() { return 9; }\n');
+ assert.strictEqual(keyA(), before);
+});
+
+check('creating a missing include changes the key', () => {
+ const M = write('m.zc', 'include "later.zc";\nfn f() { return 0; }\n');
+ const mText = fs.readFileSync(M, 'utf8');
+ const before = dc.diagKey('S1', mText, M, root);
+ assert.ok(before.deps.some(p => p.endsWith('later.zc')), 'the missing name is still a dependency');
+ write('later.zc', 'fn g() { return 1; }\n');
+ assert.notStrictEqual(dc.diagKey('S1', mText, M, root).key, before.key);
+});
+
+check('an include is found from the workspace folder as well as the including file', () => {
+ const X = write('sub/x.zc', 'include "b.zc";\nfn x() { return 0; }\n');
+ const tree = dc.sourceTree(fs.readFileSync(X, 'utf8'), X, root);
+ assert.ok(tree.files.has(path.join(root, 'b.zc')), 'b.zc is read from the workspace root');
+ assert.ok(tree.files.has(path.join(root, 'sub', 'b.zc')), 'and the directory of x.zc is tried too');
+ assert.ok(tree.complete);
+});
+
+check('a tree too large to cover has no key, so nothing is cached for it', () => {
+ let many = '';
+ for (let i = 0; i < dc.MAX_FILES + 5; i++) many += 'include "nope' + i + '.zc";\n';
+ const big = write('big.zc', many);
+ const r = dc.diagKey('S1', many, big, root);
+ assert.strictEqual(r.key, null);
+ const cache = new dc.DiagCache(null);
+ assert.strictEqual(cache.put(r.key, []), false);
+ assert.strictEqual(cache.size(), 0);
+});
+
+check('the cache answers only for the key it was given', () => {
+ const cache = new dc.DiagCache(null);
+ const d = [{ message: 'x' }];
+ assert.ok(cache.put('k1', d));
+ assert.deepStrictEqual(cache.get('k1'), d);
+ assert.strictEqual(cache.get('k2'), null);
+ assert.strictEqual(cache.get(null), null);
+ assert.strictEqual(cache.put(null, []), false, 'a tree without a key is not kept');
+ assert.strictEqual(cache.size(), 1);
+});
+
+check('two states of one document are both kept, so going back is a hit', () => {
+ const cache = new dc.DiagCache(null);
+ cache.put('state-a', []);
+ cache.put('state-b', [{ message: 'b' }]);
+ assert.deepStrictEqual(cache.get('state-a'), []);
+ assert.deepStrictEqual(cache.get('state-b'), [{ message: 'b' }]);
+});
+
+check('the least recently used entries go first past the limit', () => {
+ const cache = new dc.DiagCache(null, 2);
+ cache.put('1', []);
+ cache.put('2', []);
+ cache.get('1');
+ cache.put('3', []);
+ assert.strictEqual(cache.size(), 2);
+ assert.strictEqual(cache.get('2'), null, 'two was the least recently used');
+ assert.deepStrictEqual(cache.get('1'), []);
+ assert.deepStrictEqual(cache.get('3'), []);
+});
+
+check('the cache carries over a save and load as plain JSON', () => {
+ const cache = new dc.DiagCache(null);
+ cache.put('k1', [{ message: 'z', range: { start: { line: 1, character: 2 } } }]);
+ const back = new dc.DiagCache(JSON.parse(JSON.stringify(cache)));
+ assert.deepStrictEqual(back.get('k1'), [{ message: 'z', range: { start: { line: 1, character: 2 } } }]);
+});
+
+check('a saved entry that is not well formed is dropped, not trusted', () => {
+ const back = new dc.DiagCache({ bad: { key: 'bad' }, good: [] });
+ assert.strictEqual(back.get('bad'), null);
+ assert.deepStrictEqual(back.get('good'), []);
+});
+
+fs.rmSync(root, { recursive: true, force: true });
+console.log(passed + ' passed, ' + failed + ' failed');
+process.exit(failed ? 1 : 0);
diff --git a/tests/drivers/diagnostic_revision_test.js b/tests/drivers/diagnostic_revision_test.js
new file mode 100644
index 0000000..725fb87
--- /dev/null
+++ b/tests/drivers/diagnostic_revision_test.js
@@ -0,0 +1,47 @@
+// exercise delayed diagnostics without relying on an editor's timing.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+const root = path.resolve(__dirname, '../..');
+const displayed = [], cached = [];
+const vscode = {
+ EventEmitter: class { fire() {} },
+ SemanticTokensLegend: class {},
+ Diagnostic: class { constructor(range, message) { this.message = message; } },
+ Range: class {},
+ DiagnosticSeverity: { Hint: 4, Information: 3, Warning: 2, Error: 1 },
+ Uri: { parse: value => value },
+};
+const filename = path.join(root, 'editors/vscode/client/extension.js');
+const context = vm.createContext({
+ module: { exports: {} }, __dirname: path.dirname(filename), Buffer, setTimeout, clearTimeout,
+ require(name) {
+ if (name === 'vscode') return vscode;
+ if (name === './vmproc' || name === './wasmproc') return {};
+ if (name.startsWith('./')) return require(path.join(path.dirname(filename), name));
+ return require(name);
+ },
+ displayed, cached,
+});
+vm.runInContext(fs.readFileSync(filename, 'utf8') + `
+diagnostics = { set(uri, list) { displayed.push(list.map(d => d.message)); } };
+cache = { put(key, raw) { cached.push({ key, raw }); return false; } };
+const uri = 'file:///reopened.zc';
+const diag = message => [{ message, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } } }];
+opened.add(uri); docKeys.set(uri, 'current'); docVersions.set(uri, 3);
+sentKeys.set(uri, [{ key: 'current', version: 3 }]);
+onPublish(uri, diag('old close'), 1);
+onPublish(uri, diag('old open'), 2);
+onPublish(uri, diag('current open'), 3);
+onPublish(uri, diag('duplicate'), 3);
+opened.delete(uri);
+onPublish(uri, diag('closed'), 3);
+opened.add(uri); docKeys.set(uri, null); docVersions.set(uri, 5);
+sentKeys.set(uri, [{ key: null, version: 4 }, { key: null, version: 5 }]);
+onPublish(uri, diag('old uncacheable text'), 4);
+onPublish(uri, diag('current uncacheable text'), 5);
+`, context, { filename });
+assert.deepStrictEqual(displayed.map(list => Array.from(list)), [['current open'], ['current uncacheable text']]);
+assert.deepStrictEqual(cached.map(entry => entry.key), ['current']);
+console.log('ok closed, reopened, duplicate, and uncacheable diagnostic revisions');
diff --git a/tests/drivers/enhancement_backends_test.py b/tests/drivers/enhancement_backends_test.py
new file mode 100644
index 0000000..b7d553b
--- /dev/null
+++ b/tests/drivers/enhancement_backends_test.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+# recipes, vm lowering, fallback, and ignored optimizer regions agree across hosts.
+import pathlib
+import subprocess
+
+root = pathlib.Path(__file__).resolve().parents[2]
+source = (root / 'tests/programs/enhance.zc').read_text()
+# keep this focused check short while still exercising signed and unsigned arithmetic.
+source = source.replace('i < 5000', 'i < 200')
+source += '\n'
+reference = None
+for command in [['bin/zy'], ['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:
+ reference = result.stdout
+ else:
+ assert result.stdout == reference, (command, reference, result.stdout)
+ print('ok vm recipes, multiple outputs, arithmetic, and fallback: ' + command[0])
+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']]:
+ 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])
+
+# slot zero can be replaced, and an unsupported replacement preserves it.
+# real install descriptors are exercised without calling a compiler-specific parser.
+source = '''let descriptor[7], names[2];
+fn main() {
+ names[0] = "a"; names[1] = "b";
+ descriptor[0] = 0; descriptor[1] = 0; descriptor[2] = 2; descriptor[3] = 1;
+ descriptor[4] = "a = add a b"; descriptor[6] = names;
+ int installed = install(descriptor) == 0, first = 17 / 5;
+ descriptor[4] = "a = unknown a b";
+ int rejected = install(descriptor) < 0, second = 17 / 5;
+ descriptor[4] = "a = sdiv a b"; install(descriptor);
+ 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']]:
+ 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
new file mode 100644
index 0000000..11bf50b
--- /dev/null
+++ b/tests/drivers/ext_cache_test.js
@@ -0,0 +1,154 @@
+// the extension's diagnostics cache, end to end: the real client
+// (editors/vscode/client/extension.js) with a stub for the vscode module, and
+// the built-in wasm server. the checks: a first session asks the server and
+// saves the answer; a second session shows the saved answer before the server
+// 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
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const Module = require('module');
+
+const client = path.join(__dirname, '..', '..', 'editors', 'vscode', 'client');
+
+// the vscode module, as far as the client uses it
+const world = { lines: [], sets: [], listeners: {}, watchers: [], textDocuments: [], saved: undefined, root: '' };
+const stub = {
+ EventEmitter: class { constructor() { this.event = () => ({ dispose() {} }); } fire() {} },
+ Diagnostic: class { constructor(range, message, severity) { Object.assign(this, { range, message, severity }); } },
+ Range: class { constructor(a, b, c, d) { Object.assign(this, { start: { line: a, character: b }, end: { line: c, character: d } }); } },
+ DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 },
+ SemanticTokensLegend: class { constructor(t, m) { this.tokenTypes = t; this.tokenModifiers = m; } },
+ SemanticTokensBuilder: class { constructor() {} push() {} build() { return null; } },
+ MarkdownString: class {
+ constructor(v = '') { this.value = v; this.escaped = false; }
+ appendText(v) { this.value += v; this.escaped = true; return this; }
+ appendMarkdown(v) { this.value += v; return this; }
+ appendCodeblock(v, language) { this.value += '\n```' + language + '\n' + v + '\n```\n'; return this; }
+ },
+ Hover: class { constructor(c) { this.contents = c; } },
+ Uri: { parse: s => ({ toString: () => s, fsPath: decodeURIComponent(s.replace(/^file:\/\//, '')) }) },
+ workspace: {
+ workspaceFolders: undefined,
+ textDocuments: world.textDocuments,
+ getConfiguration: () => ({ get: () => undefined }),
+ createFileSystemWatcher: () => {
+ const w = { handlers: {}, dispose() {} };
+ world.watchers.push(w);
+ for (const k of ['onDidChange', 'onDidCreate', 'onDidDelete']) w[k] = f => { w.handlers[k] = f; return { dispose() {} }; };
+ return w;
+ },
+ ...['onDidChangeConfiguration', 'onDidOpenTextDocument', 'onDidChangeTextDocument', 'onDidSaveTextDocument', 'onDidCloseTextDocument']
+ .reduce((o, k) => { o[k] = f => { world.listeners[k] = f; return { dispose() {} }; }; return o; }, {}),
+ },
+ window: {
+ createOutputChannel: () => ({ appendLine: s => world.lines.push(String(s)), show() {}, dispose() {} }),
+ showWarningMessage() {}, showErrorMessage() {},
+ },
+ languages: {
+ createDiagnosticCollection: () => ({
+ set: (uri, list) => world.sets.push({ uri: uri.toString(), list, at: world.lines.length }),
+ delete() {}, dispose() {},
+ }),
+ registerDocumentSemanticTokensProvider: () => ({ dispose() {} }),
+ registerHoverProvider: (sel, p) => { world.hover = p; return { dispose() {} }; },
+ },
+ commands: { registerCommand: () => ({ dispose() {} }) },
+};
+const realResolve = Module._resolveFilename;
+Module._resolveFilename = function (req, ...rest) { return req === 'vscode' ? 'vscode' : realResolve.call(this, req, ...rest); };
+require.cache['vscode'] = { id: 'vscode', filename: 'vscode', loaded: true, exports: stub };
+
+let passed = 0, failed = 0;
+function check(name, ok, detail) {
+ if (ok) { passed++; console.log('ok ' + name); }
+ else { failed++; console.log('FAIL ' + name + (detail ? ': ' + detail : '')); }
+}
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+async function until(pred, ms = 20000) {
+ const end = Date.now() + ms;
+ while (Date.now() < end) { if (pred()) return true; await sleep(50); }
+ return pred();
+}
+
+function freshClient() {
+ for (const f of Object.keys(require.cache)) if (f.startsWith(client)) delete require.cache[f];
+ return require(path.join(client, 'extension.js'));
+}
+
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-extcache-'));
+stub.workspace.workspaceFolders = [{ uri: { fsPath: root } }];
+const helper = path.join(root, 'helper.zc');
+const main = path.join(root, 'main.zc');
+const helperOk = 'fn helper() { return 1; }\n';
+const helperBad = 'fn helper2() { return 1; }\n';
+fs.writeFileSync(helper, helperOk);
+const mainText = 'include "helper.zc";\nfn main() { return helper(); }\n';
+fs.writeFileSync(main, mainText);
+
+function doc(p, text) {
+ const d = { uri: { toString: () => 'file://' + p, fsPath: p }, languageId: 'zc', version: 1, text, getText() { return this.text; } };
+ return d;
+}
+
+const ctx = () => ({
+ subscriptions: [],
+ globalState: { get: () => world.saved, update: (k, v) => { world.saved = JSON.parse(JSON.stringify(v)); return Promise.resolve(); } },
+});
+const mainDoc = doc(main, mainText);
+const mainUri = 'file://' + main;
+
+(async () => {
+ // session one: nothing cached, so the server is asked
+ world.textDocuments.push(mainDoc);
+ let ext = freshClient();
+ let c1 = ctx();
+ ext.activate(c1);
+ check('session one: the server starts and checks the document', await until(() => world.lines.some(l => /server ready/.test(l))) && await until(() => world.sets.some(s => s.uri === mainUri)));
+ const first = world.sets.filter(s => s.uri === mainUri).pop();
+ check('session one: the server answered with a list', first && Array.isArray(first.list), JSON.stringify(first && first.list));
+ await sleep(800);
+ const savedKeys = Object.keys(world.saved || {});
+ // the hover is the server's markdown, parsed: a zc code block, not escaped text
+ const hv = world.hover && await world.hover.provideHover(mainDoc, { line: 1, character: 4 });
+ const hc = hv && hv.contents[0];
+ check('the hover is markdown with a zc code block, parsed and not escaped', hc && !hc.escaped && /^```zc\n/.test(hc.value) && /\n```/.test(hc.value), JSON.stringify(hc));
+ check('session one: the answer is saved under a key', savedKeys.length === 1 && /^[0-9a-f]{64}$/.test(savedKeys[0]), JSON.stringify(savedKeys));
+ ext.deactivate();
+ await sleep(700);
+
+ // session two: the saved answer is shown before the server has started
+ world.lines.length = 0; world.sets.length = 0;
+ ext = freshClient();
+ ext.activate(ctx());
+ check('session two: the saved diagnostics are shown at once', world.sets.some(s => s.uri === mainUri) && world.lines.some(l => /from the cache/.test(l)));
+ check('session two: the server has not started yet', !world.lines.some(l => /server ready/.test(l)));
+ await until(() => world.lines.some(l => /server ready/.test(l)));
+ await sleep(300);
+ check('session two: the server was not asked about the document', !world.lines.some(l => /-> textDocument\/didOpen|-> textDocument\/didChange/.test(l)), JSON.stringify(world.lines.slice(0, 6)));
+ check('session two: the cache holds the same key as before', JSON.stringify(Object.keys(world.saved || {})) === JSON.stringify(savedKeys));
+
+ // a change to the included file, saved: the document is checked again and the error shows
+ world.lines.length = 0; world.sets.length = 0;
+ fs.writeFileSync(helper, helperBad);
+ for (const h of world.watchers) if (h.handlers.onDidChange) h.handlers.onDidChange({ fsPath: helper });
+ check('an included file changed: the document is checked again', await until(() => world.lines.some(l => /-> textDocument\/didOpen|-> textDocument\/didChange/.test(l)) && world.sets.some(s => s.uri === mainUri)), JSON.stringify(world.lines.slice(-4)));
+ await sleep(900);
+ check('an included file changed: the new state is saved under its own key', Object.keys(world.saved || {}).length === 2, JSON.stringify(Object.keys(world.saved || {})));
+
+ // the file goes back: its tree is the one the cache has, so the old answer is shown from the cache
+ world.lines.length = 0; world.sets.length = 0;
+ fs.writeFileSync(helper, helperOk);
+ for (const h of world.watchers) if (h.handlers.onDidChange) h.handlers.onDidChange({ fsPath: helper });
+ await sleep(200);
+ check('the old tree is answered from the cache, without asking the server', world.lines.some(l => /from the cache/.test(l)) && !world.lines.some(l => /-> textDocument\/didChange/.test(l)), JSON.stringify(world.lines.slice(-4)));
+
+ ext.deactivate();
+ await sleep(700);
+ fs.rmSync(root, { recursive: true, force: true });
+ console.log(passed + ' passed, ' + failed + ' failed');
+ process.exit(failed ? 1 : 0);
+})().catch(e => { console.log('FAIL harness: ' + (e.stack || e)); process.exit(1); });
diff --git a/tests/drivers/float_backends_test.py b/tests/drivers/float_backends_test.py
new file mode 100644
index 0000000..2cb9473
--- /dev/null
+++ b/tests/drivers/float_backends_test.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+# compare float instructions, packed binary32 conversion, and decimal output.
+import pathlib
+import subprocess
+
+root = pathlib.Path(__file__).resolve().parents[2]
+commands = [['bin/zy'], ['bin/zym'], ['node', 'src/js/tinyvm.js']]
+sources = [(root / 'tests/programs/float.zc').read_text(), '''fn render(bits, precision, mode) { asm { bits @ precision @ mode @ #1236 } return 0; }
+fn convert(bits) { return asm { bits @ #1218 }; }
+fn main() {
+ [4]f32 values;
+ values[0] = 1.5; values[1] = -2.25; values[2] = 1e-45; values[3] = 1e40;
+ put_n(values[0] == 1.5); put_c(32); put_n(values[1] == -2.25); put_c(32);
+ put_n(values[2] > 0.0); put_c(32); put_n(values[3] == 1.0 / 0.0); put_c(10);
+ render(0.125, 2, 1); put_c(32); render(-0.125, 2, 1); put_c(32);
+ render(1e30, 6, 1); put_c(32); render(1.7976931348623157e308, 6, 1); put_c(10);
+ render(0.1, 0, 0); put_c(32); render(1e-5, 0, 0); put_c(32); render(1e16, 0, 0); put_c(10);
+ render(9.999, 3, 2); put_c(32); render(5e-324, 3, 2); put_c(32); render(0, 3, 2); put_c(10);
+ let negative_zero = 1 << 63; render(negative_zero, 6, 1); put_c(32); render(negative_zero, 0, 0); put_c(10);
+ put_n(convert(0.0 / 0.0)); put_c(32); put_n(convert(1.0 / 0.0)); put_c(32); put_n(convert(-1.0 / 0.0)); put_c(10);
+ return 0;
+}
+''']
+expected_arithmetic = '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\n'
+for i, source in enumerate(sources):
+ reference = None
+ for command in commands:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=30)
+ assert result.returncode == 0 and result.stdout and 'zc:' not in result.stdout, (command, result.returncode, result.stdout, result.stderr)
+ if reference is None:
+ if i == 0:
+ assert result.stdout == expected_arithmetic, result.stdout
+ else:
+ assert result.stdout.startswith('1 1 1 1\n0.13 -0.13 '), result.stdout
+ assert result.stdout.endswith('0 9223372036854775807 -9223372036854775808\n'), result.stdout
+ reference = result.stdout
+ else:
+ assert result.stdout == reference, (command, i, reference, result.stdout)
+ print('ok float arithmetic and output fixture ' + str(i + 1) + ': ' + command[0])
+
+# an exponent after a long fractional spelling must reach the parser intact.
+literal = '1.' + '0' * 300 + 'e1'
+source = f'fn main() {{ put_n(({literal} as word) == 0x4024000000000000); put_c(10); return 0; }}\n'
+for command in commands:
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=15)
+ assert result.returncode == 0 and result.stdout == '1\n', (command, result.stdout, result.stderr)
+ print('ok float parsing reads the exponent beyond 127 characters: ' + command[0])
diff --git a/tests/drivers/frame_growth_test.py b/tests/drivers/frame_growth_test.py
new file mode 100644
index 0000000..74156e3
--- /dev/null
+++ b/tests/drivers/frame_growth_test.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# growing frames keep pointers into older segments alive through collection.
+import subprocess
+import sys
+
+command = sys.argv[1:]
+source = '''unchecked int descend(int depth, int* root) {
+ [2000]int padding;
+ padding[0] = depth;
+ if (depth == 0) { gc(); return *root; }
+ return descend(depth - 1, root) + padding[0];
+}
+unchecked int oversized(int* root) {
+ [300000]int values;
+ values[0] = *root; values[299999] = 9;
+ gc(); return values[0] + values[299999];
+}
+unchecked fn main() {
+ int value = 7;
+ put_n(descend(300, &value)); put_c(32);
+ put_n(oversized(&value)); put_c(32);
+ gc(); put_n(frame_node == 0); put_c(10);
+ return 0;
+}
+'''
+result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=40)
+assert result.returncode == 0 and result.stdout == '45157 16 1\n', (command, result.returncode, result.stdout, result.stderr)
+print('ok deep and oversized frames preserve borrowed locals through collection: ' + command[0])
+optimized = source.replace('unchecked int descend', 'unchecked o3 int descend').replace('unchecked int oversized', 'unchecked o3 int oversized')
+result = subprocess.run(command, input=optimized, text=True, capture_output=True, timeout=40)
+assert result.returncode == 0 and result.stdout == '45157 16 1\n', (command, result.returncode, result.stdout, result.stderr)
+print('ok optimized recursive calls wait for the matching return depth: ' + command[0])
diff --git a/tests/drivers/genome_blob_test.py b/tests/drivers/genome_blob_test.py
new file mode 100644
index 0000000..81533a7
--- /dev/null
+++ b/tests/drivers/genome_blob_test.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# documentation precedes the symbol table; the boot header must skip those bytes.
+import pathlib
+import re
+
+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
+cursor = words[5] - org
+assert 7 <= cursor < length
+records = {}
+while words[cursor]:
+ kind, value, signature = words[cursor:cursor + 3]
+ assert kind in (1, 2, 3, 4, 6, 8), (cursor, kind)
+ cursor += 3
+ name = ''
+ while words[cursor]:
+ assert 0 < words[cursor] < 128, (cursor, words[cursor])
+ name += chr(words[cursor])
+ cursor += 1
+ 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 any(name.startswith('doc ') for name in records), 'the fixture contains documentation bytes'
+for name, (kind, value, _) in records.items():
+ if kind == 8:
+ assert org <= value < org + length and words[value - org] >= 0, name
+print('ok documented genome starts its symbol table at the header address')
diff --git a/tests/drivers/hover_features_test.js b/tests/drivers/hover_features_test.js
new file mode 100644
index 0000000..895e923
--- /dev/null
+++ b/tests/drivers/hover_features_test.js
@@ -0,0 +1,85 @@
+// focused type, inference, and block-documentation hover regressions.
+// usage: node tests/drivers/hover_features_test.js path/to/zclsp
+const assert = require('assert');
+const cp = require('child_process');
+const server = process.argv[2];
+const uri = 'file:///hover-features.zc';
+const frame = m => {
+ const body = Buffer.from(JSON.stringify(m));
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]);
+};
+function hover(text, needle, nth = 0) {
+ let offset = -1;
+ for (let i = 0; i <= nth; i++) offset = text.indexOf(needle, offset + 1);
+ assert.ok(offset >= 0, 'fixture contains ' + needle);
+ const before = text.slice(0, offset);
+ const line = before.split('\n').length - 1;
+ const character = before.length - before.lastIndexOf('\n') - 1;
+ const messages = [
+ { 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', 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 });
+ assert.strictEqual(result.status, 0, String(result.stderr));
+ let data = result.stdout;
+ while (data.length) {
+ const end = data.indexOf('\r\n\r\n');
+ const count = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
+ const msg = JSON.parse(data.subarray(end + 4, end + 4 + count).toString());
+ data = data.subarray(end + 4 + count);
+ if (msg.id === 2) return msg.result && msg.result.contents.value;
+ }
+ return null;
+}
+for (const type of ['word', 'int', 'char', 'bool', 'float', 'f64', 'f32', 'byte', 'u8', 'i8', 'u16', 'i16', 'u32', 'i32']) {
+ const value = hover(type + ' global;\nfn main() { return 0; }\n', type);
+ assert.ok(value && value.startsWith('```zc\n') && value.includes('\n\n'), type + ' has formatted built-in documentation');
+}
+assert.match(hover('void noop() {}\nfn main() { return 0; }\n', 'void'), /no meaningful result/);
+const array = 'fn main() {\n [4]u8 values;\n return values[0];\n}\n';
+assert.match(hover(array, '[4]'), /fixed length of 4/);
+assert.match(hover(array, 'values', 1), /type \[4\]u8/);
+const struct = 'struct pair { int left; float right; };\nfn main() { pair value; return value.left; }\n';
+assert.match(hover(struct, 'pair', 1), /int left;[\s\S]*float right;/);
+assert.match(hover(struct, 'value', 1), /type pair/);
+const fields = 'struct pair { int /* left field */ left, /* right field */ right; };\nfn main() { pair p; return p.left + p.right; }\n';
+assert.match(hover(fields, 'left +'), /field; type int[\s\S]*left field/);
+assert.match(hover(fields, 'right;'), /field; type int[\s\S]*right field/);
+const empty = '/**/ int helper() { return 1; }\nfn main() { return helper(); }\n';
+assert.match(hover(empty, 'helper();'), /int helper\(\)/);
+const variableDocs = 'fn main() {\n int /* first variable */ first = 1, /* second variable */ second = 2;\n return first + second;\n}\n';
+const first = hover(variableDocs, 'first +');
+const second = hover(variableDocs, 'second;');
+assert.ok(first.includes('first variable') && !first.includes('second variable'), first);
+assert.ok(second.includes('second variable') && !second.includes('first variable'), second);
+const paramDocs = 'int add(int /* left argument */ a, int /* right argument */ b) { return a + b; }\nfn main() { return add(1, 2); }\n';
+const a = hover(paramDocs, 'a +');
+const b = hover(paramDocs, 'b;');
+assert.ok(a.includes('left argument') && !a.includes('right argument'), a);
+assert.ok(b.includes('right argument') && !b.includes('left argument'), b);
+const functionDocs = '/**\n * adds **one** to its argument.\n */\nint add(int x) { return x + 1; }\nfn main() { return add(2); }\n';
+assert.match(hover(functionDocs, 'add(2)'), /adds \*\*one\*\*/);
+const inferred = 'fn twice(x) { return x * 2; }\nfn main() { let value = twice(3); return value; }\n';
+assert.match(hover(inferred, 'twice(3)'), /int twice\(int x\)/);
+assert.match(hover(inferred, 'x *'), /type int/);
+assert.match(hover(inferred, 'value;'), /type int/);
+const pointer = 'int read(int* p) { return p[0]; }\nfn main() { int value = 3; return read(&value); }\n';
+assert.match(hover(pointer, 'p[0]'), /type int\*[\s\S]*pointer to `int`/);
+const alias = 'type row = [3]int;\nfn main() { row values; return values[0]; }\n';
+assert.match(hover(alias, 'row values'), /\[3\]int[\s\S]*fixed length of 3/);
+const enumeration = 'enum color { red, green = 5, blue };\nfn main() { color shade = green; return shade; }\n';
+assert.match(hover(enumeration, 'shade;'), /type color/);
+const dynamic = 'fn main() { []int items; push(items, 3); return len(items); }\n';
+assert.match(hover(dynamic, 'items);'), /type \[\]int[\s\S]*growable array handle/);
+const callable = 'fn main() { let apply = fn(int x) { return x + 1; }; return apply(2); }\n';
+assert.match(hover(callable, 'apply(2)'), /type .*\([\s\S]*callable value/);
+assert.match(hover(callable, 'apply(2)'), /type int\(int\)/);
+const floatClosure = 'fn main() { let f = fn(float x) { return x + 1.5; }; let result = f(2.0); return result; }\n';
+assert.match(hover(floatClosure, 'result;'), /type float/);
+const inferredPointer = 'struct node { int value; };\nfn make_node() { return new(node); }\nfn main() { let p = make_node(); return p.value; }\n';
+assert.match(hover(inferredPointer, 'p.value'), /type node\*/);
+const aggregateAlias = 'fn main() { [3]int a; a[1] = 7; let alias = a; return alias[1]; }\n';
+assert.match(hover(aggregateAlias, 'alias[1]'), /type int\*/);
+console.log('ok built-in and composite types, inferred signatures, and separate block docs');
diff --git a/tests/drivers/hover_test.js b/tests/drivers/hover_test.js
new file mode 100644
index 0000000..10af643
--- /dev/null
+++ b/tests/drivers/hover_test.js
@@ -0,0 +1,23 @@
+// protocol conversion checks. run with node tests/drivers/hover_test.js.
+const assert = require('assert');
+const { hoverContents } = require('../../editors/vscode/client/hover');
+class MarkdownString {
+ constructor() { this.value = ''; this.calls = []; }
+ appendMarkdown(value) { this.calls.push('markdown'); this.value += value; return this; }
+ appendText(value) { this.calls.push('text'); this.value += value; return this; }
+ appendCodeblock(value, language) { this.calls.push('code'); this.value += '\n```' + language + '\n' + value + '\n```\n'; return this; }
+}
+const api = { MarkdownString };
+const markdown = '```zc\nint add(int a, int b)\n```\n\nAdds **two** numbers.';
+const md = hoverContents(api, { kind: 'markdown', value: markdown })[0];
+assert.strictEqual(md.value, markdown);
+assert.deepStrictEqual(md.calls, ['markdown']);
+assert.strictEqual(md.isTrusted, false);
+assert.strictEqual(md.supportHtml, false);
+assert.deepStrictEqual(hoverContents(api, { kind: 'plaintext', value: '*literal*' })[0].calls, ['text']);
+const legacy = hoverContents(api, { language: 'zc', value: 'int x;' })[0];
+assert.match(legacy.value, /```zc\nint x;\n```/);
+assert.deepStrictEqual(legacy.calls, ['code']);
+assert.strictEqual(hoverContents(api, ['**Doc**', { language: 'zc', value: 'fn main()' }]).length, 2);
+for (const contents of [null, undefined, '', [], {}, { value: 123 }]) assert.deepStrictEqual(hoverContents(api, contents), []);
+console.log('ok hover conversion preserves Markdown, plaintext, code, and arrays');
diff --git a/tests/drivers/image_growth_test.py b/tests/drivers/image_growth_test.py
new file mode 100644
index 0000000..064eae7
--- /dev/null
+++ b/tests/drivers/image_growth_test.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# image output can grow past its first buffer and the building host's genome slot.
+import pathlib
+import subprocess
+import sys
+
+root = pathlib.Path(__file__).resolve().parents[2]
+source = '''fn main() {
+ int retained = 7;
+ int n = build_image_grow(GORG, 8);
+ gc();
+ put_n(n > FBOT - GORG); put_c(32);
+ put_n(image_size == n && image_words[6] == GORG + n); put_c(32);
+ put_n(image_words[5] > GORG && image_words[5] < image_words[6]); put_c(32);
+ put_n(retained); put_c(10);
+ return 0;
+}
+`'''
+source += (root / 'src/runtime/rt.zc').read_text()
+source += (root / 'src/runtime/run.zc').read_text()
+source += 'let padding[350000]; fn main() { return padding[349999]; }\n'
+result = subprocess.run(sys.argv[1:], input=source, capture_output=True, text=True, timeout=40)
+assert result.returncode == 0, (result.stdout, result.stderr)
+assert result.stdout == '1 1 1 7\n', result.stdout[:1000]
+print('ok growing image output keeps logical addresses and older frames intact')
diff --git a/tests/drivers/image_run.js b/tests/drivers/image_run.js
new file mode 100644
index 0000000..6d0b949
--- /dev/null
+++ b/tests/drivers/image_run.js
@@ -0,0 +1,14 @@
+// run a generated image directly, avoiding compiler time in vm execution tests.
+const fs = require('fs');
+const { TinyVM } = require('../../src/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);
+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);
+if (process.argv[4] === '--stdin') machine.setInput(fs.readFileSync(0));
+const status = machine.run();
+process.stdout.write(machine.getOutput());
+process.exit(status);
diff --git a/tests/drivers/inference_growth_test.py b/tests/drivers/inference_growth_test.py
new file mode 100644
index 0000000..d747f70
--- /dev/null
+++ b/tests/drivers/inference_growth_test.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+# infer past the original function-record and edit-count limits.
+import re
+import subprocess
+import sys
+
+source = ''.join(f'fn function_{i}(x) {{ return x + 1; }}\n' for i in range(600))
+source += 'fn main() {\n'
+source += ''.join(f'let value_{i} = {i};\n' for i in range(5000))
+source += ''.join(f'function_{i}(1);\n' for i in range(600))
+source += 'return value_4999; }\n'
+result = subprocess.run([sys.argv[1], '--annotate'], input=source, text=True, capture_output=True, timeout=45)
+assert result.returncode == 0, result.stderr
+assert re.search(r'int function_599\(int x\)', result.stdout), result.stdout[-1000:]
+assert 'int value_4999 = 4999;' in result.stdout, result.stdout[-1000:]
+assert 'let value_' not in result.stdout, 'later inference edits were dropped'
+print('ok inference handles 600 functions and 5000 local declarations')
+
+# later parameters and nested functions retain the enclosing inference context.
+params = ', '.join(f'arg_{i}' for i in range(20))
+source = f'fn wide({params}) {{ let nested = fn(int y) {{ return y + 1; }}; return arg_19 + nested(1); }}\n'
+source += 'fn main() { return wide(' + ', '.join('1' for _ in range(20)) + '); }\n'
+result = subprocess.run([sys.argv[1], '--annotate'], input=source, text=True, capture_output=True, timeout=15)
+assert result.returncode == 0, result.stderr
+assert 'int arg_19' in result.stdout, result.stdout
+assert 'int wide(' in result.stdout, result.stdout
+print('ok later arguments and nested closures preserve inferred signatures')
+
+source = 'struct point { int x, y; };\nfn main() { let a[3]; a[1] = 7; let alias = a; point p; p.x = 9; let ref = p; return alias[1] + ref.x; }\n'
+result = subprocess.run([sys.argv[1], '--annotate'], input=source, text=True, capture_output=True, timeout=15)
+assert result.returncode == 0 and 'word* alias' in result.stdout and 'point* ref' in result.stdout, result.stdout
+again = subprocess.run([sys.argv[1], '--annotate'], input=result.stdout, text=True, capture_output=True, timeout=15)
+assert again.returncode == 0 and again.stdout == result.stdout, again.stdout
+print('ok array and struct aliases get compatible pointer annotations')
diff --git a/tests/drivers/layout_config_test.py b/tests/drivers/layout_config_test.py
new file mode 100644
index 0000000..469e7dd
--- /dev/null
+++ b/tests/drivers/layout_config_test.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# generated layouts expose initial reservations and reject arithmetic overflow.
+import pathlib
+import subprocess
+import tempfile
+
+root = pathlib.Path(__file__).resolve().parents[2]
+tool = root / 'bin/genmap'
+result = subprocess.run([tool, '--help'], capture_output=True, text=True)
+assert result.returncode == 0 and '--generated-text' in result.stderr
+invalid = [
+ ['--symbols', '2147483647'], ['--code', '2147483647'],
+ ['--frame', '2147483647'], ['--return', '2147483647'],
+ ['--asm-labels', '2147483647'], ['--enum-members', '2147483647'],
+ ['--regions', '2147483647'], ['--heap', '7'], ['--hash', '3'],
+]
+for options in invalid:
+ result = subprocess.run([tool, '--preset', 'default', *options], capture_output=True, text=True)
+ assert result.returncode != 0 and result.stderr, options
+with tempfile.TemporaryDirectory(dir=root / 'build') as directory:
+ options = ['--asm-labels', '4', '--asm-refs', '8', '--exit', '32', '--enum-members', '8', '--tagged-enums', '2', '--methods', '2', '--generated-text', '64', '--facts', '2', '--paths', '2', '--regions', '2']
+ subprocess.run([tool, '--preset', 'default', *options, '-o', directory], check=True, capture_output=True)
+ source = (pathlib.Path(directory) / 'compiler.zc').read_text()
+ for name, value in [('ASM_LABELS', '4'), ('ASM_REFS', '8'), ('EXIT_WORDS', '20'), ('ENUM_MEMBERS', '8'), ('TAGGED_ENUMS', '2'), ('STRUCT_METHODS', '2'), ('GENERATED_WORDS', '40'), ('RANGE_FACTS', '2'), ('JOIN_PATHS', '2'), ('O3_REGIONS', '2')]:
+ assert f'const {name} = 0x{value};' in source, name
+print('ok parser layout controls and invalid configuration checks')
diff --git a/tests/drivers/learn.py b/tests/drivers/learn.py
new file mode 100644
index 0000000..42f729f
--- /dev/null
+++ b/tests/drivers/learn.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+# checks docs/LEARN.md against the examples it shows. every examples/learn/*.zc runs
+# from the repo root with its stdout compared to the matching .out file, and
+# every ```zc block in docs/LEARN.md must be exactly one of those files, so the
+# guide can't drift from the code it teaches. usage: learn.py path/to/zy
+import glob, os, platform, re, subprocess, sys
+
+root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+os.chdir(root)
+zy = os.path.abspath(sys.argv[1])
+x86_only = {"22-raw-x86"}
+bad = 0
+
+def ok(name):
+ print(f"ok {name}")
+
+def fail(name, why):
+ global bad
+ bad += 1
+ print(f"FAIL {name}: {why}")
+
+files = sorted(glob.glob("examples/learn/*.zc"))
+for path in files:
+ name = os.path.splitext(os.path.basename(path))[0]
+ if name in x86_only and platform.machine() not in ("x86_64", "AMD64"):
+ print(f"skip {name} (x86-64 only)")
+ continue
+ with open(path, "rb") as f:
+ src = f.read()
+ try:
+ r = subprocess.run([zy], stdin=open(path, "rb"), capture_output=True, timeout=30)
+ except subprocess.TimeoutExpired:
+ fail(f"learn {name}", "timed out")
+ continue
+ got = r.stdout.decode("utf-8", "replace")
+ want_path = os.path.splitext(path)[0] + ".out"
+ if not os.path.exists(want_path):
+ fail(f"learn {name}", f"no {want_path}")
+ elif got == open(want_path, encoding="utf-8").read():
+ ok(f"learn {name} prints what it says")
+ else:
+ fail(f"learn {name}", f"got [{got}]")
+
+# every zc block in docs/LEARN.md is one of the example files
+with open("docs/LEARN.md", encoding="utf-8") as f:
+ doc = f.read()
+blocks = re.findall(r"^```zc\n(.*?)^```", doc, re.S | re.M)
+srcs = {}
+for path in files:
+ with open(path, encoding="utf-8") as f:
+ srcs[os.path.basename(path)] = f.read()
+used = set()
+for b in blocks:
+ hit = [n for n, s in srcs.items() if s == b]
+ if hit:
+ used.add(hit[0])
+ else:
+ fail("learn block", "a ```zc block in docs/LEARN.md is not an examples/learn file: " + b.splitlines()[0])
+for n in srcs:
+ if n not in used:
+ fail(f"learn {n}", "not shown in docs/LEARN.md")
+if not bad:
+ ok(f"docs/LEARN.md: {len(blocks)} blocks, all match examples/learn")
+sys.exit(1 if bad else 0)
diff --git a/tests/drivers/local_index_test.py b/tests/drivers/local_index_test.py
new file mode 100644
index 0000000..95bfd78
--- /dev/null
+++ b/tests/drivers/local_index_test.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# local name heads survive shadowing, scope reuse, closures, and context switches.
+import subprocess
+import sys
+source = '''
+fn choose(int x) {
+ let total = x;
+ { let x = 7; total += x; }
+ { let x = 9; total += x; }
+ let f = fn(int x) { return x + 1; };
+ let g = fn() { return total; };
+ return total + x + f(3) + g();
+}
+o3 int fast(int x) { int result = x; { int x = 8; result += x; } return result + x; }
+fn main() { put_n(choose(2)); put_c(32); put_n(fast(3)); put_c(10); return 0; }
+'''
+result = subprocess.run(sys.argv[1:], input=source, text=True, capture_output=True, timeout=15)
+assert result.returncode == 0 and result.stdout == '42 14\n', (result.returncode, result.stdout, result.stderr)
+print('ok local shadow chains, reused scopes, closure parameters, captures, and o3 contexts')
+source = 'struct point { int x, y; };\nfn main() { let a[3]; a[1] = 7; let alias = a; point p; p.x = 9; let ref = p; put_n(alias[1]); put_c(32); put_n(ref.x); put_c(10); return 0; }\n'
+result = subprocess.run(sys.argv[1:], input=source, text=True, capture_output=True, timeout=15)
+assert result.returncode == 0 and result.stdout == '7 9\n', (result.returncode, result.stdout, result.stderr)
+print('ok inferred aliases load array and struct addresses rather than the alias slot')
diff --git a/tests/drivers/lsp_growth_test.js b/tests/drivers/lsp_growth_test.js
new file mode 100644
index 0000000..b06230a
--- /dev/null
+++ b/tests/drivers/lsp_growth_test.js
@@ -0,0 +1,53 @@
+// 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
+const assert = require('assert');
+const cp = require('child_process');
+const { wasmProcess } = require('../../editors/vscode/client/wasmproc');
+const target = process.argv[2];
+const frame = m => {
+ const body = Buffer.from(JSON.stringify(m));
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]);
+};
+function parse(input) {
+ const messages = [];
+ for (let offset = 0; offset < input.length;) {
+ const end = input.indexOf('\r\n\r\n', offset);
+ assert.ok(end >= 0, 'complete response header');
+ const length = Number(/Content-Length:\s*(\d+)/i.exec(input.subarray(offset, end).toString())[1]);
+ const start = end + 4;
+ assert.ok(start + length <= input.length, 'complete response body');
+ messages.push(JSON.parse(input.subarray(start, start + length).toString()));
+ offset = start + length;
+ }
+ return messages;
+}
+const uri = 'file:///' + 'long-directory/'.repeat(30) + 'main.zc';
+const docs = '// **formatted** documentation with `inline code`.\n' + ('// a long paragraph of documentation.\n').repeat(150) + '// final documentation line\n';
+const source = docs + 'int helper() { return 1; }\nfn main() { return helper(); }\n';
+const messages = [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }];
+for (let i = 0; i < 32; i++) messages.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri: i === 31 ? uri : 'file:///doc' + i + '.zc', languageId: 'zc', version: 1, text: source } } });
+messages.push({ jsonrpc: '2.0', id: 2, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: source.split('\n').length - 2, character: 19 } } });
+messages.push({ jsonrpc: '2.0', method: 'exit' });
+let output;
+if (target.endsWith('.wasm')) {
+ const chunks = [];
+ const server = wasmProcess(target);
+ server.stdout.on('data', data => chunks.push(data));
+ 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 });
+ assert.strictEqual(result.status, 0, String(result.stderr));
+ output = result.stdout;
+}
+const responses = parse(output);
+const published = responses.filter(m => m.method === 'textDocument/publishDiagnostics');
+assert.strictEqual(published.length, 32, 'every open document is published');
+assert.strictEqual(published[31].params.uri, uri, 'a long uri is echoed without truncation');
+assert.ok(published.every(m => !m.params.diagnostics.some(d => d.severity === 1)), 'all documents check cleanly');
+const hover = responses.find(m => m.id === 2).result.contents;
+assert.strictEqual(hover.kind, 'markdown');
+assert.match(hover.value, /^```zc\nint helper\(\)\n```\n\n/);
+assert.ok(hover.value.includes('**formatted**'));
+assert.ok(hover.value.includes('final documentation line'), 'hover documentation grows past its initial buffer');
+console.log('ok lsp grows beyond 16 documents, 160 uri bytes, and 2300 hover characters');
diff --git a/tests/drivers/lsp_live.js b/tests/drivers/lsp_live.js
new file mode 100644
index 0000000..b5fe805
--- /dev/null
+++ b/tests/drivers/lsp_live.js
@@ -0,0 +1,23 @@
+// the native server must answer while the client keeps the pipe open and waits, as an editor
+// does: one message in, the answer out, before anything else is sent. an output buffer that
+// is only flushed at exit, or an end-of-input peek that waits for the next message, fails this.
+// usage: node tests/drivers/lsp_live.js path/to/zclsp
+const cp = require('child_process');
+const p = cp.spawn(process.argv[2], []);
+let inbox = Buffer.alloc(0), step = 0, done = false;
+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 finish = (ok, what) => { if (done) return; done = true; p.kill(); console.log((ok ? 'ok ' : 'FAIL ') + what); process.exit(ok ? 0 : 1); };
+const timer = setTimeout(() => finish(false, 'lsp live: no answer in 5 s while the pipe stayed open (step ' + step + ')'), 5000);
+p.stdout.on('data', d => {
+ inbox = Buffer.concat([inbox, d]);
+ const e = inbox.indexOf('\r\n\r\n'); if (e < 0) return;
+ const n = parseInt(/Content-Length: (\d+)/.exec(inbox.slice(0, e).toString())[1], 10);
+ if (inbox.length < e + 4 + n) return;
+ const m = JSON.parse(inbox.slice(e + 4, e + 4 + n).toString());
+ inbox = inbox.slice(e + 4 + n);
+ if (step === 0 && m.id === 1) {
+ step = 1; // the answer came with the pipe still open: ask for something else
+ p.stdin.write(frame({ jsonrpc: '2.0', id: 2, method: 'shutdown', params: null }));
+ } else if (step === 1 && m.id === 2) { clearTimeout(timer); finish(true, 'lsp live: initialize and shutdown are answered while the pipe stays open'); }
+});
+p.stdin.write(frame({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } }));
diff --git a/tests/drivers/lsp_sandbox.js b/tests/drivers/lsp_sandbox.js
new file mode 100644
index 0000000..14bc1e3
--- /dev/null
+++ b/tests/drivers/lsp_sandbox.js
@@ -0,0 +1,22 @@
+// opening a file in the editor checks it, and a check runs the file's compile-time code
+// (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
+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 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: {} } }),
+ frame({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri: 'file:///evil.zc', languageId: 'zc', version: 1, text: src } } }),
+ 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 wrote = fs.existsSync(target);
+if (wrote) fs.unlinkSync(target);
+const answered = r.stdout && r.stdout.includes('"id":3');
+const ok = !wrote && answered;
+console.log((ok ? 'ok ' : 'FAIL ') + 'lsp sandbox: compile-time code cannot write files' + (ok ? '' : ' (wrote: ' + wrote + ', answered: ' + !!answered + ')'));
+process.exit(ok ? 0 : 1);
diff --git a/tests/drivers/lsp_stream.js b/tests/drivers/lsp_stream.js
new file mode 100644
index 0000000..0dd0553
--- /dev/null
+++ b/tests/drivers/lsp_stream.js
@@ -0,0 +1,11 @@
+// expose either extension server host as a stdin/stdout process for focused tests.
+const fs = require('fs');
+const path = require('path');
+const target = path.resolve(process.argv[2]);
+const { wasmProcess } = require('../../editors/vscode/client/wasmproc');
+const { vmProcess } = require('../../editors/vscode/client/vmproc');
+const server = target.endsWith('.wasm') ? wasmProcess(target) : vmProcess(target);
+server.stdout.on('data', bytes => process.stdout.write(bytes));
+server.stderr.on('data', bytes => process.stderr.write(bytes));
+server.on('exit', status => { if (status !== 0) process.exitCode = 1; });
+server.stdin.write(fs.readFileSync(0));
diff --git a/tests/drivers/lsp_test.js b/tests/drivers/lsp_test.js
new file mode 100644
index 0000000..a783e1e
--- /dev/null
+++ b/tests/drivers/lsp_test.js
@@ -0,0 +1,246 @@
+// protocol tests for the language server (node, no packages).
+// usage: node tests/drivers/lsp_test.js path/to/zclsp
+const { spawnSync } = require('child_process');
+const fs = require('fs');
+const server = process.argv[2];
+let ok = 0, bad = 0;
+function check(cond, label, extra) {
+ if (cond) { ok++; console.log('ok lsp ' + label); }
+ else { bad++; console.log('FAIL lsp ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra))); }
+}
+function frame(m) {
+ const b = Buffer.from(JSON.stringify(m), 'utf8');
+ return Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b]);
+}
+function parse(buf) {
+ const out = []; let i = 0;
+ while (i < buf.length) {
+ const j = buf.indexOf('\r\n\r\n', i);
+ if (j < 0) break;
+ const n = parseInt(/Content-Length: (\d+)/.exec(buf.slice(i, j).toString())[1], 10);
+ out.push(JSON.parse(buf.slice(j + 4, j + 4 + n).toString('utf8')));
+ i = j + 4 + n;
+ }
+ return out;
+}
+function run(msgs) {
+ const r = spawnSync(server, [], { input: Buffer.concat(msgs.map(frame)), timeout: 30000, maxBuffer: 1 << 28 });
+ return { code: r.status, msgs: parse(r.stdout) };
+}
+const init = id => ({ jsonrpc: '2.0', id, method: 'initialize', params: {} });
+const open = (uri, text) => ({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } });
+const change = (uri, text) => ({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version: 2 }, contentChanges: [{ text }] } });
+const sem = (id, uri) => ({ jsonrpc: '2.0', id, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } });
+const diags = (msgs, uri) => msgs.filter(m => m.method === 'textDocument/publishDiagnostics' && (!uri || m.params.uri === uri));
+const A = 'file:///a.zc', B2 = 'file:///b.zc';
+
+// initialize: capabilities and the 12 type legend
+let r = run([init(1), { jsonrpc: '2.0', method: 'exit' }]);
+let res = r.msgs.find(m => m.id === 1);
+check(res && res.result.capabilities.semanticTokensProvider.legend.tokenTypes.length === 12, 'initialize advertises the legend');
+check(res && res.result.capabilities.textDocumentSync.change === 1, 'initialize asks for full-text sync');
+
+// a request before initialize is refused, and the id is echoed
+r = run([{ jsonrpc: '2.0', id: 7, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri: A } } }]);
+check(r.msgs.length === 1 && r.msgs[0].error && r.msgs[0].error.code === -32002 && r.msgs[0].id === 7, 'a request before initialize gets -32002');
+
+// unknown methods: MethodNotFound with numeric and string ids, silence for notifications
+r = run([init(1), { jsonrpc: '2.0', id: 'abc', method: 'workspace/nothing' }, { jsonrpc: '2.0', method: 'workspace/nothing' }]);
+const nf = r.msgs.find(m => m.id === 'abc');
+check(nf && nf.error && nf.error.code === -32601, 'unknown request gets MethodNotFound, string id echoed', nf);
+check(r.msgs.length === 2, 'an unknown notification is ignored');
+
+// a clean program has no diagnostics
+const good = 'fn main() {\n\tput_n(1 + 2);\n\treturn 0;\n}\n';
+r = run([init(1), open(A, good)]);
+let d = diags(r.msgs, A);
+check(d.length === 1 && d[0].params.diagnostics.length === 0, 'a clean program publishes an empty list', d);
+
+// an error lands on the right line and column
+const bad1 = 'fn main() {\n\tlet x = 5\n\treturn x;\n}\n';
+r = run([init(1), open(A, bad1)]);
+d = diags(r.msgs, A);
+const e1 = d.length && d[0].params.diagnostics[0];
+check(e1 && e1.severity === 1 && e1.range.start.line === 1 && /expected ';', found 'return'/.test(e1.message), 'a missing semicolon is reported on its line', e1);
+
+// utf-16 columns: an emoji (two code units) and an accented letter before the error
+const uni = '// café 😀\nfn main() { put_s("é😀"); return undefined_name; }\n';
+r = run([init(1), open(A, uni)]);
+d = diags(r.msgs, A);
+const e2 = d.length && d[0].params.diagnostics[0];
+const col = uni.split('\n')[1].indexOf('undefined_name');
+check(e2 && e2.range.start.line === 1 && e2.range.start.character === col, 'columns count utf-16 units', e2 && e2.range);
+
+// fixing the text through didChange clears the error
+r = run([init(1), open(A, bad1), change(A, good)]);
+d = diags(r.msgs, A);
+check(d.length === 2 && d[0].params.diagnostics.length > 0 && d[1].params.diagnostics.length === 0, 'didChange re-checks the document');
+
+// two documents do not leak into each other, and checks repeat the same way
+r = run([init(1), open(A, bad1), open(B2, good), change(A, bad1)]);
+const da = diags(r.msgs, A), db = diags(r.msgs, B2);
+check(da.length === 2 && JSON.stringify(da[0].params.diagnostics) === JSON.stringify(da[1].params.diagnostics) && db[0].params.diagnostics.length === 0, 'documents are checked in isolation');
+
+// syntax modifications made by the program itself apply to the check, like the compiler
+const pipeDef = 'immediate fn pipe_op() { next(); add_binop("|>", 1, "c"); return 0; }\npipe_op\n';
+const pipeUse = 'fn double(x) { return x * 2; }\nfn main() { return 4 |> double; }\n';
+r = run([init(1), open(A, pipeDef + pipeUse), open(B2, pipeUse)]);
+check(diags(r.msgs, A)[0].params.diagnostics.filter(x => x.severity === 1).length === 0, 'a parse word that adds an operator is understood');
+check(diags(r.msgs, B2)[0].params.diagnostics.filter(x => x.severity === 1).length > 0, 'the same program without the parse word is an error');
+
+// semantic tokens: groups of five, keywords and functions classified from the compiler's own tables
+r = run([init(1), open(A, good), sem(2, A)]);
+res = r.msgs.find(m => m.id === 2);
+const data = res && res.result && res.result.data;
+check(data && data.length > 0 && data.length % 5 === 0, 'semantic tokens come in groups of five');
+check(data && data[3] === 0 && data[0] === 0 && data[1] === 0, 'fn is a keyword at 0:0', data && data.slice(0, 5));
+check(data && data[8] === 2, 'a function name is a function', data && data.slice(5, 10));
+
+// decode the semantic tokens of a document into text:type pairs
+function tokens(text) {
+ const rr = run([init(1), open(A, text), sem(2, A)]);
+ const dd = rr.msgs.find(m => m.id === 2).result.data;
+ const kinds = ['keyword', 'type', 'function', 'variable', 'string', 'number', 'operator', 'comment', 'parameter', 'property', 'macro', 'enumMember'];
+ const lines = text.split('\n'); let l = 0, c = 0; const out = [];
+ for (let i = 0; i < dd.length; i += 5) {
+ l += dd[i]; c = dd[i] ? dd[i + 1] : c + dd[i + 1];
+ out.push(lines[l].substr(c, dd[i + 2]) + ':' + kinds[dd[i + 3]]);
+ }
+ return out;
+}
+const meth = 'struct point {\n\tlet x, y;\n\tfn sum() { return this.x + this.y; }\n}\nfn main() {\n\tpoint* p = new(point);\n\treturn p.sum();\n}\n';
+const tk = tokens(meth);
+check(tk.filter(t => t === 'sum:function').length === 2, 'a method is a function where it is declared and where it is called', tk);
+check(tk.includes('this:parameter') && tk.includes('x:property'), 'this is a parameter and a field is a property', tk);
+check(tk.filter(t => t === 'point:type').length >= 3, 'a user type is a type, also before a postfix star', tk);
+
+// a large document goes through the streaming reader
+let big = '';
+for (let i = 0; i < 2500; i++) big += 'fn f' + i + '(a, b) { return a * b + ' + i + '; }\n';
+big += 'fn main() { return f0(1, 2); }\n';
+r = run([init(1), open(A, big)]);
+d = diags(r.msgs, A);
+check(d.length === 1 && d[0].params.diagnostics.filter(x => x.severity === 1).length === 0, 'a document of ' + big.length + ' bytes is checked', d.length);
+
+// local tables grow past their initial reservation, and the next document still checks
+let many = 'fn main() {\n';
+for (let i = 0; i < 600; i++) many += '\tlet v' + i + ' = ' + i + ';\n';
+many += '\treturn 0;\n}\n';
+r = run([init(1), open(A, many), open(B2, good)]);
+d = diags(r.msgs, A);
+check(d.length === 1 && !d[0].params.diagnostics.some(x => x.severity === 1) && diags(r.msgs, B2).length === 1, 'locals grow past 512 and the server carries on', d.length);
+
+// hover: the text at a position, zero-based line and character, or null
+function hover(text, line, ch) {
+ const rr = run([init(1), open(A, text), { jsonrpc: '2.0', id: 3, method: 'textDocument/hover', params: { textDocument: { uri: A }, position: { line, character: ch } } }]);
+ const x = rr.msgs.find(m => m.id === 3);
+ return x && x.result ? x.result.contents.value : null;
+}
+const at = (text, line, word, nth) => { const s = text.split('\n')[line]; let i = -1; for (let k = 0; k <= (nth || 0); k++) i = s.indexOf(word, i + 1); return i; };
+const hsrc = '// adds two numbers\nint add(int a, int b) { return a + b; }\nint twice(int a) { return a * 2; }\nint main() {\n\tint n = add(1, 2);\n\tlet i = 3;\n\treturn twice(n) + i;\n}\n';
+let hv = hover(hsrc, 4, at(hsrc, 4, 'add'));
+check(hv && hv.includes('int add(int a, int b)') && hv.includes('adds two numbers'), 'hover on a call shows the signature and the doc', hv);
+check(hv && /^```zc\n/.test(hv) && /\n```\n\nadds two numbers/.test(hv), 'hover separates the code fence from Markdown documentation', hv);
+const unicodeHover = 'fn main() { put_s("é😀"); put_c(10); return 0; }\n';
+hv = hover(unicodeHover, 0, at(unicodeHover, 0, 'put_c'));
+check(hv && hv.includes('put_c('), 'hover converts UTF-16 columns after BMP and astral characters', hv);
+hv = hover(hsrc, 0, hsrc.split('\n')[0].length + 10);
+check(hv === null, 'an out-of-range hover column cannot select a name on the following line', hv);
+hv = hover(hsrc, 6, at(hsrc, 6, 'twice'));
+check(hv && hv.includes('int twice(int a)') && !hv.includes('adds two numbers'), 'a function with no doc still shows its signature', hv);
+hv = hover(hsrc, 1, at(hsrc, 1, 'a', 2));
+check(hv && hv.includes('parameter') && hv.includes('int a'), 'hover on a parameter shows its declaration', hv);
+hv = hover(hsrc, 5, at(hsrc, 5, 'i'));
+check(hv && hv.includes('let i') && hv.includes('local'), 'hover on a local shows its declaration', hv);
+hv = hover(hsrc, 6, at(hsrc, 6, 'i', 1));
+check(hv && hv.includes('let i') && hv.includes('local'), 'hover on a use of a local shows its declaration', hv);
+const tsrc = 'fn main() {\n\tlet i = 3;\n\tlet p = new(int);\n\treturn i;\n}\n';
+// an untyped parameter and an untyped function show the types annotate gives them
+const isrc = 'fn twice(x) { return x * 2; }\nfn main() {\n\tlet n = twice(4);\n\treturn n;\n}\n';
+let hi = hover(isrc, 0, 9);
+check(hi && hi.includes('x // parameter; type int'), 'an untyped parameter shows its inferred type', hi);
+hi = hover(isrc, 0, 3);
+check(hi && hi.includes('int twice(int x)'), 'an untyped function shows its inferred parameter and return types', hi);
+hv = hover(tsrc, 1, 5);
+check(hv && hv.includes('type int'), 'an untyped number local shows the type annotate gives it', hv);
+hv = hover(tsrc, 2, 5);
+check(hv && hv.includes('type int*'), 'a pointer local shows its pointer type on its declaration line', hv);
+hv = hover('fn main() {\n\tlet i = 3;\n\treturn i;\n}\n', 2, 8);
+check(hv && hv.includes('let i'), 'a return of a local is a use, not a declaration', hv);
+hv = hover(hsrc, 6, at(hsrc, 6, 'return'));
+check(hv && hv.includes('leaves the function with a value'), 'a keyword shows what it does', hv);
+hv = hover('fn main() {\n\tput_c(32);\n\treturn 0;\n}\n', 1, 1);
+check(hv && hv.includes('put_c('), 'a built-in function shows its signature', hv);
+const kwsrc = 'fn main() {\n\tint* p = new(int);\n\tfree(p);\n\treturn 0;\n}\n';
+hv = hover(kwsrc, 1, at(kwsrc, 1, 'new'));
+check(hv && hv.includes('new(T)') && hv.includes('fresh object'), 'hover on new shows its form and what it does', hv);
+hv = hover(kwsrc, 2, at(kwsrc, 2, 'free'));
+check(hv && hv.includes('frees a heap object'), 'hover on free shows its doc', hv);
+const kdoc = '// the number of lines\nfn main() {\n\treturn 0;\n}\n';
+hv = hover(kdoc, 2, 1);
+check(hv === null || !hv.includes('number of lines'), 'a doc comment is not attached to a keyword in a body', hv);
+
+// a method of a struct is indented in the body, and its hover is still its line and doc
+const msrc = 'struct point {\n\tlet x;\n\t// the sum of the parts\n\tfn sum() { return this.x; }\n\tstatic fn zero() { return 0; }\n}\nfn main() {\n\tpoint* p = new(point);\n\treturn p.sum() + point.zero();\n}\n';
+hv = hover(msrc, 8, at(msrc, 8, 'sum'));
+check(hv && hv.includes('fn sum()') && hv.includes('the sum of the parts'), 'a method called through a pointer shows its signature and doc', hv);
+hv = hover(msrc, 8, at(msrc, 8, 'zero'));
+check(hv && hv.includes('static fn zero()'), 'a static function of a struct shows its signature', hv);
+
+// a typed method shows its return type and parameters
+const tmsrc = 'struct point {\n\tlet x;\n\t// the sum of the parts\n\tint sum(int k) { return this.x + k; }\n}\nfn main() {\n\tpoint* p = new(point);\n\treturn p.sum(1);\n}\n';
+hv = hover(tmsrc, 7, at(tmsrc, 7, 'sum'));
+check(hv && hv.includes('int sum(int k)') && hv.includes('the sum of the parts'), 'a typed method shows its return type and parameters', hv);
+
+// externs are declarations, so they draw no "never reassigned" hint
+const ext = 'extern double sqrt(double);\nextern double pow(double x, double y);\nfn main() {\n\treturn 0;\n}\n';
+r = run([init(1), open(A, ext)]);
+d = diags(r.msgs, A);
+check(d.length === 1 && !d[0].params.diagnostics.some(x => /never reassigned/.test(x.message)), 'an extern draws no hint about its stub', d.length && d[0].params.diagnostics);
+
+// shutdown and exit end the process cleanly
+r = run([init(1), { jsonrpc: '2.0', id: 9, method: 'shutdown' }, { jsonrpc: '2.0', method: 'exit' }]);
+check(r.code === 0 && r.msgs.some(m => m.id === 9 && m.result === null), 'shutdown then exit ends with status 0', r.code);
+
+// 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 });
+ return r.stdout.toString();
+}
+let clean = 0, dirty = [];
+for (const dir of ['examples', 'tests/programs']) {
+ for (const f of fs.readdirSync(dir).filter(n => n.endsWith('.zc'))) {
+ if (f === 'manyerrors.zc' || f === 'undefined_call.zc' || f === 'zc0typed.zc') continue; // manyerrors and undefined_call are bad on purpose; zc0typed.zc is stage 0 syntax
+ const out = cli(dir + '/' + f);
+ if (/: error: /.test(out)) dirty.push(dir + '/' + f + ' ' + out.split('\n')[0]); else clean++;
+ }
+}
+check(dirty.length === 0, 'every example and test program checks clean (' + clean + ' files)', dirty);
+let missed = [], n = 0;
+for (const dir of ['errors', 'typeerrors', 'langerrors']) {
+ for (const f of fs.readdirSync('tests/' + dir).filter(x => x.endsWith('.zc'))) {
+ const want = fs.readFileSync('tests/' + dir + '/' + f, 'utf8').split(/\r?\n/)[0].replace('// expect: ', '');
+ const out = cli('tests/' + dir + '/' + f);
+ n++;
+ if (!out.includes(want)) missed.push(dir + '/' + f + ' wanted [' + want + '] got [' + out.split('\n')[0] + ']');
+ }
+}
+check(missed.length === 0, 'every bad program is rejected with the compiler\'s message (' + n + ' files)', missed);
+const manyOut = cli('tests/programs/manyerrors.zc').split('\n').filter(l => /: error: /.test(l));
+check(manyOut.length === 4, 'several errors in one run are all reported', manyOut);
+
+// names resolve the way the compiler resolves them: a local or a parameter named
+// like a built-in is that variable, and the built-in is only what the scope leaves
+const scp = 'int cap = 2;\nfn grow(int cap) { return cap + 1; }\nfn main() {\n\tint cap = 3;\n\treturn len(grow(cap));\n}\n';
+const sct = tokens(scp);
+check(sct.includes('cap:parameter') && sct.includes('cap:variable') && !sct.includes('cap:keyword') && !sct.includes('cap:function'), 'a name like a built-in is a variable or parameter where the scope says', sct);
+check(sct.includes('len:keyword'), 'a built-in that is a keyword in the compiler is still a keyword', sct);
+let hcv = hover(scp, 4, at(scp, 4, 'cap'));
+check(hcv && hcv.includes('int cap') && hcv.includes('local'), 'hover on a use of a local named like a built-in shows the local', hcv);
+hcv = hover(scp, 1, at(scp, 1, 'cap', 1));
+check(hcv && hcv.includes('parameter'), 'hover on a use of a parameter named like a built-in shows the parameter', hcv);
+hcv = hover(scp, 4, at(scp, 4, 'len'));
+check(hcv && hcv.includes('the number of items'), 'hover on the built-in still shows its text where no local shadows it', hcv);
+
+console.log(ok + ' lsp checks passed, ' + bad + ' failed');
+process.exit(bad ? 1 : 0);
diff --git a/tests/drivers/lsp_vm.js b/tests/drivers/lsp_vm.js
new file mode 100644
index 0000000..24c1a5b
--- /dev/null
+++ b/tests/drivers/lsp_vm.js
@@ -0,0 +1,112 @@
+// tests for the language server run by the javascript vm (src/js/tinyvm.js), through
+// the extension's own host (editors/vscode/client/vmproc.js): the same checks as
+// tests/drivers/lsp_wasm.js. the vm starts at the image's heap and grows as the server asks.
+// usage: node tests/drivers/lsp_vm.js path/to/server/dir (tinyvm.js, lsp.json, layout.js)
+const path = require('path');
+const { vmProcess } = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'vmproc.js'));
+const fs = require('fs');
+const dir = path.resolve(process.argv[2]);
+let ok = 0, bad = 0;
+function check(cond, label, extra) {
+ if (cond) { ok++; console.log('ok lsp vm ' + label); }
+ else { bad++; console.log('FAIL lsp vm ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra).slice(0, 300))); }
+}
+// runs one session: the messages are sent, and the answers collected by id
+function session(docs) {
+ const replies = {}, notes = [];
+ const p = vmProcess(dir, { log: () => {} });
+ const exited = { code: undefined };
+ p.on('exit', c => { exited.code = c; });
+ let inbox = Buffer.alloc(0);
+ p.stdout.on('data', d => {
+ inbox = Buffer.concat([inbox, d]);
+ for (;;) {
+ const e = inbox.indexOf('\r\n\r\n'); if (e < 0) return;
+ const n = parseInt(/Content-Length: (\d+)/.exec(inbox.slice(0, e).toString())[1], 10);
+ if (inbox.length < e + 4 + n) return;
+ const m = JSON.parse(inbox.slice(e + 4, e + 4 + n).toString('utf8'));
+ inbox = inbox.slice(e + 4 + n);
+ if (m.id !== undefined) replies[m.id] = m; else notes.push(m);
+ }
+ });
+ const send = m => { const b = Buffer.from(JSON.stringify(m), 'utf8'); p.stdin.write(Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b])); };
+ send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } });
+ send({ jsonrpc: '2.0', method: 'initialized', params: {} });
+ for (const [uri, text] of docs) send({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } });
+ return { p, replies, notes, send, exited };
+}
+const clean = 'int main() {\n\tint x = 1;\n\treturn x;\n}\n';
+const bad_src = 'int main() {\n\tint x = 1\n\treturn x;\n}\n';
+const tokOf = (s, uri) => {
+ s.send({ jsonrpc: '2.0', id: 2, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } });
+ s.send({ jsonrpc: '2.0', id: 3, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: 1, character: 9 } } });
+ s.send({ jsonrpc: '2.0', id: 4, method: 'shutdown', params: null });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+};
+const diagsOf = (s, uri) => s.notes.filter(m => m.method === 'textDocument/publishDiagnostics' && m.params.uri === uri);
+
+// a clean program: initialize, no diagnostics, tokens, and it exits on its own
+{
+ const s = session([['file:///a.zc', clean]]);
+ tokOf(s, 'file:///a.zc');
+ const init = s.replies[1];
+ check(init && init.result.capabilities.semanticTokensProvider && init.result.capabilities.hoverProvider, 'initialize advertises tokens and hover', init);
+ check(diagsOf(s, 'file:///a.zc').length === 1 && diagsOf(s, 'file:///a.zc')[0].params.diagnostics.every(x => x.severity !== 1), 'a clean program has no errors (hints are fine)');
+ const t = s.replies[2];
+ check(t && t.result && t.result.data.length > 0 && t.result.data.length % 5 === 0, 'semantic tokens come back in groups of five', t && t.result && t.result.data.length);
+ check(s.replies[4] && s.replies[4].result === null, 'shutdown is answered');
+ check(s.p.stdin.write(Buffer.alloc(0)) === false, 'the server has exited after exit');
+}
+// a broken program: the compiler's own message is the diagnostic
+{
+ const s = session([['file:///b.zc', bad_src]]);
+ const d = diagsOf(s, 'file:///b.zc');
+ check(d.length === 1 && d[0].params.diagnostics.length > 0, 'a missing semicolon gives a diagnostic');
+ check(d.length === 1 && d[0].params.diagnostics.some(x => /expected/.test(x.message)), 'the message is the compiler\'s', d);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+}
+// hover: a name the document declares, and a compiler name with docs in the image
+{
+ const src = '// adds one to its argument\nint inc(int x) { return x + 1; }\nint main() {\n\treturn inc(2);\n}\n';
+ const s = session([['file:///c.zc', src]]);
+ s.send({ jsonrpc: '2.0', id: 5, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///c.zc' }, position: { line: 3, character: 9 } } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ const h = s.replies[5];
+ check(h && h.result && /adds one/.test(h.result.contents.value), 'hover shows the comment above a declaration', h);
+ const s2 = session([['file:///d.zc', 'int main() {\n\tint z = docofs("x");\n\treturn 0;\n}\n']]);
+ s2.send({ jsonrpc: '2.0', id: 6, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///d.zc' }, position: { line: 1, character: 10 } } });
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ const h2 = s2.replies[6];
+ check(h2 && h2.result && /packed documentation/.test(h2.result.contents.value), 'hover shows the docs of a compiler name', h2);
+}
+// a document that is bigger than one feed: the input is split and comes out whole
+{
+ let big = 'int main() {\n';
+ for (let i = 0; i < 4000; i++) big += '\tint v' + i + ' = ' + i + ';\n';
+ big += '\treturn 0;\n}\n';
+ const s = session([['file:///e.zc', big]]);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///e.zc').length === 1, 'a document bigger than one feed is read whole (' + big.length + ' bytes)');
+}
+// the regression: the server halted without an answer on any document that
+// needed more memory than its first heap (examples/bf.zc, and a document twice
+// the size of the earlier one). each must be checked and published, and the
+// server must still be running until it is told to exit
+{
+ const bfPath = path.join(__dirname, '..', '..', 'examples', 'bf.zc');
+ const bf = fs.readFileSync(bfPath, 'utf8');
+ const s = session([['file:///bf.zc', bf]]);
+ s.send({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri: 'file:///bf.zc', version: 2 }, contentChanges: [{ text: bf }] } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///bf.zc').length >= 1, 'examples/bf.zc is checked and published (' + bf.length + ' bytes)');
+ check(s.exited.code === 0, 'the server runs until exit, then leaves with status 0', s.exited);
+ let huge = 'int main() {\n';
+ for (let i = 0; i < 12000; i++) huge += '\tint h' + i + ' = ' + i + ' + ' + (i % 7) + ';\n';
+ huge += '\treturn 0;\n}\n';
+ const s2 = session([['file:///huge.zc', huge]]);
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s2, 'file:///huge.zc').length === 1, 'a document past the first heap is checked and published (' + huge.length + ' bytes)', s2.exited);
+ check(s2.exited.code === 0, 'the big document leaves the server with status 0 after exit', s2.exited);
+}
+console.log(ok + ' lsp vm checks passed, ' + bad + ' failed');
+process.exit(bad ? 1 : 0);
diff --git a/tests/drivers/lsp_wasm.js b/tests/drivers/lsp_wasm.js
new file mode 100644
index 0000000..4ff2d16
--- /dev/null
+++ b/tests/drivers/lsp_wasm.js
@@ -0,0 +1,112 @@
+// tests for the language server as WebAssembly, through the extension's own
+// host (editors/vscode/client/wasmproc.js). the same protocol checks as the
+// native server, plus the hover docs of a compiler name.
+// usage: node tests/drivers/lsp_wasm.js path/to/lsp.wasm
+const path = require('path');
+const { wasmProcess } = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'wasmproc.js'));
+const fs = require('fs');
+const wasm = process.argv[2];
+let ok = 0, bad = 0;
+function check(cond, label, extra) {
+ if (cond) { ok++; console.log('ok lsp wasm ' + label); }
+ else { bad++; console.log('FAIL lsp wasm ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra).slice(0, 300))); }
+}
+// runs one session: the messages are sent, and the answers collected by id
+function session(docs) {
+ const replies = {}, notes = [];
+ const p = wasmProcess(wasm, { log: () => {} });
+ const exited = { code: undefined };
+ p.on('exit', c => { exited.code = c; });
+ let inbox = Buffer.alloc(0);
+ p.stdout.on('data', d => {
+ inbox = Buffer.concat([inbox, d]);
+ for (;;) {
+ const e = inbox.indexOf('\r\n\r\n'); if (e < 0) return;
+ const n = parseInt(/Content-Length: (\d+)/.exec(inbox.slice(0, e).toString())[1], 10);
+ if (inbox.length < e + 4 + n) return;
+ const m = JSON.parse(inbox.slice(e + 4, e + 4 + n).toString('utf8'));
+ inbox = inbox.slice(e + 4 + n);
+ if (m.id !== undefined) replies[m.id] = m; else notes.push(m);
+ }
+ });
+ const send = m => { const b = Buffer.from(JSON.stringify(m), 'utf8'); p.stdin.write(Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b])); };
+ send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } });
+ send({ jsonrpc: '2.0', method: 'initialized', params: {} });
+ for (const [uri, text] of docs) send({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } });
+ return { p, replies, notes, send, exited };
+}
+const clean = 'int main() {\n\tint x = 1;\n\treturn x;\n}\n';
+const bad_src = 'int main() {\n\tint x = 1\n\treturn x;\n}\n';
+const tokOf = (s, uri) => {
+ s.send({ jsonrpc: '2.0', id: 2, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } });
+ s.send({ jsonrpc: '2.0', id: 3, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: 1, character: 9 } } });
+ s.send({ jsonrpc: '2.0', id: 4, method: 'shutdown', params: null });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+};
+const diagsOf = (s, uri) => s.notes.filter(m => m.method === 'textDocument/publishDiagnostics' && m.params.uri === uri);
+
+// a clean program: initialize, no diagnostics, tokens, and it exits on its own
+{
+ const s = session([['file:///a.zc', clean]]);
+ tokOf(s, 'file:///a.zc');
+ const init = s.replies[1];
+ check(init && init.result.capabilities.semanticTokensProvider && init.result.capabilities.hoverProvider, 'initialize advertises tokens and hover', init);
+ check(diagsOf(s, 'file:///a.zc').length === 1 && diagsOf(s, 'file:///a.zc')[0].params.diagnostics.every(x => x.severity !== 1), 'a clean program has no errors (hints are fine)');
+ const t = s.replies[2];
+ check(t && t.result && t.result.data.length > 0 && t.result.data.length % 5 === 0, 'semantic tokens come back in groups of five', t && t.result && t.result.data.length);
+ check(s.replies[4] && s.replies[4].result === null, 'shutdown is answered');
+ check(s.p.stdin.write(Buffer.alloc(0)) === false, 'the server has exited after exit');
+}
+// a broken program: the compiler's own message is the diagnostic
+{
+ const s = session([['file:///b.zc', bad_src]]);
+ const d = diagsOf(s, 'file:///b.zc');
+ check(d.length === 1 && d[0].params.diagnostics.length > 0, 'a missing semicolon gives a diagnostic');
+ check(d.length === 1 && d[0].params.diagnostics.some(x => /expected/.test(x.message)), 'the message is the compiler\'s', d);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+}
+// hover: a name the document declares, and a compiler name with docs in the image
+{
+ const src = '// adds one to its argument\nint inc(int x) { return x + 1; }\nint main() {\n\treturn inc(2);\n}\n';
+ const s = session([['file:///c.zc', src]]);
+ s.send({ jsonrpc: '2.0', id: 5, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///c.zc' }, position: { line: 3, character: 9 } } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ const h = s.replies[5];
+ check(h && h.result && /adds one/.test(h.result.contents.value), 'hover shows the comment above a declaration', h);
+ const s2 = session([['file:///d.zc', 'int main() {\n\tint z = docofs("x");\n\treturn 0;\n}\n']]);
+ s2.send({ jsonrpc: '2.0', id: 6, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///d.zc' }, position: { line: 1, character: 10 } } });
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ const h2 = s2.replies[6];
+ check(h2 && h2.result && /packed documentation/.test(h2.result.contents.value), 'hover shows the docs of a compiler name', h2);
+}
+// a document that is bigger than one feed: the input is split and comes out whole
+{
+ let big = 'int main() {\n';
+ for (let i = 0; i < 4000; i++) big += '\tint v' + i + ' = ' + i + ';\n';
+ big += '\treturn 0;\n}\n';
+ const s = session([['file:///e.zc', big]]);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///e.zc').length === 1, 'a document bigger than one feed is read whole (' + big.length + ' bytes)');
+}
+// the regression: the server halted without an answer on any document that
+// needed more memory than its first heap (examples/bf.zc, and a document twice
+// the size of the earlier one). each must be checked and published, and the
+// server must still be running until it is told to exit
+{
+ const bfPath = path.join(__dirname, '..', '..', 'examples', 'bf.zc');
+ const bf = fs.readFileSync(bfPath, 'utf8');
+ const s = session([['file:///bf.zc', bf]]);
+ s.send({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri: 'file:///bf.zc', version: 2 }, contentChanges: [{ text: bf }] } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///bf.zc').length >= 1, 'examples/bf.zc is checked and published (' + bf.length + ' bytes)');
+ check(s.exited.code === 0, 'the server runs until exit, then leaves with status 0', s.exited);
+ let huge = 'int main() {\n';
+ for (let i = 0; i < 12000; i++) huge += '\tint h' + i + ' = ' + i + ' + ' + (i % 7) + ';\n';
+ huge += '\treturn 0;\n}\n';
+ const s2 = session([['file:///huge.zc', huge]]);
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s2, 'file:///huge.zc').length === 1, 'a document past the first heap is checked and published (' + huge.length + ' bytes)', s2.exited);
+ check(s2.exited.code === 0, 'the big document leaves the server with status 0 after exit', s2.exited);
+}
+console.log(ok + ' lsp wasm checks passed, ' + bad + ' failed');
+process.exit(bad ? 1 : 0);
diff --git a/tests/drivers/memory_limits_test.py b/tests/drivers/memory_limits_test.py
new file mode 100644
index 0000000..a973480
--- /dev/null
+++ b/tests/drivers/memory_limits_test.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# focused host memory configuration and growth checks.
+import os
+import subprocess
+
+source = '''fn main() {
+ int initial = syscall(9, 0, 0, 0);
+ put_n(syscall(9, 5, initial, 0) == initial); put_c(32);
+ put_n(syscall(9, 4, initial + 4096, 0) == -1); put_c(32);
+ put_n(syscall(9, 5, initial + 8192, 0) == initial + 8192); put_c(32);
+ put_n(syscall(9, 4, initial + 4096, 0) == initial + 4096); put_c(32);
+ put_n(syscall(9, 5, initial - 1, 0) == -1); put_c(10);
+ return 0;
+}
+'''
+commands = [['bin/zy'], ['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)
+ assert result.returncode == 0 and result.stdout == '1 1 1 1 1\n', (command, result.stdout, result.stderr)
+ invalid = subprocess.run(command, input=source, text=True, capture_output=True, env=dict(environment, TINYVM_MEMORY_WORDS='1'), timeout=15)
+ assert invalid.returncode != 0 and invalid.stderr, (command, invalid.stdout)
+ print('ok runtime memory limits, denied growth, successful growth, and invalid startup: ' + command[0])
diff --git a/tests/drivers/parser_growth_test.py b/tests/drivers/parser_growth_test.py
new file mode 100644
index 0000000..4cbfbb2
--- /dev/null
+++ b/tests/drivers/parser_growth_test.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# parser storage grows across enum members, tagged enums, methods, and generated text.
+import subprocess
+import sys
+
+command = sys.argv[1:]
+
+
+def run(source, expected):
+ result = subprocess.run(command, input=source, text=True, capture_output=True, timeout=30)
+ assert result.returncode == 0, (result.stdout, result.stderr)
+ assert result.stdout == expected, result.stdout[:1000]
+
+
+source = 'enum values { ' + ', '.join(f'member_{i}' for i in range(300)) + ' };\n'
+source += 'fn main() { gc(); put_n(member_299); put_c(10); return 0; }\n'
+run(source, '299\n')
+source = ''.join(f'enum tagged_{i} {{ item_{i} {{ int value; }}, empty_{i} }};\n' for i in range(25))
+source += 'fn main() { tagged_24*.m item = tagged_24.item_24(7); gc(); put_n(item.item_24_value); put_c(10); return 0; }\n'
+run(source, '7\n')
+source = 'struct methods { int value;\n'
+source += ''.join(f'int get_{i}() {{ return this.value + {i}; }}\n' for i in range(100))
+source += '}\nfn main() { methods item; item.value = 7; gc(); put_n(item.get_99()); put_c(10); return 0; }\n'
+run(source, '106\n')
+source = 'enum expanded {\n'
+source += ',\n'.join(f'member_with_long_generated_name_{i} {{ ' + ''.join(f'int field_with_long_generated_name_{j}; ' for j in range(8)) + '}' for i in range(40))
+source += '\n};\nfn main() { gc(); put_n(ebcap > 8192); put_c(10); return 0; }\n'
+run(source, '1\n')
+source = 'fn main() { int result = 0; switch (127) {\n'
+source += ''.join(f'case {i}: result = {i};\n' for i in range(128))
+source += '} gc(); put_n(result); put_c(32); put_n(jpscap > 64); put_c(10); return 0; }\n'
+run(source, '127 1\n')
+source = 'fn main() { int result = 0;\n'
+source += ''.join('o3 { result += 1; }\n' for _ in range(40))
+source += 'gc(); put_n(result); put_c(32); put_n(o3pendcap > 10); put_c(10); return 0; }\n'
+run(source, '40 1\n')
+source = 'fn main() { int i = 0; while (i < 120) { putfact(i, i, 1); i++; } gc(); put_n(factcap > 32); put_c(32); put_n(getlo(119)); put_c(10); return 0; }\n'
+run(source, '1 119\n')
+long_type = 'structure_' + 't' * 180
+long_method = 'method_' + 'm' * 180
+source = f'struct {long_type} {{ int {long_method}() {{ return 7; }} }};\n'
+source += f'fn main() {{ {long_type} value; put_n(value.{long_method}()); put_c(10); return 0; }}\n'
+run(source, '7\n')
+name = 'generic_' + 'g' * 180
+source = f'int {name}_one<T>() {{ return sizeof(T); }}\nint {name}_two<T>() {{ return sizeof(T) + 1; }}\n'
+source += f'fn main() {{ put_n({name}_one<int>() + {name}_two<int>()); put_c(10); return 0; }}\n'
+run(source, '3\n')
+print('ok parser tables, analyzer paths, queued regions, and generated names grow')
diff --git a/tests/drivers/protocol_growth_test.js b/tests/drivers/protocol_growth_test.js
new file mode 100644
index 0000000..64015d8
--- /dev/null
+++ b/tests/drivers/protocol_growth_test.js
@@ -0,0 +1,34 @@
+// preserve long request identifiers and ignore deeply nested unknown json fields.
+const assert = require('assert');
+const cp = require('child_process');
+const target = process.argv[2];
+const id = 'request_' + 'x'.repeat(500);
+let nested = { ['unknown_' + 'y'.repeat(100)]: 1 };
+for (let i = 0; i < 40; i++) nested = { child: nested };
+const frame = message => {
+ const body = Buffer.from(JSON.stringify(message));
+ return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\nX-Extra: ' + 'z'.repeat(500) + '\r\n\r\n'), body]);
+};
+const messages = [{ jsonrpc: '2.0', id, method: 'initialize', extra: nested, params: {} }];
+for (let i = 0; i < 24; i++) messages.push({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: {
+ uri: 'file:///enum_' + i + '.zc', version: 1, languageId: 'zc',
+ 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 });
+assert.strictEqual(result.status, 0, String(result.stderr));
+const responses = [];
+let data = result.stdout;
+while (data.length) {
+ const end = data.indexOf('\r\n\r\n');
+ assert.ok(end >= 0);
+ const length = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
+ responses.push(JSON.parse(data.subarray(end + 4, end + 4 + length).toString()));
+ data = data.subarray(end + 4 + length);
+}
+assert.ok(responses.some(message => message.id === id && message.result.capabilities), 'the full request id is echoed');
+assert.ok(responses.some(message => message.id === 'finish'), 'the next request is parsed');
+const diagnostics = responses.filter(message => message.method === 'textDocument/publishDiagnostics');
+assert.strictEqual(diagnostics.length, 24);
+assert.ok(diagnostics.every(message => !message.params.diagnostics.some(diagnostic => diagnostic.severity === 1)), JSON.stringify(diagnostics));
+console.log('ok long protocol fields, deep json, and tagged enums across document checks');
diff --git a/tests/drivers/seed_growth_test.py b/tests/drivers/seed_growth_test.py
new file mode 100644
index 0000000..426c970
--- /dev/null
+++ b/tests/drivers/seed_growth_test.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# stage 0 accepts long source streams and grows a tiny initial image reservation.
+import pathlib
+import subprocess
+import tempfile
+
+root = pathlib.Path(__file__).resolve().parents[2]
+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)
+ 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)
+ 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
new file mode 100644
index 0000000..d4abe81
--- /dev/null
+++ b/tests/drivers/stack_growth_test.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+# operand values and return addresses survive explicit and automatic growth.
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+command = sys.argv[1:]
+
+
+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.
+ 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)
+ 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;
+}
+''', '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;
+}
+''', '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'
+run(source, '800020000 1\n', compile_image=True)
+
+run('''unchecked int descend(int n) {
+ if (!n) { 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; }
+''', '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
new file mode 100644
index 0000000..da967f5
--- /dev/null
+++ b/tests/drivers/storage_analysis_test.js
@@ -0,0 +1,71 @@
+// definite storage findings use the compiler's parser and the editor protocol.
+const assert = require('assert');
+const cp = require('child_process');
+const server = process.argv[2];
+function check(text, needle) {
+ 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 } } }];
+ if (needle) {
+ const at = text.lastIndexOf(needle), before = text.slice(0, at);
+ assert.ok(at >= 0);
+ messages.push({ jsonrpc: '2.0', id: 2, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } } });
+ }
+ 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 });
+ assert.strictEqual(r.status, 0, String(r.stderr));
+ const replies = []; let data = r.stdout;
+ while (data.length) {
+ const end = data.indexOf('\r\n\r\n'); assert.ok(end >= 0);
+ const n = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
+ replies.push(JSON.parse(data.subarray(end + 4, end + 4 + n)));
+ data = data.subarray(end + 4 + n);
+ }
+ const diagnostics = replies.find(m => m.method === 'textDocument/publishDiagnostics').params.diagnostics;
+ return { diagnostics, warnings: diagnostics.filter(d => d.severity === 2), hover: replies.find(m => m.id === 2)?.result?.contents?.value };
+}
+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[');
+ 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');
+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[');
+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');
+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');
+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 + ' 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]; }',
+ '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]; }',
+];
+for (const source of safe) assert.deepStrictEqual(check(source).warnings, [], source);
+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/);
+console.log('ok definite bounds, free and scope lifetimes, alias hovers, and conservative unknown cases');
diff --git a/tests/drivers/vscode_hover_test.js b/tests/drivers/vscode_hover_test.js
new file mode 100644
index 0000000..378dda2
--- /dev/null
+++ b/tests/drivers/vscode_hover_test.js
@@ -0,0 +1,48 @@
+// run inside a vscode extension test host to check the real hover api objects.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vscode = require('vscode');
+exports.run = async () => {
+ const root = path.resolve(__dirname, '..', '..');
+ const fixture = path.join(root, 'build', 'review', 'hover-fixture.zc');
+ fs.mkdirSync(path.dirname(fixture), { recursive: true });
+ fs.writeFileSync(fixture, '// adds **two** numbers with `integer` arithmetic.\nint add(int a, int b) { return a + b; }\nfn main() { return add(1, 2); }\n');
+ const extension = vscode.extensions.getExtension('tinyvm.zc-language-support');
+ assert.ok(extension, 'development extension is loaded');
+ const doc = await vscode.workspace.openTextDocument(fixture);
+ await vscode.window.showTextDocument(doc);
+ await extension.activate();
+ let hover;
+ for (let attempt = 0; attempt < 100; attempt++) {
+ const replies = await vscode.commands.executeCommand('vscode.executeHoverProvider', doc.uri, new vscode.Position(2, 19));
+ hover = replies && replies.find(reply => reply.contents.some(md => md.value.includes('adds **two** numbers')));
+ if (hover) break;
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ assert.ok(hover, 'real vscode provider returns declaration documentation');
+ const md = hover.contents[0];
+ assert.ok(md instanceof vscode.MarkdownString, 'hover contains a real MarkdownString');
+ assert.match(md.value, /^```zc\nint add\(int a, int b\)\n```\n\n/);
+ assert.ok(md.value.includes('**two**') && md.value.includes('`integer`'));
+ assert.strictEqual(md.isTrusted, false);
+ const storageFile = path.join(root, 'build', 'review', 'hover-storage.zc');
+ fs.writeFileSync(storageFile, 'unchecked fn main() {\n let p = new(int, 3);\n let /* borrowed array */ q = p;\n free(p);\n return q[0];\n}\n');
+ const storageDoc = await vscode.workspace.openTextDocument(storageFile);
+ await vscode.window.showTextDocument(storageDoc);
+ let storageHover;
+ for (let attempt = 0; attempt < 100; attempt++) {
+ const replies = await vscode.commands.executeCommand('vscode.executeHoverProvider', storageDoc.uri, new vscode.Position(4, 8));
+ storageHover = replies && replies.find(reply => reply.contents.some(md => md.value.includes('size: 3 elements')));
+ if (storageHover) break;
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ assert.ok(storageHover, 'real editor returns storage metadata');
+ const storageMd = storageHover.contents[0];
+ assert.ok(storageMd instanceof vscode.MarkdownString);
+ assert.match(storageMd.value, /borrowed array/);
+ assert.match(storageMd.value, /lifetime: ended on line 4/);
+ const result = 'ok real vscode markdown, code fences, block docs, inferred pointer types, and lifetime metadata\n';
+ fs.writeFileSync(path.join(root, 'build', 'review', 'vscode-hover.log'), result);
+ console.log(result.trim());
+};
diff --git a/tests/drivers/wasm_test.js b/tests/drivers/wasm_test.js
new file mode 100644
index 0000000..ddab3be
--- /dev/null
+++ b/tests/drivers/wasm_test.js
@@ -0,0 +1,28 @@
+// runs programs on the webassembly build (src/web/zc.wasm) 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 cases = [
+ ['examples/hello.zc', 'hello, world!'],
+ ['examples/fact.zc', '1101110101111100000000\n3628800\n'],
+ ['examples/selfpatch.zc', 'ABC'],
+ ['examples/lists.zc', '[0 1 4 9 16 25]\nlive: 7\nafter the block: 0\n'],
+ ['examples/wiring.zc', '16 1234 25 4/9'],
+ ['examples/struct_closure_opt.zc', '299\n'],
+ ['examples/opt_test.zc', '1303\n'],
+ ['tests/programs/basics.zc', '285 610 42 512\nhey 65 -3 -2 1024 -8\n12456\n757\n9 100-101\n5\nok\n'],
+ ];
+ let ok = 0, bad = 0;
+ for (const [file, want] of cases) {
+ const r = await zc.run(fs.readFileSync(__dirname + '/../../' + file, 'utf8'));
+ if (r.output.replace(/\n+$/, '') === want.replace(/\n+$/, '')) { ok++; console.log('ok wasm ' + file); }
+ else { bad++; console.log('FAIL wasm ' + file + ': got ' + JSON.stringify(r.output) + ' ' + r.messages); }
+ }
+ // an error is reported the way the interpreter reports it
+ const r = await zc.run('fn main() { return undefined_name; }');
+ if (/undefined_name/.test(r.output)) { ok++; console.log('ok wasm compiler errors'); } else { bad++; console.log('FAIL wasm compiler errors: ' + JSON.stringify(r.output)); }
+ console.log(ok + ' wasm checks passed, ' + bad + ' failed');
+ process.exit(bad ? 1 : 0);
+})();
diff --git a/tests/drivers/word_literals_test.py b/tests/drivers/word_literals_test.py
new file mode 100644
index 0000000..a2d230d
--- /dev/null
+++ b/tests/drivers/word_literals_test.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+# every 64-bit word is representable, even outside a single literal's 63-bit range.
+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']]:
+ 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])
+source = '''immediate fn add_word_operator() { next(); add_binop("**", 11, "4611686018427387904 | +"); return 0; }
+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']]:
+ 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/errors/alias_field.zc b/tests/errors/alias_field.zc
new file mode 100644
index 0000000..09086ca
--- /dev/null
+++ b/tests/errors/alias_field.zc
@@ -0,0 +1,9 @@
+// expect: c: used after it was given away
+struct node { int v; node* next; };
+fn main() {
+ node* p = new(node), n = new(node);
+ p.next = n;
+ node* c = p.next;
+ free(d*p);
+ return c.v;
+}
diff --git a/tests/errors/alias_index.zc b/tests/errors/alias_index.zc
new file mode 100644
index 0000000..3c036fe
--- /dev/null
+++ b/tests/errors/alias_index.zc
@@ -0,0 +1,9 @@
+// expect: c: used after it was given away
+fn main() {
+ int** list = new(int*, 2);
+ int* item = new(int);
+ list[1] = item;
+ int* c = list[1];
+ free(d*list);
+ return c[0];
+}
diff --git a/tests/errors/alias_plus.zc b/tests/errors/alias_plus.zc
new file mode 100644
index 0000000..d42d196
--- /dev/null
+++ b/tests/errors/alias_plus.zc
@@ -0,0 +1,2 @@
+// expect: q: used after it was given away
+fn main() { let p = new(2); p[0] = 1; let q = p + 0; free(d*p); return q[0]; }
diff --git a/tests/errors/alias_scope.zc b/tests/errors/alias_scope.zc
new file mode 100644
index 0000000..b51bec5
--- /dev/null
+++ b/tests/errors/alias_scope.zc
@@ -0,0 +1,6 @@
+// expect: q: used after it was given away
+fn main() {
+ let q;
+ { let p = new(2); q = p + 0; }
+ return q[0];
+}
diff --git a/tests/errors/back_to_m.zc b/tests/errors/back_to_m.zc
new file mode 100644
index 0000000..b8c160f
--- /dev/null
+++ b/tests/errors/back_to_m.zc
@@ -0,0 +1,2 @@
+// expect: x: can't be recast back to m
+fn main() { let x = new(3); d*x; m*x; return 0; }
diff --git a/tests/errors/borrow_escape.zc b/tests/errors/borrow_escape.zc
new file mode 100644
index 0000000..ce24480
--- /dev/null
+++ b/tests/errors/borrow_escape.zc
@@ -0,0 +1,3 @@
+// expect: p: is owned and can't be returned
+fn f(.m p) { return p; }
+fn main() { let x = new(1); f(x); return 0; }
diff --git a/tests/errors/branch_mismatch.zc b/tests/errors/branch_mismatch.zc
new file mode 100644
index 0000000..bd19968
--- /dev/null
+++ b/tests/errors/branch_mismatch.zc
@@ -0,0 +1,3 @@
+// expect: x: ends the branches of an if in different states
+fn take(.d p) { return p[0]; }
+fn main() { let x = new(1); if (x[0]) take(d*x); else u*x; return 0; }
diff --git a/tests/errors/child_dies.zc b/tests/errors/child_dies.zc
new file mode 100644
index 0000000..67caca9
--- /dev/null
+++ b/tests/errors/child_dies.zc
@@ -0,0 +1,3 @@
+// expect: c: used after it was given away
+fn take(.d p) { return p[0]; }
+fn main() { let p = new(2), c = new(1); p[0] = c; take(d*p); return c[0]; }
diff --git a/tests/errors/child_recast.zc b/tests/errors/child_recast.zc
new file mode 100644
index 0000000..c9ccd62
--- /dev/null
+++ b/tests/errors/child_recast.zc
@@ -0,0 +1,2 @@
+// expect: c: belongs to a container; recast the container instead
+fn main() { let p = new(2), c = new(1); p[0] = c; d*c; return 0; }
diff --git a/tests/errors/copy_owned.zc b/tests/errors/copy_owned.zc
new file mode 100644
index 0000000..862c7ef
--- /dev/null
+++ b/tests/errors/copy_owned.zc
@@ -0,0 +1,2 @@
+// expect: x: is an owned object and can't be copied
+fn main() { let x = new(1); let y = x; return y[0]; }
diff --git a/tests/errors/d_into_block.zc b/tests/errors/d_into_block.zc
new file mode 100644
index 0000000..2d7f526
--- /dev/null
+++ b/tests/errors/d_into_block.zc
@@ -0,0 +1,2 @@
+// expect: x: used after it was given away
+fn main() { let x = new(1); d*x; { x[0] = 5; } return x[0]; }
diff --git a/tests/errors/d_kept.zc b/tests/errors/d_kept.zc
new file mode 100644
index 0000000..dcbfb86
--- /dev/null
+++ b/tests/errors/d_kept.zc
@@ -0,0 +1,2 @@
+// expect: x: d object was never given away
+fn main() { let x = new(3); d*x; x[0] = 1; return 0; }
diff --git a/tests/errors/did_you_mean.zc b/tests/errors/did_you_mean.zc
new file mode 100644
index 0000000..734b903
--- /dev/null
+++ b/tests/errors/did_you_mean.zc
@@ -0,0 +1,3 @@
+// expect: did you mean 'helper'?
+fn helper(x) { return x; }
+fn main() { return helpr(1); }
diff --git a/tests/errors/discard_u.zc b/tests/errors/discard_u.zc
new file mode 100644
index 0000000..2743f9e
--- /dev/null
+++ b/tests/errors/discard_u.zc
@@ -0,0 +1,3 @@
+// expect: a new object (or a u result) can't be thrown away
+.u mk() { let p = new(1); return u*p; }
+fn main() { mk(); return 0; }
diff --git a/tests/errors/found_token.zc b/tests/errors/found_token.zc
new file mode 100644
index 0000000..73e63dc
--- /dev/null
+++ b/tests/errors/found_token.zc
@@ -0,0 +1,2 @@
+// expect: expected ';', found 'return'
+fn main() { let a = 1 return 0; }
diff --git a/tests/errors/give_m.zc b/tests/errors/give_m.zc
new file mode 100644
index 0000000..cd67896
--- /dev/null
+++ b/tests/errors/give_m.zc
@@ -0,0 +1,3 @@
+// expect: x: is m; write d*x
+fn take(.d p) { return p[0]; }
+fn main() { let x = new(3); take(x); return 0; }
diff --git a/tests/errors/global_store.zc b/tests/errors/global_store.zc
new file mode 100644
index 0000000..dc0e546
--- /dev/null
+++ b/tests/errors/global_store.zc
@@ -0,0 +1,3 @@
+// expect: x: must be d to be stored in a global
+let g;
+fn main() { let x = new(1); g = x; return 0; }
diff --git a/tests/errors/leak_overwrite.zc b/tests/errors/leak_overwrite.zc
new file mode 100644
index 0000000..d19b5fd
--- /dev/null
+++ b/tests/errors/leak_overwrite.zc
@@ -0,0 +1,2 @@
+// expect: x: still owns an object
+fn main() { let x = new(1); x = new(2); return x[0]; }
diff --git a/tests/errors/lend_d.zc b/tests/errors/lend_d.zc
new file mode 100644
index 0000000..55ac7e0
--- /dev/null
+++ b/tests/errors/lend_d.zc
@@ -0,0 +1,4 @@
+// expect: x: is d; pass it to a d parameter
+fn look(.m p) { return p[0]; }
+fn take(.d p) { return p[0]; }
+fn main() { let x = new(3); d*x; look(x); take(x); return 0; }
diff --git a/tests/errors/loop_move.zc b/tests/errors/loop_move.zc
new file mode 100644
index 0000000..df3c94a
--- /dev/null
+++ b/tests/errors/loop_move.zc
@@ -0,0 +1,3 @@
+// expect: x: is given away inside a loop that comes back around to it
+fn take(.d p) { return p[0]; }
+fn main() { let x = new(1); let i = 0; d*x; while (i < 3) { take(x); i++; } return 0; }
diff --git a/tests/errors/plain_return.zc b/tests/errors/plain_return.zc
new file mode 100644
index 0000000..e23cc51
--- /dev/null
+++ b/tests/errors/plain_return.zc
@@ -0,0 +1,3 @@
+// expect: is called before its body, so its owned return can't be known there
+fn main() { return mk(); }
+fn mk() { return new(1); }
diff --git a/tests/errors/return_m.zc b/tests/errors/return_m.zc
new file mode 100644
index 0000000..c450711
--- /dev/null
+++ b/tests/errors/return_m.zc
@@ -0,0 +1,3 @@
+// expect: q: has to be u to be returned
+.u mk(.m q) { return q; }
+fn main() { let x = new(1); let y = mk(x); return y[0]; }
diff --git a/tests/errors/tag_mismatch.zc b/tests/errors/tag_mismatch.zc
new file mode 100644
index 0000000..079f249
--- /dev/null
+++ b/tests/errors/tag_mismatch.zc
@@ -0,0 +1,2 @@
+// expect: c: its tag doesn't match its container's
+fn main() { let p = new(2), c = new(1); u*p; p[0] = c; return 0; }
diff --git a/tests/errors/temporary_output.zc b/tests/errors/temporary_output.zc
new file mode 100644
index 0000000..02a8a07
--- /dev/null
+++ b/tests/errors/temporary_output.zc
@@ -0,0 +1,9 @@
+// expect: a new object can only go straight to a d parameter; bind it to a name first
+char*.u make_text(int n) {
+ let text = new(char, n);
+ return u*text;
+}
+fn main() {
+ put_s(make_text(3));
+ return 0;
+}
diff --git a/tests/errors/u_dropped.zc b/tests/errors/u_dropped.zc
new file mode 100644
index 0000000..e1ca68a
--- /dev/null
+++ b/tests/errors/u_dropped.zc
@@ -0,0 +1,2 @@
+// expect: x: u object must leave this block
+fn main() { let x = new(3); u*x; return 0; }
diff --git a/tests/errors/u_skip_level.zc b/tests/errors/u_skip_level.zc
new file mode 100644
index 0000000..6caeeb3
--- /dev/null
+++ b/tests/errors/u_skip_level.zc
@@ -0,0 +1,2 @@
+// expect: t: is u; it can only be handed to a variable of the enclosing block
+fn main() { let r; { { let t = new(1); u*t; r = t; } } return r[0]; }
diff --git a/tests/errors/unchecked_touch.zc b/tests/errors/unchecked_touch.zc
new file mode 100644
index 0000000..141639d
--- /dev/null
+++ b/tests/errors/unchecked_touch.zc
@@ -0,0 +1,2 @@
+// expect: x: is tracked by the analyzer and can't be used in unchecked code
+fn main() { let x = new(1); unchecked { x[0] = 1; } return 0; }
diff --git a/tests/errors/unused.zc b/tests/errors/unused.zc
new file mode 100644
index 0000000..64aff1f
--- /dev/null
+++ b/tests/errors/unused.zc
@@ -0,0 +1,2 @@
+// expect: x: m object is never used
+fn main() { let x = new(3); return 0; }
diff --git a/tests/errors/use_after_give.zc b/tests/errors/use_after_give.zc
new file mode 100644
index 0000000..8a10922
--- /dev/null
+++ b/tests/errors/use_after_give.zc
@@ -0,0 +1,3 @@
+// expect: x: used after it was given away
+fn take(.d p) { return p[0]; }
+fn main() { let x = new(3); take(d*x); return x[0]; }
diff --git a/tests/langerrors/cfunc_name.zc b/tests/langerrors/cfunc_name.zc
new file mode 100644
index 0000000..0b0b0d4
--- /dev/null
+++ b/tests/langerrors/cfunc_name.zc
@@ -0,0 +1,2 @@
+// expect: isn't a function
+fn main() { let p = cfunc(nothere); return 0; }
diff --git a/tests/langerrors/const_index.zc b/tests/langerrors/const_index.zc
new file mode 100644
index 0000000..03e927b
--- /dev/null
+++ b/tests/langerrors/const_index.zc
@@ -0,0 +1,2 @@
+// expect: the index is out of range for this array
+fn main() { [4]int a; a[4] = 1; return 0; }
diff --git a/tests/langerrors/extern_bigret.zc b/tests/langerrors/extern_bigret.zc
new file mode 100644
index 0000000..37de467
--- /dev/null
+++ b/tests/langerrors/extern_bigret.zc
@@ -0,0 +1,4 @@
+// expect: a native call can return at most 7 words
+struct Big { int a; int b; int c; int d; int e; int f; int g; int h; }
+extern struct Big mkbig(long a);
+int main() { return 0; }
diff --git a/tests/langerrors/extern_struct.zc b/tests/langerrors/extern_struct.zc
new file mode 100644
index 0000000..52d9ecc
--- /dev/null
+++ b/tests/langerrors/extern_struct.zc
@@ -0,0 +1,3 @@
+// expect: isn't a zc struct
+extern int stat2(struct stat s);
+fn main() { return 0; }
diff --git a/tests/langerrors/extern_type.zc b/tests/langerrors/extern_type.zc
new file mode 100644
index 0000000..c0655ba
--- /dev/null
+++ b/tests/langerrors/extern_type.zc
@@ -0,0 +1,3 @@
+// expect: isn't a c type extern knows
+extern widget frob(int);
+fn main() { return 0; }
diff --git a/tests/langerrors/fixed_nolen.zc b/tests/langerrors/fixed_nolen.zc
new file mode 100644
index 0000000..b79c31e
--- /dev/null
+++ b/tests/langerrors/fixed_nolen.zc
@@ -0,0 +1,2 @@
+// expect: a fixed array needs its length
+fn main() { fixed []int f = new(fixed []int); free(d*f); return 0; }
diff --git a/tests/langerrors/fixed_push.zc b/tests/langerrors/fixed_push.zc
new file mode 100644
index 0000000..a19bcb4
--- /dev/null
+++ b/tests/langerrors/fixed_push.zc
@@ -0,0 +1,2 @@
+// expect: a fixed array can't grow or shrink
+fn main() { fixed []int f = new(fixed []int, 3); push(f, 1); free(d*f); return 0; }
diff --git a/tests/langerrors/fixed_to_dyn.zc b/tests/langerrors/fixed_to_dyn.zc
new file mode 100644
index 0000000..483f62f
--- /dev/null
+++ b/tests/langerrors/fixed_to_dyn.zc
@@ -0,0 +1,4 @@
+// expect: type error: argument: expected []int, got fixed []int
+typecheck on;
+int take([]int.m g) { return len(g); }
+fn main() { fixed []int f = new(fixed []int, 3); take(f); free(d*f); return 0; }
diff --git a/tests/langerrors/gen_undeclared.zc b/tests/langerrors/gen_undeclared.zc
new file mode 100644
index 0000000..ef85a7b
--- /dev/null
+++ b/tests/langerrors/gen_undeclared.zc
@@ -0,0 +1,3 @@
+// expect: T: isn't a type
+T first(int* l) { return l[0]; }
+fn main() { return 0; }
diff --git a/tests/langerrors/o3_addr.zc b/tests/langerrors/o3_addr.zc
new file mode 100644
index 0000000..9ff029f
--- /dev/null
+++ b/tests/langerrors/o3_addr.zc
@@ -0,0 +1,2 @@
+// expect: an o3 variable has no address
+fn main() { o3 int z = 2; int* q = &z; return *q; }
diff --git a/tests/langerrors/o3_asm.zc b/tests/langerrors/o3_asm.zc
new file mode 100644
index 0000000..c8afb44
--- /dev/null
+++ b/tests/langerrors/o3_asm.zc
@@ -0,0 +1,2 @@
+// expect: asm can't reach an o3 variable
+fn main() { o3 int zz = 2; asm { zz @ _ } return 0; }
diff --git a/tests/langerrors/o3_let.zc b/tests/langerrors/o3_let.zc
new file mode 100644
index 0000000..d2aff9a
--- /dev/null
+++ b/tests/langerrors/o3_let.zc
@@ -0,0 +1,2 @@
+// expect: o3 variables need a type
+fn main() { o3 { let x = 1; x++; } return 0; }
diff --git a/tests/langerrors/o3_param.zc b/tests/langerrors/o3_param.zc
new file mode 100644
index 0000000..5694720
--- /dev/null
+++ b/tests/langerrors/o3_param.zc
@@ -0,0 +1,3 @@
+// expect: o3 parameters need a type
+o3 int f(x) { return x; }
+fn main() { return f(1); }
diff --git a/tests/langerrors/parseword_noread.zc b/tests/langerrors/parseword_noread.zc
new file mode 100644
index 0000000..e7c5b1a
--- /dev/null
+++ b/tests/langerrors/parseword_noread.zc
@@ -0,0 +1,5 @@
+// expect: a parse word has to read its own name
+// a parse word that never calls next() would be called on the same token forever
+immediate fn pw() { return 0; }
+pw
+fn main() { return 0; }
diff --git a/tests/langerrors/pk_addr.zc b/tests/langerrors/pk_addr.zc
new file mode 100644
index 0000000..17ada12
--- /dev/null
+++ b/tests/langerrors/pk_addr.zc
@@ -0,0 +1,6 @@
+// expect: has no address
+fn main() {
+ u8 buf[8];
+ u8* p = &buf[2];
+ return 0;
+}
diff --git a/tests/langerrors/tagenum_args.zc b/tests/langerrors/tagenum_args.zc
new file mode 100644
index 0000000..4d1850f
--- /dev/null
+++ b/tests/langerrors/tagenum_args.zc
@@ -0,0 +1,3 @@
+// expect: wrong number of arguments
+enum shape { circle { int r; }, dot };
+fn main() { shape* s = shape.circle(1, 2); free(d*s); return 0; }
diff --git a/tests/langerrors/tagenum_bind.zc b/tests/langerrors/tagenum_bind.zc
new file mode 100644
index 0000000..9fcb9bf
--- /dev/null
+++ b/tests/langerrors/tagenum_bind.zc
@@ -0,0 +1,10 @@
+// expect: a case binds every field of its member
+enum shape { circle { int r; }, dot };
+fn f(shape* s) {
+ switch (s) {
+ case circle(a, b): return 1;
+ case dot: return 2;
+ }
+ return 0;
+}
+fn main() { return 0; }
diff --git a/tests/langerrors/tagenum_value.zc b/tests/langerrors/tagenum_value.zc
new file mode 100644
index 0000000..dbd807e
--- /dev/null
+++ b/tests/langerrors/tagenum_value.zc
@@ -0,0 +1,3 @@
+// expect: a tagged enum's members take no explicit values
+enum shape { circle { int r; }, dot = 5 };
+fn main() { return 0; }
diff --git a/tests/programs/alias.zc b/tests/programs/alias.zc
new file mode 100644
index 0000000..2ebc3e5
--- /dev/null
+++ b/tests/programs/alias.zc
@@ -0,0 +1,21 @@
+// a pointer computed from a live object is a borrow of it: fine while the owner lives
+struct node { int v; node* next; };
+fn main() {
+ let p = new(4);
+ p[0] = 11; p[1] = 22;
+ let q = p + 1;
+ put_n(q[-1]); put_c(' ');
+ put_n(q[0]); put_c(' ');
+ node* a = new(node), b = new(node);
+ a.v = 3; b.v = 4; a.next = b;
+ node* c = a.next;
+ put_n(c.v); put_c(' ');
+ int** list = new(int*, 2);
+ int* item = new(int);
+ *item = 8;
+ list[1] = item;
+ int* e = list[1];
+ put_n(*e); put_c(10);
+ free(d*p); free(d*a); free(d*list);
+ return 0;
+}
diff --git a/tests/programs/allocfast.zc b/tests/programs/allocfast.zc
new file mode 100644
index 0000000..5ef2df2
--- /dev/null
+++ b/tests/programs/allocfast.zc
@@ -0,0 +1,45 @@
+// repeated allocations of a size known at compile time: new(T) of a struct of
+// numbers, and new(n) with a constant n. each pass of the loop makes an object
+// and frees it at the end of the block, so the allocator should hand back the
+// block it just took. the sums prove the objects were zeroed every time.
+struct point { int x, y; };
+struct line { point* a; int len; };
+
+fn loop_leaf(int n) {
+ let total = 0;
+ for (int i = 0; i < n; i++) {
+ point* p = new(point);
+ total += p.x + p.y; // zero from the allocator, every pass
+ p.x = i; p.y = 2;
+ }
+ return total;
+}
+fn loop_words(int n) {
+ let total = 0, i = 0;
+ while (i < n) {
+ let w = new(6);
+ total += w[0] + w[5];
+ w[0] = 7; w[5] = 9;
+ i++;
+ }
+ return total;
+}
+fn loop_owner(int n) {
+ let i = 0, total = 0;
+ while (i < n) {
+ line* l = new(line);
+ l.a = new(point);
+ l.a.x = i;
+ total += l.a.x + l.len;
+ i++;
+ }
+ return total;
+}
+fn main() {
+ let before = live_objects();
+ put_n(loop_leaf(50000)); put_c(' ');
+ put_n(loop_words(50000)); put_c(' ');
+ put_n(loop_owner(1000)); put_c(' ');
+ put_n(live_objects() - before); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/annotate.expected b/tests/programs/annotate.expected
new file mode 100644
index 0000000..553353e
--- /dev/null
+++ b/tests/programs/annotate.expected
@@ -0,0 +1,33 @@
+// zclsp --annotate writes the types the compiler can see into a program that
+// has none. this one has none: locals take the type of what they start as, a
+// parameter takes the type every call gives it, and a function the type of
+// what it returns. what it can't tell it leaves alone (twice is called with
+// a plain word, so its parameter stays), and the program still means the
+// same thing: tests/test.sh runs both and compares.
+include "src/lib/std.zc";
+struct point { int x, y; };
+int calls = 0;
+char* name = "annotate";
+int table[4];
+int add(int a, int b) { calls = calls + 1; return a + b; }
+point* scale(point* p, int k) { p.x = p.x * k; p.y = p.y * k; return p; }
+point*.u fresh(int x, int y) {
+ point* p = new(point);
+ p.x = x; p.y = y;
+ return u*p;
+}
+int twice(int v) { return v + v; }
+fn len3(char* s) { return len_w(s) + 3; }
+fn half(f) { return f / 2.0; }
+int main() {
+ int i = 0, sum = 0, buf[6];
+ let word_ = table, text = "abc";
+ point* pt = fresh(2, 3);
+ let r = 1.5;
+ while (i < 4) { table[i] = add(i, 10); sum = sum + table[i]; i++; }
+ scale(pt, 3);
+ buf[0] = twice(sum);
+ put_n(sum); put_c(32); put_n(pt.x + pt.y); put_c(32); put_n(buf[0]); put_c(32);
+ put_n(len3(text)); put_c(32); put_n(calls); put_c(32); put_n(half(r) * 4); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/annotate.zc b/tests/programs/annotate.zc
new file mode 100644
index 0000000..75dad97
--- /dev/null
+++ b/tests/programs/annotate.zc
@@ -0,0 +1,33 @@
+// zclsp --annotate writes the types the compiler can see into a program that
+// has none. this one has none: locals take the type of what they start as, a
+// parameter takes the type every call gives it, and a function the type of
+// what it returns. what it can't tell it leaves alone (twice is called with
+// a plain word, so its parameter stays), and the program still means the
+// same thing: tests/test.sh runs both and compares.
+include "src/lib/std.zc";
+struct point { int x, y; };
+let calls = 0;
+let name = "annotate";
+let table[4];
+fn add(a, b) { calls = calls + 1; return a + b; }
+fn scale(p, k) { p.x = p.x * k; p.y = p.y * k; return p; }
+.u fresh(x, y) {
+ let p = new(point);
+ p.x = x; p.y = y;
+ return u*p;
+}
+fn twice(v) { return v + v; }
+fn len3(s) { return len_w(s) + 3; }
+fn half(f) { return f / 2.0; }
+fn main() {
+ let i = 0, sum = 0, buf[6];
+ let word_ = table, text = "abc";
+ let pt = fresh(2, 3);
+ let r = 1.5;
+ while (i < 4) { table[i] = add(i, 10); sum = sum + table[i]; i++; }
+ scale(pt, 3);
+ buf[0] = twice(sum);
+ put_n(sum); put_c(32); put_n(pt.x + pt.y); put_c(32); put_n(buf[0]); put_c(32);
+ put_n(len3(text)); put_c(32); put_n(calls); put_c(32); put_n(half(r) * 4); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/arena.zc b/tests/programs/arena.zc
new file mode 100644
index 0000000..5156b80
--- /dev/null
+++ b/tests/programs/arena.zc
@@ -0,0 +1,16 @@
+// the bump arena: interior pointers, a rewind, and a full bump.
+fn main() {
+ let a = arena_new(32), p, q, r;
+ p = arena_alloc(a, 4);
+ p[0] = 7;
+ q = arena_alloc(a, 4);
+ put_n(q - p); put_c(' ');
+ put_n(p[0]); put_c(' ');
+ put_n(q[0]); put_c(' ');
+ arena_reset(a);
+ r = arena_alloc(a, 4);
+ put_n(r == p); put_c(' ');
+ put_n(arena_alloc(a, 100) == 0); put_c(10);
+ free(d*a);
+ return 0;
+}
diff --git a/tests/programs/args.zc b/tests/programs/args.zc
new file mode 100644
index 0000000..aac72ec
--- /dev/null
+++ b/tests/programs/args.zc
@@ -0,0 +1,16 @@
+// the command line and the environment, as the host hands them over. argv[0]
+// is the program path, and <eval> when the source came from stdin, so argc is
+// 1 even with nothing on the command line. run through a shell with no extra
+// arguments, which is how test.sh calls it, so the line is fixed.
+include "src/lib/std.zc";
+
+fn main() {
+ put_n(argc); put_c(32); put_s(argv[0]);
+ put_c(32); put_c(32); put_s(argv[argc - 1]);
+ // envc counts every entry, envp holds NAME=value strings
+ put_c(32); put_n(envc > 0);
+ let hit = 0;
+ for (int i = 0; i < envc; i++) if (eq_w(envp[i], "ZC_ARG_TEST=ok")) hit = 1;
+ put_c(32); put_n(hit);
+ return 0;
+} \ No newline at end of file
diff --git a/tests/programs/arrays.zc b/tests/programs/arrays.zc
new file mode 100644
index 0000000..a8a12bf
--- /dev/null
+++ b/tests/programs/arrays.zc
@@ -0,0 +1,23 @@
+// growable arrays and fixed ones
+struct point { int x, y; };
+fn main() {
+ let before = live_objects();
+ {
+ []int a = new([]int);
+ for (int i = 0; i < 100; i++) push(a, i * i);
+ put_n(len(a)); put_c(' '); put_n(cap(a)); put_c(' '); put_n(a[99]); put_c(' ');
+ put_n(pop(a)); put_c(' '); put_n(len(a)); put_c(' ');
+ resize(a, 3); put_n(len(a)); put_c(' '); put_n(a[2]); put_c(' ');
+ fixed []int f = new(fixed []int, 5);
+ f[4] = 7; put_n(f[4] + len(f)); put_c(' ');
+ []point ps = new([]point, 2);
+ ps[1].y = 9; resize(ps, 10); put_n(ps[1].y + len(ps)); put_c(' ');
+ [8]int st; put_n(len(st)); put_c(' ');
+ []point* owned = new([]point*);
+ for (int i = 0; i < 20; i++) { point* p = new(point); p.x = i; push(owned, d*p); }
+ put_n(owned[19].x); put_c(' ');
+ put_n(live_objects() - before); put_c(10);
+ }
+ put_n(live_objects() - before); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/backtick.zc b/tests/programs/backtick.zc
new file mode 100644
index 0000000..3610f70
--- /dev/null
+++ b/tests/programs/backtick.zc
@@ -0,0 +1,9 @@
+// a backtick ends the source only where the lexer would read it as a token.
+// this one, in a line comment: ` does not end anything.
+/* nor does this, in a block comment ` with a star / and a slash */
+fn main() {
+ let s = "a`b", t = "q\"`", c = '`', d = '\'';
+ put_n(len_w(s)); put_c(' '); put_n(len_w(t)); put_c(' '); put_n(c); put_c(' '); put_n(d); put_c(' ');
+ while (!eof()) put_c(get_c());
+ return 0;
+}
diff --git a/tests/programs/bareexit.zc b/tests/programs/bareexit.zc
new file mode 100644
index 0000000..905710e
--- /dev/null
+++ b/tests/programs/bareexit.zc
@@ -0,0 +1,7 @@
+// exit(3) must stop the machine before the next print.
+fn main() {
+ put_s("bye\n");
+ syscall(7, 3, 0, 0);
+ put_s("still\n");
+ return 0;
+}
diff --git a/tests/programs/bareio.zc b/tests/programs/bareio.zc
new file mode 100644
index 0000000..6da50b1
--- /dev/null
+++ b/tests/programs/bareio.zc
@@ -0,0 +1,28 @@
+// bare-metal syscalls: clock, a RAM file, random, and the machine size.
+// zc0 subset, so a run-only image can carry it. names stay off u/m/d.
+fn main() {
+ let buf[16], n, t0, t1, i, fd, r, board[8];
+ t0 = syscall(6, 0, 0, 0);
+ i = 0;
+ while (i < 20000) i = i + 1;
+ t1 = syscall(6, 0, 0, 0);
+ if (t0 >= 0 && t1 >= t0) put_s("tick ");
+ else put_s("notick ");
+ fd = syscall(1, "note", 1, 0);
+ n = syscall(4, fd, "zy", 2);
+ syscall(2, fd, 0, 0);
+ fd = syscall(1, "note", 0, 0);
+ n = syscall(3, fd, buf, 8);
+ syscall(2, fd, 0, 0);
+ buf[n] = 0;
+ put_n(n); put_c(' '); put_s(buf); put_c(' ');
+ r = syscall(8, 0, 0, 0);
+ if (r != -1) put_s("rand ");
+ else put_s("norand ");
+ n = syscall(5, "BOARD", board, 8);
+ if (n > 0) put_s("board ");
+ else put_s("noboard ");
+ put_n(syscall(9, 0, 0, 0));
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/basics.zc b/tests/programs/basics.zc
new file mode 100644
index 0000000..9865527
--- /dev/null
+++ b/tests/programs/basics.zc
@@ -0,0 +1,21 @@
+let table[10];
+let greeting = "hey";
+const N = 10, BIG = 0x100 * 2;
+fn fib(n) { if (n < 2) return n; return fib(n - 1) + fib(n - 2); }
+fn twice(f, x) { return f(f(x)); }
+fn inc(x) { return x + 1; }
+fn main() {
+ let i, s = 0, a[5];
+ for (i = 0; i < N; i++) table[i] = i * i;
+ for (let j = 0; j < N; j++) s += table[j];
+ put_n(s); put_c(' '); put_n(fib(15)); put_c(' '); put_n(twice(inc, 40)); put_c(' '); put_n(BIG); put_c(10);
+ put_s(greeting); put_c(' '); put_n('A'); put_c(' '); put_n(-17 / 5); put_c(' '); put_n(-17 % 5); put_c(' '); put_n(1 << 10); put_c(' '); put_n(-64 >> 3); put_c(10);
+ i = 0; while (1) { i++; if (i == 3) continue; if (i > 6) break; put_n(i); } put_c(10);
+ a[0] = 5; a[1] = a[0]++; a[2] = ++a[0]; put_n(a[0]); put_n(a[1]); put_n(a[2]); put_c(10);
+ let b = { let t = 3; t * t };
+ put_n(b); put_c(' '); put_n(3 > 2 && 2 > 1); put_n(0 || 0); put_n(!5); put_n(~0); put_n(5 != 5); put_n(4 >= 4); put_c(10);
+ let x = asm { 2 3 + };
+ put_n(x); put_c(10);
+ asm { 'o' . 'k' . 10 . }
+ return 0;
+}
diff --git a/tests/programs/bench.zc b/tests/programs/bench.zc
new file mode 100644
index 0000000..7d34ead
--- /dev/null
+++ b/tests/programs/bench.zc
@@ -0,0 +1,29 @@
+// four kernels with and without o3; bench.sh runs each one on each backend
+int csum(int lim) { int s = 0; for (int n = 1; n < lim; n++) { int x = n; while (x != 1) { if (x & 1) x = 3 * x + 1; else x = x / 2; s++; } } return s; }
+o3 int csum3(int lim) { int s = 0; for (int n = 1; n < lim; n++) { int x = n; while (x != 1) { if (x & 1) x = 3 * x + 1; else x = x / 2; s++; } } return s; }
+int sieve(fixed []int f) { int c = 0, n = len(f); for (int i = 0; i < n; i++) f[i] = 1; for (int i = 2; i < n; i++) if (f[i]) { c++; for (int j = i + i; j < n; j += i) f[j] = 0; } return c; }
+o3 int sieve3(fixed []int f) { int c = 0, n = len(f); for (int i = 0; i < n; i++) f[i] = 1; for (int i = 2; i < n; i++) if (f[i]) { c++; for (int j = i + i; j < n; j += i) f[j] = 0; } return c; }
+int digits(int n) { int s = 0; for (int i = 0; i < n; i++) { int x = i; while (x) { s = s + x % 10; x = x / 10; } } return s; }
+o3 int digits3(int n) { int s = 0; for (int i = 0; i < n; i++) { int x = i; while (x) { s = s + x % 10; x = x / 10; } } return s; }
+int fib(int n) { if (n < 2) return n; return fib(n - 1) + fib(n - 2); }
+o3 int fib3(int n) { if (n < 2) return n; return fib3(n - 1) + fib3(n - 2); }
+// input: a letter (the kernel; z runs none) and how many times to run it
+fn main() {
+ fixed []int f = new(fixed []int, 50000);
+ let which = get_c(), reps = 0, ch = get_c(), r = 0;
+ while (ch >= '0' && ch <= '9') { reps = reps * 10 + ch - '0'; ch = get_c(); }
+ while (reps > 0) {
+ if (which == 'a') r = csum(30000);
+ if (which == 'b') r = csum3(30000);
+ if (which == 'c') r = sieve(f);
+ if (which == 'd') r = sieve3(f);
+ if (which == 'e') r = digits(300000);
+ if (which == 'f') r = digits3(300000);
+ if (which == 'g') r = fib(22);
+ if (which == 'h') r = fib3(22);
+ reps--;
+ }
+ put_n(r); put_c(10);
+ free(d*f);
+ return 0;
+}
diff --git a/tests/programs/bench_bytes.zc b/tests/programs/bench_bytes.zc
new file mode 100644
index 0000000..cb878f6
--- /dev/null
+++ b/tests/programs/bench_bytes.zc
@@ -0,0 +1,19 @@
+fn sumbytes(n) {
+ let buf[64], i = 0, s = 0, b, reps, ch;
+ while (i < 64) { buf[i] = i * 0x0101010101010101; i++; }
+ b = addr_b(buf);
+ ch = get_c();
+ reps = 0;
+ while (ch >= '0' && ch <= '9') { reps = reps * 10 + ch - '0'; ch = get_c(); }
+ while (reps > 0) {
+ i = 0; s = 0;
+ while (i < n) { s = s + ld8(b + (i & 511)); i++; }
+ reps--;
+ }
+ return s;
+}
+fn main() {
+ put_n(sumbytes(4000));
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/bench_share.zc b/tests/programs/bench_share.zc
new file mode 100644
index 0000000..6fa478f
--- /dev/null
+++ b/tests/programs/bench_share.zc
@@ -0,0 +1,30 @@
+// walk one shared spine from two parents. the checksum is one repetition.
+fn build() {
+ let i = 0, spine = 0;
+ while (i < 40) { spine = cons(i, spine); i++; }
+ return cons(cons(spine, 1), cons(spine, 2));
+}
+fn sum(t) {
+ let a = 0;
+ while (t) { a = a + car(t); t = cdr(t); }
+ return a;
+}
+fn work() {
+ let ch = get_c(), reps = 0, acc = 0, root, spine, k;
+ while (ch >= '0' && ch <= '9') { reps = reps * 10 + ch - '0'; ch = get_c(); }
+ root = build();
+ spine = car(car(root));
+ while (reps > 0) {
+ k = 0;
+ acc = 0;
+ while (k < 200) { acc = acc + sum(spine); k++; }
+ acc = acc + cdr(car(root)) + cdr(cdr(root));
+ reps--;
+ }
+ return acc;
+}
+fn main() {
+ put_n(work());
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/bench_strings.zc b/tests/programs/bench_strings.zc
new file mode 100644
index 0000000..455cff9
--- /dev/null
+++ b/tests/programs/bench_strings.zc
@@ -0,0 +1,27 @@
+fn work() {
+ let s = b"abcdefghijklmnopqrstuvwxyz0123456789";
+ let i, acc, n, t, part, k, reps, ch;
+ ch = get_c();
+ reps = 0;
+ while (ch >= '0' && ch <= '9') { reps = reps * 10 + ch - '0'; ch = get_c(); }
+ while (reps > 0) {
+ k = 0; acc = 0; n = len_s(s);
+ while (k < 200) {
+ i = 0;
+ while (i < n) { acc = acc + at_s(s, i); i++; }
+ k++;
+ }
+ t = s + b"!";
+ part = t[1:8];
+ acc = acc + len_s(t) + len_s(part) + at_s(part, 0);
+ free(part);
+ free(t);
+ reps--;
+ }
+ return acc;
+}
+fn main() {
+ put_n(work());
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/bitops.expected b/tests/programs/bitops.expected
new file mode 100644
index 0000000..e93b905
--- /dev/null
+++ b/tests/programs/bitops.expected
@@ -0,0 +1,2 @@
+63 64 3 64 8 64 0 1 2 1 12345 -1 4 1069547520 1 1065353216 8388608
+5470960040408376558
diff --git a/tests/programs/bitops.zc b/tests/programs/bitops.zc
new file mode 100644
index 0000000..97817b9
--- /dev/null
+++ b/tests/programs/bitops.zc
@@ -0,0 +1,26 @@
+// bit tricks and float32 conversion as micro-ops
+fn show(v) { put_n(v); put_c(' '); }
+fn once() {
+ show(clz(1)); show(clz(0)); show(ctz(8)); show(ctz(0));
+ show(popcnt(255)); show(popcnt(-1)); show(popcnt(0));
+ show(bswap(1) == 72057594037927936);
+ show(rotl(1, 65)); show(rotr(1, 1) < 0); show(rotl(rotr(12345, 7), 7));
+ show(mulhi(-1, 5)); show(umulhi(-1, 5));
+ show(f2s(0x3ff8000000000000)); show(s2f(0x3fc00000) == 0x3ff8000000000000);
+ show(f2s(0x3ff0000000000000)); show(f2s(0x3810000000000000));
+ put_c(10);
+}
+// the same in a loop, so o3 lifts and fuses them
+o3 int sums(int n) {
+ int a = 0;
+ for (int i = 0; i < n; i++) {
+ a += popcnt(i) + ctz(i + 1) + clz(i + 1);
+ a ^= rotl(a, 3) + bswap(i) + f2s(s2f(i));
+ }
+ return a;
+}
+int main() {
+ once();
+ put_n(sums(1000)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/borrowmove.zc b/tests/programs/borrowmove.zc
new file mode 100644
index 0000000..b0f0e42
--- /dev/null
+++ b/tests/programs/borrowmove.zc
@@ -0,0 +1,2 @@
+fn put(.m box, int v) { int* c = new(int); *c = v; box[0] = c; return 0; }
+fn main() { let b = new(1); put(b, 6); put_n(*(b[0] as int*)); put_c(10); return 0; }
diff --git a/tests/programs/bounds.zc b/tests/programs/bounds.zc
new file mode 100644
index 0000000..42c3997
--- /dev/null
+++ b/tests/programs/bounds.zc
@@ -0,0 +1,28 @@
+// a +1 loop whose bound is len of the fixed array: the check is gone, the sum is not
+fn acc(fixed []int f) {
+ int n = len(f), i = 0, s = 0;
+ for (; i < n; i += 1) f[i] = i + 1;
+ for (i = 0; i < n; i++) s = s + f[i];
+ return s;
+}
+o3 fn acc3(fixed []int f) {
+ int n = len(f), i = 0, s = 0;
+ for (; i < n; i++) f[i] = i + 1;
+ for (i = 0; i < n; i++) s += f[i];
+ return s;
+}
+o3 fn small(fixed []int f) {
+ int n = len(f), c = 0, i, j;
+ for (i = 2; i < n; i++) f[i] = 1;
+ for (i = 2; i * i < n; i++)
+ if (f[i]) for (j = i * i; j < n; j += i) f[j] = 0;
+ for (i = 2; i < n; i++) c += f[i];
+ return c;
+}
+fn main() {
+ fixed []int f = new(fixed []int, 10);
+ fixed []int g = new(fixed []int, 20);
+ put_n(acc(f)); put_c(32); put_n(acc3(f)); put_c(32); put_n(small(g)); put_c(10);
+ free(d*f); free(d*g);
+ return 0;
+}
diff --git a/tests/programs/bounds_hi.zc b/tests/programs/bounds_hi.zc
new file mode 100644
index 0000000..d61179d
--- /dev/null
+++ b/tests/programs/bounds_hi.zc
@@ -0,0 +1,8 @@
+// the index is not the induction variable. i + 1 walks off the end.
+o3 fn main() {
+ fixed []int f = new(fixed []int, 4);
+ int n = len(f), i;
+ for (i = 0; i < n; i++) f[i + 1] = 1;
+ free(d*f);
+ return 0;
+}
diff --git a/tests/programs/bounds_lo.zc b/tests/programs/bounds_lo.zc
new file mode 100644
index 0000000..896d864
--- /dev/null
+++ b/tests/programs/bounds_lo.zc
@@ -0,0 +1,8 @@
+// i starts negative. i < len is true for the first pass, and the index is still illegal.
+fn main() {
+ fixed []int f = new(fixed []int, 4);
+ int n = len(f), i;
+ for (i = -1; i < n; i++) f[i] = 1;
+ free(d*f);
+ return 0;
+}
diff --git a/tests/programs/bstr_io.zc b/tests/programs/bstr_io.zc
new file mode 100644
index 0000000..9cf7ca9
--- /dev/null
+++ b/tests/programs/bstr_io.zc
@@ -0,0 +1,22 @@
+fn main() {
+ let path = b"/tmp/zc-bstr.txt";
+ let msg = b"byte-io";
+ let fd = bopen(path, 1);
+ let n = bwrite(fd, msg, len_s(msg));
+ syscall(SYS_CLOSE, fd, 0, 0);
+ fd = bopen(path, 0);
+ let buf = new_s(7);
+ let g = bread(fd, buf, 7);
+ syscall(SYS_CLOSE, fd, 0, 0);
+ let name = b"ZC_BSTR";
+ let envb = new_s(16);
+ let k = bgetenv(name, envb, 16);
+ put_s(buf); put_c(32);
+ put_n(n); put_c(32);
+ put_n(g); put_c(32);
+ put_n(buf == msg); put_c(32);
+ put_n(k); put_c(32);
+ put_n(at_s(envb, 0)); put_c(32);
+ put_n(at_s(envb, 5)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/buddy.zc b/tests/programs/buddy.zc
new file mode 100644
index 0000000..ab351d2
--- /dev/null
+++ b/tests/programs/buddy.zc
@@ -0,0 +1,16 @@
+// the buddy allocator: 20000 small blocks, freed, merge back into one big one
+let keep[20000];
+fn main() {
+ let i = 0, p;
+ while (i < 20000) { keep[i] = new(2); i++; } // 20000 8-word blocks
+ put_n(largest_free()); put_c(' ');
+ i = 0; while (i < 20000) { free(keep[i]); keep[i] = 0; i++; }
+ scrub(); // stale stack words would keep the quarantined blocks
+ gc();
+ put_n(largest_free()); put_c(' ');
+ p = new(200000); // only possible if the small blocks merged back
+ put_n(osize(p) >= 200000); put_c(' ');
+ free(d*p);
+ put_n(live_objects()); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/bytes.zc b/tests/programs/bytes.zc
new file mode 100644
index 0000000..b4e1079
--- /dev/null
+++ b/tests/programs/bytes.zc
@@ -0,0 +1,44 @@
+fn sp() { put_c(32); return 0; }
+fn litw(v) { return v + v + 1; }
+fn f() { put_c('A'); return 0; }
+fn sumbytes(n) {
+ let buf[64], i = 0, s = 0, b;
+ while (i < 64) { buf[i] = i * 0x0101010101010101; i++; }
+ b = addr_b(buf);
+ i = 0;
+ while (i < n) { s = s + ld8(b + (i & 511)); i++; }
+ return s;
+}
+fn main() {
+ let buf[3], b;
+ buf[0] = 0; buf[1] = 0; buf[2] = 0;
+ b = addr_b(buf);
+ st8(b, 0x11);
+ st8(b + 1, 0x22);
+ st8(b + 7, 0x7f);
+ st16(b + 7, 0xabcd);
+ st32(b + 10, 0x01020304);
+ put_n(ld8(b)); sp();
+ put_n(ld8(b + 1)); sp();
+ put_n(ld16(b + 7)); sp();
+ put_n(ld32(b + 10)); sp();
+ put_n(addr_w(b) == buf); sp();
+ put_n(ld8(b + 7)); sp();
+ put_n(ld8(b + 8)); sp();
+ put_n(buf[0]); sp();
+ put_n(buf[1]); sp();
+ st8(0 - 1, 99);
+ put_n(ld8(0 - 1)); sp();
+ put_n(ld16(addr_b(MW) - 1)); sp();
+ put_n(sumbytes(1000));
+ put_c(10);
+ let p = f;
+ f();
+ while (*p != litw('A')) p++;
+ st8(addr_b(p), litw('B') & 255);
+ f();
+ st8(addr_b(p), litw('C') & 255);
+ f();
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/cabi.expected b/tests/programs/cabi.expected
new file mode 100644
index 0000000..89b168c
--- /dev/null
+++ b/tests/programs/cabi.expected
@@ -0,0 +1,5 @@
+1.414214 1024.000000 4.000000 12 42 1234 1 2 3 5 7 9
+9 7 5 3 2 1
+1 2 3 5 7 9 9 7 5 3 2 1
+6 7:2.50
+5.000000
diff --git a/tests/programs/cabi.zc b/tests/programs/cabi.zc
new file mode 100644
index 0000000..6a608ab
--- /dev/null
+++ b/tests/programs/cabi.zc
@@ -0,0 +1,46 @@
+// native calls: extern declares a c function, cfunc hands a zc function to c
+extern double sqrt(double);
+extern double pow(double x, double y);
+extern float sqrtf(float);
+extern long c_strlen(str s) = "strlen";
+extern int c_abs(int x) = "abs";
+extern int c_atoi(str s) = "atoi";
+extern void qsort(word *base, long n, long size, void *cmp);
+extern int snprintf2(word *buf, long cap, str fmt, long a, double b) = "snprintf";
+extern double hypot(double, double);
+
+fn put_f(x) { asm { x @ #1220 } return 0; }
+fn sp() { put_c(32); return 0; }
+
+int cmp(int* a, int* b) { return *a - *b; }
+// a callback that makes a native call of its own, and an o3 one
+int cmpdesc(int* a, int* b) { return c_abs(*b - *a) * (*b - *a) / c_abs(*b - *a + (*b == *a)); }
+o3 int cmpo3(int* a, int* b) { int x = *a; int y = *b; if (x < y) return 1; if (x > y) return -1; return 0; }
+
+fn main() {
+ let a[6];
+ let i = 0;
+ put_f(sqrt(2.0)); sp();
+ put_f(pow(2.0, 10.0)); sp();
+ put_f(sqrtf(16.0)); sp();
+ put_n(c_strlen(b"hello, world")); sp();
+ put_n(c_abs(0 - 42)); sp();
+ put_n(c_atoi(b"1234xyz")); sp();
+ a[0] = 5; a[1] = 3; a[2] = 9; a[3] = 1; a[4] = 7; a[5] = 2;
+ qsort(a, 6, 8, cfunc(cmp));
+ while (i < 6) { put_n(a[i]); sp(); i++; }
+ put_c(10);
+ qsort(a, 6, 8, cfunc(cmpdesc));
+ i = 0; while (i < 6) { put_n(a[i]); sp(); i++; }
+ put_c(10);
+ qsort(a, 6, 8, cfunc(cmp));
+ i = 0; while (i < 6) { put_n(a[i]); sp(); i++; }
+ qsort(a, 6, 8, cfunc(cmpo3));
+ i = 0; while (i < 6) { put_n(a[i]); sp(); i++; }
+ put_c(10);
+ put_n(snprintf2(a, 48, b"%ld:%.2f", 7, 2.5)); sp();
+ i = 0; while (i < 6) { put_c(ld8(addr_b(a) + i)); i++; }
+ put_c(10);
+ put_f(hypot(3.0, 4.0)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/cabi_o3.expected b/tests/programs/cabi_o3.expected
new file mode 100644
index 0000000..6842510
--- /dev/null
+++ b/tests/programs/cabi_o3.expected
@@ -0,0 +1,2 @@
+921.462947 1.414214 2500
+11 1-2-3-4-5-6
diff --git a/tests/programs/cabi_o3.zc b/tests/programs/cabi_o3.zc
new file mode 100644
index 0000000..5c9a55a
--- /dev/null
+++ b/tests/programs/cabi_o3.zc
@@ -0,0 +1,30 @@
+// native calls from o3 code: lifted into the region as micro-ops, called directly by llvm
+extern double sqrt(double);
+extern double pow(double, double);
+extern double fma(double, double, double);
+extern int snprintf9(word *buf, long cap, str fmt, long a, long b, long c, long d, long e, long f) = "snprintf";
+extern long labs(long);
+
+fn put_f(x) { asm { x @ #1220 } return 0; }
+
+o3 float norm(int n) {
+ float acc = 0.0;
+ for (int i = 1; i <= n; i++) acc = acc + sqrt(i * 1.0) + fma(1.0, 2.0, 0.5);
+ return acc;
+}
+o3 int absum(int n) {
+ int t = 0;
+ for (int i = 0; i < n; i++) t = t + labs(i - 50);
+ return t;
+}
+fn main() {
+ let buf[16];
+ let i = 0;
+ put_f(norm(100)); put_c(32);
+ put_f(pow(2.0, 0.5)); put_c(32);
+ put_n(absum(100)); put_c(10);
+ put_n(snprintf9(buf, 100, b"%ld-%ld-%ld-%ld-%ld-%ld", 1, 2, 3, 4, 5, 6)); put_c(32);
+ while (i < 11) { put_c(ld8(addr_b(buf) + i)); i++; }
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/cabi_sized.expected b/tests/programs/cabi_sized.expected
new file mode 100644
index 0000000..9be5bf0
--- /dev/null
+++ b/tests/programs/cabi_sized.expected
@@ -0,0 +1 @@
+1.414214 1 5.000000 77 65 5 1234 2.500000
diff --git a/tests/programs/cabi_sized.zc b/tests/programs/cabi_sized.zc
new file mode 100644
index 0000000..d1dc2ba
--- /dev/null
+++ b/tests/programs/cabi_sized.zc
@@ -0,0 +1,28 @@
+// extern and cfunc with f32 and the sized integers
+extern f32 sqrtf(f32);
+extern f64 hypot(f64, f64);
+extern i32 abs(i32);
+extern u8 toupper(u8);
+extern long labs(long);
+fn put_f(x) { asm { x @ #1220 } return 0; }
+fn sp() { put_c(32); return 0; }
+extern void qsort(word *base, long n, long size, void *cmp);
+i32 cmpi(int* a, int* b) { return *a - *b; }
+f32 half(f32 v) { return v / 2; }
+fn main() {
+ f32 r = sqrtf(2.0);
+ put_f(r); sp();
+ put_n(r == sqrtf(2.0)); sp();
+ put_f(hypot(3.0, 4.0)); sp();
+ put_n(abs(0 - 77)); sp();
+ put_n(toupper(97)); sp();
+ i32 big = 0 - 5;
+ put_n(abs(big)); sp();
+ int v[4];
+ v[0] = 4; v[1] = 1; v[2] = 3; v[3] = 2;
+ qsort(v, 4, 8, cfunc(cmpi));
+ put_n(v[0]); put_n(v[1]); put_n(v[2]); put_n(v[3]); sp();
+ put_f(half(5.0));
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/capture.zc b/tests/programs/capture.zc
new file mode 100644
index 0000000..c178791
--- /dev/null
+++ b/tests/programs/capture.zc
@@ -0,0 +1,31 @@
+// capture.start() sends everything the program writes into a string instead of
+// the terminal, and buf.end() stops it and terminates the string. nothing may
+// be printed during a capture, or it lands in the buffer too.
+include "src/lib/std.zc";
+
+fn main() {
+ let buf = capture.start();
+ put_s("hello world!");
+ put_n(42);
+ put_c(10);
+ buf.end(); // stops the capture and terminates the string
+
+ // the capture is off again, so these go to the terminal
+ put_n(outmode); put_c(10); // 0
+ put_n(outbuf == 0); put_c(10); // 1, the buffer is handed over
+ put_n(len_w(buf)); put_c(10); // 15
+ put_s(buf); // hello world!42
+
+ // a second capture does not disturb the first, and both read back
+ let two = capture.start();
+ put_s("second");
+ two.end();
+ put_n(eq_w(buf, "hello world!42\n")); put_c(10); // 1
+ put_n(eq_w(two, "second")); put_c(10); // 1
+
+ // capture_stdin returns an empty string, so an unused capture is empty
+ let three = capture.start();
+ three.end();
+ put_n(len_w(three)); put_c(10); // 0
+ return 0;
+} \ No newline at end of file
diff --git a/tests/programs/closure.zc b/tests/programs/closure.zc
new file mode 100644
index 0000000..660c32a
--- /dev/null
+++ b/tests/programs/closure.zc
@@ -0,0 +1,52 @@
+// closures: calls through values, captures by value, state in a capture,
+// nested captures, closures passed as arguments and returned from functions
+fn apply(f, x) { return f(x); }
+fn twice(g, x) { return g(g(x)); }
+fn sq(x) { return x * x; }
+word.u make(n) {
+ let h = fn(x) { return x * n; };
+ return u*h;
+}
+fn main() {
+ let k = 10, m = 3;
+ let add = fn(x) { return x + k * m; };
+ put_n(add(1)); put_c(10);
+ put_n(apply(add, 5)); put_c(10);
+ let none = fn() { return 42; };
+ put_n(none()); put_c(10);
+ let n = 0;
+ let bump = fn(d) { n = n + d; return n; };
+ put_n(bump(2)); put_c(10);
+ put_n(bump(5)); put_c(10);
+ put_n(n); put_c(10);
+ let a = 2;
+ let outer = fn(x) {
+ let inner = fn(y) { return y + a + x; };
+ return inner(100);
+ };
+ put_n(outer(7)); put_c(10);
+ let base = 5;
+ let scale = fn(i) { return i * base; };
+ let s = 0, i = 0;
+ while (i < 4) { s = s + scale(i); i++; }
+ put_n(s); put_c(10);
+ let c = 3;
+ let plus = fn(v) { return v + c; };
+ put_n(twice(plus, 1)); put_c(10);
+ put_n(sq(4)); put_c(10);
+ let total = 0, j = 0;
+ while (j < 3) {
+ let kk = j;
+ let g = fn(x) { return x + kk; };
+ total = total + g(10);
+ j++;
+ }
+ put_n(total); put_c(10);
+ let f = make(4);
+ put_n(f(5)); put_c(10);
+ let h = 1;
+ let frozen = fn(x) { return x + h; };
+ h = 100;
+ put_n(frozen(1)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/csyn.zc b/tests/programs/csyn.zc
new file mode 100644
index 0000000..0099d1c
--- /dev/null
+++ b/tests/programs/csyn.zc
@@ -0,0 +1,18 @@
+// do/while, enum constants, and a C cast
+typecheck on;
+enum color { RED, GREEN = 5, BLUE };
+fn main() {
+ int n = 0, i = 0;
+ do {
+ i++;
+ if (i == 2) continue;
+ n = n + i;
+ } while (i < 5);
+ int* p = (int*)0;
+ int z = (int)p;
+ put_n(n); put_c(' ');
+ put_n(RED); put_c(' '); put_n(GREEN); put_c(' ');
+ put_n(BLUE); put_c(' ');
+ put_n(z); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/decl_undefined.zc b/tests/programs/decl_undefined.zc
new file mode 100644
index 0000000..0ee9312
--- /dev/null
+++ b/tests/programs/decl_undefined.zc
@@ -0,0 +1,2 @@
+fn helper();
+fn main() { helper(); return 0; }
diff --git a/tests/programs/discard_m.zc b/tests/programs/discard_m.zc
new file mode 100644
index 0000000..35e9f3b
--- /dev/null
+++ b/tests/programs/discard_m.zc
@@ -0,0 +1,3 @@
+// m* on an unnamed u result lets the value be discarded; the plain call is an error
+.u mk(int v) { let p = new(1); p[0] = v; return u*p; }
+fn main() { m*mk(7); m*mk(8); put_n(7); put_c(10); return 0; }
diff --git a/tests/programs/doublefree.zc b/tests/programs/doublefree.zc
new file mode 100644
index 0000000..296b194
--- /dev/null
+++ b/tests/programs/doublefree.zc
@@ -0,0 +1,9 @@
+memdebug on;
+unchecked fn main()
+{
+ let p = new(1);
+ free(p);
+ free(p);
+ put_s("ok\n");
+ return 0;
+}
diff --git a/tests/programs/enhance.zc b/tests/programs/enhance.zc
new file mode 100644
index 0000000..327b4bd
--- /dev/null
+++ b/tests/programs/enhance.zc
@@ -0,0 +1,28 @@
+// custom opcodes: recipes, lifted vm code, several outputs, and a fallback
+enhance clampadd(a, b) micro "a = add a b; a = min a 100; a = max a 0";
+enhance cube(x) vm { 0 $ 0 $ * * };
+enhance 2 divmod(a, b) micro "r2 = sdiv a b" "\n r1 = srem a b\n r0 = r2";
+enhance div7(a) micro "a = sdiv a 7";
+enhance urem10(a) micro "a = urem a 10";
+enhance hi(a, b) micro "a = mulhu a b";
+enhance noisy(x) vm { 0 $ . } else soft_noisy; // i/o can't be lifted, so calls use the fallback
+fn soft_noisy(x) { return x + 1; }
+let qq, rr, seed = 7;
+fn rnd() { seed = seed * 1103515245 + 12345; return seed ^ (seed >> 11); }
+fn main() {
+ let i = 0, a, bad = 0;
+ put_n(clampadd(70, 50)); put_c(' '); put_n(clampadd(-50, 2)); put_c(' '); put_n(cube(7)); put_c(' ');
+ asm { 17 5 divmod rr ! qq ! }
+ put_n(qq); put_c(','); put_n(rr); put_c(' ');
+ put_n(noisy(4)); put_c(' ');
+ put_n(hi(-1, -1)); put_c(' ');
+ while (i < 5000) {
+ a = rnd();
+ if (div7(a) != zdiv(a, 7)) bad++;
+ if (urem10(a) < 0 || urem10(a) > 9) bad++;
+ if (a / (i + 1) != zdiv(a, i + 1)) bad++;
+ i++;
+ }
+ put_n(bad); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/enumscope.zc b/tests/programs/enumscope.zc
new file mode 100644
index 0000000..5c6d7d0
--- /dev/null
+++ b/tests/programs/enumscope.zc
@@ -0,0 +1,9 @@
+// enum members are reached through the enum's name as well as plainly
+enum color { RED, GREEN = 5, BLUE };
+fn main() {
+ int c = color.BLUE;
+ put_n(color.RED); put_c(' '); put_n(GREEN); put_c(' ');
+ put_n(c); put_c(' ');
+ switch (c) { case color.BLUE: put_s("blue"); break; default: put_s("other"); }
+ return 0;
+}
diff --git a/tests/programs/enumtype.zc b/tests/programs/enumtype.zc
new file mode 100644
index 0000000..9a6e89b
--- /dev/null
+++ b/tests/programs/enumtype.zc
@@ -0,0 +1,12 @@
+// enums are a type of their own in typed code: their members, and only those, take them; an enum still becomes an int
+typecheck on;
+enum color { RED, GREEN = 5, BLUE };
+int paint(color c) { return c; }
+int main() {
+ color c = color.BLUE;
+ color d = RED;
+ int n = c;
+ put_n(n); put_c(32); put_n(paint(GREEN)); put_c(32); put_n(d == color.RED);
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/f32.expected b/tests/programs/f32.expected
new file mode 100644
index 0000000..8220b5f
--- /dev/null
+++ b/tests/programs/f32.expected
@@ -0,0 +1 @@
+16777216.000000 16777217.000000 001 1.500000 1.000000 3.600000 10 7.800000 4950.000000 450
diff --git a/tests/programs/f32.zc b/tests/programs/f32.zc
new file mode 100644
index 0000000..0f60dcd
--- /dev/null
+++ b/tests/programs/f32.zc
@@ -0,0 +1,37 @@
+// f32: single precision values and packed arrays of them
+struct vec3 { f32 x, y, z; };
+f32 gsum;
+fn put_f(x) { asm { x @ #1220 } return 0; }
+[8]f32 gbuf;
+
+f32 half(f32 v) { return v / 2; }
+
+fn main() {
+ gsum = 0.1;
+ f32 a = 16777216.0;
+ f32 b = a + 1.0; // rounds back to 16777216 in single precision
+ float c = 16777216.0 + 1.0;
+ f32 t = 0.1;
+ float u = 0.1;
+ put_f(b); put_c(' '); put_f(c); put_c(' ');
+ put_n(t == 0.1); put_n(t == u); put_n(t == (f32)u); put_c(' ');
+ put_f(half(3.0)); put_c(' ');
+ put_f(gsum * 10); put_c(' '); // 1.000000 (0.1f * 10 in double)
+ vec3 v; v.x = 1.5; v.y = 0.1; v.z = 2;
+ put_f(v.x + v.y + v.z); put_c(' ');
+ int i = 0;
+ gbuf[0] = 0.1; put_n(gbuf[0] == t); put_n(gbuf[0] == u); put_c(' ');
+ while (i < 8) { gbuf[i] = i * 0.25 + 0.1; i++; }
+ float s = 0;
+ i = 0;
+ while (i < 8) { s += gbuf[i]; i++; }
+ put_f(s); put_c(' ');
+ f32* p = new(f32, 100);
+ i = 0;
+ while (i < 100) { p[i] = i; i++; }
+ s = 0; i = 0;
+ while (i < 100) { s += p[i]; i++; }
+ put_f(s); put_c(' ');
+ put_n(sizeof(gbuf)); put_n(sizeof([100]f32)); put_c(10);
+ free(d*p);
+}
diff --git a/tests/programs/f32o3.zc b/tests/programs/f32o3.zc
new file mode 100644
index 0000000..8f1ec2b
--- /dev/null
+++ b/tests/programs/f32o3.zc
@@ -0,0 +1,14 @@
+// f32 packed arrays and rounding inside an o3 region
+fn put_f(x) { asm { x @ #1220 } return 0; }
+o3 float dot(f32* p, int n) {
+ float s = 0;
+ f32 acc = 0;
+ for (int i = 0; i < n; i++) { s += p[i] * p[i]; acc = acc + p[i]; p[i] = p[i] + 0.1; }
+ return s + acc;
+}
+fn main() {
+ f32* p = new(f32, 50);
+ for (int i = 0; i < 50; i++) p[i] = i * 0.5;
+ put_f(dot(p, 50)); put_c(' '); put_f(p[3]); put_c(' '); put_n(p[3] == 1.6); put_c(10);
+ free(d*p);
+}
diff --git a/tests/programs/fbench.zc b/tests/programs/fbench.zc
new file mode 100644
index 0000000..1658ba7
--- /dev/null
+++ b/tests/programs/fbench.zc
@@ -0,0 +1,37 @@
+// float kernel, plain and o3. bench_float.sh times them. the letter picks the
+// kernel and the digits are how many times to run it, same shape as bench.zc.
+float poly(int n) {
+ float s = 0.0;
+ float x = 1.25;
+ int i = 0;
+ while (i < n) {
+ s = s + x * 1.5;
+ x = x * 0.5 + 1.0;
+ i = i + 1;
+ }
+ return s;
+}
+o3 float poly3(int n) {
+ float s = 0.0;
+ float x = 1.25;
+ int i = 0;
+ while (i < n) {
+ s = s + x * 1.5;
+ x = x * 0.5 + 1.0;
+ i = i + 1;
+ }
+ return s;
+}
+fn main() {
+ let which = get_c(), reps = 0, ch = get_c(), r = 0;
+ while (ch >= 48 && ch <= 57) { reps = reps * 10 + ch - 48; ch = get_c(); }
+ while (reps > 0) {
+ if (which == 97) r = (int)poly(20000);
+ if (which == 98) r = (int)poly3(20000);
+ if (which == 99) r = (int)poly(2000);
+ reps = reps - 1;
+ }
+ put_n(r);
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/float.zc b/tests/programs/float.zc
new file mode 100644
index 0000000..81b8668
--- /dev/null
+++ b/tests/programs/float.zc
@@ -0,0 +1,32 @@
+fn put_f(x) { asm { x @ #1220 } return 0; }
+fn f2i(x) { return asm { x @ #1218 }; }
+fn i2f(x) { return asm { x @ #1216 }; }
+fn sp() { put_c(32); return 0; }
+
+fn main() {
+ let a = 1.5;
+ let b = 2.5;
+ put_f(a + b); sp();
+ put_f(7.0 - 2.5); sp();
+ put_f(1.5 * 2); sp();
+ put_f(10.0 / 4.0); sp();
+ put_f(1 + 1.5); sp();
+ put_f(1.5e-3); sp();
+ put_f(0.1); sp();
+ put_f(-0.125); sp();
+ put_f(1.0 / 0.0); sp();
+ put_f(-1.0 / 0.0); sp();
+ put_f(0.0 / 0.0); sp();
+ put_n(1.5 < 2.0); sp();
+ put_n(2.0 <= 2.0); sp();
+ put_n(2.0 == 2); sp();
+ put_n(1.5 == 1); sp();
+ put_n(3.0 > 3.5); sp();
+ put_n(f2i(3.9)); sp();
+ put_n(f2i(-3.9)); sp();
+ put_f(i2f(3)); sp();
+ put_f(i2f(-2)); sp();
+ put_n(10 / 4);
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/fo3.zc b/tests/programs/fo3.zc
new file mode 100644
index 0000000..593e88a
--- /dev/null
+++ b/tests/programs/fo3.zc
@@ -0,0 +1,50 @@
+// o3 lifts float ops: the plain kernel and the o3 kernel agree, a constant
+// float expression folds, x - x stays a NaN, and -0 is false inside o3.
+fn put_f(x) { asm { x @ #1220 } return 0; }
+fn sp() { put_c(32); return 0; }
+float poly(int n) {
+ float s = 0.0;
+ float x = 1.25;
+ int i = 0;
+ while (i < n) {
+ s = s + x * 1.5;
+ x = x * 0.5 + 1.0;
+ i = i + 1;
+ }
+ return s;
+}
+o3 float poly3(int n) {
+ float s = 0.0;
+ float x = 1.25;
+ int i = 0;
+ while (i < n) {
+ s = s + x * 1.5;
+ x = x * 0.5 + 1.0;
+ i = i + 1;
+ }
+ return s;
+}
+o3 float folded() { return 1.5 + 2.5; }
+o3 float subself(float x) { return x - x; }
+o3 int zfalse() {
+ float z = 0.0 * -1.0;
+ int n = 0;
+ if (z) n = 1;
+ while (z) n = n + 2;
+ return n;
+}
+o3 float addtwo(float a) { return a + 2; }
+o3 int below(float a, float b) { return a < b; }
+fn main() {
+ put_n((int)poly(2000)); sp();
+ put_n((int)poly3(2000)); sp();
+ put_f(folded()); sp();
+ put_f(subself(0.0 / 0.0)); sp();
+ put_n(zfalse()); sp();
+ put_f(addtwo(1.25)); sp();
+ put_n(below(1.0, 2.0)); sp();
+ put_n(below(2.0, 2.0)); sp();
+ put_n(below(0.0 / 0.0, 1.0));
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/ftext.zc b/tests/programs/ftext.zc
new file mode 100644
index 0000000..b1c0acf
--- /dev/null
+++ b/tests/programs/ftext.zc
@@ -0,0 +1,37 @@
+fn sp() { put_c(32); return 0; }
+fn pm(x, prec, mode) { asm { x @ prec @ mode @ #1236 } return 0; }
+fn parsed(s) { return asm { s @ #1222 }; }
+fn ok(a, b) {
+ let w = asm { a @ b @ ^ };
+ if (w) put_n(0); else put_n(1);
+ return 0;
+}
+
+fn main() {
+ let z = 0.0 * -1.0;
+ let hi = parsed("9007199254740994");
+ let lo = parsed("9007199254740992");
+ pm(1.0e308, 0, 0); sp();
+ pm(1.0e-10, 0, 0); sp();
+ pm(z, 0, 0); sp();
+ pm(1.0 / 0.0, 0, 0); sp();
+ pm(-1.0 / 0.0, 0, 0); sp();
+ pm(0.0 / 0.0, 0, 0); sp();
+ pm(0.1, 0, 0); sp();
+ pm(1.5, 2, 1); sp();
+ pm(1.25, 4, 2); sp();
+ pm(1.0e20, 6, 1); sp();
+ pm(1.0e40, 0, 0); sp();
+ ok(parsed("1.5"), 3.0 / 2.0);
+ ok(parsed("0.1"), 1.0 / 10.0);
+ ok(parsed("inf"), 1.0 / 0.0);
+ ok(parsed("-inf"), -1.0 / 0.0);
+ ok(parsed("-0"), z);
+ ok(parsed("9007199254740993"), lo);
+ if (hi == lo) put_n(0); else put_n(1);
+ if (parsed("5e-324")) put_n(1); else put_n(0);
+ if (parsed("1e-400")) put_n(0); else put_n(1);
+ if (parsed("-nan") == parsed("nan")) put_n(0); else put_n(1);
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/ftruth.zc b/tests/programs/ftruth.zc
new file mode 100644
index 0000000..9d24a42
--- /dev/null
+++ b/tests/programs/ftruth.zc
@@ -0,0 +1,22 @@
+fn main() {
+ let z = 0.0 * -1.0;
+ let p = 0.0;
+ let n = 1.5;
+ if (z) put_n(1); else put_n(0);
+ if (p) put_n(1); else put_n(0);
+ if (n) put_n(1); else put_n(0);
+ if (!z) put_n(1); else put_n(0);
+ if (!p) put_n(1); else put_n(0);
+ if (!n) put_n(1); else put_n(0);
+ if (z && n) put_n(1); else put_n(0);
+ if (n && z) put_n(1); else put_n(0);
+ if (z || n) put_n(1); else put_n(0);
+ if (n || z) put_n(1); else put_n(0);
+ if (p || z) put_n(1); else put_n(0);
+ for (; z;) put_n(9);
+ put_n(z == p);
+ put_n(z < p);
+ put_n(7);
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/ftype.zc b/tests/programs/ftype.zc
new file mode 100644
index 0000000..6db7f5a
--- /dev/null
+++ b/tests/programs/ftype.zc
@@ -0,0 +1,47 @@
+fn put_f(x) { asm { x @ #1220 } return 0; }
+fn sp() { put_c(32); return 0; }
+
+struct cell { int tag; float field; }
+
+float g;
+
+float add(float a, float b) { return a + b; }
+float pick(cell* p) { return p.field; }
+
+fn main() {
+ float x;
+ float y;
+ cell c;
+ cell* q;
+ float buf[1];
+ let w = 0;
+ let z = 0.0 * -1.0;
+ let n = 0;
+ x = 1.5;
+ y = 2;
+ g = 10;
+ put_f(add(x, y)); sp();
+ put_f(add(g, 0.5)); sp();
+ put_f(add(1, 2) + 0.5); sp();
+ c.tag = 4;
+ c.field = 4.25;
+ q = &c;
+ put_f(pick(q)); sp();
+ put_f(q.field + 1); sp();
+ put_f(c.field); sp();
+ buf[0] = 6.5;
+ put_f(buf[0]); sp();
+ put_n(&(1.field)); sp();
+ w = (&c) as word;
+ put_n(w.tag); sp();
+ put_f(w.field); sp();
+ w.field = 8.5;
+ put_f(c.field); sp();
+ put_n(g > 3); sp();
+ if (z) n = 1;
+ do { if (z) n = n + 2; } while (z);
+ if (!z && g) n = n + 4;
+ put_n(n);
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/gc.zc b/tests/programs/gc.zc
new file mode 100644
index 0000000..81412a3
--- /dev/null
+++ b/tests/programs/gc.zc
@@ -0,0 +1,18 @@
+let keep;
+unchecked fn churn(n) {
+ let i = 0, p;
+ while (i < n) { p = new(10); p[0] = i; if (i % 100 == 0) keep = p; i++; }
+ return 0;
+}
+fn main() {
+ let base = live_objects(), f;
+ churn(1000);
+ put_n(live_objects() - base); put_c(' ');
+ f = gc();
+ put_n(f >= 990); put_c(' '); // conservative: a stale word or two may keep one alive
+ put_n(keep[0]); put_c(' '); // the one a global still points at survives
+ churn(200000); // 3.2m words through a 393k word heap: needs the automatic collector
+ put_n(gc_runs > 1); put_c(' ');
+ put_n(keep[0]); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/generics.expected b/tests/programs/generics.expected
new file mode 100644
index 0000000..f3f5f7b
--- /dev/null
+++ b/tests/programs/generics.expected
@@ -0,0 +1,2 @@
+9 6 3 1 2 3 2
+42 pt int 42 10 1
diff --git a/tests/programs/generics.zc b/tests/programs/generics.zc
new file mode 100644
index 0000000..e841b19
--- /dev/null
+++ b/tests/programs/generics.zc
@@ -0,0 +1,54 @@
+// generics are compiled per set of type arguments: T can be any size, new(T)
+// stamps the real type, and a struct can take several parameters
+struct pt { int x, y; };
+struct record<K, V> { K key; V val; };
+struct box<T> { int n; T* items; };
+
+K rkey<K, V>(record<K, V>* r) { return r.key; }
+V* rval<K, V>(record<K, V>* r) { return &r.val; }
+int tsize<T>() { return sizeof(T); }
+T*.u make<T>(int n) { T* p = new(T, n) as T*; return u*p; }
+record<K, V>*.u mkrec<K, V>(K k) {
+ record<K, V>* r = new(record<K, V>);
+ r.key = k;
+ return u*r;
+}
+T pick<T>(T* a, int i) { return a[i]; }
+int sumn<T>(T* a, int n) {
+ int s = 0;
+ for (int i = 0; i < n; i++) s += a[i];
+ return s;
+}
+fn nameof<T>() { return tname(typeof(T)); }
+o3 int osum<T>(T* a, int n) {
+ int s = 0;
+ for (int i = 0; i < n; i++) s += a[i].x + a[i].y;
+ return s;
+}
+int depth<T>(int n) {
+ if (n == 0) return tsize<T>();
+ return depth<T>(n - 1);
+}
+int main() {
+ record<int, pt>* a = mkrec<int, pt>(9);
+ a.val.x = 5; a.val.y = 6;
+ put_n(rkey<int, pt>(a)); put_c(' ');
+ put_n(rval<int, pt>(a).y); put_c(' ');
+ put_n(sizeof(record<int, pt>)); put_c(' ');
+ put_n(tsize<int>()); put_c(' ');
+ put_n(tsize<pt>()); put_c(' ');
+ put_n(tsize<record<int, pt>>()); put_c(' ');
+ put_n(depth<pt>(5)); put_c(10);
+ pt* ps = make<pt>(3);
+ ps[2].x = 40; ps[2].y = 2;
+ put_n(pick<pt>(ps, 2).x + ps[2].y); put_c(' ');
+ put_s(nameof<pt>()); put_c(' '); put_s(nameof<int>()); put_c(' ');
+ put_n(osum<pt>(ps, 3)); put_c(' ');
+ int* is = make<int>(4);
+ is[0] = 1; is[1] = 2; is[2] = 3; is[3] = 4;
+ put_n(sumn<int>(is, 4)); put_c(' ');
+ record<pt*, int>* b = mkrec<pt*, int>(ps);
+ put_n(b.key == ps); put_c(10);
+ free(d*a); free(d*b); free(d*ps); free(d*is);
+ return 0;
+}
diff --git a/tests/programs/genmeth.expected b/tests/programs/genmeth.expected
new file mode 100644
index 0000000..3d418c1
--- /dev/null
+++ b/tests/programs/genmeth.expected
@@ -0,0 +1 @@
+3 60 11 21 12
diff --git a/tests/programs/genmeth.zc b/tests/programs/genmeth.zc
new file mode 100644
index 0000000..305c55c
--- /dev/null
+++ b/tests/programs/genmeth.zc
@@ -0,0 +1,36 @@
+// methods on generic structs: each set of type arguments gets its own copy
+struct pt { int x, y; };
+struct table<K, V> {
+ int n; K* keys; V* vals;
+ fn init(int cap) {
+ this.n = 0;
+ this.keys = new(K, cap) as K*;
+ this.vals = new(V, cap) as V*;
+ return 0;
+ }
+ fn put(K k, V v) {
+ this.keys[this.n] = k;
+ this.vals[this.n] = v;
+ this.n++;
+ return this.n;
+ }
+ fn width() { return sizeof(K) * 10 + sizeof(V); }
+ fn total() {
+ int s = 0;
+ for (int i = 0; i < this.n; i++) s += this.vals[i];
+ return s;
+ }
+};
+int main() {
+ table<int, int> a;
+ a.init(8);
+ a.put(1, 10); a.put(2, 20); a.put(3, 30);
+ put_n(a.n); put_c(' '); put_n(a.total()); put_c(' '); put_n(a.width()); put_c(' ');
+ table<pt, int>* b = new(table<pt, int>);
+ b.init(4);
+ put_n(b.width()); put_c(' ');
+ table<int, pt>* c = new(table<int, pt>);
+ c.init(4);
+ put_n(c.width()); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/growheap.zc b/tests/programs/growheap.zc
new file mode 100644
index 0000000..07e10a6
--- /dev/null
+++ b/tests/programs/growheap.zc
@@ -0,0 +1,32 @@
+// the heap grows past its initial size: eight live blocks of 300000 words
+// don't fit in the 393216-word start, so the allocator takes more memory from
+// the host, and a block bigger than the start fits once they are freed
+let keep[8];
+unchecked fn fill(n) {
+ let i = 0, a;
+ while (i < n) { a = new(300000); a[0] = i + 1; a[299999] = (i + 1) * 3; keep[i] = a; i++; }
+ return 0;
+}
+unchecked fn total(n) {
+ let i = 0, a, sum = 0;
+ while (i < n) { a = keep[i]; sum = sum + a[0] + a[299999]; i++; }
+ return sum;
+}
+unchecked fn drop(n) {
+ let i = 0;
+ while (i < n) { free(keep[i]); keep[i] = 0; i++; }
+ return 0;
+}
+fn main() {
+ let a, start = heap_size();
+ fill(8);
+ put_n(total(8)); put_c(' ');
+ put_n(heap_size() > start); put_c(' ');
+ drop(8);
+ scrub(); gc();
+ a = new(3000000);
+ a[2999999] = 7;
+ put_n(a[2999999]); put_c(' ');
+ put_n(heap_size() >= 3000000); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/growone.zc b/tests/programs/growone.zc
new file mode 100644
index 0000000..12980ff
--- /dev/null
+++ b/tests/programs/growone.zc
@@ -0,0 +1,9 @@
+// one block bigger than the initial heap: the machine grows, and the block is usable
+fn main() {
+ let a = new(400000);
+ a[0] = 5;
+ a[399999] = 7;
+ put_n(a[0] + a[399999]); put_c(' ');
+ put_n(heap_size() >= 400000); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/infer_return.zc b/tests/programs/infer_return.zc
new file mode 100644
index 0000000..a9f12bd
--- /dev/null
+++ b/tests/programs/infer_return.zc
@@ -0,0 +1,3 @@
+// a owned return with no .u on the signature: the return type is inferred from the body
+fn mk(v) { int* p = new(1); p[0] = v; return p; }
+fn main() { int* q = mk(3); put_n(q[0]); put_c(10); return 0; }
diff --git a/tests/programs/interior.zc b/tests/programs/interior.zc
new file mode 100644
index 0000000..bccf377
--- /dev/null
+++ b/tests/programs/interior.zc
@@ -0,0 +1,17 @@
+unchecked fn main() {
+ let p = new(4), r, i = 0, t, bad = 0;
+ mem_set(4);
+ p[0] = 111; p[1] = 222; p[2] = 333; p[3] = 444;
+ r = p + 1;
+ free(p);
+ gc();
+ while (i < 8) {
+ t = new(4);
+ if (t == r - 1) bad = 1;
+ i++;
+ }
+ if (r[0] != 222) bad = bad + 2;
+ put_n(bad);
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/joins.zc b/tests/programs/joins.zc
new file mode 100644
index 0000000..98275ad
--- /dev/null
+++ b/tests/programs/joins.zc
@@ -0,0 +1,31 @@
+// joins: something given away on one path is dead afterwards, and the paths
+// that still own it free it on the way in. nothing here leaks.
+fn take(.d p) { return p[0]; }
+fn later(.d p); // not needed any more, but still allowed
+int*.u mk(int v) { int* p = new(int); *p = v; return u*p; }
+fn count(int n) { let s = 0; switch (n % 4) { case 0: s = 10; case 1, 2: s = 20; break; default: s = 30; } return s; }
+fn main() {
+ let base = live_objects(), i = 0, hits = 0, k;
+ while (i < 50) {
+ let x = mk(i);
+ if (i % 3 == 0) take(d*x); // else: freed here by the fixup
+ i++;
+ }
+ put_n(live_objects() - base); put_c(' ');
+ // give away and break: fine now; the normal exit frees it
+ i = 0;
+ let y = mk(7);
+ while (i < 10) { if (i == 4) { take(d*y); break; } i++; }
+ put_n(live_objects() - base); put_c(' ');
+ let z = mk(5);
+ while (1) { if (z[0] == 5) { hits = take(d*z); break; } }
+ put_n(hits); put_c(' ');
+ put_n(count(0) + count(1) + count(2) + count(3)); put_c(' ');
+ k = i > 3 ? i * 2 : 0 - 1;
+ put_n(k); put_c(' ');
+ put_n(forward(new(int))); put_c(' '); // tagged and defined below: the prescan knows .d
+ int** pp; int* q = new(int); *q = 9; pp = &q; put_n(**pp); free(d*q); put_c(' ');
+ put_n(live_objects() - base); put_c(10);
+ return 0;
+}
+fn forward(int*.d p) { return 3 + *p; }
diff --git a/tests/programs/manyerrors.zc b/tests/programs/manyerrors.zc
new file mode 100644
index 0000000..3ba2c5a
--- /dev/null
+++ b/tests/programs/manyerrors.zc
@@ -0,0 +1,6 @@
+// several errors in one run: a syntax error, an analyzer error, a type error
+typecheck on;
+fn f() { let a = ; return 0; }
+fn g() { let x = new(3); return 0; }
+int h() { char* s = 5; return 1; }
+fn main() { return 0 }
diff --git a/tests/programs/memleak.zc b/tests/programs/memleak.zc
new file mode 100644
index 0000000..a61c331
--- /dev/null
+++ b/tests/programs/memleak.zc
@@ -0,0 +1,11 @@
+memdebug on;
+int* g;
+fn main()
+{
+ int* p = new(int);
+ *p = 7;
+ g = d*p;
+ put_n(memreport());
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/memory_bounds.zc b/tests/programs/memory_bounds.zc
new file mode 100644
index 0000000..0863d03
--- /dev/null
+++ b/tests/programs/memory_bounds.zc
@@ -0,0 +1,13 @@
+// raw word loads, stores, and stack picks outside memory follow portable semantics.
+unchecked int raw_pick(int depth) { return asm { depth @ #6 }; }
+unchecked fn main() {
+ int p = -1;
+ put_n(*p); put_c(32); *p = 7;
+ p = 0x7FFFFFFF;
+ put_n(*p); put_c(32); *p = 9;
+ p = 0x100000000;
+ put_n(*p); put_c(32);
+ put_n(raw_pick(0x100000000)); put_c(32);
+ put_n(raw_pick(0 - 0x100000000)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/meta.zc b/tests/programs/meta.zc
new file mode 100644
index 0000000..6afdf36
--- /dev/null
+++ b/tests/programs/meta.zc
@@ -0,0 +1,43 @@
+fn ipow(a, b) { let r = 1; while (b > 0) { r *= a; b--; } return r; }
+// a parse word that adds an operator to the compiler's tables
+immediate fn pow_op() { next(); add_binop("**", 11, "ipow c"); return 0; }
+pow_op
+
+// unless (cond) stmt: a parse word that emits code itself
+immediate fn unless() {
+ let a;
+ next();
+ expect(intern("(")); rval(); plainuse(expr()); rval(); expect(intern(")"));
+ emitseq(useq[0], ulen[0]); // not
+ a = hole(); op(O_JZ); // skip the body when the condition was true
+ substmt();
+ setlit(a, here);
+ return 0;
+}
+// repeat N { body }: a macro, built from token lists
+immediate fn repeat() {
+ let n, body, i = 0;
+ next(); n = tv; next();
+ body = grab();
+ while (i < n) { feed(qdup(body)); i++; }
+ free(d*body);
+ return 0;
+}
+fn main() {
+ put_n(2 ** 10); put_c(' '); put_n(3 + 2 ** 3 * 2); put_c(10);
+ unless (1 > 2) put_s("unless ok\n");
+ unless (2 > 1) put_s("wrong\n");
+ repeat 3 { put_c('r'); }
+ put_c(10);
+ let q = quote { put_n(40 + 2); put_c(10); return 7; };
+ put_n(q[0]); put_c(' ');
+ put_n(eval(q)); put_c(10);
+ let f = compile(q);
+ put_n(f()); put_c(10);
+ free(d*q);
+ let defs = quote { fn sq(x) { return x * x; } };
+ load(defs);
+ free(d*defs);
+ put_n(evals("return sq(12);")); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/methodown.zc b/tests/programs/methodown.zc
new file mode 100644
index 0000000..98ba146
--- /dev/null
+++ b/tests/programs/methodown.zc
@@ -0,0 +1,15 @@
+// a struct method can return an owned object: its header declares the .u return
+struct Node { int v; }
+struct Maker {
+ static Node*.u make(int n) { Node* p = new(Node); p.v = n; return u*p; }
+}
+Node*.u mk(int n) { Node* p = new(Node); p.v = n; return u*p; }
+int main() {
+ Node*.u a = Maker.make(7);
+ put_n(a.v); put_c(32);
+ free(d*a);
+ Node*.u b = mk(9);
+ put_n(b.v); put_c(10);
+ free(d*b);
+ return 0;
+}
diff --git a/tests/programs/misc.zc b/tests/programs/misc.zc
new file mode 100644
index 0000000..530322a
--- /dev/null
+++ b/tests/programs/misc.zc
@@ -0,0 +1,27 @@
+// ct { ... } runs a block while compiling and leaves its result as a constant
+immediate fn ct() {
+ let q, v;
+ next();
+ q = grab();
+ v = eval(q);
+ free(d*q);
+ lit(v);
+ return 0;
+}
+// teach the asm syntax that p means out, like the old repl demo
+immediate fn teach() { next(); mnem[12] = 'p'; return 0; }
+teach
+
+fn countdown() {
+ asm { 5 :top 0 $ '0' + p 1 - 0 $ done z top j :done _ 10 p }
+ return 0;
+}
+fn main() {
+ put_n(ct { let i = 0, s = 0; while (i < 10) { s += i; i++; } return s; }); put_c(10);
+ countdown();
+ put_s(lazy()); put_c(10);
+ return 0;
+}
+// the analyzer is off from here on
+unchecked;
+fn lazy() { let p = new(3); p[0] = 'o'; p[1] = 'k'; return p; }
diff --git a/tests/programs/names.zc b/tests/programs/names.zc
new file mode 100644
index 0000000..6ccb2eb
--- /dev/null
+++ b/tests/programs/names.zc
@@ -0,0 +1,9 @@
+fn f(d) { let u = 3, m = 4; if (d > 0) return u + m + d; return 0; }
+fn dmul(d) { return d * 4; }
+fn main() {
+ let u = 1;
+ put_n(f(5)); put_c(' ');
+ put_n(u); put_c(' ');
+ put_n(dmul(2)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/o3.zc b/tests/programs/o3.zc
new file mode 100644
index 0000000..c875d09
--- /dev/null
+++ b/tests/programs/o3.zc
@@ -0,0 +1,75 @@
+// o3 regions: every kind of control flow, against the same code without o3
+struct pt { int x; int y; };
+int g = 3;
+int sq(int x) { return x * x; }
+o3 int collatz(int n) {
+ int steps = 0;
+ while (n != 1) { if (n % 2 == 0) n = n / 2; else n = 3 * n + 1; steps++; }
+ return steps;
+}
+o3 int ack(int am, int n) {
+ if (am == 0) return n + 1;
+ if (n == 0) return ack(am - 1, 1);
+ return ack(am - 1, ack(am, n - 1));
+}
+o3 int many(int a, int b, int c) {
+ int dd = a + b, e = b + c, f = c + a, h = dd * e, i2 = e * f, j = f * dd, k = h + i2 + j, l = k / 7;
+ for (int t = 0; t < 5; t++) { dd += t; e -= t; f = f ^ t; l = l + dd * e - f; }
+ return dd + e + f + h + i2 + j + k + l;
+}
+o3 int mix(int n) {
+ int acc = 0;
+ for (int i = 0; i < n; i++) {
+ if (i % 3 == 0 && i % 5 == 0) acc += 15;
+ else if (i % 3 == 0 || i > 90) acc += 3;
+ else acc = acc + (i & 1 ? sq(i) : g);
+ switch (i % 4) { case 0: acc += 1; case 1: acc -= 2; default: acc = acc ^ 1; }
+ if (acc > 100000) break;
+ if (i == 7) continue;
+ acc = acc + i / 10 - i % 7 + (i << 2) - (i >> 1);
+ }
+ return acc;
+}
+o3 int dyn(int n) {
+ []int a = new([]int, n);
+ fixed []int f = new(fixed []int, n);
+ int s = 0;
+ for (int i = 0; i < len(a); i++) { a[i] = i * i; f[i] = n - i; }
+ for (int i = 0; i < n; i++) s = s + a[i] * f[i];
+ free(d*a); free(d*f);
+ return s;
+}
+o3 int pts(int n) {
+ pt* p = new(pt);
+ int s = 0;
+ p->x = 1; p->y = 2;
+ for (int i = 0; i < n; i++) { p->x = p->x + p->y; p->y = p->x - p->y; s = s + p->x % 1000; }
+ free(d*p);
+ return s;
+}
+o3 int divs(int x) {
+ int s = 0;
+ for (int i = -50; i < 50; i++) s = s + (x + i) / 7 + (x + i) % 7 + (x - i) / -3 + i / 16 + i % 16;
+ return s;
+}
+fn main() {
+ o3 int lone = 2;
+ let r = 0;
+ lone = lone * 21;
+ o3 {
+ int i = 0, s = 0;
+ while (i < 100) { s = s + collatz(i + 1); i++; }
+ put_n(s); put_c(' ');
+ put_n(lone); put_c(' ');
+ lone++;
+ }
+ put_n(lone); put_c(' ');
+ put_n(ack(2, 3)); put_c(' ');
+ put_n(many(1, 2, 3)); put_c(' ');
+ put_n(mix(100)); put_c(' ');
+ put_n(dyn(50)); put_c(' ');
+ put_n(pts(30)); put_c(' ');
+ put_n(divs(1000)); put_c(' ');
+ put_n(divs(-77)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/o3_bounds.zc b/tests/programs/o3_bounds.zc
new file mode 100644
index 0000000..fb347c0
--- /dev/null
+++ b/tests/programs/o3_bounds.zc
@@ -0,0 +1,2 @@
+// an index outside a []T inside o3 code: caught by the inline check
+fn main() { []int a = new([]int, 5); o3 { int i = 7; a[i] = 1; } free(d*a); return 0; }
diff --git a/tests/programs/o3bytes.zc b/tests/programs/o3bytes.zc
new file mode 100644
index 0000000..bada583
--- /dev/null
+++ b/tests/programs/o3bytes.zc
@@ -0,0 +1,52 @@
+// byte ops called from o3 stay in the region. ld8/st8 and the 16- and 32-bit
+// pair are opcodes there, on the interpreter, the arm64 jit and llvm.
+fn sp() { put_c(32); return 0; }
+fn litw(v) { return v + v + 1; }
+fn f() { put_c('A'); return 0; }
+fn main() {
+ let buf[4], b, i, s, t, p, v, w;
+ i = 0;
+ while (i < 4) { buf[i] = 0; i++; }
+ b = addr_b(buf);
+ i = 0;
+ s = 0;
+ o3 {
+ while (i < 20) {
+ t = b + i;
+ st8(t, i);
+ s = s + ld8(t);
+ i++;
+ }
+ }
+ put_n(s); sp();
+ o3 { st16(b, 4660); w = ld16(b); }
+ put_n(w); sp();
+ t = b + 7;
+ o3 { w = ld8(t); }
+ put_n(w); sp();
+ o3 { w = ld16(t); }
+ put_n(w); sp();
+ t = b + 10;
+ v = 16909060;
+ o3 { st32(t, v); w = ld32(t); }
+ put_n(w); sp();
+ t = 0 - 1;
+ o3 { st8(t, v); w = ld8(t); }
+ put_n(w); sp();
+ t = addr_b(MW) - 1;
+ o3 { w = ld16(t); }
+ put_n(w);
+ put_c(10);
+ p = f;
+ f();
+ while (*p != litw('A')) p++;
+ t = addr_b(p);
+ v = litw('B') & 255;
+ o3 { st8(t, v); }
+ f();
+ v = litw('C') & 255;
+ o3 { st8(t, v); }
+ f();
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/o3call.zc b/tests/programs/o3call.zc
new file mode 100644
index 0000000..b2be717
--- /dev/null
+++ b/tests/programs/o3call.zc
@@ -0,0 +1,29 @@
+// new stamps the header, so an o3 caller and an ordinary caller both see a type.
+// the call itself is not a boundary: no fatten, and the callee's prologue does not run.
+struct node { int v; node* c; };
+unchecked o3 node* leaf(int v) {
+ node* n = new(node);
+ n.v = v; n.c = 0;
+ return n;
+}
+unchecked o3 int inner() {
+ node* n = leaf(7);
+ int* p = n;
+ return p[-1];
+}
+unchecked o3 int add(int a, int b) { return a + b; }
+unchecked o3 int nest(int n) {
+ if (n == 0) return 0;
+ return n + nest(n - 1);
+}
+unchecked fn main() {
+ node* n = leaf(4);
+ int* p = n;
+ put_n(p[-1] < 0); put_c(' ');
+ put_n(inner() < 0); put_c(' ');
+ put_n(n.v); put_c(' ');
+ put_n(add(20, 3)); put_c(' ');
+ put_n(nest(10)); put_c(' ');
+ tprint(typeof_obj(n)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/o3copy.zc b/tests/programs/o3copy.zc
new file mode 100644
index 0000000..90b9ade
--- /dev/null
+++ b/tests/programs/o3copy.zc
@@ -0,0 +1,12 @@
+// copies of a variable that a loop reads again: the global pass reads the variable itself
+o3 int g(int n, int b) {
+ int s = 0, c = b, i = 0;
+ while (i < n) { c = b; s = s + c * 3 + i; i++; }
+ return s + c;
+}
+o3 int h(int n, int k) {
+ int a = k, t = 0, i = 0;
+ while (i < n) { t = t + a; if (t > 1000) t = t - 1000; a = k; i++; }
+ return t + a;
+}
+fn main() { put_n(g(1000, 7)); put_c(32); put_n(h(1000, 5)); put_c(10); return 0; }
diff --git a/tests/programs/o3deep.zc b/tests/programs/o3deep.zc
new file mode 100644
index 0000000..e715d9e
--- /dev/null
+++ b/tests/programs/o3deep.zc
@@ -0,0 +1,3 @@
+// o3 recursion nests on the host stack as deep as the machine's own stacks allow
+o3 int deep(int n) { if (n == 0) return 0; return 1 + deep(n - 1); }
+fn main() { put_n(deep(30000)); put_c(10); return 0; }
diff --git a/tests/programs/o3deep_over.zc b/tests/programs/o3deep_over.zc
new file mode 100644
index 0000000..45816f6
--- /dev/null
+++ b/tests/programs/o3deep_over.zc
@@ -0,0 +1,3 @@
+// recursion can grow beyond the original operand and frame reservations
+o3 int deep(int n) { if (n == 0) return 0; return 1 + deep(n - 1); }
+fn main() { put_n(deep(100000)); put_c(10); return 0; }
diff --git a/tests/programs/o3direct.expected b/tests/programs/o3direct.expected
new file mode 100644
index 0000000..ec32091
--- /dev/null
+++ b/tests/programs/o3direct.expected
@@ -0,0 +1,2 @@
+6765 1 0 123 4567 7 9 8.000000 5050 1926
+4 3 2 1 10
diff --git a/tests/programs/o3direct.zc b/tests/programs/o3direct.zc
new file mode 100644
index 0000000..3c5d7ee
--- /dev/null
+++ b/tests/programs/o3direct.zc
@@ -0,0 +1,33 @@
+// o3 calls between functions: arguments in registers, results on the stack, and the ways a call can leave the fast path
+fn put_f(x) { asm { x @ #1220 } return 0; }
+struct node { int v; node *next; };
+o3 int fib(int n) { if (n < 2) return n; return fib(n - 1) + fib(n - 2); }
+o3 int ev(int n);
+o3 int od(int n) { if (n == 0) return 0; return ev(n - 1); }
+o3 int ev(int n) { if (n == 0) return 1; return od(n - 1); }
+o3 int three(int a, int b, int c) { return a * 100 + b * 10 + c; }
+o3 int four(int a, int b, int c, int d) { return three(a, b, c) * 10 + d; }
+o3 int zero() { return 7; }
+o3 int ack(int m, int n) { if (m == 0) return n + 1; if (n == 0) return ack(m - 1, 1); return ack(m - 1, ack(m, n - 1)); }
+o3 float half(float x, int k) { if (k == 0) return x; return half(x / 2.0, k - 1); }
+o3 int noisy(int n) { if (n == 0) return 0; put_n(n); put_c(' '); return n + noisy(n - 1); }
+unchecked o3 int sumlist(node *p) { int s = 0; while (p) { s = s + p.v; p = p.next; } return s; }
+unchecked o3 int build(int n) {
+ node *h = 0;
+ for (int i = 1; i <= n; i++) { node *x = new(node); x.v = i; x.next = h; h = x; }
+ int s = sumlist(h);
+ while (h) { node *t = h.next; free(h); h = t; }
+ return s;
+}
+unchecked o3 int mix(int a, int b) { return three(a, zero(), b) + fib(a) + four(1, 2, 3, b); }
+unchecked fn main() {
+ put_n(fib(20)); put_c(' ');
+ put_n(ev(10)); put_c(' '); put_n(ev(7)); put_c(' ');
+ put_n(three(1, 2, 3)); put_c(' '); put_n(four(4, 5, 6, 7)); put_c(' '); put_n(zero()); put_c(' ');
+ put_n(ack(2, 3)); put_c(' ');
+ put_f(half(64.0, 3)); put_c(' ');
+ put_n(build(100)); put_c(' ');
+ put_n(mix(6, 9)); put_c(10);
+ put_n(noisy(4)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/o3fat.zc b/tests/programs/o3fat.zc
new file mode 100644
index 0000000..937b0f3
--- /dev/null
+++ b/tests/programs/o3fat.zc
@@ -0,0 +1,20 @@
+// o3 objects reaching normal code get their types: through a return (all the way down the list), an assignment, a store into a field
+struct node { int v; node* next; };
+struct box { [3]node* slots; int n; };
+unchecked o3 node* build(int n) {
+ node* head = 0;
+ for (int i = 0; i < n; i++) { node* k = new(node); k.v = i; k.next = head; head = k; }
+ return head;
+}
+unchecked fn main() {
+ node* l = build(5);
+ tprint(typeof_obj(l)); put_c(' '); tprint(typeof_obj(l.next.next.next)); put_c(' ');
+ put_n(l.next.v); put_c(' ');
+ box* b = 0;
+ o3 { b = new(box); b.slots[1] = new(node); b.n = 2; }
+ tprint(typeof_obj(b)); put_c(' '); tprint(typeof_obj(b.slots[1])); put_c(' ');
+ int* raw = 0;
+ o3 { raw = new(int, 6); }
+ tprint(typeof_obj(raw)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/o3licm.zc b/tests/programs/o3licm.zc
new file mode 100644
index 0000000..61e6a46
--- /dev/null
+++ b/tests/programs/o3licm.zc
@@ -0,0 +1,46 @@
+// loop invariants are built once in front of the loop, and a value two blocks both build is shared.
+// every result below has to match the plain run (ZYNOSHARE=1 turns the pass off).
+unchecked o3 int nest(int n, int k) {
+ int s = 0;
+ for (int i = 0; i < n; i++) {
+ for (int j = 0; j < n; j++) {
+ s = s + (k * 37 + 11) * (i + j);
+ }
+ }
+ return s;
+}
+unchecked o3 int loads(int* a, int n) {
+ int s = 0;
+ for (int i = 0; i < n; i++) s = s + a[0] * a[1] + i;
+ return s;
+}
+unchecked o3 int stores(int* a, int n) {
+ int s = 0;
+ for (int i = 0; i < n; i++) { a[0] = a[0] + 1; s = s + a[0] * 3; }
+ return s;
+}
+unchecked o3 int branchy(int x, int y, int n) {
+ int s = 0;
+ for (int i = 0; i < n; i++) {
+ if (i & 1) s = s + x * y * 5;
+ else s = s - x * y * 5 + i;
+ }
+ return s;
+}
+unchecked o3 int twice(int x, int y) {
+ int r = 0;
+ if (x > 0) r = (x * y + 3) * (x * y + 3);
+ else r = x * y + 3;
+ return r + (x * y + 3) * 7;
+}
+unchecked fn main() {
+ int* a = new(int, 4);
+ a[0] = 5; a[1] = 6;
+ put_n(nest(30, 3)); put_c(' ');
+ put_n(loads(a, 100)); put_c(' ');
+ put_n(stores(a, 10)); put_c(' ');
+ put_n(branchy(3, 4, 21)); put_c(' ');
+ put_n(twice(5, 2)); put_c(' ');
+ put_n(twice(-5, 2)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/o3push.expected b/tests/programs/o3push.expected
new file mode 100644
index 0000000..b6857dc
--- /dev/null
+++ b/tests/programs/o3push.expected
@@ -0,0 +1 @@
+1000 1498500 2997 1005 1 10 1 1498530 0
diff --git a/tests/programs/o3push.zc b/tests/programs/o3push.zc
new file mode 100644
index 0000000..e5f0090
--- /dev/null
+++ b/tests/programs/o3push.zc
@@ -0,0 +1,38 @@
+// push in o3 code: the quick path when there is room, dpush when there is not
+o3 int fill([]int a, int n) {
+ for (int i = 0; i < n; i++) push(a, i * 3);
+ return len(a);
+}
+o3 int sum([]int a) {
+ int s = 0;
+ for (int i = 0; i < len(a); i++) s += a[i];
+ return s;
+}
+o3 float fsum([]float a) {
+ float s = 0.0;
+ for (int i = 0; i < len(a); i++) s += a[i];
+ return s;
+}
+o3 int ffill([]float a, int n) {
+ for (int i = 0; i < n; i++) push(a, 0.5 * i);
+ return len(a);
+}
+int main() {
+ []int a = new([]int, 0);
+ put_n(fill(a, 1000)); put_c(' ');
+ put_n(sum(a)); put_c(' ');
+ put_n(a[999]); put_c(' ');
+ put_n(fill(a, 5)); put_c(' ');
+ put_n(cap(a) >= 1005); put_c(' ');
+ []float f = new([]float, 0);
+ ffill(f, 10);
+ put_n(len(f)); put_c(' '); put_n(fsum(f) == 22.5); put_c(' ');
+ put_n(drain(a)); put_c(' '); put_n(len(a)); put_c(10);
+ free(d*a); free(d*f);
+ return 0;
+}
+o3 int drain([]int a) {
+ int s = 0;
+ while (len(a) > 0) s += pop(a);
+ return s;
+}
diff --git a/tests/programs/o3scopes.zc b/tests/programs/o3scopes.zc
new file mode 100644
index 0000000..4dbf68c
--- /dev/null
+++ b/tests/programs/o3scopes.zc
@@ -0,0 +1,17 @@
+// registers reused by scopes that never overlap, and a fixed array read through its cached items and length
+o3 int scopes(fixed []int f) {
+ int total = 0;
+ for (int a = 0; a < 3; a++) { int x = a * 2; total += x; }
+ for (int b = 0; b < 3; b++) { int y = b * 3; total += y; }
+ for (int c = 0; c < 3; c++) { int z = c * 5; total += z; }
+ for (int e = 0; e < len(f); e++) { int w = f[e]; total += w; }
+ for (int g = 0; g < 4; g++) { int h = g + 1, q = h * h, r = q - 1; total += r; }
+ return total;
+}
+fn main() {
+ fixed []int f = new(fixed []int, 4);
+ for (int i = 0; i < 4; i++) f[i] = 10 * i;
+ put_n(scopes(f)); put_c(10);
+ free(d*f);
+ return 0;
+}
diff --git a/tests/programs/o3zero.zc b/tests/programs/o3zero.zc
new file mode 100644
index 0000000..cfb3061
--- /dev/null
+++ b/tests/programs/o3zero.zc
@@ -0,0 +1,8 @@
+unchecked o3 int raw() {
+ int* p = new(4);
+ return p[0] + p[1] + p[2] + p[3] + (p[-1] < 0);
+}
+fn main() {
+ put_n(raw()); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/overflow_data.zc b/tests/programs/overflow_data.zc
new file mode 100644
index 0000000..db24f8d
--- /dev/null
+++ b/tests/programs/overflow_data.zc
@@ -0,0 +1 @@
+fn main() { syscall(9, 5, syscall(9, 0, 0, 0), 0); while (1) asm { 1 } return 0; }
diff --git a/tests/programs/overflow_frame.zc b/tests/programs/overflow_frame.zc
new file mode 100644
index 0000000..c017453
--- /dev/null
+++ b/tests/programs/overflow_frame.zc
@@ -0,0 +1,2 @@
+fn big(n) { let a[500]; a[0] = n; return big(n + 1) + a[0]; }
+fn main() { syscall(9, 5, syscall(9, 0, 0, 0), 0); return big(0); }
diff --git a/tests/programs/overflow_return.zc b/tests/programs/overflow_return.zc
new file mode 100644
index 0000000..91e572d
--- /dev/null
+++ b/tests/programs/overflow_return.zc
@@ -0,0 +1,2 @@
+fn deep(n) { return deep(n + 1) + 1; }
+fn main() { syscall(9, 5, syscall(9, 0, 0, 0), 0); return deep(0); }
diff --git a/tests/programs/packedfields.expected b/tests/programs/packedfields.expected
new file mode 100644
index 0000000..df81ff5
--- /dev/null
+++ b/tests/programs/packedfields.expected
@@ -0,0 +1,2 @@
+4 3 3.500000 0 200 255 44 7 -3 65535 99 44 0 -4 0 98 1000 5 3.250000 77 5 42 17 3 -9 0 12 12 123456789
+15200 11 50
diff --git a/tests/programs/packedfields.zc b/tests/programs/packedfields.zc
new file mode 100644
index 0000000..8f6fe38
--- /dev/null
+++ b/tests/programs/packedfields.zc
@@ -0,0 +1,42 @@
+// struct fields of the sized types and f32 pack into shared words
+struct vtx { f32 x, y, z; u8 r, g, b, a; i16 id; u16 flags; int next; };
+struct pair { u8 a; int b; u8 c; };
+fn put_f(x) { asm { x @ #1220 } return 0; }
+fn sp() { put_c(32); return 0; }
+vtx gv;
+fn main() {
+ vtx v;
+ vtx* p = new(vtx);
+ pair q;
+ [3]vtx arr;
+ put_n(sizeof(vtx)); sp(); put_n(sizeof(pair)); sp();
+ v.x = 1.5; v.y = 0.1; v.z = 2; v.r = 200; v.g = 255; v.b = 300; v.a = 7; v.id = 0 - 3; v.flags = 65535; v.next = 99;
+ put_f(v.x + v.z); sp(); put_n(v.y == 0.1); sp();
+ put_n(v.r); sp(); put_n(v.g); sp(); put_n(v.b); sp(); put_n(v.a); sp(); put_n(v.id); sp(); put_n(v.flags); sp(); put_n(v.next); sp();
+ v.r += 100; v.flags++; v.id--; v.g = v.g + 1;
+ put_n(v.r); sp(); put_n(v.flags); sp(); put_n(v.id); sp(); put_n(v.g); sp();
+ p.r = 9; p.a = 8; p.id = 1000; p.next = 5; p.x = 3.25;
+ put_n(p.r); put_n(p.a); sp(); put_n(p.id); sp(); put_n(p.next); sp(); put_f(p.x); sp();
+ gv.b = 77; gv.flags = 5; put_n(gv.b); sp(); put_n(gv.flags); sp();
+ arr[1].a = 42; arr[2].r = 17; arr[1].next = 3; arr[0].id = 0 - 9;
+ put_n(arr[1].a); sp(); put_n(arr[2].r); sp(); put_n(arr[1].next); sp(); put_n(arr[0].id); sp(); put_n(arr[0].a); sp();
+ put_n(sizeof(arr)); sp();
+ q.a = 1; q.b = 123456789; q.c = 2; put_n(q.a); put_n(q.c); sp(); put_n(q.b);
+ put_c(10);
+ free(d*p);
+ second();
+ return 0;
+}
+struct rgba { u8 r, g, b, a; };
+o3 int bright(rgba* px, int n) {
+ int s = 0;
+ for (int i = 0; i < n; i++) { s += px[i].r + px[i].g + px[i].b; px[i].a = px[i].r + 1; }
+ return s;
+}
+fn second() {
+ rgba* px = new(rgba, 50);
+ for (int i = 0; i < 50; i++) { px[i].r = i; px[i].g = i * 2; px[i].b = 255 - i; }
+ put_n(bright(px, 50)); put_c(' '); put_n(px[10].a); put_c(' '); put_n(px[49].a); put_c(10);
+ free(d*px);
+ return 0;
+}
diff --git a/tests/programs/quarantine.zc b/tests/programs/quarantine.zc
new file mode 100644
index 0000000..5e3e188
--- /dev/null
+++ b/tests/programs/quarantine.zc
@@ -0,0 +1,25 @@
+// free quarantines the block. a stale alias still reads it, and freeing
+// through the alias does nothing because the header already says free.
+// unchecked so the analyzer doesn't reject the use after free.
+unchecked fn main() {
+ let p = new(4), q, r, i = 0, t, bad = 0;
+ p[0] = 111; p[1] = 222; p[2] = 333; p[3] = 444;
+ q = p;
+ r = p + 2;
+ free(p);
+ while (i < 400) { t = new(4); t[0] = 999; t[1] = 888; t[2] = 777; t[3] = 666; i++; }
+ if (q[0] != 111 || q[1] != 222 || r[0] != 333 || r[1] != 444) bad = 1;
+ free(q);
+ if (q[0] != 111 || q[1] != 222) bad = 2;
+ gc(); // q still points at the block, so the sweep must not reuse it
+ if (q[0] != 111 || r[0] != 333) bad = 3;
+ p = 0; q = 0; r = 0; t = 0;
+ scrub();
+ gc();
+ t = new(4);
+ t[0] = 1234;
+ put_n(bad); put_c(' ');
+ put_n(t[0]); put_c(10);
+ free(t);
+ return 0;
+}
diff --git a/tests/programs/rawpid.zc b/tests/programs/rawpid.zc
new file mode 100644
index 0000000..b3e7c2a
--- /dev/null
+++ b/tests/programs/rawpid.zc
@@ -0,0 +1,6 @@
+include "src/lib/std.zc";
+// syscall 39 is getpid on x86-64 only; the arm64 number differs, so test.sh runs this there alone.
+fn main() {
+ put_n(Host.raw(39, 0, 0, 0, 0, 0, 0) > 0); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/rawsys.zc b/tests/programs/rawsys.zc
new file mode 100644
index 0000000..753e1b7
--- /dev/null
+++ b/tests/programs/rawsys.zc
@@ -0,0 +1,5 @@
+include "src/lib/std.zc";
+fn main() {
+ put_n(Host.raw(9999, 0, 0, 0, 0, 0, 0)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/recycle.zc b/tests/programs/recycle.zc
new file mode 100644
index 0000000..98c9b50
--- /dev/null
+++ b/tests/programs/recycle.zc
@@ -0,0 +1,23 @@
+// a uniquely owned free returns the block to its size class at once.
+// a plain free still quarantines; that contract lives in quarantine.zc.
+struct node { node* c; int n; };
+unchecked fn addr(p) { return p; }
+fn main() {
+ let base = live_objects(), p, q, slot;
+ node* a;
+ node* b;
+ p = new(4);
+ a = new(node);
+ b = new(node);
+ p[0] = 111;
+ a.c = b; a.n = 1; b.c = 0; b.n = 2;
+ slot = addr(p);
+ free(d*p);
+ q = new(4);
+ free(d*a);
+ put_n(q == slot); put_c(' ');
+ put_n(q[0]); put_c(' ');
+ put_n(live_objects() - base); put_c(10);
+ free(d*q);
+ return 0;
+}
diff --git a/tests/programs/roots.zc b/tests/programs/roots.zc
new file mode 100644
index 0000000..22f12a5
--- /dev/null
+++ b/tests/programs/roots.zc
@@ -0,0 +1,17 @@
+// a pointer left below the live stack is not a root. scrub then clears that word.
+unchecked fn main() {
+ let p, slot, before, after;
+ p = new(2);
+ p[0] = 7;
+ p[1] = 8;
+ slot = DSLO + 8;
+ *slot = p;
+ before = live_objects();
+ p = 0;
+ gc();
+ after = live_objects();
+ put_n(after < before); put_c(32);
+ scrub();
+ put_n(*slot); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/scrub.zc b/tests/programs/scrub.zc
new file mode 100644
index 0000000..ebc6cfa
--- /dev/null
+++ b/tests/programs/scrub.zc
@@ -0,0 +1,9 @@
+unchecked fn main() {
+ mem_set(2);
+ let p = new(2);
+ p[0] = 11;
+ free(p);
+ put_n(p[0]);
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/share.zc b/tests/programs/share.zc
new file mode 100644
index 0000000..e9c381f
--- /dev/null
+++ b/tests/programs/share.zc
@@ -0,0 +1,34 @@
+// shared cons cells, a shared string, and a cycle. the collector reclaims
+// them once nothing points at them. free of a shared cell does nothing.
+fn main() {
+ let base = live_objects(), a, b, c, s, p, q, cyc, n, tri;
+ a = cons(1, 2);
+ b = cons(a, 3);
+ c = cons(a, 4);
+ put_n(car(a)); put_c(32);
+ put_n(cdr(a)); put_c(32);
+ put_n(car(car(b))); put_c(32);
+ put_n(cdr(c)); put_c(32);
+ put_n(car(b) == a); put_c(32);
+ put_n(is_shared(a)); put_c(32);
+ n = live_objects();
+ free(a);
+ put_n(live_objects() == n); put_c(32);
+ put_n(car(a)); put_c(32);
+ s = n_to_s(42);
+ p = cons(s, 0);
+ q = cons(s, 1);
+ put_n(s_to_n(car(p))); put_c(32);
+ put_n(car(p) == car(q)); put_c(32);
+ cyc = cons(0, 1);
+ cyc[0] = cyc;
+ put_n(cdr(cyc)); put_c(32);
+ put_n(car(cyc) == cyc); put_c(32);
+ tri = triple(5, 6, 7);
+ put_n(tget(tri, 1)); put_c(32);
+ a = 0; b = 0; c = 0; s = 0; p = 0; q = 0; cyc = 0; tri = 0;
+ scrub();
+ gc();
+ put_n(live_objects() - base); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/share_dbg.zc b/tests/programs/share_dbg.zc
new file mode 100644
index 0000000..5485b51
--- /dev/null
+++ b/tests/programs/share_dbg.zc
@@ -0,0 +1,10 @@
+// memdebug still checks a shared cell. free does not quarantine it, and car
+// reads the payload through checkptr.
+memdebug on;
+fn main() {
+ let a = cons(7, 8);
+ free(a);
+ put_n(car(a)); put_c(32);
+ put_n(is_shared(a)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/sized.expected b/tests/programs/sized.expected
new file mode 100644
index 0000000..c5f51ab
--- /dev/null
+++ b/tests/programs/sized.expected
@@ -0,0 +1,5 @@
+4 -56 -32768 0 44 -56 44 -56 44
+2008 -25536 4464 705032704 -128 1 8000000000 3 2
+124716 1 77 255 0
+255 0 -1 7 44 2
+54285
diff --git a/tests/programs/sized.zc b/tests/programs/sized.zc
new file mode 100644
index 0000000..0a18467
--- /dev/null
+++ b/tests/programs/sized.zc
@@ -0,0 +1,80 @@
+// sized integers: scalars wrap to their size, arrays and pointers of them are packed
+struct pix { u8 r, g, b, a; i16 dx; [4]u8 tag; };
+u8 gbyte = 300;
+i8 gneg = 200;
+u16 gword[5];
+[10]u8 gbuf;
+
+u8 add8(u8 a, u8 b) { return a + b; }
+
+fn scalars() {
+ u8 a = 250;
+ i8 s = 100;
+ i16 h = 32767;
+ u32 w = 4294967295;
+ a = a + 10; put_n(a); put_c(' '); // 4
+ s = s + 100; put_n(s); put_c(' '); // -56
+ h++; put_n(h); put_c(' '); // -32768
+ w++; put_n(w); put_c(' '); // 0
+ put_n(gbyte); put_c(' '); put_n(gneg); put_c(' '); // 44 -56
+ put_n(add8(200, 100)); put_c(' '); // 44
+ put_n((i8)200); put_c(' '); put_n(300 as u8); put_c(10); // -56 44
+ return 0;
+}
+fn arrays() {
+ u8 buf[20];
+ i16 sh[7];
+ u32 ws[3];
+ i8 sb[9];
+ int i = 0, sum = 0;
+ while (i < 20) { buf[i] = i * 20; i++; } // wraps past 255
+ i = 0; while (i < 20) { sum = sum + buf[i]; i++; }
+ put_n(sum); put_c(' '); // 1250
+ sh[3] = 40000; put_n(sh[3]); put_c(' '); // -25536
+ sh[3] += 30000; put_n(sh[3]); put_c(' '); // 4464
+ ws[1] = 5000000000; put_n(ws[1]); put_c(' '); // 705032704
+ sb[8] = 127; sb[8]++; put_n(sb[8]); put_c(' '); // -128
+ sb[0] = 200; put_n(sb[0] < 0); put_c(' '); // 1: -56 is negative
+ ws[0] = 4000000000; put_n(ws[0] + ws[0]); put_c(' '); // arithmetic is done in int: 8000000000
+ put_n(sizeof(buf)); put_c(' '); // 3 words
+ put_n(sizeof(sh)); put_c(10); // 2 words
+ return 0;
+}
+fn heap() {
+ u8* p = new(u8, 1000);
+ int i = 0, sum = 0;
+ while (i < 1000) { p[i] = i; i++; }
+ i = 0; while (i < 1000) { sum = sum + p[i]; i++; }
+ put_n(sum); put_c(' '); // 1000 bytes of 0..255: 3 * 32640 + 28 * ... = 127..
+ put_n(osize(p) < 256); put_c(32); // 125 words plus the header, not 1000
+ *p = 77; put_n(p[0]); put_c(' ');
+ free(d*p);
+ gbuf[9] = 255; gword[4] = 65535; gword[4]++;
+ put_n(gbuf[9]); put_c(' '); put_n(gword[4]); put_c(10);
+ return 0;
+}
+fn fields() {
+ pix* q = new(pix);
+ q.r = 255; q.g = 256; q.dx = 65535; q.tag[2] = 7; q.tag[3] = 300;
+ put_n(q.r); put_c(' '); put_n(q.g); put_c(' '); put_n(q.dx); put_c(' ');
+ put_n(q.tag[2]); put_c(' '); put_n(q.tag[3]); put_c(' '); put_n(sizeof(pix)); put_c(10);
+ free(d*q);
+ return 0;
+}
+o3 fn checksum(u8* p, int n) {
+ int i = 0, a = 1, b = 0;
+ while (i < n) { a = (a + p[i]) % 251; b = (b + a) % 251; i++; }
+ return b * 256 + a;
+}
+fn o3run() {
+ u8* p = new(u8, 5000);
+ int i = 0;
+ while (i < 5000) { p[i] = i * 7; i++; }
+ put_n(checksum(p, 5000)); put_c(10);
+ free(d*p);
+ return 0;
+}
+fn main() {
+ scalars(); arrays(); heap(); fields(); o3run();
+ return 0;
+}
diff --git a/tests/programs/sizedvec.expected b/tests/programs/sizedvec.expected
new file mode 100644
index 0000000..9c373cc
--- /dev/null
+++ b/tests/programs/sizedvec.expected
@@ -0,0 +1,2 @@
+100 11866 181 44 181 99 -25536 0 -5 4464 0 2.500000 1.100000 1400 1
+33586
diff --git a/tests/programs/sizedvec.zc b/tests/programs/sizedvec.zc
new file mode 100644
index 0000000..2055ade
--- /dev/null
+++ b/tests/programs/sizedvec.zc
@@ -0,0 +1,39 @@
+// growable arrays of the sized types and f32: []u8, []i16, []f32
+fn put_f(x) { asm { x @ #1220 } return 0; }
+fn sp() { put_c(32); return 0; }
+fn main() {
+ []u8 b = new([]u8, 0);
+ []i16 h = new([]i16, 3);
+ []f32 f = new([]f32, 0);
+ int i = 0, s = 0;
+ while (i < 100) { push(b, i * 7); i++; }
+ i = 0;
+ while (i < 100) { s = s + b[i]; i++; }
+ put_n(len(b)); sp(); put_n(s); sp(); put_n(b[99]); sp();
+ b[5] = 300; put_n(b[5]); sp();
+ put_n(pop(b)); sp(); put_n(len(b)); sp();
+ h[0] = 40000; h[2] = 0 - 5; push(h, 70000);
+ put_n(h[0]); sp(); put_n(h[1]); sp(); put_n(h[2]); sp(); put_n(h[3]); sp();
+ push(f, 0.1); push(f, 2.5); push(f, 1.0);
+ put_n(f[0] == 0.1); sp(); put_f(f[1]); sp(); put_f(f[0] + f[2]); sp();
+ resize(b, 3); resize(b, 12);
+ put_n(b[2]); put_n(b[3]); put_n(b[11]); sp();
+ put_n(cap(b) >= 12);
+ put_c(10);
+ free(d*b); free(d*h); free(d*f);
+ second();
+ return 0;
+}
+o3 int sumv([]u8 v) {
+ int s = 0;
+ for (int i = 0; i < len(v); i++) s += v[i];
+ return s;
+}
+fn second() {
+ []u8 v = new([]u8, 0);
+ int i = 0;
+ while (i < 300) { push(v, i); i++; }
+ put_n(sumv(v)); put_c(10);
+ free(d*v);
+ return 0;
+}
diff --git a/tests/programs/static.zc b/tests/programs/static.zc
new file mode 100644
index 0000000..ef54339
--- /dev/null
+++ b/tests/programs/static.zc
@@ -0,0 +1,2 @@
+struct Str { int len; static fn size(s) { return s * 2; } static fn none() { return 5; } fn get() { return this.len; } }
+int main() { Str a; a.len = 9; put_n(Str.size(7)); put_c(32); put_n(Str.none()); put_c(32); put_n(a.get()); put_c(10); return 0; }
diff --git a/tests/programs/stdbuf.zc b/tests/programs/stdbuf.zc
new file mode 100644
index 0000000..ab8f9dc
--- /dev/null
+++ b/tests/programs/stdbuf.zc
@@ -0,0 +1,18 @@
+include "src/lib/std.zc";
+fn main() {
+ let a = arena(8), p, q, b, i = 0, s = 0;
+ p = Arena.alloc(a, 4);
+ q = Arena.alloc(a, 4);
+ put_n(q - p); put_c(' ');
+ Arena.reset(a);
+ put_n(Arena.alloc(a, 4) == p); put_c(' ');
+ b = bnew(8);
+ while (i < 4) { Buf.set(b, i, 'A' + i); i++; }
+ i = 0;
+ while (i < Buf.length(b)) { s = s + Buf.get(b, i); i++; }
+ put_n(s); put_c(' ');
+ put_n(Buf.capacity(b)); put_c(10);
+ free(d*a);
+ free(d*b);
+ return 0;
+}
diff --git a/tests/programs/stdmore.zc b/tests/programs/stdmore.zc
new file mode 100644
index 0000000..fb225ec
--- /dev/null
+++ b/tests/programs/stdmore.zc
@@ -0,0 +1,32 @@
+include "src/lib/std.zc";
+fn main() {
+ let buf[32];
+ put_n(Text.len("hello")); put_c(' ');
+ put_n(Text.eq("abc", "abc")); put_c(' ');
+ put_n(Text.eq("abc", "abd")); put_c(' ');
+ put_n(Text.eq("ab", "abc")); put_c(' ');
+ put_n(Text.copy(buf, "copy me")); put_c(' ');
+ put_s(buf); put_c(' ');
+ put_n(Text.find("needle in a haystack", "hay")); put_c(' ');
+ put_n(Text.find("abc", "zz")); put_c(' ');
+ put_n(Text.find("abc", "")); put_c(10);
+ Text.copy(buf, "MiXeD 123");
+ put_n(Text.lower(buf)); put_c(' ');
+ put_s(buf); put_c(' ');
+ put_n(Text.upper(buf)); put_c(' ');
+ put_s(buf); put_c(10);
+ Text.copy(buf, " \t padded line \r\n");
+ put_n(Text.trim(buf)); put_c(' ');
+ put_c('['); put_s(buf); put_c(']'); put_c(10);
+ put_n(Num.abs(0 - 42)); put_c(' ');
+ put_n(Num.min(3, 9)); put_c(' ');
+ put_n(Num.max(3, 9)); put_c(' ');
+ put_n(Num.clamp(50, 0, 10)); put_c(' ');
+ put_n(Num.gcd(0 - 84, 36)); put_c(' ');
+ put_n(Num.pow(3, 4)); put_c(' ');
+ put_n(Num.isqrt(99)); put_c(' ');
+ put_n(Num.isqrt(100)); put_c(' ');
+ put_n(Num.sign(0 - 5)); put_c(' ');
+ put_n(Num.sign(0)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/strings.zc b/tests/programs/strings.zc
new file mode 100644
index 0000000..431bd4e
--- /dev/null
+++ b/tests/programs/strings.zc
@@ -0,0 +1,37 @@
+fn main() {
+ let a = b"hi", b = b"!", c = a + b;
+ let mid = c[1:3];
+ let neg = s_to_n(b"-42");
+ let p = n_to_s(42);
+ let q = n_to_s(0 - 42);
+ let ten = s_to_n(b"10");
+ let big = n_to_s(1 << 63);
+ let empty = b"";
+ let tail = c[2:99];
+ let esc = b"a\nb";
+ let z = b"" + b"ab";
+ put_s(c); put_c(32);
+ put_n(len(c)); put_c(32);
+ put_n(len_s(c)); put_c(32);
+ put_n(c[0]); put_c(32);
+ put_s(mid); put_c(32);
+ put_n(c == b"hi!"); put_c(32);
+ put_n(c != b"hi"); put_c(32);
+ put_n(c < b"hj"); put_c(32);
+ put_n(neg); put_c(32);
+ put_s(p); put_c(32);
+ put_s(q); put_c(32);
+ put_n(ten + 1); put_c(32);
+ put_n(p == b"42"); put_c(32);
+ put_s(big); put_c(32);
+ put_n(len(empty)); put_c(32);
+ put_n(len(tail)); put_c(32);
+ put_n(len_s(esc)); put_c(32);
+ put_n(at_s(esc, 1)); put_c(32);
+ put_n(b"ab" > b"a"); put_c(32);
+ put_n(b"ab" >= b"ab"); put_c(32);
+ put_n(b"a" <= b"a"); put_c(32);
+ put_s("W"); put_c(32);
+ put_s(z); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/structret.zc b/tests/programs/structret.zc
new file mode 100644
index 0000000..708c83c
--- /dev/null
+++ b/tests/programs/structret.zc
@@ -0,0 +1,24 @@
+// structs returned from native functions: the stub makes the result (a new object,
+// owned by the caller) and the native call fills it in. the helper library is built
+// by test.sh (build/sbv.so).
+fn put_f(x) { asm { x @ #1220 } return 0; }
+struct Pair { int a; int b; }
+struct Vec { float x; float y; }
+struct Trio { int a; int b; int c; }
+struct Mixed { int a; float x; }
+struct Vec3 { float x; float y; float z; }
+extern struct Pair mkpair(long a, long b);
+extern struct Vec mkvec(double x, double y);
+extern struct Trio mktrio(long a, long b, long c);
+extern struct Mixed mkmixed(long a, double x);
+extern struct Vec3 mkv3(double x);
+int main() {
+ Pair* p; Vec* v; Trio* t; Mixed* m; Vec3* w;
+ cload(b"build/sbv.so");
+ p = mkpair(3, 4); put_n(p.a); put_c(32); put_n(p.b); put_c(32);
+ v = mkvec(2.5, 3.0); put_f(v.x); put_c(32); put_f(v.y); put_c(32);
+ t = mktrio(7, 8, 9); put_n(t.a + t.b + t.c); put_c(32);
+ m = mkmixed(7, 0.5); put_n(m.a); put_c(32); put_f(m.x); put_c(32);
+ w = mkv3(1.5); put_f(w.z); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/structval.zc b/tests/programs/structval.zc
new file mode 100644
index 0000000..a1bd041
--- /dev/null
+++ b/tests/programs/structval.zc
@@ -0,0 +1,26 @@
+// structs by value in native calls: a zc struct of word fields is passed to a c
+// function as its words. the helper library is built by test.sh (build/sbv.so).
+struct Pair { int a; int b; }
+struct Vec { float x; float y; }
+struct Trio { int a; int b; int c; }
+extern long pair_sum(struct Pair p);
+extern long pair_sum2(struct Pair p, struct Pair q);
+extern long vec_scaled(struct Vec v, long k);
+extern long many(long a, long b, long c, long d, long e, long f, struct Pair p);
+extern long trio_sum(long a, long b, long c, long d, long e, long f, struct Trio t);
+extern long eight(long a, long b, long c, long d, long e, long f, long g, long h, struct Pair p);
+int main() {
+ Pair p; Pair q; Vec v; Trio t;
+ cload(b"build/sbv.so");
+ p.a = 3; p.b = 4;
+ put_n(pair_sum(p)); put_c(32);
+ q.a = 10; q.b = 20;
+ put_n(pair_sum2(p, q)); put_c(32);
+ v.x = 1.5; v.y = 2.0;
+ put_n(vec_scaled(v, 2)); put_c(32);
+ put_n(many(1, 2, 3, 4, 5, 6, p)); put_c(32);
+ t.a = 7; t.b = 8; t.c = 9;
+ put_n(trio_sum(1, 2, 3, 4, 5, 6, t)); put_c(32);
+ put_n(eight(1, 2, 3, 4, 5, 6, 7, 8, p)); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/sys.zc b/tests/programs/sys.zc
new file mode 100644
index 0000000..52924fa
--- /dev/null
+++ b/tests/programs/sys.zc
@@ -0,0 +1,11 @@
+include "src/lib/std.zc";
+fn main() {
+ let s, n;
+ n = Host.writefile("/tmp/zc-sys-out.txt", "zy");
+ s = readfile("/tmp/zc-sys-out.txt");
+ put_n(n); put_c(' '); put_s(s); put_c(' ');
+ if (Host.now() > 1000000000) put_s("time\n");
+ else put_s("notime\n");
+ free(d*s);
+ return 0;
+}
diff --git a/tests/programs/tagenum.zc b/tests/programs/tagenum.zc
new file mode 100644
index 0000000..f438c5b
--- /dev/null
+++ b/tests/programs/tagenum.zc
@@ -0,0 +1,26 @@
+// tagged enums: a member can carry fields, a constructor builds it, and a
+// switch matches on the member and binds its fields
+enum shape { circle { int r; }, rect { int w; int h; }, dot };
+
+fn area(shape* s) {
+ switch (s) {
+ case circle(r): return r * r * 3;
+ case rect(w, h): return w * h;
+ case dot: return 0;
+ }
+ return -1;
+}
+
+fn main() {
+ shape* a = shape.circle(3);
+ shape* b = shape.rect(4, 5);
+ shape* c = shape.dot();
+ put_n(area(a)); put_c(32);
+ put_n(area(b)); put_c(32);
+ put_n(area(c)); put_c(32);
+ put_n(a.tag); put_c(32);
+ put_n(b.tag); put_c(32);
+ put_n(c.tag); put_c(10);
+ free(d*a); free(d*b); free(d*c);
+ return 0;
+}
diff --git a/tests/programs/tagenum_two.zc b/tests/programs/tagenum_two.zc
new file mode 100644
index 0000000..eef45dd
--- /dev/null
+++ b/tests/programs/tagenum_two.zc
@@ -0,0 +1,20 @@
+// two tagged enums may share a member name: each keeps its own fields and tag
+enum a { item { int x; }, other };
+enum b { item { int y; }, nope };
+o3 int tot(a* p, int n) {
+ int t = 0;
+ for (int i = 0; i < n; i++) {
+ switch (p) {
+ case item(x): t += x;
+ case other: t += 100;
+ }
+ }
+ return t;
+}
+int main() {
+ a* p = a.item(3);
+ b* q = b.item(8);
+ put_n(p.item_x); put_c(32); put_n(q.item_y); put_c(32); put_n(tot(p, 4)); put_c(10);
+ free(d*p); free(d*q);
+ return 0;
+}
diff --git a/tests/programs/tagenum_warn.zc b/tests/programs/tagenum_warn.zc
new file mode 100644
index 0000000..f50ab7c
--- /dev/null
+++ b/tests/programs/tagenum_warn.zc
@@ -0,0 +1,17 @@
+// a switch on a tagged enum that leaves out a member, with no default, warns
+enum shape { circle { int r; }, rect { int w; int h; }, dot };
+
+fn name(shape* s) {
+ switch (s) {
+ case circle(r): return 1;
+ case dot: return 2;
+ }
+ return 0;
+}
+
+fn main() {
+ shape* a = shape.rect(2, 2);
+ put_n(name(a)); put_c(10);
+ free(d*a);
+ return 0;
+}
diff --git a/tests/programs/typedmethods.zc b/tests/programs/typedmethods.zc
new file mode 100644
index 0000000..facd9f1
--- /dev/null
+++ b/tests/programs/typedmethods.zc
@@ -0,0 +1,16 @@
+// struct methods declare a return type and typed parameters, like functions do
+struct Str {
+ int len;
+ static int size(int s) { return s * 2; }
+ int plus(int k) { return this.len + k; }
+ static fn none() { return 5; }
+}
+int main() {
+ Str a;
+ a.len = 9;
+ put_n(Str.size(7)); put_c(32);
+ put_n(Str.none()); put_c(32);
+ put_n(a.plus(4));
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/typeoff.zc b/tests/programs/typeoff.zc
new file mode 100644
index 0000000..d82bca8
--- /dev/null
+++ b/tests/programs/typeoff.zc
@@ -0,0 +1,2 @@
+// the typechecker is off unless asked for: this mismatch compiles and runs
+fn main() { char* s = 5; put_n(s as int + 1); put_c(10); return 0; }
diff --git a/tests/programs/typeok.zc b/tests/programs/typeok.zc
new file mode 100644
index 0000000..26ab3ad
--- /dev/null
+++ b/tests/programs/typeok.zc
@@ -0,0 +1,21 @@
+// passes the typechecker: words mix with everything, literal 0 is null,
+// arrays decay to pointers, generic calls bind their parameters
+typecheck on;
+struct pair<A, B> { A a; B b; };
+pair<A, B>*.u mk<A, B>(A x, B y) { pair<A, B>* p = new(pair<A, B>); p.a = x; p.b = y; return u*p; }
+A left<A, B>(pair<A, B>* p) { return p.a; }
+int strlen2(char* s) { int n = 0; while (s[n]) n++; return n; }
+int main() {
+ pair<int, char*>* p = mk(3, "four");
+ int a = left(p);
+ char* s = p.b;
+ [8]char buf;
+ char* q = buf;
+ int* nothing = 0;
+ let w = 5;
+ int mixed = w + 1;
+ q[0] = 'h'; q[1] = 0;
+ put_n(a + strlen2(s) + strlen2(buf) + mixed); put_c(10);
+ free(d*p);
+ return 0;
+}
diff --git a/tests/programs/types.zc b/tests/programs/types.zc
new file mode 100644
index 0000000..86488db
--- /dev/null
+++ b/tests/programs/types.zc
@@ -0,0 +1,55 @@
+struct point { int x, y; };
+struct rect { point a, b; int tag; };
+struct list<T> { int len; T* items; };
+struct node { int v; node* next; };
+type num = int;
+
+int counter = 5;
+[4]int squares;
+point origin;
+
+int area(rect* r) { return (r.b.x - r.a.x) * (r.b.y - r.a.y); }
+list<T>*.u mklist<T>(int cap) {
+ list<T>* l = new(list<T>);
+ l.len = 0;
+ l.items = new(T, cap) as T*;
+ return u*l;
+}
+T first<T>(list<T>* l) { return l.items[0]; }
+int sum(node* n) { int s = 0; while (n) { s += n.v; n = n.next; } return s; }
+
+fn main() {
+ rect r;
+ r.a.x = 1; r.a.y = 2; r.b.x = 4; r.b.y = 6;
+ put_n(area(&r)); put_c(' ');
+ put_n(sizeof(rect)); put_c(' '); put_n(sizeof(point)); put_c(' '); put_n(sizeof([3]rect)); put_c(' ');
+ let p = new(point); // let infers *point
+ p.x = 7; p->y = 8;
+ put_n(p.x * p.y); put_c(' ');
+ free(d*p);
+ list<int>* l = mklist<int>(4);
+ l.items[0] = 42; l.len = 1;
+ put_n(first(l)); put_c(' ');
+ free(d*l);
+ node* a = new(node), b = new(node);
+ a.v = 3; b.v = 4; a.next = b;
+ put_n(sum(a)); put_c(' ');
+ free(d*a); // b went with it: a.next = b made a its owner
+ [3]point pts;
+ pts[2].y = 9;
+ put_n(pts[2].y + counter); put_c(' ');
+ num k = 3;
+ put_n(k); put_c(10);
+
+ // reflection
+ tprint(typeof(r)); put_c(' '); tprint(typeof(&r)); put_c(' '); tprint(typeof(l)); put_c(' ');
+ tprint(sigof(mklist)); put_c(' '); tprint(sigof(first)); put_c(' '); tprint(sigofs("free")); put_c(10);
+ let t = typeof(r), i = 0;
+ while (i < tnfields(t)) { put_s(tfname(t, i)); put_c(':'); tprint(tftype(t, i)); put_c('@'); put_n(tfoff(t, i)); put_c(' '); i++; }
+ put_c(10);
+ point* q = new(point);
+ tprint(typeof_obj(q)); put_c(' '); put_n(typeof_obj(q) == typeof(point)); put_c(' '); put_n(tkind(typeof_obj(q)) == TY_STRUCT);
+ free(d*q);
+ put_c(10);
+ return 0;
+}
diff --git a/tests/programs/uaf.zc b/tests/programs/uaf.zc
new file mode 100644
index 0000000..4c14e91
--- /dev/null
+++ b/tests/programs/uaf.zc
@@ -0,0 +1,8 @@
+memdebug on;
+unchecked fn main()
+{
+ let p = new(2);
+ p[0] = 42;
+ free(p);
+ return p[0];
+}
diff --git a/tests/programs/uaf_in.zc b/tests/programs/uaf_in.zc
new file mode 100644
index 0000000..89ad9f9
--- /dev/null
+++ b/tests/programs/uaf_in.zc
@@ -0,0 +1,9 @@
+memdebug on;
+unchecked fn main()
+{
+ let p = new(4), r;
+ p[2] = 5;
+ r = p + 2;
+ free(p);
+ return r[0];
+}
diff --git a/tests/programs/umd.zc b/tests/programs/umd.zc
new file mode 100644
index 0000000..fb1640e
--- /dev/null
+++ b/tests/programs/umd.zc
@@ -0,0 +1,50 @@
+// lists as [len, items...], items owned by the list
+.u mklist(n) {
+ let l = new(n + 1);
+ l[0] = 0;
+ return u*l;
+}
+fn push(.m l, .d item) { l[l[0] + 1] = d*item; l[0] = l[0] + 1; return 0; }
+fn sum(.m l) { let s = 0, i = 1; while (i <= l[0]) { s += l[i][0]; i++; } return s; }
+.u box(v) { let b = new(1); b[0] = v; return u*b; }
+fn consume(.d b) { return b[0]; }
+
+fn main() {
+ let base = live_objects();
+ {
+ let l = mklist(8); // u from mklist arrives m*here
+ let i = 0;
+ while (i < 5) {
+ let b = box(i * 10); // m in the loop body
+ d*b; // give it down
+ push(l, b);
+ i++;
+ }
+ put_n(sum(l)); put_c(' ');
+ put_n(live_objects() - base); put_c(' '); // list + 5 boxes
+ }
+ put_n(live_objects() - base); put_c(' '); // freed at the block's end, children too
+
+ // u handed up one block by assignment, and as a block value
+ let outer;
+ {
+ let t = box(7);
+ u*t;
+ outer = t;
+ }
+ let v = { let w = box(8); u*w };
+ put_n(outer[0] + v[0]); put_c(' ');
+
+ // d given away to a callee
+ let g = box(99);
+ put_n(consume(d*g)); put_c(' ');
+
+ // containers: child takes its parent's tag
+ let p = new(2), c = new(1);
+ c[0] = 5;
+ p[0] = c;
+ p[1] = new(3);
+ put_n(p[0][0]); put_c(' ');
+ put_n(live_objects() - base); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/undefined_call.zc b/tests/programs/undefined_call.zc
new file mode 100644
index 0000000..e6a3b48
--- /dev/null
+++ b/tests/programs/undefined_call.zc
@@ -0,0 +1 @@
+fn main() { nope(); return 0; }
diff --git a/tests/programs/use_std.zc b/tests/programs/use_std.zc
new file mode 100644
index 0000000..c15e49b
--- /dev/null
+++ b/tests/programs/use_std.zc
@@ -0,0 +1,9 @@
+include "src/lib/std.zc";
+fn main() {
+ let out[12];
+ Text.itoa(42, out);
+ put_s(out); put_c(' ');
+ put_n(Text.atoi("-7")); put_c(' ');
+ put_n(Text.starts("hello", "he")); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/wide.zc b/tests/programs/wide.zc
new file mode 100644
index 0000000..c20004b
--- /dev/null
+++ b/tests/programs/wide.zc
@@ -0,0 +1,9 @@
+// a word is 64 bits: shifts past 31, and equality uses bit 63
+fn main() {
+ put_n(1 << 40); put_c(' ');
+ put_n((1 << 40) >> 40); put_c(' ');
+ put_n((1 << 40) == (1 << 40)); put_c(' ');
+ put_n((1 << 40) != 0); put_c(' ');
+ put_n(~0 >> 63); put_c(10);
+ return 0;
+}
diff --git a/tests/programs/zc0plain.zc b/tests/programs/zc0plain.zc
new file mode 100644
index 0000000..1092f76
--- /dev/null
+++ b/tests/programs/zc0plain.zc
@@ -0,0 +1,23 @@
+// the stage 0 compiler (src/tools/zc0.c) has no types. it reads them and throws
+// them away, so a typed program and the same program without the types come
+// out as the same image. tests/programs/zc0typed.zc is this file with the types on.
+let total;
+let table[8];
+let names;
+const LIMIT = 10;
+
+fn add(a, b) { return a + b; }
+.u grow(v, n) { return v; }
+fn keep(.d x, .m y) { return x != y; }
+fn apply(f, p, q) { return f(*p); }
+fn sq(x) { return x * x; }
+
+fn start() {
+ let i = 0, sum = 0;
+ let a[4];
+ let p = a;
+ for (let j = 0; j < LIMIT; j++) sum = add(sum, sq(j));
+ while (i < 4) { a[i] = i; i++; }
+ total = sum + a[3] + table[2];
+ return total;
+}
diff --git a/tests/programs/zc0typed.zc b/tests/programs/zc0typed.zc
new file mode 100644
index 0000000..4b7f438
--- /dev/null
+++ b/tests/programs/zc0typed.zc
@@ -0,0 +1,23 @@
+// the stage 0 compiler (src/tools/zc0.c) has no types. it reads them and throws
+// them away, so a typed program and the same program without the types come
+// out as the same image. tests/programs/zc0plain.zc is this file with the types off.
+int total;
+[8]int table;
+[]word names;
+const int LIMIT = 10;
+
+int add(int a, int b) { return a + b; }
+.u grow([]word v, int n) { return v; }
+bool keep(int.d x, .m y) { return x != y; }
+int apply(int(int) f, int* p, vec<vec<int>>*.u q) { return f(*p); }
+int sq(int x) { return x * x; }
+
+fn start() {
+ int i = 0, sum = 0;
+ [4]int a;
+ int* p = a;
+ for (int j = 0; j < LIMIT; j++) sum = add(sum, sq(j));
+ while (i < 4) { a[i] = i; i++; }
+ total = sum + a[3] + table[2];
+ return total;
+}
diff --git a/tests/runerr.sh b/tests/runerr.sh
new file mode 100755
index 0000000..23ea054
--- /dev/null
+++ b/tests/runerr.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+# runs every .zc in a folder (tests/errors by default) and checks the compiler
+# rejects it with the message on its first line: // expect: ...
+vm=${1:-zy}; dir=${2:-$(dirname "$0")/errors}; pass=0; fail=0
+for f in "$dir"/*.zc; do
+ want=$(head -1 "$f" | sed 's#^// expect: ##')
+ got=$($vm < "$f" 2>&1 | head -1)
+ if [[ "$got" == *"$want"* ]]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL $(basename $f): got [$got]"; fi
+done
+echo "rejected: $pass passed, $fail failed"
diff --git a/tests/test.sh b/tests/test.sh
new file mode 100755
index 0000000..27d60f7
--- /dev/null
+++ b/tests/test.sh
@@ -0,0 +1,577 @@
+#!/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
+# 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.
+#
+# -j N runs N sections at a time, where N is how many cores to use. the
+# sections are independent and each gets its own scratch directory, so they
+# can overlap; the output still reads in the same order either way. -j 1 is
+# the default, and is what make test does unless JOBS says otherwise.
+#
+# -q (make check) is the minimal suite for iterating: the bootstrap and the
+# fixpoint, the interpreter section and the language server's own checks.
+# it skips the other backends, the node protocol and corpus tests, and the
+# js and make sections. run the whole thing (no -q) when the work is done.
+cd "$(dirname "$0")/.."; T=$(mktemp -d); trap 'rm -rf $T' EXIT
+ok=0; bad=0
+chk(){ if [ "$1" = "$2" ]; then echo "ok $3"; ok=$((ok+1)); else echo "FAIL $3: got [$1]"; bad=$((bad+1)); fi; }
+src(){ cat src/runtime/rt.zc src/compiler/compiler.zc; }
+# -j N runs N sections at a time: j is how many cores to use. each section
+# writes its own log, so the output still reads in the order it always did.
+JOBS=1; QUICK=0; SELECT=""
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -j) JOBS=$2; shift 2;;
+ -j*) JOBS=${1#-j}; shift;;
+ -q) QUICK=1; shift;;
+ --section) SELECT=$2; shift 2;;
+ *) echo "usage: $0 [-q] [-j N] [--section NAME]"; exit 2;;
+ esac
+done
+case "$JOBS" in ''|*[!0-9]*|0) JOBS=1;; esac
+# each section gets its own scratch directory, so none of them can tread on
+# another's files when they overlap
+SECS="interp micro small runonly bare jit llvm lsp js make learn"
+[ $QUICK = 1 ] && SECS="interp lsp learn"
+if [ -n "$SELECT" ]; then
+ case " $SECS " in *" $SELECT "*) SECS=$SELECT;; *) echo "unknown test section: $SELECT" >&2; exit 2;; esac
+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 -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -o $T/zy || exit 1
+gcc -O2 -w -fwrapv src/tools/genmap.c -Isrc/core -o $T/genmap || 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
+
+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 -o $T/zc0 && gcc -O1 -w -fwrapv src/tools/mkgenome.c -Isrc/core -o $T/mkg || exit 1
+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 -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 -O2 -w -fwrapv -DJTYPE=int $T/s1/core.c $T/s1/io_linux.c -o $T/zy1
+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 src/core/genome.h && chk 1 1 "genome.h is up to date" || chk 0 1 "genome.h is stale (cp the bootstrap output over it)"
+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
+ mkdir -p $ROOT/$n
+ cp $ROOT/g1.bin $ROOT/$n/ # the bootstrap's image, read by micro, jit and llvm
+ cp $ROOT/zy $ROOT/$n/zy # the interpreter, built once above
+ cp $ROOT/genmap $ROOT/$n/genmap
+done
+
+run(){ # $1 = vm command, $2 = label
+ local vm="$1" l="$2"
+ chk "$($vm < examples/hello.zc)" "hello, world!" "$l hello"
+ chk "$($vm < examples/fact.zc | tr '\n' '/')" "1101110101111100000000/3628800/" "$l factorial"
+ chk "$( (cat examples/echo.zc; printf '`abc\n') | $vm)" "abc" "$l echo after the backtick"
+ 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 "$($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"
+ chk "$($vm < tests/programs/basics.zc | tr '\n' '/')" "285 610 42 512/hey 65 -3 -2 1024 -8/12456/757/9 100-101/5/ok/" "$l language basics"
+ chk "$($vm < tests/programs/umd.zc)" "100 6 0 15 99 5 5" "$l u m d lifetimes"
+ chk "$($vm < tests/programs/names.zc)" "12 1 8" "$l u, m and d are ordinary names"
+ chk "$($vm < tests/programs/gc.zc)" "1000 1 900 1 199900" "$l mark-sweep collector"
+ chk "$($vm < tests/programs/meta.zc | tr '\n' '/')" "1024 19/unless ok/rrr/15 42/7/42/7/144/" "$l parse words, quote, eval, load, operator tables"
+ chk "$($vm < tests/programs/misc.zc | tr '\n' '/')" "45/54321/ok/" "$l compile-time eval, patched mnemonics, unchecked"
+ chk "$($vm < tests/programs/types.zc | tr '\n' '/')" "12 5 2 15 56 42 7 14 3/rect rect* list<int>* list<T>*.u(int) T(list<T>*) fn(word.d)/a:point@0 b:point@2 tag:int@4 /point 1 1/" "$l types, generics, reflection"
+ chk "$($vm < tests/programs/typeok.zc)" "14" "$l a program the typechecker accepts"
+ chk "$($vm < tests/programs/typeoff.zc)" "6" "$l the typechecker is off by default"
+ chk "$($vm < tests/programs/enhance.zc)" "100 0 343 3,2 5 -2 0" "$l virtual enhancements"
+ chk "$($vm < tests/programs/joins.zc)" "0 0 5 80 8 3 9 0" "$l joins free what one path gave away; switch, ?:, no prototypes"
+ chk "$($vm < tests/programs/buddy.zc)" "131072 262144 1 0" "$l buddy allocator merges freed blocks"
+ chk "$($vm < tests/programs/growheap.zc)" "144 1 7 1" "$l the heap grows past its initial size and a bigger block fits after the frees"
+ chk "$($vm < tests/programs/sized.zc | md5sum)" "$(md5sum < tests/programs/sized.expected)" "$l sized integers wrap, and arrays and pointers of them are packed"
+ chk "$($vm < tests/programs/bitops.zc | md5sum)" "$(md5sum < tests/programs/bitops.expected)" "$l ctz, popcnt, bswap, rotates, mulh and float32 conversion micro-ops"
+ chk "$($vm < tests/programs/f32.zc | md5sum)" "$(md5sum < tests/programs/f32.expected)" "$l f32: single precision scalars and packed arrays"
+ chk "$($vm < tests/programs/f32o3.zc)" "10718.750000 1.600000 0" "$l f32 inside an o3 region"
+ chk "$($vm < tests/programs/sizedvec.zc | md5sum)" "$(md5sum < tests/programs/sizedvec.expected)" "$l growable arrays of the sized types and f32"
+ chk "$($vm < tests/programs/packedfields.zc | md5sum)" "$(md5sum < tests/programs/packedfields.expected)" "$l struct fields of the sized types pack into words"
+ chk "$($vm < tests/programs/generics.zc | md5sum)" "$(md5sum < tests/programs/generics.expected)" "$l generic functions are compiled per type arguments"
+ chk "$($vm < tests/programs/genmeth.zc | md5sum)" "$(md5sum < tests/programs/genmeth.expected)" "$l methods of generic structs are compiled per type arguments"
+ chk "$($vm < tests/programs/static.zc)" "14 5 9" "$l static functions of a struct are called as Struct.name(args)"
+ chk "$($vm < tests/programs/methodown.zc)" "7 9" "$l struct methods and functions return owned objects (.u)"
+ chk "$($vm < tests/programs/typedmethods.zc)" "14 5 13" "$l struct methods declare a return type and typed parameters"
+ 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_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"
+ chk "$($vm < tests/programs/borrowmove.zc)" "6" "$l storing into a borrowed object moves it"
+ chk "$($vm < tests/programs/alias.zc)" "11 22 4 8" "$l aliases of a live owner: arithmetic, field, index"
+ chk "$($vm < tests/programs/quarantine.zc)" "0 1234" "$l quarantine: stale alias reads intact data, second free is a no-op"
+ chk "$($vm < tests/programs/recycle.zc)" "1 0 1" "$l recycle returns a dropped block to its size class"
+ chk "$($vm < tests/programs/roots.zc)" "1 0" "$l the collector marks the live stack, and scrub clears the rest"
+ chk "$($vm < tests/programs/arena.zc)" "4 7 0 1 1" "$l bump arena allocates, rewinds, and refuses a bump past the end"
+ chk "$($vm < tests/programs/csyn.zc)" "13 0 5 6 0" "$l do/while, enum, and a C cast"
+ chk "$($vm < tests/programs/use_std.zc)" "42 -7 1" "$l include and the string helpers"
+ chk "$($vm < tests/programs/stdbuf.zc)" "4 1 266 8" "$l stdlib arena and packed byte buffer"
+ chk "$($vm < tests/programs/stdmore.zc)" "$(printf '5 1 0 0 7 copy me 12 -1 0\n9 mixed 123 9 MIXED 123\n11 [padded line]\n42 3 9 10 12 81 9 10 -1 0')" "$l stdlib text and num helpers"
+ chk "$(ZC_FILE=tests/data/hi.txt $vm < examples/cli.zc)" "6 h" "$l reading a file named by the environment"
+ chk "$(ZC_ARG_TEST=ok $vm < tests/programs/args.zc)" "1 <eval> <eval> 1 1" "$l argv and envp from the host, argv[0] is <eval> on stdin"
+ chk "$(ZC_ARG_TEST=ok $vm < tests/programs/capture.zc)" "0
+1
+15
+hello world!42
+1
+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/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"
+ chk "$($vm < tests/programs/ftruth.zc)" "00111000110107" "$l negative zero is false"
+ chk "$($vm < tests/programs/ftype.zc)" "3.500000 10.500000 3.500000 4.250000 5.250000 4.250000 6.500000 2 4 4.250000 8.500000 1 4" "$l float type through calls, globals, fields"
+ chk "$($vm < tests/programs/fo3.zc)" "5997 5997 4.000000 nan 0 3.250000 1 0 0" "$l o3 lifts float ops"
+ chk "$($vm < tests/programs/ftext.zc)" "1e+308 1e-10 -0 inf -inf nan 0.1 1.50 1.250e+0 100000000000000000000.000000 1e+40 1111111111" "$l float text: shortest, fixed, scientific, 17 digits"
+ chk "$($vm < tests/programs/bytes.zc)" "17 34 43981 16909060 1 205 171 -3674937295934316015 1108152156331 0 0 30768
+ABC" "$l byte addressing"
+ chk "$($vm < tests/programs/o3bytes.zc)" "190 4660 7 2055 16909060 0 0
+ABC" "$l o3 lifts byte loads and stores"
+ chk "$($vm < tests/programs/o3zero.zc)" "1" "$l untyped new inside o3 zeroes its words"
+ chk "$($vm < tests/programs/strings.zc)" "hi! 3 3 104 i! 1 1 1 -42 42 -42 11 1 -9223372036854775808 0 1 3 10 1 1 1 W ab" "$l byte strings"
+ chk "$($vm < tests/programs/share.zc)" "1 2 1 4 1 1 1 1 42 1 1 1 6 0" "$l shared cons cells"
+ chk "$($vm < tests/programs/share_dbg.zc)" "7 1" "$l memdebug accepts a shared cell"
+ chk "$($vm < examples/strings.zc)" "hello, zc
+9 42" "$l byte string example"
+ chk "$(ZC_BSTR=zybyte $vm < tests/programs/bstr_io.zc)" "byte-io 7 7 1 6 122 101" "$l byte-string file and getenv"
+ chk "$($vm < tests/programs/manyerrors.zc | tr '\n' '/')" "zc: line 3: expected an expression, found ';'/zc: line 4: x: m object is never used/zc: line 5: type error: initializer: expected char*, got int/zc: line 6: expected ';', found '}'/zc: 4 errors/" "$l several errors in one run"
+ chk "$($vm < tests/programs/overflow_return.zc 2>&1)" "zy: return stack overflow" "$l return stack overflow is caught"
+ chk "$($vm < tests/programs/overflow_data.zc 2>&1)" "zy: data stack overflow" "$l data stack overflow is caught"
+ chk "$($vm < tests/programs/overflow_frame.zc 2>&1)" "zc: out of heap memory" "$l frame growth stops at a configured memory limit"
+ chk "$($vm < examples/mandel.zc 2>&1 | md5sum | cut -c1-32)" "f41aecbf2c844168050413cfa832db0e" "$l example: mandelbrot with a fixed-point enhancement"
+ 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/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"
+ chk "$($vm < examples/life.zc 2>&1 | md5sum | cut -c1-32)" "aefb036b6f9ad1dadfc3e6bb47a6885d" "$l example: game of life"
+ chk "$($vm < tests/programs/arrays.zc | tr '\n' '/')" "100 128 9801 9801 99 3 4 12 19 8 19 28/0/" "$l growable and fixed arrays"
+ chk "$($vm < tests/programs/o3.zc)" "3142 42 43 9 57 98781 520625 10192 -18782 1152" "$l o3 regions: loops, calls, recursion, switch, arrays, more vars than registers"
+ chk "$($vm < tests/programs/o3scopes.zc)" "116" "$l o3 registers shared by scopes, cached fixed arrays"
+ chk "$($vm < tests/programs/o3fat.zc)" "node node 3 box node [0]int" "$l o3 data gets fattened on the way out"
+ chk "$($vm < tests/programs/o3call.zc)" "0 0 4 23 55 node" "$l o3 calls o3 directly, and new stamps the header"
+ chk "$($vm < tests/programs/o3direct.zc | md5sum)" "$(md5sum < tests/programs/o3direct.expected)" "$l o3 calls: arguments, results, recursion, floats, pointers, the ways out of a fast call"
+ chk "$($vm < tests/programs/o3licm.zc)" "3184200 7950 315 50 260 -56" "$l o3 hoists loop invariants and shares values between blocks"
+ chk "$($vm < tests/programs/o3deep.zc)" "30000" "$l o3 recursion thirty thousand deep"
+ chk "$($vm < tests/programs/o3deep_over.zc 2>&1)" "100000" "$l o3 recursion grows past the original stack"
+ chk "$($vm < tests/programs/o3_bounds.zc 2>&1)" "zy: index 7 out of range (length 5)" "$l o3 bounds check"
+ chk "$($vm < tests/programs/bounds.zc)" "55 55 8" "$l a proved +1 loop skips the bounds check"
+ chk "$($vm < tests/programs/bounds_lo.zc 2>&1)" "zy: index -1 out of range (length 4)" "$l a negative loop index is still checked"
+ chk "$($vm < tests/programs/bounds_hi.zc 2>&1)" "zy: index 4 out of range (length 4)" "$l an index that is not the induction variable is still checked"
+ local r; r=$(bash tests/runerr.sh "$vm" tests/errors | tail -1)
+ chk "$r" "rejected: $(ls tests/errors/*.zc | wc -l | tr -d ' ') passed, 0 failed" "$l analyzer rejects every bad program"
+ r=$(bash tests/runerr.sh "$vm" tests/typeerrors | tail -1)
+ chk "$r" "rejected: $(ls tests/typeerrors/*.zc | wc -l | tr -d ' ') passed, 0 failed" "$l typechecker rejects every bad program"
+ r=$(bash tests/runerr.sh "$vm" tests/langerrors | tail -1)
+ chk "$r" "rejected: $(ls tests/langerrors/*.zc | wc -l | tr -d ' ') passed, 0 failed" "$l o3 and array misuse is rejected"
+}
+s_interp() {
+ T=$ROOT/interp
+ gcc -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -o $T/zy
+ run $T/zy "interp"
+ chk "$(ZC_ARG_TEST=ok $T/zy tests/programs/args.zc alpha beta < tests/programs/args.zc)" "4 <eval> beta 1 1" "interp the command line reaches argv, after <eval>"
+ chk "$($T/zy < tests/programs/cabi.zc | md5sum)" "$(md5sum < tests/programs/cabi.expected)" "interp native calls and callbacks (extern, cfunc)"
+ chk "$($T/zy < tests/programs/cabi_o3.zc | md5sum)" "$(md5sum < tests/programs/cabi_o3.expected)" "interp native calls from o3 code, more arguments than registers"
+ chk "$($T/zy < tests/programs/cabi_sized.zc | md5sum)" "$(md5sum < tests/programs/cabi_sized.expected)" "interp native calls with f32 and the sized integers"
+ chk "$($T/zy < tests/programs/structval.zc)" "34 3520 18 325 810 340" "interp structs by value to a c function (registers, sse, stack)"
+ chk "$($T/zy < tests/programs/structret.zc)" "3 4 3.500000 4.000000 24 8 1.000000 3.500000" "interp structs returned from a c function (registers, sse, memory)"
+ chk "$(ZYTRACE=1 $T/zy < tests/programs/o3.zc 2>&1 | grep -c 'micro-ops$')" "8" "every o3 region lifts"
+ 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 0 109 30 7 16 33 20 2 " "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"
+ 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 -O2 -w -fwrapv src/micro/micro.c src/micro/port_linux.c -Isrc/core -Isrc/micro -o $T/zym || exit 1
+ run $T/zym "micro"
+ 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 -O2 -w -fwrapv -DJTYPE=int -DZY_SOFTFLOAT -DZYINTERP src/core/core.c src/core/io_linux.c -Isrc/core -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"
+ chk "$($T/zysoft < tests/programs/ftext.zc)" "1e+308 1e-10 -0 inf -inf nan 0.1 1.50 1.250e+0 100000000000000000000.000000 1e+40 1111111111" "soft-float text matches"
+}
+
+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 -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 \
+ --genome 245760 --frame 2048 --return 512 --mark 512 --data 3072 \
+ --snap-cap 1000 --quote-cap 300 -o $T/small-flags || exit 1
+ 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 -O2 -w -fwrapv $T/small/micro.c $T/small/port_linux.c -o $T/zyms || exit 1
+ gcc -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"
+ sameout() { # $1 = other vm, $2 = label, $3 = source file
+ local a b
+ a=$($T/zy < "$3" 2>&1 | tr '\n' '/')
+ b=$($1 < "$3" 2>&1 | tr '\n' '/')
+ chk "$b" "$a" "$2"
+ }
+ for f in examples/hello.zc examples/fact.zc examples/lists.zc examples/typed.zc examples/wiring.zc examples/life.zc \
+ tests/programs/basics.zc tests/programs/wide.zc tests/programs/types.zc tests/programs/alias.zc tests/programs/meta.zc tests/programs/arrays.zc \
+ tests/programs/joins.zc tests/programs/enhance.zc tests/programs/o3.zc tests/programs/csyn.zc tests/programs/umd.zc tests/programs/quarantine.zc tests/programs/roots.zc \
+ tests/programs/float.zc tests/programs/ftruth.zc tests/programs/ftype.zc tests/programs/fo3.zc tests/programs/ftext.zc tests/programs/bytes.zc \
+ tests/programs/strings.zc examples/strings.zc tests/programs/share.zc tests/programs/share_dbg.zc; do
+ sameout $T/zyms "small $(basename "$f" .zc)" "$f"
+ sameout $T/zys "small core $(basename "$f" .zc)" "$f"
+ done
+
+ # code literals can look like pointers on a compact map. test collection and
+ # the retained root without assuming the default map's exact reclaim count.
+ for vm in $T/zyms $T/zys; do
+ out=$(sed 's/f >= 990/f > 0/' tests/programs/gc.zc | $vm | tr '\n' '/')
+ chk "$out" "1000 1 900 1 199900/" "small collector makes progress and preserves its root"
+ done
+
+ $T/genmap --preset run -o $T/run || exit 1
+ $T/genmap --mode run --words 20480 -o $T/run-flags || exit 1
+ cmp -s $T/run/memmap.h $T/run-flags/memmap.h && cmp -s $T/run/rt.zc $T/run-flags/rt.zc \
+ && cmp -s $T/run/map.txt $T/run-flags/map.txt \
+ && chk 1 1 "run flags reproduce the growing run-only map" || chk 0 1 "run flags reproduce the growing run-only map"
+}
+
+s_runonly() {
+ T=$ROOT/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
+ 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 -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"
+ }
+ runonly hello examples/hello.zc
+ runonly fact examples/fact.zc
+ runonly wide tests/programs/wide.zc
+ runonly selfpatch examples/selfpatch.zc
+ runonly gc tests/programs/gc.zc
+ runonly float tests/programs/float.zc
+ runonly ftruth tests/programs/ftruth.zc
+ runonly ftext tests/programs/ftext.zc
+ runonly bytes tests/programs/bytes.zc
+ 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 -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
+ 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"
+ if [ "$(uname -s)" = Linux ]; then chk "$(file -b $T/zcc-hello | grep -c 'statically linked')" "1" "zcc: the binary is statically linked"; else echo "skip zcc static linkage (unavailable on this host)"; fi
+ else chk 0 1 "zcc builds examples/hello.zc"; fi
+ 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"
+}
+
+s_bare() {
+ T=$ROOT/bare
+ bare() { # $1 target, $2 run|compile, $3 source, $4 label. compare UART stdout to the hosted interpreter.
+ local a b
+ a=$($T/zy < "$3" 2>&1 | tr '\n' '/')
+ b=$(bash src/baremetal/run.sh "$1" "$2" "$3" 2>/dev/null | tr '\n' '/')
+ chk "$b" "$a" "$4"
+ }
+ bareio() { # clock, RAM file, random, BOARD, MW, a long echo, and exit status
+ local mw
+ mw=$(GENMAP=$T/genmap bash src/tools/automap.sh run $T/run-bareio src/runtime/run.zc tests/programs/bareio.zc | sed -n 's/.*MW \([0-9]*\) .*/\1/p')
+ local t="$1" pay a b out st
+ pay=abcdefghijklmnopqrstuvwxyz0123456789
+ a=$( (cat examples/echo.zc; printf '`%s' "$pay") | $T/zy | tr -d '\n')
+ b=$(printf '%s' "$pay" | bash src/baremetal/run.sh "$t" io examples/echo.zc 2>/dev/null | tr -d '\n')
+ chk "$b" "$a" "bare $t echo until eof"
+ b=$(bash src/baremetal/run.sh "$t" run tests/programs/bareio.zc 2>/dev/null | tr -d '\n')
+ chk "$b" "tick 2 zy rand board $mw" "bare $t clock, files, random, memory"
+ out=$(bash src/baremetal/run.sh "$t" run tests/programs/bareexit.zc 2>/dev/null)
+ st=$?
+ chk "$(printf '%s' "$out" | tr -d '\n')" "bye" "bare $t exit status text"
+ chk "$st" "3" "bare $t exit status code"
+ }
+ if bash src/baremetal/run.sh ready riscv; then
+ bare riscv run examples/hello.zc "bare riscv hello"
+ bare riscv run examples/fact.zc "bare riscv fact"
+ bare riscv run tests/programs/wide.zc "bare riscv wide"
+ bare riscv run tests/programs/gc.zc "bare riscv gc"
+ bare riscv run tests/programs/float.zc "bare riscv float"
+ bare riscv run tests/programs/ftruth.zc "bare riscv negative zero"
+ bare riscv run tests/programs/ftext.zc "bare riscv float text"
+ bare riscv run tests/programs/bytes.zc "bare riscv bytes"
+ bare riscv run tests/programs/strings.zc "bare riscv strings"
+ bare riscv run tests/programs/share.zc "bare riscv share"
+ bare riscv compile tests/programs/float.zc "bare riscv compile float"
+ bare riscv compile tests/programs/ftruth.zc "bare riscv compile negative zero"
+ bare riscv compile tests/programs/ftype.zc "bare riscv compile float type"
+ bare riscv compile tests/programs/fo3.zc "bare riscv compile o3 floats"
+ bare riscv compile tests/programs/ftext.zc "bare riscv compile float text"
+ bare riscv compile tests/programs/bytes.zc "bare riscv compile bytes"
+ bare riscv compile tests/programs/strings.zc "bare riscv compile strings"
+ bare riscv compile tests/programs/share.zc "bare riscv compile share"
+ bare riscv compile examples/hello.zc "bare riscv compile hello"
+ bare riscv compile examples/fact.zc "bare riscv compile fact"
+ bare riscv compile tests/programs/wide.zc "bare riscv compile wide"
+ bareio riscv
+ else echo "skip baremetal riscv (no riscv64-linux-gnu-gcc or qemu-system-riscv64)"; fi
+ if bash src/baremetal/run.sh ready aarch64; then
+ bare aarch64 run examples/hello.zc "bare aarch64 hello"
+ bare aarch64 run examples/fact.zc "bare aarch64 fact"
+ bare aarch64 run tests/programs/wide.zc "bare aarch64 wide"
+ bare aarch64 run tests/programs/gc.zc "bare aarch64 gc"
+ bare aarch64 run tests/programs/float.zc "bare aarch64 float"
+ bare aarch64 run tests/programs/ftruth.zc "bare aarch64 negative zero"
+ bare aarch64 run tests/programs/ftext.zc "bare aarch64 float text"
+ bare aarch64 run tests/programs/bytes.zc "bare aarch64 bytes"
+ bare aarch64 run tests/programs/strings.zc "bare aarch64 strings"
+ bare aarch64 run tests/programs/share.zc "bare aarch64 share"
+ bare aarch64 compile tests/programs/float.zc "bare aarch64 compile float"
+ bare aarch64 compile tests/programs/ftruth.zc "bare aarch64 compile negative zero"
+ bare aarch64 compile tests/programs/ftype.zc "bare aarch64 compile float type"
+ bare aarch64 compile tests/programs/fo3.zc "bare aarch64 compile o3 floats"
+ bare aarch64 compile tests/programs/ftext.zc "bare aarch64 compile float text"
+ bare aarch64 compile tests/programs/bytes.zc "bare aarch64 compile bytes"
+ bare aarch64 compile tests/programs/strings.zc "bare aarch64 compile strings"
+ bare aarch64 compile tests/programs/share.zc "bare aarch64 compile share"
+ bare aarch64 compile examples/hello.zc "bare aarch64 compile hello"
+ bare aarch64 compile examples/fact.zc "bare aarch64 compile fact"
+ bare aarch64 compile tests/programs/wide.zc "bare aarch64 compile wide"
+ bareio aarch64
+ else echo "skip baremetal aarch64 (no aarch64-linux-gnu-gcc or qemu-system-aarch64)"; fi
+}
+
+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 -O2 -w -fwrapv -DJTYPE=int -static src/core/core.c src/core/io_linux.c -Isrc/core -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 -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -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"
+ # structs by value: the helper library has to be arm64, and cload reads build/sbv.so from the working directory
+ R=$(pwd); aarch64-linux-gnu-gcc -shared -fPIC -w tests/data/sbv.c -o $T/sbv_arm.so && mkdir -p $T/build && cp $T/sbv_arm.so $T/build/sbv.so
+ chk "$(cd $T && qemu-aarch64 -L /usr/aarch64-linux-gnu $T/zyjd < $R/tests/programs/structval.zc)" "34 3520 18 325 810 340" "jit structs by value to a c function (aapcs64: registers, hfa, reference, stack)"
+ chk "$(cd $T && qemu-aarch64 -L /usr/aarch64-linux-gnu $T/zyjd < $R/tests/programs/structret.zc)" "3 4 3.500000 4.000000 24 8 1.000000 3.500000" "jit structs returned from a c function (aapcs64 results)"
+ fi
+ else echo "skip jit (no aarch64 toolchain or qemu)"; fi
+}
+
+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 -O2 -w -fwrapv -DJTYPE=int -DZYLLVM $(llvm-config --cflags) src/core/core.c src/core/io_linux.c -Isrc/core -o $T/zyl $(llvm-config --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"
+ printf '#!/bin/sh\nZYHOT=1 exec %s "$@"\n' $T/zyl > $T/llall; chmod +x $T/llall
+ chk "$($T/zyl < tests/programs/structval.zc)" "34 3520 18 325 810 340" "llvm structs by value (the call goes through the interpreter's native call)"
+ chk "$($T/zyl < tests/programs/structret.zc)" "3 4 3.500000 4.000000 24 8 1.000000 3.500000" "llvm structs returned from a c function"
+ chk "$($T/llall < tests/programs/types.zc | md5sum)" "$($T/zy < tests/programs/types.zc | md5sum)" "llvm compiling every block agrees"
+ chk "$($T/llall < 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" "llvm compiling every block agrees on floats"
+ chk "$($T/llall < tests/programs/ftruth.zc)" "00111000110107" "llvm compiling every block agrees on negative zero"
+ chk "$($T/llall < tests/programs/ftype.zc)" "3.500000 10.500000 3.500000 4.250000 5.250000 4.250000 6.500000 2 4 4.250000 8.500000 1 4" "llvm compiling every block agrees on the float type"
+ chk "$($T/llall < tests/programs/fo3.zc)" "5997 5997 4.000000 nan 0 3.250000 1 0 0" "llvm compiling every block agrees on o3 floats"
+ chk "$($T/llall < tests/programs/ftext.zc)" "1e+308 1e-10 -0 inf -inf nan 0.1 1.50 1.250e+0 100000000000000000000.000000 1e+40 1111111111" "llvm compiling every block agrees on float text"
+ chk "$($T/llall < tests/programs/bytes.zc)" "17 34 43981 16909060 1 205 171 -3674937295934316015 1108152156331 0 0 30768
+ABC" "llvm compiling every block agrees on bytes"
+ chk "$($T/llall < tests/programs/strings.zc)" "hi! 3 3 104 i! 1 1 1 -42 42 -42 11 1 -9223372036854775808 0 1 3 10 1 1 1 W ab" "llvm compiling every block agrees on strings"
+ chk "$($T/llall < tests/programs/share.zc)" "1 2 1 4 1 1 1 1 42 1 1 1 6 0" "llvm compiling every block agrees on shared cells"
+ chk "$($T/zyl < tests/programs/cabi.zc | md5sum)" "$(md5sum < tests/programs/cabi.expected)" "llvm native calls and callbacks (extern, cfunc)"
+ chk "$($T/llall < tests/programs/cabi.zc | md5sum)" "$(md5sum < tests/programs/cabi.expected)" "llvm compiling every block agrees on native calls"
+ chk "$($T/zyl < tests/programs/cabi_o3.zc | md5sum)" "$(md5sum < tests/programs/cabi_o3.expected)" "llvm calls natives from o3 regions directly"
+ chk "$(ZYTRACE=1 $T/zyl < tests/programs/cabi_o3.zc 2>&1 | grep -c 'compiled by llvm')" "2" "llvm compiles the o3 regions that call natives"
+ chk "$($T/llall < tests/programs/cabi_o3.zc | md5sum)" "$(md5sum < tests/programs/cabi_o3.expected)" "llvm compiling every block agrees on o3 native calls"
+ chk "$($T/zyl < tests/programs/cabi_sized.zc | md5sum)" "$(md5sum < tests/programs/cabi_sized.expected)" "llvm native calls with f32 and the sized integers"
+ chk "$($T/llall < tests/programs/sized.zc | md5sum)" "$(md5sum < tests/programs/sized.expected)" "llvm compiling every block agrees on sized integers"
+ chk "$($T/llall < tests/programs/bitops.zc | md5sum)" "$(md5sum < tests/programs/bitops.expected)" "llvm compiling every block agrees on the new micro-ops"
+ chk "$($T/llall < tests/programs/f32.zc | md5sum)" "$(md5sum < tests/programs/f32.expected)" "llvm compiling every block agrees on f32"
+ chk "$($T/llall < tests/programs/sizedvec.zc | md5sum)" "$(md5sum < tests/programs/sizedvec.expected)" "llvm compiling every block agrees on growable sized arrays"
+ chk "$($T/llall < tests/programs/packedfields.zc | md5sum)" "$(md5sum < tests/programs/packedfields.expected)" "llvm compiling every block agrees on packed fields"
+ chk "$($T/llall < tests/programs/generics.zc | md5sum)" "$(md5sum < tests/programs/generics.expected)" "llvm compiling every block agrees on generic instances"
+ chk "$($T/llall < tests/programs/genmeth.zc | md5sum)" "$(md5sum < tests/programs/genmeth.expected)" "llvm compiling every block agrees on generic struct methods"
+ chk "$($T/llall < tests/programs/o3push.zc | md5sum)" "$(md5sum < tests/programs/o3push.expected)" "llvm compiling every block agrees on inline push and pop"
+ selfc $T/zyl $T/gl.bin; cmp -s $T/g1.bin $T/gl.bin && chk 1 1 "llvm self-compile matches" || chk 0 1 "llvm self-compile matches"
+ else echo "skip llvm (no llvm-config, or its headers and libraries are missing)"; fi
+}
+
+s_lsp() {
+ T=$ROOT/lsp
+ # the language server: the compiler itself in check mode, so these are the real compiler's findings
+ if bash src/tools/mklsp.sh $T/lsp >/dev/null 2>&1; then
+ lsp=$T/lsp/zclsp
+ chk "$($lsp < examples/hello.zc)" "no errors" "lsp: a clean program"
+ chk "$($lsp < tests/errors/back_to_m.zc | head -1 | cut -d' ' -f2-)" "error: $(head -1 tests/errors/back_to_m.zc | sed 's#^// expect: ##')" "lsp: the compiler's own message"
+ h=$($lsp < examples/opt_test.zc)
+ for w in "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"; do
+ case "$h" in *"$w"*) chk 1 1 "lsp hint: $w";; *) chk 0 1 "lsp hint: $w";; esac
+ done
+ chk "$($lsp < examples/struct_closure_opt.zc | grep -c error)" "0" "lsp: methods and closures check clean"
+ # a file of a larger build checks on its own: its bodyless declarations are not errors in check mode
+ for f in src/runtime/rt.zc src/runtime/run.zc src/lsp/server.zc; do chk "$($lsp < $f | grep -c ': error:')" "0" "lsp: $f checks alone"; done
+ chk "$($lsp < tests/programs/decl_undefined.zc)" "no errors" "lsp: a bodyless declaration is not an error in check mode"
+ chk "$($lsp < tests/programs/undefined_call.zc | grep -c ': error:')" "1" "lsp: a call to a name nothing defines is still an error"
+ chk "$($lsp --annotate < tests/programs/annotate.zc | md5sum)" "$(md5sum < tests/programs/annotate.expected)" "lsp --annotate writes the types the compiler can see into a program"
+ chk "$($lsp --annotate < tests/programs/annotate.zc | $T/zy)" "$($T/zy < tests/programs/annotate.zc)" "an annotated program means what the original did"
+ chk "$($lsp --types < tests/programs/annotate.zc | head -3 | tr '\n' '/')" "9:1 3 int/10:1 3 char*/11:1 3 int/" "lsp --types lists the edits without making them"
+ chk "$($lsp --annotate < tests/programs/annotate.expected | md5sum)" "$(md5sum < tests/programs/annotate.expected)" "annotating an annotated program changes nothing more"
+ printf 'Content-Length: 40\r\n\r\n{"jsonrpc": [2.0", "method": "x"}' | timeout 10 $lsp > /dev/null 2>&1; chk $? 0 "lsp: a stray brace in an array ends the message, and the server exits"
+ if [ $QUICK = 1 ]; then echo "skip lsp protocol and corpus tests (quick)"
+ elif command -v node >/dev/null; then
+ node tests/drivers/storage_analysis_test.js $lsp > $T/storage.out 2>&1; r=$?
+ grep -v '^ok' $T/storage.out
+ chk $r 0 "lsp: definite storage warnings and reference lifetime hovers"
+ node tests/drivers/lsp_test.js $lsp > $T/lsp.out 2>&1; r=$?
+ grep -v '^ok' $T/lsp.out
+ chk $r 0 "lsp: protocol and corpus tests ($(grep -c '^ok' $T/lsp.out) checks)"
+ node tests/drivers/lsp_live.js $lsp > $T/lsplive.out 2>&1; r=$?
+ grep -v '^ok' $T/lsplive.out
+ chk $r 0 "lsp: the native server answers while the client keeps the pipe open"
+ node tests/drivers/lsp_sandbox.js $lsp > $T/lspsb.out 2>&1; r=$?
+ grep -v '^ok' $T/lspsb.out
+ chk $r 0 "lsp: compile-time code in a checked file cannot write files"
+ # the diagnostics cache: its key rules, then the extension's client end to end on the built-in server
+ 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
+ node tests/drivers/lsp_wasm.js $T/lsp/lsp.wasm > $T/lspw.out 2>&1; r=$?
+ grep -v '^ok' $T/lspw.out
+ chk $r 0 "lsp: wasm server through the extension's host ($(grep -c '^ok' $T/lspw.out) checks)"
+ 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 src/js/tinyvm.js src/js/layout.js src/js/lsp.json $T/vmserver/
+ node tests/drivers/lsp_vm.js $T/vmserver > $T/lspvm.out 2>&1; r=$?
+ grep -v '^ok' $T/lspvm.out
+ chk $r 0 "lsp: the server in the javascript vm, through the extension's host ($(grep -c '^ok' $T/lspvm.out) checks)"
+ else echo "skip lsp protocol tests (no node)"; fi
+ else chk 0 1 "lsp server builds"; fi
+}
+
+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('./src/js/genome.json').glen)")" "$(grep -m1 '#define GLEN' src/core/genome.h | awk '{print $3}')" "src/js/genome.json is up to date"
+ chk "$(node src/js/tinyvm.js examples/hello.zc)" "hello, world!" "js vm: hello"
+ chk "$(node src/js/tinyvm.js examples/fact.zc | tail -1)" "3628800" "js vm: factorial"
+ chk "$(node src/js/tinyvm.js examples/primes.zc | tail -1)" "widest gap: 72, after 31397" "js vm: primes"
+ chk "$(node src/js/tinyvm.js tests/programs/growone.zc)" "12 1" "js vm: a block past the initial heap grows the machine"
+ 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=$?
+ grep -v '^ok' $T/wasm.out
+ chk $r 0 "wasm: the microcore in webassembly ($(grep -c '^ok' $T/wasm.out) checks)"
+ else echo "skip wasm (no clang with the wasm32 target, or no wasm-ld)"; fi
+ else echo "skip js and wasm (no node)"; 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"
+ 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"
+ else echo "skip make (not installed)"; fi
+}
+
+# docs/LEARN.md: every example it shows runs and prints what the guide says, and
+# every block in it is one of those examples (tests/drivers/learn.py)
+s_learn() {
+ T=$ROOT/learn
+ gcc -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c -Isrc/core -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
+}
+
+# ---- run the sections ----
+# each section runs in its own subshell with its own counters and its own log,
+# so several can share the machine. the logs are printed afterwards in the
+# order below, which is the order the checks used to run in.
+pids=(); names=()
+reap(){
+ local p=${pids[0]} n=${names[0]} st
+ wait "$p"; st=$?
+ [ "$st" = 0 ] || echo "FAIL $n: the section exited with status $st" >> $T/log.$n
+ pids=("${pids[@]:1}"); names=("${names[@]:1}")
+}
+sec(){ # $1 = the name the section reports under, $2 = the function to run
+ local n=$1
+ "$2" > $T/log.$n 2>&1 &
+ pids+=($!); names+=("$n")
+ [ ${#pids[@]} -ge $JOBS ] && reap
+ return 0
+}
+# the bootstrap's own checks stay counted; the sections are tallied from their
+# logs below, so a section that dies half way still reports what it got through.
+b_ok=$ok; b_bad=$bad; ok=0; bad=0
+for n in $SECS; do sec $n s_$n; done
+while [ ${#pids[@]} -gt 0 ]; do reap; done
+for n in $SECS; do
+ cat $T/log.$n
+ ok=$((ok + $(grep -c '^ok ' $T/log.$n)))
+ bad=$((bad + $(grep -c '^FAIL' $T/log.$n)))
+done
+ok=$((ok + b_ok)); bad=$((bad + b_bad))
+echo "$ok passed, $bad failed"; [ $bad = 0 ]
diff --git a/tests/test_win.ps1 b/tests/test_win.ps1
new file mode 100644
index 0000000..aee60e8
--- /dev/null
+++ b/tests/test_win.ps1
@@ -0,0 +1,258 @@
+# TinyVM Windows test suite (PowerShell implementation of test.sh)
+$ErrorActionPreference = "Stop"
+
+# detect compiler (prioritizing LLVM-MinGW with UCRT headers)
+$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."
+ exit 1
+}
+
+Write-Host "===============================================================================" -ForegroundColor Cyan
+Write-Host " TinyVM Windows Automated Test Suite " -ForegroundColor Cyan
+Write-Host "===============================================================================" -ForegroundColor Cyan
+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
+
+function Report-Result($success, $name, $detail = "") {
+ if ($success) {
+ $script:totalPassed++
+ Write-Host " [OK] $name" -ForegroundColor Green
+ } else {
+ $script:totalFailed++
+ Write-Host " [FAIL] $name $detail" -ForegroundColor Red
+ }
+}
+
+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"
+
+ 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"
+
+ $bzc = [IO.File]::ReadAllText("src\tools\build.zc")
+ $rt = [IO.File]::ReadAllText("src\runtime\rt.zc")
+ $cp = [IO.File]::ReadAllText("src\compiler\compiler.zc")
+ $combined = $bzc + [char]96 + $rt + $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"
+
+ & $cc -fwrapv -O2 -w -DJTYPE=int "$tempDir\s1\core.c" "$tempDir\s1\io_win.c" -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 "src\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 ---
+ 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"
+
+ # --- 3. feature tests on zy.exe ---
+ Write-Host "`n=== 3. Running Feature Tests on VM Core (zy.exe) ===" -ForegroundColor Yellow
+ $featureTests = @(
+ @{ name = "hello"; file = "examples\hello.zc"; expect = "hello, world!" },
+ @{ name = "factorial"; file = "examples\fact.zc"; expect = "1101110101111100000000`n3628800`n" },
+ @{ name = "self-patching code"; file = "examples\selfpatch.zc"; expect = "ABC" },
+ @{ name = "ownership frees everything"; file = "examples\lists.zc"; expect = "[0 1 4 9 16 25]`nlive: 7`nafter the block: 0`n" },
+ @{ name = "typed example"; file = "examples\typed.zc"; expect = "last: 4`npoint is 2 words: x int, y int`nvec<point*> is 3 words: len int, cap int, items point**`npush is bool(vec<T>*, T.d)`nthe heap says v is a vec<point*>, and v.items[0] is a point`n" },
+ @{ name = "enhancement example"; file = "examples\wiring.zc"; expect = "16 1234 25 4/9" },
+ @{ name = "o3 prime sieve"; file = "examples\primes.zc"; expect = "widest gap: 72, after 31397" },
+ @{ name = "language basics"; file = "tests\programs\basics.zc"; expect = "285 610 42 512`nhey 65 -3 -2 1024 -8`n12456`n757`n9 100-101`n5`nok`n" },
+ @{ name = "u m d lifetimes"; file = "tests\programs\umd.zc"; expect = "100 6 0 15 99 5 5" },
+ @{ name = "u, m and d names"; file = "tests\programs\names.zc"; expect = "12 1 8" },
+ @{ name = "mark-sweep collector"; file = "tests\programs\gc.zc"; expect = "1000 1 900 1 199900" },
+ @{ name = "parse words and operator tables"; file = "tests\programs\meta.zc"; expect = "1024 19`nunless ok`nrrr`n15 42`n7`n42`n7`n144`n" },
+ @{ name = "compile-time eval and mnemonics"; file = "tests\programs\misc.zc"; expect = "45`n54321`nok`n" },
+ @{ name = "types, generics, reflection"; file = "tests\programs\types.zc"; expect = "12 5 2 15 56 42 7 14 3`nrect rect* list<int>* list<T>*.u(int) T(list<T>*) fn(word.d)`na:point@0 b:point@2 tag:int@4 `npoint 1 1`n" },
+ @{ name = "typechecker accepts valid program"; file = "tests\programs\typeok.zc"; expect = "14" },
+ @{ name = "typechecker off by default"; file = "tests\programs\typeoff.zc"; expect = "6" },
+ @{ name = "virtual enhancements"; file = "tests\programs\enhance.zc"; expect = "100 0 343 3,2 5 -2 0" },
+ @{ name = "joins and paths"; file = "tests\programs\joins.zc"; expect = "0 0 5 80 8 3 9 0" },
+ @{ name = "buddy allocator"; file = "tests\programs\buddy.zc"; expect = "131072 262144 1 0" },
+ @{ name = "borrow move"; file = "tests\programs\borrowmove.zc"; expect = "6" },
+ @{ name = "alias tracking"; file = "tests\programs\alias.zc"; expect = "11 22 4 8" },
+ @{ name = "quarantine stale alias"; file = "tests\programs\quarantine.zc"; expect = "0 1234" },
+ @{ name = "recycle dropped block"; file = "tests\programs\recycle.zc"; expect = "1 0 1" },
+ @{ name = "roots collector mark"; file = "tests\programs\roots.zc"; expect = "1 0" },
+ @{ name = "bump arena"; file = "tests\programs\arena.zc"; expect = "4 7 0 1 1" },
+ @{ name = "do/while, enum, cast"; file = "tests\programs\csyn.zc"; expect = "13 0 5 6 0" },
+ @{ name = "include and string helpers"; file = "tests\programs\use_std.zc"; expect = "42 -7 1" },
+ @{ name = "stdlib arena and byte buffer"; file = "tests\programs\stdbuf.zc"; expect = "4 1 266 8" },
+ @{ name = "64-bit shifts"; file = "tests\programs\wide.zc"; expect = "1099511627776 1 1 1 -1" },
+ @{ name = "floating point"; file = "tests\programs\float.zc"; expect = "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" },
+ @{ name = "negative zero is false"; file = "tests\programs\ftruth.zc"; expect = "00111000110107" },
+ @{ name = "float types"; file = "tests\programs\ftype.zc"; expect = "3.500000 10.500000 3.500000 4.250000 5.250000 4.250000 6.500000 2 4 4.250000 8.500000 1 4" },
+ @{ name = "o3 lifts float ops"; file = "tests\programs\fo3.zc"; expect = "5997 5997 4.000000 nan 0 3.250000 1 0 0" },
+ @{ name = "float formatting"; file = "tests\programs\ftext.zc"; expect = "1e+308 1e-10 -0 inf -inf nan 0.1 1.50 1.250e+0 100000000000000000000.000000 1e+40 1111111111" },
+ @{ name = "byte addressing"; file = "tests\programs\bytes.zc"; expect = "17 34 43981 16909060 1 205 171 -3674937295934316015 1108152156331 0 0 30768`nABC" },
+ @{ name = "byte strings"; file = "tests\programs\strings.zc"; expect = "hi! 3 3 104 i! 1 1 1 -42 42 -42 11 1 -9223372036854775808 0 1 3 10 1 1 1 W ab" },
+ @{ name = "shared cons cells"; file = "tests\programs\share.zc"; expect = "1 2 1 4 1 1 1 1 42 1 1 1 6 0" },
+ @{ name = "memdebug shared cell"; file = "tests\programs\share_dbg.zc"; expect = "7 1" },
+ @{ name = "growable and fixed arrays"; file = "tests\programs\arrays.zc"; expect = "100 128 9801 9801 99 3 4 12 19 8 19 28`n0`n" },
+ @{ name = "o3 regions"; file = "tests\programs\o3.zc"; expect = "3142 42 43 9 57 98781 520625 10192 -18782 1152" },
+ @{ name = "o3 scopes"; file = "tests\programs\o3scopes.zc"; expect = "116" },
+ @{ name = "o3 fat data"; file = "tests\programs\o3fat.zc"; expect = "node node 3 box node [0]int" },
+ @{ name = "o3 direct calls"; file = "tests\programs\o3call.zc"; expect = "0 0 4 23 55 node" },
+ @{ name = "bounds check optimization"; file = "tests\programs\bounds.zc"; expect = "55 55 8" }
+ )
+
+ foreach ($t in $featureTests) {
+ $out = (cmd.exe /c "$tempDir\zy.exe < $($t.file) 2>&1") -join "`n"
+ $normOut = $out.Trim().Replace("`r`n", "`n")
+ $normExp = $t.expect.Trim().Replace("`r`n", "`n")
+ $match = ($normOut -eq $normExp) -or ($normOut.Contains($normExp))
+ Report-Result $match $t.name
+ }
+
+ # --- 4. microcore tests ---
+ Write-Host "`n=== 4. Running Microcore Tests (zym.exe) ===" -ForegroundColor Yellow
+ $microTests = @(
+ @{ name = "microcore: hello"; file = "examples\hello.zc"; expect = "hello, world!" },
+ @{ name = "microcore: factorial"; file = "examples\fact.zc"; expect = "3628800" },
+ @{ name = "microcore: primes"; file = "examples\primes.zc"; expect = "widest gap: 72" },
+ @{ name = "microcore: basics"; file = "tests\programs\basics.zc"; expect = "ok" },
+ @{ name = "microcore: umd"; file = "tests\programs\umd.zc"; expect = "100 6 0 15 99 5 5" }
+ )
+ foreach ($t in $microTests) {
+ $out = (cmd.exe /c "$tempDir\zym.exe < $($t.file) 2>&1") -join "`n"
+ $normOut = $out.Trim().Replace("`r`n", "`n")
+ $match = $normOut.Contains($t.expect)
+ Report-Result $match $t.name
+ }
+
+ # --- 5. error rejection suites ---
+ Write-Host "`n=== 5. Error Rejection Suites ===" -ForegroundColor Yellow
+ foreach ($suite in @(@{ dir = "tests\errors"; name = "Lifetime errors" },
+ @{ dir = "tests\typeerrors"; name = "Typechecker errors" },
+ @{ dir = "tests\langerrors"; name = "Language/o3 errors" })) {
+ $files = Get-ChildItem "$($suite.dir)\*.zc"
+ $pass = 0
+ $fail = 0
+ foreach ($f in $files) {
+ $want = ((Get-Content $f.FullName -TotalCount 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++
+ } else {
+ $fail++
+ Write-Host " Failed $($f.Name): got [$got], want [$want]" -ForegroundColor DarkYellow
+ }
+ }
+ Report-Result ($fail -eq 0) "$($suite.name): $pass rejected as expected"
+ }
+
+ # --- 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"
+
+ # --- 7. ZC language server (zclsp.exe) ---
+ 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)
+ $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"
+ Report-Result (Test-Path "$lspDir\zclsp.exe") "zclsp.exe compiled"
+
+ # CLI check: a clean file
+ $lspHello = (cmd.exe /c "`"$lspDir\zclsp.exe`" < examples\hello.zc 2>&1") -join "`n"
+ Report-Result ($lspHello.Trim() -eq "no errors") "zclsp: a clean program (hello.zc)"
+
+ # CLI check: the compiler's own message
+ $lspErr = (cmd.exe /c "`"$lspDir\zclsp.exe`" < tests\errors\back_to_m.zc 2>&1") -join "`n"
+ Report-Result ($lspErr.Contains("can't be recast back to m")) "zclsp: lifetime error detection (back_to_m.zc)"
+
+ # 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")
+ foreach ($h in $hints) { Report-Result ($lspOpt.Contains($h)) "zclsp hint: $h" }
+
+ # methods with this, and closures, check clean
+ $lspStruct = (cmd.exe /c "`"$lspDir\zclsp.exe`" < examples\struct_closure_opt.zc 2>&1") -join "`n"
+ Report-Result (-not $lspStruct.Contains(": error:")) "zclsp: methods and closures check clean"
+
+ # protocol and corpus tests
+ if (Get-Command node -ErrorAction SilentlyContinue) {
+ $lspOut = (node tests\drivers\lsp_test.js "$lspDir\zclsp.exe" 2>&1) -join "`n"
+ $lspCode = $LASTEXITCODE
+ Report-Result ($lspCode -eq 0) "zclsp: protocol and corpus tests"
+ if ($lspCode -ne 0) { Write-Host $lspOut }
+ } else {
+ Write-Host " [SKIP] Node.js not detected in PATH, skipping lsp protocol tests." -ForegroundColor DarkYellow
+ }
+
+ # --- 8. JavaScript / TypeScript vm runtime (src/js/tinyvm.js) ---
+ Write-Host "`n=== 8. JavaScript VM Runtime (node src/js/tinyvm.js) ===" -ForegroundColor Yellow
+ $nodeCmd = Get-Command node -ErrorAction SilentlyContinue
+ if ($nodeCmd) {
+ $jsHello = (node src\js\tinyvm.js examples\hello.zc 2>&1) -join "`n"
+ Report-Result ($jsHello.Trim() -eq "hello, world!") "JS VM: hello.zc"
+
+ $jsFact = (node src\js\tinyvm.js examples\fact.zc 2>&1) -join "`n"
+ Report-Result ($jsFact.Contains("3628800")) "JS VM: fact.zc"
+
+ $jsPrimes = (node src\js\tinyvm.js examples\primes.zc 2>&1) -join "`n"
+ Report-Result ($jsPrimes.Contains("widest gap: 72")) "JS VM: primes.zc"
+
+ $jsMandel = (node src\js\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
+ }
+}
+finally {
+ Start-Sleep -Milliseconds 200
+ Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir
+}
+
+Write-Host "`n===============================================================================" -ForegroundColor Cyan
+$summaryColor = if ($totalFailed -eq 0) { "Green" } else { "Red" }
+Write-Host "Test Results: $totalPassed passed, $totalFailed failed" -ForegroundColor $summaryColor
+Write-Host "===============================================================================" -ForegroundColor Cyan
+
+if ($totalFailed -gt 0) { exit 1 } else { exit 0 }
diff --git a/tests/typeerrors/arg.zc b/tests/typeerrors/arg.zc
new file mode 100644
index 0000000..bf39e85
--- /dev/null
+++ b/tests/typeerrors/arg.zc
@@ -0,0 +1,5 @@
+// expect: type error: argument: expected point*, got int*
+typecheck on;
+struct point { int x, y; };
+int f(point* p) { return p.x; }
+fn main() { int* q = new(int); f(q); free(d*q); return 0; }
diff --git a/tests/typeerrors/arith.zc b/tests/typeerrors/arith.zc
new file mode 100644
index 0000000..eda4aec
--- /dev/null
+++ b/tests/typeerrors/arith.zc
@@ -0,0 +1,3 @@
+// expect: type error: arithmetic needs numbers, got int*
+typecheck on;
+fn main() { int* p = 0; int x = p * 2; return x; }
diff --git a/tests/typeerrors/assign.zc b/tests/typeerrors/assign.zc
new file mode 100644
index 0000000..b83e1a2
--- /dev/null
+++ b/tests/typeerrors/assign.zc
@@ -0,0 +1,3 @@
+// expect: type error: initializer: expected char*, got int
+typecheck on;
+fn main() { char* s = 5; return 0; }
diff --git a/tests/typeerrors/call.zc b/tests/typeerrors/call.zc
new file mode 100644
index 0000000..7b03dbf
--- /dev/null
+++ b/tests/typeerrors/call.zc
@@ -0,0 +1,3 @@
+// expect: type error: only functions can be called, got int
+typecheck on;
+fn main() { int k = 3; return k(1); }
diff --git a/tests/typeerrors/deref.zc b/tests/typeerrors/deref.zc
new file mode 100644
index 0000000..ed030f5
--- /dev/null
+++ b/tests/typeerrors/deref.zc
@@ -0,0 +1,3 @@
+// expect: type error: only pointers can be dereferenced, got int
+typecheck on;
+fn main() { int x = 3; return *x; }
diff --git a/tests/typeerrors/enum_int.zc b/tests/typeerrors/enum_int.zc
new file mode 100644
index 0000000..c710d50
--- /dev/null
+++ b/tests/typeerrors/enum_int.zc
@@ -0,0 +1,4 @@
+// expect: type error: initializer: expected color, got int
+typecheck on;
+enum color { RED, GREEN = 5, BLUE };
+int main() { color c = 1; return c; }
diff --git a/tests/typeerrors/enum_other.zc b/tests/typeerrors/enum_other.zc
new file mode 100644
index 0000000..5b27370
--- /dev/null
+++ b/tests/typeerrors/enum_other.zc
@@ -0,0 +1,5 @@
+// expect: type error: initializer: expected color, got size
+typecheck on;
+enum color { RED, GREEN = 5, BLUE };
+enum size { SMALL, LARGE };
+int main() { color c = size.LARGE; return c; }
diff --git a/tests/typeerrors/field.zc b/tests/typeerrors/field.zc
new file mode 100644
index 0000000..ba03697
--- /dev/null
+++ b/tests/typeerrors/field.zc
@@ -0,0 +1,3 @@
+// expect: point has no field z
+struct point { int x, y; };
+fn main() { point p; return p.z; }
diff --git a/tests/typeerrors/generic.zc b/tests/typeerrors/generic.zc
new file mode 100644
index 0000000..04e5381
--- /dev/null
+++ b/tests/typeerrors/generic.zc
@@ -0,0 +1,4 @@
+// expect: type error: argument: expected int, got char*
+typecheck on;
+T same<T>(T a, T b) { return a; }
+fn main() { return same(1, "x"); }
diff --git a/tests/typeerrors/notstruct.zc b/tests/typeerrors/notstruct.zc
new file mode 100644
index 0000000..0dff6bd
--- /dev/null
+++ b/tests/typeerrors/notstruct.zc
@@ -0,0 +1,2 @@
+// expect: .x: that's a int, not a struct or a pointer to one
+fn main() { int n = 1; return n.x; }
diff --git a/tests/typeerrors/ret.zc b/tests/typeerrors/ret.zc
new file mode 100644
index 0000000..d44267e
--- /dev/null
+++ b/tests/typeerrors/ret.zc
@@ -0,0 +1,4 @@
+// expect: type error: return value: expected bool, got char*
+typecheck on;
+bool f() { return "no"; }
+fn main() { return f(); }
diff --git a/tests/typeerrors/voidret.zc b/tests/typeerrors/voidret.zc
new file mode 100644
index 0000000..86c9c17
--- /dev/null
+++ b/tests/typeerrors/voidret.zc
@@ -0,0 +1,4 @@
+// expect: type error: return value: expected void, got int
+typecheck on;
+void f() { return 3; }
+fn main() { f(); return 0; }