> Ideally it would be nice to support something like scala.util.parsing.input.Position, where the push action would automatically set the Position on any value object that includes that trait. Then an AST tree could look something like:
>
> sealed trait Expr extends Position
> case class Add(left: Expr, right: Expr) extends Expr
> case class Subtract(left: Expr, right: Expr) extends Expr
>
> And the end user could query position without worrying about setting it manually from each rule action.
As this proposal introduces mutability into cases classes it is not something that I’d recommend.
Why not go for fully immutable AST nodes?:
case class Position(index: Int, line: Int, column: Int)
sealed trait Expr {
def pos: Position
}
case class Add(pos: Position, left: Expr, right: Expr) extends Expr
case class Subtract(pos: Position, left: Expr, right: Expr) extends Expr
> Extending Positional was just one idea on implementation, but even something as straightforward as a Map of AST nodes -> offset they were created, maintained by the Parser, would be helpful. It gets very repetitive to attach actions to every rule to poll the cursor position and attach to created AST nodes.
Have you tried building such a map of AST nodes -> Position?
It shouldn’t require more than a few lines of code from your side to get it done.
I don’t see why you’d need to access internal methods for any of this.
Can you show your code?
I’d recommend staying within the bounds of the value stack even for things like position tracking.
The type safety the DSL provides comes with very little cost, so there should be no real reason not to rely on it.
One way of managing the position map might be something like this (untested):
var nodes = Map.empty[AstNode, Position]
def someAstNode = rule {
startAstNode ~ … /* match AST node syntax */ … ~> MyAstNode ~ endAstNode
}
def startAstNode = rule { push(cursor) }
def endAstNode[A <: AstNode]: Rule[Int :: A :: HNil, A :: HNil] = rule {
run { (start: Int, node: A) =>
val pos = Position(start, cursor)
nodes = nodes.updated(node, pos)
node
}
}
Ideally you’d be able to factor out the startAstNode / endAstNode wrapping into something like an `astRule` helper, so you can say:
def someAstNode = astRule {
/* match AST node syntax */ … ~> MyAstNode
}
instead of
def someAstNode = rule {
startAstNode ~ … /* match AST node syntax */ … ~> MyAstNode ~ endAstNode
}
However, that requires that `astRule` is implemented as a macro and thus cannot live in the same compilation unit as the code using it, which is unfortunate.
We are still thinking about a good solution to this common problem of wanting to write custom rule transformation logic (which is what this is actually about).