// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Stack introspection: PROCNAME, PROCLINE, PROCFILE, ERRORLEVEL package hbrtl import ( "five/hbrt" "os" "strconv" "strings" "time" ) // Silence unused import warning var _ = strconv.Itoa // PROCNAME([nLevel]) → cFunctionName func ProcName(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() level := 0 if nParams >= 1 && !t.Local(1).IsNil() { level = t.Local(1).AsInt() } stack := t.DebugCallStack() if level >= 0 && level < len(stack) { t.RetString(stack[level].Function) } else { t.RetString("") } } // PROCLINE([nLevel]) → nLineNumber func ProcLine(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() level := 0 if nParams >= 1 && !t.Local(1).IsNil() { level = t.Local(1).AsInt() } stack := t.DebugCallStack() if level >= 0 && level < len(stack) { t.RetInt(int64(stack[level].Line)) } else { t.RetInt(0) } } // PROCFILE([nLevel]) → cSourceFileName func ProcFile(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() level := 0 if nParams >= 1 && !t.Local(1).IsNil() { level = t.Local(1).AsInt() } stack := t.DebugCallStack() if level >= 0 && level < len(stack) { t.RetString(stack[level].Module) } else { t.RetString("") } } var exitLevel int // ERRORLEVEL([nNewLevel]) → nOldLevel func ErrorLevel(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() old := exitLevel if nParams >= 1 && !t.Local(1).IsNil() { exitLevel = t.Local(1).AsInt() } t.RetInt(int64(old)) } // TONE(nFrequency [, nDuration]) → NIL func Tone(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() // Terminal bell os.Stdout.Write([]byte{7}) t.RetNil() } // CENTER(cString, nWidth [, cFill]) → cCentered (alias: PADC) func Center(t *hbrt.Thread) { PadC(t) } // STRZERO already exists, HB_NTOS already exists // HB_NTOS(nValue) → cString (no leading spaces) — already in missing.go // FIELDPOS(cFieldName) → nPos func FieldPos(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProcFast() fname := strings.ToUpper(t.Local(1).AsString()) wam := getWA(t) if wam == nil { t.RetInt(0) return } area := wam.Current() if area == nil { t.RetInt(0) return } // Try DBFArea's built-in field position cache (O(1) hash lookup). // Falls back to linear scan for non-DBF areas (mem RDD, etc.). type fieldPosCacher interface { FieldPosCache(name string) int } if fpc, ok := area.(fieldPosCacher); ok { pos := fpc.FieldPosCache(fname) t.RetInt(int64(pos)) return } // Fallback: linear scan for i := 0; i < area.FieldCount(); i++ { fi := area.GetFieldInfo(i) if strings.EqualFold(fi.Name, fname) { t.RetInt(int64(i + 1)) return } } t.RetInt(0) } func eqFold(a, b string) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { ca, cb := a[i], b[i] if ca >= 'a' && ca <= 'z' { ca -= 32 } if cb >= 'a' && cb <= 'z' { cb -= 32 } if ca != cb { return false } } return true } // FIELDBLOCK(nField) → bBlock — create block that reads field n func FieldBlockFunc(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() nField := t.Local(1).AsInt() // Create a block that calls FieldGet(nField) on the current area blk := hbrt.MakeBlock(func(t2 *hbrt.Thread) { t2.Frame(0, 0) defer t2.EndProc() wam := getWA(t2) if wam != nil { if area := wam.Current(); area != nil { val, _ := area.GetValue(nField - 1) // 1-based to 0-based t2.PushValue(val) t2.RetValue() return } } t2.RetNil() }, 0) t.RetVal(blk) } // FIELDNAME(nField) → cName func FieldNameFunc(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() nField := t.Local(1).AsInt() wam := getWA(t) if wam != nil { if area := wam.Current(); area != nil { if nField >= 1 && nField <= area.FieldCount() { fi := area.GetFieldInfo(nField - 1) t.RetString(fi.Name) return } } } t.RetString("") } // AFIELDS(@aNames [, @aTypes, @aWidths, @aDecs]) → nFieldCount func AFields(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetInt(0) return } area := wam.Current() if area == nil { t.RetInt(0) return } nFields := area.FieldCount() t.RetInt(int64(nFields)) } // DBSTRUCT() → aStruct (array of {name, type, len, dec}) func DbStruct(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetVal(hbrt.MakeArray(0)) return } area := wam.Current() if area == nil { t.RetVal(hbrt.MakeArray(0)) return } nFields := area.FieldCount() items := make([]hbrt.Value, nFields) for i := 0; i < nFields; i++ { fi := area.GetFieldInfo(i) // 5-element row: name / type / len / dec / flags. Harbour // dbStruct() is 4-element; the extra flags byte preserves // FieldFlagNullable (and future system/binary/autoinc bits) // across ALTER-TABLE table rebuilds so callers that feed // dbStruct output back into dbCreate don't silently drop // nullability. Four-element callers still index [1..4] as // before. row := []hbrt.Value{ hbrt.MakeString(fi.Name), hbrt.MakeString(string(fi.Type)), hbrt.MakeInt(int(fi.Len)), hbrt.MakeInt(int(fi.Dec)), hbrt.MakeInt(int(fi.Flags)), } items[i] = hbrt.MakeArrayFrom(row) } t.RetVal(hbrt.MakeArrayFrom(items)) } // HB_DATETIME([nYear, nMonth, nDay [, nHour [, nMin [, nSec [, nMsec]]]]]) → tTimestamp func HbDatetime(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() if nParams == 0 { // No args: current date+time now := time.Now() y, m, d := now.Date() julian := dateToJulian(y, int(m), d) ms := int32(now.Hour()*3600000 + now.Minute()*60000 + now.Second()*1000 + now.Nanosecond()/1000000) t.RetVal(hbrt.MakeTimestamp(julian, ms)) return } // With args: construct from components y := 0; m := 1; d := 1; hh := 0; mm := 0; ss := 0; ms := 0 if nParams >= 1 { y = t.Local(1).AsInt() } if nParams >= 2 { m = t.Local(2).AsInt() } if nParams >= 3 { d = t.Local(3).AsInt() } if nParams >= 4 { hh = t.Local(4).AsInt() } if nParams >= 5 { mm = t.Local(5).AsInt() } if nParams >= 6 { ss = t.Local(6).AsInt() } if nParams >= 7 { ms = t.Local(7).AsInt() } julian := dateToJulian(y, m, d) timeMs := int32(hh*3600000 + mm*60000 + ss*1000 + ms) t.RetVal(hbrt.MakeTimestamp(julian, timeMs)) }