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>
114 lines
2.7 KiB
Go
114 lines
2.7 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Console I/O functions for the Five runtime library.
|
|
// Implements Harbour's QOut (?), QQOut (??), and related output functions.
|
|
package hbrtl
|
|
|
|
import (
|
|
"five/hbrt"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// QOut implements the ? command. Prints newline then values separated by space.
|
|
// Harbour: QOut() / hb_conOutStd()
|
|
func QOut(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
args := make([]hbrt.Value, nParams)
|
|
for i := 0; i < nParams; i++ {
|
|
args[i] = t.Local(i + 1)
|
|
}
|
|
qoutImpl(args)
|
|
t.RetNil()
|
|
}
|
|
|
|
// qoutImpl is the actual implementation called with pre-collected args.
|
|
func qoutImpl(args []hbrt.Value) {
|
|
parts := make([]string, len(args))
|
|
for i, v := range args {
|
|
parts[i] = valueToDisplay(v)
|
|
}
|
|
fmt.Print("\r\n" + strings.Join(parts, " "))
|
|
}
|
|
|
|
// qqoutImpl prints without leading newline (??).
|
|
func qqoutImpl(args []hbrt.Value) {
|
|
parts := make([]string, len(args))
|
|
for i, v := range args {
|
|
parts[i] = valueToDisplay(v)
|
|
}
|
|
fmt.Print(strings.Join(parts, " "))
|
|
}
|
|
|
|
// valueToDisplay converts a Value to its display string.
|
|
// Harbour: hb_itemString()
|
|
func valueToDisplay(v hbrt.Value) string {
|
|
switch {
|
|
case v.IsNil():
|
|
return "NIL"
|
|
case v.IsLogical():
|
|
if v.AsBool() {
|
|
return ".T."
|
|
}
|
|
return ".F."
|
|
case v.IsInt():
|
|
return fmt.Sprintf("%d", v.AsInt())
|
|
case v.IsLong():
|
|
return fmt.Sprintf("%d", v.AsLong())
|
|
case v.IsDouble():
|
|
dec := v.Decimal()
|
|
if dec == 255 {
|
|
return fmt.Sprintf("%g", v.AsDouble())
|
|
}
|
|
return fmt.Sprintf("%.*f", dec, v.AsDouble())
|
|
case v.IsString():
|
|
return v.AsString()
|
|
case v.IsDate():
|
|
return julianToDateStr(v.AsJulian())
|
|
case v.IsTimestamp():
|
|
y, m, d := julianToDate(v.AsJulian())
|
|
ms := v.AsTimeMs()
|
|
hh := ms / 3600000
|
|
mm := ms / 60000 % 60
|
|
ss := ms / 1000 % 60
|
|
return fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", y, m, d, hh, mm, ss)
|
|
case v.IsArray():
|
|
return fmt.Sprintf("{Array(%d)}", len(v.AsArray().Items))
|
|
default:
|
|
return v.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
|