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:
2026-04-18 08:24:27 +09:00
parent 523d3fcf2e
commit a0acdf0289

View File

@@ -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)