Skip to content

Commit 8734e00

Browse files
committed
WIP
1 parent 51f5cc9 commit 8734e00

File tree

12 files changed

+714
-1
lines changed

12 files changed

+714
-1
lines changed

lib/volt/compiled.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defmodule Volt.Compiled do
2+
defstruct []
3+
end

lib/volt/compiler.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
defmodule Volt.Compiler do
2+
def compile(dsl) do
3+
render_expr = Volt.Info.volt_render!(dsl)
4+
5+
with {:ok, expr} <- Volt.Compiler.Passes.Inline.run(render_expr, dsl),
6+
{:ok, expr} <- Volt.Compiler.Passes.IsolateLeafs.run(expr, dsl),
7+
{:ok, expr} <- Volt.Compiler.Passes.MarkRecompute.run(expr, dsl) do
8+
{:ok, expr} |> IO.inspect()
9+
%Volt.Compiled{}
10+
else
11+
{:error, error} -> {:error, error}
12+
end
13+
end
14+
end

lib/volt/compiler/leaf.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defmodule Volt.Compiler.Leaf do
2+
defstruct [:expr]
3+
end

lib/volt/compiler/passes/inline.ex

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
defmodule Volt.Compiler.Passes.Inline do
2+
@moduledoc false
3+
4+
def run(expr, _dsl) do
5+
{:ok, inline(expr)}
6+
end
7+
8+
defp inline(expr) do
9+
Macro.postwalk(expr, fn
10+
{:volt, _meta, [component | rest]} = node ->
11+
props_kw = List.first(rest) || []
12+
13+
case try_inline(component, props_kw) do
14+
{:ok, inlined} -> inline(inlined)
15+
:skip -> node
16+
end
17+
18+
other ->
19+
other
20+
end)
21+
end
22+
23+
defp try_inline(component, props_kw) do
24+
with {:ok, module} <- resolve_module(component),
25+
true <- props_only?(module) do
26+
render_expr = Volt.Info.volt_render!(module)
27+
prop_defs = Volt.Info.props(module)
28+
prop_values = build_prop_values(prop_defs, props_kw)
29+
{:ok, substitute_props(render_expr, prop_values)}
30+
else
31+
_ -> :skip
32+
end
33+
end
34+
35+
defp resolve_module({:__aliases__, _, parts}) do
36+
module = Module.concat(parts)
37+
38+
case Code.ensure_loaded(module) do
39+
{:module, ^module} -> {:ok, module}
40+
_ -> :error
41+
end
42+
end
43+
44+
defp resolve_module(module) when is_atom(module) do
45+
case Code.ensure_loaded(module) do
46+
{:module, ^module} -> {:ok, module}
47+
_ -> :error
48+
end
49+
end
50+
51+
defp resolve_module(_), do: :error
52+
53+
defp props_only?(module) do
54+
function_exported?(module, :spark_dsl_config, 0) &&
55+
Volt.Info.data(module) == [] &&
56+
Volt.Info.props(module) != []
57+
end
58+
59+
defp build_prop_values(prop_defs, props_kw) do
60+
Map.new(prop_defs, fn prop ->
61+
value = Keyword.get_lazy(props_kw, prop.name, fn -> Macro.escape(prop.default) end)
62+
{prop.name, value}
63+
end)
64+
end
65+
66+
defp substitute_props(expr, prop_values) do
67+
Macro.prewalk(expr, fn
68+
{:@, _meta, [{name, _, ctx}]} when is_atom(name) and is_atom(ctx) ->
69+
case Map.fetch(prop_values, name) do
70+
{:ok, value} -> value
71+
:error -> {:@, [], [{name, [], ctx}]}
72+
end
73+
74+
other ->
75+
other
76+
end)
77+
end
78+
end
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
defmodule Volt.Compiler.Passes.IsolateLeafs do
2+
alias Volt.Compiler.Leaf
3+
4+
def run(expr, dsl) do
5+
has_data = Volt.Info.data(dsl) != []
6+
has_props = Volt.Info.props(dsl) != []
7+
8+
if has_data or has_props do
9+
case isolate_leafs(expr) do
10+
{expr, true} -> {:ok, %Leaf{expr: expr}}
11+
{expr, false} -> {:ok, expr}
12+
end
13+
else
14+
{:ok, %Leaf{expr: expr}}
15+
end
16+
end
17+
18+
defp isolate_leafs({:@, _meta, _} = expr) do
19+
{expr, false}
20+
end
21+
22+
defp isolate_leafs({:volt, _meta, _} = expr) do
23+
# TODO: inline static volts? Optimize somehow?
24+
{expr, false}
25+
end
26+
27+
defp isolate_leafs({fun, meta, args}) when is_list(args) do
28+
{tagged, all_leafs} = reduce_tagged(args)
29+
30+
case all_leafs do
31+
true -> {%Leaf{expr: {fun, meta, args}}, true}
32+
false -> {{fun, meta, unwrap_tags(tagged)}, false}
33+
end
34+
end
35+
36+
defp isolate_leafs(list) when is_list(list) do
37+
{tagged, all_leafs} = reduce_tagged(list)
38+
39+
case all_leafs do
40+
true -> {%Leaf{expr: list}, true}
41+
false -> {unwrap_tags(tagged), false}
42+
end
43+
end
44+
45+
defp isolate_leafs(tuple) when is_tuple(tuple) do
46+
case isolate_leafs(Tuple.to_list(tuple)) do
47+
{_list, true} -> {%Leaf{expr: tuple}, true}
48+
{list, false} -> {List.to_tuple(list), false}
49+
end
50+
end
51+
52+
defp isolate_leafs(other), do: {other, true}
53+
54+
defp reduce_tagged(items) do
55+
Enum.reduce(items, {[], true}, fn item, {acc, all_leafs} ->
56+
case isolate_leafs(item) do
57+
{expr, true} -> {[{expr, true} | acc], all_leafs}
58+
{expr, false} -> {[{expr, false} | acc], false}
59+
end
60+
end)
61+
|> then(fn {tagged, all_leafs} -> {Enum.reverse(tagged), all_leafs} end)
62+
end
63+
64+
defp unwrap_tags(tagged) do
65+
Enum.map(tagged, fn
66+
{%Leaf{} = leaf, true} -> leaf
67+
{expr, true} -> %Leaf{expr: expr}
68+
{expr, false} -> expr
69+
end)
70+
end
71+
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
defmodule Volt.Compiler.Passes.MarkRecompute do
2+
@moduledoc false
3+
alias Volt.Compiler.{Leaf, Recompute}
4+
5+
def run(expr, dsl) do
6+
prop_names = dsl |> Volt.Info.props() |> MapSet.new(& &1.name)
7+
8+
state_names =
9+
dsl
10+
|> Volt.Info.data()
11+
|> Enum.filter(&is_struct(&1, Volt.Dsl.State))
12+
|> MapSet.new(& &1.name)
13+
14+
recompute_names = MapSet.union(prop_names, state_names)
15+
16+
{result, _assigns} = mark(expr, recompute_names)
17+
{:ok, result}
18+
end
19+
20+
defp mark(%Leaf{} = leaf, _names), do: {leaf, []}
21+
22+
defp mark(%Recompute{} = r, _names), do: {r, r.assigns}
23+
24+
# volt() calls are component boundaries — don't wrap in Recompute,
25+
# but poison parent nodes as dynamic
26+
defp mark({:volt, _meta, _} = expr, _names), do: {expr, [:__dynamic__]}
27+
28+
defp mark({:@, _meta, [{name, _, ctx}]} = expr, names) when is_atom(name) and is_atom(ctx) do
29+
if MapSet.member?(names, name) do
30+
{%Recompute{assigns: [name], expr: expr}, [name]}
31+
else
32+
# pipe ref — not a recompute trigger, but poisons parent as dynamic
33+
{expr, [:__dynamic__]}
34+
end
35+
end
36+
37+
defp mark({fun, meta, args}, names) when is_list(args) do
38+
{new_args, assigns} = mark_children(args, names)
39+
wrap_if_dynamic({fun, meta, new_args}, assigns)
40+
end
41+
42+
defp mark(list, names) when is_list(list) do
43+
{new_items, assigns} = mark_children(list, names)
44+
wrap_if_dynamic(new_items, assigns)
45+
end
46+
47+
defp mark(tuple, names) when is_tuple(tuple) do
48+
{new_list, assigns} = mark_children(Tuple.to_list(tuple), names)
49+
wrap_if_dynamic(List.to_tuple(new_list), assigns)
50+
end
51+
52+
defp mark(other, _names), do: {other, []}
53+
54+
defp mark_children(items, names) do
55+
Enum.map_reduce(items, [], fn item, acc ->
56+
{new_item, assigns} = mark(item, names)
57+
{new_item, acc ++ assigns}
58+
end)
59+
end
60+
61+
defp wrap_if_dynamic(expr, []), do: {expr, []}
62+
63+
defp wrap_if_dynamic(expr, assigns) do
64+
real_assigns = assigns |> Enum.reject(&(&1 == :__dynamic__)) |> Enum.uniq()
65+
66+
case real_assigns do
67+
[] ->
68+
# dynamic (has volt/pipe children) but no recompute triggers
69+
{expr, assigns}
70+
71+
_ ->
72+
{%Recompute{assigns: real_assigns, expr: expr}, assigns}
73+
end
74+
end
75+
end

