Proposal Details
Programs that use go/ast
often need to make traversals over the tree to find all nodes of a given type (or types), or to search for a specific node. The existing ast.Inspect
function allows them to do this, but it can impose a significant syntactic overhead. This example shows a use of Inspect to print the first interesting node, aborting the traversal once the node is found:
var found bool
ast.Inspect(root, func(n ast.Node) bool {
if !found && n != nil && interesting(n) {
print(n)
found = true
}
return !found
})
We propose to add this function to go/ast:
// DepthFirst returns a go1.23 iterator over the nodes of the syntax
// tree beneath the specified root, in depth-first order.
//
// For greater control over the traversal of each subtree, use [Inspect].
func DepthFirst(root Node) func(yield func(Node) bool) { // = iter.Seq[Node]
return func(yield func(Node) bool) {
ok := true
Inspect(root, func(n Node) bool {
if n != nil {
ok = ok && yield(n)
}
return ok
})
}
}
which would allow the previous example to be simplified to:
for n := range ast.DepthFirst(root) {
if interesting(n) {
print(n)
break
}
}
cc @findleyr @griesemer
Comment From: gopherbot
Change https://go.dev/cl/570680 mentions this issue: go/ast: add DepthFirst go1.23 iterator
Comment From: adonovan
For the curious, it adds 10-15% to the cost of a trivial traversal.
package ast_test
import (
"go/ast"
"go/parser"
"go/token"
"testing"
)
var file *ast.File
func init() {
file, _ = parser.ParseFile(token.NewFileSet(), "ast.go", nil, 0)
}
const k = 2
func BenchmarkInspect(b *testing.B) {
total := 0
for range b.N {
var nodes []ast.Node
ast.Inspect(file, func(n ast.Node) bool {
if n != nil {
total++
if total%k == 0 {
nodes = append(nodes, n)
}
}
return true
})
}
}
func BenchmarkRange(b *testing.B) {
total := 0
for range b.N {
var nodes []ast.Node
for n := range ast.DepthFirst(file) {
total++
if total%k == 0 {
nodes = append(nodes, n)
}
}
}
}
Comment From: rsc
This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — rsc for the proposal review group
Comment From: rsc
The name DepthFirst doesn't indicate whether its Preorder or Postorder. It sounds like we should call it Preorder. (This matches Inspect, but Inspect is kind of both, it passes n before the visit of children and nil afterward. This function drops the nils.)
Comment From: rsc
Have all remaining concerns about this proposal been addressed?
The proposal is to add:
// Preorder returns a go1.23 iterator over the nodes of the syntax
// tree beneath the specified root, in depth-first order.
// Each node is produced before its children.
//
// For greater control over the traversal of each subtree, use [Inspect].
func Preorder(root Node) iter.Seq[Node]
Comment From: rsc
Based on the discussion above, this proposal seems like a likely accept. — rsc for the proposal review group
The proposal is to add:
// Preorder returns a go1.23 iterator over the nodes of the syntax
// tree beneath the specified root, in depth-first order.
// Each node is produced before its children.
//
// For greater control over the traversal of each subtree, use [Inspect].
func Preorder(root Node) iter.Seq[Node]
Comment From: rsc
No change in consensus, so accepted. 🎉 This issue now tracks the work of implementing the proposal. — rsc for the proposal review group
The proposal is to add:
// Preorder returns a go1.23 iterator over the nodes of the syntax
// tree beneath the specified root, in depth-first order.
// Each node is produced before its children.
//
// For greater control over the traversal of each subtree, use [Inspect].
func Preorder(root Node) iter.Seq[Node]
Comment From: gopherbot
Change https://go.dev/cl/585520 mentions this issue: go/ast: honor the result of yield in Preorder