From bba107e3e39f8a4786319d55174c649010248396 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 14 Jun 2024 10:36:58 -0400 Subject: [PATCH 1/7] improvement: track `supertree`, and add `Zipper.all_the_way_up/1` --- .tool-versions | 1 + lib/sourceror/zipper.ex | 78 ++++++++++++++++++++++++----------------- test/zipper_test.exs | 7 ++++ 3 files changed, 53 insertions(+), 33 deletions(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..bfd7c27 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +elixir 1.16.3 diff --git a/lib/sourceror/zipper.ex b/lib/sourceror/zipper.ex index e171ffe..c0a6972 100644 --- a/lib/sourceror/zipper.ex +++ b/lib/sourceror/zipper.ex @@ -26,11 +26,12 @@ defmodule Sourceror.Zipper do alias Sourceror.Zipper, as: Z - defstruct [:node, :path] + defstruct [:node, :path, :supertree] @type t :: %Z{ node: tree, - path: path | nil + path: path | nil, + supertree: t | nil } @opaque path :: %{ @@ -41,10 +42,10 @@ defmodule Sourceror.Zipper do @type tree :: Macro.t() - @compile {:inline, new: 1, new: 2} + @compile {:inline, new: 1, new: 3} defp new(node), do: %Z{node: node} - defp new(node, nil), do: %Z{node: node} - defp new(node, %{left: _, parent: _, right: _} = path), do: %Z{node: node, path: path} + defp new(node, nil, supertree), do: %Z{node: node, supertree: supertree && top(supertree)} + defp new(node, %{left: _, parent: _, right: _} = path, supertree), do: %Z{node: node, path: path, supertree: supertree && top(supertree)} @spec branch?(tree) :: boolean def branch?({_, _, args}) when is_list(args), do: true @@ -81,12 +82,22 @@ defmodule Sourceror.Zipper do def zip(node), do: new(node) @doc """ - Walks the `zipper` all the way up and returns the top `zipper`. + Walks the `zipper` to the top of the current subtree and returns that `zipper`. """ @spec top(t) :: t def top(%Z{path: nil} = zipper), do: zipper def top(zipper), do: zipper |> up() |> top() + @doc """ + Walks the `zipper` all the way up, breaking out of any subtrees and returns the top-most `zipper`. + """ + @spec all_the_way_up(t) :: t + def all_the_way_up(%Z{supertree: supertree}) when not is_nil(supertree) do + all_the_way_up(supertree) + end + def all_the_way_up(%Z{path: nil} = zipper), do: zipper + def all_the_way_up(zipper), do: zipper |> up() |> top() + @doc """ Walks the `zipper` all the way up and returns the root `node`. """ @@ -104,12 +115,12 @@ defmodule Sourceror.Zipper do `nil` if there's no children. """ @spec down(t) :: t | nil - def down(%Z{node: tree} = zipper) do + def down(%Z{node: tree, supertree: supertree} = zipper) do case children(tree) do nil -> nil [] -> nil - [first] -> new(first, %{parent: zipper, left: nil, right: nil}) - [first | rest] -> new(first, %{parent: zipper, left: nil, right: rest}) + [first] -> new(first, %{parent: zipper, left: nil, right: nil}, supertree) + [first | rest] -> new(first, %{parent: zipper, left: nil, right: rest}, supertree) end end @@ -120,10 +131,10 @@ defmodule Sourceror.Zipper do @spec up(t) :: t | nil def up(%Z{path: nil}), do: nil - def up(%Z{node: tree, path: path}) do + def up(%Z{node: tree, path: path, supertree: supertree}) do children = Enum.reverse(path.left || []) ++ [tree] ++ (path.right || []) %Z{node: parent, path: parent_path} = path.parent - new(make_node(parent, children), parent_path) + new(make_node(parent, children), parent_path, supertree) end @doc """ @@ -133,8 +144,8 @@ defmodule Sourceror.Zipper do @spec left(t) :: t | nil def left(zipper) - def left(%Z{node: tree, path: %{left: [ltree | l], right: r} = path}), - do: new(ltree, %{path | left: l, right: [tree | r || []]}) + def left(%Z{node: tree, path: %{left: [ltree | l], right: r} = path, supertree: supertree}), + do: new(ltree, %{path | left: l, right: [tree | r || []]}, supertree) def left(_), do: nil @@ -142,10 +153,10 @@ defmodule Sourceror.Zipper do Returns the leftmost sibling of the `node` at this `zipper`, or itself. """ @spec leftmost(t) :: t - def leftmost(%Z{node: tree, path: %{left: [_ | _] = l} = path}) do + def leftmost(%Z{node: tree, path: %{left: [_ | _] = l} = path, supertree: supertree}) do [left | rest] = Enum.reverse(l) r = rest ++ [tree] ++ (path.right || []) - new(left, %{path | left: nil, right: r}) + new(left, %{path | left: nil, right: r}, supertree) end def leftmost(zipper), do: zipper @@ -157,8 +168,8 @@ defmodule Sourceror.Zipper do @spec right(t) :: t | nil def right(zipper) - def right(%Z{node: tree, path: %{right: [rtree | r]} = path}), - do: new(rtree, %{path | right: r, left: [tree | path.left || []]}) + def right(%Z{node: tree, path: %{right: [rtree | r]} = path, supertree: supertree}), + do: new(rtree, %{path | right: r, left: [tree | path.left || []]}, supertree) def right(_), do: nil @@ -166,10 +177,10 @@ defmodule Sourceror.Zipper do Returns the rightmost sibling of the `node` at this `zipper`, or itself. """ @spec rightmost(t) :: t - def rightmost(%Z{node: tree, path: %{right: [_ | _] = r} = path}) do + def rightmost(%Z{node: tree, path: %{right: [_ | _] = r} = path, supertree: supertree}) do [right | rest] = Enum.reverse(r) l = rest ++ [tree] ++ (path.left || []) - new(right, %{path | left: l, right: nil}) + new(right, %{path | left: l, right: nil}, supertree) end def rightmost(zipper), do: zipper @@ -199,12 +210,12 @@ defmodule Sourceror.Zipper do def remove(%Z{path: nil}), do: raise(ArgumentError, message: "Cannot remove the top level node.") - def remove(%Z{path: path} = zipper) do + def remove(%Z{path: path, supertree: supertree} = zipper) do case path.left do [{:__block__, meta, [name]} = left | rest] when is_reserved_block_name(name) -> if meta[:format] == :keyword do left - |> new(%{path | left: rest}) + |> new(%{path | left: rest}, supertree) |> do_prev() else zipper @@ -214,7 +225,7 @@ defmodule Sourceror.Zipper do [left | rest] -> left - |> new(%{path | left: rest}) + |> new(%{path | left: rest}, supertree) |> do_prev() _ -> @@ -223,7 +234,7 @@ defmodule Sourceror.Zipper do parent |> make_node(children) - |> new(parent_path) + |> new(parent_path, supertree) end end @@ -238,8 +249,8 @@ defmodule Sourceror.Zipper do def insert_left(%Z{path: nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.") - def insert_left(%Z{node: tree, path: path}, child) do - new(tree, %{path | left: [child | path.left || []]}) + def insert_left(%Z{node: tree, path: path, supertree: supertree}, child) do + new(tree, %{path | left: [child | path.left || []]}, supertree) end @doc """ @@ -253,18 +264,18 @@ defmodule Sourceror.Zipper do def insert_right(%Z{path: nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.") - def insert_right(%Z{node: tree, path: path}, child) do - new(tree, %{path | right: [child | path.right || []]}) + def insert_right(%Z{node: tree, path: path, supertree: supertree}, child) do + new(tree, %{path | right: [child | path.right || []]}, supertree) end @doc """ Inserts the `child` as the leftmost `child` of the `node` at this `zipper`, without moving. """ - def insert_child(%Z{node: tree, path: path}, child) do + def insert_child(%Z{node: tree, path: path, supertree: supertree}, child) do tree |> do_insert_child(child) - |> new(path) + |> new(path, supertree) end defp do_insert_child(list, child) when is_list(list), do: [child | list] @@ -278,10 +289,10 @@ defmodule Sourceror.Zipper do Inserts the `child` as the rightmost `child` of the `node` at this `zipper`, without moving. """ - def append_child(%Z{node: tree, path: path}, child) do + def append_child(%Z{node: tree, path: path, supertree: supertree}, child) do tree |> do_append_child(child) - |> new(path) + |> new(path, supertree) end defp do_append_child(list, child) when is_list(list), do: list ++ [child] @@ -453,7 +464,8 @@ defmodule Sourceror.Zipper do end @compile {:inline, into: 2} - defp into(%Z{path: nil} = zipper, %Z{path: path}), do: %{zipper | path: path} + defp into(zipper, nil), do: zipper + defp into(%Z{path: nil} = zipper, %Z{path: path, supertree: supertree}), do: %{zipper | path: path, supertree: supertree} @doc """ Returns a `zipper` to the `node` that satisfies the `predicate` function, or @@ -486,7 +498,7 @@ defmodule Sourceror.Zipper do """ @spec subtree(t) :: t @compile {:inline, subtree: 1} - def subtree(%Z{} = zipper), do: %{zipper | path: nil} + def subtree(%Z{supertree: supertree} = zipper), do: %{zipper | path: nil, supertree: into(top(zipper), supertree)} @doc """ Runs the function `fun` on the subtree of the currently focused `node` and diff --git a/test/zipper_test.exs b/test/zipper_test.exs index 941badc..11d7ec8 100644 --- a/test/zipper_test.exs +++ b/test/zipper_test.exs @@ -410,6 +410,13 @@ defmodule SourcerorTest.ZipperTest do end end + describe "all_the_way_up/1" do + test "returns the top zipper, breaking out of subtrees" do + assert Z.zip([1, [2, [3, 4]]]) |> Z.next() |> Z.next() |> Z.next() |> Z.subtree() |> Z.all_the_way_up() == + %Z{node: [1, [2, [3, 4]]]} + end + end + describe "root/1" do test "returns the root node" do assert Z.zip([1, [2, [3, 4]]]) |> Z.next() |> Z.next() |> Z.next() |> Z.root() == From b686b8c4a9927b6dd95f3cb5cb5efd270b7bd483 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 14 Jun 2024 10:44:03 -0400 Subject: [PATCH 2/7] chore: format --- lib/sourceror/zipper.ex | 12 +++++++++--- test/zipper_test.exs | 7 ++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/sourceror/zipper.ex b/lib/sourceror/zipper.ex index c0a6972..3a60a4f 100644 --- a/lib/sourceror/zipper.ex +++ b/lib/sourceror/zipper.ex @@ -45,7 +45,9 @@ defmodule Sourceror.Zipper do @compile {:inline, new: 1, new: 3} defp new(node), do: %Z{node: node} defp new(node, nil, supertree), do: %Z{node: node, supertree: supertree && top(supertree)} - defp new(node, %{left: _, parent: _, right: _} = path, supertree), do: %Z{node: node, path: path, supertree: supertree && top(supertree)} + + defp new(node, %{left: _, parent: _, right: _} = path, supertree), + do: %Z{node: node, path: path, supertree: supertree && top(supertree)} @spec branch?(tree) :: boolean def branch?({_, _, args}) when is_list(args), do: true @@ -95,6 +97,7 @@ defmodule Sourceror.Zipper do def all_the_way_up(%Z{supertree: supertree}) when not is_nil(supertree) do all_the_way_up(supertree) end + def all_the_way_up(%Z{path: nil} = zipper), do: zipper def all_the_way_up(zipper), do: zipper |> up() |> top() @@ -465,7 +468,9 @@ defmodule Sourceror.Zipper do @compile {:inline, into: 2} defp into(zipper, nil), do: zipper - defp into(%Z{path: nil} = zipper, %Z{path: path, supertree: supertree}), do: %{zipper | path: path, supertree: supertree} + + defp into(%Z{path: nil} = zipper, %Z{path: path, supertree: supertree}), + do: %{zipper | path: path, supertree: supertree} @doc """ Returns a `zipper` to the `node` that satisfies the `predicate` function, or @@ -498,7 +503,8 @@ defmodule Sourceror.Zipper do """ @spec subtree(t) :: t @compile {:inline, subtree: 1} - def subtree(%Z{supertree: supertree} = zipper), do: %{zipper | path: nil, supertree: into(top(zipper), supertree)} + def subtree(%Z{supertree: supertree} = zipper), + do: %{zipper | path: nil, supertree: into(top(zipper), supertree)} @doc """ Runs the function `fun` on the subtree of the currently focused `node` and diff --git a/test/zipper_test.exs b/test/zipper_test.exs index 11d7ec8..4ad0ba9 100644 --- a/test/zipper_test.exs +++ b/test/zipper_test.exs @@ -412,7 +412,12 @@ defmodule SourcerorTest.ZipperTest do describe "all_the_way_up/1" do test "returns the top zipper, breaking out of subtrees" do - assert Z.zip([1, [2, [3, 4]]]) |> Z.next() |> Z.next() |> Z.next() |> Z.subtree() |> Z.all_the_way_up() == + assert Z.zip([1, [2, [3, 4]]]) + |> Z.next() + |> Z.next() + |> Z.next() + |> Z.subtree() + |> Z.all_the_way_up() == %Z{node: [1, [2, [3, 4]]]} end end From a213f42866fd8f44dc2beb6403106baad01f83bc Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 14 Jun 2024 11:26:42 -0400 Subject: [PATCH 3/7] chore: small simplifications of `supertree` logic. No need to go to top --- lib/sourceror/zipper.ex | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/sourceror/zipper.ex b/lib/sourceror/zipper.ex index 3a60a4f..a28d09c 100644 --- a/lib/sourceror/zipper.ex +++ b/lib/sourceror/zipper.ex @@ -44,10 +44,10 @@ defmodule Sourceror.Zipper do @compile {:inline, new: 1, new: 3} defp new(node), do: %Z{node: node} - defp new(node, nil, supertree), do: %Z{node: node, supertree: supertree && top(supertree)} + defp new(node, nil, supertree), do: %Z{node: node, supertree: supertree} defp new(node, %{left: _, parent: _, right: _} = path, supertree), - do: %Z{node: node, path: path, supertree: supertree && top(supertree)} + do: %Z{node: node, path: path, supertree: supertree} @spec branch?(tree) :: boolean def branch?({_, _, args}) when is_list(args), do: true @@ -98,8 +98,7 @@ defmodule Sourceror.Zipper do all_the_way_up(supertree) end - def all_the_way_up(%Z{path: nil} = zipper), do: zipper - def all_the_way_up(zipper), do: zipper |> up() |> top() + def all_the_way_up(zipper), do: top(zipper) @doc """ Walks the `zipper` all the way up and returns the root `node`. @@ -503,8 +502,8 @@ defmodule Sourceror.Zipper do """ @spec subtree(t) :: t @compile {:inline, subtree: 1} - def subtree(%Z{supertree: supertree} = zipper), - do: %{zipper | path: nil, supertree: into(top(zipper), supertree)} + def subtree(%Z{} = zipper), + do: %{zipper | path: nil, supertree: zipper} @doc """ Runs the function `fun` on the subtree of the currently focused `node` and From deda30d1f227862bcaf6fbbf406b19df255c775a Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 14 Jun 2024 11:38:13 -0400 Subject: [PATCH 4/7] fix: join the zipper w/ its parent when going all the way up --- lib/sourceror/zipper.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sourceror/zipper.ex b/lib/sourceror/zipper.ex index a28d09c..92e031b 100644 --- a/lib/sourceror/zipper.ex +++ b/lib/sourceror/zipper.ex @@ -94,8 +94,8 @@ defmodule Sourceror.Zipper do Walks the `zipper` all the way up, breaking out of any subtrees and returns the top-most `zipper`. """ @spec all_the_way_up(t) :: t - def all_the_way_up(%Z{supertree: supertree}) when not is_nil(supertree) do - all_the_way_up(supertree) + def all_the_way_up(%Z{supertree: supertree} = zipper) when not is_nil(supertree) do + all_the_way_up(into(zipper, supertree)) end def all_the_way_up(zipper), do: top(zipper) From 9b9bbfc3c8284f750c144b4382b4a483c49c5e4b Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 14 Jun 2024 11:38:41 -0400 Subject: [PATCH 5/7] fix: go to top of zipper before joining to parent --- lib/sourceror/zipper.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sourceror/zipper.ex b/lib/sourceror/zipper.ex index 92e031b..ed87a42 100644 --- a/lib/sourceror/zipper.ex +++ b/lib/sourceror/zipper.ex @@ -95,7 +95,7 @@ defmodule Sourceror.Zipper do """ @spec all_the_way_up(t) :: t def all_the_way_up(%Z{supertree: supertree} = zipper) when not is_nil(supertree) do - all_the_way_up(into(zipper, supertree)) + all_the_way_up(into(top(zipper), supertree)) end def all_the_way_up(zipper), do: top(zipper) From 8dd30d775ce8917b11c9da3bba1e0bb0adf8f73c Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 14 Jun 2024 13:56:40 -0400 Subject: [PATCH 6/7] chore: remove .tool-versions --- .tool-versions | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index bfd7c27..0000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -elixir 1.16.3 From 3b4788b95b694a599b1c7084a10afc3630a68bf2 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 14 Jun 2024 13:59:51 -0400 Subject: [PATCH 7/7] improvement: add `topmost` to replace `all_the_way_up` and `topmost_root` --- lib/sourceror/zipper.ex | 18 ++++++++++++------ test/zipper_test.exs | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/sourceror/zipper.ex b/lib/sourceror/zipper.ex index ed87a42..58d69bc 100644 --- a/lib/sourceror/zipper.ex +++ b/lib/sourceror/zipper.ex @@ -91,21 +91,27 @@ defmodule Sourceror.Zipper do def top(zipper), do: zipper |> up() |> top() @doc """ - Walks the `zipper` all the way up, breaking out of any subtrees and returns the top-most `zipper`. + Walks the `zipper` to the topmost node, breaking out of any subtrees and returns the top-most `zipper`. """ - @spec all_the_way_up(t) :: t - def all_the_way_up(%Z{supertree: supertree} = zipper) when not is_nil(supertree) do - all_the_way_up(into(top(zipper), supertree)) + @spec topmost(t) :: t + def topmost(%Z{supertree: supertree} = zipper) when not is_nil(supertree) do + topmost(into(top(zipper), supertree)) end - def all_the_way_up(zipper), do: top(zipper) + def topmost(zipper), do: top(zipper) @doc """ - Walks the `zipper` all the way up and returns the root `node`. + Walks the `zipper` to the top of the current subtree and returns the that `node`. """ @spec root(t) :: tree def root(zipper), do: zipper |> top() |> node() + @doc """ + Walks the `zipper` to the topmost node, breaking out of any subtrees and returns the root `node`. + """ + @spec topmost_root(t) :: tree + def topmost_root(zipper), do: zipper |> topmost() |> node() + @doc """ Returns the `node` at the `zipper`. """ diff --git a/test/zipper_test.exs b/test/zipper_test.exs index 4ad0ba9..f67e2fb 100644 --- a/test/zipper_test.exs +++ b/test/zipper_test.exs @@ -410,18 +410,30 @@ defmodule SourcerorTest.ZipperTest do end end - describe "all_the_way_up/1" do + describe "topmost/1" do test "returns the top zipper, breaking out of subtrees" do assert Z.zip([1, [2, [3, 4]]]) |> Z.next() |> Z.next() |> Z.next() |> Z.subtree() - |> Z.all_the_way_up() == + |> Z.topmost() == %Z{node: [1, [2, [3, 4]]]} end end + describe "topmost_root/1" do + test "returns the top zipper's node, breaking out of subtrees" do + assert Z.zip([1, [2, [3, 4]]]) + |> Z.next() + |> Z.next() + |> Z.next() + |> Z.subtree() + |> Z.topmost_root() == + [1, [2, [3, 4]]] + end + end + describe "root/1" do test "returns the root node" do assert Z.zip([1, [2, [3, 4]]]) |> Z.next() |> Z.next() |> Z.next() |> Z.root() ==