Narrowing a model-defined type

Copy Markdown View Source

Narrowing › named types. The names in the examples are those of section 2.

A model-defined type is a structural alias, not a nominal tag. A scope can declare m as Person, where Person resolves to context<age: number, name: string>. Feel.Type.resolve/2 resolves aliases recursively against scope.types; narrowing then uses the resulting context type and context rules. The value does not need to have been constructed by that model.

ExpressionTypeWarnings
mcontext<age: number | Null, name: string | Null> | Null—
m.agenumber | Null—
if m.age > 5 then m else nullcontext<age: number in (5..max], name: string | Null> | Null—
if m != null then m else nullcontext<age: number | Null, name: string | Null> | Null—
m instance of Personboolean—
{age: 1, name: "a"} instance of Personboolean = true—
if v instance of Person then v else nullcontext<age: number | Null, name: string | Null> | Null—

instance of checks the resolved structural type. Item-definition allowed values and type constraints are separate checks, not part of that type test. They can narrow a bound variable when its declaration policy permits it (structural constraints). With a valid target type, instance of returns a boolean; null is an instance of Null only.

Missing and recursive definitions

An unresolved type in a variable declaration is an analysis error when that declaration is read. An invalid target of instance of is different: preparation keeps a warning. Runtime reports an error and yields null in lenient mode, which can select the else branch of a surrounding if; strict execution stops at the error.

ExpressionTypeWarnings
1 instance of MissingNullinvalid_type_expression
if 1 instance of Missing then 1 else 2number = 2invalid_type_expression

Feel.Type.resolve/2 returns {:error, {:cyclic_type, name}} for a cycle of unresolved named references. Feel.ItemDefinitions.new/2 rejects recursive definitions by default, but recursive: :guarded produces finite graph descriptors that the resolver keeps. Runtime membership checks full finite values; declaration boundaries also enforce their descendant constraints. Analysis sees a bounded structural upper approximation. DMN selects guarded mode with its :recursion feature. FEEL's legacy recursive: :any instead cuts back references and does not provide complete descendant validation.

An upper projection cannot prove an instance of test true, remove its check, or exclude the projection from the negative branch. Conversion can change a value even if it fits the projection, so recursive parameter/result boundaries use post-conversion bounds. Collection heads and aliases are unfolded for singleton wrapping and element allowed values. See the guarded-type contract for supported cycles, function signatures and conservative type relations.

Preserve the type scope when reusing source

Resolution erases a type's name from the IR, and folding can erase the predicate altogether. The original source still needs its type definitions when it is prepared again. With N defined as number, if 1 instance of N then 1 else 2 returns 1; without N, its invalid test returns null and the expression returns 2 with an error in lenient mode. Even parsing can change: a known type named number and false must be read as one name, not a built-in type followed by a boolean operator.

DMN preserves each unit's named types, including imported names, in the types: map option to Feel.Rules queries. This applies to explicit entries and wildcard inputValues, even when the prepared IR no longer records the original reference. Value bindings, filter fields and extension calls have a different contract: unsupported dependencies stay unknown rather than being reinterpreted outside their scope. See the table-proof contract.

Unresolved internal alternatives

Feel.Type.Union.from_spec/1 can represent an unresolved {:named, name} alternative. Readers that encounter it must stay conservative: facts do not inspect an unavailable definition and operations can fall back to Any | Null. Ordinary prepared variable declarations resolve names before those facts are used; this fallback is not a substitute for supplying a valid scope.

See also: context, structure; Feel.ItemDefinitions reads model item definitions into a scope (D-99).