Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Operator precedence table

From lowest to highest binding:

PrecedenceOperatorsAssociativityDescription
1|>LeftPipe
2? :RightTernary conditional
3||LeftLogical OR
4&&LeftLogical AND
5== !=LeftEquality
6< > <= >= in not inLeftComparison / membership
7+ -LeftAdditive
8??LeftNil coalescing
9* / %LeftMultiplicative
10! - (unary)Right (prefix)Unary
11**RightExponentiation
12. ?. [] ?.[] [:] () ? !LeftPostfix

! appears at two precedence levels with two distinct meanings: a prefix ! (level 10) is logical negation (!ok), while a postfix ! (level 12) is the non-null assertion (value!). They never collide — a ! before an operand is negation, a ! after one is the assertion — and != is a single token, so neither reading is ambiguous.

Exponentiation binds more tightly than a unary prefix on its left operand, so -2 ** 2 parses as -(2 ** 2) (-4), matching Python, Ruby, and ordinary math notation rather than the spreadsheet (-2) ** 2 reading. The right (exponent) operand still accepts a unary prefix, so 2 ** -3 is 2 ** (-3), and chained ** remains right-associative (2 ** 3 ** 2 is 2 ** (3 ** 2)).

Multiline expressions

Binary operators |>, ||, &&, ==, !=, <, >, <=, >=, ??, +, *, /, %, **, and the . / ?. member access operators can span multiple lines. The operator at the start of a continuation line causes the parser to treat it as a continuation of the previous expression rather than a new statement.

Note: - does not support multiline continuation because it is also a unary negation prefix. Keyword operators in, not in, and to also require an explicit backslash continuation.

const result = items
  .filter({ x -> x > 0 })
  .map({ x -> x * 2 })

const msg = "hello"
  + " "
  + "world"

const ok = check_a()
  && check_b()
  || fallback()

Pipe placeholder (_)

When the right side of |> contains _ identifiers, the expression is automatically wrapped in a closure where _ is replaced with the piped value:

"hello world" |> split(_, " ")     // desugars to: |> { __pipe -> split(__pipe, " ") }
[3, 1, 2] |> _.sorted()             // desugars to: |> { __pipe -> __pipe.sorted() }
items |> len(_)                    // desugars to: |> { __pipe -> len(__pipe) }

Without _, the pipe passes the value as the sole argument to the callable on the right side. Use _ whenever the piped value should be placed inside a larger expression or a specific argument position.