Files
five/hbrtl/missing.go
CharlesKWON 3adc9d7d59 fix: PCount, Break/RECOVER, SET INDEX TO — 3 Harbour compat fixes
Release-blocking compatibility issues discovered during the 258-test
pre-release validation suite (100 syntax + 44 RDD + 114 RTL).

1. PCount() always returned 0 in PRG code

   Root cause: ParamCount() returned t.pendingParams, which is
   overwritten by every nested Function() call. By the time the
   PCount() RTL's Frame() executes, pendingParams is already 0.

   Fix: Frame() now stores pendingParams in frame.paramCount.
   PCount() RTL uses CallerParamCount() which reads callSP-2
   (the PRG caller's frame), while RTL functions still use
   ParamCount() (reads pendingParams before their own Frame).

   Verified: PCount(1,2,3)=3, PCount(1)=1, PCount()=0

2. Break("string") panicked instead of being caught by RECOVER USING

   Root cause: Generated SEQUENCE code only caught *HbError panics.
   Break() panics with BreakValue (a different type), which fell
   through to EndProc's "runtime error" message and re-panic.

   Fix (two parts):
   a) gengo emitBeginSequence: recover closure now catches any
      panic (interface{}), then dispatches via type switch:
      - *HbError → extract .Error() string
      - hasValue interface (BreakValue) → extract .GetValue()
      - other → static "error" string
   b) hbrtl/error.go: BreakValue gets GetValue() method for
      duck-type detection without import cycles
   c) hbrt/thread.go EndProc: BreakValue type name check added
      so it re-panics silently (no stderr noise)

3. SET INDEX TO a, b, c only opened the last file

   Root cause: Parser's parseSet() called parseExpr() once for
   INDEX setting, stopping at the first comma. Remaining file
   names were consumed by the "eat rest of line" loop.

   Fix: Parser now collects comma-separated identifiers into a
   single string literal "a,b,c". gengo splits on comma and
   calls OrderListAdd() for each file.

   Verified: SET INDEX TO si_name, si_city → OrdCount=2

All tests pass:
  go test ./...          14 packages OK
  FiveSql2               43/43  100%
  compat_harbour         51/51
  Syntax test           100/100
  RDD test               44/44
  RTL test              114/114
  Windows cross-compile  OK
  Linux cross-compile    OK

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:06:28 +09:00

