Files
five/hbrtl/strings.go
Charles KWON OhJun 5b378318a0 perf: RTL optimization — cached WA, spaces pool, stack-alloc fmt_int64
rdd.go:
- getWA() cached type assertion (avoid repeated interface check)
- waCache stores last WA pointer → O(1) for repeated calls

strings.go:
- spacesCache[257]: pre-built space strings for pad sizes 0-256
- spaces(n) returns cached string (no Repeat allocation)
- PadR/PadL use spaces() for fill=" " (most common case)
- Str() uses spaces() for right-padding

missing.go:
- fmt_int64: stack-allocated [20]byte array (was heap make([]byte))
- Reverse iteration (no prepend overhead)
- PadC uses spaces() for left/right padding

Benchmark (ext4, home dir):
  10K APPEND: 28ms → 26ms (Harbour 27ms!)
  50K APPEND: 130ms → 113ms (13% improvement)
  50K SCAN: 24ms → 23ms
  50K DUPKEY: 42ms → 35ms (17% improvement)
  CDX SCOPE: 12ms → 10ms (17% improvement)

82/82 stress PASS. 14 packages ALL PASS.

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

446 lines
8.3 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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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.EndProc()
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()
}