lib/volt/compiler/recompute.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
defmodule Volt.Compiler.Recompute do
2+
@moduledoc false
3+
defstruct [:assigns, :expr]
4+
end

lib/volt/transformers/compile_render.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ defmodule Volt.Transformers.CompileRender do
55
alias Spark.Dsl.Transformer
66

77
def transform(dsl) do
8-
render_expr = Transformer.get_option(dsl, [:volt], :render)
8+
dsl |> Volt.Compiler.compile() |> IO.inspect()
9+
render_expr = Volt.Info.volt_render!(dsl)
910

1011
if render_expr do
1112
transformed = transform_assigns(render_expr)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule Volt.Test.Greeting do
2+
use Volt.Component
3+
4+
props do
5+
prop(:name, :string, default: "World")
6+
end
7+
8+
render("Hello, " <> @name)
9+
end
10+
11+
defmodule Volt.Test.Wrapper do
12+
use Volt.Component
13+
14+
props do
15+
prop(:content, :string, default: "")
16+
end
17+
18+
render(%{wrapped: @content})
19+
end
20+
21+
defmodule Volt.Test.WithState do
22+
use Volt.Component
23+
24+
data do
25+
state(:x, :integer, default: 0)
26+
end
27+
28+
render(%{x: @x})
29+
end
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
defmodule Volt.Compiler.Passes.InlineTest do
2+
use ExUnit.Case
3+
4+
alias Volt.Compiler.Passes.Inline
5+
6+
describe "props-only components" do
7+
test "inlines render expression with prop substitution" do
8+
expr = {:volt, [], [Volt.Test.Greeting, [name: "Zach"]]}
9+
{:ok, result} = Inline.run(expr, nil)
10+
11+
# Should be "Hello, " <> "Zach"
12+
assert {:<>, _, ["Hello, ", "Zach"]} = result
13+
end
14+
15+
test "uses default when prop not provided" do
16+
expr = {:volt, [], [Volt.Test.Greeting]}
17+
{:ok, result} = Inline.run(expr, nil)
18+
19+
# Should be "Hello, " <> "World"
20+
assert {:<>, _, ["Hello, ", "World"]} = result
21+
end
22+
23+
test "substitutes AST expressions for props" do
24+
expr = {:volt, [], [Volt.Test.Greeting, [name: {:@, [], [{:x, [], nil}]}]]}
25+
{:ok, result} = Inline.run(expr, nil)
26+
27+
# Should be "Hello, " <> @x
28+
assert {:<>, _, ["Hello, ", {:@, _, [{:x, _, _}]}]} = result
29+
end
30+
31+
test "inlines wrapper component" do
32+
expr = {:volt, [], [Volt.Test.Wrapper, [content: "inner"]]}
33+
{:ok, result} = Inline.run(expr, nil)
34+
35+
# Should be %{wrapped: "inner"}
36+
assert {:%{}, _, [{:wrapped, "inner"}]} = result
37+
end
38+
end
39+
40+
describe "components with data are NOT inlined" do
41+
test "volt with state is left as-is" do
42+
expr = {:volt, [], [Volt.Test.WithState]}
43+
{:ok, result} = Inline.run(expr, nil)
44+
45+
assert {:volt, _, [Volt.Test.WithState]} = result
46+
end
47+
end
48+
49+
describe "unresolvable modules are skipped" do
50+
test "unknown module is left as-is" do
51+
expr = {:volt, [], [{:__aliases__, [], [:NoSuchModule]}, [name: "hi"]]}
52+
{:ok, result} = Inline.run(expr, nil)
53+
54+
assert {:volt, _, _} = result
55+
end
56+
end
57+
58+
describe "nested inlining" do
59+
test "recursively inlines nested props-only volts" do
60+
# volt(Wrapper, content: volt(Greeting, name: "Zach"))
61+
inner = {:volt, [], [Volt.Test.Greeting, [name: "Zach"]]}
62+
expr = {:volt, [], [Volt.Test.Wrapper, [content: inner]]}
63+
{:ok, result} = Inline.run(expr, nil)
64+
65+
# Wrapper inlines to %{wrapped: @content}
66+
# @content was volt(Greeting, name: "Zach") which inlines to "Hello, " <> "Zach"
67+
assert {:%{}, _, [{:wrapped, {:<>, _, ["Hello, ", "Zach"]}}]} = result
68+
end
69+
end
70+
71+
describe "mixed with non-inlineable content" do
72+
test "inlines only props-only volts, leaves others" do
73+
# [volt(Greeting, name: "A"), volt(WithState)]
74+
expr = [
75+
{:volt, [], [Volt.Test.Greeting, [name: "A"]]},
76+
{:volt, [], [Volt.Test.WithState]}
77+
]
78+
79+
{:ok, result} = Inline.run(expr, nil)
80+
81+
assert [{:<>, _, ["Hello, ", "A"]}, {:volt, _, [Volt.Test.WithState]}] = result
82+
end
83+
end
84+
end

0 commit comments

Comments
 (0)