Current section

Files

Jump to
vtc lib timecode_eval.ex
Raw

lib/timecode_eval.ex

defmodule Vtc.Timecode.Eval do
@moduledoc false
alias Vtc.Framerate
alias Vtc.Source.Frames
alias Vtc.Timecode
@self_alias [:Vtc, :Timecode, :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
@tc_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
}
@single_tc_ops [
{:*, 2},
{:/, 2},
{:%, 2},
{:div, 2},
{:divrem, 2},
{:-, 1},
{:abs, 1}
]
@spec replace_ops_ast(Macro.t()) :: Macro.t()
defp replace_ops_ast({op, _, args} = ast) when is_map_key(@tc_ops, {op, length(args)}) do
tc_func_name = Map.fetch!(@tc_ops, {op, length(args)})
tc_arg_count = if {op, length(args)} in @single_tc_ops, do: 1, else: 2
replace_tc_op(tc_func_name, tc_arg_count, ast)
end
defp replace_ops_ast(ast), do: ast
# Replaces ops where both args are timecodes.
@spec replace_tc_op(atom(), pos_integer(), Macro.t()) :: Macro.t()
defp replace_tc_op(tc_func_name, tc_arg_count, ast) do
{_, meta, args} = ast
args
|> Enum.with_index()
|> Enum.map(fn
{arg_ast, i} when i < tc_arg_count -> wrap_tc_val_in_cast(arg_ast, meta)
{arg_ast, _} -> arg_ast
end)
|> then(&timecode_func(tc_func_name, meta, &1))
end
@spec wrap_tc_val_in_cast(Macro.t(), Macro.metadata()) :: Macro.t()
def wrap_tc_val_in_cast({:num, _, [arg]}, _), do: arg
def wrap_tc_val_in_cast(arg_ast, meta),
do: aliased_func_call([:Vtc, :Timecode, :Eval], :cast_timecode_arg, meta, [arg_ast, @default_rate_var_ast])
# Casts `frames` to `default` rate if one was provided, and `frames` is not already
# a timecode value.
@doc false
@spec cast_timecode_arg(Timecode.t() | Frames.t(), Framerate.t() | nil) :: Timecode.t() | Frames.t()
def cast_timecode_arg(%Timecode{} = timecode, _), do: timecode
def cast_timecode_arg(frames, nil), do: frames
def cast_timecode_arg(frames, default_rate) do
if Frames.impl_for(frames) do
Timecode.with_frames!(frames, default_rate)
else
frames
end
end
@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: Framerate.new!(rate, opts)
# Escapes a timecode function name for use in an ast.
@spec timecode_func(atom(), Macro.metadata(), [Macro.t()]) :: Macro.t()
defp timecode_func(name, meta, args), do: aliased_func_call([:Vtc, :Timecode], name, meta, args)
@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