/OKFunkin-

An Oak + Kaboom mix of a bunch of cool stuff in the shape of a Dedicated Funkin clone. https://replit.com/@Spcfork/OKFunk

Primary LanguageGLSL

Okin

Okin

OKF Server

A simple frame for quick building and testing Oak + Kaboom software.

Press RUN to build and start.

Oak 🌳

Oak is an expressive, dynamically typed programming language.

Here's an example Oak program.

std := import('std')

fn fizzbuzz(n) if [n % 3, n % 5] {
    [0, 0] -> 'FizzBuzz'
    [0, _] -> 'Fizz'
    [_, 0] -> 'Buzz'
    _ -> string(n)
}

std.range(1, 101) |> std.each(fn(n) {
    std.println(fizzbuzz(n))
})

Oak has good support for asynchronous I/O. Here's how you read a file and print it.

std := import('std')
fs := import('fs')

with fs.readFile('./file.txt') fn(file) if file {
    ? -> std.println('Could not read file!')
    _ -> print(file)
}

Oak also has a pragmatic standard library that comes built into the oak executable. For example, there's a built-in HTTP server and router in the http library.

std := import('std')
fmt := import('fmt')
http := import('http')

server := http.Server()
with server.route('/hello/:name') fn(params) {
    fn(req, end) if req.method {
        'GET' -> end({
            status: 200
            body: fmt.format('Hello, {{ 0 }}!'
                std.default(params.name, 'World'))
        })
        _ -> end(http.MethodNotAllowed)
    }
}
server.start(9999)

Overview

Oak has 7 primitive and 3 complex types.

?        // null, also "()"
_        // "empty" value, equal to anything
1, 2, 3  // integers
3.14     // floats
true     // booleans
'hello'  // strings
:error   // atoms

[1, :number]    // list
{ a: 'hello' }  // objects
fn(a, b) a + b  // functions

These types mostly behave as you'd expect. Some notable details:

  • There is no implicit type casting between any types, except during arithmetic operations when ints may be cast up to floats.
  • Both ints and floats are full 64-bit.
  • Strings are mutable byte arrays, also used for arbitrary data storage in memory, like in Lua. For immutable strings, use atoms.
  • Lists are backed by a vector data structure -- appending and indexing is cheap, but cloning is not
  • For lists and objects, equality is defined as deep equality. There is no identity equality in Oak.

We define a function in Oak with the fn keyword. A name is optional, and if given, will define that function in that scope. If there are no arguments, the () may be omitted.

fn double(n) 2 * n
fn speak {
    println('Hello!')
}

Besides the normal set of arithmetic operators, Oak has a few strange operators.

  • The assignment operator := binds values on the right side to names on the left, potentially by destructuring an object or list. For example:

    a := 1              // a is 1
    [b, c] := [2, 3]    // b is 2, c is 3
    d := double(a)      // d is 2
  • The nonlocal assignment operator <- binds values on the right side to names on the left, but only when those variables already exist. If the variable doesn't exist in the current scope, the operator ascends up parent scopes until it reaches the global scope to find the last scope where that name was bound.

    n := 10
    m := 20
    {
        n <- 30
        m := 40
    }
    n // 30
    m // 20
  • The push operator << pushes values onto the end of a string or a list, mutating it, and returns the changed string or list.

    str := 'Hello '
    str << 'World!' // 'Hello World!'
    
    list := [1, 2, 3]
    list << 4
    list << 5 << 6 // [1, 2, 3, 4, 5, 6]
  • The pipe operator |> takes a value on the left and makes it the first argument to a function call on the right.

    // print 2n for every prime n in range [0, 10)
    range(10) |> filter(prime?) |>
        each(double) |> each(println)
    
    // adding numbers
    fn add(a, b) a + b
    10 |> add(20) |> add(3) // 33

Oak uses one main construct for control flow -- the if match expression. Unlike a traditional if expression, which can only test for truthy and falsy values, Oak's if acts like a sophisticated switch-case, comparing values until the right match is reached.

fn pluralize(word, count) if count {
    1 -> word
    2 -> 'a pair of ' + word
    _ -> word + 's'
}

This match expression, combined with safe tail recursion, makes Oak Turing-complete.

Lastly, because callback-based asynchronous concurrency is common in Oak, there's special syntax sugar, the with expression, to help. The with syntax sugar de-sugars like this.

with readFile('./path') fn(file) {
    println(file)
}

// desugars to
readFile('./path', fn(file) {
    println(file)
})

For a more detailed description of the language, see the work-in-progress language spec.

Builds and deployment

While the Oak interpreter can run programs and modules directly from source code on the file system, Oak also offers a build tool, oak build, which can bundle an Oak program distributed across many files into a single "bundle" source file. oak build can also cross-compile Oak bundles into JavaScript bundles, to run in the browser or in JavaScript environments like Node.js and Deno. This allows Oak programs to be deployed and distributed as single-file programs, both on the server and in the browser.

To build a new bundle, we can simply pass an "entrypoint" to the program.

oak build --entry src/main.oak --output dist/bundle.oak

Compiling to JavaScript works similarly, but with the --web flag, which turns on JavaScript cross-compilation.

oak build --entry src/app.js.oak --output dist/bundle.js --web

The bundler and compiler are built on top of my past work with the September toolchain for Ink, but slightly re-architected to support bundling and multiple compilation targets. In the future, the goal of oak build is to become a lightly optimizing compiler and potentially help yield an oak compile command that could package the interpreter and an Oak bundle into a single executable binary. For more information on oak build, see oak help build.