2. Lexical structure
- Comments:
# this is a line comment. There are no block comments. A run of # comment lines directly above an item, with no blank line between, is that item's documentation comment, and a fenced ` Hanki block inside it is a runnable example (§20). - File extension:
.hk. One file is one module. - Identifiers: ASCII
[A-Za-z_][A-Za-z0-9_]*, optionally followed by a single ! or ? suffix.foo! is by convention an action, an effectful function. The ! is part of the name.foo? is a predicate, and H0621 enforces it: a pure def/prop returning bool must end in ?, and a ? name must return bool. The suffix is the marker and the name says it once: empty?, leap_year?, contains?, never is_empty or is_empty?. Actions are outside the rule. A name takes one suffix character and an action's ! has taken it: stdin_tty! returns bool and has no ?, and foo!? does not lex. Two spellings are unavailable to a predicate: a keyword stem (trait? does not lex, keyword lookup preceding suffix-taking), and, inside a file that imports it, a stdlib module's own name. The bool in this rule is exact and remains so. Hanki has no truthiness: there is no Falsifiable-style trait making arbitrary types testable, and 0 and "" are valid data. A condition takes a bool and nothing besides; the test is spelled at the call site (if not xs.empty?, never if xs).
- Keywords:
def actor on match if else elif end then var type struct opaque impl trait throw try catch spawn async await move loop do break continue return use open meta true false self and or not where effect provide test. - Contextual keywords:
state, migrate, as, prop. Each opens a construct in one position: a state field or migrate hook in an actor body (§15), an alias on use (§14), a property in a trait or impl body (§10). Each is an ordinary identifier in every other position, and none of them costs you a name. open and move remain reserved. Both sit in expression position, where the same trick has nothing to key on. - Named values and types:
= associates a value with a name in bindings, keyword arguments, constructor fields and configurable attributes. : associates a type or trait bound with a name. Thus @property(cases=1000) and Todo(id=1) use =, while x: i32 and T: Display use :. - Whitespace: spaces and tabs are insignificant. Newlines terminate statements. The exception is a leading dot: a line whose first token is
.name continues the expression above it, and a method chain reads top to bottom with no named intermediate and no line-continuation marker. This is unambiguous. No other construct in the grammar begins with ., neither a statement nor an expression nor a match arm, and a newline run followed by .name can only be more of what came before; a newline run followed by anything else ends the statement. Member access and calls only: .5 on its own line is not a continuation. hanki fmt preserves the break where the writer put it and normalises the indent to two spaces under the receiver's line. It never breaks a single-line chain, however long.
def total(xs: List<int>) -> int
xs.filter(|x| x > 1)
.map(|x| x * 2)
.fold(0, |acc, x| acc + x)
end
- Line continuation: end a line with
\ to continue on the next. - String literals:
"…" with \n \t \r \\ \" \0 \# escapes and #{expr} interpolation.