Skip to content

Latest commit

 

History

History
336 lines (249 loc) · 8.64 KB

README.md

File metadata and controls

336 lines (249 loc) · 8.64 KB

YAIL: Yet Another Interpreted Language

Yail is a simple interpreted language built with Go.

Quick Start (for Mac)

Run the following command in your terminal for quick installation.

curl -o /usr/local/bin/yail 'https://raw.githubusercontent.com/bugoverdose/yail/master/build/yail' && chmod 755 /usr/local/bin/yail

After installation, you can just run yail anywhere to start the interactive mode. To exit the interactive mode, just type q or exit command and press enter. Copy and paste each of the lines below to if you are stuck.

yail
val x = "Hello World!";
x;
exit

If you want to delete the program, just run the command below.

rm -rf /usr/local/bin/yail

Variables

Identifier format

The name of the variables, or identifiers, must follow the following rules.

  1. The identifier should be a string with at least one character.
  2. The first character should be an alphabet(a-Z) or an underscore(_).
  3. The rest of the characters should be alphabets(a-Z), numbers(0-9), or an underscore(_).
  4. The reserved keywords used by Yail can not be used as an identifier(var, return, etc).
// allowed
var a = 1;
var _ = true;
var a_b_2 = 3;

// not allowed
var 1a = 1; // starts with a number
var a b = 2; // whitespace included
var val = 3; // val is a keyword
var 변수 = 4; // non-ASCII characters are not allowed

Variable binding

There are two ways of assigning local variables, using the var and val keywords. The basic rule is similar to Kotlin. But there are some differences.

  1. You can't specify the type of the variable.
  2. It's possible to reassign the same declared identifier with different data types.
  3. Each assignment statement must end with a semicolon(;).
var a = 5;
val b = 20;

val c = 10; // [ERROR] missing token: ;

The var keyword stands for variable assignment. Variables defined with the var keyword can be reassigned. Remember to omit the val and var keyword for reassigning already declared identifiers.

var a = 5;
a = 10;
a = true;

var a = 20; // [ERROR] given identifier 'a' is already declared

Read-only local variables are defined using the val keyword, which stands for value assignment. If you try to reassign the value-assigned variable with another value, the interpreter will throw an error during the evaluation process.

val b = 10;
b = 20; // [ERROR] can not reassign variables declared with 'val'

Data types

Currently, Yail has six data types: integer, boolean, string, array, hash map and null.

As mentioned above, variables defined with the var keyword can be reassigned with a different data type.

val x = 5;
val y = true;
var z = null;
z = 10;
z = false;

Basic Operations

All the following operations are supported.

  • basic arithmetic operations(+, -, *, /, %) for integers
  • negative prefix(-) for integers
  • basic comparison operations(==, !=, <=, >=, <, >)
  • not prefix(!) for reversing a boolean
  • grouping(()) for changing the priority of operations
5 + 5; // 10
5 - 5; // 0
5 * 5; // 25
5 / 5; // 1
5 % 5; // 0

5 == 5; // true
5 != 5 // false
5 <= 5 // true
5 >= 5 // true
10 > 5 // true
10 < 5 // false

!false; // true

1 + 2 * 3; // 7
(1 + 2) * 3; // 9

Strings

