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
andstd::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
nornil
.
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
: Returnsnil
. This method cleans the contents from the value ofstr
type. This change is made in-place.empty
: Returns abool
value that signals whether thestr
value is empty or not.find
: Returns anum
value that identifies the index where there is the first occurrence of a provided substring (a value of typestr
) inside another value of typestr
. If the provided substring doesn't appear, the method returns-1
.length
: Returns anum
value that represents the number of characters in thestr
value.pop_back
: Returns astr
value. This method removes the last character from thestr
value.push_back
: Returnsnil
. This method adds another value of typestr
at the end of thestr
value.size
: Returns anum
value that represents the number of characters in thestr
value.substr
: Returns a value of typestr
which is the substring of another value of typestr
. 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 typelist
. However, it does not make any changes to the value of typelist
.clear
: Returnsnil
. This method cleans the content from the value oflist
type. This change is made in-place.empty
: Returns abool
value that signals whether thelist
value is empty or not.front
: Returns the first element of a value of typelist
. However, it does not make any changes to the value of typelist
.pop_back
: Returns the last element of a value of typelist
. This method removes the last element from thelist
value.push_back
: Returnsnil
. This method adds an element of any type (whether it's a built-in or user-defined one) at the end of thelist
value.size
: Returns anum
value that represents the number of elements in thelist
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 thenum
type to thestr
type. Then, concatenates the second (right) operand to the first (left) operand, producing a new value of typestr
.
print 2 + "two"; // "2two"
- Left operand (
str
) and Right operand (num
): Converts the second (right) operand from the typenum
to the typestr
. Then, concatenates the second (right) operand to the first (left) operand, producing a new value of typestr
.
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.
Precendence | Operator |
---|---|
1 | ! , - (unary) |
2 | * , / |
3 | + , - (binary) |
4 | > , >= , < , <= |
5 | == , != |
6 | and |
7 | or |
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 tofalse
, 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:
- Attributes/Fields
- Methods
- Instances
- 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