commit a52dc7712af059af01946616204fd6aebefc7652
parent adbf3758d169db6ab5e662c4b1e436fcdfd9da06
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 23 Apr 2026 16:27:51 -0700
tests/p1: hello.P1 + harness exercising the full P1 -> ELF pipeline
First end-to-end test that drives a P1-language program through every
layer of the stack:
cat p1/P1-aarch64.M1pp p1/P1.M1pp tests/p1/hello.P1
-> m1pp (M1 build) -> .M1 macro / builtin substitution
-> m1pp/build.sh -> aarch64 ELF lint, M0, hex2-0
podman run binary -> diff stdout vs .expected
hello.P1 is the smallest useful program: write(stdout, "Hello, World!\n")
then exit(0), spelled with the P1 frontend's macros (%li, %la, %syscall,
%sys_write, %sys_exit). Proves that:
- the cat-three-files convention works (define-before-use holds across
arch backend, portable frontend, and user program)
- m1pp accepts inputs above its previous 8 KB cap (combined source is
~15 KB), and that its emit_hex_value output round-trips through M0
(the recent quoted-hex change makes this work)
- the full toolchain produces a runnable ELF that the kernel actually
invokes
Filenames starting with `_` are skipped (parked), matching m1pp/test.sh.
Diffstat:
3 files changed, 152 insertions(+), 0 deletions(-)
diff --git a/tests/p1/hello.P1 b/tests/p1/hello.P1
@@ -0,0 +1,37 @@
+# tests/p1/hello.P1 — P1-language hello world.
+#
+# Build pipeline (driven by tests/p1/test.sh):
+# cat p1/P1-aarch64.M1pp p1/P1.M1pp tests/p1/hello.P1
+# -> m1pp expander -> .M1 (macro and builtin substitution)
+# -> m1pp/build.sh -> aarch64 ELF binary (lint, M0, hex2-0)
+#
+# The binary writes "Hello, World!\n" (14 bytes) to stdout and exits 0.
+#
+# Notes on the syntax:
+# %li(rd) — emits an "ldr xN, [pc,#8]; b +12" prefix; the next 8 source
+# bytes are the literal pool slot. We supply them as either
+# a sized syscall-number macro (%sys_write -> $(64) = 8B hex)
+# or as a `%lo %hi` decimal pair (each %N is 4 LE bytes).
+# %la(rd) — emits an "ldr wN, [pc,#8]; b +8" prefix; the next 4 source
+# bytes are the literal pool slot. `&msg` resolves to a
+# 4-byte label address at M0 link time.
+# %syscall — wraps the P1v2 syscall ABI (number in a0, args in
+# a1..a3,t0,s0,s1) into the Linux/aarch64 register layout
+# and issues SVC #0.
+
+:_start
+ %li(a0) %sys_write()
+ %li(a1) %1 %0
+ %la(a2) &msg
+ %li(a3) %14 %0
+ %syscall()
+
+ %li(a0) %sys_exit()
+ %li(a1) %0 %0
+ %syscall()
+
+:msg
+"Hello, World!
+"
+
+:ELF_end
diff --git a/tests/p1/hello.expected b/tests/p1/hello.expected
@@ -0,0 +1 @@
+Hello, World!
diff --git a/tests/p1/test.sh b/tests/p1/test.sh
@@ -0,0 +1,114 @@
+#!/bin/sh
+## tests/p1/test.sh — run the P1-language test suite.
+##
+## A P1 fixture is `<name>.P1`. The runner concatenates the P1 frontend
+## (p1/P1-aarch64.M1pp + p1/P1.M1pp) with the fixture, expands the result
+## with the M1 build of m1pp, then hands the resulting .M1 source to
+## m1pp/build.sh which lints / preprocesses / M0-assembles / ELF-links it
+## into an aarch64 binary. The binary is executed inside the standard
+## distroless-busybox container; its stdout is diffed against
+## `<name>.expected`.
+##
+## Filenames starting with `_` are skipped (parked).
+##
+## Usage: tests/p1/test.sh [fixture-name ...]
+## No args: every non-`_` fixture under tests/p1/.
+
+set -eu
+
+REPO=$(cd "$(dirname "$0")/../.." && pwd)
+PLATFORM=linux/arm64
+IMAGE=localhost/distroless-busybox:latest
+
+EXPANDER_BIN=build/m1pp/m1pp
+EXPANDER_BUILT=0
+
+cd "$REPO"
+
+build_expander() {
+ if [ "$EXPANDER_BUILT" = 0 ]; then
+ sh m1pp/build.sh m1pp/m1pp.M1 "$EXPANDER_BIN" >/dev/null 2>&1 || {
+ echo "FATAL: failed to build m1pp/m1pp.M1" >&2
+ sh m1pp/build.sh m1pp/m1pp.M1 "$EXPANDER_BIN" 2>&1 | sed 's/^/ /' >&2
+ exit 1
+ }
+ EXPANDER_BUILT=1
+ fi
+}
+
+if [ "$#" -gt 0 ]; then
+ NAMES="$*"
+else
+ NAMES=$(ls tests/p1/ 2>/dev/null \
+ | sed -n 's/^\([^_][^.]*\)\.P1$/\1/p' \
+ | sort -u)
+fi
+
+if [ -z "$NAMES" ]; then
+ echo "no fixtures to run" >&2
+ exit 1
+fi
+
+pass=0
+fail=0
+for name in $NAMES; do
+ fixture=tests/p1/$name.P1
+ expected=tests/p1/$name.expected
+
+ if [ ! -e "$fixture" ]; then
+ echo " SKIP $name (no .P1)"
+ continue
+ fi
+ if [ ! -e "$expected" ]; then
+ echo " SKIP $name (no .expected)"
+ continue
+ fi
+
+ build_expander
+
+ work=build/p1-tests/$name
+ mkdir -p "$work"
+ combined=$work/combined.M1pp
+ expanded=$work/$name.M1
+ binary=$work/$name
+
+ cat p1/P1-aarch64.M1pp p1/P1.M1pp "$fixture" > "$combined"
+
+ if ! podman run --rm --pull=never --platform "$PLATFORM" \
+ -v "$REPO":/work -w /work "$IMAGE" \
+ "./$EXPANDER_BIN" "$combined" "$expanded" >/dev/null 2>&1; then
+ echo " FAIL $name (m1pp expansion failed)"
+ podman run --rm --pull=never --platform "$PLATFORM" \
+ -v "$REPO":/work -w /work "$IMAGE" \
+ "./$EXPANDER_BIN" "$combined" "$expanded" 2>&1 | sed 's/^/ /'
+ fail=$((fail + 1))
+ continue
+ fi
+
+ if ! sh m1pp/build.sh "$expanded" "$binary" >/dev/null 2>&1; then
+ echo " FAIL $name (m1pp/build.sh failed)"
+ sh m1pp/build.sh "$expanded" "$binary" 2>&1 | sed 's/^/ /'
+ fail=$((fail + 1))
+ continue
+ fi
+
+ actual=$(podman run --rm --pull=never --platform "$PLATFORM" \
+ -v "$REPO":/work -w /work "$IMAGE" \
+ "./$binary" 2>&1 || true)
+ expected_content=$(cat "$expected")
+
+ if [ "$actual" = "$expected_content" ]; then
+ echo " PASS $name"
+ pass=$((pass + 1))
+ else
+ echo " FAIL $name"
+ echo " --- expected ---"
+ printf '%s\n' "$expected_content" | sed 's/^/ /'
+ echo " --- actual ---"
+ printf '%s\n' "$actual" | sed 's/^/ /'
+ fail=$((fail + 1))
+ fi
+done
+
+echo "$pass passed, $fail failed"
+[ "$fail" -eq 0 ]