commit 24f445586a405a0772b5d45a48b55d7c4fc82c00
parent b8876feffff2156a0e73be9e0b48281bf496a370
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 28 May 2026 21:51:44 -0700
test aa64 tail calls
Diffstat:
2 files changed, 98 insertions(+), 1 deletion(-)
diff --git a/test/opt/aa64_tail_call.sh b/test/opt/aa64_tail_call.sh
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+# Structural aarch64 tail-call checks.
+#
+# These cases prove that realized tails are sibling calls:
+# - direct stack-arg tail calls reuse the caller's incoming stack-arg window
+# - direct tails lower to AARCH64_JUMP26 (`b`), not AARCH64_CALL26 (`bl`)
+# - indirect tails lower to `br`, not `blr`
+# - the caller frame is restored before the sibling branch
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+CFREE="${CFREE:-$ROOT/build/cfree}"
+WORK="$ROOT/build/test/opt/aa64_tail_call"
+mkdir -p "$WORK"
+
+fail() {
+ printf 'aa64-tail-call check FAILED: %s\n' "$1" >&2
+ if [ -n "${2:-}" ] && [ -f "$2" ]; then
+ sed 's/^/ | /' "$2" >&2
+ fi
+ exit 1
+}
+
+slice_func() {
+ local src="$1" func="$2" out="$3"
+ awk -v name="$func" '
+ $0 ~ "^[0-9a-f]+ <" name ">:" { in_fn = 1; print; next }
+ /^[0-9a-f]+ </ { in_fn = 0 }
+ in_fn { print }
+ ' "$src" > "$out"
+}
+
+compile_case() {
+ local name="$1" src="$2"
+ "$CFREE" cc -target aarch64-linux-gnu -O1 -c "$src" \
+ -o "$WORK/$name.o" > "$WORK/$name.cc.out" 2> "$WORK/$name.cc.err"
+ "$CFREE" objdump -d "$WORK/$name.o" \
+ > "$WORK/$name.dis" 2> "$WORK/$name.objdump.err"
+ "$CFREE" objdump -r "$WORK/$name.o" \
+ > "$WORK/$name.relocs" 2> "$WORK/$name.relocs.err"
+}
+
+compile_case direct_stack "$ROOT/test/toy/cases/25_tail_many_stack_args.toy"
+slice_func "$WORK/direct_stack.dis" caller "$WORK/direct_stack.caller.dis"
+
+if grep -Eq '\bbl\b.*target' "$WORK/direct_stack.caller.dis"; then
+ fail 'direct stack-arg tail used bl target' "$WORK/direct_stack.caller.dis"
+fi
+if ! grep -Eq '\bb\b.*target.*AARCH64_JUMP26' "$WORK/direct_stack.caller.dis"; then
+ fail 'direct stack-arg tail missing b target / AARCH64_JUMP26' \
+ "$WORK/direct_stack.caller.dis"
+fi
+if ! grep -Eq '\bstr\b.*\[x29, #16\]' "$WORK/direct_stack.caller.dis" ||
+ ! grep -Eq '\bstr\b.*\[x29, #24\]' "$WORK/direct_stack.caller.dis"; then
+ fail 'direct stack-arg tail did not write caller incoming stack window' \
+ "$WORK/direct_stack.caller.dis"
+fi
+if ! awk '
+ /add[[:space:]]+sp, sp,/ { restored = 1 }
+ /[[:space:]]b[[:space:]]+.*target.*AARCH64_JUMP26/ {
+ found = 1; ok = restored; exit
+ }
+ END { exit(found && ok ? 0 : 1) }
+' "$WORK/direct_stack.caller.dis"; then
+ fail 'direct tail branched before restoring sp' "$WORK/direct_stack.caller.dis"
+fi
+if ! grep -Eq 'AARCH64_JUMP26[[:space:]]+target' "$WORK/direct_stack.relocs"; then
+ fail 'direct tail relocation is not AARCH64_JUMP26' "$WORK/direct_stack.relocs"
+fi
+if grep -Eq 'AARCH64_CALL26[[:space:]]+target' "$WORK/direct_stack.relocs"; then
+ fail 'direct tail emitted AARCH64_CALL26 relocation to target' \
+ "$WORK/direct_stack.relocs"
+fi
+
+compile_case indirect "$ROOT/test/toy/cases/32_musttail_indirect.toy"
+slice_func "$WORK/indirect.dis" apply "$WORK/indirect.apply.dis"
+
+if grep -Eq '\bblr\b' "$WORK/indirect.apply.dis"; then
+ fail 'indirect musttail used blr' "$WORK/indirect.apply.dis"
+fi
+if ! grep -Eq '\bbr[[:space:]]+x[0-9]+' "$WORK/indirect.apply.dis"; then
+ fail 'indirect musttail missing br' "$WORK/indirect.apply.dis"
+fi
+if ! awk '
+ /ldp[[:space:]]+x29, x30,/ { restored = 1 }
+ /[[:space:]]br[[:space:]]+x[0-9]+/ { found = 1; ok = restored; exit }
+ END { exit(found && ok ? 0 : 1) }
+' "$WORK/indirect.apply.dis"; then
+ fail 'indirect tail branched before restoring frame record' \
+ "$WORK/indirect.apply.dis"
+fi
+
+printf 'aa64-tail-call: ok\n'
diff --git a/test/test.mk b/test/test.mk
@@ -521,7 +521,7 @@ test-macho: lib $(TEST_RT_DEP) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_R
OPT_TEST_BIN = build/test/cg_ir_lower_test
TINY_INLINE_TEST_BIN = build/test/tiny_inline_test
-test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64
+test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail
$(OPT_TEST_BIN)
$(OPT_TEST_BIN): test/opt/cg_ir_lower_test.c $(LIB_OBJS)
@@ -548,6 +548,10 @@ test-opt-zero-arg: bin
test-opt-static-prune-aa64: bin
@CFREE=$(abspath $(BIN)) bash test/opt/static_prune_aa64.sh
+.PHONY: test-opt-aa64-tail
+test-opt-aa64-tail: bin
+ @CFREE=$(abspath $(BIN)) bash test/opt/aa64_tail_call.sh
+
test-parse: test-parse-ok test-parse-err
test-parse-ok: lib $(TEST_RT_DEP) $(PARSE_RUNNER) $(ROUNDTRIP_BIN) $(LINK_EXE_RUNNER) $(JIT_RUNNER)