Docs / Syntax

Syntax

Kex borrows Ruby’s do … end blocks and identifier conventions, layers on type annotations with :, and uses -> for return types and = for expression-bodied functions.

Comments

# this is a line comment

Bindings

let binds an immutable name. var opts into local mutation. The ! suffix rebinds a var to a method’s updated return value.

bindings.kex
let name = "kex"          # immutable
var count = 0             # mutable
count = count + 1         # reassign

var list = [1, 2, 3]
list.push!(4)             # list := list.push(4)
examples/bindings.kex

A binding may carry a type annotation. Inference covers most code, so an annotation is for when you want to pin the type down rather than discover it.

let count : Integer = 3
var total : Float = 0.0
foul config : [String] = FS.File.lines("app.conf").or([])

Functions

A function is either a single expression after =, or a do … end block ending in return.

functions.kex
let double(n: Integer) = n * 2

let greet(name: String) -> String do
  return "Hello, ${name}!"
end

let add(a, b) = a + b   # types can be inferred
examples/functions.kex

Blocks and lambdas

blocks.kex
[1, 2, 3].map { |x| x * 2 }

[1, 2, 3].each do |x|
  IO.printLine(x.to(String))
end
examples/blocks.kex

Records and construction

point.kex
record Point do
  x : Float
  y : Float
end

let p = Point { x: 1.0, y: 2.0 }
examples/point.kex

A bare field name is shorthand for name: name, mirroring how record patterns destructure with { x, y }. The two forms mix freely.

let x = 1.0
let y = 2.0

let p = Point { x, y }         # Point { x: x, y: y }
let q = Point { x, y: 5.0 }

Spread

... splices a collection into the one being built. It is a spread, not an operator — it is only valid inside a list or map literal, or as a statement in a do block body. In a map, later entries win, so a spread overrides what precedes it and is overridden by what follows.

let xs = [1, 2]
[0, ...xs, 5]                  # [0, 1, 2, 5]

let base = { "host": "localhost", "port": 80 }
{ ...base, "port": 8080 }      # port becomes 8080

Control flow

Branching is if / elif / else, optionally with a trailing if guard. Rich dispatch lives in match.

control.kex
return Error(EmptyInput) if s.empty?

if n > 0
  "positive"
elif n < 0
  "negative"
else
  "zero"
end
examples/control.kex

String interpolation

interp.kex
let name = "world"
"Hello, ${name}!"
"${(1..3).sum}"   # "6"
examples/interp.kex