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_CASEToken/terminal
lower_caseNon-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: @deprecated and @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 true
  • assert-eq! expected actual - Assert two values are equal
  • assert-ne! a b - Assert two values are not equal
  • assert-true! value - Assert value is true
  • assert-false! value - Assert value is false

Option/Result Assertions:

  • assert-some! value - Assert value is Some variant
  • assert-none! value - Assert value is None variant
  • assert-ok! value - Assert value is Ok variant
  • assert-err! value - Assert value is Err variant

Comparison Assertions (integers only):

  • assert-lt! a b - Assert a < b
  • assert-gt! a b - Assert a > b
  • assert-lte! a b - Assert a <= b
  • assert-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, returns Bool
  • as - Extract value if pattern matches, return alternative otherwise
  • guard - 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-c declares a C function binding
  • extern-zig declares 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 fails
  • T?(expr) - Safe construction; returns Option T
  • T?!(expr) - Safe construction; returns Result 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 deferprefixEvidence/deferred execution
2??rightNull coalesce
3|> |>>leftPipe operators
4<--Actor send
5|| orleftLogical OR
6&& andleftLogical AND
7is as-Pattern test/extract
8== !=leftEquality
9< <= > >=leftComparison
10..< ..=-Range literals
11++leftString concatenation
12:: @leftList cons/append
13+ -leftAddition/subtraction
14* / %leftMultiplication/etc
15- !rightUnary negation/not
16calls, ., ?!leftApplication/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
𝑓fnLambda 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

  1. Indentation: Kit uses significant whitespace for multi-line constructs. Indented blocks are used in lambda bodies, if/else branches, match arms, and test blocks.
  2. Kebab-case: Identifiers can contain hyphens (my-function), which is idiomatic for function names.
  3. Constructor Recognition: Uppercase identifiers are treated as constructors in patterns.
  4. Negative Numbers in Application: When using Haskell-style function application, negative numbers must be parenthesized: func (-1) not func -1.
  5. 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.
  6. Refinement Types: Refinement types use the syntax {binding: Type | predicate} following Liquid Haskell. Construction uses T!(expr) for assert, T?(expr) for Option, or T?!(expr) for Result.
  7. For Expressions and Comprehensions: For loops (for x in list => body) desugar to each. List comprehensions ([body for pattern in list]) produce a list and desugar to List.map; adding if predicate filters with List.filter first. Both forms support pattern destructuring in the binding.
  8. SQL Expressions: SQL blocks support string interpolation with ${expr}. The optional connection expression allows specifying a database connection.
  9. Heredocs: Squiggly heredocs (<<~DELIM) strip common leading indentation from the content, making embedded code and markup easier to read.
  10. Contracts: @pre and @post attributes on bindings define preconditions and postconditions that are checked at runtime.
  11. Zero-arity Functions: Lambda expressions without parameters can omit the parentheses: fn => expr is equivalent to fn() => expr.
  12. Trailing Lambdas: A bare fn lambda 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.
  13. Lazy-accepting Parameters: The ~ sigil before a parameter indicates it accepts lazy values without auto-forcing. Regular parameters auto-force any Lazy or Memo values passed to them. Example: fn(~value) => ....
  14. Match Macros: The is, as, and guard macros provide concise pattern matching. is returns a boolean, as extracts with a default, and guard binds or returns early. These expand to equivalent match expressions.
  15. Compile-Time Macros: User-defined macros use quasiquote syntax: `(expr) creates a code template, and $param substitutes parameters. Macros are expanded at compile time before type checking.
  16. Row Polymorphism: Record types are closed by default (require exact fields). Add ... to make them open: {name: String, ...} accepts records with at least a name field. Use {...} to accept any record.
  17. Linearity Annotations: Bindings, patterns, and function parameters can use @linear, @affine, @relevant, @unrestricted, or borrowed callback annotations such as Int @borrow.
  18. Field Accessor Shorthand: The syntax .field is shorthand for fn(x) => x.field, useful in pipelines: users |>> map (.name).
  19. Destructuring in Bindings: Tuple patterns (a, b) = tuple and record patterns {x, y} = record can be used in bindings to extract values. Record shorthand {x} means {x: x} (bind field x to variable x). Use {field: name} to rename, and {field: _} to ignore fields.
  20. Error Propagation: The postfix ?! operator unwraps Ok/Some values or returns early from the enclosing function with Err/None. It is parsed at call-expression level (same precedence as . and function application).
  21. Actor Send: The <- operator sends a message to an actor: actor <- message desugars to Actor.send actor message. Its precedence is between pipe operators and logical OR.
  22. Keyword Aliases: or can be used in place of ||, and and can be used in place of &&.