A string is a sequence of characters wrapped by quotation marks("). Any ASCII characters can be used as content, even if it can not be used for identifiers.

+ operator can be used for concatenating multiple strings to return a new string. Also, builtin function len can be used for counting the length of the string, which is the number of characters including all the whitespace.

val a = "Hello";
val b = "World!";
val c = a + " " + b;
c; // Hello World!

len(""); // 0
len("abc123#$%^"); // 10

Arrays

An array is a list of elements wrapped by brackets([, ]). Any type of data can be used as an element and arrays in Yail can contain elements with different data types.

Each element can be accessed based on its index. If the given index is out of range, null would be returned instead of throwing an error.

val arr = [1, "two", 3 + 3];

arr[0]; // 1
arr[1]; // "two"
arr[2]; // 6
arr[3]; // null
arr[-1] // null

Many builtin functions are supported for arrays.

  • len returns the number of elements in the array.
  • head returns the first element in the array without changing the array.
  • tail returns the last element in the array without changing the array.
  • push and pushleft each adds a new element as the new last or first element in the array.
  • pop and popleft each removes the last or first element in the array.
val arr = [1, 2, 3];
len(arr); // 3
head(arr); // 1
tail(arr); // 3
arr; // [1, 2, 3]

push(arr, 10); arr; // [1, 2, 3, 10]
pushleft(arr, 20); arr; // [20, 1, 2, 3, 10]
pop(arr); arr; // [20, 1, 2, 3]
popleft(arr); arr; // [1, 2, 3]

Hash Map

A hash map consists of multiple key-value pairs wrapped by curly brackets({, }). Only hashable data types can be used for keys, which are strings, integers, and booleans. Any data type can be used for values including arrays, functions, and another hash maps.

Each value can be accessed based on the corresponding key. If the given key does not exist, null would be returned instead of throwing an error.

val map = { "one": 1, true: [10, 20, 30], 100: { "a": 1, "b": 2 }, "f": func(x) { x * x }};

map["one"]; // 1
map["two"]; // null;

map[true]; // [10, 20, 30]
map[true][0]; // 10

map[100]; // { a: 1, b: 2 }
map[100]["b"]; // 2

val square = map["f"];
square(10); // 100

Conditional Expressions

Basic if and else keywords are supported. When the conditions are met, multiple statements inside a selected block are consecutively executed.

if (true) { val x = 10; }
x; // 10

if (5 > 10) { val a = 10; } else { val b = 15; val c = 20; }
a; // [ERROR] identifier not found: a
b; // 15
c; // 20

It's important to understand that if and if-else statements are actually expressions because they always return a value.

  1. If a block is executed, the value of the last expression is returned.
  2. If the executed block ends with a statement, it returns null.
  3. If no block is executed, the if expression returns null
val x = if (false) { 10 } else { 20 };
x; // 20

var y = if (false) { 10 };
y; // null

val z = if (true) { y = 15; };
z; // null

Functions

To support functional programming, all functions are first-class citizens in Yail.

This means that they are expressions, so you must assign the function to an identifier to call them.

val f = func(x) { x + 3; };
f(5); // 8
f(6); // 9

This also means that it's possible to implement higher order functions, functions that take another functions as arguments or return a function.

val callTwoTimes = func(x, f) { 
    f(f(x)); 
};

callTwoTimes(2, func(x) { x * x; }); // 16
callTwoTimes(3, func(x) { x * x; }); // 81
callTwoTimes(1, func(x) { x + 10; }); // 21

Scopes

When you try to use an identifier inside a function body, evaluator looks up the identifier following these steps.

  1. If it's the name of a parameter or a variable declared inside the function, the evaluator uses the bound value.
  2. If it's not one of them, it searches the outer scope.
  3. The 2nd step is repeated until it reaches the outermost scope.
var i = 5; 
val useLocalVariableI = func() {
    return i;
};
val useLocalVariableInsideFunction = func() {
    val i = 15;
    return i;
};
val returnParameterI = func(i) {
    return i;
};

useLocalVariableI(); // 5
useLocalVariableInsideFunction(); // 15
returnParameterI(10); // 10
i; // 5

i = 30;

useLocalVariableI(); // 30
useLocalVariableInsideFunction(); // 15
returnParameterI(10); // 10
i; // 30

However, you can not reassign the variables that was declared at the outer scope from inside the function.

var i = 5; 
val reassignFunc = func() {
    i = 10; 
}; 
reassignFunc(); // [ERROR] identifier not found: 'i'

Closures

Yail also supports closures, functions that captures the environment on the moment it was declared.

For example, closure function captures the environment that binds the x identifier to the argument 2 when higherOrderFunc function is called. After that, when closure function is called, it uses the captured environment instead of using the environment of the scope it is being called. Therefore, closure function still interprets x as 2, even though x is bound 1000 on the scope it is being called.

val higherOrderFunc = func(x) {
    func(y) { x + y };
};

val closure = higherOrderFunc(2);
val x = 1000;
closure(5); // 7
x; // 1000