Skip to content

Adds host function providers for guards - #457

Merged
johnnyt merged 2 commits into
mainfrom
st-g648-guard-function-providers
Oct 12, 2026
Merged

johnnyt merged 2 commits into
mainfrom
st-g648-guard-function-providers

Conversation

@johnnyt

@johnnyt johnnyt commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Why

A host building rich charts wants pure named guards it can call from cond and expr - a blast radius over a list of paths, business hours at a timestamp in a zone, whether two intervals overlap - instead of precomputing every such fact into the event payload. predicator already supports this through Predicator.FunctionProvider. statifier did not expose it: every evaluation context starts from one compile-time constant holding In/1 and the builtins, and no option added to it.

What

  • :function_providers, a list of Predicator.FunctionProvider modules, accepted by Statifier.compile/2, Statifier.MachineState.new/2 and Statifier.Session.start_link/2. nil or [] keeps today's behaviour.
  • Evaluation. Statifier.Evaluator.Functions.from_providers/1 resolves the list once into the full dispatch map, and it is stamped onto a new MachineState functions field. Statifier.Evaluator.context/1 swaps that map into the constant context with one field write. With the field nil, the constant is used unchanged. A provider may not shadow In or a builtin.
  • Resume and replay. Statifier.Position drops the field like invoke_types and send_types, and MachineState.put_function_providers/2 re-stamps it. A session started with :resume re-stamps the list itself. A recording keeps the list as module name strings, and Statifier.Replay stamps it on both of its start branches.
  • Compile-time check. Statifier.Compiler.FunctionCalls.check/2 refuses any call whose name is not In, a builtin or a declared function, with {:unknown_function, name, source} at the element that holds the call. The declared names persist as :function_names in compile_opts, so a chart blob recompiles without the provider modules. :function_names is also accepted as an input.
  • Refusals before boot. Session.start_link/2 refuses a list that does not resolve before it spawns anything: {:error, {:function_providers, reason}}.
  • Docs. docs/extending.md gains "Host functions for cond and expr", which states the purity contract: a function reads only its arguments and the host term, does no I/O and reads no clock, so a replay or a resume evaluates it the same.
  • Record. ADR-0078 records the decision at proposed.

Notes

Review round: wurk-diff-critic and wurk-test-critic, one fresh round over origin/main...9400c10, returned 12 findings (4 diff, 8 test; none must-fix). 11 fixed in e595eeb: the call check reads only top-level call instructions, so a literal list shaped like a call is no longer refused; Statifier.Chart.from_binary/1 answers :not_a_statifier_blob for a blob whose options compile/2 would raise on instead of raising; the Statifier.Compiler.Error moduledoc indentation is restored; and new tests pin every expression site, error order, dedupe and location, option shape errors, two providers through the recording codec, from_binary/2 and import/2 blanking the functions, an unknown provider module in a recording, and no key when the list is empty. 1 declined: a chart given as <invoke> content cannot call a host function, because children do not inherit the list; this is documented in the guide and ADR-0078 and left for a later change. An advisory that the builtins Math.random and Date.now are themselves impure is outside this change and was not acted on. Head reviewed: 9400c10; head after fixes: e595eeb.

  • Behaviour change. A chart that calls a function nothing declares used to compile and raise error.execution at every evaluation of that call. It now fails to compile. The check stays at compile time as built, ruled by the operator, 2026-10-12. The changelog fragment lists it under Changed.
  • Not inherited by children. A child session started for an <invoke> does not inherit the list. That is left for a later change.
  • Formats. The recording format version does not move: a recording with no provider is byte-identical to one written before this change.
  • Test edit. Two existing row S19 tests used f() as a non-assignable namelist entry. They now use trim(copies), a builtin call with the same intent. The MachineState field-set test counts the new field.
  • Gate. On 9400c10, mix quality --profile merge passed, ADR judge included. On e595eeb, the full gate is green and attested. A full conformance run fails the same three optional cases as main, and the regression ratchet passes.
  • Sabotage. Every new test was sabotaged against the lines it covers, and each one went red.
  • Load flake. Several full-gate runs on a heavily loaded machine failed only on Basic HTTP cases, where the expected event never arrived in time. The same cases pass when run alone, and the gate is green on retry.

Closes st-g648

A host registers named functions a chart may call from cond and expr
with :function_providers, a list of Predicator.FunctionProvider
modules, on Statifier.compile/2, MachineState.new/2 and
Session.start_link/2.

- Evaluator.context/1 swaps the stamped map into the constant
  context's functions field; nil keeps the constant unchanged.
- MachineState gains a functions field; Position drops it and
  put_function_providers/2 re-stamps it; a resumed session, a
  recording and Replay all carry the list.
- compile/2 refuses a call to a name that is not In, a builtin or a
  declared function ({:unknown_function, name, source}); the
  declared names persist as :function_names in compile_opts.
- A provider may not shadow In or a builtin.

ADR-0078 records the decision as proposed; docs/extending.md gains
the guide section. No conformance result moves.

Refs: st-g648
- FunctionCalls reads only top-level call instructions, so a literal
  list shaped like a call is data, not a call.
- Chart.from_binary refuses a blob whose options compile/2 would raise
  on (:function_providers, or :function_names not a list of strings)
  as :not_a_statifier_blob, so it never raises or calls a module.
- Restores Compiler.Error's moduledoc indentation.
- Tests pin every expression site the check reads, error order,
  dedupe and location, option shape errors, two providers' order
  through the recording codec, from_binary/import blanking functions,
  an unknown provider module in a recording, and no key when empty.

Refs: st-g648
@johnnyt
johnnyt merged commit 8f1ff73 into main Oct 12, 2026
1 check passed
@johnnyt
johnnyt deleted the st-g648-guard-function-providers branch October 12, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant