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:
| Type | Identifier | Size (Bits) |
|---|---|---|
| int8 | i8 | 8 |
| int16 | i16 | 16 |
| int32 | i32 | 32 |
| int64 | i64 || int | 64 |
| uint8 | u8 | 8 |
| uint16 | u16 | 16 |
| uint32 | u32 | 32 |
| uint64 | u64 || uint | 64 |
| float32 | f32 | 32 |
| float | f64 || float | 64 |
| char | char | 8 |
| bool | bool | 1 |
| void | void | N|A |
| enum | enum | ?? |
| struct | struct | ?? |
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
structkeyword 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 ofmy_structstructs.)
Expressions
Operations (Binary and Unary)
Higher precedences are executed first.
Supported Binary Operations (and Precedence):
| Operation | Symbol | Precedence |
|---|---|---|
| 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:
| Operation | Symbol |
|---|---|
| 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
voidfor a function that returns anintwill 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.iowould link thestd/io.oobject 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_mangleattribute. 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:
i8i16i32i64(hasintalias)
Float Types:
f32f64(hasfloatalias)
Other Types:
strboolvoid
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
libxml2requirement for LLVM seems to be missing on Windows, luckily you can download it withvcpkg install libxml2:x64-windows. Once it’s downloaded, rename the file fromlibxml2tolibxml2sand put it in yourllvm/libfolder.- If you need
vcpkgclick here.
- If you need
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:
| Variable | Value |
|---|---|
| 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:
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:
Compiling KSL-STDLIB
To compile the standard library:
Requirements:
- KSL Source Code
- Clang (for windows)
- GCC (for linux)
Steps:
- Navigate to the
/std/folder - Run the
build.cmd(windows) orbuild.sh(linux)
- (Optionally) run
build.cmd debugto enable debug messages in runtime libraries (build.sh debugfor 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:
- Periods in identifiers are translated to two underscores.
- Space between the function name and types are four underscores.
- 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:
namespaceis the current namespace of the functionfunction_nameis the name of the functionparameter_typesare the type identifiers of all parameters concatenated with a_return_typeis 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_typeis the type (int, float, arr) of the variable being operated onmethod_nameis the name of the methodparameter_typesare 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 stages: 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 stages: Semantic analysis stagec: Code generation stage