From 29ca02e1bcf749e64b1c00adadd39b9ee2a383bc Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Mon, 4 May 2026 11:32:38 +0900 Subject: [PATCH] fix(genpc,parser,pcinterp): pcode wider regression sweep (Tier 1 #3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- compiler/genpc/genpc.go | 135 +++++++++++++++-- compiler/parser/parser.go | 49 ++++-- hbrt/pcinterp.go | 19 ++- tests/frb/run.sh | 1 + tests/frb/test_frb_pcode_sweep.prg | 231 +++++++++++++++++++++++++++++ 5 files changed, 415 insertions(+), 20 deletions(-) create mode 100644 tests/frb/test_frb_pcode_sweep.prg diff --git a/compiler/genpc/genpc.go b/compiler/genpc/genpc.go index 5b67627..3ea59fb 100644 --- a/compiler/genpc/genpc.go +++ b/compiler/genpc/genpc.go @@ -123,16 +123,17 @@ func (g *generator) emitFunc(fn *ast.FuncDecl) { g.code = nil g.locals = make(map[string]int) - // Build local map + // Build local map. PRG is case-insensitive so all keys are + // uppercased here; every lookup site below must mirror this. idx := 1 for _, p := range fn.Params { - g.locals[p.Name] = idx + g.locals[strings.ToUpper(p.Name)] = idx idx++ } for _, d := range fn.Decls { if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal { for _, v := range vd.Vars { - g.locals[v.Name] = idx + g.locals[strings.ToUpper(v.Name)] = idx idx++ } } @@ -140,7 +141,7 @@ func (g *generator) emitFunc(fn *ast.FuncDecl) { for _, s := range fn.Body { if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal { for _, v := range vd.Vars { - g.locals[v.Name] = idx + g.locals[strings.ToUpper(v.Name)] = idx idx++ } } @@ -228,7 +229,7 @@ func (g *generator) emitStmt(stmt ast.Stmt) { for _, v := range s.Vars { if v.Init != nil { g.emitExpr(v.Init) - if idx, ok := g.locals[v.Name]; ok { + if idx, ok := g.locals[strings.ToUpper(v.Name)]; ok { g.emit(hbrt.PcOpPopLocal) g.emitU16(uint16(idx)) } else { @@ -287,7 +288,7 @@ func (g *generator) emitDoWhile(s *ast.DoWhileStmt) { } func (g *generator) emitFor(s *ast.ForStmt) { - idx, ok := g.locals[s.Var] + idx, ok := g.locals[strings.ToUpper(s.Var)] if !ok { return } @@ -400,7 +401,12 @@ func (g *generator) emitExpr(expr ast.Expr) { g.emit(hbrt.PcOpPushSelf) return } - if idx, ok := g.locals[e.Name]; ok { + // Locals are keyed case-insensitively. Look up via uppercase + // (also covers blocks: their params are stored ToUpper). The + // previous raw `e.Name` lookup missed any caller that wrote + // the identifier in different case from the declaration — + // `{|x| x * x }` invoked via Eval(b, 7) silently saw x=NIL. + if idx, ok := g.locals[upper]; ok { g.emit(hbrt.PcOpPushLocal) g.emitU16(uint16(idx)) } else { @@ -463,6 +469,117 @@ func (g *generator) emitExpr(expr ast.Expr) { g.emit(hbrt.PcOpArrayGen) g.emitU16(uint16(len(e.Items))) + case *ast.BlockExpr: + // `{|p| body }` — compile body to its own pcode buffer with + // the block's params occupying locals 1..len(Params), then + // emit PcOpPushBlock + length + body bytes + nDetached (zero + // — closure capture isn't wired up in pcode mode yet, so + // blocks see their declared params and any module-local + // symbol but no caller locals). + // Without this case, BlockExpr fell through to the generic + // PushNil and Eval(NIL, ...) returned NIL — silently + // breaking every higher-order function (Eval / AEval / + // SqlScan predicate compile / etc.) inside a pcode body. + savedCode := g.code + savedLocals := g.locals + g.code = nil + g.locals = make(map[string]int, len(e.Params)) + for i, p := range e.Params { + g.locals[strings.ToUpper(p)] = i + 1 + } + g.emitExpr(e.Body) + g.emit(hbrt.PcOpRetValue) + body := g.code + g.code = savedCode + g.locals = savedLocals + + g.emit(hbrt.PcOpPushBlock) + g.emitI32(int32(len(body))) + g.code = append(g.code, body...) + g.emitU16(uint16(len(e.Params))) // nParams + g.emitU16(0) // nDetached — no closure capture yet + + case *ast.SeqExpr: + // Comma-separated expression list inside a code block: + // `{|| e1, e2, e3 }`. Evaluate each in order, pop intermediate + // results so only the last value remains. Same semantics as + // gengo's SeqExpr handler. + for i, item := range e.Items { + g.emitExpr(item) + if i < len(e.Items)-1 { + g.emit(hbrt.PcOpPop) + } + } + + case *ast.HashLitExpr: + // `{ "k" => 1, ... }` — push each key+value pair, HashGen + // builds the hash from the top-N stack pairs. Without this + // case, the hash literal silently fell through to PushNil + // and any subsequent `h[key]` panicked at ArrayPush with + // "argument error (op: [])". + for i, k := range e.Keys { + g.emitExpr(k) + g.emitExpr(e.Values[i]) + } + g.emit(hbrt.PcOpHashGen) + g.emitU16(uint16(len(e.Keys))) + + case *ast.IndexExpr: + // arr[idx] — push array, push index, ArrayPush reads element. + // (ArrayPush is the "get" op; ArrayPop is the "set" op — names + // kept to match the Harbour stack-machine convention.) + // Without this case, indexed reads in pcode silently emitted + // PushNil via the default fallback, so `arr[i]` always + // returned NIL and `n + arr[i]` panicked at the +. + g.emitExpr(e.X) + g.emitExpr(e.Index) + g.emit(hbrt.PcOpArrayPush) + + case *ast.PostfixExpr: + // `x++` / `x--` — read current value (becomes the expression + // result), apply Inc/Dec to the LOCAL slot, leave the + // pre-modification value on the stack so it round-trips + // correctly when used as an expression. As a statement the + // caller does Pop afterward. + // Without this case, postfix on pcode-mode silently emitted + // PushNil → `n++` was a no-op, breaking DO WHILE / FOR + // patterns that mutate the loop counter. + if id, isIdent := e.X.(*ast.IdentExpr); isIdent { + if idx, found := g.locals[strings.ToUpper(id.Name)]; found { + g.emit(hbrt.PcOpPushLocal) + g.emitU16(uint16(idx)) + delta := int64(1) + if e.Op == token.DEC { + delta = -1 + } + g.emit(hbrt.PcOpLocalAddInt) + g.emitU16(uint16(idx)) + g.emitI32(int32(delta)) + return + } + } + // Anything else (memvar, alias->field, arr[i]) — emit the + // expression as a no-op for now and document the gap. + g.emitExpr(e.X) + + case *ast.AliasExpr: + // Pcode mode: only the M-> / MEMVAR-> namespace (memvar + // access) is wired up. The general workarea-alias form + // (`FOO->bar`, `(expr)->(body)`) needs new opcodes for + // alias dispatch + workarea context save/restore — until + // then it falls through to the generic NIL fallback so + // callers see "missing data" rather than crash. + if aliasIdent, ok1 := e.Alias.(*ast.IdentExpr); ok1 { + if fieldIdent, ok2 := e.Field.(*ast.IdentExpr); ok2 { + upper := strings.ToUpper(aliasIdent.Name) + if upper == "M" || upper == "MEMVAR" { + g.emitString(hbrt.PcOpPushMemvar, fieldIdent.Name) + return + } + } + } + g.emit(hbrt.PcOpPushNil) + default: g.emit(hbrt.PcOpPushNil) // fallback } @@ -584,7 +701,7 @@ func (g *generator) emitAssign(a *ast.AssignExpr) { op, ok := compoundBinOp(a.Op) if ok { if ident, isIdent := a.Left.(*ast.IdentExpr); isIdent { - if idx, found := g.locals[ident.Name]; found { + if idx, found := g.locals[strings.ToUpper(ident.Name)]; found { g.emit(hbrt.PcOpPushLocal) g.emitU16(uint16(idx)) g.emitExpr(a.Right) @@ -597,7 +714,7 @@ func (g *generator) emitAssign(a *ast.AssignExpr) { } } if ident, ok := a.Left.(*ast.IdentExpr); ok { - if idx, found := g.locals[ident.Name]; found { + if idx, found := g.locals[strings.ToUpper(ident.Name)]; found { g.emitExpr(a.Right) g.emit(hbrt.PcOpPopLocal) g.emitU16(uint16(idx)) diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 582b5e2..d2498a6 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -1154,17 +1154,36 @@ func (p *Parser) parseIdentStmt() ast.Stmt { // CLOSE / REINDEX / PACK / ZAP / UNLOCK / KEYBOARD / RUN are now // rewritten by compiler/pp/std.ch into function calls before the // parser sees them. + // + // Guard against shadowing variables — the keywords here (LABEL, + // REPORT, INPUT, NOTE, ...) are also extremely common LOCAL/PRIVATE + // names. If the very next token is an assignment / index / paren / + // alias-arrow operator, the user is doing a variable assignment or + // function call, not invoking the xBase command — fall through to + // expression parsing. This was a real silent bug: `LOCAL label` + + // `label := "x"` had the assignment swallowed by the LABEL case + // because the no-op consumed-to-EOL path doesn't care about :=. switch upper { case "LABEL", "REPORT", "ACCEPT", "INPUT", "RELEASE", "SAVE", "RESTORE", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT", "WITH", "CLEAR": - p.advance() - for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { + switch p.peekAt(1) { + case token.ASSIGN, token.PLUSEQ, token.MINUSEQ, token.STAREQ, + token.SLASHEQ, token.PERCENTEQ, token.POWEREQ, + token.LBRACKET, token.LPAREN, token.ARROW, + token.INC, token.DEC, + token.DOT: + // Looks like a variable / call / member-access — not + // the xBase command. Fall through. + default: p.advance() + for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { + p.advance() + } + p.expectEndOfStmt() + return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}} } - p.expectEndOfStmt() - return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}} case "FIVE_GODUMP__": // GoDump is a Decl, wrap as ExprStmt for statement context @@ -1229,13 +1248,25 @@ func (p *Parser) parseExprStmt() ast.Stmt { "RELEASE", "SAVE", "RESTORE", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT", "WITH", "CLEAR": - // Consume entire line — these are complex multi-word commands - p.advance() - for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { + // Same shadowing-guard as parseIdentStmt — see comment + // there. Without this, `LOCAL label ; label := "x"` had + // the assignment swallowed. + switch p.peekAt(1) { + case token.ASSIGN, token.PLUSEQ, token.MINUSEQ, token.STAREQ, + token.SLASHEQ, token.PERCENTEQ, token.POWEREQ, + token.LBRACKET, token.LPAREN, token.ARROW, + token.INC, token.DEC, + token.DOT: + // Variable / call / member-access — fall through. + default: + // Consume entire line — these are complex multi-word commands p.advance() + for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { + p.advance() + } + p.expectEndOfStmt() + return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}} } - p.expectEndOfStmt() - return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}} } } diff --git a/hbrt/pcinterp.go b/hbrt/pcinterp.go index 4fb719b..65c42c6 100644 --- a/hbrt/pcinterp.go +++ b/hbrt/pcinterp.go @@ -277,6 +277,15 @@ func execPcodeBody(t *Thread, fn *PcodeFunc, mod *PcodeModule) { case PcOpArrayPop: t.ArrayPop() + // --- Hash --- (PcOpHashGen has been declared since the + // initial pcode design but its dispatch case was missing, + // so any pcode body that built a hash literal panicked + // with "unknown pcode opcode: 0x51".) + case PcOpHashGen: + count := int(binary.LittleEndian.Uint16(code[pc:])) + pc += 2 + t.HashGen(count) + // --- Block --- case PcOpPushBlock: codeLen := int(binary.LittleEndian.Uint32(code[pc:])) @@ -284,11 +293,17 @@ func execPcodeBody(t *Thread, fn *PcodeFunc, mod *PcodeModule) { blockCode := make([]byte, codeLen) copy(blockCode, code[pc:pc+codeLen]) pc += codeLen + nParams := int(binary.LittleEndian.Uint16(code[pc:])) + pc += 2 nDetached := int(binary.LittleEndian.Uint16(code[pc:])) pc += 2 - // Create a Go function that interprets the block's pcode - blockFn := &PcodeFunc{Code: blockCode} + // Create a Go function that interprets the block's pcode. + // Params count must be threaded through so ExecPcode's + // Frame() pulls Eval()'s args off the stack into the + // block's locals — without it, `{|x| x*x }` saw x=NIL + // and `x * x` panicked on the multiplication. + blockFn := &PcodeFunc{Code: blockCode, Params: nParams} modCopy := mod t.PushBlock(func(t2 *Thread) { ExecPcode(t2, blockFn, modCopy) diff --git a/tests/frb/run.sh b/tests/frb/run.sh index d33dfc1..34fb6ac 100755 --- a/tests/frb/run.sh +++ b/tests/frb/run.sh @@ -37,6 +37,7 @@ TESTS=( test_frb_loop test_frb_step test_frb_goroutine + test_frb_pcode_sweep ) pass=0 diff --git a/tests/frb/test_frb_pcode_sweep.prg b/tests/frb/test_frb_pcode_sweep.prg new file mode 100644 index 0000000..8f1d3a9 --- /dev/null +++ b/tests/frb/test_frb_pcode_sweep.prg @@ -0,0 +1,231 @@ +/* Pcode-mode wider-coverage sweep. Every function below is compiled + to pcode via FrbCompile (in-process) and called via FrbDo, so any + pcode interpreter regression that escaped the focused fixtures + surfaces here. Failures are accumulated and printed at the end. */ + +PROCEDURE Main() + LOCAL pMod, src, pass := 0, fail := 0, label, expect, got + + src := ; + 'FUNCTION StringOps(s)' + Chr(10) + ; + ' RETURN Upper(s) + "_" + Lower(s)' + Chr(10) + ; + 'FUNCTION ArithMix(a, b)' + Chr(10) + ; + ' RETURN (a + b) * 2 - a / 2' + Chr(10) + ; + 'FUNCTION CmpChain(n)' + Chr(10) + ; + ' IF n > 0 .AND. n < 100' + Chr(10) + ; + ' RETURN "in"' + Chr(10) + ; + ' ELSEIF n == 0' + Chr(10) + ; + ' RETURN "zero"' + Chr(10) + ; + ' ENDIF' + Chr(10) + ; + ' RETURN "out"' + Chr(10) + ; + 'FUNCTION ArrSum(arr)' + Chr(10) + ; + ' LOCAL n := 0, i' + Chr(10) + ; + ' FOR i := 1 TO Len(arr)' + Chr(10) + ; + ' n += arr[i]' + Chr(10) + ; + ' NEXT' + Chr(10) + ; + ' RETURN n' + Chr(10) + ; + 'FUNCTION DoWhileTest(n)' + Chr(10) + ; + ' LOCAL r := 0' + Chr(10) + ; + ' DO WHILE n > 0' + Chr(10) + ; + ' r += n' + Chr(10) + ; + ' n--' + Chr(10) + ; + ' ENDDO' + Chr(10) + ; + ' RETURN r' + Chr(10) + ; + 'FUNCTION NestedIf(a, b)' + Chr(10) + ; + ' IF a > 0' + Chr(10) + ; + ' IF b > 0' + Chr(10) + ; + ' RETURN "++"' + Chr(10) + ; + ' ENDIF' + Chr(10) + ; + ' RETURN "+0"' + Chr(10) + ; + ' ENDIF' + Chr(10) + ; + ' RETURN "00"' + Chr(10) + ; + 'FUNCTION IIfTest(n)' + Chr(10) + ; + ' RETURN iif(n > 5, "big", "small")' + Chr(10) + ; + 'FUNCTION HashLit()' + Chr(10) + ; + ' LOCAL h := { "a" => 1, "b" => 2 }' + Chr(10) + ; + ' RETURN h["a"] + h["b"]' + Chr(10) + ; + 'FUNCTION BlockEval(n)' + Chr(10) + ; + ' LOCAL b := {|x| x * x }' + Chr(10) + ; + ' RETURN Eval(b, n)' + Chr(10) + ; + 'FUNCTION CountChars(s, c)' + Chr(10) + ; + ' LOCAL i, n := 0' + Chr(10) + ; + ' FOR i := 1 TO Len(s)' + Chr(10) + ; + ' IF SubStr(s, i, 1) == c' + Chr(10) + ; + ' n++' + Chr(10) + ; + ' ENDIF' + Chr(10) + ; + ' NEXT' + Chr(10) + ; + ' RETURN n' + Chr(10) + + pMod := FrbCompile(src) + IF pMod == NIL + ? "FAIL: compile returned NIL" + RETURN + ENDIF + + /* Each row: label, expected, FrbDo call (deferred via codeblock) */ + ? "--- pcode sweep ---" + + label := "1. StringOps('Hi')" + expect := "HI_hi" + got := FrbDo(pMod, "STRINGOPS", "Hi") + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "2. ArithMix(10, 4)" + expect := 23 /* (10+4)*2 - 10/2 = 28 - 5 = 23 */ + got := FrbDo(pMod, "ARITHMIX", 10, 4) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "3a. CmpChain(50)" + expect := "in" + got := FrbDo(pMod, "CMPCHAIN", 50) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "3b. CmpChain(0)" + expect := "zero" + got := FrbDo(pMod, "CMPCHAIN", 0) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "3c. CmpChain(200)" + expect := "out" + got := FrbDo(pMod, "CMPCHAIN", 200) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "4. ArrSum({1,2,3,4,5})" + expect := 15 + got := FrbDo(pMod, "ARRSUM", { 1, 2, 3, 4, 5 }) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "5. DoWhileTest(5)" + expect := 15 /* 5+4+3+2+1 */ + got := FrbDo(pMod, "DOWHILETEST", 5) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "6a. NestedIf(1,1)" + expect := "++" + got := FrbDo(pMod, "NESTEDIF", 1, 1) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "6b. NestedIf(1,0)" + expect := "+0" + got := FrbDo(pMod, "NESTEDIF", 1, 0) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "6c. NestedIf(-1,0)" + expect := "00" + got := FrbDo(pMod, "NESTEDIF", -1, 0) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "7. IIfTest(10)" + expect := "big" + got := FrbDo(pMod, "IIFTEST", 10) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "8. HashLit()" + expect := 3 + got := FrbDo(pMod, "HASHLIT") + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "9. BlockEval(7)" + expect := 49 + got := FrbDo(pMod, "BLOCKEVAL", 7) + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + label := "10. CountChars('mississippi', 's')" + expect := 4 + got := FrbDo(pMod, "COUNTCHARS", "mississippi", "s") + IF got == expect + ? "PASS", label, "=", got + pass++ + ELSE + ? "FAIL", label, "expect", expect, "got", got + fail++ + ENDIF + + FrbUnload(pMod) + + ? "" + ? "================================================================" + ? " pcode sweep:", pass, "passed,", fail, "failed" + ? "================================================================" + IF fail > 0 + ? "FAIL summary" + ENDIF + RETURN