fix: 3 RDD compat bugs — FIELD->, AsNumInt Double, PACK/ZAP with index

Bug 1: FIELD->NAME in INDEX ON expression
- evalKeyExprInner: strip FIELD->/alias-> prefix before field lookup
- exprToString: handle AliasExpr (FIELD->NAME → "FIELD->NAME")

Bug 2: AsNumInt() on Double returned IEEE 754 raw bits
- Value.AsNumInt(): check tDouble and convert via Float64frombits
- Fixed array index crash when index is result of % modulo

Bug 3: PACK/ZAP crash with open indexes
- OrderListRebuild: fully implemented (was TODO stub)
  Saves index info, closes all, sets idxState=nil, recreates
- OrderCreate: set current=-1 during key evaluation (natural GoTo)
- PACK/ZAP: save/restore idxState, rebuild after operation
- Register __DBPACK, __DBZAP, DBRECALL symbol aliases

Harbour vs Five: 45/47 match (96%), 2 diffs are duplicate-key sort order

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 04:41:19 +09:00
parent 53370e7cbc
commit 6e78d12cc2
6 changed files with 108 additions and 7 deletions

View File

@@ -1253,6 +1253,11 @@ func exprToString(expr ast.Expr) string {
}
return ident.Name + "(" + args + ")"
}
case *ast.AliasExpr:
// FIELD->NAME, M->VAR, ALIAS->FIELD
alias := exprToString(e.Alias)
field := exprToString(e.Field)
return alias + "->" + field
}
return ""
}

View File

@@ -17,7 +17,7 @@ PROCEDURE Main()
APPEND BLANK
REPLACE ID WITH i
REPLACE NAME WITH PadR("Name_" + LTrim(Str(i)), 20)
nIdx := Int(((i-1) % 5)) + 1
nIdx := ((i-1) % 5) + 1
REPLACE CITY WITH PadR(aCities[nIdx], 15)
REPLACE SALARY WITH 30000 + i * 1000.50
REPLACE ACTIVE WITH (i % 3 != 0)
@@ -122,7 +122,7 @@ PROCEDURE Main()
RECALL
SET DELETED OFF
INDEX ON NAME TO compat_idx1
INDEX ON FIELD->NAME TO compat_idx1
Out("T26", "OK")
GO TOP
@@ -164,7 +164,7 @@ PROCEDURE Main()
GO TOP
Out("T39", RTrim(FieldGet(2)))
INDEX ON CITY TO compat_idx2
INDEX ON FIELD->CITY TO compat_idx2
SEEK PadR("Seoul", 15)
Out("T40", IIF(Found(), ".T.", ".F.") + " " + LTrim(Str(FieldGet(1))))

View File

