From 7e4079f845a2b2ad20ed567450b755cf7fb74bc5 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Sat, 18 Apr 2026 08:52:08 +0900 Subject: [PATCH] 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) --- compiler/gengo/gengo.go | 45 +++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 640b812..86d7ceb 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -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