fix(frb,genpc): in-process compile + 4 pcode bugs

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>
This commit is contained in:
2026-05-02 10:25:35 +09:00
parent 3ce0eceed5
commit efb615bed9
11 changed files with 429 additions and 36 deletions

12
tests/frb/frb_simple.prg Normal file
View File

@@ -0,0 +1,12 @@
FUNCTION GiveFive()
RETURN 5
FUNCTION AddOne(n)
RETURN n + 1
FUNCTION CountTo3()
LOCAL i, sum := 0
FOR i := 1 TO 3
sum := sum + i
NEXT
RETURN sum

70
tests/frb/run.sh Executable file
View File

@@ -0,0 +1,70 @@
#!/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 ]

View File

@@ -0,0 +1,38 @@
/* In-memory PRG compilation via FrbCompile / FrbExec. */
FUNCTION Main()
LOCAL pStr, cSource
/* 1. FrbCompile a small lib, call its functions */
cSource := ;
'FUNCTION Reverse(cStr)' + Chr(10) + ;
' LOCAL i, cResult := ""' + Chr(10) + ;
' FOR i := Len(cStr) TO 1 STEP -1' + Chr(10) + ;
' cResult += SubStr(cStr, i, 1)' + Chr(10) + ;
' NEXT' + Chr(10) + ;
' RETURN cResult' + Chr(10) + ;
'FUNCTION Triple(n)' + Chr(10) + ;
' RETURN n * 3' + Chr(10)
pStr := FrbCompile(cSource)
IF pStr == NIL
? "FAIL: FrbCompile returned NIL"
RETURN NIL
ENDIF
? "1. Reverse('Hello') =", FrbDo(pStr, "REVERSE", "Hello"), "(expect olleH)"
? "2. Triple(7) =", FrbDo(pStr, "TRIPLE", 7), "(expect 21)"
FrbUnload(pStr)
/* 2. One-shot FrbExec */
? "3. Sum 1..100 via FrbExec:"
? " ", FrbExec( ;
'FUNCTION Main()' + Chr(10) + ;
' LOCAL i, n := 0' + Chr(10) + ;
' FOR i := 1 TO 100' + Chr(10) + ;
' n += i' + Chr(10) + ;
' NEXT' + Chr(10) + ;
' RETURN n' + Chr(10) ), "(expect 5050)"
? "DONE"
RETURN NIL

View File

@@ -0,0 +1,33 @@
FUNCTION Main()
LOCAL r
/* in-memory compile a simple loop */
r := FrbExec( ;
'FUNCTION Main()' + Chr(10) + ;
' LOCAL i, n := 0' + Chr(10) + ;
' FOR i := 1 TO 10' + Chr(10) + ;
' n := n + i' + Chr(10) + ;
' NEXT' + Chr(10) + ;
' RETURN n' + Chr(10) )
? "sum 1..10 (using :=) =", r, "(expect 55)"
r := FrbExec( ;
'FUNCTION Main()' + Chr(10) + ;
' LOCAL i, n := 0' + Chr(10) + ;
' FOR i := 1 TO 10' + Chr(10) + ;
' n += i' + Chr(10) + ;
' NEXT' + Chr(10) + ;
' RETURN n' + Chr(10) )
? "sum 1..10 (using +=) =", r, "(expect 55)"
/* string accumulator */
r := FrbExec( ;
'FUNCTION Main()' + Chr(10) + ;
' LOCAL i, s := ""' + Chr(10) + ;
' FOR i := 1 TO 5' + Chr(10) + ;
' s := s + Str(i, 1)' + Chr(10) + ;
' NEXT' + Chr(10) + ;
' RETURN s' + Chr(10) )
? "concat 1..5 =", r, '(expect "12345")'
RETURN NIL

View File

@@ -0,0 +1,20 @@
/* Test loading + calling a pcode FRB module. */
FUNCTION Main()
LOCAL pMod
pMod := FrbLoad("/tmp/mathlib_pc.frb")
IF pMod == NIL
? "FAIL: FrbLoad returned NIL"
RETURN NIL
ENDIF
? "CircleArea(5.0) =", FrbDo(pMod, "CIRCLEAREA", 5.0), "(expect 78.539...)"
? "Fibonacci(10) =", FrbDo(pMod, "FIBONACCI", 10), "(expect 55)"
? "Fibonacci(20) =", FrbDo(pMod, "FIBONACCI", 20), "(expect 6765)"
? "IsPrime(97) =", FrbDo(pMod, "ISPRIME", 97), "(expect .T.)"
? "IsPrime(100) =", FrbDo(pMod, "ISPRIME", 100), "(expect .F.)"
FrbUnload(pMod)
? "DONE"
RETURN NIL

View File

@@ -0,0 +1,9 @@
FUNCTION Main()
LOCAL pMod := FrbLoad("/tmp/frb_simple.frb")
? "A:", FrbDo(pMod, "GIVEFIVE"), "(expect 5)"
? "B:", FrbDo(pMod, "ADDONE", 100), "(expect 101)"
? "C:", FrbDo(pMod, "COUNTTO3"), "(expect 6)"
FrbUnload(pMod)
RETURN NIL

View File

@@ -0,0 +1,24 @@
FUNCTION Main()
LOCAL r
/* STEP -1 (downward) */
r := FrbExec( ;
'FUNCTION Main()' + Chr(10) + ;
' LOCAL i, n := 0' + Chr(10) + ;
' FOR i := 5 TO 1 STEP -1' + Chr(10) + ;
' n := n + i' + Chr(10) + ;
' NEXT' + Chr(10) + ;
' RETURN n' + Chr(10) )
? "FOR 5 TO 1 STEP -1 sum =", r, "(expect 15)"
/* STEP 2 (upward by 2) */
r := FrbExec( ;
'FUNCTION Main()' + Chr(10) + ;
' LOCAL i, n := 0' + Chr(10) + ;
' FOR i := 1 TO 10 STEP 2' + Chr(10) + ;
' n := n + i' + Chr(10) + ;
' NEXT' + Chr(10) + ;
' RETURN n' + Chr(10) )
? "FOR 1 TO 10 STEP 2 sum =", r, "(expect 25)"
RETURN NIL