
 # ===================================================== #
 # === EXPRESSIONS ===================================== #
 # ===================================================== #
 
 ## Basics ##
 
 Expression use infix notation. You can have expressions 
 of numeric values, boolean values and other datatypes like
 strings of text.
 
 When expressions are used as statements, they need to be
 terminated using a semicolon. Here's an example:

   1 | 2 * (1 + 2); 

 The basic values that can be used are integers, floats,
 booleans and "none".

 
 ## Integers, floats and arithmetic operators ##
 
 Both integers and floats (floating point values) are 
 signed and represented using 8 bytes. Integers can
 represent integer values between [2^61-1, 2^61], like
 "int64_t"s in C/C++. On the other hand, floats are
 equivalent to C/C++ "double"s. Numeric values can be 
 operated onto using the arithmetic operators: 
 
   - addition "+" (binary and unary)
   - subtraction "-" (binary and unary)
   - multiplication "*"
   - division "/"
   - modulo "%"
 
 Here "modulo" refers to the remainder of the division.
 These operations mainly behave like one would expect and
 have the following type conversion rules:

   - Operations involving integers evaluate to integers, except
     division. The result of a division is always a float.

   - If an arithmetic operation involves a float, the result is
     also float. 
 
 If operations on integers overflow or underflow, the 
 program's execution is aborted.
 
 ## Booleans and logical operators ##

 Boolean values are values that can either be "true" or
 "false". They have the property that the logical negation
 of one equals the other.
 
 Booleans can be operated onto using logical operators such
 as "and", "or" and "not". These operators expect boolean
 values and return a new boolean value. You probably know
 these well, but for completeness sake, here's how they
 work:

    1 | true  and true;  # = true
    2 | true  and false; # = false
    3 | false and true;  # = false
    4 | false and false; # = false
    5 | 
    6 | true  or true;   # = true
    7 | true  or false;  # = true
    8 | false or true;   # = true
    9 | false or false;  # = false
   10 |
   11 | not true;  # = false
   12 | not false; # = true
 
 If any of the operands aren't booleans, the program's
 execution is aborted.
 
 Operators "and" and "or" are short-circuit operators. This
 means that they only consider the right operand if they can't
 deduce the solution from the left one. For example, if the
 left operand of an "and" evaluates to "false", it isn't 
 necessary to evaluate the right one, since the result of the
 overall operation can only be "false".


 ## Relational operators ##

 Relational operators are those which evaluate to booleans
 and take, in general, non-boolean operands. They are:

   - equal "=="
   - not equal "!="
   - less than "<"
   - less than or equal "<="
   - greater than ">"
   - greater than or equal ">="
 
 Equal (not equal) can be applied to all type of operands
 and return true (false) when the operands have the same
 type and hold the same value. These operator don't allow
 funny business like 1 == "1" evaluating to "true".
 
 The remaining relational operators are applied to numeric
 datatypes (ints and floats). 

 
 ## The none value ##

 The none value used to represent the void of a value and
 has the only property of being equal to itself and itself
 only. You can use the none value using the "none" keyword:

   1 | x = none; 
 

 ## Variables and assignments ##

 You can store computed values into variables in order to
 reuse them later on. Variables are created using the
 assignment operator:

   1 | x = 1 + 4;
   2 | y = x + 2;
 
 here we're assigning to the variable "x" the number 5 then,
 we're assigning to "y" the value 7 by accessing the value
 previously stored into "x".
 The left operand of the assignment operator must be a variable
 name while the right operator can have any type.

 Variable names can consist of digits, letters or underscores,
 but the first character can't be a digit though.
 
 Since the assignment operator is an operator, other than
 do an assignment it also returns a value. Any assignment
 evaluates to it's right operand. By instance

   1 | y = (x = 1 + 4) + 2;

 this expression will result in "x" having value 5 and "y"
 value 7. This is because "x = 1 + 4", other than assigning
 to "x", is equivalent to writing "5".


 ## Composit types and square bracket notation ##

 Composit values are collections of other values, which may
 also be composite. The composite types are "List", "Map" and
 "String". For all collection types it's possible to insert 
 and retrieve values using the "[]" notation:

   1 | coll[key] = item; # Store the value associated to the 
   2 |                   # variable "item" with key "key" in 
   3 |                   # the collection "coll".
   4 |
   5 | item = coll[key]; # Get the item back by selecting it 
   6 |                   # using it's key
   7 |

 In this example, the "coll" variable is a collection type, 
 while the types of "key" and "item" depend on the type of
 collection.


 ## Lists ##

 Lists are heterogeneous and ordered collections of values. 
 Each item they contain is associated to it's position in 
 the list (the first element has position 0). They're defined 
 and used with the following syntax:

   1 | my_list = [true, 1.2, 19];
   2 |
   3 | x = my_list[0]; # true
   4 | y = my_list[2]; # 19
   5 |
   6 | my_list[0] = 13;
   7 |
   8 | z = my_list[0]; # z is 13 now!
 
 Trying to access an item using as key something which isn't
 an integer or an in range integer (less then 0 or higher 
 than the length of the array minus one) will result in an 
 error. The only exception to this rule is the index equal 
 to the item count of the list (which is out of bounds since 
 the last item of the list has index equals to the item count 
 minus one): by inserting a value at this index, the list 
 will increase it's size by 1. There isn't a limit on how 
 many values a list can contain.
 

 ## Strings ##

 Strings are values which contain UTF-8 encoded text. 
 A string can be instanciated placing text between single or 
 double quotes:

   1 | "I'm a string!";
   2 |
   3 | 'I am too!';
   4 |
 
 Special character (such as horizontal tabs and carriage 
 returns) can be specified using the "\x" notation:

   "\t" - tab
   "\r" - carriage return
   "\n" - newline
 
 When strings contain quotes that match the ones surrounding
 them or the "\" character, it's necessary to escape them:
 
   1 | 'Hi, I\'m Francesco!';
   2 | "Hi \"Francesco\", how old are you?";
   3 | "This is a backlash \\ and you can do nothing about it";
 
 Like arrays, single characters can be selected referring to them
 by their position relative to the first character using the
 "[]" notation. When selecting single characters from a string,
 they're returned as new strings.

 Once a string is created, it's not possible to modify it.
 If you want to change a string's value, you need to create
 a new updated version of the string.

 
 ## Maps and the dot operator ##
 
 Maps are collections of key-value pairs, where both keys and
 values can have any type.
 The syntax for defining and using maps is this:
 
   1 | me = {"name": "Francesco", "age": 24};
   2 |
   3 | my_name = me["name"]; # Francesco
   4 |
   5 | me["name"] = true;
   6 |
   7 | my_name = me["name"]; # true
 
 When selecting from a map a value associated to a key which
 was never inserted, "none" is returned:

   1 | my_map = {1: "one", 3: "three"};
   2 | two = my_map[2]; # none

 Because of the existence of compount statements, expression
 statements can't start with the "{" token. The parser would 
 assume it's a badly formatted compount statement. To avoid 
 the ambiguity, you can add some tokens that have no effect 
 before the "{":

   1 | {"day": "Monday"}; # invalid
   2 | +{"day": "Monday"}; # valid
   3 | ({"day": "Monday"}); # also valid
 
 This isn't very pretty but it's a case that doesn't occur
 in practice.

 When instantiating a map, when a key is a string that follows
 variable name rules, the encoling quotes can be dropped:

   1 | # These are equivalent
   2 | +{"name": "Francesco", "age": 25};
   3 | +{name: "Francesco", age: 25};
 
 If instead you wanted to use the variable named "name" as a
 key, you can do that by adding some redundancy:

    1 | name = "x";
    2 |
    3 | # These are equivalent
    4 | +{(name): "Francesco"};
    5 | +{ +name: "Francesco"};
    6 | +{"x": "Francesco"};
    7 |
    8 | # And are different from these
    9 | +{name: "Francesco"};
   10 | +{"name": "Francesco"};

 Similarly, when querying a map for an item associated to a
 string that follows variable name rules, you can use the dot
 operator:

   1 | # These are equivalent
   2 | me["name"] = "Francesco";
   3 | me.name = "Francesco";
 

 ## Function calls ##

 We haven't seen how function definitions work yet, but you 
 can imagine they work like other languages such as Python 
 or JavaScript for now. Assuming we defined a function named 
 "sayHello", we can call it using the usual "()" notation:

   1 | sayHello();
   2 | sayHello(1);
   3 | sayHello(1, 2, 3);
 

 ## Functions useful for collections ##
 count, keysof
  
 # ===================================================== #
 # === FUNCTIONS ======================================= #
 # ===================================================== #
 
 ## Definition and basics ##
 
 Functions are defined like this:

   1 | fun sayHello() {
   2 |   print("Hello!\n");
   3 | }

 they must always start with the "fun" keyword followed
 by the function's name. Function name rules are the same
 as variables: they can contain letters, numbers or
 underscores, but the first character can't be a number.
   One or more arguments can be provided by specifying
 their names between the "()" following the name:
 
   1 | fun sayHelloTo(name1, name2) {
   2 |   print("Hello ", name1, " and ", name2, "!\n");
   3 | }

 When a function is called with more arguments than the
 ones specified in it's definition, the extra ones are
 discarded. If less arguments than expected are provided,
 the remaining ones are "none" by default.
   It's possible to specify default values for any of the
 arguments. When the caller passes "none" as an argument, 
 the default value is used:

   1 | fun sayHelloTo(name1="Francesco", b="Giovanni") {
   2 |   print("Hello ", name1, " and ", name2, "!\n");
   3 | }
   4 |
   5 | sayHello(none, "Filippo"); # Hello Francesco and Filippo!
   6 |
   7 | # Here the arguments are implicitly none
   8 | sayHello(); # Hello Francesco and Giovanni!
 
 Return value can be specified using the "return" keyword:

   1 | fun sum(a, b) {
   2 |   return a + b;
   3 | }
   4 |
   5 | print("The sum of 3 and 7 is ", sum(3, 7), "\n");
 
 functions that don't return a value explicitly will
 return "none" implicitly.
   Multiple values can be returned. To get the extra return
 values, one must assign them to variables. If called outside
 of an assignment, only the first return value is considered.

   1 | fun divmod(x, y) {
   2 |   return (x / y), (x % y);
   3 | }
   4 |
   5 | res1, res2 = divmod(100, 20); # 5, 0
   6 |
   7 | print(divmod(100, 20)); # Prints 5 (the modulo's result is discarded) 
 
 When a function returns more values than what was expected
 in an assignment expression, the extra values are ignored.
 If an assignment expects a function to return more values
 than it actually returns, the extra variables are set to
 "none".

 ## Scoping and closures ##
 
 Functions are the only construct that creates a new scope:
 when variables are defined inside a functions, they're only
 accessible from within that function. When the function
 returns, all variables defined by it are no longer accessible.
 Here are some examples:

   1 |
   2 | fun doSomething() {
   3 |   name = "Francesco";
   4 | }
   5 |
   6 | doSomething();
   7 | print(name); # Runtime Error: No variable "name" is defined!

 By contrast, functions can access variables defined in their
 parent scope (relative to their definition)

   1 | name = "Francesco";
   2 | age = 24;
   3 |
   4 | fun printVars() {
   5 |   print(name, age);
   6 | }
   7 |
   8 | printVars(); # prints: Francesco24

 An alternative way of saying this is that functions create 
 closures. This mechanism also works recursively, functions
 defined within other functions can access both the parent
 function's variables and global variables.
 
   1 | X = 1;
   3 |
   4 | fun wrapper() {
   5 |
   6 |   Y = 2;
   7 |
   8 |   fun printVars() {
   9 |     print(X, Y);
  10 |   }
  11 |
  12 |   printVars();
  13 | }
  14 |
  15 | wrapper(); # prints: 12
 
 A cool implication of closures is the ability to create
 parametric definitions of functions. This is done by
 returning from a function a function defined inside it

   1 | fun createDivisibilityCheck(n) {
   2 |   fun isDivisibleByN(k) {
   3 |     return k % n == 0;
   4 |   }
   5 |  
   6 |   return isDivisibleByN;
   7 | }
   8 |
   9 | isDivisibleBy2 = createDivisibilityCheck(2);
  10 | isDivisibleBy3 = createDivisibilityCheck(3);
  11 |
  12 | isDivisibleBy2(100); # true
  13 | isDivisibleBy2(107); # false
  14 |
  15 | isDivisibleBy3(39);  # true
  16 | isDivisibleBy3(100); # false
 
 in this example, "isDivisibleBy2" and "isDivisibleBy3"
 share their implementation. What changes, is the closure
 of their parent scopes.