Scripting language implemented with Rust
Teascade 5c3bc9963f Update links and add Reid logo 1 year ago
README.md Update links and add Reid logo 1 year ago

README.md

reid logo

What?

Reid is a language intended for scripting purposes. Reid compiler reidc reads .reid files which contain Reid-code (specified below), which will then be parsed into bytecode. The parsed bytecode then, unless otherwise stated (via a --no-output compile-flag), will produce .reidc files, which function as compiled Reid, which can then be run with the Reid-interpreter reid. If stated, the reidc compiler can contain the reid interpreter with it aswell, and in such case, can run the parsed bytecode right away with a --run -flag.

But why "Reid"?
Reid is a letter in the old nordic alphabet that means "ride" or "journey", so the meaning was quite fitting (this langauge is quite a "wild ride"), and the name was pretty cool, and the letter made a cool and easy to make logo.

Why?

The original version of Reid was written in TypeScript for a NodeJS server for a dungeons and dragons system client. The new server is being written in Rust, so the language must be re-written. To make the process easier, here is the specifications (and technically the documentation) for the re-visited version of the language. To the same repository I will be creating the actual Rust implementation of this language too.

The original version of Reid (originally called Omega) can be viewed here

Who?

Reid is created by Teascade, original version being written with TypeScript in 2016, and new specification and Rust implementation written in 2017.

License?

Currently Reid has no license, since it is only a specification, but most likely it will be licensed under MIT later.

The Reid specification is simply CC-BY-SA:

Creative Commons License

Table of Contents

Table of contents for the Reid spec

Examples

Before any of the following examples, a print-function has been defined in the global scope. The print-function takes in a String-type as a parameter.

Example Hello World

print("Hello World!");

Example loop

let max = 15;
for (let i = 0; i < max; i++) {
  print("i: " + i);
}

General Syntax

The general syntax of Reid is fairly similar to that of TypeScript or Rust. The syntax is a mix of keywords and expressions displayed such as in the examples.

Expressions

Expressions are a set of values, function calls and operators that Reid interprets and returns a new value out of.

For example 2 + 3 is an expression combined by a +-operator which will result 5

Function calls

Function calls are also very similar to other languages. If there esists a function called function_one, it can be called with function_one();. If there exists a function called function_two which requires two i32's as arguments, it can be called as function_two(5, 2); where 5 and 2 are example integer values.

  • Function calls must have parenthesises in the end of them to signify the call, and after the parenthesis there must be a semicolon ;.
  • Any possible arguments must be given between the parenthesis separated by commas,.

Operators

Operators are a number of individual and combined symbols which together form meanings which are interpreted in a special way.

There a few types of operators, and all of these types are either unary of binary operators, although some of them are valid as both:

Unary and binary operators

The difference between unary and binary operators is that unary operators require one value and binary operators require two values.

Unary operators have such examples as:

  • ! NOT operator, converts !true into false etc.
  • - minus operator, negates the next value -5 etc.

Binary operators have such examples as:

  • && AND operator, checks whether both sides of the operator are true.
  • + plus-operator, adds both sides of the operator together.

Logical operators

These are the operators ofthen called as "conditions" and most commonly used in if-statements and such.

  • && AND binary operator. Checks whether both sides of the operator are true
    • true && true returns true
    • false && true return false
  • || OR binary operator. Checks whether either side of the operator is true
    • true || true returns true
    • true || false return true
  • ^ XOR binary operator. Checks whether only one side of the operator is true.
    • true ^ true return false
    • true ^ false returns true
    • false ^ false returns false
  • == Equals binary operator. Checks whether both sides of the operator are the same.
    • "not" == "test" returns false
    • 3 == 3 returns true
  • != Not equals binary operator. Checks whether both sides of the operator are not the same.
    • "not" != "test" returns true
    • 3 == 3 returns false
  • ! Not unary operator. Negates the value associated with it.
    • !true returns false
    • !(true ^ true) returns true
  • ? Optional-exists unary operator. Checks whether the operator-wrapped value before the operator is empty or not.
    • empty? returns false. See empty-keyword
    • optional? returns true, if optional contains a value.

Arithmetic operators

