perf(gengo): constant-propagate literal-init LOCALs
Scan each function body for LOCALs whose sole write is a literal initialiser (never ++/-- / += / @byref / MultiAssign target / FOR var / @GET target / macro). Reads substitute the literal inline at emit time, which cascades into all earlier folds: dead IF branches, AND/OR short-circuit, NOT, string-concat reassoc, and the FOR LocalLessEqualInt fast path (extended to see through a propagated ident limit). Walker is bounded — unrecognised AST nodes abort propagation for the whole function rather than risk missing a hidden write. Harbour compat 56/56, FiveSql2 43/43. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,9 @@ type Generator struct {
|
|||||||
forLabelSeq int // monotonic counter for FOR..NEXT LOOP labels
|
forLabelSeq int // monotonic counter for FOR..NEXT LOOP labels
|
||||||
curForLabel string // current FOR loop's LOOP goto label ("" if not in FOR)
|
curForLabel string // current FOR loop's LOOP goto label ("" if not in FOR)
|
||||||
blockSeq int // monotonic counter for unique closure capture names
|
blockSeq int // monotonic counter for unique closure capture names
|
||||||
|
// Per-function constant locals: LOCAL names (uppercase) whose sole
|
||||||
|
// assignment is a literal initializer. Reads get substituted inline.
|
||||||
|
constLocals map[string]*ast.LiteralExpr
|
||||||
}
|
}
|
||||||
|
|
||||||
type symbolEntry struct {
|
type symbolEntry struct {
|
||||||
@@ -387,6 +390,282 @@ func tryFoldBinary(e *ast.BinaryExpr) (*ast.LiteralExpr, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectConstLocals returns a map of LOCAL names (uppercase) whose
|
||||||
|
// only assignment is a literal initializer — these can be propagated
|
||||||
|
// inline. Any reassignment, ++/--, += family, @byref, MultiAssignStmt
|
||||||
|
// target, FOR/FOREACH loop var, or AtGet target disqualifies the name.
|
||||||
|
//
|
||||||
|
// The walker is bounded: if it encounters a macro expansion or any
|
||||||
|
// AST node it doesn't recognise, it aborts and returns an empty map.
|
||||||
|
// Correctness trumps coverage — an unrecognised node might hide a
|
||||||
|
// write, so we refuse to propagate.
|
||||||
|
func collectConstLocals(fn *ast.FuncDecl) map[string]*ast.LiteralExpr {
|
||||||
|
v := &constLocalVisitor{
|
||||||
|
candidates: map[string]*ast.LiteralExpr{},
|
||||||
|
}
|
||||||
|
// Seed candidates from top-level LOCAL decls with literal init.
|
||||||
|
for _, d := range fn.Decls {
|
||||||
|
vd, ok := d.(*ast.VarDecl)
|
||||||
|
if !ok || vd.Scope != ast.ScopeLocal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, vi := range vd.Vars {
|
||||||
|
if vi.Init == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lit, ok := vi.Init.(*ast.LiteralExpr); ok {
|
||||||
|
v.candidates[strings.ToUpper(vi.Name)] = lit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(v.candidates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Params are writable even without explicit assignment (by-value
|
||||||
|
// but reassignable) — disqualify any candidate that shadows a param.
|
||||||
|
// Params come from a separate slot but guard in case of odd decls.
|
||||||
|
for _, p := range fn.Params {
|
||||||
|
delete(v.candidates, strings.ToUpper(p.Name))
|
||||||
|
}
|
||||||
|
for _, st := range fn.Body {
|
||||||
|
v.stmt(st)
|
||||||
|
if v.aborted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(v.candidates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v.candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
type constLocalVisitor struct {
|
||||||
|
candidates map[string]*ast.LiteralExpr
|
||||||
|
aborted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *constLocalVisitor) abort() {
|
||||||
|
v.aborted = true
|
||||||
|
v.candidates = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *constLocalVisitor) writeIdent(e ast.Expr) {
|
||||||
|
if id, ok := e.(*ast.IdentExpr); ok {
|
||||||
|
delete(v.candidates, strings.ToUpper(id.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *constLocalVisitor) writeName(name string) {
|
||||||
|
delete(v.candidates, strings.ToUpper(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *constLocalVisitor) exprs(es []ast.Expr) {
|
||||||
|
for _, e := range es {
|
||||||
|
v.expr(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *constLocalVisitor) stmts(ss []ast.Stmt) {
|
||||||
|
for _, s := range ss {
|
||||||
|
v.stmt(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *constLocalVisitor) expr(e ast.Expr) {
|
||||||
|
if v.aborted || e == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch x := e.(type) {
|
||||||
|
case *ast.LiteralExpr, *ast.IdentExpr, *ast.SelfExpr:
|
||||||
|
// leaf; reads don't disqualify
|
||||||
|
case *ast.BinaryExpr:
|
||||||
|
v.expr(x.Left)
|
||||||
|
v.expr(x.Right)
|
||||||
|
case *ast.UnaryExpr:
|
||||||
|
if x.Op == token.INC || x.Op == token.DEC {
|
||||||
|
v.writeIdent(x.X)
|
||||||
|
}
|
||||||
|
v.expr(x.X)
|
||||||
|
case *ast.PostfixExpr:
|
||||||
|
v.writeIdent(x.X)
|
||||||
|
v.expr(x.X)
|
||||||
|
case *ast.AssignExpr:
|
||||||
|
// All assign ops (:= += -= *= /= %= ^=) are writes to Left's
|
||||||
|
// outer ident. Compound assigns also read, but disqualification
|
||||||
|
// is based on being written at all.
|
||||||
|
v.writeIdent(x.Left)
|
||||||
|
// Still walk Left in case of indexing: arr[i] := v — the ident
|
||||||
|
// arr is read (and we don't want to accidentally treat it as a
|
||||||
|
// write since writeIdent only triggers on a bare IdentExpr).
|
||||||
|
if _, isIdent := x.Left.(*ast.IdentExpr); !isIdent {
|
||||||
|
v.expr(x.Left)
|
||||||
|
}
|
||||||
|
v.expr(x.Right)
|
||||||
|
case *ast.CallExpr:
|
||||||
|
v.expr(x.Func)
|
||||||
|
v.exprs(x.Args)
|
||||||
|
case *ast.DotExpr:
|
||||||
|
v.expr(x.X)
|
||||||
|
case *ast.SendExpr:
|
||||||
|
v.expr(x.Object)
|
||||||
|
if x.MacroMethod != nil {
|
||||||
|
v.expr(x.MacroMethod)
|
||||||
|
}
|
||||||
|
v.exprs(x.Args)
|
||||||
|
case *ast.IndexExpr:
|
||||||
|
v.expr(x.X)
|
||||||
|
v.expr(x.Index)
|
||||||
|
case *ast.AliasExpr:
|
||||||
|
v.expr(x.Alias)
|
||||||
|
v.expr(x.Field)
|
||||||
|
case *ast.MacroExpr:
|
||||||
|
// Macros can expand to any name including writes. Bail.
|
||||||
|
v.abort()
|
||||||
|
case *ast.BlockExpr:
|
||||||
|
v.expr(x.Body)
|
||||||
|
case *ast.ArrayLitExpr:
|
||||||
|
v.exprs(x.Items)
|
||||||
|
case *ast.HashLitExpr:
|
||||||
|
v.exprs(x.Keys)
|
||||||
|
v.exprs(x.Values)
|
||||||
|
case *ast.IIfExpr:
|
||||||
|
v.expr(x.Cond)
|
||||||
|
v.expr(x.True)
|
||||||
|
v.expr(x.False)
|
||||||
|
case *ast.RefExpr:
|
||||||
|
// @ident — passes by reference; callee may mutate.
|
||||||
|
v.writeIdent(x.X)
|
||||||
|
v.expr(x.X)
|
||||||
|
case *ast.SliceExpr:
|
||||||
|
v.expr(x.X)
|
||||||
|
v.expr(x.Low)
|
||||||
|
v.expr(x.High)
|
||||||
|
case *ast.NilSafeExpr:
|
||||||
|
v.expr(x.X)
|
||||||
|
case *ast.InterpolatedString:
|
||||||
|
v.exprs(x.Parts)
|
||||||
|
default:
|
||||||
|
v.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *constLocalVisitor) stmt(s ast.Stmt) {
|
||||||
|
if v.aborted || s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch x := s.(type) {
|
||||||
|
case *ast.ExprStmt:
|
||||||
|
v.expr(x.X)
|
||||||
|
case *ast.ReturnStmt:
|
||||||
|
v.expr(x.Value)
|
||||||
|
case *ast.QOutStmt:
|
||||||
|
v.exprs(x.Exprs)
|
||||||
|
case *ast.IfStmt:
|
||||||
|
v.expr(x.Cond)
|
||||||
|
v.stmts(x.Body)
|
||||||
|
for _, ei := range x.ElseIfs {
|
||||||
|
v.expr(ei.Cond)
|
||||||
|
v.stmts(ei.Body)
|
||||||
|
}
|
||||||
|
v.stmts(x.ElseBody)
|
||||||
|
case *ast.DoWhileStmt:
|
||||||
|
v.expr(x.Cond)
|
||||||
|
v.stmts(x.Body)
|
||||||
|
case *ast.ForStmt:
|
||||||
|
v.writeName(x.Var)
|
||||||
|
v.expr(x.Start)
|
||||||
|
v.expr(x.To)
|
||||||
|
v.expr(x.Step)
|
||||||
|
v.stmts(x.Body)
|
||||||
|
case *ast.ForEachStmt:
|
||||||
|
v.writeName(x.Var)
|
||||||
|
v.expr(x.Collection)
|
||||||
|
v.stmts(x.Body)
|
||||||
|
case *ast.SwitchStmt:
|
||||||
|
v.expr(x.Expr)
|
||||||
|
for _, c := range x.Cases {
|
||||||
|
v.expr(c.Value)
|
||||||
|
v.stmts(c.Body)
|
||||||
|
}
|
||||||
|
v.stmts(x.Otherwise)
|
||||||
|
case *ast.SeqStmt:
|
||||||
|
v.stmts(x.Body)
|
||||||
|
if x.RecoverVar != "" {
|
||||||
|
v.writeName(x.RecoverVar)
|
||||||
|
}
|
||||||
|
v.stmts(x.RecoverBody)
|
||||||
|
case *ast.MultiAssignStmt:
|
||||||
|
for _, t := range x.Targets {
|
||||||
|
v.writeName(t)
|
||||||
|
}
|
||||||
|
v.exprs(x.Values)
|
||||||
|
case *ast.VarDecl:
|
||||||
|
// Init exprs are reads. The LOCAL name itself was already
|
||||||
|
// collected as a candidate by collectConstLocals; we don't
|
||||||
|
// treat its own init as a reassignment.
|
||||||
|
for _, vi := range x.Vars {
|
||||||
|
v.expr(vi.Init)
|
||||||
|
}
|
||||||
|
case *ast.DeferStmt:
|
||||||
|
v.expr(x.Call)
|
||||||
|
case *ast.ExitStmt, *ast.LoopStmt:
|
||||||
|
// no expression
|
||||||
|
case *ast.SkipCmd:
|
||||||
|
v.expr(x.Count)
|
||||||
|
case *ast.GoCmd:
|
||||||
|
v.expr(x.RecNo)
|
||||||
|
case *ast.SeekCmd:
|
||||||
|
v.expr(x.Key)
|
||||||
|
case *ast.UseCmd:
|
||||||
|
v.expr(x.File)
|
||||||
|
v.expr(x.AliasExpr)
|
||||||
|
case *ast.SelectCmd:
|
||||||
|
v.expr(x.Area)
|
||||||
|
case *ast.ReplaceCmd:
|
||||||
|
for _, f := range x.Fields {
|
||||||
|
v.expr(f.Field)
|
||||||
|
v.expr(f.Value)
|
||||||
|
}
|
||||||
|
case *ast.AppendCmd, *ast.DeleteCmd, *ast.ReadCmd:
|
||||||
|
// no expressions
|
||||||
|
case *ast.IndexCmd:
|
||||||
|
v.expr(x.KeyExpr)
|
||||||
|
v.expr(x.File)
|
||||||
|
v.expr(x.ForCond)
|
||||||
|
case *ast.SetCmd:
|
||||||
|
v.expr(x.Expr)
|
||||||
|
case *ast.AtSayCmd:
|
||||||
|
v.expr(x.Row)
|
||||||
|
v.expr(x.Col)
|
||||||
|
v.expr(x.SayExpr)
|
||||||
|
v.expr(x.Picture)
|
||||||
|
case *ast.AtGetCmd:
|
||||||
|
// @ GET var writes to Var at READ time.
|
||||||
|
v.writeIdent(x.Var)
|
||||||
|
if x.VarName != "" {
|
||||||
|
v.writeName(x.VarName)
|
||||||
|
}
|
||||||
|
v.expr(x.Row)
|
||||||
|
v.expr(x.Col)
|
||||||
|
v.expr(x.Picture)
|
||||||
|
v.expr(x.Valid)
|
||||||
|
v.expr(x.When)
|
||||||
|
case *ast.AtSayGetCmd:
|
||||||
|
v.writeIdent(x.Var)
|
||||||
|
if x.VarName != "" {
|
||||||
|
v.writeName(x.VarName)
|
||||||
|
}
|
||||||
|
v.expr(x.Row)
|
||||||
|
v.expr(x.Col)
|
||||||
|
v.expr(x.SayExpr)
|
||||||
|
v.expr(x.Picture)
|
||||||
|
v.expr(x.Valid)
|
||||||
|
v.expr(x.When)
|
||||||
|
default:
|
||||||
|
v.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// emitSymCache writes the package-level `var _sym_NAME *hbrt.Symbol`
|
// emitSymCache writes the package-level `var _sym_NAME *hbrt.Symbol`
|
||||||
// declarations discovered during body emission. Called after all
|
// declarations discovered during body emission. Called after all
|
||||||
// function bodies are emitted so every PushSymbol call site has had
|
// function bodies are emitted so every PushSymbol call site has had
|
||||||
@@ -656,6 +935,9 @@ func (g *Generator) emitFuncDecl(fn *ast.FuncDecl) {
|
|||||||
|
|
||||||
// Build local map FIRST (needed for init expressions that reference params)
|
// Build local map FIRST (needed for init expressions that reference params)
|
||||||
g.curLocals = g.buildLocalMap(fn)
|
g.curLocals = g.buildLocalMap(fn)
|
||||||
|
// Scan for LOCALs that are literal-initialised and never reassigned
|
||||||
|
// so reads can be constant-propagated at emit time.
|
||||||
|
g.constLocals = collectConstLocals(fn)
|
||||||
|
|
||||||
// Emit LOCAL initializers
|
// Emit LOCAL initializers
|
||||||
localIdx := nParams + 1 // 1-based, params come first
|
localIdx := nParams + 1 // 1-based, params come first
|
||||||
@@ -682,6 +964,9 @@ func (g *Generator) emitFuncDecl(fn *ast.FuncDecl) {
|
|||||||
g.writeln("}")
|
g.writeln("}")
|
||||||
g.writeln("")
|
g.writeln("")
|
||||||
|
|
||||||
|
// Drop per-function constant map so it doesn't leak to siblings.
|
||||||
|
g.constLocals = nil
|
||||||
|
|
||||||
// Remove function-scoped STATIC vars from the map so they don't
|
// Remove function-scoped STATIC vars from the map so they don't
|
||||||
// leak into other functions that happen to share a variable name.
|
// leak into other functions that happen to share a variable name.
|
||||||
for _, d := range fn.Decls {
|
for _, d := range fn.Decls {
|
||||||
@@ -1647,8 +1932,18 @@ func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
|
|||||||
g.writeln("for {")
|
g.writeln("for {")
|
||||||
g.indent++
|
g.indent++
|
||||||
|
|
||||||
// Comparison: fused opcode when limit is literal int (most common)
|
// Comparison: fused opcode when limit is literal int (most common).
|
||||||
if lit, ok := s.To.(*ast.LiteralExpr); ok && lit.Kind == token.INT {
|
// Also see through const-propagated LOCALs: `LOCAL n := 100; FOR i := 1
|
||||||
|
// TO n` should hit the same fast path as a bare literal.
|
||||||
|
toLit, _ := s.To.(*ast.LiteralExpr)
|
||||||
|
if toLit == nil {
|
||||||
|
if id, ok := s.To.(*ast.IdentExpr); ok {
|
||||||
|
if l, ok2 := g.constLocals[strings.ToUpper(id.Name)]; ok2 {
|
||||||
|
toLit = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lit := toLit; lit != nil && lit.Kind == token.INT {
|
||||||
if isNegStep {
|
if isNegStep {
|
||||||
g.writeln(fmt.Sprintf("if !t.LocalGreaterEqualInt(%d, %s) { break }", idx, lit.Value))
|
g.writeln(fmt.Sprintf("if !t.LocalGreaterEqualInt(%d, %s) { break }", idx, lit.Value))
|
||||||
} else {
|
} else {
|
||||||
@@ -2321,6 +2616,15 @@ func (g *Generator) emitIdent(e *ast.IdentExpr) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Constant-propagate literal-init LOCALs that collectConstLocals
|
||||||
|
// proved are never reassigned or passed by reference. Emitting the
|
||||||
|
// literal inline lets downstream folding (dead IF, AND/OR, FOR
|
||||||
|
// fused ops) fire on what was a variable reference.
|
||||||
|
if lit, ok := g.constLocals[upper]; ok {
|
||||||
|
g.emitLiteral(lit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if idx, found := g.curLocals[upper]; found {
|
if idx, found := g.curLocals[upper]; found {
|
||||||
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
||||||
} else if goVar, found := g.staticVars[upper]; found {
|
} else if goVar, found := g.staticVars[upper]; found {
|
||||||
|
|||||||
Reference in New Issue
Block a user