diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 0da9c9b..3e31b41 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -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)