Skip to content

Commit

Permalink
feat: add constants support (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
fahchen authored Dec 7, 2024
1 parent 39319eb commit 6d4b84b
Show file tree
Hide file tree
Showing 23 changed files with 904 additions and 394 deletions.
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
erlang 26.2.5
elixir 1.16.3-otp-26
erlang 27.1
elixir 1.17.3-otp-27
97 changes: 42 additions & 55 deletions lib/coloured_flow/definition/arc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ defmodule ColouredFlow.Definition.Arc do

@type label() :: binary()
@type orientation() :: :p_to_t | :t_to_p
@typep binding() :: ArcExpression.binding()

typed_structor enforce: true do
plugin TypedStructor.Plugins.DocFields
Expand Down Expand Up @@ -57,84 +56,72 @@ defmodule ColouredFlow.Definition.Arc do
else
bind {2, 1}
end
```
# the bindings are:
# [{{:cpn_bind_literal, 2}, 1}, {{:cpn_bind_literal, 1}, {:x, [], nil}}]
```elixir
# use `bind` with guard to bind the variable
bind {1, x} when x > 2
```
"""

field :bindings,
list(binding()),
default: [],
doc: """
The result that are returned by the arc, is form of a multi-set of tokens.
- `[{{:cpn_bind_literal, 1}, {:x, [], nil}}]`: binds 1 token of colour `:x`
- `[{{:cpn_bind_literal, 2}, {:x, [], nil}}, {3, {:cpn_bind_variable, :y}}]`: binds 2 tokens of colour `:x` or 3 tokens of colour `:y`
- `[{{:cpn_bind_variable, :x}, {:y, [], nil}}]`: binds `x` tokens of colour `:y`
- `[{{:cpn_bind_literal, 0}, {:x, [], nil}}]`: binds 0 tokens (empty tokens) of colour `:x`
"""
end

@doc """
Build bindings from the expression of the in-coming arc.
Build the expression for the arc.
## Examples
iex> expression = ColouredFlow.Definition.Expression.build!("bind {a, b}")
iex> {:ok, binding} = build_bindings(expression)
iex> [{{:cpn_bind_variable, {:a, [line: 1, column: 7]}}, {:b, [line: 1, column: 10], nil}}] = binding
iex> {:ok, %ColouredFlow.Definition.Expression{}} = build_expression(:p_to_t, "bind {a, b}")
iex> expression = ColouredFlow.Definition.Expression.build!("{a, b}")
iex> {:error, {[], "missing `bind` in expression", "{a, b}"}} = build_bindings(expression)
iex> {:error, {[], "missing `bind` in expression", "{a, b}"}} = build_expression(:p_to_t, "{a, b}")
iex> {:ok, %ColouredFlow.Definition.Expression{}} = build_expression(:t_to_p, "{a, b}")
"""
@spec build_bindings(Expression.t()) ::
{:ok, list(binding())} | {:error, ColouredFlow.Expression.compile_error()}
def build_bindings(%Expression{} = expression) do
case extract_bindings(expression.expr) do
[] -> {:error, {[], "missing `bind` in expression", expression.code}}
bindings -> check_binding_vars(expression.vars, bindings)
@spec build_expression(orientation(), code :: binary() | nil) ::
{:ok, Expression.t()} | {:error, ColouredFlow.Expression.compile_error()}
def build_expression(orientation, code)

def build_expression(:p_to_t, code) do
with {:ok, expression} <- Expression.build(code) do
case validate_bind_exprs(expression.expr) do
[] ->
{:error, {[], "missing `bind` in expression", code}}

validations ->
case Enum.find(validations, &match?({:error, _reason}, &1)) do
nil -> {:ok, expression}
{:error, reason} -> {:error, reason}
end
end
end
end

@spec build_bindings!(Expression.t()) :: list(binding())
def build_bindings!(%Expression{} = expression) do
case build_bindings(expression) do
{:ok, bindings} -> bindings
def build_expression(:t_to_p, code) do
Expression.build(code)
end

@spec build_expression!(orientation(), code :: binary() | nil) :: Expression.t()
def build_expression!(orientation, code) do
case build_expression(orientation, code) do
{:ok, expression} -> expression
{:error, reason} -> raise inspect(reason)
end
end

defp extract_bindings(quoted) do
defp validate_bind_exprs(quoted) do
quoted
|> Macro.prewalk([], fn
{:bind, _meta, [binding]} = ast, acc ->
{ast, [ArcExpression.extract_binding(binding) | acc]}
{:bind, meta, [bind_expr]} = ast, acc ->
validation =
case ArcExpression.validate_bind_expr(bind_expr) do
{:error, reason} -> {:error, {meta, reason, Macro.to_string(ast)}}
:ok -> :ok
end

{ast, [validation | acc]}

ast, acc ->
{ast, acc}
end)
|> elem(1)
end

defp check_binding_vars(vars, bindings) do
binding_vars = Enum.flat_map(bindings, &ArcExpression.get_var_names/1)
binding_vars = Map.new(binding_vars)
diff = Map.drop(binding_vars, vars)

case Map.to_list(diff) do
[] ->
{:ok, bindings}

[{name, meta} | _rest] ->
{
:error,
{
meta,
"missing binding variable in vars: #{inspect(name)}",
""
}
}
end
end
end
22 changes: 11 additions & 11 deletions lib/coloured_flow/definition/expression.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,23 @@ defmodule ColouredFlow.Definition.Expression do
Note that, `""` and `nil` are valid codes that are always evaluated to `nil`,
and are treated as `false` in the guard of a transition.
"""
@spec build(binary() | nil) ::
{:ok, t()}
| {:error, ColouredFlow.Expression.compile_error()}
def build(expr) when is_nil(expr) when expr === "", do: {:ok, %__MODULE__{}}

def build(expr) when is_binary(expr) do
with({:ok, quoted, vars} <- ColouredFlow.Expression.compile(expr, __ENV__)) do
{:ok, %__MODULE__{code: expr, expr: quoted, vars: Map.keys(vars)}}
@spec build(binary() | nil, Macro.Env.t()) ::
{:ok, t()} | {:error, ColouredFlow.Expression.compile_error()}
def build(expr, env \\ __ENV__)
def build(expr, _env) when is_nil(expr) when expr === "", do: {:ok, %__MODULE__{}}

def build(expr, env) when is_binary(expr) do
with({:ok, quoted, vars} <- ColouredFlow.Expression.compile(expr, env)) do
{:ok, %__MODULE__{code: expr, expr: quoted, vars: vars |> Map.keys() |> Enum.sort()}}
end
end

@doc """
Build an expression from code, raise if failed. See `build/1`.
"""
@spec build!(binary() | nil) :: t()
def build!(expr) do
case build(expr) do
@spec build!(binary() | nil, Macro.Env.t()) :: t()
def build!(expr, env \\ __ENV__) do
case build(expr, env) do
{:ok, expr} -> expr
{:error, reason} -> raise "failed to build expression: #{inspect(reason)}"
end
Expand Down
10 changes: 2 additions & 8 deletions lib/coloured_flow/definition/helper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,13 @@ defmodule ColouredFlow.Definition.Helper do
@spec build_arc!([{:transition, Transition.name()} | transition_arc_param()]) :: Arc.t()
def build_arc!(params) do
params = Keyword.validate!(params, [:label, :place, :transition, :orientation, :expression])
expr = Expression.build!(params[:expression])

bindings =
case params[:orientation] do
:p_to_t -> Arc.build_bindings!(expr)
:t_to_p -> []
end
expr = Arc.build_expression!(params[:orientation], params[:expression])

struct!(
Arc,
params
|> Keyword.take([:label, :place, :transition, :orientation])
|> Keyword.merge(expression: expr, bindings: bindings)
|> Keyword.merge(expression: expr)
)
end

Expand Down
138 changes: 87 additions & 51 deletions lib/coloured_flow/enabled_binding_elements/binding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ defmodule ColouredFlow.EnabledBindingElements.Binding do
iex> combine([[[x: 1, y: 2]]])
[[x: 1, y: 2]]
"""
@spec combine(bindings_list :: [[binding()]]) :: [binding()]
@spec combine(bindings_list :: [[[binding()]]]) :: [[binding()]]
def combine(bindings_list) do
Enum.reduce(bindings_list, [[]], fn bindings, prevs ->
for prev <- prevs, binding <- bindings, reduce: [] do
Expand All @@ -87,91 +87,101 @@ defmodule ColouredFlow.EnabledBindingElements.Binding do
"""
@spec match_bag(
place_tokens_bag :: MultiSet.t(),
arc_binding :: ArcExpression.binding(),
arc_bind_expr :: ArcExpression.bind_expr(),
value_var_context :: atom()
) :: [binding()]
def match_bag(place_tokens_bag, arc_binding, value_var_context \\ nil) do
def match_bag(place_tokens_bag, arc_bind_expr, value_var_context \\ nil) do
place_tokens_bag
|> MultiSet.to_pairs()
|> Enum.flat_map(&match(&1, arc_binding, value_var_context))
|> Enum.flat_map(&match(&1, arc_bind_expr, value_var_context))
end

@doc """
Get the bindings from a token (`t:ColouredFlow.MultiSet.pair/0`) that match the arc_binding expression (see detail at`t:ColouredFlow.Expression.Arc.binding/0`).
Get the bindings from a token (`t:ColouredFlow.MultiSet.pair/0`) that match the arc_bind expression (see detail at`t:ColouredFlow.Expression.Arc.bind_expr/0`).
"""
@spec match(
place_tokens :: MultiSet.pair(),
arc_binding :: ArcExpression.binding(),
value_var_context :: atom()
arc_bind_expr :: ArcExpression.bind_expr(),
arc_bind_var_context :: atom()
) :: [binding()]
def match(place_tokens, arc_binding, value_var_context \\ nil)

def match(place_tokens, {{:cpn_bind_literal, coefficient}, value_pattern}, value_var_context),
do: literal_match(place_tokens, coefficient, value_pattern, value_var_context)
def match(place_tokens, arc_bind_expr, arc_bind_var_context \\ nil) do
coefficient =
case arc_bind_expr do
{coefficient, _value_pattern} -> coefficient
{:when, _meta, [{coefficient, _value_pattern}, _guard]} -> coefficient
end

def match(place_tokens, {{:cpn_bind_variable, coefficient}, value_pattern}, value_var_context),
do: variable_match(place_tokens, coefficient, value_pattern, value_var_context)
if Macro.quoted_literal?(coefficient) do
if is_integer(coefficient) and coefficient >= 0 do
literal_match(place_tokens, coefficient, arc_bind_expr, arc_bind_var_context)
else
# skip if coefficient is not a non-negative integer
[]
end
else
variable_match(place_tokens, arc_bind_expr, arc_bind_var_context)
end
end

defp literal_match(place_tokens, expected_coefficient, value_pattern, value_var_context)

defp literal_match(
{token_coefficient, _token_value},
expected_coefficient,
_value_pattern,
_binding_context
_bind,
_bind_context
)
when expected_coefficient > token_coefficient,
do: []

defp literal_match(
{token_coefficient, token_value},
expected_coefficient,
value_pattern,
value_var_context
expr,
bind_var_context
) do
case match_value_pattern(token_value, value_pattern, value_var_context) do
case match_bind_expr({expected_coefficient, token_value}, expr, bind_var_context) do
:error ->
[]

{:ok, binding} ->
prepend_coefficient_binding(token_coefficient, expected_coefficient, binding)
{:ok, expr, _place_tokens} ->
duplicate_binding(token_coefficient, expected_coefficient, expr)
end
end

defp variable_match(
{token_coefficient, token_value},
{coefficient_var, _meta},
value_pattern,
value_var_context
) do
case match_value_pattern(token_value, value_pattern, value_var_context) do
:error ->
[]
defp variable_match(place_tokens, expr, bind_var_context)

{:ok, binding} ->
case Keyword.fetch(binding, coefficient_var) do
:error ->
for coeff <- 0..token_coefficient do
[{coefficient_var, coeff} | binding]
end
defp variable_match({token_coefficient, token_value}, expr, bind_var_context) do
Enum.flat_map(0..token_coefficient, fn coefficient ->
case match_bind_expr({coefficient, token_value}, expr, bind_var_context) do
:error ->
[]

{:ok, coefficient} ->
prepend_coefficient_binding(token_coefficient, coefficient, binding)
end
end
{:ok, expr, {coefficient, _value}} ->
duplicate_binding(token_coefficient, coefficient, expr)
end
end)
end

@spec match_value_pattern(
MultiSet.value(),
ArcExpression.value_pattern(),
value_var_context :: atom()
) ::
{:ok, [binding()]} | :error
defp match_value_pattern(token_value, value_pattern, value_var_context) do
@spec match_bind_expr(MultiSet.pair(), ArcExpression.bind_expr(), bind_var_context :: atom()) ::
{:ok, Code.binding(), MultiSet.pair()} | :error
defp match_bind_expr(place_tokens, expr, bind_var_context) do
value = Macro.escape(place_tokens)
coefficient_var = Macro.var(:coefficient, __MODULE__)
value_var = Macro.var(:value, __MODULE__)

ast =
quote do
case unquote(Macro.escape(token_value)) do
unquote(value_pattern) -> {:ok, binding(unquote(value_var_context))}
quote generated: true do
with(
{unquote(coefficient_var), unquote(value_var)} <- unquote(value),
unquote(expr) <- {unquote(coefficient_var), unquote(value_var)}
) do
{
:ok,
binding(unquote(bind_var_context)),
{unquote(coefficient_var), unquote(value_var)}
}
else
_other -> :error
end
end
Expand All @@ -180,15 +190,41 @@ defmodule ColouredFlow.EnabledBindingElements.Binding do
|> Code.eval_quoted()
|> elem(0)
rescue
_error -> []
_error -> :error
end

defp prepend_coefficient_binding(token_coefficient, expected_coefficient, binding) do
defp duplicate_binding(token_coefficient, expected_coefficient, binding) do
if 0 === expected_coefficient do
[binding]
else
result = div(token_coefficient, expected_coefficient)
List.duplicate(binding, result)
end
end

@spec build_match_expr(expr :: ArcExpression.bind_expr()) :: Macro.t()
def build_match_expr(expr) do
quote generated: true do
case nil do
unquote(expr) -> binding()
end
end
end

@spec apply_constants_to_bind_expr(
arc_bind_expr :: ArcExpression.bind_expr(),
constants :: %{ColourSet.name() => ColourSet.value()}
) :: ArcExpression.bind_expr()
def apply_constants_to_bind_expr(arc_bind_expr, constants) do
Macro.postwalk(arc_bind_expr, fn
{var, meta, context} when is_atom(var) and is_atom(context) ->
case Map.fetch(constants, var) do
{:ok, value} -> Macro.escape(value)
:error -> {var, meta, context}
end

other ->
other
end)
end
end
Loading

0 comments on commit 6d4b84b

Please sign in to comment.