Kos is a dynamically typed scripting language. It is lightweight and easy to use. Some use cases where it is useful include stand-alone scripts executable as programs and built-in scripting in applications. Kos also supports full multithreading, which makes it easy to be used in multithreaded applications.
The reference Kos interpreter is written in C, giving it maximum portability. It also comes with C++ hooks, making it very easy to embed in C++ programs.
A stand-alone script written in Kos typically has the .kos
extension. This
also applies to imported Kos modules.
Kos scripts use UTF-8 character encoding, which is a superset of ASCII.
On UNIX-like operating systems (e.g. Linux, OS X), the first line of a script
can start with a hash character, followed by an exclamation mark, followed by
path to the interpreter. This will allow the shell to automatically recognize
that this is a script and run the correct interpreter for it. Such runnable
script does not have to have the .kos
extension. Typically the first line
of an executable stand-alone Kos script looks like this:
#!/usr/bin/env kos
Alternatively, a Kos script can be executed by directly invoking the Kos interpreter, like so:
kos myscript.kos
Single line comments in Kos start with //
or #
and end at the end of the line.
# This is a comment.
// This is also a comment.
Multi-line comments in Kos start with /*
and end with */
.
/* This is
one comment. */
Multi-line comments do not support nesting.
/* This is a /* comment, but */
this is a syntax error. */
Variables in Kos are declared using the var
keyword.
var a = 1 // This is a variable containing an integer.
var b = "abc" // This is a variable containing a string.
The type of the data contained by a variable can change over time. You can assign data with a different type to a variable.
var c = 1 // This variable starts off with an integer.
c = "abc" // But now it has a string.
Variables follow scope. They can only be accessed in the scope where they were declared or its inner scopes.
var d = 1
repeat { // Inner scope.
var e = d + 1
} while false
d = e + 1 // ERROR!!! Variable e was not declared in this scope.
When variables are declared, they always must be assigned a value.
var f // ERROR!!! Variable must be assigned a value.
The void
keyword can be used to assign an opaque "void" value when
a variable needs to be declared, but a value for it is not known yet.
var g = void
Variable names consist of ASCII letters, digits and underscore _
, but the
first character of a variable name cannot be a digit. Unicode letters are not
supported in variable names.
The const
keyword can be used to create a variable, which cannot be assigned
to again. The const
does not make a variable immutable though. Mutablility
is a type trait, so if a variable contains data of a mutable type (e.g. array),
the variable's contents can be modified.
const h = 300
h = 0 // ERROR!!! Cannot assign to a const variable.
const i = ["a"]
i[1] = "b" // OK, array can be modified.
The following table lists all the types in Kos and their properties.
Type | Mutable | Properties | Prototype | Notes |
---|---|---|---|---|
integer | no | no | number | 64-bit signed integer |
float | no | no | number | 64-bit floating point |
string | no | no | object | Character string |
void | no | no | - | An "empty" type |
boolean | no | no | object | true or false |
array | yes | no | object | Regular array |
buffer | yes | no | object | Array of bytes |
object | yes | yes | void | Object with properties |
function | no | no | object | Regular function |
class | yes* | yes | function | Constructor for objects |
Most types in Kos are immutable. The "class" type is mutable in the sense that it has properties which can be added/modified/deleted, just like in the "object" type, however the underlying function cannot be modified.
The typeof
unary operator can be used to determine the type of data at run
time. It returns a string which is the literal name of the type, as in the
first column in the above table.
Example use of the typeof
operator:
var itype = typeof 42 // "integer"
var stype = typeof "word" // "string"
There are two numeric types in Kos: integer and floating-point. Integer numbers have 64-bit signed precision. Floating-point numbers also have 64-bit precision, as defined by the IEEE 754 standard.
An integer numeric constant is either a sole 0
, or it is a sequence of
digits, but the first digit in that sequence must not be a 0
.
var zero = 0
var largest_integer = 9223372036854775807
var smallest_integer = -9223372036854775808
var invalid = 0123 // ERROR!!! Invalid numeric constant.
Integer numbers can be specified in hexadecimal form, with prefix 0x
or 0X
:
var some_hex = 0x1234
var max_hex = 0x7FFFFFFFFFFFFFFF
var min_hex = -0x8000000000000000
Integer numbers can also be specified in binary form, with prefix 0b
or 0B
:
var eleven = 0b1011
A floating-point numeric constant consists of a base followed by a mantissa, exponent or both.
var float_zero = 0.0
var float_one = 1.0
var float_pi = 3.14159265359
var one_thousand = 1e3 // 1*10^3
var one_hundredth = 1e-2 // 1*10^-2
The following binary artithmetic operators are available for numeric types:
+
add-
subtract*
multiply/
divide%
modulo
When two numbers, to which the above arithmetic operations are applied, are of the same numeric type, the result is also of that type. When the two numbers are of a different type, i.e. one is integer and the other one is floating-point, the integer is promoted to a floating-point and the result is also floating-point.
var two_int = 1 + 1 // 2 (int)
var two_float = 1 + 1.0 // 2.0 (float)
var one_int = 2 - 1 // 1 (int)
var four_float = 2.0 * 2.0 // 4.0 (float)
var two_int = 5 / 2 // 2 (int)
var one_int = 5 % 2 // 1 (int)
var twopt5_flt = 5.0 / 2.0 // 2.5 (float)
The promotion from integer to floating-point is lossy. Very high and very low integers which require more than 53 bits to represent (excluding sign) loose precision when converted to a floating-point value.
The following binary bitwise operators are available for integer numbers:
&
bitwise and|
bitwise or^
bitwise xor<<
bitwise shift left>>
bitwise shift right (signed)>>>
bitwise shift right (unsigned)
The -
unary operator can be applied to any number to negate its sign.
The ~
unary operator can be applied to an integer number to flip all bits.
var minus_one = ~0 // -1, all bits set
When any operand to the binary bitwise operators or to the unary ~
operator
is a floating-point number, it is converted to an integer using round-to-zero
mode.
var three = 1.1 ^ 2.9
All of the above operators can be paired with assignment operator =
to
modify the first operand in-place.
var x = 1
x += 2 // 3
x -= 1 // 2
x *= 3 // 6
x /= 2 // 3
x %= 2 // 1
x |= 2 // 3
x &= ~1 // 2
x ^= 4 // 6
x <<= 1 // 12
x >>= 2 // 3
Unary operators are always evaluated from right to left, and before any binary operators.
An expression with all binary arithmetic operators does not need parentheses.
The *
, /
and %
operators have all the same level of precedence, and have
a higher precedence level than +
and -
. The +
and -
operators have
the same level of precedence. Operators of the same precedence level are
evaluated from left to right.
var seventeen = 1 + 2 * 3 * 4 - 5 - 6 / 2 // equals 17
// equivalent to 1 + (2*3*4) - 5 - (6/2)
Bitwise operators &
, |
, ^
, <<
, >>
, >>>
cannot be mixed with
each other nor with arithmetic operators. In addition, &
, |
and ^
bitwise operators can be chained, but the shift operators cannot be.
var nine = 15 & ~4 & ~2 // equals 9
var eight = 2 << 2 // equals 8
var error1 = 13 & 4 | 2 // ERROR!!! Cannot mix & and |
var error2 = 1 << 2 << 3 // ERROR!!! Cannot mix shift operators
In cases where multiple types of operators are used, parentheses are necessary.
The ==
and !=
operators can be used to test object equality and
inequality.
For integers and floating-point numbers, these operators will actually compare the numbers. If the compared numbers are of a different numeric type, i.e. one is integer and one is floating-point, the integer number is promoted to floating-point before comparison.
If one of the numbers being compared is NaN (floating-point not-a-number), the
==
and !=
operators always return false
.
If one of the compared objects is not of a numeric type, then the operators
check if both objects are of the same type. If the objects are of a different
type, the ==
operator evaluates to false
and the !=
operator evaluates to
true
. In other words, two objects of a different type are always not equal
(except for integer vs. floating-point).
If the compared objects are of the same, non-numeric type, then:
- For strings, the strings are actually compared. The
==
operator returnstrue
if the strings are the same andfalse
otherwise. - For voids, the
==
operator returnstrue
. - For booleans, the
==
operator returnstrue
if the boolean values are the same, andfalse
otherwise. - For all other types - arrays, buffers, objects, classes and functions - the
==
operator returnstrue
if the same object is being compared against itself, andfalse
otherwise, even if the contents of the objects being compared are the same.
Except for number comparison, when one of the numbers is NaN, the !=
operator always returns the negated value of the ==
operator.
The <
, <=
, >
and >=
operators can be used to compare numbers and
strings. For numbers, if the numbers are of a different type, the integer
number is promoted to floating-point before the comparison. If one of the
numbers is NaN, every comparison operator returns false
. When two strings
are being compared, they are compared according to the rules of the current
locale settings.
Floating-point numbers are automatically converted to integers using the
floor
rounding mode in the following situations:
- Referencing an array, buffer or string element.
- Referencing a range using slice operator.
- Passing a floating-point number to a bitwise operator:
~
,&
,|
,^
,<<
,>>
or>>>
. - Various module functions convert floating-point numbers to integers using
this method, for example array
resize()
, fileset_file_pos()
, etc.
There are two values of boolean type, denoted by true
and false
literals.
The following values are falsy, i.e. they are implicitly treated as false
by logical operators and in the if
statement:
false
void
- integer zero
0
- floating-point zero
0.0
Any other values are truthy.
TODO: ! && || ?:
TODO
Strings in Kos are delimited by "
characters.
Strings can contain any characters, including any Unicode characters.
var s = "abc"
var t = "\"def\"" // Double quotes inside a string
Strings are immutable. The only way to modify a string is to create a new, modified one.
Individual characters can be extracted from a string using the array operator. There is also the slicing operator, which allows extracting ranges of characters. Negative indexes indicate counting from the end of the string.
var hello_world = "Hello, World!"
var h = hello_world[0] // "H"
var bang = hello_world[-1] // "!"
var w = hello_world[7] // "W"
var o = hello_world[-5] // "o"
var world = hello_world[7:12] // "World"
var hello = hello_world[:5] // "Hello"
var world_bang = hello_world[-6:] // "World!"
Strings can be added together, to concatenate them.
var hello = "Hello"
var world = "World"
var hello_world = hello + ", " + world + "!" // "Hello, World!"
Escape sequences can be used to encode characters which are difficult to obtain otherwise.
var cr = "\r" // "\x0D"
var lf = "\n" // "\x0A"
var double_quote = "\""
var m = "\x6D" // "m"
var copyright = "\xA9" // Copyright sign
var pi = "\x{3C0}" // Greek lowercase PI letter
var empty_array = []
var five_numbers = [ 3, 5, 7, 11, 13 ]
var three_items = [ "abc", 1, false ]
var letters = [ "a", "b", "c", "d", "e", "f" ]
var a = letters[0]
var f = letters[-1]
var def = letters[3:]
var abc = letters[:3]
var a = [ 1, 2, 3, 4, 5 ]
a.length = 3 // [ 1, 2, 3 ]
a.length = 5 // [ 1, 2, 3, void, void ]
var b = [ 1 ]
v[3] = 2 // [ 1, void, void, 2 ]
const t_circle = 1
const t_box = 2
var circle = {
name: "circle",
type: t_circle,
center: { x: 10, y: 8 },
radius: 3
}
fun sum(x, y)
{
return x + y
}
var sum = fun(x, y) {
return x + y
}
var sum = (x, y) => x + y
import math
class Vector
{
constructor(x, y)
{
this.x = x
this.y = y
}
fun length
{
return math.sqrt(this.x * this.x + this.y * this.y)
}
}
var v1 = Vector(3, 4)
var v1_len = v1.length() // 5
import base
var integer = base.integer("1") // 1
var float = base.float("2") // 2.0
var string = base.string(3) // "3"
var boolean = base.boolean(0) // false
var _void = base.void
var array = base.array(void, 2) // [ void, 2 ]
var buffer = base.buffer(10) // buffer of 10 bytes
var object = base.object // { }
var func = base.function((x,y)=>x+y)
var _class = base.class(class { })
Buffers in Kos are useful for efficiently handling and processing sequences and streams of bytes, such as file contents.
A buffer is in many ways like an array, it works like an array in principle. However, unlike an array, a buffer only contains integer numbers from 0 through 255. Only integer numbers can be assigned to the buffer elements (floating-point numbers are converted to integers using round-to-zero mode).
Attempt to write a value less than 0 or greater than 255 triggers an exception.
To create a new buffer of size 100, filled with zeroes:
import base
var buf = base.buffer(100)
To load a file:
import io
var buf = io.open("myfile").read()