Skip to main content

Shake Programming Language Features

1 Introduction

Shake is a high level, object-oriented, multi-targeting, modern programming language. It can be compiled into different languages (targets), such as C, C++, C#, Java, JavaScript and binary executables as well as interpreted (currently the only implemented target is JavaScript). It also provides a scripting language for faster and more efficient creation of small projects.

1.2 Overview

The following code is an example for a simple program written in Shake.

fun main() {
println("Hello World!")

val i: int = 10;
val j: int = 5;
val k: int = i + j;
println(k);
}

This code will print "Hello World!" to the console, then calculate the sum of the two integers i=10 and j=5 and print the result (15) also to the console.

2 Primitive Datatypes

There are 8 primitive datatypes in Shake:

Type# of BytesRangeDescription
byte (int8)1Whole numbers from -2^7 to 2^ - 1Signed 8-bit integer
short (int16)2Whole numbers from -2^15 to 2^15 - 1Signed 16-bit integer
int (int32)4Whole numbers from -2^31 to 2^31 - 1Signed 32-bit integer
long (int64)8Whole numbers from -2^63 to 2^63 - 1Signed 64-bit integer
float (float32)4Floating point numbers from ±3.402823e3832-bit floating point number
double (float64)8Floating point numbers from ±1.7976931348623e30864-bit floating point number
boolean1 [*]Either true or falseBoolean, either true or false
char2Unicode characters16-bit Unicode character

* A boolean behaves like 1 bit, but it occupies 8 bits (one byte) in RAM

additionally there is one unsigned variant of each integer type.

Type# of BytesRangeDescription
ubyte1Whole numbers from 0 - 2^8 - 1Unsigned 8-bit integer
ushort2Whole numbers from 0 - 2^16 - 1Unsigned 16-bit integer
uint4Whole numbers from 0 - 2^32 - 1Unsigned 32-bit integer
ulong8Whole numbers from 0 - 2^64 - 1Unsigned 64-bit integer

3 Operators

3.1 Mathematical Operators

Shake has 6 different types of mathematical operators

10 + 3   // plus (=13)
103 // minus (=7)
10 * 3 // multiply (=30)
10 / 3 // divide (=3)
10 % 3 // modulo (=1)
10 ** 3 // power (>> 10 * 10 * 10) (=1000)

3.2 Comparison Operators

These are Shake's comparison-operators

9 == 8  // equals (false)
9 != 8 // not equals (true)
9 >= 8 // bigger Equals (true)
9 <= 8 // lower Equals (false)
9 > 8 // bigger (true)
9 < 8 // lower (false)

3.3 Logical Operators

true || false  // or (at least one of them has to be correct)
true && false // and (both of them have to be correct)
true ^^ false // xor (either one, but not both have to be correct)

NOTE: All binary operators will work on booleans as well. Via operator overloading they do exactly the same as logical operators on booleans, so true ^ false will work the same as true ^^ false. It is better practice to use the logical operators though!

3.4 Bitwise operators

3.4.1 Understanding binary numbers

You can skip this paragraph if you understand the basic concept of binary numbers which is necessary for sections 3.4.2 and 3.4.3

Binary operators can manipulate individual bits of values. To understand binary operations, you first have to understand binary numbers. In binary numbers, each digit is represented as either 0 or 1. You can write each number as base 2, which is very similar to decimal numbers (base 10), but with only 2 instead of 10 (0 through 9) possible characters per digit. Since some humans previously decided to use 0 and 1 as these two numbers, we would count like this:

0 (0), 1 (1), 10 (2), 11 (3), 100 (4), 101 (5), 110 (6), 111 (7)... (and so on)

for a negative number you can use the same principle, but start with a minus sign.

to convert a binary number to decimal you can use the following formula:

decimal = binary * 2^0 + binary * 2^1 + ... + binary * 2^n

and to convert a decimal number to binary you can use the following formula:

binary = decimal / 2^0 + decimal / 2^1 + ... + decimal / 2^n

This is how most primitive datatypes work. They just have a differing number of bits.

These data types also have to be able to store negative values. So the first digit is used to store the sign. So for negative numbers the first digit is 1 and the rest of the digits are 0. The formula to convert a binary number to decimal is the same as positive numbers, but we calculate (-1) + [positive amount] because we don't need a negative zero.

Now that we know how to convert numbers to binary and back we can start to understand the different operators.

3.4.2 Bitwise and, or, xor

0b1010 & 0b0101  // 0b0000 Binary AND
0b1010 | 0b0101 // 0b1101 Binary OR
0b1010 ^ 0b0101 // 0b1011 Binary XOR

