fix(gengo): INDEX ON ... TO (expr) evaluates filename at runtime

Prior behavior used exprToString() to serialize the TO expression
back into a string, so a runtime-evaluated filename like
`( Lower(cTable) + "_pk.ntx" )` ended up as the literal filename
`Lower(cTable) + "_pk.ntx"` on disk. Visible in FiveSql2's PRIMARY
KEY / UNIQUE DDL path: test_sql1999 was creating files with that
literal name, which the test happened not to care about because the
USE inside BEGIN SEQUENCE caught the failure.

Fix: if the File expression contains any function call (detected by
new containsCall walker), emit emitExpr + Pop2 + AsString — runtime
evaluation path. Static filenames (`TO test.ntx`) still use the
cheap exprToString branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 10:41:15 +09:00
parent 11b41eda30
commit b9296412af

View File

@@ -617,8 +617,20 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
g.indent++
keyStr := exprToString(s.KeyExpr)
g.writeln(fmt.Sprintf("_keyExpr := %q", keyStr))
fileStr := exprToString(s.File)
g.writeln(fmt.Sprintf("_file := %q", fileStr))
// File expression: if it contains a function call, evaluate at
// runtime — Harbour `INDEX ON ... TO ( cExpr )` semantics. Prior
// behavior was static exprToString which serialized calls like
// `Lower(cTable) + "_pk.ntx"` into the literal filename string.
// Detect via containsCall; preserve static path for simple
// `test.ntx` style identifiers.
if containsCall(s.File) {
g.emitExpr(s.File)
g.writeln("_file := t.Pop2().AsString()")
} else {
fileStr := exprToString(s.File)
g.writeln(fmt.Sprintf("_file := %q", fileStr))
}
forExpr := `""`
if s.ForCond != nil {
forExpr = fmt.Sprintf("%q", exprToString(s.ForCond))
@@ -1783,6 +1795,26 @@ func (g *Generator) emitIndexKeyExpr(expr ast.Expr) {
}
// exprToString extracts a string representation from an AST expression.
// containsCall reports whether an expression contains any function call.
// Used to distinguish runtime-evaluated INDEX ON filenames
// (`TO ( Lower(cTable) + "_pk.ntx" )`) from static ones (`TO test.ntx`).
func containsCall(expr ast.Expr) bool {
if expr == nil {
return false
}
switch e := expr.(type) {
case *ast.CallExpr:
return true
case *ast.BinaryExpr:
return containsCall(e.Left) || containsCall(e.Right)
case *ast.UnaryExpr:
return containsCall(e.X)
case *ast.SendExpr:
return true
}
return false
}
// Used for INDEX ON key and filename, where idents are field/file names, not variables.
func exprToString(expr ast.Expr) string {
switch e := expr.(type) {