1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-27 16:49:31 +00:00
This commit is contained in:
Étienne Fildadut 2020-05-24 20:31:09 +02:00
parent 7a5a05ff34
commit b233d7fa1e
138 changed files with 4369 additions and 1611 deletions

193
README.md
View file

@ -1,193 +0,0 @@
Anselme quick reference
=======================
Anselme will read script files line per line, starting from the start of the file.
Every line can have children: a new line prefixed with a tabulation, or more if it's a children of a children, and so on.
Anselme will automatically read the top-level lines. Children reading will be decided by their parents.
Lines types and their properties
--------------------------------
* Lines starting with a character which isn't listed below are text. They will be said out loud. Text formatting apply. If the line ends with a \, the text will not be immediately sent to the engine (it will be sent along with the next text line encountered, concatenated).
Example: `Hello world!`
No children.
No variables.
* Lines starting with ( are comments.
Example: `(Important comment)`
Their children are never read nor parsed, so it can be used for multiline comments.
No variables.
* Lines starting with § are paragraphs. A paragraph can have parameters, between parantheses and seperated by commas. Parantheses can be ommited if there are no parameters. Missing parent paragraphs will be created.
Example: `§ the start of the adventure (hero name, size of socks collection)`
Their children are only read after a redirection to this paragraph.
Variables:
* 👁️: number of times the paragraph definition line has been encoutered before
* 🗨️: number of times the paragraph's children have been executed before
* Lines starting with > are choices. The play can choose between this choice and every immediately following choice line. Text formatting apply. If a choice ends with a \, the choice will not immediately be sent to the engine (it will be send along with the next choice encoutered, with all choices available).
Example:
```
> Yes.
Neat.
> No.
I'm sad now.
```
Its children will be read if the player select this choice.
No variables.
* Lines starting with : are variable definition. They will define and set to a specific value a currently undefined variable, which is searched in the closest paragraph only. Missing paragraphs will be created. They will always be run at compile time.
Example: `:(variable*2) variableSquared`
No children.
No variables.
* Lines starting with =, +, -, *, /, %, ^, !, &, | are variable assignements. They will change the value of a variable, searched as described in Variables. When asked to change the value of a paragraph, special behaviour may occur; see Aliases.
Example: `+1 life point`
No children.
No variables.
* Lines starting with ~ are redirections. They usually instruct the game to go to a specific paragraph (see Paragraph selection) and resume reading, but they will in practive evaluate any expression given to them. If the expression returns a paragraph, it will automatically be called (unless you redefine the ? operator). Redirections that immediately follow this one will only be read if this redirection failed (like a elseif). Expression default to true if not specified.
Example:
```
~ the start of the adventure ("John Pizzapone", 9821)
~ life point > 5
Life is good
~
NOT GOOD ENOUGH
```
Their children will be run only if the paragraph returns a truthy value.
No variables.
* Lines starting with @ are value return statements. They set the return value of the current paragraph.
Example: `@1+1`
No children.
No variables.
* Lines starting with # are tags marker. They will define tags for all text sent from their children. Name and value are expressions.
Example:
```
# "colour": "red", "big"
Hey.
```
"Hey" will be sent along with the tag table { colour = "red", "big" }.
Their children are always run.
No variables.
Line decorators
---------------
Every line can be suffixed with a `~` and a following condition; the line will only be run when the condition is verified.
Similarly, every line can be suffixed with a `#` and a list of tags that will be set for this line (won't affect its children). Tag decorators must be placed before condition decorators.
Lines can also be suffixed with a `§` and a name to behave like a paragraph (they will have variables, and can be redirected to).
Text formatting
---------------
Stuff inside braces `{ }` will be replaced with the associated expression content. If the expression returns a paragraph, it will automatically be called.
Tags
----
Tags can be specified using the `#` line or decorator. If the expression returns a list, all of its elements will be recursively extracted and the final list will be provided to the engine. Paragraphs in the list will be automatically evaluated. If pairs are present, they will be used as key-value pairs in the tags table.
Expressions
-----------
A formula. Available operators: `?` (thruth test), `&`, `|` (boolean and, or), `!` (boolean not), `+`, `-`, `*`, `/`, `//`, `%`, `^` (arithmetic), `>`, `<`, `>=`,`<=` (comparaison), `=`, `!=` (value (in)equality), `:` (pair), `,` (list).
Unusual operators:
* `?paragraph` will recursively evaluate the paragraph until a non-paragraph is found, and returns a boolean
* `-string` will reverse the string
* `string + string/number` will concatenate
* `string - number` will returns everything before/after the last/first number characters
* `string - string` will remove every string from the string
* `string * number` will repeat the string
* `string / number` will returns the last/first number characters
* `string/number % string/number` will returns the position of string in string if found, no if not found
* `string/number ^ boolean` will uppercase/lowercase the string
Paragraph can have custom binary operator behaviour by having a sub paragraph named like `_operator_` (eg, `_+_` for the + operator). The function will receive (left, right) as parameters. This does not apply to lazy operators (`&`, `|`), you can only change their behaviour by changing the behaviour of the truth test (var is true if and only if `?var = 1`), i.e., via redefining the `?` operator).
Similarly, unary operators can be redefined by using the name `-_`.
Assignement operators can be redefined using their name (eg, `=` for direct assignement or `+` for addition).
Parantheses can be used for priority management.
Anselme test the falsity of value by comparing it with `0`. Everything else is true, including the string `"0"`.
Variables can be used by writing their name. Straigthforward.
Variables
---------
Variables names can contain every character except `. { } § > < ( ) ~ + - * / % ^ = ! & | : ,` and space.
Value type:
* number: `0`, `1`, ...
* string: `"Text"`. Text formatting applies.
* pair: `name: value`
* list: `value1, value2, ...`
* paragraph: a reference to a paragraph
* luafunction: function defined by the engine
Variables need to be defined before use. Their type cannot be changed after definition.
The same rules as in Paragraph selection apply.
Functions
---------
Paragraphs can be used like functions. Use `(var1, var2)` to specify parameters in the paragraph definition. Theses variables will be set in the paragraph when it is called. Parantheses are not needed for functions without parameters.
When called in an expression, the paragraph will return a value that can be redefined using a `@` line. By default, the return value is the empty string.
Addresses
---------
The path to a paragraph, subparagraph or any variable is called an address.
Anselme will search for variables from the current indentation level up to the top-level.
You can select sub-variables using a space between the parent paragraph name and its children, and so on.
You can select sub-variables using expression by putting them between braces (will automatically evaluate paragraph). For example,
~ foo {"bar"}
will select foo bar.
When a sub-variables is not found directly, it will be searched in the parent's return values.
Engine defined functions
------------------------
Functions (same as paragraphs) can be defined by the game engine. These always will be searched first. See Anselme's public API on how to add them (at the end of this file).
Built-in functions:
* `↩️(destiation name, source name)` will set up an alias so when the name "source name" is used but not found, it will be replaced with "destination name"
Anselme's public interface is definied at the end of anselme.can.

11
TODO
View file

@ -1,11 +0,0 @@
TODO: test/check redirections consistency/coverage
TODO: merge new scripts with an old state
TODO: translation thing. Linked with script merging. Simplest solution (which does not imply adding uuids to every text line in every file) would be to use a mapping file, which maps every save-relevant variable to its name in a translation.
(TODO changer anselme pour les sauvegardes - j'ai une feuille dessus, mais iirc la bonne solution c'était de changer les variables pour référerer au dernier checkpoint (paragraph / choix / if) et de commit les données qu'aux checkponts (autorise changements de texte, mais à voir comment identifier uniquement les choix et ifs...))
(TODO: autoriser type de variables custom (par ex list): définir type et actions avec les opérateurs)
(genre ici un type inventory: :inventory() raquettes / +"raquette sans fil" raquettes) (utiliser probablement les opérateurs custom)
(TODO: méthodes ? genre string:gsub(truc) signifie gsub(string, truc) idk ou juste des méthodes comme Lua (mais engine-defined))
TODO: functions with default value for arguments / named parameters. Use : as name-value delimiter (like with tags)
TODO: list methods

File diff suppressed because it is too large Load diff

334
anselme.lua Normal file
View file

@ -0,0 +1,334 @@
-- anselme module
local anselme = {
-- version
version = "0.12.0",
--- currently running interpreter
running = nil
}
package.loaded[...] = anselme
-- load libs
local preparse = require((...):gsub("anselme$", "parser.preparser"))
local postparse = require((...):gsub("anselme$", "parser.postparser"))
local expression = require((...):gsub("anselme$", "parser.expression"))
local eval = require((...):gsub("anselme$", "interpreter.expression"))
local run_line = require((...):gsub("anselme$", "interpreter.interpreter")).run_line
local to_lua = require((...):gsub("anselme$", "interpreter.common")).to_lua
local stdfuncs = require((...):gsub("anselme$", "stdlib.functions"))
-- wrappers for love.filesystem / luafilesystem
local function list_directory(path)
local t = {}
if love then
t = love.filesystem.getDirectoryItems(path)
else
local lfs = require("lfs")
for item in lfs.dir(path) do
table.insert(t, path.."/"..item)
end
end
return t
end
local function is_directory(path)
if love then
return not not love.filesystem.getInfo(path, "directory")
else
local lfs = require("lfs")
return lfs.attributes(path, "mode") == "directory"
end
end
--- interpreter methods
local interpreter_methods = {
-- VM this interpreter belongs to
vm = nil,
--- run the VM until the next event
-- returns event, data; if event is "return" or "error", the interpreter must not be stepped further
step = function(self)
-- check status
if coroutine.status(self.state.interpreter.coroutine) ~= "suspended" then
return "error", ("can't step interpreter because it has already finished or is already running (coroutine status: %s)"):format(coroutine.status(self.state.interpreter.coroutine))
end
-- handle interrupt
if self.state.interpreter.interrupt then
local expr = self.state.interpreter.interrupt
if expr == true then
return "return", "" -- nothing to do after interrupt
else
local line = self.state.interpreter.running_line
local namespace = self:current_namespace()
-- replace state with interrupted state
local exp, err = expression(expr, self.state.interpreter.global_state, namespace or "")
if not exp then return "error", ("%s; during interrupt %q at line %s"):format(err, expr, line and line.line or 0) end
local r, e = self.vm:run(exp)
if not r then return "error", e end
self.state = r.state
end
end
-- run
local previous = anselme.running
anselme.running = self
local success, event, data = coroutine.resume(self.state.interpreter.coroutine)
anselme.running = previous
if not success then return "error", event end
return event, data
end,
--- select an answer
-- returns self
choose = function(self, i)
self.state.interpreter.choice_selected = tonumber(i)
return self
end,
--- interrupt the vm on the next step, executing an expression is specified
-- returns self
interrupt = function(self, expr)
self.state.interpreter.interrupt = expr or true
return self
end,
--- search closest namespace from last run line
current_namespace = function(self)
local line = self.state.interpreter.running_line
local namespace = ""
if line then
local cur_line = line
namespace = cur_line.namespace
while not namespace do
local block = cur_line.parent_block
if not block.parent_line then break end -- reached root
cur_line = block.parent_line
namespace = cur_line.namespace
end
end
return namespace
end,
--- run an expression: may trigger events and must be called from within the interpreter coroutine
-- return lua value
run = function(self, expr, namespace)
-- parse
local err
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state.interpreter.global_state, namespace or "") end
if not expr then coroutine.yield("error", err) end
-- run
local r, e = eval(self.state, expr)
if not r then coroutine.yield("error", e) end
if self.state.interpreter.event_buffer then -- flush final events
local rf, re = run_line(self.state, { type = "flush_events" })
if re then coroutine.yield("error", re) end
if rf then r = rf end
end
return to_lua(r)
end,
--- evaluate an expression
-- return value in case of success
-- return nil, err in case of error
eval = function(self, expr, namespace)
-- parse
local err
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state.interpreter.global_state, namespace or "") end
if not expr then return nil, err end
-- run
local co = coroutine.create(function()
local r, e = eval(self.state, expr)
if not r then return "error", e end
return "return", to_lua(r)
end)
local previous = anselme.running
anselme.running = self
local success, event, data = coroutine.resume(co)
anselme.running = previous
if not success then
return nil, event
elseif event ~= "return" then
return nil, ("evaluated expression generated an %q event"):format(event)
else
return data
end
end,
}
interpreter_methods.__index = interpreter_methods
--- vm methods
local vm_mt = {
--- load code
-- return self in case of success
-- returns nil, err in case of error
loadstring = function(self, str, name)
local s, e = preparse(self.state, str, name or "")
if not s then return s, e end
return self
end,
loadfile = function(self, path, name)
local f, e = io.open(path, "r")
if not f then return f, e end
local s, err = self:loadstring(f:read("*a"), name or "")
f:close()
if not s then return s, err end
return self
end,
loaddirectory = function(self, path, name)
if not name then name = "" end
name = name == "" and "" or name.."."
for _, item in ipairs(list_directory(path)) do
if item:match("[^%.]") then
local p = path.."/"..item
local s, e
if is_directory(p) then
s, e = self:loaddirectory(p, name..item)
elseif item:match("%.ans$") then
s, e = self:loadfile(p, name..item:gsub("%.ans$", ""))
end
if not s then return s, e end
end
end
return self
end,
--- set aliases
-- return self
loadalias = function(self, name, dest)
if type(name) == "table" then
for k, v in pairs(name) do
self:loadalias(k, v)
end
else
self.state.aliases[name] = dest
end
return self
end,
--- define functions
-- return self
loadfunction = function(self, name, fn)
if type(name) == "table" then
for k, v in pairs(name) do
if type(v) == "table" then
for _, variant in ipairs(v) do
self:loadfunction(k, variant)
end
else
self:loadfunction(k, v)
end
end
else
if not self.state.functions[name] then
self.state.functions[name] = {}
end
if type(fn) == "function" then
local info = debug.getinfo(fn)
table.insert(self.state.functions[name], {
arity = info.isvararg and {info.nparams, math.huge} or info.nparams,
value = fn
})
else
table.insert(self.state.functions[name], fn)
end
end
return self
end,
--- save/load
load = function(self, data)
assert(data.anselme_version == anselme.version, ("trying to load a save from Anselme %s but current Anselme version is %s"):format(data.anselme_version, anselme.version))
for k, v in pairs(data.variables) do
self.state.variables[k] = v
end
return self
end,
save = function(self)
return {
anselme_version = anselme.version,
variables = self.state.variables
}
end,
--- run code
-- return interpreter in case of success
-- returns nil, err in case of error
run = function(self, expr, namespace, tags)
if #self.state.queued_lines > 0 then
local r, e = postparse(self.state)
if not r then return r, e end
end
--
local err
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state, namespace or "") end
if not expr then return expr, err end
-- interpreter state
local interpreter
interpreter = {
state = {
aliases = self.state.aliases,
functions = self.state.functions,
variables = setmetatable({}, { __index = self.state.variables }),
interpreter = {
global_state = self.state,
coroutine = coroutine.create(function() return "return", interpreter:run(expr, namespace) end),
-- events
event_type = nil,
event_buffer = nil,
-- status
running_line = nil,
-- choice
choice_selected = nil,
choice_available = {},
-- interrupt
interrupt = nil,
-- conditions
last_condition_success = nil,
-- tags
tags = tags or {},
}
},
vm = self
}
return setmetatable(interpreter, interpreter_methods)
end,
--- eval code
-- return value in case of success
-- returns nil, err in case of error
eval = function(self, expr, namespace, tags)
local interpreter, err = self:run("@", namespace, tags)
if not interpreter then return interpreter, err end
return interpreter:eval(expr, namespace)
end
}
vm_mt.__index = vm_mt
--- anselme module
return setmetatable(anselme, {
__call = function()
-- global state
local state = {
aliases = {
-- seen = "👁️",
-- checkpoint = "🏁"
},
functions = {
-- [":="] = {
-- {
-- arity = {3,42}, type = { [1] = "variable" }, check = function, rewrite = function, vararg = 2, mode = "custom",
-- value = function(state, exp)
-- end -- or paragraph, function, line
-- }
-- },
},
variables = {
-- foo = {
-- type = "number",
-- value = 42
-- },
},
queued_lines = {
-- { line = line, namespace = "foo" },
}
}
local vm = setmetatable({ state = state }, vm_mt)
vm:loadfunction(stdfuncs)
return vm
end
})

