Compile-time AST scanner that rejects calls known to be non-deterministic inside workflow code.
The scanner is invoked from Continuum.Workflow (and Continuum.Pure) at
module compile time. Each forbidden call produces a CompileError with a
remediation hint pointing at the deterministic equivalent.
See the :forbidden_calls/0 and :trusted_stdlib/0 functions for the
curated denylist and allowlist. Users can extend the allowlist via:
config :continuum, trusted_modules: [Decimal, Money]Calls from workflow code into helper modules that are not stdlib-trusted,
allowlisted, or marked with use Continuum.Pure emit warnings by default.
Use config :continuum, untrusted_call_severity: :error to make those
diagnostics fail compilation. Error mode raises on the first untrusted
helper module found in the current definition.
Summary
Types
A {module, function} pair.
An untrusted external helper call found during AST scan.
A violation found during AST scan.
Functions
Warn on catch arms inside workflow clauses.
Warn on dynamic-receiver calls (some_var.fun(...)) in workflow code.
Emit or raise diagnostics for external helper modules that are not trusted.
The full denylist as a map of {mod, fun} => hint.
Format a list of violations into a single human-readable string suitable
for CompileError.
Scan an AST. Returns :ok or {:error, [violation]}.
Stdlib modules considered pure-by-construction.
Types
A {module, function} pair.
@type helper_call() :: %{ module: module(), function: atom(), arity: non_neg_integer(), line: pos_integer() | nil, file: String.t() | nil }
An untrusted external helper call found during AST scan.
@type violation() :: %{ mfa: call(), line: pos_integer() | nil, file: String.t() | nil, hint: String.t() }
A violation found during AST scan.
Functions
@spec check_catch_warnings(Macro.t(), Macro.Env.t(), atom(), non_neg_integer()) :: :ok
Warn on catch arms inside workflow clauses.
Continuum suspends a workflow by throwing a control tuple after the
pending effect has been journaled; a catch arm (especially _, _ -> or
:throw, _ ->) can intercept it. The runtime detects the swallow and
fails the run with Continuum.SuspendLeakError, but the right fix is in
the code: use rescue/after, or re-throw the engine's control tuples.
@spec check_dynamic_call_warnings(Macro.t(), Macro.Env.t(), atom(), non_neg_integer()) :: :ok
Warn on dynamic-receiver calls (some_var.fun(...)) in workflow code.
A call whose receiver is a runtime value cannot be checked against the
denylist — m = DateTime; m.utc_now() would silently bypass the scanner.
Plain field access (input.seed, no parentheses) is not flagged.
@spec check_helper_calls(Macro.t(), Macro.Env.t(), atom(), non_neg_integer()) :: :ok
Emit or raise diagnostics for external helper modules that are not trusted.
Activity calls are skipped because their side effects are deliberately routed through the DSL and journal. Same-module calls are also skipped; their bodies are scanned by the workflow compiler hook.
The full denylist as a map of {mod, fun} => hint.
Format a list of violations into a single human-readable string suitable
for CompileError.
@spec scan(Macro.t(), Macro.Env.t() | String.t() | nil) :: :ok | {:error, [violation()]}
Scan an AST. Returns :ok or {:error, [violation]}.
Pass the caller's %Macro.Env{} (as Continuum.Workflow and
Continuum.Pure do) so unqualified calls are resolved through the imports
in scope — import DateTime followed by a bare utc_now() is caught the
same as the qualified spelling. Passing just a file string keeps
diagnostics located but limits local-call detection to the auto-imported
Kernel denylist.
Stdlib modules considered pure-by-construction.