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 Bytes | Range | Description |
---|---|---|---|
byte (int8) | 1 | Whole numbers from -2^7 to 2^ - 1 | Signed 8-bit integer |
short (int16) | 2 | Whole numbers from -2^15 to 2^15 - 1 | Signed 16-bit integer |
int (int32) | 4 | Whole numbers from -2^31 to 2^31 - 1 | Signed 32-bit integer |
long (int64) | 8 | Whole numbers from -2^63 to 2^63 - 1 | Signed 64-bit integer |
float (float32) | 4 | Floating point numbers from ±3.402823e38 | 32-bit floating point number |
double (float64) | 8 | Floating point numbers from ±1.7976931348623e308 | 64-bit floating point number |
boolean | 1 [*] | Either true or false | Boolean, either true or false |
char | 2 | Unicode characters | 16-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 Bytes | Range | Description |
---|---|---|---|
ubyte | 1 | Whole numbers from 0 - 2^8 - 1 | Unsigned 8-bit integer |
ushort | 2 | Whole numbers from 0 - 2^16 - 1 | Unsigned 16-bit integer |
uint | 4 | Whole numbers from 0 - 2^32 - 1 | Unsigned 32-bit integer |
ulong | 8 | Whole numbers from 0 - 2^64 - 1 | Unsigned 64-bit integer |
3 Operators
3.1 Mathematical Operators
Shake has 6 different types of mathematical operators
10 + 3 // plus (=13)
10 – 3 // 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
- brackets
- bitwise and, or, xor
&
,|
,^
- bitwise shift
<<
,>>
,>>>
- mathematical > power
**
- mathematical > multiply, divide, modulo
*
/
%
- mathematical > plus, minus
+
-
- logical operators > and
&&
- logical operators > or
||
- 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
}