Lexical rules#
Whitespace#
Spaces (' '), tabs ('\t'), and carriage returns ('\r') are insignificant and skipped
between tokens. Newlines ('\n') are significant tokens used as statement separators.
The parser skips newlines between statements but they are preserved in the token stream.
Semicolons (';') are also accepted as optional statement separators in statement-list
contexts (top-level items, block statements, tool bodies, and skill fields), but they
are non-canonical input syntax. harn fmt normalizes them back to newline-separated form.
Backslash line continuation#
A backslash (\) immediately before a newline joins the current line with the next.
Both the backslash and the newline are removed from the token stream, so the two
physical lines are treated as a single logical line by the lexer.
const total = 1 + 2 \
+ 3 + 4
// equivalent to: const total = 1 + 2 + 3 + 4
This is useful for breaking long expressions that do not involve a binary operator eligible for multiline continuation (see "Multiline expressions").
Comments#
// Line comment: everything until the next newline is ignored.
/* Block comment: can span multiple lines.
/* Nesting is supported. */
Still inside the outer comment. */
Block comments track nesting depth, so /* /* */ */ is valid. An unterminated block comment produces a lexer error.
Keywords#
The following identifiers are reserved:
| Keyword | Token |
|---|---|
pipeline | .pipeline |
extends | .extends |
override | .overrideKw |
let | .letKw |
const | .constKw |
var | .varKw (reserved for the removed-keyword migration diagnostic) |
if | .ifKw |
else | .elseKw |
for | .forKw |
in | .inKw |
match | .matchKw |
retry | .retry |
parallel | .parallel |
defer | .defer |
return | .returnKw |
import | .importKw |
true | .trueKw |
false | .falseKw |
nil | .nilKw |
try | .tryKw |
catch | .catchKw |
throw | .throwKw |
throws | .throwsKw |
finally | .finally |
fn | .fnKw |
spawn | .spawnKw |
while | .whileKw |
type | .typeKw |
enum | .enum |
struct | .struct |
interface | .interface |
pub | .pub |
guard | .guard |
yield | .yield |
break | .break |
continue | .continue |
impl | .impl |
Contextual keywords#
These words introduce syntax only in one position and are ordinary
identifiers everywhere else, so let tool = ..., let select = ..., and
config.from work:
| Word | Introduces syntax when |
|---|---|
select | followed by { at statement position |
tool, skill, eval_pack | followed by a declaration name |
mutex | followed by { or by ( key ) { |
deadline | followed by a duration, number, or name, or by ( duration ) { |
require, emit | at statement position, unless the next token continues a value expression (=, ., a binary operator, ,, a closing bracket, or a line break) |
scope, block, cost_route | followed by { at statement position |
from | after an import list or a select case variable |
gen | followed by fn |
to and exclusive are not keywords. They spelled ranges before Harn 0.11;
ranges are now a..b and a..=b, and a to b reports HARN-PAR-007.
Identifiers#
An identifier starts with a letter or underscore, followed by zero or more letters, digits, or underscores:
identifier ::= [a-zA-Z_][a-zA-Z0-9_]*
Number literals#
int_literal ::= digit+
float_literal ::= digit+ '.' digit+
A number followed by . where the next character is not a digit is lexed as an integer
followed by the . operator (enabling 42.method). 1..3 therefore lexes as an
integer, the .. range operator, and an integer.
Range operators#
.. and ..= are single tokens. A run of three dots stays three . tokens, which the
parser reads as a spread or rest marker ([...xs]).
Duration literals#
A duration literal is an integer followed immediately (no whitespace) by a time-unit suffix:
duration_literal ::= digit+ ('ms' | 's' | 'm' | 'h' | 'd' | 'w')
| Suffix | Unit | Equivalent |
|---|---|---|
ms | milliseconds | -- |
s | seconds | 1000 ms |
m | minutes | 60 s |
h | hours | 60 m |
d | days | 24 h |
w | weeks | 7 d |
Duration literals evaluate to an integer number of milliseconds. They can be used anywhere an expression is expected:
harness.clock.sleep_ms(500ms)
deadline 30s { /* ... */ }
const one_day = 1d // 86400000
const two_weeks = 2w // 1209600000
String literals#
Single-line strings#
string_literal ::= '"' (char | escape | interpolation)* '"'
escape ::= '\' ('n' | 'r' | 't' | '0' | '\\' | '"' | 'Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes
Lexical rules#
Whitespace#
Spaces (' '), tabs ('\t'), and carriage returns ('\r') are insignificant and skipped
between tokens. Newlines ('\n') are significant tokens used as statement separators.
The parser skips newlines between statements but they are preserved in the token stream.
Semicolons (';') are also accepted as optional statement separators in statement-list
contexts (top-level items, block statements, tool bodies, and skill fields), but they
are non-canonical input syntax. harn fmt normalizes them back to newline-separated form.
Backslash line continuation#
A backslash (\) immediately before a newline joins the current line with the next.
Both the backslash and the newline are removed from the token stream, so the two
physical lines are treated as a single logical line by the lexer.
const total = 1 + 2 \
+ 3 + 4
// equivalent to: const total = 1 + 2 + 3 + 4
This is useful for breaking long expressions that do not involve a binary operator eligible for multiline continuation (see "Multiline expressions").
Comments#
// Line comment: everything until the next newline is ignored.
/* Block comment: can span multiple lines.
/* Nesting is supported. */
Still inside the outer comment. */
Block comments track nesting depth, so /* /* */ */ is valid. An unterminated block comment produces a lexer error.
Keywords#
The following identifiers are reserved:
| Keyword | Token |
|---|---|
pipeline | .pipeline |
extends | .extends |
override | .overrideKw |
let | .letKw |
const | .constKw |
var | .varKw (reserved for the removed-keyword migration diagnostic) |
if | .ifKw |
else | .elseKw |
for | .forKw |
in | .inKw |
match | .matchKw |
retry | .retry |
parallel | .parallel |
defer | .defer |
return | .returnKw |
import | .importKw |
true | .trueKw |
false | .falseKw |
nil | .nilKw |
try | .tryKw |
catch | .catchKw |
throw | .throwKw |
throws | .throwsKw |
finally | .finally |
fn | .fnKw |
spawn | .spawnKw |
while | .whileKw |
type | .typeKw |
enum | .enum |
struct | .struct |
interface | .interface |
pub | .pub |
guard | .guard |
yield | .yield |
break | .break |
continue | .continue |
impl | .impl |
Contextual keywords#
These words introduce syntax only in one position and are ordinary
identifiers everywhere else, so let tool = ..., let select = ..., and
config.from work:
| Word | Introduces syntax when |
|---|---|
select | followed by { at statement position |
tool, skill, eval_pack | followed by a declaration name |
mutex | followed by { or by ( key ) { |
deadline | followed by a duration, number, or name, or by ( duration ) { |
require, emit | at statement position, unless the next token continues a value expression (=, ., a binary operator, ,, a closing bracket, or a line break) |
scope, block, cost_route | followed by { at statement position |
from | after an import list or a select case variable |
gen | followed by fn |
to and exclusive are not keywords. They spelled ranges before Harn 0.11;
ranges are now a..b and a..=b, and a to b reports HARN-PAR-007.
Identifiers#
An identifier starts with a letter or underscore, followed by zero or more letters, digits, or underscores:
identifier ::= [a-zA-Z_][a-zA-Z0-9_]*
Number literals#
int_literal ::= digit+
float_literal ::= digit+ '.' digit+
A number followed by . where the next character is not a digit is lexed as an integer
followed by the . operator (enabling 42.method). 1..3 therefore lexes as an
integer, the .. range operator, and an integer.
Range operators#
.. and ..= are single tokens. A run of three dots stays three . tokens, which the
parser reads as a spread or rest marker ([...xs]).
Duration literals#
A duration literal is an integer followed immediately (no whitespace) by a time-unit suffix:
duration_literal ::= digit+ ('ms' | 's' | 'm' | 'h' | 'd' | 'w')
| Suffix | Unit | Equivalent |
|---|---|---|
ms | milliseconds | -- |
s | seconds | 1000 ms |
m | minutes | 60 s |
h | hours | 60 m |
d | days | 24 h |
w | weeks | 7 d |
Duration literals evaluate to an integer number of milliseconds. They can be used anywhere an expression is expected:
harness.clock.sleep_ms(500ms)
deadline 30s { /* ... */ }
const one_day = 1d // 86400000
const two_weeks = 2w // 1209600000
String literals#
Single-line strings#
string_literal ::= '"' (char | escape | interpolation)* '"'
escape ::= '\' ('n' | 'r' | 't' | '0' | '\\' | '"' | '