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>
446 lines
8.4 KiB
Go
446 lines
8.4 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// String functions for the Five runtime library.
|
|
// Implements Harbour-compatible string functions.
|
|
package hbrtl
|
|
|
|
import (
|
|
"five/hbrt"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
// spacesCache: pre-built space strings for common pad sizes.
|
|
// Avoids strings.Repeat(" ", n) allocation in hot paths.
|
|
var spacesCache [257]string
|
|
|
|
func init() {
|
|
for i := range spacesCache {
|
|
spacesCache[i] = strings.Repeat(" ", i)
|
|
}
|
|
}
|
|
|
|
// spaces returns a string of n spaces, using cache for n <= 256.
|
|
func spaces(n int) string {
|
|
if n <= 0 {
|
|
return ""
|
|
}
|
|
if n < len(spacesCache) {
|
|
return spacesCache[n]
|
|
}
|
|
return strings.Repeat(" ", n)
|
|
}
|
|
|
|
// Str converts a numeric value to a string.
|
|
// Harbour: Str(nValue [, nWidth [, nDec]]) → cString
|
|
func Str(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
v := t.Local(1)
|
|
if !v.IsNumeric() {
|
|
t.PushString("")
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
d := v.AsNumDouble()
|
|
|
|
// Width and decimals: use caller's args if provided, else Value metadata
|
|
width := int(v.Length())
|
|
dec := int(v.Decimal())
|
|
|
|
if nParams >= 2 && !t.Local(2).IsNil() {
|
|
width = t.Local(2).AsInt()
|
|
}
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
dec = t.Local(3).AsInt()
|
|
}
|
|
|
|
if width == 0 || width == 255 {
|
|
width = 10 // default width
|
|
}
|
|
if dec == 255 {
|
|
dec = 0
|
|
}
|
|
|
|
s := fmt.Sprintf("%*.*f", width, dec, d)
|
|
// Harbour pads with spaces if shorter
|
|
if len(s) < width {
|
|
s = spaces(width-len(s)) + s
|
|
}
|
|
// Harbour returns asterisks if wider than width
|
|
if len(s) > width && width > 0 {
|
|
s = strings.Repeat("*", width)
|
|
}
|
|
|
|
t.PushString(s)
|
|
t.RetValue()
|
|
}
|
|
|
|
// Val converts a string to a numeric value.
|
|
// Harbour: Val(cString) → nValue
|
|
func Val(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
|
|
v := t.Local(1)
|
|
if !v.IsString() {
|
|
t.RetInt(0)
|
|
return
|
|
}
|
|
|
|
s := strings.TrimSpace(v.AsString())
|
|
if s == "" {
|
|
t.RetInt(0)
|
|
return
|
|
}
|
|
|
|
// Try integer first
|
|
var n int64
|
|
if _, err := fmt.Sscanf(s, "%d", &n); err == nil {
|
|
// Check if there's a decimal point
|
|
if !strings.Contains(s, ".") {
|
|
t.RetInt(n)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Try float
|
|
var f float64
|
|
if _, err := fmt.Sscanf(s, "%f", &f); err == nil {
|
|
// Count decimal places
|
|
dec := 0
|
|
if idx := strings.Index(s, "."); idx >= 0 {
|
|
dec = len(s) - idx - 1
|
|
}
|
|
t.PushValue(hbrt.MakeDouble(f, uint16(len(s)), uint16(dec)))
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
t.RetInt(0)
|
|
}
|
|
|
|
// Len returns the length of a string or array.
|
|
// Harbour: Len(xValue) → nLen
|
|
func Len(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
|
|
v := t.Local(1)
|
|
switch {
|
|
case v.IsString():
|
|
t.RetInt(int64(v.StringLen()))
|
|
case v.IsArray():
|
|
t.RetInt(int64(len(v.AsArray().Items)))
|
|
case v.IsHash():
|
|
t.RetInt(int64(len(v.AsHash().Keys)))
|
|
default:
|
|
t.RetInt(0)
|
|
}
|
|
}
|
|
|
|
// SubStr extracts a substring.
|
|
// Harbour: SubStr(cString, nStart [, nLen]) → cString
|
|
func SubStr(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
|
|
v := t.Local(1)
|
|
if !v.IsString() {
|
|
t.PushString("")
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
s := v.AsString()
|
|
start := int(t.Local(2).AsNumInt())
|
|
|
|
// Harbour: 1-based index, negative = from end
|
|
if start < 0 {
|
|
start = len(s) + start + 1
|
|
}
|
|
if start < 1 {
|
|
start = 1
|
|
}
|
|
start-- // convert to 0-based
|
|
|
|
if start >= len(s) {
|
|
t.PushString("")
|
|
t.RetValue()
|
|
return
|
|
}
|
|
|
|
result := s[start:]
|
|
|
|
// Optional 3rd param: length
|
|
if nParams >= 3 && !t.Local(3).IsNil() {
|
|
nLen := int(t.Local(3).AsNumInt())
|
|
if nLen < 0 {
|
|
nLen = 0
|
|
}
|
|
if nLen < len(result) {
|
|
result = result[:nLen]
|
|
}
|
|
}
|
|
|
|
t.PushString(result)
|
|
t.RetValue()
|
|
}
|
|
|
|
// Upper converts string to uppercase.
|
|
// Harbour: Upper(cString) → cString
|
|
func Upper(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.ToUpper(v.AsString()))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// Lower converts string to lowercase.
|
|
func Lower(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.ToLower(v.AsString()))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// AllTrim removes leading and trailing spaces.
|
|
func AllTrim(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.TrimSpace(v.AsString()))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// LTrim trims leading spaces only. Harbour: LTRIM(cString) → cString
|
|
func LTrim(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.TrimLeft(v.AsString(), " "))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// RTrim trims trailing spaces only. Harbour: RTRIM(cString) / TRIM(cString)
|
|
func RTrim(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsString() {
|
|
t.PushString(strings.TrimRight(v.AsString(), " "))
|
|
} else {
|
|
t.PushString("")
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// Space returns a string of n spaces.
|
|
// Harbour: Space(nCount) → cString
|
|
func Space(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
n := t.Local(1).AsNumInt()
|
|
if n < 0 {
|
|
n = 0
|
|
}
|
|
t.PushString(spaces(int(n)))
|
|
t.RetValue()
|
|
}
|
|
|
|
// PadR pads a string on the right to a specified length.
|
|
func PadR(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
s := valueToDisplay(t.Local(1))
|
|
n := int(t.Local(2).AsNumInt())
|
|
fill := " "
|
|
if nParams >= 3 && t.Local(3).IsString() {
|
|
f := t.Local(3).AsString()
|
|
if len(f) > 0 {
|
|
fill = f[:1]
|
|
}
|
|
}
|
|
if len(s) >= n {
|
|
t.PushString(s[:n])
|
|
} else {
|
|
pad := n - len(s)
|
|
if fill == " " {
|
|
t.PushString(s + spaces(pad))
|
|
} else {
|
|
t.PushString(s + strings.Repeat(fill, pad))
|
|
}
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// PadL pads a string on the left to a specified length.
|
|
func PadL(t *hbrt.Thread) {
|
|
nParams := t.ParamCount()
|
|
t.Frame(nParams, 0)
|
|
defer t.EndProcFast()
|
|
s := valueToDisplay(t.Local(1))
|
|
n := int(t.Local(2).AsNumInt())
|
|
fill := " "
|
|
if nParams >= 3 && t.Local(3).IsString() {
|
|
f := t.Local(3).AsString()
|
|
if len(f) > 0 {
|
|
fill = f[:1]
|
|
}
|
|
}
|
|
if len(s) >= n {
|
|
t.PushString(s[len(s)-n:])
|
|
} else {
|
|
pad := n - len(s)
|
|
if fill == " " {
|
|
t.PushString(spaces(pad) + s)
|
|
} else {
|
|
t.PushString(strings.Repeat(fill, pad) + s)
|
|
}
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// Replicate repeats a string n times.
|
|
func Replicate(t *hbrt.Thread) {
|
|
t.Frame(2, 0)
|
|
defer t.EndProcFast()
|
|
s := t.Local(1).AsString()
|
|
n := int(t.Local(2).AsNumInt())
|
|
if n < 0 {
|
|
n = 0
|
|
}
|
|
t.PushString(strings.Repeat(s, n))
|
|
t.RetValue()
|
|
}
|
|
|
|
// --- Utility functions ---
|
|
|
|
// ValType returns a single character indicating the value type.
|
|
// Harbour: ValType(xValue) → cType
|
|
func ValType(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"
|
|
case v.IsPointer():
|
|
c = "P"
|
|
case v.IsSymbol():
|
|
c = "S"
|
|
default:
|
|
c = "U"
|
|
}
|
|
t.PushString(c)
|
|
t.RetValue()
|
|
}
|
|
|
|
// Empty checks if a value is "empty".
|
|
// Harbour: Empty(xValue) → lEmpty
|
|
func Empty(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
var empty bool
|
|
switch {
|
|
case v.IsNil():
|
|
empty = true
|
|
case v.IsLogical():
|
|
empty = !v.AsBool()
|
|
case v.IsNumeric():
|
|
empty = v.AsNumDouble() == 0
|
|
case v.IsString():
|
|
empty = strings.TrimSpace(v.AsString()) == ""
|
|
case v.IsDate():
|
|
empty = v.AsJulian() == 0
|
|
case v.IsArray():
|
|
empty = len(v.AsArray().Items) == 0
|
|
case v.IsHash():
|
|
empty = len(v.AsHash().Keys) == 0
|
|
case v.IsBlock():
|
|
empty = false
|
|
default:
|
|
empty = true
|
|
}
|
|
t.PushValue(hbrt.MakeBool(empty))
|
|
t.RetValue()
|
|
}
|
|
|
|
// Abs returns the absolute value.
|
|
func Abs(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsNumInt() {
|
|
n := v.AsNumInt()
|
|
if n < 0 {
|
|
n = -n
|
|
}
|
|
t.PushValue(hbrt.MakeNumInt(n))
|
|
} else if v.IsDouble() {
|
|
t.PushValue(hbrt.MakeDouble(math.Abs(v.AsDouble()), v.Length(), v.Decimal()))
|
|
} else {
|
|
t.RetInt(0)
|
|
return
|
|
}
|
|
t.RetValue()
|
|
}
|
|
|
|
// Int returns the integer part of a numeric value.
|
|
func Int(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
v := t.Local(1)
|
|
if v.IsNumInt() {
|
|
t.PushValue(v)
|
|
} else if v.IsDouble() {
|
|
t.PushValue(hbrt.MakeLong(int64(v.AsDouble())))
|
|
} else {
|
|
t.PushValue(hbrt.MakeInt(0))
|
|
}
|
|
t.RetValue()
|
|
}
|