php2go/hoistpass.go

182 lines
4.8 KiB
Go

package main
import (
"fmt"
"github.com/z7zmey/php-parser/node"
"github.com/z7zmey/php-parser/node/expr"
"github.com/z7zmey/php-parser/node/expr/binary"
"github.com/z7zmey/php-parser/node/scalar"
"github.com/z7zmey/php-parser/node/stmt"
"php2go/parseutil"
)
type hoistPass struct {
mw *parseutil.MutatingWalker
nextHoist func(stmts ...node.Node) error
}
func (this *hoistPass) Enter(n_ *node.Node) error {
n := *n_ // not a copy
switch n := n.(type) {
case *stmt.StmtList:
return this.enterStmtList(&n.Stmts) // We can inject extra entries here
case *stmt.Function:
// We can inject extra entries here
// Note that we are NOT walking the other parts of this AST node
// (e.g. function parameters, function name, ...) since presumably they
// can't contain any hoistable statements
return this.enterStmtList(&n.Stmts)
case *expr.Closure:
return this.enterStmtList(&n.Stmts)
case *node.Root:
return this.enterStmtList(&n.Stmts)
case *binary.BooleanAnd:
return this.enterShortCircuitingBinOp(&n.Left, &n.Right)
case *binary.BooleanOr:
return this.enterShortCircuitingBinOp(&n.Left, &n.Right)
case *binary.LogicalAnd:
return this.enterShortCircuitingBinOp(&n.Left, &n.Right)
case *binary.LogicalOr:
return this.enterShortCircuitingBinOp(&n.Left, &n.Right)
// Don't try to hoist a ++i statement in the loop conditional
// Bypass by just walking the stmts in a new hoist context
// TODO handle hoisting within closures in conditional statements
case *stmt.For:
if sl, ok := n.Stmt.(*stmt.StmtList); ok {
return this.enterStmtList(&sl.Stmts)
} else {
// single statement, ignore
}
case *stmt.Do:
if sl, ok := n.Stmt.(*stmt.StmtList); ok {
return this.enterStmtList(&sl.Stmts)
} else {
// single statement, ignore
}
case *stmt.While:
if sl, ok := n.Stmt.(*stmt.StmtList); ok {
return this.enterStmtList(&sl.Stmts)
} else {
// single statement, ignore
}
case *expr.PreInc:
// Hoist the whole increment expression up to statement context
// We also have to convert it to a post-increment
err := this.nextHoist(stmt.NewExpression(expr.NewPostInc(n.Variable)))
if err != nil {
return parseErr{n, err}
}
// FIXME detect interior function calls since we may be dangerously doubling up on that
// For a more complex expression it is probably OK to use a temporary variable
// We also need to replace our own node with just the value itself
*n_ = n.Variable
case *expr.PostInc:
// Hoist the whole increment expression up to statement context
err := this.nextHoist(stmt.NewExpression(n))
if err != nil {
return parseErr{n, err}
}
// We also need to replace our own node. The resulting value would normally
// be one less than the current value
// PHP only supports lvalues for pre/postinc expressions so it should
// be fine in all Go contexts too to reuse the variable expression
// FIXME except for $foo[rand()]++
*n_ = binary.NewPlus(n.Variable, scalar.NewLnumber(`1`))
}
return nil // TODO
}
func (this *hoistPass) enterShortCircuitingBinOp(left, right *node.Node) error {
// These wreck our ability to hoist, since we can't neccessarily just move
// the right-hand side operand out
// Although it's still safe to walk the left-hand side
err := this.mw.Walk(left)
if err != nil {
return err
}
lastHoist := this.nextHoist
this.nextHoist = func(stmts ...node.Node) error {
return parseErr{*right, fmt.Errorf("Can't determine how to hoist expression on right-hand side of && expression")}
}
err = this.mw.Walk(right)
if err != nil {
return err
}
this.nextHoist = lastHoist
return parseutil.ErrDoNotWalkDeeper // since we already did it
}
func (this *hoistPass) enterStmtList(stmtList *[]node.Node) error {
// We can inject extra entries here
// In order to do so, we need to perform the interior Walk ourselves
// so that we know exactly where to put the extra entries
// Take stmtList by pointer so we can replace the slice outright
anyMutations := false
lastHoist := this.nextHoist
ret := make([]node.Node, 0, len(*stmtList))
this.nextHoist = func(stmts ...node.Node) error {
anyMutations = true
ret = append(ret, stmts...)
return nil
}
for i, _ := range *stmtList {
err := this.mw.Walk(&(*stmtList)[i])
if err != nil {
return err
}
ret = append(ret, (*stmtList)[i]) // *after* the hoisted statements
}
this.nextHoist = lastHoist
if anyMutations {
*stmtList = ret // our modified copy
}
return parseutil.ErrDoNotWalkDeeper // since we already did it
}
func runHoistPass(root *node.Node) error {
hp := hoistPass{}
hp.mw = &parseutil.MutatingWalker{
EnterNode: hp.Enter,
LeaveNode: parseutil.MutatingWalkerNoop,
}
hp.nextHoist = func(stmts ...node.Node) error {
return fmt.Errorf("nowhere available to hoist %d statements", len(stmts))
}
return hp.mw.Walk(root)
}