npx @teaklang/teak your-script.tk — scripts use the .tk
extension.
Language Syntax
val / var
val creates an immutable binding. var creates a mutable one that can be
reassigned. Compound assignment operators work on var.
val name = "teak" // immutable binding
var count = 0 // mutable
count += 1 // compound: += -= *= /=
count *= 2
val pi = 3.1415
val ok = true
val nothing = null
Functions & Receiver Functions
Regular functions use fun name(params) { ... }. A single-expression body can use
= expr. Receiver functions attach a method to a data class type using
fun (varName TypeName) methodName(params).
Functions support default parameters. Parameters with default values must come at the end of the list. Defaults can refer to earlier parameters.
fun add(a, b) = a + b
fun greet(name = "Guest", prefix = "Hello") = "$prefix, $name!"
println(greet()) // Hello, Guest!
println(greet("Ada")) // Hello, Ada!
// Default values can refer to previous parameters
fun volume(w, h = w, d = h) = w * h * d
println(volume(3)) // 27 (3*3*3)
// receiver function — attaches .area() to Rect instances
data class Rect(w, h)
fun (r Rect) area() = r.w * r.h
fun (r Rect) scale(s) = Rect(r.w * s, r.h * s)
val box = Rect(3, 4)
println(box.area()) // 12
println(box.scale(2)) // Rect(w=6, h=8)
Data Classes
Declare with data class Name(field1, field2, ...). Instances print with a readable
Name(field=value) format and expose their fields as properties. Fields can have
default values.
data class Point(x, y)
data class User(name, age = 18, active = true)
val p = Point(3, 4)
println(p) // Point(x=3, y=4)
val u = User("Ada")
println(u) // User(name=Ada, age=18, active=true)
// receiver functions extend data classes
fun (p Point) distanceTo(other) =
sqrt((p.x - other.x) * (p.x - other.x) +
(p.y - other.y) * (p.y - other.y))
println(Point(0,0).distanceTo(Point(3,4))) // 5
Lambdas & Anonymous Functions
Three ways to write a callable value:
- Block lambda —
{ params -> body } - Anonymous function —
fun(params) { body }orfun(params) = expr - Trailing lambda — last argument can be moved outside the parens; parens optional when it's the only argument
Single-parameter lambdas with no explicit param name get the implicit it variable.
// block lambda
val double = { x -> x * 2 }
val add = { a, b -> a + b }
println(double(5)) // 10
println(add(3, 4)) // 7
// anonymous function expression
val triple = fun(x) = x * 3
val clamp = fun(v, lo, hi) {
if (v < lo) return lo
if (v > hi) return hi
return v
}
println(triple(4)) // 12
println(clamp(15, 0, 10)) // 10
// implicit 'it' — single-param lambda, no arrow needed
listOf(1, 2, 3).forEach { println(it) }
// trailing lambda syntax
listOf(1, 2, 3).map { it * it }.forEach { println(it) }
// trailing lambda — parens omitted when only argument
mutableListOf(3, 1, 2).sort { a, b -> a - b }
// multi-line trailing lambda
val result = listOf(1, 2, 3, 4, 5).fold(0) { acc, v ->
acc + v
}
println(result) // 15
Control Flow
// if / else (also an expression)
val max = if (a > b) a else b
// when with a value — multi-match with commas
val label = when (code) {
200 -> "OK"
404, 403 -> "Not allowed"
else -> "Unknown"
}
// when without a value — acts as if-else chain
val sign = when {
x < 0 -> "negative"
x == 0 -> "zero"
else -> "positive"
}
// for..in with inclusive range
for (i in 1..5) println(i)
// for..in over any collection or sequence
for (item in listOf("a", "b", "c")) println(item)
// while, break, continue, return
var i = 0
while (i < 10) {
i += 1
if (i == 3) continue
if (i == 8) break
}
Operators
| Operator | Meaning | Example |
|---|---|---|
+ - * / % |
Arithmetic | 10 * 2 + 5 |
+= -= *= /= |
Compound assignment | x += 1 |
== != < > <= >= |
Comparison | a != b |
&& || ! |
Logical | a && !b |
.. |
Inclusive range | 1..100 |
to |
Pair / map entry | "key" to 42 |
Strings & Interpolation
Double-quoted strings support two interpolation forms. Triple-quoted strings ("""...""")
are raw and multiline — no escape sequences, no interpolation.
val lang = "teak"
println("Hello from $lang!") // $name — simple variable
println("2 + 2 = ${2 + 2}") // ${expr} — any expression
println("upper: ${lang.toUpperCase()}")
// Note: $name stops at the first non-identifier character
// Use ${obj.field} for member access inside strings
val p = Point(3, 4)
println("x is ${p.x}, y is ${p.y}") // correct
// println("x is $p.x") // would print the whole object + ".x"
// triple-quoted strings: raw, multiline, no escaping needed
val html = """
Hello
"""
// JS string methods are available
val s = " hello, world "
println(s.trim()) // "hello, world"
println(s.trim().split(",").join("-")) // "hello- world"
println("hello".startsWith("he")) // true
println("test".includes("es")) // true
println("abc".toUpperCase()) // ABC
Collections
List
listOf(...) returns an ImmutableList — mutation methods are not
available.
mutableListOf(...) returns a full ArrayList with all mutation methods.
toList() returns an ImmutableList. toMutableList() returns an ArrayList.
val nums = listOf(1, 2, 3) // ImmutableList
val items = mutableListOf(1, 2, 3) // ArrayList — can mutate
items.add(4)
items.set(0, 99)
items.remove(2)
items.removeAt(0)
items.clear()
| Category | Methods |
|---|---|
| Access | get(i) · size() · isEmpty() · contains(x) ·
indexOf(x) · first() · last() · head()
|
| Mutation (mutableListOf only) | add(x) · addAll(xs) · set(i,v) · remove(x) ·
removeAt(i) · clear() · sort(fn?)
|
| Slicing | take(n) · drop(n) · rest() · reversed() ·
chunked(n) · windowed(n,step?) · zip(other)
|
| Sorting | sorted() · sortedDescending() · sortedBy(fn) ·
sortedByDescending(fn) · sortedWith(cmp)
|
| Filtering | filter(fn) · filterNot(fn) · filterNotNull() ·
takeWhile(fn) · dropWhile(fn) · distinct() ·
distinctBy(fn)
|
| Predicates | any(fn) · all(fn) · none(fn) · find(fn) |
| Mapping | map(fn) · mapIndexed(fn) · mapNotNull(fn) ·
flatMap(fn) · flatten()
|
| Reduction | fold(init, fn) · reduce(fn) · sum() ·
sumOf(fn) · average() · count(fn?) ·
maxOrNull() · minOrNull() · maxByOrNull(fn) ·
minByOrNull(fn)
|
| Grouping | groupBy(fn) · associate(fn) · associateBy(fn) ·
partition(fn) · joinToString(sep?,prefix?,postfix?)
|
| Iteration | forEach(fn) · forEachIndexed(fn) · onEach(fn) |
| Conversion | toList() · toMutableList() · toSet() ·
toMutableSet() · toSequence() · asSequence()
|
val nums = mutableListOf(5, 1, 3, 2, 4)
println(nums.sorted()) // [1, 2, 3, 4, 5]
println(nums.filter { it > 2 }.sum()) // 12
println(nums.fold(0) { acc, v -> acc + v }) // 15
// Grouping and counting
val words = listOf("apple", "banana", "apple", "cherry")
val counts = words.groupBy { it }.mapValues { it.size() }
println(counts) // {apple: 2, banana: 1, cherry: 1}
Set
setOf(...) returns an ImmutableSet — no
add/remove/clear.
mutableSetOf(...) returns a TkSet with full mutation.
val s = setOf(1, 2, 3) // ImmutableSet
val ms = mutableSetOf(1, 2, 3) // TkSet
ms.add(4)
ms.remove(1)
println(ms.contains(2)) // true
println(ms.size()) // 3
// set operations (return a new TkSet)
val a = mutableSetOf(1, 2, 3)
val b = mutableSetOf(2, 3, 4)
println(a.union(b)) // [1, 2, 3, 4]
println(a.intersect(b)) // [2, 3]
println(a.subtract(b)) // [1]
// conversion
val list = s.toList() // ImmutableList
val mlist = s.toMutableList() // ArrayList
Map
mapOf("k" to v, ...) returns an ImmutableMap —
put/set/remove are not available.
mutableMapOf(...) returns a full TkMap.
Entries are created with the to operator: "key" to value.
val m = mapOf("a" to 1, "b" to 2) // ImmutableMap
val mm = mutableMapOf("a" to 1) // TkMap — can mutate
mm.put("b", 2)
mm.remove("a")
// read (works on both)
println(m.get("a")) // 1
println(m.containsKey("b")) // true
println(m.keys()) // [a, b]
println(m.values()) // [1, 2]
println(m.size()) // 2
// escape to mutable
val editable = m.toMutableMap()
editable.put("c", 3)
// mapping maps
val doubled = m.mapValues { it * 2 } // {a: 2, b: 4}
val upper = m.mapKeys { it.toUpperCase() } // {A: 1, B: 2}
// holistic operations (it is {key, value})
val entries = m.map { "${it.key}=${it.value}" } // [a=1, b=2]
val filtered = m.filter { it.value > 1 } // {b: 2}
val hasLarge = m.any { it.value > 10 } // false
TreeMap
A sorted map backed by a red-black tree. Pass numCmp or strCmp (or any
comparator function) to set the ordering.
val tm = treeMap(numCmp)
tm.put(3, "three")
tm.put(1, "one")
tm.put(2, "two")
// or use treeMapOf with pairs
val tm2 = treeMapOf(strCmp, "z" to 26, "a" to 1)
println(tm.keys()) // [1, 2, 3]
println(tm.get(2)) // two
println(tm.firstKey()) // 1
println(tm.lastKey()) // 3
println(tm.containsKey(2)) // true
// range query — returns list of [key, value] pairs
val range = tm.subMap(1, 3) // keys >= 1 and < 3
for (p in range) {
println(p[0], "=>", p[1])
}
tm.remove(2)
tm.clear()
println(tm.size()) // 0
Sequence — lazy pipelines
Sequences are lazy — intermediate operations build a pipeline; nothing runs until a terminal operation
is called. Use asSequence() or toSequence() to convert a list, or use
File.walkTopDown() which returns a Sequence directly.
| Kind | Methods |
|---|---|
| Lazy | map · filter · filterNot · filterNotNull ·
mapNotNull · flatMap · flatten · take ·
takeWhile · drop · dropWhile · distinct ·
distinctBy · onEach · chunked · windowed ·
zip
|
| Terminal | forEach · forEachIndexed · toList ·
toMutableList · toSet · reduce · fold ·
sum · sumOf · average · count ·
any · all · none · find · first ·
last · maxOrNull · minOrNull · groupBy ·
associate · partition · joinToString · sorted
· sortedBy
|
val sum = listOf(1, 2, 3, 4, 5)
.asSequence()
.filter { it % 2 == 1 }
.map { it * it }
.take(2)
.sum() // 1 + 9 = 10
// walk files lazily
val tkFiles = File(".")
.walkTopDown()
.filter { it.isFile() }
.map { it.path }
.take(10)
.toList()
Standard Library
Output & Core
println("hello") // print any value
println(a, "=>", b) // multiple args, space-separated
listOf(1, 2, 3) // ImmutableList
mutableListOf(1, 2, 3) // ArrayList
setOf(1, 2, 3) // ImmutableSet
mutableSetOf(1, 2, 3) // TkSet
mapOf("a" to 1) // ImmutableMap
mutableMapOf("a" to 1) // TkMap
numCmp(a, b) // -1 / 0 / 1 (numeric comparator)
strCmp(a, b) // string comparator
// dynamic method definition (ArrayList, Sequence, Data Classes)
ArrayList.defineMethod("sumPlusOne") { self -> self.sum() + 1 }
println(listOf(1, 2).sumPlusOne()) // 4
Math
abs(-5) // 5
floor(2.9) // 2
ceil(2.1) // 3
round(2.5) // 3
sqrt(16.0) // 4
pow(2, 10) // 1024
log(2.718) // ~1
max(3, 7) // 7
min(3, 7) // 3
random() // 0.0 .. 1.0
toInt(3.9) // 3 (truncates toward zero)
PI // 3.141592...
Encoding, Crypto, Modules & JSON
sha256("hello") // hex digest string
base64Encode("hi") // "aGk="
base64Decode("aGk=") // "hi"
jsonParse("{\"x\":1}") // plain object { x: 1 }
jsonStringify(obj) // compact JSON string
// modules
include("other.tk") // execute another file in global scope
val mod = requireFile("m.tk") // execute and return its 'exports' map
// allowed: "lodash", "axios", "dayjs", "uuid"
val _ = importNpm("lodash")
Time
val t = now() // epoch milliseconds
sleep(1000) // suspend for 1 s (non-blocking)
formatTime(now()) // ISO-8601 string
File System
val f = File("data.txt")
f.exists() // true / false
f.isFile()
f.isDirectory()
f.size() // bytes
f.lastModified() // epoch ms
f.readText() // full string
f.writeText("hello")
f.appendText("\nworld")
f.readJson() // parse JSON file
f.writeJson(obj)
f.delete()
f.mkdir()
f.copyTo("backup.txt")
f.moveTo("archive/data.txt")
f.list() // ArrayList of File objects
f.listFiles() // same as list()
f.walkTopDown() // lazy Sequence of File objects
f.tailFile() // infinite Sequence following file additions
// readLines() — streaming line pipeline
File("log.txt").readLines()
.filter { it.includes("ERROR") }
.map { it.trim() }
.take(20)
.forEach { println(it) }
// tailFile() — infinite stream that follows file additions
File("live.log").tailFile()
.filter { it.includes("CRITICAL") }
.forEach { println("ALERT: $it") }
// toList() collects all results
val lines = File("data.csv").readLines()
.drop(1) // skip header
.filter { it.size() > 0 }
.toList() // ImmutableList
Process & Environment
// exec returns a handle — call .wait() to get results
val proc = exec("ls -la")
val res = proc.wait()
println(res.stdout) // output string
println(res.code) // exit code (0 = ok)
println(proc.pid) // process ID
proc.kill() // SIGINT
proc.killForcefully() // SIGKILL
spawn("echo", ["hello"]) // fire-and-forget subprocess
env("HOME") // read env var
setEnv("DEBUG", "1") // set env var
HTTP Client
val body = httpGet("https://api.example.com/data")
println(body) // raw response text
val resp = httpPost(
"https://api.example.com/users",
mapOf("name" to "Ada", "role" to "admin")
)
println(resp)
download("https://example.com/image.jpg", "image.jpg")
HTTP Server
HttpServer(port) returns a server object. Route handlers receive a context
— use it (implicit) or a named parameter via trailing lambda or fun(ctx).
Path segments starting with : are captured as params. ctx.params and
ctx.query are both TkMap — use .get(key).
| Method | Description |
|---|---|
server.use { ctx, next -> } |
Add middleware — call next() to continue |
server.get(path) { } |
GET route |
server.post(path) { } |
POST route |
server.put(path) { } |
PUT route |
server.patch(path) { } |
PATCH route |
server.delete(path) { } |
DELETE route |
server.start() |
Start listening |
server.stop() |
Graceful shutdown (returns Promise) |
| Context property / method | Description |
|---|---|
ctx.params |
TkMap of path params — ctx.params.get("id") |
ctx.query |
TkMap of query string params — ctx.query.get("q") |
ctx.body |
Parsed JSON body (or raw string if not JSON) |
ctx.path |
Request pathname string |
ctx.method |
HTTP method string ("GET", "POST", …) |
ctx.status(code) |
Set response status — chainable |
ctx.header(k, v) |
Set a response header — chainable |
ctx.json(obj) |
Send JSON response |
ctx.text(str) |
Send plain text response |
ctx.html(str) |
Send HTML response |
val server = HttpServer(8080)
// middleware — runs before every handler
server.use { ctx, next ->
println("${ctx.method} ${ctx.path}")
ctx.header("X-Powered-By", "teak")
next()
}
// trailing lambda (preferred style)
server.get("/") {
it.html("""<h1>Hello from teak!</h1>""")
}
// path params — :id captured into ctx.params
server.get("/users/:id") {
val id = it.params.get("id")
it.json(mutableMapOf("userId" to id))
}
// query params — ?q=hello into ctx.query
server.get("/search") {
val q = it.query.get("q")
it.json(mutableMapOf("query" to q))
}
// POST with JSON body
server.post("/users") {
val name = it.body.name // body is parsed JSON
it.status(201).json(mutableMapOf("created" to name))
}
// multi-segment path params
server.get("/items/:category/:id") { ctx ->
val cat = ctx.params.get("category")
val id = ctx.params.get("id")
ctx.json(mutableMapOf("category" to cat, "id" to id))
}
// anonymous fun style also works
server.delete("/users/:id", fun(ctx) {
val id = ctx.params.get("id")
ctx.status(204).text("")
})
server.start() // Server listening on :8080
// server.stop() // graceful shutdown
Concurrency
go { } runs a block on the event loop without blocking. chan() creates a
channel for communicating between goroutines. waitGroup() coordinates multiple concurrent
tasks.
// fire-and-forget
go {
sleep(1000)
println("one second later")
}
// channel communication
val ch = chan()
go { ch.send(1) }
go { ch.send(2) }
for (i in 1..2) {
println(ch.receive())
}
// waitGroup
val wg = waitGroup()
for (i in 1..3) {
wg.add(1)
go {
sleep(100)
println("worker $i done")
wg.done()
}
}
wg.wait()
println("all workers finished")
Examples
data class Vec2(x, y)
fun (v Vec2) length() = sqrt(v.x * v.x + v.y * v.y)
fun (v Vec2) add(other) = Vec2(v.x + other.x, v.y + other.y)
fun (v Vec2) scale(s) = Vec2(v.x * s, v.y * s)
fun (v Vec2) dot(other) = v.x * other.x + v.y * other.y
fun (v Vec2) normalize() = v.scale(1.0 / v.length())
val a = Vec2(3, 4)
val b = Vec2(1, 2)
println(a) // Vec2(x=3, y=4)
println(a.length()) // 5
println(a.add(b)) // Vec2(x=4, y=6)
println(a.scale(2)) // Vec2(x=6, y=8)
println(a.dot(b)) // 11
println(a.normalize()) // Vec2(x=0.6, y=0.8)
// pipeline with data classes
val points = mutableListOf(Vec2(3,4), Vec2(1,1), Vec2(0,5))
val sorted = points.sortedBy { it.length() }
sorted.forEach { println("${it.x},${it.y} len=${it.length()}") }
// recursive sum
fun sumList(lst) {
if (lst.isEmpty()) return 0
return lst.get(0) + sumList(lst.drop(1))
}
println(sumList(listOf(1, 2, 3, 4, 5))) // 15
// recursive tree
data class TreeNode(value, left, right)
fun countLeaves(node) {
if (node == null) return 0
if (node.left == null && node.right == null) return 1
return countLeaves(node.left) + countLeaves(node.right)
}
// 1
// / \
// 2 3
// / \
// 4 5
val root = TreeNode(1,
TreeNode(2, TreeNode(4, null, null), TreeNode(5, null, null)),
TreeNode(3, null, null)
)
println(countLeaves(root)) // 3
// stream a large file line-by-line with zero memory overhead
File("server.log").readLines()
.filter { it.includes("ERROR") }
.map { it.trim() }
.take(50)
.forEach { println(it) }
// collect to an immutable list
val errors = File("server.log").readLines()
.filter { it.includes("ERROR") }
.distinct()
.sorted()
.toList() // ImmutableList
println("${errors.size()} unique error lines")
// walk directory tree
val sources = File(".")
.walkTopDown()
.filter { it.isFile() }
.map { it.path }
.filter { it.includes(".tk") }
.toList()
sources.forEach { println(it) }
// tail -f style
File("access.log").tailFile()
.map { it.toUpperCase() }
.forEach { println(it) }
// producer / consumer via channel
val ch = chan()
val wg = waitGroup()
// producer
go {
for (i in 1..5) {
ch.send(i)
sleep(50)
}
ch.close()
}
// 2 consumers
for (w in 1..2) {
wg.add(1)
go {
while (true) {
val job = ch.receive()
if (job == null) break
println("worker $w got $job")
}
wg.done()
}
}
sleep(1000)
for (i in 1..5) {
println(ch.receive())
}
data class Todo(id, title, done)
val todos = mutableListOf(
Todo(1, "Buy milk", false),
Todo(2, "Write code", true),
Todo(3, "Read docs", false)
)
val server = HttpServer(8080)
// middleware: log + CORS
server.use { ctx, next ->
println("${ctx.method} ${ctx.path}")
ctx.header("Access-Control-Allow-Origin", "*")
next()
}
// HTML homepage
server.get("/") {
it.html("""
<!DOCTYPE html>
<html>
<head><title>Todos</title></head>
<body>
<h1>Todo API</h1>
<p>GET /todos | POST /todos | GET /todos/:id</p>
</body>
</html>
""")
}
// list all — optional ?done=true filter via query params
server.get("/todos") {
val filter = it.query.get("done")
val result = if (filter == "true")
todos.filter { t -> t.done }
else
todos
it.json(mutableMapOf("todos" to result))
}
// get by id — path param
server.get("/todos/:id") {
val id = toInt(it.params.get("id"))
val todo = todos.find { t -> t.id == id }
if (todo == null) it.status(404).json(mutableMapOf("error" to "not found"))
else it.json(todo)
}
// create — POST body
server.post("/todos") {
val next = Todo(todos.size() + 1, it.body.title, false)
todos.add(next)
it.status(201).json(next)
}
// delete by id — path param
server.delete("/todos/:id") {
val id = toInt(it.params.get("id"))
todos.remove(todos.find { t -> t.id == id })
it.status(204).text("")
}
server.start() // Server listening on :8080
Why teak?
Familiar — Kotlin-style val, fun, data class, lambdas, when
Safe — Immutable lists, sets, and maps by default
Complete — Collections, HTTP, files, concurrency, crypto
© 2026 teak Language. Docs built with Bulma. Back to top ↑