Introduction and Inspirations

What is Bleach and why it was created?

Bleach is a programming language designed to give Computer Science students a more interesting and rewarding experience while taking their first "Introduction to Compilers" course.

The language was implemented with this purpose due to the fact that, based on my personal experience and on certain studies (which I will cite at the end of the chapter), such courses tend to be too much focused on its theoretical aspects to the detriment of its practical ones.

Therefore, it's common for students that are taking this course to sometimes find it boring or even uninteresting and demotivating.

Based on this, the motivation to build Bleach was born. The cornerstone idea behind Bleach is: a language that can be used in a classroom environment to teach the most fundamental ideas and concepts of Programming Languages implementation in an incremental way, using well-known languagues and techniques as a basis to such objective.

A glimpse on Bleach's features

High-Level

Since an introductory Compilers course generally lasts 1 semester in most CS programs, Bleach can't be a big or complex programming language like Java, Rust, C++. Instead, it must be compact while also having some core features that any minimally functional language has.

Usually, when most people (myself included) think about a small but useful language, some options come to mind. To be more precise, two options come to mind: JavaScript, Lua and Scheme.

What all of these options have in common? All of them are considered to be "high-level" scripting programming languages. (I'll explain below why choose such type of language to implement).

When it comes to syntax, Bleach looks most like JavaScript. This was an intentional choice. Since JavaScript has its syntax rooted in C, I thought it would a good idea to follow this approach because most CS students have already worked (at least a little) with these two languages. Thus, familiarity would kick in.

However, you will notice, as you go through this book, that Bleach also has similarities with Java, Lox, Python, Ruby and Rust when it comes to syntax.

Dynamically Typed

Here is the first reason to make Bleach a "high-level" scripting language. Our three inspirations that fit in this category are all dynamically typed languages.

If you don't remember (or don't know) what a dynamically typed language is, don't get nervous. A dynamically typed language is simply a programming language where the variables don't store type information. Instead, the value stored inside a variable holds the type information. This essentialy implies two things:

  • Variables are able to store a value of any type.
  • Variables are able to store values of different types at different points in time.

In contrast to dynamically typed programming languages, there are also statically typed ones. This begs the question:

Why not make Bleach a statically typed one?

The answer is straightforward: If I decided to make it static, I would need to implement a static type system for it and this is simply too much work to learn and implement. Moreover, type systems are no joke. There is a reason this subject has its own course in master's or doctoral programs. This being said, I think it's obvious to conclude that such a thing could not be teached in a complete way in an undergraduate and introductory course. So... yeah, if I made this decision, I would be contradicting myself about Bleach's reason to exist, which is (in case you don't remember) teach the most fundamental ideas and concepts of Programming Languages implementation. Thus, I didn't follow design decisions that led to advanced ideas and concepts in the area of ​​compilers and programming languages

Automatic Memory Management

One of the reasons that motivates the creation of "high-level" scripting languages is the need to free programmers from the burden of manually managing memory (Yes, I am looking at you: malloc, calloc, realloc and free).

Since Bleach fits into the category of "high-level" scripting languages, it's not a language with manual memory management. Instead, it has an automatic one.

Essentialy, this means that Bleach's runtime () will handle the allocation and deallocation of memory for us. Since this implementation of Bleach is a Tree-Walk Interpreter, things work in a different way than a Garbage Collector, for example. Yes, this implementation doesn't contain one written from scratch.

Briefly explaining, in a Tree-Walk Interpreter an AST (Abstract Syntax Tree) will be generated by the parser. After some static analysis steps, the AST will be given to the interpreter, so it can execute the code represented by the AST.

In such type of interpreter, the execution of code simply means that a traversal through the generated AST will be done. When doing such traversal, two things happen:

  • Dynamic Memory Allocation: When the interpreter creates objects (such as lists, dicts, instances of classes or other complex data structures), it allocates memory for these objects dynamically at runtime.
  • Garbage Collection: In most modern interpreter implementations, especially in high-level languages like those typically used for Lox (such as Java or Python), garbage collection is used to manage this dynamically allocated memory. Right now, you might be wondering "Hey, but this implementation uses C++ and such language is not garbage collected. So, how can Bleach be?". The answer is simple: We are using C++17 to implement such interpreter and this version of C++ has lots of different features that allows us to write code that don't require us to manually manage memory. Everything is done automatically. If you want to learn more about it, search for C++ smart pointers and automatic memory management features like: std::shared_ptr, std::unique_ptr, std::weak_ptr, std::make_shared and std::enable_shared_from_this.

Data Types

Overview

In short, Bleach's built-in types are just the 5 listed below (4 built-in types have been implemented by now):

Scalar Types:

  • bool
  • num
  • nil

Compound Types:

  • str (Work in progress)
  • list (Not implemented yet)

It's important to mention that such built-in types are divided into two groups: scalar types and compound types. I'll walkthrough each of such groups and their respective types, giving brief explanations and examples.

Scalar Types

A scalar type is a type that can only represent a single value. As seen above, Bleach has three primary scalar types: bool, num and nil. You may recognize these from other programming languages. Let’s see how they work in Bleach:

Type: bool

The Boolean type. Such type is used to perform logical operations and is vastly used in logic and Boolean algebra.

In Bleach, things are not different, we have two possible values for such type (and, obviously, a literal for each value):

true
false

Side Note: Truthy and Falsey values

Bleach, like Ruby and many other programming languages, has the concepts of truthy and falsey values to determine the truthiness or falseness of values when evaluating conditions, such as in if statements, do-while loops, for loops, while loops and ternary operators (?).

Basically, this means that values of any type (built-in or user-defined) can be used where a value of bool type is expected.

In short, Bleach follows this convention:

  • Falsey values: false, nil.
  • Truthy values: Any other value that is not false nor nil.

Type: num

For the sake of simplicity, Bleach has only one type to represent numbers: the num type.

Behind the scenes, such type is implemented using a double-precision floating point number (The C++ double type).

I implemented it this way because double-precision floating point numbers can also represent a wide range of integers, covering a lot of territory, while maintaining the simplicity that I innitialy envisioned.

Usually, mature and popular programming languages have lots of syntax for numbers (binary, hexadecimal, octal, scientific notation, etc).

Since Bleach's purpose is to be a programming language , we'll settle for basic integer and decimal literals, like the ones shown below:

2.71828
3.14159
23

Type: nil

This is an old friend to many of us.

The nil type is a type that has only one value (nil). It conveys the idea of “no value” or "absence of a value".

nil

In other programming languages, such type is called null, nil, NULL or nullptr.

