Files
five/hbrdd/workarea.go
CharlesKWON e95afad4ee 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>
2026-04-11 16:37:47 +09:00

283 lines
6.7 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// WorkArea manager — goroutine-local (no locks needed).
// Harbour: HB_STACKRDD in hbstack.h — per-thread workarea list.
//
// Each Thread owns its own WorkAreaManager. No cross-goroutine sharing.
// This eliminates Harbour's global workarea table threading issues.
package hbrdd
import (
"five/hbrt"
"fmt"
"strings"
)
// WorkAreaManager manages open work areas for a single goroutine.
// Harbour: waList[], waNums[], pCurrArea in HB_STACKRDD.
type WorkAreaManager struct {
areas map[uint16]Area // area number → Area
aliases map[string]uint16 // alias name (UPPER) → area number
current uint16 // currently selected area number
nextNum uint16 // next available area number
}
// NewWorkAreaManager creates a new goroutine-local workarea manager.
func NewWorkAreaManager() *WorkAreaManager {
return &WorkAreaManager{
areas: make(map[uint16]Area),
aliases: make(map[string]uint16),
nextNum: 1, // Harbour: area numbers start at 1
}
}
// Open opens a table and registers it with an alias.
// Returns the assigned area number.
func (wm *WorkAreaManager) Open(driverName, path, alias string, shared, readOnly bool) (uint16, error) {
drv, err := GetDriver(driverName)
if err != nil {
return 0, err
}
if alias == "" {
// Default alias: filename without extension
alias = extractBaseName(path)
}
alias = strings.ToUpper(alias)
// Check duplicate alias
if _, exists := wm.aliases[alias]; exists {
return 0, fmt.Errorf("alias already in use: %s", alias)
}
area, err := drv.Open(OpenParams{
Path: path,
Alias: alias,
Shared: shared,
ReadOnly: readOnly,
})
if err != nil {
return 0, err
}
// Store alias in the area so Alias() returns it
area.SetAlias(alias)
// Use the pre-selected area number if available and empty
num := wm.current
if num == 0 || wm.areas[num] != nil {
num = wm.nextNum
}
if num >= wm.nextNum {
wm.nextNum = num + 1
}
wm.areas[num] = area
wm.aliases[alias] = num
wm.current = num
return num, nil
}
// Close closes the current work area.
func (wm *WorkAreaManager) Close() error {
if wm.current == 0 {
return nil
}
area, ok := wm.areas[wm.current]
if !ok {
return nil
}
// Remove alias
for alias, num := range wm.aliases {
if num == wm.current {
delete(wm.aliases, alias)
break
}
}
err := area.Close()
delete(wm.areas, wm.current)
wm.current = 0
return err
}
// Select switches to a work area by alias name or number.
// Harbour: SELECT command.
func (wm *WorkAreaManager) Select(aliasOrNum interface{}) error {
switch v := aliasOrNum.(type) {
case string:
// Try as alias first
num, ok := wm.aliases[strings.ToUpper(v)]
if ok {
wm.current = num
return nil
}
// Try as numeric string ("1", "2", etc.)
if n := parseAreaNum(v); n > 0 {
wm.current = n
return nil
}
// Select 0 = select unused area
if v == "0" || v == "" {
wm.current = wm.nextNum
return nil
}
return fmt.Errorf("alias not found: %s", v)
case int:
// Harbour: dbSelectArea(n) always switches, even to empty areas
wm.current = uint16(v)
case uint16:
wm.current = v
default:
return fmt.Errorf("invalid area selector: %T", aliasOrNum)
}
return nil
}
// Current returns the currently selected work area, or nil.
func (wm *WorkAreaManager) Current() Area {
if wm.current == 0 {
return nil
}
return wm.areas[wm.current]
}
// CurrentNum returns the current work area number.
func (wm *WorkAreaManager) CurrentNum() uint16 {
return wm.current
}
// ByAlias returns a work area by alias name.
func (wm *WorkAreaManager) ByAlias(alias string) Area {
num, ok := wm.aliases[strings.ToUpper(alias)]
if !ok {
return nil
}
return wm.areas[num]
}
// FindByAlias returns the area number for an alias (0 if not found).
func (wm *WorkAreaManager) FindByAlias(alias string) uint16 {
num, ok := wm.aliases[strings.ToUpper(alias)]
if !ok {
return 0
}
return num
}
// SelectByAlias switches to a work area by alias name.
// Used by static alias context expressions: CUSTOMERS->(RecCount())
func (wm *WorkAreaManager) SelectByAlias(alias string) {
num, ok := wm.aliases[strings.ToUpper(alias)]
if ok {
wm.current = num
}
}
// SelectByNum switches to a work area by number, silently allowing unused areas.
// Used by workarea context expressions: (nArea)->(expr)
// Unlike Select(), this does NOT error on unused area numbers —
// Harbour allows selecting any area 1-250 even if nothing is open there.
func (wm *WorkAreaManager) SelectByNum(num uint16) {
wm.current = num
}
// AreaAt returns the Area at a given area number (nil if not in use).
func (wm *WorkAreaManager) AreaAt(num uint16) Area {
return wm.areas[num]
}
// parseAreaNum tries to parse a string as a work area number.
func parseAreaNum(s string) uint16 {
s = strings.TrimSpace(s)
n := 0
for _, c := range s {
if c >= '0' && c <= '9' {
n = n*10 + int(c-'0')
} else {
return 0
}
}
return uint16(n)
}
// CloseAll closes all open work areas.
func (wm *WorkAreaManager) CloseAll() {
for num, area := range wm.areas {
area.Close()
delete(wm.areas, num)
}
wm.aliases = make(map[string]uint16)
wm.current = 0
}
// GetAliasField returns a field value from a named 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 hbrt.MakeNil()
}
// Find field by name
for i := 0; i < area.FieldCount(); i++ {
fi := area.GetFieldInfo(i)
if strings.EqualFold(fi.Name, field) {
val, _ := area.GetValue(i)
return val
}
}
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 ---
func extractBaseName(path string) string {
// Extract filename without path and extension
name := path
// Remove path
for i := len(name) - 1; i >= 0; i-- {
if name[i] == '/' || name[i] == '\\' {
name = name[i+1:]
break
}
}
// Remove extension
for i := len(name) - 1; i >= 0; i-- {
if name[i] == '.' {
name = name[:i]
break
}
}
return name
}