52 Static vs dynamic typing
Static typing
The keyword var introduces a statically typed variable.
The element type of the variable is either supplied by an explicit type
annotation or determined by static type deduction applied to the expression used
to assign its initial value. Subsequent assignments must be compatible with
this static element type; otherwise a static type error is reported.
A variable declaration may also include a cardinality qualifier, such as * or
?. That qualifier is a constraint on the variable location, not part of the
element type. Assignment to the variable must satisfy both the static element
type and the location's cardinality constraint.
For example, var Integer id* = {}; declares a location named id whose
extension contains zero or more Integer values. The element type is still
Integer, not Set<Integer>.
These are separate static checks. Assigning a String expression to an
Integer variable is a static type check failure. Assigning an expression whose
static cardinality may be many to a cardinality-one variable is a static
cardinality constraint check failure.
// Introduces a statically typed variable named x. Its static type is Integer.
var x = 10;
// Gives an empty multi-valued variable an explicit element type.
var Integer id* = {};
// Static type check failure: String is not assignable to Integer.
x := "hello";
Cardinality flow
A common case is an optional path that has been tested before it is used:
value List
{
Integer head;
List tail?;
}
func Boolean memberOf(List list, Integer value)
{
if list.head = value {
return true;
}
if list.tail = none {
return false;
}
return list.tail.memberOf(value);
}
The attribute tail? makes list.tail optional, so the expression has static
cardinality zero or one. The final statement passes list.tail as the receiver
argument to memberOf, whose first parameter requires a single List.
The call is valid because the preceding test has refined the cardinality. The
test list.tail = none proves that the extension is empty in the guarded block.
Because that block returns, the following statements are checked in the
remaining case, where list.tail is non-empty. Since the original cardinality
was at most one, the remaining cardinality is exactly one.
This is cardinality flow. Static checking tracks both an expression's element type and its static cardinality. Flow-sensitive checks may refine cardinality in guarded scopes, just as a positive type test may refine the static type of an expression.
Similarly, inside the then-branch of if E ≠ none { ... }, an expression E
whose static cardinality is zero or one is narrowed to cardinality one. For an
expression whose static cardinality may be many, E ≠ none proves only that the
extension is non-empty; it does not make the expression single-valued.
Cardinality flow is a static analysis rule. It does not execute a query while checking the program. If a required cardinality fact cannot be proved from the static expression shape and the enclosing guards, the use is rejected as a static cardinality constraint check failure.
Dynamic typing
Dynamic variables are disabled by default. A file with no @dynamic
directive is in strict mode, so assigning to a name that has not been declared
with var is a static error.
A script or interactive file may opt into dynamic variables by putting
@dynamic = true; in the source-file prologue: after any
component declaration and before any using declaration, ordinary
declaration, or statement.
In a script with no component declaration, this is the first non-comment item in
the file. In that mode, assigning to an
undeclared name introduces a dynamically typed variable. A dynamically typed
variable can later be assigned values of any type. This mode is intended for
exploratory, interactive, or script-like code.
@dynamic = true;
// First assignment introduces a dynamic variable named y.
y := 10;
// y can be freely reassigned a value of any type.
y := "hello";
Default strict mode
Reusable component source must use the default strict mode and must not enable
dynamic variables. In strict mode, dynamic variables are not permitted and every
variable must be introduced explicitly using var. Because strict mode is the default, source
normally omits the directive; @dynamic = false; is allowed as an explicit spelling of the default.
@dynamic = false;
// Static error: dynamic variables are disabled.
y := 10;
Rationale
Predica is fundamentally a statically typed language designed for clarity, safety, and formal reasoning. Static typing ensures that errors are detected early and that programs remain predictable and analyzable.
However, practical development workflows also benefit from a lightweight,
flexible mechanism for experimentation, scripting, and interactive
exploration. Dynamic variables provide this flexibility without weakening
the core type system, because they are introduced only when the programmer
omits var in a file that explicitly enables dynamic mode.
This design keeps the language mathematically disciplined while still supporting rapid prototyping and REPL-style usage.