diff --git a/lib/farmbot/celery_script/ast/heap.ex b/lib/farmbot/celery_script/ast/heap.ex new file mode 100644 index 00000000..a30e0b27 --- /dev/null +++ b/lib/farmbot/celery_script/ast/heap.ex @@ -0,0 +1,96 @@ +defmodule Farmbot.CeleryScript.AST.Heap do + @moduledoc """ + A heap-ish data structure required when converting canonical CeleryScript AST + nodes into the Flat IR form. + This data structure is useful because it addresses each node in the + CeleryScript tree via a unique numerical index, rather than using mutable + references. + MORE INFO: https://github.com/FarmBot-Labs/Celery-Slicer + """ + alias Farmbot.CeleryScript.AST + alias AST.Heap + + defmodule Address do + @moduledoc "Address on the heap." + + defstruct [:value] + + @doc "New heap address." + def new(num) when is_integer(num) do + %__MODULE__{value: num} + end + + @doc "Increment an address." + def inc(%__MODULE__{value: num}) do + %__MODULE__{value: num + 1} + end + + @doc "Decrement an address." + def dec(%__MODULE__{value: num}) do + %__MODULE__{value: num - 1} + end + + defimpl Inspect, for: __MODULE__ do + def inspect(%{value: val}, _) do + "Address(#{val})" + end + end + end + + @link "__" + @parent String.to_atom(@link <> "parent" <> @link) + @body String.to_atom(@link <> "body" <> @link) + @next String.to_atom(@link <> "next" <> @link) + @kind :__kind__ + + @primary_fields [@parent, @body, @kind, @next] + + @null Address.new(0) + @nothing %{ + @kind => AST.Node.Nothing, + @parent => @null, + @body => @null, + @next => @null + } + + defstruct [:entries, :here] + + def new do + %{struct(Heap) | here: @null, entries: %{@null => @nothing}} + end + + def alot(%Heap{} = heap, kind) do + here_plus_one = Address.inc(heap.here) + new_entries = Map.put(heap.entries, here_plus_one, %{@kind => kind}) + %{heap | here: here_plus_one, entries: new_entries} + end + + def put(%Heap{here: addr} = heap, key, value) do + put(heap, addr, key, value) + end + + def put(%Heap{} = heap, %Address{} = addr, key, value) do + block = heap[addr] || raise "Bad node address: #{inspect addr}" + new_block = Map.put(block, String.to_atom(to_string(key)), value) + new_entries = Map.put(heap.entries, addr, new_block) + %{heap | entries: new_entries} + end + + @doc "Gets the values of the heap entries." + def values(%Heap{entries: entries}), do: Enum.map(entries, &elem(&1, 1)) + + @doc false + def fetch(%Heap{} = heap, %Address{} = addr), do: Map.fetch(heap.entries, addr) + + def link, do: @link + def parent, do: @parent + def body, do: @body + def next, do: @next + def kind, do: @kind + def primary_fields, do: @primary_fields + def null, do: @null + + @compile inline: [ + link: 0, parent: 0, body: 0, next: 0, kind: 0, primary_fields: 0, null: 0 + ] +end diff --git a/lib/farmbot/celery_script/ast/slicer.ex b/lib/farmbot/celery_script/ast/slicer.ex new file mode 100644 index 00000000..fe8e9997 --- /dev/null +++ b/lib/farmbot/celery_script/ast/slicer.ex @@ -0,0 +1,59 @@ +defmodule Farmbot.CeleryScript.AST.Slicer do + @moduledoc """ + ORIGINAL IMPLEMENTATION HERE: https://github.com/FarmBot-Labs/Celery-Slicer + Take a nested ("canonical") representation of a CeleryScript sequence and + transofrms it to a flat/homogenous intermediate representation which is better + suited for storage in a relation database. + """ + alias Farmbot.CeleryScript.AST + alias AST.Heap + alias AST.Heap.Address + + def run(%AST{} = canonical) do + Heap.new() + |> allocate(canonical, Heap.null()) + |> Map.update(Heap.body(), Heap.null(), fn(x) -> Map.get(x, Heap.body()) end) + |> Map.update(Heap.next(), Heap.null(), fn(x) -> Map.get(x, Heap.next()) end) + |> Heap.values() + end + + def allocate(%Heap{} = heap, %AST{} = node, %Address{} = parent_addr) do + heap + |> Heap.alot(node.kind) + |> Heap.put(Heap.parent(), parent_addr) # puts "here" + |> iterate_over_body(node) + |> iterate_over_args(node) + end + + def iterate_over_args(%Heap{here: %Address{} = parent_addr} = heap, %AST{} = canonical_node) do + keys = Map.keys(canonical_node.args) + Enum.reduce(keys, heap, fn(key, %Heap{} = heap) -> + case canonical_node.args[key] do + %AST{} = another_node -> + k = Heap.link <> to_string(key) + new_heap = heap |> allocate(another_node, parent_addr) + Heap.put(new_heap, parent_addr, k, new_heap.here) + val -> + Heap.put(heap, parent_addr, key, val) + end + end) + end + + def iterate_over_body(%Heap{} = heap, %AST{} = canonical_node) do + recurse_into_body(heap, canonical_node.body) + end + + def recurse_into_body(heap, body, index \\ 0) + def recurse_into_body(%Heap{here: %Address{} = previous_address} = heap, [body_item | rest], index) do + %Heap{here: my_heap_address} = heap = allocate(heap, body_item, previous_address) + is_head? = index == 0 + prev_next_key = if is_head?, do: Heap.null(), else: my_heap_address + prev_body_key = if is_head?, do: my_heap_address, else: Heap.null() + heap + |> Heap.put(previous_address, Heap.next(), prev_next_key) + |> Heap.put(previous_address, Heap.body(), prev_body_key) + |> recurse_into_body(rest, index + 1) + end + + def recurse_into_body(heap, [], _), do: heap +end diff --git a/test/farmbot/celery_script/ast/slicer_test.ex b/test/farmbot/celery_script/ast/slicer_test.ex new file mode 100644 index 00000000..f952d970 --- /dev/null +++ b/test/farmbot/celery_script/ast/slicer_test.ex @@ -0,0 +1,7 @@ +defmodule Farmbot.CeleryScript.AST.SlicerTest do + use ExUnit.Case + alias Farmbot.CeleryScript.AST + alias AST.Slicer + + +end