@@ -631,6 +631,13 @@ func (a *DBFArea) Pack() error {
a.flushRecord()
}
// Temporarily disable index to avoid indexed navigation during PACK
var savedIdx *indexState
if a.idxState != nil {
savedIdx = a.idxState
a.idxState = nil
}
outRec := uint32(0)
buf := make([]byte, a.header.RecordLen)
@@ -661,7 +668,7 @@ func (a *DBFArea) Pack() error {
// Update header
a.updateHeader()
// Reposition
// Reposition (natural order, no index yet)
if a.recCount > 0 {
a.GoTo(1)
} else {
@@ -669,6 +676,14 @@ func (a *DBFArea) Pack() error {
a.recNo = 1
}
// Rebuild indexes (record numbers changed after PACK)
if savedIdx != nil {
a.idxState = savedIdx
if err := a.OrderListRebuild(); err != nil {
return err
}
}
return nil
}
@@ -678,6 +693,13 @@ func (a *DBFArea) Zap() error {
return fmt.Errorf("ZAP requires exclusive access")
}
// Save index state
var savedIdx *indexState
if a.idxState != nil {
savedIdx = a.idxState
a.idxState = nil
}
a.recCount = 0
a.header.RecCount = 0
@@ -688,6 +710,13 @@ func (a *DBFArea) Zap() error {
a.updateHeader()
a.FEof = true
a.recNo = 1
// Rebuild indexes (empty after ZAP)
if savedIdx != nil {
a.idxState = savedIdx
a.OrderListRebuild() // rebuilds empty indexes
}
return nil
}

View File

@@ -38,6 +38,9 @@ func (a *DBFArea) ensureIndexState() {
func (a *DBFArea) OrderCreate(params hbrdd.OrderCreateParams) error {
a.ensureIndexState()
// Disable indexed navigation during key evaluation (GoTo must use natural order)
a.idxState.current = -1
idxPath := params.FilePath
if idxPath == "" {
return fmt.Errorf("index file path required")
@@ -170,8 +173,58 @@ func (a *DBFArea) OrderListFocus(tagName string) error {
}
// OrderListRebuild rebuilds all indexes.
// Harbour: ORDLISTREBUILD / REINDEX — recreates all open indexes from current data.
func (a *DBFArea) OrderListRebuild() error {
// TODO: reindex all open indexes
if a.idxState == nil || len(a.idxState.indexes) == 0 {
return nil
}
// Save current index info
savedCurrent := a.idxState.current
type idxInfo struct {
name string
tag string
keyExpr string
}
infos := make([]idxInfo, len(a.idxState.indexes))
for i := range a.idxState.indexes {
infos[i] = idxInfo{
name: a.idxState.names[i],
tag: a.idxState.tags[i],
keyExpr: a.idxState.keyExprs[i],
}
}
// Close all indexes and disable indexed navigation
for _, idx := range a.idxState.indexes {
idx.Close()
}
a.idxState.indexes = nil
a.idxState.names = nil
a.idxState.tags = nil
a.idxState.keyExprs = nil
a.idxState.current = -1
// Remove idxState so GoTo uses natural order during rebuild
a.idxState = nil
// Recreate each index
for _, info := range infos {
err := a.OrderCreate(hbrdd.OrderCreateParams{
KeyExpr: info.keyExpr,
FilePath: info.name,
TagName: info.tag,
})
if err != nil {
return fmt.Errorf("rebuild index %s: %w", info.name, err)
}
}
// Restore active index
if a.idxState != nil && savedCurrent >= 0 && savedCurrent < len(a.idxState.indexes) {
a.idxState.current = savedCurrent
}
return nil
}
@@ -361,10 +414,16 @@ func (a *DBFArea) evalKeyExprInner(expr string) []byte {
return []byte(expr[1 : len(expr)-1])
}
// Strip FIELD-> or _FIELD-> or alias-> prefix (Harbour: M->var, FIELD->var)
fieldName := upper
if idx := strings.Index(fieldName, "->"); idx >= 0 {
fieldName = fieldName[idx+2:]
}
// Simple field name
for i := 0; i < a.FieldCount(); i++ {
fi := a.GetFieldInfo(i)
if strings.ToUpper(fi.Name) == upper {
if strings.ToUpper(fi.Name) == fieldName {
val, _ := a.GetValue(i)
return formatKeyValue(val, fi)
}

View File

@@ -155,7 +155,12 @@ func (v Value) AsLong() int64 { return int64(v.scalar) }
func (v Value) AsDouble() float64 { return math.Float64frombits(v.scalar) }
func (v Value) AsJulian() int64 { return int64(v.scalar) }
func (v Value) AsTimeMs() int32 { return int32(v.info & auxMask) }
func (v Value) AsNumInt() int64 { return int64(v.scalar) }
func (v Value) AsNumInt() int64 {
if v.Type() == tDouble {
return int64(math.Float64frombits(v.scalar))
}
return int64(v.scalar)
}
// AsNumDouble returns a double value from any numeric type.
func (v Value) AsNumDouble() float64 {

View File

@@ -178,6 +178,9 @@ func RegisterRTL(vm *hbrt.VM) {
hbrt.Sym("RECALL", hbrt.FsPublic, rtlDbRecall),
hbrt.Sym("PACK", hbrt.FsPublic, rtlDbPack),
hbrt.Sym("ZAP", hbrt.FsPublic, rtlDbZap),
hbrt.Sym("__DBPACK", hbrt.FsPublic, rtlDbPack),
hbrt.Sym("__DBZAP", hbrt.FsPublic, rtlDbZap),
hbrt.Sym("DBRECALL", hbrt.FsPublic, rtlDbRecall),
// Locate / Filter
hbrt.Sym("DBLOCATE", hbrt.FsPublic, rtlDbLocate),