If we just think about the bits as boolean values, we can use the AND, OR and XOR operators to manipulate the bits. so bit 1 from the first number is ANDed with bit 1 from the second number, bit 2 from the first number is ANDed with bit 2 from the second number and so on.

3.4.3 Bitwise shift

0b1010 << 1  // 0b1010 Binary left shift
0b1010 >> 1 // 0b0101 Binary right shift
0b1010 >>> 1 // 0b0101 Binary right shift (unsigned)

Using lshift and rshift we can shift the bits of a number to the left or right by a certain amount.

3.4.4 Bitwise not

~0b1010  // 0b0101 Binary NOT

The NOT operator inverts all bits of a number.

Note: The not operator targets all bits, so the byte 0b0001 is actually 0b00000001 and the not operator will therefore return 0b11111110.

3.5 Brackets & Priorities

3.5.1 Brackets

As the standard math rules still apply, Shake first performs all multiplications (and divisions) in the term and then any additions or subtractions. Hence, to multiply the sum of some numbers, brackets have to be placed around the numbers to be added up before they are then multiplied, eg.

4 * 10 + 3    // >> 43
4 * (10 + 3) // >> 52

(Hint: Brackets can be placed inside other brackets, inner brackets are always calculated first)

As seen above, Shake "prioritizes" certain operations over others. For a complete overview of these priorities, take a look at the list below.

3.5.2 Priorities

  1. brackets
  2. bitwise and, or, xor &, |, ^
  3. bitwise shift <<, >>, >>>
  4. mathematical > power **
  5. mathematical > multiply, divide, modulo * / %
  6. mathematical > plus, minus + -
  7. logical operators > and &&
  8. logical operators > or ||
  9. comparison > equals, not equals == !=

4 Variables, Values and Constants

4.1 Variables

Variables are used to store data, which can be changed during the execution of the program. In Shake, variables are declared using the var keyword, followed by the name of the variable, a colon and the type of the variable.

A variable that is declared outside a function's body is called a field.

val i: int = 10;  // i is a variable of type int and is assigned the value 10

4.2 Values

Values are used to store data, which cannot be changed after it has been assigned. In Shake, values are declared using the val keyword, followed by the name of the value, a colon and the type of the value.

val PI: float = 3.14159;  // PI is a constant of type float and is assigned the value 3.14159

4.3 Constants

Constants are used to store data, which cannot be changed during the execution of the program. A const binds even stronger than a val, the content of a const must also be constant and known at compile time.

const val PI: float = 3.14159;  // PI is a const of type float and is assigned the value 3.14159

4.4 Type Inference

Shake has a feature called type inference, which allows the programmer to omit the type of a variable or constant, if it can be inferred from the value.

val i = 10;  // i is a variable of type int and is assigned the value 10
val PI = 3.14159; // PI is a constant of type float and is assigned the value 3.14159

5 Control Structures

5.1 If-Else

The if statement is used to execute a block of code if a condition is true. If the condition is false, the code block is skipped.

if (condition) {
// code block
} else {
// code block
}

5.2 While

The while statement is used to execute a block of code as long as a condition is true.

while (condition) {
// code block
}

5.3 Do-While

The do-while statement is used to execute a block of code as long as a condition is true. The block of code is executed at least once, even if the condition is false.

do {
// code block
} while (condition);

5.4 For

The for statement is an enhanced version of the while statement. It can be used to execute a block of code a specific number of times. It consists of three parts: the initialization, the condition and the increment. The initialization is executed once at the beginning of the loop. The condition is checked before each iteration of the loop. The increment is executed at the end of each iteration of the loop.

NOTE: The increment is even evaluated if the remaining code block is skipped using continue.

for (initialization; condition; increment) {
// code block
}

5.5 Break

The break statement is used to exit a loop, switch or block of code.

while (true) {
if (condition) {
break;
}

// some code that will not be executed if the condition is true
}

5.6 Continue

The continue statement is used to skip the remaining code block of a loop and continue with the next iteration.

for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue;
}

// will only print odd numbers
println(i);
}

Return

The return statement is used to exit a function and return a value.

return "Hello World!";

For functions that do not return a value, the return statement can be omitted.

fun functionName() {
// code block
}

Return with no value is equivalent to return can be used to exit a function early.

fun functionName() {
if (condition) {
return;
}

// code block
}

6 Functions

6.1 Function Declaration

A function is a block of code that can be called from other parts of the program.

fun functionName() {
// code block
}

6.2 Function Return

