Visitor & Fold Guide
Ascribe provides visitor traits and fold operations for both the AST and ASG, enabling type-safe tree traversal without manual recursion. All fold operations use trampolining (scala.util.control.TailCalls) for stack safety.
AstVisitor
The AstVisitor[A] trait defines methods for each AST node type, with hierarchical defaults:
trait AstVisitor[A]:
def visitNode(node: AstNode): A // must implement
def visitDocument(node: Document): A = visitNode(node)
def visitBlock(node: Block): A = visitNode(node)
def visitInline(node: Inline): A = visitNode(node)
def visitHeading(node: Heading): A = visitBlock(node)
def visitParagraph(node: Paragraph): A = visitBlock(node)
def visitSection(node: Section): A = visitBlock(node)
def visitText(node: Text): A = visitInline(node)
def visitBold(node: Bold): A = visitInline(node)
def visitTable(node: Table): A = visitBlock(node)
def visitTableRow(node: TableRow): A = visitNode(node)
def visitTableCell(node: TableCell): A = visitNode(node)
// ... etc.
Override only the methods you need. Unoverridden methods delegate up the hierarchy: visitParagraph calls visitBlock, which calls visitNode.
Dispatching
Use AstVisitor.visit or the extension method:
val result = myNode.visit(myVisitor)
// or
val result = AstVisitor.visit(myNode, myVisitor)
AsgVisitor
The AsgVisitor[A] trait follows the same pattern for ASG nodes:
trait AsgVisitor[A]:
def visitNode(node: Node): A
def visitDocument(node: Document): A = visitNode(node)
def visitBlock(node: Block): A = visitNode(node)
def visitInline(node: Inline): A = visitNode(node)
def visitParagraph(node: Paragraph): A = visitBlock(node)
def visitTable(node: Table): A = visitBlock(node)
def visitTableRow(node: TableRow): A = visitBlock(node)
def visitTableCell(node: TableCell): A = visitBlock(node)
def visitSpan(node: Span): A = visitInline(node)
def visitText(node: Text): A = visitInline(node)
// ... covers all 28+ ASG node types
Fold Operations
Both AstVisitor and AsgVisitor companion objects provide fold operations. These are stack-safe via trampolining.
foldLeft (Pre-Order)
Visits each node before its children, accumulating left-to-right:
val totalNodes = AstVisitor.foldLeft(doc)(0)((count, _) => count + 1)
// or via extension method:
val totalNodes = doc.foldLeft(0)((count, _) => count + 1)
foldRight (Post-Order)
Visits children before their parent, accumulating right-to-left:
val result = doc.foldRight(List.empty[String]) { (node, acc) =>
node match
case t: Text => t.content :: acc
case _ => acc
}
fold
Alias for foldLeft:
val count = doc.fold(0)((n, _) => n + 1)
collect
Collects values from all nodes matching a partial function (pre-order):
// Extract all text content from an AST document
val texts = AstVisitor.collect(doc) {
case t: Text => t.content
}
// Returns: List[String]
collectPostOrder
Same as collect but in post-order:
val texts = doc.collectPostOrder {
case t: Text => t.content
}
count
Count all nodes in a subtree:
val nodeCount = doc.count
Extension Methods
All operations are available as extension methods on AstNode and Node:
import io.eleven19.ascribe.ast.*
val doc: Document = ???
doc.visit(myVisitor)
doc.foldLeft(init)(f)
doc.foldRight(init)(f)
doc.fold(init)(f)
doc.children // List[AstNode] -- direct children
doc.collect(pf)
doc.collectPostOrder(pf)
doc.count
The same extensions exist for ASG Node:
import io.eleven19.ascribe.asg.*
val asgDoc: Document = ???
asgDoc.visit(myAsgVisitor)
asgDoc.foldLeft(init)(f)
asgDoc.children // Chunk[Node] -- direct children
asgDoc.collect(pf)
asgDoc.count
Practical Examples
Extract all text from a document
val allText = doc.collect { case t: ast.Text => t.content }
println(allText.mkString(" "))
Count paragraphs
val paragraphCount = doc.foldLeft(0) { (count, node) =>
node match
case _: ast.Paragraph => count + 1
case _ => count
}
Find all headings with their levels
val headings = doc.collect {
case h: ast.Heading => (h.level, h.title.collect { case t: ast.Text => t.content }.mkString)
}
// Returns: List[(Int, String)]
Custom visitor: collect section titles from ASG
val sectionTitles = new AsgVisitor[Option[String]]:
def visitNode(node: Node): Option[String] = None
override def visitSection(node: Section): Option[String] =
node.title.map(_.collect { case t: Text => t.value }.mkString)
asgDoc.children.flatMap(child => child.visit(sectionTitles))