# `Continuum.AstCheck`
[🔗](https://github.com/Yyeger/Continuum/blob/main/lib/continuum/ast_check.ex#L1)

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.

# `call`

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

A `{module, function}` pair.

# `helper_call`

```elixir
@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`

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

A violation found during AST scan.

# `check_catch_warnings`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

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

# `scan`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
