A boolean value is either true or false. Booleans appear naturally wherever you make a decision: in if conditions, loop filters, and comparisons. Declared as boolean?, a boolean gets a third state called null — meaning "no value" — and any null behaves like false whenever a boolean is expected.
This design means you rarely need to write a separate null-check: !x is true both when x is false and when x is null.
fn main() {
A comparison produces a boolean result directly. ! flips a boolean: true → false, false → true.
assert(!(3 > 2 + 4), "3 is not greater than 6");
assert(true as text == "true", "Convert boolean to text");
Logical operators: and / or
and (also &&) is true only when both sides are true. or (also ||) is true when at least one side is true. Both skip the right side when the left side already determines the result. For example, false and X is always false, so X is never evaluated. This matters when the right side could produce null or has a side effect.
assert(1 > 0 and 2 > 1, "Both conditions true");
assert(1 > 2 or 3 > 2, "Second condition true");
assert(!(1 > 2 && 3 > 2), "First false → whole 'and' is false");
assert(!(1 > 2 || 3 > 4), "Both false → 'or' is false");
Null in boolean context
Division by zero (and other failed operations) produce null. Null in a boolean context is treated as false, so ! is true. This lets you write guard clauses without a separate null-check syntax: if !result { ... handle missing value ... } Even without ?? null, a bare divide never halts: it yields null and execution continues, the same everywhere — development, test, and production.
The ?? null is not what makes that safe, and it silences nothing. loft warns about a divisor that is a literal 0 in your source, while READING your code, and it warns whether or not you wrote ?? null; a divisor that is only zero at RUN time is not warned about at all. Either way the result is null, and a null prints as null. Reach for ?? <value> when you want a number — not to quiet a warning.
zero = 0;
assert(!(12 / zero ?? null), "null from division-by-zero is false-like when defended");
assert("{12 / zero}" == "null", "an undefended result prints as null");
assert("{12 / zero ?? null}" == "null", "…and so does a defended one");
assert(12 > 0, "positive integer is true");
Bitwise operators
While and/or work on true/false values, bitwise operators work on the individual bits of an integer. They are useful for flags, masks, and low-level data manipulation.
'&' — keeps only bits set in BOTH operands (AND)
'|' — keeps bits set in EITHER operand (OR)
'<<' — shift bits left N positions (×2^N)
'>>' — shift bits right N positions (÷2^N)
Tip: & binds more tightly than comparison operators, so x & 15 == 8 reads as (x & 15) == 8. Parentheses make that grouping visible to a reader when you mix bitwise and comparison expressions.
assert((0x0f & 0xa8) == 8, "Bitwise AND: keeps only bits in both");
assert((0xf0 | 0x0f) == 0xff, "Bitwise OR: combines bits from either");
assert(1 << 4 == 16, "Left shift: 1 × 2^4 = 16");
assert(256 >> 3 == 32, "Right shift: 256 ÷ 2^3 = 32");
Negation
! is the logical NOT operator. It flips any boolean expression.
assert(!false, "not false is true");
assert(!(1 > 2), "not false comparison is true");
Formatting booleans
Booleans can be embedded in a format string like any other value. The ^ alignment specifier centres the value in a field of given width. < aligns left, > aligns right (a boolean aligns left by default, like text).
assert("1{true:^7}2" == "1 true 2", "Centred boolean in field of width 7");
assert("{false}" == "false", "Plain boolean format");
& vs and
& is bitwise AND on integers; and (or &&) is logical AND on booleans. Writing a & b where both sides are booleans is a compile error — the compiler requires and or && for boolean logic.
flag_a = 3 > 1;
true
flag_b = 4 > 2;
true
assert(flag_a and flag_b, "Correct: logical AND on two booleans");
}
Run it yourself
This page is a loft program. It runs here, in your browser — press Run, then ask it something.
Stop on a line
// Copyright (c) 2024-2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// @NAME: Boolean
// @TITLE: True, false, and null
// A boolean value is either 'true' or 'false'. Booleans appear naturally
// wherever you make a decision: in 'if' conditions, loop filters, and
// comparisons. Declared as 'boolean?', a boolean gets a third state called
// 'null' — meaning "no value" — and any null behaves like false whenever a
// boolean is expected.
//
// This design means you rarely need to write a separate null-check: '!x' is
// true both when x is false and when x is null.
fn main() {
// A comparison produces a boolean result directly.
// '!' flips a boolean: true → false, false → true.
assert(!(3 > 2 + 4), "3 is not greater than 6");
assert(true as text == "true", "Convert boolean to text");
// ## Logical operators: and / or
// 'and' (also '&&') is true only when both sides are true.
// 'or' (also '||') is true when at least one side is true.
// Both skip the right side when the left side already determines the result.
// For example, 'false and X' is always false, so X is never evaluated.
// This matters when the right side could produce null or has a side effect.
assert(1 > 0 and 2 > 1, "Both conditions true");
assert(1 > 2 or 3 > 2, "Second condition true");
assert(!(1 > 2 && 3 > 2), "First false → whole 'and' is false");
assert(!(1 > 2 || 3 > 4), "Both false → 'or' is false");
// ## Null in boolean context
// Division by zero (and other failed operations) produce null.
// Null in a boolean context is treated as false, so
// '!' is true. This lets you write guard clauses without a separate
// null-check syntax:
// if !result { ... handle missing value ... }
// Even without `?? null`, a bare divide never halts: it yields null and execution
// continues, the same everywhere — development, test, and production.
//
// The `?? null` is not what makes that safe, and it silences nothing. loft warns
// about a divisor that is a literal 0 in your source, while READING your code, and
// it warns whether or not you wrote `?? null`; a divisor that is only zero at RUN
// time is not warned about at all. Either way the result is null, and a null prints as
// 'null'. Reach for `?? <value>` when you want a number — not to quiet a warning.
zero = 0;
assert(!(12 / zero ?? null), "null from division-by-zero is false-like when defended");
assert("{12 / zero}" == "null", "an undefended result prints as null");
assert("{12 / zero ?? null}" == "null", "…and so does a defended one");
assert(12 > 0, "positive integer is true");
// ## Bitwise operators
// While 'and'/'or' work on true/false values, bitwise operators work on
// the individual bits of an integer. They are useful for flags, masks,
// and low-level data manipulation.
//
// '&' — keeps only bits set in BOTH operands (AND)
// '|' — keeps bits set in EITHER operand (OR)
// '<<' — shift bits left N positions (×2^N)
// '>>' — shift bits right N positions (÷2^N)
//
// Tip: '&' binds more tightly than comparison operators, so 'x & 15 == 8'
// reads as '(x & 15) == 8'. Parentheses make that grouping visible to a
// reader when you mix bitwise and comparison expressions.
assert((0x0f & 0xa8) == 8, "Bitwise AND: keeps only bits in both");
assert((0xf0 | 0x0f) == 0xff, "Bitwise OR: combines bits from either");
assert(1 << 4 == 16, "Left shift: 1 × 2^4 = 16");
assert(256 >> 3 == 32, "Right shift: 256 ÷ 2^3 = 32");
// ## Negation
// '!' is the logical NOT operator. It flips any boolean expression.
assert(!false, "not false is true");
assert(!(1 > 2), "not false comparison is true");
// ## Formatting booleans
// Booleans can be embedded in a format string like any other value.
// The '^' alignment specifier centres the value in a field of given width.
// '<' aligns left, '>' aligns right (a boolean aligns left by default, like text).
assert("1{true:^7}2" == "1 true 2", "Centred boolean in field of width 7");
assert("{false}" == "false", "Plain boolean format");
// ## '&' vs 'and'
// '&' is bitwise AND on integers; 'and' (or '&&') is logical AND on booleans.
// Writing 'a & b' where both sides are booleans is a compile error — the
// compiler requires 'and' or '&&' for boolean logic.
flag_a = 3 > 1;
// true
flag_b = 4 > 2;
// true
assert(flag_a and flag_b, "Correct: logical AND on two booleans");
}