Sign in

    #traversal

    A read-only node model over the Smithy IDL AST, and traversal styles that all agree. NodeRef points into the AST (nothing is copied). With the same Control decisions, walk, fold, accept, an EventReader, push_events and reduce_pruned give the same enter and leave order, and a full TreeCursor navigation enters the same nodes. No walk recurses: each one keeps its own stack, so deep trees work on every target.

    Kinds and field names follow the Smithy JSON AST where it has a name (a shape's kind is its JSON type; a member has a target), and the IDL grammar for the rest. (category, kind) identifies a node type.

    #The node model

    ///|
    test "fields and children" {
    let r = @parser.parse_text(
    (
    #|$version: "2"
    #|namespace a
    #|
    #|/// A person.
    #|structure Person {
    #| name: String
    #|}
    #|
    ),
    )
    let root = @traversal.NodeRef::of_result(r).unwrap()
    let person = root.field("shapes")[0]
    inspect("\{person.category()}/\{person.kind()}", content="shape/structure")
    inspect(
    person.children().map(c => c.category()).join(" "),
    content="doc name member",
    )
    }

    Spans: span() is the AST span, widened so that it covers every child. The parser's span of a shape, member, enum member or inline shape starts after its doc comments, because doc comments are trivia; span() starts at the first doc comment. An operation property covers the docs of its inline shape, and the file covers every comment. The file node holds every // comment and every /// comment that documents nothing.

    #Walk, fold and reduce

    ///|
    test "walk, fold and reduce" {
    let r = @parser.parse_text("namespace a\n\n@t\nstring S\n")
    let root = @traversal.NodeRef::of_result(r).unwrap()
    let log = []
    @traversal.walk(root, n => {
    log.push(n.category())
    if n.category() == "trait" {
    SkipChildren
    } else {
    Continue
    }
    })
    inspect(log.join(" "), content="file namespace name shape trait name")
    let count = @traversal.fold(root, 0, (a, _) => (a + 1, Continue))
    inspect(count, content="7")
    // Bottom up: each node with the results of its children.
    let leaves = @traversal.reduce(root, (_, rs : ArrayView[Int32]) => {
    if rs.length() == 0 {
    1
    } else {
    rs.iter().fold(init=0, (a, b) => a + b)
    }
    })
    inspect(leaves, content="3")
    // reduce_pruned can skip a subtree, or stop with None.
    let pruned = @traversal.reduce_pruned(
    root,
    n => if n.category() == "trait" { SkipChildren } else { Continue },
    (_, rs : ArrayView[Int32]) => 1 + rs.iter().fold(init=0, (a, b) => a + b),
    )
    debug_inspect(pruned, content="Some(6)")
    }

    #Events

    ///|
    test "pull events" {
    let r = @parser.parse_text("namespace a\n\nstring S\n")
    let root = @traversal.NodeRef::of_result(r).unwrap()
    let reader = @traversal.EventReader::new(root)
    let log = []
    while reader.next() is Some(e) {
    match e {
    Enter(e) => {
    log.push("+\{e.path}")
    if e.category == "namespace" {
    reader.skip_children()
    }
    }
    Leave(_) => ()
    }
    }
    inspect(
    log.join(" "),
    content="+ +namespace[0] +shapes[0] +shapes[0].name[0]",
    )
    }

    push_events(source, handler) drives any EventSource into a Handler; accept(root, visitor) calls one Visitor method per node category (visit_file, visit_statement, visit_shape, visit_member, visit_trait, visit_value, visit_comment, visit_other). Both traits have defaults, so a type implements only the methods it needs:

    ///|
    struct TraitIds(Array[String])

    ///|
    impl @traversal.Visitor for TraitIds with fn visit_trait(self, n) {
    if n is Trait(t) {
    self.0.push(t.id.value.to_string())
    }
    SkipChildren
    }

    ///|
    test "a visitor" {
    let r = @parser.parse_text(
    "namespace a\n\n@required\n@length(min: 1)\nstring S\n",
    )
    let ids = TraitIds([])
    @traversal.accept(@traversal.NodeRef::of_result(r).unwrap(), ids)
    debug_inspect(ids.0, content="[\"required\", \"length\"]")
    }

    A Handler for push_events steers the run with the Control of on_enter:

    ///|
    struct Paths(Array[String])

    ///|
    impl @traversal.Handler for Paths with fn on_enter(self, e) {
    self.0.push(e.path.to_string())
    if e.category == "shape" {
    SkipChildren
    } else {
    Continue
    }
    }

    ///|
    test "push events into a handler" {
    let r = @parser.parse_text("namespace a\n\nstring S\n")
    let root = @traversal.NodeRef::of_result(r).unwrap()
    let paths = Paths([])
    @traversal.push_events(@traversal.EventReader::new(root), paths)
    debug_inspect(
    paths.0,
    content="[\"\", \"namespace[0]\", \"namespace[0].name[0]\", \"shapes[0]\"]",
    )
    }

    #Tree, cursor and paths

    ///|
    test "tree, cursor and path" {
    let text =
    #|namespace a
    #|
    #|structure Foo {
    #| x: String
    #|}
    #|
    let tree = @traversal.Tree::new(@parser.parse_text(text)).unwrap()
    let at = @span.Offset::new(text.find("x:").unwrap()).unwrap()
    let name = tree.node_at(at).unwrap()
    inspect(tree.node_path(name).unwrap(), content="shapes[0].members[0].name[0]")
    let c = @traversal.TreeCursor::new(tree.root())
    inspect(c.goto_node_at(at), content="true")
    inspect(c.path(), content="shapes[0].members[0].name[0]")
    }

    #Kind table

    One row per (category, kind). Fields are in fields() order. A feature checks this table against kind_table().

    CategoryKindFields
    filefilecontrol, metadata, namespace, uses, shapes, comments
    controlcontrolkey, value
    metadatametadatakey, value
    namespacenamespacename
    useusetarget
    shapeblobdocs, traits, name, mixins
    shapebooleandocs, traits, name, mixins
    shapedocumentdocs, traits, name, mixins
    shapestringdocs, traits, name, mixins
    shapebytedocs, traits, name, mixins
    shapeshortdocs, traits, name, mixins
    shapeintegerdocs, traits, name, mixins
    shapelongdocs, traits, name, mixins
    shapefloatdocs, traits, name, mixins
    shapedoubledocs, traits, name, mixins
    shapebigIntegerdocs, traits, name, mixins
    shapebigDecimaldocs, traits, name, mixins
    shapetimestampdocs, traits, name, mixins
    shapeenumdocs, traits, name, mixins, members
    shapeintEnumdocs, traits, name, mixins, members
    shapelistdocs, traits, name, for, mixins, members
    shapemapdocs, traits, name, for, mixins, members
    shapeuniondocs, traits, name, for, mixins, members
    shapestructuredocs, traits, name, for, mixins, members
    shapesetdocs, traits, name, for, mixins, members
    shapeservicedocs, traits, name, mixins, value
    shaperesourcedocs, traits, name, mixins, value
    shapeoperationdocs, traits, name, mixins, properties
    shapeapplytarget, traits
    shapeinlinedocs, traits, for, mixins, members
    mixinsmixinstargets
    membermemberdocs, traits, name, target, default
    memberenumMemberdocs, traits, name, value
    operation_propertyinputtarget
    operation_propertyoutputtarget
    operation_propertyerrorstargets
    traittraitid, value, entries
    valuearrayelements
    valueobjectentries
    valuenumber—
    valueboolean—
    valuenull—
    valuestring—
    valuetextBlock—
    valueshapeId—
    entryentrykey, value
    shape_idshape_id—
    namename—
    docdoc—
    commentline—
    commentdoc—

    EventSource

    pub(open) trait EventSource {
    fn next(Self) -> Event?
    fn skip_children(Self) -> Unit
    }

    A source of events in walk's order. skip_children right after an Enter makes the next event that node's Leave; elsewhere it does nothing. A streaming parser can implement it; push_events drives any source.

    Handler

    pub(open) trait Handler {
    fn on_enter(Self, EnterEvent) -> Control = _
    fn on_leave(Self, LeaveEvent) -> Unit = _
    }

    Callbacks for push_events. on_enter steers the run with its Control; both methods have defaults (Continue and nothing), so a handler implements only what it needs.

    Visitor

    pub(open) trait Visitor {
    fn visit_file(Self, NodeRef) -> Control = _
    fn visit_statement(Self, NodeRef) -> Control = _
    fn visit_shape(Self, NodeRef) -> Control = _
    fn visit_member(Self, NodeRef) -> Control = _
    fn visit_trait(Self, NodeRef) -> Control = _
    fn visit_value(Self, NodeRef) -> Control = _
    fn visit_comment(Self, NodeRef) -> Control = _
    fn visit_other(Self, NodeRef) -> Control = _
    fn leave(Self, NodeRef) -> Unit = _
    }

    One method per node category, for accept. Every visit_* method defaults to Continue and leave to nothing, so a visitor implements only what it needs; the Control it returns steers the walk as in walk.

    Control

    pub(all) enum Control {
    Continue
    SkipChildren
    Stop
    } derive(Eq,
    Debug
    )

    What a traversal does after it enters a node.

    EnterEvent

    pub struct EnterEvent {
    category : String
    kind : String
    field : String?
    start :
    Offset

    depth : Int
    path : NodePath
    } derive(Eq,
    Debug
    )

    A node starts. It has only what a streaming parser knows at that point: the node's category and kind, its field in the parent (None for the start node), its start, and its depth and path from the start node. Build one with EnterEvent::new, which sets depth from the path.

    EnterEvent::new

    fn EnterEvent::new(category~ : String, kind~ : String, field~ : String?, start~ :
    Offset
    , path~ : NodePath) -> EnterEvent

    An EnterEvent whose depth is the path's depth.

    Event

    pub(all) enum Event {
    Enter(EnterEvent)
    Leave(LeaveEvent)
    } derive(Eq,
    Debug
    )

    One event of a traversal: a node starts or ends.

    EventReader

    pub struct EventReader {
    // private fields
    }

    The walk of a node as pull events: next() returns Enter or Leave, then None after the start node's Leave. To stop, stop calling next(). An explicit stack holds the open nodes.

    test {
    let r = @parser.parse_text("namespace a\n\nstring S\n")
    let root = @traversal.NodeRef::of_result(r).unwrap()
    let reader = @traversal.EventReader::new(root)
    let log = []
    for e in reader.iter() {
    match e {
    Enter(e) => log.push("+" + e.category)
    Leave(e) => log.push("-" + e.node.category())
    }
    }
    inspect(
    log.join(" "),
    content="+file +namespace +name -name -namespace +shape +name -name -shape -file",
    )
    }

    EventReader::iter

    fn EventReader::iter(self : EventReader) -> Iter[Event]

    The remaining events, read from this reader: skip_children() on the reader still steers it.

    EventReader::new

    fn EventReader::new(root : NodeRef) -> EventReader

    A reader at the start of root's walk.

    LeaveEvent

    pub struct LeaveEvent {
    node : NodeRef
    depth : Int
    path : NodePath
    } derive(Eq,
    Debug
    )

    A node ends: the finished node, with the depth and path of its Enter. Build one with LeaveEvent::new.

    LeaveEvent::new

    fn LeaveEvent::new(node~ : NodeRef, path~ : NodePath) -> LeaveEvent

    A LeaveEvent whose depth is the path's depth.

    NodePath

    pub struct NodePath {
    // private fields
    } derive(Eq, Hash)

    The steps from a start node down to a node: the field and index of each child on the way, like a path into the Smithy JSON AST (shapes[2].members[0].target[0]). It is immutable; a child's path shares its parent's steps.

    test {
    let r = @parser.parse_text(
    (
    #|namespace a
    #|
    #|structure Foo {
    #| x: String
    #|}
    #|
    ),
    )
    let root = @traversal.NodeRef::of_result(r).unwrap()
    let path = @traversal.NodePath::root()
    .child("shapes", 0)
    .child("members", 0)
    .child("target", 0)
    inspect(path, content="shapes[0].members[0].target[0]")
    debug_inspect(
    path.resolve(root).map(n => n.kind()),
    content="Some(\"shape_id\")",
    )
    }
    impl Show for NodePath
    impl Debug for NodePath

    NodePath::append

    fn NodePath::append(self : NodePath, rest : NodePath) -> NodePath

    This path's steps followed by the steps of rest.

    NodePath::child

    fn NodePath::child(self : NodePath, field : String, index : Int) -> NodePath

    The path of the child at index in field of the node at this path.

    NodePath::depth

    fn NodePath::depth(self : NodePath) -> Int

    The number of steps.

    NodePath::from_steps

    fn NodePath::from_steps(steps : ArrayView[PathStep]) -> NodePath

    The path with these steps, from the start node down.

    NodePath::last

    fn NodePath::last(self : NodePath) -> PathStep?

    The last step, or None for the start node.

    NodePath::parent

    fn NodePath::parent(self : NodePath) -> NodePath?

    The path of the parent, or None for the start node.

    NodePath::resolve

    fn NodePath::resolve(self : NodePath, start : NodeRef) -> NodeRef?

    The node this path reaches from start, or None where a step does not exist.

    NodePath::root

    fn NodePath::root() -> NodePath

    The path of the start node itself: no steps.

    NodePath::steps

    fn NodePath::steps(self : NodePath) -> Array[PathStep]

    The steps from the start node down (a new array).

    NodeRef

    A read-only reference to one node of a parse result. Each case holds the AST value it points to (nothing is copied). Generic code uses category, kind, span, children and field; typed code matches the cases.

    Kinds and field names follow the Smithy JSON AST where it has a name (a shape's kind is its type, a member has a target), and the IDL grammar for the rest. (category, kind) identifies a node type.

    The file node also holds the comments that the AST does not keep: every // comment and every /// comment that documents nothing.

    test {
    let r = @parser.parse_text(
    (
    #|$version: "2"
    #|namespace a
    #|
    #|string Name
    #|
    ),
    )
    let root = @traversal.NodeRef::of_result(r).unwrap()
    inspect("\{root.category()}/\{root.kind()}", content="file/file")
    guard root is File(file, _) else { fail("not a file") }
    inspect(file.statements.length(), content="1")
    }

    NodeRef::category

    fn NodeRef::category(self : NodeRef) -> String

    The node's category: file, control, metadata, namespace, use, shape, mixins, member, operation_property, trait, value, entry, shape_id, name, doc or comment.

    NodeRef::children

    fn NodeRef::children(self : NodeRef) -> Array[NodeRef]

    All child nodes in source order (a new array). Doc comments come before the traits they precede, and the file's comments sit among its statements.

    NodeRef::children_with_fields

    fn NodeRef::children_with_fields(self : NodeRef) -> Array[(PathStep, NodeRef)]

    All child nodes in source order (a new array), each with its step: its field and its index in that field. Children that start at the same place keep their field order. The nodes are the ones that children gives.

    Use it to get the field of every child in one call. field_of looks through all the fields on each call.

    test {
    let r = @parser.parse_text(
    (
    #|$version: "2"
    #|namespace a
    #|
    #|structure Foo {
    #| a: String
    #| b: String
    #|}
    #|
    ),
    )
    let shape = @traversal.NodeRef::of_result(r).unwrap().field("shapes")[0]
    let labels = shape
    .children_with_fields()
    .map(x => "\{x.0.field}[\{x.0.index}]:\{x.1.kind()}")
    inspect(
    labels.join(" "),
    content="name[0]:name members[0]:member members[1]:member",
    )
    }

    NodeRef::field

    fn NodeRef::field(self : NodeRef, name : String) -> Array[NodeRef]

    The nodes in field name (a new array; empty for an absent or unknown field).

    NodeRef::field_of

    fn NodeRef::field_of(self : NodeRef, child : NodeRef) -> String?

    The field of this node that holds child, or None when child is not a child of this node. It looks through all the fields on each call; to get the field of every child, use children_with_fields.

    NodeRef::fields

    fn NodeRef::fields(self : NodeRef) -> Array[String]

    The node's field names, in kind-table order, whether or not they hold a child. A leaf has none.

    test {
    let r = @parser.parse_text(
    (
    #|$version: "2"
    #|namespace a
    #|
    #|structure Foo {
    #| bar: String
    #|}
    #|
    ),
    )
    let shape = @traversal.NodeRef::of_result(r).unwrap().field("shapes")[0]
    inspect(
    shape.fields().join(" "),
    content="docs traits name for mixins members",
    )
    inspect(shape.field("for").length(), content="0")
    let member = shape.field("members")[0]
    inspect(member.field("target")[0].kind(), content="shape_id")
    }

    NodeRef::kind

    fn NodeRef::kind(self : NodeRef) -> String

    The node's kind within its category. A shape's kind is its JSON AST type (structure, intEnum, bigDecimal, …, or apply and inline); a value's kind is its JSON node type (boolean for true and false), or textBlock and shapeId.

    NodeRef::of_result

    The file node of result, or None when the parser read no AST. The comments come from the CST trivia, in source order, without the doc comments that the AST attaches to a shape, member or inline shape.

    NodeRef::span

    The node's source span, widened over what the AST span leaves out, so a node's span covers all of its children. Doc comments are trivia, so the parser's span of a shape, member, enum member or inline shape starts after them; here it starts at the first doc comment. An operation property covers the docs of its inline shape, and the file covers every comment. Every other node has its AST span.

    PathStep

    pub(all) struct PathStep {
    field : String
    index : Int
    } derive(Eq, Hash,
    Debug
    )

    One step of a path: the field that holds a child and its index there.

    Tree

    pub struct Tree {
    // private fields
    }

    A parse result as a tree with random access: parents, ancestors, paths, the node at an offset and the tokens in a span. It is built once and never changes; its indexes are private.

    Tree::ancestors

    fn Tree::ancestors(self : Tree, n : NodeRef) -> Array[NodeRef]

    The ancestors of n, nearest first (a new array).

    Tree::field_of

    fn Tree::field_of(self : Tree, n : NodeRef) -> String?

    The field of n in its parent, or None for the root.

    Tree::new

    The tree of result, or None when the parser read no AST. It walks the whole tree once to index the parents, so build it once and keep it.

    test {
    let r = @parser.parse_text(
    (
    #|namespace a
    #|
    #|structure Foo {
    #| x: String
    #|}
    #|
    ),
    )
    let tree = @traversal.Tree::new(r).unwrap()
    let member = tree.root().field("shapes")[0].field("members")[0]
    inspect(tree.parent(member).unwrap().kind(), content="structure")
    debug_inspect(
    tree.path(member).map(n => n.kind()),
    content="[\"file\", \"structure\", \"member\"]",
    )
    inspect(tree.node_path(member).unwrap(), content="shapes[0].members[0]")
    }

    Tree::node_at

    The innermost node that contains offset, or None when the root does not contain it. A doc comment's offset gives the doc node.

    Tree::node_path

    fn Tree::node_path(self : Tree, n : NodeRef) -> NodePath?

    The path of n from the root, or None for a node not in the tree.

    Tree::parent

    fn Tree::parent(self : Tree, n : NodeRef) -> NodeRef?

    The parent of n, or None for the root and for a node not in the tree.

    Tree::path

    fn Tree::path(self : Tree, n : NodeRef) -> Array[NodeRef]

    The nodes from the root down to n, n included (a new array).

    Tree::root

    fn Tree::root(self : Tree) -> NodeRef

    The root: the file node.

    Tree::step

    fn Tree::step(self : Tree, n : NodeRef) -> PathStep?

    The field and index of n in its parent, or None for the root.

    Tree::tokens_in

    The tokens that lie inside span, in order (a new array; tokens and their trivia are read-only).

    TreeCursor

    pub struct TreeCursor {
    // private fields
    }

    A cursor that the caller moves through the tree, as in tree-sitter. Each goto_* returns false and leaves the cursor in place when there is nowhere to go. The cursor keeps its own stack of levels, so it never recurses.

    test {
    let r = @parser.parse_text("namespace a\n\nstring S\n")
    let c = @traversal.TreeCursor::new(@traversal.NodeRef::of_result(r).unwrap())
    inspect(c.goto_first_child(), content="true")
    debug_inspect(c.field_name(), content="Some(\"namespace\")")
    inspect(c.goto_next_sibling(), content="true")
    inspect(c.path(), content="shapes[0]")
    inspect(c.goto_next_sibling(), content="false")
    }

    TreeCursor::copy

    fn TreeCursor::copy(self : TreeCursor) -> TreeCursor

    An independent cursor at the same place.

    TreeCursor::depth

    fn TreeCursor::depth(self : TreeCursor) -> Int

    The number of levels below the start node.

    TreeCursor::field_name

    fn TreeCursor::field_name(self : TreeCursor) -> String?

    The field of the node in its parent; None at the start node.

    TreeCursor::goto_first_child

    fn TreeCursor::goto_first_child(self : TreeCursor) -> Bool

    Move to the first child.

    TreeCursor::goto_first_child_for

    fn TreeCursor::goto_first_child_for(self : TreeCursor, offset :
    Offset
    ) -> Bool

    Move to the innermost child that contains offset, or else to the first child that starts after it. A plain loop of this call can go one node too far when that node has children after the offset; use goto_node_at to reach the node at an offset.

    TreeCursor::goto_last_child

    fn TreeCursor::goto_last_child(self : TreeCursor) -> Bool

    Move to the last child.

    TreeCursor::goto_next_sibling

    fn TreeCursor::goto_next_sibling(self : TreeCursor) -> Bool

    Move to the next sibling.

    TreeCursor::goto_node_at

    fn TreeCursor::goto_node_at(self : TreeCursor, offset :
    Offset
    ) -> Bool

    Move to the innermost node below the cursor's node that contains offset, the rule of Tree::node_at. false, and no move, when the cursor's node does not contain it.

    TreeCursor::goto_parent

    fn TreeCursor::goto_parent(self : TreeCursor) -> Bool

    Move to the parent; false at the start node.

    TreeCursor::goto_previous_sibling

    fn TreeCursor::goto_previous_sibling(self : TreeCursor) -> Bool

    Move to the previous sibling.

    TreeCursor::new

    fn TreeCursor::new(root : NodeRef) -> TreeCursor

    A cursor at root, its start node.

    TreeCursor::node

    fn TreeCursor::node(self : TreeCursor) -> NodeRef

    The node at the cursor.

    TreeCursor::path

    fn TreeCursor::path(self : TreeCursor) -> NodePath

    The path of the node from the start node.

    TreeCursor::reset

    fn TreeCursor::reset(self : TreeCursor, node : NodeRef) -> Unit

    Move the cursor to node, as a new start node.

    accept

    fn[V : Visitor] accept(root : NodeRef, visitor : V) -> Unit

    Walk root and call one Visitor method per node by its category, and leave after its children, in walk's order.

    A visitor is a type of yours with impl Visitor for the methods it needs; its own fields hold its state.

    fold

    fn[A] fold(root : NodeRef, init : A, enter : (A, NodeRef) -> (A, Control), leave? : (A, NodeRef) -> A) -> A

    Thread an accumulator through a walk of root: enter gets the accumulator and the node and returns the new accumulator and the Control for the node; leave gets the accumulator after the node's children. The calls come in walk's order.

    test {
    let r = @parser.parse_text("namespace a\n\nstring S\n")
    let root = @traversal.NodeRef::of_result(r).unwrap()
    let names = @traversal.fold(root, [], (acc, n) => {
    if n is Name(name) {
    acc.push(name.value)
    }
    (acc, Continue)
    })
    debug_inspect(names, content="[\"a\", \"S\"]")
    }

    kind_table

    fn kind_table() -> Array[(String, String, Array[String])]

    Every node type: (category, kind, fields), one row per kind. Kinds and fields follow the Smithy JSON AST where it has a name, and the IDL grammar for the rest.

    test {
    let rows = @traversal.kind_table()
    let row = rows.filter(r => r.0 == "member" && r.1 == "member")[0]
    debug_inspect(
    row.2,
    content="[\"docs\", \"traits\", \"name\", \"target\", \"default\"]",
    )
    // `boolean` is a shape kind and a value kind.
    let booleans = rows.filter(r => r.1 == "boolean").map(r => r.0)
    debug_inspect(booleans, content="[\"shape\", \"value\"]")
    }

    push_events

    fn[S : EventSource, H : Handler] push_events(source : S, handler : H) -> Unit

    Read every event of source and call handler: on_enter for an Enter (SkipChildren calls source.skip_children(), Stop ends the run) and on_leave for a Leave. A handler is a type of yours with impl Handler for the methods it needs.

    reduce

    fn[R] reduce(root : NodeRef, f : (NodeRef, ArrayView[R]) -> R) -> R

    Reduce root bottom up: f gets each node with the results of its children, in children() order, so index i matches children_with_fields()[i]. The calls to f come in the order of walk's leave calls, and every node is reduced.

    test {
    let r = @parser.parse_text(
    (
    #|namespace a
    #|
    #|structure Foo {
    #| a: String
    #|}
    #|
    ),
    )
    let root = @traversal.NodeRef::of_result(r).unwrap()
    let outline = @traversal.reduce(root, (n, rs) => {
    if rs.length() == 0 {
    n.kind()
    } else {
    n.kind() + "(" + rs.iter().join(",") + ")"
    }
    })
    inspect(
    outline,
    content="file(namespace(name),structure(name,member(name,shape_id)))",
    )
    }

    reduce_pruned

    fn[R] reduce_pruned(root : NodeRef, enter : (NodeRef) -> Control, f : (NodeRef, ArrayView[R]) -> R) -> R?

    reduce, steered as walk is: enter runs before a node's children. Continue reduces the children; SkipChildren does not enter them and calls f with no results for that node; Stop ends the run with no further calls, and the result is None. With Continue throughout it is Some of what reduce gives.

    walk

    fn walk(root : NodeRef, enter : (NodeRef) -> Control, leave? : (NodeRef) -> Unit) -> Unit

    Visit root and every node below it in pre-order (source order). enter runs when a node is reached and decides what happens next; leave runs after all of a node's children, with the same node object. An explicit stack, not recursion, holds the open nodes, so trees of any depth work on every target. All traversal state belongs to this call.

    test {
    let r = @parser.parse_text(
    (
    #|namespace a
    #|
    #|@t
    #|string S
    #|
    ),
    )
    let root = @traversal.NodeRef::of_result(r).unwrap()
    // Log enter and leave; skip the children of the trait.
    let log = []
    @traversal.walk(
    root,
    n => {
    log.push("+" + n.category())
    if n.category() == "trait" {
    SkipChildren
    } else {
    Continue
    }
    },
    leave=n => log.push("-" + n.category()),
    )
    inspect(
    log.join(" "),
    content="+file +namespace +name -name -namespace +shape +trait -trait +name -name -shape -file",
    )
    }