perf(gengo): unary-minus literal fold + x:=x+y → LocalAdd peephole

Two more leaf-level code-gen cleanups now that the const folder is in.

 - UnaryExpr MINUS over a LITERAL (INT/DOUBLE) emits the negated value
   directly, so `-42` becomes PushInt(-42) instead of PushInt(42) +
   Negate(). Guarded: MinInt64 passes through to the VM so the
   coerce-to-double path stays authoritative. Variables fall through
   to the normal Negate path — the LiteralExpr type assertion is the
   gate, so runtime-typed `-x` keeps its semantics.

 - `x := x + <expr>` / `x := x - <expr>` detected when the LHS ident
   resolves to the same local as the self-reference on the RHS,
   emits the same LocalAdd / Negate+LocalAdd shape that x += y already
   used. Non-matching locals (shadowing, module statics) fall through.

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:26:59 +09:00
parent a0acdf0289
commit 111ab8a6f0

View File

@@ -232,6 +232,45 @@ func (g *Generator) emitPushSymbol(name string) {
g.writeln(fmt.Sprintf("t.PushSymbol(t.GetSym(&%s, %q))", v, name))
}
// negateLiteral produces a new literal that represents -lit. Handles
// INT and DOUBLE (as a textual prefix). Returns (nil, false) for
// non-numeric literals or an already-negative INT whose negation would
// overflow int64.
func negateLiteral(lit *ast.LiteralExpr) (*ast.LiteralExpr, bool) {
switch lit.Kind {
case token.INT:
n, err := strconv.ParseInt(lit.Value, 10, 64)
if err != nil {
return nil, false
}
// Guard: math.MinInt64 has no positive twin — let the VM's
// runtime coerce-to-double path handle it.
if n == -1<<63 {
return nil, false
}
return &ast.LiteralExpr{
ValuePos: lit.ValuePos,
Kind: token.INT,
Value: strconv.FormatInt(-n, 10),
}, true
case token.DOUBLE:
// Syntactically prefix `-` or flip an existing leading `-`.
if strings.HasPrefix(lit.Value, "-") {
return &ast.LiteralExpr{
ValuePos: lit.ValuePos,
Kind: token.DOUBLE,
Value: lit.Value[1:],
}, true
}
return &ast.LiteralExpr{
ValuePos: lit.ValuePos,
Kind: token.DOUBLE,
Value: "-" + lit.Value,
}, true
}
return nil, false
}
// 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
@@ -1137,6 +1176,22 @@ func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) {
if idx, found := locals[strings.ToUpper(ident.Name)]; found {
switch a.Op {
case token.ASSIGN:
// Peephole: `x := x + <expr>` / `x := x - <expr>` →
// LocalAdd. Same result as `x += <expr>` but lets the
// PRG side use the explicit form without penalty.
if be, ok := a.Right.(*ast.BinaryExpr); ok &&
(be.Op == token.PLUS || be.Op == token.MINUS) {
if lid, isIdent := be.Left.(*ast.IdentExpr); isIdent {
if selfIdx, found := locals[strings.ToUpper(lid.Name)]; found && selfIdx == idx {
g.emitExpr(be.Right)
if be.Op == token.MINUS {
g.writeln("t.Negate()")
}
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
return
}
}
}
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
case token.PLUSEQ:
@@ -1826,6 +1881,16 @@ func (g *Generator) emitExpr(expr ast.Expr) {
g.emitBinaryOp(e.Op)
}
case *ast.UnaryExpr:
// Fold `-<literal>` to a negative literal at compile time so
// `-42` becomes PushInt(-42) instead of PushInt(42) + Negate().
if e.Op == token.MINUS {
if lit, ok := e.X.(*ast.LiteralExpr); ok {
if neg, ok := negateLiteral(lit); ok {
g.emitLiteral(neg)
break
}
}
}
g.emitExpr(e.X)
g.emitUnaryOp(e.Op)
case *ast.AssignExpr: