Current section

Files

Jump to
vtc lib framestamp_eval.ex
Raw

lib/framestamp_eval.ex

defmodule Vtc.Framestamp.Eval do
@moduledoc false
alias Vtc.Framerate
alias Vtc.Framestamp
alias Vtc.Source.Frames
@self_alias [:Vtc, :Framestamp, :Eval]
# Escapes variable that the default rate for the eval will be kept in,
@default_rate_var_ast Macro.var(:default_rate, :vtc_eval)
@spec eval([at: Framerate.t() | number() | Ratio.t(), ntsc: Framerate.ntsc()], Macro.input()) :: Macro.t()
def eval(opts, body) do
body =
case body do
[do: body] -> body
body -> body
end
default_rate = Keyword.get(opts, :at, nil)
ntsc = Keyword.take(opts, [:ntsc])
setup_default = aliased_func_call(@self_alias, :setup_rate, [], [default_rate, ntsc])
set_default_var = {:=, [], [@default_rate_var_ast, setup_default]}
body = Macro.postwalk(body, &replace_ops_ast/1)
quote do
with do
unquote(set_default_var)
unquote(body)
end
end
end
@two_stamp_ops %{
{:+, 2} => :add,
{:-, 2} => :sub,
{:==, 2} => :eq?,
{:<, 2} => :lt?,
{:<=, 2} => :lte?,
{:>, 2} => :gt?,
{:>=, 2} => :gte?,
{:*, 2} => :mult,
{:/, 2} => :div,
{:%, 2} => :rem,
{:div, 2} => :div,
{:divrem, 2} => :divrem,
{:-, 1} => :minus,
{:abs, 1} => :abs
}
@one_stamp_ops [
{:*, 2},
{:/, 2},
{:%, 2},
{:div, 2},
{:divrem, 2},
{:-, 1},
{:abs, 1}
]
# Replaces operators with calls to a `Framestamp` function.
@spec replace_ops_ast(Macro.t()) :: Macro.t()
defp replace_ops_ast({op, _, args} = ast) when is_map_key(@two_stamp_ops, {op, length(args)}) do
stamp_func_name = Map.fetch!(@two_stamp_ops, {op, length(args)})
stamp_arg_count = if {op, length(args)} in @one_stamp_ops, do: 1, else: 2
replace_framestamp_op(stamp_func_name, stamp_arg_count, ast)
end
defp replace_ops_ast(ast), do: ast
# Replaces ops where both args are framestamps.
@spec replace_framestamp_op(atom(), pos_integer(), Macro.t()) :: Macro.t()
defp replace_framestamp_op(stamp_func_name, stamp_arg_count, ast) do
{_, meta, args} = ast
args
|> Enum.with_index()
|> Enum.map(fn
{arg_ast, i} when i < stamp_arg_count -> wrap_stamp_val_in_cast(arg_ast, meta)
{arg_ast, _} -> arg_ast
end)
|> then(&framestamp_func(stamp_func_name, meta, &1))
end
# Wraps framestamp args in `cast_framestamp_arg/2`.
@spec wrap_stamp_val_in_cast(Macro.t(), Macro.metadata()) :: Macro.t()
def wrap_stamp_val_in_cast({:num, _, [arg]}, _), do: arg
def wrap_stamp_val_in_cast(arg_ast, meta),
do: aliased_func_call([:Vtc, :Framestamp, :Eval], :cast_framestamp_arg, meta, [arg_ast, @default_rate_var_ast])
# Casts `frames` to `default` rate if one was provided, and `frames` is not already
# a framestamp value.
@doc false
@spec cast_framestamp_arg(Framestamp.t() | Frames.t(), Framerate.t() | nil) :: Framestamp.t() | Frames.t()
def cast_framestamp_arg(%Framestamp{} = framestamp, _), do: framestamp
def cast_framestamp_arg(frames, nil), do: frames
def cast_framestamp_arg(frames, default_rate) do
if Frames.impl_for(frames) do
Framestamp.with_frames!(frames, default_rate)
else
frames
end
end
# Inserted into the head of each eval block. Sets up the default framerate variable.
@spec setup_rate(Framerate.t() | number() | Ratio.t() | nil, ntsc: Framerate.ntsc()) :: Framerate.t() | nil
def setup_rate(nil, _), do: nil
def setup_rate(%Framerate{} = rate, _), do: rate
def setup_rate(rate, opts) do
opts = if is_float(rate), do: Keyword.put_new(opts, :coerce_ntsc?, true), else: opts
Framerate.new!(rate, opts)
end
# Escapes a framestamp function name for use in an ast.
@spec framestamp_func(atom(), Macro.metadata(), [Macro.t()]) :: Macro.t()
defp framestamp_func(name, meta, args), do: aliased_func_call([:Vtc, :Framestamp], name, meta, args)
# Constructs the AST for an alias + function call.
@spec aliased_func_call([atom()], atom(), Macro.metadata(), [Macro.t()]) :: Macro.t()
defp aliased_func_call(aliases, func_name, meta, args) do
func_call = {:., meta, [{:__aliases__, meta, aliases}, func_name]}
{func_call, meta, args}
end
end