Files
five/compiler/gengo/gen_util.go
CharlesKWON ed33af41c5 perf: FieldPos O(1) cache + xbase import detection for function-call PRGs
Two SQLite-style optimizations for RDD and SQL workloads:

1. FieldPos() O(1) column binding cache

   Before: FieldPos(name) linear scan — O(n) per call with string
           comparison. In SQL engines that call FieldPos per row per
           column, this is hundreds of thousands of calls.

   After:  DBFArea builds a map[UPPER(name)]→pos on first lookup.
           All subsequent lookups are O(1) hash. SQLite calls this
           "column affinity binding" — positions resolved at prepare,
           not per row.

   Implementation:
     - hbrdd/dbf/dbf.go: DBFArea.FieldPosCache(name) method
     - hbrtl/procinfo.go: FieldPos RTL uses fieldPosCacher interface
     - Lazy init: only pays for tables that get queried

2. hbrdd import auto-detection for function-call style PRGs

   Before: compiler only added hbrdd import when PRG used xBase commands
           (USE, SKIP, INDEX...). Pure function-call style like
           `dbUseArea(.T.,,"t")`, `FieldPut(1, val)` was missed —
           generated Go failed to compile ("undefined: hbrdd").

   After:  scanStmtsForXBase walks ExprStmt bodies too, detecting
           CallExpr to any of the ~40 xBase RTL function names.
           FIELD->NAME alias expressions also trigger the import.

   Resolves: small PRGs that use only dbUseArea/FieldGet/FieldPut.

Benchmark notes (50k records):
  Raw RDD scan:              7 ms    (baseline)
  FiveSql2 SELECT WHERE:   157 ms    (unchanged — bottleneck is
                                      not FieldPos, it's PRG-level
                                      expression tree walk per row)
  compat_harbour 51/51:    PASS
  FiveSql2 43/43:          100%

The FieldPos cache helps heavy field-name-based code paths but the
primary FiveSql2 bottleneck is the PRG interpreter walking expression
ASTs per row (needs bytecode compilation to close the gap).

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

118 lines
3.1 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
package gengo
import (
"five/compiler/ast"
"strings"
)
// hasXBaseCommands checks if the file contains any xBase commands.
func hasXBaseCommands(file *ast.File) bool {
for _, d := range file.Decls {
switch decl := d.(type) {
case *ast.FuncDecl:
if scanStmtsForXBase(decl.Body) {
return true
}
case *ast.MethodDecl:
if scanStmtsForXBase(decl.Body) {
return true
}
}
}
return false
}
// xbaseFuncNames is the set of RTL functions that require the hbrdd import
// in the generated Go code. When a PRG only uses these via function-call
// syntax (no xBase commands like USE), the compiler still needs hbrdd.
var xbaseFuncNames = map[string]bool{
"DBUSEAREA": true, "DBCREATE": true, "DBAPPEND": true, "DBSKIP": true,
"DBGOTO": true, "DBGOTOP": true, "DBGOBOTTOM": true, "DBSEEK": true,
"DBDELETE": true, "DBRECALL": true, "DBCLOSEAREA": true, "DBCLOSEALL": true,
"DBSELECTAREA": true, "DBCOMMIT": true, "DBPACK": true, "DBZAP": true,
"DBRLOCK": true, "DBRUNLOCK": true, "FLOCK": true, "DBUNLOCK": true,
"FIELDGET": true, "FIELDPUT": true, "FIELDPOS": true, "FIELDNAME": true,
"FCOUNT": true, "RECNO": true, "RECCOUNT": true, "EOF": true, "BOF": true,
"FOUND": true, "DELETED": true, "LASTREC": true, "ALIAS": true,
"ORDSETFOCUS": true, "ORDCOUNT": true, "ORDNAME": true, "ORDKEY": true,
"USED": true, "SELECT": true, "SETDELETED": true,
"FIVE_SQL": true, // FiveSql2 entry point also needs hbrdd
}
func scanExprForXBase(expr ast.Expr) bool {
if expr == nil {
return false
}
switch e := expr.(type) {
case *ast.CallExpr:
if ident, ok := e.Func.(*ast.IdentExpr); ok {
if xbaseFuncNames[strings.ToUpper(ident.Name)] {
return true
}
}
for _, arg := range e.Args {
if scanExprForXBase(arg) {
return true
}
}
case *ast.BinaryExpr:
return scanExprForXBase(e.Left) || scanExprForXBase(e.Right)
case *ast.AssignExpr:
return scanExprForXBase(e.Right)
case *ast.AliasExpr:
// FIELD->NAME always needs hbrdd
return true
}
return false
}
func scanStmtsForXBase(stmts []ast.Stmt) bool {
for _, s := range stmts {
switch v := s.(type) {
case *ast.UseCmd, *ast.GoCmd, *ast.SkipCmd, *ast.SeekCmd,
*ast.ReplaceCmd, *ast.AppendCmd, *ast.DeleteCmd,
*ast.SelectCmd, *ast.IndexCmd, *ast.SetCmd:
return true
case *ast.ExprStmt:
if scanExprForXBase(v.X) {
return true
}
case *ast.IfStmt:
if scanStmtsForXBase(v.Body) || scanStmtsForXBase(v.ElseBody) {
return true
}
for _, ei := range v.ElseIfs {
if scanStmtsForXBase(ei.Body) {
return true
}
}
case *ast.ForStmt:
if scanStmtsForXBase(v.Body) {
return true
}
case *ast.ForEachStmt:
if scanStmtsForXBase(v.Body) {
return true
}
case *ast.DoWhileStmt:
if scanStmtsForXBase(v.Body) {
return true
}
case *ast.SeqStmt:
if scanStmtsForXBase(v.Body) || scanStmtsForXBase(v.RecoverBody) {
return true
}
case *ast.SwitchStmt:
for _, c := range v.Cases {
if scanStmtsForXBase(c.Body) {
return true
}
}
}
}
return false
}