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 |
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 |
emit | .emit |
spawn | .spawnKw |
while | .whileKw |
type | .typeKw |
enum | .enum |
eval_pack | .evalPack |
struct | .struct |
interface | .interface |
pub | .pub |
from | .from |
to | .to |
tool | .tool |
skill | .skill |
exclusive | .exclusive |
guard | .guard |
require | .require |
deadline | .deadline |
yield | .yield |
mutex | .mutex |
break | .break |
continue | .continue |
select | .select |
impl | .impl |
request_approval | .requestApproval |
dual_control | .dualControl |
ask_user | .askUser |
escalate_to | .escalateTo |
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).
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:
sleep(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' | '\\' | '"' | '