test(native): add internal-assembler conformance guard
Neither fixpoint exercises the in-process internal assembler (--assembler internal): fixpoint.sh assembles via qbe+gcc, fixpoint-native.sh assembles the native .s with gcc. A miscompilation that only corrupts the internal assembler's object output therefore passes both fixpoints cleanly — which is exactly how the sret-Result field-read bug (fixed in the previous commit) escaped the fixpoint gate. scripts/fixpoint-native-internal.sh closes that gap. It compiles a small representative program (record-returning function with sret-Result field reads, plus immutable string literals) with BOTH --assembler internal and --assembler external, then asserts the two binaries behave identically (stdout + exit code). Behavioural equivalence is the sound invariant: the two assemblers may emit different-but-valid encodings, so a byte-level section compare would false-positive. A true self-hosting internal-assembler fixpoint (compile the compiler with itself via --assembler internal) is deferred: the internal assembler buffers the whole object in memory and needs ~53 GB for the compiler's own 631k-line .s, OOM-killing. That scalability limit is tracked in bugs.txt; until the assembler streams its output, this differential conformance check plus TInternalAsmE2ETests are the internal-assembler guards. Verified: prints NATIVE_INTERNAL_OK on a fixed compiler, exits non-zero with EXIT_MISMATCH on a compiler carrying the sret-Result bug.
This commit is contained in:
parent
eee7f54284
commit
0631bb0cdf
143
scripts/fixpoint-native-internal.sh
Executable file
143
scripts/fixpoint-native-internal.sh
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
#!/bin/bash
|
||||
# Internal-assembler conformance check for the Blaise self-hosting toolchain.
|
||||
#
|
||||
# WHY THIS EXISTS, separately from fixpoint-native.sh:
|
||||
# fixpoint-native.sh validates native CODEGEN by diffing the emitted .s
|
||||
# text, but it assembles that text with the EXTERNAL assembler (gcc).
|
||||
# The in-process internal assembler (blaise.assembler.x86_64, reached via
|
||||
# --assembler internal) is a DIFFERENT .s -> object path that NEITHER
|
||||
# fixpoint exercises. A miscompilation that only corrupts the internal
|
||||
# assembler's object output therefore passes both fixpoints cleanly
|
||||
# (this is exactly how the sret-Result field-read bug hid; see bugs.txt
|
||||
# 2026-06-16).
|
||||
#
|
||||
# WHY THIS 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
|
||||
# --assembler internal" 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-assembler binary BEHAVES identically (stdout + exit code) to
|
||||
# the trusted external-assembler binary. Note: the two assemblers may emit
|
||||
# different-but-valid encodings, so a byte-level section 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"
|
||||
if ! "$COMPILER" --source "$PROBE" $UNIT_ARGS \
|
||||
--backend native --assembler 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 (reference)"
|
||||
if ! "$COMPILER" --source "$PROBE" $UNIT_ARGS \
|
||||
--backend native --assembler 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
|
||||
# assemblers may emit DIFFERENT-but-valid encodings for the same .s (so a
|
||||
# byte-level .text/.rodata compare would false-positive), but a binary built
|
||||
# by the internal assembler must BEHAVE identically to the external-assembler
|
||||
# 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
|
||||
Loading…
Reference in a new issue