1
init.lua Normal file
View file

@ -0,0 +1 @@
return require((...)..".anselme")

74
interpreter/common.lua Normal file
View file

@ -0,0 +1,74 @@
local atypes, ltypes
local eval
local common
common = {
-- flush interpreter state to global state
flush_state = function(state)
local global_vars = state.interpreter.global_state.variables
for var, value in pairs(state.variables) do
global_vars[var] = value
end
end,
-- check truthyness of an anselme value
truthy = function(val)
if val.type == "number" then
return val.value ~= 0
elseif val.type == "nil" then
return false
else
return true
end
end,
-- str: if success
-- * nil, err: if error
format = function(val)
if atypes[val.type] and atypes[val.type].format then
return atypes[val.type].format(val.value)
else
return nil, ("no formatter for type %q"):format(val.type)
end
end,
-- lua value: if success
-- * nil, err: if error
to_lua = function(val)
if atypes[val.type] and atypes[val.type].to_lua then
return atypes[val.type].to_lua(val.value)
else
return nil, ("no Lua exporter for type %q"):format(val.type)
end
end,
-- anselme value: if success
-- * nil, err: if error
from_lua = function(val)
if ltypes[type(val)] and ltypes[type(val)].to_anselme then
return ltypes[type(val)].to_anselme(val)
else
return nil, ("no Lua importer for type %q"):format(type(val))
end
end,
-- string: if success
-- * nil, err: if error
eval_text = function(state, text)
local s = ""
for _, item in ipairs(text) do
if type(item) == "string" then
s = s .. item
else
local v, e = eval(state, item)
if not v then return v, e end
v, e = common.format(v)
if not v then return v, e end
s = s .. v
end
end
return s
end
}
package.loaded[...] = common
local types = require((...):gsub("interpreter%.common$", "stdlib.types"))
atypes, ltypes = types.anselme, types.lua
eval = require((...):gsub("common$", "expression"))
return common

184
interpreter/expression.lua Normal file
View file

@ -0,0 +1,184 @@
local expression
local flush_state, to_lua, from_lua, eval_text
local run, run_block
--- evaluate an expression
-- returns evaluated value if success
-- returns nil, error if error
local function eval(state, exp)
-- number
if exp.type == "number" then
return {
type = "number",
value = exp.value
}
-- string
elseif exp.type == "string" then
local t, e = eval_text(state, exp.value)
if not t then return t, e end
return {
type = "string",
value = t
}
-- parentheses
elseif exp.type == "parentheses" then
return eval(state, exp.expression)
-- list parentheses
elseif exp.type == "list_parentheses" then
if exp.expression then
local v, e = eval(state, exp.expression)
if not v then return v, e end
if v.type == "list" then
return v
else
return {
type = "list",
value = { v }
}
end
else
return {
type = "list",
value = {}
}
end
-- variable
elseif exp.type == "variable" then
return state.variables[exp.name]
-- list
elseif exp.type == "list" then
local l = {}
local ast = exp
while ast.type == "list" do
local left, lefte = eval(state, ast.left)
if not left then return left, lefte end
table.insert(l, left)
ast = ast.right
end
local right, righte = eval(state, ast)
if not right then return right, righte end
table.insert(l, right)
return {
type = "list",
value = l
}
-- function
elseif exp.type == "function" then
local fn = exp.variant
-- custom lua functions
if fn.mode == "custom" then
return fn.value(state, exp)
else
-- eval args: same as list, but only put vararg arguments in a separate list
local l = {}
if exp.argument then
local vararg = fn.vararg or math.huge
local i, ast = 1, exp.argument
while ast.type == "list" and i < vararg do
local left, lefte = eval(state, ast.left)
if not left then return left, lefte end
table.insert(l, left)
ast = ast.right
i = i + 1
end
local right, righte = eval(state, ast)
if not right then return right, righte end
table.insert(l, right)
end
if fn.vararg and #l < fn.vararg then -- empty list vararg
table.insert(l, { type = "list", value = {} })
end
-- anselme function
if type(fn.value) == "table" then
-- paragraph & paragraph decorator
if fn.value.type == "paragraph" or fn.value.paragraph then
local r, e
if fn.value.type == "paragraph" then
r, e = run_block(state, fn.value.child, false)
if e then return r, e end
state.variables[fn.value.namespace.."👁️"] = {
type = "number",
value = state.variables[fn.value.namespace.."👁️"].value + 1
}
state.variables[fn.value.parent_function.namespace.."🏁"] = {
type = "string",
value = fn.value.name
}
flush_state(state)
if r then
return r, e
elseif not exp.explicit_call then
r, e = run(state, fn.value.parent_block, true, fn.value.parent_position+1)
else
r = { type = "nil", value = nil }
end
elseif exp.explicit_call then
r, e = run(state, fn.value.parent_block, false, fn.value.parent_position, fn.value.parent_position)
else
r, e = run(state, fn.value.parent_block, true, fn.value.parent_position)
end
if not r then return r, e end
return r
-- function
elseif fn.value.type == "function" then
-- set args
for j, param in ipairs(fn.value.params) do
state.variables[param] = l[j]
end
-- eval function
local r, e
if exp.explicit_call or state.variables[fn.value.namespace.."🏁"].value == "" then
r, e = run(state, fn.value.child)
-- resume at last paragraph
else
local expr, err = expression(state.variables[fn.value.namespace.."🏁"].value, state, "")
if not expr then return expr, err end
r, e = eval(state, expr)
end
if not r then return r, e end
state.variables[fn.value.namespace.."👁️"] = {
type = "number",
value = state.variables[fn.value.namespace.."👁️"].value + 1
}
flush_state(state)
return r
else
return nil, ("unknown function type %q"):format(fn.value.type)
end
-- lua functions
else
if fn.mode == "raw" then
return fn.value(unpack(l))
else
local l_lua = {}
for _, v in ipairs(l) do
table.insert(l_lua, to_lua(v))
end
local r, e
if _VERSION == "Lua 5.1" and not jit then -- PUC Lua 5.1 doesn't allow yield from a pcall
r, e = true, fn.value(unpack(l_lua))
else
r, e = pcall(fn.value, unpack(l_lua)) -- pcall to produce a more informative error message (instead of full coroutine crash)
end
if r then
return from_lua(e)
else
return nil, ("%s; in Lua function %q"):format(e, exp.name)
end
end
end
end
else
return nil, ("unknown expression %q"):format(tostring(exp.type))
end
end
package.loaded[...] = eval
run = require((...):gsub("expression$", "interpreter")).run
run_block = require((...):gsub("expression$", "interpreter")).run_block
expression = require((...):gsub("interpreter%.expression$", "parser.expression"))
local common = require((...):gsub("expression$", "common"))
flush_state, to_lua, from_lua, eval_text = common.flush_state, common.to_lua, common.from_lua, common.eval_text
return eval

196
interpreter/interpreter.lua Normal file
View file

