1 - Introduction
João is a programming language. You can use it to tell your computer to do stuff.
There is a lot of software on the internet can declare that, but João has a few gimmicks: It is lightweight and made to be easy to learn. It uses a novel directory-oriented manner of representing object-orientation that's tolerable to experts and palatable to new programmers who are tired of the concept being confused by the syntax and strange mechanics OOP often has in other languages.
Initially developed to suit my personal taste in programming languages, I have come to realize that other people may have some interest in learning it themselves, so I have written for you, kind reader, this documentation. If you are new to programming, I strongly recommend reading everything mostly in order and taking some time to play with new concepts before trying to learn new ones. An associated guide, more suited for first-time programmers, will be published at a later time.
There are bugs in this language; it is not complete and it never will be. Please report any bug or undesirable behaviour on the associated Github repository of this project, especially if it clashes with the information presented in this documentation.
1.1 - Installation & Use
The current only official software that can run João programs is an interpreter located on Github. Its source code may be viewed there, and it may be downloaded there as well. The official releases ship out with a 64-bit pre-compiled binary for Windows and a Linux binary created on a Linux Mint system.
Using the binary is simple, but may be confusing to those new to command-line-only software. To use, create a text file containing your script, such as a simple hello world program:
/main() { print("Hello, world!"); }
Then, open the command line and call the executable with the location of the file as an operand:
C:\joao_folder>Joao.exe your_file.txtJoão will then attempt to run the program, hopefully printing a friendly "Hello, world!" into the command line before exiting.
1.2 - Building João
João uses Meson, a build system. I'm not willing to put the entire documentation for that here, but you will likely need Meson if you wish to compile João yourself.
To compile, clone this repository with Git:
git clone https://github.com/Altoids1/Joao.git
After that, go into the directory and, assuming you have Meson installed, execute this command in your terminal:
meson setup --buildtype release release
This will create a folder named "release" within the directory, with everything Meson needs in order to compile an optimized build of João.
Enter the "release" directory and execute this command to compile & test João:
meson test
If everything went well, all tests should be either passing or an expected failure. Within the "release" directory is now a self-contained binary (called "joao" on Linux and "joao.exe" on Windows) which you can now interact with.
1.2.1 - Emscripten
João has tenuous support for running as a WASM executable under Emscripten. To create a Meson folder capable of compiling to the target, place this text file into the João directory (with the proper paths to your local copy of the emscripten repo), and run the following command in the João directory:
meson setup --cross-file cross_wasm.txt wasm
When compiled as normal, this should generate some JS and WASM, plus an example HTML page.
2 - The Language
Several concepts are necessary to understand in order to operate any programming language, including this one. Here they are, starting with elements likely familiar to anyone that has used a good calculator, and working upwards in complexity and downwards in usefulness.
2.1 - Variables and Types
Computers need to remember things. As with most languages, this is resolved by the use of Variables.
In João, variables are things that store Values. A Value is the dynamic type in João which is capable of storing data of a particular Type. There are several types in João:
- Null, a type (and value) which indicates a lack of data. If you declare a variable but do not set it to anything, or try to access something that doesn't exist, the value you will receive is null and its type will be Null.
- Boolean, which stores a singular bit of data, with two possible values: true and false.
- Double, which stores a number in a sort of scientific notation internally. This type is used to store very large numbers and fractions.
- Integer, which store some whole number. It can be positive or negative or zero, but it can be no bigger than 2,147,483,647 and no smaller than -2,147,483,648. Attempting to exceed these limits will result in integer overflow or underflow, "wrapping the number around" to keep within this range.
- String, which stores text. Strings are created by encasing the text in quotes, "like this".
- Object, which is for storing anything else. It stores a value that has a “type” created by you (or a library of João) with certain special functionality. This includes things like tables, which are used to store groups of data, and files, which are handles to an actual file on the computer.
2.1.1 - Strings
Strings have the usual meaning: a sequence of characters, like the word “apple.” Each character is any eight-bit number, and so strings may contain characters with any numeric value, but are typically assumed to contain ASCII text.
Escape Sequences allow a string to contain special characters. For instance, if you wanted to actually have a " character within a string, it would need to be escaped so that it isn't perceived as being the end of the string. Strings in João can contain the following C-like escape sequences:
Escape Character | Meaning |
---|---|
\n | Newline |
\t | Tab character |
\\ | The backslash character itself |
\" | Double quote |
\' | Single quote |
Strings and numbers are different things. A comparison like 10 == “10” is always false, because 10 is an integer and “10” is a string. When in doubt, typecast it out for clarity and confidence.
2.1.2 - Objects
Objects are the bread and butter of João. They use the directory system to organize groups of code and variables which can be used together to form some greater, more complex thing.
Since their concept is a bit abstract, lets start with a concrete example. Lets say you want to make a video game, and ergo might want something that can store the state of a particular player. You may want to, on a player-by-player basis, store things like their health and position on the map. Here's an object-oriented way of storing such data:
/player { Number health = 100; ## The player's health, out of 100 points Number x = 0; ## The x-coordinate of this player Number y = 0; ## y-coordinate Number z = 0; ## z-coordinate } /main() { Object my_player = /player/New(); print("My player has " .. my_player.health .. " HP!"); ## Prints 'My player has 100 HP!' }
There's several steps in the creation of an object:
- The object must have a class that is defined in a class definition, a braced block containing the class' name and a list of its properties: variables that belong to any object created from that class.
- Once the class is defined, an object of that class can be created anywhere by doing class_name/New(), with class_name being whatever the directory name of the class is. New() is a function inherent to all classes, which creates an Object with that class as its class.
Further, classes can "inherit" from other classes via the directory tree, like so:
/player { Number health = 100; Number x = 0; Number y = 0; Number z = 0; } /player/red { String team = "Red"; } /player/blue { String team = "Blue"; } /main() { Object redmann = /player/red/New(); Object blumann = /player/blue/New(); print(blumann.team .. " team hates " .. redmann.team .. " team!"); ## Prints 'Blue team hates Red team!' }
Unlike some other languages, the classes (and functions) of João can be declared in any particular order, regardless of how they inherit from one another. The inheritence scheme is only calculated after all code has been read by the interpreter.
2.2 - Arithmetic and other Operations
João can be used to carry out typical math operations. The syntax for such mimics what one may see on a fancier calculator, phrases like 1 + 5 for addition or 2 * 3.5 for multiplication. Exponentiation is expressed using the caret symbol, as in 2 ^ 10.
Assignment is also an operation, and one necessary in order to store data in a variable. It is typically written in the form x = value, where x is the name of the variable and value is the value.
Operations that go beyond typical mathematics also exist. For instance, there is an operator for concatenating two strings together:
/main() { print("Pineapple " .. "pen"); ## Prints 'Pineapple pen'. Note the explicitly-stated space character. }When the concatenation operator is used with things that are not strings, those things are coerced into being strings; 10 .. 1 becomes the String "101".
Sometimes, you may want to manipulate the bits of an Integer or Double manually; flipping them on and off like a set of switches. The typical bitwise operations are all implemented and available for this purpose. Using bitwise on doubles works, but the results may be slightly machine-dependent.
There are also operators for determining that a system of truths are correct, like asserting that a variable is less than seven, or that two statements of fact are true at the same time. They behave as you'd expect:
/main() { Value x = 5; print(x < 7 && 2 >= 1); ## Prints 'true', since both x is less than seven and 2 is greater than or equal to one. }
The order of operations behaves somewhat as you'd expect, while being somewhat expanded to accommodate the expanded set of operations. When multiple operations are used in the same expression, they are carried out in the following order, top to bottom:
Name | Operators | Associativity |
---|---|---|
Grouping & Function Calls | () | None, left |
Exponentiation | ^ | Right |
Unary Operators | - ! ~ # | Right |
Multiplication & Division | * / // % | Left |
Addition & Subtraction | + - | Left |
Concatenation | .. | Left |
Bitwise Operators | & ~ | >> << | Left |
Comparison Operators | < <= > >= != | Left |
Logical Operators | && || ~~ | Left |
Assignment Operators | = += -= *= /= %= | None |
2.3 - Iteration and Flow Control
Most programs need to be able to execute certain code only if a particular condition is met, i.e. a video game character should only be able to shoot their gun when they are not dead or out of ammo. This behaviour is implemented through the if, elseif, and else keywords.
/gun { Number ammo = 6; } /player { Number health = 100; Object gun = /gun/New(); } /main() { Object gamer = /player/New(); if(gamer.health <= 0) { print("You are dead! You can't shoot!"); } elseif(gamer.gun.ammo == 0) { print("*click*"); } else { print("The gamer fires their gun!!"); } }
There are also keywords for handling doing a repetitive process many times, avoiding having to write the same lines of code perhaps one million times. Such behaviour is achieved through the for and while keywords.
There are two varieties of for-loop in João. The first is the explicit, C-like variety. It has three statements that describe its behaviour:
- an initializer,
- a condition to test against (making the program exit the loop if the condition resolves to false), and
- an increment line to run at the end of every loop around.
Its syntax looks as follows:
/main() { for(Number i = 0; i < 100; i += 1) { if(i % 2 == 0) ## If i is even { print(i); ## Print i. Prints all of the even numbers less than 100. } } }
The second variety of for-loop acts on table Objects, and anything that derives from the /table class. It is especially useful when you need to iterate over a table which has Strings as keys, instead of just being a contiguous array.
/main() { Object tbl = {a = 1, b = 2, c = 3}; ## Constructs a Table for(key,value in tbl) { print(key,value); ## Prints, in some unknown order, the strings 'a 1', 'b 2', and 'c 3'. } }
There is a particular order that this variety of for-loop goes in. First, it iterates in consecutive order all non-negative integer indices, 0 and up. When it runs out or hits a non-consecutive integer, it begins to iterate over all other elements in the table in an undefined order, depending on the internal hashtable in which those elements are stored. There is no currently-implemented way of iterating over the keys in any other ordering.
While-loops have the same behaviour as C while loops:
/main() { Number x = 128; while(x != 1) { x /= 2; print(x); ## Prints all the powers of 2 between 1 and 128. } }
2.4 - Tables
The table class implements associative arrays. An associative array is an array that can be indexed not only with integers, but also with strings. Moreover, unlike the properties of objects, tables have no fixed size; you can add as many elements as you want to a table dynamically. Tables are the main ad-hoc data structuring mechanism in João, and a powerful one, sidestepping the rigidity of the typical class system. We can use tables to represent ordinary arrays, databases, and even other tables, in a simple, uniform, and efficient way.
You can construct tables simply through a typical /New() call. As well, you can use brackets to index into it, to set values and acquire them later:
/main() { Object tbl = /table/New(); tbl[0] = "apple"; ## Tables are zero-indexed in João. print("I love " .. tbl[0] .. "s!"); ## Prints 'I love apples!' print("There are " .. #tbl .. " elements in tbl!"); ## Prints 'There are 1 elements in tbl!' }
Like all objects, when a program has no references to a table left, João memory management deletes the table and frees the memory it was using for other purposes.
Beyond this functionality, the /table class has several methods, and there are several global functions which take tables as arguments, as described in #3.2.
2.5 - Exception Handling
It is very possible for a João program you write to come to an error condition, either as a result of imperfectly-written code on your part, or a failure of the computer the program is running on.
When this happens, João emits an Object of type /error, which is stored internally and transmitted up the stack until it is either caught in a try-catch block, or ascends beyond /main and causes the João executable itself to error out.
This /error Object holds two properties: /error.code, which stores an Integer that describes the category of error that has taken place, and /error.what, which is a human-readable String which attempts to explain the error.
The error code is just an integer. The following is a table the lists each error code that can be emitted from João and what it signifies:
Name | Error Code | Meaning |
---|---|---|
NoError | 0 | No error. This will eventually have special significance, but for now is a reserved error code value. |
Unknown | 1 | An unknown error has taken place. |
FailedTypecheck | 2 | A value failed to be of a certain type during a type-check. |
FailedOperation | 3 | An operation failed because its operand(s) were of invalid type. |
BadBreak | 4 | Signifies that a break statement held an integer value too large to make sense. |
BadCall | 5 | There was an attempt to call something that isn't a function. |
BadArgType | 6 | The arguments given to a native function were of the wrong type. |
NotEnoughArgs | 7 | An insufficient number of arguments were given to a native function. |
BadMemberAccess | 8 | There was an attempt to access a member which doesn't exist, or to do a member access on something that isn't an Object. |
BadAccess | 9 | There was an attempt to access a variable which doesn't exist. |
Error objects can also be constructed and thrown by the user, via the throw keyword:
/main() { Object err = /error/New(420,"Illegal substance detected!"); throw err; ## Presumably prints the above error into stdout/stderr and quits the program. }
Sometimes it is not necessary for João to crash when an error takes place. In such a case, the programmer may use a try-catch block to "catch" the error as its propagating from any code executed within (or as a result of the code within) the try block:
/main() { try { x = 5; ## Uh-oh! x has yet to be initialized anywhere! } catch(err) { print("Caught this error:",err.what); } print("I can resume without a problem! :-)"); }
The user, as with any other native class, can provide derived classes of /error to add additional functionality if necessary. Regardless, throw will only accept its input if it argument is an Object whose class derives from or is /error.
3 - The Standard Library
Several native functions come already in the scope of any João program, to allow for the use of common mathematical functions, I/O with the computer, and other functionality.
These functions are usually preferrable to any implementation made directly in João, as their body is typically written in compiled C++, making them faster, and are allowed access to things otherwise inaccessible by the João user.