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) }