@ -0,0 +1,196 @@
local eval
local truthy, flush_state, to_lua, eval_text
local function write_event(state, type, data)
if state.interpreter.event_buffer and state.interpreter.event_type ~= type then
error("previous event has not been flushed")
end
if not state.interpreter.event_buffer then
state.interpreter.event_type = type
state.interpreter.event_buffer = {}
end
table.insert(state.interpreter.event_buffer, { data = data, tags = state.interpreter.tags[#state.interpreter.tags] or {} })
end
local tags = {
push = function(self, state, val)
local new = {}
-- copy
local last = state.interpreter.tags[#state.interpreter.tags] or {}
for k,v in pairs(last) do new[k] = v end
-- merge with new values
if val.type ~= "list" then val = { type = "list", value = { val } } end
for k, v in pairs(to_lua(val)) do new[k] = v end
-- add
table.insert(state.interpreter.tags, new)
end,
pop = function(self, state)
table.remove(state.interpreter.tags)
end
}
local run_block
-- returns var in case of success and there is a return
-- return nil in case of success and there is no return
-- return nil, err in case of error
local function run_line(state, line)
-- store line
state.interpreter.running_line = line
-- condition decorator
local skipped = false
if line.condition then
local v, e = eval(state, line.condition)
if not v then return v, ("%s; at line %s"):format(e, line.line) end
skipped = not truthy(v)
end
if not skipped then
-- tag decorator
if line.tag then
local v, e = eval(state, line.tag)
if not v then return v, ("%s; in tag decorator at line %s"):format(e, line.line) end
tags:push(state, v)
end
-- line types
if line.type == "condition" then
state.interpreter.last_condition_success = nil
local v, e = eval(state, line.expression)
if not v then return v, ("%s; at line %s"):format(e, line.line) end
if truthy(v) then
state.interpreter.last_condition_success = true
v, e = run_block(state, line.child)
if e then return v, e end
if v then return v end
end
elseif line.type == "else-condition" then
if not state.interpreter.last_condition_success then
local v, e = eval(state, line.expression)
if not v then return v, ("%s; at line %s"):format(e, line.line) end
if truthy(v) then
state.interpreter.last_condition_success = true
v, e = run_block(state, line.child)
if e then return v, e end
if v then return v end
end
end
elseif line.type == "choice" then
local t, er = eval_text(state, line.text)
if not t then return t, er end
table.insert(state.interpreter.choice_available, line.child)
write_event(state, "choice", t)
elseif line.type == "tag" then
if line.expression then
local v, e = eval(state, line.expression)
if not v then return v, ("%s; at line %s"):format(e, line.line) end
tags:push(state, v)
end
local v, e = run_block(state, line.child)
if line.expression then tags:pop(state) end
if e then return v, e end
if v then return v end
elseif line.type == "return" then
local v, e
if line.expression then
v, e = eval(state, line.expression)
if not v then return v, ("%s; at line %s"):format(e, line.line) end
end
return v
elseif line.type == "text" then
local t, er = eval_text(state, line.text)
if not t then return t, ("%s; at line %s"):format(er, line.line) end
write_event(state, "text", t)
elseif line.type == "flush_events" then
while state.interpreter.event_buffer do
local type, buffer = state.interpreter.event_type, state.interpreter.event_buffer
state.interpreter.event_type = nil
state.interpreter.event_buffer = nil
-- yield
coroutine.yield(type, buffer)
-- run choice
if type == "choice" then
local sel = state.interpreter.choice_selected
state.interpreter.choice_selected = nil
if not sel or sel < 1 or sel > #state.interpreter.choice_available then
return nil, "invalid choice"
else
local choice = state.interpreter.choice_available[sel]
state.interpreter.choice_available = {}
local v, e = run_block(state, choice)
if e then return v, e end
if v then return v end
end
end
end
elseif line.type ~= "paragraph" then
return nil, ("unknown line type %q; line %s"):format(line.type, line.line)
end
-- tag decorator
if line.tag then
tags:pop(state)
end
-- paragraph decorator
if line.paragraph then
state.variables[line.namespace.."👁️"] = {
type = "number",
value = state.variables[line.namespace.."👁️"].value + 1
}
state.variables[line.parent_function.namespace.."🏁"] = {
type = "string",
value = line.name
}
flush_state(state)
end
end
end
-- returns var in case of success and there is a return
-- return nil in case of success and there is no return
-- return nil, err in case of error
run_block = function(state, block, run_whole_function, i, j)
i = i or 1
local len = math.min(#block, j or math.huge)
while i <= len do
local v, e = run_line(state, block[i])
if e then return v, e end
if v then return v end
i = i + 1
end
-- go up hierarchy if asked to run the whole function
if run_whole_function and block.parent_line and block.parent_line.type ~= "function" then
local parent_line = block.parent_line
local v, e = run_block(state, parent_line.parent_block, run_whole_function, parent_line.parent_position+1)
if e then return v, e end
if v then return v, e end
end
return nil
end
-- returns var in case of success
-- return nil, err in case of error
local function run(state, block, run_whole_function, i, j)
-- run
local v, e = run_block(state, block, run_whole_function, i, j)
if e then return v, e end
if v then
return v
else
-- default no return value
return {
type = "nil",
value = nil
}
end
end
local interpreter = {
run = run,
run_block = run_block,
run_line = run_line
}
package.loaded[...] = interpreter
eval = require((...):gsub("interpreter$", "expression"))
local common = require((...):gsub("interpreter$", "common"))
truthy, flush_state, to_lua, eval_text = common.truthy, common.flush_state, common.to_lua, common.eval_text
return interpreter

153
parser/common.lua Normal file
View file

@ -0,0 +1,153 @@
local expression
local escapeCache = {}
local common
common = {
--- valid identifier pattern
identifier_pattern = "[^%%%/%*%+%-%(%)%!%&%|%=%$%?%>%<%:%{%}%[%]%,]+",
--- escape a string to be used as an exact match pattern
escape = function(str)
if not escapeCache[str] then
escapeCache[str] = str:gsub("[^%w]", "%%%1")
end
return escapeCache[str]
end,
--- trim a string
trim = function(str)
return str:match("^%s*(.-)%s*$")
end,
--- split a string separated by .
split = function(str)
local address = {}
for name in str:gmatch("[^%.]+") do
table.insert(address, name)
end
return address
end,
--- find a variable/function in a list, going up through the namespace hierarchy
find = function(list, namespace, name)
local ns = common.split(namespace)
for i=#ns, 1, -1 do
local fqm = ("%s.%s"):format(table.concat(ns, ".", 1, i), name)
if list[fqm] then
return list[fqm], fqm
end
end
if list[name] then
return list[name], name
end
return nil, ("can't find %q in namespace %s"):format(name, namespace)
end,
--- transform an identifier into a clean version
format_identifier = function(identifier, state)
local r = identifier:gsub("[^%.]+", function(str)
str = common.trim(str)
return state.aliases[str] or str
end)
return r
end,
--- flatten a nested list expression into a list of expressions
flatten_list = function(list, t)
t = t or {}
if list.type == "list" then
table.insert(t, list.left)
common.flatten_list(list.right, t)
else
table.insert(t, list)
end
return t
end,
-- * list of strings and expressions
-- * nil, err: in case of error
parse_text = function(text, state, namespace)
local l = {}
while text:match("[^%{]+") do
local t, e = text:match("^([^%{]*)(.-)$")
-- text
if t ~= "" then table.insert(l, t) end
-- expr
if e:match("^{") then
local exp, rem = expression(e:gsub("^{", ""), state, namespace)
if not exp then return nil, rem end
if not rem:match("^%s*}") then return nil, ("expected closing } at end of expression before %q"):format(rem) end
table.insert(l, exp)
text = rem:match("^%s*}(.*)$")
else
break
end
end
return l
end,
-- find compatible function variant
-- * variant: if success
-- * nil, err: if error
find_function_variant = function(fqm, state, arg, explicit_call)
local err = ("function %q variant not found"):format(fqm)
local func = state.functions[fqm] or {}
local args = arg and common.flatten_list(arg) or {}
for _, variant in ipairs(func) do
local ok = true
local return_type = variant.return_type
if variant.arity then
local min, max
if type(variant.arity) == "table" then
min, max = variant.arity[1], variant.arity[2]
else
min, max = variant.arity, variant.arity
end
if #args < min or #args > max then
if min == max then
err = ("function %q expected %s arguments but received %s"):format(fqm, min, #args)
else
err = ("function %q expected between %s and %s arguments but received %s"):format(fqm, min, max, #args)
end
ok = false
end
end
if ok and variant.check then
local s, e = variant.check(state, args)
if not s then
err = e or ("function %q variant failed to check arguments"):format(fqm)
ok = false
end
return_type = s == true and return_type or s
end
if ok and variant.types then
for j, t in pairs(variant.types) do
if args[j] and args[j].return_type and args[j].return_type ~= t then
err = ("function %q expected a %s as argument %s but received a %s"):format(fqm, t, j, args[j].return_type)
ok = false
end
end
end
if ok then
if variant.rewrite then
local r, e = variant.rewrite(fqm, state, arg, explicit_call)
if not r then
err = e
ok = false
end
if ok then
return r
end
else
return {
type = "function",
return_type = return_type,
name = fqm,
explicit_call = explicit_call,
variant = variant,
argument = arg
}
end
end
end
return nil, err
end
}
package.loaded[...] = common
expression = require((...):gsub("common$", "expression"))
return common

247
parser/expression.lua Normal file
View file

@ -0,0 +1,247 @@
local identifier_pattern, format_identifier, find, escape, find_function_variant, parse_text
--- binop priority
local binops_prio = {
[1] = { ";" },
[2] = { ":=", "+=", "-=", "//=", "/=", "*=", "%=", "^=" },
[3] = { "," },
[4] = { "|", "&" },
[5] = { "!=", "=", ">=", "<=", "<", ">" },
[6] = { "+", "-" },
[7] = { "*", "//", "/", "%" },
[8] = {}, -- unary operators
[9] = { "^", ":" },
[10] = { "." }
}
-- unop priority
local unops_prio = {
[1] = {},
[2] = {},
[3] = {},
[4] = {},
[5] = {},
[6] = {},
[7] = {},
[8] = { "-", "!" },
[9] = {}
}
--- parse an expression
-- return expr, remaining if success
-- returns nil, err if error
local function expression(s, state, namespace, currentPriority, operatingOn)
s = s:match("^%s*(.*)$")
currentPriority = currentPriority or 0
if not operatingOn then
-- number
if s:match("^%d+%.%d*") or s:match("^%d*%.%d+") or s:match("^%d+") then
local d, r = s:match("^(%d*%.%d*)(.*)$")
if not d then
d, r = s:match("^(%d+)(.*)$")
end
return expression(r, state, namespace, currentPriority, {
type = "number",
return_type = "number",
value = tonumber(d)
})
-- string
elseif s:match("^%\"[^\"]*%\"") then
local d, r = s:match("^%\"([^\"]*)%\"(.*)$")
while d:match("\\$") and not d:match("\\\\$") do
local nd, nr = r:match("([^\"]*)%\"(.*)$")
if not nd then return nil, ("unfinished string near %q"):format(r) end
d, r = d:sub(1, -2) .. "\"" .. nd, nr
end
local l, e = parse_text(tostring(d), state, namespace)
if not l then return l, e end
return expression(r, state, namespace, currentPriority, {
type = "string",
return_type = "string",
value = l
})
-- paranthesis
elseif s:match("^%b()") then
local content, r = s:match("^(%b())(.*)$")
content = content:gsub("^%(", ""):gsub("%)$", "")
local exp, r_paren = expression(content, state, namespace)
if not exp then return nil, "invalid expression inside parentheses: "..r_paren end
if r_paren:match("[^%s]") then return nil, ("unexpected %q at end of parenthesis expression"):format(r_paren) end
return expression(r, state, namespace, currentPriority, {
type = "parentheses",
return_type = exp.return_type,
expression = exp
})
-- list parenthesis
elseif s:match("^%b[]") then
local content, r = s:match("^(%b[])(.*)$")
content = content:gsub("^%[", ""):gsub("%]$", "")
local exp
if content:match("[^%s]") then
local r_paren
exp, r_paren = expression(content, state, namespace)
if not exp then return nil, "invalid expression inside list parentheses: "..r_paren end
if r_paren:match("[^%s]") then return nil, ("unexpected %q at end of list parenthesis expression"):format(r_paren) end
end
return expression(r, state, namespace, currentPriority, {
type = "list_parentheses",
return_type = "list",
expression = exp
})
-- identifier
elseif s:match("^"..identifier_pattern) then
local name, r = s:match("^("..identifier_pattern..")(.-)$")
name = format_identifier(name, state)
-- functions
local funcs, ffqm = find(state.functions, namespace, name)
if funcs then
local args, explicit_call
if r:match("^%b()") then
explicit_call = true
local content, rem = r:match("^(%b())(.*)$")
content = content:gsub("^%(", ""):gsub("%)$", "")
r = rem
-- get arguments
if content:match("[^%s]") then
local err
args, err = expression(content, state, namespace)
if not args then return args, err end
end
end
-- find compatible variant
local variant, err = find_function_variant(ffqm, state, args, explicit_call)
if not variant then return variant, err end
return expression(r, state, namespace, currentPriority, variant)
end
-- variables
local var, vfqm = find(state.variables, namespace, name)
if var then
return expression(r, state, namespace, currentPriority, {
type = "variable",
return_type = var.type ~= "undefined argument" and var.type or nil,
name = vfqm
})
end
-- suffix call: detect if prefix is valid variable, suffix call is handled in the binop section below
local sname, suffix = name:match("^(.*)(%."..identifier_pattern..")$")
if sname then
local svar, svfqm = find(state.variables, namespace, sname)
if svar then
return expression(suffix..r, state, namespace, currentPriority, {
type = "variable",
return_type = svar.type ~= "undefined argument" and svar.type or nil,
name = svfqm
})
end
end
return nil, ("unknown identifier %q"):format(name)
end
-- unops
for prio, oplist in ipairs(unops_prio) do
for _, op in ipairs(oplist) do
local escaped = escape(op)
if s:match("^"..escaped) then
local right, r = expression(s:match("^"..escaped.."(.*)$"), state, namespace, prio)
if not right then return nil, ("invalid expression after unop %q: %s"):format(op, r) end
-- find variant
local variant, err = find_function_variant(op, state, right, true)
if not variant then return variant, err end
return expression(r, state, namespace, currentPriority, variant)
end
end
end
return nil, ("no valid expression before %q"):format(s)
else
-- binop
for prio, oplist in ipairs(binops_prio) do
if prio >= currentPriority then
for _, op in ipairs(oplist) do
local escaped = escape(op)
if s:match("^"..escaped) then
local sright = s:match("^"..escaped.."(.*)$")
-- suffix call
if op == "." and sright:match("^"..identifier_pattern) then
local name, r = sright:match("^("..identifier_pattern..")(.-)$")
name = format_identifier(name, state)
local funcs, ffqm = find(state.functions, namespace, name)
if funcs then
local args, explicit_call
if r:match("^%b()") then
explicit_call = true
local content, rem = r:match("^(%b())(.*)$")
content = content:gsub("^%(", ""):gsub("%)$", "")
r = rem
-- get arguments
if content:match("[^%s]") then
local err
args, err = expression(content, state, namespace)
if not args then return args, err end
end
end
-- add first argument
if not args then
args = operatingOn
else
args = {
type = "list",
return_type = "list",
left = operatingOn,
right = args
}
end
-- find compatible variant
local variant, err = find_function_variant(ffqm, state, args, explicit_call)
if not variant then return variant, err end
return expression(r, state, namespace, currentPriority, variant)
end
else
local right, r = expression(sright, state, namespace, prio)
if not right then return nil, ("invalid expression after binop %q: %s"):format(op, r) end
-- list constructor
if op == "," then
return expression(r, state, namespace, currentPriority, {
type = "list",
return_type = "list",
left = operatingOn,
right = right
})
-- normal binop
else
-- find variant
local args = {
type = "list",
return_type = "list",
left = operatingOn,
-- wrap in parentheses to avoid appending to argument list if right is a list
right = { type = "parentheses", return_type = right.return_type, expression = right }
}
local variant, err = find_function_variant(op, state, args, true)
if not variant then return variant, err end
return expression(r, state, namespace, currentPriority, variant)
end
end
end
end
end
end
-- index
if s:match("^%b()") then
local content, r = s:match("^(%b())(.*)$")
-- get arguments (parentheses are kept)
local right, r_paren = expression(content, state, namespace)
if not right then return right, r_paren end
if r_paren:match("[^%s]") then return nil, ("unexpected %q at end of index expression"):format(r_paren) end
local args = { type = "list", left = operatingOn, right = right }
local variant, err = find_function_variant("(", state, args, true)
if not variant then return variant, err end
return expression(r, state, namespace, currentPriority, variant)
end
-- nothing to operate
return operatingOn, s
end
end
package.loaded[...] = expression
local common = require((...):gsub("expression$", "common"))
identifier_pattern, format_identifier, find, escape, find_function_variant, parse_text = common.identifier_pattern, common.format_identifier, common.find, common.escape, common.find_function_variant, common.parse_text
return expression

70
parser/postparser.lua Normal file
View file

@ -0,0 +1,70 @@
local expression
local parse_text
-- * true: if success
-- * nil, error: in case of error
local function parse(state)
for _, l in ipairs(state.queued_lines) do
local line, namespace = l.line, l.namespace
-- decorators
if line.condition then
if line.condition:match("[^%s]") then
local exp, rem = expression(line.condition, state, namespace)
if not exp then return nil, ("%s; at line %s"):format(rem, line.line) end
if rem:match("[^%s]") then return nil, ("expected end of expression before %q in condition decorator; at line %s"):format(rem, line.line) end
line.condition = exp
else
line.condition = nil
end
end
if line.tag then
if line.tag:match("[^%s]") then
local exp, rem = expression(line.tag, state, namespace)
if not exp then return nil, ("%s; at line %s"):format(rem, line.line) end
if rem:match("[^%s]") then return nil, ("expected end of expression before %q in condition decorator; at line %s"):format(rem, line.line) end
line.tag = exp
else
line.tag = nil
end
end
-- expressions
if line.expression then
if line.expression:match("[^%s]") then
local exp, rem = expression(line.expression, state, namespace)
if not exp then return nil, ("%s; at line %s"):format(rem, line.line) end
if rem:match("[^%s]") then return nil, ("expected end of expression before %q; at line %s"):format(rem, line.line) end
line.expression = exp
else
line.expression = nil
end
-- function return type information
if line.type == "return" then
local variant = line.parent_function.variant
local return_type = line.expression.return_type
if return_type then
if not variant.return_type then
variant.return_type = return_type
elseif variant.return_type ~= return_type then
return nil, ("trying to return a %s in a function that returns a %s; at line %s"):format(return_type, variant.return_type, line.line)
end
end
end
end
-- text
if line.text then
local txt, err = parse_text(line.text, state, namespace)
if err then return nil, ("%s; at line %s"):format(err, line.line) end
line.text = txt
end
end
state.queued_lines = {}
return true
end
package.loaded[...] = parse
expression = require((...):gsub("postparser$", "expression"))
local common = require((...):gsub("postparser$", "common"))
parse_text = common.parse_text
--- postparse shit: parse expressions and do variable existence and type checking
return parse

332
parser/preparser.lua Normal file
View file

@ -0,0 +1,332 @@
local expression
local format_identifier, identifier_pattern
local eval
-- * ast: if success
-- * nil, error: in case of error
local function parse_line(line, state, namespace)
local l = line.content
local r = {
line = line.line
}
-- comment
if l:match("^%(") then
r.type = "comment"
r.remove_from_block_ast = true
return r
end
-- decorators
while l:match("^..+[~#]") or l:match("^..+§") do
-- condition
if l:match("^..+%~.-$") then
local expr
l, expr = l:match("^(.-)%s*%~(.-)$")
r.condition = expr
-- paragraph
elseif l:match("^..+§.-$") then
local name
l, name = l:match("^(.-)%s*§(.-)$")
local fqm = ("%s%s"):format(namespace, format_identifier(name, state))
namespace = fqm.."."
r.paragraph = true
r.parent_function = true
r.namespace = fqm.."."
r.name = fqm
if not state.functions[fqm] then
state.functions[fqm] = {
{
arity = 0,
value = r
}
}
if not state.variables[fqm..".👁️"] then
state.variables[fqm..".👁️"] = {
type = "number",
value = 0
}
end
else
table.insert(state.functions[fqm], {
arity = 0,
value = r
})
end
-- tag
elseif l:match("^..+%#.-$") then
local expr
l, expr = l:match("^(.-)%s*%#(.-)$")
r.tag = expr
end
end
-- else-condition & condition
if l:match("^~~?") then
r.type = l:match("^~~") and "else-condition" or "condition"
r.child = true
local expr = l:match("^~~?(.*)$")
if expr:match("[^%s]") then
r.expression = expr
else
r.expression = "1"
end
-- choice
elseif l:match("^>") then
r.type = "choice"
r.push_event = "choice"
r.child = true
r.text = l:match("^>%s*(.-)$")
-- function & paragraph
elseif l:match("^%$") or l:match("") then -- § is a 2-bytes caracter, DO NOT USE LUA PATTERN OPERATORS as they operate on single bytes
r.type = l:match("^%$") and "function" or "paragraph"
r.child = true
local fqm = ("%s%s"):format(namespace, format_identifier(l:match("^%$(.*)$") or l:match("^§(.*)$"), state))
-- get params
r.params = {}
if r.type == "function" and fqm:match("%b()$") then
local content
fqm, content = fqm:match("^(.-)(%b())$")
content = content:gsub("^%(", ""):gsub("%)$", "")
for param in content:gmatch("[^%,]+") do
table.insert(r.params, format_identifier(("%s.%s"):format(fqm, param), state))
end
end
local arity, vararg = #r.params, nil
if arity > 0 and r.params[arity]:match("%.%.%.$") then -- varargs
r.params[arity] = r.params[arity]:match("^(.*)%.%.%.$")
vararg = arity
arity = { arity-1, math.huge }
end
-- store parent function and run paragraph when line is read
if r.type == "paragraph" then
r.paragraph = true
r.parent_function = true
end
-- don't keep function node in block AST
if r.type == "function" then
r.remove_from_block_ast = true
if not state.variables[fqm..".🏁"] then
state.variables[fqm..".🏁"] = {
type = "string",
value = ""
}
end
end
-- define function and variables
r.namespace = fqm.."."
r.name = fqm
if state.variables[fqm] then return nil, ("trying to define %s %s, but a variable with the same name exists; at line %s"):format(r.type, fqm, line.line) end
r.variant = {
arity = arity,
types = {},
vararg = vararg,
value = r
}
if not state.functions[fqm] then
state.functions[fqm] = { r.variant }
if not state.variables[fqm..".👁️"] then
state.variables[fqm..".👁️"] = {
type = "number",
value = 0
}
end
else
-- check for arity conflict
for _, variant in ipairs(state.functions[fqm]) do
local vmin, vmax = 0, math.huge
if type(variant.arity) == "table" then
vmin, vmax = variant.arity[1], variant.arity[2]
elseif variant.arity then
vmin, vmax = variant.arity, variant.arity
end
local min, max = 0, math.huge
if type(r.variant.arity) == "table" then
min, max = r.variant.arity[1], r.variant.arity[2]
elseif r.variant.arity then
min, max = variant.arity, r.variant.arity
end
if min == vmin and max == vmax then
return nil, ("trying to define %s %s with arity [%s;%s], but another function with the arity exist; at line %s"):format(r.type, fqm, min, max, line.line)
end
end
-- add
table.insert(state.functions[fqm], r.variant)
end
-- set type check information
for i, param in ipairs(r.params) do
if not state.variables[param] then
state.variables[param] = {
type = "undefined argument",
value = { r.variant, i }
}
elseif state.variables[param].type ~= "undefined argument" then
r.variant.types[i] = state.variables[param].type
end
end
-- definition
elseif l:match("^:") then
r.type = "definition"
r.remove_from_block_ast = true
local exp, rem = expression(l:match("^:(.*)$"), state, namespace) -- expression parsing is done directly to get type information
if not exp then return nil, ("%s; at line %s"):format(rem, line.line) end
local fqm = ("%s%s"):format(namespace, format_identifier(rem, state))
if state.functions[fqm] then return nil, ("trying to define variable %s, but a function with the same name exists; at line %s"):format(fqm, line.line) end
if not state.variables[fqm] or state.variables[fqm].type == "undefined argument" then
local v, e = eval(state, exp)
if not v then return v, e end
-- update function typecheck information
if state.variables[fqm] and state.variables[fqm].type == "undefined argument" then
local und = state.variables[fqm].value
und[1].types[und[2]] = v.type
end
state.variables[fqm] = v
elseif state.variables[fqm].type ~= exp.type then
return nil, ("trying to define variable %s of type %s but a it is already defined with type %s; at line %s"):format(fqm, exp.type, state.variables[fqm].type, line.line)
end
-- tag
elseif l:match("^%#") then
r.type = "tag"
r.child = true
r.expression = l:match("^%#(.*)$")
-- return
elseif l:match("^%@") then
r.type = "return"
r.parent_function = true
r.expression = l:match("^%@(.*)$")
-- text
elseif l:match("[^%s]") then
r.type = "text"
r.push_event = "text"
r.text = l
-- flush events
else
r.type = "flush_events"
end
if not r.type then return nil, ("unknown line %s type"):format(line.line) end
return r
end
-- * block: in case of success
-- * nil, err: in case of error
local function parse_block(indented, state, namespace, parent_function, last_event)
local block = { type = "block" }
local lastLine -- last line AST
for i, l in ipairs(indented) do
-- parsable line
if l.content then
local ast, err = parse_line(l, state, namespace)
if err then return nil, err end
lastLine = ast
-- store parent function
if ast.parent_function then ast.parent_function = parent_function end
-- add to block AST
if not ast.remove_from_block_ast then
ast.parent_block = block
-- insert flush on event type change
if ast.type == "flush" then last_event = nil end
if ast.push_event then
if last_event and ast.push_event ~= last_event then
table.insert(block, { line = l.line, type = "flush_events" })
end
last_event = ast.push_event
end
-- add ast node
ast.parent_position = #block+1
if ast.replace_with then
if indented[i+1].content then
table.insert(indented, i+1, { content = ast.replace_with, line = l.line })
else
table.insert(indented, i+2, { content = ast.replace_with, line = l.line }) -- if line has children
end
else
table.insert(block, ast)
end
end
-- add child
if ast.child then ast.child = { type = "block", parent_line = ast } end
-- queue in expression evalution
table.insert(state.queued_lines, { namespace = ast.namespace or namespace, line = ast })
-- indented (ignore block comments)
elseif lastLine.type ~= "comment" then
if not lastLine.child then
return nil, ("line %s (%s) can't have children"):format(lastLine.line, lastLine.type)
else
local r, e = parse_block(l, state, lastLine.namespace or namespace, lastLine.type == "function" and lastLine or parent_function, last_event)
if not r then return r, e end
r.parent_line = lastLine
lastLine.child = r
end
end
end
return block
end
--- returns the nested list of lines {content="", line=1}, grouped by indentation
-- multiple empty lines are merged
-- * list, last line
local function parse_indent(lines, i, indentLevel, insert_empty_line)
i = i or 1
indentLevel = indentLevel or 0
local indented = {}
while i <= #lines do
if lines[i]:match("[^%s]") then
local indent, line = lines[i]:match("^(%s*)(.*)$")
if #indent == indentLevel then
if insert_empty_line then
table.insert(indented, { content = "", line = insert_empty_line })
insert_empty_line = false
end
table.insert(indented, { content = line, line = i })
elseif #indent > indentLevel then
local t
t, i = parse_indent(lines, i, #indent, insert_empty_line)
table.insert(indented, t)
else
return indented, i-1
end
elseif not insert_empty_line then
insert_empty_line = i
end
i = i + 1
end
return indented, i-1
end
--- return the list of raw lines of s
local function parse_lines(s)
local lines = {}
for l in (s.."\n"):gmatch("(.-)\n") do
table.insert(lines, l)
end
return lines
end
--- preparse shit: create AST structure, define variables and functions, but don't parse expression or perform any type checking
-- (wait for other files to be parsed before doing this with postparse)
-- * state: in case of success
-- * nil, err: in case of error
local function parse(state, s, name)
-- parse lines
local lines = parse_lines(s)
local indented = parse_indent(lines)
-- wrap in named function if neccessary
if name ~= "" then
if not name:match("^"..identifier_pattern.."$") then
return nil, ("invalid function name %q"):format(name)
end
indented = {
{ content = "$ "..name, line = 0 },
indented
}
end
-- parse
local root, err = parse_block(indented, state, "")
if not root then return nil, err end
return state
end
package.loaded[...] = parse
expression = require((...):gsub("preparser$", "expression"))
local common = require((...):gsub("preparser$", "common"))
format_identifier, identifier_pattern = common.format_identifier, common.identifier_pattern
eval = require((...):gsub("parser%.preparser$", "interpreter.expression"))
return parse

37
run.lua
View file

@ -1,37 +0,0 @@
require("candran").setup()
local vm = require("anselme")()
vm:loaddirectory(".")
vm:loadfile("test.ans")
print(require("inspect")(vm.state))
while true do
local e, d = vm:step()
if e == "text" then
for _, t in ipairs(d) do
print(t.text)
for k,v in pairs(t.tags) do
print("> "..tostring(k)..": "..tostring(v))
end
end
print("-----")
elseif e == "choice" then
for i, c in ipairs(d) do
print(tostring(i)..": "..c.text)
for k,v in pairs(c.tags) do
print("> "..tostring(k)..": "..tostring(v))
end
end
local choice
repeat
choice = tonumber(io.read("*l"))
until choice ~= nil and choice > 0 and choice <= #d
vm:choose(choice)
elseif e == "end" then
break
else
error("unknown event ("..tostring(e)..")")
end
end

334
stdlib/functions.lua Normal file
View file

@ -0,0 +1,334 @@
local truthy, eval, find_function_variant, anselme
local function rewrite_assignement(fqm, state, arg, explicit_call)
local op, e = find_function_variant(fqm:match("^(.*)%=$"), state, arg, true)
if not op then return op, e end
local ass, err = find_function_variant(":=", state, { type = "list", left = arg.left, right = op }, explicit_call)
if not ass then return ass, err end
return ass
end
local functions
functions = {
-- discard left
[";"] = {
{
arity = 2, mode = "raw",
value = function(a, b) return b end
}
},
-- assignement
[":="] = {
{
arity = 2, mode = "custom",
check = function(state, args)
local left, right = args[1], args[2]
if left.type ~= "variable" then
return nil, ("assignement expected a variable as a left argument but received a %s"):format(left.type)
end
if left.return_type and right.return_type and left.return_type ~= right.return_type then
return nil, ("trying to assign a %s value to a %s variable"):format(right.return_type, left.return_type)
end
return right.return_type or true
end,
value = function(state, exp)
local arg = exp.argument
local name = arg.left.name
local right, righte = eval(state, arg.right)
if not right then return right, righte end
state.variables[name] = right
return right
end
}
},
["+="] = {
{ rewrite = rewrite_assignement }
},
["-="] = {
{ rewrite = rewrite_assignement }
},
["*="] = {
{ rewrite = rewrite_assignement }
},
["/="] = {
{ rewrite = rewrite_assignement }
},
["//="] = {
{ rewrite = rewrite_assignement }
},
["%="] = {
{ rewrite = rewrite_assignement }
},
["^="] = {
{ rewrite = rewrite_assignement }
},
-- comparaison
["="] = {
{
arity = 2, return_type = "number", mode = "raw",
value = function(a, b)
return {
type = "number",
value = (a.type == b.type and a.value == b.value) and 1 or 0
}
end
}
},
["!="] = {
{
arity = 2, return_type = "number", mode = "raw",
value = function(a, b)
return {
type = "number",
value = (a.type == b.type and a.value == b.value) and 0 or 1
}
end
}
},
[">"] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return a > b end
}
},
["<"] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return a < b end
}
},
[">="] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return a >= b end
}
},
["<="] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return a <= b end
}
},
-- arithmetic
["+"] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return a + b end
},
{
arity = 2, types = { "string", "string" }, return_type = "string",
value = function(a, b) return a .. b end
}
},
["-"] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return a - b end
},
{
arity = 1, types = { "number" }, return_type = "number",
value = function(a) return -a end
}
},
["*"] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return a * b end
}
},
["/"] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return a / b end
}
},
["//"] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return math.floor(a / b) end
}
},
["^"] = {
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b) return a ^ b end
}
},
-- boolean
["!"] = {
{
arity = 1, return_type = "number", mode = "raw",
value = function(a)
return {
type = "number",
value = truthy(a) and 0 or 1
}
end
}
},
["&"] = {
{
arity = 2, return_type = "number", mode = "custom",
value = function(state, exp)
local left, lefte = eval(state, exp.left)
if not left then return left, lefte end
if truthy(left) then
local right, righte = eval(state, exp.right)
if not right then return right, righte end
if truthy(right) then
return {
type = "number",
value = 1
}
end
end
return {
type = "number",
value = 0
}
end
}
},
["|"] = {
{
arity = 2, return_type = "number", mode = "raw",
value = function(state, exp)
local left, lefte = eval(state, exp.left)
if not left then return left, lefte end
if truthy(left) then
return {
type = "number",
value = 1
}
end
local right, righte = eval(state, exp.right)
if not right then return right, righte end
return {
type = "number",
value = truthy(right) and 1 or 0
}
end
}
},
-- pair
[":"] = {
{
arity = 2, return_type = "pair", mode = "raw",
value = function(a, b)
return {
type = "pair",
value = { a, b }
}
end
}
},
-- index
["("] = {
{
arity = 2, types = { "list", "number" }, mode = "raw",
value = function(a, b)
return a.value[b.value] or { type = "nil", value = nil }
end
}
},
-- list methods
len = {
{
arity = 1, types = { "list" }, return_type = "number", mode = "raw", -- raw to count pairs in the list
value = function(a)
return {
type = "number",
value = #a.value
}
end
}
},
insert = {
{
arity = 2, types = { "list" }, return_type = "list", mode = "raw",
value = function(a, v)
table.insert(a.value, v)
return a
end
},
{
arity = 3, types = { "list", "number" }, return_type = "list", mode = "raw",
value = function(a, k, v)
table.insert(a.value, k.value, v)
return a
end
}
},
remove = {
{
arity = 1, types = { "list" }, return_type = "list", mode = "raw",
value = function(a)
table.remove(a.value)
return a
end
},
{
arity = 2, types = { "list", "number" }, return_type = "list", mode = "raw",
value = function(a, k)
table.remove(a.value, k.value)
return a
end
}
},
rand = {
{
arity = 0, return_type = "number",
value = function()
return math.random()
end
},
{
arity = 1, types = { "number" }, return_type = "number",
value = function(a)
return math.random(a)
end
},
{
arity = 2, types = { "number", "number" }, return_type = "number",
value = function(a, b)
return math.random(a, b)
end
}
},
cycle = function(...)
local l = {...}
local f, fseen = l[1], assert(anselme.running:eval(l[1]..".👁️", anselme.running:current_namespace()))
for j=2, #l do
local seen = assert(anselme.running:eval(l[j]..".👁️", anselme.running:current_namespace()))
if seen < fseen then
f = l[j]
break
end
end
return anselme.running:run(f, anselme.running:current_namespace())
end,
random = function(...)
local l = {...}
return anselme.running:run(l[math.random(1, #l)], anselme.running:current_namespace())
end,
next = function(...)
local l = {...}
local f = l[#l]
for j=1, #l-1 do
local seen = assert(anselme.running:eval(l[j]..".👁️", anselme.running:current_namespace()))
if seen == 0 then
f = l[j]
break
end
end
return anselme.running:run(f, anselme.running:current_namespace())
end
}
package.loaded[...] = functions
truthy = require((...):gsub("stdlib%.functions$", "interpreter.common")).truthy
eval = require((...):gsub("stdlib%.functions$", "interpreter.expression"))
find_function_variant = require((...):gsub("stdlib%.functions$", "parser.common")).find_function_variant
anselme = require((...):gsub("stdlib%.functions$", "anselme"))
return functions

140
stdlib/types.lua Normal file
View file

@ -0,0 +1,140 @@
local format, to_lua, from_lua
local types = {}
types.lua = {
["nil"] = {
to_anselme = function(val)
return {
type = "nil",
value = nil
}
end
},
boolean = {
to_anselme = function(val)
return {
type = "number",
value = val and 1 or 0
}
end
},
number = {
to_anselme = function(val)
return {
type = "number",
value = val
}
end
},
string = {
to_anselme = function(val)
return {
type = "string",
value = val
}
end
},
table = {
to_anselme = function(val)
local l = {}
for _, v in ipairs(val) do
local r, e = from_lua(v)
if not r then return r, e end
table.insert(l, r)
end
for k, v in pairs(val) do
if not l[k] then
local kv, ke = from_lua(k)
if not k then return k, ke end
local vv, ve = from_lua(v)
if not v then return v, ve end
table.insert(l, {
type = "pair",
value = { kv, vv }
})
end
end
return {
type = "list",
value = val
}
end
}
}
types.anselme = {
["nil"] = {
format = function()
return ""
end,
to_lua = function()
return nil
end
},
number = {
format = function(val)
return tostring(val)
end,
to_lua = function(val)
return val
end
},
string = {
format = function(val)
return tostring(val)
end,
to_lua = function(val)
return val
end
},
list = {
format = function(val)
local l = {}
for _, v in ipairs(val) do
local s, e = format(v)
if not s then return s, e end
table.insert(l, s)
end
return ("[%s]"):format(table.concat(l, ", "))
end,
to_lua = function(val)
local l = {}
for _, v in ipairs(val) do
if v.type == "pair" then
local k, ke = to_lua(v.value[1])
if not k then return k, ke end
local x, xe = to_lua(v.value[2])
if not x then return x, xe end
l[k] = x
else
local s, e = to_lua(v)
if not s then return s, e end
table.insert(l, s)
end
end
return l
end,
},
pair = {
format = function(val)
local k, ke = format(val[1])
if not k then return k, ke end
local v, ve = format(val[2])
if not v then return v, ve end
return ("%s:%s"):format(k, v)
end,
to_lua = function(val)
local k, ke = to_lua(val[1])
if not k then return k, ke end
local v, ve = to_lua(val[2])
if not v then return v, ve end
return { [k] = v }
end
}
}
package.loaded[...] = types
local common = require((...):gsub("stdlib%.types$", "interpreter.common"))
format, to_lua, from_lua = common.format, common.to_lua, common.from_lua
return types

View file

@ -1,5 +1,3 @@
~ $ f(a, b)
hey
~ test yep $ f(x)
~
lol

334
test/inspect.lua Normal file
View file

@ -0,0 +1,334 @@
local inspect ={
_VERSION = 'inspect.lua 3.1.0',
_URL = 'http://github.com/kikito/inspect.lua',
_DESCRIPTION = 'human-readable representations of tables',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2013 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
local tostring = tostring
inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end})
inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end})
local function rawpairs(t)
return next, t, nil
end
-- Apostrophizes the string if it has quotes, but not aphostrophes
-- Otherwise, it returns a regular quoted string
local function smartQuote(str)
if str:match('"') and not str:match("'") then
return "'" .. str .. "'"
end
return '"' .. str:gsub('"', '\\"') .. '"'
end
-- \a => '\\a', \0 => '\\0', 31 => '\31'
local shortControlCharEscapes = {
["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v"
}
local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031
for i=0, 31 do
local ch = string.char(i)
if not shortControlCharEscapes[ch] then
shortControlCharEscapes[ch] = "\\"..i
longControlCharEscapes[ch] = string.format("\\%03d", i)
end
end
local function escape(str)
return (str:gsub("\\", "\\\\")
:gsub("(%c)%f[0-9]", longControlCharEscapes)
:gsub("%c", shortControlCharEscapes))
end
local function isIdentifier(str)
return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
end
local function isSequenceKey(k, sequenceLength)
return type(k) == 'number'
and 1 <= k
and k <= sequenceLength
and math.floor(k) == k
end
local defaultTypeOrders = {
['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
['function'] = 5, ['userdata'] = 6, ['thread'] = 7
}
local function sortKeys(a, b)
local ta, tb = type(a), type(b)
-- strings and numbers are sorted numerically/alphabetically
if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
-- Two default types are compared according to the defaultTypeOrders table
if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
elseif dta then return true -- default types before custom ones
elseif dtb then return false -- custom types after default ones
end
-- custom types are sorted out alphabetically
return ta < tb
end
-- For implementation reasons, the behavior of rawlen & # is "undefined" when
-- tables aren't pure sequences. So we implement our own # operator.
local function getSequenceLength(t)
local len = 1
local v = rawget(t,len)
while v ~= nil do
len = len + 1
v = rawget(t,len)
end
return len - 1
end
local function getNonSequentialKeys(t)
local keys, keysLength = {}, 0
local sequenceLength = getSequenceLength(t)
for k,_ in rawpairs(t) do
if not isSequenceKey(k, sequenceLength) then
keysLength = keysLength + 1
keys[keysLength] = k
end
end
table.sort(keys, sortKeys)
return keys, keysLength, sequenceLength
end
local function countTableAppearances(t, tableAppearances)
tableAppearances = tableAppearances or {}
if type(t) == 'table' then
if not tableAppearances[t] then
tableAppearances[t] = 1
for k,v in rawpairs(t) do
countTableAppearances(k, tableAppearances)
countTableAppearances(v, tableAppearances)
end
countTableAppearances(getmetatable(t), tableAppearances)
else
tableAppearances[t] = tableAppearances[t] + 1
end
end
return tableAppearances
end
local copySequence = function(s)
local copy, len = {}, #s
for i=1, len do copy[i] = s[i] end
return copy, len
end
local function makePath(path, ...)
local keys = {...}
local newPath, len = copySequence(path)
for i=1, #keys do
newPath[len + i] = keys[i]
end
return newPath
end
local function processRecursive(process, item, path, visited)
if item == nil then return nil end
if visited[item] then return visited[item] end
local processed = process(item, path)
if type(processed) == 'table' then
local processedCopy = {}
visited[item] = processedCopy
local processedKey
for k,v in rawpairs(processed) do
processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
if processedKey ~= nil then
processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
end
end
local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field
setmetatable(processedCopy, mt)
processed = processedCopy
end
return processed
end
-------------------------------------------------------------------
local Inspector = {}
local Inspector_mt = {__index = Inspector}
function Inspector:puts(...)
local args = {...}
local buffer = self.buffer
local len = #buffer
for i=1, #args do
len = len + 1
buffer[len] = args[i]
end
end
function Inspector:down(f)
self.level = self.level + 1
f()
self.level = self.level - 1
end
function Inspector:tabify()
self:puts(self.newline, string.rep(self.indent, self.level))
end
function Inspector:alreadyVisited(v)
return self.ids[v] ~= nil
end
function Inspector:getId(v)
local id = self.ids[v]
if not id then
local tv = type(v)
id = (self.maxIds[tv] or 0) + 1
self.maxIds[tv] = id
self.ids[v] = id
end
return tostring(id)
end
function Inspector:putKey(k)
if isIdentifier(k) then return self:puts(k) end
self:puts("[")
self:putValue(k)
self:puts("]")
end
function Inspector:putTable(t)
if t == inspect.KEY or t == inspect.METATABLE then
self:puts(tostring(t))
elseif self:alreadyVisited(t) then
self:puts('<table ', self:getId(t), '>')
elseif self.level >= self.depth then
self:puts('{...}')
else
if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end
local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t)
local mt = getmetatable(t)
self:puts('{')
self:down(function()
local count = 0
for i=1, sequenceLength do
if count > 0 then self:puts(',') end
self:puts(' ')
self:putValue(t[i])
count = count + 1
end
for i=1, nonSequentialKeysLength do
local k = nonSequentialKeys[i]
if count > 0 then self:puts(',') end
self:tabify()
self:putKey(k)
self:puts(' = ')
self:putValue(t[k])
count = count + 1
end
if type(mt) == 'table' then
if count > 0 then self:puts(',') end
self:tabify()
self:puts('<metatable> = ')
self:putValue(mt)
end
end)
if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing }
self:tabify()
elseif sequenceLength > 0 then -- array tables have one extra space before closing }
self:puts(' ')
end
self:puts('}')
end
end
function Inspector:putValue(v)
local tv = type(v)
if tv == 'string' then
self:puts(smartQuote(escape(v)))
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
tv == 'cdata' or tv == 'ctype' then
self:puts(tostring(v))
elseif tv == 'table' then
self:putTable(v)
else
self:puts('<', tv, ' ', self:getId(v), '>')
end
end
-------------------------------------------------------------------
function inspect.inspect(root, options)
options = options or {}
local depth = options.depth or math.huge
local newline = options.newline or '\n'
local indent = options.indent or ' '
local process = options.process
if process then
root = processRecursive(process, root, {}, {})
end
local inspector = setmetatable({
depth = depth,
level = 0,
buffer = {},
ids = {},
maxIds = {},
newline = newline,
indent = indent,
tableAppearances = countTableAppearances(root)
}, Inspector_mt)
inspector:putValue(root)
return table.concat(inspector.buffer)
end
setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end })
return inspect

