| examples | ||
| lvera | ||
| sketches | ||
| .gitignore | ||
| README.md | ||
LVERA — /ˈlueː.ra/
A LuaJIT implementation of Vera, the low-level bedrock of the Nova language family.
Lvera (stylized LVERA) is a modular compiler for Nova.
Implementation Status
LVERA is currently a work in progress. Things will be in flux. I am in the process of reworking LVERA to use compiler passes and a modular backend approach rather than a hard coded Lua backend and rigid annotation definition.
Using LVERA
This project is still in the very early stages of development. There is still lots of work to do. It's also run by a mild disorganized lizard (which may or may not live on the moon). However, here's a quick run down of using Lvera.
This is very much a "rough draft"
Add to Project
Adding lvera to a project currently requires copying the lvera/ directory.Assuming lvera/ at the root directary of your project, the bare minimum to
make Lvera useful is the following:
|_| hello-world.nv
|#port, on hello, needs, @hello world|
|#port body, on hello, lua| [%
print("Hello, world!")
%]
|hello world, @once|
@hello world, @once
|| hello world:10, @once
-- main.lua
local lvera = require("lvera").new()
require "passes" (lvera)
lvera.module_name = "hello-world"
lvera:load("hello-world.nv")
lvera:build()
local machine = require "hello-world"
machine:run()
The examples/ folder contain some more advanced setups.
API
Static Functions
The following are static methods that exist within the Lvera module.
Lvera.new()
Constructs a new compiler for you to add rules and compiler passes to.
local Lvera = require("lvera.lvera")
local compiler = Lvera.new()
Lvera.parse_rule_set(rules, program)
Takes a table and a string. Program string is parsed into the rules
table given. The table will be filled with a series of records in the
form of {left : string[], right : string[]}. For example, given the
following:
|flour, sugar, apples|
apple cake
|apples, oranges, cherries|
fruit salad
|fruit salad, apple cake|
fruit cake
|| sugar, oranges, apples, cherries, flour, apples
You will recieve back this:
{ { left = {"flour", "sugar", "apples"}
, right = {"apples cake"}
}
, { left = {"apples", "oranges", "cherries"}
, right = {"fruit salad"}
}
, { left = {"fruit salad", "apple cake"}
, right = {"fruit cake"}
}
, { left = {}
, right = {"sugar", "oranges", "apples", "cherries", "flour", "apples"}
}
}
Compiler Methods
The following are instance method for the compiler. A instance of a Lvera
compiler can be made using Lvera.new().
compiler:load_string(program)
Reads a program from a string, parsers it, then addes the rules to its internal rule list. This method allows you to aggerate multiple rule sets.
compiler:load_file(source_file)
Takes a string representing a file path and loads the rule set it contains.
Like compiler:load_string(program), this adds rules to an internal list,
allowing you to merge multiple rule sets.
compiler:add_pass(pass)
Add pass allows you to preform arbatrary transformations to the rule set. Each compiler pass is expected to have the following form:
compiler:add_pass(function(rules)
local new_rules
-- do stuff with rules producing a new rule set ---
return new_rules
end)
You can chain an unlimited number of these passes together. Examples of
writing compiler passes can be found in lvera/passes. Compiler passes
are expected to return the format shown in Lvera.parse_string(string).
compiler:build()
This function will apply all your compiler passes over your rule set. If there are no compiler passes, this is effectively a no-op.
Built-in Rules / Ports
Lvera provides a compiler pass that allows you to generate an external API for your program. This API can then be filled in from the Lua side. Ports are configured directly from Nova using the following format:
|#port, port name,
, needs, @condition #1, @condition #2
, takes, @argument #1, @argument #2
, reads, @argument #3, @argument #4 |
, @return value #1, @return value #2
The #port is used to flag this rule as metadata for passes.ports. The
section after that is the name of the port. Lvera will convert spaces to
_. In the future, this name managling process will be customizable, but
currently it's hard coded.
For the lua code generator, the above annotation will generate the following code inside the module it creates:
if counters["@condition #1"] > 0 and counters["@condition #2"] > 0 then
self.port_name(counters, counters["@argument #1"], counters["@argument #2"], counters["@argument #3"], counters["@argument #4"])
counters["@condition #1"] = 0
counters["@condition #2"] = 0
counters["@argument #1"] = 0
counters["@argument #2"] = 0
end
You can define the body of a port using the #port body rule:
|#port body, port name, lua| [%
counters["@return value #1"] = math.floor(arg_1 + arg_2 / 2)
counters["@return value #2"] = math.floor(arg_3 + arg_4 / 2)
%]
needs
The needs keyword denotes the start of your condition list. Facts in this
list will be checked before you port executes. For the lua code generator,
they corresponed to the following:
if counters["@condition #1"] > 0 and counters["@condition #2"] > 0 then
-- snip --
counters["@condition #1"] = 0
counters["@condition #2"] = 0
-- snip --
end
Conditions are automatically cleared. Work to except counters from this is in progress.
takes
The takes keyword denotes the start of destructively read inputs. Facts in
this list will be passed to your port then cleared after their usage. For the
lua code generator, they corresponed to the following:
if --[[ omitted ]] then
self.port_name(counters, counters["@argument #1"], counters["@argument #2"], --[[ omitted ]])
counters["@argument #1"] = 0
counters["@argument #2"] = 0
end
reads
The reads keyword is like the takes keyword, except it does not destroy its
argument. For the lua code generator, they corresponed to the following:
if --[[ omitted ]] then
self.port_name(counters, --[[ omitted ]], counters["@argument #3"], counters["@argument #4"])
-- snip --
end
Returning values
The right hand side of a port annotation specifies which counters a port wants
to return there is no requirement for ports to actually return values to
these slots. Returning values from a port is done by writing to counters:
machine.port_name = function(counters, arg_1, arg_2, arg_3, arg_4)
counters["@return value #1"] = math.floor(arg_1 + arg_2 / 2)
counters["@return value #2"] = math.floor(arg_3 + arg_4 / 2)
end
Counters are expected to be "positive integer values". Setting counters to non-integers or negative values will have unexpected behavior. Consider saving those values as external state. Using ports to handle events and signal new ones.
Counter safety is currently uninforced. So, the following will crash your program:
machine.port_name = function(counters, arg_1, arg_2, arg_3, arg_4)
counters["@return value #1"] = "uh oh"
counters["@return value #2"] = {"not", "good"}
end