Compiling _FiveSql2/test/test_sql_extreme.prg + a sweep of the FRB
demos surfaced four real bugs in the dynamic-compilation pipeline.
All fixes shipped together because they were on the same critical
path; each is independently revertible.
* **pcode FOR loop ignored STEP and direction.** emitFor in
compiler/genpc emitted a fixed `<= to` comparison and a hardcoded
`+1` increment, then deleted the actual step expression with
slice arithmetic on the byte buffer. Result: `FOR 5 TO 1 STEP
-1` exited on the first iteration; `FOR 1 TO 10 STEP 2` summed
1..10 (55) instead of 1+3+5+7+9 (25). Rewritten to mirror
gengo's emitFor: detect negative step from a literal `-N` or
unary MINUS, pick `<=` vs `>=` accordingly, and emit a clean
`var := var + step` increment per iteration.
* **pcode compound `+=` operator stored only the RHS.** emitAssign
looked at AssignExpr.Op only for the := case; +=/-=/etc.
silently took the same path, so `n += i` compiled as `n := i`,
discarding the accumulator. Loop reduces were wrong: `Reverse`
returned "" and `n := 0; FOR i ... n += i; NEXT` returned only
the last increment. New compoundBinOp helper maps PLUSEQ /
MINUSEQ / STAREQ / SLASHEQ / PERCENTEQ / POWEREQ to their
matching binary opcode; emitAssign emits `local + rhs ; pop
local` for compound forms.
* **Pcode body stack leaks polluted the caller's frame.** A pcode
function whose body left intermediate values on the data stack
(FOR control values, etc.) returned with extra entries past
its declared retVal. FrbDoFunc / FrbExecFunc / FrbRunFunc then
pushed retVal on top of those leaks, so the caller saw the
leaked values where its own preceding arguments should have
been: `? "Fibonacci(10) =", FrbDo(...), "(expect 55)"` printed
`1 55 (expect 55)` because the FOR loop's `1` lived in arg-1's
slot. Two new Thread methods (`SP()` / `SetSP(int)`) let the
three FRB dispatchers snapshot stack depth before the inner
call and clamp it back afterward, so the leaks evaporate before
they reach the caller's frame.
* **FrbExec / FrbRun recursed into the host's Main forever.** Both
looked up "MAIN" via t.VM().FindSymbol, which always resolved
to the OUTER program's Main since FRB modules deliberately keep
Main local. Compile + run + unload became compile + recurse +
OOM. Both now look up Main via mod.FindFunc("MAIN") (module
scope) — Frbload's policy of leaving Main module-local now
actually has the intended effect.
Plus an architectural improvement: in-memory compilation no longer
depends on shelling out to an external `five` binary. New
hbrtl.frbCompileInProc parses + preprocesses + generates pcode in
process, building a FrbModule directly. FrbCompile and FrbExec use
this exclusively, which means dynamic compilation works from any
directory regardless of PATH and without a second process. The
plugin-mode path (with its runtime-version-mismatch fragility) is
left available via hbrt.FrbCompileSource for callers that want it,
but FrbCompile no longer reaches for it by default.
Test suite: tests/frb/ holds five fixtures + a runner. 5/5 pass:
test_frb_simple / test_frb_pcode_load / test_frb_compile /
test_frb_loop / test_frb_step.
Other gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
std.ch suite : 14/14
FRB suite : 5/5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71 lines
2.2 KiB
Bash
Executable File
71 lines
2.2 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.
|
|
#
|
|
# 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
|
|
)
|
|
|
|
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 ]
|