ok

command module
v0.17.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2020 License: MIT Imports: 11 Imported by: 0

README

GitHub release Build Status codecov

ok is a strongly-duck-typed language. If you notice a lot of similarities with Go, that is no accident. There are a lot of things I liked about Go that I brought to OK. Also, OK is written in Go until which time it can be written in itself.

Why OK?

Strongly Typed

OK is strongly typed. However, it has less types that most strongly typed languages because it doesn't directly expose underlying CPU types. These types are:

  • bool is a true or false value.
  • char is a single character. Supports all UTF-8 up to 4 bytes.
  • data is raw binary data.
  • number is a decimal value with an exact value (this is not an approximation like floating-point types) so all arithmetic and math operation are exact. An internal maximum precision is used for non-exact calculations.
  • string is a UTF-8 string of any length (including zero).

These five fundamental types can be used in:

  • arrays - an ordered sequence of values.
  • maps - an unordered collection of key-value pairs

First-class Testing

Testing is not just an added library of helpers, but rather it is built into the language itself.

Here is an example of some OK code we may want to test:

// main.ok

func add(a, b number) number {
    return a + b
}

Tests are placed in the same package (directory) as the source code, but with a different extension (.okt):

// main.okt

test "adding some numbers" {
    assert(add(3, 5) == 8)
    assert(add(3, 5) == 10)
}

The second assertion will fail. Here is how we run the tests with the output:

$ ok test tests/example-tests
tests/example-tests: adding some numbers: assert(8 == 10) failed
tests/example-tests: 1 failed 0 passed 2 asserts (60.638µs)

Everything in a Single Binary

The binary for the release (you can download it from the Releases page) contains all of the tools and standard library so there is zero installation or configuration. Just place the binary anywhere and start using it.

Installation

Precompiled Binaries

You can find ready to go binaries for Mac, Windows and Linux on the Releases page.

These do not require any dependencies.

go get

If you have Go installed, you can install or update the latest version of ok with:

go get -u github.com/elliotchance/ok

From Source

You will need to have Go 1.14+ installed to build from source.

IDE Support

Visual Studio Code

You can install the ok extension inside of VSCode.

Command Line Interface

asm

The asm tool is useful for debugging. It will print out human-readable descriptions of each of the compiled instructions for one or more functions. Usage:

ok asm package-name function1 function2 ...

Without providing any arguments it will use the current package and output the main function, this is equivalent to running:

ok asm . main

Here is the output from the hello-world example:

func main():
    1 Assign                 # $1 = "Hello, World!"
    2 Print                  # print($1)

build

The build tool will compile a package and produce a binary. The binary is completely standalone and can be run on another computer.

$ ok build tests/hello-world
$ ./hello-world
Hello, World!

doc

The doc tool will generate Github-friendly Markdown for a package. To see what this looks like you can have a look at the standard library.

ok doc my/package

run

Programs in ok are directories containing one or more .ok files. You can run a program by specifying the directory (or see the included tests/):

ok run my/program

test

Use test to run all tests in a package. Tests are placed in the same package (directory) as the code they will test, but with a different file extension (.okt):

ok test my/program

version

ok version will show the current version and the date it was built.

Learn By Example

These have been heavily influenced (copied) from gobyexample because it's such a great source!

Hello World

Our first program will print the classic "hello world" message. Here’s the full source code.

func main() {
    print("hello world")
}

To run the program, put the code in hello-world/main.ok and use ok run.

$ ok run tests/example-hello-world
hello world

Now that we can run and build basic ok programs, let’s learn more about the language.

Values

ok has various value types including strings, numbers, booleans, etc. Here are a few basic examples.

func main() {
    // Strings, which can be added together with +.
    print("he" + "llo")

    // Numbers.
    print("1+1 =", 1+1)
    print("7.0/3.0 =", 7.0/3.0)

    // Booleans.
    print(true and false)
    print(true or false)
    print(not true)
}
$ ok run tests/example-values
hello
1+1 = 2
7.0/3.0 = 2.3333333333333333333
false
true
false

Variables

In ok, variables are explicitly declared and used by the compiler to e.g. check type-correctness of function calls.

Every variable has a type, but that type is inferred from the expression or value being assigned to it.

func main() {

    // Declare a variable.
    a = "initial"
    print(a)

    // ok will infer the type of initialized variables.
    b = true
    print(b)

    c = 1.23
    print(c)
}
$ ok run tests/example-variables
initial
true
1.23

For

