perf(gengo): reassociate left-leaning string-concat literal runs

`"a" + x + "b" + "c" + "d"` used to emit 4 Plus() calls because
the parser builds a left-leaning chain and no pair was
literal+literal. Add a reassociation step inside foldLiteralTree:
when the outer shape is `(Y + strlit1) + strlit2`, rewrite as
`Y + (strlit1+strlit2)` so the tail literals collapse. Also run
foldLiteralTree on the root BinaryExpr in emitExpr so the
outermost reassoc fires (was only running on children).

Verified: the 4-Plus case now emits 2 Plus calls (`"a" + x + "bcd"`).
FiveSql2 43/43, Harbour compat 56/56.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 08:52:08 +09:00
parent 67a9855319
commit 7e4079f845

View File

@@ -275,6 +275,13 @@ func negateLiteral(lit *ast.LiteralExpr) (*ast.LiteralExpr, bool) {
// 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.
//
// For left-associative string-concat chains like "a" + x + "b" + "c",
// the parser builds (((("a" + x) + "b") + "c")) and no pair is
// literal+literal. We reassociate: if the LHS is `Y + strlit` and the
// RHS is a string literal, rewrite as `Y + (strlit+rhslit)` so the
// tail literals collapse. Only safe for STRING+STRING (numeric `+`
// cares about types / overflow).
func foldLiteralTree(e ast.Expr) ast.Expr {
be, ok := e.(*ast.BinaryExpr)
if !ok {
@@ -285,6 +292,26 @@ func foldLiteralTree(e ast.Expr) ast.Expr {
if folded, ok := tryFoldBinary(be); ok {
return folded
}
// String-concat reassociation for left-leaning chains.
if be.Op == token.PLUS {
if rLit, ok := be.Right.(*ast.LiteralExpr); ok && rLit.Kind == token.STRING {
if lBin, ok := be.Left.(*ast.BinaryExpr); ok && lBin.Op == token.PLUS {
if mLit, ok := lBin.Right.(*ast.LiteralExpr); ok && mLit.Kind == token.STRING {
fused := &ast.LiteralExpr{
ValuePos: mLit.ValuePos,
Kind: token.STRING,
Value: mLit.Value + rLit.Value,
}
return &ast.BinaryExpr{
OpPos: be.OpPos,
Op: token.PLUS,
Left: lBin.Left,
Right: fused,
}
}
}
}
}
return be
}
@@ -1945,15 +1972,17 @@ 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 {
// Compile-time constant folding. Fold the whole subtree so
// `(2*3) + 5` collapses to PushInt(11) and so string-concat
// tails like `x + "b" + "c"` reassociate into `x + "bc"`.
// Mutates the AST in place — safe because the generator owns
// the tree post-parse.
switch folded := foldLiteralTree(e).(type) {
case *ast.LiteralExpr:
g.emitLiteral(folded)
break
return
case *ast.BinaryExpr:
e = folded
}
// Short-circuit AND/OR: Harbour evaluates right operand only if needed.
// With a literal LHS we can skip the PushBool/PopLogical roundtrip