Here, in Bleach, we are going to follow Ruby's influence and use nil to denote this idea. This will help us distinguish between Bleach's nil value and C++ nullptr value (which is the equivalent value in the underlying implementation language).

I know what you might be thinking right now... There are lots of arguments for not having a null value in a language since null pointer errors have been causing a lot of headache since Tony Hoare introduced this idea back in the 60s.

However, according to Robert Nystrom (author of the "Crafting Interpreters" book and creator of the Lox programming language, which are both the major references Bleach where got its inspiration), if you want to implement a statically-typed language, it would be worth trying to ban NULL values. However, in a dynamically-typed language (which is Bleach's case), eliminating it is often more annoying and troublesome than allowing it. So I opted to follow Nystrom's advice on this matter.

Compound Types

Compound types are basically types that can group multiple values into one. Bleach has two primitive compound types: str and list. Let's take a look at them:

Type: str

Again, nothing new here. If you have some experience with programming you already recognize this type. In Bleach, the str type represents an indexed-sequence of characters (a string), typically used to store and manipulate text.

Some examples of literal values of this type:

"I am a string";
""; // The empty string.
"123"; // This is a string, not a value of type "num".

There are some aspects of this type that might differ from what you have seen in the previous languages you have worked with. Thus, I think it's a good idea to explain such aspects in more details:

It's a sequence type. This means that value of the str type can be indexed. Indexing allows you to access individual characters from the value (which, in Bleach, are also values of type str).

In Bleach, literals values of this type are always enclosed by double quotes.

Finally, this type has the following methods associated with it (None of them have been implemented yet):

  • clear: Returns nil. This method cleans the contents from the value of str type. This change is made in-place.
  • empty: Returns a bool value that signals whether the str value is empty or not.
  • find: Returns a num value that identifies the index where there is the first occurrence of a provided substring (a value of type str) inside another value of type str. If the provided substring doesn't appear, the method returns -1.
  • length: Returns a num value that represents the number of characters in the str value.
  • pop_back: Returns a str value. This method removes the last character from the str value.
  • push_back: Returns nil. This method adds another value of type str at the end of the str value.
  • size: Returns a num value that represents the number of characters in the str value.
  • substr: Returns a value of type str which is the substring of another value of type str. Such method expects two indexes (left, right), both of which are inclusive.

Type: list

If you are familiar with Python, then here it is an old friend of you. In Bleach, the list type represents an indexed-sequence of elements.

Some examples of literal values of this type:

[0, 1, 2, 3, 2.71, 3.14159]; // A list where all elements are of type "num".
["hello", "there"]; // A list where all elements are of type "str".
[]; // An empty list.
[false, true]; // A list where all elements are of type "bool".
[nil, nil, nil]; // A list where all elements are of type "nil".
[false, "Brazil", 9.98, true, nil]; // A list where elements are of different types. This is allowed in Bleach.

There are some aspects of this type that might differ from what you have seen in the previous languages you have worked with. Thus, I think it's a good idea to explain such aspects in more details:

It's a sequence type. This means that lists can be indexed. Indexing allows you to access individual elements from the list.

Finally, this type has the following methods associated with it (None of them have been implemented yet):

  • back: Returns the last element of a value of type list. However, it does not make any changes to the value of type list.
  • clear: Returns nil. This method cleans the content from the value of list type. This change is made in-place.
  • empty: Returns a bool value that signals whether the list value is empty or not.
  • front: Returns the first element of a value of type list. However, it does not make any changes to the value of type list.
  • pop_back: Returns the last element of a value of type list. This method removes the last element from the list value.
  • push_back: Returns nil. This method adds an element of any type (whether it's a built-in or user-defined one) at the end of the list value.
  • size: Returns a num value that represents the number of elements in the list value.

Comments

Overview

Ideally, every programmer tries its best to make their code readable and understandable. However, there might be scenarios in which extra explanation is very welcomed.

In such scenarios, programmers leave comments in their code that the compiler/interpreter will ignore but people reading the code may find useful.

In Bleach, a programmer is allowed to write two different types of comments inside the code present in a Bleach file (.bch).

Single-Line Comments

In Bleach, a single-line comment starts with two slashes (//), and the comment continues until the end of the line, just like the example below:

// Here you can put some extra information that the person reading your code might find useful.

For comments that extend beyond a single line, you’ll need to include // on each line, like this:

// This is the beginning of what it's going to be a very long comment.
// Instead of making the user go all the way to the end of the line above
// we can divide comments in different lines. However, always needing to use
// // character seems to be very cumbersome.

Remember that comments can also be placed at the end of lines containing code:

let x = 123.45; // It's allowed to put a comment after a line of code.

However, Bleach, as Rust, recommends that the comment should appear on a separate line above the code it’s annotating:

// The variable below holds an approximate value of the constant Pi.
let pi = 3.14159;

Multi-Line Comments

For the reason mentioned above, Bleach also has support for multi-line comments.

In Bleach, a multi-line comment has a beginning and also and ending. The beginning is denoted by a /*, while the ending is denoted by a */. Everything that is written between these characters is considered a comment and, therefore, will be ignored by the Bleach Interpreter during runtime.

The example below shows how to use them properly:

/*
This
is
a
multi-line
comment.

Below, we are storing an
approximation of Euler's
constant inside the variable "e".
*/
let e = 2.71;

Variables

Overview

As you might already know, in a programming language, a variable is just a name associated with a storage location in memory. Such storage location is responsible for holding data that can be changed during the execution of a program.

Another way to think about this is that variables allow us, developers, to store, retrieve and manipulate data by using the variable's name instead of needing to interact with the memory address where variable's data is at.

Variables in Bleach

The first point that is important to mention when it comes to variables in the Bleach language is that all of them are mutable.

Which means that, once a variable is declared, the programmer is allowed to perform any amount of assignments later on the program that is being written.

The second point that must be mentioned is that there is no concept of constants in Bleach. There are just mutable variables, as explained above.

The third point is that, in order to declare a new variable in Bleach, the programmer must use the let keyword:

let variable = "A str value";

The fourth one is that, if during the declaration of a variable, an initializer is not provided, then, by default, the variable will store the nil value inside itself:

let someVariable;
print someVariable; // nil

The last point, and maybe the most important of them all, is that since Bleach is a dynamically-typed programming language, its variables don't have types associated with them. Instead, it's the values that have types. The most important implication of this is that a variable can hold a value of any type at different points in time.

In practice, Bleach allows the code snippet below with no problems:

let a = "hello";
print a; // "hello"

a = 42;
print a; // 42

Global Variables

In case you need a refresh, a global variable is just a variable that has been declared outside of all functions, methods or classes, making it accessible from any part of the program.

Unlike local variables, that are confined to the scope in which they are declared (e.g., within a function), a global one has a larger scope and, thus, can be accessed, modified, or referenced from anywhere in the program, including within functions, methods or other code blocks.

One point that makes Bleach kind of unique is the fact that the programmer is allowed to redeclare a global variable anytime. This decision was made considering the fact that Bleach's interpreter is not only restricted to execute Bleach files, but can also run in a REPL mode and keeping track of which global variables were declared in such scenario is kind of annoying most of the times. Thus, I opted to make this implementation decision.

Essentialy, this means that the line of code below is perfectly valid:

let pi = 3.14159;
// write some code in the global scope.
let pi = 9.51413;

Local Variables

Refreshing your mind: a local variable is a variable that is declared within a specific block of code, such as a function, method, an if statement or a loop statement. By the way, this is exactly how local variables work in Bleach.

The scope of a local variable is limited to the block in which it is defined, meaning it can only be accessed and used within that block.

Once the block of code finishes executing, the local variable is typically destroyed, and its memory is released.

The code snippet below shows an example of a variable that been declared inside a function and, thus, is an example of a local variable:

function foo(){
  let bar = "This is a local variable";
  print bar;

  return;
}

foo();

Variable Assignment

As shown above, a variable in Bleach can be declared through the use of the let keyword.

Moreover, since there is no idea of immutability of variables in Bleach, it's allowed to assign different values of different types at different points in time to the declared variable. To do that, the programmer uses the assignment operator =.

However, I would like to present some interesting semantics related to variable assignment in Bleach.

The first one is that, in Bleach, an assignment is an expression. Not a statement. This has some interesting consequences.

For example, since a variable assignment is an expression, it produces a value. Such value is the one that is being assigned to the variable:

let foo;
print foo = 20; // 20

The second one is that it's completely possible to assign a value to more than one variable at once in the same assignment expression:

let x = 20;
let y = 42;

x = y = 13;

print x; // 13;
print y; // 13;

Variable Shadowing

In case you are not familiar (or don't remember), variable shadowing is a process that commonly happens in programming.

It's an effect where a variable declared within a certain scope (typically a local scope) has the same name as a variable in an outer scope.

Then, the inner variable "shadows" or "hides" the outer variable within the scope where it is declared, meaning that within that scope, the inner variable takes precedence, and the outer variable cannot be directly accessed.

The shadowing effect occurs because, before the program actually starts its execution, usually compilers an interpreters (and this includes the Bleach interpreter) do a static-time pass through the source code called "resolving". In this pass, variables are resolved by searching through scopes, starting from the innermost scope and moving outward. If a variable is found in an inner scope with the same name as one in an outer scope, the inner variable is used, effectively "shadowing" the outer one.

Pay attention to the fact that the shadowing effect is limited to the scope of the inner variable. Once the program exits that scope, the outer variable becomes "visible" to the program again.

The example below shows a clear example of variable shadowing:

let a = 42;

print a; // 42

{
  let a = "Hello, there!";
  print a; // "Hello, there!" --> In this scope, the variable a that holds the value "Hello, there!" hides the other one that has the same name but stores a different value: 42.
}

print a; // 42

Another example, now involving functions:

let foo = "hi";

print foo; // "hi"

function f(){
  let foo = 42;
  print foo; // 42;

  return;
}

f();

print foo; // "hi"

Operators

Overview

As you also already know, operators in a programming language are symbols that are responsible for executing an operation on one or more operands (values or variables) in order to produce a result.

Operators are fundamental building blocks in a programming language, allowing the programmer to manipulate data, perform calculations, compare values, and much more.

In this page, we'll see what operators Bleach provides to us and how each of them behave and, at the end, we'll also see what is the precedence of each of these operators.

Unary Operators

An unary operator is, as its name suggests, an operator that expects just one operand. Bleach has two operators that fall in this category.

Arithmetical

Negation (-): Negates the value of an operand of type num.

let number = 5;
print -number // -5;

Logical

Not (!): Inverts the value of an operand of type bool.

let b = true;
print !b; // false
print !!b; // true

Binary Operators

A binary operator is, as its name suggests, an operator that expects just two operands. Bleach has 12 operators that fall in this category.

Arithmetical

Addition (+): This operator expects two operands. However it behaves differently depending on the types of the two received operands.

  • Left operand (num) and Right operand (num): Adds the second (right) operand operands to the first (left) operand.
print 2 + 3; // 5
print 2.71 + 3.14159; // 5.85159
  • Left operand (str) and Right operand (str): Concatenates the second (right) operand to the first (left) operand.
print "hello," + " there!"; // "hello, there!"
print "a" + "b"; // "ab"
  • Left operand (num) and Right operand (str): Converts the first (left) operand from the num type to the str type. Then, concatenates the second (right) operand to the first (left) operand, producing a new value of type str.
print 2 + "two"; // "2two"
  • Left operand (str) and Right operand (num): Converts the second (right) operand from the type num to the type str. Then, concatenates the second (right) operand to the first (left) operand, producing a new value of type str.
print "two" + 2; // "two2"

Subtraction (-): This operator expects two operands of type num. It subtracts the second (right) operand from the first (left) operand.

print 5 - 3; // 2

Multiplication (*): This operator expects two operands of type num. It multiplies the first (left) operand by the second (right) operand.

print 1.5 * 4; // 6

Division (/): This operator expects two operands of type num. It divides the first (left) operand by the second (right) operand.

print 5 / 2; // 2
print 1 / 3; // 0.333333333333333

Comparison/Relational

Greater Than (>): This operator expects two operands of type num. It checks whether or not the first (left) operand is greater than the second (right) operand.

print 5 > 2; // true
print 1 > 3; // false

Greater Than or Equal (>=): This operator expects two operands of type num. It checks whether or not the first (left) operand is greater than or equal to the second (right) operand.

print 5 >= 2; // true
print -1 >= -1 // true
print 1 >= 3; // false

Lesser Than (<): This operator expects two operands of type num. It checks whether or not the first (left) operand is lesser than the second (right) operand.

print 5 < 2; // false
print 1 < 3; // true

Lesser Than or Equal (<=): This operator expects two operands of type num. It checks whether or not the first (left) operand is lesser than or equal to the second (right) operand.

print 5 <= 2; // false
print 0 <= 0; // true
print 1 <= 3; // true

Equality

Equal (==): This operator expects two operands of the following built-in types (bool, nil, num, str). It checks whether the values are of the same type and, if that's the case, checks whether such values are the same.

print 2 == 2; // true
print 2 == (1 + 1) // true
print 2 == 3; // false
print "hello" == "hello"; // true
print "hello" == "hell"; // false
print 2 == nil; // false
print nil == nil; // true
print true == true; // true
print true == false; // false
print true == !!true; // true

Not Equal (!=): This operator expects two operands of the following built-in types (bool, nil, num, str). It checks whether the values are of the same type (if they are not of the same type, such comparison returns false) and, if they are of the same type, it then checks whether such values are not the same.

print 2 != 2; // false
print 2 != (1 + 1) // false
print 2 != 3; // true
print "hello" != "hello"; // false
print "hello" != "hell"; // true
print 2 != nil; // true
print nil != nil; // false
print true != true; // false
print true != false; // true
print true != !!true; // false

Logical

And (and): This operator returns true if both of its operands are truthy values. Otherwise, it returns false. Remember that this operator performs short-circuiting when possible.

print 5 and 2; // true
print 5 and false; // false
print false and nil; // false

Or (or): This operator returns true if one of its operands are truthy values. Otherwise, it returns false. Remember that this operator performs short-circuiting when possible.

print 5 or 2; // true
print 5 or false; // true
print false or nil; // false

Ternary Operator

An unary operator is, as its name suggests, an operator that expects just three operands. Bleach has just one operator that fall in this category.

Ternary (? :): This operator is essentialy a concise way to perform conditional operations in a programming language. It is used to evaluate a condition and return one of two values based on whether the condition is true or false. As previously seen, the ternary operator is called "ternary" because it operates on three operands.

print 2 == 2 ? "2 is equal to 2" : "2 is not equal to two"; // "2 is equal to 2"

Operator Precedence

Below, there is a scheme that shows us the precedence of all of the presented operators when a expression in being evaluated in a Bleach program at runtime.

In the list shown below, the smaller the index is, the higher the precedence of such operator is. Operators present in the same index have the same precedence and will be evaluated from left-to-right as they appear in an expression.

PrecendenceOperator
1!, - (unary)
2*, /
3+, - (binary)
4>, >=, <, <=
5==, !=
6and
7or
8? : (ternary)
9= (assignment)

Control Flow Structures

Overview

Control Flow Structures are constructs that dictate the order in which statements are executed. They allow the program to make decisions, repeat operations, and manage the flow of execution based on certain conditions.

Conditional Statements

Such statements allow the program to execute certain blocks of code based on whether a condition evaluates to true or false.

if Statement

The if statement is one of the fundamental control flow structures in programming.

It allows the program to execute a block of code only if a specified condition is true.

If the condition is false, then the code block associated to the if is not executed, and alternative blocks of code can be executed by using elif clauses or an else clause.

Remember that, during runtime, the clauses of an if statement are evaluated from top to bottom. Also, once the condition of a clause is met, its associated block will be executed and then the rest of clauses won't even be looked at by the interpreter. Instead, it goes straight forward to the next statement. Bleach is no exception in this matter.

Also remember that is completely possible to nest an infinite amount of if statements in a program. Bleach can handle such scenarios with no issues.

It's important to mention that one does not need to add a block after an if, elif or else clause. This means that the code snippet below executes without any problems:

let foo = "hi";

if(foo == "hi")
  print "Found a 'hi' string!";
elif(foo == "oi")
  print "Fount an 'oi' string!";
else
  print "Found something else inside the 'foo' variable";

Last but not least, Bleach, different from some other programming languages, allows the programmers to declare variables inside the code blocks associated to the clauses of an if statement. If you do this, you must keep in mind that the scope of such variable will be restricted to the block associated to the specific clause.

Simple example of a code snippet that uses an if statement:

let number = 2;

if(number == 3){
  print "The value of 'number' is 3.";
}elif(number == 2){
  print "The value of 'number' is 2."; // "The value of 'number' is 2."
}else{
  print "The value of 'number is something else.";
}

Another example of a code snippet that also uses an if statement. However, now there are nested if statements:

let n = 42;
let s = "Meaning of life";

if(n == 42){
  if(s == "Meaning of life"){
    print "The value of n is 42 and the value of s is not 'Meaning of life'";
  }else{
    print "The value of n is 42 and the value of s is not 'Meaning of life'";
  }
}else{
  print "The value of n is not 42";
}

Loops

Loop statements are used to repeat a block of code multiple times, either a fixed number of times or until a certain condition is met.

One important thing to mention is that, for all types of loop structures, there is support for early exiting (with the break keyword) and iteration skipping (with the continue keyword).

While

This one is a control flow structure present in almost every programming language that allows the programmer to repeatedly execute a block of code as long as a specified condition remains true.

It is typically used when the number of iterations is not known beforehand and the loop should continue while a certain condition is met. When such condition is not met anymmore, then the execution of the loop terminates.

Also remember that the condition of a while loop is evaluated before each iteration of the loop. Which means that, in some cases, the while loop might not even execute.

Another thing that is important to mention is that the programmer can use a variable of any type as the condition of a while loop.

As we've seen, each value, no matter its type is considered either "truthy" or "falsey". In practice, this means that you don't need to necessarily only use values of type bool or expressions that evaluate to such type.

One important peculiarity of while loops in Bleach is that they need a block after the condition. In practice, this means that the following code snippet will not even execute:

let counter = 0;

while(counter < 10) // No block present.
  counter = counter + 1;

However, the following code snippet executes without any problems:

let counter = 0;

while(counter < 10){ // A block is present.
  counter = counter + 1;
}

Do-While Loop

This is another a type of control flow structure very similar to the while loop present above and that is ubiquitous in programming languages.

A do-while executes a block of code at least once before checking its condition.

That is key difference between a do-while loop and a regular while loop: the condition in a do-while loop is checked after the loop’s code has executed, not before. This guarantees that the loop’s code will always run at least once, even if the condition evaluates to the false value. This might sound problematic, but there are some niche scenarios where this behavior comes in handy.

Another thing that is important to mention is that, as the programmer could use a variable of any type as the condition of a while loop, he/she/they can do the same thing in a do-while loop.

As we've seen, each value, no matter its type is considered either "truthy" or "falsey". In practice, this means that you don't need to necessarily only use values of type bool or expressions that evaluate to such type.

Last, but not least, as the while loops require, the do-while loops in Bleach need a block between the keywords do and while. In practice, this means that the following code snippet will not even execute:

let counter = 0;

do
  counter = counter + 1;
while(counter <  10);

On the other hand, the following one will execute without any issues:

let counter = 0;

do{
  counter = counter + 1;
}while(counter < 10);

For Loop

Finally! Here is the last kind of control flow structure that fits into the loops category. The for statement is a control flow structure that allows the programmer to repeatedly execute a block of code not only by a specific number of times but also until some condition evaluates to true.

Unlike the while and do-while loops, a for loop is particularly useful in scenarios where the developer knows in advance how many times he/she/they want such loop to iterate.

In case you don't remember the structure of a for loop, let's do a simple refresh: A for loop has 4 components, which are:

  • Initialization: This is where you declare and initialize a loop control variable. This happens only once, at the beginning of the loop. In Bleach, the programmer is also allowed to put an expression statement in this place.
  • Condition: This is a logical expression that is evaluated before each iteration of the loop. If the condition evalutes to true, the loop continues. Otherwise, if it evaluates to false, the loop finishes its execution.
  • Increment: This is an expression that updates the loop control variable after each iteration. It usually increments or decrements the loop variable. To be honest, this can be any kind of expression. Not necessarily an expression that updates the loop control variable of the for loop.
  • Code Block: The block of code that will be executed each time the condition is true.

It's also important to mention that the first three components might be absent inside the structure of a for loop.

In short, this is the expected structure of a for loop in Bleach:

for(initialization; condition; increment){
  // the code of the block goes here.
}

Last, but not least, as the while and do-while loops require, the for loops in Bleach need a block between the keywords after its ) character. In practice, this means that the following code snippet will not even execute:

for(let counter = 0; counter < 10; counter = counter + 1)
  print counter;

On the other hand, the following one will execute without any issues:

for(let counter = 0; counter < 10; counter = counter + 1){
  print counter;
}

Branching

Branching statements allow the program to jump to a different part of the code based on certain conditions during runtime.

In Bleach, there are just two keywords that allow the user to execute such statements:

  • break
  • continue

The statements that use these keywords are used to control the flow of loops (for, while and do-while) statements.

They provide mechanisms that allow the programmer to modify the normal flow of loop execution based on particular conditions. This is useful when the developer wants to exit a loop early based on a dynamic condition instead of waiting for the loop to naturally complete all its iterations.

The break keyword/statement

This statement is used to immediately exit a loop.

When the break statement is encountered (break;), the program stops the execution of the innermost loop that contains such statement and goes on to the first line of code after such loop block.

Usage of the break statement inside a loop in Bleach:

for(let counter = 0; counter < 10; counter = counter + 1){
  if(counter == 9){
    break;
  }
  print counter;
}

The output of the code snippet above will be:

0
1
2
3
4
5
6
7
8

The continue keyword/statement

The statement is used to skip the remaining code in the current iteration of a loop and immediately proceed to the next iteration.

It does not exit the loop. Instead it moves the control back to the top of the loop, where the next iteration begins.

A continue statement is normally used when the programmer wants to skip certain iterations of a loop based on a condition, but still wants the loop to keep running for the other iterations.

Usage of the continue statement inside a loop in Bleach:

for(let counter = 0; counter < 10; counter = counter + 1){
  if(counter == 0 or counter == 2 or counter == 4 or counter == 6 or counter == 8){
    continue;
  }
  print counter;
}

The output of the code snippet above will be:

1
3
5
7
9

Functions

Overview

A function is a reusable block of code that is usually made by a programmer with the intent to make it perform a specific task.

Functions, as we already know, are fundamental building blocks in programming, allowing programmers to organize, reuse, and manage code more efficiently.

Functions

In other words, a function is a self-contained block of code that can be ran by calling the its name.

Functions can take inputs (or not), process them, and return an output (or not). They help in breaking down complex problems into smaller, manageable tasks.

Below, you can see a code snippet that shows the structure of a function declaration statement in Bleach:

function funtionName(parameter1, parameter2, parameter3){
    // Code to execute
    return value;
}

It's important to mention that if the programmer ommits the return statement from the function declaration statement, then when such function is called, it will, by default, return the nil value.

Function Calls

To execute the function and get the result, you call it by its name and provide the necessary arguments:

function add(a, b){
    return a + b;
}

let a = 2;
let b = 3;

let result = add(a, b);

print result;  // 5

Important limitations that must be mentioned: In Bleach, functions don't have support optional parameters, nor for default value for its parameters, and also cannot be overloaded.

Functions are First-Class citizens

As the title says, in Bleach, function are considered first-class citizens.

This means that functions are treated like any other value in the Bleach:

  • They can be assigned to variables
  • They can be passed as arguments to other functions
  • They can bereturned from functions
  • They can be stored in data structures.

Essentially, functions can be manipulated and used just like any other data type available in the Bleach language.

Anonymous Functions (Lambda Functions)

Bleach also has support for anonymous functions (also known as lambda functions).

If you are not familiar with the concept, then bear with me: An anonymous function is a function that is defined without a name.

In many programming languages, such as C++, JavaScript, Python and Typescrip, these functions are often used for short, simple operations where defining a full named function might be considered an overkill.

Anonymous functions are typically used in situations where a function is required only temporarily, often as an argument to another function.

In Bleach, this is not different.

When it comes down to syntax, Bleach took the Python approach to this problem and added a little twist aiming for more readability. It uses the lambda keyword and also the arrow operator ->.

When it comes to semantics, rest assured. Bleach anonymous/lambda functions are as powerful as the normal ones.

The example below shows how to define an anonymous/lambda function and call it:

let add = lambda -> (x, y){ return x + y; };

print add(10, 5); // 15

Object-Oriented Programming Features

Overview

This means that Bleach not only supports, but also encourages programmers to use the key concepts behind object-oriented programming. Which makes Bleach a multi-paradigm language due to the fact it also follows concepts of procedural paradigm.

In case you don't remember, OOP is a paradigm or style of programming that uses "objects" to represent data and methods to manipulate that data.

The key concepts of OOP include encapsulation, inheritance, polymorphism, and abstraction.

The concepts that were mentioned above are what enable the creation of modular, reusable, and maintainable code by organizing software into objects, entities that are capable of representing both data and behavior.

Classes

A class is basically a blueprint for creating objects (instances).

Usually, a class defines a set of attributes (variables) and methods (functions) that the created objects will have as its disposal.

Classes are a fundamental part of object-oriented programming (OOP) due to the fact that such featus allows the programmer to define custom data types and their associated behaviors based on his/her/their needs.

When it comes down to the concept of classes, there are a couple of concepts that directly tied to it:

  1. Attributes/Fields
  2. Methods
  3. Instances
  4. Inheritance

For the rest of this page, I am going to explain how each of these concepts are handled in Bleach.

In Bleach, a class declaration statement follows the syntax shown below:

class Person{
  method init(name, age){
    self.name = name;
    self.age = age;
  }

  method greet(){
    return "Hello there! " + "my name is " + self.name + " and I have " + self.age + " years.";
  }
}

Attributes/Fields

Usually, in statically-typed programming languages, attributes/fields are variables that belong to the class itself.

Attributes are used to store data that is relevant to the objects that were created from the class.

However, when dealing with dynamically-typed languages (such as JavaScript or Python), this concept is a little bit different.

In Bleach's case, attributes/fields are unique to each instance of a class. This means that there is no concept of class attributes/fields. In other words, there aren't attributes/fields that are shared across all instances of the class and, thus, be affected by a change in an instance.

The Bleach code snippet below shows the flexibility of dealing with attributes/fields when it comes down to classes and instances. The programmer is free to add new attributes/fields after the creation of the instance from a class with no issues:

class Square{
  method init(length){
    self.length = length;
  }
}

let square = Square(5);

square.area = square.length * square.length;
print square.area; // 25

square.perimeter = 4 * square.length;
print square.perimeter; // 20

function compute_square_diagonal(length){
  return std::math::sqrt(2) * length;
}

square.compute_diagonal = compute_square_diagonal;
print square.compute_diagonal(square.length); // 7.071067811865476

Methods

A method is just a function that belongs to a class and is responsible for defining actions and behaviors that objects created from the class can execute at runtime.

Usually, methods typically operate on the data that is stored inside the attributes/fields of an instance and,thus, can modify the state of the instance.

Making it very simple, a method is essentialy a function that is tied to a class definition.

As seen above, in a class declaration statement, if the programmer wants to declare a new method, then he/she/they just need to follow the same syntax of a function declaration statement, but instead of using the function keyword, change it by the method keyword.

The init method: It's just a special method usually present in classes and known as the constructor of a class. It’s automatically called when a new instance of the class is created. This method is where the programmer typically sets up the initial state of an object by initializing instance attributes.

Instances

Essentialy, an instance is an individual object created from a class.

Remember that each instance has its own unique set of attributes/fields, but shares methods with other instances of the same class.

In short, you can think of an instance of just a bag of data that can change during runtime and that has methods associated with it that might operate on such data.

Self

The keyword self serves for a very specific purpose inside the methods of a class: It's a reference to the current instance of the class. It's used to refer to the current instance on which a method is being invoked.

By using it, the progammer is able to access and modify the instance’s attributes and methods. Contrary to Python's approach, in Bleach the name self is mandatory if the programmer wants to refer to the current instance of the class.

Inheritance

As we all know, inheritance is a fundamental concept in object-oriented programming which allows a class to inherit attributes and methods from another class.

The major purpose for the existence of inheritance is to promote code reuse and to establish a natural hierarchy between classes.

In Bleach, as in other popular languages such as C++, C#, Java , JavaScript and Python, inheritance allows the programmer to create a new class based on an existing class, extending or modifying its functionality.

Going further, Bleach follows a very similar implementation of inheritance to that of Python with just one major difference: For simplicity purposes, Bleach only supports single inheritance whereas Python has support for multiple inheritance (Talking about this, such a thing can be easily added to the Bleach Interpreter and it's a good practice for students to deepen their knowledge).

As usual, the code snippet below shows how inheritance, method overriding and the super keyword work in practice:

class Animal{
  method init(name){
    self.name = name;
  }

  method speak(){
    std::io::print(self.name, "makes a sound.");
  }
}

class Dog inherits Animal{
  method init(name, breed){
    super.init(name);
    self.breed = breed;
  }

  method speak(){
    std::io::print(self.name, "is a ", self.breed, "and barks.");
  }
}

class Cat inherits Animal{
  method init(name, breed){
    super.init(name);
    self.breed = breed;
  }

  method speak(){
    std::io::print(self.name, "is a ", self.breed, "and meows.");
  }
}

let dog = Dog("Thor", "Rottweiller");
dog.speak(); // "Thor is a Rottweiller and barks."

let cat = Cat("Felicia", "Siamese");
cat.speak(); // "Felicia is a Siamese and meows."

Bleach Language Native Functions

Overview

In case the reader is not familiar with this concept: Native functions (a.k.a built-in functions or intrinsic functions) are functions that are directly provided by the programming language or its runtime environment, typically written in a lower-level language (C++ in Bleach's case) and are part of the core language or runtime.

These functions are "native" because they are implemented at the system or runtime level rather than being written in the higher-level language itself (i.e., written in Bleach).

Some Key Characteristics of Native Functions

  • Performance: Native functions are usually optimized for performance since they are closely integrated with the language runtime (which was written in C++ in Bleach's case) or the underlying hardware.
  • Availability: Native Functions are readily available without needing to be explicitly imported or defined by the programmer.
  • Language-Specific: Native functions varies between languages and is closely tied to the language's runtime environment.

Namespaces

For clarity and organization purposes, Bleach's native functions are organized in namespaces.

If you are not familiar with this concept, a namespace is a way to logically group functions, variables and other identifiers to prevent naming conflicts and to organize code more effectively (in Bleach's case I opted to use namespaces just to group native functions as I've mentioned above).

By using namespaces, you can create multiple functions or variables with the same name, as long as they belong to different namespaces, thereby avoiding potential collisions in a large codebase.

Namespace: std::io

Contains the native functions related to input/output operations.

Function: std::io::readLine

This native function is responsible for reading the content provided by the user inside the console/terminal until it reaches the end of line character (\n). It takes 0 arguments and its return value is a value of str type: The content provided by the user in the console/terminal.

Usage:

let user_input = std::io::readLine(); // Say the user writes "hello" in console/terminal.

// At this point, the value of the user_input variable is "hello";

Function: std::io::print

This native function is responsible for printing content provided by the user to console/terminal. It can take any number arguments of any type, whether it's built-in or user-defined. Then it prints the string representation of such values to the console separating them by a character.

Usage:

let n = 3.14;
let name = "Ryan";
let nothing = nil;
let is_earth_flat = false;

std::io::print(n, name, nothing, is_earth_flat); // 3.14 Ryan nil false

Function std::io::fileRead

Not implemented yet.

Function std::io::fileWrite

Not implemented yet.

Namespace: std::chrono

Contains native functions related to time and clock operations.

Function: std::chrono::clock

This native function is responsible for calculating the amount of seconds that have been passed since "January 1, 1970, 00:00:00 UTC", which is a value of type num. It takes 0 arguments. Its return value is, as said above, the amount of seconds that have been passed since the date provided above, a value of type num.

This native function is, as you might have already thought, very useful for benchmarking purposes.

Usage:

let begin = std::chrono::clock();
// Execution of some code that the user wants to evaluate the performance/speed.
let end = std::chrono::clock();

let duration_in_seconds = end - begin;

std::io::print("The observed task took", duration_in_seconds, "seconds to be executed.");

Namespace: std::math

Contains native functions related to mathematical operations.

Function: std::math::abs

This native function is responsible for calculating the absolute value of a provided value of type num. It can takes just one argument of such type. Its return value is the absolute value of the provided value of type num.

Usage:

let n = -10;
let absolute_number = std::math::abs(n); // 10

Function: std::math::ceil

Not implemented yet.

Function: std::math::floor

Not implemented yet.

Function: std::math::log

This native function is responsible for calculating the logarithm of a value given the base to be used. It takes two arguments of type num. The first argument is base the and the second argument is the mantissa. Its return value is the computed logarithm, a value of type num, given the provided arguments.

Important: The first argument (the base of the logarithm) must not be equal to 1. Also, the second argument (the argument) must be a number greater than 0. If either of these conditions is not met, then such native function will throw a runtime error when called.

Usage:

let base = 2;
let mantissa = 8;
let logarithm = std::math::log(base, mantissa); // 3

Function: std::math::pow

This native function is responsible for calculating the power of a value given its exponent. It takes two arguments of type num. The first argument is base the and the second argument is the exponent. Its return value is the value resulted from the computation of the exponentiation operation, a value of type num, given the provided arguments.

Usage:

let base = 2;
let exponent = 3;
let exponentiation_result = std::math::pow(base, exponent); // 8

Function: std::math::setprecision

Not implemented yet.

Function: std::math::sqrt

This native function is responsible for calculating the square root of a given value (the radicand). It takes just one argument of type num. Its return value is the square root of the provided radicand, a value of type num, given the provided argument.

Important: This native function is not able to deal with negative values. If the user provide such a value, a runtime error will be thrown.

Usage:

let n = 2;
let square_root_of_two = std::math::sqrt(n); // 1.414213562373095

Namespace: std::random

Contains native functions related to random number generation.

Function: std::random::random

This native function is responsible for generating a random number that is between an interval. It takes two arguments of type num. The first argument is the left boundary of the interval and the second argument is the right boundary of the interval. Its return value is the generated random number, a value of type num, given the provided arguments.

Important: If the first argument is greater than the second argument the native function will perform a swap between such arguments internally when called.

Usage:

let left = 3.5;
let right = 3.7;
std::io::print(std::random::random(left, right));

Bleach Context-Free Grammar Versions

  • This page shows the development of the Bleach language in terms of ripening of its Context-Free Grammar.
  • As you can see below, Bleach's CFG grew in an incremental manner, feature by feature.

Version 0.1.0

expression → equality
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → NUMBER | STRING | "true" | "false" | "nil" | "(" expression ")"

Version 0.2.0

program → statement* EOF
statement → exprStmt | printStmt
exprStmt → expression ";"
printStmt → "print" expression ";"
expression → equality
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → NUMBER | STRING | "true" | "false" | "nil" | "(" expression ")

Version 0.3.0

program → statement* EOF
statement → exprStmt | printStmt | varDeclStmt
exprStmt → expression ";"
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
expression → equality
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.4.0

program → statement* EOF
statement → exprStmt | printStmt | varDeclStmt
exprStmt → expression ";"
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
expression → assignment
assignment → IDENTIFIER "=" assignment | equality
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.5.0

program → statement* EOF
statement → block | exprStmt | printStmt | varDeclStmt
block → "{" statement* "}"
exprStmt → expression ";"
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
expression → assignment
assignment → IDENTIFIER "=" assignment | equality
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.6.0

program → statement* EOF
statement → block | exprStmt | ifStmt | printStmt | varDeclStmt
block → "{" statement* "}"
exprStmt → expression ";"
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
expression → assignment
assignment → IDENTIFIER "=" assignment | equality
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.7.0

program → statement* EOF
statement → block | exprStmt | ifStmt | printStmt | varDeclStmt
block → "{" statement* "}"
exprStmt → expression ";"
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
expression → assignment
assignment → IDENTIFIER "=" assignment | logic_or
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.8.0

program → statement* EOF
statement → block | exprStmt | ifStmt | printStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
exprStmt → expression ";"
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" statement
expression → assignment
assignment → IDENTIFIER "=" assignment | logic_or
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.9.0

program → statement* EOF
statement → block | doWhileStmt | exprStmt | ifStmt | printStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
doWhileStmt → "do" statement "while" "(" expression ")" ";"
exprStmt → expression ";"
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" statement
expression → assignment
assignment → IDENTIFIER "=" assignment | logic_or
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.10.0

program → statement* EOF
statement → block | doWhileStmt | exprStmt | forStmt | ifStmt | printStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
doWhileStmt → "do" statement "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" statement
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" statement
expression → assignment
assignment → IDENTIFIER "=" assignment | logic_or
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.11.0

program → statement* EOF
statement → block | doWhileStmt | exprStmt | forStmt | ifStmt | printStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
doWhileStmt → "do" statement "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" statement
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" statement
expression → assignment
assignment → IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.12.0

program → statement* EOF
statement → block | doWhileStmt | exprStmt | forStmt | ifStmt | printStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
doWhileStmt → "do" statement "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" statement
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" statement
expression → assignment
assignment → IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.13.0

program → statement* EOF
statement → block | doWhileStmt | exprStmt | forStmt | funcDeclStmt | ifStmt | printStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
doWhileStmt → "do" statement "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" statement
funcDeclStmt → "function" function
function → IDENTIFIER "(" parameters? ")" block
parameters → IDENTIFIER ( "," IDENTIFIER )*
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" statement
expression → assignment
assignment → IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.14.0

program → statement* EOF
statement → block | doWhileStmt | exprStmt | forStmt | funcDeclStmt | ifStmt | printStmt | returnStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
doWhileStmt → "do" statement "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" statement
funcDeclStmt → "function" function
function → IDENTIFIER "(" parameters? ")" block
parameters → IDENTIFIER ( "," IDENTIFIER )*
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
returnStmt → "return" expression? ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" statement
expression → assignment
assignment → IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | IDENTIFIER

Version 0.15.0

program → statement* EOF
statement → block | doWhileStmt | exprStmt | forStmt | funcDeclStmt | ifStmt | printStmt | returnStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
doWhileStmt → "do" statement "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" statement
funcDeclStmt → "function" function
function → IDENTIFIER "(" parameters? ")" block
parameters → IDENTIFIER ( "," IDENTIFIER )*
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
returnStmt → "return" expression? ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" statement
expression → assignment
assignment → IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | lambdaFunctionExpr | IDENTIFIER
lambdaFunctionExpr → "lambda" "(" parameters? ")" block

Version 0.16.0

program → statement* EOF
statement → block | breakStmt | classDeclStmt | continueStmt | doWhileStmt | exprStmt | forStmt | funcDeclStmt | ifStmt | printStmt | returnStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
break → "break" ";"
classDeclStmt → "class" IDENTIFIER "{" methodDeclStmt* "}"
methodDeclStmt → "method" method
method → IDENTIFIER "(" parameters? ")" block
continueStmt → "continue" ";"
doWhileStmt → "do" statement "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" statement
funcDeclStmt → "function" function
function → IDENTIFIER "(" parameters? ")" block
parameters → IDENTIFIER ( "," IDENTIFIER )*
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
returnStmt → "return" expression? ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" statement
expression → assignment
assignment → IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | lambdaFunctionExpr | IDENTIFIER
lambdaFunctionExpr → "lambda" "(" parameters? ")" block

Version 0.17.0

  • Now loops (for, do-while, while) must be followed by a block.
program → statement* EOF
statement → block | breakStmt | classDeclStmt | continueStmt | doWhileStmt | exprStmt | forStmt | funcDeclStmt | ifStmt | printStmt | returnStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
break → "break" ";"
classDeclStmt → "class" IDENTIFIER "{" methodDeclStmt* "}"
methodDeclStmt → "method" method
method → IDENTIFIER "(" parameters? ")" block
continueStmt → "continue" ";"
doWhileStmt → "do" block "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" block
funcDeclStmt → "function" function
function → IDENTIFIER "(" parameters? ")" block
parameters → IDENTIFIER ( "," IDENTIFIER )*
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
returnStmt → "return" expression? ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" block
expression → assignment
assignment → IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | lambdaFunctionExpr | IDENTIFIER
lambdaFunctionExpr → "lambda" "(" parameters? ")" block

Version 0.18.0

  • Now loops (for, do-while, while) must be followed by a block.
program → statement* EOF
statement → block | breakStmt | classDeclStmt | continueStmt | doWhileStmt | exprStmt | forStmt | funcDeclStmt | ifStmt | printStmt | returnStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
break → "break" ";"
classDeclStmt → "class" IDENTIFIER "{" methodDeclStmt* "}"
methodDeclStmt → "method" method
method → IDENTIFIER "(" parameters? ")" block
continueStmt → "continue" ";"
doWhileStmt → "do" block "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" block
funcDeclStmt → "function" function
function → IDENTIFIER "(" parameters? ")" block
parameters → IDENTIFIER ( "," IDENTIFIER )*
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
returnStmt → "return" expression? ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" block
expression → assignment
assignment → IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" | "." IDENTIFIER )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | lambdaFunctionExpr | IDENTIFIER
lambdaFunctionExpr → "lambda" "(" parameters? ")" block

Version 0.19.0

  • Now loops (for, do-while, while) must be followed by a block.
program → statement* EOF
statement → block | breakStmt | classDeclStmt | continueStmt | doWhileStmt | exprStmt | forStmt | funcDeclStmt | ifStmt | printStmt | returnStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
break → "break" ";"
classDeclStmt → "class" IDENTIFIER "{" methodDeclStmt* "}"
methodDeclStmt → "method" method
method → IDENTIFIER "(" parameters? ")" block
continueStmt → "continue" ";"
doWhileStmt → "do" block "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" block
funcDeclStmt → "function" function
function → IDENTIFIER "(" parameters? ")" block
parameters → IDENTIFIER ( "," IDENTIFIER )*
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
returnStmt → "return" expression? ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" block
expression → assignment
assignment → ( call "." )? IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" | "." IDENTIFIER )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | lambdaFunctionExpr | IDENTIFIER
lambdaFunctionExpr → "lambda" "(" parameters? ")" block

Version 0.20.0

  • Now loops (for, do-while, while) must be followed by a block.
program → statement* EOF
statement → block | breakStmt | classDeclStmt | continueStmt | doWhileStmt | exprStmt | forStmt | funcDeclStmt | ifStmt | printStmt | returnStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
break → "break" ";"
classDeclStmt → "class" IDENTIFIER ( "inherits" IDENTIFIER )? "{" methodDeclStmt* "}"
methodDeclStmt → "method" method
method → IDENTIFIER "(" parameters? ")" block
continueStmt → "continue" ";"
doWhileStmt → "do" block "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" block
funcDeclStmt → "function" function
function → IDENTIFIER "(" parameters? ")" block
parameters → IDENTIFIER ( "," IDENTIFIER )*
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
returnStmt → "return" expression? ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" block
expression → assignment
assignment → ( call "." )? IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" | "." IDENTIFIER )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | lambdaFunctionExpr | IDENTIFIER
lambdaFunctionExpr → "lambda" "(" parameters? ")" block

Version 0.21.0

  • Now loops (for, do-while, while) must be followed by a block.
program → statement* EOF
statement → block | breakStmt | classDeclStmt | continueStmt | doWhileStmt | exprStmt | forStmt | funcDeclStmt | ifStmt | printStmt | returnStmt | varDeclStmt | whileStmt
block → "{" statement* "}"
breakStmt → "break" ";"
classDeclStmt → "class" IDENTIFIER ( "inherits" IDENTIFIER )? "{" methodDeclStmt* "}"
methodDeclStmt → "method" method
method → IDENTIFIER "(" parameters? ")" block
continueStmt → "continue" ";"
doWhileStmt → "do" block "while" "(" expression ")" ";"
exprStmt → expression ";"
forStmt → "for" "(" ( varDecl | exprStmt | ";" ) expression? ";" expression? ")" block
funcDeclStmt → "function" function
function → IDENTIFIER "(" parameters? ")" block
parameters → IDENTIFIER ( "," IDENTIFIER )*
ifStmt → "if" "(" expression ")" statement
         ( "elif" "(" expression ")" statement )*
         ( "else" statement )?
printStmt → "print" expression ";"
returnStmt → "return" expression? ";"
varDeclStmt → "let" IDENTIFIER ( "=" expression )? ";"
whileStmt → "while" "(" expression ")" block
expression → assignment
assignment → ( call "." )? IDENTIFIER "=" assignment | ternary
ternary → logic_or ( "?" expression ":" expression )*
logic_or → logic_and ( "or" logic_and )*
logic_and → equality ( "and" equality )*
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | call
call → primary ( "(" arguments? ")" | "." IDENTIFIER )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | lambdaFunctionExpr | IDENTIFIER | "super" . IDENTIFIER
lambdaFunctionExpr → "lambda" "->" "(" parameters? ")" block