Home Docs Blog Repo

Language reference

Last updated

Caracal is an imperative, compiled language with a focus on simple and enjoyable syntax. It is designed to be statically typed while still feeling ergonomic through type inference.

Note Everything from syntax to semantics is work-in-progress and may change. This document describes the language as the compiler implements it today. Sections with //TODO are planned but not implemented yet.

A first look

Hello, world

def main() i32
{
    // C.puts is temporary until we got a print function
    C.puts("Hello, world!");
    return 0;
}

The main function is the program's entry point, it needs to return an integer at the end.

Language tour

// We can declare global constants, global variables however are not allowed
Version :: 3;

// Enums are scoped constants, they are similar to C enums
enum Level : i32
{
    Low    :: 1
    Medium :: 2
    High   :: 3
}

def main() i32
{
    // Constructing an object of type Counter with a named argument
    counter := Counter.new(start = 0);
    // Parenthesis for control flow is optional
    while !counter.atLimit()
    {
        counter.increment();
        skip if counter.value == 3;         // skip with trailing if
        break if counter.value > 8;             
    }

    // We declare variables with infered types using the := symbols
    numbers := [1, 2, 3, 4];                // fixed array of type [i32; 4]
    total := 0;
    i := 0;
    while i < numbers.length
    {
        // Arithmethic operations with a leading % allow for overflow/underflow
        total = total %+ numbers.at(i);
        i = i %+ 1;
    }

    // We evaluate the function but discard the result
    _ := describe(Level.High);
    return total %+ counter.value %+ Version;
}

// We can give parameters default values with the = symbol
def describe(level: Level, prefix: string = "level") i32
{
    return prefix.length() %+ 1 if level == Level.High;
    return prefix.length();
}

type Counter(start: i32)
{
    // Fields are required to be initialized, either with a literal or type parameter
    value := start
    limit : i32 : 10                        // Constant with explicit type

    def increment()
    {
        // We access fields with a leading dot, there is no this keyword like in other languages
        .value = .value %+ 1;
    }

    def atLimit() bool
    {
        return .value >= .limit;
    }
}

Syntax summary

// declarations
name :: value;                        // constant
name : Type : value;                  // constant, explicit type
name := value;                        // variable (local only)
name : Type = value;                  // variable, explicit type
name :: init Type;                    // write-once global
_ :: expression;                      // discard

def name(a: T, b: U = default) R { }  // function
def Type.name(a: T) R { }             // static method
#extern(symbol = "s") def f() R {}    // extern

type Name(params) { fields methods }  // type
enum Name : Base { Members }          // enum

// types
bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 rune cstring rawptr string void
ref T          // reference
[T; N]         // fixed array
[T; _]         // dynamic array
[T]            // slice

// expressions
a %+ b   a %- b   a %* b   a / b
-a       !a       ref a
a == b   a != b   a < b   a <= b   a > b   a >= b
a and b  a or b
f(x, name = y)   obj.field   obj.method()   .field   Type.new()   Type.member
42'i32   1.5'f32   82'rune   "text"   true   false
[1, 2, 3]   [1, 2, ...]   [...]

// statements
if cond { } else if cond { } else { }
while cond { }
break;   skip;   return;   return value;
break if cond;   skip if cond;   return value if cond;

Compiling

The compiler can currently only run one file with the following cli command. Dependencies like the standard library are automatically pulled in.

Caracal.exe path/to/program.cara

Debug and release modes

//TODO

Basics

Source encoding

Source files have to be valid UTF-8 with or without a BOM, other encodings are rejected.

Comments

// Line comment, runs to the end of the line.

/* Block comment,
   spans lines. */

Identifiers

Identifiers can contain ASCII letters, numbers and underscores but are not allowed to start with a number. A single underscore is not an identifier, its a discard.

Keywords

def     // function and method declarations
type    // type declarations
enum    // enum declarations
if      // if control flow
else    // else branch
while   // while control flow
break   // breaking out of blocks 
skip    // skipping the rest of blocks
return  // returning data
ref     // taking references or 
and     // logical and
or      // logical or
true    // boolean true literal
false   // boolean false literal
init    // contextual keyword for global init constants
new     // contextual keyword for creating instances

Literals

awesome := true;                     // boolean literal
value   := 67;                       // i32 by default
pi      := 1.5;                      // f32 by default
hello   := "Hello, caracal 🐈!";    // utf-8 string

Typed literals

Literals can be explicitly typed with a suffix.

value  := 42'i32;
byte   := 200'u8;
big    := 9000000000'i64;
ratio  := 1.5'f32;
letter := 82'rune;

Semicolons

Statements generally have to end with a ;. There are a few places where semicolons are not needed, for example between enum members or type fields.

enum Values
{
    First
    Second
}

type Vector2D(x: f32, y: f32)
{
    x := x
    y := y
}

Order independent declarations

The order of declarations doesn't matter, you can use functions, enums and types before they are declared. Forward declarations arent needed either, the compiler collects whatever it needs on it's own.

def make() Number
{
    return Number.new(7);
}

type Number(value: i32)
{
    value := value
}

More stuff

//TODO