Skip to the content.

Features

Stack

In Scale, every operation uses the Stack. For example, adding two numbers would look like this:

1 2 +

This pushes the numbers 1 and 2 onto the stack, and then adds them together, leaving the result 3 on the stack.

Variables

Variables are declared using the decl keyword. For example:

decl x: int

This declares a variable named x of type int. Variables can be assigned to using the => operator:

1 => x

This pops the number 1 off the stack, and assigns it to the variable x. These two operations can be combined into one:

1 => decl x: int

This pushes the number 1 onto the stack, and then assigns it to a new variable named x.

Functions

Functions are declared using the function keyword. For example:

function add(a: int, b: int): int
    a b + return
end

This declares a function named add that takes two ints and returns an int. The function adds the two arguments together, and returns the result.

Calling a function is as simple as putting the arguments on the stack, and then writing the function name:

1 2 add

Control Flow

Scale has a few control flow statements, such as if, while, and for. For example:

1 => decl x: int
while x 10 < do
    x 1 + => x
done

This declares a variable named x and sets it to 1. Then, while x is less than 10, it adds 1 to x.

for i in 1 to 10 do
    i puts
done

This prints the numbers 1 through 9. The for loop is inclusive of the start, but exclusive of the end.

if 1 2 == then
    "1 is equal to 2" puts
else
    "1 is not equal to 2" puts
fi

This prints 1 is not equal to 2 (as expected).

Comments

Line Comments are started with # and end at the end of the line. Block Comments are started with ` and end with `.

Types

Scale has a few built-in types, such as int, float and any. int is a 64-bit signed integer. float is a 64-bit floating point number. any is a type that can hold any value.

Primitive Arrays are also supported. They are declared by enclosing the type in square brackets. For example:

decl x: [int]

This declares a variable named x as an array of ints.

Strings

Strings are declared like many other languages, with double quotes. For example:

"Hello, World!" puts

This prints Hello, World! to the console.

Strings have a type of str, which is the only type that is not directly compatible with C. To create a C-compatible string, prefix the string with c. For example:

c"Hello, World!"

Structs

Structs in Scale are similar to structs in C++. For example:

struct Point
    decl x: int
    decl y: int
end

This declares a struct named Point with two fields, x and y, both of type int.

Structs can be instantiated using the new keyword. For example:

Point::new => decl p: Point

This creates a new Point and assigns it to the variable p.

Struct members can be accessed using .. For example:

34 => p.x

This assigns 34 to the x field of p.

Structs can also have methods. For example:

struct Point
    decl x: int
    decl y: int

    function add(other: Point): Point
        x other.x + => x
        y other.y + => y
        self return
    end
end

This adds a method named add to the Point struct. The method takes another Point as an argument, and adds the x and y fields of the argument to the x and y fields of the struct, respectively. It then returns the struct.

Struct methods can be called using : on an instance of the struct. For example:

Point::new => decl p: Point
Point::new => decl p2: Point
p p2:add => p

This creates two new Points, and then calls the add method on p2 with p as an argument. It then assigns the result to p.

Struct Initialization

There are two ways to initialize a struct in Scale. The first is to use the new static function. For example:

Point::new => decl p: Point

This creates a new Point and assigns it to the variable p.

The second way is to use property initialization. For example:

Point {
    1 => x
    2 => y
} => decl p: Point

This creates a new Point with the x field set to 1 and the y field set to 2, and assigns it to the variable p. With property initialization it is necessary to initialize all fields.

Custom Property getters and setters

Structs can have custom property getters and setters. For example:

struct MyStruct
    decl someString: str
        get()
            "someString: " field + return
        end
        set(value)
            value => field
        end
end

This declares a struct named MyStruct with a field named someString. The field has a custom getter and setter. The getter returns the value of the field with someString: prepended to it. The setter sets the value of the field to the value passed to it. The getter and setter are called automatically when accessing the field. For example:

MyStruct {
    "Hello, World!" => someString
} => decl s: MyStruct

s.someString puts

This prints someString: Hello, World! to the console.

Pass-by-Value

By default, structs in scale are passed by reference. This means that when you pass a struct to a function, the function will receive a pointer to the struct, and any changes made to the struct will be reflected in the original struct. If you want to pass a struct by value, you have to put @ before the type. For example:

function increment(p: @Point): Point
    p.x 1 + => p.x
    p.y 1 + => p.y
    p return
end

Any changes made to p will not be reflected in the original struct.

Enums

Enums in Scale are similar to enums in C. For example:

enum Color
    Red
    Green
    Blue
end

This declares an enum named Color with three variants, Red, Green, and Blue. Each variant corresponds to an integer, starting at 0. For example:

Color::Red => decl c: Color

This assigns the variant Red to the variable c. This is equivalent to:

0 => decl c: Color

Variant Values

Each variant can have a value associated with it. For example:

enum Color
    [0xFF0000] Red
    [0x00FF00] Green
    [0x0000FF] Blue
end

This assigns the value 0xFF0000 to the Red variant, 0x00FF00 to the Green variant, and 0x0000FF to the Blue variant.