Arithmetic operators are operators used to do math calculations such as addition or multiplication. any integer or float -based types can be used together.

  • + Plus binary and unary operator. As a binary operator combines both sides of the operator, as an unary operator simply returns the associated value.
    • + 3 returns 3
    • 2 + 3 returns 5
  • - Minus binary and unary operator. As binary operator subtracts the latter side of the operator from the first, as an unary operator simply returns the negated associated value.
    • - 3 returns -3
    • 2 - 3 returns -1
  • * Multiplication binary operator. Returns the value of the both sides multiplied.
    • 2 * 3 returns 6
    • 5 * 5 returns 25
  • / Division binary operator. Returns the division of the first value with the second.
    • 2.0 / 3 returns 0.666..
    • 6 / 2 returns 3
  • % Modulo binary operator. Returns the remainder of the division between the two values.
    • 2 % 3 returns 2
    • 6 % 2 returns 0

Assignment operators

Assignment operators are a special kind of operator used assign values to variables.
The most basic type of assignment operator being of course =.
For example:

  • test_var = 3 sets the value of test_var to 3

= can be combined with any of the binary arithmetic operators however to create other kinds of assignment operators which work like so:

  • test_var += 3 would be the same as test_var = test_var + 3
  • test_var *= 3 would be the same as test_var = test_var * 3

If you however try to use these kinds of assignment operators on variabes which have not been initialized with a value yet, an exception will occur.

All assignment operators return the value the assignee (leftside of the operator) was after the operator. For more examples:

let test = 0;
let new = (test = 2); // new becomes 2, test becomes 2
let another = (new += 5); /// another becomes 7, new becomes 7.

But there are also two kinds of "special" assignment operators, which return the old value of the assignee, after the operator.

  • ++ increment unary operator which adds 1 to the leftside value.
  • -- decrement unary operator which subtracts 1 from the leftside value.

for example:

let i = 0;
let first = i++; // first becomes 0, i increments to 1
let second = i--; // second becomes 1, i decrements back to 0.

Scopes

Scopes are areas of code surrounded by brackets {}. E.g.

let variable = "text";
{
  let this_is_scoped = "scoped_text";
  this_is_scoped = variable + "!";
}
variable = this_is_scoped; // Exception! Cannot access inner-scope.

As is visible in the example, variables defined in the scope are no longer accessible outside the scope. Scopes exist in their individual "environments", where they can access the variables in their upper scopes, but not inner scopes.

Parenthesis

Parenthesis() can be added to surround any operators, expressions, values or keywords to guide on what order and how the code should be run.

For example:

  • (2 + 3) * 5 = 5 * 5 = 25
  • !(true ^ true)
  • (print("test")) Here parenthesis won't do much through
  • 2 + (3) Here parenthesis are somewhat useless aswell.
  • (unwrap optional) * 5

Values

There are a number of values you can assign to your variables, as of Reid 1.0, only primitive values are possible. Such types are:

  • string, a basic piece of text, defined as followes: "String here".
  • char, contains a single character, defined as follows: 'c'
  • i16 (or usually short), a basic 16-bit integer value, such as 3 or 11.
  • i32 (or usually int), a basic 32-bit integer value, such as 3 or 11.
  • i64 (or usually long), a basic 64-bit integer value, such as 3 or 11.
  • f32 (or usually float), a basic 32-bit float value, such as 1.5 or 6.32.
  • f64 (or usually double), a basic 64-bit float value, such as 1.5 or 6.32.
  • boolean, contains value of true or false. This also includes conditions.
  • T? is an optional type. Any type can be an optional type, but the optional type must be checked with var?-operator before it can be accessed via unwrap, or the execution will crash.
  • T[] is a primitive array-type. warning: this part is heavily work-in-progress
    • New arrays may be created as such T[len](), where T is the type of the array, and len is the size of it.
    • For example: i32[4]() would create an i32-array with 4 slots.
    • Slots in an array are accessible with the standard array[i] syntax.

Values in Reid are strongly typed, meaning combining two different types cannot be combined, unless they are successfully cast.

Default values for these types are as follows:

  • 0 for i16, i32, i64, f32, and f64.
  • "" for string.
  • false for boolean.
  • empty for T?.
  • [] (empty array) for T[]

Conditions

For the sake of glossary, conditions can simply be true or false, but in all cases where "conditions" are said, logical operators also apply.

Using numbers

When using numbers directly (like 5, 32 or 753), if their type cannot be deducted easily (via parameter type or variable type), the number's type will default to i32, then i64, then i16, then f32 and finally f64.

If you however use numbers with decimals like 5.0, 32.2 or 73.1, their type will default to f32 and then f64.

If it is necessary to specify the type of the number (ie. for function overloading), you can simply add the type immediately after the number, e.g. 5i32, 32f32, or 12i16.

Special cases

There are some special (mostly arithmetic) cases where different languages might act one way or another, so here are a list of those cases and how Reid handles them:

Division by zero (x / 0)
This causes a runtime exception.