for is ok's only looping construct. Here are some basic types of for loops.

func main() {

    // The most basic type, with a single condition.
    i = 1
    for i <= 3 {
        print(i)
        i = i + 1
    }

    // A classic initial/condition/after for loop.
    for j = 7; j <= 9; ++j {
        print(j)
    }

    // for without a condition will loop repeatedly until you break out of the
    // loop or return from the enclosing function.
    for {
        print("loop")
        break
    }

    // You can also continue to the next iteration of the loop.
    for n = 0; n <= 5; ++n {
        if n%2 == 0 {
            continue
        }
        print(n)
    }
}
$ ok run tests/example-for
1
2
3
7
8
9
loop
1
3
5

If/Else

Branching with if and else in ok is straight-forward. In ok, there is no "if else". Instead you should use a switch if there are multiple cases.

func main() {

    // Here's a basic example.
    if 7%2 == 0 {
        print("7 is even")
    } else {
        print("7 is odd")
    }

    // You can have an if statement without an else.
    if 8%4 == 0 {
        print("8 is divisible by 4")
    }

    // Note that you don't need parentheses around conditions in ok, but that
    // the braces are required.
    num = 9
    if num < 10 {
        print(num, "has 1 digit")
    } else {
        print(num, "has multiple digits")
    }
}
$ ok run tests/example-if-else
7 is odd
8 is divisible by 4
9 has 1 digit

Switch

Switch statements express conditionals across many branches.

func main() {

    // Here's a basic switch.
    i = 2
    print("Write", i, "as")
    switch i {
        case 1 {
            print("one")
        }
        case 2 {
            print("two")
        }
        case 3 {
            print("three")
        }
    }

    // You can use commas to separate multiple expressions in the same case
    // statement. We use the optional else case in this example as well.
    weekday = "sunday"
    switch weekday {
        case "saturday", "sunday" {
            print("It's the weekend")
        }
        else {
            print("It's a weekday")
        }
    }

    // switch without an expression is an alternate way to express if/else
    // logic. Here we also show how the case expressions can be non-constants.
    hour = 11
    switch {
        case hour < 12 {
            print("It's before noon")
        }
        else {
            print("It's after noon")
        }
    }
}
$ ok run tests/example-switch
Write 2 as
two
It's the weekend
It's before noon

Arrays

Arrays are an ordered sequence of values.

func main() {
    // Arrays are defined within square brackets. If all the elements are the
    // same type, then the type of the array is inferred. In this case the type
    // of "a" will be "[]number".
    a = [1, 2, 3]
    print(a)

    // Arrays can contain mixed types but the type of the array must be given
    // explicitly to indicate this.
    b = []any [123, "foo", true]

    // Arrays are always printed as valid JSON. This makes it easier and more
    // natural to pass data to other systems.
    print(b)

    // Access an element in an array by using its index. Remember, 0 is the
    // index for the first element.
    print(a[0], b[1])

    // Assigning an element works the same way with the index.
    a[1] = 7
    print(a)
}
$ ok run tests/example-arrays
[1, 2, 3]
[123, "foo", true]
1 foo
[1, 7, 3]

Maps

Maps store an unordered collection of key-value pairs. The keys are strings and must be unique within the same map. The values can be of any type and there is no restriction on duplicate values.

func main() {
    // Maps are defined within curly brackets. If all the elements are the
    // same type, then the type of the map is inferred. In this case the type
    // of "a" will be "{}number".
    a = {"a": 1, "b": 2, "c": 3}
    print(a)

    // Like arrays, maps can contain mixed values by it has to be declared
    // explicitly.
    b = {}any {"a": 123, "b": "foo", "c": true}

    // Maps are always printed as valid JSON with their keys sorted. This makes
    // it easier and more natural to pass data to other systems.
    print(b)

    // Access an element in an map by using its key. Maps only support strings
    // as the key.
    print(a["b"], b["b"])

    // Assigning an element works the same way with the key.
    a["b"] = 7
    print(a)
}
$ ok run tests/example-maps
{"a": 1, "b": 2, "c": 3}
{"a": 123, "b": "foo", "c": true}
2 foo
{"a": 1, "b": 7, "c": 3}

Iteration

