defmodule Egglog.Program do
@moduledoc """
Loaded egglog program used for query-local runs.
A `Program` is a plain handle to a loaded native egglog base e-graph
containing static declarations, rulesets, rules, and any persistent theory
facts. Each `run/3` call clones that base inside native egglog and executes
the supplied input as query-local work.
Concurrent calls against the same `Program` are safe and query-local, but
they are serialized by that native resource. This protects the loaded base
and the clone/run operation for one handle without introducing a process-wide
lock or an Elixir worker pool. Load multiple `Program` handles explicitly
when you need parallel throughput.
"""
alias Egglog.{Commands, Common, Native}
@enforce_keys [:ref]
defstruct [:ref, modes: %{}, default_budget: %{rounds: 0}, name: nil]
@opaque t :: %__MODULE__{
ref: reference(),
modes: %{optional(atom() | String.t()) => String.t()},
default_budget: map(),
name: String.t() | nil
}
@typedoc """
Decoded value returned by `eval/4` and `lookup/5`.
`:value` contains a native egglog e-class value rendered as text. Primitive
egglog values are decoded to ordinary Elixir values when possible:
integers, floats, booleans, strings, and `nil` for unit.
"""
@type value :: %{
required(:sort) => String.t(),
required(:type) => atom(),
required(:value) => term()
}
@typedoc """
One output-producing egglog request.
A request may be written directly as an egglog command string, or as a small
Elixir map that the wrapper renders to native egglog syntax.
"""
@type request ::
String.t()
| %{required(:type) => :check, required(:expr) => String.t()}
| %{
required(:type) => :extract,
required(:expr) => String.t(),
optional(:variants) => pos_integer(),
optional(:limit) => pos_integer()
}
| %{required(:type) => :print_size, optional(:name) => String.t()}
| %{
required(:type) => :print_function,
required(:name) => String.t(),
optional(:limit) => pos_integer()
}
| %{optional(String.t()) => term()}
@typedoc """
Query-local input for `run/3`, `check/4`, `extract/4`, `eval/4`, and
`lookup/5`.
Strings are passed as raw egglog source. Lists are joined as egglog commands.
Parsed `Egglog.Commands` handles can be reused when the same source is run or
inspected repeatedly. Maps let Elixir code build common egglog commands
without string concatenation.
"""
@type input ::
%{
optional(:source) => String.t(),
optional(:program) => String.t(),
optional(:commands) => [String.t()],
optional(:facts) => [String.t()],
optional(:terms) => %{String.t() => String.t()},
optional(:sets) => [{String.t(), String.t()}],
optional(:unions) => [{String.t(), String.t()}],
optional(:requests) => [request()],
optional(:schedule) => String.t(),
optional(:snapshot) => :dot | :json | :none | String.t() | boolean(),
optional(:snapshot_max_functions) => non_neg_integer(),
optional(:snapshot_max_calls_per_function) => non_neg_integer(),
optional(:snapshot_inline_leaves) => non_neg_integer(),
optional(:snapshot_split_primitive_outputs) => boolean(),
optional(:mode) => atom() | String.t(),
optional(:budget) => map()
}
| %{optional(String.t()) => term()}
| Commands.t()
@typedoc """
Result returned by `run/3`.
"""
@type run_result :: %{
required(:status) => atom(),
required(:outputs) => [%{required(:type) => atom(), required(:text) => String.t()}],
required(:stats) => map(),
required(:report) => map(),
optional(:snapshot) => map()
}
@load_doc """
Loads a static egglog program into a reusable native handle.
The theory may be a raw egglog source string or a map containing `:source` or
`:program`.
Supported options:
* `:name` - human-readable program name stored in the returned struct.
Defaults to `nil`; it is not passed to native egglog.
* `:proofs` - create the native e-graph with proof support enabled.
Defaults to `false`. Truthy values are `true`, `"true"`, `"1"`, and `1`;
all other values are treated as false.
* `:modes` - map from mode names to egglog schedule forms or commands.
Defaults to `%{}`. During `run/3`, the selected `:mode` is looked up by
the mode value and by `to_string(mode)`.
* `:default_budget` - map merged into each non-parsed `run/3` input
budget. Defaults to `%{}`. Currently `:rounds`/`"rounds"` and
`:output_limit`/`"output_limit"` are read by `run/3`.
String option keys are also accepted, matching the rest of the wrapper.
"""
@run_doc """
Runs query-local egglog code against the loaded program.
The loaded base program is cloned inside native egglog for each run. The
clone receives query-local input and the loaded base is not mutated.
`input` may be:
* a raw egglog source string,
* a list of raw egglog command strings,
* a parsed `%Egglog.Commands{}` handle returned by `parse/1`, or
* a map with structured fields rendered to egglog source.
Structured input map fields:
* `:source` or `:program` - raw egglog source emitted first. Defaults to
unset.
* `:commands` - list of raw egglog commands emitted after source.
Defaults to `[]`.
* `:terms` - map rendered as `(let name expr)` commands. Defaults to `%{}`.
* `:facts` - list of raw fact/command strings. Defaults to `[]`.
* `:sets` - list of `{call, value}` tuples rendered as `(set call value)`.
Defaults to `[]`.
* `:unions` - list of `{left, right}` tuples rendered as
`(union left right)`. Defaults to `[]`.
* `:requests` - list of output-producing requests emitted last. Defaults
to `[]`. Supported request maps are `:check`, `:extract`,
`:print_size`, and `:print_function`; raw request strings are also
accepted.
* `:schedule` - egglog schedule string. Defaults to unset. Blank strings
emit no schedule; strings starting with `(run` are emitted unchanged;
other nonblank strings are wrapped as `(run-schedule schedule)`.
* `:mode` - mode name used when no `:schedule` is supplied. Defaults to
`:default`.
* `:budget` - map merged after the program default budget and before the
`opts[:budget]` map. Defaults to `%{}`.
* snapshot fields listed below may also be supplied in the input map; opts
take precedence over input map snapshot fields.
Supported options:
* `:mode` - selected mode when no input `:schedule` is supplied. Defaults
to input `:mode`, then `:default`. The selected mode is looked up in the
program's `:modes` map before `:budget` rounds are considered.
* `:budget` - map merged after `program.default_budget` and input
`:budget`. Defaults to `%{}`. Currently `:rounds`/`"rounds"` emits
`(run rounds)` when positive and no selected mode schedule exists.
`:output_limit`/`"output_limit"` is used as the default output limit for
non-parsed inputs.
* `:output_limit` - maximum number of native command outputs to keep in
the returned `:outputs` list. Defaults to `budget[:output_limit]` for
non-parsed inputs and unset for parsed command handles. Values must be
positive integers; invalid values are ignored. When outputs are
truncated, the returned result has `status: :limit`.
* `:snapshot` - request a native snapshot after the input has run.
Defaults to input `:snapshot`, then `:none`.
Accepted values:
* `:none`, `"none"`, `false`, or `nil` - do not include a snapshot.
* `:dot`, `"dot"`, or `true` - include native Graphviz DOT text.
* `:json` or `"json"` - include native JSON snapshot text.
`run/3` does not render DOT to SVG. Use `snapshot/3` when you want
`render: :auto` or `render: :svg` post-processing.
* `:snapshot_max_functions` - value passed to native egglog's
`SerializeConfig.max_functions` when `:snapshot` is enabled. Defaults to
input `:snapshot_max_functions`, then `0`. Values greater than `0` become
`Some(value)`; `0` and invalid values become `None`.
* `:snapshot_max_calls_per_function` - value passed to native egglog's
`SerializeConfig.max_calls_per_function` when `:snapshot` is enabled.
Defaults to input `:snapshot_max_calls_per_function`, then `0`. Values
greater than `0` become `Some(value)`; `0` and invalid values become
`None`.
* `:snapshot_inline_leaves` - number of native leaf-inlining passes to use
on the serialized snapshot before producing DOT or JSON text. Defaults
to input `:snapshot_inline_leaves`, then `0`. Negative or invalid values
behave like `0`.
* `:snapshot_split_primitive_outputs` - whether the native serialized
snapshot should split primitive outputs into separate classes before
producing DOT or JSON text. Defaults to input
`:snapshot_split_primitive_outputs`, then `false`.
For parsed `%Egglog.Commands{}` input, `:output_limit` and the snapshot
options above are used. `:mode` is passed through to native result metadata,
but no mode schedule is injected because the command sequence is already
parsed. Structured input fields and budget-derived schedules are not rebuilt.
The returned result map contains:
* `:status` - native run status, or `:limit` when `:output_limit`
truncated outputs.
* `:outputs` - list of `%{type: atom(), text: String.t()}` command
outputs.
* `:stats` - compact numeric/text stats from native egglog.
* `:report` - per-rule/per-ruleset reporting data from native egglog.
* `:snapshot` - present only when `:snapshot` is `:dot` or `:json`;
contains `%{format: :dot | :json, text: binary(), omitted: term()}`.
String option keys are also accepted, matching the rest of the wrapper.
"""
@program_query_opts_doc """
Supported options are the query-construction subset of `run/3` options:
* `:mode` - selected mode when no input `:schedule` is supplied. Defaults
to input `:mode`, then `:default`.
* `:budget` - map merged after program default budget and input `:budget`.
Defaults to `%{}`. Currently `:rounds`/`"rounds"` can emit a `(run n)`
command when no selected mode schedule exists.
`:output_limit` and snapshot options are not used by this function because it
does not return command outputs or snapshots.
String option keys are also accepted, matching the rest of the wrapper.
"""
@program_check_doc """
Runs a native egglog check and returns `{:ok, true}` or `{:ok, false}`.
Supported options are the same as `run/3`; they are passed through to the
internal run after appending `(check fact)` to the input requests. Defaults
are therefore the `run/3` defaults.
Only a failed egglog check is converted to `false`; parse/type/native errors
are returned as `{:error, reason}`. Snapshot and output options may affect
the internal run, but their result data is not exposed by this function.
"""
@program_extract_doc """
Extracts the cheapest native egglog term for `expr`.
Supported options:
* `:variants` - optional value inserted as the second argument to native
egglog's `(extract expr variants)` command. Defaults to unset, which uses
`(extract expr)`.
All `run/3` options are also accepted and passed through after appending the
extract request to the input. Defaults are the `run/3` defaults. Snapshot and
output options may affect the internal run, but `extract/4` returns only the
extracted text.
String option keys are also accepted, matching the rest of the wrapper.
"""
@snapshot_doc """
Builds a query-local e-graph snapshot.
This is a convenience wrapper around `run/3`: it runs the query-local input
against a native clone of the loaded program, requests a native DOT or JSON
snapshot, then optionally renders or writes the serialized graph.
Supported options:
* `:render` - output format to return. Defaults to `:auto`.
Accepted values:
* `:auto`, `"auto"`, or `true` - render SVG with Graphviz when the
`dot` executable is available on `PATH`; otherwise return DOT text.
* `:svg` or `"svg"` - require Graphviz SVG rendering. Returns
`{:error, {:graphviz_missing, message}}` if `dot` is unavailable.
* `:dot`, `"dot"`, `false`, or `nil` - return Graphviz DOT text.
* `:json` or `"json"` - return egglog's native JSON snapshot.
Internally, `:render` determines the native `:snapshot` format passed to
`run/3`: `:json` requests JSON; every other render value requests DOT.
* `:mode` - selected mode when no input `:schedule` is supplied. Defaults
to input `:mode`, then `:default`.
* `:budget` - map merged after program default budget and input `:budget`.
Defaults to `%{}`. Currently `:rounds`/`"rounds"` can emit a `(run n)`
command when no selected mode schedule exists.
* `:output_limit` - passed to the underlying `run/3`; it can truncate
`snapshot.result.outputs`. Defaults to `budget[:output_limit]` for
non-parsed inputs and unset for parsed command handles.
* `:snapshot_max_functions` - value passed to native egglog's
`SerializeConfig.max_functions`. Defaults to input
`:snapshot_max_functions`, then `0`. Values greater than `0` become
`Some(value)`; `0` and invalid values become `None`.
* `:snapshot_max_calls_per_function` - value passed to native egglog's
`SerializeConfig.max_calls_per_function`. Defaults to input
`:snapshot_max_calls_per_function`, then `0`. Values greater than `0`
become `Some(value)`; `0` and invalid values become `None`.
* `:snapshot_inline_leaves` - number of native leaf-inlining passes to use
on the serialized snapshot before producing DOT or JSON text. Defaults
to input `:snapshot_inline_leaves`, then `0`. Negative or invalid values
behave like `0`.
* `:snapshot_split_primitive_outputs` - whether the native serialized
snapshot should split primitive outputs into separate classes before
producing DOT or JSON text. Defaults to input
`:snapshot_split_primitive_outputs`, then `false`.
* `:path` - optional file path where the returned `:text` payload is
written. Defaults to unset.
* `:dot_path` - optional file path where DOT is written when DOT text is
available. Defaults to unset. For `render: :json`, this key is recorded
as `nil` if provided because no DOT text is produced.
* `:svg_path` - optional file path where SVG is written when SVG rendering
succeeds. Defaults to unset. For non-SVG renders, this key is recorded as
`nil` if provided.
* `:json_path` - optional file path where JSON is written when
`render: :json` is used. Defaults to unset. For non-JSON renders, this
key is recorded as `nil` if provided.
The returned snapshot map contains:
* `:format` - one of `:dot`, `:svg`, or `:json`.
* `:text` - the primary payload for the selected render.
* `:dot`, `:svg`, or `:json` - the format-specific payload when present.
* `:omitted` - native omission metadata for capped snapshots.
* `:stats` - snapshot-specific native stats, currently
`:snapshot_nodes` and `:snapshot_classes` when available.
* `:result` - the underlying `run/3` result used to obtain the native
serialization.
* any provided artifact path keys after successful writes.
String option keys are also accepted, matching the rest of the wrapper.
"""
@doc """
Parses egglog source into a reusable command handle.
Parsed commands can be passed to `run/3`. This is useful when the same command
sequence is executed repeatedly; native egglog parsing happens once, while
execution still happens against a fresh clone of the loaded base program.
"""
@spec parse(String.t()) :: {:ok, Commands.t()} | {:error, term()}
defdelegate parse(source), to: Commands
@doc """
Bang variant of `parse/1`.
"""
@spec parse!(String.t()) :: Commands.t()
defdelegate parse!(source), to: Commands
@doc @load_doc
@spec load(String.t() | map(), keyword() | map()) :: {:ok, t()} | {:error, term()}
def load(theory_spec, opts \\ []) do
opts = Common.option_map(opts)
source = source_from(theory_spec)
proofs? = Common.truthy?(Common.get(opts, :proofs, false))
with {:ok, ref} <- Native.load_program(source, proofs?) |> Common.from_native_load() do
{:ok,
%__MODULE__{
ref: ref,
modes: normalize_map(Common.get(opts, :modes, %{})),
default_budget: normalize_map(Common.get(opts, :default_budget, %{})),
name: Common.get(opts, :name)
}}
end
end
@doc """
Bang variant of `load/2`.
#{@load_doc}
Raises if native egglog rejects the loaded theory.
"""
@spec load!(String.t() | map(), keyword() | map()) :: t()
def load!(theory_spec, opts \\ []) do
theory_spec
|> load(opts)
|> Common.bang("failed to load egglog program")
end
@doc @run_doc
@spec run(t(), input(), keyword() | map()) :: {:ok, run_result()} | {:error, term()}
def run(program, input, opts \\ [])
def run(%__MODULE__{} = program, %Commands{} = commands, opts) do
opts = Common.option_map(opts)
mode = Common.get(opts, :mode, :default)
snapshot = Common.snapshot_options(opts)
program.ref
|> Native.run_parsed_program(
commands.ref,
to_string(mode || :default),
snapshot.format,
snapshot.max_functions,
snapshot.max_calls_per_function,
snapshot.inline_leaves,
snapshot.split_primitive_outputs?
)
|> Common.from_native_result(Common.get(opts, :output_limit))
end
def run(%__MODULE__{} = program, input, opts) do
opts = Common.option_map(opts)
input_map = normalize_input(input)
budget = budget(program, input_map, opts)
mode = mode(input_map, opts)
source = build_run_source(program, input_map, budget, mode)
output_limit = Common.get(opts, :output_limit, Common.get(budget, :output_limit))
snapshot = Common.snapshot_options(opts, input_map)
program.ref
|> Native.run_program(
source,
to_string(mode || :default),
snapshot.format,
snapshot.max_functions,
snapshot.max_calls_per_function,
snapshot.inline_leaves,
snapshot.split_primitive_outputs?
)
|> Common.from_native_result(output_limit)
end
@doc """
Bang variant of `run/3`.
#{@run_doc}
Raises if native egglog rejects the input or any option validation raises.
"""
@spec run!(t(), input(), keyword() | map()) :: run_result()
def run!(%__MODULE__{} = program, input, opts \\ []) do
program
|> run(input, opts)
|> Common.bang("failed to run egglog program")
end
@doc @program_check_doc
@spec check(t(), input(), String.t(), keyword() | map()) ::
{:ok, boolean()} | {:error, term()}
def check(%__MODULE__{} = program, input, fact, opts \\ []) when is_binary(fact) do
program
|> run(append_request(input, "(check #{fact})"), opts)
|> Common.check_result()
end
@doc """
Boolean variant of `check/4`.
#{@program_check_doc}
It raises on non-check errors so syntax/type mistakes are not silently treated
as false mathematical assertions.
"""
@spec check?(t(), input(), String.t(), keyword() | map()) :: boolean()
def check?(%__MODULE__{} = program, input, fact, opts \\ []) do
program
|> check(input, fact, opts)
|> Common.bang("failed to check egglog fact")
end
@doc """
Asserts that a native egglog check fails.
This maps to egglog's `(fail (check ...))` form and is useful for tests and
tutorials that want to show a negative example without surfacing an exception.
Supported options are the same as `run/3`; they are passed through to the
internal run after appending `(fail (check fact))` to the input requests.
Defaults are therefore the `run/3` defaults. Only `:ok` or `{:error, reason}`
is returned.
"""
@spec check_fail(t(), input(), String.t(), keyword() | map()) :: :ok | {:error, term()}
def check_fail(%__MODULE__{} = program, input, fact, opts \\ []) when is_binary(fact) do
program
|> run(append_request(input, "(fail (check #{fact}))"), opts)
|> Common.ok_only()
end
@doc @program_extract_doc
@spec extract(t(), input(), String.t(), keyword() | map()) ::
{:ok, String.t()} | {:error, term()}
def extract(%__MODULE__{} = program, input, expr, opts \\ []) when is_binary(expr) do
program
|> run(append_request(input, Common.extract_request(expr, opts)), opts)
|> Common.extraction()
end
@doc """
Bang variant of `extract/4`.
#{@program_extract_doc}
Raises if native egglog rejects the input or extraction output is not
available.
"""
@spec extract!(t(), input(), String.t(), keyword() | map()) :: String.t()
def extract!(%__MODULE__{} = program, input, expr, opts \\ []) do
program
|> extract(input, expr, opts)
|> Common.bang("failed to extract egglog term")
end
@doc """
Evaluates an egglog expression after applying query-local input.
Primitive values are decoded into Elixir values. Non-primitive e-class values
are returned with `type: :value`.
#{@program_query_opts_doc}
"""
@spec eval(t(), input(), String.t(), keyword() | map()) :: {:ok, value()} | {:error, term()}
def eval(%__MODULE__{} = program, input, expr, opts \\ []) when is_binary(expr) do
opts = Common.option_map(opts)
program.ref
|> Native.eval_program(run_source(program, input, opts), expr)
|> Common.from_native_value()
end
@doc """
Bang variant of `eval/4`.
Evaluates an egglog expression after applying query-local input.
#{@program_query_opts_doc}
Raises if native egglog rejects the query-local input or expression.
"""
@spec eval!(t(), input(), String.t(), keyword() | map()) :: value()
def eval!(%__MODULE__{} = program, input, expr, opts \\ []) do
program
|> eval(input, expr, opts)
|> Common.bang("failed to evaluate egglog expression")
end
@doc """
Looks up a function row after evaluating the given argument expressions.
Returns `{:ok, nil}` when no row exists for the evaluated key.
#{@program_query_opts_doc}
"""
@spec lookup(t(), input(), String.t(), [String.t()], keyword() | map()) ::
{:ok, value() | nil} | {:error, term()}
def lookup(%__MODULE__{} = program, input, name, arg_exprs, opts \\ [])
when is_binary(name) and is_list(arg_exprs) do
opts = Common.option_map(opts)
program.ref
|> Native.lookup_program(run_source(program, input, opts), name, arg_exprs)
|> Common.from_native_lookup()
end
@doc """
Bang variant of `lookup/5`.
Looks up a function row after evaluating the given argument expressions.
#{@program_query_opts_doc}
Raises if native egglog rejects the query-local input or lookup expression.
"""
@spec lookup!(t(), input(), String.t(), [String.t()], keyword() | map()) :: value() | nil
def lookup!(%__MODULE__{} = program, input, name, arg_exprs, opts \\ []) do
program
|> lookup(input, name, arg_exprs, opts)
|> Common.bang("failed to look up egglog function")
end
@doc @snapshot_doc
@spec snapshot(t(), input(), keyword() | map()) :: {:ok, map()} | {:error, term()}
def snapshot(%__MODULE__{} = program, input, opts \\ []) do
opts = Common.option_map(opts)
{render, run_opts} = Common.snapshot_run_opts(opts)
with {:ok, result} <- run(program, input, run_opts),
{:ok, snapshot} <- Common.snapshot_from_result(result, opts, render) do
{:ok, snapshot}
end
end
@doc """
Bang variant of `snapshot/3`.
#{@snapshot_doc}
Raises if native egglog rejects the input, Graphviz rendering fails, or
artifact writing fails.
"""
@spec snapshot!(t(), input(), keyword() | map()) :: map()
def snapshot!(%__MODULE__{} = program, input, opts \\ []) do
program
|> snapshot(input, opts)
|> Common.bang("failed to snapshot egglog e-graph")
end
@doc """
Returns native egglog's tuple count for the loaded base program.
This wraps Rust egglog's `EGraph::num_tuples()` for the reusable base program.
Query-local facts inserted by `run/3` do not mutate this base. For
query-local node/class counts, visualization, or JSON inspection, use
`snapshot/3` on the query you want to inspect.
"""
@spec num_tuples(t()) :: {:ok, non_neg_integer()} | {:error, term()}
def num_tuples(%__MODULE__{} = program) do
program.ref |> Native.program_num_tuples() |> Common.from_native_count()
end
@doc """
Closes the native loaded-program resource.
"""
@spec close(t()) :: :ok | {:error, term()}
def close(%__MODULE__{} = program) do
program.ref |> Native.close_program() |> Common.from_native_close()
end
defp build_run_source(program, input, budget, mode) do
[
Common.get(input, :source) || Common.get(input, :program),
Common.get(input, :commands, []),
raw_terms(Common.get(input, :terms, %{})),
Common.get(input, :facts, []),
raw_sets(Common.get(input, :sets, [])),
raw_unions(Common.get(input, :unions, [])),
schedule_command(program, input, budget, mode),
raw_requests(Common.get(input, :requests, []))
]
|> Common.join_commands()
end
defp run_source(program, input, opts) do
input = normalize_input(input)
build_run_source(program, input, budget(program, input, opts), mode(input, opts))
end
defp budget(program, input, opts) do
program.default_budget
|> Map.merge(normalize_map(Common.get(input, :budget, %{})))
|> Map.merge(normalize_map(Common.get(opts, :budget, %{})))
end
defp mode(input, opts), do: Common.get(opts, :mode, Common.get(input, :mode, :default))
defp append_request(input, request) when is_binary(input),
do: %{source: input, requests: [request]}
defp append_request(input, request) when is_list(input),
do: %{commands: input, requests: [request]}
defp append_request(%Commands{source: source}, request),
do: %{source: source || "", requests: [request]}
defp append_request(input, request) when is_map(input) do
requests = Common.get(input, :requests, [])
Map.put(input, :requests, List.wrap(requests) ++ [request])
end
defp schedule_command(program, input, budget, mode) do
case Common.get(input, :schedule) do
nil -> mode_or_budget_schedule(program, budget, mode)
schedule -> schedule_to_command(schedule)
end
end
defp mode_or_budget_schedule(program, budget, mode) do
mode_schedule = Map.get(program.modes, mode) || Map.get(program.modes, to_string(mode))
cond do
is_binary(mode_schedule) ->
schedule_to_command(mode_schedule)
rounds =
Common.positive_integer(Common.get(budget, :rounds) || Common.get(budget, "rounds")) ->
"(run #{rounds})"
true ->
nil
end
end
defp schedule_to_command(schedule) when is_binary(schedule) do
trimmed = String.trim(schedule)
cond do
Common.blank?(trimmed) -> nil
String.starts_with?(trimmed, "(run") -> trimmed
true -> "(run-schedule #{trimmed})"
end
end
defp raw_terms(terms) when is_map(terms) do
Enum.map(terms, fn {name, expr} -> "(let #{name} #{expr})" end)
end
defp raw_sets(sets) when is_list(sets) do
Enum.map(sets, fn {call, value} -> "(set #{call} #{value})" end)
end
defp raw_unions(unions) when is_list(unions) do
Enum.map(unions, fn {left, right} -> "(union #{left} #{right})" end)
end
defp raw_requests(requests) when is_list(requests), do: Enum.map(requests, &raw_request/1)
defp raw_request(request) when is_binary(request), do: request
defp raw_request(request) when is_map(request) do
type = Common.get(request, :type)
case normalize_type(type) do
:check ->
"(check #{Common.required!(request, :expr)})"
:extract ->
expr = Common.required!(request, :expr)
case Common.get(request, :variants) || Common.get(request, :limit) do
nil -> "(extract #{expr})"
variants -> "(extract #{expr} #{variants})"
end
:print_size ->
case Common.get(request, :name) do
nil -> "(print-size)"
name -> "(print-size #{name})"
end
:print_function ->
name = Common.required!(request, :name)
limit = Common.get(request, :limit, 20)
"(print-function #{name} #{limit})"
other ->
raise ArgumentError, "unsupported egglog request type: #{inspect(other)}"
end
end
defp source_from(source) when is_binary(source), do: source
defp source_from(spec) when is_map(spec),
do: Common.get(spec, :source) || Common.get(spec, :program) || ""
defp normalize_input(%Commands{source: source}), do: %{source: source || ""}
defp normalize_input(source) when is_binary(source), do: %{source: source}
defp normalize_input(commands) when is_list(commands), do: %{commands: commands}
defp normalize_input(input) when is_map(input), do: input
defp normalize_map(nil), do: %{}
defp normalize_map(map) when is_map(map), do: map
defp normalize_map(list) when is_list(list), do: Map.new(list)
defp normalize_type(type) when is_atom(type), do: type
defp normalize_type(type) when is_binary(type) do
case String.trim(type) do
"check" -> :check
"extract" -> :extract
"print_size" -> :print_size
"print-function" -> :print_function
"print_function" -> :print_function
other -> other
end
end
end