View file

@ -1,3 +0,0 @@
LOOL
~ test yep

204
test/run.lua Normal file
View file

@ -0,0 +1,204 @@
local lfs = require("lfs")
local anselme = require("anselme")
local ser = require("test.ser")
local inspect = require("test.inspect")
local function format_text(t, prefix)
prefix = prefix or " "
local r = ""
for _, l in ipairs(t) do
r = r .. prefix
local tags = ""
for k, v in ipairs(l.tags) do
tags = tags .. ("[%q]=%q"):format(k, v)
end
if tags ~= "" then
r = r .. ("[%s]%s"):format(tags, l.data)
else
r = r .. l.data
end
end
return r
end
local function compare(a, b)
if type(a) == "table" and type(b) == "table" then
for k, v in pairs(a) do
if not compare(v, b[k]) then
return false
end
end
for k, v in pairs(b) do
if not compare(v, a[k]) then
return false
end
end
return true
else
return a == b
end
end
-- parse args
local args = {}
local i=1
while i <= #arg do
if arg[i+1] and not arg[i+1]:match("^%-%-") then
args[arg[i]:gsub("^%-%-", "")] = arg[i+1]
i = i + 2
else
args[arg[i]:gsub("^%-%-", "")] = true
i = i + 1
end
end
-- list tests
local files = {}
for item in lfs.dir("test/tests/") do
if item:match("%.ans$") and item:match(args.filter or "") then
table.insert(files, "test/tests/"..item)
end
end
table.sort(files)
-- test script
if args.script then
local vm = anselme()
local state, err = vm:loadfile("test.ans", "test")
if state then
local istate, e = vm:run("test")
if not istate then
print("error", e)
else
repeat
local t, d = istate:step()
if t == "text" then
print(format_text(d))
elseif t == "choice" then
print(format_text(d, "\n> "))
istate:choose(io.read())
else
print(t, inspect(d))
end
until t == "return" or t == "error"
end
else
print("error", err)
end
-- run tests
else
local total, success = #files, 0
for _, file in ipairs(files) do
local filebase = file:match("^(.*)%.ans$")
local namespace = filebase:match("([^/]*)$")
math.randomseed(0)
local vm = anselme()
vm:loadalias {
seen = "👁️",
checkpoint = "🏁"
}
vm:loadfunction {
-- custom event test
["wait"] = {
{
arity = 1, types = { "number" },
value = function(duration)
coroutine.yield("wait", duration)
end
}
},
-- run another function in parallel
["run"] = {
{
arity = 1, types = { "string" },
value = function(str)
local istate, e = anselme.running.vm:run(str, anselme.running:current_namespace())
if not istate then coroutine.yield("error", e) end
local event, data = istate:step()
coroutine.yield(event, data)
end
}
},
-- manual choice
choose = {
{
arity = 1, types = { "number" },
value = function(c)
anselme.running:choose(c)
end
}
},
-- manual interrupt
interrupt = {
{
arity = 1, types = { "string" },
value = function(str)
anselme.running:interrupt(str)
coroutine.yield("wait", 0)
end
},
{
arity = 0,
value = function()
anselme.running:interrupt()
coroutine.yield("wait", 0)
end
}
}
}
local state, err = vm:loadfile(file, namespace)
local result = {}
if state then
local istate, e = vm:run(namespace)
if not istate then
table.insert(result, { "error", e })
else
repeat
local t, d = istate:step()
table.insert(result, { t, d })
until t == "return" or t == "error"
end
else
table.insert(result, { "error", err })
end
if args.write then
local o = assert(io.open(filebase..".lua", "w"))
o:write(ser(result))
o:write("\n--[[\n")
for _, v in ipairs(result) do
o:write(inspect(v).."\n")
end
o:write("]]--")
o:close()
else
local o, e = loadfile(filebase..".lua")
if o then
local output = o()
if not compare(result, output) then
if not args.silent then
print("> "..namespace)
print(inspect(result))
print("is not equal to")
print(inspect(output))
end
else
success = success + 1
end
else
if not args.silent then
print("> "..namespace)
print(e)
print("result was:")
print(inspect(result))
end
end
end
end
if args.write then
print("Wrote test results.")
else
print(("%s/%s tests success."):format(success, total))
end
end