func main() {
    myArray = [7, 11, 13]

    // When iterating an array the first and second variable are assigned the
    // value and the index respectively.
    for v, i in myArray {
        print(i, v)
    }

    myMap = {"foo": 1.23, "bar": 4.56}

    // Maps work the same way but the second variable will be the key.
    for value, key in myMap {
        print(key, value)
    }

    // For both arrays and maps you may omit the second variable if you only
    // need to iterate the values.
    for value in myArray {
        print(value)
    }

    for value in myMap {
        print("value is", value)
    }

    // If you also need to keep a numeric iterator while iterating a map you can
    // use another form of for.
    for i = 0; value, key in myMap; ++i {
        print(i, key, value)
    }
}
$ ok run tests/example-iteration
0 7
1 11
2 13
foo 1.23
bar 4.56
7
11
13
value is 1.23
value is 4.56
0 foo 1.23
1 bar 4.56

Functions

Functions are central in ok. We'll learn about functions with a few different examples.

// Here's a function that takes two numbers and returns their sum.
func plus(a number, b number) number {
    // ok requires explicit returns, i.e. it won’t automatically return the
    // value of the last expression.
    return a + b
}

// When you have multiple consecutive parameters of the same type, you may omit
// the type name for the like-typed parameters up to the final parameter that
// declares the type.
func plusPlus(a, b, c number) number {
    return a + b + c
}

func main() {
    // Call a function just as you'd expect, with name(args).

    res = plus(1, 2)
    print("1+2 =", res)

    res = plusPlus(1, 2, 3)
    print("1+2+3 =", res)
}
$ ok run tests/example-functions
1+2 = 3
1+2+3 = 6

There are several other features to Go functions. One is multiple return values, which we'll look at next.

Multiple Return Values

OK has built-in support for multiple return values.

// The (number, number) in this function signature shows
// that the function returns 2 numbers.
func vals() (number, number) {
    return 3, 7
}

func main() {
    // Here we use the 2 different return values from the
    // call with multiple assignment.
    a, b = vals()
    print(a)
    print(b)

    // If you only want a subset of the returned values,
    // use the blank identifier _.
    _, c = vals()
    print(c)
}
$ ok run tests/example-multiple-return-values
3
7
7

Interpolation

import "math"

func main() {
    // Variables can be placed in {}. This is called interpolation.
    name = "Bob"
    print("Hello, {name}. How are you?")

    // Interpolation can be used with any expression. The result doesn't
    // even have to be a string.
    print("3 + 5 = {3+5}")

    // The spaces added between {} are not required, but might be easier
    // to read.
    total = 3.557
    print("The total is ${ math.Round(total, 2) }")
}
$ ok run tests/example-interpolation
Hello, Bob. How are you?
3 + 5 = 8
The total is $3.56

Closures

// ok supports anonymous functions, which can form closures. Anonymous functions
// are useful when you want to define a function inline without having to name
// it.

// This function intSeq returns another function, which we define anonymously in
// the body of intSeq. The returned function closes over the variable i to form
// a closure.
func intSeq() func() number {
    i = 0

    return func() number {
        // To reference a variable outside of this function we need to prefix
        // the name with '^'.
        ++^i

        // This "i" is local - unrelated to the "i" in the parent scope.
        i = 0

        return ^i
    }
}

func main() {
    // We call intSeq, assigning the result (a function) to nextInt. This
    // function value captures its own i value, which will be updated each time
    // we call nextInt.
    nextInt = intSeq()

    // See the effect of the closure by calling nextInt a few times.
    print(nextInt())
    print(nextInt())
    print(nextInt())

    // To confirm that the state is unique to that particular function, create
    // and test a new one.
    newInts = intSeq()
    print(newInts())
}
$ ok run tests/example-closures
1
2
3
1

Recursion

// ok supports recursive functions. Here's a classic factorial example.
func fact(n number) number {
    if n == 0 {
        return 1
    }

    // This fact function calls itself until it reaches the base case of fact(0).
    return n * fact(n-1)
}

func main() {
    print(fact(7))
}
$ ok run tests/example-recursion
5040

Stateful Functions (Objects)

// When a function has a single return type that is also the same name
// as the function, this is called a constructor. Constructors are used
// to create stateful functions. Sometimes called objects, records or
// structures in other languages.
func person(name string) person {
    // When you set variables here, they will be accessible outside.
    Name = name
    Age = 42

    // A constructor does not need to have a return statement. It will
    // always return itself.
}

func main() {
    // Create an instance by calling the constructor.
    p = person("Bob")

    // You can read and write properties using the dot notation.
    p.Name = "John"
    print(p.Age)

    // Objects are always printed as JSON objects.
    print(p)
}
$ ok run tests/example-objects
42
{"Age": 42, "Name": "John"}

