Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Welcome to the KSL Docs!

Hopefully you learn something useful about KSL! You can access all the pages in the navigation menu (left side on desktop and hamburger menu icon on mobile.)

If you’re just getting started, check out the Setup Guide!

Here by accident? KSL Hopepage.

Introduction

KSL is a language designed to be strongly typed, aot-compiled, fast, and memory safe. Along with some other neat features like function attributes and pretty much whatever else seems worthy of having.

So why should I pick KSL over another language?

  • KSL is statically typed, although that can make development less fun at times, it catches a lot of possible runtime bugs allowing you to write safer code.
  • KSL is fast, it uses the same compiler backend as C, C++, and Rust. In early testing it was slightly slower than C and faster than Rust.
  • KSL is memory safe, although not as much as Rust might be, KSL does include a reference counting system which will take care of memory management for you!
  • KSL was designed with developer experience in mind, with a beautiful and understandable syntax, fast compile times, rich documentation, and various other language features, I can say this is the best language I’ve ever developed in (with that being said I’m very baised.)

Think something is missing or could be improved? Contribute Now!

Getting Started

Let’s get KSL working on your device!

Once you get KSL installed and working, try learning about some of the language!

If you run into any problems check out the error codes documentation:

Finally, if you already have a background in programming and just want to learn the KSL syntax, check out the quick syntax guide.

Installing KSL

Configuring KSL

It is highly recommended to familiarize yourself with the compiler flags, viewable at any time with KSL -h. There’s also an in-depth breakdown here.

Tutorials

Hello World in KSL

Ah yes, the traditional “hello world” program. The hello world program seems to be something that a lot of developers end up building, how strange, this should be investigated…

Let’s build it!

So first, we’re going to need a main function, our entry point.

fn main() -> void {}

Since we don’t need our main function to return anything we can just set the return type to void (notice the -> after the parenthesis and before void?)

We’re also writing to the console, so we’re going to need our good old standard io module. Go ahead and add it with: using std.io; You really should do this at the top of the file (it’s best practice,) but really anywhere works.

That should give us access to the writeln function, so let’s finish off your “World, Hello?” program.

using std.io;

fn main() -> void {
	io.writeln("World, Hello?");
	return;
}

Isn’t that awesome!! If this wasn’t torture then you should check out some other tutorials and learn some more about KSL! It might take some time but don’t give up!!!

For Loops

If you’re here you probably want to learn how to make a for loop in KSL. If that’s the case, welcome! Otherwise, uh, check this out??

So, we want to make something interesting that expresses how KSL works. A great idea for that would be to make a simple program that generates a random decimal value, we’ll then round that and count the number of 1’s and 0’s in a sample number!

The first thing we need to do, is consider our requirements. We need to generate a random number, round it, and we’ll want to display the results at the end. So we need std.random, std.math, and std.io.

Let’s start our new file, you can call it whatever you want but I’ll call mine super_awesome_epic_for_loop_example_4_the_ksl_docs_tutorial.k! Without further adieu, let’s begin.

using std.io;
using std.math;
using std.random;

This should get all our needs taken care of in regards to the standard library modules! Now we need to define our entry point.

fn main() -> int {
	return 0;
}

As covered in some other tutorials (hopefully) the main function is the entry point and the -> int defines the function derivative (basically the expected function return type, but KSL has to be sooooo special and call it something different because it wants to be like the cool kids.)

Now let’s define our zero and one counts. By default all variables in KSL are mutable, this is great news for us because that means we don’t have to do any extra work to update these values!

fn main() -> int {
	int zeros = 0;
	int ones = 0;
	return 0;
}

We’ve made so much progress so far! Go ahead and get a drink of water, because this is where it gets real.

We need to add the famed for loop now.

fn main() -> int {
	int zeros = 0;
	int ones = 0;
	for (int i = 0; i < 100; i++) {

	}
	return 0;
}

So, let’s disect this a little bit. The first statement is our initializer, we’re setting the i variable to an integer with a value of 0. The next is an expression, it’s expected to be a boolean. If the value is true then it will run again, otherwise it will move on in the program. Finally, we have the iterative statement, the i++, which is just a shorthand to increase the variable i by 1 in this case.

Great, that was a lot, but it only gets better from here! Let’s add the random number generation function!

fn main() -> int {
	int zeros = 0;
	int ones = 0;
	for (int i = 0; i < 100; i++) {
		f64 random_float = random.rand()->f64;
	}
	return 0;
}

This line of code actually covers a lot of topics in KSL, for now most of them aren’t super necessary to know. If you want to read more later check out inline function calls, specifically inline hints. All you need to know for this tutorial is that this is getting a random number and saving it to the random_float variable, which we have set to a f64.

Now we need to round this value to figure out if it will be a 1 or a 0!

We can do this with the following code, go ahead and add it after the random_float variable line.

int random_rounded = math.round(random_float)->int;

If you’ve figured that out we can move on to the if statement, which will be the part of our program that actually tells us which variable to increment (ones or zeros, if you’ve already forgotten.)

if (random_rounded == 1) {
	ones++;
} else {
	zeros++;
}

What this is doing is checking if the random_rounded variable is equal to 1, if it is, then we increment the ones variable (remember that shorthand from earlier?) Otherwise, we add 1 to the zeros variable. Luckily, since the result of our random number rounding can only be 1 or 0 in this situation, we don’t have to add any other logic for counting!

