Narrowing › context. The names in the examples are those of section 2.
A context type is {:context, entries}, open, or {:closed_context, entries}, closed (D-65): a declared type is open (context<a: number> is every context that has an entry a that is a number, and may have others), and a context literal has a closed type (the entries it has and no others). A context has no value set, but its entries are unions, so a fact can narrow an entry. And a fact about a path is a fact about the source of the path as well.
Exclusions stay inside those entry and list-element unions. Path and descendant readers request a positive upper approximation where they inspect one, rather than treating :except as a kind with no properties. For a list whose elements exclude context<a: number>, an element can still be {a: "s"} and selecting .a can return ["s"]. The inferred path type must contain that result; declaring list<Null> must still reject it, with or without optimization. Selecting a context entry itself keeps its exclusion-bearing type for the next reader.
Filter entry lookup follows the same rule: xs[a = "s"] must see an a supplied by an excluded context before falling back to an outer name. This includes entries named item or like a built-in. Narrowing an implicit entry works on the positive context base and keeps the exclusions; failure of the outer fallback does not discard a matching context. Structural summaries used by descendant iteration also treat an excluded context as a possible context, never as proof of an empty domain.
A fact about a path narrows the source
p.age > 18 is a condition about the subject p.age (a path that starts at a variable, a local or the element of a filter). Where p is read in the branch, it has to be a value for which p.age can be what the fact leaves (Narrowing.narrow_source/3, D-194):
- an alternative of
pstays if selecting from it can be what the fact leaves, and goes if it cannot. Anulland a number selectnull, sop.age > 18leaves neither; - a context type that lists the entry has it narrowed (
age: number in (18..max]); - one that does not list it gets it listed, when what the fact leaves is not
null: then the entry is there, since an entry that is not there is selected asnull; - a closed context that does not list the entry selects
null, so it stays only if the fact can hold fornull; Anyis taken kind by kind where the fact leaves nonull(a function has no entries);- a path of several keys narrows the entries in turn.
| Condition | p when true | p when not true |
|---|---|---|
p.age > 18 | context<age: number in (18..max], name: string | Null> | context<age: number in [-max..18] | Null, name: string | Null> | Null |
p.age < 0 | context<age: number in [-max..0), name: string | Null> | context<age: number in [0..max] | Null, name: string | Null> | Null |
p.age >= 18 and p.name = "a" | context<age: number in [18..max], name: string = "a"> | context<age: number | Null, name: string | Null> | Null |
p.age > 18 or p.name = "a" | context<age: number | Null, name: string | Null> | Null | context<age: number in [-max..18] | Null, name: string except "a" | Null> | Null |
p.age + 1 > 18 | context<age: number in (17..max], name: string | Null> | context<age: number in [-max..17] | Null, name: string | Null> | Null |
p.age = x | context<age: number | Null, name: string | Null> | Null | context<age: number | Null, name: string | Null> | Null |
The then of the second-last row is the preimage of the sum (number); the last row has nothing to say because x can be null, and then p.age = x can be true for a null age and a null p. The or row shows the rule of the logic: in then neither side narrows alone, and in else both do.
The path itself, and the other side, are subjects as well:
| Condition | p.age when true | p.age when not true |
|---|---|---|
p.age > 18 | number in (18..max] | number in [-max..18] | Null |
| Condition | p.name when true | p.name when not true |
|---|---|---|
p.name = "a" | string = "a" | string except "a" | Null |
| Condition | x when true | x when not true |
|---|---|---|
p.age > x | number in [-max..max) | number | Null |
null, shape, and an entry the type does not list
| Condition | p when true | p when not true |
|---|---|---|
p = null | Null | context<age: number | Null, name: string | Null> |
p != null | context<age: number | Null, name: string | Null> | Null |
p.age = null | context<age: Null, name: string | Null> | Null | context<age: number, name: string | Null> |
p.age != null | context<age: number, name: string | Null> | context<age: Null, name: string | Null> | Null |
p.age instance of number | context<age: number, name: string | Null> | context<age: Null, name: string | Null> | Null |
p = {age: 1, name: "a"} | context<age: number = 1, name: string = "a"> | context<age: number | Null, name: string | Null> | Null |
p instance of context<age: number> | context<age: number | Null, name: string | Null> | Null |
p instance of context<age: string> | context<age: Null, name: string | Null> | context<age: number | Null, name: string | Null> except context<age: string | Null> | Null |
p instance of context<zip: string> | context<age: number | Null, name: string | Null, zip: string | Null> | context<age: number | Null, name: string | Null> except context<zip: string | Null> | Null |
p.zip = "x" | context<age: number | Null, name: string | Null, zip: string = "x"> | context<age: number | Null, name: string | Null> | Null |
p.zip != null | context<age: number | Null, name: string | Null, zip: Any> | context<age: number | Null, name: string | Null> | Null |
p.age = nullis true for apwhoseageisnull, or for apthat isnullitself (selecting fromnullgivesnull); when it is not truepis notnulland has anage.p = {age: 1, name: "a"}is true only for a context of that shape, part by part, and leaves the closedcontext<age: number = 1, name: string = "a">. Contexts of other keys are unequal.p instance of context<age: string>leavesage: Null: the context types are open and anullconforms to every type, so the onlypthat is both is one whoseageisnull.p.zip = "x"lists an entry the type does not have.p.zip != nulllists it asAny;p.zip = nullwould list nothing, since it is true of an entry that is missing too.
A name of type Any
Any is taken kind by kind where the fact leaves no null. A path selects from a context, and from a list too (a list of the entries):
| Condition | v when true | v when not true |
|---|---|---|
v.age > 5 | context<age: number in (5..max]> | Any | Null |
v.name = "a" | context<name: string = "a"> | Any | Null |
v.a.b > 1 | context<a: context<b: number in (1..max]>> | Any | Null |
v.age != null | list<Any | Null> | context<age: Any> | Any | Null |
v instance of context<a: number> | context<a: number | Null> | Any except context<a: number | Null> | Null |
v = {a: 1} | context<a: number = 1> | Any | Null |
| Condition | v.a when true | v.a when not true |
|---|---|---|
v.a.b > 1 | context<b: number in (1..max]> | Any | Null |
v.age != null is true of a context with an age, and of a list (the path from a list is a list, which is not null), and of nothing else: a date has no age, and null.age is null.
Locals, aliases and closed contexts
A path starting at a local is a subject too, identified by the depth of the context that binds it (D-193). So a local that is an alias of a name or a path is narrowed by what the condition says of its own entries, and a context literal has the closed type of its entries:
| Expression | Type | Warnings |
|---|---|---|
{c: {a: x}, r: if c.a > 5 then c else null}.r | context<a: number in (5..max]> | Null | — |
{q: p, r: if q.age > 5 then q else null}.r | context<age: number in (5..max], name: string | Null> | Null | — |
{c: {a: 1}, r: if c.a > 5 then 1 else 2}.r | number = 2 | — |
{a: x, r: if a > 5 then a else null}.r | number in (5..max] | Null | — |
The third row: c.a is the constant 1, so c.a > 5 is never true.
The functions of a context
get value, get entries, context put, context merge and context are typed by the entries of the context they are given (Feel.Analysis.Images.Context, D-170): the entry of get value is the type of the entry, null for a key that a closed context does not list and Any or null for one that an open context does not, and context put is the context with the entry set (a closed context stays closed). A list of one context is that context, so a list is null where it does not have one. A merge of several contexts is any context, and null only where an item can be no context.
| Expression | Type | Warnings |
|---|---|---|
get value(q, "age") | number in [0..150] | Null | — |
get value(p, "name") | string | Null | — |
get value(q, "zz") | Any | Null | — |
get entries(p) | non-empty list<context<key: string, value: Any | Null>> | Null | — |
context put(q, "x", 1) | context<age: number in [0..150] | Null, name: string | Null, x: number = 1> | Null | — |
get value(context put(q, "x", 1), "x") | number = 1 | Null | — |
context merge([p, q]) | context<> | Null | null_possible |
context([{key: "a", value: 1}]) | context<a: number = 1> | — |
Which paths have a subject
A fact is kept for a path that starts at a variable, a local, or the element of a filter. A path that starts at anything else ({p: 1}.p, a call) has none, because what it selects could be shadowed, and a fact about it would be about another value (D-26). The path is read by its keys as written, so p.address.city is the subject {:path, "p", ["address", "city"]}.
Limits
- 16 facts about the other paths from a name are applied by one read (
@max_path_facts, D-154); a name with more is read as if it had 16, and its type says less. The facts about the paths a read follows are always applied. - A fact about an element of a filter over an open context type is lost (list).
max_narrowing_factscounts the facts about one path, which in a chain ofp.age > 1 and 1 / p.age > 0 and …cannot be answered from its type (limits).
See also: list, Any, date (a fact about a property narrows the date); type-system.md, 3.2.