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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user