Welcome to latria! I'm glad you're interested in learning about this exciting new scripting language. With latria you can expect to bridge between languages in a jiffy, stand up simple servers in a few lines and more. All in a familiar C style of programming.
This guide will do its best to help you get up and running with latria in as little time as possible. If you follow through from the start you should have enough of an understanding in latria to build your own latria scripts. If you ever have any problems, questions or corrections concerning this guide you can contact me at ben@axolsoft.com.
A little about what latria is, what it's intended for, and how it came about. Firstly the project was started on a whim due to issues with embedding lua on certain systems. I am a big fan of lua, but I was wondering what if I wrote something that could be, well, even smaller. Something that was almost less of a langauge and more of a basic management system. A language that you could use to collectively manage your individual scripting languages, and tasks. Think of it like a script for a dispatch system, which just also happens to be it's own standalone system as well.
With the intention to create this 'dispatch' language I set about designing a language syntax that would match this. Just a step back to explain another thing as well. I like to say that when we program/script we aren't really making anything. Now that may seem a bit harsh, but truly we're telling the computer to utilize existing resources: memory, pixels on the screen, disk space, etc. to reorganize into our 'program'. The machine you're reading this on was already capable of doing it, it just needed to be told how. With the stress on communication it comes down to one thing, how brief can I be while still expressing proper intent? That is the general issue at hand when programming. Programs that are too concise may not be explaining how to do a task as explicitly as it should, or a bloated program may be explaining far too redundantly how to do some.
With latria the intention was to make sure that the language was simple, so intent could always be expressed briefly. If the intent is of a complex nature and requires detailed instructions, it could be written in latria, but the desire was to avoid such complex builds in latria. The incentive to the programmer is to use the embedded bridging feature of latria, a glorified piping mechanism of source. Source code from multiple languages could be written into one file, with instructions embedded on how to interpret the embedded source. The details for this mechanism will be explained in depth later on, but to sum it up developers can write 'frankenstein' scripts in latria, allowing you to dynamically interpolate the outputs of programs into other programs.
Well that's about enough said in vaguely technical terms. After first starting the project in December of 2014 the language was opensourced in December of 2015. After about a year's worth of work the language has undergone various reconstructions, tear downs, and changes of focus, but the core concept of language interpolation mentioned above was always kept at the heart. I think it's a fascinating concept to create a language that leverages other languages as one of it's core concepts, after all they may be different, but fundamentally the concepts expressed in all languages can be nearly universally translated. Similar to speaking in spanglish, or another compilation of languages in order to express an idea on the spot in a personally convenient manner, latria is intended to be expressed not by itself but in conjunction with one or more languages to complement itself.
You have a few choices when it comes to using latria.
For either a prebuilt version or the source visit latria.uphouseworks.com. Links for prebuilt binaries and the source can be found at the bottom of the site.
When possible I would recommend that you build latria from source, as the builds may not be as up to date as the current source. Currently there is build support for linux & mac using make mac or make linux from the terminal. For building on windows it's a bit trickier, but I'll elaborate later on.
To build latria from source you'll need the code, you can grab that over from the link at latria.uphouseworks.com. Once you have the source you can utilize the make [platform] command from the terminal on mac/linux. Depending on your platform substitute the name for your given platform, for example make mac or make linux. Both of these commands will build a completely standalone version of latria that is capable of compiling and interpreting code. If you wish to create a smaller binary, you can build the interpreter and compiler seperately. For mac, to build the interpreter and compiler you would type the following in the console. make mac-interpreter and make mac-compiler (make sure to run make clean first if you already built the standalone binary).
To see more build targets you can run make help, which will list all valid build targets, including the debuggable targets.
To build latria for windows, you'll need to have Visual Studio installed with access to the developer command prompt, which provides the required environment to build under. Nmake is utilized to build on windows, and makes use of an additional nmake compatible Makefile (appropriately called NMakefile).
To build latria on windows you invoke nmake on the NMakefile as follows:
::Be sure to invoke with /F NMakefile, otherwise nmake will unsuccessfully attempt to use the regular Makefile
nmake /F NMakefile latria
This creates a standard latria binary, capable of running latria scripts standalone. Some other build options are as follows.
:: makes the interpreter only
nmake /F NMakefile interpreter
:: makes the compiler only
nmake /F NMakefile compiler
To set latria up on your system path for mac/linux you can use make install, which will install the interpreter/compiler on your machine.
For windows there is no supported system installation, simply place it where you want to use it and continue from there.
Once you have finished installing latria you can test whether or not latria is installed by running the command latria -v from anywhere on your system (for mac/linux). You should see the version number of latria printed out to the console if it is properly installed.
Congratulations, you now have a working copy of latria installed locally! Now we can actually start writing some latria script.
Keeping in line with the way things work and acknowledging the concept of languages working together, we'll write a simple script to get started.
print("hola mundo!")
//a comment, prints 'hola mundo' to the console
A 'traditional' hello world program! You can write this up in a file called first.lra and run latria first.lra to execute it. By convention all latria script files end in 'lra', and in an organizational sense it would be best to follow this convention. Now from this script you should see 'hola mundo!' print to the the console screen. If you see this print out then you've successfully run your first script! From here it's a matter of getting the basic syntax down and learning what Latria has to offer in terms of functionality.
If you were looking in the directory after running your script you may have noticed the first.lrac file automatically generated, this is the compiled latria file that is actually run. I won't delve into details, but latria executes what is referred to as bytecode, something that is generated from the code written in your scripts and stored in the .lrac files. For the most part you need not worry yourself with these files, just know that they are the product of your scripts for latria to execute.
As a language latria has a fairly non-original syntactical structure. This entire guide goes over each component of the latria language you will encounter and explains how to work with, so here we'll just go over the basics.
First off, variables. Variables are textual representations of data. They can be assigned to, passed, and deleted. To 'set' a variable you pass a name, an = and a value to assign.
x = 5
Now the variable x holds the value 5. You can now use x as it were 5. Here's another example, but with a string. If you're not familiar with the concept you can think of a string as a series of characters 'strung' together. Here we'll assign x to a string, and print it out to the screen.
x = "a string of text contained within double quotes"
print(x)
If done right this should print 'a string of text contained within double quotes'. Note that you can use double or single quotes to contain a string. If you have to include a ' or " inside a string use one or the other to avoid ending the string early. If you have to use both or prefer using one in particular, you can escape ' or " from being seen as a boundary of a string by writing a backslash character \ before.
x = 'tom\'s diner'
print(x)
Code not being spoken or written language we're used to can make it difficult to convey what is intended by that same code. When you wish to add a little info about what it is you're writing in your native tongue, use a comment. A comment can be written 2 ways in latria. Two slashes // or /* followed by */ (a block comment). When using // everything that follows is a comment until the end of that line. When using /* anything following is a comment until */ is read, even if it means going across multiple lines. Anything in a comment is completely ignored by latria and is strictly for marking up your scripts for you and other users to read. When using comments, use them early, use them often. It's too easy to comment a piece of code and save yourself the dilemma of attempting to reinterpret what it was you originally wrote, in latria or any language for that matter.
// this entire line is a comment
// x = 2 does nothing since this is a comment
/* a block comment on one line */
/*
a block comment
across many
lines
*/
As seen earlier with the 'print' statement, function calls are a major part of latria. Without the print function you would have a difficult time trying to see what latria is doing!
In Latria there are user provided functions and system functions, here I'll be explaining the later (user defined functions are explained later on). Let's look at the print function again. This time let's assign a string to a variable and print the variable instead.
// assign a string to x
x = "printed out!"
// call print with x as the argument
print(x)
// prints 'printed out!'
Function calls are capable of taking expressions instead of raw values or variables as well. See for yourself.
// assign 5 to x
x = 5
// call print with the expression 'x * 4' as the argument
print(x * 4)
// prints 20
As you can see the expression of x * 4, or 5 times 4 is evaluated and passed as it's result to the print function. This kind of functionality can allow you to make many operations on line instead of having to write them out line by line, saving you space and time where complexity is not a concern.
If you wish to clear a variable, you can 'null' it by setting it equal to a special type called null. Setting any variable to null unsets that variable, removing the value and variable simultaneously.
// set x to 2
x = 2
print(x) //2
//set x to null
x = null
print(x) // null (as in not set)
// set x to 'baby back ribs!'
x = 'baby back ribs!'
print(x) // baby back ribs!
//set x to null again
x = null
print(x) // null (as in not set)
There are 6 data types in latria that you need to be aware of to get started:
Number
String
File
Array
Connection
null
Strings are a basic type, bounded by either single or double quotes and containing any number of characters up to a closing single or double quote.
Numbers are represented internally as a 'double' type, providing an acceptable degree of precision. A number value can be set from either a Integer or a Float value, both will be converted to double internally.
Files are a type that represents an open file handle. These types are set by opening a file and used by standard IO functions.
Arrays are a type that holds an arbitrary amount of key/value pairs inside of itself. Arrays can hold any type of value (although arrays in arrays are a bit off as of this moment).
Connections are created when a server received a connection or a client successfully connects to a server. These are used for managing an established connection, such as writing/reading.
null is a special type used to denote the absence of a value in a variable.
In latria these types are all associated with variables, which are dynamically typed. This means their type is determined based on the value being assigned to it. Latria automatically handles the variable appropriately based on the underlying type of the value that was assigned to it. The code below shows an example of assigning values of varying types to the same variable.
x = 2 //a Number
print(x) //prints 2
x = "bacon" //a String
print(x) //prints 'bacon'
Putting this in a .lra and running it should print out the following:
2
bacon
When working with variables it is possible to change the type of a current variable depending on what you assign it to, but latria will always checks the type provided to any binary operator or function to verify it as expected. If it isn't as expected latria will print out an error to let you know.
Arithmetic in latria is straight forward, with PEMDAS order of operations prevailing over compound expressions. An example expression of arithmetic in latria is as follows. Note that the parentheses are respected as aforementioned via PEMDAS.
x = 2 * 3 + (5 * 4 / 2) - 2
print(x) //14
The standard arithmetic operators +, -, *, /, ^ and % are supported. Bitwise operators are also supported, but as system functions instead.
All of the aforementioned operators or binary, meaning they work on two elements. Many other languages feature these and unary operators. These work on a single element, such as the ++ increment and -- decrement operators. No unary operators are present in latria.
Strings in latria are a useful type, allowing you create basic webservers that output data or to read/write from a file. When it comes to strings they are a special case as well, being the only other first class type that can be acted on by the + operator besides Numbers. This is referred to as string concatenation, an example of this is as follows:
x = "latria strings" + " glued together"
print(x) //'latria strings glued together'
So, great, we can stick strings together, what else can we do? Beyond this latria provides a suite of built in system functions for performing standard operations with strings. Some of these functions include find, replace, substitution and match.
Find finds a string within a string, and returns the index it was found at. It takes 2 string arguments, the first the target string to search in and the second the string to find within the first string.
x = find("string or string", "or")
print(x) //7
This by itself, is not too useful, however if we wish to use this to get a string we can use the sub string function with this index we have found.
x = substr("string or string", 7, 9) //takes the substring between the two indexes provided in a string
print(x) //'or'
Now what if we wanted to take the sub string from 7 to the end of the string? We can use the same function above with one less argument to achieve this.
x = substr("string or string", 7) //takes the substring from 7 to the end of this string
print(x) //'or string'
Excellent! We've taken a substring from a string. Now what if we wanted to replace that instead though. We can use the replace instead.
x = replace("string or string", "or", "and")
print(x) //'string and string'
String are pretty maleable with this, but what if we want more? It turns out there's a 4th function to match a string within a string by a pattern. This 'pattern' can be as simple as another raw string, like what is provided to find. However this is no ordinary function. This is a regex function! For those of you who know regex, they are a powerful component found almost universally in all languages. The underlying regex engine and the special characters it supports will be detailed later on. A simple example for now is as follows:
x = match("string or string", "or[^s]") //matches 'o' and 'r' followed by anything that is not 's'
print(x)//1 (for success, 0 for failure)
Again, I won't go into details right here but capturing and non-capturing parentheses are supported as well, for those who care to know.
Arrays in latria are a special data type that holds a collection of keys and associated values, keys being strings and values being other variables. Utilizing arrays it is possible create complex data structures that can keep track of such things as time sheets, scoreboards, recipe lists and even documentation.
First I'll show an example of the syntax for initializing an array, then I'll explain it afterwards.
//creates an array with one key/value
x = {"animal":"cow"}
//creates an array with 2 elements
x = {"age":66 , "height":6}
Creating an array starts with a regular variable assignment, until after the = sign. At this point our array definition begins. An array initializer is always marked with a pair of curly braces {} following an equals sign. Within these curly braces can be 1 or more key/value pairs to initialize the array with. For each key/value pair the key is a string, followed by a colon :, followed by a value or variable to assign to that key. If another key/value pair is desired a comma is used to separate the individual pairs.
Accessing an array element allows you to either reference it as a variable or to set it to another value. An example of both getting and setting an array value is below.
//setting x to an array with 2 key/value pairs
x={"1":"cat" , "2":"dog"}
//print the value of "1" in x
print(x["1"])
//change the value of "1" in x to moose
x["1"] = "moose"
//print the value of "1" in x again
print(x["1"])
Accessing an array element is doing with the variable name and a pair of square brackets enclosing the key. When referencing an array element you can either use the value from the key, or reassign a new value to that key. The syntax for the actual reference is the same both ways as shown above.
It is also possible to create an array simply by setting an array element on a variable. The variable will be created or converted to an array type behind the scenes and the single key/value will be set.
Control flows are an essential part of any language, allowing a developer to create detailed and complex programs and scripts. If you are familiar with C style control flows most of these should be immediately familiar to you. However I will be going over each control flow in detail, so if you feel the need you may quickly pass over this section.
An if statement is a staple in terms of control flows. With it you can execute code conditionally. When compounded with other if statements you can create a complex program capable of a variety of tasks (such as a language like this!). So first let's look at what a basic if statement looks like.
if( 1 == 1 ) {
print("1 is equal to 1")
}
Now to explain a bit. To start an if statement begins with the 'if' keyword, and is followed by a pair of containing parentheses wrapped around a condition. This condition is a statement that is evaluated to a boolean result. An expression can be created by comparing two values using the following:
equals ==, not-equals !=, less-than <, greater-than, >, less-than equals <= and greater-than equals >=.
Expressions can created by compounding these using 'And' && and 'Or' ||. Additionally these expressions can be further compounded using containing parentheses, but we won't be going into detail about that here.
If the expression is evaluated to be true, the code contained within the following curly braces is evaluated. If false the code contained within the curly braces is skipped entirely. This conditional branching is an important concept to note.
In our statement the expression 1 == 1 evaluates to true and our print statement is executed.
An elseif statement is almost exactly like an if statement. The exception being that an elseif must always follow a preceding if or elseif statement. In this way 2 or more statements can be chained onto each other, creating a list of conditions to try. Although an elseif can follow an if, an if following an if will be treated independently. On any condition evaluating to true, it's contents are executed, and the following statements are skipped. An couple examples are as follows.
//an elseif being executed
if( 1 == 2 ) {
print("incorrect.")
} elseif( 1 == 1 ) {
print("correct!")
}
//an elseif being executed before another
if( 1 == 2 ) {
print("incorrect.")
} elseif( 1 == 1 ) {
print("correct!")
} elseif( 2 == 2) {
print("also correct, but never run!")
}
An else statement is another component of an existing 'if' or 'elseif'. It is never on it's own and most always be preceded by an if or elseif statement. You can imagine an else statement something akin to a failsafe condition. If the preceding if or elseif conditions all fail, the else statement will always evaluate to true and be executed. Note that no elseifs can follow an else statement. An example of an else is as follows.
if( 1 == 2 ) {
print("this can't be right...")
} elseif( 2 == 1 ) {
print("and neither can this")
} else {
print("i guess else will have to do!")
}
The for loop is a construct designed to allow a program to iterate over a section of code a certain number of times. A for loop in latria looks like the following.
//an empty for loop
for( x = 0, x < 10, x = x + 1 ) {
}
Now what exactly did we just see? Well, for starters a for loop is always initiated with the 'for' keyword, after which an opening parenthese, followed by 3 parts and a closing parenthese, follow. It's these three elements that are the key to the functionality of a for loop. The first element, in our case x = 0 is what's called the initialization. The initialization declares and assigns any variables that must be used for our loop. The second element, x < 10 , is called the condition. The condition checks the provided condition, and exits the loop when it evaluates to false. The third and final element, x = x + 1 , is called the afterthought. The afterthought is a line of code that is executed once at the end of every iteration.
When you put these three together one can see we are simply assigning x to 0, checking that x is less than 10, and incrementing x by 1 at the end of every iteration. The last bit would be to address those curly braces, which indicate the start and stop of the code we will be looping over. Anything written in here will be executed until the loop evaluates to false. Using the above example we can see this by printing out the value of x every time we loop.
//a printing for loop
for( x = 0, x < 10, x = x + 1 ) {
print(x)
}
This should print out the values 0-9, each on separate lines. So great, we can loop over whatever we like. What if we want to skip an iteration though? This is possible by using the continue keyword. Similar to it's usage in C based languages, continue simply skips to the end of the current loop and executes the afterthought (mentioned earlier). Let's say we wanted to skip printing the number 5? We can do that by putting a continue in an if statement.
//a printing for loop that skips 5
for( x = 0, x < 10, x = x + 1 ) {
if( x == 5 ) {
//skip this iteration
continue
}
print(x)
}
This prints out the values 0-9 while skipping 5. That's great, but what if we wanted to simple exit the loop altogether at 5? We could alter the condition of the for loop, but we can also use the break keyword to 'break' out of the loop. An example of this is as follows
//a printing for loop that breaks out at 5
for( x = 0, x < 10, x = x + 1 ) {
if( x == 5 ) {
//break out
break
}
print(x)
}
Now this prints out the values 0-4, exiting the loop when the value is 5.
The while loop is a construct similar to a for loop, allowing a program to loop over a given section of code. A while loop is like a for loop that has no initialization sequence and no afterthought sequence (as mentioned in for), only retaining the condition. This condition is evaluated before each run, and if evaluated to true, executes another loop. When this condition evaluates to false, the loop is exited. An example of a while loop is below.
x = 0
// a while loop counting up to 10
while( x <= 10 ) {
//print x
print(x)
//increment x by 1
x = x + 1
}
Similarly to a for loop, you can also use continue and break to skip an iteration or to exit the loop entirely.
x = 0
// a while loop counting up to 10, but skipping 5
while( x <= 10 ) {
if( x == 5 ) {
//increment one up (otherwise we will loop forever) and skip this iteration
x = x +1
continue
}
//print x
print(x)
//increment x by 1
x = x + 1
}
x = 0
// a while loop counting up to 5
while( x <= 10 ) {
if( x == 5 ) {
//break out
break
}
//print x
print(x)
//increment x by 1
x = x + 1
}
Declaring functions allows you to encapsulate functionality that you would otherwise have to continually write out. They are are useful component in simple scripts and can become near essential in complex ones. The functionality of a function in Latria is near identical to those of most other programming languages (minus the initial syntax) and so should be relatively easy to grasp if you're familiar with the concept.
Some functions have already been provided to you by the system, like print. To call these functions you provide the function name and a parentheses enclosed list of arguments.
print("an argument passed to print")
Declaring functions is fairly straightforward. For a quick and easy function, you could write one as follows.
//a function called 'function'
@function() {
print("calling a function!")
}
//call 'function'
function()
So what exactly does everything above do? Well, let's break apart and see what makes a function in latria.
To start, a function definition always begins with an '@' symbol. This indicates to latria that what follows is a function definition. After this the name of the function follows. A function name can be any number of alpha numeric characters, but must start with an alpha character. A name like 'a123Function' would work, but a name like '123function' would not. After the function name we have the parameter list contained within parentheses. The parameter list can be empty, or filled with a comma separated list of variables. If you noticed the above example has no parameters, so let's see an example with some.
//a function called 'function' that takes variables x,y,z
@function(x,y,z) {
print(x+y+z)
}
//call function with 1,2,3 passed as arguments
function(1,2,3)
Notice that since latria uses inferred types there is no control over what kind of variables you can pass to a function. The above function could take numbers or strings, or both! This kind of flexibility can come as a double edged blade, allowing you to stretch the applications of a single function definition or to end up performing string math by accident. Name your functions clearly to guard against such mistakes.
Now finally a function body is contained within a pair of curly braces {}. In the examples here you'll see curly braces on the same line as the function definition. This is optional and you can place them anywhere you want so long as the opening brace precedes the function body and the closing brace follows it. Anything that goes between these braces is run as latria code, including other function definitions! It's important to note that when within a function you are within a scope that only has access to the variables passed to it. A scope in this case is referring to a closure that is defined by the function body. Anything within the closure operates with a set of variables independent of any code preceding or following the function when called. A good example of the separation of variables across scopes is below, where the variable x is declared outside of a function and passed as an argument. Although it is altered inside once the function body is exited the value of x returns to what it was before. This is an example of x being declared in scope a, and another x being declared in scope b. They may both be called x, but they are distinctly different variables.
//set x to 2
x=2
//create a function that takes x as a parameter
@f(x) {
//increment x by 1
x=x+1
//print x
print(x)
}
//print x, call our function and then print x again to see it has not changed in this scope
print(x)
f(x)
print(x)
The above will print x 3 times, showing the original, the 'new' x and the original again. Even though the name of the variable 'x' is the same throughout, the scope changes when the function body is entered. Any variables passed or declared inside a function body remain independent from any variables before. This also means that once you exit the function body any variables you declare will 'fall' out of scope, making them no longer accessible. In most cases this is desirable, as it makes latria memory managed. However when you calculate something that you wish to get back from a function you will most likely want to return a variable.
Returning a variable from a function is essential if you wish to have access to the result of your function's work. To do this you simply utilize the return keyword followed by a space and the variable you wish to return.
//declaring a function called 'double' that will double and return our input
@double(x) {
x=x*2
//returns x
return x
}
//call double and assign it to var
var = double(5)
print(var) //10
You can return any variable of any type you wish, but you may only return one. If you wish to return one or more you may declare an array inside your function and return that instead.
An additional note, although variables are limited to scope, function definitions are not currently held by this same limitation. Functions declared within functions are available globally regardless of how nested they may be. I want to make sure that you as the developer are aware of this in the meantime while it is an outstanding issue.
Latria encapsulates the standard 'rand' function in C. Making it available via the function random. It's takes one Number argument and returns a Number.
x = random(1024)
print(x) //a random number between 0 and 1023
The rand function is a pseudorandom number generator. Meaning it's output is predictable from a given seed. In order to prevent random from returning the same series of values on startup it is recommended that you alter it using the seed function, which takes one Number arg and does not return a value.
//x is equal to a previously obtained arbitrary Number
seed(x) //seeds the random number generator with this Number
y = random(1024)
print(y) //a 'hopefully' unexpected random number between 0 and 1023
With these two functions you can create a reliable prng (pseudo-random number generator) to create psuedo-arbitrary values from.
Reading user input is a simple but important feature of any language. In latria it is achieved via the input function. When called it halts until the user finishes typing their input and hits enter. Upon completion it returns a String value.
x = input() //waits for user to enter input
print("You entered: " + x)
User input can be used to allow selections, seeding of random values and even to chat in a simple server/client setup.
In latria the ability to read files is an important aspect. Reading and writing files is supported, but binary is not (potentially in the future when a 'char' type is added). In keeping with the overall design, reading from files is incredibly simply, reading only 1 line at a time. Writing is straightforward as well, writing only one line at a time. All the file modes that are supported in C ( w, r, r+, a+, etc.) are supported in latria.
To open a file you can use the open function, which takes two string arguments. The first is the name of the file, and the second is the mode to open it in ( r, w, a+, etc.). On success this function will return a valid pointer to a file that can used in latria.
x = open("file.txt", "r")
To read from a file you use the read function, which takes a single File argument. It will return lines or null on the reaching the end of the file.
x = open("file.txt", "r")
line = read(x)
print(line) //a line we read from 'file.txt'
To write to a file you use the write function, which takes a File argument and a String argument. The string value is written out as a line in the file.
x = open("file.txt", "w")
write(x,"a line!") //writes a line into this file
Once you are done reading or writing to a file use close to close the file. It takes a single argument, being the File to close.
x = open("file.txt", "r")
line = read(x)
print(line)
close(x) //closes this file
If you wish to remove a file completely you can call remove to delete it. Remove takes the name of the file as a String argument.
remove("file.txt") //deletes this file
Latria provides only one math function at this time, the square root function. It takes one Number argument and returns the square root of the given value.
x = sqrt(64)
print(x) //8
Latria provides bitwise operations in the form of function calls. The provided bitwise functions are listed below with examples:
The bitwise 'not' operator is expressed using the function not. It is often referred to as the complement operator, performing a logical negation on the bits of the passed in value. 0's become 1's, and 1's become 0's, 'flipping' the value.
x = not(100)
print(x) //-101
The bitwise inclusive or (or) operator is expressed using the function or. It takes two Number arguments. The resulting output bits are 0 for each position where bits in both arguments are 0, otherwise they are 1.
x = or(101,500)
print(x) //501
The bitwise exclusive or (xor) operator is expressed using the function xor. It takes two Number arguments. The resulting output bits are 1 for each position where only the first bit is 1 or only the second bit is 1, otherwise they are 0.
x = xor(101,500)
print(x) //401
The bitwise and (and) operator is expressed using the function and. It takes two Number arguments. The output bits are the result of the first and second bit being multiplied.
x = and(101,500)
print(x) //100
A basic regular expressions engine is provided in latria for matching purposes. It supports most of the basic regular expression syntax with a few exceptions (most notably look-ahead and look-behind). This engine can be used to match against strings and to even return the portion matched within a string.
Regular expressions are utilized by calling the match function. Which takes two parameters, a target string, and a string representation of a regular expression to use on the target string. A simple example is as follows.
x = match("a basic string", "basic") //will return 1 for a match and 0 for a non-match
print(x) //prints 1, indicating a match (0 is a non-match)
A simple match, so let's try something a little more substantial.
Let's try and match anything starting with 'b'.
x = match("a less basic string", "b.*") //matching b followed by anything
print(x) //1
So far so good, but we can do better with capturing parentheses.
Capturing parentheses are supported in latria, allowing you to match and return specific sub-strings.
x = match("a simple string", "(simple)") //searches for 'string' and returns 1 if found in the target string
x = capture(0) //returns the first completed capture 'simple'
// a more complex match
x = match("another string", "([a-z]+)\s")
x = capture(0) //returns the first completed capture, 'another'
It's an important point to stress that latria's strength is in how it works with other languages. Executing code in a shell environment can open up a wealth of potential resources to be used as you see fit.
It is these additional resources that can be utilized to 'extend' latria's functionality on a system by system basis. This comes as a doubly beneficial aspect, providing a way to retrofit latria on the go and allowing you to utilize other scripts and programs that you have on your local machine.
With all of that said, this part of latria is just a glorified 'piping' or 'bridging' mechanism. Even so it stands out in a unique syntactical style all it's own, as well as allowing multiple lines in your script to be executed in the same shell. This multi-line aspect becomes incredibly useful for literally inlining other languages (when needed), or simply to include a few configuration steps in addition to calling another program or script. Anything that would be valid in a shell environment on your local machine would be just as valid here.
So, how does this work? Latria provides a special syntax to execute code in a shell on your local system.
It is defined by an opening and closing block sequence, both of which must not be followed or preceded by anything (with the exception of assigning the block's result to a variable, that will come up in a moment)
Here's a simple example below.
//opening sequence, after this everything is executed verbatim in a shell environment (including comments) up to a valid closing sequence
##
echo done on the shell
#>
//closing sequence, after this everything is interpreted normally as latria code
Now upon running this you may have realized something, nothing happens. Well, technically something happens.
The issue is that we're just not capturing what our shell did. It certainly did echo the provided statement, but only to itself and not out to latria.
We can rectify this by assigning the result of our shell to a latria variable. Modifying the example above, assignment looks a little something like this.
//assigns the result of what is executed on the shell below into the variable 'var'
var##
echo done on the shell
#>
print(var) //prints 'done on the shell\n' (note the extra linebreak that comes from the shell output)
With this you can execute whatever you'd like in latria and be able to include results from system shell executions. Additionally these shell executions can be spread out across as many lines as you like for legibility.
There is an important rule you must regard however. An opening sequence must never be followed by code, and a closing sequence must never be preceded by code.
A few examples of this are as follows.
//can't do this
var##echo this
#>
//or this
var##
echo this#>
//or this
var##echo this#>
We can get output from our shell executions, but what if we want to alter what we send programmatically? To solve this variable interpolation is supported in shell executions. The syntax is a bit bulky, but it's designed to be very obvious regardless of what language you're interpolating a latria variable into, minimizing any confusion on where and when you're interpolating a value.
//assign x to 'bacon'
x = 'bacon'
//interpolate x into our shell execution
var##
echo IN{x}
#>
print(var) //prints 'bacon\n'
It is important to note that currently only variables are supported in this form of interpolation. In the future function calls will be as well.
Since Latria is a scripting language it can be interpreted dynamically, which gives us the opportunity to execute latria code from inside latria or to execute additional latria scripts. This gives incredible flexibility to latria, allowing one to modularize their code and simply load up that section when needed. This means changes are reflected quickly, and there is no need to recompile latria code repeatedly, this is handled for you on an as needed basis.
Load allows you to run a latria script in the context of your current script. The child script you fire up will have access to all of the current variables and functions that are available to your calling script's scope.
This function is provided as load and only takes 1 argument, the relative path to the script to load.
//loads a latria script
load("myscript.lra")
Run allows you to attempt to dynamically interpret and run latria code from a string. This is provided as the function run and only takes 1 argument, the string to interpret and run as latria code.
//assign a string
x = "print('valid latria')"
//attempt to interpret and run this string as latria code
run(x) //prints 'valid latria'
In today's world nearly everything is interconnected. In response latria incorporates server and client functionality as a basic need in this day and age. With the built in support for server and client functionality setting up either can be done in as few as a couple lines of code. When a connection is established both parties can either read, write or terminate the connection.
Setting up a server in latria is a very straightforward process. To set up a server one need only call the startServer function. This function takes only one number argument, the port number to bind to. An example of starting a server is as follows.
/*
listens for an incoming connection on port 5000
NOTE this blocks until the script is terminated or an incoming connection is made
*/
x = startServer(5000)
//'x' now references an established connection (successful or failed)
Be aware that calling this function will block until a connection is made.
A client side application in latria is just about as simple as it is to create a server-side one. The connect function is called with two arguments, a string of the ip address (IPv4) to connect to and a number for the port to try. Similarly to the 'startServer' function, the 'connect' function returns what's referred to as a 'connection' object. That will be explained in just a moment, but first let's see how connecting to a server looks written out.
//attempts to connect to a server at the address on the port given
x = connect("127.0.0.1", 5000)
//'x' now references an established connection (successful or failed)
As mentioned before a connection is managed via a 'connection' object. A connection object is returned from 'startServer' or 'connect' when a connection is established. When a connection attempted by the 'connect' function fails however, it returns a -1 value, which should be checked before continuing.
A connection is a first class citizen, capable of being passed and returned like any other value. Once a connection has been obtained it can be manipulated by 3 distinct functions: readData, sendData and closeConnection.
Reading data from an established connection is done through the readData function, which takes 1 established connection as an argument and returns a string of the data read. It is important to note that this is a blocking call, and will remain so until data is available to be read from the connection. With this in mind take care not to immediately call 'readData' when it is possible there may not be data to read. If you do this there is a risk of a hangup with both sides simply listening forever! With that noted, here's an example of reading data from an established connection.
//connect to a server on port 5000
x = connect("127.0.0.1", 5000)
//read data from our connection, returns a string
read = readData(x)
//print what we read
print(read)
Sending data through an established connection is done through the sendData function, which takes an established connection and a string value to send as arguments. It returns a number value of the number of bytes written to the output stream. If you send a string such as "meep meep" and 'sendData' returns a value of 0 you can be relatively sure that the other side has disconnected or is otherwise no longer available. Here's an example of sending over an established connection and checking if we sent successfully using an if-else statement.
//connect to a server on port 5000
x = connect("127.0.0.1", 5000)
result = sendData(x,"meep meep")
//check the value to make sure we wrote out successfully
if(result == 0) {
print("Error! Was unable to write to the stream.")
} else {
print("Success!")
}
Closing an established connection is something you should make a habit of when you're all done. It's a manner of good housekeeping, to speak. Closing a connection frees up not only the associated variable but also any underlying resources the system has involved in this connection as well. To do this you call the closeConnection function, passing a connection argument. Below is an example of the whole connection life cycle from the client's perspective with the connection being closed at the end.
//Shows the whole cycle of connecting to a server, sending data, reading data, and closing the connection
//connect to a server on port 5000
x = connect("127.0.0.1", 5000)
result = sendData(x,"meep meep")
//check the value to make sure we wrote out successfully
if(result == 0) {
print("Error! Was unable to write to the stream.")
} else {
print("Wrote successfully!")
}
//reads data from the connection source
read = readData(x)
//prints what we read
print(read)
//Closes our connection
closeConnection(x)
Latria is a garbage collected language. The user need not worry about managing memory manually. On the other hand it is important that this 'garbage collection' is given attention, as it can lead to undesirable circumstances should the user not be careful. The summary below is a bit technical but I recommend you going over it.
To start I'll relate the inner workings in an abstract way. Normally a GC could be described by such things as conservative, precise, mark-and-sweep, incremental, etc. The GC in Latria does not fit into any one of these categories completely, and in a technical sense it isn't even really a GC at all. The way this collection system works is something that can be generally described as follows.
Imagine a recycling plant that takes whatever you give them. When possible they recycle and return whatever is given to them. It's easy to imagine that over time they would inevitably end up with a large quantity of unusable junk which they would have to eventually dispose of. They would like to dispose of this junk as infrequently as possible, as it takes a large amount of time to do, so they wait until they can't handle anything else. Once they're reached capacity, they dispose of everything they're holding, creating room for more junk/recyclables.
Ultimately Latria's 'garbage collector' is more accurately a dynamically formed memory pool. This pool is very similar to the aforementioned recycling plant. Every time you set a variable to null or a variable falls out of scope, it is trashed. These trashed variables end up in our pool. In addition to the variables you see, all latria's allocation logic is managed by way of this pool. When any allocations are made (internally or by the user) latria makes a request to the pool for a block. The pool then rummages around through it's 'junk' and looks for a chunk of memory that is of the appropriate size. Upon finding a block that is at least large enough, the pool returns that block (a pointer to it to be exact). If the pool does not find a valid block it defers to requesting a block via a managed call to malloc.
Great, so now we have a chunk of memory to do with as we please. What happens when we no longer want this memory? We return it to the system! Now in C this is usually achieved via the 'free' function, which does as it says. However, in C, there is not only an overhead to allocation, but also to freeing. In addition, memory is memory. It's universal despite what it may be containing. With this in mind, Latria attempts to recycle memory rather than free it. Not only can we avoid a future malloc but we can potentially keep our memory footprint indefinitely small
With this scheme a few issues rise though. One being we can only hold so much memory to recycle before we need to start giving back, and two we don't have a way of telling whether two variables reference the same underlying object. Within latria itself, this is not a problem. The calls to allocate and free memory are synonymous with malloc and free in how they are utilized. The problem of multiple references to a single object becomes an issue when the user assigns a File value to an additional variable. When done both variables will reference the same File, and if one is closed the other will become an invalid pointer. It is at this point that this designs becomes, faulty. When working with such objects it is best to avoid multiple references when possible, and when not possible they should be treated with caution.
To more technically describe this memory is freed to and allocated from the pool's contents whenever possible. When not possible the pool is bypassed and memory is asked for using a managed call to malloc. When this memory is freed later on it is added to the pool as an additional resource. When latria is shutdown or a scope is exited any allocated memory from the enclosing scope or the latria vm is sent to this pool. When the pool exceeds a certain size latria momentarily defers itself to managing the flushing of the pool. Generally this goes unnoticed, but should the user allow an enormous max program size (more on that later), a flush could very well freeze the program for a few seconds or more. However the default maximum pool size should be more than sufficient to allow for smooth operation.
So, having established that this GC is more along the lines of a recycler, how do we indicate how much we're willing to hold? Their is a function __setGCRate which takes a single number value, the value by which to multiply the pool's size. The baseline pool size is 1024 bytes. Passing 2.0 would double the pool size, while 0.5 would halve it. By running your program with a pool size of 0 you can run latria at just over 500kb. Using the aforementioned function you could modify the 'collection' rate as follows.
//sets the pool size to 0, freeing all memory immediately after being added to the pool
__setGCRate(0.0)
//note this has a performance hit, but if memory is an issue latria can run at just over 500kb using this
The following are additional latria functions that didn't quite make it into a category of their own. While they are important, they can be summed up here.
The sleep function is called with a single number argument, indicating the number of seconds to sleep.
//sleeps for 2 seconds
sleep(2)
Returns the current time as a number since 00:00 hours, Jan 1, 1970 UTC. You can get this time by calling time with no arguments.
//gets the current time since 00:00 hours, Jan 1, 1970 UTC
x = time()
Returns the name of the platform as a string that this instance of latria was compiled for. Called via the function platformName with no arguments. The possible values it can return are mac, linux, windows, or undefined.
//returns the platform name
x = platformName()
//prints the platform name
print(x)
Purges all allocated variables, functions, etc. You can call this via the function __purge with no arguments and no return value.
//set x to something
x = "something"
//prints 'something'
print(x)
__purge()
//prints 'null'
print(x)
Now that you've reached the end of this guide you're ready to deploy latria in real-world applications with effective results. You understand what makes latria tick and how to best approach a problem that has presented itself to you.
From here on out you can follow the latest releases on github to see what's going on and what new features have been introduced. You can also see the latest releases on github to make sure you have an up to date version of latria.