Continuum.AstCheck (continuum v0.6.0)

Copy Markdown View Source

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

call()

@type call() :: {module(), atom()}

A {module, function} pair.

helper_call()

@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.

violation()

@type violation() :: %{
  mfa: call(),
  line: pos_integer() | nil,
  file: String.t() | nil,
  hint: String.t()
}

A violation found during AST scan.

Functions

check_catch_warnings(ast, env, caller_fun, caller_arity)

@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.

check_dynamic_call_warnings(ast, env, caller_fun, caller_arity)

@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.

check_helper_calls(ast, env, caller_fun, caller_arity)

@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.

forbidden_calls()

The full denylist as a map of {mod, fun} => hint.

format(violations)

@spec format([violation()]) :: String.t()

Format a list of violations into a single human-readable string suitable for CompileError.

scan(ast, file_or_env \\ nil)

@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.

trusted_stdlib()

Stdlib modules considered pure-by-construction.