How does the EK9 compiler decide a variable is definitely set or definitely unset?
← Data Flow Safety · Ref: Q2255
At every point in a function the compiler holds one of three facts about each variable: it is SURELY SET, it is SURELY UNSET, or it does NOT KNOW. Five errors are made from that one fact, and each fires only when the compiler is sure.
WHAT MAKES IT SURE THE VALUE IS SET
- it was declared from a literal: 'name <- "Steve"', 'count <- 3', 'items <- ["a", "b"]'
- it is a collection, which is set the moment it exists: 'names <- List() of String'
- it was just given a literal with ':='
- inside 'if x?', and inside a guard 'if x <- lookup()' - the check has just found it set
- after 'if not x?' that gives it a literal with ':='
WHAT MAKES IT SURE THE VALUE IS UNSET
- it was made by a built-in type's no-argument constructor: 'String()', 'Integer()', 'Boolean()', 'Date()' - a value that is present but unset
- it was given such a value with ':=', or declared as a returning variable '<- rtn as Integer: Integer()'
- inside 'if not x?', and in the 'else' of 'if x?'
- every branch of an if/else or a switch left it unset
WHAT MAKES IT GIVE UP (IT DOES NOT KNOW)
- a value returned by a function, or a parameter: either may be set or unset
- a constructor WITH an argument: 'Integer("201")' is set but 'Integer("abc")' is unset, so neither is assumed
- your own type's no-argument constructor: your type says what '?' means
- passing the variable to a function, a method that is not pure (a Date's 'clear()'), or capturing it in a dynamic function or class: code it cannot see may change the object in place
- an operator that changes it in place: '+=', '++', '/=' (an Integer divided by zero becomes unset)
- ':=:' (copy) and ':~:' (merge)
- an assignment inside a loop body, which may run no times or many
- branches that leave it in different states: set on one path and unset on another
THE RULE: AN ERROR ONLY WHEN SURE
EK9 has no warnings, so a compile error must be certain. When the compiler does not know, it says nothing - it never guesses. That is why a fallback chain from functions is fine:
fromEnvironment()
<- rtn as String: String()
fromFile()
<- rtn as String: String()
setting <- fromEnvironment() setting :=? fromFile() setting :=? "default" Stdout().println(setting)
Each function may or may not return a value, so each ':=?' genuinely decides at run time and none is reported.
THE FIVE ERRORS
- E08088: 'if x?' on a value it is sure is SET - always true (Q559)
- E08089: 'if x?' on a value it is sure is UNSET - never true (Q2256)
- E08095: 'x :=? v' on a value it is sure is SET - never applies, and v is never evaluated (Q1961, Q1263)
- E08096: '+=' or '++' on a value it is sure is UNSET - the operator keeps it unset (Q2253)
- E08097: 'x :=? v' on a value it is sure is UNSET - always applies, so it is a plain ':=' (Q2254)
label <- "Untitled" label :=? "Draft" //E08095: label is always set here Stdout().println(label)
WHAT THE COMPILER DOES NOT ASSUME
Some values are always set (or always unset) when the program runs, but the compiler does not assume it, so it stays silent there. Do not rely on an error to point them out:
- every branch of an if/else gives the value a set value (it does assume UNSET when every branch leaves it unset)
- a do-while body or a for-range that always runs at least once
- after ':=:' or ':~:', and after ':=?' of a literal onto a value it did not know
- after '+=' on a value that was set
HOW THIS IS CHECKED
The compiler's own test suite generates thousands of small programs - every kind of value, where it came from, and one or two steps of flow before an 'if x?', 'x :=? v' or 'x += v' - compiles each, then RUNS each, and fails if any sure verdict is ever contradicted by a run. Silence is always allowed; a wrong error never is. See Q880 for why EK9 has no null, and Q936 for the '?' check.
Example
#!ek9 defines module qa.dataflowsafety.definitelysetorunset defines function //Each of these MAY return an unset value - the compiler cannot see which, so it decides nothing about them. savedGreeting() <- rtn as String: String() savedLabel() <- rtn as String: "Saved label" storedTheme() <- rtn as String: String() userTheme() <- rtn as String: "dark" tally() -> start as Integer <- rtn as Integer: start defines program DefinitelySetOrUnsetDemo() stdout <- Stdout() //From a function: '?' decides at run time greeting <- savedGreeting() if greeting? stdout.println(`greeting: ${greeting}`) else stdout.println("greeting: none saved") count <- tally(Integer()) if count? stdout.println(`count: ${count}`) else stdout.println("count: unset") //From a function: ':=?' decides at run time - here the saved label is kept label <- savedLabel() label :=? "Untitled" stdout.println(`label: ${label}`) total <- tally(5) total += 1 stdout.println(`total: ${total}`) //A fallback chain started from its first source: each ':=?' decides theme <- storedTheme() theme :=? userTheme() theme :=? "light" stdout.println(`theme: ${theme}`) //A constructor with an argument is not assumed either way parsed <- Integer("abc") stdout.println(`parsed set: ${parsed?}`) //Not assumed, though always true here: after ':=:' the value is set copied <- Integer() copied :=: 42 stdout.println(`copied set: ${copied?}`)
Common mistakes
E08088 — A value declared from a literal is always set, so 'if greeting?' is always true. Check a value that may be unset - one from a function, a parameter or a lookup. See ek9 -h E08088 for details.
Incorrect:
greeting <- "Hello"
Correct:
greeting <- savedGreeting()
E08089 — Integer() makes an unset Integer, so 'if count?' can never be true. Give it a value, or take it from where it may be set. See ek9 -h E08089 for details.
Incorrect:
count <- Integer()
Correct:
count <- tally(Integer())
E08095 — A value declared from a literal is always set, so 'label :=? ...' can never apply. Keep ':=?' for a value that may be unset. See ek9 -h E08095 for details.
Incorrect:
label <- "Draft"
Correct:
label <- savedLabel()
E08096 — Integer() makes an unset Integer and '+=' keeps it unset, so the total never gets a value. Start from a value. See ek9 -h E08096 for details.
Incorrect:
total <- Integer()
Correct:
total <- tally(5)
E08097 — String() makes an unset String, so the first ':=?' always applies. Start the chain from its first source. See ek9 -h E08097 for details.
Incorrect:
theme <- String()
Correct:
theme <- storedTheme()
Other ways to ask this
- When does the compiler know a value is always set?
- What makes the compiler sure a variable is unset?
- Why does EK9 report some '?' and ':=?' checks but not others?
- How does EK9's set and unset flow analysis work?
- Which errors come from the compiler knowing a value is set or unset?
Coming from another language?
Kotlin smart casts and TypeScript's narrowing track null through a function, but report nothing when a check is pointless. Java and C# give an IDE warning at most. EK9 tracks set and unset (EK9 has no null) at every point, and because it has no warnings it reports a pointless or impossible check as an error - but only when it is sure. A value from a function, a parameter or a loop is never assumed.
Keywords: :=?, ?, E08088, E08089, E08095, E08096, E08097, always set, always unset, data flow, definitely set, definitely unset, flow analysis, isSet, provably, tri-state