diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index f492124..3d9b977 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -482,8 +482,11 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) { if s.ForCond != nil { forExpr = fmt.Sprintf("%q", exprToString(s.ForCond)) } + // Set VM callback for UDF evaluation during index build + g.writeln("dbf.KeyEvalFunc = func(expr string) hbrt.Value { return t.MacroEval(expr) }") g.writeln(fmt.Sprintf("idx.OrderCreate(hbrdd.OrderCreateParams{KeyExpr: _keyExpr, FilePath: _file, ForExpr: %s, Unique: %v, Descending: %v})", forExpr, s.Unique, s.Descending)) + g.writeln("dbf.KeyEvalFunc = nil") g.indent-- g.writeln("}") g.indent-- diff --git a/examples/test_index_udf.prg b/examples/test_index_udf.prg new file mode 100644 index 0000000..5ec5007 --- /dev/null +++ b/examples/test_index_udf.prg @@ -0,0 +1,55 @@ +// Test INDEX ON with User Defined Functions +PROCEDURE Main() + LOCAL i, aStruct, aCit, nCidx + + aStruct := {{"ID","N",6,0}, {"NAME","C",20,0}, {"CITY","C",15,0}} + dbCreate("udf_test", aStruct) + USE "udf_test" NEW + + aCit := {"Seoul","Tokyo","Beijing"} + FOR i := 1 TO 10 + APPEND BLANK + REPLACE ID WITH i + REPLACE NAME WITH PadR("Name_" + PadL(LTrim(Str(i)), 3, "0"), 20) + nCidx := Int((i-1) % 3) + 1 + REPLACE CITY WITH PadR(aCit[nCidx], 15) + NEXT + ? "Records:", RecCount() + + // Test 1: INDEX ON with built-in function (baseline) + INDEX ON UPPER(NAME) TO udf_idx1 + GO TOP + ? "T1 UPPER top:", RTrim(FieldGet(2)), RecNo() + SEEK PadR("NAME_005", 20) + ? "T2 UPPER seek:", Found(), RecNo() + + // Test 2: INDEX ON with UDF + INDEX ON MyKey(FIELD->NAME) TO udf_idx2 + GO TOP + ? "T3 UDF top:", RTrim(FieldGet(2)), RecNo() + SEEK PadR("KEY:Name_001", 20) + ? "T4 UDF seek:", Found(), RecNo() + + // Test 3: INDEX ON with two-param UDF + INDEX ON CityKey(FIELD->CITY, FIELD->NAME) TO udf_idx3 + GO TOP + ? "T5 UDF2 top:", RTrim(FieldGet(3)), RTrim(FieldGet(2)), RecNo() + + // Test 4: Nested: built-in wrapping UDF + INDEX ON Left(MyKey(FIELD->NAME), 15) TO udf_idx4 + GO TOP + ? "T6 Nested top:", RTrim(FieldGet(2)), RecNo() + + CLOSE ALL + ? "DONE" +RETURN + +FUNCTION MyKey(cName) + LOCAL cResult + cResult := "KEY:" + RTrim(cName) +RETURN PadR(cResult, 20) + +FUNCTION CityKey(cCity, cName) + LOCAL cResult + cResult := RTrim(cCity) + ":" + RTrim(cName) +RETURN PadR(cResult, 30) diff --git a/hbrdd/dbf/indexer.go b/hbrdd/dbf/indexer.go index b4df5d2..fa272a8 100644 --- a/hbrdd/dbf/indexer.go +++ b/hbrdd/dbf/indexer.go @@ -46,6 +46,12 @@ type indexState struct { scopeBottom []byte // bottom scope key (nil = no scope) } +// KeyEvalFunc is a callback for evaluating index key expressions via the VM. +// Set by the generated code (via SetKeyEval) before calling OrderCreate. +// This allows evalKeyExprInner to call UDFs and evaluate complex expressions. +// Signature: func(exprString) → Value (called on the current Thread) +var KeyEvalFunc func(expr string) hbrt.Value + // ensureIndexState initializes the index state if nil. func (a *DBFArea) ensureIndexState() { if a.idxState == nil { @@ -848,9 +854,16 @@ func (a *DBFArea) evalKeyExprInner(expr string) []byte { } return []byte(inner[:width]) } + default: + // Unknown function — use VM MacroEval for UDF calls + if KeyEvalFunc != nil { + fullExpr := expr[:parenOpen] + "(" + argsStr + ")" + val := KeyEvalFunc(fullExpr) + return valueToKeyBytes(val) + } + // Fallback: evaluate inner as field + return a.evalKeyExprInner(argsStr) } - // Unknown function — try to evaluate inner as field - return a.evalKeyExprInner(argsStr) } // Concatenation: expr1 + expr2 (find + not inside parens) @@ -866,6 +879,12 @@ func (a *DBFArea) evalKeyExprInner(expr string) []byte { return []byte(s) } + // Final fallback: use VM MacroEval for any unresolvable expression + if KeyEvalFunc != nil { + val := KeyEvalFunc(expr) + return valueToKeyBytes(val) + } + return []byte(expr) } @@ -937,6 +956,26 @@ func (a *DBFArea) evalForInner(expr string) bool { return true // default: include record } +// valueToKeyBytes converts a hbrt.Value to index key bytes. +func valueToKeyBytes(v hbrt.Value) []byte { + switch { + case v.IsString(): + return []byte(v.AsString()) + case v.IsNumeric(): + return []byte(fmt.Sprintf("%20.10f", v.AsNumDouble())) + case v.IsDate(), v.IsTimestamp(): + y, m, d := julianToDate(v.AsJulian()) + return []byte(fmt.Sprintf("%04d%02d%02d", y, m, d)) + case v.IsLogical(): + if v.AsBool() { + return []byte("T") + } + return []byte("F") + default: + return []byte("") + } +} + // Helper: find matching close parenthesis func findMatchingParen(s string, openPos int) int { depth := 1