Files
tms-c-compiler/scripts/tests/m4/diff/check.sh
T

569 lines
21 KiB
Bash
Executable File

#!/bin/sh
# M4 differential TI-compiler check: function pointers and the Xlib image ABI.
#
# scripts/tests/m4/diff/check.sh
#
# The stored ti-fnptr.s/ti-image.s are one-time outputs of the 1987 TI
# compiler (GSP C Compiler 2.01) under DOSBox. CI never runs DOSBox: this
# script greps the stored golden files and compares their ABI shape with the
# assembly our compiler emits for the equivalent C.
#
# The oracle is the ABI shape, not instruction selection:
#
# apply the old-STK save, one program-stack argument push, a call through
# an A register (TI CALL, ours CALL; CALLR is accepted too), and a
# last RETS of RETS 2. A direct CALLA in apply is a failure.
# test the direct CALLA _apply calls (at least three; TI has three,
# ours has four because the fixture calls apply again in the return).
# image XCreateImage and XPutImage take ten argument pushes, XCreatePixmap
# five, and XCreateSimpleWindow nine. Every call saves old STK.
# The first pushes are the last arguments, so the leading constants
# are XCreateImage 80,32,480,640; XPutImage 32,640; XCreatePixmap
# 1,480,640; XCreateSimpleWindow 0,0,0,480,640. The final push is
# the Display pointer, not a constant. This is what makes the
# reversed-argument negative control fail.
# offsets Display.screens (bit 0x480, byte 0x90), Screen.root (bit 0x40,
# byte 0x8), and XImage data/byte_order/bytes_per_line/bits_per_pixel/
# f (bits 0x80/0xA0/0x140/0x160/0x200). TI uses the packed direct
# displacements; ours materialises an ADDI and the matching load or
# store through the same register.
#
# The plan's "XCreateImage/XPutImage 10/9" is a typo for 10/10: XPutImage is
# (display, drawable, gc, image, src_x, src_y, dest_x, dest_y, width, height),
# ten arguments. Nine is XCreateSimpleWindow.
#
# Negative controls run at the end: the check is re-run on a copy of the
# fnptr golden with the indirect call removed, on a copy of the image golden
# with the XPutImage argument pushes fully reversed, and on a copy with only
# the Display and drawable pushes swapped. All must fail. The stored goldens
# are never modified.
#
# Our fnptr source is scripts/m4/fnptr.c (the runtime gate fixture). The image
# equivalent is generated here because it needs only the layout declarations.
#
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC TI_FNPTR TI_IMAGE
# OUR_FNPTR; M4_DIFF_POSITIVE_ONLY=1 skips the negative controls.
set -eu
ROOT=$(cd "$(dirname "$0")/../../../.." && pwd)
DIR=$ROOT/scripts/tests/m4/diff
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
TI_FNPTR=${TI_FNPTR:-$DIR/ti-fnptr.s}
TI_IMAGE=${TI_IMAGE:-$DIR/ti-image.s}
OUR_FNPTR=${OUR_FNPTR:-$ROOT/scripts/m4/fnptr.c}
export TMS_CLANG TMS_OPT TMS_BITADDR
fail() {
echo "M4 diff: FAIL: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
[ -f "$TI_FNPTR" ] || fail "stored TI golden missing: $TI_FNPTR"
[ -f "$TI_IMAGE" ] || fail "stored TI golden missing: $TI_IMAGE"
[ -f "$OUR_FNPTR" ] || fail "our fnptr source missing: $OUR_FNPTR"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m4-diff.XXXXXX")
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
echo "== compile our fnptr.s and image.s"
TMS_CLANG="$TMS_CLANG" TMS_OPT="$TMS_OPT" TMS_BITADDR="$TMS_BITADDR" \
"$ROOT/scripts/tms-clang" -O2 -o "$TMP/fnptr.ll" "$OUR_FNPTR"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm \
"$TMP/fnptr.ll" -o "$TMP/fnptr.s"
cat > "$TMP/image.c" <<'EOF'
struct display { unsigned char pad[0x90]; void *screens; };
struct screen { unsigned char pad[0x8]; unsigned int root; };
struct ximage_funcs {
void *create_image; void *destroy_image; void *get_pixel;
void *put_pixel; void *sub_image; void *add_pixel;
};
struct ximage {
int width, height; int xoffset; int format; char *data; int byte_order;
int bitmap_unit; int bitmap_bit_order; int bitmap_pad; int depth;
int bytes_per_line; int bits_per_pixel;
unsigned long red_mask, green_mask, blue_mask;
void *obdata;
struct ximage_funcs f;
};
extern struct display *XOpenDisplay(const char *);
extern int XCreateSimpleWindow(struct display *, unsigned int, int, int,
unsigned int, unsigned int, unsigned int,
unsigned long, unsigned long);
extern unsigned int XCreatePixmap(struct display *, unsigned int, unsigned int,
unsigned int, unsigned int);
extern struct ximage *XCreateImage(struct display *, void *, unsigned int, int,
int, char *, unsigned int, unsigned int,
int, int);
extern int XPutImage(struct display *, unsigned int, void *, struct ximage *,
int, int, int, int, unsigned int, unsigned int);
extern int XMapWindow(struct display *, unsigned int);
extern int XSync(struct display *, int);
__attribute__((noinline))
unsigned int image(struct display *d, unsigned int w, void *gc, char *data) {
struct ximage *img;
unsigned int root;
unsigned int pm;
root = ((struct screen *)d->screens)->root;
img = XCreateImage(d, 0, 1, 0, 0, data, 640, 480, 32, 80);
img->byte_order = 0;
img->bytes_per_line = 80;
img->bits_per_pixel = 1;
img->f.create_image = 0;
pm = XCreatePixmap(d, w, 640, 480, 1);
XPutImage(d, pm, gc, img, 0, 0, 0, 0, 640, 32);
img->data = data;
XMapWindow(d, w);
XSync(d, 0);
return root + pm;
}
__attribute__((noinline))
unsigned int window(struct display *d) {
unsigned int root = ((struct screen *)d->screens)->root;
return XCreateSimpleWindow(d, root, 20, 20, 640, 480, 0, 0, 0);
}
EOF
TMS_CLANG="$TMS_CLANG" TMS_OPT="$TMS_OPT" TMS_BITADDR="$TMS_BITADDR" \
"$ROOT/scripts/tms-clang" -O2 -o "$TMP/image.ll" "$TMP/image.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm \
"$TMP/image.ll" -o "$TMP/image.s"
echo " fnptr.s: $(grep -c . "$TMP/fnptr.s") lines, image.s: $(grep -c . "$TMP/image.s") lines"
# Our llc output has `;` end-of-line comments. Strip them so a comment cannot
# satisfy a check; TI asm has none.
for f in "$TMP/fnptr.s" "$TMP/image.s"; do
sed 's/;.*//' "$f" > "$f.code"
done
# body FILE FUNC: from `_func:` to the next function label or section directive.
body() {
awk -v want="_$2:" '
$1 == want { infn = 1; next }
infn {
if ($0 ~ /^[[:space:]]*\.(globl|sect|text|bss|data|end|file)/) exit
print
}
' "$1"
}
require() {
_file=$1
_pat=$2
_what=$3
if grep -Eiq "$_pat" "$_file"; then
echo " ok $_what"
return 0
fi
echo "M4 diff: FAIL: missing $_what" >&2
echo " pattern: $_pat" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
require_last_rets2() {
if awk '
/^[[:space:]]*RETS([[:space:]]|$)/ { last = $0; seen = 1 }
END {
if (!seen)
exit 1
exit (last ~ /^[[:space:]]*RETS[[:space:]]+2[[:space:]]*$/) ? 0 : 1
}
' "$1"; then
echo " ok $2"
return 0
fi
echo "M4 diff: FAIL: $2 (last RETS is not RETS 2)" >&2
sed 's/^/ /' "$1" >&2
exit 1
}
# call_info FILE FUNC CALLEE: one line
# "N PUSHES OLDSTK LASTDISP TOKENS..." for the first call to CALLEE in the
# function body. Each TOKENS element is a resolved decimal constant for a
# push whose source register last held a constant, or X for anything else.
# LASTDISP is 1 when the final push's source is the first-argument slot: the
# TI golden's `*FP(-32)`, or our register loaded from the ADDI -32 slot.
call_info() {
body "$1" "$2" | awk -v want="$3" '
function isreg(x) { return x ~ /^A([0-9]|1[0-5])$/ }
function trim(x) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", x); return x }
function unknown(r) { has[r] = 0 }
function operands(line, a, n, i) {
n = split(line, a, ",")
for (i = 1; i <= n; i++)
a[i] = trim(a[i])
return n
}
/^[[:space:]]*MOVE[[:space:]]/ {
line = $0
sub(/^[[:space:]]*MOVE[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 3 && a[2] ~ /^\*(STK|A14)\+$/ && a[3] == "1") {
npush++
if (isreg(a[1]) && has[a[1]])
consts = consts " " val[a[1]]
else
consts = consts " X"
if (a[1] == "*FP(-32)" ||
(isreg(a[1]) && hasdisp[a[1]]))
lastdisp = 1
else
lastdisp = 0
next
}
if (n >= 3 && a[2] ~ /^-\*SP$/ && (a[1] == "STK" || a[1] == "A14")) {
oldstk = 1
next
}
if (n >= 2 && isreg(a[2])) {
dsrc = 0
if (a[1] == "*FP(-32)")
dsrc = 1
else if (a[1] ~ /^\*A[0-9]+$/ && firstaddr[substr(a[1], 2)])
dsrc = 1
else if (isreg(a[1]) && hasdisp[a[1]])
dsrc = 1
if (a[1] ~ /^-?[0-9]+$/) {
val[a[2]] = a[1]; has[a[2]] = 1; hasdisp[a[2]] = 0
} else if (isreg(a[1]) && has[a[1]]) {
val[a[2]] = val[a[1]]; has[a[2]] = 1; hasdisp[a[2]] = dsrc
} else {
has[a[2]] = 0; hasdisp[a[2]] = dsrc
}
}
next
}
/^[[:space:]]*(MOVI|MOVK)[[:space:]]/ {
line = $0
sub(/^[[:space:]]*(MOVI|MOVK)[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 2 && isreg(a[2]) && a[1] ~ /^-?[0-9]+$/) {
val[a[2]] = a[1]; has[a[2]] = 1; hasdisp[a[2]] = 0
} else if (n >= 2 && isreg(a[2])) {
unknown(a[2])
}
next
}
/^[[:space:]]*CLR[[:space:]]/ {
line = $0
sub(/^[[:space:]]*CLR[[:space:]]+/, "", line)
if (isreg(trim(line))) {
val[trim(line)] = 0; has[trim(line)] = 1
hasdisp[trim(line)] = 0
}
next
}
/^[[:space:]]*SUB[[:space:]]/ {
line = $0
sub(/^[[:space:]]*SUB[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 2 && isreg(a[2]) && a[1] == a[2]) {
val[a[2]] = 0; has[a[2]] = 1; hasdisp[a[2]] = 0
} else if (n >= 2 && isreg(a[2])) {
unknown(a[2])
}
next
}
/^[[:space:]]*(ADDK|SUBK)[[:space:]]/ {
op = $1
line = $0
sub(/^[[:space:]]*(ADDK|SUBK)[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 2 && isreg(a[2]) && a[1] ~ /^-?[0-9]+$/ && has[a[2]]) {
val[a[2]] = val[a[2]] + (op == "ADDK" ? a[1] : -a[1])
hasdisp[a[2]] = 0
} else if (n >= 2 && isreg(a[2])) {
unknown(a[2])
}
next
}
/^[[:space:]]*ADDI[[:space:]]/ {
line = $0
sub(/^[[:space:]]*ADDI[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 2 && isreg(a[2])) {
unknown(a[2])
if (a[1] == "-32")
firstaddr[a[2]] = 1
else
firstaddr[a[2]] = 0
}
next
}
/^[[:space:]]*CALLA[[:space:]]/ {
callee = $2
sub(/^_/, "", callee)
if (callee == want) {
print npush, oldstk, lastdisp, consts
exit
}
npush = 0; oldstk = 0; lastdisp = 0; consts = ""
next
}
'
}
# check_call FILE FUNC CALLEE NPUSH "CONST PREFIX"
check_call() {
_file=$1
_func=$2
_callee=$3
_wantn=$4
_wantc=$5
_info=$(call_info "$_file" "$_func" "$_callee")
[ -n "$_info" ] || fail "$_func has no CALLA $_callee"
set -- $_info
_n=$1
_stk=$2
_disp=$3
shift 3
_tokens="$*"
[ "$_n" = "$_wantn" ] \
|| fail "$_func/$_callee has $_n argument pushes, want $_wantn"
[ "$_stk" = 1 ] || fail "$_func/$_callee does not save old STK"
case "$_tokens" in
"$_wantc "*) ;;
*)
echo "M4 diff: FAIL: $_func/$_callee leading constants are '$_tokens', want '$_wantc ...'" >&2
exit 1
;;
esac
_last=${_tokens##* }
[ "$_last" = X ] \
|| fail "$_func/$_callee last push looks like a constant, not the Display argument"
[ "$_disp" = 1 ] \
|| fail "$_func/$_callee last push is not the Display argument (first-argument slot)"
echo " ok $_func/$_callee: $_n pushes, old STK, leading [$_wantc], Display last"
}
# check_fnptr FILE WHO TESTFUNC
check_fnptr() {
_f=$1
_who=$2
_test=$3
body "$_f" apply > "$TMP/apply.body"
require "$TMP/apply.body" '^[[:space:]]*CALL(R)?[[:space:]]+A[0-9]+' \
"$_who: apply calls through an A register"
if grep -Eq '^[[:space:]]*CALLA[[:space:]]' "$TMP/apply.body"; then
sed 's/^/ /' "$TMP/apply.body" >&2
fail "$_who: apply still contains a direct CALLA"
fi
require "$TMP/apply.body" '^[[:space:]]*MOVE[[:space:]]+(STK|A14),[[:space:]]*-\*SP' \
"$_who: apply saves the old STK"
require "$TMP/apply.body" \
'^[[:space:]]*MOVE[[:space:]]+[^,]+,[[:space:]]*\*(STK|A14)\+,[[:space:]]*1' \
"$_who: apply pushes the callback argument"
require_last_rets2 "$TMP/apply.body" "$_who: apply ends with RETS 2"
body "$_f" "$_test" > "$TMP/$_test.body"
_calls=$(grep -Ec '^[[:space:]]*CALLA[[:space:]]+_apply' "$TMP/$_test.body" || true)
[ "$_calls" -ge 3 ] \
|| { sed 's/^/ /' "$TMP/$_test.body" >&2; fail "$_who: $_test makes $_calls direct CALLA _apply calls, want at least 3"; }
echo " ok $_who: $_test makes $_calls direct CALLA _apply calls (want >= 3)"
}
# has_addi_access FILE KIND DISP: an ADDI DISP,REG immediately followed by a
# MOVE that uses *REG as a load (KIND=load) or store destination (KIND=store).
has_addi_access() {
awk -v kind="$2" -v disp="$3" '
function disp_ok(d) {
if (d == disp) return 1
if (disp == "1152" && (d == "0x480" || d == "480h")) return 1
if (disp == "64" && (d == "0x40" || d == "40h")) return 1
if (disp == "128" && (d == "0x80" || d == "80h")) return 1
if (disp == "160" && (d == "0xA0" || d == "0a0h")) return 1
if (disp == "320" && (d == "0x140" || d == "140h")) return 1
if (disp == "352" && (d == "0x160" || d == "160h")) return 1
if (disp == "512" && (d == "0x200" || d == "200h")) return 1
return 0
}
/^[[:space:]]*ADDI[[:space:]]/ {
line = $0
sub(/^[[:space:]]*ADDI[[:space:]]+/, "", line)
split(line, f, /[[:space:]]*,[[:space:]]*/)
d = f[1]
reg = f[2]
if (!disp_ok(d))
next
if (getline nxt <= 0)
next
while (nxt ~ /^[[:space:]]*$/) {
if (getline nxt <= 0)
next
}
if (kind == "load" &&
nxt ~ ("^[[:space:]]*MOVE[[:space:]]+\\*" reg "([,\\(]|[[:space:]]|$)"))
found = 1
if (kind == "store" &&
nxt ~ ("^[[:space:]]*MOVE[[:space:]]+[^,]+, ?\\*" reg "([,\\(]|[[:space:]]|$)"))
found = 1
}
END { exit found ? 0 : 1 }
' "$1"
}
check_offsets() {
_f=$1
_who=$2
_side=$3
body "$_f" image > "$TMP/image.body"
if [ "$_side" = ti ]; then
require "$TMP/image.body" \
'^[[:space:]]*MOVE[[:space:]]+\*(A[0-9]+|FP|SP)\((1152|0x480|480h)\),' \
"$_who: Display.screens is loaded at bit 0x480 (byte 0x90)"
require "$TMP/image.body" \
'^[[:space:]]*MOVE[[:space:]]+\*(A[0-9]+|FP|SP)\((64|0x40|40h)\),[[:space:]]*\*(A[0-9]+|FP|SP)\([0-9]+\)' \
"$_who: Screen.root is loaded at bit 0x40 (byte 0x8)"
for pair in 128:data 160:byte_order 320:bytes_per_line 352:bits_per_pixel 512:f; do
d=${pair%%:*}
name=${pair##*:}
case "$d" in
128) pat='(128|0x80|80h)' ;;
160) pat='(160|0xA0|0a0h)' ;;
320) pat='(320|0x140|140h)' ;;
352) pat='(352|0x160|160h)' ;;
512) pat='(512|0x200|200h)' ;;
esac
require "$TMP/image.body" \
"^[[:space:]]*MOVE[[:space:]]+.*\\*A[0-9]+\\($pat\\)," \
"$_who: XImage.$name is at bit $d"
done
return 0
fi
for pair in load:1152:screens load:64:root store:128:data store:160:byte_order \
store:320:bytes_per_line store:352:bits_per_pixel store:512:f; do
kind=${pair%%:*}
rest=${pair#*:}
d=${rest%%:*}
name=${rest##*:}
if has_addi_access "$TMP/image.body" "$kind" "$d"; then
echo " ok $_who: XImage/Display/Screen $name access at bit $d (ADDI + $kind)"
else
sed 's/^/ /' "$TMP/image.body" >&2
fail "$_who: no $kind through bit displacement $d for $name"
fi
done
}
# check_image FILE WHO TI|ours
check_image() {
_f=$1
_who=$2
_side=$3
check_call "$_f" image XCreateImage 10 "80 32 480 640"
check_call "$_f" image XCreatePixmap 5 "1 480 640"
check_call "$_f" image XPutImage 10 "32 640"
check_call "$_f" window XCreateSimpleWindow 9 "0 0 0 480 640"
body "$_f" image > "$TMP/rets.body"
require_last_rets2 "$TMP/rets.body" "$_who: image ends with RETS 2"
body "$_f" window > "$TMP/rets2.body"
require_last_rets2 "$TMP/rets2.body" "$_who: window ends with RETS 2"
check_offsets "$_f" "$_who" "$_side"
}
echo "== ABI shape: TI fnptr golden"
check_fnptr "$TI_FNPTR" "TI" test
echo "== ABI shape: our fnptr"
check_fnptr "$TMP/fnptr.s.code" "ours" fnptr_test
echo "== ABI shape: TI image golden"
check_image "$TI_IMAGE" "TI" ti
echo "== ABI shape: our image"
check_image "$TMP/image.s.code" "ours" ours
if [ "${M4_DIFF_POSITIVE_ONLY:-0}" = 1 ]; then
echo "M4 diff: PASS"
exit 0
fi
echo "== negative controls"
# 1. Remove the indirect call from the fnptr golden: apply must then fail.
sed 's/^[[:space:]]*CALL[[:space:]].*/ CALLA _dec/' "$TI_FNPTR" \
> "$TMP/fnptr-no-indirect.s"
grep -q 'CALLA _dec' "$TMP/fnptr-no-indirect.s" \
|| fail "could not construct the no-indirect-call fnptr golden"
if M4_DIFF_POSITIVE_ONLY=1 TI_FNPTR="$TMP/fnptr-no-indirect.s" \
"$0" > "$TMP/neg-fnptr.out" 2>&1; then
sed 's/^/ /' "$TMP/neg-fnptr.out" >&2
fail "check accepted a fnptr golden without the indirect call"
fi
echo " ok negative control: fnptr without the indirect call rejected"
grep 'M4 diff: FAIL' "$TMP/neg-fnptr.out" | head -1 | sed 's/^/ /'
# 2. Reverse the ten XPutImage argument pushes: Display must no longer be the
# last push and the leading constants must change.
awk '
{ lines[NR] = $0 }
/^[[:space:]]*MOVE[[:space:]]+[^,]+,[[:space:]]*\*STK\+,[[:space:]]*1[[:space:]]*$/ {
npush++
pos[npush] = NR
push[npush] = $0
next
}
/^[[:space:]]*CALLA[[:space:]]+_XPutImage/ {
for (i = 1; i <= npush; i++)
lines[pos[i]] = push[npush - i + 1]
npush = 0
next
}
/^[[:space:]]*CALLA[[:space:]]/ { npush = 0; next }
END { for (i = 1; i <= NR; i++) print lines[i] }
' "$TI_IMAGE" > "$TMP/image-reversed.s"
if cmp -s "$TI_IMAGE" "$TMP/image-reversed.s"; then
fail "reversed-argument mutation did not change the image golden"
fi
if M4_DIFF_POSITIVE_ONLY=1 TI_IMAGE="$TMP/image-reversed.s" \
"$0" > "$TMP/neg-image.out" 2>&1; then
sed 's/^/ /' "$TMP/neg-image.out" >&2
fail "check accepted the reversed XPutImage argument order"
fi
echo " ok negative control: reversed XPutImage arguments rejected"
grep 'M4 diff: FAIL' "$TMP/neg-image.out" | head -1 | sed 's/^/ /'
# 3. Swap only the last two XPutImage pushes (Display and drawable). The
# leading constants and the non-constant last-token rule still hold, so
# only the first-argument (Display) check can reject this.
awk '
{ lines[NR] = $0 }
/^[[:space:]]*MOVE[[:space:]]+[^,]+,[[:space:]]*\*STK\+,[[:space:]]*1[[:space:]]*$/ {
npush++
pos[npush] = NR
next
}
/^[[:space:]]*CALLA[[:space:]]+_XPutImage/ {
if (npush >= 2) {
tmp = lines[pos[npush]]
lines[pos[npush]] = lines[pos[npush - 1]]
lines[pos[npush - 1]] = tmp
}
npush = 0
next
}
/^[[:space:]]*CALLA[[:space:]]/ { npush = 0; next }
END { for (i = 1; i <= NR; i++) print lines[i] }
' "$TI_IMAGE" > "$TMP/image-swapped.s"
if cmp -s "$TI_IMAGE" "$TMP/image-swapped.s"; then
fail "Display/drawable swap mutation did not change the image golden"
fi
if M4_DIFF_POSITIVE_ONLY=1 TI_IMAGE="$TMP/image-swapped.s" \
"$0" > "$TMP/neg-swap.out" 2>&1; then
sed 's/^/ /' "$TMP/neg-swap.out" >&2
fail "check accepted the Display/drawable push swap"
fi
echo " ok negative control: Display swapped with the drawable rejected"
grep 'M4 diff: FAIL' "$TMP/neg-swap.out" | head -1 | sed 's/^/ /'
echo "M4 diff: PASS"