Unions

Unions in Scale are similar to unions in C. For example:

union IntOrFloat
    asInt: int
    asFloat: float
end

This declares a union named IntOrFloat with two variants, asInt and asFloat. Each variant can hold a different type. For example:

1 IntOrFloat::asInt => decl x: IntOrFloat

This assigns the variant asInt to the variable x, and sets the value to 1. Unions in Scale are tagged, meaning an error will be thrown if you try to access the wrong variant. For example:

x.asFloat

This will throw an error, as x is currently the asInt variant.

C Interop

Scale can interoperate with existing C code. For example:

expect foreign function puts(string: [int8]): int

This imports the puts function from C. The expect keyword tells the compiler that the function will be implemented in a different translation unit and the foreign keyword tells the compiler to not mangle the function symbol.

export foreign function add(a: int, b: int): int
    a b + return
end

This exports the add function to C. The export keyword tells the compiler to generate a function declaration in scale_interop.h. The foreign keyword tells the compiler to not mangle the function symbol. Calling Scale functions from C is as simple as including the scale_interop.h header file, and calling the function. For example:

#include <stdio.h>
#include "scale_interop.h"

int main() {
    printf("%d\n", add(1, 2));
    return 0;
}

This prints 3 to the console.

Arrays

Arrays in Scale are similar to arrays in C. For example:

decl x: [int]

This declares a variable named x as an array of ints. This, however, does not allocate any memory for the array. To allocate memory, use the new keyword. For example:

new<int>[10] => x

This allocates enough memory for 10 ints, and assigns it to the variable x.

Arrays can be indexed using []. For example:

x[0] => decl y: int

This assigns the first element of x to the variable y.

Map syntax

Scale has a special syntax for creating and modifying arrays. For example:

[10 do i fib] => decl fibs: [int]

This creates an array of 10 elements, and assigns it to the variable fibs. Each element is the result of calling the fib function with the index as an argument.

Modifying arrays is also simple. For example:

[fibs do val 2 *]

This multiplies each element of fibs by 2.

If you just want to loop over the array without modifying it, make sure to not leave any values on the stack. For example:

[fibs do val puts]

This prints each element of fibs to the console.

Imports

Scale has a simple import system. For example:

import <file>

This imports the file named file.scale relative to the current file.

import <dir>.<file>

This imports the file named file.scale relative to the directory named dir.

import <module>

This imports the module named module.

Modules

Modules are defined by Scale frameworks. For example the following index.drg defines a module named test:

framework: {
    # ...
    modules: [
        test: [
            "foo.scale";
            "bar.scale";
            "baz.scale";
        ];
    ];
};

This defines a module named test with the files foo.scale, bar.scale, and baz.scale. Importing this module will import all of the files in the module.

Macros

Scale has a simple macro system. For example:

macro! MyMacro(str1, str2) {
    $str1 ": " + $str2 +
}

This defines a macro named MyMacro that takes two arguments, str1 and str2. For example:

MyMacro! "Hello" "World!"

This expands to "Hello" ": " + "World!" +.

More complex macros can be defined as such:

macro! ComplicatedMacro() in "lib"

Where lib is the name of a dynamic library. The dynamic library must be visible to the compiler. These function-like macros must be defined like so:

foreign function ComplicatedMacro(loc: SourceLocation, parser: Parser): Result

Where loc is the location of the macro invocation, and parser can be used to parse the arguments to the macro. The return value must be of type Result, where the Ok variant is [Token] and the Err variant is MacroError. For example:

foreign function ComplicatedMacro(loc: SourceLocation, parser: Parser): Result
    parser:peek => decl token: Token?
    if token nil == then
        MacroError {
            "Expected a token, but got nothing" => msg
            nextTok.location => location
        } Result::Err return
    fi

    new<Token>{
        token!!
    } Result::Ok return
end

This macro will expand to the next token in the source file.

Data Type Literals

A few of Scale’s data types have literals. For example:

new<int>{1, 2, 3} => decl x: [int]

This creates an array of ints with the values 1, 2, and 3, and assigns it to the variable x.

(1, 2) => decl x: Pair

This creates a Pair with the values 1 and 2, and assigns it to the variable x. This assumes that the Pair struct has been declared.

(1, 2, 3) => decl x: Triple

This creates a Triple with the values 1, 2, and 3, and assigns it to the variable x. This assumes that the Triple struct has been declared.

(1 to 3) => decl x: Range

This creates a Range with the values 1 and 3, and assigns it to the variable x. This assumes that the Range struct has been declared.

(1 to) => decl x: PartialRange

This creates a PartialRange with the lower bound 1, and assigns it to the variable x. This assumes that the PartialRange struct has been declared.

(to 3) => decl x: PartialRange

This creates a PartialRange with the upper bound 3, and assigns it to the variable x. This assumes that the PartialRange struct has been declared.

(to) => decl x: UnboundRange

This creates an UnboundRange, and assigns it to the variable x. This assumes that the UnboundRange struct has been declared.

Examples

For more detailed examples of the language, see the examples folder at The Repository