diff options
Diffstat (limited to 'tests')
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; } |
