Statements and Expressions

Variables

There are three keywords to define variables: var, let and const. var and let get assigned at runtime and const gets evaluated at compile time. let may only be assigned during its declaration, otherwise the value is supposed to be a constant.

Variables in global scope might additionally be specified with export to make other modules able to refer to them.

Additionally, the #extern pragma may be applied. Normally variables are prefixed by the module name in the form of module::name in the resulting binary. If extern is specified, only the name of the variable is used as the name inside the binary.

// Let must be assigned a value
let a = 20
// a = 30 // This is illegal because
// let creates a constant

// You may assign multiple values at once
var b, c = 10, 30

// You can define the type
// of a variable like this
var d: int64 = 4

// This variable is a compile time constant
const MY_CONST = 10 * 20
//               ~~~~~~~
//      This runs at compile time

// This variable is exported
export var special = 20

A variable that is called _ may be defined multiple times. Accessing it is not recommended but it is going to return the last written value. This is useful for discarding certain arguments:

def a_lot_of_stuff -> int, int, int {
    return 10, 20, 30
}

let a, _, _ = a_lot_of_stuff()

Cast expression

You can convert between types using the cast expression. It looks like this:

let a: double = 20.5
let b = a !int

Be careful, sometimes there might be data loss!

If you want to convert the bit pattern, then you have to use pointers. Unlike in C, an operation like this is not undefined behavior, so you are safe to do it that way.

If statements

The if statements in Princess work just like in any other programming language. You do not need parenthesis around the condition. However, every if statement needs to be followed by {}.

var a = 20

if a > 15 {
    print("A is bigger than 15\n")
} else if a > 10 {
    print("A is bigger than 10\n")
} else {
    print("A is something else!\n")
}

Additionally there is an if expression which works like this:

let a = 20 if 10 == 20 else 30

It works basically like the ternary operator in C just with a more sensible syntax.

Static if

Static ifs work just like normal if statements, except that the expression gets evaluated at compile time. This means that the body of the if statement is getting substituted in the constant evaluation stage. Only the parser needs to be happy about the contents, and the branches that are cut out don’t get typechecked. An #if statement doesn’t create a new scope but instead everything inside is of the parent scope. That means variables defined inside of it are visible from outside.

#if defined WIN32 {
    def my_code() -> int { return 1 }
} else {
    def my_code() -> int { return 2 }
}

Switch statement

The switch statement currently only works for integral types and enums. Unlike in C, there is no fallthrough. You can specify multiple values in a single case, or use ranges to match multiple values. Case labels need to be compile time constants at the moment. This will change as soon as pattern matching becomes a thing.

There might optionally be one case label without arguments, which is called when none of the other cases match.

var i = 20
switch i {
    case 10, 20; print("Good number")
    case 20..40; print("Bad number")
    case; print("I don't know about this")
}

Loops

In Princess there are three kinds of loops. while, loop and for. Additionally you may exit a looop with break or go to the next iteration with continue.

var i = 0
loop {
    if i == 2 { continue }
    print(i, " ")
    i += 1
    if i == 10 { break }
}
print("\n")

A loop is basically analog to while true. It executes forever until a break is used to exit the loop.

A while loop executes while a condition holds true:

var i = 0
while i < 10 {
    print(i, " ")
    i += 1
}
print("\n")

A for loop might go over a range, or the values in a generator.

for var i in 0..10 {
    print(i, " ")
}
print("\n")

Assert

Assert is basically a way to make sure that a condition is true, and abort in the case of failure. This is to make sure that the program is in a safe state, and not to return errors to the user of your program. When an assertion is failing, it is printing the message specified and a stack trace. Assertions behave a bit differently inside of tests, see the section on this for more information.

// This simply fails (unreachable code)
assert
// This fails if the condition is met
assert 1 + 5 == 6
// This also prints a message
assert 10 == 20, "This fails"

Functions

Functions may be defined at top level with the keyword def like this: The arguments are specified in parenthesis after the function name and are of the form “name”: “type”. The types need to be defined here and can’t be inferred like for variables.

The return type is optionally defined by using an arrow followed by one or more return types.

def add(a: int, b: int) -> int {
    return a + b
}

def foo -> int {
    return 10
}

Functions may be defined in any order, so this is perfectly valid:

hello

def hello {
    print("Hello World!\n")
}

