INTRO
Everything in FOL follows the same structure:
declaration[options] name: type[options] = { implementation; };
declarations
use // imports, includes ...
def // preporcesr, macros, bocks, definitions ...
var // all variables: ordinal, container, complex, special
pro // subporgrams with side effects - procedures
fun // subporgrams with no side effects - functions
log // subporgrams with logic only - logicals
typ // new types: records, entries, classes, aiases, extensions
std // standards: protocols, blueprints
control flow
when(condition){ case (){}; case (){}; * {}; };
loop(condition){ };
example
use log: mod[std] = {fmt::log};
def argo: mod[init] = {
-var const: str = "something here"
+pro main: int = {
log.warn("Last warning!...");
.echo(add(3, 5));
}
fun add(a, b: int): int = { a + b }
}
Lexical Analysis
A lexical analyzer is essentially a pattern matcher. A pattern matcher attempts to find a substring of a given string of characters that matches a given character pattern. All FOL's input is interpreted as a sequence of UNICODE code points encoded in UTF-8. A lexical analyzer serves as the front end of a syntax analyzer. Technically, lexical analysis is a part of syntax analysis. A lexical analyzer performs syntax analysis at the lowest level of program structure. An input program appears to a compiler as a single string of characters. The lexical analyzer collects characters into logical groupings and assigns internal codes to the groupings according to their structure.
Syntax analyzers, or parsers, are nearly always based on a formal description of the syntax of programs. FOL compiler separates the task of analyzing syntax into two distinct parts, lexical analysis and syntax analysis, although this terminology is confusing. The lexical analyzer deals with small-scale language constructs, such as names and numeric literals. The syntax analyzer deals with the large-scale constructs, such as expressions, statements, and program units.
There are three reasons why lexical analysis is separated from syntax analysis:
- Simplicity— Techniques for lexical analysis are less complex than those required for syntax analysis, so the lexical- analysis process can be simpler if it is separate. Also, removing the low- level details of lexical analysis from the syntax analyzer makes the syntax analyzer both smaller and less complex.
- Efficiency— Although it pays to optimize the lexical analyzer, because lexical analysis requires a significant portion of total compilation time, it is not fruitful to optimize the syntax analyzer. Separation facilitates this selective optimization.
- Portability— Because the lexical analyzer reads input program files and often includes buffering of that input, it is somewhat platform dependent. However, the syntax analyzer can be platform independent. It is always good to isolate machine- dependent parts of any software system.
Keywords
Fol has a number of restricted groups of keywords:
BK (build-in keywords)
BK_OR `or`
BK_XOR `xor`
BK_AND `and`
BK_IF `if`
BK_FOR `for`
BK_WHEN `when`
BK_EACH `each`
BK_LOOP `loop`
BK_IS `is`
BK_HAS `has`
BK_IN `in`
BK_THIS `this`
BK_SELF `self`
BK_BREAK `break`
BK_RETURN `return`
BK_YEILD `yeild`
BK_PANIC `panic`
BK_REPORT `report`
BK_CHECK `check`
BK_ASSERT `assert`
BK_WHERE `where`
BK_TRUE `true`
BK_FALSE `false`
BK_AS `as`
BK_CAST `cast`
BK_DO `do`
BK_GO `go`
BUILD-IN KEYWORDS - BK:
`(BK_AS|BK_IN|...)`
AK (assignment keywords)
AK_USE `use`
AK_DEF `def`
AK_VAR `var`
AK_FUN `fun`
AK_PRO `pro`
AK_LOG `log`
AK_TYP `typ`
AK_STD `std`
ASSIGNMENT KEYWORDS - AK:
`(AK_USE|AK_DEF|...)`
TK (type keywords)
TK_INT `int`
TK_FLT `flt`
TK_CHR `chr`
TK_BOL `bol`
TK_ARR `arr`
TTKVEC `vec`
TK_SEQ `seq`
TK_MAT `mat`
TK_SET `set`
TK_MAP `map`
TK_STR `str`
TK_NUM `num`
TK_OPT `opt`
TK_MUL `mul`
TK_ANY `any`
TK_PTR `ptr`
TK_ERR `err`
TK_NON `non`
TK_REC `rec`
TK_LST `lst`
TK_ENM `enm`
TK_UNI `uni`
TK_CLS `cls`
TK_STD `std`
TK_MOD `mod`
TK_BLK `blk`
TYPE KEYWORDS - TK:
`(TK_INT|TK_FLT|...)`
Note that all of the type keywords are of three characters long. It is recomanded that new identifiers not to be of the same number of characters, as one day in the future that same identifier can be used s a keyword in FOL compiler.
OK (option keywords)
OK_PUB `pub`
OK_EXP `exp`
OPTION KEYWORDS - OK:
`((OK_PUB|OK_EXP|...),?)*`
Assigning
`(`*WS*`)*(\W)?(`*AK*`)(\[(`*OK*`)?\])?`
`(`*WS*`)*(`*AK*`)`
| `(`*WS*`)*\W(`*AK*`)`
| `(`*WS*`)*(`*AK*`)(\[\])`
| `(`*WS*`)*\W(`*AK*`)(\[\])`
| `(`*WS*`)*(`*AK*`)(\[(`*OK*`)\])`
| `(`*WS*`)*\W(`*AK*`)(\[(`*OK*`)\])`
Identifiers
Identifiers in FOL can be any string of letters, digits and underscores, but beginning with a letter. Two immediate following underscores __ are not allowed.
IDENTIFIER: [a-z A-Z] [a-z A-Z 0-9 _]* | _ [a-z A-Z 0-9 _]+
An identifier is any nonempty ASCII string of the following form:
Either
- The first character is a letter.
- The remaining characters are alphanumeric or _.
Or
- The first character is _.
- The identifier is more than one character. _ alone is not an identifier.
- The remaining characters are alphanumeric or _.
Identifier equality
Two identifiers are considered equal if the following algorithm returns true:
pro sameIdentifier(a, b: string): bol = {
result = a.replace("_", "").toLowerAscii == b.replace("_", "").toLowerAscii
}
That means all letters are compared case insensitively within the ASCII range and underscores are ignored. This rather unorthodox way to do identifier comparisons is called partial case insensitivity and has some advantages over the conventional case sensitivity: It allows programmers to mostly use their own preferred spelling style, be it humpStyle or snake_style, and libraries written by different programmers cannot use incompatible conventions
Comments
Comments in FOL code DON'T follow the traditional style of line (//
) comment forms.
Normal comments
They are represented with backtick.
SINGLE_LINE_COMMENT :
`this is a single line comment`
MULTI_LINE_COMMENT :
`this is a
multi
line
comment`
Docs comments
Doc comments have at the beggining of comment the optinon [doc]
.
DOC_COMMENT:
`[doc] this is a documentation comment`
Whitespaces
Whitespace is any non-empty string containing only characters that have the below Unicode properties:
U+0009
(horizontal tab, '\t')U+000B
(vertical tab)U+000C
(form feed)U+0020
(space, ' ')U+0085
(next line)U+200E
(left-to-right mark)U+200F
(right-to-left mark)U+2028
(line separator)U+2029
(paragraph separator)
New lines
New line are used as end-of-line separators:
U+000A
(line feed, '\n')U+000D
(carriage return, '\r')
Strings
Characters
A character is a single Unicode element enclosed within quotes U+0022
("
) with the exception of U+0022
itself, which must be escaped by a preceding U+005C
character (\
).
var aCharacter: chr = "z\n"
var anotherOne: str = "語\n"
Raw characters
Raw character literals do not process any escapes. They are enclosed within single-quotes U+0027
('
) with the exception of U+0027
itself:
var aCharacter: chr = 'z'
Strings
A string is a single or a sequence of Unicode elements enclosed within quotes U+0022
("
) with the exception of U+0022
itself, which must be escaped by a preceding U+005C
character (\
).
var hiInEnglish: str = "Hello, world!\n"
var hInCantonese: str = "日本語"
Line-breaks are allowed in strings. A line-break is either a newline (U+000A
) or a pair of carriage return and newline (U+000D
, U+000A
). Both byte sequences are normally translated to U+000A
, but as a special exception, when an unescaped U+005C
character (\
occurs immediately before the line-break, the U+005C
character, the line-break, and all whitespace at the beginning of the next line are ignored. Thus a and b are equal:
var a: str = "foobar";
var b: str = "foo\
bar";
assert(a,b);
Escape sequences
Some additional escapes are available in either character or non-raw string literals.
code | description |
---|---|
\p | platform specific newline: CRLF on Windows, LF on Unix |
\r, \c | carriage return |
\n, \l | line feed (often called newline) |
\f | form feed |
\t | tabulator |
\v | vertical tabulator |
\\ | backslash |
\" | quotation mark |
\' | apostrophe |
\ '0'..'9'+ | character with decimal value d; all decimal digits directly following are used for the character |
\a | alert |
\b | backspace |
\e | escape [ESC] |
\x HH | character with hex value HH; exactly two hex digits are allowed |
\u HHHH | unicode codepoint with hex value HHHH; exactly four hex digits are allowed |
\u {H+} | unicode codepoint; all hex digits enclosed in {} are used for the codepoint |
Raw strings
Just like raw characters, raw string literals do not process any escapes either. They are enclosed within single-quotes U+0027
('
) with the exception of U+0027
itself:
var hiInEnglish: str = 'Hello, world!'
Booleans
The two values of the boolean type are written true
and false
:
var isPresent: bol = false;
Numbers
A number is either an integer, floating-point or imaginary. The grammar for recognizing the kind of number is mixed.
Intigers
An integer has one of four forms:
- A decimal literal starts with a decimal digit and continues with any mixture of decimal digits and underscores.
- A hex literal starts with the character sequence
U+0030
U+0078
(0x
) and continues as any mixture (with at least one digit) of hex digits and underscores. - An octal literal starts with the character sequence
U+0030 U+006F
(0o
) and continues as any mixture (with at least one digit) of octal digits and underscores. - A binary literal starts with the character sequence
U+0030 U+0062
(0b
) and continues as any mixture (with at least one digit) of binary digits and underscores.
var decimal: int = 45;
var hexadec: int = 0x6HF53BD5;
var octal: int = 0o822371;
var binary: int = 0b010010010;
Underscore
Underscore character U+005F
(_
) is a special character, that does not represent anything withing the number laterals. An integer lateral containing this character is the same as the one without. It is used only as a syntastc sugar:
var aNumber: int = 540_467;
var bNumber: int = 540467;
assert(aNumber, bNumber)
Floating points
A floating-point has one of two forms:
- A decimal literal followed by a period character
U+002E
(.
). This is optionally followed by another decimal literal. - A decimal literal that follows a period character
U+002E
(.
).
var aFloat: flt = 3.4;
var bFloat: flt = .4;
Imaginary numbers
Symbols
Operators
Fol allows user defined operators. An operator is any combination of the following characters:
= + - * / > .
@ $ ~ & % < :
! ? ^ # ` \ _
The grammar uses the terminal OP
to refer to operator symbols as defined here.
Brackets
Bracket punctuation is used in various parts of the grammar. An open bracket must always be paired with a close bracket. Here are type of brackets used in FOL:
bracket | type | purpose |
---|---|---|
{ } | Curly brackets | Code blocks, Namespaces, Containers |
[ ] | Square brackets | Type options, Container acces, Multithreading |
( ) | Round brackets | Calculations, Comparisons, Argument passing |
< > | Angle brackets |
The grammar uses the terminal BR
to refer to operator symbols as defined here.
Code blocks
Statements
The actions that a program takes are expressed in statements. Common actions include declaring variables, assigning values, calling routine, looping through collections, and branching to one or another block of code, depending on a given condition. The order in which statements are executed in a program is called the flow of control or flow of execution. The flow of control may vary every time that a program is run, depending on how the program reacts to input that it receives at run time.
A statement can consist of a single line of code that ends in a semicolon, or a series of single-line statements in a block. A statement block is enclosed in {} brackets and can contain nested blocks. A statement in programming language theory usually results in something called a side effect. A side effect, loosely defined, is a permanent change of state in a program, such as modifying a global variable or changing the buffer stack.
Types of statement:
- declaration
- control
Here are some statements:
var x: int; // Also a declaration.
x = 0; // Also an assignment.
if(expr) { /*...*/ } // This is why it's called an "if-statement".
for(expr) { /*...*/ } // For-loop.
Expressions
An expression is a sequence of operators and their operands, that specifies a computation. It is a sequence of one or more operands and zero or more operators that can be evaluated to a single value, object, routine, or namespace. Expressions can consist of a literal value, a routine invocation, an operator and its operands, or a simple name. Simple names can be the name of a variable, type member, routine parameter, namespace or type.
Expressions can use operators that in turn use other expressions as parameters, or routine calls whose parameters are in turn other routine calls, so expressions can range from simple to very complex.
Types of expressions are divided into two groups:
- calculations
- literals
- ranges
- access
Statements
The actions that a program takes are expressed in statements. Common actions include declaring variables, assigning values, calling routines, looping through collections, and branching to one or another block of code, depending on a given condition. The order in which statements are executed in a program is called the flow of control or flow of execution. The flow of control may vary every time that a program is run, depending on how the program reacts to input that it receives at run time.
A statement can consist of a single line of code that ends in a semicolon, or a series of single-line statements in a block. A statement block is enclosed in {} brackets and can contain nested blocks. A statement in programming language theory usually results in something called a side effect. A side effect, loosely defined, is a permanent change of state in a program, such as modifying a global variable or changing the buffer stack.
Types of statement:
- declaration
- control
Here are some statements:
var x: int; // Also a declaration.
x = 0; // Also an assignment.
if(expr) { /*...*/ } // This is why it's called an "if-statement".
for(expr) { /*...*/ } // For-loop.
Control
At least two linguistic mechanisms are necessary to make the computations in programs flexible and powerful: some means of selecting among alternative control flow paths (of statement execution) and some means of causing the repeated execution of statements or sequences of statements. Statements that provide these kinds of capabilities are called control statements. A control structure is a control statement and the collection of statements whose execution it controls. This set of statements is in turn generally structured as a block, which in addition to grouping, also defines a lexical scope.
There are two types of control flow mechanisms:
- choice -
when
- loop -
loop
Choice type
when(condition){ case(condition){} case(condition){} * {} };
when(variable){ is (value){}; is (value){}; * {}; };
when(variable){ in (iterator){}; in (iterator){}; * {}; };
when(iterable){ has (member){}; has (member){}; * {}; };
when(generic){ of (type){}; of (type){}; * {}; };
when(type){ on (channel){}; on (channel){}; };
Condition
when(true) {
case (x == 6){ // implementation }
case (y.set()){ // implementation }
* { // default implementation }
}
Valueation
when(x) {
is (6){ // implementation }
is (>7){ // implementation }
* { // default implementation }
}
Iteration
when(2*x) {
in ({0..4}){ // implementation }
in ({ 5, 6, 7, 8, }){ // implementation }
* { // default implementation }
}
Contains
when({4,5,6,7,8,9,0,2,3,1}) {
has (5){ // implementation }
has (10){ // implementation }
* { // default implementation }
}
Generics
when(T) {
of (int){ // implementation }
of (str){ // implementation }
* { // default implementation }
}
Channel
when(str) {
on (channel){ // implementation }
on (channel){ // implementation }
* { // default implementation }
}
Loop type
loop(condition){};
loop(iterable){};
Condition
loop( x == 5 ){
// implementation
};
Enumeration
loop( x in {..100}){
// implementation
}
loop( x in {..100}) if ( x % 2 == 0 )){
// implementation
}
loop( x in {..100} if ( x in somearra ) and ( x in anotherarray )){
// implementation
}
Iteration
loop( x in array ){
// implementation
}
Expressions
An expression is a sequence of operators and their operands, that specifies a computation. It is a sequence of one or more operands and zero or more operators that can be evaluated to a single value, object, routine, or namespace. Expressions can consist of a literal value, a routine invocation, an operator and its operands, or a simple name. Simple names can be the name of a variable, type member, routine parameter, namespace or type.
Expressions can use operators that in turn use other expressions as parameters, or routine calls whose parameters are in turn other routine calls, so expressions can range from simple to very complex.
Types of expressions are divided into two groups:
- calculations
- literals
- ranges
- access
Calculations
In fol, every calcultaion, needs to be enclosed in rounded brackets ( //to evaluate )
- except in one line evaluating, the curly brackets are allowed too { // to evaluate }
:
fun adder(a, b: int): int = {
retun a + b // this will throw an error
}
fun adder(a, b: int): int = {
retun (a + b) // this is the right way to enclose
}
Order of evaluation is strictly left-to-right, inside-out as it is typical for most others imperative programming languages:
.echo((12 / 4 / 8)) // 0.375 (12 / 4 = 3.0, then 3 / 8 = 0.375)
.echo((12 / (4 / 8))) // 24 (4 / 8 = 0.5, then 12 / 0.5 = 24)
Calculation expressions include:
- arithmetics
- comparison
- logical
- compounds
Arithmetics
The behavior of arithmetic operators is only on intiger and floating point primitive types. For other types, there need to be operator overloading implemented.
symbol | description |
---|---|
- | substraction |
* | multiplication |
+ | addition |
/ | division |
% | reminder |
^ | exponent |
assert((3 + 6), 9);
assert((5.5 - 1.25), 4.25);
assert((-5 * 14), -70);
assert((14 / 3), 4);
assert((100 % 7), 2);
Comparisons
Comparison operators are also defined both for primitive types and many type in the standard library. Parentheses are required when chaining comparison operators. For example, the expression a == b == c
is invalid and may be written as ((a == b) == c)
.
Symbol | Meaning |
---|---|
== | equal |
!= | not equal |
>> | greater than |
<< | Less than |
>= | greater than or equal to |
<= | Less than or equal to |
assert((123 == 123));
assert((23 != -12));
assert((12.5 >> 12.2));
assert(({1, 2, 3} << {1, 3, 4}));
assert(('A' <= 'B'));
assert(("World" >= "Hello"));
Logical
A branch of algebra in which all operations are either true or false, thus operates only on booleans, and all relationships between the operations can be expressed with logical operators such as:
and
(conjunction), denoted(x and y)
, satisfies(x and y) = 1
ifx = y = 1
, and(x and y) = 0
otherwise.or
(disjunction), denoted(x or y)
, satisfies(x or y) = 0
ifx = y = 0
, and(x or) = 1
otherwise.not
(negation), denoted(not x)
, satisfies(not x) = 0
ifx = 1
and (not x) = 1if
x = 0`.
assert((true and false), (false and true));
assert((true or false), true)
assert((not true), false)
Compounds
There are further assignment operators that can be used to modify the value of an existing variable. These are the compounds or aka compound assignments. A compound assignment operator is used to simplify the coding of some expressions. For example, using the operators described earlier we can increase a variable's value by ten using the following code:
value = value + 10;
This statement has an equivalent using the compound assignment operator for addition (+=).
value += 10;
There are compound assignment operators for each of the six binary arithmetic operators: +
, -
, *
, /
, %
and ^
. Each is constructed using the arithmetic operator followed by the assignment operator. The following code gives examples for addition +=
, subtraction -=
, multiplication *=
, division /=
and modulus %=
:
var value: int = 10;
(value += 10); // value = 20
(value -= 5); // value = 15
(value *= 10); // value = 150
(value /= 3); // value = 50
(value %= 8); // value = 2
Compound assignment operators provide two benefits. Firstly, they produce more compact code; they are often called shorthand operators for this reason. Secondly, the variable being operated upon, or operand, will only be evaluated once in the compiled application. This can make the code more efficient.
Literals
A literal expression consists of one or more of the numerical/letter forms described earlier. It directly describes a numbers, characters, booleans, containers and constructs.
There are two type of literals:
- values
- calls
Value literals
Value literals are the simpliest expressions. They are direct values assigned to variables and are divided into two types:
- singletons
- clusters
Singelton literals
Singleton literals represent one sigle values:
4 // intiger literal
0xA8 // hex-intiger literal
4.6 // floating-point literal
5i // imaginary literal
"c" // character literal
"one" // string literal
true // boolean literal
Cluster literals
Cluster literals represent both container types and construct types. Cluster literals are always enclosed within curly brackets { }
. The difference between scopes and cluster literals is that cluster literals shoud always have comma ,
within the initializaion and assignment brackets, e.g { 5, }
.
Containers
Some simple container expressions
{ 5, 6, 7, 8, } // array, vector, sequences
{ "one":1, "two":2, } // maps
{ 6, } // single element container
A 3x3x3 matrix
{{{1,2,3},{4,5,6},{7,8,9}},{{1,2,3},{4,5,6},{7,8,9}},{{1,2,3},{4,5,6},{7,8,9}}}
Constructs
// constructs
{ email = "someone@example.com", username = "someusername123", active = true, sign_in_count = 1 }
// nested constructs
{
FirstName = "Mark",
LastName = "Jones",
Email = "mark@gmail.com",
Age = 25,
MonthlySalary = {
Basic = 15000.00,
Bonus = {
HTA = 2100.00,
RA = 5000.00,
},
},
}
Call literals
Call literals are function calls that resolve to values:
var seven: int = add(2, 5); // assigning variables "seven" to function call "add"
`typ Vector: rec = { var x: flt var y: flt }
typ Rect: rec = { var pos: Vector var size: Vecotr }
fun make_rect(min, max: Vector): Rect { return [Rect]{{min.x, min.y}, {max.x - max.y, max.y - max.y}} return [Rect]{pos = {min.x, min.y}, size = {max.x - max.y, max.y - max.y}} }
`
Ranges
There are two range expressions:
- Defined ranges
- Undefined ranges
Defined ranges
Defined ranges represent a group of values that are generated as a sequence based on some predefined rules. Ranges are represented with two dots ..
operator.
{ 1..8 } // a range from 1 to 8
{ 1,2,3,4,5,6,7,8 }
{ 8..1 } // a range from 8 to 1
{ 8,7,6,5,4,3,2,1 }
{ 1..8..2 } // a range from 1 to 8 jumping by 2
{ 1,3,5,7 }
{ 3..-3 } // a range from 4 to -4
{ 3,2,1,0,-1,-2,-3 }
{ -3..3 } // a range from -3 to 3
{ -3,-2,-1,0,1,2,3 }
{ ..5 } // a range form 0 to 5
{ 0,1,2,3,4,5 }
{ ..-5 } // a range from 0 to -5
{ 0,-1,-2,-3,-4,-5 }
{ 5.. } // a range from 5 to 0
{ 5,4,3,2,1,0 }
{ -5.. } // a range from -5 to 0
{ -5,-4,-3,-2,-1,0 }
syntax | meaning |
---|---|
start.. end | from start to end |
.. end | from zero to end |
start.. | from start to zero |
Undefined ranges
Undefined ranges represent values that have only one side defined at the definition time, and the compiler defines the other side at compile time. They are represented with three dots ...
{ 2... } // from 2 to infinity
syntax | meaning |
---|---|
start... | from start to infinite |
In most of the cases, they are used for variadic parameters passing:
fun calc(number: ...int): int = { return number[0] + number[1] + number[2] * number[3]}
Access
There are four access expresions:
- namespace member access
- routine member access
- container memeber access
- field member access
Subprogram access
In most programming languages, it is called "method-call expresion". A method call consists of an expression (the receiver) followed by a single dot .
, an expression path segment, and a parenthesized expression-list:
"3.14".cast(float).pow(2); // casting a numbered string to float, then rising it to power of 2
Namespaces access
Accesing namespaces is done through double colon operator ::
:
use log mod[std] = { fmt::log }; // using the log namespace of fmt
io::console::write_out.echo(); // echoing out
Container access
Array, Vectors, Sequences, Sets
Containers can be indexed by writing a square-bracket-enclosed expression of type int[arch]
(the index) after them.
var collection: int = { 5, 4, 8, 3, 9, 0, 1, 2, 7, 6 }
collection[5] // get the 5th element staring from front (this case is 0)
collection[-2] // get the 3th element starting from back (this case is 1)
Containers can be accessed with a specified range too, by using colon within a square-bracket-enclosed:
syntax | meaning |
---|---|
: | the whole container |
elA: elB | from element elA to element elB |
: elA | from beginning to element elA |
elA: | from element elA to end |
collection[-0] // last item in the array
{ 6 }
collection[-1:] // last two items in the array
{ 7, 6 }
collection[:-2] // everything except the last two items
{ 5, 4, 8, 3, 9, 0, 1, 2 }
If we use double colon within a square-bracket-enclosed then the collection is inversed:
syntax | meaning |
---|---|
:: | the whole container in reverse |
elA:: elB | from element elA to element elB in reverse |
:: elA | from beginning to element elA in reverse |
elA:: | from element elA to end in reverse |
collection[::] // all items in the array, reversed
{ 6, 7, 2, 1, 0, 9, 3, 8, 4, 5 }
collection[2::] // the first two items, reversed
{ 4, 5 }
collection[-2::] // the last two items, reversed
{ 6, 7 }
collection[::-3] // everything except the last three items, reversed
{ 2, 1, 0, 9, 3, 8, 4, 5 }
Matrixes
Matrixes are 2D+ arrays, thus they have a bit more complex acces way:
var aMat = mat[int, int] = { {1,2,3}, {4,5,6}, {7,8,9} };
nMat[[1][0]] // this will return 4
// first [] accesses the first dimension, then second [] accesses the second
All other operations are the same like arrays.
Maps
Accesing maps is donw by using the key within square-bracket-enclosed:
var someMap: map[str, int] = { {"prolog", 1}, {"lisp", 2}, {"c", 3} }
someMap["lisp"] // will return 2
Axioms
Accesing axioms is more or less like accessing maps, but more verbose and matching through backtracing, and the return is always a vector of elements (empty if no elements are found):
var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"} };
parent["albert",*] // this will return strng vector: {"bob"}
parent["bob",*] // this will return strng vector: {"carl","tom"}
parent[*,_] // this will match to {"albert", "alice", "bob"}
Matching can be with a vector too:
var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"}, {"maggie","bill"} };
var aVec: vec[str] = { "tom", "bob" };
parent[*,aVec] // will match all possible values that have "tom" ot "bob" as second element
// in this case will be a strng vector: {"albert", "alice", "bob"}
a more complex matching:
var class: axi;
class.add({"cs340","spring",{"tue","thur"},{12,13},"john","coor_5"})
class.add({"cs340","winter",{"tue","fri"},{12,13},"mike","coor_5"})
class.add({"cs340",winter,{"wed","fri"},{15,16},"bruce","coor_3"})
class.add({"cs101",winter,{"mon","wed"},{10,12},"james","coor_1"})
class.add({"cs101",spring,{"tue","tue"},{16,18},"tom","coor_1"})
var aClass = "cs340"
class[aClass,_,[_,"fri"],_,*,_] // this will return string vector: {"mike", bruce}
// it matches everything that has aClass ad "fri" within
// and ignore ones with meh symbol
Avaliability
To check if an element exists, we add :
before accessing with []. Thus this will return true
if element exists.
var val: vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
val:[5] // returns true
val:[15] // returns false
var likes: axi[str, str] = { {"bob","alice"} , {"alice","bob"}, {"dan","sally"} };
likes["bob","alice"]: // will return true
likes["sally","dan"]: // will return false
In-Place assignment
One of the features that is very important in arrays is that they can assign variables immediately:
var val: vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var even: vec = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
val[even => Y] // this equals to {2, 4, 6, 8, 10} and same time assign to Y
.echo(Y) // will print {2, 4, 6, 8, 10}
This dows not look very much interesting here, you can just as easy assign the whole filtered array to a variable, but it gets interesting for axioms:
var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"}, {"maggie","bill"} };
parent:[* => Y,"bob"] // this one returns true if we find a parent of "bob"
// same time it assigns parent to a string vector `Y`
Field access
Field access expressoin accesses fields inside constructs. Here is a recorcalled user
:
var user1: user = {
email = "someone@example.com",
username = "someusername123",
active = true,
sign_in_count = 1
};
fun (user)getName(): str = { result = self.username; };
There are two types of fields that can be accesed within constructs:
- methods
- data
Methods
Methods are accesed the same way like routine member access.
user1.getName()
Data
There are multiple ways to acces data within the construct. The easiest one is by dot operator .
:
user1.email // accessing the field through dot-accesed-memeber
Another way is by using square bracket enclosed by name:
user1[email] // accessing the field through square-bracket-enclosed by name
And lastly, by square bracket enclosed by index:
user1[0] // accessing the field through square-bracket-enclosed by index
Metaprogramming
Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running. In some cases, this allows programmers to minimize the number of lines of code to express a solution, in turn reducing development time. It also allows programs greater flexibility to efficiently handle new situations without recompilation.
Macro system (aka template metaprogramming) is a metaprogramming technique in which templates are used by a compiler to generate temporary source code, which is merged by the compiler with the rest of the source code and then compiled. The output of these templates include compile-time constants, data structures, and complete functions. The use of templates can be thought of as compile-time execution.
{{% notice warn %}}
IT IS NOT SUGGESTED TO RELAY HEAVILY ON MACROS BECAUSE THE CODE MIGHT LOOSES THE READABILITY WHEN SOMEONE TRIES TO USE YOUR CODE.
{{% /notice %}}
Usage
If you have a large application where many of the functions include a lot of boilerplate code, you can create a mini-language that will do the boilerplate code for you and allow you to code only the important parts. Now, if you can, it's best to abstract out the boilerplate portions into a function. But often the boilerplate code isn't so pretty. Maybe there's a list of variables to be declared in every instance, maybe you need to register error handlers, or maybe there are several pieces of the boilerplate that have to have code inserted in certain circumstances. All of these make a simple function call impossible. In such cases, it is often a good idea to create a mini-language that allows you to work with your boilerplate code in an easier fashion. This mini-language will then be converted into your regular source code language before compiling.
Metaprogramming works by circumventing the language. It allows for the alteration of languages through program transformation systems. This procedure gives metaprogramming the freedom to use languages even if the language does not employ any metaprogramming characteristics.
Types
In FOL there are many templates and metaprogramming elements.
- Build-In
- Macros
- Alternatives
- Defaults
- Templates
{{% notice tip %}}
WITH BUILD-INS, ALTERNATIVES, MACROS, DEFAULTS AND TEMPLATES, YOU CAN COMPLETELY MAKE A NEW TYPESYSTEM, WITH ITS OWN KEYWORDS, IDENTIFIERS, AND BEHAVIOUR.
{{% /notice %}}
Build-in
Fol has many build-in functions and macros offered by compiler, and you access them by . (with space/newline/bracket before):
var contPoint: ptr[int] = 10; // make a pointer and asign the memory to value of 10
.print(.pointer_value(contPoint)); // print the dereferenced value of pointer
{{% placeholder %}}
.echo()
- print on screen
.not()
- negate
.cast()
- type casting
.as()
- type casting
.eq()
- check for equality
.nq()
- check for inequality
.gt()
- greater than
.lt()
- less than
.ge()
- greater or equal
.le()
- less or equal
.de_alloc()
- drop variable from scope
.give_back()
- return ownership
.size_of()
- variable type size
.addres_of()
- pointer address
.pointer_value()
- value of the pointer
{{% /placeholder %}}
Macros
Are a very complicated system, and yet can be used as simply as in-place replacement. A lot of build-in macros exist in the language to make the code more easy to type. Below are some system defined macros.
For example, wherever $
is before any
variable name, its replaced with .to_string
. Or wherever !
is before bol
name, its replaced with .not
but when the same !
is placed before ptr
it is replaced with .delete_pointer
.
def '$'(a: any): mac = '.to_string'
def '!'(a: bol): mac = '.not '
def '!'(a: ptr): mac = '.delete_pointer';
def '*'(a: ptr): mac = '.pointer_value';
def '#'(a: any): mac = '.borrow_from';
def '&'(a: any): mac = '.address_of';
Alternatives
Alternatives are used when we want to simplify code. For example, define an alternative, so whenever you write +var
it is the same as var[+]
.
def '+var': alt = 'var[+]'
def '~var': alt = 'var[~]'
def '.pointer_content': alt = '.pointer_value'
Defaults
Defaults are a way to change the default behaviour of options. Example the default behaviour of str
when called without options. By defalt str
is it is saved on stack, it is a constant and not public, thus has str[pil,imu,nor]
, and we want to make it mutable and saved on heap by default:
def 'str': def[] = 'str[new,mut,nor]'
Templates
Templates are supposed to be mostly used for operator overloading. They are glorified functions, hence used with pro
or fun
instead of def
.
For example here is how the !=
is defined:
fun '!='(a, b: int): bol = { return .not(.eq(a, b)) }
.assert( 5 != 4 )
or define $
to return the string version of an object (careful, it is object$
and not $object
, the latest is a macro, not a template):
pro (file)'$': str = { return "somestring" }
.echo( file$ )
Type system
A data type defines a collection of data values and a set of predefined operations on those values. Computer programs produce results by manipulating data. An important factor in determining the ease with which they can perform this task is how well the data types available in the language being used match the objects in the real world of the problem being addressed. Therefore, it is crucial that a FOL supports an appropriate collection of data types and structures.
The type system of a programming language defines how a type is associated with each expression in the language and includes its rules for type equivalence and type compatibility. Certainly, one of the most important parts of understanding the semantics of a programming language is understanding its type system.
Data types that are not defined in terms of other types are called primitive data types. Nearly all programming languages provide a set of primitive data types. Some of the primitive types are merely reflections of the hardware for example, most integer types. Others require only a little nonhardware support for their implementation.
Types
Every value in Fol is of a certain data type, which tells Fol what kind of data is being specified so it knows how to work with that data.
Types are divided into two groups:
- perdefined
- constructed
Predefned types
There are four predefned types:
- ordinal ( integer, float, boolean, character )
- container ( array, vector, sequence, matrix, map, set )
- complex (string, number, pointer, error )
- special ( optional, never, any, null )
Constructed types
- records
- tables
- entries
Ordinal
Ordinal types
Ordinal types have the following characteristics:
- Ordinal types are countable and ordered. This property allows the operation of functions as inc, ord, dec on ordinal types to be defined.
- Ordinal values have a smallest possible value. Trying to count further down than the smallest value gives a checked runtime or static error.
- Ordinal values have a largest possible value. Trying to count further than the largest value gives a checked runtime or static error.
Ordinal types are the most primitive type of data:
- Intigers:
int[options]
- Floating:
flt[options]
- Characters:
chr[options]
- Booleans:
bol
Intiger type
An integer is a number without a fractional component. We used one integer of the u32 type, the type declaration indicates that the value it’s associated with should be an unsigned integer (signed integer types start with i, instead of u) that takes up 32 bits of space:
var aVar: int[u32] = 45;
Each variant can be either signed or unsigned and has an explicit size. Signed and unsigned refer to whether it’s possible for the number to be negative or positive—in other words, whether the number needs to have a sign with it (signed) or whether it will only ever be positive and can therefore be represented without a sign (unsigned). It’s like writing numbers on paper: when the sign matters, a number is shown with a plus sign or a minus sign; however, when it’s safe to assume the number is positive, it’s shown with no sign.
Length | Signed | Unsigned |
-----------------------------------
8-bit | 8 | u8 |
16-bit | 16 | u16 |
32-bit | 32 | u32 |
64-bit | 64 | u64 |
128-bit | 128 | u128 |
arch | arch | uarch |
Float type
Fol also has two primitive types for floating-point numbers, which are numbers with decimal points. Fol’s floating-point types are flt[32]
and flt[64]
, which are 32 bits and 64 bits in size, respectively. The default type is flt[64]
because on modern CPUs it’s roughly the same speed as flt[32]
but is capable of more precision.
Length | Type |
--------------------
32-bit | 32 |
64-bit | 64 |
arch | arch |
Floating-point numbers are represented according to the IEEE-754 standard. The flt[32]
type is a single-precision float, and flt[f64]
has double precision.
pro[] main: int = {
var aVar: flt = 2.; // float 64 bit
var bVar: flt[64] = .3; // float 64 bit
.assert(.sizeof(aVar) == .sizeof(bVar)) // this will true
var bVar: flt[32] = .54; // float 32 bit
}
Character type
In The Unicode Standard 8.0, Section 4.5 "General Category" defines a set of character categories. Fol treats all characters in any of the letter as Unicode letters, and those in the Number category as Unicode digits.
chr[utf8,utf16,utf32]
def testChars: tst["some testing on chars"] = {
var bytes = "hello";
.assert(.typeof(bytes) == *var [5:0]u8);
.assert(bytes.len == 5);
.assert(bytes[1] == "e");
.assert(bytes[5] == 0);
.assert("e" == "\x65");
.assert("\u{1f4a9}" == 128169);
.assert("💯" == 128175);
.assert(.mem.eql(u8, "hello", "h\x65llo"));
}
Boolean type
The boolean type is named bol
in Fol and can be one of the two pre-defined values true
and false
.
bol
Container
Containers are of compound types. They contain other primitive or constructed types. To access the types in container those brackets are used: []
, so:
var container: type = { element, element, element } // declaring a container
var varable: type = container[2] // accessing the last element
{{% notice note %}}
Containers are always zero indexed
{{% /notice %}}
Static Arrays
Arrays
arr[type,size]
Arrays are the most simple type of container. They contain homogeneous type, meaning that each element in the array has the same type. Arrays always have a fixed length specified as a constant expression arr[type, size]
. They can be indexed by any ordinal type to acces its members.
pro[] main: int = {
var anArray: arr[int, 5] = { 0, 1, 2, 3, 4 }; // declare an array of intigers of five elements
var element = anArray[3]; // accessing the element
.echo(element) // prints: 3
}
To allocate memory on heap, the var[new]
is used more about memory, ownreship and pointer :
pro[] main: int = {
var[new] aSequence: arr[str] = { "get", "over", "it" }; // this array is stored in stack
}
Dynamic arrays
Dynamic are similar to arrays but of dynamic length which may change during runtime (like strings). A dynamic array s
is always indexed by integers from 0
to .len(s)-1
and its bounds are checked.
The lower bound of an array or sequence may be received by the built-in .low()
, the higher bound by .high()
. The length may be received by .len()
.
{{% notice tip %}}
Dynamic arrays are a dynamically allocated (hence the name), thus if not allocated in heap but in stack, the size will be defined automatically in compile time and will be changed to static array.
{{% /notice %}}
There are two implementations of dynamic arrays:
- vectors
vec[]
- sequences
seq[]
Vecotors
Vectors are dynamic arrays, that resizes itself up or down depending on the number of content.
Advantage:
- accessing and assignment by index is very fast O(1) process, since internally index access is just [address of first member] + [offset].
- appending object (inserting at the end of array) is relatively fast amortized O(1). Same performance characteristic as removing objects at the end of the array. Note: appending and removing objects near the end of array is also known as push and pop.
Disadvantage:
- inserting or removing objects in a random position in a dynamic array is very slow O(n/2), as it must shift (on average) half of the array every time. Especially poor is insertion and removal near the start of the array, as it must copy the whole array.
- Unpredictable performance when insertion or removal requires resizing
- There is a bit of unused space, since dynamic array implementation usually allocates more memory than necessary (since resize is a very slow operation)
In FOL vecotrs are represented like this:
vec[type]
Example:
pro[] main: int = {
var[new] aSequence: seq[str] = { "get", "over", "it" }; // declare an array of intigers of five elements
var element = aSequence[3]; // accessing the element
}
Sequences
Sequences are linked list, that have a general structure of [head, [tail]], head is the data, and tail is another Linked List. There are many versions of linked list: singular, double, circular etc...
Advantage:
- fast O(1) insertion and removal at any position in the list, as insertion in linked list is only breaking the list, inserting, and repairing it back together (no need to copy the tails)
- linked list is a persistent data structure, rather hard to explain in short sentence, see: wiki-link . This advantage allow tail sharing between two linked list. Tail sharing makes it easy to use linked list as copy-on-write data structure.
Disadvantage:
- Slow O(n) index access (random access), since accessing linked list by index means you have to recursively loop over the list.
- poor locality, the memory used for linked list is scattered around in a mess. In contrast with, arrays which uses a contiguous addresses in memory. Arrays (slightly) benefits from processor caching since they are all near each other
In FOL sequneces are represented like this:
seq[type]
Example:
pro[] main: int = {
var[new] aSequence: seq[str] = { "get", "over", "it" }; // declare an array of intigers of five elements
var element = aSequence[3]; // accessing the element
}
SIMD
Matrixes are of type SIMD (single instruction, multiple data )
Matrix
mat[sizex]
mat[sizex,sizey]
mat[sizex,sizey,sizez]
Sets
set[type,type,type..]
A set is a general way of grouping together a number of values with a variety of types into one compound type. Sets have a fixed length: once declared, they cannot grow or shrink in size. In other programming languages they usually are referenced as tuples.
pro[] main: int = {
var aSet: set[str, flt, arr[int, 2]] = { "go", .3, { 0, 5, 3 } };
var element = aSet[2][1]; // accessing the [1] element of the `arr` in the set
.echo(element) // prints: 5
}
Maps
map[key,value]
A map is an unordered group of elements of one type, called the element type, indexed by a set of unique keys of another type, called the key type.
pro[] main: int = {
var aMap: map[str, int] = { {"US",45}, {"DE",82}, {"AL",54} };
var element = aMap["US"]; // accessing the "US" key
.echo(element) // prints: 45
}
The number of map elements is called its length. For a map aMap
, it can be discovered using the built-in function .len
and may change during execution To add a new element, we use name+[element]
or add
function:
.echo(.len(aMap)) // prints: 3
aMap.add( {"IT",55} )
aMap+[{"RU",24}]
.echo(.len(aMap)) // prints: 4
The comparison operators ==
and !=
must be fully defined for operands of the key type; thus the key type must not be a function, map, or sequence.
{{% notice tip %}}
Maps are a growable containers too, thus if not allocated in heap but in stack, the size will be defined automatically in compile time and will be changet to static containers
{{% /notice %}}
Axiom
axi[typ, typ]
A axiom is a list of facts. A fact is a predicate expression that makes a declarative statement about the problem domain. And whenever a variable occurs in a expression, it is assumed to be universally quantified as silent.
var likes: axi[str, str] = { {"bob","alice"} , {"alice","bob"}, {"dan","sally"} };
{{% notice info %}}
Accesing any container always returns the value, but if we put an :
before the access symbol so :[]
, then it will return true
or false
if there is data or not on the specified access.
{{% /notice %}}
likes["bob","alice"] // will return {"bob","alice"}
likes:["bob","alice"] // will return true
likes["sally","dan"] // will return {}
likes:["sally","dan"] // will return false
Axioms are a data types that are meant to be used with logic programming. There are containers where facts are stated, and when we want to acces the data, they are always served as containers.
var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"} };
parent["bob",*] // this gets all elements that "bob" relates to
{"carl", "tom"}
parent[*,"bob"] // this gets all elements that "bob" relates from
{"albert", "alice"}
Adding new element can be done like in other containers:
var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"} };
parent.add({"albert","betty"})
parent.add({"albert","bill"})
parent.add({"alice","betty"})
parent.add({"alice","bill"})
And they can be nesetd too:
var line: axi[axi[int, int], axi[int, int]] = {{{4,5},{4,8}},{{8,5},{4,5}}}
And we can use the simplified form too, just axi
instead of all the type. We let the compiler fill in the for us:
var line: axi = {{{4,5},{4,8}},{{8,5},{4,5}}}
Complex
Strings
Strings are a complex type that are made of array of chars with null terminator '\0', and by default is utf8 encoded:
str[]
Number
Number type is an abstraction of intiger and float type. It can be imaginary too.
num[]
Pointer
ptr[]
Error
err[]
Special
Optional
Either are empty or have a value
opt[]
Never
nev[]
The never type is a type with no values, representing the result of computations that never complete.
Union
Union is a data type that allows different data types to be stored in the same memory locations. Union provides an efficient way of reusing the memory location, as only one of its members can be accessed at a time. It uses a single memory location to hold more than one variables. However, only one of its members can be accessed at a time and all other members will contain garbage values. The memory required to store a union variable is the memory required for the largest element of the union.
We can use the unions in the following locations.
- Share a single memory location for a variable and use the same location for another variable of different data type.
- Use it if you want to use, for example, a long variable as two short type variables.
- We don’t know what type of data is to be passed to a function, and you pass union which contains all the possible data types.
var aUnion: uni[int[8], int, flt];
Any
any[]
Null
nil
Syntax items
FOL uses different keywords to bind functionality and data to variables.
modules
used for: imports and includes
use[]
definitions
used for: preporcesr, macros, blocks, definitions ...
def[]
variables
used for all variables: ints, strings, bools, arrays, vecotrs ...
var[]
functions
used for subporgrams: procedures and functions
pro[]
fun[]
log[]
constructs
used for: new types, records, objects, interfaces, enums ...
typ[]
std[]
Variables
Here are some of the ways that variables can be defined:
var[pub,mut] somename: num[i32] = 98;
var[pub,exp] snotherone: str = "this is a string"
var[~] yetanother = 192.56
var[+] shortlet = true
var anarray: arr[str,3] = { "one", "two", "three" }
var asequence : seq[num[i8]] = { 20, 25, 45, 68, 73,98 }
var multiholder: set[num, str] = { 12, "word" }
var anothermulti: set[str, seq[num[f32]]] = { "string", {5.5, 4.3, 7, .5, 3.2} }
var shortvar = anothermulti[1][3]
var anymulti: any = {5, 10, "string", {'a',"word",{{0, "val"},{1, "nal"}}}, false}
var getSomeVal = anymulti[3][2][0][0] | < 15 | shortvar
Assignments
Following the general rule of FOL:
declaration[options] name: type[options] = { implementation; };
then declaring a new variable is like this:
var[pub] aVar: int[32] = 64
however, the short version can be used too, and the compiler figures out at compute time the type:
var shortVar = 24; // compiler gives this value of `int[arch]`
When new variable is created, and uses an old variable to assign, the value is cloned, not referenced:
pro[] main: int = {
var aVar: int = 55;
var newVar: int = aVar;
.assert(&aVar == &newVar) // this will return false
}
Two variables can not have the same memory location, unless we either borrow, or use pointers.
Variables can be assigned to an output of a function:
pro[] main: int = {
fun addFunc(x, y: int): int = {
return x + y;
}
var aVar: int = addFunc(4, 5);
}
Piping / Ternary
Piping can be used as ternary operator. More about piping can be found here. Here is an example, the code below basically says: if the function internally had an error, don't exit the program, but assign another value (or default value) to the variable:
pro[] main: int = {
fun addFunc(x, y: int): int = {
return x + y;
}
var aVar: int = addFunc(4, 5) | result > 8 | return 6;
}
Borrowing
If we want to reference a variable, the easiest way is to borrow the variable, use inside another scope (or the same) and return it back. If the ownership is not returned manually, by the end of the scope, it gets returned automatically.
pro[] main: int = {
var[~] aVar: int = 55;
{
var[bor] newVar: int = aVar // var[bor] represents borrowing
.echo(newVar) // this return 55
}
.echo(aVar) // here $aVar it not accesible, as the ownership returns at the end of the scope
.echo(newVar) // we cant access the variable because the scope has ended
}
More on borrowing you can find here
Options
As with all other blocks, var
have their options: var[opt]
:
Options can be of two types:
- flags eg.
var[mut]
- values eg.
var[pri=2]
Flag options can have symbol aliases eg. var[mut]
is the somename as var[~]
.
| opt | s | type | description | control |
----------------------------------------------------------------------------------------------
| mut | ~ | flag | making a variable mutable | mutability |
| imu | | flag | making a variable imutable (default) | |
| sta | ! | flag | making a variable a static | |
| rac | ? | flag | making a variable reactive | |
----------------------------------------------------------------------------------------------
| exp | + | flag | making a global variable pubic | visibility |
| nor | | flag | making a global variable normal (default) | |
| hid | - | flag | making a global variable hidden | |
Alternatives
There is a shorter way for variables using alternatives, for example, instead of using var[+]
, a leaner +var
can be used instead.
def shko: mod[] = {
+var aVar: int = 55;
pro[] main: int { .echo(aVar) }
}
However, when we use two option in varable, only one can use the alternative form, so instead of using var[mut,exp]
, this can be used +var[mut]
or +var[~]
, or vice varsa ~var[exp]
or ~var[+]
:
def shko: mod[] = {
+var[mut] aVar: int = 55;
pro[] main: int { .echo(aVar) }
}
Types
Immutable types (constants)
By default when a variable is defined without options, it is immutable type, for example here an intiger variable:
pro[] main: int = {
var aNumber: int = 5;
aNumber = 54; // reassigning varibale $aNumber thorws an error
}
Mutable types
If we want a variable to be mutable, we have to explicitly pass as an option to the variable var[mut]
or var[~]
:
pro[] main: int = {
var[mut] aNumber: int = 5
var[~] anotherNumber: int = 24
aNumber, anotherNumber = 6 // this is completely fine, we assign two wariables new values
}
Reactive types
Reactive types is a types that flows and propagates changes.
For example, in an normal variable setting, var a = b + c
would mean that a
is being assigned the result of b + c
in the instant the expression is evaluated, and later, the values of b
and c
can be changed with no effect on the value of a
. On the other hand, declared as reactive, the value of a
is automatically updated whenever the values of b
or c
change, without the program having to re-execute the statement a = b + c
to determine the presently assigned value of a
.
pro[] main: int = {
var[mut] b, c = 5, 4;
var[rac] a: int = b + c
.echo(a) // prints 9
c = 10;
.echo(a) // now it prints 10
}
Static types
Is a variable which allows a value to be retained from one call of the function to another, meaning that its lifetime declaration. and can be used as var[sta]
or var[!]
. This variable is special, because if it is initialized, it is placed in the data segment (aka: initialized data) of the program memory. If the variable is not set, it is places in .bss segmant (aka: uninitialized data)
pro[] main: int = {
{
var[!] aNumber: int = 5
}
{
.echo(aNumber) // it works as it is a static variable.
}
}
Scope
As disscussed before, files with same name share the same functions and global variables. However, those variables and functions can't be accesed if the whole module is imported in another project. In order for a variable to be accest by the importer, it needs to be have the exp
flag option, so var[exp]
, or var[+]
module shko, file1.fol
def shko: mod[] = {
fun[+] add(a, b: int) = { return a + b }
fun sub(a, b: int) = { return a - b }
}
module vij, file1.fol
use shko: mod[loc] = {../folder/shko}
def vij: mod[] = {
pro[] main: int {
.echo(add( 5, 4 )) // this works perfectly fine, we use a public/exported function
.echo(sub( 5, 4 )) // this throws an error, we are trying use a function that is not visible to other libraries
}
}
There is even the opposite option too. If we want a function/variable to be only used inside the file ( so same package but only for that file ) then we use hid
option flag: var[hid]
or var[-]
file1.fol
def shko: mod[] = {
var[-] aVar: str = "yo, sup!"
}
file2.fol
def shko: mod[] = {
pro[] main: int { .echo(aVar) } // this will thro an error (cos $aVar is declared private/hidden)
}
Multiple
Many to many
Many variables can be assigned at once, This is especially usefull, if variables have same options but different types eg. variable is mutabe and exported:
~var[exp] oneVar: int[32] = 24, twoVar = 13, threeVar: string = "shko";
Or to assign multiple variables of the same type:
~var[exp] oneVar, twoVar: int[32] = 24, 13;
To assign multiple variables of multiple types, the type is omitted, however, this way we can not put options on the type (obviously, the default type is assign by compiler):
~var[exp] oneVar, twoVar, threeVar = 24, 13, "shko";
Another "shameless plagiarism" from golang can be used by using ( ... )
to group variables:
~var[exp] (
oneVar: int[32] = 13,
twoVar: int[8] = 13,
threeVar: str = "shko",
)
Many to one
Many variables of the same type can be assigned to one output too:
var oneVar, twoVar: int[8] = 2;
However, each of them gets a copy of the variable on a new memory address:
.assert(&oneVar == &twoVar) // this will return false
One to many
And lastly, one variable can be assigned to multiple ones. This by using container types:
oneVar grouppy: seq[int] = { 5, 2, 4, 6 }
Or a more complicated one:
var anothermulti: set[str, seq[num[f32]]] = { "string", {5.5, 4.3, 7, .5, 3.2} }
Or a very simple one:
var simplemulti: any = { 5, 6, {"go", "go", "go"} }
Containers
Containers are of special type, they hold other types within. As described before, there are few of them
Access
To acces container variables, brackets like this []
are use:
var shortvar = anothermulti[1][3] // compiler will copy the value `anothermulti[1][3]` (which is a float) to a new memory location
Routines
A rutine definition describes the interface to and the actions of the routine abstraction. A routine call is the explicit request that a specific routine be executed. A routine is said to be active if, after having been called, it has begun execution but has not yet completed that execution. A routine declaration consists of an identifier, zero or more argument parameters, a return value type and a block of code.
// version 1
fun[] add(el1, el2: int[64]): int[64] = { result = el1 + el2 }
// version 2
fun[] add: int[64] = (el1, el2: int[64]){ result = el1 + el2 }
You’ve already seen one of the most important routines in the language: the main routine, which is the entry point of many programs. You’ve also seen the fun
or pro
keyword, which allows you to declare new routine.
Types
There are two main types of routines in fol:
-
Procedurues
A procedure is a piece of code that is called by name. It can be passed data to operate on (i.e. the parameters) and can optionally return data (the return value). All data that is passed to a procedure is explicitly passed.
-
Functions
A function is called pure function if it always returns the same result for same argument values and it has no side effects like modifying an argument (or global variable) or outputting to I/O. The only result of calling a pure function is the return value.
Parameters
Formal parameters
Routines typically describe computations. There are two ways that a routine can gain access to the data that it is to process: through direct access to nonlocal variables (declared elsewhere but visible in the routine) or through parameter passing. Data passed through parameters are accessed using names that are local to the routine. Routine create their own unnamed namespace. Every routine has its own Workspace. This means that every variable inside the routine is only usable during the execution of the routine (and then the variables go away).
Parameter passing is more flexible than direct access to nonlocal variables. Prrameters are special variables that are part of a routine’s signature. When a routine has parameters, you can provide it with concrete values for those parameters. The parameters in the routine header are called formal parameters. They are sometimes thought of as dummy variables because they are not variables in the usual sense: In most cases, they are bound to storage only when the routine is called, and that binding is often through some other program variables.
Parameters are declared as a list of identifiers separated by semicolon (or by a colon, but for code cleanness, the semicolon is preferred). A parameter is given a type by : typename. If after the parameter the :
is not declared, but ,
colon to identfy another paremeter, of which both parameters are of the same type if after the second one the :
and the type is placed. Then the same type parameters continue to grow with ,
until :
is reached.
fun[] calc(el1, el2, el3: int[64]; changed: bol = true): int[64] = { result = el1 + el2 - el3 }
In routine signatures, you must declare the type of each parameter. Requiring type annotations in routine definitions is obligatory, which means the compiler almost never needs you to use them elsewhere in the code to figure out what you mean. Routine can parameter overloaded too. It makes possible to create multiple routine of the same name with different implementations. Calls to an overloaded routine will run a specific implementation of that routine appropriate to the context of the call, allowing one routine call to perform different tasks depending on context:
fun retBigger(el2, el2: int): int = { return el1 | this > el2 | el2 }
fun retBigger(el2, el2: flt): flt = { return el1 | this > el2 | el2 }
pro main: int = {
retBigger(4, 5); // calling a routine with intigers
retBigger(4.5, .3); // calling another routine with same name but floats
}
The overloading resolution algorithm determines which routine is the best match for the arguments. Example:
pro toLower(c: char): char = { // toLower for characters
if (c in {'A' ... 'Z'}){
result = chr(ord(c) + (ord('a') - ord('A')))
} else {
result = c
}
}
pro toLower(s: str): str = { // toLower for strings
result = newString(.len(s))
for i in {0 ... len(s) - 1}:
result[i] = toLower(s[i]) // calls toLower for characters; no recursion!
}
Actual parameters
routine call statements must include the name of the routine and a list of parameters to be bound to the formal parameters of the routine. These parameters are called actual parameters. They must be distinguished from formal parameters, because the two usually have different restrictions on their forms.
Positional parameters
The correspondence between actual and formal parameters, or the binding of actual parameters to formal parameters - is done by position: The first actual parameter is bound to the first formal parameter and so forth. Such parameters are called positional parameters. This is an effective and safe method of relating actual parameters to their corresponding formal parameters, as long as the parameter lists are relatively short.
fun[] calc(el1, el2, el3: int): int = { result = el1 + el2 - el3 }
pro main: int = {
calc(3,4,5); // calling routine with positional arguments
}
Keyword parameters
When parameter lists are long, however, it is easy to make mistakes in the order of actual parameters in the list. One solution to this problem is with keyword parameters, in which the name of the formal parameter to which an actual parameter is to be bound is specified with the actual parameter in a call. The advantage of keyword parameters is that they can appear in any order in the actual parameter list.
fun[] calc(el1, el2, el3: int): int = { result = el1 + el2 - el3 }
pro main: int = {
calc(el3 = 5, el2 = 4, el1 = 3); // calling routine with keywords arguments
}
Mixed parameters
Keyword and positional arguments can be used at the same time too. The only restriction with this approach is that after a keyword parameter appears in the list, all remaining parameters must be keyworded. This restriction is necessary because a position may no longer be well defined after a keyword parameter has appeared.
fun[] calc(el1, el2, el3: int, el4, el5: flt): int = { result[0] = ((el1 + el2) * el4 ) - (el3 ** el5); }
pro main: int = {
calc(3, 4, el5 = 2, el4 = 5, el3 = 6); // element $el3 needs to be keyeorded at the end because
// its positional place is taken by keyword argument $el5
}
Default arguments
Formal parameters can have default values too. A default value is used if no actual parameter is passed to the formal parameter. The default parameter is assigned directly after the formal parameter declaration. The compiler converts the list of arguments to an array implicitly. The number of parameters needs to be known at compile time.
fun[] calc(el1, el2, el3: rise: bool = true): int = { result[0] = el1 + el2 * el3 | this | el1 + el2; }
pro main: int = {
calc(3,3,2); // this returns 6, last positional parameter is not passed but
// the default `true` is used from the routine declaration
calc(3,3,2,false) // this returns 12
}
Variadic routine
The use of ...
as the type of argument at the end of the argument list declares the routine as variadic. This must appear as the last argument of the routine. When variadic routine is used, the default arguments can not be used at the same time.
fun[] calc(rise: bool; ints: ... int): int = { result[0] = ints[0] + ints[1] + ints[2] * ints[3] | this | ints[0] + ints[1]; }
pro main: int = {
calc(true,3,3,3,2); // this returns 81, four parmeters are passed as variadic arguments
calc(true,3,3,2) // this returns 0, as the routine multiplies with the forth varadic parameter
// and we have given only three (thus the forth is initialized as zero)
}
...
is called unpack operator - just like in Golang. In the routine above, you see ...
, which means pack all incoming arguments into seq[int]
after the first argument. The sequence then is turned into a list at compile time.
{{% notice warn %}}
Nested procedures don't have access to the outer scope, while nested function have but can't change the state of it.
{{% /notice %}}
Return
The return type of the routine has to always be defined, just after the formal parameter definition. Following the general rule of FOL:
fun[] add(el1, el2: int[64]): int[64] = { result = el1 + el2 }
To make it shorter (so we don't have to type int[64]
two times), we can use a short form by omitting the return type. The compiler then will assign the returntype the same as the functions return value.
fun[] add(el1, el2: int[64]) = { result = el1 + el2 }
{{% notice info %}}
Each function in FOL has two defined variables that are automatically returned at the end of the function.
{{% /notice %}}
Those variables are:
- a variable called
result
, which is the one that is returned and is same type as return type - an error variable (called
error
), that can be reported from the funciton
{{% notice info %}}
Internally, those are a set of two variables, set[result: any, eror: err]. The result is of type any, and the any type shoud be known at compile time.
{{% /notice %}}
The implicitly declared variable result
is of the same type of the return type. For it top be implicitly declared, the return type of the function shoud be always declared, and not use the short form. The variable is initialized with zero value, and if not changed during the body implementation, the same value will return (so zero).
pro main(): int = {
fun[] add(el1, el2: int[64]): int[64] = { result = el1 + el2 } // using the implicitly declared $result variable
fun[] sub(el1, el2: int[64]) = { return el1 - el2 } // can't access the result variable, thus we use return
}
In addition, another implicitly decpared variable error
of ype err
is declared too. We talk for errors in details here, but here is a short example:
pro main(): int = {
fun[] add(el1, el2: int[64]): int[64] = { result = el1 + el2 } // using the implicitly declared $result variable
check(add(5,6)) // this will check if the error is nil
}
The final expression in the function will be used as return value. For this to be used, the return type of the function needs to be defined (so the function cnat be in the short form)). ver this can be used only in one statement body.
pro main(): int = {
fun[] add(el1, el2: int[64]): int[64] = { el1 + el2 } // This is tha last statement, this will serve as return
fun[] someting(el1,el2: int): int = {
if (condition) {
} else {
}
el1 + el2 // this will throw an error, cand be used in kulti statement body
}
fun[] add(el1, el2: int[64]) = { el1 + el2 } // this will throw an error, we can't use the short form of funciton in this way
Alternatively, the return
and report
statements can be used to return a value or error earlier from within the function, even from inside loops or other control flow mechanisms.
The example below is just to show the return
and report
statements, there is a better way to handle errors as shown in error section
use file: mod[std] = { std::fs::File }
pro main(): int = {
fun[] fileReader(path: str): str = {
var aFile = file.readfile(path)
if ( check(aFile) ) {
report "File could not be opened" + file // report will not break the program, but will return the error here, and the funciton will stop
} else {
return file | stringify(this) | return $ // this will be executed only if file was oopened without error
}
}
}
Procedures
Procedures are most common type of routines in Fol. When a procedure is "called" the program "leaves" the current section of code and begins to execute the first line inside the procedure. Thus the procedure "flow of control" is:
- The program comes to a line of code containing a "procedure call".
- The program enters the procedure (starts at the first line in the procedure code).
- All instructions inside of the procedure are executed from top to bottom.
- The program leaves the procedure and goes back to where it started from.
- Any data computed and RETURNED by the procedure is used in place of the procedure in the original line of code.
Procedures have side-effects, it can modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation. State data updated "outside" of the operation may be maintained "inside" a stateful object or a wider stateful system within which the operation is performed.
Passing values
The semantics for passing a value to a procedure are similar to those for assigning a value to a variable. Passing a variable to a procedure will move or copy, just as assignment does. If the procedure is stack-based, it will automatically copy the value. If it is heap-based, it will move the value.
pro[] modifyValue(someStr: str) = {
someStr = someStr + " world!"
}
pro[] main: int = {
//case1
var[mut] aString: str = "hello"; // a string varibale $aString is declared (in stack as default)
modifyValue(aString); // the value is passed to a procedure, since $aVar is in stack, the value is copied
.echo(aString) // this prints: "hello",
// value is not changed and still exists here, because was copied
//case2
@var[mut] aString: str = "hello"; // a string varibale $bString is declared (in stack with '@')
modifyValue(bString); // the value is passed to a procedure, since $aVar is in heap, the value is moved
.echo(bString) // this throws ERROR,
// value does not exists anymore since it moved and ownership wasn't return
}
As you can see from above, in both cases, the .echo(varable)
does not reach the desired procedure, to print hello world!
. In first case is not changed (because is coped), in second case is changed but never returned. To fix the second case, we can just use the .give_back()
procedure to return the ownership:
pro[] modifyValue(someStr: str) = {
someStr = someStr + " world!"
.give_back(someStr) // this returns the ownership (if there is an owner, if not just ignores it)
}
pro[] main: int = {
//case1
var[mut] aString: str = "hello"; // a string varibale $aString is declared (in stack as default)
modifyValue(aString); // the value is passed to a procedure, since $aVar is in stack, the value is copied
.echo(aString) // this still prints: "hello",
// value is not changed and still exists here, because was copied
//case2
@var[mut] aString: str = "hello"; // a string varibale $bString is declared (in stack with '@')
modifyValue(bString); // the value is passed to a procedure, since $aVar is in heap, the value is moved
.echo(aString) // this now prints: "hello world!",
// value now exists since the ownership is return
}
Lend parameters
But now, we were able to change just the variable that is defined in heap (case two), by moving back the ownership. In case one, since the value is copied, the owner of newly copied value is the procedure itself. So the .give_back()
is ignored. To fix this, we use borrowing to lend a value to the procedure
pro[] modifyValue(SOMESTR: str) = { // we use allcaps `SOMESTR` to mark it as borrowable
somestr = someStr + " world!" // when we refer, we can both refer with ALLCAPS or lowecaps
}
pro[] main: int = {
//case1
var[mut] aString: str = "hello"; // a string varibale $aString is declared (in stack as default)
modifyValue(aString); // the value is lended to the procedure
.echo(aString) // this now prints: "hello world!",
//case2
@var[mut] aString: str = "hello"; // a string varibale $bString is declared (in heap with '@')
modifyValue(aString); // the value is lended to the procedure
.echo(aString) // this now prints: "hello world!",
}
{{% notice warn %}}
So to make a procedure borrow a varibale it uses all caps name A_VAR
.
Remember that two variables are the same if have same characters (does not matter the caps)
{{% /notice %}}
pro[] borrowingProcedure(aVar: str; BVAR: bol; cVar, DVAR: int)
To call this procedure, the borrowed parameters always shoud be a variable name and not a direct value:
var aBool, anInt = true, 5
borrowingProcedure("get", true, 4, 5) // this will throw an error, cos it expects borrowable not direct value
borrowingProcedure("get", aBool, 4, anInt) // this is the proper way
When the value is passed as borrowable in procedure, by default it gives premission to change, so the same as var[mut, bor]
as disscussed here.
Return ownership
Return values can be though as return of ownership too. The ownership of a variable follows the same pattern every time: assigning a value to another variable moves or copies it.
pro main(): int = {
var s1 = givesOwnership(); // the variable $s1 is given the ownership of the procedure's $givesOwnership return
.echo(s1) // prints "hi"
var s2 = returnACopy(); // the variable $s2 is given the ownership of the procedure's $returnACopy return
.echo(s2) // prints: "there"
}
pro givesOwnership(): str = { // This procedure will move its return value into the procedure that calls it
@var someString = "hi"; // $someString comes into scope
return someString // $someString is returned and MOVES out to the calling procedure
}
pro returnACopy(): int = { // This procedure will move its return value into the procedure that calls it
var anotherString = "there" // $anotherString comes into scope
return anotherString // $anotherString is returned and COPIES out to the calling procedure
}
When a variable that includes data on the heap goes out of scope, the value will be cleaned up automatically by .de_alloc()
unless the data has been moved to be owned by another variable, in this case we give the ownership to return value. If the procedure with the retun value is not assigned to a variable, the memory will be freed again.
We can even do a transfer of ownership by using this logic:
pro main(): int = {
@var s2 = "hi"; // $s2 comes into scope (allocatd in the heap)
var s3 = transferOwnership(s2); // $s2 is moved into $transferOwnership procedure, which also gives its return ownership to $s3
.echo(s3) // prints: "hi"
.echo(s2) // this throws an error, $s2 is not the owner of anything anymore
}
pro transferOwnership(aString: str): str = { // $aString comes into scope
return aString // $aString is returned and moves out to the calling procedure
}
This does not work with borrowing though. When a variable is lended to a procedure, it has permissions to change, but not lend to someone else. The only thing it can do is make a .deep_copy()
of it:
pro main(): int = {
@var s2 = "hi"; // $s2 comes into scope (allocatd in the heap)
var s3 = transferOwnership(s2); // $s2 is moved into $transferOwnership procedure, which also gives its return ownership to $s3
.echo(s3) // prints: "hi"
.echo(s2) // prints: "hi" too
}
pro transferOwnership((aString: str)): str = { // $aString comes into scope which is borrowed
return aString // $aString is borrowed, thus cant be lended to someone else
// thus, the return is a deep_copy() of $aString
}
Functions
Functions compared to procedure are pure. A pure function is a function that has the following properties:
- Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams from I/O devices).
- Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).
Thus a pure function is a computational analogue of a mathematical function. Pure functions are declared with fun[]
fun[] add(el1, el2: int[64]): int[64] = { result = el1 + el2 }
{{% notice warn %}}
Functions in FOL are lazy-initialized.
{{% /notice %}}
So it is an evaluation strategy which delays the evaluation of the function until its value is needed. You call a function passing it some arguments that were expensive to calculate and then the function don’t need all of them due to some other arguments.
Consider a function that logs a message:
log.debug("Called foo() passing it " + .to_string(argument_a) + " and " + .to_string(argument_b));
The log library has various log levels like “debug”, “warning”, “error” etc. This allows you to control how much is actually logged; the above message will only be visible if the log level is set to the “debug” level. However, even when it is not shown the string will still be constructed and then discarded, which is wasteful.
{{% notice tip %}}
Since Fol supports first class functions, it allows functions to be assigned to variables, passed as arguments to other functions and returned from other functions.
{{% /notice %}}
Anonymous functoins
Anonymous function is a function definition that is not bound to an identifier. These are a form of nested function, in allowing access to variables in the scope of the containing function (non-local functions).
Staring by assigning a anonymous function to a vriable:
var f = fun (a, b: int): int = { // assigning a variable to function
return a + b
}
.echo(f(5,6)) // prints 11
var f: int = (a, b: int){ // this is an short alternative of same variable assignmet to function
return a + b
}
It is also possible to call a anonymous function without assigning it to a variable.
`version 1`
fun[] (a, b: int) = { `define anonymous function`
.echo(a + b)
}(5, 6) `calling anonymous function`
`version 2`
(a, b: int){ `define anonymous function`
.echo(a + b)
}(5, 6) `calling anonymous function`
Closures
Functions can appear at the top level in a module as well as inside other scopes, in which case they are called nested functions. A nested function can access local variables from its enclosing scope and if it does so it becomes a closure. Any captured variables are stored in a hidden additional argument to the closure (its environment) and they are accessed by reference by both the closure and its enclosing scope (i.e. any modifications made to them are visible in both places). The closure environment may be allocated on the heap or on the stack if the compiler determines that this would be safe.
There are two types of closures:
- anonymous
- named
Anonymus closures automatically capture variables, while named closures need to be specified what to capture. For capture we use the []
just before the type declaration.
fun[] add(n: int): int = {
fun added(x: int)[n]: int = { // we make a named closure
return x + n // variable $n can be accesed because we have captured ti
}
return adder()
}
var added = add(1) // assigning closure to variable
added(5) // this returns 6
fun[] add(n: int): int = {
return fun(x: int): int = { // we make a anonymous closure
return x + n // variable $n can be accesed from within the nested function
}
}
Currying
Currying is converting a single function of "n" arguments into "n" functions with a "single" argument each. Given the following function:
fun f(x,y,z) = { z(x(y));}
When curried, becomes:
fun f(x) = { fun(y) = { fun(z) = { z(x(y)); } } }
And calling it woud be like:
f(x)(y)(z)
However, the more iportant thing is taht, currying is a way of constructing functions that allows partial application of a function’s arguments. What this means is that you can pass all of the arguments a function is expecting and get the result, or pass a subset of those arguments and get a function back that’s waiting for the rest of the arguments.
fun calc(x): int = {
return fun(y): int = {
return fun (z): int = {
return x + y + z
}
}
}
var value: int = calc(5)(6) // this is okay, the function is still finished
var another int = value(8) // this completes the function
var allIn: int = calc(5)(6)(8) // or this as alternative
Higer-order functions
A higher-order function is a function that takes a function as an argument. This is commonly used to customize the behavior of a generically defined function, often a looping construct or recursion scheme.
They are functions which do at least one of the following:
- takes one or more functions as arguments
- returns a function as its result
//function as parameter
fun[] add1({fun adder(x: int): int}): int = {
return adder(x + n)
}
//function as return
fun[] add2(): {fun (x: int): int} = {
var f = fun (a, b: int): int = {
return a + b
}
return f
}
Generators
A generator is very similar to a function that returns an array, in that a generator has parameters, can be called, and generates a sequence of values. However, instead of building an array containing all the values and returning them all at once, a generator yields the values one at a time, which requires less memory and allows the caller to get started processing the first few values immediately. In short, a generator looks like a function but behaves like an iterator.
For a function to be a generator (thus to make the keyword yeild
accesable), it needs to return a type of container: arr, vec, seq, mat
but not set, any
.
fun someIter: vec[int] = {
var curInt = 0;
loop(){
yeild curInt.inc(1)
}
}
Methods
There is another type of routine, called method, but it can be either a pure function either a procedure. A method is a piece of code that is called by a name that is associated with an object where it is implicitly passed the object on which it was called and is able to operate on data that is contained within the object.
They either are defined inside the object, or outside the object then the object in which they operate is passed like so (just like in Golang):
pro (object)getDir(): str = { result = self.dir; };
Logicals
Logicals, which are logic routines, and represent logic programming, state the routine as a set of logical relations (e.g., a grandparent is the parent of a parent of someone). Such rutines are similar to the database languages. A program is executed by an “inference engine” that answers a query by searching these relations systematically to make inferences that will answer a query.
{{% notice info %}}
One of the main goals of the development of symbolic logic hasbeen to capture the notion of logical consequence with formal, mechanical, means. If the conditions for a certain class of problems can be formalized within a suitable logic as a set of premises, and if a problem to be solved can bestated as a sentence in the logic, then a solution might be found by constructing a formal proof of the problem statement from the premises
{{% /notice %}}
Declaration
In FOL, logic programming is considered as a first class citzen with axioms (axi
) as facts and logicals (log
) as rules, thus resembling Prolog language. For example:
Facts
Declaring a list of facts (axioms)
var likes: axi[str, str] = { {"bob","alice"} , {"alice","bob"}, {"dan","sally"} };
Rules
Declaring a rule that states if A likes B and B likes A, they are dating
log dating(a, b: str): bol = {
likes:[a,b] and
likes:[b,a]
}
Declaring a rule that states if A likes B and B likes A, they are just friends
log frends(a, b): bol = {
likes:[a,b] or
likes:[b,a]
}
{{% notice warn %}}
Rules can have only facts and varibles within
{{% /notice %}}
Return
A logical log
can return different values, but they are either of type bol
, or of type container (axioms axi
or vectors vec
):
Lets define a axiom of parents and childrens called parents
and another one of parents that can dance called dances
:
var parent: axi[str, str] = { {"albert","bob"},
{"albert","betty"},
{"albert","bill"},
{"alice","bob"},
{"alice","betty"},
{"alice","bill"},
{"bob","carl"},
{"bob","tom"} };
var dances axi[str] = { "albert", "alice", "carl" };
Boolean
Here we return a boolean bol
. This rule check if a parent can dance:
log can_parent_dance(a: str): bol = {
parent:[a,_] and dances:[a]
}
can_parent_dance("albert") // return true, "albert" is both a parent and can dance
can_parent_dance("bob") // return false, "bob" is a parent but can't dance
can_parent_dance("carl") // return false, "carl" is not a parent
Lets examine this:
parent:[a,_] and dances:[a]
this is a combintion of two facts. Here we say if a
is parent of anyone (we dont care whose, that's why we use meh symbol [a,_]
) and if true, then we check if parent a
(since he is a parent now, we fact-checked) can dance.
Vector
The same, we can create a vector of elements. For example, if we want to get the list of parents that dance:
log all_parents_that_dance(): vec[str] = {
parent:[*->X,_] and
dances:[X->Y]
Y
}
all_parents_that_dance() // this will return a string vector {"albert", "alice"}
Now lets analyze the body of the rule:
parent:[*->X,_] and
dances:[X->Y]
Y
Here are a combination of facts and variable assignment through silents. Silents are a single letter identifiers. If a silent constant is not declared, it gets declared and assigned in-place.
Taking a look each line:
parent:[X,_] and
this gets all parents ([*->X,_]
),and assign them to silent X
. So, X
is a list of all parents.
then:
dances[X->Y]:
this takes the list of parents X
and checks each if they can dance, and filter it by assigning it to Y
so [X->Y]
it will have only the parents that can dance.
then:
Y
this just returns the list Y
of parents that can dance.
Relationship
If A
is object
and objects
can be destroyed, then A
can be destroyed. As a result axioms can be related or conditioned to other axioms too, much like facts.
For example: if carl
is the son of bob
and bob
is the son of albert
then carl
must be the grandson of albert
:
log grandparent(a: str): vec[str] = {
parent[*->X,a]: and
parent[*->Y,X]:
Y
}
Or: if bob
is the son of albert
and betty
is the doughter of albert
, then bob
and betty
must be syblings:
log are_syblings(a, b: str): vec[str] = {
parent[*->X,a]: and
parent[X->Y,b]:
Y
}
Same with uncle relationship:
var brothers: axi[str] = { {"bob":"bill"}, {"bill","bob"} };
log has_uncle(a: str): vec[str] = {
parent[*->Y,a]: and
brothers[Y,*->Z]:;
Z
}
Conditional facts
Here an example, the axioms hates
will add a memeber romeo
only if the relation x
is satisfied:
var stabs: axi = {{"tybalt","mercutio","sword"}}
var hates: axi;
log romeHates(X: str): bol = {
stabs[X,"mercutio",_]:
}
hates+["romeo",X] if (romeHates(X));
Anonymous logicals
Conditional facts can be added with the help of anonymous logicals/rules:
eats+[x,"cheesburger"] if (eats[x,"bread"] and eats[X,"cheese"]);
eats+[x:"cheesburger"] if (log (a: str): bol = {
eats[a,"bread"]: and
eats[a,"cheese"]:
}(x));
Nested facts
var line: axi = { {{4,5},{4,8}}, {{8,5},{4,5}} }
log vertical(line: axi): bol = {
line[*->A,*->B]: and
A[*->X,Y*->]: and
B[X,*->Y2]:
}
log horizontal(line: axi): bol = {
line[*->A,*->B]: and
A[*->X,*->Y]: and
B[*->X2,Y]:
}
assert(vertical(line.at(0))
assert(horizontal(line.at(1))
Filtering
Another example of filtering a more complex axion:
var class: axi;
class.add({"cs340","spring",{"tue","thur"},{12,13},"john","coor_5"})
class.add({"cs340",winter,{"wed","fri"},{15,16},"bruce","coor_3"})
log instructor(class: str): vec[str] = {
class[class,_,[_,"fri"],_,*->X,_]
X
}
Constructs
A construct is a collection of fields, possibly of different data types, typically in fixed number and sequence. It is a custom data type that lets you name and package together multiple related values that make up a meaningful group. The fields of a construct may also be called members.
Constructs come in two forms:
- alias declarations (aliases) and
- type definitions (structs)
Aliases
An alias declaration binds an identifier to an existing type. All the properties of the existing type are bound to the alias too.
There are two type of aliasing:
- aliasing
- extending
Aliasing
typ[ali] I5: arr[int, 5];
So now the in the code, instead of writing arr[int, 5]
we could use I5
:
~var[pub] fiveIntigers: I5 = { 0, 1, 2, 3, 4, 5 }
Another example is creating a rgb
type that can have numbers only form 0 to 255:
typ[ali] rgb: int[8][.range(255)] ; // we create a type that holds only number from 0 to 255
typ[ali] rgbSet: set[rgb, rgb, rgb]; // then we create a type holding the `rgb` type
Alias declaration are created because they can simplify using them multiple times, their identifier (their name) may be expressive in other contexts, and–most importantly–so that you can define (attach) methods to it (you can't attach methods to built-in types, nor to anonymous types or types defined in other packages).
Attaching methods is of outmost importance, because even though instead of attaching methods you could just as easily create and use functions that accept the "original" type as parameter, only types with methods can implement standards std[]
that list/enforce those methods, and you can't attach methods to certain types unless you create a new type derived from them.
Extending
Extensions add new functionality to an existing constructs. This includes the ability to extend types for which you do not have access to the original source code (known as retroactive modeling).
typ[ext] type: type;
For example, adding a print
function to the default integer type int
:
typ[ext] int: int;
pro (int)print(): non = {
.echo(self)
}
pro main: int = {
5.print() // method print on int
}
Or turning a string str
into a vector of characters:
typ[ext] str: str;
fun (str)to_array(): vec[chr] = {
loop(x in self){
yeild x;
}
}
pro main(): int = {
var characters: vec[chr] = "a random str".to_array();
.echo(characters) // will print: {"a"," ","r","a","n","d","o","m"," ","s","t","r"}
}
Structs
Structs are the way to declare new type of data. A struct binds an identifier, the type name, to a type.
A struct definition creates a new, distinct type and are few of them in FOL:
- records
- entries
Definition
Records
A record is an aggregate of data elements in which the individual elements are identified by names and types and accessed through offsets from the beginning of the structure. There is frequently a need in programs to model a collection of data in which the individual elements are not of the same type or size. For example, information about a college student might include name, student number, grade point average, and so forth. A data type for such a collection might use a character string for the name, an integer for the student number, a floating- point for the grade point average, and so forth. Records are designed for this kind of need.
It may appear that records and heterogeneous set are the same, but that is not the case. The elements of a heterogeneous set[]
are all references to data objects that reside in scattered locations, often on the heap. The elements of a record are of potentially different sizes and reside in adjacent memory locations. Records are normally used as encapsulation structures, rather than data structures.
typ user: rec = {
var username: str;
var email: str;
var sign_in_count: int[64];
var active: bol;
};
Records as classes
Calsses are the way that FOL can apply OOP paradigm. They basically are a glorified record. Instead of methods to be used fom outside the body, they have the method declaration within the body. For example, creating an class computer
and its methods within the body:
~typ[pub] computer: rec = {
var[pub] brand: str;
var[pub] memory: int[16];
+fun getType(): str = { brand + .to_string(memory) };
};
var laptop: computer = { member1 = value, member2 = value };
.echo(laptop.getType());
Entries
Is an a group of constants (identified with ent
) consisting of a set of named values called elements.
typ color: ent = {
var BLUE: str = "#0037cd"
var RED str = "#ff0000"
var BLACK str = "#000000"
var WHITE str = "#ffffff"
};
if( something == color.BLUE ) { doathing } else { donothing }
Entries as enums
Unums represent enumerated data. An enumeration type (or enum type) is a value type defined by a set of named constants of the underlying integral numeric type.
typ aUnion: ent = {
var BLUE, RED, BLACK, WHITE: int[8] = {..3}
}
Initializaion
To use a record after we’ve defined it, we create an instance of that record by specifying concrete values for each of the fields. We create an instance by stating the name of the record and then add curly brackets containing key: value pairs, where the keys are the names of the fields and the values are the data we want to store in those fields. We don’t have to specify the fields in the same order in which we declared them in the record. In other words, the record definition is like a general template for the type, and instances fill in that template with particular data to create values of the type.
@var user1: user = {
email = "someone@example.com",
username = "someusername123",
active = true,
sign_in_count = 1,
};
Named initialization:
@var[mut] user1: user = { email = "someone@example.com", username = "someusername123", active = true, sign_in_count = 1 }
Ordered initialization
@var[mut] user1: user = { "someone@example.com", "someusername123", true, 1 }
Accessing
To get a specific value from a record, we can use dot notation or the access brackets. If we wanted just this user’s email address, we could use user1.email
or user1[email]
wherever we wanted to use this value. If the instance is mutable, we can change a value by assigning into a particular field. Note that the entire instance must be mutable; FOL doesn’t allow us to mark only certain fields as mutable.
@var[mut] user1: user = {
email = "someone@example.com",
username = "someusername123",
active = true,
sign_in_count = 1,
};
user1.email = "new.mail@example.com"
user1[username] = "anotherusername"
Returning
As with any expression, we can construct a new instance of the record as the last expression in the function body to implicitly return that new instance. As specified in function return, the final expression in the function will be used as return value. For this to be used, the return type of the function needs to be defined (here is defined as user
) and this can be used only in one statement body. Here we have declared only one variable user1
and that itslef spanc into multi rows:
pro buildUser(email, username: str): user = { user1: user = {
email = "someone@example.com",
username = "someusername123",
active = true,
sign_in_count = 1,
} }
Nesting
Records can be nested by creating a record type using other record types as the type for the fields of record. Nesting one record within another can be a useful way to model more complex structures:
var empl1: employee = {
FirstName = "Mark",
LastName = "Jones",
Email = "mark@gmail.com",
Age = 25,
MonthlySalary = {
Basic = 15000.00,
Bonus = {
HTA = 2100.00,
RA = 5000.00,
},
},
}
Defauling
Records can have default values in their fields too.
typ user: rec = {
var username: str;
var email: str;
var sign_in_count: int[64] = 1;
var active: bol = true;
};
This makes possible to enforce some fields (empty ones), and leave the defaults untouched:
@var[mut] user1: user = { email = "someone@example.com", username = "someusername123" }
Limiting
We can also restrict the values (with ranges) assigned to each field:
typ rgb: rec[] = {
var r: int[8][.range(255)];
var g: int[8][.range(255)];
var b: int[8][.range(255)];
}
var mint: rgb = { 153, 255, 187 }
This of course can be achieve just with variable types and aliased types and sets too, but we would need to create two types:
typ rgb: set[int[8][.range(255)], int[8][.range(255)], int[8][.range(255)]];
var mint: rgb = { 153, 255, 187 }
Methods
A record may have methods associated with it. It does not inherit any methods bound to the given type, but the method set of an standard type remains unchanged.To create a method for a record, it needs to be declared as the reciever of that method, in FOL's. Making a getter fucntion:
fun (recieverRecord)someFunction(): str = { self.somestring; };
After declaring the record receiver, we then we have access to the record with the keyword self
. A receiver is essentially just a type that can directly call the method.
typ user: rec = {
var username: str;
var email: str;
var sign_in_count: int[64];
var active: bol;
};
fun (user)getName(): str = { result = self.username; };
Methods have some benefits over regular routines. In the same package routines with the same name are not allowed but the same is not true for a method. One can have multiple methods with the same name given that the receivers they have are different.
Then each instantiation of the record can access the method. Receivers allow us to write method calls in an OOP manner. That means whenever an object of some type is created that type can call the method from itself.
var[mut] user1: user = { email = "someone@example.com", username = "someusername123", active = true, sign_in_count = 1 }
.echo(user1.getName());
Standards
Satndard
A standard is an established norm or requirement for a repeatable technical task. It is usually a formal declaration that establishes uniform technical criteria, methods, processes, and practices.
S, what is a to be considered a standard:
- A standard specification is an explicit set of requirements for an item, object or service. It is often used to formalize the technical aspects of a procurement agreement or contract.
- A standard test method describes a definitive procedure that produces a test result. It may involve making a careful personal observation or conducting a highly technical measurement.
- A standard procedure gives a set of instructions for performing operations or functions.
- A standard guide is general information or options that do not require a specific course of action.
- A standard definition is formally established terminology.
In FOL, standards are named collection of method signatures and are created by using std
keyword:
typ geometry = {
fun area(): flt[64];
fun perim(): flt[64];
};
There are three types of standards,
- protocol
pro[]
that enforce just function implementation - blueprint
blu[]
that enforces just data implementation - extended
ext[]
, that enforces function and data:
std geometry: pro = {
fun area(): flt[64];
fun perim(): flt[64];
};
std geometry: blu = {
var color: rgb;
var size: int;
};
std geometry: ext = {
fun area(): flt[64];
fun perim(): flt[64];
var color: rgb;
var size: int;
};
Contract
A contract is a legally binding agreement that recognises and governs the rights and duties of the parties to the agreement. A contract is enforceable because it meets the requirements and approval of an higher authority. An agreement typically involves a written declaration given in exchange for something of value that binds the maker to do. Its an specific act which gives to the person to whom the declaration is made the right to expect and enforce performance. In the event of breach of contract, the higher authority will refrain the contract from acting.
In fol contracts are used to bind a type to a standard. If a type declares to use a standard, it is the job of the contract (compiler internally) to see the standard full-filled.
std geo: pro = {
fun area(): flt[64];
fun perim(): flt[64];
};
std rect(geo): rec[] = { // this type makes a contract to use the geometry standard
width: int[64];
heigh: int[64];
}
Now we can make rect
records or classes, we have to respect the contract. If we don't implement the geo
methods, when we instantiate a new object of type rect
it will throw an error.
var aRectangle: rect = { width = 5, heigh = 6 } // this throws an error, we haven't fullfill the ocntract
To do so, we need first to create the default rect
methods from geo
standard, then instantiate a new object:
fun (rect)area(): flt[64] = { result = self.width + self.heigh }
fun (rect)perim(): flt[64] = { result = 2 * self.width + 2 * self.heigh }
var aRectangle: rect = { width = 5, heigh = 6 } // this from here on will work
The benifit of standard is that, we can create a routine that as parameter takes a standard, thus all objects with the standard can use afterwards that routine:
std geo: pro = {
fun area(): flt[64];
fun perim(): flt[64];
};
typ rect(geo): rec[] = { // this type makes a contract to use the geometry standard
width: int[64];
heigh: int[64];
}
fun (rect)area(): flt[64] = { result = self.width + self.heigh }
fun (rect)perim(): flt[64] = { result = 2 * self.width + 2 * self.heigh }
typ circle(geo): rec[] = { // another type makes a contract to use the geometry standard
radius: int[64];
}
fun (circle)area(): flt[64] = { result = math::const.pi * self.radius ** 2 }
fun (circle)perim(): flt[64] = { result = 2 * math::const.pi * self.radius}
typ square: rec[] = { // this type does not make contract with `geo`
heigh: int[64]
}
pro measure( shape: geo) { .echo(shape.area() + "m2") } // a siple method to print the standard's area
// instantiate two objects
var aRectangle: rect = { width = 5, heigh = 6 } // creating a new rectangle
var aCircle: circle = { radius = 5 } // creating a new rectangle
var aSquare: square = { heigh = 6 } // creating a new square
// to call the measure function that rpints the surface
measure(aRectangle) // this prints: 30m2
measure(aSquare) // this throws error, square cant use measure method
measure(aCircle) // this prints: 78m2
Generics
Types
Generic functions - lifting
The generic programming process focuses on finding commonality among similar implementations of the same algorithm, then providing suitable abstractions so that a single, generic algorithm can cover many concrete implementations. This process, called lifting, is repeated until the generic algorithm has reached a suitable level of abstraction, where it provides maximal reusability while still yielding efficient, concrete implementations. The abstractions themselves are expressed as requirements on the parameters to the generic algorithm.
pro max[T: gen](a, b: T): T = {
result = a | a < b | b;
};
fun biggerFloat(a, b: flt[32]): flt[32] = { max(a, b) }
fun biggerInteger(a, b: int[64]): int[64] = { max(a, b) }
Generic types - concepts
Once many algorithms within a given problem domain have been lifted, we start to see patterns among the requirements. It is common for the same set of requirements to be required by several different algorithms. When this occurs, each set of requirements is bundled into a concept. A concept contains a set of requirements that describe a family of abstractions, typically data types. Examples of concepts include Input Iterator, Graph, and Equality Comparable. When the generic programming process is carefully followed, the concepts that emerge tend to describe the abstractions within the problem domain in some logical way.
typ container[T: gen, N: int](): obj = {
var anarray: arr[T,N];
+fun getsize(): num = { result = N; }
};
var aContainer: container[int, 5] = { anarray = {zero, one, two, three, four}; };
Dispach
Static dispatch (or early binding) happens when compiler knows at compile time which function body will be executed when I call a method. In contrast, dynamic dispatch (or run-time dispatch or virtual method call or late binding) happens when compiler defers that decision to run time. This runtime dispatch requires either an indirect call through a function pointer, or a name-based method lookup.
std foo: pro = { fun bar(); }
typ[ext] int, str: int, str;
fun (int)bar() = { }
fun (str)bar() = { }
pro callBar(T: foo)(value: T) = { value.bar() } // dispatch with generics
pro barCall( value: foo ) = { value.bar() } // dispatch with standards
pro main: int = {
callBar(2);
callBar("go");
barCall(2);
barCall("go")
}
Modules
FOL programs are constructed by linking together packages. A package in turn is constructed from one or more source files that together declare constants, types, variables and functions belonging to the package and which are accessible in all files of the same package. Those elements may be exported and used in another package.
Every file with extension .fol
in a folder is part of a package. Thus every file in the folder that uses the same package name, share the same scope between each other.
Two packages can't exist in same folder, so it is suggested using hierarchy folders to separate packages.
Types
Packages can be either:
- defined
- imported
Imports
An import declaration states that the source file containing the declaration depends on functionality of the imported package and enables access to exported identifiers of that package.
Syntax to import a library is:
use package_name: mod = { path }
There are two type of import declartions:
- system libraries
- local libraries
System libraries
This is how including other libraries works, for example include fmt
module from standard library:
use fmt: std = {"fmt"};
pro main: ini = {
fmt::log.warn("Last warning!...")
}
To use only the log
namespace of fmt
module:
use log: std = {"fmt/log"};
pro[] main: int = {
log.warn("Last warning!...")
}
But let's say you only wanna use ONLY the warn
functionality of log
namespace from fmt
module:
use warn std = {"fmt/log.warn"};
pro[] main: int = {
warn("Last warning!...")
}
Local libraries
To include a local package (example, package name bender
), then we include the folder where it is, followed by the package name (folder is where files are located, package is the name defned with mod[])
use bend: loc = {"../folder/bender"};
Then to acces only a namespace:
use space: loc = {"../folder/bender/space"};
URL libraries
Libraries can be directly URL imported:
use space: url = {"https://github.com/follang/std"};
Declarations
Each file in a folder (with extension .fol
) is part of a package. There is no need for imports or other things at the top of the file. They share the same scope, and each declaration is order independent for all files.
Namespaces
A namespace can be defined in a subfolder of the main foler. And they can be nested.
To acces the namespace there are two ways:
- direct import with
use
- or code access with
::
Direct import
use aNS: loc = { "home/folder/printing/logg" }
pro[] main: int = {
logg.warn("something")
}
Code access
use aNS: loc = { "home/folder/printing" }
pro[] main: int = {
printing::logg.warn("something")
}
Blocks
Block statement is used for scopes where members get destroyed when scope is finished. And there are two ways to define a block:
- unnamed blocks and
- named blocks
Unnamed blocks
Are simply scopes, that may or may not return value, and are represented as: { //block }
, with .
before the brackets for return types and _
for non return types:
pro[] main: int = {
_{
.echo("simple type block")
}
.echo(.{ return "return type block" })
}
Named blocks
Blocks can be used as labels too, when we want to unconditionally jump to a specific part of the code.
pro[] main: int = {
def block: blk[] = { // $block A named block that can be referenced
// implementation
}
def mark: blk[] // $mark A named block that can be referenced, usually for "jump" statements
}
Tests
Blocks defined with type tst
, have access to the module (or namespace) defined in tst["name", access]
.
def test1: tst["sometest", shko] = {}
def "some unit testing": tst[shko] = {}
Error handling
Unlike other programming languages, FOL does not have exceptions (Rust neither). It has only two types of errors:
- braking errors
- recoverable errors
Breaking errors cause a program to fail abruptly. A program cannot revert to its normal state if an unrecoverable error occurs. It cannot retry the failed operation or undo the error. An example of an unrecoverable error is trying to access a location beyond the end of an array.
Recoverable error are errors that can be corrected. A program can retry the failed operation or specify an alternate course of action when it encounters a recoverable error. Recoverable errors do not cause a program to fail abruptly. An example of a recoverable error is when a file is not found.
There are two keywords reserved and associated to two types of errors: report
for a recoverable error and panic
for the braking error.
{{% notice tip %}}
By default, all errors are either conctinated up with report, or exited with panic.
{{% /notice %}}
A simplier way to hande errors is through pipes
Breaking errors
panic
keyword allows a program to terminate immediately and provide feedback to the caller of the program. It should be used when a program reaches an unrecoverable state. This most commonly occurs when a bug of some kind has been detected and it’s not clear to the programmer how to handle the error.
pro main(): int = {
panic "Hello";
.echo("End of main"); //unreachable statement
}
In the above example, the program will terminate immediately when it encounters the panic
keyword.
Output:
main.fol:3
routine 'main' panicked at 'Hello'
-------
Trying to acces an out of bound element of array:
pro main(): int = {
var a: arr[int, 3] = [10,20,30];
a[10]; //invokes a panic since index 10 cannot be reached
}
Output:
main.fol:4
routine 'main' panicked at 'index out of bounds: the len is 3 but the index is 10'
-------
a[10];
^-------- index out of bounds: the len is 3 but the index is 10
A program can invoke panic
if business rules are violated, for example: if the value assigned to the variable is odd it throws an error:
pro main(): int = {
var no = 13;
//try with odd and even
if (no % 2 == 0) {
.echo("Thank you , number is even");
} else {
panic "NOT_AN_EVEN";
}
.echo("End of main");
}
Output:
main.fol:9
routine 'main' panicked at 'NOT_AN_EVEN'
-------
Recoverable errors
report
can be used to handle recoverable errors. As discussed here, FOL uses two variables result
nd error
in return of each routine. As name implies, result
represents the type of the value that will be returned in a success case, and error
represents the type of the error err[]
that will be returned in a failure case.
When we use the keyword report
, the error is returned to the routine's error variable and the routine qutis executing (the routine, not the program).
use file: mod[std] = { std::fs::File }
pro main(): int = {
pro[] fileReader(path: str): str = {
var aFile = file.readfile(path)
if ( check(aFile) ) {
report "File could not be opened" + file // report will not break the program, but will return the error here, and the routine will stop
} else {
return file.to_string() // this will be executed only if file was oopened without error
}
}
}
Form this point on, the error is concatinated up to the main function. This is known as propagating the error and gives more control to the calling code, where there might be more information or logic that dictates how the error should be handled than what you have available in the context of your code.
use file: mod[std] = { std::fs::File }
pro main(): int = {
var f = file.open("main.jpg"); // main.jpg doesn't exist
if (check(f)) {
report "File could not be opened" + file // report will not break the program
} else {
.echo("File was open sucessfulley") // this will be executed only if file was oopened without error
}
}
Language Sugar
Language sugar, is a visually or logically-appealing "shortcut" provided by the language, which reduces the amount of code that must be written in some common situation. It makes the language "sweeter" for human use: things can be expressed more clearly, more concisely, or in an alternative style that some may prefer.
A construct in a language is called "language sugar" if it can be removed from the language without any effect on what the language can do: functionality and expressive power will remain the same.
{{% notice info %}}
Language sugars don't add functionality to a language, they are plain textual replacements for expressions that could also be written in a more analytic way.
{{% /notice %}}
There are lots of pros and cons to language sugar. The goal generally being to balance the amount of it available in a language so as to maximise readability -- giving enough freedom to allow the author to emphasize what is important, while being restrictive enough that readers will know what to expect.
Too much language sugar can make the underlying semantics unclear, but too little can obscure what is being expressed. The author of a piece of code chooses from the available syntax in order to emphasize the important aspects of what it does, and push the minor ones aside. Too much freedom in doing this can make code unreadable because there are many special cases in the syntax for expressing peculiar things which the reader must be familiar with. Too little freedom can also result in unreadability because the author has no way to emphasize any particular way of thinking about the code, even though there may be many ways to look at it when someone new to the code starts reading. It can also make it harder to write code, because there are fewer ways to express any given idea, so it can be more difficult to find one which suits you. One can argue that this is a task for comments to perform, but when it comes time to read the code, it is really the code itself that is readable or not.
Silents
Single letter identifiers (SILENTs) identifiers are a form of languages sugar assignment.
Letter
Lowercase
Many times is needed to use a variable in-place and to decluter the code we use silents:
each(var x: str; x in {..10}){
// implementation
}
each(x in {..10}){ // we use the sicale `x` here
// implementation
}
Uppercase
If a silent is uppercase, then it is a constant, can't be changed. This is very important when using FOL for logic programming:
log vertical(l: line): bol = {
l[A:B] and // we assign sicales `A` and `B`
A[X:Y] and // we assign sicales `X` and `Y`
B[X:Y2] // here we assign only `Y2` becase `X` exists from before
}
Symbols
Meh
Meh is the _
identifier. The use of the term "meh" shows that the user is apathetic, uninterested, or indifferent to the question or subject at hand. It is occasionally used as an adjective, meaning something is mediocre or unremarkable.
We use meh when we want to discard the variable, or we dont intend to use:
var array: arr[int, 3] = {1, 2, 3};
var a, _, b: int = array; // we discard, the middle value
Y'all
Y'all is the *
identifier. It represents app possible values that can be.
when(true) {
case (x == 6){ // implementation }
case (y.set()){ // implementation }
* { // default implementation }
}
Pipes
Piping is a process that connects the output of the expression to the left to the input of the expression of the right. You can think of it as a dedicated program that takes care of copying everything that one expressionm prints, and feeding it to the next expression. The idea is the same as bash pipes
. For example, an routine output is piped to a conditional through pipe symbol |
then the conditional takes the input and returns true or false. If returned false, then the second part of pipe is returned. To access the piped variable, this
keyword is used:
pro[] main: int = {
fun addFunc(x, y: int): int = {
return x + y;
}
var aVar: int = addFunc(4, 5) | if(this > 8) | return 6;
}
However, when we assign an output of a function to a variable, we shoud expect that errors within funciton can happen. By default, everytime a function is called, and the function throws an error in will be reported up.
var aVar: int = addFunc(4, 5); // if there are errors, and the call is in main function, the program will exit
// because is the last concatinator of the 'report' error
However, when we use pipes, we pass the function values (result and the error) to the next expression, and then, it is the second expression's responsibility to deal with it. We use the built-in check
that checks for error on the function:
var aVar: int = addFunc(4, 5) | check(this) | return 5; // if there are errors, the error is passed to the next sepression with pipe
// here, if there is errors, will be checked and the default value of 5 will return
There is a shorter way to do this kind of error checking. For that we use double pipe ||
. For example, we assign the output of a function to a variable, but the function may fail, so we want a default variable:
var aVar: int = addFunc(4, 5) || return 5;
Or to handle the error ourselves. This simply says, if i get the error, then we can panic
or report
with custom message:
var aVar: int = addFunc(4, 5) || panic "something bad inside function has happened";
More on error handling can be found here
Mixture
Optional
var someMixtureInt: ?int = 45;
Never
var someNverType: !int = panic();
Limits
Limiting is a syntactic way to set boundaries for variables. The way FOL does is by using []
right after the type declaration type[]
, so: type[options][limits]
Initger limiting
Example, making a intiger variable have only numbers from 0 to 255 that represents an RGB value for a single color:
var rgb: int[][.range(255)];
Character limiting
It works with strings too, say we want a string that can should be of a particular form, for example an email:
var email: str[][.regex('[._a-z0-9]+@[a-z.]+')]
Matching
Variable
As variable assignment:
var checker: str = if(variable) {
in {..10} -> "in range of 1-10";
in {11..20} -> "in range of 11-20";
* -> "out of range";
}
var is_it: int = if(variable) {
is "one" -> 1;
is "two" -> 2;
* -> 0;
}
var has_it: bol = if(variable) {
has "o", "k" -> true;
* -> false;
}
Function
As function return:
fun someValue(variable: int): str = when(variable) {
in {..10} -> "1-10";
in {11..20} -> "11-20";
* -> "0";
}
Rolling
Rolling or list comprehension is a syntactic construct available FOL for creating a list based on existing lists. It follows the form of the mathematical set-builder notation - set comprehension.
Rolling has the same syntactic components to represent generation of a list in order from an input list or iterator:
- A variable representing members of an input list.
- An input list (or iterator).
- An optional predicate expression.
- And an output expression producing members of the output list from members of the input iterable that satisfy the predicate.
The order of generation of members of the output list is based on the order of items in the input. Syntactically, rolling consist of an iterable containing an expression followed by a for statement. In FOL the syntax follows exacly the Python's list comprehension syntax:
var aList: vec[] = { x for x in iterable if condition }
Rolling provides an alternative syntax to creating lists and other sequential data types. While other methods of iteration, such as for loops, can also be used to create lists, rolling may be preferred because they can limit the number of lines used in your program.
var aList: vec[] = {..12};
var another: vec[] = { ( x * x ) for ( x in aList ) if ( x % 3 == 0 ) }
var matrix: mat[int, int] = { x * y for ( x in {..5}, y in {..5} ) }
Unpacking
Unpacking—also known as iterable destructuring—is another form of pattern matching used to extract data from collections of data. Take a look at the following example:
var start, *_ = { 1, 4, 3, 8 }
.echo(start) // Prints 1
.echo(_) // Prints [4, 3, 8]
In this example, we’re able to extract the first element of the list and ignore the rest. Likewise, we can just as easily extract the last element of the list:
var *_, end = { "red", "blue", "green" }
.echo(end) // Prints "green"
In fact, with pattern matching, we can extract whatever we want from a data set assuming we know it’s structure:
var start, *_, (last_word_first_letter, *_) = { "Hi", "How", "are", "you?" }
.echo(last_word_first_letter) // Prints "y"
.echo(start) // Prints "Hi"
Now, that has to be one of the coolest programming language features. Instead of extracting data by hand using indices, we can just write a pattern to match which values we want to unpack or destructure.
Inquiry
Inquiries are inline unit tests and are a part of the basic syntax sugar. In other words, we don’t have to import any libraries or build up any suites to run tests.
Instead, FOL includes a couple of clauses for testing within the source code:
fun sum(l: ...?int): int = {
when(l.length()) {
is 0 => 0;
is 1 => l[0];
$ => l[0] + sum(l[1:]);
}
where(self) {
sum() is 0;
sum(8) is 8;
sum(1, 2, 3) is 6;
}
}
Here, we can see an awesome list sum function. Within the function, there are two basic cases: empty and not empty. In the empty case, the function returns 0. Otherwise, the function performs the sum.
At that point, most languages would be done, and testing would be an afterthought. Well, that’s not true in FOL. To add tests, we just include a where clause. In this case, we test an empty list and a list with an expected sum of 6.
When the code is executed, the tests run. However, the tests are non-blocking, so code will continue to run barring any catastrophic issues.
Chaining
Optional chaining is a process for querying and calling properties, methods, and subscripts on an optional that might currently be nil. If the optional contains a value, the property, method, or subscript call succeeds; if the optional is nil, the property, method, or subscript call returns nil. Multiple queries can be chained together, and the entire chain fails gracefully if any link in the chain is nil.
Before I can really explain optional chaining, we have to get a grasp of what an optional value is. In FOL, variables cannot be empty. In other words, variables cannot store a value of NIL, at least not directly. This is a great feature because we can assume that all variables contain some value. Of course, sometimes variables need to be NIL. Fortunately, FOL provides that through a boxing feature called optionals. Optionals allow a user to wrap a value in a container which can be unwrapped to reveal either a value or NIL:
var printString: ?str;
printString = "Hello, World!"
.echo(printString!)
In this example, we declare an optional string and give it a value of “Hello, World!” Since we know that the variable stores a str
, we can unconditionally unwrap the value and echo it. Of course, unconditional unwrapping is typically bad practice, so I’m only showing it for the purposes of showing off optionals.
At any rate, optional chaining takes this concept of optionals and applies it to method calls and fields. For instance, imagine we have some long chain of method calls:
important_char = commandline_input.split('-').get(5).charAt(7)
In this example, we take some command line input and split it by hyphen. Then, we grab the fifth token and pull out the seventh character. If at any point, one of these method calls fails, our program will crash.
With optional chaining, we can actually catch the NIL return values at any point in the chain and fail gracefully. Instead of a crash, we get an important_char value of NIL. Now, that’s quite a bit more desirable than dealing with the pyramid of doom.
Value Conversion
A type cast converts a value of one type to another.
FOL uses two ways for value conversion:
- coercion for conversions that are known to be completely safe and unambiguous, and
- casting for conversions that one would not want to happen on accident.
Coercion
Casting
Memory model
The Stack
What is the stack? It's a special region of your computer's memory that stores temporary variables created by each function (including the main() function). The stack is a "LIFO" (last in, first out) data structure, that is managed and optimized by the CPU quite closely. Every time a function declares a new variable, it is "pushed" onto the stack. Then every time a function exits, all of the variables pushed onto the stack by that function, are freed (that is to say, they are deleted). Once a stack variable is freed, that region of memory becomes available for other stack variables.
The advantage of using the stack to store variables, is that memory is managed for you. You don't have to allocate memory by hand, or free it once you don't need it any more. What's more, because the CPU organizes stack memory so efficiently, reading from and writing to stack variables is very fast.
A key to understanding the stack is the notion that when a function exits, all of its variables are popped off of the stack (and hence lost forever). Thus stack variables are local in nature. This is related to a concept we saw earlier known as variable scope, or local vs global variables. A common bug is attempting to access a variable that was created on the stack inside some function, from a place in your program outside of that function (i.e. after that function has exited).
Another feature of the stack to keep in mind, is that there is a limit (varies with OS) on the size of variables that can be stored on the stack. This is not the case for variables allocated on the heap.
- very fast access
- don't have to explicitly deallocate variables
- space is managed efficiently by CPU (memory will not become fragmented)
- local variables only
- limit on stack size (OS-dependent)
- variables cannot be resized
The Heap
The heap is a region of your computer's memory that is not managed automatically for you, and is not as tightly managed by the CPU. It is a more free-floating region of memory (and is larger). To allocate memory on the heap, you must use var[new]
. Once you have allocated memory on the heap, you are responsible to deallocate that memory once you don't need it any more. If you fail to do this, your program will have what is known as a memory leak. That is, memory on the heap will still be set aside (and won't be available to other processes).
Unlike the stack, the heap does not have size restrictions on variable size (apart from the obvious physical limitations of your computer). Heap memory is slightly slower to be read from and written to, because one has to use pointers to access memory on the heap. Unlike the stack, variables created on the heap are accessible by any function, anywhere in your program. Heap variables are essentially global in scope.
Element of the heap have no dependencies with each other and can always be accessed randomly at any time. You can allocate a block at any time and free it at any time. This makes it much more complex to keep track of which parts of the heap are allocated or free at any given time.
- variables can be accessed globally
- no limit on memory size
- slower access
- no guaranteed efficient use of space, memory may become fragmented over time as blocks of memory are allocated, then freed
- you must manage memory (you're in charge of allocating and freeing variables)
- variables can be resized anytime
Multithread
In a multi-threaded situation each thread will have its own completely independent stack but they will share the heap. Stack is thread specific and Heap is application specific. The stack is important to consider in exception handling and thread executions.
Memory and Allocation
In the case of a normal variable, we know the contents at compile time, so the value is hardcoded directly into the final executable. This is why they are fast and efficient. But these properties only come from the variable immutability. Unfortunately, we can’t put a blob of memory into the binary for each piece of variable whose size is unknown at compile time and whose size might change while running the program.
Lets take an example, the user input as str
(string), in order to support a mutable, growable piece of variable, we need to allocate an amount of memory on the heap, unknown at compile time, to hold the contents. This means:
- The memory must be requested from the operating system at runtime.
- We need a way of returning this memory to the operating system when we’re done with our String.
That first part is done by us: when we call var[new]
, its implementation requests the memory it needs. This is pretty much universal in any programming languages.
However, the second part is different. In languages with a garbage collector (GC), the GC keeps track and cleans up memory that isn’t being used anymore, and we don’t need to think about it. Without a GC, it’s our responsibility to identify when memory is no longer being used and call code to explicitly return it, just as we did to request it. Doing this correctly has historically been a difficult programming problem. If we forget, we’ll waste memory. If we do it too early, we’ll have an invalid variable. If we do it twice, that’s a bug too. We need to pair exactly one allocate with exactly one free.
Fol, copies Rust in this aspect: the memory is automatically returned once the variable that owns it goes out of scope.
{{% notice warn %}}
When a variable goes out of scope, Fol calls a special function for us to deallocate all the new memories we have allocated during this dunction call.
{{% /notice %}}
This function is called .de_alloc()
- just like Rust's drop()
, and it’s where the author of var[new]
can put the code to return the memory. Fol calls .de_alloc()
automatically at the closing curly bracket.
Ownership
Much like C++ and Rust, in Fol every variable declared, by default is created in stack unless explicitly specified othervise. Using option [new]
or [@]
in a variable, it allocates memory in the heap. The size of the allocation is defined by the type. Internally this creates a pointer to heap address, and dereferences it to the type you are having. Usually those behind the scene pointers here are unique pointers. This means that when the scope ends, the memory that the pointer used is freed.
var[new] intOnHeap: int[64];
@var intOnHeap: int[64];
Assignments
As discussed before, declaring a new variable is like this:
var[pub] aVar: int[32] = 64
{{% notice warn %}}
However, when new variable is created and uses an old variable as value, the value is always cloned for "stack" declared values, but moved for "heap" declared values.
{{% /notice %}}
@var aVar: int[32] = 64
{
var bVar = aVar // this moves the content from $aVar to $bVar
}
.echo(aVar) // this will throw n error, since the $aVar is not anymore owner of any value
When the variable is moved, the owner is changed. In example above, the value 64
(saved in stack) is owned my aVar
and then the ownership is moved to bVar
. Now bVar
is the new owner of the variable, making the aVar
useless and can't be refered anymore. Since the bVar
now controls the value, it's lifetime lasts until the end of the scope. When the scope ends, the variable is destroyed with .de_alloc()
function. This because when the ovnership is moved, the attributes are moved too, so the @
of aVar
is now part of the bVar
even if not implicitly specified. To avoid destruction, the bVar
needs to return the ownership back to aVar
before the scope ends with .give_back(bVar)
or !bVar
.
@var aVar: int[32] = 64
{
var bVar = aVar // this moves the content from $aVar to $bVar
!bvar // return ownership
}
.echo(aVar) // this now will print 64
This can be done automatically by using borrowing.
Borrowing
Borrowing does as the name says, it borrows a value from another variable, and at the end of the scope it automatically returns to the owner.
pro[] main: int = {
var[~] aVar: int = 55;
{
var[bor] newVar: int = aVar // represents borrowing
.echo(newVar) // this return 55
}
.echo(aVar) // here $aVar it not accesible, as the ownership returns at the end of the scope
.echo(newVar) // we cant access the variable because the scope has ended
}
Borrowing uses a predefined option [bor]
, which is not conventional like other languages that use &
or *
. This because you can get away just with "borrowing" without using pointers (so, symbols like *
and &
are strictly related to pointers)
However, while the value is being borrowed, we can't use the old variable while is being borrowed but we still can lend to another variable:
pro[] main: int = {
var[~] aVar: int = 55;
{
var[bor] newVar = aVar // represents borrowing
.echo(newVar) // this prints 55
.echo(aVar) // this throws an error, cos we already have borrowd the value from $aVar
var[bor] anotherVar = aVar // $anotherVar again borrows from a $aVar
}
}
{{% notice warn %}}
When borrowed, a the value is read-only (it's immutable). To make it muttable, firtsly, the owner needs to be muttable, secondly the borrower needs to declarare that it intends to change.
{{% /notice %}}
To do so, the borrower uses var[mut, bor]
. However, when the value is declared mutable by owner, only one borrower within one scope can declare to modify it:
pro[] main: int = {
var[~] aVar: int = 55;
{
var[mut, bor] newVar = aVar // [mut, bor] represents a mutable borrowing
var[mut, bor] anotherVar = aVar // this throws an error, cos we already have borrowed the muttable value before
}
{
var[mut, bor] anotherVar = aVar // this is okay, s it is in another scope
}
}
Pointers
The only way to access the same memory with different variable is by using pointers. In example below, we create a pointer, and when we want to dereference it to modify the content of the address that the pointer is pointing to, we use *ptrname
or .pointer_value(ptrname)
.
@var aContainer: arr[int, 5]; //allocating memory on the heap
var contPoint: ptr[] = aContainer;
*contPoint = { zero, one, two, three, four }; //dereferencing and then assigning values
Bare in mind, that the pointer (so, the address itself) can't be changes, unless when created is marked as var[mut]
. To see tha address of a pointer we use &ptrname
or .address_of(ptrname)
@var aContainer: arr[int, 5]; //allocating memory on the heap
var contPoint: ptr[] = aContainer;
var anotherPoint: ptr[] = &contPoint; //assigning the same adress to another pointer
Unique pointer
Ponter of a pointer is very simimilar to RUST move pointer, it actually, deletes the first pointer and references the new one to the location of deleted one. However this works only when the pointer is unique (all pointers by default all unique). This is like borrowing, but does not invalidate the source variable:
var aContainer: arr[int, 5] = { zero, one, two, three, four };
var contPoint: ptr[] = aContainer;
var anotherPoint: ptr[] = &contPoint;
with borrowing, we use #varname
or .borrow_from(varname)
var aContainer: arr[int, 5] = { zero, one, two, three, four };
{
var borrowVar = #aContainer; //this makes a new var form the old var, but makes the old invalid (until out of scope)
}
Shred pointer
Ponter can be shared too. They can get referenced by another pointer, and they don't get destroyed until the last reference's scope is finished. This is exacly like smart shared_ptr in C++. Pointer to this pointer makes a reference not a copy as unique pointers. Dereferencing is a bit complicated here, as when you dereference a pointer pointer you get a pointer, so you need to dereference it too to get the value.
@var aContainer: arr[int, 5] = { zero, one, two, three, four };
var contPoint: ptr[] = aContainer;
var pointerPoint: ptr[shared] = &contPoint;
Dereferencing (step-by-step):
var apointerValue = *pointerPoint
var lastpointerValue = *apointer
Dereferencing (all-in-one):
var lastpointer = *(*pointerPoint)
Raw pointer
Lastly, pointers can be raw too. This is the base of ALL POINTERS AND VARIABLES. Pointers of this type need to MANUALLY GET DELETED. If a pointer gets deleted before the new pointer that points at it, we get can get memory corruptions:
var aContainer: arr[int, 5] = { zero, one, two, three, four };
var contPoint: ptr[raw] = aContainer;
var pointerPoint: ptr[raw] = &contPoint;
Deleting:
!(pointerPoint)
!(contPoint)
Concurrency
Concurrency is the ability of different tasks of a program to be executed out-of-order or in partial order, without affecting the final outcome. This allows for parallel execution of the concurrent tasks, which can significantly improve overall speed of the execution in multi-processor and multi-core systems. In more technical terms, concurrency refers to the decomposability property of a program into order-independent or partially-ordered tasks.
There are two distinct categories of concurrent task control.
- The most natural category of concurrency is that in which, assuming that more than one processor is available, several program tasks from the same program literally execute simultaneously. This is physical concurrency - parallel programming.
- Or programm can assume that there are multiple processors providing actual concurrency, when in fact the actual execution of programs is taking place in interleaved fashion on a single processor. This is logical concurrency concurrent programming.
From the programmer’s points of view, concurrency is the same as parallelism. It is the language’s task, using the capabilities of the underlying operating system, to map the logical concurrency to the host hardware.
There are at least four different reasons to use concurrency:
- The first reason is the speed of execution of programs on machines with multiple processors.
- The second reason is that even when a machine has just one processor, a program written to use concurrent execution can be faster.
- The third reason is that concurrency provides a different method of conceptualizing program solutions to problems.
- The fourth reason for using concurrency is to program applications that are distributed over several machines, either locally or network.
To achieve concurrent programming, there are two main paradigms used:
- Eventuals
- Cooutines
Eventuals
Eventuals describe an object that acts as a proxy for a result that is initially unknown, usually because the computation of its value is not yet complete.
Async/Await
Async methods are intended to be non-blocking operations. An await expression in an async routine doesn’t block the current thread while the awaited task is running. Instead, the expression signs up the rest of the routine as a continuation and returns control to the caller of the async routine and it means “Once this is done, execute this function”. It’s basically a “when done” hook for your code, and what is happening here is an async routine, when executed, returns a coroutine which can then be awaited. This is done usually in one thread, but can be done in multiple threads too, but thread invocations are invisible to the programmer in this case.
pro main(): int = {
doItFast() | async // compiler knows that this routine has an await routine, thus continue when await rises
.echo("dosomething to echo")
// the main program does not exit until the await is resolved
}
fun doItFast(): str = {
result = client.get(address).send() | await // this tells the routine that it might take time
.echo(result)
}
Coroutines
A coroutine is a task given form the main thread, similar to a routine, that can be in concurrent execution with other tasks of the same program though other routines. A worker takes the task and runs it, concurrently. Each task in a program can be assigned to one or multiple workers.
Three characteristics of coroutine distinguish them from normal routines:
- First, a task may be implicitly started, whereas a routine must be explicitly called.
- Second, when a program unit invokes a task, in some cases it need not wait for the task to complete its execution before continuing its own.
- Third, when the execution of a task is completed, control may or may not return to the unit that started that execution.
- Fourth and most importantly, the execution of the routine is entirely independent from main thread.
In fol to assign a task to a worker, we use the symbols [>]
Channels
FOL provides asynchronous channels for communication between threads. Channels allow a unidirectional flow of information between two end-points: the Transmitter and the Receiver. It creates a new asynchronous channel, returning the tx/tx halves. All data sent on the Tx (transmitter) will become available on the Rx (receiver) in the same order as it was sent. The data is sent in a sequence of a specifies type seq[type]
. tx
will not block the calling thread while rx
will block until a message is available.
pro main(): int = {
var channel: chn[str];
for (0 ... 4) {
[>]doItFast() | channel[tx] // sending the output of four routines to a channel transmitter
// each transmitter at the end sends the close signal
}
var fromCh1 = channel[rx][0] // reciveing data from one transmitter, `0`
}
fun doItFast(i: int; found: bol): str = {
return "hello"
}
If we want to use the channel within the function, we have to clone the channel's tx and capture with an ananymus routine: Once the channels transmitter goes out of scope, it gets disconnected too.
pro main(): int = {
var channel: chn[str]; // a channel with four buffer transmitters
var sequence: seq[str];
for (0 ... 4) {
[>]fun()[channel[tx]] = { // capturin gthe pipe tx from four coroutines
for(0 ... 4){
"hello" | channel[tx] // the result are sent fom withing the funciton eight times
}
} // when out of scope a signal to close the `tx` is sent
}
select(channel as c){
sequence.push(channel[rx][c]) // select statement will check for errors and check which routine is sending data
}
}
Locks - Mutex
Mutex is a locking mechanism that makes sure only one task can acquire the mutexed varaible at a time and enter the critical section. This task only releases the mutex when it exits the critical section. It is a mutual exclusion object that synchronizes access to a resource.
In FOL mutexes can be passed only through a routine. When declaring a routine, instead of using the borrow form with ( // borrowing variable )
, we use double brackets (( // mutex ))
. When we expect a mutex, then that variable, in turn has two method more:
- the
lock()
which unwraps the variable from mutex and locks it for writing and - the
unlock()
which releases the lock and makes the file avaliable to other tasks
fun loadMesh(path: str, ((meshes)): vec[mesh]) = { // declaring a *mutex and *atomic reference counter with double "(( //declaration ))"
var aMesh: mesh = mesh.loadMesh(path)
meshes.lock()
meshes.push(aMesh) // there is no need to unlock(), FOL automatically drops at the end of funciton
// if the function is longer, then we can unlock to not keep other tasks waiting
}
pro main(): int = {
~var meshPath: vec[str];
~var meshes: vec[mesh];
var aFile = file.readfile(filepath) || .panic("cant open the file")
each( line in aFile.line() ) { meshPath.push(line) };
for(m in meshPath) { [>]loadMesh(m, meshes) };
}