Six more silent miscompiles in the pcode path, all uncovered by a
new pcode regression sweep that exercises the full PRG surface a
dynamic FrbCompile body could legitimately use.
* **xBase-keyword shadowing of variable names.** parseIdentStmt
and parseExprStmt's fallback switches consumed an entire line
when the leading IDENT matched LABEL / REPORT / ACCEPT / INPUT
/ NOTE / etc. Those words are also extremely common LOCAL /
PRIVATE names — `LOCAL label ; label := "x"` had the
assignment swallowed because the switch didn't peek at the
next token. Both switches now look at peek(1): an assignment
operator, [], (, -, ++, --, or `.` means it's a variable /
call / member access, not the xBase command, and we fall
through to expression parsing. Real silent bug — bit
test_frb_pcode_sweep's `LOCAL label` declaration.
* **`arr[i]` indexing not implemented in genpc.** ast.IndexExpr
fell through to the default PushNil path, so any indexed read
in a pcode-mode body returned NIL. New case emits the array,
the index, and PcOpArrayPush (the get-op; PcOpArrayPop is the
set-op — naming follows Harbour convention). Hashes go
through the same opcode, which already special-cases
IsHash() in ops_collection.go.
* **Hash literals not implemented in genpc + dispatch missing
in pcinterp.** `{ "k" => v, ... }` fell to PushNil. Added
HashLitExpr emit (Push key, Push value pairs, then PcOpHashGen
with count). Also wired up the PcOpHashGen dispatch in
execPcodeBody — it had been declared in pcode.go since the
initial design but the case statement was never added, so
even hand-written modules couldn't use hashes.
* **`x++` / `x--` postfix were silent no-ops.** PostfixExpr fell
to PushNil and the surrounding ExprStmt then popped the NIL.
DO WHILE loops with `n--` couldn't terminate; FOR loops with
`i++` in the body were broken too. New case: PushLocal +
LocalAddInt(±1).
* **BlockExpr (`{|p| body }`) wasn't compiled.** Eval(b, n)
inside a pcode body returned NIL. Added: build the body in a
sub-codebuffer with the block's params occupying its locals,
emit PcOpRetValue at the end, then PushBlock with the
serialized bytes. Format extended with a uint16 nParams field
so the runtime's PcOpPushBlock dispatch can set
PcodeFunc.Params correctly — without it, ExecPcode's
Frame(0, 0) pulled none of Eval's args and the block saw
every parameter as NIL.
* **All g.locals accesses were case-sensitive.** PRG is case-
insensitive, but the pcode generator stored block params via
strings.ToUpper while every other lookup site (function decl,
mid-decl, ForStmt, IdentExpr read, AssignExpr write,
PostfixExpr) used the raw .Name. So `{|x| x*x }` stored "X"
but read "x" and missed. Normalized: all insertions and all
lookups now go through strings.ToUpper.
* **SeqExpr in pcode** — added the matching emit for comma-
separated expression lists in code blocks (`{|| a, b, c }`).
Same shape as the gengo SeqExpr case from Wave 1.
Test fixture: tests/frb/test_frb_pcode_sweep.prg covers 14 shapes
(string ops, arithmetic, comparison chains, array indexing, DO
WHILE with postfix, nested IF, IIf, hash literal + indexing,
block + Eval, character iteration). All 14 pass. Wired into the
FRB runner — suite now stands at 7/7.
Other gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
std.ch suite : 15/15
FRB suite : 7/7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.4 KiB
Bash
Executable File
75 lines
2.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# FRB regression runner. Each test exercises a different aspect of
|
|
# Five's runtime compilation / loading pipeline:
|
|
#
|
|
# frb_simple — fixture PRG built into a pcode FRB module.
|
|
# test_frb_simple — load `frb_simple.frb`, call its functions.
|
|
# test_frb_pcode_load — load mathlib (multi-function pcode FRB).
|
|
# test_frb_compile — FrbCompile / FrbExec — in-memory compile.
|
|
# test_frb_loop — FOR loop accumulators (`+=` and `:=`).
|
|
# test_frb_step — FOR ... STEP -1 / STEP 2 in pcode mode.
|
|
# test_frb_goroutine — Go("WORKER", ...) launches dynamic FRB
|
|
# function in a goroutine + Channel rendezvous.
|
|
#
|
|
# Builds frb_simple.frb (and mathlib_pc.frb if needed) into the
|
|
# scratch dir before running the loaders.
|
|
set -e
|
|
|
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
FIVE="$ROOT/five"
|
|
[ -x "$FIVE" ] || { echo "five not built — run 'go build -o five ./cmd/five'" >&2; exit 2; }
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
# Pre-build pcode FRB fixtures the loader tests refer to.
|
|
"$FIVE" frb "$ROOT/tests/frb/frb_simple.prg" -o /tmp/frb_simple.frb --pcode >/dev/null
|
|
"$FIVE" frb "$ROOT/examples/frb_mathlib.prg" -o /tmp/mathlib_pc.frb --pcode >/dev/null
|
|
|
|
# Test files in the order they should run. test_frb_compile
|
|
# exercises the in-process compiler, which has no external
|
|
# dependencies — runs from any directory.
|
|
TESTS=(
|
|
test_frb_simple
|
|
test_frb_pcode_load
|
|
test_frb_compile
|
|
test_frb_loop
|
|
test_frb_step
|
|
test_frb_goroutine
|
|
test_frb_pcode_sweep
|
|
)
|
|
|
|
pass=0
|
|
fail=0
|
|
for name in "${TESTS[@]}"; do
|
|
src="$ROOT/tests/frb/${name}.prg"
|
|
bin="$work/${name}"
|
|
if ! "$FIVE" build "$src" -o "$bin" >/dev/null 2>"$work/${name}.err"; then
|
|
echo "FAIL build $name"
|
|
sed 's/^/ /' "$work/${name}.err"
|
|
fail=$((fail+1))
|
|
continue
|
|
fi
|
|
if ! out="$("$bin" 2>&1)"; then
|
|
echo "FAIL run $name"
|
|
echo "$out" | sed 's/^/ /'
|
|
fail=$((fail+1))
|
|
continue
|
|
fi
|
|
if echo "$out" | grep -qE 'FAIL|expect.*got|panic'; then
|
|
echo "FAIL assert $name"
|
|
echo "$out" | sed 's/^/ /'
|
|
fail=$((fail+1))
|
|
continue
|
|
fi
|
|
echo "PASS $name"
|
|
pass=$((pass+1))
|
|
done
|
|
|
|
echo
|
|
echo "================================================================"
|
|
echo " Results: $pass / $((pass+fail)) passed"
|
|
echo "================================================================"
|
|
[ $fail -eq 0 ]
|