Hi everyone,
I’m currently building QyCore, a lightweight open-source DAG task scheduler and execution engine written in Elixir. The origin goal is to create a backend for node-based editors similar to ComfyUI, as shown in this thread earlier.
I’ve hit a wall with a persistent Dialyzer error that I can’t seem to resolve, even after cleaning builds and explicitly defining specs.
The Context
I have a Scheduler.build/2 function that validates a Recipe struct and returns {:ok, context} or {:error, reason}. It works perfectly at runtime, all tests passed, but Dialyzer is convinced that the {:ok, …} path is impossible.
The Error
When checking Executor.Async and Executor.Serial(Scheduler.build/2’s downstream), Dialyzer complains:
lib/qy_core/executor/async.ex:17:13:pattern_match
The pattern can never match the type.
Pattern:
{:ok, _ctx}
Type:
{:error,
{:cyclic, [any()]} | {:missing_inputs, map()} | {:option_validation_failed, [any()]}}
________________________________________________________________________________
...
________________________________________________________________________________
lib/qy_core/executor/serial.ex:15:13:pattern_match
The pattern can never match the type.
Pattern:
{:ok, _ctx}
Type:
{:error,
{:cyclic, [any()]} | {:missing_inputs, map()} | {:option_validation_failed, [any()]}}
________________________________________________________________________________
This implies that my Scheduler.build/2 function (shown below) is inferred to only return {:error, …}.
The Code
defmodule QyCore.Scheduler do
# ...
@spec build(Recipe.t(), initial_params()) ::
{:ok, Context.t()} | {:error, term()}
def build(%Recipe{} = recipe, initial_params) do
# ... (initial map preparation) ...
initial_keys = Map.keys(initial_map)
# Dialyzer seems to think this `with` block never reaches the `do` block
with :ok <- validate_step_option(recipe.steps),
:ok <- Recipe.Graph.validate(recipe.steps, initial_keys) do
do_build(recipe, initial_map)
else
{:error, reason} -> {:error, reason}
end
end
# I added an explicit spec here hoping to fix it, but it didn't work.
@spec validate_step_option([Step.t()]) :: :ok | {:error, {:option_validation_failed, list()}}
defp validate_step_option(steps) do
errors =
steps
|> Enum.with_index()
|> Enum.reduce([], fn {step, idx}, acc ->
# ... validation logic (returns list of errors) ...
# If valid, `acc` remains []
# If validator not exist, no error appended
end)
case errors do
[] -> :ok
_ -> {:error, {:option_validation_failed, errors}}
end
end
defp do_build(recipe, initial_map) do
# ... logic ...
{:ok, %Context{...}}
end
# ...
end
What I’ve tried
- mix do clean, compile multiple times.
- Added explicit @spec to the private function validate_step_option/1.
- Verified that Recipe.Graph.validate/2 (in another module) has a spec that includes :ok as a return type.
It feels like Dialyzer’s success typing analysis determines that validate_step_option (or Graph.validate) can never return :ok, thus marking the do_build call as unreachable code.
Has anyone encountered this specific behavior where Dialyzer ignores an explicit :ok path in a reducer?
Any insights would be appreciated! Also, if you are interested in DAG scheduling in Elixir, feel free to check out the repo structure.
Thanks!
P.S. Warings were ignored by configure .dialyzer_ignore.exs. Hovewer, It seems trigger warning within ALL downstream functions which invoked Scheduler.build/2.
As anticipated, I want to leverage the community’s resources after showcasing the demo to create a more production-ready, fault-tolerant and Elixit-like Executor. But it seems necessary to manually declare that alarms should be ignored in all relevant modules of the new application.
Is there a more-elegant way to solve?























