From e95afad4eec8bd53e9f483e3e5f9ca622ad76eb6 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Sat, 11 Apr 2026 16:37:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Harbour=20RDD=20parity=20=E2=80=94=20NT?= =?UTF-8?q?X/CDX=20100%=20compatible,=20FIELD->=20works?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- compiler/analyzer/analyzer.go | 4 +++- compiler/gengo/gengo.go | 22 ++++++++++++++++++++ hbrdd/cdx/cdx.go | 9 ++++++++ hbrdd/workarea.go | 39 ++++++++++++++++++++++++++++++----- hbrtl/console.go | 27 ++++++++++++++++++++++++ hbrtl/indexrtl.go | 5 +++-- hbrtl/register.go | 2 ++ 7 files changed, 100 insertions(+), 8 deletions(-) diff --git a/compiler/analyzer/analyzer.go b/compiler/analyzer/analyzer.go index 8d806cc..7057e4d 100644 --- a/compiler/analyzer/analyzer.go +++ b/compiler/analyzer/analyzer.go @@ -467,7 +467,7 @@ var rtlFunctions = map[string]bool{ "TYPE": true, "PCOUNT": true, "BREAK": true, "ARRAY": true, "FCOUNT": true, "FIELDNAME": true, "SELECT": true, "FILE": true, "INKEY": true, "TRANSFORM": 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, // Terminal "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, // Harbour commands treated as identifiers "QUIT": true, "ERRORLEVEL": true, + // Field/Memvar alias prefixes + "FIELD": true, "_FIELD": true, "M": true, "MEMVAR": true, // Keyboard constants "K_ESC": true, "K_ENTER": true, "K_UP": true, "K_DOWN": true, "K_LEFT": true, "K_RIGHT": true, "K_PGUP": true, "K_PGDN": true, diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 9669032..c4672ee 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -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 idx, found := locals[strings.ToUpper(ident.Name)]; found { switch a.Op { @@ -1749,6 +1760,17 @@ func (g *Generator) emitCall(e *ast.CallExpr) { // emitAssignExpr handles := / += / -= in expression context (e.g. code block body). 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 idx, found := g.curLocals[strings.ToUpper(ident.Name)]; found { switch e.Op { diff --git a/hbrdd/cdx/cdx.go b/hbrdd/cdx/cdx.go index 34d3ba8..39cdfc1 100644 --- a/hbrdd/cdx/cdx.go +++ b/hbrdd/cdx/cdx.go @@ -22,6 +22,7 @@ import ( "encoding/binary" "fmt" "os" + "sort" "strings" "syscall" ) @@ -376,6 +377,14 @@ func OpenIndex(path string) (*Index, error) { // points to the tag header at a specific file offset. 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 { tagHdr, err := ReadTagHeader(f, entry.offset) if err != nil { diff --git a/hbrdd/workarea.go b/hbrdd/workarea.go index 9bd0939..3c17ab7 100644 --- a/hbrdd/workarea.go +++ b/hbrdd/workarea.go @@ -9,6 +9,7 @@ package hbrdd import ( + "five/hbrt" "fmt" "strings" ) @@ -213,11 +214,18 @@ func (wm *WorkAreaManager) CloseAll() { } // GetAliasField returns a field value from a named alias. -// Used by alias->field syntax. -func (wm *WorkAreaManager) GetAliasField(alias, field string) interface{} { - area := wm.ByAlias(alias) +// Used by alias->field syntax (e.g., CUSTOMERS->NAME, FIELD->AGE). +func (wm *WorkAreaManager) GetAliasField(alias, field string) hbrt.Value { + 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 { - return nil + return hbrt.MakeNil() } // Find field by name for i := 0; i < area.FieldCount(); i++ { @@ -227,7 +235,28 @@ func (wm *WorkAreaManager) GetAliasField(alias, field string) interface{} { 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 --- diff --git a/hbrtl/console.go b/hbrtl/console.go index d9d3dad..598ae81 100644 --- a/hbrtl/console.go +++ b/hbrtl/console.go @@ -8,6 +8,7 @@ package hbrtl import ( "five/hbrt" "fmt" + "os" "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 diff --git a/hbrtl/indexrtl.go b/hbrtl/indexrtl.go index 6f91cf7..9fbbca9 100644 --- a/hbrtl/indexrtl.go +++ b/hbrtl/indexrtl.go @@ -11,6 +11,7 @@ import ( "five/hbrt" "five/hbrdd" "five/hbrdd/dbf" + "fmt" ) // INDEXORD() → nCurrentOrder (1-based, 0 = natural) @@ -74,8 +75,8 @@ func OrdSetFocus(t *hbrt.Thread) { if idx, ok := area.(hbrdd.Indexer); ok { v := t.Local(1) if v.IsNumeric() { - // SET ORDER TO n - idx.OrderListFocus(v.AsString()) + // SET ORDER TO n — convert number to digit string for OrderListFocus + idx.OrderListFocus(fmt.Sprintf("%d", v.AsNumInt())) } else { idx.OrderListFocus(v.AsString()) } diff --git a/hbrtl/register.go b/hbrtl/register.go index 8c5c661..a88bf77 100644 --- a/hbrtl/register.go +++ b/hbrtl/register.go @@ -24,6 +24,8 @@ func RegisterRTL(vm *hbrt.VM) { // Console hbrt.Sym("QOUT", hbrt.FsPublic, rtlQOut), hbrt.Sym("QQOUT", hbrt.FsPublic, rtlQQOut), + hbrt.Sym("OUTSTD", hbrt.FsPublic, rtlOutStd), + hbrt.Sym("OUTERR", hbrt.FsPublic, rtlOutErr), // Strings / Conversion hbrt.Sym("STR", hbrt.FsPublic, Str),