Skip to content

Commit

Permalink
Wrap JuliaSyntax.GreenNode in Runic.Node
Browse files Browse the repository at this point in the history
Using JuliaSyntax.GreenNode directly have worked for a long time, but at
this point it seems that it is easier to re-package the tree in a custom
type. This will be used to attach metadata to nodes, for example.

Also include the following renames:
 - s/verified_children/verified_kids/
 - s/children/kids/
 - s/children′/kids′/
 - s/children′′/kids′′/
 - s/child/kid/
 - s/child′/kid′/
 - s/child′′/kid′′/
  • Loading branch information
fredrikekre committed Jun 1, 2024
1 parent f4e7d13 commit f56e723
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 327 deletions.
170 changes: 112 additions & 58 deletions src/Runic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,55 @@ using JuliaSyntax:
end
end

# JuliaSyntax extensions and other utilities
# Debug and assert utilities
include("debug.jl")

########
# Node #
########

# This is essentially just a re-packed `JuliaSyntax.GreenNode`.
struct Node
# The next three fields directly match JuliaSyntax.GreenNode. We can not store a
# GreenNode directly because the type of the children vector should be `Vector{Node}`
# and not `Vector{GreenNode}`.
head::JuliaSyntax.SyntaxHead
span::UInt32
kids::Union{Tuple{}, Vector{Node}}
end

# Re-package a GreenNode as a Node
function Node(node::JuliaSyntax.GreenNode)
return Node(
JuliaSyntax.head(node), JuliaSyntax.span(node),
map(Node, JuliaSyntax.children(node)),
)
end

# Defining this allow using many duck-typed methods in JuliaSyntax directly without having
# to re-package the Node as a GreenNode.
JuliaSyntax.head(node::Node) = head(node)
JuliaSyntax.span(node::Node) = span(node)

# Matching JuliaSyntax.(head|span|flags|kind)
head(node::Node) = node.head
span(node::Node) = node.span
flags(node::Node) = JuliaSyntax.flags(node)
kind(node::Node) = JuliaSyntax.kind(node)

function is_leaf(node::Node)
return node.kids === ()
end

# This function must only be be called after verifying that the node is not a leaf. We can
# then type-assert the return value to narrow it down from `Union{Tuple{}, Vector{Node}}` to
# `Vector{Node}`.
function verified_kids(node::Node)
@assert !is_leaf(node)
return node.kids::Vector{Node}
end

# Node utilities and JuliaSyntax extensions
include("chisels.jl")

# Return the result of expr if it doesn't evaluate to `nothing`
Expand All @@ -24,14 +72,18 @@ macro return_something(expr)
end)
end

#######################################################
# Main drivers for traversing and formatting the tree #
#######################################################

mutable struct Context
# Input
@const src_str::String
@const src_tree::JuliaSyntax.GreenNode{JuliaSyntax.SyntaxHead}
@const src_tree::Node
@const src_io::IOBuffer
# Output
@const fmt_io::IOBuffer
fmt_tree::Union{JuliaSyntax.GreenNode{JuliaSyntax.SyntaxHead}, Nothing}
fmt_tree::Union{Node, Nothing}
# User settings
quiet::Bool
verbose::Bool
Expand All @@ -40,18 +92,20 @@ mutable struct Context
check::Bool
diff::Bool
# Current state
# node::Union{JuliaSyntax.GreenNode{JuliaSyntax.SyntaxHead}, Nothing}
prev_sibling::Union{JuliaSyntax.GreenNode{JuliaSyntax.SyntaxHead}, Nothing}
next_sibling::Union{JuliaSyntax.GreenNode{JuliaSyntax.SyntaxHead}, Nothing}
# parent::Union{JuliaSyntax.GreenNode{JuliaSyntax.SyntaxHead}, Nothing}
# node::Union{Node, Nothing}
prev_sibling::Union{Node, Nothing}
next_sibling::Union{Node, Nothing}
# parent::Union{Node, Nothing}
end

