blaise/scripts/fixpoint-native-internal.sh
Graeme Geldenhuys de0ea5defb chore(phase3): deprecate QBE in help text, guard internal linker, document toolchain
Phase 3 of the native-default drive — the locally-verifiable items (CI and
rolling-bootstrap changes are deferred to Phase 4, where they become correct
once v0.12.0 is the rolling-bootstrap anchor):

- Help text now marks the backends explicitly: "qbe (deprecated) | native
  (default)". QBE is retained as the e2e parity oracle and QBE-fixpoint guard;
  removal is a later release.

- fixpoint-native-internal.sh now drives the FULL internal pipeline (internal
  assembler + internal linker) against an all-external (gcc assemble + gcc link)
  reference, instead of only differing on the assembler. Both internal tools are
  now the default toolchain, so the conformance guard must exercise both. Header
  comments updated to reflect that it guards the internal linker too and why it
  stays a differential probe (the internal assembler buffers the whole object in
  memory and OOMs on the compiler's own ~631k-line .s).

- language-rationale.adoc records the decision: native backend + internal
  assembler + internal linker as the default toolchain (no external tools beyond
  the C runtime startup objects), why QBE is kept-but-deprecated, and the
  per-invocation escape hatches (--assembler external, --linker external,
  --backend qbe, --incremental).

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3541 tests / 0 failures on both the QBE-built and native-built test runners.
2026-06-19 18:16:46 +01:00

153 lines
5.9 KiB
Bash
Executable file

#!/bin/bash
# Internal-toolchain conformance check for the Blaise self-hosting toolchain.
#
# WHAT IT GUARDS:
# The DEFAULT native pipeline since v0.12.0 is the in-process internal
# assembler (blaise.assembler.x86_64) + internal linker (blaise.linker.elf):
# source -> native .s -> internal assembler -> .o -> internal linker ->
# executable, with zero external tools (only distro CRT objects are read).
# This script asserts that the fully-internal pipeline produces a binary
# that BEHAVES identically to the trusted all-external (gcc assemble + gcc
# link) reference.
#
# WHY IT EXISTS, separately from fixpoint-native.sh:
# fixpoint-native.sh validates native CODEGEN by diffing the emitted .s
# text, but it assembles + links that text with EXTERNAL gcc. Neither the
# internal assembler nor the internal linker is on that path, so a defect
# in either silently passes both fixpoints (this is exactly how the
# sret-Result field-read bug hid; see bugs.txt 2026-06-16). This script is
# the only fixpoint-level guard that drives both internal tools end to end.
#
# WHY IT IS A DIFFERENTIAL CHECK, NOT A SELF-HOSTING FIXPOINT:
# The internal assembler currently buffers the whole assembly + ELF
# object in memory, so assembling the compiler's own ~631k-line .s needs
# ~53 GB and OOM-kills. A true "compile the compiler with itself via the
# internal toolchain" fixpoint is therefore not feasible until the internal
# assembler streams its output. Instead we compile a representative program
# that exercises the bug-prone paths (record-return sret-Result field reads,
# immutable string literals) BOTH ways and assert the internal-toolchain
# binary BEHAVES identically (stdout + exit code) to the all-external
# reference. Note: the internal and external tools may emit
# different-but-valid encodings / section layouts, so a byte-level compare
# would false-positive; behavioural equivalence is the sound invariant.
#
# Requires: a native compiler at compiler/target/blaise (run the QBE
# fixpoint or `pasbuild compile` first) and compiler/target/blaise_rtl.a.
set -e
if [ ! -f "compiler/src/main/pascal/Blaise.pas" ]; then
echo "Run this script from the project root: ./scripts/fixpoint-native-internal.sh" >&2
exit 1
fi
COMPILER="${1:-compiler/target/blaise}"
if [ ! -x "$COMPILER" ]; then
echo "Compiler not found: $COMPILER" >&2
echo "Build it first: pasbuild compile -m blaise-compiler --compiler ..." >&2
exit 10
fi
if [ -f compiler/target/blaise_rtl.a ]; then
RTL_ARCHIVE=compiler/target/blaise_rtl.a
elif [ -f runtime/target/blaise_rtl.a ]; then
RTL_ARCHIVE=runtime/target/blaise_rtl.a
else
echo "blaise_rtl.a not found — build the runtime first" >&2
exit 11
fi
# FindRTL looks for blaise_rtl.a beside the compiler binary.
cp "$RTL_ARCHIVE" "$(dirname "$COMPILER")/blaise_rtl.a" 2>/dev/null || true
UNIT_ARGS="--unit-path runtime/src/main/pascal --unit-path stdlib/src/main/pascal"
PROBE=/tmp/fpni_probe.pas
# Representative program. Exercises the sret-Result field-read path (a
# record-returning function reads one Result field into another) plus many
# immutable string literals (each emits a `.long -1` header the internal
# assembler must encode correctly).
cat > "$PROBE" <<'PROBE_EOF'
program fpni_probe;
type
TPoint = record
X: Int64;
Y: Int64;
Name: string;
end;
function MakePoint(AX, AY: Int64; const AName: string): TPoint;
begin
Result.X := AX;
Result.Y := AY;
Result.Name := AName;
if Result.X >= Result.Y then
Result.Name := AName + '-wide'
else
Result.Name := AName + '-tall';
end;
var
P: TPoint;
I: Integer;
Total: Int64;
begin
Total := 0;
for I := 0 to 4 do
begin
P := MakePoint(I, I * 2, 'pt');
Total := Total + P.X + P.Y;
WriteLn(P.Name, ' ', P.X, ',', P.Y);
end;
WriteLn('total=', Total);
WriteLn('immutable string literals and record return survived');
end.
PROBE_EOF
echo "compiler: $COMPILER"
echo "[1/3] compile probe with INTERNAL assembler + INTERNAL linker (default pipeline)"
if ! "$COMPILER" --source "$PROBE" $UNIT_ARGS \
--backend native --assembler internal --linker internal \
--output /tmp/fpni_int 2>/tmp/fpni_int.err; then
echo "INTERNAL_COMPILE_FAIL"; head -5 /tmp/fpni_int.err; exit 2
fi
echo "[2/3] compile probe with EXTERNAL assembler + EXTERNAL linker (reference)"
if ! "$COMPILER" --source "$PROBE" $UNIT_ARGS \
--backend native --assembler external --linker external \
--output /tmp/fpni_ext 2>/tmp/fpni_ext.err; then
echo "EXTERNAL_COMPILE_FAIL"; head -5 /tmp/fpni_ext.err; exit 3
fi
echo "[3/3] run both and compare stdout + exit code"
# Behavioural equivalence is the sound invariant: the internal and external
# toolchains may emit DIFFERENT-but-valid encodings / section layouts (so a
# byte-level .text/.rodata compare would false-positive), but a binary built
# by the internal assembler + internal linker must BEHAVE identically to the
# all-external reference. The sret-Result bug broke exactly this (corrupted
# string headers => wrong / garbage output), so stdout + exit code catch it.
# Disable -e around the runs: a miscompiled internal binary may crash
# (e.g. SIGSEGV from a corrupted string header), and we must REPORT that as
# a guard failure rather than let -e abort the script mid-diagnostic.
set +e
/tmp/fpni_int > /tmp/fpni_int.out 2>&1; INT_RC=$?
/tmp/fpni_ext > /tmp/fpni_ext.out 2>&1; EXT_RC=$?
set -e
if [ "$INT_RC" -ne "$EXT_RC" ]; then
echo "EXIT_MISMATCH internal=$INT_RC external=$EXT_RC"
echo " (internal-assembler binary misbehaved — likely a codegen/assembler regression)"
echo "--- internal stdout ---"; cat /tmp/fpni_int.out
echo "--- external stdout ---"; cat /tmp/fpni_ext.out
exit 6
fi
if ! diff -q /tmp/fpni_int.out /tmp/fpni_ext.out >/dev/null; then
echo "OUTPUT_MISMATCH (internal assembler produced a binary that behaves differently)"
echo "--- internal ---"; cat /tmp/fpni_int.out
echo "--- external ---"; cat /tmp/fpni_ext.out
exit 6
fi
echo "NATIVE_INTERNAL_OK"
exit 0