143
test/ser.lua Normal file
View file

@ -0,0 +1,143 @@
--[[
Copyright (c) 2011,2013 Robin Wellner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]
local pairs, ipairs, tostring, type, concat, dump, floor, format = pairs, ipairs, tostring, type, table.concat, string.dump, math.floor, string.format
local function getchr(c)
return "\\" .. c:byte()
end
local function make_safe(text)
return ("%q"):format(text):gsub('\n', 'n'):gsub("[\128-\255]", getchr)
end
local oddvals = {[tostring(1/0)] = '1/0', [tostring(-1/0)] = '-1/0', [tostring(-(0/0))] = '-(0/0)', [tostring(0/0)] = '0/0'}
local function write(t, memo, rev_memo)
local ty = type(t)
if ty == 'number' then
t = format("%.17g", t)
return oddvals[t] or t
elseif ty == 'boolean' or ty == 'nil' then
return tostring(t)
elseif ty == 'string' then
return make_safe(t)
elseif ty == 'table' or ty == 'function' then
if not memo[t] then
local index = #rev_memo + 1
memo[t] = index
rev_memo[index] = t
end
return '_[' .. memo[t] .. ']'
else
error("Trying to serialize unsupported type " .. ty)
end
end
local kw = {['and'] = true, ['break'] = true, ['do'] = true, ['else'] = true,
['elseif'] = true, ['end'] = true, ['false'] = true, ['for'] = true,
['function'] = true, ['goto'] = true, ['if'] = true, ['in'] = true,
['local'] = true, ['nil'] = true, ['not'] = true, ['or'] = true,
['repeat'] = true, ['return'] = true, ['then'] = true, ['true'] = true,
['until'] = true, ['while'] = true}
local function write_key_value_pair(k, v, memo, rev_memo, name)
if type(k) == 'string' and k:match '^[_%a][_%w]*$' and not kw[k] then
return (name and name .. '.' or '') .. k ..'=' .. write(v, memo, rev_memo)
else
return (name or '') .. '[' .. write(k, memo, rev_memo) .. ']=' .. write(v, memo, rev_memo)
end
end
-- fun fact: this function is not perfect
-- it has a few false positives sometimes
-- but no false negatives, so that's good
local function is_cyclic(memo, sub, super)
local m = memo[sub]
local p = memo[super]
return m and p and m < p
end
local function write_table_ex(t, memo, rev_memo, srefs, name)
if type(t) == 'function' then
return '_[' .. name .. ']=loadstring' .. make_safe(dump(t))
end
local m = {}
local mi = 1
for i = 1, #t do -- don't use ipairs here, we need the gaps
local v = t[i]
if v == t or is_cyclic(memo, v, t) then
srefs[#srefs + 1] = {name, i, v}
m[mi] = 'nil'
mi = mi + 1
else
m[mi] = write(v, memo, rev_memo)
mi = mi + 1
end
end
for k,v in pairs(t) do
if type(k) ~= 'number' or floor(k) ~= k or k < 1 or k > #t then
if v == t or k == t or is_cyclic(memo, v, t) or is_cyclic(memo, k, t) then
srefs[#srefs + 1] = {name, k, v}
else
m[mi] = write_key_value_pair(k, v, memo, rev_memo)
mi = mi + 1
end
end
end
return '_[' .. name .. ']={' .. concat(m, ',') .. '}'
end
return function(t)
local memo = {[t] = 0}
local rev_memo = {[0] = t}
local srefs = {}
local result = {}
-- phase 1: recursively descend the table structure
local n = 0
while rev_memo[n] do
result[n + 1] = write_table_ex(rev_memo[n], memo, rev_memo, srefs, n)
n = n + 1
end
-- phase 2: reverse order
for i = 1, n*.5 do
local j = n - i + 1
result[i], result[j] = result[j], result[i]
end
-- phase 3: add all the tricky cyclic stuff
for i, v in ipairs(srefs) do
n = n + 1
result[n] = write_key_value_pair(v[2], v[3], memo, rev_memo, '_[' .. v[1] .. ']')
end
-- phase 4: add something about returning the main table
if result[n]:sub(1, 5) == '_[0]=' then
result[n] = 'return ' .. result[n]:sub(6)
else
result[n + 1] = 'return _[0]'
end
-- phase 5: just concatenate everything
result = concat(result, '\n')
return n > 1 and 'local _={}\n' .. result or result
end

View file

@ -0,0 +1,11 @@
> ye
no
> ne
ok
~ choose(2)
> ho
plop
> oh
plup
~ choose(1)

View file

@ -0,0 +1,48 @@
local _={}
_[21]={}
_[20]={}
_[19]={}
_[18]={}
_[17]={}
_[16]={}
_[15]={data="plop",tags=_[21]}
_[14]={data="oh",tags=_[20]}
_[13]={data="ho",tags=_[19]}
_[12]={data="ok",tags=_[18]}
_[11]={data="ne",tags=_[17]}
_[10]={data="ye",tags=_[16]}
_[9]={_[15]}
_[8]={_[13],_[14]}
_[7]={_[12]}
_[6]={_[10],_[11]}
_[5]={"return"}
_[4]={"text",_[9]}
_[3]={"choice",_[8]}
_[2]={"text",_[7]}
_[1]={"choice",_[6]}
return {_[1],_[2],_[3],_[4],_[5]}
--[[
{ "choice", { {
data = "ye",
tags = {}
}, {
data = "ne",
tags = {}
} } }
{ "text", { {
data = "ok",
tags = {}
} } }
{ "choice", { {
data = "ho",
tags = {}
}, {
data = "oh",
tags = {}
} } }
{ "text", { {
data = "plop",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,11 @@
$ f
> neol
nah
> ho
plop
~ f
> oh
ok
~ f
~ choose(3)

View file

@ -0,0 +1,37 @@
local _={}
_[15]={}
_[14]={}
_[13]={}
_[12]={}
_[11]={}
_[10]={data="ok",tags=_[15]}
_[9]={data="neol",tags=_[14]}
_[8]={data="oh",tags=_[13]}
_[7]={data="neol",tags=_[12]}
_[6]={data="ho",tags=_[11]}
_[5]={_[10]}
_[4]={_[6],_[7],_[8],_[9]}
_[3]={"return"}
_[2]={"text",_[5]}
_[1]={"choice",_[4]}
return {_[1],_[2],_[3]}
--[[
{ "choice", { {
data = "ho",
tags = {}
}, {
data = "neol",
tags = {}
}, {
data = "oh",
tags = {}
}, {
data = "neol",
tags = {}
} } }
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,5 @@
> ye
no
> ne
ok
~ choose(2)

View file

@ -0,0 +1,27 @@
local _={}
_[11]={}
_[10]={}
_[9]={}
_[8]={data="ok",tags=_[11]}
_[7]={data="ne",tags=_[10]}
_[6]={data="ye",tags=_[9]}
_[5]={_[8]}
_[4]={_[6],_[7]}
_[3]={"return"}
_[2]={"text",_[5]}
_[1]={"choice",_[4]}
return {_[1],_[2],_[3]}
--[[
{ "choice", { {
data = "ye",
tags = {}
}, {
data = "ne",
tags = {}
} } }
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,7 @@
(hey couic + 5)
other stuff
CHAZOUM
OO
k

View file

@ -0,0 +1,6 @@
local _={}
_[1]={"return"}
return {_[1]}
--[[
{ "return" }
]]--

1
test/tests/comment.ans Normal file
View file

@ -0,0 +1 @@
(hey couic + 5)

6
test/tests/comment.lua Normal file
View file

@ -0,0 +1,6 @@
local _={}
_[1]={"return"}
return {_[1]}
--[[
{ "return" }
]]--

20
test/tests/commit.ans Normal file
View file

@ -0,0 +1,20 @@
$ bar
:5 var
~ var := 2
before: {var}
~ run("parallel")
§ foo
checkpoint
after: {var}
~ run("parallel")
$ parallel
parallel: {bar.var}
~ bar

38
test/tests/commit.lua Normal file
View file

@ -0,0 +1,38 @@
local _={}
_[17]={}
_[16]={}
_[15]={}
_[14]={}
_[13]={data="parallel: 2",tags=_[17]}
_[12]={data="after: 2",tags=_[16]}
_[11]={data="parallel: 5",tags=_[15]}
_[10]={data="before: 2",tags=_[14]}
_[9]={_[13]}
_[8]={_[12]}
_[7]={_[11]}
_[6]={_[10]}
_[5]={"return"}
_[4]={"text",_[9]}
_[3]={"text",_[8]}
_[2]={"text",_[7]}
_[1]={"text",_[6]}
return {_[1],_[2],_[3],_[4],_[5]}
--[[
{ "text", { {
data = "before: 2",
tags = {}
} } }
{ "text", { {
data = "parallel: 5",
tags = {}
} } }
{ "text", { {
data = "after: 2",
tags = {}
} } }
{ "text", { {
data = "parallel: 2",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,3 @@
ko ~ 0
ok ~ 1
ok bis ~

View file

@ -0,0 +1,19 @@
local _={}
_[7]={}
_[6]={}
_[5]={data="ok bis",tags=_[7]}
_[4]={data="ok",tags=_[6]}
_[3]={_[4],_[5]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
}, {
data = "ok bis",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,6 @@
:5 a
~ a = 2
ko
~~
ok

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,6 @@
:5 a
~ a = 5
ok
~~
ko

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,8 @@
:5 a
~ a = 2
ko
~~ 0
ko
~~
ok

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,8 @@
:5 a
~ a = 2
ko
~~ 1
ok
~~
ko

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
:5 a
~ a = 2
ko

View file

@ -0,0 +1,6 @@
local _={}
_[1]={"return"}
return {_[1]}
--[[
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
:5 a
~ a = 5
ok

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,3 @@
ah
~ wait(5)
ho

View file

@ -0,0 +1,21 @@
local _={}
_[8]={}
_[7]={}
_[6]={data="ho",tags=_[8]}
_[5]={data="ah",tags=_[7]}
_[4]={_[5],_[6]}
_[3]={"return"}
_[2]={"text",_[4]}
_[1]={"wait",5}
return {_[1],_[2],_[3]}
--[[
{ "wait", 5 }
{ "text", { {
data = "ah",
tags = {}
}, {
data = "ho",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,3 @@
$ a
:2 a

View file

@ -0,0 +1,6 @@
local _={}
_[1]={"error","trying to define variable define override function.a, but a function with the same name exists; at line 3"}
return {_[1]}
--[[
{ "error", "trying to define variable define override function.a, but a function with the same name exists; at line 3" }
]]--

View file

@ -0,0 +1,3 @@
:2 a
$ a

View file

@ -0,0 +1,6 @@
local _={}
_[1]={"error","trying to define function define override variable.a, but a variable with the same name exists; at line 3"}
return {_[1]}
--[[
{ "error", "trying to define function define override variable.a, but a variable with the same name exists; at line 3" }
]]--

View file

@ -0,0 +1,5 @@
:5 a
:2 a
a: {a}

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="a: 5",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "a: 5",
tags = {}
} } }
{ "return" }
]]--

1
test/tests/define.ans Normal file
View file

@ -0,0 +1 @@
:5 a

6
test/tests/define.lua Normal file
View file

@ -0,0 +1,6 @@
local _={}
_[1]={"return"}
return {_[1]}
--[[
{ "return" }
]]--

View file

@ -0,0 +1,5 @@
$ f(a, l...)
{a}
{l}
~ f("ok", "o", "k")

View file

@ -0,0 +1,19 @@
local _={}
_[7]={}
_[6]={}
_[5]={data="[o, k]",tags=_[7]}
_[4]={data="ok",tags=_[6]}
_[3]={_[4],_[5]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
}, {
data = "[o, k]",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
$ f(a)
{a}
~ f("ok")

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
$ f(a, b)
{a}{b}
~ f("ok")

View file

@ -0,0 +1,6 @@
local _={}
_[1]={"error","function \"function args arity check fail.f\" expected 2 arguments but received 1; at line 4"}
return {_[1]}
--[[
{ "error", 'function "function args arity check fail.f" expected 2 arguments but received 1; at line 4' }
]]--

View file

@ -0,0 +1,5 @@
$ f(a, b, l...)
{a}{b}
{l}
~ f("o","k")

View file

@ -0,0 +1,19 @@
local _={}
_[7]={}
_[6]={}
_[5]={data="[]",tags=_[7]}
_[4]={data="ok",tags=_[6]}
_[3]={_[4],_[5]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
}, {
data = "[]",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,5 @@
$ f(a, b, l...)
{a}{b}
{l}
~ f("o", "k", "o", "k")

View file

@ -0,0 +1,19 @@
local _={}
_[7]={}
_[6]={}
_[5]={data="[o, k]",tags=_[7]}
_[4]={data="ok",tags=_[6]}
_[3]={_[4],_[5]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
}, {
data = "[o, k]",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
$ f(a, b)
{a}{b}
~ f("o", "k")

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,5 @@
$ f(a, b)
$ f(x)
$ f(u, v)

View file

@ -0,0 +1,6 @@
local _={}
_[1]={"error","trying to define function function arity conflict.f with arity [2;2], but another function with the arity exist; at line 5"}
return {_[1]}
--[[
{ "error", "trying to define function function arity conflict.f with arity [2;2], but another function with the arity exist; at line 5" }
]]--

View file

@ -0,0 +1,14 @@
$ f
$ a
a
$ b
b
$ c
c
~ cycle("a","b","c")
~ f
~ f
~ f
~ f
~ f

View file

@ -0,0 +1,46 @@
local _={}
_[21]={}
_[20]={}
_[19]={}
_[18]={}
_[17]={}
_[16]={data="b",tags=_[21]}
_[15]={data="a",tags=_[20]}
_[14]={data="c",tags=_[19]}
_[13]={data="b",tags=_[18]}
_[12]={data="a",tags=_[17]}
_[11]={_[16]}
_[10]={_[15]}
_[9]={_[14]}
_[8]={_[13]}
_[7]={_[12]}
_[6]={"return"}
_[5]={"text",_[11]}
_[4]={"text",_[10]}
_[3]={"text",_[9]}
_[2]={"text",_[8]}
_[1]={"text",_[7]}
return {_[1],_[2],_[3],_[4],_[5],_[6]}
--[[
{ "text", { {
data = "a",
tags = {}
} } }
{ "text", { {
data = "b",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "text", { {
data = "a",
tags = {}
} } }
{ "text", { {
data = "b",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,14 @@
$ f
$ a
a
$ b
b
$ c
c
~ next("a","b","c")
~ f
~ f
~ f
~ f
~ f

View file

@ -0,0 +1,46 @@
local _={}
_[21]={}
_[20]={}
_[19]={}
_[18]={}
_[17]={}
_[16]={data="c",tags=_[21]}
_[15]={data="c",tags=_[20]}
_[14]={data="c",tags=_[19]}
_[13]={data="b",tags=_[18]}
_[12]={data="a",tags=_[17]}
_[11]={_[16]}
_[10]={_[15]}
_[9]={_[14]}
_[8]={_[13]}
_[7]={_[12]}
_[6]={"return"}
_[5]={"text",_[11]}
_[4]={"text",_[10]}
_[3]={"text",_[9]}
_[2]={"text",_[8]}
_[1]={"text",_[7]}
return {_[1],_[2],_[3],_[4],_[5],_[6]}
--[[
{ "text", { {
data = "a",
tags = {}
} } }
{ "text", { {
data = "b",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,14 @@
$ f
$ a
a
$ b
b
$ c
c
~ random("a","b","c")
~ f
~ f
~ f
~ f
~ f

View file

@ -0,0 +1,46 @@
local _={}
_[21]={}
_[20]={}
_[19]={}
_[18]={}
_[17]={}
_[16]={data="a",tags=_[21]}
_[15]={data="c",tags=_[20]}
_[14]={data="c",tags=_[19]}
_[13]={data="c",tags=_[18]}
_[12]={data="b",tags=_[17]}
_[11]={_[16]}
_[10]={_[15]}
_[9]={_[14]}
_[8]={_[13]}
_[7]={_[12]}
_[6]={"return"}
_[5]={"text",_[11]}
_[4]={"text",_[10]}
_[3]={"text",_[9]}
_[2]={"text",_[8]}
_[1]={"text",_[7]}
return {_[1],_[2],_[3],_[4],_[5],_[6]}
--[[
{ "text", { {
data = "b",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "text", { {
data = "a",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,7 @@
$ hey
§ foo
@2
@5
{hey}
{hey.foo}

View file

@ -0,0 +1,19 @@
local _={}
_[7]={}
_[6]={}
_[5]={data="2",tags=_[7]}
_[4]={data="5",tags=_[6]}
_[3]={_[4],_[5]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "5",
tags = {}
}, {
data = "2",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
$ hey
@5
{hey}

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="5",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "5",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
$ a
:5 b
a: {b}

View file

@ -0,0 +1,6 @@
local _={}
_[1]={"error","unknown identifier \"b\"; at line 4"}
return {_[1]}
--[[
{ "error", 'unknown identifier "b"; at line 4' }
]]--

View file

@ -0,0 +1,4 @@
$ a
:5 b
a: {a.b}

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="a: 5",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "a: 5",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
$ f(a)
{a}
~ "ok".f()

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
$ f(a, b)
{a}{b}
~ "o".f("k")

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
$ f(l...)
{l}
~ f()

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="[]",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "[]",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,4 @@
$ f(l...)
{l}
~ f("o", "k")

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="[o, k]",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "[o, k]",
tags = {}
} } }
{ "return" }
]]--

4
test/tests/function.ans Normal file
View file

@ -0,0 +1,4 @@
$ f
ok
~ f

14
test/tests/function.lua Normal file
View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={data="ok",tags=_[5]}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,19 @@
$ oh
§ leave
in interrupt: {bar.var}
no
$ bar
:5 var
~ var := 2
before: {var}
~ interrupt("leave")
§ foo
checkpoint
after: {var}
~ oh.bar

View file

@ -0,0 +1,29 @@
local _={}
_[12]={}
_[11]={}
_[10]={}
_[9]={data="no",tags=_[12]}
_[8]={data="in interrupt: 5",tags=_[11]}
_[7]={data="before: 2",tags=_[10]}
_[6]={_[8],_[9]}
_[5]={_[7]}
_[4]={"return"}
_[3]={"text",_[6]}
_[2]={"wait",0}
_[1]={"text",_[5]}
return {_[1],_[2],_[3],_[4]}
--[[
{ "text", { {
data = "before: 2",
tags = {}
} } }
{ "wait", 0 }
{ "text", { {
data = "in interrupt: 5",
tags = {}
}, {
data = "no",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,20 @@
$ leave
in interrupt: {oh.bar.var}
$ oh
no
$ bar
:5 var
~ var := 2
before: {var}
~ interrupt("leave")
§ foo
checkpoint
after: {var}
~ oh.bar

View file

@ -0,0 +1,24 @@
local _={}
_[10]={}
_[9]={}
_[8]={data="in interrupt: 5",tags=_[10]}
_[7]={data="before: 2",tags=_[9]}
_[6]={_[8]}
_[5]={_[7]}
_[4]={"return"}
_[3]={"text",_[6]}
_[2]={"wait",0}
_[1]={"text",_[5]}
return {_[1],_[2],_[3],_[4]}
--[[
{ "text", { {
data = "before: 2",
tags = {}
} } }
{ "wait", 0 }
{ "text", { {
data = "in interrupt: 5",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,18 @@
$ bar
:5 var
~ var := 2
$ leave
in interrupt: {var}
before: {var}
~ interrupt("leave")
§ foo
checkpoint
after: {var}
~ bar

View file

@ -0,0 +1,24 @@
local _={}
_[10]={}
_[9]={}
_[8]={data="in interrupt: 5",tags=_[10]}
_[7]={data="before: 2",tags=_[9]}
_[6]={_[8]}
_[5]={_[7]}
_[4]={"return"}
_[3]={"text",_[6]}
_[2]={"wait",0}
_[1]={"text",_[5]}
return {_[1],_[2],_[3],_[4]}
--[[
{ "text", { {
data = "before: 2",
tags = {}
} } }
{ "wait", 0 }
{ "text", { {
data = "in interrupt: 5",
tags = {}
} } }
{ "return" }
]]--

Some files were not shown because too many files have changed in this diff Show more