Files
five/hbrtl/missing.go
Charles KWON OhJun 05ccef05e2 perf: EndProcFast — eliminate defer recover() from RTL hot paths
Problem: every RTL function calls defer t.EndProc() which does recover().
50K SEEK loop = 250K recover() calls = ~12ms wasted.

Solution: EndProcFast() skips recover (only needs endFrame restore).
Applied to ALL RTL functions in strings.go, rdd.go, missing.go, database.go.
EndProc() with recover kept for generated PRG code (needs BEGIN SEQUENCE).

Analysis (50K sequential SEEK breakdown):
  Go NTX Seek direct: 7ms (faster than Harbour 27ms!)
  PRG VM overhead:    38ms (Frame + RTL calls + key generation)
  Key generation:     25ms (Str+LTrim+PadL+PadR = 5 RTL Frame/EndProc per iter)

With EndProcFast: RTL overhead reduced ~30%.

CDX SCOPE: 2ms (Harbour 4ms — 2x FASTER!)
82/82 stress PASS. 14 packages ALL PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:43:39 +09:00

454 lines
9.1 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.
func PCount(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProcFast()
t.RetInt(int64(t.ParamCount()))
}
// 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 returns current workarea number.
func SelectFunc(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProcFast()
// TODO: integrate with RDD
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:])
}