Language tour
This is Kon as the compiler runs it today. Every complete program here was built and run by that compiler, and its output is what you see under it.
Hello
func main(): i32 {
print("xin chào")
return 0
}main returns the program's exit code. Lines end statements, so there are no semicolons. Run it with kon run hello.kon.
Values
let binds once and var can be reassigned. Types are usually worked out for you, and you write them where a plain number could be several things.
func main(): i32 {
let name = "Kon"
var count: i32 = 1
count += 41
let ratio: f64 = 1.5
let small = count as u8
print(name)
print(count)
print(ratio)
print(small)
return 0
}The number types are i8 to i64, u8 to u64, f32 and f64. Conversions are always written with as. Integer arithmetic wraps around when it overflows, the same way on every machine, and dividing by zero stops the program. A float converted to an integer saturates at the type's limit.
Functions
const LIMIT: i32 = 100
func clamp(value: i32): i32 {
if value > LIMIT { return LIMIT }
return value
}
func main(): i32 {
print(clamp(250))
return 0
}Control flow
func main(): i32 {
var total: i64 = 0
for i in 0..(5 as i64) {
if i == 3 { continue }
total += i
}
var left: i32 = 3
while left > 0 {
left -= 1
}
print(total)
print(left)
return 0
}Structs and methods
Methods live inside the struct. The first parameter says what the method does with it: view self reads and edit self changes.
struct Counter {
value: i32
func get(view self): i32 {
return self.value
}
func bump(edit self, by: i32) {
self.value += by
}
}
func main(): i32 {
var counter = Counter { value: 40 }
counter.bump(2)
print(counter.get())
return 0
}Enums and match
A variant can carry data, and match must cover every case.
enum Shape {
Circle(f64)
Square(f64)
Empty
}
func area(shape: view Shape): f64 {
match shape {
Shape.Circle(radius) => { return 3.14 * radius * radius }
Shape.Square(side) => { return side * side }
Shape.Empty => { return 0.0 }
}
return 0.0
}
func main(): i32 {
let square = Shape.Square(3.0)
print(area(square))
return 0
}Option and Result
There is no null. A value that may be missing is an Option, and try passes a missing value or an error up to the caller.
func half(value: i32): Option<i32> {
if value % 2 != 0 { return none }
return some(value / 2)
}
func quarter(value: i32): Option<i32> {
let once = try half(value)
return half(once)
}
func main(): i32 {
match quarter(20) {
some(result) => { print(result) }
none => { print("not whole") }
}
let missing = quarter(6)
print(missing.isSome())
return 0
}Lists and arrays
List grows on the heap and fixed arrays do not. Every index is checked: going past the end stops the program instead of reading other memory.
func main(): i32 {
var scores: List<i32> = list[]
scores.add(7)
scores.add(35)
var total: i32 = 0
var at: u64 = 0
while at < scores.length {
total += scores[at]
at += 1
}
print(total)
let fixed: [u8; 3] = [1, 2, 3]
print(fixed[2 as u64])
return 0
}Text
str is text you only look at, and String is text you own and can grow. Both are UTF-8. Asking for "the length" is an error on purpose, because bytes, characters and visible letters are three different numbers.
func main(): i32 {
var greeting = String("xin ")
greeting.append("chào")
print(greeting)
print(greeting.byteLength)
print(greeting.characterCount())
return 0
}For numbers, splitting, searching and files, see std/text.
Ownership
Every value has one owner, and the owner frees it, with no garbage collector. When you pass a value, the call shows what happens to it:
| You write | Meaning | Afterward |
|---|---|---|
show(player) | The function reads it; view needs no mark at the call | You still own it |
heal(edit player, 5) | The function changes it, and nothing else may touch it meanwhile | You still own it |
keep(take player) | You hand it over | You can no longer use it |
Using a value after handing it over does not compile, and the error names the line:
func main(): i32 {
var items: List<u8> = list[]
let other = take items
print(items.length)
return 0
}Neither does changing a value while something else is reading it:
func copyInto(target: edit List<u8>, source: view List<u8>) {
}
func main(): i32 {
var items: List<u8> = list[]
copyInto(edit items, items)
return 0
}Moving a named value must be visible, so forgetting take is also an error:
func keep(items: List<u8>) {
}
func main(): i32 {
var items: List<u8> = list[]
keep(items)
return 0
}Generics and interfaces
interface Named {
func label(view self): i32
}
struct Cat implements Named {
lives: i32
func label(view self): i32 {
return self.lives
}
}
func labelOf<T>(item: view T): i32 where T: Named {
return item.label()
}
func main(): i32 {
let cat = Cat { lives: 9 }
print(labelOf<Cat>(cat))
return 0
}Modules
A file is a module, and only pub names are visible outside it. An import looks beside the source file first, and std/ paths then fall back to the library that ships with the toolchain.
import "std/text.kon" as text
func main(): i32 {
let number = text.fromInt(42)
print(number)
return 0
}Cleanup with defer
defer runs when the block ends, in reverse order, however it ends.
func main(): i32 {
defer {
print("last")
}
defer {
print("second")
}
print("first")
return 0
}Calling C
Foreign calls are the one place the compiler cannot check, so they must sit inside unsafe, where a reader can find them.
extern "C" func abs(value: i32): i32
func main(): i32 {
var result: i32 = 0
unsafe {
result = abs(-42)
}
print(result)
return 0
}Not in the language yet
The specification goes further than the compiler does so far. These do not work today: closures, text interpolation such as "n = {n}", enums that hold a List of themselves, and automatic conversion between str and String. The status page has the full picture.