The environment of one run: its mode, its dialect, its extensions, its clock, its limits, and the diagnostics it has produced so far.
The runtime is made of pure functions on values, and the environment is the one thing that is threaded through a run. Everything that depends on the run and not on the expression lives here:
:mode-:lenient(the default) or:strict, as inFeel.run/3.:dialect- the dialect the expression was prepared in (Feel.Dialect, D-49). It is a property of the prepared expression, not of a run: the backend puts it here. What is defined for an error in FEEL isnull; B-FEEL will read this to give the value of the type instead. S-FEEL has the semantics of FEEL, so nothing reads it for that.:extensions- theFeel.Extensionsthe expression was prepared with (D-81). Like the dialect, it is a property of the prepared expression: the backend puts it here, and a call of a function finds the function through it (Feel.Builtin.call/3).:clock- the momentnow()andtoday()see. It is fixed when the environment is built, so repeated calls in one run agree. ADateTimethat was given is kept as it is. Without one (D-148), the instant is taken as a number of microseconds ({:os_time, microseconds}), and made into aDateTimeonly whenclock/1is asked, since most expressions never ask and aDateTimecosts about as much as everything else a run does. Read it withclock/1. It isnilin the template of an executable (template/1), which has no moment yet.:limits- theFeel.Limits.t/0of the run.:entropy- where random bytes come from, for the functions of an extension that are random (D-81): a function from a number of bytes to that many bytes,:crypto.strong_rand_bytes/1by default. A run can be given its own to be repeatable. Nothing in DMN 1.6 is random.:diagnostics- what has been reported, newest first. Usediagnostics/1.:diagnostic_count- how many there are in:diagnostics, kept so that the ones that an operation reported can be told from the ones before it without counting the list (D-103). Whoever puts another list in:diagnosticsputs its length here.:declared- the errors that the checks of the bound values reported when the run began (declared/2, D-103), newest first. They are at the span of the first read of each name, and so inside the operations that read it.:call_depth- how many calls of a function value the run is in (D-82), whichmax_call_depthof the limits caps.:calls- how many calls of a function of FEEL the run has made, nested or not (D-154), whichmax_callsof the limits caps.:iterations- how many elements the iterators offor,someandeveryhave taken in the run (D-86), whichmax_iterationsof the limits caps.:links- the closures thatFeel.Recursionhas made in this run (D-200), by the link and the member they are for. A function that calls itself needs its closure at each call, and makes it at the first: the closure is a value of the run like the diagnostics, and a function value that outlives the run finds this empty in the next one and makes it again.:origin- the origin of the text that is being run (D-201): the:originoption of the run (nilunless the host gave one) for the expression of the run, and for the body of a function the origin that function has, which a function written in an expression takes from the run it is made in. A diagnostic is reported with it (Feel.Diagnostic,:origin), so that its span can be found in the text it is of.
Outcomes
A runtime operation does not touch the environment. It returns an outcome/0,
and settle/3 turns that into a value and a new environment:
{:ok, value}- the result. The value may benull: some results are defined to be.{:ok, value, env}- the result, from a function that called a function value on the way, aslist replacecalls itsmatch(D-82). The environment it gives back has what those calls reported, and the diagnostics in it that have no span are given the one of the operation.{:ok, value, [diagnostic]}- the result, with what the operation did on a value's behalf and says (the implicit conversion of a date in a subtraction, D-56): the diagnostics are reported, with the span of the operation if they have none, and then the value is the result.:missing- the result isnullbecause an operand isnull: missing data, not a mistake. Nothing is reported.{:error, diagnostic}- the result isnullbecause of a semantic error, such as a type mismatch or a division by zero (DMN 1.6, section 7.3.8). It is reported.
Which null results count as errors is D-103 (was D-103), after the TCK's flags: a null operand of
an arithmetic operator, and of <= and >=, is {:error, _} (Feel.Runtime.NullOperand), and one of
<, >, and, or and not is :missing; settle/3 is the one place that applies the split. In lenient mode a reported diagnostic is collected and evaluation goes on. In
strict mode the first error ends the run, and warnings and infos do not. run/2 is where
that ends up as {:error, error}.
Limits
settle/3 also checks the size of the value it is given against :limits, since
every backend passes its results through it. A string or list that is too long ends
the run with an error of kind :limit, in either mode. A function whose result can be as long as the product of
the sizes of its arguments asks the same question before it makes the result, with check_string_length/2 and
check_list_length/2 (D-154 does it for a loop), so that the error is the same and nothing is built that the limit
refuses (Feel.Builtin.Size).
report/2 counts the diagnostics against max_diagnostics.
Summary
Functions
Ends the run with an error, in either mode.
Ends the run, in either mode, with an error of kind :limit if a list of length elements is longer than
max_list_length, and gives the environment back if it is not (D-154).
Ends the run, in either mode, with an error of kind :limit if a string of size bytes is longer than
max_string_length, and gives the environment back if it is not.
The moment now() and today() see: the DateTime that was given as the :clock, or, without one, the instant at which
the environment was built, as a DateTime in UTC with the precision of a microsecond (what DateTime.utc_now/0 gives).
Every call on one environment gives the same moment (D-148).
Counts one element taken by an iterator of for, some or every (D-86). A run in which they have taken more than
max_iterations ends, in either mode, with an error of kind :limit.
Keeps the errors that were reported since since as the ones of the declarations of the names (D-103).
Returns the diagnostics reported so far, oldest first.
Enters a call of a function of FEEL (D-82): the depth of the run goes up by one, and so does the number of calls
it has made (D-154). A run that passes max_call_depth or max_calls ends in either mode, as one that passes
any other limit does.
Leaves a call: the depth is the one that the run was at before it (depth). The number of calls is not: they
have been made.
Builds the environment of a run.
Runs fun on the environment and keeps what it reports out of it (D-100).
count random bytes, from the :entropy of the environment. A source that gives anything but that many bytes is a
mistake of its caller and raises ArgumentError.
Reports a diagnostic.
Runs fun on the environment and returns the result in the form Feel.run/3 does.
Turns the outcome of an operation into a value and an environment.
The environment of one run, from the template of its executable (D-148) and the run options :mode, :clock, :limits
:entropy and :origin, which mean what they mean for new/1, with the same errors. It is what new/1 gives for the options of
the template and these, and does the work of checking only the options that are given: a run with none takes the clock
and nothing else.
The environment that the runs of one executable start from (D-148): new/1 with the options of the executable (its
:dialect and :extensions) and no clock, which start/2 takes at each run. An executable makes it once, when it is
compiled, and so a run does not check and build the same options again. It has no clock so that two executables of the
same expression are equal.
Runs fun with origin as the origin of the text, and gives the environment back with the origin it had (D-201).
Types
@type mode() :: :lenient | :strict
@type outcome() :: {:ok, Feel.Value.t()} | {:ok, Feel.Value.t(), t()} | {:ok, Feel.Value.t(), [Feel.Diagnostic.t(), ...]} | :missing | {:error, Feel.Diagnostic.t()}
What a runtime operation returns; see the module documentation.
@type t() :: %Feel.Runtime.Env{ budget: Feel.Runtime.Budget.t() | nil, call_depth: non_neg_integer(), calls: non_neg_integer(), clock: DateTime.t() | {:os_time, integer()} | nil, declared: [Feel.Diagnostic.t()], diagnostic_count: non_neg_integer(), diagnostics: [Feel.Diagnostic.t()], dialect: Feel.Dialect.t(), entropy: (non_neg_integer() -> binary()) | nil, extensions: Feel.Extensions.t(), iterations: non_neg_integer(), limits: Feel.Limits.t(), links: %{optional({reference(), term()}) => Feel.Value.t()}, mode: mode(), monitor: (Feel.IR.t(), Feel.Value.t() -> term()) | nil, origin: term() }
Functions
@spec abort(t(), Feel.Error.kind(), Feel.Diagnostic.t()) :: no_return()
Ends the run with an error, in either mode.
A strict run ends at its first error (report/2), and a run that passes a limit ends in
either mode; this is for a mistake that is not the expression's and that makes the run
worthless, as a value that is not allowed by the declaration of the scope. run/2 returns
{:error, error}. The error has the diagnostic and, after it, those reported so far.
iex> env = Feel.Runtime.Env.new()
iex> diagnostic = Feel.Diagnostic.new(:null_not_allowed, "x")
iex> {:error, error} = Feel.Runtime.Env.run(env, fn env -> Feel.Runtime.Env.abort(env, :input, diagnostic) end)
iex> {error.kind, error.code}
{:input, :null_not_allowed}
@spec check_list_length(t(), non_neg_integer()) :: t()
Ends the run, in either mode, with an error of kind :limit if a list of length elements is longer than
max_list_length, and gives the environment back if it is not (D-154).
A list is checked when it is finished (settle/3), and a loop that makes one checks it as it makes it, with the
number of results so far, so that a loop that passes the limit ends at the first result over it and not when
it is done. A built-in function that makes a list out of lists, as flatten and concatenate do, counts the items as it
walks them. The diagnostics reported until then are in the error, as for any limit.
iex> env = Feel.Runtime.Env.new(limits: Feel.Limits.new(max_list_length: 2))
iex> ^env = Feel.Runtime.Env.check_list_length(env, 2)
iex> {:error, error} = Feel.Runtime.Env.run(env, fn env -> {Feel.Runtime.Env.check_list_length(env, 3), env} end)
iex> {error.kind, error.code}
{:limit, :limit_exceeded}
@spec check_string_length(t(), non_neg_integer()) :: t()
Ends the run, in either mode, with an error of kind :limit if a string of size bytes is longer than
max_string_length, and gives the environment back if it is not.
settle/3 checks a string when it is finished, and a built-in function whose result is longer than its arguments
checks the size it is going to make before it makes anything, so that a result that the limit refuses is not built.
It is the same function, so the error is the same (D-154 does it for a list). size need not be the exact size
of the string to come: a count that has passed the limit already (the bytes added up until then) is enough.
iex> env = Feel.Runtime.Env.new(limits: Feel.Limits.new(max_string_length: 2))
iex> ^env = Feel.Runtime.Env.check_string_length(env, 2)
iex> {:error, error} = Feel.Runtime.Env.run(env, fn env -> {Feel.Runtime.Env.check_string_length(env, 3), env} end)
iex> {error.kind, hd(error.diagnostics).details}
{:limit, %{limit: :max_string_length}}
@spec clock(t()) :: DateTime.t()
The moment now() and today() see: the DateTime that was given as the :clock, or, without one, the instant at which
the environment was built, as a DateTime in UTC with the precision of a microsecond (what DateTime.utc_now/0 gives).
Every call on one environment gives the same moment (D-148).
iex> env = Feel.Runtime.Env.new(clock: ~U[2026-10-13 12:00:00Z])
iex> Feel.Runtime.Env.clock(env)
~U[2026-10-13 12:00:00Z]
iex> env = Feel.Runtime.Env.new()
iex> Feel.Runtime.Env.clock(env) == Feel.Runtime.Env.clock(env)
true
Counts one element taken by an iterator of for, some or every (D-86). A run in which they have taken more than
max_iterations ends, in either mode, with an error of kind :limit.
iex> env = Feel.Runtime.Env.new(limits: Feel.Limits.new(max_iterations: 2))
iex> env = env |> Feel.Runtime.Env.count_iteration() |> Feel.Runtime.Env.count_iteration()
iex> env.iterations
2
iex> {:error, error} = Feel.Runtime.Env.run(env, fn env -> {Feel.Runtime.Env.count_iteration(env), env} end)
iex> {error.kind, error.code}
{:limit, :limit_exceeded}
Keeps the errors that were reported since since as the ones of the declarations of the names (D-103).
The checks of the bound values (Feel.Runtime.BindingTypes, Feel.Runtime.AllowedValues) run before the
expression, and report at the span of the first read of each name. An operation that reads the name has that span
inside its own without its operands having reported anything, and the error explains its null operand all the
same (settle/4), in whatever turn of a loop or call of a function it is. There is one for a name at most, so
looking through them costs the names and not the diagnostics of the run.
iex> env = Feel.Runtime.Env.new()
iex> {nil, checked} = Feel.Runtime.Env.settle(env, {:error, Feel.Diagnostic.new(:value_not_allowed, "x")}, {0, 3})
iex> operand = Feel.Runtime.NullOperand.error("+")
iex> {nil, kept} = Feel.Runtime.Env.settle(checked, operand, {0, 7}, checked)
iex> Enum.map(Feel.Runtime.Env.diagnostics(kept), & &1.code)
[:value_not_allowed, :null_operand]
iex> declared = Feel.Runtime.Env.declared(checked, env)
iex> {nil, explained} = Feel.Runtime.Env.settle(declared, operand, {0, 7}, declared)
iex> Enum.map(Feel.Runtime.Env.diagnostics(explained), & &1.code)
[:value_not_allowed]
@spec diagnostics(t()) :: [Feel.Diagnostic.t()]
Returns the diagnostics reported so far, oldest first.
iex> env = Feel.Runtime.Env.new()
iex> {nil, env} = Feel.Runtime.Env.settle(env, {:error, Feel.Diagnostic.new(:division_by_zero, "x")})
iex> Enum.map(Feel.Runtime.Env.diagnostics(env), & &1.code)
[:division_by_zero]
Enters a call of a function of FEEL (D-82): the depth of the run goes up by one, and so does the number of calls
it has made (D-154). A run that passes max_call_depth or max_calls ends in either mode, as one that passes
any other limit does.
iex> env = Feel.Runtime.Env.new(limits: Feel.Limits.new(max_call_depth: 1))
iex> env = Feel.Runtime.Env.enter_call(env)
iex> {env.call_depth, env.calls}
{1, 1}
iex> {:error, error} = Feel.Runtime.Env.run(env, fn env -> {Feel.Runtime.Env.enter_call(env), env} end)
iex> {error.kind, error.code}
{:limit, :limit_exceeded}
iex> env = Feel.Runtime.Env.new(limits: Feel.Limits.new(max_calls: 1))
iex> env = env |> Feel.Runtime.Env.enter_call() |> Feel.Runtime.Env.leave_call(0)
iex> {env.call_depth, env.calls}
{0, 1}
iex> {:error, error} = Feel.Runtime.Env.run(env, fn env -> {Feel.Runtime.Env.enter_call(env), env} end)
iex> {error.kind, hd(error.diagnostics).details}
{:limit, %{limit: :max_calls}}
@spec leave_call(t(), non_neg_integer()) :: t()
Leaves a call: the depth is the one that the run was at before it (depth). The number of calls is not: they
have been made.
Builds the environment of a run.
Options
:mode-:lenient(default) or:strict.:dialect-:feel(default),:s_feel, or:b_feel.:extensions- a list of extensions or aFeel.Extensions.t/0, none by default.:clock- aDateTime, or a function of no arguments that returns one. It defaults to the current time, and a function is called once, here.:limits- aFeel.Limits.t/0. It defaults toFeel.Limits.default/0.:entropy- a function from a number of bytes to that many bytes, for the random functions of an extension. It defaults to:crypto.strong_rand_bytes/1, and is called each time a function wants bytes (the clock is read once).:origin- which text the run is of, for the host to find the span of a diagnostic in (D-201). It isnilby default. A function written in the expression keeps it, wherever the function is called.
Raises ArgumentError for an unknown option or a value of the wrong kind.
iex> env = Feel.Runtime.Env.new(mode: :strict, clock: ~U[2026-10-13 12:00:00Z])
iex> {env.mode, env.dialect, env.clock}
{:strict, :feel, ~U[2026-10-13 12:00:00Z]}
@spec quietly(t(), (t() -> {Feel.Value.t(), t()})) :: {Feel.Value.t(), t()}
Runs fun on the environment and keeps what it reports out of it (D-100).
fun takes the environment and returns {value, environment}, as the code of an expression does. In fun the
run is lenient, so an error does not end it, and what it reports is dropped: the diagnostics and the mode are the
ones of env. What it counted (the iterations and the depth of the calls) is kept, and so is the end of a run by a
limit or by abort/3, which is not a report.
iex> env = Feel.Runtime.Env.new(mode: :strict)
iex> {nil, quiet} = Feel.Runtime.Env.quietly(env, fn env ->
...> {nil, Feel.Runtime.Env.report(env, Feel.Diagnostic.new(:invalid_property, "x"))}
...> end)
iex> {quiet.mode, Feel.Runtime.Env.diagnostics(quiet)}
{:strict, []}
@spec random_bytes(t(), non_neg_integer()) :: binary()
count random bytes, from the :entropy of the environment. A source that gives anything but that many bytes is a
mistake of its caller and raises ArgumentError.
iex> env = Feel.Runtime.Env.new(entropy: fn count -> :binary.copy(<<7>>, count) end)
iex> Feel.Runtime.Env.random_bytes(env, 3)
<<7, 7, 7>>
iex> byte_size(Feel.Runtime.Env.random_bytes(Feel.Runtime.Env.new(), 16))
16
@spec report(t(), Feel.Diagnostic.t()) :: t()
Reports a diagnostic.
In lenient mode it is collected, and so is a warning or an info (D-46) in either mode. In strict mode
an error ends the run: this function does not return, and the run's run/2 returns
{:error, error} of kind :strict, whose diagnostics start with this one and go on
with the ones reported before.
A run that has reported max_diagnostics already ends, in either mode, with an error of kind :limit and the
diagnostics it has (D-154).
iex> env = Feel.Runtime.Env.new(limits: Feel.Limits.new(max_diagnostics: 1))
iex> env = Feel.Runtime.Env.report(env, Feel.Diagnostic.new(:invalid_property, "x"))
iex> env.diagnostic_count
1
iex> {:error, error} = Feel.Runtime.Env.run(env, fn env ->
...> {nil, Feel.Runtime.Env.report(env, Feel.Diagnostic.new(:invalid_property, "y"))}
...> end)
iex> {error.kind, hd(error.diagnostics).details, length(error.diagnostics)}
{:limit, %{limit: :max_diagnostics}, 2}
@spec run(t(), (t() -> {Feel.Value.t(), t()})) :: Feel.result(Feel.Value.t())
Runs fun on the environment and returns the result in the form Feel.run/3 does.
fun takes the environment and returns {value, environment}. The result is
{:ok, value, diagnostics}, or {:error, error} if the run was ended by a strict
mode error or by a limit.
iex> env = Feel.Runtime.Env.new()
iex> Feel.Runtime.Env.run(env, fn env -> {1, env} end)
{:ok, 1, []}
iex> env = Feel.Runtime.Env.new(mode: :strict)
iex> {:error, error} = Feel.Runtime.Env.run(env, fn env ->
...> Feel.Runtime.Env.settle(env, {:error, Feel.Diagnostic.new(:type_mismatch, "x")})
...> end)
iex> error.kind
:strict
@spec settle(t(), outcome(), Feel.Diagnostic.span() | nil, t() | nil) :: {Feel.Value.t(), t()}
Turns the outcome of an operation into a value and an environment.
span is the byte range of the source the operation came from; it is put on a
diagnostic that has none. A value that is too long for the limits ends the run, as
described in the module documentation. Construction checks apply to each string and list.
A null operand (Feel.Runtime.NullOperand) is not reported when an error has been reported inside the
operation already. since is the environment from before the operands of the operation were evaluated, and what
was reported after it is what they can have reported (D-103):
iex> env = Feel.Runtime.Env.new()
iex> since = env
iex> {nil, env} = Feel.Runtime.Env.settle(env, {:error, Feel.Diagnostic.new(:division_by_zero, "x")}, {1, 6})
iex> {nil, env} = Feel.Runtime.Env.settle(env, Feel.Runtime.NullOperand.error("+"), {0, 11}, since)
iex> Enum.map(Feel.Runtime.Env.diagnostics(env), & &1.code)
[:division_by_zero]An error that was reported before the operands were evaluated, in an earlier turn of a loop or by an earlier call of
the same function, has the span of this operation around it too, and is not the reason for this null. Without
since, nothing is:
iex> env = Feel.Runtime.Env.new()
iex> {nil, env} = Feel.Runtime.Env.settle(env, {:error, Feel.Diagnostic.new(:division_by_zero, "x")}, {1, 6})
iex> since = env
iex> {nil, env} = Feel.Runtime.Env.settle(env, Feel.Runtime.NullOperand.error("+"), {0, 11}, since)
iex> Enum.map(Feel.Runtime.Env.diagnostics(env), & &1.code)
[:division_by_zero, :null_operand]
The environment of one run, from the template of its executable (D-148) and the run options :mode, :clock, :limits
:entropy and :origin, which mean what they mean for new/1, with the same errors. It is what new/1 gives for the options of
the template and these, and does the work of checking only the options that are given: a run with none takes the clock
and nothing else.
Options of any other kind (a :dialect, an unknown one, one given twice) are left to new/1, which raises for the ones
that are wrong and lets the options of the executable win.
iex> template = Feel.Runtime.Env.template(dialect: :s_feel)
iex> env = Feel.Runtime.Env.start(template, mode: :strict, clock: ~U[2026-10-13 12:00:00Z])
iex> {env.mode, env.dialect, env.clock}
{:strict, :s_feel, ~U[2026-10-13 12:00:00Z]}
iex> Feel.Runtime.Env.start(template, []).mode
:lenient
The environment that the runs of one executable start from (D-148): new/1 with the options of the executable (its
:dialect and :extensions) and no clock, which start/2 takes at each run. An executable makes it once, when it is
compiled, and so a run does not check and build the same options again. It has no clock so that two executables of the
same expression are equal.
iex> template = Feel.Runtime.Env.template(dialect: :s_feel)
iex> {template.dialect, template.mode, template.clock}
{:s_feel, :lenient, nil}
@spec within(t(), term(), (t() -> {Feel.Value.t(), t()})) :: {Feel.Value.t(), t()}
Runs fun with origin as the origin of the text, and gives the environment back with the origin it had (D-201).
What fun reports has origin (Feel.Diagnostic, :origin) unless a function it calls has one of its own. It is how
the body of a function that another expression made (Feel.Value.Function.with_origin/2) says whose text its spans are in.
iex> env = Feel.Runtime.Env.new()
iex> {nil, inner} = Feel.Runtime.Env.within(env, :member, fn env ->
...> {nil, Feel.Runtime.Env.report(env, Feel.Diagnostic.new(:invalid_property, "x", span: {0, 1}))}
...> end)
iex> {inner.origin, Enum.map(Feel.Runtime.Env.diagnostics(inner), & &1.origin)}
{nil, [:member]}