Errors

// Errors in ok work very similar to exceptions in some other languages. If you
// are not familar with exceptions, read on.

// Any function can raise an error. Raising an error prevents any more code from
// running, the code will jump to an error handler. If there is no error handler
// in this function it will be raised to the caller, and so on until it's
// handled, as we will see.
func f1(arg number) number {
    if arg == 42 {
        // Error is a build in type that takes a message. Since we do not handle
        // the error here, it will be passed up to the caller.
        raise Error("can't work with 42")
    }

    return arg + 3
}

// Sometimes just a message is not enough. We can create our own custom error
// type by implementing the Error interface. That is, any type that has the
// property Error of type string.
func argError(arg number, prob string) argError {
    // Save these properties for later.
    Arg = arg
    Prob = prob

    // This satisfies the Error interface.
    Error = "{arg} - {prob}"
}

// f2 performs the same logic as f1, but we raise our custom error instead.
func f2(arg number) number {
    if arg == 42 {
        // Our custom error will act like an Error, but we can also inspect it.
        raise argError(arg, "can't work with it")
    }

    return arg + 3
}

func main() {
    for i in [7, 42] {
        try {
            r = f1(i)

            // If f1 raised an error this will not run because the code will
            // jump immediately to the error handler below.
            print("f1 worked:", r)
        } on Error {
            // When an error is caught, a special "err" variable is provided.
            // Since the Error type must include an Error string property, we
            // can print it out now.
            print("f1 failed:", err.Error)
        }
    }

    // Let's try the same thing with f2, which has the same logic, but raises a
    // custom error type.
    for i in [7, 42] {
        try {
            r = f2(i)
            print("f2 worked:", r)
        } on Error {
            // The type of "err" is still an Error, so we cannot access Arg and
            // Prob. We can rely on the Error message through.
            print("f2 failed:", err.Error)
        }
    }

    try {
        f2(42)
    } on argError {
        // The "err" is now a type of argError, so we can inpect those extra
        // properties. Be careful, if the error raised was not of type argError,
        // then it will not be handled here. Instead it would be passed up to
        // the caller to handle.
        print(err.Arg)
        print(err.Prob)
    }

    // You can have multiple handlers. Only the first match will be executed.
    // This let's you provide more generic handlers if specific cases are not
    // raised.
    try {
        f1(42)
    } on argError {
        // f1 raises an Error, so it will not be caught here.
        print("This should not happen!")
    } on Error {
        // This will handle the error.
        print("f1 failed, all we have is the message:", err.Error)
    }
}
$ ok run tests/example-errors
f1 worked: 10
f1 failed: can't work with 42
f2 worked: 10
f2 failed: 42 - can't work with it
42
can't work with it
f1 failed, all we have is the message: can't work with 42

Finally

func f1(arg number) number {
    if arg == 42 {
        raise Error("can't work with 42")
    }

    return arg + 3
}

func main() {
    for i in [7, 42] {
        try {
            r = f1(i)
            print("f1 worked:", r)
        } on Error {
            print("f1 failed:", err.Error)
        } finally {
            // Finally will always run after the try block or the error
            // handler.
            print("finally f1")
        }
    }

    for i in [7, 42] {
        try {
            r = f2(i)
            print("f2 worked:", r)
        } on Error {
            print("f2 failed:", err.Error)
        }
    }
}

func f2(arg number) number {
    try {
        return f1(arg)
    } finally {
        // Finally will always run before the return, even if there is
        // an error.
        print("f2 done")
    }
}
$ ok run tests/example-finally
f1 worked: 10
finally f1
f1 failed: can't work with 42
finally f1
f2 done
f2 worked: 10
f2 done
f2 failed: can't work with 42

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
ast
Package ast provides structures for representing parsed source code.
Package ast provides structures for representing parsed source code.
cmd
asm
doc
run
Package compiler translates the ast structures that represent the parsed course code into instructions that can be executed by the VM.
Package compiler translates the ast structures that represent the parsed course code into instructions that can be executed by the VM.
Package lexer converts source code into tokens.
Package lexer converts source code into tokens.
Package number contains the implementation of the number data type.
Package number contains the implementation of the number data type.
Package parser translates the linear stream of tokens into ast structures based on the syntax of the ok language.
Package parser translates the linear stream of tokens into ast structures based on the syntax of the ok language.
Package instruction contains all the instructions created by the compiler and executed by the VM.
Package instruction contains all the instructions created by the compiler and executed by the VM.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL