Current section
Files
Jump to
Current section
Files
lib/exun.ex
defmodule Exun do
alias Exun.Cyclic
alias Exun.Collect
alias Exun.Eq
alias Exun.UI
def debug() do
eval "$x/x,x"
end
@moduledoc """
Symbolic Math for Elixir, with Units support
"""
@doc ~S"""
Parse a math expression, not a 'equality', with context definitions
For example, express 'x' squared meters, and then define x to be 3.
```
iex> Exun.parse( "x[m^2]", %{"x"=>"3"})
{{:unit, {:vari, "x"}, {:elev, {:vari, "m"}, {:numb, 2}}}, %{{:vari, "x"} => {:numb, 3}}}
```
returns a tuple {expression, parsed_conext} where
expression is a tuple that holds math AST and
parsed_context is a map whith all equalities (definitions) parsed as
"name" => expression
"""
def parse(txt, context \\ %{}) do
case Cyclic.check(context) do
{:ok, _deps} ->
tree = parse_text(txt)
{tree,
for {func, defi} <- context, into: %{} do
{parse_text(func), parse_text(defi)}
end}
{:err, msg} ->
throw(msg)
{:err, msg, _lst} ->
throw(msg)
end
end
@doc """
Parse and evaluate an expression. If ast is true returns de AST tuple,
if it is false return a human-readable (and parseable) expression.
```
iex> Exun.eval "x[m^2]+4[cm^2]",%{"x"=>"3"}
"3.0004[m^2]"
```
"""
def eval(txt, context \\ %{}) do
eval_ast(txt, context)
|> UI.tostr()
end
@doc """
Same as eval but returns AST
"""
def eval_ast(txt, context \\ %{}) do
{ast, pctx} = parse(txt, context)
# |> IO.inspect(label: "ast and pctx")
case ast do
{:error, {line, _app, list}} ->
throw("Error line:#{line} #{list}")
_ ->
# First Collect context
pctx = for {k,v} <- pctx, into: %{} do
{k, Collect.coll(v)}
end
ast
# |> IO.inspect(label: "eval01,AST")
|> replace(pctx)
# |> IO.inspect(label: "eval02,Replaced")
|> Collect.coll()
end
end
def parse_text(txt) do
with {:ok, toks, _} <- :exun_lex.string(txt |> String.to_charlist()),
{:ok, tree} <- :exun_yacc.parse(toks) do
tree
end
end
@doc """
Replace definitions in context into
main tree expression until no more
expansion is posssible
"""
def replace(tree, pc) do
newtree = repl(tree, pc)
if not Eq.eq(tree, newtree) do
replace(newtree, pc)
else
newtree
end
end
defp repl(tree, pc) do
case tree do
{:vari, var} ->
Map.get(pc, {:vari, var}, {:vari, var})
{:fcall, name, args} ->
args = Enum.map(args, &repl(&1, pc))
arity = length(args)
user_function =
Map.keys(pc)
# |> IO.inspect(label: "user_function1")
|> Enum.filter(fn el -> elem(el, 0) == :fcall end)
# |> IO.inspect(label: "user_function2")
|> Enum.filter(fn el -> elem(el, 1) == name and length(elem(el, 2)) == arity end)
# |> IO.inspect(label: "user_function3")
cond do
length(user_function) > 1 ->
{_, dupe_name, _} = user_function |> List.first()
throw("Multiple definition for function #{dupe_name}")
length(user_function) == 1 ->
key = {:fcall, _, args_names} = user_function |> List.first()
ast = pc[key]
nv =
List.zip([args_names, args])
|> Enum.reduce(%{}, fn {n, v}, ac ->
Map.put(ac, n, v)
end)
replace_args(ast, nv)
true ->
{:fcall, name, args}
end
{op, l, r} ->
{op, replace(l, pc), replace(r, pc)}
_ ->
tree
end
end
defp replace_args(ast, nv) do
case ast do
{:vari, v} ->
Map.get(nv, {:vari, v}, {:vari, v})
{:unit, un, ut} ->
{:unit, replace_args(un, nv), replace_args(ut, nv)}
{:fcall, subname, subargs} ->
{:fcall, subname, subargs |> Enum.map(&replace_args(&1, nv))}
{op, l, r} ->
{op, replace_args(l, nv), replace_args(r, nv)}
other ->
other
end
end
end