Six audit-driven blockers landed together because they're tangled:
* MENU TO removed from std.ch — the rule expanded to a call to a
nonexistent __MenuTo() RTL symbol, so any user code with `MENU
TO choice` compiled clean and panicked at runtime. Behavior
pre-this-round was a parser silent no-op, which is at least
consistent. Restore that until @ PROMPT (the companion command)
actually lands.
* COUNT now requires `TO <var>`. The earlier `[TO <v>]` optional
bracket was a Harbour-pattern transcription error: the result
template references `<v>` unconditionally, so a bare `COUNT`
expanded to ungrammatical ` := 0 ; dbEval(...)` and the
PRG parser rejected it. Match Harbour's std.ch which makes TO
mandatory.
* UPDATE FROM ... REPLACE now requires `FROM`/`ON`/`REPLACE` all
three. Same root cause as COUNT: the result template uses
`<key>`, `<f1>`, `<x1>` unconditionally; missing any of them
produced broken syntax. Tightened to fail loudly rather than
silently mis-expand.
* CLOSE <unknown_alias> no longer closes the *current* workarea.
SelectByAlias was a silent no-op when the alias was missing,
leaving WASaveAndSelectAlias to evaluate the inner DbCloseArea()
against the originally-selected WA — a real data-loss footgun.
SelectByAlias now returns bool; WASaveAndSelectAlias switches to
the no-area sentinel (0) on miss so the inner expression's
Current() returns nil and short-circuits.
* SUM <x1>, <xN> TO <v1>, <vN> — multi-pair form supported.
Required two pieces:
1. matchSegment's regular-marker stop-boundary now combines
outerTail literals AND the segment's repeat boundary so
`[, <xN>]` doesn't let `<xN>` swallow past the next ','.
2. **Five parser miscompiled comma-separated expressions in
code blocks.** `{|| e1, e2, e3 }` kept only the last expr
and threw away earlier ones at *AST level*, so all their
side effects vanished. New SeqExpr AST node + emitter
(emit each, pop intermediate results) + folding/walk
updates fix the underlying bug, which also unbreaks any
other block that relied on comma sequencing.
* pp.go's `;` continuation joiner now strips exactly one trailing
`;` per iteration, preserving Harbour's `;;` convention (literal
`;` followed by a continuation marker). Without this the SUM
rule's chained `<v1> :=[ <vN> :=] 0 ; ; dbEval(...)` collapsed
to a missing statement separator.
* parseExprStmt's xBase fallback switch is back in sync with
parseIdentStmt — COPY/SORT/COUNT/SUM/AVERAGE/TOTAL/UPDATE/JOIN/
DISPLAY/LIST removed (std.ch handles all of them now). Leaving
them in the fallback masked typos as silent no-ops.
Gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
311 lines
7.8 KiB
Go
311 lines
7.8 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. Returns true
|
|
// when the alias resolved, false when nothing matched. Callers that
|
|
// intend to evaluate an expression in the named workarea (alias
|
|
// context: `FOO->(...)`) must check the return value — a missed
|
|
// alias used to silently leave the original WA selected, so a
|
|
// subsequent DbCloseArea/RecCount/etc. ran against the *current*
|
|
// workarea instead of the named one. That's the exact data-loss
|
|
// foot-gun that bit `CLOSE bad_alias`. WASaveAndSelectAlias now
|
|
// switches to the no-area sentinel (0) on miss so the inner
|
|
// expression sees Current() == nil and short-circuits cleanly.
|
|
func (wm *WorkAreaManager) SelectByAlias(alias string) bool {
|
|
num, ok := wm.aliases[strings.ToUpper(alias)]
|
|
if ok {
|
|
wm.current = num
|
|
}
|
|
return ok
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// EnumerateAreas invokes fn once per open workarea with (nWA, alias, area).
|
|
// Snapshot of the slot→area map is taken first so fn can safely manipulate
|
|
// workareas without mutating the loop. Used by the diagnostic ErrorLog
|
|
// writer to dump every open table's state.
|
|
func (wm *WorkAreaManager) EnumerateAreas(fn func(nWA uint16, alias string, area Area)) {
|
|
type slot struct {
|
|
num uint16
|
|
alias string
|
|
area Area
|
|
}
|
|
snapshot := make([]slot, 0, len(wm.areas))
|
|
for num, area := range wm.areas {
|
|
snapshot = append(snapshot, slot{num, area.Alias(), area})
|
|
}
|
|
for _, s := range snapshot {
|
|
fn(s.num, s.alias, s.area)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|