Rework the walk module a bit (#4707)

* start a rework of the walk module

I'd like to have the code explicitly recurse in the visitor rather than
implicitly. This allows the calling code to build up an idea of the
depth of each node, or skip parts of the AST entirely.

i'm going to rebuild this enough to get the old API to work before
looking to merge this.

I've also discovered a number of new AST types that were just papered
over and/or excluded entirely -- I'm going to have to go through and
make sure that both Digest and the walker are still current, or if we
silently added types or, more likely, fields getting ignored.
This commit is contained in:
Paul Tagliamonte
2024-12-09 19:23:53 -05:00
committed by GitHub
parent 667500d1b9
commit eac7b83504
6 changed files with 318 additions and 321 deletions

View File

@ -441,8 +441,11 @@ impl Backend {
let token_modifiers_bitset = if let Some(ast) = self.ast_map.get(params.uri.as_str()) {
let token_index = Arc::new(Mutex::new(token_type_index));
let modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
crate::walk::walk(&ast, &|node: crate::walk::Node| {
let node_range: SourceRange = (&node).into();
crate::walk::walk(&ast, |node: crate::walk::Node| {
let Ok(node_range): Result<SourceRange, _> = (&node).try_into() else {
return Ok(true);
};
if !node_range.contains(source_range.start()) {
return Ok(true);
}

View File

@ -184,7 +184,7 @@ impl Node<Program> {
/// Walk the ast and get all the variables and tags as completion items.
pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> {
let completions = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, &|node: crate::walk::Node<'a>| {
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
let mut findings = completions.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
match node {
crate::walk::Node::TagDeclarator(tag) => {
@ -195,7 +195,7 @@ impl Node<Program> {
}
_ => {}
}
Ok(true)
Ok::<bool, anyhow::Error>(true)
})?;
let x = completions.lock().unwrap();
Ok(x.clone())
@ -204,7 +204,7 @@ impl Node<Program> {
/// Returns all the lsp symbols in the program.
pub fn get_lsp_symbols<'a>(&'a self, code: &str) -> Result<Vec<DocumentSymbol>> {
let symbols = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, &|node: crate::walk::Node<'a>| {
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
let mut findings = symbols.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
match node {
crate::walk::Node::TagDeclarator(tag) => {
@ -215,7 +215,7 @@ impl Node<Program> {
}
_ => {}
}
Ok(true)
Ok::<bool, anyhow::Error>(true)
})?;
let x = symbols.lock().unwrap();
Ok(x.clone())
@ -227,10 +227,10 @@ impl Node<Program> {
RuleT: crate::lint::Rule<'a>,
{
let v = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, &|node: crate::walk::Node<'a>| {
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
findings.append(&mut rule.check(node)?);
Ok(true)
Ok::<bool, anyhow::Error>(true)
})?;
let x = v.lock().unwrap();
Ok(x.clone())

View File

@ -5,7 +5,7 @@ use crate::{
/// The "Node" type wraps all the AST elements we're able to find in a KCL
/// file. Tokens we walk through will be one of these.
#[derive(Clone, Debug)]
#[derive(Copy, Clone, Debug)]
pub enum Node<'a> {
Program(NodeRef<'a, types::Program>),
@ -31,6 +31,7 @@ pub enum Node<'a> {
MemberExpression(NodeRef<'a, types::MemberExpression>),
UnaryExpression(NodeRef<'a, types::UnaryExpression>),
IfExpression(NodeRef<'a, types::IfExpression>),
ElseIf(&'a types::ElseIf),
Parameter(&'a types::Parameter),
@ -38,11 +39,22 @@ pub enum Node<'a> {
MemberObject(&'a types::MemberObject),
LiteralIdentifier(&'a types::LiteralIdentifier),
KclNone(&'a types::KclNone),
}
impl From<&Node<'_>> for SourceRange {
fn from(node: &Node) -> Self {
match node {
/// Returned during source_range conversion.
#[derive(Debug)]
pub enum AstNodeError {
/// Returned if we try and [SourceRange] a [types::KclNone].
NoSourceForAKclNone,
}
impl TryFrom<&Node<'_>> for SourceRange {
type Error = AstNodeError;
fn try_from(node: &Node) -> Result<Self, Self::Error> {
Ok(match node {
Node::Program(n) => SourceRange::from(*n),
Node::ImportStatement(n) => SourceRange::from(*n),
Node::ExpressionStatement(n) => SourceRange::from(*n),
@ -68,6 +80,62 @@ impl From<&Node<'_>> for SourceRange {
Node::MemberObject(m) => SourceRange::new(m.start(), m.end(), m.module_id()),
Node::IfExpression(n) => SourceRange::from(*n),
Node::LiteralIdentifier(l) => SourceRange::new(l.start(), l.end(), l.module_id()),
// This is broken too
Node::ElseIf(n) => SourceRange::new(n.cond.start(), n.cond.end(), n.cond.module_id()),
// The KclNone type here isn't an actual node, so it has no
// start/end information.
Node::KclNone(_) => return Err(Self::Error::NoSourceForAKclNone),
})
}
}
impl<'tree> From<&'tree types::BodyItem> for Node<'tree> {
fn from(node: &'tree types::BodyItem) -> Self {
match node {
types::BodyItem::ImportStatement(v) => v.as_ref().into(),
types::BodyItem::ExpressionStatement(v) => v.into(),
types::BodyItem::VariableDeclaration(v) => v.as_ref().into(),
types::BodyItem::ReturnStatement(v) => v.into(),
}
}
}
impl<'tree> From<&'tree types::Expr> for Node<'tree> {
fn from(node: &'tree types::Expr) -> Self {
match node {
types::Expr::Literal(lit) => lit.as_ref().into(),
types::Expr::TagDeclarator(tag) => tag.as_ref().into(),
types::Expr::Identifier(id) => id.as_ref().into(),
types::Expr::BinaryExpression(be) => be.as_ref().into(),
types::Expr::FunctionExpression(fe) => fe.as_ref().into(),
types::Expr::CallExpression(ce) => ce.as_ref().into(),
types::Expr::CallExpressionKw(ce) => ce.as_ref().into(),
types::Expr::PipeExpression(pe) => pe.as_ref().into(),
types::Expr::PipeSubstitution(ps) => ps.as_ref().into(),
types::Expr::ArrayExpression(ae) => ae.as_ref().into(),
types::Expr::ArrayRangeExpression(are) => are.as_ref().into(),
types::Expr::ObjectExpression(oe) => oe.as_ref().into(),
types::Expr::MemberExpression(me) => me.as_ref().into(),
types::Expr::UnaryExpression(ue) => ue.as_ref().into(),
types::Expr::IfExpression(e) => e.as_ref().into(),
types::Expr::None(n) => n.into(),
}
}
}
impl<'tree> From<&'tree types::BinaryPart> for Node<'tree> {
fn from(node: &'tree types::BinaryPart) -> Self {
match node {
types::BinaryPart::Literal(lit) => lit.as_ref().into(),
types::BinaryPart::Identifier(id) => id.as_ref().into(),
types::BinaryPart::BinaryExpression(be) => be.as_ref().into(),
types::BinaryPart::CallExpression(ce) => ce.as_ref().into(),
types::BinaryPart::CallExpressionKw(ce) => ce.as_ref().into(),
types::BinaryPart::UnaryExpression(ue) => ue.as_ref().into(),
types::BinaryPart::MemberExpression(me) => me.as_ref().into(),
types::BinaryPart::IfExpression(e) => e.as_ref().into(),
}
}
}
@ -116,4 +184,6 @@ impl_from!(Node, ObjectProperty);
impl_from_ref!(Node, Parameter);
impl_from_ref!(Node, MemberObject);
impl_from!(Node, IfExpression);
impl_from!(Node, ElseIf);
impl_from_ref!(Node, LiteralIdentifier);
impl_from!(Node, KclNone);

View File

@ -0,0 +1,197 @@
use anyhow::Result;
use crate::walk::Node;
/// Walk-specific trait adding the ability to traverse the KCL AST.
///
/// This trait is implemented on [Node] to handle the fairly tricky bit of
/// recursing into the AST in a single place, as well as helpers for traversing
/// the tree. for callers to use.
pub trait Visitable<'tree> {
/// Return a `Vec<Node>` for all *direct* children of this AST node. This
/// should only contain direct descendants.
fn children(&self) -> Vec<Node<'tree>>;
/// Return `self` as a [Node]. Generally speaking, the [Visitable] trait
/// is only going to be implemented on [Node], so this is purely used by
/// helpers that are generic over a [Visitable] and want to deref back
/// into a [Node].
fn node(&self) -> Node<'tree>;
/// Call the provided [Visitor] in order to Visit `self`. This will
/// only be called on `self` -- the [Visitor] is responsible for
/// recursing into any children, if desired.
fn visit<VisitorT>(&self, visitor: VisitorT) -> Result<bool, VisitorT::Error>
where
VisitorT: Visitor<'tree>,
{
visitor.visit_node(self.node())
}
}
/// Trait used to enable visiting members of KCL AST.
///
/// Implementing this trait enables the implementer to be invoked over
/// members of KCL AST by using the [Visitable::visit] function on
/// a [Node].
pub trait Visitor<'tree> {
/// Error type returned by the [Self::visit] function.
type Error;
/// Visit a KCL AST [Node].
///
/// In general, implementers likely wish to check to see if a Node is what
/// they're looking for, and either descend into that [Node]'s children (by
/// calling [Visitable::children] on [Node] to get children nodes,
/// calling [Visitable::visit] on each node of interest), or perform
/// some action.
fn visit_node(&self, node: Node<'tree>) -> Result<bool, Self::Error>;
}
impl<'a, FnT, ErrorT> Visitor<'a> for FnT
where
FnT: Fn(Node<'a>) -> Result<bool, ErrorT>,
{
type Error = ErrorT;
fn visit_node(&self, n: Node<'a>) -> Result<bool, ErrorT> {
self(n)
}
}
impl<'tree> Visitable<'tree> for Node<'tree> {
fn node(&self) -> Node<'tree> {
*self
}
fn children(&self) -> Vec<Node<'tree>> {
match self {
Node::Program(n) => n.body.iter().map(|node| node.into()).collect(),
Node::ExpressionStatement(n) => {
vec![(&n.expression).into()]
}
Node::BinaryExpression(n) => {
vec![(&n.left).into(), (&n.right).into()]
}
Node::FunctionExpression(n) => {
let mut children = n.params.iter().map(|v| v.into()).collect::<Vec<Node>>();
children.push((&n.body).into());
children
}
Node::CallExpression(n) => {
let mut children = n.arguments.iter().map(|v| v.into()).collect::<Vec<Node>>();
children.insert(0, (&n.callee).into());
children
}
Node::CallExpressionKw(n) => {
let mut children = n.unlabeled.iter().map(|v| v.into()).collect::<Vec<Node>>();
// TODO: this is wrong but it's what the old walk code was doing.
// We likely need a real LabeledArg AST node, but I don't
// want to tango with it since it's a lot deeper than
// adding it to the enum.
children.extend(n.arguments.iter().map(|v| (&v.arg).into()).collect::<Vec<Node>>());
children
}
Node::PipeExpression(n) => n.body.iter().map(|v| v.into()).collect(),
Node::ArrayExpression(n) => n.elements.iter().map(|v| v.into()).collect(),
Node::ArrayRangeExpression(n) => {
vec![(&n.start_element).into(), (&n.end_element).into()]
}
Node::ObjectExpression(n) => n.properties.iter().map(|v| v.into()).collect(),
Node::MemberExpression(n) => {
vec![(&n.object).into(), (&n.property).into()]
}
Node::IfExpression(n) => {
let mut children = n.else_ifs.iter().map(|v| v.into()).collect::<Vec<Node>>();
children.insert(0, n.cond.as_ref().into());
children.push(n.final_else.as_ref().into());
children
}
Node::VariableDeclaration(n) => vec![(&n.declaration).into()],
Node::ReturnStatement(n) => {
vec![(&n.argument).into()]
}
Node::VariableDeclarator(n) => {
vec![(&n.id).into(), (&n.init).into()]
}
Node::UnaryExpression(n) => {
vec![(&n.argument).into()]
}
Node::Parameter(n) => {
vec![(&n.identifier).into()]
}
Node::ObjectProperty(n) => {
vec![(&n.value).into()]
}
Node::ElseIf(n) => {
vec![(&n.cond).into(), n.then_val.as_ref().into()]
}
Node::PipeSubstitution(_)
| Node::TagDeclarator(_)
| Node::Identifier(_)
| Node::ImportStatement(_)
| Node::MemberObject(_)
| Node::LiteralIdentifier(_)
| Node::KclNone(_)
| Node::Literal(_) => vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
macro_rules! kcl {
( $kcl:expr ) => {{
$crate::parsing::top_level_parse($kcl).unwrap()
}};
}
#[test]
fn count_crows() {
let program = kcl!(
"\
const crow1 = 1
const crow2 = 2
fn crow3() {
const crow4 = 3
crow5()
}
"
);
#[derive(Debug, Default)]
struct CountCrows {
n: Box<Mutex<usize>>,
}
impl<'tree> Visitor<'tree> for &CountCrows {
type Error = ();
fn visit_node(&self, node: Node<'tree>) -> Result<bool, Self::Error> {
if let Node::VariableDeclarator(vd) = node {
if vd.id.name.starts_with("crow") {
*self.n.lock().unwrap() += 1;
}
}
for child in node.children().iter() {
if !child.visit(*self)? {
return Ok(false);
}
}
Ok(true)
}
}
let prog: Node = (&program).into();
let count_crows: CountCrows = Default::default();
Visitable::visit(&prog, &count_crows).unwrap();
assert_eq!(*count_crows.n.lock().unwrap(), 4);
}
}

View File

@ -1,329 +1,55 @@
use anyhow::Result;
use super::ast_visitor::{Visitable, Visitor};
use crate::{
parsing::ast::types::{
BinaryPart, BodyItem, Expr, IfExpression, LiteralIdentifier, MemberExpression, MemberObject, NodeRef,
ObjectExpression, ObjectProperty, Parameter, Program, UnaryExpression, VariableDeclarator,
},
parsing::ast::types::{NodeRef, Program},
walk::Node,
};
/// Walker is implemented by things that are able to walk an AST tree to
/// produce lints. This trait is implemented automatically for a few of the
/// common types, but can be manually implemented too.
/// *DEPRECATED* Walk trait.
///
/// This was written before [Visitor], which is the better way to traverse
/// a AST.
///
/// This trait continues to exist in order to not change all the linter
/// as we refine the walk code.
///
/// This, internally, uses the new [Visitor] trait, and is only provided as
/// a stub until we migrate all existing code off this trait.
pub trait Walker<'a> {
/// Walk will visit every element of the AST.
/// Walk will visit every element of the AST, recursing through the
/// whole tree.
fn walk(&self, n: Node<'a>) -> Result<bool>;
}
impl<'a, FnT> Walker<'a> for FnT
impl<'tree, VisitorT> Walker<'tree> for VisitorT
where
FnT: Fn(Node<'a>) -> Result<bool>,
VisitorT: Visitor<'tree>,
VisitorT: Clone,
anyhow::Error: From<VisitorT::Error>,
VisitorT::Error: Send,
VisitorT::Error: Sync,
{
fn walk(&self, n: Node<'a>) -> Result<bool> {
self(n)
fn walk(&self, n: Node<'tree>) -> Result<bool> {
if !n.visit(self.clone())? {
return Ok(false);
}
for child in n.children() {
if !Self::walk(self, child)? {
return Ok(false);
}
}
Ok(true)
}
}
/// Run the Walker against all [Node]s in a [Program].
pub fn walk<'a, WalkT>(prog: NodeRef<'a, Program>, f: &WalkT) -> Result<bool>
pub fn walk<'a, WalkT>(prog: NodeRef<'a, Program>, f: WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(prog.into())? {
return Ok(false);
}
for bi in &prog.body {
if !walk_body_item(bi, f)? {
return Ok(false);
}
}
Ok(true)
}
fn walk_variable_declarator<'a, WalkT>(node: NodeRef<'a, VariableDeclarator>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !f.walk((&node.id).into())? {
return Ok(false);
}
walk_value(&node.init, f)
}
fn walk_parameter<'a, WalkT>(node: &'a Parameter, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
f.walk((&node.identifier).into())
}
fn walk_member_object<'a, WalkT>(node: &'a MemberObject, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
f.walk(node.into())
}
fn walk_literal_identifier<'a, WalkT>(node: &'a LiteralIdentifier, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
f.walk(node.into())
}
fn walk_member_expression<'a, WalkT>(node: NodeRef<'a, MemberExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !walk_member_object(&node.object, f)? {
return Ok(false);
}
walk_literal_identifier(&node.property, f)
}
fn walk_binary_part<'a, WalkT>(node: &'a BinaryPart, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
match node {
BinaryPart::Literal(lit) => f.walk(lit.as_ref().into()),
BinaryPart::Identifier(id) => f.walk(id.as_ref().into()),
BinaryPart::BinaryExpression(be) => f.walk(be.as_ref().into()),
BinaryPart::CallExpression(ce) => f.walk(ce.as_ref().into()),
BinaryPart::CallExpressionKw(ce) => f.walk(ce.as_ref().into()),
BinaryPart::UnaryExpression(ue) => walk_unary_expression(ue, f),
BinaryPart::MemberExpression(me) => walk_member_expression(me, f),
BinaryPart::IfExpression(e) => walk_if_expression(e, f),
}
}
// TODO: Rename this to walk_expr
fn walk_value<'a, WalkT>(node: &'a Expr, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
match node {
Expr::Literal(lit) => f.walk(lit.as_ref().into()),
Expr::TagDeclarator(tag) => f.walk(tag.as_ref().into()),
Expr::Identifier(id) => {
// sometimes there's a bare Identifier without a Value::Identifier.
f.walk(id.as_ref().into())
}
Expr::BinaryExpression(be) => {
if !f.walk(be.as_ref().into())? {
return Ok(false);
}
if !walk_binary_part(&be.left, f)? {
return Ok(false);
}
walk_binary_part(&be.right, f)
}
Expr::FunctionExpression(fe) => {
if !f.walk(fe.as_ref().into())? {
return Ok(false);
}
for arg in &fe.params {
if !walk_parameter(arg, f)? {
return Ok(false);
}
}
walk(&fe.body, f)
}
Expr::CallExpression(ce) => {
if !f.walk(ce.as_ref().into())? {
return Ok(false);
}
if !f.walk((&ce.callee).into())? {
return Ok(false);
}
for e in &ce.arguments {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::CallExpressionKw(ce) => {
if !f.walk(ce.as_ref().into())? {
return Ok(false);
}
if !f.walk((&ce.callee).into())? {
return Ok(false);
}
if let Some(ref e) = ce.unlabeled {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
for e in &ce.arguments {
if !walk_value::<WalkT>(&e.arg, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::PipeExpression(pe) => {
if !f.walk(pe.as_ref().into())? {
return Ok(false);
}
for e in &pe.body {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::PipeSubstitution(ps) => f.walk(ps.as_ref().into()),
Expr::ArrayExpression(ae) => {
if !f.walk(ae.as_ref().into())? {
return Ok(false);
}
for e in &ae.elements {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::ArrayRangeExpression(are) => {
if !f.walk(are.as_ref().into())? {
return Ok(false);
}
if !walk_value::<WalkT>(&are.start_element, f)? {
return Ok(false);
}
if !walk_value::<WalkT>(&are.end_element, f)? {
return Ok(false);
}
Ok(true)
}
Expr::ObjectExpression(oe) => walk_object_expression(oe, f),
Expr::MemberExpression(me) => walk_member_expression(me, f),
Expr::UnaryExpression(ue) => walk_unary_expression(ue, f),
Expr::IfExpression(e) => walk_if_expression(e, f),
Expr::None(_) => Ok(true),
}
}
/// Walk through an [ObjectProperty].
fn walk_object_property<'a, WalkT>(node: NodeRef<'a, ObjectProperty>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
walk_value(&node.value, f)
}
/// Walk through an [ObjectExpression].
fn walk_object_expression<'a, WalkT>(node: NodeRef<'a, ObjectExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
for prop in &node.properties {
if !walk_object_property(prop, f)? {
return Ok(false);
}
}
Ok(true)
}
/// Walk through an [IfExpression].
fn walk_if_expression<'a, WalkT>(node: NodeRef<'a, IfExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !walk_value(&node.cond, f)? {
return Ok(false);
}
for else_if in &node.else_ifs {
if !walk_value(&else_if.cond, f)? {
return Ok(false);
}
if !walk(&else_if.then_val, f)? {
return Ok(false);
}
}
let final_else = &(*node.final_else);
if !f.walk(final_else.into())? {
return Ok(false);
}
Ok(true)
}
/// walk through an [UnaryExpression].
fn walk_unary_expression<'a, WalkT>(node: NodeRef<'a, UnaryExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
walk_binary_part(&node.argument, f)
}
/// walk through a [BodyItem].
fn walk_body_item<'a, WalkT>(node: &'a BodyItem, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
// We don't walk a BodyItem since it's an enum itself.
match node {
BodyItem::ImportStatement(xs) => {
if !f.walk(xs.as_ref().into())? {
return Ok(false);
}
Ok(true)
}
BodyItem::ExpressionStatement(xs) => {
if !f.walk(xs.into())? {
return Ok(false);
}
walk_value(&xs.expression, f)
}
BodyItem::VariableDeclaration(vd) => {
if !f.walk(vd.as_ref().into())? {
return Ok(false);
}
walk_variable_declarator(&vd.declaration, f)
}
BodyItem::ReturnStatement(rs) => {
if !f.walk(rs.into())? {
return Ok(false);
}
walk_value(&rs.argument, f)
}
}
let prog: Node = prog.into();
f.walk(prog)
}
#[cfg(test)]
@ -345,10 +71,10 @@ const bar = 2
"
);
walk(&program, &|node| {
walk(&program, |node| {
if let Node::VariableDeclarator(vd) = node {
if vd.id.name == "foo" {
return Ok(false);
return Ok::<bool, anyhow::Error>(false);
}
panic!("walk didn't stop");
}

View File

@ -1,4 +1,5 @@
mod ast_node;
mod ast_visitor;
mod ast_walk;
pub use ast_node::Node;