Note however that this does not apply to compile time code. Inside of a const or a function that is called at compile time, the function may only refer to code that has already been defined. This might not be a problem if you import a module as everything in there has been defined already, but watch out if using compile time code in the same module. This restriction is subject to change in the future.

You might mark a function with implicit. When trying to convert from a type A into a type B it is going to search for an implicit function that is imported into the current scope.

type A = struct { s: Str }

implicit def to_string(a: A) -> Str {
    return a.s
}

let a = [ s = "Hello" ] !A
let b: Str = a // implicit call here

Just like variables, #extern may be used on a function, this does exactly the same thing.

Functions may optionally be defined without a body, this might be useful if you are referring to a function that is provided by an external library.

A windows only functionality is the #dllimport and #dllexport flags which do import a function from a DLL or export it when creating a DLL.

Additionally a function may have a variable amount of parameters. For this you either define the last argument as ... or together with a type like this: a: int.... The first form is only useful when calling to C as there is no way to read out the arguments which are supplied in that way. The second form passes an array of type [int] in this case. The function may also be called with an array as the last argument, which does pass the array as varargs.

Warning

The arguments array gets cleaned up by the calling function after calling your function, so you need to copy it if you plan to store it in a global variable.

def some_function(...) {}

some_function(10, "string", -2.5)

def sum(args: int...) -> int {
    var sum = 0
    for var arg in args {
        sum += arg
    }
    return sum
}

Functions may be overloaded, that is they might have the same name but accept different arguments:

def add(a: int, b: int) -> int {
    return a + b
}

def add(a: double, b: double) -> double {
    return a + b
}

add(10, 5) // This calls the first function
add(10.5, 5.2) // second function

An overloaded function may be accessed by using the parameters directly on the identifier:

let a = *add::(double, double)
let b = *add::(int, int)

Polymorphic Functions

A function may become polymorphic if it gets specialized with a specific type at compile time. Currently there are a few ways to create polymorphic functions. One is by accepting a specific type like so:

def my_function(type A) -> A {
    return [] !A
}

This essentially means, that a type is provided to the function at compile time. So multiple versions of this function get created if different types get specified here. A is only valid after its creation, so you can not refer to the type A from an argument that is defined prior to the type argument.

There is also a way to accept arguments of a type directly like so:

def my_function(a: type T) -> T

This will take the type that is supplied as the argument itself. You can also provide types that are more complex:

def my_function(a: type [T]) -> T

This essentially means that the function only accepts arrays as a parameter.

The other way of creating polymorphic functions is by using interfaces. More on that see the section about Interfaces.

Operator Overloading

A function may refer to an overloaded operator if it is using that operator as a name. These functions get converted to have a different name in the final output and can be referenced as that.

Neither pointer arithmetic nor the and, or or the . operator can be overloaded. Additionally, the reference * and dereference @ operators may not be overloaded either. This might change in the future.

:: also looks like an operator but it is actually part of an identifier.

Unary Additon

+

def __pos__

Binary Additon

+

def __add__

Unary Subtraction

-

def __neg__

Binary Subtraction

-

def __sub__

Multiplication

*

def __mul__

Division

/

def __div__

Modulo

%

def __mod__

Right Shift

>>

def __rshift__

Left Shift

<<

def __lshift__

Bitwise and

&

def __and__

Bitwise or

|

def __or__

Bitwise xor

^

def __xor__

Bitwise negation

~

def __invert__

Less than

<

def __lt__

Greater than

>

def __gt__

Less or equal

<=

def __le__

Bigger or equal

>=

def __ge__

Equal

==

def __eq__

Not equal

!=

def __ne__

Compound assignment

-=

def __isub__

Compound assignment

+=

def __iadd__

Compound assignment

*=

def __imul__

Compound assignment

/=

def __idiv__

Compound assignment

%=

def __imod__

Compound assignment

>>=

def __irshift__

Compound assignment

<<=

def __ilshift__

Compound assignment

&=

def __iand__

Compound assignment

|=

def __ior__

Compound assignment

^=

def __ixor__

Tests

A function may be marked with #test, this renames the function by prepending __test:: to it and adds an extra context parameter called env. The built in testrunner can compile a file with test functions in it and it is basically going to run a separate process to call these functions. This means that a segmentation fault or similar is not going to bring down the entire test runner.

