phase6_instrumentation.sh raw

   1  #!/bin/bash
   2  # Phase 6: build tags and allocation instrumentation.
   3  #
   4  # -tags and -print-allocs are compiler features whose whole point is the
   5  # binary they produce, so every check here builds AND runs the result. Both
   6  # compilers must behave identically: legacy is the bootstrap reference and
   7  # stage4 has to mirror it.
   8  set -euo pipefail
   9  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  10  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
  11  export MOXIEROOT="$ROOT"
  12  source "$SCRIPT_DIR/util.sh"
  13  
  14  LEGACY="${LEGACY:-$ROOT/legacy/moxie}"
  15  
  16  echo ""
  17  echo "=== Phase 6: build tags and allocation instrumentation ==="
  18  
  19  if [ ! -x "$LEGACY" ]; then
  20  	log_skip "phase6" "legacy compiler not built at $LEGACY"
  21  	exit 0
  22  fi
  23  
  24  # The legacy compiler is a prebuilt binary and the suite never builds it. A
  25  # stale one silently disagrees with its own source (a changed logAlloc
  26  # signature showed up as an LLVM verifier failure), so say so instead of
  27  # reporting a compiler bug that is not there.
  28  if [ -n "$(find "$ROOT/legacy" -name '*.go' -newer "$LEGACY" -print -quit 2>/dev/null)" ]; then
  29  	log_skip "phase6" "legacy compiler older than its source; run legacy/build.sh"
  30  	exit 0
  31  fi
  32  
  33  # fixture <name> copies a fixture directory out of the tree. In-tree
  34  # directories are built in place by the rest of the suite, but these tests
  35  # want a scratch copy so a stray build product never lands in the repo.
  36  fixture() {
  37  	local name="$1"
  38  	local d
  39  	d="$(tempdir "fx_$name")"
  40  	rm -rf "$d"; mkdir -p "$d"
  41  	cp "$SCRIPT_DIR/data/$name"/*.mx "$d/"
  42  	# A fixture that exercises module manifests carries its own.
  43  	if [ -f "$SCRIPT_DIR/data/$name/moxie.mod" ]; then
  44  		cp "$SCRIPT_DIR/data/$name/moxie.mod" "$d/"
  45  	fi
  46  	echo "$d"
  47  }
  48  
  49  # 6a: the stage4 regexp matcher must agree with the Go regexp legacy uses.
  50  # tests/tools/regexp_ref is the reference (it must be Go: it calls Go's
  51  # regexp). Cases are a shared file so the two sides cannot drift.
  52  name="regexp:table"
  53  t0=$(date +%s%N)
  54  td="$(tempdir "regexp")"; rm -rf "$td"; mkdir -p "$td"
  55  rxdir="$(fixture regexp)"
  56  rc=0
  57  MOXIEROOT="$MOXIEROOT" "$MOXIE" build -o "$td/rx_moxie" "$rxdir" >"$td/build.log" 2>&1 || rc=$?
  58  if [ $rc -ne 0 ]; then
  59  	log_fail "$name" "stage4 build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
  60  else
  61  	"$td/rx_moxie" "$SCRIPT_DIR/data/regexp/cases.txt" >"$td/moxie.txt" 2>&1 || rc=$?
  62  	if [ $rc -ne 0 ]; then
  63  		log_fail "$name" "case program exited $rc"
  64  	elif ! command -v go >/dev/null 2>&1; then
  65  		log_skip "$name" "go not available for the regexp reference"
  66  	else
  67  		# shellcheck disable=SC2086
  68  		go run "$SCRIPT_DIR/tools/regexp_ref/main.go" "$SCRIPT_DIR/data/regexp/cases.txt" >"$td/go.txt" 2>"$td/go.err" || rc=$?
  69  		if [ $rc -ne 0 ]; then
  70  			log_fail "$name" "reference failed: $(tail -2 "$td/go.err" | tr '\n' ' ')"
  71  		elif cmp -s "$td/go.txt" "$td/moxie.txt"; then
  72  			t1=$(date +%s%N)
  73  			log_pass "$name" "$(wc -l < "$td/moxie.txt") cases in $(( (t1 - t0) / 1000000 ))ms"
  74  		else
  75  			log_fail "$name" "first difference: $(diff "$td/go.txt" "$td/moxie.txt" | head -3 | tr '\n' ' ')"
  76  		fi
  77  	fi
  78  fi
  79  rm -rf "$td" "$rxdir"
  80  
  81  # 6b: -tags selects a //:build-gated file, in both compilers, including the
  82  # comma-separated and negated forms.
  83  for cc in "$MOXIE" "$LEGACY"; do
  84  	label="stage4"
  85  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
  86  	td="$(tempdir "tags_$label")"; rm -rf "$td"; mkdir -p "$td"
  87  	tdir="$(fixture tags)"
  88  	for spec in "none:plain" "probe_tag:tagged" "a,probe_tag:tagged" "!probe_tag:plain"; do
  89  		tagset="${spec%%:*}"
  90  		want="${spec##*:}"
  91  		name="tags:$label:${tagset}"
  92  		args=()
  93  		[ "$tagset" = "none" ] || args=(-tags "$tagset")
  94  		rc=0
  95  		MOXIEROOT="$MOXIEROOT" "$cc" build "${args[@]}" -o "$td/bin" "$tdir" >"$td/build.log" 2>&1 || rc=$?
  96  		if [ $rc -ne 0 ]; then
  97  			log_fail "$name" "build failed: $(tail -2 "$td/build.log" | tr '\n' ' ')"
  98  			continue
  99  		fi
 100  		got="$("$td/bin" 2>&1 | head -1)"
 101  		if [ "$got" = "$want" ]; then
 102  			log_pass "$name" "ok"
 103  		else
 104  			log_fail "$name" "got '$got', want '$want'"
 105  		fi
 106  	done
 107  	rm -rf "$td" "$tdir"
 108  done
 109  
 110  # 6c/6d: allocation instrumentation. The site table must list the matched
 111  # function, the runtime must count that site, and without the alloc_trace tag
 112  # the same call must reach the no-op stub instead.
 113  for cc in "$MOXIE" "$LEGACY"; do
 114  	label="stage4"
 115  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 116  
 117  	# instrumented build: counters fire
 118  	name="allocs:$label:matched"
 119  	td="$(tempdir "allocs_$label")"; rm -rf "$td"; mkdir -p "$td"
 120  	adir="$(fixture allocs)"
 121  	rc=0
 122  	MOXIEROOT="$MOXIEROOT" "$cc" build -tags alloc_trace -print-allocs allocA \
 123  		-o "$td/bin" "$adir" >"$td/build.log" 2>&1 || rc=$?
 124  	if [ $rc -ne 0 ]; then
 125  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 126  	else
 127  		msg=""
 128  		[ -f "$td/bin.alloc_sites.json" ] || msg="no alloc_sites.json"
 129  		if [ -z "$msg" ]; then
 130  			sites=$(grep -o '"id":' "$td/bin.alloc_sites.json" | wc -l)
 131  			[ "$sites" -eq 1 ] || msg="expected 1 site, got $sites"
 132  		fi
 133  		if [ -z "$msg" ] && ! grep -q '"name":"[^"]*allocA' "$td/bin.alloc_sites.json"; then
 134  			msg="site table does not name allocA: $(cat "$td/bin.alloc_sites.json")"
 135  		fi
 136  		if [ -z "$msg" ]; then
 137  			out="$("$td/bin" 2>&1)"
 138  			got="$(echo "$out" | grep -c 'allocsite .* count=[1-9]')"
 139  			[ "$got" -ge 1 ] || msg="no site counted: $(echo "$out" | tr '\n' ',')"
 140  		fi
 141  		if [ -z "$msg" ]; then
 142  			log_pass "$name" "$(echo "$out" | grep allocsite | head -1)"
 143  		else
 144  			log_fail "$name" "$msg"
 145  		fi
 146  	fi
 147  
 148  	# matched function is the only thing instrumented
 149  	name="allocs:$label:selector"
 150  	rc=0
 151  	MOXIEROOT="$MOXIEROOT" "$cc" build -tags alloc_trace -print-allocs allocB \
 152  		-o "$td/binB" "$adir" >"$td/buildB.log" 2>&1 || rc=$?
 153  	if [ $rc -ne 0 ]; then
 154  		log_fail "$name" "build failed: $(tail -3 "$td/buildB.log" | tr '\n' ' ')"
 155  	elif ! grep -q '"name":"[^"]*allocB' "$td/binB.alloc_sites.json"; then
 156  		log_fail "$name" "site table does not name allocB: $(cat "$td/binB.alloc_sites.json" 2>/dev/null)"
 157  	else
 158  		out="$("$td/binB" 2>&1)"
 159  		line="$(echo "$out" | grep allocsite | head -1)"
 160  		if [ -n "$line" ] && echo "$line" | grep -q 'bytes=32'; then
 161  			log_pass "$name" "$line"
 162  		else
 163  			log_fail "$name" "expected a 32-byte site, got: $(echo "$out" | tr '\n' ',')"
 164  		fi
 165  	fi
 166  
 167  	# untagged build: the same call reaches the stub, so nothing is counted
 168  	name="allocs:$label:untagged"
 169  	rc=0
 170  	MOXIEROOT="$MOXIEROOT" "$cc" build -print-allocs allocA \
 171  		-o "$td/binU" "$adir" >"$td/buildU.log" 2>&1 || rc=$?
 172  	if [ $rc -ne 0 ]; then
 173  		log_fail "$name" "build failed: $(tail -3 "$td/buildU.log" | tr '\n' ' ')"
 174  	else
 175  		out="$("$td/binU" 2>&1)"
 176  		if echo "$out" | grep -q allocsite; then
 177  			log_fail "$name" "untagged build still counted: $(echo "$out" | grep allocsite | head -1)"
 178  		else
 179  			log_pass "$name" "no counters without -tags alloc_trace"
 180  		fi
 181  	fi
 182  
 183  	# an unusable pattern is rejected before anything is compiled
 184  	name="allocs:$label:bad-pattern"
 185  	rc=0
 186  	MOXIEROOT="$MOXIEROOT" "$cc" build -print-allocs 'a(' \
 187  		-o "$td/binBad" "$adir" >"$td/bad.log" 2>&1 || rc=$?
 188  	if [ $rc -eq 0 ]; then
 189  		log_fail "$name" "unbalanced pattern accepted"
 190  	else
 191  		log_pass "$name" "rejected"
 192  	fi
 193  
 194  	rm -rf "$td" "$adir"
 195  done
 196  
 197  # 6e: package-level float globals keep their initializer in both compilers.
 198  # The legacy interp read a constant float with LLVMConstRealGetDouble, whose
 199  # double return travels in XMM0 while the purego binding read RAX, so every
 200  # package-level float32/float64 variable was silently initialized to zero
 201  # (integers return in RAX and were fine).
 202  for cc in "$MOXIE" "$LEGACY"; do
 203  	label="stage4"
 204  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 205  	name="globalinit:$label"
 206  	td="$(tempdir "globalinit_$label")"; rm -rf "$td"; mkdir -p "$td"
 207  	gdir="$(fixture globalinit)"
 208  	rc=0
 209  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$gdir" >"$td/build.log" 2>&1 || rc=$?
 210  	if [ $rc -ne 0 ]; then
 211  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 212  	else
 213  		out="$("$td/bin" 2>&1 || true)"
 214  		if [ "$(echo "$out" | head -1)" = "ok" ]; then
 215  			log_pass "$name" "package-level float globals initialized"
 216  		else
 217  			log_fail "$name" "got '$(echo "$out" | head -1)', want 'ok'"
 218  		fi
 219  	fi
 220  	rm -rf "$td" "$gdir"
 221  done
 222  
 223  # 6f: len()/cap() on a channel report the ring in both compilers. stage4's
 224  # len/cap lowering had no channel arm and returned zero, while legacy called
 225  # runtime.chanLen/chanCap.
 226  for cc in "$MOXIE" "$LEGACY"; do
 227  	label="stage4"
 228  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 229  	name="chanlen:$label"
 230  	td="$(tempdir "chanlen_$label")"; rm -rf "$td"; mkdir -p "$td"
 231  	cdir="$(fixture chanlen)"
 232  	rc=0
 233  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$cdir" >"$td/build.log" 2>&1 || rc=$?
 234  	if [ $rc -ne 0 ]; then
 235  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 236  	else
 237  		out="$("$td/bin" 2>&1 || true)"
 238  		if [ "$(echo "$out" | head -1)" = "ok" ]; then
 239  			log_pass "$name" "channel len/cap report the ring"
 240  		else
 241  			log_fail "$name" "got '$(echo "$out" | head -1)', want 'ok'"
 242  		fi
 243  	fi
 244  	rm -rf "$td" "$cdir"
 245  done
 246  
 247  # 6g: testing.T.Fatal/Skip abort the test body and the run continues with the
 248  # next test. This is the cross-frame recover path: stage4 never armed the defer
 249  # frame's JumpPC (call lines are emitted in fragments), and explicit
 250  # panic("const") went to the abort-only runtime._panicstr.
 251  for cc in "$MOXIE" "$LEGACY"; do
 252  	label="stage4"
 253  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 254  	name="fatal:$label"
 255  	td="$(tempdir "fatal_$label")"; rm -rf "$td"; mkdir -p "$td"
 256  	fdir="$(fixture fatal)"
 257  	rc=0
 258  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$fdir" >"$td/build.log" 2>&1 || rc=$?
 259  	if [ $rc -ne 0 ]; then
 260  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 261  	else
 262  		out="$("$td/bin" 2>&1 || true)"
 263  		msg=""
 264  		if echo "$out" | grep -q "unreachable after Fatal"; then msg="Fatal did not abort the test body"; fi
 265  		if [ -z "$msg" ] && echo "$out" | grep -q "unreachable after Skip"; then msg="Skip did not abort the test body"; fi
 266  		if [ -z "$msg" ] && ! echo "$out" | grep -q "second-ran"; then msg="the run did not continue to the next test"; fi
 267  		if [ -z "$msg" ] && ! echo "$out" | grep -q -e "--- FAIL: TestStopsAtFatal"; then msg="the Fatal test was not reported as failed"; fi
 268  		if [ -z "$msg" ] && ! echo "$out" | grep -q -e "--- SKIP: TestStopsAtSkip"; then msg="the Skip test was not reported as skipped"; fi
 269  		if [ -z "$msg" ]; then
 270  			log_pass "$name" "Fatal/Skip abort and the run continues"
 271  		else
 272  			log_fail "$name" "$msg"
 273  		fi
 274  	fi
 275  	rm -rf "$td" "$fdir"
 276  done
 277  
 278  # 6h: the compiler's build identity is part of the cache directory, so a stale
 279  # binary cannot seed the next compiler's cache with objects it compiled under
 280  # the old sources but keyed on the new source hash. Two binaries from the same
 281  # tree (same source hash) must use different cache dirs.
 282  name="cacheid:stage4"
 283  td="$(tempdir cacheid)"; rm -rf "$td"; mkdir -p "$td"
 284  cp "$MOXIE" "$td/a"
 285  cp "$MOXIE" "$td/b"
 286  printf 'x' >> "$td/b"
 287  chmod +x "$td/b"
 288  ca="$(MOXIEROOT="$MOXIEROOT" "$td/a" env 2>/dev/null | sed -n 's/^CACHE="\(.*\)"$/\1/p' | head -1)"
 289  cb="$(MOXIEROOT="$MOXIEROOT" "$td/b" env 2>/dev/null | sed -n 's/^CACHE="\(.*\)"$/\1/p' | head -1)"
 290  if [ -z "$ca" ]; then
 291  	log_fail "$name" "could not read CACHE from mxc env"
 292  elif [ -z "$cb" ]; then
 293  	log_skip "$name" "the modified binary did not run (signed binary?)"
 294  elif [ "$ca" = "$cb" ]; then
 295  	log_fail "$name" "cache dir does not carry the build identity: $ca"
 296  else
 297  	log_pass "$name" "cache dir differs with the binary"
 298  fi
 299  rm -rf "$td"
 300  
 301  # 6i: untyped int/float constant promotion in both compilers. stage4's
 302  # SSA-builder fold used the left (int) operand's type, so `1/2.0` folded to
 303  # the int constant 0 and `1/Ln2` truncated - which also mis-sized math/big's
 304  # base-10 conversion buffer.
 305  for cc in "$MOXIE" "$LEGACY"; do
 306  	label="stage4"
 307  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 308  	name="constpromo:$label"
 309  	td="$(tempdir "constpromo_$label")"; rm -rf "$td"; mkdir -p "$td"
 310  	pdir="$(fixture constpromo)"
 311  	rc=0
 312  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$pdir" >"$td/build.log" 2>&1 || rc=$?
 313  	if [ $rc -ne 0 ]; then
 314  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 315  	else
 316  		out="$("$td/bin" 2>&1 || true)"
 317  		if [ "$(echo "$out" | head -1)" = "ok" ]; then
 318  			log_pass "$name" "untyped int/float promotion"
 319  		else
 320  			log_fail "$name" "got '$(echo "$out" | head -1)', want 'ok'"
 321  		fi
 322  	fi
 323  	rm -rf "$td" "$pdir"
 324  done
 325  
 326  # 6j: clear() zeroes a slice or a map in both compilers. stage4's builtin
 327  # emitter had no clear arm, so the call was dropped: math/big's basicMul
 328  # starts with clear(z[0:len(x)+len(y)]) and accumulates into z, so a reused
 329  # receiver kept its previous product's digits (Exp(10,20) = 10^20+10^5) and
 330  # a 300-digit Text(10) divided by a stale zero and died with SIGFPE.
 331  for cc in "$MOXIE" "$LEGACY"; do
 332  	label="stage4"
 333  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 334  	name="clearzero:$label"
 335  	td="$(tempdir "clearzero_$label")"; rm -rf "$td"; mkdir -p "$td"
 336  	zdir="$(fixture clearzero)"
 337  	rc=0
 338  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$zdir" >"$td/build.log" 2>&1 || rc=$?
 339  	if [ $rc -ne 0 ]; then
 340  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 341  	else
 342  		out="$("$td/bin" 2>&1 || true)"
 343  		if [ "$(echo "$out" | head -1)" = "ok" ]; then
 344  			log_pass "$name" "clear zeroes slices and maps"
 345  		else
 346  			log_fail "$name" "got '$(echo "$out" | head -1)', want 'ok'"
 347  		fi
 348  	fi
 349  	rm -rf "$td" "$zdir"
 350  done
 351  
 352  # 6k: the spawn ring's index word is the parking word. A receiver parked on the
 353  # write index must be woken by the publish (no lost wakeup), and a parent
 354  # parked on a ring whose child died must unpark, report the closed channel and
 355  # not hang. Before this both sides polled with a 1ms sleep.
 356  #
 357  # The same fixture also pins the discarded-receive ABI: `<-done` with no
 358  # assignment passes a nil destination from the legacy compiler (stage4 passes a
 359  # scratch), and the runtime wrote the delivered value through it - a
 360  # legacy-built spawn program died in memcpy right after its first exchange.
 361  # recvStore/recvZero in runtime.mx treat a nil destination as "discard", so
 362  # both compilers run this now.
 363  for cc in "$MOXIE" "$LEGACY"; do
 364  	label="stage4"
 365  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 366  	name="futexpark:$label"
 367  	td="$(tempdir "futexpark_$label")"; rm -rf "$td"; mkdir -p "$td"
 368  	fpdir="$(fixture futexpark)"
 369  	rc=0
 370  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$fpdir" >"$td/build.log" 2>&1 || rc=$?
 371  	if [ $rc -ne 0 ]; then
 372  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 373  	else
 374  		# A hang is the failure mode under test, so cap the run.
 375  		out="$(timeout 60 "$td/bin" 2>&1 || true)"
 376  		if echo "$out" | grep -q "^wake ok$" && echo "$out" | grep -q "^dead ok$"; then
 377  			log_pass "$name" "park wakes on publish and unparks on peer death"
 378  		else
 379  			log_fail "$name" "got '$(echo "$out" | tr '\n' ' ')'"
 380  		fi
 381  	fi
 382  	rm -rf "$td" "$fpdir"
 383  done
 384  
 385  # 6l: untyped float constants are exact rationals, in one package and across
 386  # the .mxh boundary. stage4 compared the rounded float64 and exported a folded
 387  # constant as its shortest decimal, so `math.MaxFloat64 ==
 388  # 1.7976931348623157e308` was true where go/types (legacy) says false.
 389  for cc in "$MOXIE" "$LEGACY"; do
 390  	label="stage4"
 391  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 392  	name="exactfloat:$label"
 393  	td="$(tempdir "exactfloat_$label")"; rm -rf "$td"; mkdir -p "$td"
 394  	edir="$(fixture exactfloat)"
 395  	rc=0
 396  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$edir" >"$td/build.log" 2>&1 || rc=$?
 397  	if [ $rc -ne 0 ]; then
 398  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 399  	else
 400  		out="$("$td/bin" 2>&1 || true)"
 401  		if [ "$(echo "$out" | head -1)" = "ok" ]; then
 402  			log_pass "$name" "untyped float constants are exact"
 403  		else
 404  			log_fail "$name" "got '$(echo "$out" | head -1)', want 'ok'"
 405  		fi
 406  	fi
 407  	rm -rf "$td" "$edir"
 408  done
 409  
 410  # 6m: the coverage join is one implementation (pkg/mxcover) shared by the
 411  # mxcover command and `moxie test -cover`, so both print identical text. The
 412  # same fixture and expected report are asserted by legacy/cover's Go test for
 413  # the legacy harness (legacy's own `moxie test` cannot build these packages in
 414  # this tree, so its report is locked by that unit test instead).
 415  name="coverreport:stage4"
 416  td="$(tempdir "coverreport")"; rm -rf "$td"; mkdir -p "$td"
 417  rc=0
 418  MOXIEROOT="$MOXIEROOT" "$MOXIE" build -o "$td/mxcover" "$ROOT/cmd/mxcover" >"$td/build.log" 2>&1 || rc=$?
 419  if [ $rc -ne 0 ]; then
 420  	log_fail "$name" "mxcover build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 421  else
 422  	"$td/mxcover" "$SCRIPT_DIR/data/mxcover/sites.json" "$SCRIPT_DIR/data/mxcover/counts" "git.smesh.lol/smesh/" >"$td/got.txt" 2>&1 || rc=$?
 423  	if [ $rc -ne 0 ]; then
 424  		log_fail "$name" "mxcover exited $rc: $(tail -2 "$td/got.txt" | tr '\n' ' ')"
 425  	elif cmp -s "$td/got.txt" "$SCRIPT_DIR/data/mxcover/want.txt"; then
 426  		log_pass "$name" "report matches the shared fixture"
 427  	else
 428  		log_fail "$name" "report differs: $(diff "$SCRIPT_DIR/data/mxcover/want.txt" "$td/got.txt" | head -3 | tr '\n' ' ')"
 429  	fi
 430  fi
 431  rm -rf "$td"
 432  
 433  # 6n: the reporter's parser and arithmetic units, run by the compiler that
 434  # ships them (`moxie test ./pkg/mxcover`). pkg/mxcover is a package so the
 435  # harness can print the same report the command does.
 436  name="coverunit:stage4"
 437  td="$(tempdir "coverunit")"; rm -rf "$td"; mkdir -p "$td"
 438  rc=0
 439  out="$(MOXIEROOT="$MOXIEROOT" timeout 900 "$MOXIE" test "$ROOT/pkg/mxcover" 2>&1)" || rc=$?
 440  if [ $rc -eq 0 ] && echo "$out" | grep -q "^PASS: "; then
 441  	log_pass "$name" "$(echo "$out" | grep '^PASS: ')"
 442  else
 443  	log_fail "$name" "$(echo "$out" | tail -3 | tr '\n' ' ')"
 444  fi
 445  rm -rf "$td"
 446  
 447  # 6o: a shift whose left operand is an untyped constant is typed by its
 448  # context, not by the count. stage4 typed `1 << c` (c a byte) as uint8, so the
 449  # emitter's overshift select (Go: a count >= the width shifts to zero) zeroed
 450  # every count >= 8 - `bytes.TrimRight(s, "\r\n")` never trimmed and the
 451  # mediaproxy header loop read past the blank line. legacy was already right.
 452  for cc in "$MOXIE" "$LEGACY"; do
 453  	label="stage4"
 454  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 455  	name="shiftconst:$label"
 456  	td="$(tempdir "shiftconst_$label")"; rm -rf "$td"; mkdir -p "$td"
 457  	sdir="$(fixture shiftconst)"
 458  	rc=0
 459  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$sdir" >"$td/build.log" 2>&1 || rc=$?
 460  	if [ $rc -ne 0 ]; then
 461  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 462  	else
 463  		out="$("$td/bin" 2>&1 || true)"
 464  		if [ "$(echo "$out" | head -1)" = "ok" ]; then
 465  			log_pass "$name" "untyped-constant shift takes the context type"
 466  		else
 467  			log_fail "$name" "got '$(echo "$out" | head -1)', want 'ok'"
 468  		fi
 469  	fi
 470  	rm -rf "$td" "$sdir"
 471  done
 472  
 473  # 6p: a reslice is clamped to its parent instead of producing a negative
 474  # length. `s[1:]` on an empty slice was len -1, so the tree's
 475  # `for ; len(r) > 0; r = r[1:]` loops left `r` holding a negative length and
 476  # the `if len(r) == 0` guards that follow never fired; a truncated REQ filter
 477  # then read past the end and killed the relay.
 478  for cc in "$MOXIE" "$LEGACY"; do
 479  	label="stage4"
 480  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 481  	name="sliceres:$label"
 482  	td="$(tempdir "sliceres_$label")"; rm -rf "$td"; mkdir -p "$td"
 483  	srdir="$(fixture sliceres)"
 484  	rc=0
 485  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$srdir" >"$td/build.log" 2>&1 || rc=$?
 486  	if [ $rc -ne 0 ]; then
 487  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 488  	else
 489  		out="$("$td/bin" 2>&1 || true)"
 490  		if [ "$(echo "$out" | head -1)" = "ok" ]; then
 491  			log_pass "$name" "a reslice clamps to its parent"
 492  		else
 493  			log_fail "$name" "got '$(echo "$out" | head -1)', want 'ok'"
 494  		fi
 495  	fi
 496  	rm -rf "$td" "$srdir"
 497  done
 498  
 499  # 6q: the ChaCha20-Poly1305 AEAD must produce the RFC 8439 vector when it is a
 500  # dependency of an application, not only when its own package is the build
 501  # root. The ported sealGeneric split one allocation into a head and a tail view
 502  # (the Go sliceForAppend idiom); Moxie relocates each returned slice on its
 503  # own, so the tail came back as a copy and Seal answered all zeros - which is
 504  # why Marmot DMs could not decrypt in an application build.
 505  for cc in "$MOXIE" "$LEGACY"; do
 506  	label="stage4"
 507  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 508  	name="aeaddep:$label"
 509  	td="$(tempdir "aeaddep_$label")"; rm -rf "$td"; mkdir -p "$td"
 510  	addir="$(fixture aeaddep)"
 511  	rc=0
 512  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$addir" >"$td/build.log" 2>&1 || rc=$?
 513  	if [ $rc -ne 0 ]; then
 514  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 515  	else
 516  		out="$("$td/bin" 2>&1 || true)"
 517  		if echo "$out" | grep -q "^ok d3 91$"; then
 518  			log_pass "$name" "the AEAD known-answer vector holds as a dependency"
 519  		else
 520  			log_fail "$name" "got '$(echo "$out" | head -1)', want 'ok d3 91'"
 521  		fi
 522  	fi
 523  	rm -rf "$td" "$addir"
 524  done
 525  
 526  # 6r: an untyped `{...}` in a map literal and a field select on a map index.
 527  # Both compilers must build the same program and print the same values: stage4
 528  # silently dropped the map entry when its value was an untyped composite
 529  # literal (buildExpr answered nil), and it refused `m[k].Field` for a
 530  # struct-valued map because a map value is not addressable - the key was live
 531  # and the value was not there. Legacy builds both, so stage4 has to.
 532  for cc in "$MOXIE" "$LEGACY"; do
 533  	label="stage4"
 534  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 535  	name="maplit:$label"
 536  	td="$(tempdir "maplit_$label")"; rm -rf "$td"; mkdir -p "$td"
 537  	addir="$(fixture maplit)"
 538  	rc=0
 539  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$addir" >"$td/build.log" 2>&1 || rc=$?
 540  	if [ $rc -ne 0 ]; then
 541  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 542  	else
 543  		out="$("$td/bin" 2>&1 || true)"
 544  		want="nested en = EN LOGIN
 545  nested ja = JA LOGIN
 546  slice = one,two
 547  struct = one,two
 548  array = first,second
 549  ptr = five,six"
 550  		if [ "$out" = "$want" ]; then
 551  			log_pass "$name" "untyped map elements and map-index field selects hold"
 552  		else
 553  			log_fail "$name" "got '$(echo "$out" | tr '\n' '|')'"
 554  		fi
 555  	fi
 556  	rm -rf "$td" "$addir"
 557  done
 558  
 559  # 6t: a variable declared from an untyped constant takes the constant's default
 560  # type. Left untyped, `acc := 0` made the shift in `byte((acc >> bits) & 0x1f)`
 561  # context-dependent, and the conversion retyped it to uint8 - an i8 shift on
 562  # acc's low byte. Bech32 payloads came out wrong. Legacy types it int32 and is
 563  # the reference here.
 564  for cc in "$MOXIE" "$LEGACY"; do
 565  	label="stage4"
 566  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 567  	name="untypedvar:$label"
 568  	td="$(tempdir "untypedvar_$label")"; rm -rf "$td"; mkdir -p "$td"
 569  	uvdir="$(fixture untypedvar)"
 570  	rc=0
 571  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$uvdir" >"$td/build.log" 2>&1 || rc=$?
 572  	if [ $rc -ne 0 ]; then
 573  		log_fail "$name" "build failed: $(tail -3 "$td/build.log" | tr '\n' ' ')"
 574  	else
 575  		out="$("$td/bin" 2>&1 || true)"
 576  		if [ "$out" = "groups 1202140204021a13" ]; then
 577  			log_pass "$name" "a declaration from an untyped constant is int32"
 578  		else
 579  			log_fail "$name" "got '$out', want 'groups 1202140204021a13'"
 580  		fi
 581  	fi
 582  	rm -rf "$td" "$uvdir"
 583  done
 584  
 585  # 6u: push on an untyped nil must fail loudly in both compilers. Stage4 typed
 586  # the call untyped nil, defaulted it to int32, and emitted an i32 where a slice
 587  # header belongs - or, in a short declaration, silently built an empty slice.
 588  # Legacy has always reported it; the fixture pins both sides to the same error.
 589  for cc in "$MOXIE" "$LEGACY"; do
 590  	label="stage4"
 591  	if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 592  	name="pushnil:$label"
 593  	td="$(tempdir "pushnil_$label")"; rm -rf "$td"; mkdir -p "$td"
 594  	pndir="$(fixture pushnil)"
 595  	rc=0
 596  	MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$pndir" >"$td/build.log" 2>&1 || rc=$?
 597  	if [ $rc -eq 0 ]; then
 598  		log_fail "$name" "push on an untyped nil compiled"
 599  	elif grep -q "must be a slice; have untyped nil" "$td/build.log"; then
 600  		log_pass "$name" "push on an untyped nil is rejected by name"
 601  	else
 602  		log_fail "$name" "wrong error: $(tail -2 "$td/build.log" | tr '\n' ' ')"
 603  	fi
 604  	rm -rf "$td" "$pndir"
 605  done
 606  
 607  # 6v..6x: the three language rules legacy enforces and stage4 did not, each in
 608  # its own fixture and inside a real module. Legacy applies user restrictions
 609  # only to the main module (p.Module.Main), so a manifest-less directory version
 610  # of these programs is accepted by legacy and would prove nothing. Both
 611  # compilers must reject each one by name.
 612  reject_fixture() {
 613  	local fx="$1" want="$2"
 614  	for cc in "$MOXIE" "$LEGACY"; do
 615  		label="stage4"
 616  		if [ "$cc" = "$LEGACY" ]; then label="legacy"; fi
 617  		name="$fx:$label"
 618  		td="$(tempdir "${fx}_$label")"; rm -rf "$td"; mkdir -p "$td"
 619  		fxdir="$(fixture "$fx")"
 620  		rc=0
 621  		if [ "$cc" = "$LEGACY" ]; then
 622  			# The legacy loader shells out to the Go toolchain for go/types.
 623  			PATH="/tmp/moxie-goroot/bin:$PATH" GOROOT=/tmp/moxie-goroot \
 624  				MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$fxdir" >"$td/build.log" 2>&1 || rc=$?
 625  		else
 626  			MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$fxdir" >"$td/build.log" 2>&1 || rc=$?
 627  		fi
 628  		if [ $rc -eq 0 ]; then
 629  			log_fail "$name" "the program compiled; want rejection"
 630  		elif grep -q "$want" "$td/build.log"; then
 631  			log_pass "$name" "rejected: $want"
 632  		else
 633  			log_fail "$name" "wrong error: $(tail -2 "$td/build.log" | tr '\n' ' ')"
 634  		fi
 635  		rm -rf "$td" "$fxdir"
 636  	done
 637  }
 638  
 639  # 6v: unnamed return values.
 640  reject_fixture unnamedret "unnamed return values are not allowed"
 641  # 6w: a parenthesised call target.
 642  reject_fixture parencall "parenthesized call target is not allowed"
 643  # 6x: '+' for text concatenation.
 644  reject_fixture plustext "'+' is not allowed for text concatenation"
 645  # 6x.1: a package-level var with an initializer.
 646  reject_fixture declvar "package-level var with initializer is not allowed"
 647  # 6x.2: a named slice type.
 648  reject_fixture namedslice "named slice type 'Names' is not allowed"
 649  
 650  # 6y..6z: the no-conflict rule reaches imports. Legacy rejects both shapes
 651  # through its own type checker (a redeclared import; a declaration already
 652  # declared through the dot import), stage4 through the rule it gained here, so
 653  # the two compilers reject for different reasons and the check pins each side's
 654  # message rather than pretending they share one.
 655  reject_conflict_fixture() {
 656  	local fx="$1" want_s4="$2" want_legacy="$3"
 657  	for cc in "$MOXIE" "$LEGACY"; do
 658  		label="stage4"; want="$want_s4"
 659  		if [ "$cc" = "$LEGACY" ]; then label="legacy"; want="$want_legacy"; fi
 660  		name="$fx:$label"
 661  		td="$(tempdir "${fx}_$label")"; rm -rf "$td"; mkdir -p "$td"
 662  		fxdir="$(fixture "$fx")"
 663  		rc=0
 664  		if [ "$cc" = "$LEGACY" ]; then
 665  			PATH="/tmp/moxie-goroot/bin:$PATH" GOROOT=/tmp/moxie-goroot \
 666  				MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$fxdir" >"$td/build.log" 2>&1 || rc=$?
 667  		else
 668  			MOXIEROOT="$MOXIEROOT" "$cc" build -o "$td/bin" "$fxdir" >"$td/build.log" 2>&1 || rc=$?
 669  		fi
 670  		if [ $rc -eq 0 ]; then
 671  			log_fail "$name" "the program compiled; want rejection"
 672  		elif grep -q "$want" "$td/build.log"; then
 673  			log_pass "$name" "rejected: $want"
 674  		else
 675  			log_fail "$name" "wrong error: $(tail -2 "$td/build.log" | tr '\n' ' ')"
 676  		fi
 677  		rm -rf "$td" "$fxdir"
 678  	done
 679  }
 680  
 681  # 6y: two different imports binding one name in one file.
 682  reject_conflict_fixture importconflict \
 683  	"is imported twice in one file" \
 684  	"redeclared in this block"
 685  # 6z: a declaration colliding with a name a dot import injects.
 686  reject_conflict_fixture dotconflict \
 687  	"collides with an imported package name" \
 688  	"already declared through dot-import of package math"
 689  
 690  # 6aa: `moxie test` must run the package under test's init(). The harness
 691  # concatenates the library and its test files into a synthetic main package and
 692  # injects a __varinit call into that main, but it never called init() - so an
 693  # init-set global read zero in the package's own test and nowhere else.
 694  name="testinit:stage4"
 695  td="$(tempdir "testinit")"; rm -rf "$td"; mkdir -p "$td"
 696  itdir="$(fixture inittest)"
 697  rc=0
 698  out="$(MOXIEROOT="$MOXIEROOT" "$MOXIE" test "$itdir" 2>&1)" || rc=$?
 699  if [ $rc -eq 0 ] && echo "$out" | grep -q "^PASS: "; then
 700  	log_pass "$name" "$(echo "$out" | grep '^PASS: ')"
 701  else
 702  	log_fail "$name" "$(echo "$out" | grep -v '^\[mem\]' | tail -3 | tr '\n' ' ')"
 703  fi
 704  rm -rf "$td" "$itdir"
 705  
 706  # 6ab: a named result without parentheses is a syntax error. The parser used to
 707  # recover by swallowing the body, so the function's locals landed at package
 708  # scope - every other function using one of those names then reported a
 709  # shadowing error - and the only complaint was "unnamed return values", for a
 710  # signature that names one. Two bugs behind it: the early file==nil branch
 711  # collected the parse errors into an empty loop, and a recovered parse (file
 712  # non-nil, errors recorded) was allowed to carry on with a partial tree.
 713  reject_conflict_fixture badnamedresult \
 714  	"named result values require parentheses" \
 715  	"unexpected name"
 716  
 717  failed=0
 718  if [ -f "$STATUSFILE" ]; then
 719  	failed=$(grep -c FAIL "$STATUSFILE" 2>/dev/null) || failed=0
 720  fi
 721  if [ "$failed" -gt 0 ]; then
 722  	echo ""
 723  	echo "Instrumentation suite: $failed failure(s)"
 724  	exit 1
 725  fi
 726