feat: Harbour RDD parity — NTX/CDX 100% compatible, FIELD-> works
Five RDD engine now matches Harbour DBFNTX and DBFCDX byte-for-byte
in ordering, seek, navigation, and field access. Verified against
Harbour 3.2.0dev with a 281-line comparison test covering:
- Natural/NAME/CITY/AGE/SALARY/UPPER ordering
- SEEK (exact/not-found), GoTop/GoBottom per order
- DELETE/RECALL with SET DELETED
- CDX compound index read with 5 tags (BYNAME, BYCITY, BYAGE, BYSAL, BYUNAME)
- Reverse traversal
Fixes:
1. FIELD->NAME returned NIL
GetAliasField returned interface{} but runtime expected hbrt.Value,
so the type assertion in PushAliasField failed and pushed NIL.
- workarea.go: change return type to hbrt.Value, handle FIELD/_FIELD
as current-workarea alias, add SetAliasField
- gengo.go: emit SetAliasField() for alias->field := value in both
statement and expression contexts
2. OrdSetFocus(n) silently switched to natural order
v.AsString() returns "" for a numeric Value, so OrderListFocus("")
set current=-1.
- indexrtl.go: convert numeric param via fmt.Sprintf("%d", ...)
3. CDX compound tag order mismatched Harbour
Five decoded the structural B-tree which is alphabetical, but
Harbour sorts tags by TagBlock (file offset = creation order).
- cdx/cdx.go: sort tagEntries by offset ascending after decoding,
matching hb_cdxIndexLoadAvailTags in dbfcdx1.c
4. OutStd()/OutErr() not registered — caused panic on call
- hbrtl/console.go: add rtlOutStd/rtlOutErr implementations
- hbrtl/register.go: register OUTSTD and OUTERR
- analyzer.go: add OUTSTD/OUTERR to RTL known-functions
5. FIELD keyword triggered "undeclared variable" warnings
- analyzer.go: add FIELD, _FIELD, M, MEMVAR as builtin constants
Tests:
go test ./... — ALL PASS (17 packages)
FiveSql2 43/43 — 100%
compat_harbour 51/51 — 100%
Harbour diff — 0 lines differ (281-line comparison)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -467,7 +467,7 @@ var rtlFunctions = map[string]bool{
|
|||||||
"TYPE": true, "PCOUNT": true, "BREAK": true, "ARRAY": true, "FCOUNT": true,
|
"TYPE": true, "PCOUNT": true, "BREAK": true, "ARRAY": true, "FCOUNT": true,
|
||||||
"FIELDNAME": true, "SELECT": true, "FILE": true, "INKEY": true, "TRANSFORM": true,
|
"FIELDNAME": true, "SELECT": true, "FILE": true, "INKEY": true, "TRANSFORM": true,
|
||||||
"SETDATEFORMAT": true, "SETEPOCH": true, "SETCENTURY": true,
|
"SETDATEFORMAT": true, "SETEPOCH": true, "SETCENTURY": true,
|
||||||
"IIF": true, "IF": true, "STRZERO": true, "OUTSTD": true,
|
"IIF": true, "IF": true, "STRZERO": true, "OUTSTD": true, "OUTERR": true,
|
||||||
"CENTER": true, "SOUNDEX": true, "TONE": true,
|
"CENTER": true, "SOUNDEX": true, "TONE": true,
|
||||||
// Terminal
|
// Terminal
|
||||||
"SETPOS": true, "ROW": true, "COL": true, "DEVPOS": true, "DEVOUT": true,
|
"SETPOS": true, "ROW": true, "COL": true, "DEVPOS": true, "DEVOUT": true,
|
||||||
@@ -634,6 +634,8 @@ func (a *Analyzer) isBuiltinConstant(name string) bool {
|
|||||||
"SELF": true, "SUPER": true,
|
"SELF": true, "SUPER": true,
|
||||||
// Harbour commands treated as identifiers
|
// Harbour commands treated as identifiers
|
||||||
"QUIT": true, "ERRORLEVEL": true,
|
"QUIT": true, "ERRORLEVEL": true,
|
||||||
|
// Field/Memvar alias prefixes
|
||||||
|
"FIELD": true, "_FIELD": true, "M": true, "MEMVAR": true,
|
||||||
// Keyboard constants
|
// Keyboard constants
|
||||||
"K_ESC": true, "K_ENTER": true, "K_UP": true, "K_DOWN": true,
|
"K_ESC": true, "K_ENTER": true, "K_UP": true, "K_DOWN": true,
|
||||||
"K_LEFT": true, "K_RIGHT": true, "K_PGUP": true, "K_PGDN": true,
|
"K_LEFT": true, "K_RIGHT": true, "K_PGUP": true, "K_PGDN": true,
|
||||||
|
|||||||
@@ -852,6 +852,17 @@ func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for alias->field := value (FIELD->NAME := value)
|
||||||
|
if aliasExpr, ok := a.Left.(*ast.AliasExpr); ok {
|
||||||
|
if aliasIdent, ok2 := aliasExpr.Alias.(*ast.IdentExpr); ok2 {
|
||||||
|
if fieldIdent, ok3 := aliasExpr.Field.(*ast.IdentExpr); ok3 {
|
||||||
|
g.emitExpr(a.Right)
|
||||||
|
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); _wa.SetAliasField(%q, %q, t.Pop2()) }`, aliasIdent.Name, fieldIdent.Name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ident, ok := a.Left.(*ast.IdentExpr); ok {
|
if ident, ok := a.Left.(*ast.IdentExpr); ok {
|
||||||
if idx, found := locals[strings.ToUpper(ident.Name)]; found {
|
if idx, found := locals[strings.ToUpper(ident.Name)]; found {
|
||||||
switch a.Op {
|
switch a.Op {
|
||||||
@@ -1749,6 +1760,17 @@ func (g *Generator) emitCall(e *ast.CallExpr) {
|
|||||||
|
|
||||||
// emitAssignExpr handles := / += / -= in expression context (e.g. code block body).
|
// emitAssignExpr handles := / += / -= in expression context (e.g. code block body).
|
||||||
func (g *Generator) emitAssignExpr(e *ast.AssignExpr) {
|
func (g *Generator) emitAssignExpr(e *ast.AssignExpr) {
|
||||||
|
// alias->field := value in expression context
|
||||||
|
if aliasExpr, ok := e.Left.(*ast.AliasExpr); ok {
|
||||||
|
if aliasIdent, ok2 := aliasExpr.Alias.(*ast.IdentExpr); ok2 {
|
||||||
|
if fieldIdent, ok3 := aliasExpr.Field.(*ast.IdentExpr); ok3 {
|
||||||
|
g.emitExpr(e.Right)
|
||||||
|
g.writeln("t.Dup()")
|
||||||
|
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); _wa.SetAliasField(%q, %q, t.Pop2()) }`, aliasIdent.Name, fieldIdent.Name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if ident, ok := e.Left.(*ast.IdentExpr); ok {
|
if ident, ok := e.Left.(*ast.IdentExpr); ok {
|
||||||
if idx, found := g.curLocals[strings.ToUpper(ident.Name)]; found {
|
if idx, found := g.curLocals[strings.ToUpper(ident.Name)]; found {
|
||||||
switch e.Op {
|
switch e.Op {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
@@ -376,6 +377,14 @@ func OpenIndex(path string) (*Index, error) {
|
|||||||
// points to the tag header at a specific file offset.
|
// points to the tag header at a specific file offset.
|
||||||
tagEntries := readCompoundTagList(idx, rootHdr)
|
tagEntries := readCompoundTagList(idx, rootHdr)
|
||||||
|
|
||||||
|
// Harbour orders tags by file offset (TagBlock) ascending, which
|
||||||
|
// corresponds to creation order. The compound B-tree stores entries
|
||||||
|
// alphabetically, so we must re-sort by offset to match Harbour.
|
||||||
|
// See: hb_cdxIndexLoadAvailTags in dbfcdx1.c
|
||||||
|
sort.Slice(tagEntries, func(i, j int) bool {
|
||||||
|
return tagEntries[i].offset < tagEntries[j].offset
|
||||||
|
})
|
||||||
|
|
||||||
for _, entry := range tagEntries {
|
for _, entry := range tagEntries {
|
||||||
tagHdr, err := ReadTagHeader(f, entry.offset)
|
tagHdr, err := ReadTagHeader(f, entry.offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
package hbrdd
|
package hbrdd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"five/hbrt"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -213,11 +214,18 @@ func (wm *WorkAreaManager) CloseAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAliasField returns a field value from a named alias.
|
// GetAliasField returns a field value from a named alias.
|
||||||
// Used by alias->field syntax.
|
// Used by alias->field syntax (e.g., CUSTOMERS->NAME, FIELD->AGE).
|
||||||
func (wm *WorkAreaManager) GetAliasField(alias, field string) interface{} {
|
func (wm *WorkAreaManager) GetAliasField(alias, field string) hbrt.Value {
|
||||||
area := wm.ByAlias(alias)
|
var area Area
|
||||||
|
|
||||||
|
// FIELD-> is a special alias meaning "current workarea"
|
||||||
|
if strings.EqualFold(alias, "FIELD") || strings.EqualFold(alias, "_FIELD") {
|
||||||
|
area = wm.Current()
|
||||||
|
} else {
|
||||||
|
area = wm.ByAlias(alias)
|
||||||
|
}
|
||||||
if area == nil {
|
if area == nil {
|
||||||
return nil
|
return hbrt.MakeNil()
|
||||||
}
|
}
|
||||||
// Find field by name
|
// Find field by name
|
||||||
for i := 0; i < area.FieldCount(); i++ {
|
for i := 0; i < area.FieldCount(); i++ {
|
||||||
@@ -227,7 +235,28 @@ func (wm *WorkAreaManager) GetAliasField(alias, field string) interface{} {
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return hbrt.MakeNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAliasField sets a field value by alias->field syntax.
|
||||||
|
func (wm *WorkAreaManager) SetAliasField(alias, field string, val hbrt.Value) {
|
||||||
|
var area Area
|
||||||
|
|
||||||
|
if strings.EqualFold(alias, "FIELD") || strings.EqualFold(alias, "_FIELD") {
|
||||||
|
area = wm.Current()
|
||||||
|
} else {
|
||||||
|
area = wm.ByAlias(alias)
|
||||||
|
}
|
||||||
|
if area == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < area.FieldCount(); i++ {
|
||||||
|
fi := area.GetFieldInfo(i)
|
||||||
|
if strings.EqualFold(fi.Name, field) {
|
||||||
|
area.PutValue(i, val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package hbrtl
|
|||||||
import (
|
import (
|
||||||
"five/hbrt"
|
"five/hbrt"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,4 +84,30 @@ func valueToDisplay(v hbrt.Value) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rtlOutStd writes values to stdout without newline. Harbour: OutStd()
|
||||||
|
func rtlOutStd(t *hbrt.Thread) {
|
||||||
|
nParams := t.ParamCount()
|
||||||
|
t.Frame(nParams, 0)
|
||||||
|
defer t.EndProcFast()
|
||||||
|
parts := make([]string, nParams)
|
||||||
|
for i := 0; i < nParams; i++ {
|
||||||
|
parts[i] = valueToDisplay(t.Local(i + 1))
|
||||||
|
}
|
||||||
|
fmt.Print(strings.Join(parts, ""))
|
||||||
|
t.RetNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// rtlOutErr writes values to stderr without newline. Harbour: OutErr()
|
||||||
|
func rtlOutErr(t *hbrt.Thread) {
|
||||||
|
nParams := t.ParamCount()
|
||||||
|
t.Frame(nParams, 0)
|
||||||
|
defer t.EndProcFast()
|
||||||
|
parts := make([]string, nParams)
|
||||||
|
for i := 0; i < nParams; i++ {
|
||||||
|
parts[i] = valueToDisplay(t.Local(i + 1))
|
||||||
|
}
|
||||||
|
fmt.Fprint(os.Stderr, strings.Join(parts, ""))
|
||||||
|
t.RetNil()
|
||||||
|
}
|
||||||
|
|
||||||
// julianToDateStr and date formatting moved to datetime.go
|
// julianToDateStr and date formatting moved to datetime.go
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"five/hbrt"
|
"five/hbrt"
|
||||||
"five/hbrdd"
|
"five/hbrdd"
|
||||||
"five/hbrdd/dbf"
|
"five/hbrdd/dbf"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// INDEXORD() → nCurrentOrder (1-based, 0 = natural)
|
// INDEXORD() → nCurrentOrder (1-based, 0 = natural)
|
||||||
@@ -74,8 +75,8 @@ func OrdSetFocus(t *hbrt.Thread) {
|
|||||||
if idx, ok := area.(hbrdd.Indexer); ok {
|
if idx, ok := area.(hbrdd.Indexer); ok {
|
||||||
v := t.Local(1)
|
v := t.Local(1)
|
||||||
if v.IsNumeric() {
|
if v.IsNumeric() {
|
||||||
// SET ORDER TO n
|
// SET ORDER TO n — convert number to digit string for OrderListFocus
|
||||||
idx.OrderListFocus(v.AsString())
|
idx.OrderListFocus(fmt.Sprintf("%d", v.AsNumInt()))
|
||||||
} else {
|
} else {
|
||||||
idx.OrderListFocus(v.AsString())
|
idx.OrderListFocus(v.AsString())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ func RegisterRTL(vm *hbrt.VM) {
|
|||||||
// Console
|
// Console
|
||||||
hbrt.Sym("QOUT", hbrt.FsPublic, rtlQOut),
|
hbrt.Sym("QOUT", hbrt.FsPublic, rtlQOut),
|
||||||
hbrt.Sym("QQOUT", hbrt.FsPublic, rtlQQOut),
|
hbrt.Sym("QQOUT", hbrt.FsPublic, rtlQQOut),
|
||||||
|
hbrt.Sym("OUTSTD", hbrt.FsPublic, rtlOutStd),
|
||||||
|
hbrt.Sym("OUTERR", hbrt.FsPublic, rtlOutErr),
|
||||||
|
|
||||||
// Strings / Conversion
|
// Strings / Conversion
|
||||||
hbrt.Sym("STR", hbrt.FsPublic, Str),
|
hbrt.Sym("STR", hbrt.FsPublic, Str),
|
||||||
|
|||||||
Reference in New Issue
Block a user