479 lines
9.7 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Missing RTL functions — filling the gap between Five and Harbour.
// Reference: /mnt/d/harbour-core/include/hbcompdf.h (HB_F_* list)
package hbrtl
import (
"five/hbrt"
"math"
"strings"
)
// --- String functions ---
// At returns position of cSearch in cTarget (1-based, 0 if not found).
func At(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProcFast()
search := t.Local(1).AsString()
target := t.Local(2).AsString()
idx := strings.Index(target, search)
if idx >= 0 {
t.RetInt(int64(idx + 1))
} else {
t.RetInt(0)
}
}
// Left returns leftmost n characters.
func Left(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProcFast()
s := t.Local(1).AsString()
n := int(t.Local(2).AsNumInt())
if n >= len(s) {
t.PushString(s)
} else if n <= 0 {
t.PushString("")
} else {
t.PushString(s[:n])
}
t.RetValue()
}
// Right returns rightmost n characters.
func Right(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProcFast()
s := t.Local(1).AsString()
n := int(t.Local(2).AsNumInt())
if n >= len(s) {
t.PushString(s)
} else if n <= 0 {
t.PushString("")
} else {
t.PushString(s[len(s)-n:])
}
t.RetValue()
}
// Asc returns ASCII code of first character.
func Asc(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
s := t.Local(1).AsString()
if len(s) > 0 {
t.RetInt(int64(s[0]))
} else {
t.RetInt(0)
}
}
// Chr returns character from ASCII code.
func Chr(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
n := int(t.Local(1).AsNumInt())
t.PushString(string([]byte{byte(n)}))
t.RetValue()
}
// StrTran replaces occurrences in string.
func StrTran(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProcFast()
s := t.Local(1).AsString()
search := t.Local(2).AsString()
replace := ""
if nParams >= 3 {
replace = t.Local(3).AsString()
}
t.PushString(strings.ReplaceAll(s, search, replace))
t.RetValue()
}
// Stuff inserts/replaces characters in string.
func Stuff(t *hbrt.Thread) {
t.Frame(4, 0)
defer t.EndProcFast()
s := t.Local(1).AsString()
start := int(t.Local(2).AsNumInt()) - 1 // 1-based
nDel := int(t.Local(3).AsNumInt())
insert := t.Local(4).AsString()
if start < 0 {
start = 0
}
if start > len(s) {
start = len(s)
}
end := start + nDel
if end > len(s) {
end = len(s)
}
t.PushString(s[:start] + insert + s[end:])
t.RetValue()
}
// PadC pads string centered.
func PadC(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProcFast()
s := valueToDisplay(t.Local(1))
n := int(t.Local(2).AsNumInt())
if len(s) >= n {
t.PushString(s[:n])
} else {
leftPad := (n - len(s)) / 2
rightPad := n - len(s) - leftPad
t.PushString(Spaces(leftPad) + s + Spaces(rightPad))
}
t.RetValue()
}
// --- Math functions ---
// Round rounds a number to specified decimal places.
func Round(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProcFast()
val := t.Local(1).AsNumDouble()
dec := int(t.Local(2).AsNumInt())
mult := math.Pow(10, float64(dec))
result := math.Round(val*mult) / mult
t.PushValue(hbrt.MakeDouble(result, 255, uint16(dec)))
t.RetValue()
}
// Max returns larger of two values.
func Max(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProcFast()
a := t.Local(1)
b := t.Local(2)
if a.IsNumeric() && b.IsNumeric() {
if a.AsNumDouble() >= b.AsNumDouble() {
t.PushValue(a)
} else {
t.PushValue(b)
}
} else if a.IsDateTime() && b.IsDateTime() {
if a.AsJulian() >= b.AsJulian() {
t.PushValue(a)
} else {
t.PushValue(b)
}
} else {
t.PushValue(a)
}
t.RetValue()
}
// Min returns smaller of two values.
func Min(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProcFast()
a := t.Local(1)
b := t.Local(2)
if a.IsNumeric() && b.IsNumeric() {
if a.AsNumDouble() <= b.AsNumDouble() {
t.PushValue(a)
} else {
t.PushValue(b)
}
} else if a.IsDateTime() && b.IsDateTime() {
if a.AsJulian() <= b.AsJulian() {
t.PushValue(a)
} else {
t.PushValue(b)
}
} else {
t.PushValue(a)
}
t.RetValue()
}
// Sqrt returns square root.
func Sqrt(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
t.PushValue(hbrt.MakeDoubleAuto(math.Sqrt(t.Local(1).AsNumDouble())))
t.RetValue()
}
// Log returns natural logarithm.
func Log(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
t.PushValue(hbrt.MakeDoubleAuto(math.Log(t.Local(1).AsNumDouble())))
t.RetValue()
}
// Exp returns e^x.
func Exp(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
t.PushValue(hbrt.MakeDoubleAuto(math.Exp(t.Local(1).AsNumDouble())))
t.RetValue()
}
// Mod returns modulus (same as %).
func Mod(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProcFast()
a := t.Local(1).AsNumDouble()
b := t.Local(2).AsNumDouble()
if b == 0 {
t.PushValue(hbrt.MakeDoubleAuto(0))
} else {
t.PushValue(hbrt.MakeDoubleAuto(math.Mod(a, b)))
}
t.RetValue()
}
// CToD, CDoW, CMonth moved to datetime.go
// --- Type / Misc ---
// Type returns type of an expression (as string).
func TypeFunc(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
v := t.Local(1)
var c string
switch {
case v.IsNil():
c = "U"
case v.IsLogical():
c = "L"
case v.IsNumeric():
c = "N"
case v.IsString():
c = "C"
case v.IsDate(), v.IsTimestamp():
c = "D"
case v.IsArray():
c = "A"
case v.IsHash():
c = "H"
case v.IsBlock():
c = "B"
case v.IsObject():
c = "O"
default:
c = "U"
}
t.PushString(c)
t.RetValue()
}
// PCount returns number of parameters passed to the calling PRG function.
// Harbour: hb_pcount() — returns the CALLER's param count, not PCount's own.
func PCount(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProcFast()
t.RetInt(int64(t.CallerParamCount()))
}
// Break moved to error.go — full implementation with BreakValue type.
// Array creates array of given size.
func ArrayFunc(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
n := int(t.Local(1).AsNumInt())
t.PushValue(hbrt.MakeArray(n))
t.RetValue()
}
// FCount returns number of fields in current workarea.
func FCount(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProcFast()
wam := getWA(t)
if wam != nil {
if area := wam.Current(); area != nil {
t.RetInt(int64(area.FieldCount()))
return
}
}
t.RetInt(0)
}
// FieldName returns field name by position (1-based).
func FieldName(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
nField := int(t.Local(1).AsNumInt())
wam := getWA(t)
if wam != nil {
if area := wam.Current(); area != nil {
if nField >= 1 && nField <= area.FieldCount() {
fi := area.GetFieldInfo(nField - 1) // 1-based to 0-based
t.RetString(fi.Name)
return
}
}
}
t.RetString("")
}
// Select([cAlias|nArea]) returns workarea number.
// Select() → current area number
// Select("ALIAS") → area number for alias (0 if not found)
// Select(nArea) → nArea if that area is in use, 0 otherwise
func SelectFunc(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProcFast()
wam := getWA(t)
if wam == nil {
t.RetInt(0)
return
}
if nParams == 0 {
t.RetInt(int64(wam.CurrentNum()))
return
}
v := t.Local(1)
if v.IsString() {
t.RetInt(int64(wam.FindByAlias(v.AsString())))
} else if v.IsNumeric() {
n := uint16(v.AsNumInt())
if wam.AreaAt(n) != nil {
t.RetInt(int64(n))
} else {
t.RetInt(0)
}
} else {
t.RetInt(0)
}
}
// File checks if file exists.
func FileFunc(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
// Simple implementation
t.PushBool(false)
t.RetValue()
}
// Inkey waits for keypress and returns key code.
// Harbour: Inkey(nSeconds) — 0 = wait forever
// Uses shared raw terminal from rawtty.go
func Inkey(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProcFast()
// Check keyboard buffer first
if k := PopKeyBuffer(); k >= 0 {
SetLastKey(k)
t.RetInt(int64(k))
return
}
// Check timeout param: Inkey(0) = wait forever, Inkey(n) = wait n seconds
nWait := float64(0)
if nParams >= 1 && !t.Local(1).IsNil() {
nWait = t.Local(1).AsNumDouble()
}
_ = nWait // TODO: implement timeout for non-zero
key := ReadKey() // from rawtty.go (auto-inits raw mode)
var result int
switch key {
case 'A':
result = 5 // K_UP
case 'B':
result = 24 // K_DOWN
case 'C':
result = 4 // K_RIGHT
case 'D':
result = 19 // K_LEFT
case '5':
result = 18 // K_PGUP
case '6':
result = 3 // K_PGDN
case 'H':
result = 1 // K_HOME
case 'F':
result = 6 // K_END
case kESC:
result = 27
default:
result = key
}
SetLastKey(result)
t.RetInt(int64(result))
}
// Transform formats a value with picture string.
func Transform(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProcFast()
val := t.Local(1)
pic := ""
if nParams >= 2 && !t.Local(2).IsNil() {
pic = t.Local(2).AsString()
}
t.RetString(transformHbValue(val, pic))
}
// hb_StrReplace replaces multiple substrings.
func HbStrReplace(t *hbrt.Thread) {
t.Frame(3, 0)
defer t.EndProcFast()
s := t.Local(1).AsString()
search := t.Local(2) // array of search strings
replace := t.Local(3) // array of replace strings
if search.IsArray() && replace.IsArray() {
sa := search.AsArray()
ra := replace.AsArray()
for i := 0; i < len(sa.Items) && i < len(ra.Items); i++ {
s = strings.ReplaceAll(s, sa.Items[i].AsString(), ra.Items[i].AsString())
}
}
t.PushString(s)
t.RetValue()
}
// hb_NToS converts number to string without leading spaces.
func HbNToS(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProcFast()
v := t.Local(1)
if v.IsNumInt() {
t.PushString(fmt_int64(v.AsNumInt()))
} else {
t.PushString(strings.TrimSpace(valueToDisplay(v)))
}
t.RetValue()
}
func fmt_int64(n int64) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var buf [20]byte // stack allocation, no heap
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}