Modulo of zero (x % 0)
This also causes a runtime exception, as can be deducted.

Integer under-/overflow
Trying to assign a number larger or smaller than the byte-limit of the type allows (ie. larger than 2147483647 for i32 or smaller than -2147483647), will cause a runtime exception.

Function Overloading

Function overloading in Reid is possible, meaning you can create functions that have the same name as other already existing functions, but the parameter count and/or parameter types must differ.
ie. you could have two functions called test, both of which require a parameter param, but the other function's param is a string type, and the other is i32, like so:

def test(param: string) {
  // Code
}

def test(param: i32) {
  // Code
}

When calling overloaded functions though, keep in mind that if you have e.g. test(param: i32) and test(param: i64), when calling it by test(5), the first overload will be called, since 5 defaults to i32 (See using numbers under values). To call the i64 version, you need to specify the type by calling test(5i64)

Keywords

The following keywords are specified to execute an action.

  • let initializes a new variable.
  • if enters the scope once if the condition is met.
  • else enters the scope if the if before failed.
  • def defines a new function.
  • return
  • while functions like if, but enters the scope as long as the condition is met.
  • for initializes a scope which will be ran a number of times specified after the for.
  • break breaks the loop prematurely.
  • continue skips the rest of the loop's body for a new iteration.
  • unwrap unwraps an optional-type variable.
  • some wraps a variable into an optional.
  • empty value to set in an optional variable.
  • as casts the leftside value into the type on the rightside of this keyword.
  • All operators are also somewhat considered as keywords.

let

Initializes a new variable, as such:

let uninitialized: string; // Initializes this variable as string, but does not give it a value.
let five = 5;                // Sets five to 5
let var = "text";            // Sets var to "text"
let five = 6;                // Causes a compile-time error, cannot re-define five
five = 3;                    // (without let-keyword) Re-sets five to 3
let test_var = print("hi!"); // Causes a compile-time error; print has no return type definied.
  • Initialization of new variable must contain let, but re-definition of an existing variable, cannot start with let.
  • The name of the variable being defined must follow the let after whitespace.
  • After the name of the variable, there may be a definition of the type of the variable, but it is not necassary. When re-defining a value of a variable, there cannot be a re-definition of the type.
    • If there is no type-definition, an initializing value must be set.
    • type-definition's form is as follows: : T, and it cannot be preceded by whitespace. between the colon and the T there may be whitespace.
  • After whitespace, there may be (or must be, if no type-definition is given), an equals=-sign, after which there must be more whitespace, after which the value of the variable is given. This is simply an assignment operator.
  • After the value of the variable, the let-expression must end in a semicolon ;.
  • If you try to set the value of a variable via a function that has no return type defined, a compile-time error occurs.
    • There is no "null- or void-type" in Reid.

if

Defines an if-statement, which will, if the condition is met, enter the scope defined after the if.

if true {
  // Executed code
}

if "test" == "not true" {
  // Not executed code
}
  • begins with an if, after which there must be some whitespace. After the whitespace there must be a condition.
  • After the condition there must be some whitespace, after which there is the scope definition

else

Defines an else-statement, which must proceed after the if-statement's scope. if the if statement's condition was not met, else will be entered. An if-statement can be added immediately after the else, to chain them up.

if false {
  // Not executed code
} else {
  // Executed code
}

if false {
  // Not executed code
} else if "test1" == "test2" {
  // Also not executed code
} else {
  // Executed code
}
  • The else keyword must follow immediately after the if-statement's body (as seen in the example). Only whitespace is allowed in the middle.
  • After the else, there can be a new if-statement, without a body for the else itself.
  • If there is no if-statement following the else, there must be a body for the else itself, which will then be executed if the preceding if-statement was not executed.

def

Defines a new function or method as follows:

def first_function() {
  // Code
}

def second_function(param1: string) {
  // Code
}

def third_function(param1: i32, param2: string) {
  // Code
}

def returning_function() -> i32 {
  return 5; // Returns 5
}

def erronous_returning_function(param: boolean) -> i32 {
  if (param) {
    return 5;
  }
  // Causes a compile-time exception.
}
  • The signature of the function/method must begin with def.
  • After def there must be a number of whitespace, after which the name of the defined function must follow.
  • Immediately after the name of the function, there must be an opening parenthesis (.
  • After the opening bracket there may be parameters listed.
    • Format of the parameters follows the let format, without the let-keyword.
    • There must also be a type-definition.
    • There cannot be any default values. (no assignment operators)
    • The parameters are divided by a comma,, after which there may be any number of whitespace.
  • After the list of parameters there must be a closing bracket ).
  • Between the parenthesis and the parameter-lists, there may be any number of whitespace.
  • After the parenthesis and any number of whitespace, there must be the function body.
  • If the function returns something, there must be a return-type definition (-> T) after the function signature (see 4th example).
  • If return-type is definied, but some paths of the function will not return anything, a compile-time error occurs (see 5th example).

