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 is 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 essentially 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 MsC or PhD programs. That being said, I think it is 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 is not a language with manual memory management. Instead, it has an automatic one.
Essentially, 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. These data types are divided into 2 categories (Scalar Types and Compound Types):
-
Scalar Types:
bool
nil
num
-
Compound Types:
list
str
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
, nil
and num
. You may recognize these from other programming languages. Let’s see how they work in Bleach:
Type: bool
This data type is used to represent one of two possible values: true or false. Such values are used to express logical conditions and to control the execution of certain parts of a program’s source code. As in other languages, the only two possible values for this specific data type are:
true
false
In this context, it is important to mention that Bleach takes inspiration from Ruby and other modern programming languages and implement the concept of "truthy" and "falsey" values in order to evaluate the truthiness or the falseness of values when they are being evaluated inside if
statements, loops (while
, do-while
, for
) and ternary operators (condition ? expression_1 : expression_2
).
Essentially, this means that, in Bleach, values of any type (built-in or user-defined) can be used in places where a value of type bool
is expected. Moreover, Bleach opted to follow the same convention of Ruby in this matter:
- Falsey values:
false
,nil
. - Truthy values: Any other value that is not
false
nornil
.
Type: nil
This type is an old acquaintance for most programmers. The nil type has just one possible value, the nil
value. In short this value conveys the idea of "no value" or the "absence of a value".
nil
In other programming languages, such type might be called null
, nil
, NULL
or nullptr
. Here, in Bleach, we are going to follow Ruby's influence and use nil
to denote this idea.
It is common knowledge that there are lots of arguments for not having such type 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 this type. However, in a dynamically-typed language (which is Bleach's case), eliminating it is often more annoying and troublesome than allowing it. Thus, Nystrom's advice was followed on this matter.
Type: num
In favor of simplicity, Bleach has only one type to represent numbers: the num
type. This type can be used to represent both integers and floating-point numbers.
Behind the scenes, this type is implemented with a double-precision floating-point type, which allows Bleach to cover a lot of territory when it comes to numerical values while still keeping the simplicity initially envisioned for the language.
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 an educational programming language, it has support for just basic integer and decimal literals, like the ones shown below:
2.71828
3.14159
23
-16
-9.9845
Compound Types
Compound types are basically types that can group multiple values into one. Bleach has two primitive compound types: list
and str
. Let's take a look at them:
Type: list
Taking inspiration from Python, Bleach has this type. Here, the list
type represents a linear-sequence of elements. As previously stated, Bleach is a dynamically-typed language, just like Python. Thus, the list
type can store values of different typeswith no issues. Moreover, Bleach has support for list literals, just as the ones shown below:
[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. The list
type in Bleach has the following useful methods that makes the student’s life easier when working with this type.
Finally, this type has the following methods associated with it:
append
: Responsible for adding one value of any type to the end of the list value it was called on. Returnsnil
.
let l = [];
l.append(1);
print l; // [1]
clear
: Method responsible for deleting every element that is currently stored inside the list value it was called on. Returnsnil
.
let l = [1, 2, 3, 4, nil, "hello"];
l.clear();
print l; // []
empty
: Responsible for checking whether the list value it was called on currently has values stored inside it or not. Returns abool
value.
let l = [1, 2, 3];
let isEmpty = l.empty();
print isEmpty; // false
fill
: Responsible for resizing the list value it was called on to a provided size and also filling all of its indexes with a provided value of any type. Returnsnil
.
let l = [];
l.fill("hello", 3);
print l; // ["hello", "hello", "hello"]
getAt
: Responsible for returning the value present at the provided index, which must be an integer number (represented by anum
value). Returns a value of any type. Trying to get a value from an index that does not exist in thelist
value will result in a runtime error being thrown.
let l = [1, 2, 3];
let v = l.getAt(0);
print v; // 1
pop
: Responsible for deleting and returning the last element present inside alist
value, if any. Returns a value of any type. In case thelist
value is already empty, calling this method will result in a runtime error being thrown.
let l = [42, 54, 23, 6];
let lastValue = l.pop();
print lastValue; // 6
setAt
: Responsible for setting a value stored at the provided index, which must be an integer number (represented by anum
value) to the value that was provided. Returnsnil
. Trying to set a value to an index that does not exist in thelist
value will result in a runtime error being thrown.
let l = [1, 2, 3];
l.setAt(0, 3.14159);
print l; // [3.14159, 2, 3]
size
: Responsible for checking and returning the current amount of elements that the list value it was on called on currently has. Returns anum
value.
let l = [2.71, 3.14, 9.98];
let lSize = l.size();
print lSize; // 3
Side Note #1: The list
type does not have support for indexing using square brackets ([]
). However, by using the methods getAt
and setAt
one can obtain the same behavior.
Side Note #2: It is important to highlight that any misuse of the methods presented above will result in a runtime error during the program’s execution.
Type: str
Also taking inspiration from Python, Bleach has this type. The str type represents an indexed-sequence of characters (a string), typically used to store and manipulate text. Just as the list
type, Bleach also has support for literal values of this type, as shown below:
"I am a string";
""; // The empty string.
"123"; // This is a string, not a value of type "num".
There are other aspects about the str type that must be mentioned.
First of all, it is important to recall that it is a sequence type. In short, it means that values of this type can be indexed. In a string, indexing usually allows the programmer to access individual characters from the value, which, in the Bleach programming language, are also values of the str
type.
Also, in Bleach, literals values of this type are always enclosed by double quotes (""
).
Finally, as in Python, this type has the some methods available:
empty
: Responsible for checking whether thestr
value it was called is equal to""
or not. Returns abool
value.
let s = "hello";
let isEmpty = s.empty();
print isEmpty; // false
find
: Responsible for trying to figure out if the provided sub-string (astr
value) exists within the str value the method was called on. Returns anum
value which denotes the index at which the sub-string appears, otherwise returns-1
.
let s = "racecar";
let idx = s.find("car");
print idx; // 4
getAt
: Responsible for returning thestr
value of length 1 present at the provided index, which is supposed to be an integer number (anum
value). Returns astr
value.
let s = "Uryu";
let ch = s.getAt(1);
print ch; // "r"
length
: Responsible for checking and returning the current amount of characters (which are also values of typestr
) that thestr
value it was on called on currently has. Returns anum
value.
let s = "Ichigo";
let sLength = s.length();
print sLength; // 6
split
: Responsible for generating alist
value where each value is ofstr
type. This method receives as its unique argument a value ofstr
type that works as the separator. Returns alist
value.
let s = "one-two-three";
let l = s.split("-");
print l; // ["one", "two", "three"]
setAt
: Responsible for setting the value stored at the provided index to the value that was provided (which must be astr
value of length1
). Returnsnil
.
let s = "Dice";
print s; // "Dice"
s.setAt(0, "N");
print s; // "Nice"
substr
: Responsible for retrieving a sub-string from the str value it was called on. The method receives two arguments that are integer numbers (values ofnum
type) that work as the start and end delimiters. It is worth mentioning that both delimiters are inclusive. Returns astr
value.
let s = "racecar";
let subs = s.substr(0, 2);
print subs; // "rac"
Side Note #1: The str
type does not have support for indexing using square brackets ([]
). However, by using the methods getAt
and setAt
one can obtain the same behavior.
Side Note #2: It is important to highlight that any misuse of the methods presented above will result in a runtime error during the program’s execution.
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.
By taking inspiration from C, 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 is 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 is allowed to put a comment after a line of code.
Multi-Line Comments
Also like C, Bleach has support for multi-line comments.
In Bleach, a multi-line comment has a beginning and also an 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
A variable, as in most programming languages, can be viewed as 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.
Variables in Bleach
In Bleach this is not different. Variables still have this main functionality. However, they have some particularities that might differ from variables in other programming languages. Such peculiarities are listed below:
- In Bleach, all variables are mutable. In other words, once a variable is declared, any amount of assignments are allowed to be performed on this declared variable.
- A direct consequence of the particularity explained above is that there is no concept of constants in Bleach.
- In Bleach, to declare a new variable, the let keyword must be used:
let s = "A value of type 'str'";
- In Bleach, if the declaration of a variable does not have an initializer, then, by default, the variable will store the
nil
value:
let someVariable;
print someVariable; // nil
- Since Bleach is a dynamically-typed programming language, variables do not have types associated with them. Instead, it is the value stored inside of a variable that has a type. The most important consequence of this fact is that a variable can hold values of different types at different points in time:
let a = "hello";
print a; // "hello"
a = nil;
print a; // nil
a = 3.14 + 2.71;
print a; // 5.85
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, this implementation decision was made.
In practice, 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
As expected from most programming language, Bleach supports local variables, which are those declared within a specific block of code, such as a function, method, an if-elif-else
statement or a loop statement (while
, do-while
, for
).
In this regard, it is important to remember the reader that 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. Furthermore, once the block of code finishes executing, the local variable is normally 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 is 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 =
.
Before moving forward, it is important to explain 2 semantic details of 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, which is a direct consequence of the first one, is that it is 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 do not remember), variable shadowing is a process that commonly happens in programming.
It is 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 and 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 outwards. 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, the operators provided by Bleach are going to be presented to us. Also, there will be explanations about how each of them behave in different scenarios and, at the end, the precedence table of Bleach's is presented for consulting purposes.
Unary Operators
These operators expects just 1 operand. Bleach has 2 operators that fall in this class.
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
These operators expects just 2 operands. Bleach has 13 operators that fall in this class.
Arithmetical
Addition (+
): This operator expects 2 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 to the first (left) operand, producing a new value of type num
.
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, producing a new value of type str
.
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"
Left operand (list
) and Right operand (list
): Concatenates the second (right) operand to the first (left) operand, producing a new value of type list
.
print [1, 2, 3] + [4, 5, 6] // [1, 2, 3, 4, 5, 6]
Left operand (str
) and Right operand (BleachInstance
): Calls the "str"
method of the second (right) operand, which is an instance of an user-defined type (this is a default method that returns the string representation of an instance of an user-defined type). Then, concatenates the second (right) operand to the first (left) operand, producing a new value of type str
.
Left operand (BleachInstance
) and Right operand (str
): Concatenates the second (right) operand to the string representation of the first (left) operand, which is an instance of an user-defined type. This happens because the "str"
method of the first (left) operand is called, which also returns a value of type str
. This produces a new value of type str
.
Subtraction (-
): This operator expects 2 operands of type num
. It subtracts the second (right) operand from the first (left) operand.
print 5 - 3; // 2
Multiplication (*
): This operator expects 2 operands of type num
. It multiplies the first (left) operand by the second (right) operand.
print 1.5 * 4; // 6
Division (/
): This operator expects 2 operands of type num
. It divides the first (left) operand by the second (right) operand.
print 5 / 2; // 2
print 1 / 3; // 0.333333333333333
Remainder(%
): This operator expects 2 operands of type num
. It divides the first (left) operand, also called dividend, by the second (right) operand, also called divisor, and returns the remainder of this division (a value of type num
). It is worth mentioning that if the value of the divisor is 0, then a runtime error will be thrown.
print 5 % 2; // 1
print 1 % 3; // 1
print -10 % 4; // -2
Comparison/Relational
Greater Than (>
): This operator expects 2 operands of type num
or 2 operands of type str
. It checks whether or not the first (left) operand is greater than the second (right) operand.
print 5 > 2; // true
print 1 > 3; // false
print "z" > "a"; // true
print "a" > "z"; // false
Greater Than or Equal (>=
): This operator expects two operands of type num
or 2 operands of type str
. 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
print "aa" >= "aa"; // true
print "za" >= "zz"; // false
Lesser Than (<
): This operator expects two operands of type num
or 2 operands of type str
. It checks whether or not the first (left) operand is lesser than the second (right) operand.
print 5 < 2; // false
print 1 < 3; // true
print "a" < "z"; // true
print "z" < "a"; // false
Lesser Than or Equal (<=
): This operator expects two operands of type num
or 2 operands of type str
. 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
print "z" <= "z"; // true
print "zz" <= "az"; // false
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. Returns true
if both conditions are true. Otherwise, returns false
.
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. Returns true
if one (or both) conditions mentioned above are not satisfied. Otherwise, returns false
.
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
Ternary (condition ? expression_1 : expression_2
): A ternary operator is, as its name suggests, an operator that expects 3 operands. The reader might be familiar with this operator since it is widely used in both C and C++. This operator provides another way of executing conditional operations. It is used to evaluate a condition and return one of two values based on whether the condition evaluates to true
or false
. The three operands expected by the ternary operator can be of any type (built-in or user-defined). The first one is evaluated by the ternary operator with respect to its truthiness of falseness. If the value is "truthy", then the operator returns the second operand. Otherwise, it returns the third operand. The example below shows how to properly use such operator.
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 | condition ? expr_1 : expr_2 (ternary) |
9 | = (assignment) |
Control Flow Structures
Overview
Control Flow Structures are constructs that allow the programmers to dictate in what order and by how many times instructions or code blocks are executed.
These entities are responsible for allowing branching mechanisms, decision-making and repetition in a program. The main consequence of this is that a program can have a more dynamic and sophisticated behavior.
In Bleach, there are 3 classes of control-flow structures:
Conditional Statements
Such statements allow the program to execute certain blocks of code based on whether a condition evaluates to true
or false
.
if-elif-else
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 will not 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 is important to mention that one needs to add a block after an if
, elif
or else
clause. Otherwise, a syntax error will be thrown by the interpreter. This happens because such statements create a new local scope.
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 not met anymore.
Bleach has support for 3 different types of loop statements: while
, do-while
and for
.
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).
Last but not least, just as in conditional statements, Bleach also allows the nesting of loop statements.
While
This 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 have 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. This happens because they create a local scope. 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 in its first iteration. 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, the same thing can be done in a do-while
loop.
As we have seen, each value, no matter its type is considered either "truthy" or "falsey". In practice, this means that you do not 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
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 while some condition keeps evaluating 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 a loop is supposed to iterate.
In case you don't remember the structure of a for
loop, here comes 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. 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 evaluated to
true
.
It is also important to mention that the first 3 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. Such kind of loop also creates a new scope. 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.
If any of these keywords are used out of a loop, then an error will be thrown.
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.
Moreover, functions, as widely known, 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 its name and passing arguments to it, if needed.
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 is 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
Side Note #1: In Bleach, functions don't have support for optional parameters, or for default value for its parameters, and also cannot be overloaded.
Side Note #2: An important note about function semantics in Bleach is how values of different types are passed into a function during a call: by value or by reference. In Bleach's case, the semantics are:
- By reference:
list
and instances of user-defined types. - By value:
bool
,nil
,num
andstr
.
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 symbol ->
.
When it comes to semantics, rest assured. Bleach anonymous/lambda functions are as powerful as the default 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 are directly tied to it:
- Attributes/Fields
- Methods
- Instances
- Inheritance
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 essentially 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.
Side Note #1: Bleach has 2 special methods by default in every class. Such methods are presented below:
- The
init
method: It is just a special method usually present in classes and known as the constructor of a class. It is 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. This method can be changed by the programmer to change the functionality of the constructor of a desired class. Last but not least, this method is supposed to always return the created instance. There is no need to use write areturn
statement inside it. Moreover, if one does it and tries to return a value of another type, then a runtime error will be thrown. - The
str
method: It is just a special method usually present in classes and known as the string representation of a class. It is automatically called when the programmer uses theprint
statement, or thestd::io::print
native function, or concatenates an instance of a class with a value of typestr
. Last but not least, this method can also be changed by the programmer in order to return a string that better reflects the identity of an instance created from a class.
Instances
Essentially, 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 is a reference to the current instance of the class. It is 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 is widely known, 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 is 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 is 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
This native function is responsible for or receiving a value of the type str
that represents the path (absolute or relative) to a .txt
file, reading its contents and returning them as a str
value. It takes a vlaue of type str
as its unique argument and returns a value of type str
, the content of the .txt
file.
Usage:
let filePath = "path/to/the/desired/txt/file"; // Can be absolute or relative path to the desired ".txt" file.
let fileContent = std::io::fileRead(filePath);
print fileContent; // Prints the content of the ".txt" file.
Function std::io::fileWrite
This native function is responsible for receiving a value of the type str
representing a path (absolute or relative) to a .txt
file, a value of the type str
representing the opening mode of the file to be read (which can be "a"
for append or "w"
for write), another value of the type str
representing the content to be written to the file and a value of the type bool
signaling whether or not a newline (\n
) must be inserted at the end of such content in the file. This function returns a nil
value.
Usage #1:
let filePath = "path/to/the/desired/txt/file"; // Can be absolute or relative path to the desired ".txt" file.
let openingMode = "w"; // Write Mode.
let contentToWrite = "Hello, World!";
let insertNewlineAtTheEnd = true;
std::io::fileWrite(filePath, openingMode, contentToWrite, insertNewlineAtTheEnd);
Usage #2:
let filePath = "path/to/the/desired/txt/file"; // Can be absolute or relative path to the desired ".txt" file.
let openingMode = "a"; // Append Mode.
let contentToWrite = "A new line below the 'Hello, World!' line.";
let insertNewlineAtTheEnd = true;
std::io::fileWrite(filePath, openingMode, contentToWrite, insertNewlineAtTheEnd);
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
This native function is responsible for receiving a value of type num
and returning the smallest integer number that is bigger than the provided value. This function returns a value of type num
.
Usage:
let n = 10.23;
let ceil_n = std::math::ceil(n); // 11
Function: std::math::floor
This native function is responsible for receiving a value of type num
and returning the largest integer that is smaller than the provided value. This function returns a value of type num
.
Usage:
let n = 1.21;
let floor_n = std::math::floor(n); // 1
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::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));
Namespace: std::utils
Contains native functions related to utility functions that might be useful to the programmer in specific scenarios.
Function: std::utils::ord
This native function is responsible for receiving a value of type str
of length 1 and returning its respective ASCII integer number, which is a value of type num
. Passing a value of type str
with length different than 1 will result in a runtime error.
Usage:
let A_ascIICode = std::utils::ord("A");
print A_ascIICode; // 65
let a_ascIICode = std::utils::ord("a");
print a_ascIICode; // 97
Function: std::utils::strToBool
This native function is responsible for receiving a value of type str
that represents a boolean as its unique argument. The function converts the value of type str
into its respective value of type bool
and, then, returns such value. If the value of the type str
does not represent a boolean, a runtime error is thrown.
Usage:
let t_as_string = "true";
print t_as_string; // "true"
let t_as_boolean = std::utils::strToBool(t_as_string);
print t_as_boolean; // true
Function: std::utils::strToNil
This native function is responsible for receiving a value of type str
that represents the nil
value as its unique argument. The function converts the value of the type ````strinto this value and, then, returns it. If the value of the type
strdoes not represent
nil```, a runtime error is thrown.
Usage:
let nil_as_string = "nil";
print nil_as_string; // "nil"
let nil_value = std::utils::strToNil(nil_as_string);
print nil_value; // nil
Function: std::utils::strToNum
This native function is responsible for receiving a value of type str
that represents a number as its unique argument. The function converts the value of type str
into its respective value of type num
and, then, returns such value. If the value of the type str
does not represent a number, a runtime error is thrown.
Usage:
let num_as_string = "3.14159";
print num_as_string; // "3.14159"
let num_as_string = std::utils::strToNum(nil_as_string);
print num_as_string; // 3.14159
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
Version 0.22.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 | "[" expression "]" )*
arguments → expression ( "," expression )*
primary → "true" | "false" | "nil" | NUMBER | STRING | "(" expression ")" | lambdaFunctionExpr | IDENTIFIER | "super" . IDENTIFIER
lambdaFunctionExpr → "lambda" "->" "(" parameters? ")" block