A LuaJIT implementation of Vera
Find a file
2025-02-02 00:47:59 +00:00
examples Huge update 2025-02-01 19:46:13 -05:00
lvera Huge update 2025-02-01 19:46:13 -05:00
sketches Huge update 2025-02-01 19:46:13 -05:00
.gitignore Refactoring Stuff 2024-12-22 23:44:43 -05:00
README.md Huge update 2025-02-01 19:46:13 -05:00

A lizard clings to a blue moon that says LVERA on it. The moon is a reference to the Lua programming language.

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