The env parameter essentially looks like this and is defined in runtime.pr:

export type TestEnvironment = struct {
    out: def [] -> &string
    err: def [] -> &string
    assertion_handler: def [bool, *char] -> []
}

Out and err return the captured standard output and standard error, so that the test can make assumptions based on these. Calling any of these functions will reset the buffer, which means that when calling it again it is not going to return text that has been printed prior to the first call.

def #test test_random_stuff {
    assert 10 == 10

    print("Hello World")

    assert env.out() == "Hello World"
}

assert statements will evaluate and call env.assertion_handler instead of aborting the program outright.

Generators

Generators are basically coroutines that may return multiple values. Any function can become a generator by using a yield statement in its body. The return type of the function is going to be runtime::Generator(T) with T being the specified return value of the function.

Generators can be used manually by calling generator.next() which returns an Optional(T). When this Optional is empty, the Generator was done processing.

Alternatively you may use a for loop to iterate over a generator.

def generator -> int {
    yield 1
    yield 2
    return 3 // A return stops the generator
             // and returns a last value
}

for var i in generator {
    print(i, " ")
}
print("\n")

You may also use yield from inside a generator to chain generators together, this is basically equivalent to using a for loop and yielding every value from the generator.

Closures

Functions defined inside of other functions are basically closures. They have the type [A, B] -> [C, D].

A closure has access to the variables of the outer function but only as copies. It is possible however to refer to the addresses of variables outside of the closure. You can use this to modify variables from outside of the closure.

Warning

Do note however that the lifetime of these variables is not extended. When the calling functon returns, these variables are gone.

def main {
    var a = 5
    var b = 20
    def closure {
        assert b == 20
        let pa = *a
        assert @pa == 10
    }
    b = 30
    a = 10

    closure()
}
main

Function calls

Function calls in Princess are the function name followed by an open parenthesis and a closing parenthesis. Functions with zero arguments may be called without the parenthesis. This means that if you want to take a reference of a function, you need to use the address of * operator.

A function call might optionally use named arguments at the end of the argument list. These might be mixed with normal function calls as needed. The order of the named arguments is not fixed, you may call them in any order.

def my_function(a: int, b: double, option: bool) {}

my_function(10, 1.5, option = false)

Constructors and Destructors

There are two extra magic methods in Princess to support RAII and valid copying. Both of these functions must be marked export

The first is the copy constructor. This gets called whenver a value is copied. It receives a pointer to the new object as the first argument and a pointer to your object as the second argument. If a copy constructor is defined, the object’s data is not getting cloned, you have to do that yourself!

The second special function is the destructor. It gets called whenver your object gets out of scope. You can use it to clean up resources. It gets a pointer to your object as the only argument.

type MyStruct {
    a: *int
}

def make_my_struct(a: int) {
    let ptr = allocate(int)
    @ptr = a
    return [ ptr ] !MyStruct
} -> MyStruct

export def construct(
    copy: *MyStruct, this: *MyStruct) {

    copy.a = allocate(int)
    @copy.a = @this.a
}

export def destruct(this: *MyStruct) {
    free(this.ptr)
}

Defer

A defer statement is essentially run at the end of a function. It works similar to Go, where the defers are added to a list and worked through in reverse order in which they were encountered.

This can be useful to make sure that a resource is correctly freed, without having to do such at the end of the function. This way the initialization and destruction can happen in the exact same place.

def read_file {
    let fp = open("My file.txt", "r")
    defer close(fp)

    print(read_all(fp))
    // The defer happens here
}
read_file

Sizeof and Alignof

The two keywords size_of and align_of return the size of a type and the alignment of a type respectively. These might be removed in the future as it is possible to get both by using reflection in the form of type.size and type.align.

type S = struct {
    a: int
    b: int
}

assert (size_of S) == 8
assert S.size == 8

assert (align_of S) == 4
assert S.align == 4

Typeof

The type_of keyword returns the type of an expression. This can be useful for generic programming.

assert (type_of 10 + 20) == int

Defined

Use defined to check if an identifier exists. You can use this together with typed function identifiers to check if a function for a specific type exists.

It’s also useful to check for windows or linux as the identifiers for each are only defined on the respective platforms.

#if defined WIN32 {
    #if defined special_function::(int, int) {
        special_function(10, 20)
    }
}