# How `dmn` uses FEEL

`dmn` is a layer above FEEL. It owns the model (the XML, the graph of decisions, the boxed expressions) and FEEL owns what an expression means: how names are found, how values are typed and converted, what the operators do, how a run is bounded. The two meet in a few places. This page says where, what each side hands to the other, and how a node that FEEL did not have (a decision table, a value with a declared type) was added to FEEL so that the rest of it, the analyzer, the optimizer and both backends, treats it as it does the nodes it always had.

Read it before changing either side of the seam, or before adding a boxed expression that FEEL has no node for.

The current [DMN decisions](decisions.md) explain this layer's choices. The [FEEL decisions](https://github.com/FeeJai/BDM/blob/main/apps/feel/docs/decisions.md) cover AST preparation (D-174), table nodes (D-175), declaration boundaries (D-190) and linked recursion (D-188). The [README](../README.md) covers the public API; the [XML and import policies](decisions.md#xml-and-imports) describe model loading and resolution before FEEL preparation.

[![The path from a DMN model to a prepared FEEL expression, in six steps, with the calls that cross between dmn and feel](assets/pipeline.svg)](assets/pipeline.svg)

The numbers in the figure are the steps of this page. (The figures are SVG: click one to open it at full size.) Steps 2 to 4 are done once per *unit*, a unit being one element of the decision requirements graph (a decision, a business knowledge model, a decision service); the plan of a model says which units a unit reads.

## What crosses the seam

| FEEL gives | Used by | For |
| ---------- | ------- | --- |
| `Feel.ItemDefinitions` (`new/1,2`, `scope/2`, `variable/2`) | `Dmn.Compiler.Types`, `Dmn.Compiler` | the item definitions of a model as FEEL types, every constraint of a type as a list (`variable/2`, D-190), and the typed scope of a unit |
| `Feel.Scope` (`new/1`, `put_name/3`, `fetch_name/2`) | `Dmn.Compiler`, `Dmn.Compiler.Env` | the names a unit may read, each with its declared type |
| `Feel.parse/2` with `:scope`, `:scope_index`, `:entry` | `Dmn.Compiler.Fragment` | the syntax tree of each text of the model |
| `Feel.Syntax.ScopeIndex` | `Dmn.Compiler.Env` | what the parser needs of a scope, made once and added to a name at a time (D-177) |
| `Feel.AST` (the nodes) and `Feel.AST.shift/2` | `Dmn.Compiler.Expression`, `.Table`, `.Tree`, `.Fragment` | the tree of a unit, built from nodes and from parsed fragments |
| **`Feel.prepare_ast/3`** | `Dmn.Compiler`, `Dmn.Run.invoke/4` | analysis, typing, optimization and compilation of a tree that was not parsed (D-174) |
| `{:decision_table, …}` and `{:declared, …}` | `Dmn.Compiler.Table`, `.Expression` | the two nodes that FEEL gained for a layer above it (D-175, D-190); a declared value is held to every constraint of its type (D-190) |
| `Feel.Expression.unoptimized_ir/1`, `Feel.warnings/1` | `Dmn.Compiler.Checks`, `Dmn.Compiler` | the IR with its types, and what was found while preparing |
| `Feel.Rules` (`overlaps/3`, `dead/3`, `coverage/3`) | `Dmn.Compiler.Checks` | questions about a table, asked before it runs |
| `Feel.run/3` | `Dmn.Run` | the run of a prepared expression, with `Feel.Limits`, `:mode`, the clock |
| **`Feel.Recursion.link/1`, `run/3`** | `Dmn.Run` | the units of a recursion group, each run with links to the others it reads (the `:recursion` feature, DD-22, DD-29, D-188) |
| `Feel.run/3` `:origin`, `Feel.Value.Function.with_origin/2`, `Feel.Diagnostic.origin` | `Dmn.Run` | which text the span of a diagnostic is a range of, when a run is of more than one: the key of the unit that is run, which a function written in it keeps wherever it is called (D-201) |
| `Feel.ItemDefinitions.new/2` with `recursive: :guarded` | `Dmn.Compiler` | finite graphs for guarded recursive data, with complete finite-value membership and descendant constraint checks when the recursion feature is enabled (D-189) |
| `Feel.Type.Conversion`, step `:function_signature` | the types `dmn` declares | a built-in with several signatures, given where a function type is declared, becomes the function of the signature that fits (D-197) |

The table lists the main semantic interfaces. `dmn` also uses `Feel.Worker` and its bounded preflight utilities for containment and compact input selection. No function of FEEL depends on `dmn`.

## Why a tree and not text

