Language reference
Syntax and values
Source files, indentation, bindings, calls and function results.
Source files use the .k extension. Indentation defines a block of statements and expressions. Use spaces consistently; the formatter uses two spaces per level. A colon introduces a function or control-flow body. The final expression supplies a function's result. if, while and for support value-producing forms as well as statements; a loop expression can collect values with yield, or select a single result with else and break value.
Bindings and results
fn main() -> int:
width = 6
height: int = 7
area = width * height
area This program returns 42. Numeric and Bool scalar bindings can be reassigned while preserving their type. An annotation such as height: int declares the expected type; otherwise the checker infers a type. A bare whole-number literal normally infers int, while a decimal or exponent literal normally infers float. An expected type can give a literal a narrower numeric type.
Comments and multiline expressions
Use # for line comments. Blank lines and comment-only lines do not change block structure. Parentheses and collection delimiters permit multiline expressions:
fn main() -> int:
# The final expression is the result.
total = (
20 +
22
)
total This program also returns 42. Identifiers use ASCII letters and underscores, followed by letters, digits or underscores. String literals and comments can contain Unicode. Tabs outside comments and strings are diagnosed.
Calls and pipelines
fn add(left: int, right: int) -> int:
left + right
fn main() -> int:
40 -> add(2) The program returns 42. The pipeline inserts its left value as the first argument. Here the final line is equivalent to add(40, 2). Use parentheses when a larger expression needs to be one argument.
Reassign a local scalar
Reuse = to update a numeric or Bool binding while preserving its type. A scalar function parameter is a private value; rebinding it cannot change the caller's binding. Assignments in a statement branch or loop update an existing scalar name; a new name introduced inside that body stays local to it. CPU functions also support replacing numeric and Bool array bindings. String, record and function-array bindings, records, function values and tensors cannot be reassigned.
fn double(value: int) -> int:
value = value * 2
value
fn main() -> int:
amount = 10
amount = double(amount)
amount This returns 20. double changes its private parameter; main changes amount only when it assigns the returned value.
Automatic lifetimes
You do not manually free arrays, tensors, Strings or captured values. Kalandra keeps them alive while they are needed and releases their storage automatically. Passing or returning a value does not necessarily copy its storage: arrays can have aliases, and tensor operations preserve their inputs. Read access and mutation rules still apply when storage is shared.