Grammar Specification
This document provides a formal grammar specification for the Kit programming language in Extended Backus-Naur Form (EBNF).
Notation
| Symbol | Meaning |
|---|---|
| | Alternation |
[ ] | Optional |
{ } | Zero or more repetitions |
( ) | Grouping |
" " | Terminal string |
UPPER_CASE | Token/terminal |
lower_case | Non-terminal |
Program Structure
program = { declaration } EOF ;
declaration = type_def
| type_signature
| import_stmt
| export_stmt
| module_decl
| test_block
| extern_decl
| trait_def
| trait_impl
| binding ;
Type Definitions
type_def = "type" TYPE_NAME { TYPE_VAR } "=" type_body ;
type_body = constructor { "|" constructor }
| "|" constructor { "|" constructor } ;
constructor = CONSTRUCTOR_NAME [ constructor_args ] ;
constructor_args = "(" field_list ")"
| type { type } ;
field_list = field { "," field } ;
field = IDENT { attribute } ;
Attributes
Attributes provide metadata annotations. Kit recognizes a small set of compiler-known and linter-known attributes, and preserves module, type, and constructor-field attributes for package tooling and external tools.
attribute = "@" attr_name [ "(" attr_args ")" ] ;
attr_name = IDENT { "-" IDENT } ; (* supports hyphenated names *)
attr_args = attr_arg { "," attr_arg } ;
attr_arg = STRING | INT | FLOAT | "true" | "false" | IDENT ;
Recognized attributes include:
- Compiler-known attributes:
@pre,@post,@defer-required, and@skip - Linter-known attributes:
@deprecatedand@experimental - Package/tool metadata attributes such as module-level
@check(...),@feature(...), and@target(...) - Unknown module, type, and constructor-field attributes with kebab-case names, which are preserved for downstream tools
Unknown binding and test attributes are not accepted as executable annotations. Attributes must not hide authority; capabilities and effects remain explicit values.
@target(name[, ...]) marks a module as active only for the listed targets when commands
run with --target. Target arguments may be strings or identifiers. Files without
@target are active for every target.
@experimental("reason") marks a binding or type as unstable. kit check
reports W022 for experimental declarations so package release profiles can surface them before
publication.
Import/Export
import_stmt = "import" import_path [ "as" IDENT ]
| "import" import_path "." "{" import_list "}" ;
import_path = STRING | module_path ;
module_path = IDENT { "." IDENT } ;
import_list = import_item { "," import_item } ;
import_item = IDENT [ "as" IDENT ] ;
export_stmt = "export" ( binding | type_def ) ;
module_decl = "module" module_path ;
For string path imports, the imported file's module declaration does not create an
implicit namespace in the importing file. Use selective imports for bare names
(import "./greeting.kit".{message}) or an explicit alias for qualified access
(import "./greeting.kit" as Greeting, then Greeting.message).
Test Blocks
test_block = [ "@skip" [ STRING ] ] "test" [ test_name ] "=>" indented_block ;
test_name = STRING | KEYWORD_LITERAL ;
The @skip attribute marks a test to be skipped during execution, with an optional reason string.
Test Assertions
Kit provides a comprehensive set of assertion functions for use within test blocks:
Basic Assertions:
assert! cond- Assert condition is trueassert-eq! expected actual- Assert two values are equalassert-ne! a b- Assert two values are not equalassert-true! value- Assert value is trueassert-false! value- Assert value is false
Option/Result Assertions:
assert-some! value- Assert value is Some variantassert-none! value- Assert value is None variantassert-ok! value- Assert value is Ok variantassert-err! value- Assert value is Err variant
Comparison Assertions (integers only):
assert-lt! a b- Assert a < bassert-gt! a b- Assert a > bassert-lte! a b- Assert a <= bassert-gte! a b- Assert a >= b
Float Assertions:
assert-approx! expected actual tolerance- Assert floats are approximately equal
Failure Assertions:
assert-fail! message- Unconditionally fail with message
Test Examples
test "basic assertions" =>
assert!(2 + 2 == 4)
assert-eq!(1 + 1, 2)
assert-ne!("hello", "world")
assert-true!(true)
assert-false!(false)
test "option assertions" =>
assert-some!(Some 42)
assert-none!(None)
test "result assertions" =>
assert-ok!(Ok 42)
assert-err!(Err "error")
test "comparison assertions" =>
assert-lt!(3, 5)
assert-gt!(10, 5)
assert-lte!(3, 3)
assert-gte!(5, 5)
test "float approximation" =>
assert-approx!(3.14159, 3.14, 0.01)
Note: Assertions only work in test blocks and in the Interpreter.
The Compiler does not compile tests.
Match Macros
Kit provides three concise pattern matching macros for common operations:
is_expr = expression "is" pattern ;
as_expr = expression "as" pattern "->" expression "else" expression ;
guard_stmt = "guard" pattern "=" expression "else" expression ;
is- Check if a value matches a pattern, returnsBoolas- Extract value if pattern matches, return alternative otherwiseguard- Bind pattern variables if match succeeds, return early otherwise
These macros provide concise alternatives to verbose match expressions.
Compile-Time Macros
Kit supports user-defined compile-time macros for code generation:
macro_def = "macro" IDENT { IDENT } "=" quasiquote ;
quasiquote = "`" "(" expression ")" ;
unquote = "$" IDENT ;
Macros are expanded at compile time. The quasiquote syntax (`) creates a code
template, and $param unquotes (substitutes) parameter values into the template.
# Define a macro
macro double x = `($x + $x)
# Usage - expands to (5 + 5) at compile time
result = double(5) # 10
Extern Declarations
extern_decl = extern_c_decl | extern_zig_decl ;
extern_c_decl = "extern-c" IDENT "(" [ param_list ] ")" "->" type
"from" STRING "link" STRING ;
extern_zig_decl = "extern-zig" IDENT "(" [ param_list ] ")" "->" type
"from" STRING "link" STRING ;
param_list = param { "," param } ;
param = IDENT ":" type ;
extern-cdeclares a C function bindingextern-zigdeclares a Zig function binding
Traits
trait_def = "trait" IDENT TYPE_VAR [ "requires" constraint_list ]
trait_body ;
trait_body = { trait_method } ;
trait_method = IDENT ":" type
| IDENT "=" expression ;
trait_impl = "extend" type "with" IDENT [ "where" constraint_list ]
impl_body ;
impl_body = { method_def } ;
method_def = IDENT "=" expression ;
constraint_list = constraint { "," constraint } ;
constraint = IDENT TYPE_VAR ;
Bindings
type_signature = ( IDENT | IDENT "." IDENT ) ":" type ;
binding = { contract } pattern [ ":" type ] [ linearity ] "=" expression
| qualified_binding ;
linearity = " @linear" (* must use exactly once *)
| " @affine" (* can use at most once *)
| " @relevant" (* must be used at least once *)
| " @unrestricted" (* no constraints - default *) ;
qualified_binding = TYPE_NAME "." IDENT "=" expression ;
contract = "@pre" "(" expression [ "," STRING ] ")"
| "@post" "(" expression [ "," STRING ] ")" ;
Standalone type signatures are checked declarations that must appear before exactly one binding or export with the same name in the same module:
parse : Parser a -> String -> Result a ParseError
parse = fn(parser, input) => ...
Qualified bindings can use the same checked signature form:
Either.map-left : (a -> c) -> Either a b -> Either c b
Either.map-left = fn(f, e) => ...
If a name has a standalone signature, the corresponding binding must not also use an inline annotation. Use one form or the other.
Constrained linearity annotations (@linear, @affine, @relevant)
are validated at function, lambda, and test scope boundaries. They are rejected on top-level bindings
because module initialization does not provide a usage scope where the constraint can be satisfied.
Use a local binding or lambda parameter instead. @borrow is parameter-only: a borrowed
parameter may be used only in borrow contexts and does not consume a caller-owned linear value.
Unmarked references to constrained bindings are consuming uses. @linear and
@affine bindings move on first consuming use, so using the same binding again as a
consuming argument is a use-after-move error. Selected builtin argument positions are classified as
non-consuming borrows; these currently cover observation builtins (println,
to-string, show, format, stdout/stderr writes), channel,
actor, supervisor, TCP/UDP socket, and HTTP server/request/stream operations. Direct constrained
identifiers inside string and SQL interpolation also borrow, matching observation builtins; nested
interpolation expressions still follow normal consuming rules. Borrow/consume metadata for builtins
is preserved through direct local aliases, selective imports, module aliases, wildcard imports, and
re-exports when the aliased value is itself a recognized builtin callee.
Lifecycle endpoints remain consuming where they release or complete a resource: for example channel
close, TCP close/shutdown, UDP close, HTTP respond/finish/close, actor wait/terminate, and
supervisor stop. Actor.stop is a borrowed graceful-stop request so a linear actor
handle can still be consumed by Actor.wait. The actor send operator
actor <- msg follows Actor.send ownership and borrows the actor handle.
Borrow support is intentionally narrow: only direct constrained identifiers passed to
metadata-marked builtin arguments or aliases of those builtins, directly interpolated as
${name} in strings or SQL blocks, used as the actor operand of <-,
checked as the direct subject of is Pattern, introduced as direct using
evidence, or passed to a user-defined function parameter annotated @borrow, borrow the
value. Higher-order wrappers can infer that a callback must borrow a direct constrained argument
when the wrapper reuses that argument later in the same sequence; otherwise unknown callback
arguments default to consuming uses. A value that is only borrowed still has not satisfied a
@linear or @relevant consumption requirement.
for bodies may execute zero or many times. Constrained variables declared inside the
loop body are validated for each body execution, but constrained variables from an outer scope
cannot be used inside a for body.
Contracts (@pre and @post) provide precondition and postcondition
assertions for functions. The optional string argument provides a custom error message.
Destructuring in Bindings
Bindings support pattern matching with tuple and record patterns, allowing you to extract values directly:
# Tuple destructuring
(x, y) = (10, 20)
(a, b, c) = get-triple()
# Record destructuring with shorthand (field name becomes binding name)
{name, age} = person
# Record destructuring with explicit rename
{name: user-name, age: user-age} = person
# Partial record destructuring (extract only some fields)
{x, z} = {x: 1, y: 2, z: 3}
# Config records with optional-looking fields should use explicit defaults and
# record spread. `config.field ?? default` does not make a missing field
# optional: direct field access is still type-checked, and `??` expects an
# Option or Result value.
default-options = {enabled: true, interval: 16.0}
options = {...default-options, interval: 30.0}
# Nested destructuring
{outer: {inner}} = nested-record
((a, b), (c, d)) = nested-tuples
# Wildcards to ignore values
{keep, ignore: _} = data
(_, important, _) = triple
Expressions
Precedence (lowest to highest)
expression = using_expr ;
using_expr = "using" IDENT { "," IDENT } "=>" using_body
| defer_expr ;
using_body = expression
| indented_block ;
defer_expr = "defer" expression | null_coalesce ;
null_coalesce = pipe_expr { "??" pipe_expr } ;
pipe_expr = send_expr { ( "|>" | "|>>" ) send_expr } ;
send_expr = or_expr [ "<-" or_expr ] ;
or_expr = and_expr { ( "||" | "or" ) and_expr } ;
and_expr = is_as_expr { ( "&&" | "and" ) is_as_expr } ;
is_as_expr = equality [ "is" pattern ]
| equality "as" pattern "->" expression "else" expression
| equality ;
equality = comparison { ( "==" | "!=" ) comparison } ;
comparison = range_expr { ( "<" | "<=" | ">" | ">=" ) range_expr } ;
range_expr = concat [ ( "..<" | "..=" ) concat ] ;
concat = cons { "++" cons } ;
cons = term { ( "::" | "@" ) term } ;
term = factor { ( "+" | "-" ) factor } ;
factor = unary { ( "*" | "/" | "%" ) unary } ;
unary = ( "-" | "!" ) unary | call_expr ;
call_expr = primary { call_suffix } ;
call_suffix = "(" [ arg_list ] ")" (* C-style call *)
| "." IDENT (* field access *)
| "?!" (* error propagation *)
| prefix_arg ; (* Haskell-style *)
prefix_arg = primary ; (* excluding operators *)
arg_list = expression { "," expression } ;
Kit functions are curried in the type model, with multi-argument syntax as ergonomic sugar.
fn(a, b) => body has type a -> b -> result, while
fn((a, b)) => body accepts one tuple argument. f a b and
f(a, b) have the same application semantics. Under-application returns a function;
exact application calls the function; over-application continues only when the intermediate result
is callable. Applying surplus arguments to a non-callable result is an error.
Zero-arity functions (fn => expr or fn() => expr) auto-invoke when
referenced as values. Use no-op only when intentionally passing the Unit
value to a one-argument function. See
application-semantics.md
for the per-context rules (imports, re-exports, forward references, lazy, externs).
Scoped Evidence
using evidence => body introduces direct identifier evidence for
guarded operations in body. It is compile-time-only capability evidence:
it does not pass hidden arguments, create ambient authority, import names, or run cleanup.
auth: EntropyAuth = entropy-auth RootAuth
value = using auth =>
Math.random
value2 = using root, auth =>
Random.float auth
Evidence must be one or more direct identifiers whose types are known capability
types. Evidence is borrowed, so a using scope does not count as
consumption for @linear or @relevant values.
Primary Expressions
primary = INT | FLOAT | STRING | CHAR
| "true" | "false"
| "no-op" (* unit value *)
| KEYWORD_LITERAL (* :symbol *)
| IDENT
| field_accessor
| lambda | if_expr | match_expr | for_expr | sql_expr
| group_or_tuple | list_literal | set_literal
| record_literal | interpolated_string | heredoc ;
field_accessor = "." IDENT ; (* shorthand for fn(x) => x.IDENT *)
lambda = "fn" [ "(" [ param_patterns ] ")" ] "=>" lambda_body ;
param_patterns = param_pattern { "," param_pattern } ;
param_pattern = [ "~" ] pattern ; (* ~ marks lazy-accepting parameter *)
lambda_body = expression | indented_block ;
if_expr = "if" expression "then" if_branch "else" if_branch ;
if_branch = expression | indented_block ;
match_expr = "match" expression match_arms ;
match_arms = { "|" match_arm } ;
match_arm = or_pattern [ guard ] "->" expression
| or_pattern [ guard ] "=>" expression ;
or_pattern = pattern { "|" pattern } ;
guard = "when" expression | "if" expression ;
for_expr = "for" pattern "in" expression "=>" for_body ;
for_body = expression | indented_block ;
sql_expr = "sql" [ expression [ "as" type_name ] ] "{" sql_content "}" ;
sql_content = { SQL_CHAR | "${" expression [ ":" FORMAT_SPEC ] "}" } ;
group_or_tuple = "(" expression [ "," expression { "," expression } ] ")" ;
list_literal = "[" [ list_elements | list_comprehension ] "]" ;
list_elements = expression { "," expression } [ "|" expression ] ;
list_comprehension = expression "for" pattern "in" expression [ "if" expression ] ;
set_literal = "#{" [ expression { "," expression } ] "}" ;
record_literal = "{" [ record_fields ] "}" ;
record_fields = record_field { "," record_field } ;
record_field = "..." expression (* spread *)
| IDENT ":" expression
| IDENT ; (* shorthand *)
interpolated_string = STRING_START { string_part } STRING_END ;
string_part = STRING_CONTENT
| "${" expression [ ":" FORMAT_SPEC ] "}" ;
heredoc = "<<~" IDENT NEWLINE heredoc_content IDENT ;
heredoc_content = { HEREDOC_CHAR | "${" expression [ ":" FORMAT_SPEC ] "}" } ;
Note: Zero-arity functions can omit parentheses:
fn => expr is equivalent to fn() => expr. () is not a value
expression; use no-op for the Unit value.
Heredocs use squiggly syntax (<<~) which strips common leading indentation.
They support the same string interpolation as regular strings.
Range literal syntax lowers to List.range. start..<end uses existing
half-open [start, end) semantics. start..=end is inclusive and lowers to
a half-open range with end + 1. List comprehensions lower to List.map, or
to List.map over List.filter when an if clause is present.
Patterns
pattern = "_" (* wildcard *)
| INT | FLOAT | STRING
| "true" | "false"
| KEYWORD_LITERAL
| IDENT (* binding or constructor *)
| constructor_pattern
| tuple_pattern | list_pattern | record_pattern ;
constructor_pattern = CONSTRUCTOR_NAME [ constructor_payload ] ;
constructor_payload = "(" pattern { "," pattern } ")"
| "{" record_pat_fields [ ".." ] "}"
| pattern { pattern } ; (* space-separated *)
tuple_pattern = "(" pattern "," pattern { "," pattern } ")" ;
list_pattern = "[" [ list_pat_elements ] "]" ;
list_pat_elements = ".." pattern (* rest at start *)
| pattern { "," pattern } [ ( "|" | ".." ) pattern ] ;
record_pattern = "{" [ record_pat_fields ] "}" ;
record_pat_fields = record_pat_field { "," record_pat_field } ;
record_pat_field = IDENT [ ":" pattern ] ;
Types
type = simple_type [ "->" type ] (* single-parameter function type *)
| "(" [ function_type_params ] ")" "->" type ;
function_type_params = function_type_param { "," function_type_param } ;
function_type_param = type [ " @borrow" ] ;
simple_type = "Int" | "Float" | "String" | "Bool" | "Unit"
| TYPE_NAME [ type_args ]
| tuple_type | record_type | refinement_type
| "(" type ")" ;
type_args = simple_type { simple_type } ;
tuple_type = "(" type "," type { "," type } ")" ;
record_type = "{" [ record_type_fields ] [ "..." ] "}" ;
record_type_fields = record_type_field { "," record_type_field } ;
record_type_field = IDENT ":" type ;
refinement_type = "{" IDENT ":" type "|" expression "}" ;
Space-applied type constructors can appear directly in function types, so
Parser a -> String is parsed as (Parser a) -> String.
@borrow in function type parameter position marks that argument as non-consuming for
linear values, e.g. (Int @borrow) -> String.
Refinement Types
Refinement types constrain values beyond what the base type expresses.
The syntax follows Liquid Haskell: {binding: BaseType | predicate}.
Refinement Construction
refinement_construct = TYPE_NAME "!" "(" expression ")" (* assert *)
| TYPE_NAME "?" "(" expression ")" (* Option *)
| TYPE_NAME "?!" "(" expression ")" ; (* Result *)
T!(expr)- Assert construction; panics if predicate failsT?(expr)- Safe construction; returnsOption TT?!(expr)- Safe construction; returnsResult T String
Row Polymorphism
Kit supports opt-in row polymorphism for record types using ... syntax.
By default, record types are closed (require exact fields). Adding ...
makes them open (accept extra fields).
- Closed record (default):
{name: String}- requires exactly the specified fields - Open record (opt-in):
{name: String, ...}- requires specified fields, allows extras - Empty open record:
{...}- accepts any record
Indented Blocks
indented_block = NEWLINE INDENT { block_item } DEDENT ;
block_item = binding | expression ;
Indentation is significant. An indented block begins when the indentation level increases and ends when it returns to the previous level.
Lexical Elements
Identifiers
IDENT = LOWER_IDENT | UPPER_IDENT ;
LOWER_IDENT = ( LETTER_LOWER | "_" ) { LETTER | DIGIT | "_" | "-" } ;
UPPER_IDENT = LETTER_UPPER { LETTER | DIGIT | "_" | "-" } ;
LETTER = LETTER_LOWER | LETTER_UPPER ;
LETTER_LOWER = "a" ... "z" ;
LETTER_UPPER = "A" ... "Z" ;
DIGIT = "0" ... "9" ;
Numeric Literals
INT = DECIMAL_INT | HEX_INT | BINARY_INT | OCTAL_INT ;
DECIMAL_INT = [ "-" ] DIGIT { DIGIT | "_" } [ INT_SUFFIX ] ;
HEX_INT = [ "-" ] "0" ( "x" | "X" ) HEX_DIGIT { HEX_DIGIT | "_" } [ BASE_INT_SUFFIX ] ;
BINARY_INT = [ "-" ] "0" ( "b" | "B" ) BINARY_DIGIT { BINARY_DIGIT | "_" } [ BASE_INT_SUFFIX ] ;
OCTAL_INT = [ "-" ] "0" ( "o" | "O" ) OCTAL_DIGIT { OCTAL_DIGIT | "_" } [ BASE_INT_SUFFIX ] ;
FLOAT = [ "-" ] DIGIT { DIGIT } "." DIGIT { DIGIT } [ FLOAT_SUFFIX ] ;
HEX_DIGIT = DIGIT | "a" ... "f" | "A" ... "F" ;
BINARY_DIGIT = "0" | "1" ;
OCTAL_DIGIT = "0" ... "7" ;
INT_SUFFIX = "L" (* int64 *)
| "u" (* uint32 *)
| "uL" (* uint64 *)
| "I" ; (* bigint *)
BASE_INT_SUFFIX = "L" (* int64 *)
| "u" (* uint32 *)
| "uL" ; (* uint64 *)
FLOAT_SUFFIX = "F" (* float32 *)
| "M" ; (* decimal *)
Examples: 255, 0xFF, 0b1111_1111, 0o377, 1_000_000
String Literals
STRING = '"' { STRING_CHAR | ESCAPE_SEQ } '"' ;
STRING_CHAR = any character except '"', '\', or '${' ;
ESCAPE_SEQ = "\" ( "n" | "r" | "t" | "\" | '"' | "$" ) ;
Character Literals
CHAR = "'" ( CHAR_BODY | CHAR_ESCAPE ) "'" ;
CHAR_BODY = one UTF-8 scalar except "'", "\", or newline ;
CHAR_ESCAPE = "\" ( "n" | "r" | "t" | "0" | "\" | "'" | '"' )
| "\x" HEX_DIGIT HEX_DIGIT
| "\u" HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
| "\u{" HEX_DIGIT { HEX_DIGIT } "}" ;
Character literals produce Char values. Examples: 'a',
'\n', '\'', '\x41', and '\u{1F600}'.
Keyword Literals
KEYWORD_LITERAL = ":" IDENT ;
Comments
COMMENT = "#" { any character except newline } NEWLINE ;
DOC_COMMENT = "##" { any character except newline } NEWLINE ;
Doc comments (##) are associated with the following declaration
and can be used for documentation generation.
Operator Precedence Table
| Precedence | Operators | Associativity | Description |
|---|---|---|---|
| 1 (lowest) | using defer | prefix | Evidence/deferred execution |
| 2 | ?? | right | Null coalesce |
| 3 | |> |>> | left | Pipe operators |
| 4 | <- | - | Actor send |
| 5 | || or | left | Logical OR |
| 6 | && and | left | Logical AND |
| 7 | is as | - | Pattern test/extract |
| 8 | == != | left | Equality |
| 9 | < <= > >= | left | Comparison |
| 10 | ..< ..= | - | Range literals |
| 11 | ++ | left | String concatenation |
| 12 | :: @ | left | List cons/append |
| 13 | + - | left | Addition/subtraction |
| 14 | * / % | left | Multiplication/etc |
| 15 | - ! | right | Unary negation/not |
| 16 | calls, ., ?! | left | Application/access |
| 17 (high) | literals | - | Primary expressions |
Unicode Operator Aliases
Kit supports Unicode characters as aliases for common operators:
| Unicode | ASCII | Description |
|---|---|---|
× | * | Multiplication |
÷ | / | Division |
⇒ | => | Fat arrow |
→ | -> | Right arrow |
− | - | Minus sign |
≠ | != | Not equal |
≤ | <= | Less than or equal |
≥ | >= | Greater than or equal |
… | ... | Spread operator |
𝑓 | fn | Lambda keyword |
Reserved Words
as defer else export extend extern-c extern-zig
false fn for from guard if import
in is link macro match module requires
sql test then trait true type using when
where with
Pre-registered Constructors
The following constructors are pre-registered and can be used in patterns without a type definition:
# Result and Option types
Ok Err Some None
# Backoff strategies
NoBackoff Constant Linear Exponential
# Jitter strategies
NoJitter FullJitter EqualJitter ProportionalJitter
Destructuring
Kit supports destructuring in bindings, allowing you to extract values from tuples and records directly into variables.
Tuple Destructuring
# Basic tuple destructuring
point = (10, 20)
(x, y) = point
println "x = ${x}, y = ${y}"
# Multiple values
(a, b, c) = (1, 2, 3)
# Nested tuple destructuring
((x1, y1), (x2, y2)) = ((0, 0), (10, 10))
# Ignore elements with wildcard
(first, _, third) = (1, 2, 3)
Record Destructuring
# Shorthand syntax - field name becomes variable name
person = {name: "Alice", age: 30}
{name, age} = person
println "Name: ${name}, Age: ${age}"
# Explicit rename - bind to different variable names
{name: user-name, age: user-age} = person
println "User: ${user-name}"
# Partial destructuring - only extract needed fields
config = {host: "localhost", port: 8080, debug: true}
{port} = config
# Mixed shorthand and rename
{name, age: years-old} = person
# Nested record destructuring
user = {info: {name: "Bob", email: "bob@example.com"}}
{info: {name, email}} = user
# Ignore fields with wildcard
{keep, ignore: _} = {keep: 42, ignore: 999}
Destructuring in Functions
# Tuple destructuring in function parameters
add-points = fn((x1, y1), (x2, y2)) =>
(x1 + x2, y1 + y2)
# Record destructuring from function return
make-point = fn(x, y) => {x: x, y: y}
{x, y} = make-point 5 10
# In for loops
pairs = [("name", "Alice"), ("role", "admin")]
for (key, value) in pairs =>
println "${key}: ${value}"
Examples
Type Definition
type Option a = Some a | None
type Result a e =
| Ok a
| Err e
type Point = Point(x, y)
Type Definition with Attributes
# Single-line with attributes
type User = User(id @primary-key @auto-increment, name, email @unique)
# Multi-line with attributes
type Product = Product(
id @primary-key,
price @default(0.0),
category_id @fkey(Category, id),
tags @json("tags", omitempty)
)
Lambda Functions
add-two = fn(a, b) => a + b
# Zero-arity function (parentheses optional)
get-time = fn => now()
process = fn(items) =>
result = items |>> map square
result
# Lazy-accepting parameter (~ sigil)
maybe-compute = fn(~value, should-force?) =>
if should-force? then force(value) else 0
# Linearity annotation on a parameter
consume-once = fn(resource @linear) => close resource
# Trailing lambda as the final call argument
r = twice fn(x) => x + 1
# Zero-arg trailing lambda with indented body
listen app 4000 fn =>
println "started"
# Multi-line parenthesized lambda argument
s = twice (fn(x) =>
doubled = x * 2
doubled)
For Expressions
# Simple for loop
for x in [1, 2, 3] => print(x)
# With tuple destructuring
for (k, v) in map-entries(m) => print("${k}: ${v}")
# Multi-line body
for item in items =>
processed = transform(item)
save(processed)
SQL Expressions
# Simple SQL
sql { SELECT * FROM users }
# With connection
sql db { SELECT * FROM users WHERE id = ${user_id} }
# With type casting
sql db as User { SELECT * FROM users WHERE id = 1 }
Heredocs
html = <<~HTML
<div>
<h1>${title}</h1>
<p>${content}</p>
</div>
HTML
Contracts
@pre(n >= 0, "n must be non-negative")
@post(result >= 0)
factorial = fn(n) =>
if n <= 1 then 1 else n * factorial(n - 1)
Test Blocks
test "addition works" =>
assert-eq!(1 + 1, 2)
@skip "not implemented yet"
test "future feature" =>
todo()
Pattern Matching
match value
| Some x -> x
| None -> default
match point
| Point(0, 0) -> "origin"
| Point(x, 0) -> "on x-axis"
| Point(0, y) -> "on y-axis"
| Point(x, y) -> "at (${x}, ${y})"
List Patterns
# Head and tail
match list
| [head | tail] -> process(head, tail)
| [] -> empty()
# Rest pattern
match args
| [first, second, ..rest] -> handle(first, second, rest)
| [..all] -> handle-all(all)
Numeric Literals
decimal = 1_000_000
hex = 0xFF
binary = 0b1010_1010
octal = 0o755
int64 = 100L
bigint = 999999999999999999I
float32 = 3.14F
Pipe Operators
data |> transform |> output # thread-first
list |>> map double |>> filter # thread-last
Thread-first pipes insert the left value as the first argument. Thread-last pipes insert it as the last argument.
Refinement Types
# Type declarations
type PositiveInt = {n: Int | n > 0}
type NonZero = {n: Int | n != 0}
type Percentage = {p: Float | p >= 0.0 && p <= 100.0}
type NonEmptyList a = {xs: List a | not(empty?(xs))}
# Assert construction (panics on failure)
port = ValidPort!(8080)
# Safe construction (returns Option)
port = ValidPort?(user_input)
match port
| Some p -> use-port(p)
| None -> print("Invalid port")
# Safe construction (returns Result)
port = ValidPort ?! (user_input)
match port
| Ok p -> use-port(p)
| Err msg -> print("Error: ${msg}")
# Using in function signatures
divide = fn(a: Int, b: NonZero) => a / b
Linear Types
# Linear: must use exactly once
use-linear = fn(value @linear) => value + 1
# Affine: can use at most once
maybe-use = fn(value @affine, should-use?) =>
if should-use? then value + 1 else 0
# Relevant: must use at least once, and may be used more than once
use-relevant = fn(value @relevant) =>
first = value + 1
second = value + 2
first + second
# Unrestricted: no usage constraint
use-unrestricted = fn(value @unrestricted) => value + value
# Borrow: parameter-only, observes without consuming caller-owned linear values
observe = fn(value @borrow) => to-string value
Traits
trait Eq a
eq: (a, a) -> Bool
ne = fn(a, b) => !(eq a b)
extend Int with Eq
eq = fn(a, b) => a == b
Match Macros
# is: Check if value matches pattern (returns Bool)
opt = Some 42
if opt is Some _ then "has value" else "empty"
# as: Extract value with default
name = user as Some u -> u.name else "anonymous"
# guard: Bind or return early
process = fn(opt) =>
guard Some value = opt else Err "was None"
guard true = value > 0 else Err "not positive"
Ok (value * 2)
Compile-Time Macros
# Define macros with quasiquote syntax
macro double x = `($x + $x)
macro square x = `($x * $x)
macro unless cond then-val else-val =
`(if not($cond) then $then-val else $else-val)
# Use macros (expanded at compile time)
result = double(5) # expands to (5 + 5)
squared = square(4) # expands to (4 * 4)
msg = unless((x > 0), "negative", "positive")
Row Polymorphism
# Closed record (default) - requires exact fields
greet-closed = fn(person: {name: String}) =>
"Hello, " ++ person.name
greet-closed({name: "Kit"}) # OK
# greet-closed({name: "Kit", age: 1}) # Type error: extra field
# Open record (opt-in with ...) - accepts extra fields
greet-open = fn(person: {name: String, ...}) =>
"Hello, " ++ person.name
greet-open({name: "Kit"}) # OK
greet-open({name: "Kit", age: 1}) # OK - extra field allowed
# Empty open record - accepts any record
log-record = fn(r: {...}) => println(r)
Field Accessor Shorthand
# .field is shorthand for fn(x) => x.field
users = [{name: "Alice", age: 30}, {name: "Bob", age: 25}]
# These are equivalent:
names1 = users |>> map (fn(u) => u.name)
names2 = users |>> map (.name)
# Useful in pipelines
users |>> filter (fn(u) => u.age > 21) |>> map (.name)
Notes
- Indentation: Kit uses significant whitespace for multi-line constructs. Indented blocks are used in lambda bodies, if/else branches, match arms, and test blocks.
- Kebab-case: Identifiers can contain hyphens (
my-function), which is idiomatic for function names. - Constructor Recognition: Uppercase identifiers are treated as constructors in patterns.
- Negative Numbers in Application: When using Haskell-style function
application, negative numbers must be parenthesized:
func (-1)notfunc -1. - Attributes: Attributes provide metadata using the
@prefix. Compiler-known attributes include@pre,@post,@defer-required, and@skip. Unknown module, type, and constructor-field attributes are preserved for package tooling, while unknown binding and test attributes are rejected. - Refinement Types: Refinement types use the syntax
{binding: Type | predicate}following Liquid Haskell. Construction usesT!(expr)for assert,T?(expr)for Option, orT?!(expr)for Result. - For Expressions and Comprehensions: For loops (
for x in list => body) desugar toeach. List comprehensions ([body for pattern in list]) produce a list and desugar toList.map; addingif predicatefilters withList.filterfirst. Both forms support pattern destructuring in the binding. - SQL Expressions: SQL blocks support string interpolation with
${expr}. The optional connection expression allows specifying a database connection. - Heredocs: Squiggly heredocs (
<<~DELIM) strip common leading indentation from the content, making embedded code and markup easier to read. - Contracts:
@preand@postattributes on bindings define preconditions and postconditions that are checked at runtime. - Zero-arity Functions: Lambda expressions without parameters can omit
the parentheses:
fn => expris equivalent tofn() => expr. - Trailing Lambdas: A bare
fnlambda starting on the same line as a call becomes the final application argument:twice fn(x) => x + 1. The lambda body is greedy (it may continue as an indented block on the following lines). Parenthesized lambda arguments may also span multiple lines, closing on the last body line or on their own line. - Lazy-accepting Parameters: The
~sigil before a parameter indicates it accepts lazy values without auto-forcing. Regular parameters auto-force anyLazyorMemovalues passed to them. Example:fn(~value) => .... - Match Macros: The
is,as, andguardmacros provide concise pattern matching.isreturns a boolean,asextracts with a default, andguardbinds or returns early. These expand to equivalent match expressions. - Compile-Time Macros: User-defined macros use quasiquote syntax:
`(expr)creates a code template, and$paramsubstitutes parameters. Macros are expanded at compile time before type checking. - Row Polymorphism: Record types are closed by default (require exact fields).
Add
...to make them open:{name: String, ...}accepts records with at least anamefield. Use{...}to accept any record. - Linearity Annotations: Bindings, patterns, and function parameters can use
@linear,@affine,@relevant,@unrestricted, or borrowed callback annotations such asInt @borrow. - Field Accessor Shorthand: The syntax
.fieldis shorthand forfn(x) => x.field, useful in pipelines:users |>> map (.name). - Destructuring in Bindings: Tuple patterns
(a, b) = tupleand record patterns{x, y} = recordcan be used in bindings to extract values. Record shorthand{x}means{x: x}(bind fieldxto variablex). Use{field: name}to rename, and{field: _}to ignore fields. - Error Propagation: The postfix
?!operator unwrapsOk/Somevalues or returns early from the enclosing function withErr/None. It is parsed at call-expression level (same precedence as.and function application). - Actor Send: The
<-operator sends a message to an actor:actor <- messagedesugars toActor.send actor message. Its precedence is between pipe operators and logical OR. - Keyword Aliases:
orcan be used in place of||, andandcan be used in place of&&.