Narrowing › structure. The names in the examples are those of section 2.
A constraint of a model applies to a subject: a variable, or a member of it. The member is named by the steps down to it, the key of a component of a context and :each for the elements of a list, so {"q", ["age"]} is the age of q and {"qs", [:each, "age"]} the age of each person of qs. A constraint is one of two kinds, both unary tests (D-161, step E):
- the allowed values apply to the value, and for a list to each of its elements (the
allowedValuesof an item definition, D-51); - the type constraint applies to the value as a whole, so for a list it applies to the list and not to the elements (the
typeConstraint):count(?) > 0says that the list has an element.
A subject can have several constraints, and all of them hold. A member that is missing or null is exempt, as a variable that is null is, and a value with a member that is not allowed is null as a whole, with an error that says which member (the value of "qs[3].age" is not one of its allowed values). The type of a read of the name reflects all of them, at the member they apply to: they are conditions of ?, and narrow as the number and the other pages say.
| Expression | Type | Warnings |
|---|---|---|
q | context<age: number in [0..150] | Null, name: string | Null> | Null | — |
q.age | number in [0..150] | Null | — |
q.age + 1 | number in [1..151] | Null | — |
q.age * 2 - 1 | number in [-1..299] | Null | — |
q.name | string | Null | — |
q.age > 150 | boolean = false | Null | — |
The elements of a list are narrowed in the same way, whether the list is read as a whole, by an index, by a path over the list (qs.age) or by a loop:
| Expression | Type | Warnings |
|---|---|---|
qs.age | list<number in [0..150] | Null> | Null | — |
qs[1].age | number in [0..150] | Null | — |
for e in qs return e.age + 1 | list<number in [1..151] | Null> | Null | — |
sum(qs.age) | number | Null | — |
A list of lists has :each twice for its elements' elements, and a constraint for the inner lists as values is one for [:each]. Here ls allows > 0 for the numbers in its lists, and ws has the type constraint count(?) > 0:
| Expression | Type | Warnings |
|---|---|---|
ls | list<list<number in (0..max] | Null> | Null> | Null | — |
ls[1][1] | number in (0..max] | Null | — |
ls[1][1] + 1 | number in [1..max] | Null | — |
ws | non-empty list<number | Null> | Null | — |
count(ws) | number in [1..max] | Null | — |
ws[1] | number | Null | — |
count(ws) > 0 | boolean = true | Null | — |
A constraint that mentions another name
A test reads the names of the scope as the expression does: converted to their declared types and verified against their own allowed values (D-170). So capped, which allows <= top, sees a top that is null when top is not in [18..65], and the type of top is a type that the test can rely on. The type of capped is cut by the bounds of it: capped <= top is true for a number up to the highest possible top, and for no other.
| Expression | Type | Warnings |
|---|---|---|
top | number in [18..65] | Null | — |
capped | number in [-max..65] | Null | — |
capped + 1 | number in [-max..66] | Null | — |
capped < 100 | boolean = true | Null | — |
capped > 65 | boolean = false | Null | — |
A member sees the other members of its value by the name of the variable: the age of emp allows <= emp.limit, and the limit of emp is a constraint of its own, which holds for the emp that the age sees. The age is cut by it.
| Expression | Type | Warnings |
|---|---|---|
emp.limit | number in [0..65] | Null | — |
emp.age | number in [-max..65] | Null | — |
emp.age <= 65 | boolean = true | Null | — |
Only an allowed value is a relation that the type can use, and only through the bounds of the other name: capped is not top or below for a given top, it is at most the largest. What the run does, and why the test has to see a verified name:
- The value that a test sees.
topbound to 100 is not allowed, so it isnullforcapped, andcapped <= nullisnull:cappedis not allowed either, whatever its value is. Were the test to see the 100,capped= 90 would be allowed, andtopwould benullin the expression:cappedwould be a number above 65, outside its type. - Where it stops. The constraint that is being compiled, and the ones whose tests read it, are not applied to the name that is read:
srallows<= sr, andcaandcbthat allow<= cband<= ca, read the value that was bound, as they did, and so does anything that is more than four constraints deep, and every name that the tests of a name mention when they reach more than 64 compiles of other names (a name decides that by what its tests reach, and not by what else the expression reads). The other constraints of the same name are applied (that is howemp.agesees the limit). The stances.
:enforceand:enforce_widerun it, and:enforcetypes it. The stances that leave a value as it is (:report,:audit,:trust,:ignore) read every name as it was bound, andcappedisnumber | Nullin them.- A name that has no allowed values is converted to its declared type, and its type is the declared one:
x <= yas an allowed value ofxnarrows nothing for aythat is any number.
What it does not say
- The tests of a member see its siblings only by the name of the variable (
emp.limit). A constraint of a named type, which is for every variable of the type, has no name to read the value by: how to refer to the value that a member is a member of is open ([A19], D-170). - A list that is not empty is not kept inside another list: the parts of a list type are plain (D-152), so
count(?) > 1for the inner lists does not narrowcount(ls[1]). Feel.Scopemember-constraint maps attach to scope variables, not arbitrary FEEL locals. Result allowed values have a separate option. DMN can also embed declared-value checks carrying an item definition's constraints inside boxed expressions, including parameters and internal values; those are separate from attaching a scope constraint to a FEEL loop variable.
The decisions: D-138 (allowed values), D-51 (collections), D-161 in decisions.md.