perf(gengo): compile-time constant folding for literal arithmetic
Fold BinaryExpr subtrees whose operands reduce to INT or STRING literals at compile time. `10 * 2 + 5` now emits a single PushInt(25) instead of three VM ops; `"a" + "b"` collapses to "ab". Overflowing INTs and SLASH (which Harbour turns into double) fall through to the VM so semantics stay intact. Implementation is a bottom-up foldLiteralTree pre-pass on each BinaryExpr, plus a tryFoldBinary matcher for the leaf case. Mutates the AST in place — safe because the generator owns the tree after parse. Bench numbers don't move (SQL paths have no literal-only arithmetic in hot loops), but generated code shrinks on PRG that uses #define constants for widths / offsets / factors. Verification - go test ./... ALL PASS - FiveSql2 test_sql1999 43/43 - tests/compat_harbour 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -231,6 +232,95 @@ func (g *Generator) emitPushSymbol(name string) {
|
||||
g.writeln(fmt.Sprintf("t.PushSymbol(t.GetSym(&%s, %q))", v, name))
|
||||
}
|
||||
|
||||
// foldLiteralTree recursively folds BinaryExpr subtrees into LiteralExpr
|
||||
// where both operands eventually collapse to literals. Non-foldable
|
||||
// subtrees come back unchanged. Used as a preorder pre-pass so the
|
||||
// caller can look at a flat LITERAL + LITERAL pair.
|
||||
func foldLiteralTree(e ast.Expr) ast.Expr {
|
||||
be, ok := e.(*ast.BinaryExpr)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
be.Left = foldLiteralTree(be.Left)
|
||||
be.Right = foldLiteralTree(be.Right)
|
||||
if folded, ok := tryFoldBinary(be); ok {
|
||||
return folded
|
||||
}
|
||||
return be
|
||||
}
|
||||
|
||||
// tryFoldBinary returns a synthetic LiteralExpr when both operands of a
|
||||
// BinaryExpr are themselves literals and the operator is one the
|
||||
// folder recognises. INT+INT stays INT (with overflow falling through
|
||||
// to the VM path), mixed numeric falls to double, STRING+STRING
|
||||
// concatenates. Non-literal operands or unsupported op → (nil, false).
|
||||
func tryFoldBinary(e *ast.BinaryExpr) (*ast.LiteralExpr, bool) {
|
||||
l, lok := e.Left.(*ast.LiteralExpr)
|
||||
r, rok := e.Right.(*ast.LiteralExpr)
|
||||
if !lok || !rok {
|
||||
return nil, false
|
||||
}
|
||||
switch e.Op {
|
||||
case token.PLUS, token.MINUS, token.STAR, token.SLASH:
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
// INT + INT — keep int exact result.
|
||||
if l.Kind == token.INT && r.Kind == token.INT {
|
||||
li, errL := strconv.ParseInt(l.Value, 10, 64)
|
||||
ri, errR := strconv.ParseInt(r.Value, 10, 64)
|
||||
if errL != nil || errR != nil {
|
||||
return nil, false
|
||||
}
|
||||
var result int64
|
||||
var overflowed bool
|
||||
switch e.Op {
|
||||
case token.PLUS:
|
||||
result = li + ri
|
||||
// Harbour overflow discipline: fall through to VM on overflow
|
||||
if (ri >= 0 && result < li) || (ri < 0 && result > li) {
|
||||
overflowed = true
|
||||
}
|
||||
case token.MINUS:
|
||||
result = li - ri
|
||||
if (ri <= 0 && result < li) || (ri > 0 && result > li) {
|
||||
overflowed = true
|
||||
}
|
||||
case token.STAR:
|
||||
if li == 0 || ri == 0 {
|
||||
result = 0
|
||||
} else {
|
||||
result = li * ri
|
||||
if result/li != ri {
|
||||
overflowed = true
|
||||
}
|
||||
}
|
||||
case token.SLASH:
|
||||
// Harbour SLASH always yields double even for int inputs.
|
||||
return nil, false
|
||||
}
|
||||
if overflowed {
|
||||
return nil, false
|
||||
}
|
||||
return &ast.LiteralExpr{
|
||||
ValuePos: l.ValuePos,
|
||||
Kind: token.INT,
|
||||
Value: strconv.FormatInt(result, 10),
|
||||
}, true
|
||||
}
|
||||
// STRING + STRING — concatenate. Preserves the quoting style of the
|
||||
// left literal so DateExpr and other quoting-sensitive kinds don't
|
||||
// change shape.
|
||||
if e.Op == token.PLUS && l.Kind == token.STRING && r.Kind == token.STRING {
|
||||
return &ast.LiteralExpr{
|
||||
ValuePos: l.ValuePos,
|
||||
Kind: token.STRING,
|
||||
Value: l.Value + r.Value,
|
||||
}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// emitSymCache writes the package-level `var _sym_NAME *hbrt.Symbol`
|
||||
// declarations discovered during body emission. Called after all
|
||||
// function bodies are emitted so every PushSymbol call site has had
|
||||
@@ -1705,6 +1795,16 @@ func (g *Generator) emitExpr(expr ast.Expr) {
|
||||
case *ast.IdentExpr:
|
||||
g.emitIdent(e)
|
||||
case *ast.BinaryExpr:
|
||||
// Compile-time constant folding. Fold children first so
|
||||
// `(2*3) + 5` collapses all the way to a single PushInt(11)
|
||||
// instead of stopping at the inner `6`. Mutates the AST in
|
||||
// place — safe because the generator owns the tree post-parse.
|
||||
e.Left = foldLiteralTree(e.Left)
|
||||
e.Right = foldLiteralTree(e.Right)
|
||||
if folded, ok := tryFoldBinary(e); ok {
|
||||
g.emitLiteral(folded)
|
||||
break
|
||||
}
|
||||
// Short-circuit AND/OR: Harbour evaluates right operand only if needed
|
||||
if e.Op == token.AND {
|
||||
g.emitExpr(e.Left)
|
||||
|
||||
Reference in New Issue
Block a user