Lexical structure
Khora source is UTF-8 text. Whitespace and comments separate tokens where needed; the syntax tree retains trivia so formatting and documentation tools can preserve source faithfully.
Identifiers
Section titled “Identifiers”Ordinary identifiers begin with an ASCII letter or _ and continue with ASCII letters, digits, or _:
useruser_idHTTP2_internalHard keywords cannot be used as identifiers.
Character literals
Section titled “Character literals”One Unicode scalar value between apostrophes:
'a''é''\n''\u{1F600}'The escapes are the string ones: \n, \r, \t, \0, \\, \', \", and \u{...} for a code point in hexadecimal.
Exactly one character. '' is not a literal and neither is 'ab' — a String is what holds any number of characters.
The apostrophe also begins a row variable, and the two are told apart by the closing quote: 'a' is a character and 'a is a row variable. A generic list such as <'a, 'b> is two row variables, not one character containing a comma.
Row variables
Section titled “Row variables”A row variable begins with ' followed by an identifier:
'ef'raises'capabilitiesThe spelling after ' has no built-in semantic meaning.
Integer literals
Section titled “Integer literals”0421_000_000Underscores may be used as visual separators.
Floating-point literals
Section titled “Floating-point literals”0.53.141596.02e231.0E-6A fractional literal without the decimal suffix is an IEEE Float.
An exponent needs a fractional part in front of it. 6.02e23 and 1.0E-6
are literals; 1e19 is not, and is reported as expected `;` — the lexer
stops at the 1 and the e19 that follows is not part of the number. Write
1.0e19. The same rule applies to a d suffix: 1.25e3d is a Decimal
literal and 1e19d is a parse error.
Decimal literals
Section titled “Decimal literals”Append d directly to the number for an exact Decimal literal:
0d0.01d19.99d1.25e3dd is Khora’s literal suffix; no whitespace may appear between the number and
suffix. The module needs import std::decimal::{Decimal}; for the literal to
have a type. A fractional literal without the suffix is a Float:
let approximate = 0.1;let exact = 0.1d;The distinction is visible in the source so a reader can tell whether an API does approximate binary arithmetic or exact decimal arithmetic. Decimals is the rest of it.
Boolean literals
Section titled “Boolean literals”truefalseQuoted strings
Section titled “Quoted strings”"Khora""first\nsecond""say \"hello\""Strings contain Unicode text. The escapes are \n, \r, \t, \0, \\, \', \", and \u{...} for a code point in hexadecimal:
"caf\u{e9}" // café"snowman \u{2603}" // snowman ☃\u{...} takes one to six hexadecimal digits. A code point that is not a character — a surrogate, or anything above 10FFFF — is refused at compile time rather than producing a string that is not UTF-8.
String interpolation
Section titled “String interpolation”${...} evaluates an expression inside a string:
"user ${user.id}""count = ${count}""the point is ${point} and the list is ${items}"Interpolation is expression syntax, not a separate formatting language.
A hole that already holds a String is used as it stands. Anything else is
shown through Show, so a value whose type has no
Show impl is a compile error naming the type:
error: `Color` has no `Show`, so it cannot go in a `${..}` hole. Write `derive(Show)` on it, or `impl Show for Color`Show does not have to be imported to interpolate. The hole is the use, and
the trait is never named in the source. The value’s type does have to be
imported, though: a hole over a List<Int> in a module that never wrote List
reports the same “has no Show” error even though List implements it.
A quote inside a hole is written unescaped. ${...} is scanned as source,
not as string content, so the inner literal’s quotes stand as they are:
"len = ${String::byte_length("abc")}" // correct"len = ${String::byte_length(\"abc\")}" // refusedThe escaped form is refused by name:
error: a `${..}` hole is scanned as source, not as string text, so its quotes stand as they are: write `"` here, not `\"`A hole that fails to parse for some other reason gets the general message,
error: this `${..}` does not contain an expression, with the hole
underlined. Binding the value to a let on the line before is the way out of
one that will not parse.
Interpolation is for text a person will read. Where another program consumes
the output, reach for a structured encoder such as std::json instead — a
value that renders one way for a human is not a serialization format.
Backtick strings
Section titled “Backtick strings”Backticks delimit multiline strings:
let query = ` select id, name from users where active = true`;${...} interpolation and backslash escapes work in backtick strings too. A literal backtick is escaped with ```.
When a multiline backtick literal opens on its own line, common source indentation is stripped so the surrounding code can remain indented without changing the text’s intended layout.
Three things are removed, and the last two are the ones that surprise people:
- the common indentation, measured over the whole body and taken off each line, so the deepest-indented line keeps whatever it has beyond the shallowest;
- the newline immediately after the opening backtick;
- the newline and indentation before the closing backtick.
So this literal is thirty bytes and three lines, with no leading or trailing newline:
fn block() -> String { ` line one indented line three `}It is "line one\n indented\nline three". indented keeps two spaces because the common prefix was two, not four. A literal that should end with a newline needs a blank line before the closing backtick, and one that should start with a newline needs a blank line after the opening one.
None of it applies when the content starts on the opening line, which is the form to reach for when the text must be taken exactly as written:
let raw = `first second`;Line comments
Section titled “Line comments”// ordinary commentAPI documentation uses three slashes:
/// Returns the user for `id`.pub fn load_user(id: Id) -> User;Module documentation uses //!:
//! User-domain types and operations.module app::users;khora doc reads /// and //! comments as Markdown documentation.
Block comments
Section titled “Block comments”/* a block comment */Block comments may nest:
/* outer /* inner */ outer again*/Hard keywords
Section titled “Hard keywords”These words are reserved:
module import type trait impl fn match let mut pub asif else forall const effect with raises raise catchwhile loop break continue for return true falseContextual keywords
Section titled “Contextual keywords”These words are keywords only in their grammatical position and remain usable as ordinary identifiers elsewhere:
handler in capability row test bench derive externExamples of their keyword positions:
handler for Clock { now: fn () => fixed_instant }for item in items { process(item); }context Production { clock: live_clock }test "works" { assert(true); }bench "parse" { parse(fixture); }derive(Eq, Show)type Point = { x: Int, y: Int };extern fn strlen(ptr: Ptr) -> U64;in is contextual in the for pattern in expression form.
Reserved words
Section titled “Reserved words”These words are refused as identifiers and mean nothing yet:
where yield macro unsafe unstableUsing one as a name is an error naming the word, not a syntax error:
error: `yield` is reserved for a future version of Khora and cannot be used as a name. Nothing uses it yet -- it is held back because Khora has no editions, so a word claimed after 1.0 would break every program that had named something with itThey are held back because Khora has no editions and no unstable marker, so
a keyword introduced after 1.0 would break every program that had used the
word as a name, with no mechanism to migrate one. Reserving a word now costs
one name; reserving it afterwards is not possible at all.
A reservation is an option, not a promise. None of these words is
scheduled, and the spellings are not fixed — if generators arrive written some
other way, yield will have been held back for nothing. The list is short for
that reason. They are also not keywords: an editor will not highlight them,
because there is no construct to highlight.
Punctuation with language meaning
Section titled “Punctuation with language meaning”Common multi-character tokens include:
:: compile-time path separator-> function/type arrow=> match arm or lambda separator|> pipeline||> flow lambda== != <= >=&& ||Single-character punctuation includes ; , . : | = + - * / % ! < > ( ) { } [ ] _.
See Expressions for operator precedence and Declarations for the positions in which the declaration-oriented keywords appear.
Khora 0.4.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource