Skip to content

Instantly share code, notes, and snippets.

@elliotchance
Last active June 19, 2025 15:52
Show Gist options
  • Save elliotchance/62a1e4f1d3ec80379d16a3c0a9fbb9d2 to your computer and use it in GitHub Desktop.
Save elliotchance/62a1e4f1d3ec80379d16a3c0a9fbb9d2 to your computer and use it in GitHub Desktop.
Koi

Koi

Koi is a language that seeks to make many traditional bugs impossible by preventing them at the language level. Each of these are discussed in more detail below.

  1. Prevent all runtime errors. Runtime errors are, by definition, unexpected and either have to be caught and emit and error under a sitution that can't be handled safely or cause the program to blow up.
  2. No garbage collector, but no manual memory management either. It's unsafe to manage memory manully, but we also don't want the overhead of a garbage collector.
  3. Interoperability with C. This is critical to making sure we can use existing libraries and also makes the lanaguage a lot easier if we can offload the lowest level logic to C.
  4. All objects are also interfaces. Any object can be provided for a type if it fits the receiving interface.
  5. First class testing. Language constructs for dealing with tests, assertions, mocks, etc.
  6. Sum types handle behaviors like errors.

Language Spec

Types

  • Number
  • String

And some more specific domained types:

type PositiveNumber = Number where value > 0

type Int = Number where math.floor(value) == value
type PositiveInt = Number where math.floor(value) == value and value > 0

Avoding Runtime Errors

There is a differnce between detecting and avoiding. We have to be careful avoiding doesn't become a burdon.

  1. Overflow and underflow.
  2. All matching must be exhaustive?
  3. Array and map out of bounds.
  4. Casting to an invalid type.
  5. Signals and other interupts.

Nil Dereferencing

This is not possible becuase there is no concept of nil/null values because all values are instantiated. There are times when a missing value is needed, you must use a sumtype for this:

type LinkedListNode<T> {
  value T
  next LinkedListNode<T> | None
}

Dividing By Zero

The denominator must be a NonZeroFloat

type NonZeroFloat Float where x != 0

func main() {
  var a = 7 / 5   // OK
  
  var b = 3
  var c = 7 / b   // ERROR: divide expects NonZeroFloat for denominator, got Float
  
  var d = 7 / NonZeroFloat(b)   // ERROR: does not catch DomainErr
  
  var e = 7 / NonZeroFloat(b) -> DomainErr { 1 }   // OK
  
  var f = 0
  if b != 0 {
    f = 7 / b   // OK
  }
}

Casting To Int

Force a rounding mode rather than just truncating?

func main() {
  var a = 1.23
  var b = a as Int   // ERROR: cannot cast Float to Int
  
  var c = math.floor(a)   // OK
}

NaN and Infinity

Rather than make NaN and +/- inf special values, these are actually types that must be handled separately if the function possibly returns them.

// math package

func log(x Float) (Float | Infinity | NotANumber) {
  if x == 0 {
    return Infinity{negative: true}
  }
  
  if x < 0 {
    return NotANumber{}
  }
  
  return C.log(x)
}

func main() {
  io.printLine(log(8))    // 0.903089987
  io.printLine(log(0))    // -Inf
  io.printLine(log(-1))   // NaN
  
  var result = log(8) * 2 // ERROR: log() may return multiple types
  
  // Or, translate other types into a Float
  var result = log(8) as {
    NotANumber, Infinity: 0
  } * 2
}

However, this would be quite painful to do everytime. Especially when we know the inputs are valid, so instead we can use a checked type:

type PositiveFloat Float where value > 0

func log(x PositiveFloat) Float {
  return C.log(x)
}

func main() {
  var result = log(8) * 2   // OK
  
  result = log(-1) * 2      // ERROR: -1 is not a valid PositiveFloat
  
  sneaky = -2
  result = log(sneaky) * 2  // ERROR: expected PositiveInteger, got Int
  
  safeSneaky = PositiveNumber(sneaky + 4) // ERROR: PositiveNumber() may fail check
  
  safeSneaky = PositiveNumber(sneaky + 4) on Domainrr { 1 }   // OK
  log(safeSneaky)
}

Errors

  • DomainErr when trying to cast value into a compatible type that's not allowed.

Avoiding Logic Errors

  1. Explicit order of operations.
  2. Zero out memory.
  3. Explicit mutability.
  4. No jumps/gotos (including breaking).
  5. No operator overloading.
  6. No type overloading.

Processes

Memory cannot be shared between processes. Launching a process returns a different type (ie. Process[MyObject]) that itself provides the API for syncronizing calls. Any value that attempts to cross a process boundary must implement a Copy interface.

Memory Management

Reference counting.

Types and Domains

All Objects Are Interfaces

Testing

  • Tests
  • Assertions
  • Mocks

Language Constructs

Data Types

Variables

Functions

Sum Types

type NumberOrString = number | string

type MultiValues = (number, bool) | None

type NamedTypes = @high (number, number) | @low (number, number) | number
func static[doStuff] :good number | :bad number | error {

}

func {
  const result = match static[doStuff] {
    :good n number {
      n + 5
    }
    :bad number {
      -1
    }
    error {
      0
    }
  }
}

Objects

Control Flow

Errors

Erorrs are just a return type. Auto snapshotting?

Package Management

By Example

Hello World

import io

func main() {
    io.printLine("hello world")
}

Values

// FIXME

import io

func main() {
    io.printLine("go" + "lang")

    fmt.Println("1+1 =", 1+1)
    fmt.Println("7.0/3.0 =", 7.0/3.0)

    fmt.Println(true && false)
    fmt.Println(true || false)
    fmt.Println(!true)
}

Variables

import io

func main() {
    var a = "initial"
    io.printLine(a)

    var b, c int = 1, 2
    fmt.Println(b, c)

    var d = true
    fmt.Println(d)

    var e int
    fmt.Println(e)

    f := "apple"
    fmt.Println(f)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment