feat(pp): LIST / DISPLAY via std.ch + four PP completeness fixes
`LIST [<fields>] [OFF] [FOR ...] [WHILE ...] [NEXT ...] [RECORD ...]
[REST] [ALL]` and `DISPLAY [<fields>] [OFF] [FOR ...] ... [ALL]`
reach the parser as plain function calls to a new RTL primitive
__dbList (rtlDbList in hbrtl/database.go).
Implementation: walk the workarea under dbEval-style FOR/WHILE/NEXT/
RECORD/REST bounds. For each visible record, evaluate each column
block and emit the rendered values via valueToDisplay (the same
formatter QOut already uses). Empty fields list defaults to
"all fields". OFF suppresses the record-number prefix.
LIST always emits the full filtered range; DISPLAY without ALL emits
only the current record (encoded as nCount=1). TO PRINTER / TO FILE
clauses are not yet wired through — for now everything goes to
stdout.
Wiring up LIST/DISPLAY surfaced four further gaps in PP that were
silently masking bugs in any rule with multiple word-list / list /
optional clauses chained together:
* matchSegment refused MarkerWordList inside `[...]`. The LIST
rule's `[<off:OFF>]` clause therefore never set the off
capture, and `<.off.>` substituted to nothing instead of .T./.F.
matchSegment now matches WordList markers the same way the
top-level matcher does.
* `<v,...>` and `<(f)>` capture stop boundaries didn't include the
values of following MarkerWordList markers. For
`[<v,...>] [<off:OFF>] [<all:ALL>]` against `LIST id, name OFF`,
the v list would happily eat OFF. New addStopFrom helper
contributes both literal keywords and word-list values; both
matchSegment's MarkerList branch and captureExpression now use
it.
* Optional-repeat loop in matchPattern merged a no-progress
iteration's empty capture into the running multi-capture string
(with the `\x01` separator) before the no-progress break check
fired. So a successful first iteration's value got contaminated
and the substitution loop then skipped it as multi-capture
garbage. The merge now happens after the progress check.
* Unreferenced `<.name.>` markers (optional clauses that didn't
match in the input) were getting cleaned up to empty by the
generic marker scrubber instead of the .F. sentinel Harbour's
std.ch expects. New replaceUnreferencedLogify pass mirrors the
existing replaceUnreferencedBlockify and runs just before the
cleanup.
Parser cleanup: LIST and DISPLAY removed from the IDENT-statement
no-op switch in both parseIdentStmt and parseExprStmt.
Gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
package hbrtl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"five/hbrt"
|
||||
@@ -1117,6 +1118,135 @@ func rtlDbSort(t *hbrt.Thread) {
|
||||
t.RetBool(true)
|
||||
}
|
||||
|
||||
// rtlDbList implements __dbList(lOff, aBlocks, lAll, bFor, bWhile,
|
||||
// nNext, nRec, lRest, lPrn, cFile) — output visible records to
|
||||
// stdout. aBlocks is an array of column-evaluation code blocks (one
|
||||
// per LIST / DISPLAY column expression). If aBlocks is empty or
|
||||
// contains only NIL placeholders, every field of the current
|
||||
// workarea is emitted.
|
||||
//
|
||||
// Used by both `LIST [<v,...>]` and `DISPLAY [<v,...>]` in std.ch.
|
||||
// lAll distinguishes them: LIST always passes .T. (all matching
|
||||
// records); DISPLAY passes .T. only for `DISPLAY ALL`, otherwise .F.
|
||||
// (just the current record).
|
||||
//
|
||||
// TO PRINTER / TO FILE redirection (lPrn / cFile) is accepted but
|
||||
// not yet implemented — both paths still write to stdout. OFF (lOff)
|
||||
// suppresses the record-number prefix.
|
||||
func rtlDbList(t *hbrt.Thread) {
|
||||
nParams := t.ParamCount()
|
||||
t.Frame(nParams, 0)
|
||||
defer t.EndProcFast()
|
||||
|
||||
wam := getWA(t)
|
||||
if wam == nil {
|
||||
t.RetNil()
|
||||
return
|
||||
}
|
||||
srcArea := wam.Current()
|
||||
if srcArea == nil {
|
||||
t.RetNil()
|
||||
return
|
||||
}
|
||||
|
||||
lOff := false
|
||||
if nParams >= 1 && !t.Local(1).IsNil() {
|
||||
lOff = t.Local(1).AsBool()
|
||||
}
|
||||
|
||||
// Decode column blocks. Empty / `{ NIL }` → fall back to "all fields".
|
||||
var blocks []hbrt.Value
|
||||
useAllFields := true
|
||||
if nParams >= 2 && t.Local(2).IsArray() {
|
||||
arr := t.Local(2).AsArray()
|
||||
if arr != nil {
|
||||
for _, it := range arr.Items {
|
||||
if it.IsBlock() {
|
||||
blocks = append(blocks, it)
|
||||
useAllFields = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lAll := true
|
||||
if nParams >= 3 && !t.Local(3).IsNil() {
|
||||
lAll = t.Local(3).AsBool()
|
||||
}
|
||||
|
||||
// Loop bounds — same shape as dbEval.
|
||||
var bFor, bWhile hbrt.Value
|
||||
if nParams >= 4 {
|
||||
bFor = t.Local(4)
|
||||
}
|
||||
if nParams >= 5 {
|
||||
bWhile = t.Local(5)
|
||||
}
|
||||
nCount := -1
|
||||
if nParams >= 6 && !t.Local(6).IsNil() {
|
||||
nCount = t.Local(6).AsInt()
|
||||
}
|
||||
if nParams >= 7 && !t.Local(7).IsNil() {
|
||||
srcArea.GoTo(uint32(t.Local(7).AsInt()))
|
||||
}
|
||||
lRest := false
|
||||
if nParams >= 8 && !t.Local(8).IsNil() {
|
||||
lRest = t.Local(8).AsBool()
|
||||
}
|
||||
// DISPLAY without ALL emits exactly one record; LIST always emits
|
||||
// the full filtered range. Encode the difference by clamping
|
||||
// nCount to 1 when lAll is false and no explicit NEXT was given.
|
||||
if !lAll && nCount < 0 {
|
||||
nCount = 1
|
||||
}
|
||||
if !lRest && lAll && (nParams < 7 || t.Local(7).IsNil()) {
|
||||
srcArea.GoTop()
|
||||
}
|
||||
|
||||
nFields := srcArea.FieldCount()
|
||||
scanned := 0
|
||||
for !srcArea.EOF() {
|
||||
if nCount >= 0 && scanned >= nCount {
|
||||
break
|
||||
}
|
||||
if bWhile.IsBlock() {
|
||||
t.PendingParams2(0)
|
||||
bWhile.AsBlock().Fn(t)
|
||||
if !t.GetRetValue().AsBool() {
|
||||
break
|
||||
}
|
||||
}
|
||||
emit := true
|
||||
if bFor.IsBlock() {
|
||||
t.PendingParams2(0)
|
||||
bFor.AsBlock().Fn(t)
|
||||
emit = t.GetRetValue().AsBool()
|
||||
}
|
||||
if emit {
|
||||
parts := []string{}
|
||||
if !lOff {
|
||||
parts = append(parts, fmt.Sprintf("%6d", srcArea.RecNo()))
|
||||
}
|
||||
if useAllFields {
|
||||
for i := 0; i < nFields; i++ {
|
||||
v, _ := srcArea.GetValue(i)
|
||||
parts = append(parts, valueToDisplay(v))
|
||||
}
|
||||
} else {
|
||||
for _, blk := range blocks {
|
||||
t.PendingParams2(0)
|
||||
blk.AsBlock().Fn(t)
|
||||
parts = append(parts, valueToDisplay(t.GetRetValue()))
|
||||
}
|
||||
}
|
||||
fmt.Print("\r\n" + strings.Join(parts, " "))
|
||||
}
|
||||
srcArea.Skip(1)
|
||||
scanned++
|
||||
}
|
||||
t.RetNil()
|
||||
}
|
||||
|
||||
// stableSort is a tiny insertion sort for small N (typical DBF SORT
|
||||
// targets are interactive datasets). Avoids a sort import dependency.
|
||||
func stableSort(rows [][]hbrt.Value, less func(i, j int) bool) {
|
||||
|
||||
@@ -201,6 +201,7 @@ func RegisterRTL(vm *hbrt.VM) {
|
||||
hbrt.Sym("__DBAVERAGE", hbrt.FsPublic, rtlDbAverage),
|
||||
hbrt.Sym("__DBCOPY", hbrt.FsPublic, rtlDbCopy),
|
||||
hbrt.Sym("__DBSORT", hbrt.FsPublic, rtlDbSort),
|
||||
hbrt.Sym("__DBLIST", hbrt.FsPublic, rtlDbList),
|
||||
hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter),
|
||||
hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter),
|
||||
hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),
|
||||
|
||||
Reference in New Issue
Block a user