Narrowing by the constraints of a structure

Copy Markdown View Source

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 allowedValues of 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(?) > 0 says 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.

ExpressionTypeWarnings
qcontext<age: number in [0..150] | Null, name: string | Null> | Null—
q.agenumber in [0..150] | Null—
q.age + 1number in [1..151] | Null—
q.age * 2 - 1number in [-1..299] | Null—
q.namestring | Null—
q.age > 150boolean = 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:

ExpressionTypeWarnings
qs.agelist<number in [0..150] | Null> | Null—
qs[1].agenumber in [0..150] | Null—
for e in qs return e.age + 1list<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:

ExpressionTypeWarnings
lslist<list<number in (0..max] | Null> | Null> | Null—
ls[1][1]number in (0..max] | Null—
ls[1][1] + 1number in [1..max] | Null—
wsnon-empty list<number | Null> | Null—
count(ws)number in [1..max] | Null—
ws[1]number | Null—
count(ws) > 0boolean = 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.

ExpressionTypeWarnings
topnumber in [18..65] | Null—
cappednumber in [-max..65] | Null—
capped + 1number in [-max..66] | Null—
capped < 100boolean = true | Null—
capped > 65boolean = 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.

ExpressionTypeWarnings
emp.limitnumber in [0..65] | Null—
emp.agenumber in [-max..65] | Null—
emp.age <= 65boolean = 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. top bound to 100 is not allowed, so it is null for capped, and capped <= null is null: capped is not allowed either, whatever its value is. Were the test to see the 100, capped = 90 would be allowed, and top would be null in the expression: capped would 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: sr allows <= sr, and ca and cb that allow <= cb and <= 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 how emp.age sees the limit).
  • The stances. :enforce and :enforce_wide run it, and :enforce types it. The stances that leave a value as it is (:report, :audit, :trust, :ignore) read every name as it was bound, and capped is number | Null in them.

  • A name that has no allowed values is converted to its declared type, and its type is the declared one: x <= y as an allowed value of x narrows nothing for a y that 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(?) > 1 for the inner lists does not narrow count(ls[1]).
  • Feel.Scope member-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.