Two standard Harbour functions that fivenode-style PRG code (bridge_*.prg and downstream apps) calls frequently. Without them, every reference emits an analyzer WARN and resolves to NIL at runtime. * hb_HGetDef(hHash, xKey, xDefault) — hash lookup with fallback. * PValue(nIndex[, xDefault]) — read the nth parameter of the calling PRG function. Mirrors the PCount pattern: needs the caller frame's paramCount and locals, exposed via new hbrt.Thread.CallerLocal helper that pairs with the existing CallerParamCount. Registered under PVALUE and HB_PVALUE (Harbour accepts both forms). Verified: hb_HGetDef / PValue / HB_PVALUE all return expected values for present-key, missing-key-with-default, missing-key-no-default, and out-of-range-param cases. Full regression: go test (18 packages) + Compat 56/56 + std.ch 17/17 + FRB 7/7 + FiveSql2 43/43 all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
509 lines
11 KiB
Go
509 lines
11 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"
|
|
"os"
|
|
"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()))
|
|
}
|
|
|
|
// PValue returns the nth parameter of the calling PRG function.
|
|
// Harbour: PValue(nIndex[, xDefault]) → xValue
|
|
// Returns xDefault when n is out of range, or NIL if no default was given.
|
|
func PValue(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProc()
|
|
n := int(t.Local(1).AsNumInt())
|
|
if n >= 1 && n <= t.CallerParamCount() {
|
|
t.PushValue(t.CallerLocal(n))
|
|
t.RetValue()
|
|
return
|
|
}
|
|
if nParams >= 2 {
|
|
t.PushValue(t.Local(2))
|
|
} else {
|
|
t.PushNil()
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// 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(cPath) → lExists. Harbour's File() also honours SET PATH
|
|
// and wildcards, but callers in Five use it as "does this exact
|
|
// path exist". A plain os.Stat covers that without pulling the
|
|
// whole Harbour SET PATH search order in — matches how HbFileExists
|
|
// (hb_FileExists) already behaves elsewhere.
|
|
func FileFunc(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
path := t.Local(1).AsString()
|
|
if path == "" {
|
|
t.RetBool(false)
|
|
return
|
|
}
|
|
_, err := os.Stat(path)
|
|
t.RetBool(err == nil)
|
|
}
|
|
|
|
// 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:])
|
|
}
|