1. Lexical structure
This section defines how Korrin source text is divided into tokens. It is
normative. The implementation is crates/korrin/src/lexer/.
Governing decisions: 0001, 0003, 0006, 0007.
1.1 Source encoding
A Korrin source unit is a sequence of Unicode scalar values, encoded as UTF-8. Byte offsets into this sequence are used to report positions.
The only significant line terminator is U+000A (line feed, \n). A U+000D
(carriage return, \r) is ignored wherever it appears, so \r\n is accepted as
a line ending.
1.2 Logical lines and layout tokens
The lexer produces four synthetic tokens that do not correspond to any run of source characters:
| Token | Meaning |
|---|---|
NEWLINE | The end of a logical line that contained at least one token. |
INDENT | The following logical line opens a more-indented block. |
DEDENT | The end of an indented block. One is emitted per level closed. |
EOF | End of input. Always the final token. |
A logical line is one or more physical lines that the lexer treats as a single statement-bearing unit. A physical line break is not a logical line break when:
- any bracket —
(,[, or{— opened earlier is still unclosed; or - the last token before the break was one after which the line cannot end: a
binary operator (
+ - * / % == != < <= > >=),=,,,., or a word operator (and,or,not).
There is no line-continuation character. A logical line may not be split except by the two rules above.
total = (1 +
2 +
3)
print(total)
# => 6
1.3 Blank lines and comments
A comment starts with # and runs to the end of the physical line. There are
no block comments.
A physical line that, after removing leading spaces, is empty or begins with #
is a blank line. Blank lines produce no tokens — not even NEWLINE — and
never affect indentation.
1.4 Indentation
Indentation is the run of space characters (U+0020) at the start of a physical line that begins a logical line. Its width is the count of those spaces.
- A tab (U+0009) anywhere in that leading run is an error
(
E0006). Korrin indents with spaces only. - Tabs elsewhere on a line — between tokens, inside a string — are not restricted (though between tokens a tab is simply whitespace).
The lexer maintains a stack of indentation widths, initially [0]. For each
logical line, let w be its indentation width and t the top of the stack:
w > t: pushw; emit oneINDENT.w < t: pop while the top is greater thanw, emitting oneDEDENTper pop. If the resulting top is not equal tow, that is an inconsistent dedent (E0007).w == t: emit nothing.
At EOF, a NEWLINE is emitted if the last logical line had tokens, then one
DEDENT for every stack entry above the base level 0.
The indentation width that opens a block is not fixed by the language — any consistent increase works — but the style guide requires 4 spaces.
if true:
a = 1
if true:
b = 2
c = 3
print(a + c)
# => 4
1.5 Identifiers
An identifier begins with _ or a character in Unicode XID_Start, and
continues with _ or characters in XID_Continue (Unicode UAX #31). Identifiers
are compared by exact scalar-value sequence; there is no case folding and no
normalization.
1.6 Keywords
The following 23 identifiers are reserved and may not be used as names (ADR 0006):
and break class continue elif
else except false finally fn
for if in nil not
or pass raise return super
true try while
self is not reserved — it is the conventional name of a method's first
parameter (ADR 0005) and is
otherwise an ordinary identifier.
1.7 Literals
Integer literals
One or more ASCII digits: 0, 7, 1000. The value must fit in a signed 64-bit
integer, otherwise E0005. There are no digit separators, sign
characters (a leading - is the unary operator), or radix prefixes in
Milestone 1.
Float literals
Digits, then either a fraction, an exponent, or both:
- fraction:
.followed by one or more digits —3.14,0.5; - exponent:
eorE, an optional+or-, then one or more digits —1e9,2.5e-3.
A . is only a decimal point when a digit follows it, so x.field and
3.name tokenize as an access, not a malformed number. A . or identifier
character glued to the end of a number (1.2.3, 10abc) is
E0004.
print(type(10))
print(type(10.0))
# => int
# => float
String literals
Text between double quotes: "hello". A string literal may not contain a raw
newline — an unclosed string at end of line is E0002.
Single quotes are not string delimiters.
The recognized escape sequences are exactly:
| Escape | Character |
|---|---|
\n | line feed |
\t | tab |
\r | carriage return |
\\ | backslash |
\" | double quote |
Any other \x is E0003. Raw strings and triple-quoted strings
do not exist.
Interpolation
Every string literal is a template (ADR 0015).
A { begins a hole; the text up to the matching } is a Korrin expression,
and its value is spliced into the string when the string is evaluated (§4.2).
{{and}}are literal{and}. A lone}is also literal.- A hole is tokenized as ordinary tokens, so a string inside a hole needs no
escaping:
"{m["k"]}". - An empty hole
"{}"isE0103. A{not closed before the end of the line isE0002. - A hole may not contain a newline (a string may not span lines).
A string with no holes is lexically a single string token. A string with at
least one hole is lexed as STRSTART, then a run of STRTEXT chunks and
{ expression } holes, then STREND — the hole tokens carrying their real
source positions.
1.8 Operators and punctuation
( ) [ ] { } , : .
+ - * / %
= == != < <= > >=
! is not a token on its own; it exists only as part of !=. A lone ! is
E0001, which suggests != or not.