Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Language Features

An overview of the more unique language features of Penne:

Scoped goto statements

In Penne, goto is a local forward-only jump. This is achieved by giving labels a reverse scope: similar to how variables cannot be referenced before they are declared, labels cannot be jumped to after they are declared.

fn foo() -> i32
{
    var x = 0;
    { // The scope of the label `end` ends here.
        goto end;
        x = 10; // This line is not executed.
        end: // The scope of the label `end` starts here.
    }
    x = x + 1;
    return: x
}

Like variables, label scopes extend into inner blocks. Thus, a goto statement can jump out any number of nested blocks.

fn foo()
{
    var i = 1;
    {
        var x = 10;
        {
            var y = 20;
            i = 20; // Valid because the variable `i` is in scope here.
            goto end;  // Valid because the label `end` is in scope here.
        }
        i = x; // This line is not executed.
    }
    end:
}

In order to keep goto statements sound and easy to understand, a goto statement cannot jump into an inner block.

fn foo()
{
    goto inner; // Invalid, the label `inner` is not in scope here.
    var i = 1;
    {
        var x = 10;
        { // The scope of the label `inner` ends here.
            inner: // The scope of the label `inner` starts here.
            var y = 20;
            i = y;
        }
        i = x;
    }
}

This compiler tells you that it cannot find the label in scope.

[E400] Error: Undefined label
   ╭─[tests/samples/invalid/jump_to_inner.pn:3:12]
   
 3 │     goto inner; // Invalid, the label `inner` is not in scope here.
   ·          ──┬──
   ·            ╰──── Reference to undefined label 'inner'.
───╯

Similarly, labels that have at least one in-bound goto statement automatically prune the scope of variables declared higher up in the same block. This prevents goto statements from jumping over the declaration of a variable into code that references that variable.

// ...

fn overlapping(input: i32) -> i32
{
    var result = 200;
    if input == 0
        goto return;
    if input == 1
        goto next;
    var a = 20;
    result = result + a;
    if input == 2
        goto return;
    next:
    var b = 10;
    result = result + a + b;
    return: result
}

In input were to equal 1, the declaration of the variable a would be skipped, which would leave the expression result + a + b undefined. The compiler refuses the compile this code.

[E482] Error: Variable declaration may be skipped
    ╭─[tests/samples/invalid/conditional_declaration.pn:32:21]
    
 25 │        goto next;
    ·        ──┬─
    ·          ╰─── A jump from this `goto` statement to 'next'...
 26 │     var a = 20;
    ·         
    ·         ╰── ...may skip the declaration of the variable 'a'.
    ·
 30 │     next:
    ·     ──┬──
    ·       ╰──── After this label, the existence of 'a' is dubious.
    ·
 32 │     result = result + a + b;
    ·                       
    ·                       ╰── The variable is referenced here.
────╯

Scoped loop statements

The only way to jump back is with the loop statement.

fn foo() -> i32
{
    var x = 0;

    {
        x = x + 1;
        loop;
    }

    return: x // This line is never reached.
}

In order to keep backward jumps isolated and easy to find, loop can only appears as the last statement in a block.

Views

Function arguments other than pointers (see below), primitives and words are passed as a view. For arrays this means an array view (or “slice”) is created and passed into the function. Array views remember the length of their array, which can be accessed with the length operation |x|.

fn foo()
{
    var data: [4]i32 = [1, 2, 3, 4];
    var total = sum(data);
}

fn sum(x: []i32) -> i32
{
    var total = 0;
    var i = 0;
    {
        if i == |x|
            goto return;
        total = total + x[i];
        i = i + 1;
        loop;
    }
    return: total
}

Reference pointers

Views allow you to pass a large value by reference, but they only give immutable access. For mutable access, a pointer is needed. They can be created by taking the address of a value. Unlike in most other languages, reference pointers in Penne automatically dereference to their base type, which is any type that isn’t a reference pointer.

    var x: i32 = 17;
    var a: &i32 = &x;
    var y: i32 = a;
    a = 30;
    // Now x == 30 and y == 17.

To change which value a reference pointer points to, you need to explicitly modify the address.

    var x: i32 = 17;
    var y: i32 = 30;
    var z: i32 = 88;
    var a: &i32 = &x;
    &a = &y;
    // Now a points to y instead of x.
    var b: &i32 = &z;
    &a = &b;
    // Now a and b both point to z.

Reference pointers allow a function to modify its arguments, but require the caller to explicitly pass in an address.

fn foo()
{
    var data: [4]i32 = [1, 2, 3, 4];
    set_to_zero(&data);
}

fn set_to_zero(x: &[]i32)
{
    var i = 0;
    {
        if i == |x|
            goto end;
        x[i] = 0;
        i = i + 1;
        loop;
    }
    end:
}

Structs and words

Like arrays, structural types declared with the struct keyword are implicitly passed as a view and cannot be used as the return value of a function. Fixed size structures, declared with word8, word16, word32, word64 or word128, are passed by value.

Imports

The import keyword is used to import all function signatures, structures and constants marked pub from a source file into the destination file. Imports are themselves not public and hence are not re-imported.

Interoperability with C

Functions marked extern use the C ABI, which means it is possible (though not necessarily safe) to call them from C code compiled by LLVM. Conversely, declaring a function header such as

extern fn foo(buffer: []u8, length: usize);

allows you to call a C function from Penne code. Interacting with other programming languages that utilize or support the C ABI, such as C++, Rust, Zig or WebAssembly, is also possible.

Only array views, pointers and the primitive types i8, i16, i32, i64, u8, u16, u32, u64 and usize are allowed in the signature of an extern function. Array views in extern functions correspond to (const) pointers in C, do not have a length (|x|) and must not be null. In a future version of Penne, pointers will also be assumed to be non-null and an “optional” type must be used to mark nullable pointers.

Structures and constants can also be declared extern, but as of v0.3.0 this has no effect.