function Context(
src_str; assert::Bool = true, debug::Bool = false, verbose::Bool = debug,
diff::Bool = false, check::Bool = false, quiet::Bool = false,
)
src_io = IOBuffer(src_str)
src_tree = JuliaSyntax.parseall(JuliaSyntax.GreenNode, src_str; ignore_warnings = true)
src_tree = Node(
JuliaSyntax.parseall(JuliaSyntax.GreenNode, src_str; ignore_warnings = true),
)
fmt_io = IOBuffer()
fmt_tree = nothing
# Debug mode enforces verbose and assert
Expand All @@ -71,17 +125,17 @@ end
# Read the bytes of the current node from the output io
function read_bytes(ctx, node)
pos = position(ctx.fmt_io)
bytes = read(ctx.fmt_io, JuliaSyntax.span(node))
@assert length(bytes) == JuliaSyntax.span(node)
bytes = read(ctx.fmt_io, span(node))
@assert length(bytes) == span(node)
seek(ctx.fmt_io, pos)
@assert position(ctx.fmt_io) == pos
return bytes
end

function accept_node!(ctx::Context, node::JuliaSyntax.GreenNode)
function accept_node!(ctx::Context, node::Node)
# Accept the string representation of the current node by advancing the
# output IO to the start of the next node
pos = position(ctx.fmt_io) + JuliaSyntax.span(node)
pos = position(ctx.fmt_io) + span(node)
seek(ctx.fmt_io, pos)
return
end
Expand All @@ -93,9 +147,9 @@ end
struct NullNode end
const nullnode = NullNode()

function format_node_with_children!(ctx::Context, node::JuliaSyntax.GreenNode)
# If the node doesn't have children there is nothing to do here
if !JuliaSyntax.haschildren(node)
function format_node_with_kids!(ctx::Context, node::Node)
# If the node doesn't have kids there is nothing to do here
if is_leaf(node)
return nothing
end

Expand All @@ -105,63 +159,63 @@ function format_node_with_children!(ctx::Context, node::JuliaSyntax.GreenNode)
ctx.prev_sibling = nothing
ctx.next_sibling = nothing

# The new node parts. `children′` aliases `children` and only copied below if any of the
# The new node parts. `kids′` aliases `kids` and only copied below if any of the
# nodes change ("copy-on-write").
children = verified_children(node)
children= children
any_child_changed = false

# Loop over all the children
for (i, child) in pairs(children)
# Set the siblings: previous from children′, next from children
ctx.prev_sibling = get(children′, i - 1, nothing)
ctx.next_sibling = get(children, i + 1, nothing)
child= child
this_child_changed = false
kids = verified_kids(node)
kids= kids
any_kid_changed = false