return

Returns the value rightside to the keyword. Must be inside a function definition to use this.

def returns_i32() -> i32 {
  return 5;
}

def returns_string() -> string {
  return "Test!";
}

return 3; // Compile-time error; outside any function definition.
  • Must be inside a function definition to use. If used outside any function definition, a compile-time exception occurs.
  • There must be a space between return and the returned value.
  • Must end in a semicolon;.

while

Defines a loop which will be as long as the condition defined after it is met.

while true {
  // Runs infinitely.
}

while false {
  // Never enters this loop.
}

while true == false {
  // Also never enters this loop.
}
  • To specify a while, the line must begin with a while.
  • After the while, there can be a number of whitespace, after which there must be a boolean value or otherwise known as a condition.
  • After the value there must be a scope definition.

for

Defines a loop very similar to while, but which parameters inside the parenthesis consists of three parts separated by semicolons;.

for (let i = 0; i < 10; i++) {
  // Loop through 0 to 9
}
  • The first part (let i = 0 in this example) is the beginning-expression. It can be any expression, and it will be executed as the loop begins whether or not the scope inside the loop will be accessed.
  • The second part (i < 10 in this example) is the condition defining whether the loop-scope will be accessed or not.
  • The third part (i++ in this example) is the step-expression, which will be executed after each execution of the loop-scope.
  • Another difference to while where parenthesis are not necessary, in for, te parenthesis around these three parts are necessary.
  • Otherwise for is identical to while

break

Break is a simple keyword used to break a loop immediately.

while (true) {
  break; // The loop only enters once, then leaves.
}
  • The break must end with a semicolon;.
  • If there is no loop and break is called, a error occurs.

continue

Continue is a simple keyword to skip the rest of the loop's body.

while (true) {
  continue;
  print("Hello!"); // This code is never reached.
}
  • The continue must end with a semicolon;.
  • If there is no loop and continue is called, a compile-time error occurs.

unwrap

Unwrap is a keyword used to unwrap an optional variable.

let optional: i32? = some_number as i32;
let number: i32 = unwrap optional;
  • unwrap must be followed by an optional variable. If an un-optional variable is given to unwrap, an exception occurs.
  • there must be a space between unwrap and the optional value.

some

Some is a keyword used to wrap a variable to create an optional variable.

let number: i32 = some_number;
let optional: i32? = some number;
  • Some must be followed by a variable. The variable is then wrapped into an optional and the optional is returned.
  • There must be a space between the some and the variable.

empty

Empty is the keyword used to set an optional as empty (to not contain a value).

let optional: i32? = empty;
if (optional?) {
  // Never executed
} else {
  // Executed, since optional is empty
}
  • Like true or false, empty is used as a value, except it can only be used in an assignment operator
  • Trying to set a non-optional value as empty will cause an exception.

as

As is the keyword used when you need to cast a variable to another. It will return the casted result as an optional which will be empty if the cast failed.

let long: i64 = 5;
let int_opt: i32? = long as i32;
let int = 0;
if (int_opt?) {
  int = unwrap int_opt;
} else {
  // Cast failed
}
  • Before as there must be a variable, or a value, and after there must be the type which the value is attempted to be cast as.
  • If the rightside of the operator is not a type, a parse§ time error occurs.

Built-in functions

print(text: string)

Prints text to standard (stdout, to console by default). This is configurable by changing stdout in the Reid VM.

floor(number: T) -> T

Floors number (rounding downwards), where T is either f32 or f64, and then returns the value of the floor of that same type.

ceil(number: T) -> T

Ceils number (rounding upwards), where T is either f32 or f64, and then returns the value of the ceil of that same type.

round(number: T) -> T

Rounds number, where T is either f32 or f64, and then returns the value of the round of that same type.

sqrt(number: T) -> T

Returns the square root of number, where T is either f32 or f64.

pow(number: T, exponent: N) -> T

Returns the power of number to the exponent of exponent, where T and N is either i16, i32, i64, f32 or f64.

random() -> f32

Returns a random number between 0 and 1.

time_now() -> i32

Returns the current time in milliseconds since January 1st 1970.

random64() -> f64

Functions like random() -> f32, except it returns a more accurate f64.

time_now64() -> i64

Returns the current time in nanoseconds since January 1st 1970.