If you ran this code right now it would work! But you wouldn’t be able to see the results :( and that’s kind of the point of this whole thing. So, let’s use our awesome io module from the standard library to show our counts!

io.writeln(zeros);
io.writeln(ones);

Great, now it will print our counts to the console!

For those of you that don’t care about learning the language and just want to make something quickly without thinking of the potential consequences, here is the full code:

using std.io;
using std.math;
using std.random;

fn main() -> int {
	int zeros = 0;
	int ones = 0;
	for (int i = 0; i < 100; i++) {
		f64 random_float = random.rand()->f64;
		int random_rounded = math.round(random_float)->int;
		if (random_rounded == 1) {
			ones++;
		} else {
			zeros++;
		}
	}
	io.writeln(zeros);
	io.writeln(ones);
	return 0;
}

Oh yeah, if you wanted to check your work that’s also good, no hate on that.

Rock, Paper, Scissors

Docs Coming Soon

Technically we have everything you’d need to write it, I just don’t feel like doing more documentation work right now.

Structs, Arrays, and Assignment Shenanigans

The main purpose of this tutorial is to show what’s possible with KSL regarding structs and arrays, especially regarding assignment/mutations.

For example, below is a completely valid KSL file that works!

struct Vector2 { int x, int y }

fn main() -> void {
    Vector2[] positions = [
        Vector2 { x: 10, y: 20 },
        Vector2 { x: 30, y: 40 },
        Vector2 { x: 50, y: 60 },
        Vector2 { x: 70, y: 80 },
        Vector2 { x: 90, y: 99 }
    ];
    // Lets say you wanted to change the `x` field
    // of the first item in the positions array.
    positions[0].x = <new_value>;
    // Increment and decrement shorthands also work
    positions[0].x++; // Increment
    positions[0].x--; // Decrement
}

If you want to needlessly add complexity, this will also work:

using std.io;

struct Vector2 { int x, int y }

fn main() -> void {
    Vector2[][] positions = [[
        Vector2 { x: 10, y: 20 }, // Will be at [0][0]
        Vector2 { x: 30, y: 40 }  // Will be at [0][1]
    ], [
        Vector2 { x: 50, y: 60 }, // Will be at [1][0]
        Vector2 { x: 70, y: 80 }  // Will be at [1][1]
    ]];
    // Lets add one to the positions[0][0].y struct field!
    positions[0][0].y++;
    // Should we check if it's value was actually changed?
    // It was originally 20, so it should be 21 now.
    io.writeln(positions[0][0].y);
}

Comments

Comments in KSL are similar to any other modern language. For a single line comment you can do // and for a multi-line comment, the usual /* */ syntax.

// Below is a function, and this is a comment!
fn main() -> void {
	return;
}
/*
 * Below is a function that returns void,
 * and this is a multi-line comment!
 */
fn main() -> void {
	return;
}

You can even use this inline! (If you’re crazy…)

fn /* create a function */ main /* call it `main` */ () /* no args */ -> void /* return nothing */ {
	return /* see, nothing! */;
}

Best Practices

It’s good to keep some consistency when using comments. Here’s a good pattern to follow:

  • // for code or text comments, typically short bits (1-3 lines)
    • Quick one-off comments or single line descriptions
// A quick comment telling someone that this will break legacy code
fn some_random_esoteric_function() -> void {
	// some function body
}
  • /* ... */ for code, typically large sections of code (5-20 lines)
    • Best used for test code or sections of code that are temporarily removed
/*
fn some_dummy_function_that_needs_to_be_removed_temporarily() -> void {
	// some function body
}
*/
  • /* * * */ for text, typically large sections of text (5-20 lines)
    • The best example would be a complex function that needs a lot of explaining
/*
 * Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex
 * sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis
 * convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus
 * fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada
 * lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti
 * sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
 */
fn another_random_esoteric_function() -> void {
	// seriously who keeps adding these ??
}

Understanding Types

Before you learn about specific types and their implementations, you should get a hold on the casting system. It’s different from some other languages so it’s important you understand it.

Type specific details / implementations:

Integers

Overview

Integers in KSL are a blanket type. The “real” types of integers are i8, i16, i32, and i64. Technically int is also an alias for i64. For unsigned there is u8, u16, u32, u64, and uint which is an alias for u64. There are no special rules for interacting with integers. Their space in memory is equal to the type name, e.g. an i32 will be stored as an integer 32 in memory during runtime, similarly an i8 will be stored an an integer 8.

Casting Support

Types that can be cast to integers:

  • Float (via <value>'int)
  • Bool (via <value>'int)

Types that integers can be cast to:

  • Float (via <value>'float)
  • Bool (via <value>'bool)

Method Support

There are no methods supported for integers.

Examples

int x = 20;

Notes

In some cases there will be an integer literal in your source code without a decernable type. In these cases KSL will attempt to give it the smallest possible type (e.g. 50 will be given an i8). The main thing to note here is that KSL will not give it an unsigned type, always a signed integer. If you wish to use an unsigned integer it must be explicitly stated or casted.

Floats

Overview

Floats in KSL are a blanket type. The “real” types of floats are f32 and f64. Techincally float is also an alias for f64. There is no f16 support because realistically, they won’t be used often enough to justify it. There are no special rules for interacting with floats. Their space in memory is equal to the type name, e.g. a f32 will be stored as a float 32 in memory during runtime, similarly a f64 will be stored as a float 64.

Casting Support

Types that can be cast to floats:

  • Integer (via <value>'float)
  • Bool (via <value>'float)

Types that floats can be cast to:

  • Integer (via <value>'int)
  • Bool (via <value>'bool)

Method Support

There are no methods supported for floats.

Examples

float x = 20.5;

Booleans

Overview

Booleans in KSL are represented with bool. There are no special rules for interacting with booleans. They will be stored as a full byte in storage unless manually tweaked. The only considerations to take into account are how numbers are casted into booleans as they may differ from other programming languages.

Casting Support

Types that can be cast to bools:

  • Integer (via <value>'bool)
  • Float (via <value>'bool)

Types that bools can be cast to:

  • Integer (via <value>'int)
  • Float (via <value>'float)

Method Support

There are no methods supported for booleans.

Examples

bool x = true;

Notes

A quick note on casting to booleans:

In some languages casting integers or floats to a boolean may be different. In KSL all integers other than 0 are cast to true. Similarly, all floats other than 0.0 are cast to true:

bool'1;         // true
bool'0;         // false
bool'252466642; // true
bool'-35;       // true

bool'0.0;       // false
bool'135.03111; // true
bool'-135.77;   // true

Chars

Overview

Internally the char type is represented as an i8.

Casting Support

Chars do not directly support any casting operations. However, since they’re treated as integers as soon as the semantic step, you technically can do all integer operations on them, including casting.

Method Support

Currently there are no methods supported for chars.

Examples

using std.io;

fn main() -> void {
	char a = 'a';
	io.writeln(int'a); // ascii a
	io.writeln(a); // will print the character 'a'
}

Notes

In KSL the casting operator is also an apostrophe. So KSL will scan ahead by one character (including any escape sequence) and check for another apostrophe to classify something as a character. Otherwise if KSL sees an apostrophe without a closing apostrophe, it will assume you want to cast.

Enums

Overview

Enums in KSL are defined with the enum keyword. Internally they’re replaced with the smallest unsigned integer size that fits all enum elements before code generation or even semantics. That means that enums will be treated the same way that integers are.

Casting Support

Enums do not directly support any casting operations. However, since they’re treated as integers as soon as the semantic step, you technically can do all integer operations on them, including casting.

Method Support

There are no methods supported for enums.

Examples

using std.io;

enum Status {
	Running,
	Failed,
	Passed
}

fn main() -> void {
	Status state = Status.Running;
	io.writeln(int'state); // will print 0
	io.writeln(state == Status.Running); // true
	io.writeln(state == Status.Failed); // false
}

Strings

Overview

Strings in KSL are pointers to memory locations, meaning they can’t easily be modified. Attempting to modify a string in KSL will result in an entirely new string being created. To “dynamically” handle strings use a character array (char[]) and then turn it into a string with char[].to_string() or similar.

String literals must be defined with double quotes.

Casting Support

Strings do not support any casting operations. Additionally, casting is not an operator that you can define custom behavior for. If you want to convert string types you’ll need to do it manually.

Method Support

.length()
Will return the length of the string as an integer.

Will not mutate string.

fn main() -> void {
	str a = "Hello World!";
	a.length(); // returns 12
}

.is_empty()
Will return a boolean telling you if the string is empty or not.

Will not mutate string.

fn main() -> void {
	str a = "Hello World!";
	a.is_empty(); // false

	str b = "";
	b.is_empty(); // true

	str c = "    ";
	c.is_empty(); // true
}

Examples

str message = "Hello World!";

Notes

Internally, strings can be pointers to different spots in memory. In most cases they point to memory within the binary where literals have already been defined. In the case of a modified string the pointer may be to a malloc’d memory space. Strings should be automatically reference counted by KSL so you don’t need to worry about this unless you’re building a KSL translation layer or C library.

Arrays

Overview

Arrays in KSL are dynamically allocable, meaning you don’t have to worry about memory management when using them. If you push an item longer than the array capacity it will reallocate more space for the array and continue the push operation.

Casting Support

Arrays do not support any casting operations. Additionally, casting is not an operator that you can define custom behavior for. If you want to convert array types you’ll need to do it manually.

Method Support

.length()
Will return the length of the array as an integer.

Will not mutate array.

fn main() -> void {
	int[] a = [1, 2, 3, 4, 5];
	a.length(); // Returns 5
}

.sample()
Will return a random element from the array (return type will depend on array.)

Will not mutate array.

fn main() -> void {
	int[] a = [1, 2, 3, 4, 5];
	a.sample(); // Will get a random value from the array `a`
}

.is_empty()
Will return a boolean telling you if the array has any elements.

Will not mutate array.

fn main() -> void {
	int[] a = [1, 2, 3, 4, 5];
	int[] b = [];
	a.is_empty(); // false
	b.is_empty(); // true
}

.push(<v>)
Will push a new element to the end of the array, does not return anything. Parameter type must be the same as the array base type.

Will mutate array.

fn main() -> void {
	int[] a = [1, 2, 3, 4, 5];
	a.push(6); // Array will be `[1, 2, 3, 4, 5, 6]` now
}

.includes(<v>)
Checks if the array includes a value. Returns a boolean and passed value must be the same as array base type.

Will not mutate array.

fn main() -> void {
	int[] a = [1, 2, 3, 4, 5];
	a.includes(3); // true
	a.includes(6); // false
}

.index_of(<v>) Gets the index of a value in an array, if the array has multiple instances of a value then it will only return the index of the first occurrence. Passed value type must be the same as array base type. Will return an integer.

Will not mutate array.

fn main() -> void {
	int[] a = [1, 2, 3, 4, 5];
	a.index_of(6); // -1 (`6` doesn't exist in array)
	a.index_of(2); // 1 (`2` is the second element)
	a.index_of(1); // 0 (`1` is the first element)
}

.get_or_default(<i>, <v>)
Attempts to get a value at a specific index, if the index doesn’t exist then it will return the default value (second parameter) instead. Index must be an integer. Default value and return types are must be the same as the array base type.

Will not mutate array.

fn main() -> void {
	int[] a = [1, 2, 3, 4, 5];
	a.get_or_default(1, 99); // returns `2`
	a.get_or_default(5, 99); // returns `99` (index `5` doesn't exist)
}

Examples

For arrays as variable declarations:

int[] x = [ 10, 20, 30 ];
float[] y = [ 1.1, 2.2, 3.3 ];
bool[] z = [ true, false, true ];

For inline array literals:

io.writeln(int'[ 10, 20, 30 ][1]); // will print 20

Inline array literals will be confusing at first, but they’ll make sense pretty soon! The syntax is weird, it’s like casting but then it’s an array? Isn’t that illegal?

Since KSL is a strongly typed language with just a tiny bit of inferencing here and there, we can’t determine a solid type for array literals. That means KSL relies on you to provide it with type information. Since it doesn’t have a var declaration to rely on it needs you to “cast” it (provide it a type hint).

Once you have an array literal you can use it just like a normal array though! In the example above we immediately index into the 2nd element (index 1). Here’s another example:

io.writeln(bool'[ false, true, true, false ]);

This example will print the normal way KSL prints arrays, output below.

<T>(ARRBOOL) <LENGTH>(4) <CAPACITY>(4)
[000] false
[001] true
[002] true
[003] false

Notes

Internally, arrays are handled as a struct, this will only be important if you’re building an interface for KSL in another language. You can find the current definition details in std/runtime/generic_array.h.

Structs

Overview

Structs in KSL are defined with the struct keyword. Structs are passed by pointer by default. This is subject to change with the introduction of the ref keyword, although it likely will not. Structs are the only types in KSL that can be templated.

Casting Support

Structs do not support any casting operations. Additionally, casting is not an operator that you can define custom behavior for. If you want to convert struct types you’ll need to do it manually.

Method Support

There are no methods supported for structs.

Examples

struct Vector2 {
    int x,
    int y
}

fn main() -> void {
    Vector2 my_position = Vector2 { x: 10, y: 20 };

    // Alternatively you can define a struct without the field
    // names, but you'll have to do it in the correct order.
    Vector2 my_second_position = Vector2 { 10, 20 };

    // Using the field names, you can change the order and KSL
    // will make sure it still works behind the scenes.
    Vector2 superimposed_position = Vector2 { y: 20, x: 10 };
}

Notes

Internally, defined structs are treated as new types and added to the type system after definition.

Type Casting

KSL has a potentially unintuitive type casting syntax. Just use a type identifier followed by an apostrophe:

float x = 10.0;
int y = int'x;

There are some type casts that will result in errors, for example, casting to null or void. But why do that anyway? It should be noted that casting only works for the term immediately right to the cast, see below:

Example where the entire expression is calcuated then cast:

// This turns into 2.0 and is then turned into 2.
int x = int'(1.0 + 1.0);

Example where only the first number value is cast:

// This is turned into 1 + 1 then turned into 2.
int x = int'1.0 + 1;

A quick note on casting to booleans:

In some languages casting integers or floats to a boolean may be different. In KSL all integers other than 0 are cast to true. Similarly, all floats other than 0.0 are cast to true:

bool'1;         // true
bool'0;         // false
bool'252466642; // true
bool'-35;       // true

bool'0.0;       // false
bool'135.03111; // true
bool'-135.77;   // true

For more information on the boolean type in KSL check out this page.

Important

Casting cannot be done on arrays or structs, you’ll have to manually cast the elements or fields. Casting is not a custom method you can implement for a type.

Variables

Variable declarations are extremely simple. Simply pick a variable type and identifier, then set it to a value.

int x = 20;

Note

You cannot defined a variable with no value, for example: int x; is not valid syntax. However, variables are treated as mutable, so to achieve the same effect just initialize the variable with dummy data.

Currently the supported types in KSL are:

TypeIdentifierSize (Bits)
int8i88
int16i1616
int32i3232
int64i64 || int64
uint8u88
uint16u1616
uint32u3232
uint64u64 || uint64
float32f3232
floatf64 || float64
charchar8
boolbool1
voidvoidN|A
enumenum??
structstruct??

Note

You can make virtually any type an array by adding [] to the initialization type (e.g. int[] will create an int array.)

Note

Defining structs via the struct keyword will register an entierly new type to the type system, it can be treated like most other types, which means you can also make an array of structs using the struct name (e.g. my_struct[] will create an array of my_struct structs.)

Expressions

Operations (Binary and Unary)

Higher precedences are executed first.

Supported Binary Operations (and Precedence):

OperationSymbolPrecedence
Modulus%100
Multiply*90
Divide/90
Add+80
Subtract-80
Left Shift<<70
Right Shift>>70
Less or Equal<=60
Greater or Equal>=60
Less Than<50
Greater Than>50
Equal==40
Not Equal!=40
Not!30
And&&20
Xor^12
Or||10

Supported Unary Operations:

OperationSymbol
Negative-<num>
Not!<bool>
Cast<type>’<expr>

Functions

Function Definition

For now, functions will be defined with the fn keyword, then comes the function identifier and typed parameters (inside parenthesis). As KSL is a strongly typed language, you will also need to define the function return type (or function derivative.) This can be done with -> and then the type identifier.

fn main(int a) -> int {
	return a;
}

Function Returns

In KSL return statements are technically optional for functions that return primitive types. If a return statement isn’t detected it will insert one with a default value matching the type (e.g. if the return type is an int it will use 0 as the return placeholder.)

This can be really nice for defining void functions, see below:

// This function doesn't need to have a return statement because it has a
// primitive return type and we don't care what the return value is.
fn main() -> void {
	// Do something
};

You can, of course, still manually define a return statement with return. For some types (e.g. arrays and structs) you will still have to define a return statement no matter what.

// This function must have a return statement because KSL doesn't know what a
// default integer array should be by default.
fn arr() -> int[] {
	int[] my_array = [ 1, 2, 3 ];
	return my_array;
}

Inline Function Calls

Currently, functions in KSL are defined internally in two ways, by the function body declaration or a forward declaration. In the case of a forward declaration, KSL will attempt to interpret the most likely return type. For example, if a function is called inside of an integer operation then it’s likely to be an integer. In order to override this behavior and define a return type for an inline function call you can use the derive keyword (->) on the function call. See below:

Example of inline call:

int x = add(1, 3) + 5;

Example of inline call w/ type hint:

int x = add(1, 3) -> int + 5;

Sometimes it can be easier to read if you move the type hint to right after the function:

int x = add(1, 3)->int + 5;

Important

You cannot use this syntax to change the function return type. If you wish to use a different type (post-return) then look into type casting. This is only to hint to the compiler what the expected return type is.

Important

Using this syntax to claim a function returns void for a function that returns an int will result in an unresolved symbol. The same applies for other type differences as well.

Similarly, if a function call doesn’t appear to have any context then KSL might assume it returns nothing (void.) This might cause an error so you can use the type hint syntax outside of algebraic operations as well.

Example:

add(1, 3) -> int;

Conditionals

Currently if/if else/else statements are supported. Expressions must be surrounded by parenthesis and return a boolean type.

if (<expression_one>) {
	io.writeln("Do Something");
} else if (<expression_two>) {
	io.writeln("Do Something Else");
} else {
	io.writeln("Neither Condition Was Met");
}

You can chain as many else if’s as you want.

Loops

A note on for loops: KSL does not have a for item in array loop, or a for i in 0..n loop. Both of these are cool abstractions, but totally unnecessary. It’s unlikely that KSL will ever have these.

For

The syntax for for loops is pretty simple. Just like in most languages with a for loop actually.

You’ll need an initialization statement (which is run once at the beginning), an expression to check if the for loop should continue, and finally an iterative expression (which is run at the end of every iteration.)

//       +------------------ Initial Statement
//       |        +--------- Check/Continue(?) Expression
//       |        |             +-- (Must Result in a Boolean)
//       |        |      +-- Iterative Expression
//       |        |      |
for (int i = 0; i < 30; i++) {
    io.writeln(i);
}

The output of the above code will be the numbers 0 through 29, each on a new line in the console.

Let’s say you want to iterate over elements in an array though, what then?
Combine the for loop with your array and the length() array method!

using std.io;

fn main() -> void {
    int[] my_arr = [ 35, 62, 84, 44, 27, 46, 85 ];

    for (int i = 0; i < my_arr.length(); i++) {
        io.writeln(my_arr[i]);
    }
}

While

The syntax for while loops is absurdly easy. All you need is the while keyword with some parenthesis and an expression.

while (<expression>) {
    // Do something? Maybe? If you want, I guess?
}

The only real requirement is that the expression type is a boolean. KSL will give you a nice error if you don’t. If this is a problem, you can always cast to a boolean.

while (bool'127) {
    // This loop will run forever (127 will always cast to true)
}

Libraries

The syntax for this is strange so pay attention. Both libraries and files use the using keyword to be imported, but what follows the using keyword is different.

In order to import another .k or .ksl file, follow up the using keyword with a string that contains the relative path to the file:

using "src/api.k"; // Imports the contents of src/api.k to be used.
using "src/abi.k"; // Imports the contents of src/abi.k to be used.
using "lib/ffi.k"; // Imports the contents of lib/ffi.k to be used.

To use a library (like a standard-lib module) the syntax is a little different, follow up the using keyword with an extended identifier instead:

using std.io;  // Links the standard io library.
using std.fs;  // Links the standard file system library.
using std.env; // Links the standard environment library.

Note

The biggest difference is that imports (using “”) are .k or .ksl files, most likely part of your own codebase. Links, on the other hand (using <iden>) link object files to your final executable. These object files are expected to be in the same directory as the compiler itself, but project-specific support may be added in the future. Each period represents a directory. So std.io would link the std/io.o object file into the final executable.

Namespaces

Namespaces are defined on a per-file basis. Or they should be at least. They do not have braces, just define the namespace and put the functions after it, for example:

namespace food;

fn pizza() -> void {}
fn sauce() -> void {}
fn tacos() -> void {}

All of these can then be accessed with food.<function_name> (food.pizza, food.sauce, etc.) If you’re working within the namespace then the food. part is not required.

Namespaces cannot be nested.

Function Attributes

Entry

Usage

@entry
fn new_main_function() -> void {}

fn main() -> void {}

Purpose

The purpose of the @entry function attribute is to override the binary entry point. The primary usage for this would be customization of the code base, however, in the future there are likely going to be “groups,” where you can define a custom entry point from the command line via compiler arguments. This would be helpful for setting a custom entry point for testing vs. debug/release.

Entry

Usage

@inline
fn small_function() -> bool {}

fn main() -> void {}

Purpose

The purpose of the @inline function attribute is to hint to the compiler that you want this function inlined. Using the inline function attribute does not guarantee that your function will be inlined. Typically this would be used for performance reasons. Occasionally the compiler will decide to inline a function that does not explicitly have the inline function attribute.

No Fail

Usage

@no_fail
fn function_that_cannot_fail() -> int {
	return 20 / 0;
}

In the example above, the function will throw a “cannot divide by 0” error, however, because we have the @no_fail attribute it will catch the error and return a default constant based on the defined return type of the function. For integers the default constant is 0 so this function will return 0, even though technically it failed.

You can also define custom return values via @no_fail=<value>, for example:

@no_fail=-1
fn function_that_cannot_fail() -> int {
	return 40 / 0;
}

In this new example the @no_fail function attribute has a value assigned to it (-1), so when this function fails due to the aforementioned “divide by 0” error, it will instead return -1.

Purpose

The purpose of the @no_fail function attribute is to provide a better error catching system that doesn’t rely on large code blocks. Additionally, by using the @no_fail function you can prevent potentially fatal crashes while minimizing undefined behavior.

No Mangle

Usage

@no_mangle
fn non_mangled_function() -> void {}

fn mangled_function() -> void {}

Purpose

The purpose of the @no_mangle function attribute is to prevent function names from becoming mangled in a binary/object file outputted by KSL. This supports better cross-language development efforts.

Extern/FFI Concepts

Note

Currently we’re discussing adding a @no_mangle attribute. This would do exactly what it sounds like, it would maintain it’s function name regardless of the name mangling schema.

The extern keyword allows you to use functions from outside KSL. External functions are also the only way to use variadic functions. The extern keyword still needs to be followed by the fn keyword as well as a full function signature.

You can define external functions one by one with this syntax:

extern fn func_in_another_lang_short(bool) -> void;

Or if you need to define a lot of related functions you can use a block syntax instead.

extern {
	fn func_in_another_lang_zero(int) -> void;
	fn func_in_another_lang_one(float) -> int;
	fn func_in_another_lang_two(int) -> float;
}

In order to use variadic functions you can add a ... to the function argument list in the signature. This can only be used for functions defined with extern. Additionally, you can mix the ... alongside other known parameter types. Examples below.

/*
 * This will make an external function definition that can be used in your KSL
 * source code. The `str` means that it expects a string parameter and the `...`
 * means that anything after that is totally up to you.
 *
 * This is a working example to access printf within your KSL program.
 */
extern fn printf(str _, ...) -> void;
extern {
	fn printf(str _, ...) -> void;
	<other_external_function_signatures>
}

Templates

KSL templates are super powerful ways to build generic code fast. It is fully covered by semantic analysis as well.

The basic syntax is as follows:

template <$T1, $T2>
struct ExampleTemplate {
	$T1 foo,
	$T2 bar
}

You may notice some things right away. For example, the dollar sign. The dollar sign is used to denote an unknown type that should be figured out by the template. Let’s say in the example above, someone uses <int, int>ExampleTemplate, this will fill out the entire ExampleTemplate with the type information provided.

You can even attach impl statements to the struct as you normally would!

template <$T1, $T2>
struct Map {
	$T1[] keys,
	$T2[] values
}

impl Map {
	fn get($T1 key) -> $T2 {
		for (u64 i = 0; i < this.keys.length(); i++) {
			if (this.keys[i] == key)
				return this.values[i];
		}
		return this.values[0];
	}
}

In the example above you may notice that in the Map struct there’s $T1[]. With KSL templates you can even turn unknown types into arrays or references like you would with a normal type.

You might initialize the Map struct we just created like this:

using std.io;

fn main() -> void {
	Map my_map = Map<int, int> { int'[1, 2, 3], int'[4, 5, 6] };

	io.writeln(my_map.get(2)); // will print 5
}

Note

If the int'[ ... ] syntax is unfamiliar to you, check out the array type. This is just a way of defining an inline array literal but you can read more about it there.

Error Codes

First, let’s learn to understand error messages. They’re comprised of three parts:

The first letter (message type):

  • E = Error
  • W = Warning

The second letter (error compiler location):

  • L = Lexer Stage
  • P = Parser Stage
  • S = Semantic Analysis
  • T = Symbol Table
  • N = Linking Stage
  • C = Command Line

The third part, the error number. This denotes the actual message inside of the message type and compiler location. Occasionally in alpha or debug builds an error number might show up as xxxx, this means that it hasn’t been assigned an official number yet.

[EL0000]
 ^^^
 |||
 ||+-- Message Number
 |+--- Compiler Location
 +---- Message Type

Warning

The current error code list is incomplete, a more comprehensive list will be made when the compiler is closer to a stable release.

EL0000

Message:
Unexpected End-of-File Immediately After Comment Opening

Meaning:
The compiler expected a message after the opening of a comment but the file ended instead.

EL0001

Message:
Unexpected End-of-File While Lexing Comment

Meaning:
The lexer expected a comment to extend to a newline character but the file ended instead

EL0002

Message:
Unexpected End-of-File Immediately After Comment Opening

Meaning:
The compiler ran into the end of the file while parsing a block comment

EL0003

Message:
Unexpected End-of-File In Closing of Multi-Line Comment

Meaning:
The compiler was in the process of closing a block comment when it ran into the end of the file

EL0004 [DUPLICATE] [FLAG]

EL0005

Message:
Unexpected End-of-File Immediately After Identifier Opening

Meaning:
The compiler started processing an identifier and ran into the end of the file unexpectedly

EL0006

Message:
Unexpected End-of-File While Lexing Identifier

Meaning:
The compiler ran into the end of the file while still in the process of lexing an identifier

EL0007

Message:
Unexpected End-of-File Immediately After Number Opening

Meaning:
The compiler ran into the end of the file while still in the process of lexing a number

EL0008

Message:
Unexpected End-of-File While Lexing Number

Meaning:
The compiler ran into the end of the file while still in the process of lexing a number

EL0009

Message:
Unexpected End-of-File Immediately After String Opening

Meaning:
The compiler ran into the end of the file while still in the process of lexing a string

EL0010

Message:
Unexpected End-of-File While Lexing String

Meaning:
The compiler ran into the end of the file while still in the process of lexing a string

EP0000

Message:
Expected <token> but got <token>

Meaning:
The parser was expecting a specific token, but a different one was in the spot

EP0001

Message:
Expected identifier, found <token>

Meaning:
The parser was expecting an identifier but found a different token in the spot

EP0002 [DUPLICATE] [FLAG]

EP0003 [FLAG]

Message:
Unexpected end-of-file while parsing block

Meaning:
The compiler ran into the end of the file while parsing a block of statements

EP0004

Message:
Function <name> parameter <param name> must be a valid type, currently <invalid type name>

Meaning:
One of a functions parameters isn’t a valid type

EP0005

Message:
Function <name> must derive valid type, currently <invalid type name>

Meaning:
The provided function doesn’t have a valid return type

EP0006

Message:
Invalid statement used to initialize for loop

Meaning:
The provided for loop initialization statement is not valid

EP0007 [FLAG]

Message:
Invalid statement used in for loop

Meaning:
A provided statement in the body of a for loop is invalid

EP0008

Message:
Namespace Declaration without a Name

Meaning:
A namespace was declared without a valid name afterwards

EP0009

Message:
Expected identifier or string after using but got <token>

Meaning:
An invalid token sequence was provided after the using keyword

EP0010

Message:
Unexpected Token in Expression, Found <token>

Meaning:
When parsing an expression the parser ran into a token that isn’t supported in expressions

EP0011

Message:
Expected Literal in Expression, Found <token>

Meaning:
When parsing an expression the parser expected a literal but found a different token in the spot

ES0000

Message:
Expected variable <name> to be <type> but it’s <type>, maybe cast?

Meaning:
When using a variable in a specific type context, the variable had an incompatible type

ES0001

Message:
Reference to undefined variable <name>

Meaning:
An undefined variable was used somewhere in an expression

ES0002

Message:
Invalid Binary Operation Types, Cannot use <type> With <type>, Consider Casting

Meaning:
Two types used in a binary operation are not compatible with each other

ES0003

Message:
Inline Function Call with Type Hint Doesn’t Match Function Return Type (Hint: <type>, Returns <type>)

Meaning:
A function call used a type hint but the type hint doesn’t match a possible return type of the function

ES0004

Message:
Unexpected Number of Parameters to Function <name>, Expected <value> but got <value>

Meaning:
The number of parameters passed in a function call and the number of parameters in the function definition are different

Example:

// Function Definition (3 Parameters)
fn testing(int a, int b, int c) -> void;

// Function Call (2 Parameters)
testing(10, 20);

ES0005

Message:
Type mismatch passed to function <name> in arg slot <value>, expected <type> but got <type>

Meaning:
One of the parameters passed to a function doesn’t match the expected type based on the function definition

Example:

// Function Definition (bool, int)
fn testing(bool a, int b) -> void;

// Function Call (float, int)
testing(10.0, 10);

ES0006 [FLAG]

Message:
Different types passed to forward declared function <name> in arg slot <value>, previously used <type> but <type> was passed, verify this is correct

Meaning:
Two different calls to the same forward declared function have different types in the same parameter position

ES0007 [FLAG]

Message:
Empty array literal, verify this is correct

Meaning:
There is an empty array literal.

ES0008

Message:
Array literal initialized with type <type> was passed a <type>

Meaning:
An array created with one type contains an element of an incompatible type

ES0009

Message:
Missing type hint for array literal declaration

Meaning:
A required type hint for array literal declarations is missing

ES0010

Message:
Invalid array index type, must be in the integer family, got <type>

Meaning:
Attempted to use a non-integer type to index into an array, this obviously will not work

ES0011

Message:
Expected <literal> to be Parsed as an Integer

Meaning:
The compiler expected a literal to be parsed as an integer but it was not

ES0012

Message:
Expected <literal> to be Parsed as a Float

Meaning:
The compiler expected a literal to be parsed as a float but it was not

ES0013

Message:
Cannot use <type> in the context of <type< (attempted literal upcast)

Meaning:
The type of a literal cannot be used in the context of a different incompatible type

ES0014

Message:
Cannot use <type> in the context of <type> (attempted type conversion)

Meaning:
A type cannot be used in the context of a different incompatible type

ES0015

Message:
Cannot implicitly convert <type> to <type> (would lose fractional data)

Meaning:
The compiler has detected that it needs to change types in order to work in an expression but will not implicitly convert it due to data loss, a cast is required in this situation

ES0016

Message:
Cannot implicity convert <type> to <type>

Meaning:
The compiler has detected that it needs to change types in order to work in an expression but will not implicitly convert it for some reason

ES0017

Message:
Type mismatch in variable declaration <name>, expected <type> but got <type>

Meaning:
A variable was defined with a specific type but the assignment expression resulted in a different type

ES0018

Message:
Type mismatch in variable reassignment <name>, expected <type> but got <type>

Meaning:
A variable was defined with a specific type but the re-assignment expression resulted in a different type

ES0019

Message:
Attempted to reassign variable that does not exist <name>

Meaning:
Really?

ES0020

Message:
Type mismatch in function return, expected <type> but got <type>

Meaning:
The return type of a function was expecting a specific type but got a different incompatible type

ES0021

Message:
Type mismatch in function return, expected <type> but got none

Meaning:
The return type of a function was expected to be a specific type but wasn’t anything

ES0022

Message:
Condition in if statement must be of type Boolean, got <type>

Meaning:
The conditional expression inside of an if statement was expecting to result in a boolean type but instead resulted in a different incompatible type

ES0023

Message:
Condition in while statement must be of type Boolean, got <type>

Meaning:
The conditional expression inside of a while statement was expecting to result in a boolean type but instead resulted in a different incompatible type

ES0024

Message:
Condition in for statement must be of type Boolean, got <type>

Meaning:
The conditional expression inside of a for statement was expected to result in a boolean type but instead resulted in a different incompatible type

ET0000

Message:
Scope <scope_name> not found in path

ET0001

Message:
Symbol <name> already exists in current scope

ET0002

Message:
Symbol <name> exists and is not a scope

ET0003

Message:
Symbol <name> already exists in specified scope

ET0004

Message:
Invalid scope in path <scope_name>

ET0005

Message:
Cannot enter scope <scope_name> because it’s occupied by a non-scope-containing symbol

EN0000

Message:
KSL requires <os_linker> for linking, install it or add it to path

EN0001

Message:
Failed to run linker: <linker_error>

Meaning:
The linker failed to run for some reason unknown to KSL

EN0002

Message:
Linker failed with status code <linker_status><linker_error>

Meaning:
The linker failed to run for some reason unknown to KSL

Standard Library

Standard Library Modules:

IO

Warning

The std.io module is not stable and/or partially implemented.

using std.io;

io.writeln

Parameters (one of): { i64 | f64 | bool | string | int[] }
Returns (one of): { i64 | void }

Example:

io.writeln(10)->int;
io.writeln(10); // ->void equivalent

io.writeln(2.5)->int;
io.writeln(2.5); // ->void equivalent

io.writeln(false)->int;
io.writeln(false); // ->void equivalent

io.writeln("Testing")->int;
io.writeln("Testing"); // ->void equivalent

int[] x = [1, 2, 3, 4, 5];
io.writeln(x)->int;
io.writeln(x); // ->void equivalent

C Impl:

int64_t io__writeln____i64_i64(int64_t a);
void io__writeln____i64_null(int64_t a);

int64_t io__writeln____f64_i64(double a);
void io__writeln____f64_null(double a);

int64_t io__writeln____bool_i64(bool a);
void io__writeln____bool_null(bool a);

int64_t io__writeln____str_i64(generic_array* arr);
void io__writeln____str_null(generic_array* arr);

int64_t io__writeln____i64arr_i64(generic_array* arr);
void io__writeln____i64arr_null(generic_array* arr);

io.write

Parameters: str
Returns: void

Example:

io.write("Testing"); // ->void equivalent

C Impl:

void io__write____str_null(generic_array* arr);

io.getln

Parameters: N|A
Returns: str

Example:

str input = io.getln();

C Impl:

generic_array* io__getln_____str();

FS

Warning

The std.fs module is not stable and/or partially implemented.

using std.fs;

ENV

Warning

The std.env module is not stable and/or partially implemented.

using std.env;

Math

Warning

The std.math module is not stable and/or partially implemented.

using std.math;

math.floor

Parameters (one of): { f64 | f32 }
Returns: i64

Example:

int floored = math.floor(20.5); // becomes 20

C Impl:

int64_t math__floor____f64_i64(double num);
int64_t math__floor____f32_i64(float num)

math.round

Parameters: f64
Returns: i64

Example:

int rounded_1 = math.round(20.5); // becomes 21
int rounded_2 = math.round(20.4); // becomes 20

C Impl:

int64_t math__round____f64_i64(double num);

math.min

Parameters: i64, and i64
Returns: i64

Example:

int smallest = math.min(10, 20); // becomes 10

C Impl:

int64_t math__min____i64_i64_i64(int64_t a, int64_t b);

math.max

Parameters: i64, and i64
Returns: i64

Example:

int largest = math.max(10, 20); // becomes 20

C Impl:

int64_t math__max____i64_i64_i64(int64_t a, int64_t b);

math.clamp

Parameters: { i64 | f64 }, { i64 | f64 }, and { i64, f64 }
Return: { i64 | f64 }

Example:

int clamped_int = math.clamp(100, 10, 20); // Becomes 20
float clamped_float = math.clamp(8.2, 2.5, 4.5); // Becomes 4.5

C Impl:

int64_t math__clamp____i64_i64_i64_i64(int64_t val, int64_t min, int64_t max);
double math__clamp____f64_f64_f64_f64(double val, double min, double max)

math.sqrt

Parameters: i64
Returns: f64

Example:

float sqrted = math.sqrt(100); // Becomes 10.0

C Impl:

double math__sqrt____i64_f64(int64_t num);

Random

Warning

The std.random module is not stable and/or partially implemented.

using std.random;

random.seed

Parameters: { N|A | i64 }
Returns: void

Example:

random.seed(); // Pick a seed automatically based on system time
random.seed(246); // Pick a custom seed

C Impl:

void random__seed_____null();
void random__seed____i64_null(int64_t ss);

random.rand

Parameters: N|A
Returns: { i64 | f64 }

Example:

int random_int = random.rand(); // KSL will detect int context and select that version for you
float random_float = random.rand(); // KSL will detect float context and select that version for you

C Impl:

int64_t random__rand_____i64();
double random__rand_____f64();

random.randint

Parameters: i64, and i64
Returns: i64

Example:

int random_int_in_range = random.randint(10, 20); // Random integer between 10 and 20

C Impl:

int64_t random__randint____i64_i64_i64(int64_t min, int64_t max);

Time

Warning

The std.time module is not stable and/or partially implemented.

using std.time;

time.get_ns

Parameters: N|A
Returns: i64

Example:

int nanoseconds = time.get_ns();

C Impl:

int64_t time__get_ns_____i64();

The KSL Standard

A standard for KSL features and language implementation. In the future all new features and language components will need to be submitted for review and added to the standard before being included in the compiler.

The using Keyword

04 / 09 / 25

Objective

The objective of the using keyword is to enable multi-file projects to be compiled without manually defining all included files from the command line, it also serves as a way to disclose which existing object files should be linked after codegen.

Dictionary

using - The using keyword

Examples

The using keyword can change it’s meaning depending on the syntax that follows. Following a using keyword with a string will tell the compiler to include another source file at the path provided, relative paths are preferred, example:

/* file: main.k */
using "./api/weather.k";

This syntax will include the source of ./api/weather.k in the compiler. It’s worth noting that each source file is compiled individually using a shared symbol table. The resulting object files for each individual source file are then linked together to produce the final executable.

The second way to use the using keyword is with identifiers. This syntax will tell the compiler to link an object file following the path of the identifier. For example:

/* file: main.k */
using std.io;

This example will tell the compiler to link std/io.o with the resulting object files at the end of compilation. By default the compiler will search it’s own working directory for object files to link, however if it does not find any then it will switch to the project working directory. Periods in the syntax will denote folders, for example:

/* file: main.k */
using os.api.platform;

This example will attempt to link os/api/platform.o with your KSL code.

Conditionals

Proposal unfinished.

The while Keyword

Proposal unfinished.

The for Keyword

Proposal unfinished.

Templates

04 / 09 / 25

Objective

The objective of templates is to achieve an effect similar to polymorphism. For example, defining baseline logic (the template) without static types, then during compile time a clone of the template with static types is generated for each unique call.

Dictionary

template - A keyword denoting the definition of a template function.
$[a-zA-Z] - Syntax denoting the use of an unknown type, single letters only.

Examples

In the example below you can see a template add function, it takes two parameters of unknown types. The parameter names are x and y with types $A and $B, respectively. Templates must return/derive a single type.

template add($A x, $B y) -> int {
	return int'(x + y);
}

If this template was called here is an example generated function:

fn add(float x, int y) -> int {
	return int'(x + y);
}

add(10.0, 5);

Notice how in this example the template call (add(10.0, 5)) uses a float and an integer? This will dictate how to template generates the function. A template can also be used for multiple function calls with multiple dynamic types, ex:

fn add(float x, int y) -> int {
	return int'(x + y);
}

fn add(float x, float y) -> int {
	return int'(x + y);
}

Templates can also reference the dynamic types multiple times in it’s function body as long as the result/derivitive of the function remains constant, ex:

template multiply($A x) -> int {
	$A amount = 2;
	$A result = x * amount;
	return int'result;
}

The fn, ->, & return Keywords

04 / 09 / 25

Objective

The objectives of the fn, ->, and return keywords are to add rich function support to KSL.

Dictionary

fn - Keyword used to denote the start of a function.
-> - Keyword used to denote the return type/derivative of a function.
return - Keyword used to return a value from a function.

Examples

The fn keyword followed by a single identifier and parenthesis defines a basic function. Functions also need a defined return type, which can be set using the derive keyword (->.) Once you’re done in your function body you can include a return keyword to return a value. It’s expected that the returned value matches the type defined after derive. Technically speaking, the return keyword is optional, KSL will insert one for you automatically if it doesn’t see one.

fn main(int a) -> float {
	return 10.0;
}

In the example above we can see a function defined, this function has the name main, it takes one integer parameter called a, and it returns a float. This function has the return statement defined in it’s function body.

Functions can have multiple parameters separated by a comma. They cannot have multiple return types though.

fn main(int a, int b) -> int {
	return a + b;
}

Casting

Proposal unfinished.

Miscillanious

Quick Syntax Guide

Warning

If you do not have a background in programming it is highly recommended to return to the normal documentation, this is a syntax guide for users who already understand programming fundamentals.

Valid Types

Int Types:

  • i8
  • i16
  • i32
  • i64 (has int alias)

Float Types:

  • f32
  • f64 (has float alias)

Other Types:

  • str
  • bool
  • void

Creating Functions

fn = Function Keyword
-> = Declare Return Type

The entry function will need to be called main.
Function bodies are denoted by the braces { and }.

Example:

fn main() -> void {}

Functions should return with the return keyword, if you don’t include one KSL will insert one for you automatically (with a default value) for any function that returns a primitive type.

Creating Variables

= = Assignment Operator

Variable names must follow: [a-zA-Z][a-zA-Z0-9_]+.
Variable names cannot be the same as a type name.

Example:

int my_int = 20;
f32 my_float = 20.5;

Creating Complex Types (Arrays & Structs)

Create structs with the struct keyword and braces ({ and }), struct fields are <type> <field_name> and comma separated. Examples:

struct Vector2 { int x, int y }
struct Vector3 {
	int x,
	int y,
	int z
}

Arrays can be created by adding [] to any type when defining a new variable:

int[] my_int_arr = [];
Vector2[] my_custom_struct_arr = [];
bool[] my_bool_arr = [];

n’dimensional arrays can be created by adding more [] to the type definition:

int[][] my_2d_int_arr = [[]];
Vector2[][] my_2d_Vector2_arr = [[]];
bool[][] my_2d_bool_arr = [[]];

Using Other Files & Modules

The using keyword makes it easy to include other source files and link modules against your project.

The using keyword followed by a string will search for other source files:

using "my_second_file.k";
using "my_third_file.k";

The using keyword followed by a qualified identifier will search for object files to link your project against:

using std.io;
using std.fs;

Note

Standard Library reference can be found here

Getting and Setting Things

In KSL pretty much everything is mutable and you can do some pretty cool things when attempting to get or set values, for example:

using std.io;

struct Vec2 { int x, int y }

Vec2[][] my_2d_Vec2_arr = [[ Vec2 { x: 10, y: 20 } ]];
my_2d_Vec2_arr[0][0].y++; // Increment the y value in the 2d struct array
my_2d_Vec2_arr[0][0].y = 40; // Overwrite the y value in the 2d struct array

io.writeln(my_2d_Vec_arr[0][0].y); // Get the y value in the 2d struct array

Loops

The two loops in KSL are the for and while loops.

While example:

while (true) {
	// Do something
}

For example:

for (int i = 0; i < 100; i++) {
	// Do something
}

Conditionals

if, else if, and else are all supported in KSL, see example:

if (<condition_1>) {
	// Do something (1)
} else if (<condition_2>) {
	// Do something (2)
} else {
	// Do something (3)
}

You can chain as many else if statements as you want.

Casting

Casting in KSL may be confusing for newcomers. All you need is the type name, an apostrophe, and the primary part of your expression you want to cast. Example:

float'20; // will cast this int to a float

int x = 2000;
bool'x; // will cast the int variable to a bool

/*
The next example will add 20 + 30 then cast the result
to a float. Notice the parenthesis? Casting will only
work for the symbol directly after the apostrophe, so
in order to cast the *result* of the expression they
must be wrapped in parenthesis.
*/
float y = float'(20 + 30);

Casting will not work for complex types like arrays or structs.

Namespaces

Namespaces in KSL are typically defined on a per-file basis.
They do not use braces.

Example:

namespace my_namespace;

Functions defined after the namespace my_namespace can then be accessed by other parts of the program at my_namespace.my_function().

Function Attributes

Function attributes modify the behavior of functions in KSL, for example, the @inline attribute will tell the compiler to inline the following function if possible.

Example:

@inline
fn my_func() -> void {};

Note

Function Attribute reference can be found here

Compiler Internals

This series of documentation pages is entierly dedicated to explaining how the compiler works internally. If you’re not actively working on the KSL compiler or building a KSL module then you probably don’t want to worry about anything here.

Compiling the Compiler

Warning

Nearly all the information on this page is now outdated as the compiler is being upgraded from Rust to C++ before being able to self host.

On Windows

Requirements:

  • KSL Source Code
  • Rust 1.91^: Download Rust
  • LLVM 18.1.x: Download LLVM
  • The libxml2 requirement for LLVM seems to be missing on Windows, luckily you can download it with vcpkg install libxml2:x64-windows. Once it’s downloaded, rename the file from libxml2 to libxml2s and put it in your llvm/lib folder.
    • If you need vcpkg click here.

You’ll want to make sure Rust is installed fully. I’ve found that installing it globally just takes care of a lot of potential issues. Make sure the LLVM/bin folder is added to your path as well. On top of that, you’ll want to make a new system variable:

VariableValue
LLVM_SYS_180_PREFIX<your_llvm_root_directory>

Once you have Rust and LLVM set up you shouldn’t really need anything else! Just navigate to the ksl source code root directory and run cargo build or cargo build --release.

It sounds pretty simple but some of the config stuff can take a while to figure out, I’d say this process could take anywhere between 30 minutes and few hours, depending on your technical skills. Don’t get discouraged if it takes a while.

On Linux

Note

Documentation help wanted! The Linux build instructions are not finalized and may depend on distro, any contributions would be great!

Requirements:

  • KSL Source Code
  • Rust 1.91^: Download Rust
  • LLVM 18.1.x: Download LLVM

llvm package on arch and fedora

On MacOS

Note

Documentation help wanted! The MacOS build instructions are not finalized. Any contributions toward this section would be super helpful!

Requirements:

  • KSL Source Code
  • Rust 1.91^: Download Rust
  • LLVM 18.1.x: Download LLVM

Compiling KSL-STDLIB

To compile the standard library:
Requirements:

  • KSL Source Code
  • Clang (for windows)
  • GCC (for linux)

Steps:

  1. Navigate to the /std/ folder
  2. Run the build.cmd (windows) or build.sh (linux)
  • (Optionally) run build.cmd debug to enable debug messages in runtime libraries (build.sh debug for linux)

Name Mangling Convention

Warning

The name mangling convention has changed and is slightly more detailed in recent versions of the compiler. Documentation will be changed to reflect this in the near future.

The KSL name mangling convention is designed to prevent any kind of type confusion. That includes the return type.

This also means that you can provide function overloading in KSL modules and external object files. The main reason for the return type being included is so that there can be functions that serve the same purpose but the KSL return type predictor doesn’t get too broken.

The general rules for name mangling in KSL are as follows:

  1. Periods in identifiers are translated to two underscores.
  2. Space between the function name and types are four underscores.
  3. The rest of the name will have all parameter types and the return type all separated by one underscore.

You’ll also see that the type keyword int turns into i64. This is because, as explained in variables, int and float are just aliases for i64 and f64 respectively. Reasoning for this choice are explained there.

Functions

fn main(i64 a, i64 b) -> i64 {}
//  ^    ^      ^         ^
//  |    |      |         |
//  |    |      |         +---- Return Type
//  |    |      +-------------- Parameter Type
//  |    +--------------------- Parameter Type
//  +-------------------------- Function Name

<namespace>__<function_name>____<parameter_types>_<return_type>

Where:

  • namespace is the current namespace of the function
  • function_name is the name of the function
  • parameter_types are the type identifiers of all parameters concatenated with a _
  • return_type is the function return type

So:

fn main(i64 a, i64 b) -> i64  // Becomes: __main____i64_i64_i64

namespace api;
fn add(f64 a, f64 b) -> f64   // Becomes: api__add____f64_f64_f64
fn getFloat() -> f64          // Becomes: api__getFloat_____f64

namespace ipa;
fn not(bool val) -> bool      // Becomes: ipa__not____bool_bool
fn testing() -> void          // Becomes: ipa__testing_____null

Methods

Warning

As of writing, the official conventions for methods have not been finalized.

    i64[] a = [10, 20];
    a.push(30);
//  ^  ^   ^
//  |  |   |
//  |  |   +----------- Parameter
//  |  +--------------- Method Name
//  +------------------ Variable Name

t<variable_type>_method_<method_name>____<parameter_types>

Where:

  • variable_type is the type (int, float, arr) of the variable being operated on
  • method_name is the name of the method
  • parameter_types are the type identifiers of all parameters concatenated with a _

Since methods do not have a return type and are expected to always mutate a variable directly, there is no need for there to be a return type in the mangled name.

So:

str a = "Hello, ";
a.join("World!");   // Becomes: tstr_method_join____str

i64[] b = [10, 20];
b.push(30);         // Becomes: tarr_method_push____i64

Variables

Warning

As of writing, the official conventions for variables have not been decided.

Compiler Flags

Full list of compiler flags and overview of meaning, full list with more details is shown below.

building:
  --output <name>       -o <name>       Set Name of Output Binary
  --optimize            -op             Run Optimization Passes
  --write               -wr             Write Code to Object File
  --link                -lk             Link and Produce an Executable
  --pipeline            -owl            Optimize, Write, and Link
miscillanious:
  --help                -h              Display This List of Arguments
  --dump                -d              Display Version and Environment Information
  --evasion             -ev             Prevent Warnings Related to KSL Code Safety
  --suppress-warns      -sw             Prevent Warnings from Displaying
debugging:
  --tokens              -tk             Display Token Stream(s)
  --llvm-ir             -ir             Display LLVM IR After Codegen
  --assembly            -as             Display Native Assembly After Codegen
  --type-trace          -tt             Track Custom Type Creations (Like Structs)
  --symbol-trace        -st             Track Symbol Lookups as They Occur
  --codegen-trace       -gt             Trace Codegen During AST Walk
  --compiler-trace      -ct             Trace Compiler Operations
  --ast <a,p,s>         -at <a,p,s>     Display Abstract Syntax Tree
         a                                  All Stages [Default]
         p                                  Parsing Stage
         s                                  Semantics Stage
  --symbols <a,p,s,c>   -sm <a,p,s,c>   Display Symbol Table
         a                                  All Stages [Default]
         p                                  Parsing Stage
         s                                  Semantics Stage
         c                                  Code Generation Stage

Building

--output <name> (shorthand -o <name>): Set the name of the output binary, by default this value is ksl_child (with or without .exe depending on your OS.)

--optimize (shorthand -op): Run optimization passes on the generated code before writing it to a binary or object file.

--write (shorthand -wr): Write the resulting code to an object file, by default this should be enabled, it’s mostly for development currently.

--link (shorthand -lk): Link the resulting object file(s) and product an executable binary, by default this should be enabled, it’s mostly for development currently.

--pipeline (shorthand -owl, stands for Optimize, Write, Link): this tells the compiler to optimize generated code, write it to an object file(s), and link the object file(s) to a binary in this order.

Miscillanious

--help (shorthand -h): Displays the list of flags and overview shown above.

--dump (shorthand -d): Displays environment information to the console, includes version, branch, and operating system; useful for debugging.

--suppress-warns (shorthand -sw): Suppresses all warnings raised by the compiler, this will prevent them from being displayed during and after the compiler runs.

Debugging

--tokens (shorthand -tk): Displays the token stream to the console for KSL source code files after they’ve been processed by the lexer.

--llvm-ir (shorthand -ir): Displays the LLVM IR to the console for KSL binary files after they’ve been processed by the semantics and codegen stages. Combining this with the -op flag will result in displaying both unoptimized and optimized IR to the console.

--assembly (shorthand -as): Displays the native assembly to the console for KSL binary files after they’ve been processed by the codegen stage.

--type-trace (shorthand -tt): Track custom type creations, like structs, as they’re added to the type registration system.

--symbol-trace (shorthand -st): Track symbol lookups as they occur throughout the parser, semantic analysis, and codegen stages.

--codegen-trace (shorthand -gt): Trace the code generation stage as it travels the Abstract Syntax Tree.

--compiler-trace (shorthand -ct): Trace the abstract compiler operations as they occur (e.g. “Starting Semantic Analysis”, “Semantic Analysis Finished”, “Starting Code Generation”, etc.)

--ast <stages> (shorthand -at <stages>): Displays the Abstract Syntax Tree to the console after specified stages. If <stages> is left blank it will default to all stages. Valid <stages> options are:

  • a: All stages (Default)
  • p: Parsing stage
  • s: Semantic analysis stage

--symbols <stages> (shorthand -sm <stages>): Displays the Symbol Table to the console after specified stages. If <stages> is left blank it will default to all stages. Valid <stages> options are:

  • a: All stages (Default)
  • p: Parsing stage
  • s: Semantic analysis stage
  • c: Code generation stage