# Loop over all the kids
for (i, kid) in pairs(kids)
# Set the siblings: previous from kids′, next from kids
ctx.prev_sibling = get(kids′, i - 1, nothing)
ctx.next_sibling = get(kids, i + 1, nothing)
kid= kid
this_kid_changed = false
itr = 0
# Loop until this node reaches a steady state and is accepted
while true
# Keep track of the stream position and reset it below if the node is changed
fmt_pos = position(ctx.fmt_io)
# Format the child
child′′ = format_node!(ctx, child′)
if child′′ === nullnode
# Format the kid
kid′′ = format_node!(ctx, kid′)
if kid′′ === nullnode
# This node should be deleted from the tree
# TODO: When this is fixed the sibling setting above needs to be modified to
# handle this too
this_child_changed = true
error("TODO: handle removed children")
elseif child′′ === nothing
this_kid_changed = true
error("TODO: handle removed kids")
elseif kid′′ === nothing
# The node was accepted, continue to next sibling
@assert position(ctx.fmt_io) == fmt_pos + JuliaSyntax.span(child′)
@assert position(ctx.fmt_io) == fmt_pos + span(kid′)
break
else
# The node should be replaced with the new one. Reset the stream and try
# again until it is accepted.
@assert child′′ isa JuliaSyntax.GreenNode
this_child_changed = true
@assert kid′′ isa Node
this_kid_changed = true
seek(ctx.fmt_io, fmt_pos)
child= child′′
kid= kid′′
end
if (itr += 1) == 1000
error("infinite loop?")
end
end
any_child_changed |= this_child_changed
if any_child_changed
# De-alias the children if not already done
if children=== children
children= eltype(children)[children[j] for j in 1:(i - 1)]
any_kid_changed |= this_kid_changed
if any_kid_changed
# De-alias the kids if not already done
if kids=== kids
kids= eltype(kids)[kids[j] for j in 1:(i - 1)]
end
push!(children′, child′)
push!(kids′, kid′)
end
end
# Reset the siblings
ctx.prev_sibling = prev_sibling
ctx.next_sibling = next_sibling
# Return a new node if any of the children changed
if any_child_changed
return make_node(node, children′)
# Return a new node if any of the kids changed
if any_kid_changed
return make_node(node, kids′)
else
return nothing
end
Expand All @@ -175,8 +229,8 @@ Format a node. Return values:
- `nullnode::NullNode`: The node should be deleted from the tree
- `node::JuliaSyntax.GreenNode`: The node should be replaced with the new node
"""
function format_node!(ctx::Context, node::JuliaSyntax.GreenNode)::Union{JuliaSyntax.GreenNode, Nothing, NullNode}
node_kind = JuliaSyntax.kind(node)
function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode}
node_kind = kind(node)

# Go through the runestone and apply transformations.
@return_something trim_trailing_whitespace(ctx, node)
Expand Down Expand Up @@ -226,7 +280,7 @@ function format_node!(ctx::Context, node::JuliaSyntax.GreenNode)::Union{JuliaSyn
node_kind === K"vect"
)
@assert !JuliaSyntax.is_trivia(node)
node′ = format_node_with_children!(ctx, node)
node′ = format_node_with_kids!(ctx, node)
@assert node′ !== nullnode
return node′

Expand Down Expand Up @@ -265,16 +319,16 @@ function format_node!(ctx::Context, node::JuliaSyntax.GreenNode)::Union{JuliaSyn
node_kind === K"where" ||
node_kind === K"while"
)
node′ = format_node_with_children!(ctx, node)
node′ = format_node_with_kids!(ctx, node)
@assert node′ !== nullnode
return node′

# Nodes that should recurse if they have children (all??)
elseif JuliaSyntax.haschildren(node) && (
# Nodes that should recurse if they have kids (all??)
elseif !is_leaf(node) && (
JuliaSyntax.is_operator(node) ||
node_kind === K"else" # try-(catch|finally)-else
)
node′ = format_node_with_children!(ctx, node)
node′ = format_node_with_kids!(ctx, node)
@assert node′ !== nullnode
return node′

Expand Down Expand Up @@ -374,8 +428,8 @@ function format_tree!(ctx::Context)
@assert src_pos == 0
fmt_pos = position(ctx.fmt_io)
@assert fmt_pos == 0
nb = write(ctx.fmt_io, read(ctx.src_io, JuliaSyntax.span(root)))
@assert nb == JuliaSyntax.span(root)
nb = write(ctx.fmt_io, read(ctx.src_io, span(root)))
@assert nb == span(root)
# Reset IOs so that the offsets are correct
seek(ctx.src_io, src_pos)
seek(ctx.fmt_io, fmt_pos)
Expand All @@ -391,10 +445,10 @@ function format_tree!(ctx::Context)
error("root node deleted")
elseif root′′ === nothing
# root′ = root′′
@assert position(ctx.fmt_io) == fmt_pos + JuliaSyntax.span(root′)
@assert position(ctx.fmt_io) == fmt_pos + span(root′)
break
else
@assert root′′ isa JuliaSyntax.GreenNode
@assert root′′ isa Node
# The node was changed, reset the output stream and try again
seek(ctx.fmt_io, fmt_pos)
root′ = root′′
Expand All @@ -405,7 +459,7 @@ function format_tree!(ctx::Context)
end
end
# Truncate the output at the root span
truncate(ctx.fmt_io, JuliaSyntax.span(root′))
truncate(ctx.fmt_io, span(root′))
# Set the final tree
ctx.fmt_tree = root′
return nothing
Expand Down
Loading

0 comments on commit f56e723

Please sign in to comment.