DMN 1.6 says that boxed expressions "can be mapped to FEEL text" (10.2.2). The obvious engine does that: it writes the FEEL of a context, a function, a table, and parses it. That loses what the model knows. A name has to be escaped. A diagnostic points into text that nobody wrote. A table of hundreds of cells is one long string, parsed again for every evaluation. And a decision table has no FEEL: a `UNIQUE` table with two matching rules is `null` *with an error that names the rules*, a `PRIORITY` table orders by the position of an output among the output values, and the outputs of a rule that does not count are not computed. Written as FEEL these need helper functions, lose the type of the result, and report errors that belong to the helpers (this was tried, and is why D-175 exists).

So `dmn` builds the tree itself, from the nodes of `Feel.AST` and from the parses of the texts that the model does contain, and gives it to `Feel.prepare_ast/3`. The meaning of every part stays FEEL's.

## A unit, end to end

A model with one input and one decision table:

```xml
<inputData id="customer" name="Customer type"><variable name="Customer type" typeRef="string"/></inputData>
<decision id="discount" name="Discount">
  <variable name="Discount" typeRef="number"/>
  <informationRequirement><requiredInput href="#customer"/></informationRequirement>
  <decisionTable id="t1" hitPolicy="UNIQUE">
    <input id="i1"><inputExpression typeRef="string"><text>Customer type</text></inputExpression>
      <inputValues><text>"Gold", "Silver", "Bronze"</text></inputValues></input>
    <output id="o1" typeRef="number"/>
    <rule id="r1"><inputEntry><text>"Gold"</text></inputEntry><outputEntry><text>0.2</text></outputEntry></rule>
    <rule id="r2"><inputEntry><text>"Silver", "Bronze"</text></inputEntry><outputEntry><text>0.1</text></outputEntry></rule>
  </decisionTable>
</decision>
```

`Dmn.compile/2` makes one unit for the decision `Discount`. Its prepared expression has this IR (`Feel.Expression.unoptimized_ir/1`, abridged; the types are what the analyzer found):

```text
{:conform, type: number in {0.1, 0.2}, or null}                        result_type: number   (the variable of the decision)
└─ {:conform_bindings, :enforce, [{"Customer type", {1, 14}, :string}], …}   the scope: the input is read as a string
   └─ {:decision_table, span {0, 0}, :unique, nil, inputs, rules, outputs}   the node dmn built
        inputs   [{ {:var, "Customer type"},  {:unary_tests, [? = "Gold", ? = "Silver", ? = "Bronze"]} }]
        rules    [{"r1", [{:unary_tests, :positive, [? = "Gold"]}],               [{:conform, {:const, 0.2}, :number}]},
                  {"r2", [{:unary_tests, :positive, [? = "Silver", ? = "Bronze"]}], [{:conform, {:const, 0.1}, :number}]}]
        outputs  [{nil, nil, nil}]                                              (no name, no order, no default)
```

Four things in it came from `dmn`: the type to convert the result to, the typed scope that turned the read of `Customer type` into a conversion, the table node, and the declared column: `<output typeRef="number"/>` makes each output entry a declared value, which is the `:conform` around it (DD-25). Everything else is what FEEL makes of the parts: `?` in an entry is the value of its column, typed `string`, so an entry is a test of a string. The source of the unit is its texts, one after another (next section). Running it:

```elixir
Dmn.evaluate(compiled, "Discount", %{"Customer type" => "Silver"})    #=> {:ok, Decimal.new("0.1"), []}
Dmn.evaluate(compiled, "Discount", %{"Customer type" => "Platinum"})  #=> {:ok, nil, []}
```

The second is `null` because no rule counts and there is no default (DMN 1.6, clause 8 and 10.3.2.10). This should not come as a surprise: `compile/2` already reported it, with an input that triggers it, as the `:incomplete_table` diagnostic of the decision (see "Step 5").

## Step 1: the types

`Dmn.Compiler.Types` hands the item definitions of a model, and of the models it imports under the names of the imports, to `Feel.ItemDefinitions.new/2` as plain data, with default cycle rejection or `recursive: :guarded` when the caller enables recursion. From them, `Feel.ItemDefinitions.scope/2` makes the `Feel.Scope` of a unit: each name it reads (an input, a decision) with its declared type, allowed values and type constraints. A business knowledge model or a decision service it invokes is in the scope as a function type. The model is the only source of those types, and `dmn` has no typing of its own: what a `typeRef` means, what an implicit conversion is and how a value that does not fit becomes `null` with an error are FEEL's (DMN 1.6, 10.3.2.9; `D-138`). The following declarations determine what `dmn` hands over:

