Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2 SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved as a single checkpoint before refactoring the parser to delegate xBase command translation to the preprocessor. Highlights: FiveSql2 engine (_FiveSql2/src/) - prefix-glob index attach -> explicit convention (<table>_pk.ntx, <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop - DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt) - COUNT(DISTINCT col) parsed + aggregated via hSeen hash - UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent) - DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT) - Derived table FROM (SELECT...) + JOIN right-side derived - Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect - LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs) - DATE literal round-trip validation (Feb 29 non-leap rejected) - CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists - AlterTable type dispatcher comma-wrapped (1-char type "A" no longer matches CHARACTER) Compiler / runtime - gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity) - gengo split: emit_block.go, emit_stmt.go, folding.go extracted - parser/stmtreg.go nudges - hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*), windows debug stubs collapsed - thread/vm/value/class/pcinterp tightening from panic traces RDD layer (hbrdd/) - dbf: null bitmap support (null.go + null_test.go), mmap split (mmap_posix.go / mmap_windows.go), byte-level numeric parse - ntx/cdx: windows mmap parity - workarea + mem RDD: cross-area state-bleed fixes RTL (hbrtl/) - errorlog rewrite with platform-specific FD (errorlog_fd_unix / errorlog_fd_other) - sqlscan, sqlhelpers, indexrtl, datetime extensions Gates green at checkpoint: - go test ./... : PASS - FiveSql2 SQL:1999 : 43/43 - Harbour compat : 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
488 lines
10 KiB
Go
488 lines
10 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()))
|
|
}
|
|
|
|
// 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:])
|
|
}
|