A function can return a value using the return statement.

fun functionName(): int {
return 10;
}

Note: If a function does not return a value, the return type is void.

fun functionName() {
// code block
}

is equivalent to

fun functionName(): void {
// code block
}

6.3 Function Parameters

A function can have parameters, which are used to pass data to the function.

fun functionName(param1: int, param2: int) {
// code block
}

Default Parameters

A function can have default parameters, which are used if no value is passed for the parameter.

fun functionName(param1: int = 10, param2: int = 20) {
// code block
}

Such a function can be called with no parameters, one parameter or both parameters.

functionName();  // param1 = 10, param2 = 20
functionName(5); // param1 = 5, param2 = 20
functionName(5, 15); // param1 = 5, param2 = 15

You can also select which parameter you want to set by using the parameter name.

functionName(param2=15);  // param1 = 10, param2 = 15

6.4 Function Overloading

Shake supports function overloading, which allows multiple functions with the same name but different parameters.

fun functionName(param1: int) {
// code block
}

fun functionName(param1: char) {
// code block
}

6.5 Function Recursion

A function can call itself, which is called recursion.

fun factorial(n: int): int {
if (n == 0) {
return 1;
}

return n * factorial(n - 1);
}

7 Classes

7.1 Class Declaration

A class is a blueprint for creating objects.

class ClassName {
// class content
}

7.2 Class Constructor

A class can have a constructor, which is used to initialize the object.

class ClassName {
constructor() {
// code block
}
}

Overloading Constructors

A class can have multiple constructors, which is called constructor overloading.

class ClassName {
constructor() {
// code block
}

constructor(param1: int) {
// code block
}
}

On construction, the constructor with the most matching parameters will be called.

Named Constructors

There may be cases, you want to have multiple constructors with the same parameters. In this case, you can use named constructors.

class ClassName {
constructor create() {
// code block
}

constructor other() {
// code block
}
}

7.3 Class Properties

A class can have properties, which are used to store data.

class ClassName {
val property: int = 10;
}

Properties can be accessed using the dot operator.

val obj = new ClassName();
println(obj.property);

7.4 Class Methods

A class can have methods, which are used to perform actions.

class ClassName {
fun methodName() {
// code block
}
}

Methods can be called using the dot operator.

val obj = new ClassName();
obj.methodName();

7.5 Class Inheritance

A class can inherit from another class, which is called inheritance.

class ParentClass {
// class content
}

class ChildClass : ParentClass {
// class content
}

Classes defaulty inherit from the Object class, which provides some basic methods like toString and equals.

A class can only inherit from one class, so called superclass, but a class can implement multiple interfaces.

A class can also implement one or more interfaces.

interface InterfaceName {
// interface content
}

interface OtherInterface {
// interface content
}

class ClassName : InterfaceName, OtherInterface {
// class content
}

7.6 Class Abstract

A class can be declared as abstract, which means it cannot be instantiated.

abstract class ClassName {
// class content

fun methodName() {
// code block
}

// abstract method which should be implemented by subclasses
abstract fun abstractMethod();
}

7.7 Class Static

A class can have static properties and methods, which are used to store data and perform actions without creating an object.

class ClassName {
static val property: int = 10;

static fun methodName() {
// code block
}
}

Static properties and methods can be accessed using the class name.

println(ClassName.property);
ClassName.methodName();

8 Interfaces

8.1 Interface Declaration

interface is a contract that defines the signature of the functionality. It is a blueprint of a class.

interface InterfaceName {
// interface content
}

8.2 Interface Methods

An interface can have methods, which are used to perform actions.

interface InterfaceName {
fun methodName();
}

A interface is abstract by definition, so all methods are abstract by default.

8.3 Default Methods

An interface can have default methods, which are used to provide a default implementation.

interface InterfaceName {
fun methodName() {
// default implementation
}
}

8.4 Static Methods

An interface can have properties and methods, which are used to store data and perform actions without creating an object.

interface InterfaceName {
static val property: int = 10;

static fun methodName() {
// code block
}
}

Static properties and methods can be accessed using the interface name.

println(InterfaceName.property);
InterfaceName.methodName();

8.5 Inheritance

An interface can inherit from another interface, which is called inheritance.

interface ParentInterface {
// interface content
}

interface ChildInterface : ParentInterface {
// interface content
}

An interface can inherit from multiple interfaces.

interface InterfaceName : ParentInterface, OtherInterface {
// interface content
}

8.6 Implementation

A class can implement one or more interfaces.

class ClassName : InterfaceName, OtherInterface {
// class content
}