* **Every constraint of a type.** A type can have its own allowed values, those of its components, a type constraint, a second list. FEEL checked a declared value against one list of allowed values, so `{age: 200}` satisfied a `tPerson` whose `age` is `[0..150]`. `Types.result/2` and `declared/2` now hand over the list that `Feel.ItemDefinitions.variable/2` gives, and a declared value is held to all of it (D-190, DD-25).
* **What a function gives.** A function item names its result type with `outputTypeRef`. FEEL's arrow keeps the type but not its constraints, so `Types.function_outputs/1` and `output_declared/3` recover the named result's constraints for the body (DD-25).
* **What a caller sees** (DD-33). A business knowledge model uses `Types.knowledge_type/4`: its formal parameters determine the inputs, and a function-typed variable contributes the function item's output as the result. For `functionItem outputTypeRef="number"`, a numeric `K(10)` therefore has a numeric result, rather than a function result. Aliases and imports use the same rule; parameters in the function item do not override the model's formal parameters. A higher-order model names a function item whose output is another function type. A decision service uses `Types.service_type/5`: one output keeps its decision variable's type, including a function-valued output; several outputs form a context keyed by decision name, and no outputs form an empty context. The service's own variable can declare the final result, unwrapping only its outer function-item layer. A result other than `Any` controls the caller type after conversion; service input declarations determine parameters. The generated service body enforces a derived recursive result too, so its runtime signature agrees with the declaration used by calling decisions. Individual output constraints stay with their decisions.
* **A built-in where a function type is declared** (D-197, TCK 0092 #014). A parameter, a name or a result declared `function<number> -> number` and given `abs`, which is made of signatures that differ in their types (a number, a duration), was refused: an overload is not an instance of one of its arrows (D-171, D-171), and making it one would be unsound, since a function type holds for any arguments and `abs(duration)` gives a duration. So `instance of` and the analysis are as they were, and the value is **converted**: `Feel.Type.Conversion` has a last step, `:function_signature`, that gives it the function of the one signature that fits, and the call converts its arguments to the parameter types as a call of a function written in FEEL does.
* **A type that refers to itself** is an error by default. The `:recursion` feature enables guarded data graphs: every cycle crosses a context member or list element, and no cycle crosses a function signature. Membership and component constraints visit all finite descendants, with ordinary null exemptions and no implicit conversions inside fields. Analysis uses a bounded upper projection; it cannot treat inclusion in that projection as proof of membership or unchanged conversion. Alias-only and function-type cycles remain errors. The legacy FEEL `recursive: :any` mode is approximate and is not what the DMN feature uses (D-189, DD-22).
* **What the caller switches on.** `Dmn.compile(model, features: [:recursion, :dmn_1_1_types])` (DD-29) is the one option for what DMN 1.6 does not have: each is off unless the caller asks, none can be requested by a model, and each adds what was an error before. `:recursion` is the engine's own (Step 6); the others are extensions (`Dmn.Extension`, DD-28). The first of them appends the names that DMN 1.1 gave the date and time types (`dateTime`, `dayTimeDuration`, `yearMonthDuration`) to the item definitions, only where the model defines no type of that name, so that every place that reads a `typeRef` sees them and none has to map them. `recursion: true` and `extensions: [...]` still mean what they meant, and add to `features:`. The engine is DMN 1.6 unless the caller asks for more.

[![Four places where the types of a model enter FEEL: the variable of a decision, the scope, a declared value inside the tree, and function parameters](assets/types.svg)](assets/types.svg)

The four places are the rows of the figure. The first, second and fourth use options and a scope that `prepare/2` always had. The third needed a node (`{:declared, …}`, below), because a decision's result was the only value that FEEL could convert and verify against a declaration, and a boxed context has an entry with a `typeRef` in the middle of an expression, and so does the output column of a decision table.

## Step 2: parsing the texts

Every text of a model is parsed alone, where it stands, by `Dmn.Compiler.Fragment`:

```elixir
Feel.parse(text, scope: env.scope, scope_index: env.index, entry: :expression | :unary_tests, limits: env.limits)
```

* **`:entry`** is `:expression` for a literal expression, an input expression and an output entry, and `:unary_tests` for an input entry and for the input values of a column. A table's entries are not expressions. The output values of a column are read as a list expression (`"High", "Low"` is `["High", "Low"]`), since they are the order of the priority policies.
* **`:scope`** is what lets the parser read a name of several words. `Customer type` is one name only because it is in the scope, and the parser matches the longest name that the tokens make. The scope has the names of the unit's requirements, and, inside a boxed expression, the names that the expressions around bind: the entries of a context, the parameters of a function, the variable of a loop (`Dmn.Compiler.Env.bind/2`). Those are not typed here. The analyzer finds them again from the tree, with their types.
* **`:scope_index`** exists for large scopes. The parser takes the names of a scope, lexes each to count its tokens, and builds a set, for every text. That is negligible for one expression but quadratic in the size for a context of 4,000 entries, each a text parsed against a scope that grows. `Feel.Syntax.ScopeIndex.new/2` does it once, `put_name/2` adds a name in time proportional to its tokens, and `Feel.parse/2` reads from the index instead of from the scope (D-177). A parse that is given no index behaves as before.
* **What cannot be parsed** is not an error of the model. It is a diagnostic of the element, with the place in it, and the fragment is `null` (an expression) or tests that nothing satisfies (an input entry: a rule with an entry that cannot be read never matches), in line with DMN 1.6, where an expression that cannot be evaluated gives `null` (7.3.8). The unit is prepared all the same.

## Step 3: building the tree

### The virtual source

FEEL reports where it found a problem as a byte range of one text (`Feel.Diagnostic`, a span). A unit has many texts, and syntax that has none. So a unit is prepared from a **virtual source**: the texts one after another, a newline after each, with the tree of each parse moved to where its text is (`Feel.AST.shift/2`), and a **source map** from a range of the source to a place in the model (`Dmn.Compiler.Source`).

[![The texts of a decision table, the glue parts of its nodes, the virtual source they make, and the source map that takes the span of a diagnostic back to a rule and an entry](assets/virtual-source.svg)](assets/virtual-source.svg)

* A text is added with its origin, `{element, path}`, where the path is a place in the element: `[:expression, :rule, 1, :output_entry, 1]`. `Source.add/3` returns where it starts, which is the `offset` of `shift/2`.
* A node of the tree that stands for a boxed expression has no text. It gets a **glue** part: one byte of its own, an empty text and a newline, and the zero-width span at its start. A diagnostic on it is found in that part, so it has a place in the model like any other. The table has one, and so has the `:declared` node around each output entry (the two `g` parts of the figure), with the path of the entry it declares.
* `prepare_ast/3` is given the concatenation as its `source`. It does not read it, only keeps it, and the spans of the diagnostics are ranges of it. `Source.find/2` turns a span into `{element, path, offset, box}` by a search over the parts (`locate/2` and `box/2` are its halves), and `Dmn.Diagnostic.from_feel/4` turns a FEEL diagnostic into a diagnostic of the model. This holds for the diagnostics of preparation and for those of a run.

### What a boxed expression becomes

The tree is built directly, as the analyzer wants it: a name is its list of tokens, a key is a string, and nothing is escaped.

| Boxed expression | Tree |
| ---------------- | ---- |
| literal expression | the parse of its text |
| any expression with a `typeRef` | `{:declared, …}` around it |
| context | `{:context, …}`. The entry with no variable is the result, selected from the context under a key that no text can write |
| function definition (FEEL) | `{:function, …}` with the parameters typed by their `typeRef` |
| invocation | `{:call, …}` with the arguments by name |
| relation, list | a list of contexts, a list |
| conditional, filter, for, some, every | `{:if, …}`, `{:filter, …}`, `{:for, …}`, `{:quantified, …}`; the first, second and the quantifiers are marked `boxed: true` (FEEL D-212, DD-38) |
| decision table | `{:decision_table, …}` (a node of FEEL's own tree), each output entry and default output entry a `{:declared, …}` of its column (DD-25) |
| decision service | `function(inputs) { decisions…, result }.result`, each decision a declared value that reads what it requires and no other input or decision of the service (DMN 1.6, 10.4); the decisions report their own texts and their faults are the service's (DD-34) |
| business knowledge model | a `{:function, …}` that is run once to give a closure, so calls, conversions and limits are FEEL's |

Two things are done on the way, and both are about names:

* **The names that the compiler adds** (the key of a context's result, the aliases of a service's outputs, the parameters of an `invoke`) begin with U+0001. XML 1.0 does not allow it in a document and the lexer of FEEL does not read it as part of a name, so no name, id or text of a model can be one, or collide with one (`Dmn.Compiler.Tree.internal/1`).
* **An entry that reads the name of its own key.** `{context01: context01}` is a cycle in FEEL. In a boxed context it is the decision called `context01` (TCK 1146, 0085; DD-12). `Dmn.Compiler.Expression` binds the outside value to another key of a context around it, and `Dmn.Compiler.Rename` renames the reads of the entry to that key. Every context of the tree has a span of its own, because `Feel.Analysis.References` finds the entries of a context by it.

## The two nodes FEEL gained

Both are in `Feel.AST` and have no text: a layer above FEEL builds them, and the parser never does. A node is trusted to be well formed (a node that is not one, or a name that is not a list of tokens, raises); what is not trusted is meaning, which the analyzer checks as it does for a parsed tree.

### `{:decision_table, meta, policy, aggregation, inputs, rules, outputs}`

| Part | Is |
| ---- | -- |
| `policy` | `:unique`, `:first`, `:any`, `:priority`, `:output_order`, `:rule_order` or `:collect` |
| `aggregation` | `nil`, or `:sum`, `:count`, `:min`, `:max` for a `:collect` table of one output |
| `inputs` | the columns: `{expression, input_values}`, the input expression and the unary tests that its values are expected to satisfy (`nil` for none) |
| `rules` | `{id, entries, results}`: the id for messages, one entry for each input (`{:unary_tests, …}`, or `nil` for `-`) and one output entry (an expression; for a column that declares a type, a `{:declared, …}` around it) for each output |
| `outputs` | `{name, order, default}`: the name (`nil` for the one output of a table that has one), the output values as a list expression (the order of `:priority` and `:output_order`), and the default output entry |

It is a node and not a translation (`D-175`) because its meaning is partly FEEL's, and the rest is not FEEL at all. The entries are unary tests of the input of their column, the outputs are expressions, and what the analysis knows of them gives the type of the table: those are FEEL's. Which rules count, which hit policy chooses among them, and that the outputs of the others are never computed belong to `Feel.Runtime.DecisionTable`.

### `{:declared, meta, expression, type, allowed_values}`

The value of an expression that something declares a type and allowed values for, as a decision declares them for its result. `dmn` builds it where it knows the declaration: the value of an entry of a boxed context, of a decision inside a decision service, the body of a function, and any boxed expression that has a `typeRef`, and each output entry of a decision table whose column declares a type. The value is converted to the type (a list of one element is its element for a type that is not a list, a value is a list of itself for a list type), and is `null` with an error when it does not conform or does not satisfy the allowed values (DMN 1.6, 10.3.2.9). `type` is a `Feel.Type.t()` whose names are those of the scope, and `allowed_values` is the source of a list of unary tests, or, since D-190, the list of every constraint of a declared type: each one for the value or, down its `steps` (a key of a context, `:each` for the elements of a list), for a member of it. All of them have to hold, and the first one that rejects the value makes it `null` with the error that says where. A value that is `null`, and a member that is not there, are exempt.

## Step 4: preparing

`Feel.prepare_ast(tree, source, options)` takes the options of `prepare/2` except `:entry` and `:dialect`, which are those of a tree: an expression, and `:feel`. `dmn` gives it `scope:`, `limits:`, and, for a decision, `result_type:` and `result_allowed_values:`, the declaration of its variable (the type, and every constraint of it). The shared preparation pipeline (`feel/preparation.ex`) analyzes, optimizes and compiles the tree as it does a parsed one. It records conservative binding dependencies before optimization, including declaration checks and returned closures. Recursive groups receive complete peer evidence during compilation, so their compact runtime expressions do not need saved ASTs or preparation options. The semantic diagrams below omit the outer shared-worker boundary; public compilation includes every stage under the same deadline and heap policy.

### How the analyzer learns a node

For the analyzer, a new node is a clause of `analyze_clause` (a private function) in `Feel.Analysis.Analyzer`.

[![The analyzer clauses for a declared value and for a decision table, and the type of a table by hit policy](assets/analyzer.svg)](assets/analyzer.svg)

**`:declared`** reuses existing code. The result of a whole expression is converted and verified when the scope or the options declare one (`verify_result/3`, D-138). The clause analyzes the expression, then runs `verify_result/3` with the declaration of the node in place of the declaration of the expression. The nodes it makes are the `:conform` and `:allowed` that the optimizer, both backends and the audit already handle. Since D-190 the declaration may be a list of constraints: each is an `:allowed` node around the one before, and one that is for a member of the value (a component of a context, the elements of a list) has `meta.path`, the steps down to it. That is the one thing that the backends learned for it (`Feel.Runtime.AllowedValues.verify_member/7`, a clause of `Walk.value_of/3` and of `Emitter.expression/2`). And since D-190 the type of an `:allowed` node has `null` in it unless its tests are certainly satisfied, since a value that a constraint rejects is `null` to whatever reads it afterwards.

**`:decision_table`** is analyzed in an order that the types need:

1. `Feel.Analysis.Tables.validate/6` checks the shape: a known policy and aggregation, an aggregation only for a `:collect` table of one output, named outputs when there are several, and in every rule one entry for each input and one output entry for each output. A node that is not a valid table is the error `:invalid_decision_table` at the span of the node, whoever built it.
2. The input expressions are analyzed first, because their types are the types of the columns.
3. The input values of the columns are analyzed next (`analyze_table/5`): tests of a member of the column's type, like the entries. A `-` is satisfied by the input values of its column, if it has some, so a rule with a `-` there says what they say.
4. Each rule is analyzed as a unit (`analyze_rule/3`), because what its entries say of the inputs holds in its outputs (D-195). A test is analyzed in the context of a member of its column's type, the way the tests of `in` are: `?` has the type of the column, so `> 18` in a number column is a test of a number, and a test that no value can satisfy is reported. Then the entries become facts about the input expressions, as the condition of an `if` does for its branches: a positive entry is `input in tests`, `not(…)` is its outcome `false` (D-194), and a `-` is the input values of its column; the facts of the columns are met, and the outputs of the rule are analyzed in the context that has them (`rule_context/4`, `refine/3`). So in `age >= 18 → age - 18` the `age` of the output is a number from 18, whichever hit policy the table has, since an output is computed only for a rule that matched. The default outputs and the output values are not under a rule and are analyzed in the context around. A rule whose outputs read no name, which is true of nearly every table, makes no facts. What the parts read, and what they report, is merged into the result of the node, and the errors of the entries and of the outputs of a rule are found together.
5. `refill/2` puts the answers back where the parts came from (an absent part stays absent).
6. `Feel.Analysis.Tables.result_type/5` makes the type of the table from the policy and the types of the outputs of all the rules and defaults. `null` is always in it: a table is `null` when no rule counts and there is no default, and when its hit policy breaks.

The result is an IR node of the same shape, with the type in its metadata. Other traversals of the tree (`Feel.Analysis.References`, the quoted names, `Feel.AST.shift/2`) do not need a clause for it: they walk any tuple whose second element is a metadata map with a `:span`, which is why the node has that shape.

### The optimizer and the backends

[![The optimizer, the two backends and the shared runtime of a decision table](assets/backends.svg)](assets/backends.svg)

* **The optimizer** (`Feel.Optimizer`) rewrites the parts of the table and never folds it: the entries are tests of inputs that are read when it runs.
* **The interpreter** (`Walk.value_of/3`) evaluates the input expressions in order, then gives the runtime a function for each part: an entry is `fn input, env -> … end` that evaluates its tests with the input bound as `?`, and an output is `fn env -> … end`.
* **The BEAM emitter** (`Emitter.expression/2`) emits the same funs as abstract format (`entry_function/2`, `outside_function/2`) and a call of `DecisionTable.run/5`. It puts no semantics into the generated code, as it does for every operator.
* **The runtime** (`Feel.Runtime.DecisionTable.run/5`) is where the hit policies are implemented, once, for both. Because the entries and outputs are functions, the entries of a rule after the first one that is not satisfied are not run, and the outputs of the rules that do not count never are. An error (several rules of a `UNIQUE` table, different outputs of `ANY`, an output that is not among the output values of a priority table) is `null` for the table with a diagnostic that names the rules, at the span of the node, and ends a run in `mode: :strict`.

`test/feel_decision_table_test.exs` runs every policy on both backends, which must give the same value and the same diagnostics. `dmn` runs on the interpreter unless the caller asks for the BEAM backend with `Dmn.compile(model, backend: :beam)` (DD-41); that one creates modules and atoms that are never freed and is for models that the caller trusts.

## Step 5: questions about a table

The input entries of a table are unary tests, and the input of a column has the type that the analysis gave its expression, so a rule is a set of inputs and the questions about a table are questions about sets. `Dmn.Compiler.Checks` asks `Feel.Rules` after the unit is prepared:

1. It reads `Feel.Expression.unoptimized_ir/1` and finds the `:decision_table` nodes, wherever they are.
2. Each node's span goes through the source map to a place in the model, and the place to the table of the model that it came from.
3. The columns are the types of the inputs without `null` (a `null` input matches no rule). A bound containing only null falls back to non-null `Any` for checking. These are abstract column domains; a witness need not be reachable through the rest of the decision model. Prepared wildcard entries use the column's `inputValues` when present; explicit entries use their own test, unconstrained by `inputValues`. Wildcard identity comes from the prepared IR, so empty or commented wildcards and ignored column values agree with runtime behavior. Each query receives the unit's named type definitions through `types: map`, preserving aliases, imported types and multiword type parsing even when analysis resolved or folded away `instance of`. Tests that read model or local bindings, filter fields, dynamic names, extension calls or indirect calls remain open (`:unknown` cells in `Feel.Rules`), since reparsing their source without those bindings can resolve a different function. Their possible inputs are the whole column and their certain inputs are empty. A call lowered from a builtin alias keeps its binding provenance, so the original spelling cannot be reinterpreted as a different builtin. Indirect calls stay open even without a direct extension marker, since builtin function values use the runtime extensions. This keeps independent proofs available without evaluating model functions (DD-43). For example, with column `inputValues` of `1`, a wildcard row accepts only `1`, while an explicit `2` row still accepts `2`. The second row is not dead under `FIRST`, and the two rows are disjoint under `UNIQUE`. Treating `-` as unrestricted would give the wrong proofs.
4. `Feel.Rules.overlaps/3` (an overlap in a `UNIQUE` table is a warning), `dead/3` (a rule a `FIRST` table never reaches is a warning) and `coverage/3` (an input no rule takes, in a table with no default, is an info) answer *proved*, *refuted with an input that shows it* or *unknown*, and the compiler turns definitive verdicts into diagnostics. Certainty uses lower bounds: numeric caps keep intervals without filling gaps, and string caps discard information. The [certain-set contract](https://github.com/FeeJai/BDM/blob/main/apps/feel/docs/type-system-analysis.md#23-certain-and-possible-sets-in-rule-analysis) are regressions, including `UNIQUE` and `FIRST` tables evaluated at a gap in the first rule. Unsupported questions can remain unknown; universal witness revalidation is still open work.

A table of more than 200 rules or 4,000 cells (rules times inputs) is not asked: the number of rule pairs is the square of the first, and each cell is prepared for each question. Source and column preparation happen inside the table's worker (its deadline and memory policy), after these size limits. Mismatched model and prepared row/column counts fail the check with `:table_not_checked`, rather than silently truncating it. Tables skipped for size or having no inputs also report `:table_not_checked`; unsupported questions can remain unknown. Absence of warnings or `:table_not_checked` does not establish that every question ran or had a definitive answer. `check_tables: false` turns the questions off (DD-20).

Standalone `Feel.Rules` calls accept named type definitions through the optional `:types` map, but do not receive value bindings or extensions. A caller must supply the original named types, each effective test, and an explicit `:unknown` cell when its other dependencies cannot be preserved. Passing original text that resolves differently outside the model is not a valid way to reuse the DMN checker's proof assumptions.

## Step 6: running, and the way back

`Dmn.Run` runs the units a request needs, in the order of the plan, each once. For a unit it binds the values of the units it reads under their names and calls `Feel.run(expression, bindings, options)`, with the caller's `:mode`, `:clock`, `:entropy` and `:limits`, and the key of the unit as the `:origin`. The values of the data are converted and verified where they are read, by the scope's declarations (DD-13), so no unit relies on another's check.

The public run APIs accept `:mode`, `:sandbox`, `:guard`, `:clock`, `:entropy` and `:limits`. Mode is `:lenient` (the default) or `:strict`. Containment defaults to on; `sandbox: false` opts out for trusted input. `guard:` configures the timeout, heap and input/result transfer allowances documented by `Feel.Worker`; `guard: nil` is invalid when containment is enabled. Repeated option keys are invalid, including nested guard keys with identical values. Unknown run keys, repeated keys and invalid modes are rejected before selection or worker startup; invalid guard settings are rejected before a worker starts. These errors return `{:error, %Dmn.Error{kind: :run, code: :invalid_options}}`, so a tiny worker cap cannot replace an option error with a termination error. The shared security model describes the policy.

Recursive groups keep single-signature callee evidence in their compiled expressions. Installing links during evaluation reuses that preparation; it does not analyze or optimize the model again.

What FEEL reports comes back through the source map. A diagnostic of a run has a span of the unit's source, `Source.locate/2` finds the element and the place in it, and the `Dmn.Diagnostic` says `rule 1, output entry 1 of the decision "Discount"` and keeps FEEL's diagnostic. `:strict` mode stops at the first error and returns `{:error, %Dmn.Error{kind: :run, code: :strict}}` (DMN 1.6, 7.3.8).

A span is a range of one text, and a run can involve more than one: a business knowledge model that a decision calls, and each member of a recursion group, run their own text inside another's, so a span of the member's text, put in the caller's map, landed in the caller at whatever part has that range. So a diagnostic says **which text its span is in** (D-201): it has an `origin` (`Feel.Diagnostic.origin`, a term of the host's, `nil` for the text of the expression that was run unless the host gave the run an origin). `Dmn.Run` runs each unit with its key as the origin, and a function written in it, a business knowledge model or a decision service among them, keeps it wherever it is called, as does a function that a function with another origin writes. `Feel.Recursion` gives the members of a group theirs, the environment carries the origin while the body runs (`Env.within/3`), and `Dmn.Run` places a diagnostic by the source of the unit whose key is its origin; so the `feel` of a `Dmn.Diagnostic` has the key of its unit and not `nil`. An origin that is not the key of a unit of the model is not read, and nothing in a text of FEEL can set one.

`Dmn.Run.invoke/4`, calling a business knowledge model or a service as a function, builds a tree of its own: a call of the closure by name or by position, with the arguments as values bound under U+0001 names, prepared with `prepare_ast/3` and a source of `""`.

**Limits.** The limits that a model is compiled with (`feel_limits:`) are kept by the compiled model and are the limits of its runs (`Dmn.Compiled.feel_limits`, DD-26); `:limits` given to a run replace them.

**Recursion.** DMN 1.6 has none (the requirements are acyclic, 6.3), so by default a cycle is `:cyclic_requirements`, and a business knowledge model or service that invokes itself by name (its own name is not in its scope) is `:recursion_not_enabled`. With the `:recursion` feature (`Dmn.compile(model, features: [:recursion])`, DD-22 and DD-29; `recursion: true` still works), business knowledge models and decision services that invoke themselves or each other run, and no node was added to FEEL for it. A group is the strongly connected set of those units, or a unit whose prepared logic reads its own name. Compilation establishes their single-signature callee evidence. The shared `Dmn.Compiled` planner orders the group after its external requirements; sandbox preflight carries that plan into execution with its matching compact model. Trusted calls and guarded fallbacks plan inside the run. `Dmn.Run` runs the members together and gives each the **links** to the members it reads, whereas other units are given values. A link (`Feel.Recursion.link/1` and `run/3`, D-188) is a function that makes the member's closure at the first call in a run and keeps it in the environment of the run (`Env.links`, D-200), so a function value that is returned from a run relies on no shared cache with an independent lifetime. `Feel.Limits` bounds call count and depth; the sandbox deadline also bounds a complete recursive invocation. A cycle with a decision on it stays an error, since data cannot be derived from itself.

The feature also selects guarded item-definition graphs, independently of whether a knowledge function recurses. Those graphs keep full runtime membership and constraint checks; only their static projection is bounded. See Step 1 and DD-22 for the data-cycle restrictions.

## What is not trusted

The XML, its imports, and the FEEL in them are written by someone else (DD-02). At this seam that means:

* every text is prepared with the interpreter backend (unless the caller who trusts its models asks for `backend: :beam`, DD-41, which generates modules and atoms that are never freed) and `Feel.Limits`, which bounds selected construction and runtime work. `Dmn.compile/2` includes XML loading, imports and compilation under one `Feel.Worker` policy; the evaluation and invocation entry points cover complete runs. Containment defaults to on; `sandbox: false` opts out for trusted input. Deadlines, bounded transfers and process heap policies complement engine limits; they do not bound total VM memory or arbitrary host effects (DD-15). See the shared [security model](https://github.com/FeeJai/BDM/blob/main/apps/feel/docs/architecture.md#security-model);
* a tree is trusted to be well formed, and it is `dmn` that builds it. A model cannot create a node, because every node is made from the model's data by the code that maps it, and the names that the compiler adds are not names a model can write;
* a model that is wrong (a text that cannot be read, a name that is not in scope, a table with a missing entry) is compiled all the same, with diagnostics, and a fault of the element is an error of every run of it (DD-24). An error that is about an element (an unresolved requirement, two elements of one name, a type that is not defined) is a **fault** of its unit, so a strict run ends on it, and `Dmn.validate/1`, or `validate: true` for `Dmn.compile/2`, is the gate that refuses a model with any error (DD-24).

## Adding a node

A boxed expression that FEEL has no node for (or a node that a future layer would build) goes through the same places as the two above. In `apps/feel`:

1. **`lib/feel/ast.ex`**: the node in the table, its shape in `t:Feel.AST.t/0`, and a section that says what it means. Keep the shape `{kind, %{span: …}, parts…}`, so that the generic traversals keep working.
2. **`lib/feel/analysis/analyzer.ex`**: a clause of `analyze_clause/2`. It analyzes the parts with the context they need (for the entries of a table, the member context of the column), merges what the parts found, makes the type, and reports a wrong shape at the span of the node. A new check of shape can live in its own module, as `Feel.Analysis.Tables` does.
3. **`lib/feel/ir.ex`**: the IR node in the table and in the type, if the analyzer makes a new one. If it can be expressed in the nodes that exist, as `:declared` is, there is no more to do after this step.
4. **`lib/feel/optimizer.ex`**: a clause that rewrites the parts, and a decision whether the node may be folded.
5. **Both backends**: `lib/feel/backend/interpreter/walk.ex` (`value_of/3`) and `lib/feel/backend/beam/emitter.ex` (`expression/2`). Put the semantics in a module of `lib/feel/runtime/` that both call (`Feel.Runtime.DecisionTable`), and bump the generated-code cache stamp (`Feel.Runtime.abi/0`) when changing calls emitted against that contract.
6. **Tests**: both backends give the same value and diagnostics; `Feel.Paranoid` over the node (no value outside its type, the optimizer changes nothing); a wrong shape is an `:analysis` error.
7. **`docs/decisions.md`**: an entry (`D-nnn`) with the problem, the decision, and what is open.

In `apps/dmn`, `Dmn.Compiler.Expression` (or its own module, as `Dmn.Compiler.Table`) builds the node from the model, with the glue span of its part (`Fragment.glue/3`), and registers what the checks need to find it again (`state.tables`).

Open implementation work is tracked in the [FEEL backlog](https://github.com/FeeJai/BDM/blob/main/apps/feel/docs/todo.md) and [DMN backlog](todo.md).
