Control
Control Flow
Doxa unifies conditional branching with a consistent then / else pattern across if, as, and match.
Core rules
- Expression bodies end with a semicolon; block bodies
{ ... }
are self-terminating (no extra semicolon after the closing brace). - then attaches the success branch where applicable; else attaches the failure/default branch and does not use then.
- These rules apply consistently to if, as, and match.
if / then / else
- then required, else optional.
- Body can be an expression (end with
;
) or a block{ ... }
(no trailing;
).
Examples:
// Block bodies (no trailing semicolons)
if x > 10 then {
"x is greater than 10"?;
} else {
"x is 10 or less"?;
}
// Expression body (ends with semicolon)
var condition is if true then "true" else "false";
Chaining and statement-style:
if current % 3 equals 0 and current % 5 equals 0 then "fizzbuzz"?;
else if current % 3 equals 0 then "fizz"?;
else if current % 5 equals 0 then "buzz"?;
else current?;
as / then / else (type narrowing)
- else required, then optional.
- Same body rules: expression bodies end with
;
, block bodies do not.
Examples:
// Success and failure as expressions
const n_or_zero is value as int else 0;
// Block failure case
value as int else {
"value is not an int"?;
};
// Explicit success block
value as int then {
// use the narrowed int value here
} else {
// handle non-int here
}
if vs as
In Doxa, if and as both use the then / else pattern, but they invert the emphasis:
Keyword | Requires | Optional | Implied meaning |
---|---|---|---|
if | then | else | If success then do x, failure branch optional |
as | else | then | As a type or else do x, success branch optional |
if
is a keyword which implicitly requires a success condition, as
implicity requires a fail condition, but both use then/else blocks in the same way.
Examples
// IF: Truth-driven
if x > 10 then "big"? else "small"?;
// AS: Fallback-driven
value as int else 0; // If not int, use 0
value as int then 20 else 0; // If int, use 20; else 0
// Both in action
if isReady then start() else wait();
data as string then parse(data) else log("Bad data");
match (values and union types)
Use match to branch on:
- Concrete values: numbers, strings, enum variants (
.Red
,.Green
, ...) - Union type arms:
int
,float
,string
,byte
,tetra
,nothing
, or custom types
Arm syntax and delimiters:
- Pattern arms:
pattern then BODY
- Else arm:
else BODY
(no then) - Body: expression (end with
;
) or block{ ... }
(no trailing;
) - Arms are separated by either the semicolon of an expression body, or by the end of a block body
Match on union types:
fn kind(value :: int | float) returns(string) {
return match value {
int then "integer"?;
float then "float"?;
};
}
Match on enums and values:
enum Color { Red, Green, Blue }
var msg1 is match color {
.Red then "It's red";
.Blue then "It's blue";
else "It's something else";
};
const x is 5;
var msg2 is match x {
0 then "It's zero";
5 then "It's five";
else "It's something else";
};
const s is "big";
var msg3 is match s {
"big" then "It's big";
"small" then "It's small";
else "It's something else";
};
Block arms in match:
var msg is match color {
.Red then { "stop"?; "red"; }
.Green then "green";
else { "caution"?; "yellowish"; }
};
Notes:
- Exhaustiveness: For enums, prefer covering all variants or add an
else
arm. For unions, cover the needed type arms;else
is optional. - Result type: All arms must produce a compatible result type. For block arms, the last expression is the arm’s value.
Quick reference
- if: then required; else optional; expression body ends with
;
, block body does not. - as: else required; then optional; same body rules.
- match: pattern then BODY; else BODY; arm separation via expression
;
or closing block}
.