// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Array functions for the Five runtime library. // Implements Harbour-compatible array manipulation functions. // Reference: /mnt/d/harbour-core/src/vm/arrays.c package hbrtl import ( "five/hbrt" "sort" ) // AAdd appends an element to an array and returns the array. // Harbour: AAdd(aArray, xValue) → aArray func AAdd(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() arrVal := t.Local(1) val := t.Local(2) if !arrVal.IsArray() { panic("AAdd: argument is not an array") } arr := arrVal.AsArray() arr.Items = append(arr.Items, val) t.PushValue(arrVal) t.RetValue() } // ADel deletes an element from array at position, shifts remaining left. // Harbour: ADel(aArray, nPos) → aArray func ADel(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() arrVal := t.Local(1) pos := int(t.Local(2).AsNumInt()) arr := arrVal.AsArray() if pos >= 1 && pos <= len(arr.Items) { copy(arr.Items[pos-1:], arr.Items[pos:]) arr.Items[len(arr.Items)-1] = hbrt.MakeNil() } t.PushValue(arrVal) t.RetValue() } // AIns inserts NIL at position, shifts elements right. // Harbour: AIns(aArray, nPos) → aArray func AIns(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() arrVal := t.Local(1) pos := int(t.Local(2).AsNumInt()) arr := arrVal.AsArray() if pos >= 1 && pos <= len(arr.Items) { copy(arr.Items[pos:], arr.Items[pos-1:len(arr.Items)-1]) arr.Items[pos-1] = hbrt.MakeNil() } t.PushValue(arrVal) t.RetValue() } // ASize resizes an array. // Harbour: ASize(aArray, nLen) → aArray func ASize(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() arrVal := t.Local(1) newLen := int(t.Local(2).AsNumInt()) arr := arrVal.AsArray() if newLen < 0 { newLen = 0 } if newLen > len(arr.Items) { ext := make([]hbrt.Value, newLen-len(arr.Items)) arr.Items = append(arr.Items, ext...) } else { arr.Items = arr.Items[:newLen] } t.PushValue(arrVal) t.RetValue() } // AClone creates a shallow copy of an array. // Harbour: AClone(aArray) → aNewArray func AClone(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() arrVal := t.Local(1) if !arrVal.IsArray() { t.PushValue(arrVal) t.RetValue() return } src := arrVal.AsArray() items := make([]hbrt.Value, len(src.Items)) copy(items, src.Items) t.PushValue(hbrt.MakeArrayFrom(items)) t.RetValue() } // HbDeepClone recursively clones a value. Arrays and hashes are cloned // element-by-element; scalars (string, number, logical, date, NIL) are // returned unchanged — Five strings/numbers are immutable so sharing // pointers is safe. Used by FiveSql2's plan cache to hand callers a // pristine copy of the parsed query tree on every cache hit, since // Run() mutates some nodes (SqlFoldConst in particular). // // Harbour: hb_DeepCopy(xVal) → xNewVal func HbDeepClone(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() t.PushValue(deepCloneValue(t.Local(1))) t.RetValue() } // deepCloneValue walks Array and Hash structures recursively; other // Value kinds are returned as-is (scalars are immutable in Five so // sharing is safe). // // Hot-path optimizations: // - Array items that are themselves scalars skip the function call // (just slot-copied). Recursion only fires for nested Array/Hash. // - Hash keys are shared (never cloned). PRG hashes carry string / // numeric keys in every observed call site; mutating a key after // insertion is forbidden by the Hash API, so sharing is safe and // saves the recursion plus per-key allocation. func deepCloneValue(v hbrt.Value) hbrt.Value { if v.IsArray() { src := v.AsArray() if src == nil { return v } n := len(src.Items) items := make([]hbrt.Value, n) for i := 0; i < n; i++ { item := src.Items[i] if item.IsArray() || item.IsHash() { items[i] = deepCloneValue(item) } else { items[i] = item } } return hbrt.MakeArrayFrom(items) } if v.IsHash() { src := v.AsHash() if src == nil { return v } nh := hbrt.MakeHash() dst := nh.AsHash() for i, k := range src.Keys { val := src.Values[i] if val.IsArray() || val.IsHash() { val = deepCloneValue(val) } dst.Append(k, val) } return nh } return v } // ACopy copies elements from one array to another. // Harbour: ACopy(aSource, aDest [, nStart [, nCount [, nTargetPos]]]) → aDest func ACopy(t *hbrt.Thread) { t.Frame(2, 0) // simplified: just source and dest defer t.EndProc() srcVal := t.Local(1) dstVal := t.Local(2) src := srcVal.AsArray() dst := dstVal.AsArray() n := len(src.Items) if n > len(dst.Items) { n = len(dst.Items) } copy(dst.Items[:n], src.Items[:n]) t.PushValue(dstVal) t.RetValue() } // AFill fills an array with a value. // Harbour: AFill(aArray, xValue [, nStart [, nCount]]) → aArray func AFill(t *hbrt.Thread) { t.Frame(2, 0) // simplified: array and value defer t.EndProc() arrVal := t.Local(1) val := t.Local(2) arr := arrVal.AsArray() for i := range arr.Items { arr.Items[i] = val } t.PushValue(arrVal) t.RetValue() } // ASort sorts an array using an optional comparison block. // Harbour: ASort(aArray [, nStart [, nCount [, bBlock]]]) → aArray // // Block path: invokes bBlock per compare (side-effect safe). // Default path (no block): one pre-scan picks a specialized comparator // for homogeneous arrays (string / numeric / date / timestamp / // logical); mixed or unknown element types fall back to a generic // less-than that matches Harbour's default `<` semantics across types. func ASort(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() arrVal := t.Local(1) arr := arrVal.AsArray() if arr == nil || len(arr.Items) < 2 { t.PushValue(arrVal) t.RetValue() return } if nParams >= 4 && t.Local(4).IsBlock() { blk := t.Local(4).AsBlock() sort.SliceStable(arr.Items, func(i, j int) bool { t.PushValue(arr.Items[i]) t.PushValue(arr.Items[j]) t.PendingParams2(2) blk.Fn(t) return t.GetRetValue().AsBool() }) t.PushValue(arrVal) t.RetValue() return } // Default sort — pick a type-specialized comparator when every // element shares a shape. Falls back to a generic less-than for // mixed or uncategorized types. items := arr.Items switch detectArrayKind(items) { case arrKindString: sort.SliceStable(items, func(i, j int) bool { return items[i].AsString() < items[j].AsString() }) case arrKindInt: sort.SliceStable(items, func(i, j int) bool { return items[i].AsNumInt() < items[j].AsNumInt() }) case arrKindNumeric: sort.SliceStable(items, func(i, j int) bool { return items[i].AsNumDouble() < items[j].AsNumDouble() }) case arrKindDate: sort.SliceStable(items, func(i, j int) bool { return items[i].AsJulian() < items[j].AsJulian() }) case arrKindTimestamp: sort.SliceStable(items, func(i, j int) bool { ja, jb := items[i].AsJulian(), items[j].AsJulian() if ja != jb { return ja < jb } return items[i].AsTimeMs() < items[j].AsTimeMs() }) case arrKindLogical: sort.SliceStable(items, func(i, j int) bool { return !items[i].AsBool() && items[j].AsBool() }) default: sort.SliceStable(items, func(i, j int) bool { return valueLess(items[i], items[j]) }) } t.PushValue(arrVal) t.RetValue() } type arrKind int const ( arrKindMixed arrKind = iota arrKindString arrKindInt arrKindNumeric arrKindDate arrKindTimestamp arrKindLogical ) // detectArrayKind returns a specialized kind when every element matches // one well-known type; otherwise arrKindMixed. Integer-only arrays // prefer arrKindInt to skip the int→double conversion in the hot path. // A single non-int numeric promotes the whole array to arrKindNumeric. func detectArrayKind(items []hbrt.Value) arrKind { if len(items) == 0 { return arrKindMixed } allInt := true for _, v := range items { if !v.IsNumInt() { allInt = false break } } if allInt { return arrKindInt } allNum := true for _, v := range items { if !v.IsNumeric() { allNum = false break } } if allNum { return arrKindNumeric } check := func(pred func(hbrt.Value) bool) bool { for _, v := range items { if !pred(v) { return false } } return true } if check(func(v hbrt.Value) bool { return v.IsString() }) { return arrKindString } if check(func(v hbrt.Value) bool { return v.IsDate() }) { return arrKindDate } if check(func(v hbrt.Value) bool { return v.IsTimestamp() }) { return arrKindTimestamp } if check(func(v hbrt.Value) bool { return v.IsLogical() }) { return arrKindLogical } return arrKindMixed } // valueLess implements Harbour's default `<` across types. NILs sort // first (smallest) so they group together — matches the historical // Five compareValues behavior that ASort inherited. func valueLess(a, b hbrt.Value) bool { if a.IsNil() || b.IsNil() { return a.IsNil() && !b.IsNil() } if a.IsNumeric() && b.IsNumeric() { return a.AsNumDouble() < b.AsNumDouble() } if a.IsString() && b.IsString() { return a.AsString() < b.AsString() } if a.IsDate() && b.IsDate() { return a.AsJulian() < b.AsJulian() } if a.IsTimestamp() && b.IsTimestamp() { ja, jb := a.AsJulian(), b.AsJulian() if ja != jb { return ja < jb } return a.AsTimeMs() < b.AsTimeMs() } if a.IsLogical() && b.IsLogical() { return !a.AsBool() && b.AsBool() } return false } // AEval evaluates a block for each element in array. // Harbour: AEval(aArray, bBlock [, nStart [, nCount]]) → aArray func AEval(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() arrVal := t.Local(1) arr := arrVal.AsArray() blkVal := t.Local(2) if !blkVal.IsBlock() { t.PushValue(arrVal) t.RetValue() return } blk := blkVal.AsBlock() for i, item := range arr.Items { // Harbour: AEval callback receives (element, index). // Stack order: index first, element on top — Frame picks top-N. t.PushValue(hbrt.MakeInt(i + 1)) // arg2: 1-based index t.PushValue(item) // arg1: element value t.PendingParams2(2) blk.Fn(t) } t.PushValue(arrVal) t.RetValue() } // AScan searches for a value in array, returns position (0 if not found). // Harbour: AScan(aArray, xValue|bBlock [, nStart [, nCount]]) → nPos // // Block path: per-element block invoke (side-effect safe). // Value path: specialized fast-paths for string / int / double search // values — the loop stays inside Go without running through the // generic valuesEqual type-dispatch each iteration. Mixed or rare // types (date, timestamp, logical, nil) fall back to valuesEqual. func AScan(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() arrVal := t.Local(1) arr := arrVal.AsArray() if arr == nil { t.RetInt(0) return } items := arr.Items search := t.Local(2) if search.IsBlock() { blk := search.AsBlock() for i, item := range items { t.PushValue(item) t.PendingParams2(1) blk.Fn(t) if t.GetRetValue().AsBool() { t.RetInt(int64(i + 1)) return } } t.RetInt(0) return } switch { case search.IsString(): s := search.AsString() for i, item := range items { if item.IsString() && item.AsString() == s { t.RetInt(int64(i + 1)) return } } case search.IsNumInt(): n := search.AsNumInt() for i, item := range items { if !item.IsNumeric() { continue } if item.IsNumInt() { if item.AsNumInt() == n { t.RetInt(int64(i + 1)) return } } else if item.AsNumDouble() == float64(n) { t.RetInt(int64(i + 1)) return } } case search.IsNumeric(): f := search.AsNumDouble() for i, item := range items { if item.IsNumeric() && item.AsNumDouble() == f { t.RetInt(int64(i + 1)) return } } default: for i, item := range items { if valuesEqual(item, search) { t.RetInt(int64(i + 1)) return } } } t.RetInt(0) } // ATail returns the last element of an array. // Harbour: ATail(aArray) → xValue func ATail(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() arr := t.Local(1).AsArray() if arr != nil && len(arr.Items) > 0 { t.PushValue(arr.Items[len(arr.Items)-1]) } else { t.PushNil() } t.RetValue() } // valuesEqual compares two values for equality (simplified for AScan). func valuesEqual(a, b hbrt.Value) bool { if a.Type() != b.Type() { if a.IsNumeric() && b.IsNumeric() { return a.AsNumDouble() == b.AsNumDouble() } return false } switch { case a.IsNumInt(): return a.AsNumInt() == b.AsNumInt() case a.IsDouble(): return a.AsDouble() == b.AsDouble() case a.IsString(): return a.AsString() == b.AsString() case a.IsLogical(): return a.AsBool() == b.AsBool() default: return false } }