Dear Alchemists,
Assume I have an AST fragment with some unbound variables:
iex(1)> expr = quote do: IO.puts(x)
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],
[{:x, [], Elixir}]}
what I want to archive is to find all unbound variables there, so have something like:
iex> unbound_vars(expr)
[:x]
or at least find out if there are any:
iex> has_unbound_vars(expr)
true
The easiest thing would be to eval the expression and catch the CompileError
:
iex(2)> Code.eval_quoted expr
warning: variable "x" does not exist and is being expanded to "x()", please use parentheses to remove the ambiguity or change the variable name
nofile:1
** (CompileError) nofile:1: undefined function x/0
(elixir) lib/code.ex:493: Code.eval_quoted/3
But it does not look very idiomatic, and also generates a warning, which is a big deal for me, as this will be used in a library (drab), so the warning may confuse the library users as it will appear during the normal usage.
I am thinking about Macro.prewalk
:
- collect all variables from
{atom, _, Elixir}
- collect pattern matches
{:=, _, [left, _]}
and take all variables ({atom, _, Elixir}
) fromleft
- compare those lists
But I am not sure it is enough, I have a feeling I am missing something. Is the pattern match the only way to bind a variable? Are the variables always are like {atom, _, Elixir}
?
Background, or I Need Your Opinion On The Approach
This is not an academic issue In Drab, I collect the expressions from the template, which contain Phoenix assigns (<%= function(@assign1)%>
) during the compile time, and re-evaluate them in case user wants to change it on the page, live, with poke socket, assign1: new_value
.
It works well in the most cases, but crashes when using local variables defined outside of the block:
<%= for u <- @users do %>
<%= if u != @user do %>
<%= u %> <br>
<% end %>
<% end %>
All works OK when you re-evaluate the for
expression with poke socket, users: [...]
. But when trying to poke socket, user: ...
system tries to re-evaluate only the if
expression and it finished with CompileError
.
This may be worked around with poking both assigns in the same time with poke users: [...], user: ...
(in this case Drab knows that there is no point to refresh if
, as the whole block will be re-evaluated), but it is not very convenient. And confusing the users. And does not solve all the cases.
Iâve developed the alternative, much easier approach, to archive the same: instead of evaluating this partial expressions, on each poke
Drab renders the whole Phoenix template (using update assigns), parses the html and pushes the fragment to the browser. This is much better from compatibility point of view, but way slower. For example, on Drab demo page, which is already using this approach, all poke
s are now about 50ms slower than it was before. You can feel it in the counter example.
So, in my opinion, the best would be to combine those two approaches. When the expression has some unbound variables (we can check it in the compile time for better runtime performance), Drab would render the whole template. Otherwise, use the faster way.
Or maybe Iâm wrong, and 50ms overhead of rendering template on each poke
is not a big deal? In this case I could remove probably about 50% of Drab.Live.EExEngine
code, making it way more stable and reliable.
The second approach simply removes all Drab.Live limitations. This is why I really like it. But the first is faster. HELP!
Oh No! Another Approach!
BTW while writing this long (sorry!) post, I realized that I could use the custom EEx marker to let the developer choose the approach! So there would be three markers:
-
<%= expression %>
would work with safe, slow but compatible way - re-render the whole template -
<%| expression %>
the fast way -
<%/ expression %>
do not drab this expression
Thus, when you know what youâre doing, you may choose the faster way, but by default it would just work without any limitations. Drab is made also for beginners, and I want it to me the most intuitive as it is possible. But if you know how it works, you may try to choose better performance and write your code differently.