From 37420cdea2303733fa594c65a051e2a65852fc39 Mon Sep 17 00:00:00 2001 From: Francesco Cozzuto Date: Wed, 18 Jan 2023 01:28:59 +0100 Subject: [PATCH] added the ascii version of the docs --- docs/ascii.txt | 443 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 docs/ascii.txt diff --git a/docs/ascii.txt b/docs/ascii.txt new file mode 100644 index 0000000..85545d1 --- /dev/null +++ b/docs/ascii.txt @@ -0,0 +1,443 @@ + + # ===================================================== # + # === 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. \ No newline at end of file