From 95f71e1accb3b59572ffe723ce2a15811bf0c709 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Sat, 2 May 2026 00:35:00 +0100 Subject: [PATCH] feat(test): PDR integration test suite and pasbuild-integration-test plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds compiler/src/it/ (Maven-convention integration test directory) with a Blaise-specific PDR driver. Four initial tests ported from the OPDF integration suite: breakpoint+next, local variables, locals command, and step-over. Test programs are adapted for Blaise (no FPC directives), line numbers preserved to match the original commands files. The pasbuild-integration-test plugin (phase: none) can be invoked as 'pasbuild integration-test'; it verifies pdr and the Blaise binary are present, then delegates to compiler/src/it/run_tests.sh. All four tests currently fail — line info in the OPDF section maps every statement to the function-start address (per-stmt addresses require QBE changes, tracked in future-improvements.adoc). The aspirational expected files show exactly what each test should produce once OPDF is complete, making failures a clear roadmap rather than noise. Also adds PIE/ASLR support entry to future-improvements.adoc (Linux, FreeBSD, macOS load-base strategies for the PDR debugger). --- compiler/src/it/run_tests.sh | 138 ++++++++++++++++++ .../src/it/test_02_breakpoint_next.commands | 11 ++ .../src/it/test_02_breakpoint_next.expected | 5 + compiler/src/it/test_02_breakpoint_next.pas | 23 +++ .../src/it/test_06_local_variables.commands | 10 ++ .../src/it/test_06_local_variables.expected | 2 + compiler/src/it/test_06_local_variables.pas | 24 +++ compiler/src/it/test_14_locals.commands | 7 + compiler/src/it/test_14_locals.expected | 2 + compiler/src/it/test_14_locals.pas | 24 +++ compiler/src/it/test_26_step_over.commands | 6 + compiler/src/it/test_26_step_over.expected | 2 + compiler/src/it/test_26_step_over.pas | 21 +++ docs/future-improvements.adoc | 38 +++++ plugins/pasbuild-integration-test | 63 ++++++++ 15 files changed, 376 insertions(+) create mode 100755 compiler/src/it/run_tests.sh create mode 100644 compiler/src/it/test_02_breakpoint_next.commands create mode 100644 compiler/src/it/test_02_breakpoint_next.expected create mode 100644 compiler/src/it/test_02_breakpoint_next.pas create mode 100644 compiler/src/it/test_06_local_variables.commands create mode 100644 compiler/src/it/test_06_local_variables.expected create mode 100644 compiler/src/it/test_06_local_variables.pas create mode 100644 compiler/src/it/test_14_locals.commands create mode 100644 compiler/src/it/test_14_locals.expected create mode 100644 compiler/src/it/test_14_locals.pas create mode 100644 compiler/src/it/test_26_step_over.commands create mode 100644 compiler/src/it/test_26_step_over.expected create mode 100644 compiler/src/it/test_26_step_over.pas create mode 100755 plugins/pasbuild-integration-test diff --git a/compiler/src/it/run_tests.sh b/compiler/src/it/run_tests.sh new file mode 100755 index 0000000..8d03d48 --- /dev/null +++ b/compiler/src/it/run_tests.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# +# Integration Test Runner for PDR Debugger — Blaise Compiler +# +# Compiles test programs using the Blaise compiler with --debug-opdf, +# then drives PDR interactively via .commands files and compares +# output against .expected files. +# +# Usage: +# ./run_tests.sh — run all test_*.pas files +# ./run_tests.sh test_02.pas — run a single test +# BLAISE=/path/to/blaise ./run_tests.sh +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Counters +PASSED=0 +FAILED=0 +SKIPPED=0 + +# PDR binary +PDR_BIN="pdr" + +# Blaise compiler binary discovery: +# 1. BLAISE environment variable +# 2. compiler/target/blaise in the project tree +if [ -n "$BLAISE" ]; then + BLAISE_BIN="$BLAISE" +else + BLAISE_BIN="$PROJECT_ROOT/compiler/target/blaise" +fi + +echo "=== PDR Integration Test Runner (Blaise) ===" +echo + +if ! command -v "$PDR_BIN" > /dev/null 2>&1; then + echo -e "${RED}ERROR: pdr not found in PATH${NC}" + exit 1 +fi + +if [ ! -x "$BLAISE_BIN" ]; then + echo -e "${RED}ERROR: Blaise compiler not found: $BLAISE_BIN${NC}" + echo " Set BLAISE=/path/to/blaise or run 'pasbuild compile -m blaise-compiler' first" + exit 1 +fi + +echo "Blaise : $BLAISE_BIN" +echo "PDR : $(command -v $PDR_BIN)" +echo + +# Filter non-deterministic output — identical to the OPDF integration runner. +# Keeps: variable assignments (2+ char names), step messages, callstack frames. +# Drops: single-char names (A, B), program WriteLn output, [INFO]/[DEBUG] lines. +filter_output() { + sed 's/^(pdr) //' | \ + sed -E 's/ \(0x[0-9A-Fa-f ]+\)//' | \ + sed -E 's/\(\$[0-9A-Fa-f]+\)/()/' | \ + grep -E "^(([A-Za-z][A-Za-z0-9_.]+(\[[0-9]+\])? = )|(\[INFO\] )?[Ss]tepped to line:|\[CALLSTACK\]|#[0-9]+ |Exception: [A-Za-z]+ —)" | \ + sed 's/^\[INFO\] //' || true +} + +run_test() { + local test_name=$1 + local test_base="${test_name%.pas}" + + echo -e "${YELLOW}Running: $test_base${NC}" + + # Compile with Blaise OPDF support + echo " [1/3] Compiling..." + if ! "$BLAISE_BIN" --source "$test_name" --output "$test_base" --debug-opdf \ + > "$test_base.compile.log" 2>&1; then + echo -e "${RED} FAILED: Compilation${NC}" + cat "$test_base.compile.log" + ((FAILED++)) + return 1 + fi + + # Run PDR with command script + echo " [2/3] Running PDR..." + if [ ! -f "$test_base.commands" ]; then + echo -e "${YELLOW} SKIPPED: No commands file${NC}" + ((SKIPPED++)) + return 0 + fi + cat "$test_base.commands" | "$PDR_BIN" --verbose "./$test_base" 2>&1 \ + | filter_output > "$test_base.actual" + + # Compare against expected output (case-insensitive, address-normalised) + echo " [3/3] Comparing output..." + if [ ! -f "$test_base.expected" ]; then + echo -e "${YELLOW} SKIPPED: No expected file${NC}" + ((SKIPPED++)) + return 0 + fi + if diff -u \ + <(sed -E 's/@\$[0-9A-Fa-f]+/@$/g' "$test_base.expected" | tr '[:upper:]' '[:lower:]') \ + <(sed -E 's/@\$[0-9A-Fa-f]+/@$/g' "$test_base.actual" | tr '[:upper:]' '[:lower:]') \ + > "$test_base.diff"; then + echo -e "${GREEN} PASSED${NC}" + ((PASSED++)) + return 0 + else + echo -e "${RED} FAILED${NC}" + cat "$test_base.diff" + ((FAILED++)) + return 1 + fi +} + +cd "$SCRIPT_DIR" + +if [ $# -eq 1 ]; then + TEST_NAME=$1 + [[ ! "$TEST_NAME" =~ \.pas$ ]] && TEST_NAME="${TEST_NAME}.pas" + run_test "$TEST_NAME" +else + for test_file in test_*_*.pas; do + [ -f "$test_file" ] && run_test "$test_file" && echo + done +fi + +echo "===================================" +echo -e "Passed : ${GREEN}$PASSED${NC}" +echo -e "Failed : ${RED}$FAILED${NC}" +echo -e "Skipped: ${YELLOW}$SKIPPED${NC}" +echo "===================================" + +[ $FAILED -eq 0 ] && exit 0 || exit 1 diff --git a/compiler/src/it/test_02_breakpoint_next.commands b/compiler/src/it/test_02_breakpoint_next.commands new file mode 100644 index 0000000..cd422f0 --- /dev/null +++ b/compiler/src/it/test_02_breakpoint_next.commands @@ -0,0 +1,11 @@ +run +break test_02_breakpoint_next.pas:16 +continue +print MyInt +next +print MyInt +next +print MyInt +break test_02_breakpoint_next.pas:22 +continue +quit diff --git a/compiler/src/it/test_02_breakpoint_next.expected b/compiler/src/it/test_02_breakpoint_next.expected new file mode 100644 index 0000000..58039a9 --- /dev/null +++ b/compiler/src/it/test_02_breakpoint_next.expected @@ -0,0 +1,5 @@ +MyInt = 20 +Stepped to line: test_02_breakpoint_next.pas:18 +MyInt = 30 +Stepped to line: test_02_breakpoint_next.pas:19 +MyInt = 30 diff --git a/compiler/src/it/test_02_breakpoint_next.pas b/compiler/src/it/test_02_breakpoint_next.pas new file mode 100644 index 0000000..338a841 --- /dev/null +++ b/compiler/src/it/test_02_breakpoint_next.pas @@ -0,0 +1,23 @@ +program test_02_breakpoint_next; + + + +var + MyInt: Integer; + Sentinel: Integer; + +begin + WriteLn('Test: Breakpoint and Next'); + + MyInt := 10; + WriteLn('Set MyInt to 10'); + + MyInt := 20; + WriteLn('Set MyInt to 20'); + + MyInt := 30; + WriteLn('Set MyInt to 30'); + + WriteLn('Done'); + Sentinel := 1; +end. diff --git a/compiler/src/it/test_06_local_variables.commands b/compiler/src/it/test_06_local_variables.commands new file mode 100644 index 0000000..063a021 --- /dev/null +++ b/compiler/src/it/test_06_local_variables.commands @@ -0,0 +1,10 @@ +run +break test_06_local_variables.pas:13 +continue +print A +print B +print Sum +print Product +break test_06_local_variables.pas:23 +continue +quit diff --git a/compiler/src/it/test_06_local_variables.expected b/compiler/src/it/test_06_local_variables.expected new file mode 100644 index 0000000..d20589b --- /dev/null +++ b/compiler/src/it/test_06_local_variables.expected @@ -0,0 +1,2 @@ +Sum = 15 +Product = 50 diff --git a/compiler/src/it/test_06_local_variables.pas b/compiler/src/it/test_06_local_variables.pas new file mode 100644 index 0000000..f9afd36 --- /dev/null +++ b/compiler/src/it/test_06_local_variables.pas @@ -0,0 +1,24 @@ +program test_06_local_variables; + + +{ Test program for debugging local variables and function parameters } + +function Calculate(A, B: Integer): Integer; +var + Sum: Integer; + Product: Integer; +begin + Sum := A + B; + Product := A * B; + Result := Sum + Product; { Breakpoint line for testing locals } +end; + +var + X, Y, ResultValue: Integer; + Sentinel: Integer; +begin + X := 5; + Y := 10; + ResultValue := Calculate(X, Y); + Sentinel := 1; +end. diff --git a/compiler/src/it/test_14_locals.commands b/compiler/src/it/test_14_locals.commands new file mode 100644 index 0000000..9eaea0c --- /dev/null +++ b/compiler/src/it/test_14_locals.commands @@ -0,0 +1,7 @@ +run +break test_14_locals.pas:17 +continue +locals +break test_14_locals.pas:23 +continue +quit diff --git a/compiler/src/it/test_14_locals.expected b/compiler/src/it/test_14_locals.expected new file mode 100644 index 0000000..e46a1bb --- /dev/null +++ b/compiler/src/it/test_14_locals.expected @@ -0,0 +1,2 @@ +Sum = 10 +Product = 21 diff --git a/compiler/src/it/test_14_locals.pas b/compiler/src/it/test_14_locals.pas new file mode 100644 index 0000000..e84c573 --- /dev/null +++ b/compiler/src/it/test_14_locals.pas @@ -0,0 +1,24 @@ +program test_14_locals; + + +{ Test program for 'locals' command — lists all in-scope variables } + +var + GlobalCount: Integer; + Sentinel: Integer; + +function Compute(A, B: Integer): Integer; +var + Sum: Integer; + Product: Integer; +begin + Sum := A + B; + Product := A * B; + Result := Sum + Product; { breakpoint here — line 17 } +end; + +begin + WriteLn(Compute(3, 7)); + WriteLn(GlobalCount); + Sentinel := 1; +end. diff --git a/compiler/src/it/test_26_step_over.commands b/compiler/src/it/test_26_step_over.commands new file mode 100644 index 0000000..9ee3d45 --- /dev/null +++ b/compiler/src/it/test_26_step_over.commands @@ -0,0 +1,6 @@ +run +break test_26_step_over.pas:19 +continue +next +print Counter +quit diff --git a/compiler/src/it/test_26_step_over.expected b/compiler/src/it/test_26_step_over.expected new file mode 100644 index 0000000..e1091ad --- /dev/null +++ b/compiler/src/it/test_26_step_over.expected @@ -0,0 +1,2 @@ +Stepped to line: test_26_step_over.pas:20 +Counter = 99 diff --git a/compiler/src/it/test_26_step_over.pas b/compiler/src/it/test_26_step_over.pas new file mode 100644 index 0000000..59f7650 --- /dev/null +++ b/compiler/src/it/test_26_step_over.pas @@ -0,0 +1,21 @@ +program test_26_step_over; + +{ Step-over (next) must NOT enter SetResult; must land on the WriteLn line. + SetResult body is at higher line numbers than the call site, which tests + that step-over uses function scope (address range) rather than line numbers. } + + + +procedure SetResult(var N: Integer; V: Integer); +begin + N := V; +end; + +var + Counter: Integer; + +begin + Counter := 0; + SetResult(Counter, 99); { line 19 - break here, then next } + WriteLn(Counter); { line 20 - should land here } +end. diff --git a/docs/future-improvements.adoc b/docs/future-improvements.adoc index 4f4f8d1..44d613f 100644 --- a/docs/future-improvements.adoc +++ b/docs/future-improvements.adoc @@ -420,6 +420,44 @@ stream chains. *Effort.* Moderate. Blocked on abstract method support in the class system. +== Debugger + +=== PIE (ASLR) support in PDR + +*What.* When a Blaise binary is compiled with `--debug-opdf`, the compiler +currently passes `-no-pie` to gcc to ensure OPDF `.quad symbol` entries +contain absolute link-time addresses. The PDR debugger should instead read +the binary's actual load base at runtime and adjust all OPDF addresses by the +ASLR slide, allowing debug-opdf builds to remain PIE. + +*Why it matters.* PIE (Position Independent Executable) is the default on all +modern Linux distributions and is required for ASLR protection. Disabling it +in debug builds is a pragmatic workaround that is acceptable for local +development but is not appropriate for any scenario where the binary is +distributed or run in a production-adjacent environment. Lifting the +restriction makes `--debug-opdf` safe to enable in CI and staging environments. + +*Implementation notes.* + +* On Linux, `/proc//maps` lists each memory mapping with its base + address and the backing file. At debugger attach time, read this file, + find the entry whose backing path matches the target binary, and record + its base address as `LoadBase`. +* On FreeBSD, PIE and ASLR are supported by default from FreeBSD 12 onwards. + FreeBSD's procfs is not mounted by default, so `/proc//map` is not + reliable. The correct approach is `sysctl(KERN_PROC_VMMAP, pid)`, which + returns the full VM map for a process; find the entry matching the binary + path and extract its start address as `LoadBase`. +* On macOS, the equivalent is `task_info(TASK_DYLD_INFO)` or parsing the + Mach-O load commands; `DYLD_IMAGE_BASE` is available via the dyld shared + cache APIs. +* At symbol-resolution time on all platforms: `RuntimeAddr = OPDFAddr + LoadBase`. +* Once PIE-aware address resolution is in place, remove the `-no-pie` guard + in `Blaise.pas` (`CompileToNative`). + +*Effort.* Small on Linux and FreeBSD (one sysctl/procfs read at attach time); +moderate on macOS (dyld integration). + == macOS — Debugging and Code-Signing === Debugger entitlement requirements diff --git a/plugins/pasbuild-integration-test b/plugins/pasbuild-integration-test new file mode 100755 index 0000000..61acd7d --- /dev/null +++ b/plugins/pasbuild-integration-test @@ -0,0 +1,63 @@ +#!/bin/sh +# pasbuild-integration-test — Run PDR integration tests for the Blaise compiler. +# +# Phase: none (standalone — invoke as: pasbuild integration-test) +# +# Preconditions: +# - pdr must be in PATH +# - compiler/target/blaise must exist (run 'pasbuild compile' first) +# - compiler/src/it/ must exist and contain test_*.pas files +# +# Usage: +# pasbuild integration-test +# BLAISE=/path/to/blaise pasbuild integration-test + +set -e + +case "$1" in + --pasbuild-phase) + echo "none" + exit 0 + ;; +esac + +# ---- Locate project directory ------------------------------------------------ +PROJ="${PASBUILD_PROJECT_DIR:-$1}" +if [ -z "$PROJ" ]; then + echo "[integration-test] ERROR: project directory not set" >&2 + exit 1 +fi + +# ---- Locate integration test directory --------------------------------------- +IT_DIR="$PROJ/compiler/src/it" +if [ ! -d "$IT_DIR" ]; then + echo "[integration-test] ERROR: no integration tests found at $IT_DIR" >&2 + exit 1 +fi + +if [ -z "$(find "$IT_DIR" -maxdepth 1 -name 'test_*.pas' 2>/dev/null)" ]; then + echo "[integration-test] ERROR: no test_*.pas files in $IT_DIR" >&2 + exit 1 +fi + +# ---- Check prerequisites ----------------------------------------------------- +if ! command -v pdr > /dev/null 2>&1; then + echo "[integration-test] ERROR: pdr not found in PATH" >&2 + exit 1 +fi + +BLAISE_BIN="${BLAISE:-$PROJ/compiler/target/blaise}" +if [ ! -x "$BLAISE_BIN" ]; then + echo "[integration-test] ERROR: Blaise compiler not found: $BLAISE_BIN" >&2 + echo " Run 'pasbuild compile -m blaise-compiler' first" >&2 + exit 1 +fi + +echo "[integration-test] Project : $PROJ" +echo "[integration-test] Tests : $IT_DIR" +echo "[integration-test] Blaise : $BLAISE_BIN" +echo "[integration-test] PDR : $(command -v pdr)" +echo + +# ---- Run tests --------------------------------------------------------------- +BLAISE="$BLAISE_BIN" "$IT_DIR/run_tests.sh"