Files
five/hbrtl/strings.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

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()
}