mirror of
https://github.com/Reuh/anselme.git
synced 2025-10-27 16:49:31 +00:00
1361 lines
42 KiB
Text
1361 lines
42 KiB
Text
let VERSION = "0.11.1"
|
|
|
|
--## Amazing constants ##--
|
|
|
|
--- Recurring paterns: full variable name
|
|
let dissallowedVariable = "%{%}%§%>%<%(%)%~%+%-%*%/%%%=%?%!%&%|%:%^%,%@%s%.%#\""
|
|
let pvariable = "[^%s%%d][^%s]*":format(dissallowedVariable, dissallowedVariable)
|
|
|
|
--- Operators priority, higher means first
|
|
let binopPriority = {
|
|
[","] = 0,
|
|
[":"] = 1,
|
|
["|"] = 2,
|
|
["&"] = 3,
|
|
["<"] = 4, [">"] = 4, ["<="] = 4, [">="] = 4, ["!="] = 4, ["="] = 4,
|
|
["+"] = 5, ["-"] = 5,
|
|
["*"] = 6, ["/"] = 6, ["//"] = 6, ["%"] = 6,
|
|
["^"] = 8
|
|
}
|
|
let unopPriority = {
|
|
["!"] = 7, ["-"] = 7,
|
|
["?"] = 9
|
|
}
|
|
-- +inf priority: parantheses, function calls
|
|
|
|
--## Runtime functions ##--
|
|
let expression, eval, evalBool, evalAddress, luaToAns, findVariable, lookupVariable, runFunction, formatText, sendEvent, parse, run, tryPotentialFunction, pushTags, readable, evalList, evalNoParagraph, defineVariable, runChildren, step, evalFlatListNoParagraph, insertAnselmsScriptsFromDirectory
|
|
|
|
--- Parse code.
|
|
parse = (context, code, origin="a unnamed chunk", temporary)
|
|
let root = context.root
|
|
let parent = context -- current parent element
|
|
let parentParagraph = (parent.paragraph or parent.type == "root") and parent or parent.parentParagraph -- closest parent paragraph
|
|
let lastParsed = nil -- last parsed element
|
|
let lastLineEmpty = false -- if the last line was empty
|
|
let indent = 0 -- current indentation level
|
|
let lineno = 0 -- line number
|
|
let skipChildren = false -- true to not parse this line's children
|
|
for l in (code.."\n"):gmatch("([^\n]*)\n") do
|
|
lineno += 1
|
|
|
|
-- Indentation parse
|
|
let tabs, line = l:match("^(\t*)(.*)$")
|
|
let level = #tabs
|
|
if line == "" or (skipChildren and level > indent) then
|
|
lastLineEmpty = true
|
|
continue
|
|
end
|
|
if skipChildren and level <= indent then
|
|
skipChildren = false
|
|
end
|
|
|
|
-- Children. Childrenize!
|
|
if level == indent+1 then
|
|
indent += 1
|
|
parent = lastParsed
|
|
if parent.type == "paragraph" then
|
|
parentParagraph = parent
|
|
end
|
|
if not parent.children then
|
|
error("a %s line doesn't want children but was given some; at line %s in %s":format(parent.type, lineno, origin))
|
|
end
|
|
-- Uh... Unchildrenization ?
|
|
elseif level < indent then
|
|
while level < indent do
|
|
indent -= 1
|
|
if parent.type == "paragraph" then
|
|
parentParagraph = parentParagraph.parent
|
|
end
|
|
parent = parent.parent
|
|
end
|
|
-- Invalid childrenization.
|
|
elseif level ~= indent then
|
|
error("invalid indentation; at line %s in %s":format(lineno, origin))
|
|
end
|
|
|
|
-- Staaaaaart parsing
|
|
let parsed = { -- element
|
|
type = "unknown", -- element type
|
|
parent = parent, -- parent element. Should be nil only for root.
|
|
children = nil, -- nil if the element shouldn't have children
|
|
parentParagraph = parentParagraph, -- closest parent paragraph. Should be nil only for root.
|
|
root = root, -- ast root
|
|
variables = nil, -- variable map.
|
|
-- condition decorator
|
|
condition = nil, -- condition expression if the line has been decorated with a condition. nil if no condition.
|
|
-- tags decorator
|
|
tags = nil,
|
|
-- paragraph, paragraph decorator
|
|
paragraph = nil,
|
|
parameters = nil,
|
|
["return"] = nil,
|
|
value = nil,
|
|
-- choice, text
|
|
text = nil,
|
|
-- definition, assignement, paragraph, paragraph decorator
|
|
address = nil,
|
|
-- assignement
|
|
operator = nil,
|
|
-- redirection, tag, return, definition, assignement
|
|
expression = nil,
|
|
-- redirection, choice
|
|
continue = nil,
|
|
-- other
|
|
temporary = temporary, -- true if the line should be removed immediately after it was run.
|
|
line = lineno, -- line number. For debug/error message information.
|
|
origin = origin -- origin name. For debug/error message information.
|
|
}
|
|
|
|
-- Decorators
|
|
if not line:match("^%(") then
|
|
-- Condition decorator
|
|
if line:match("^%s*[^%~].*%~[^%~]+$") then
|
|
let s, c, e = line:match("^(%s*[^%~].*)%~([^%~%#%§]+)(.-)$")
|
|
let exp, rem = expression(parsed, c)
|
|
if rem:match("[^%s]") then
|
|
error("invalid condition decorator expression near %q; at line %s in %s":format(rem, lineno, origin))
|
|
else
|
|
line = s..e
|
|
parsed.condition = exp
|
|
end
|
|
end
|
|
-- Tag decorator
|
|
if line:match("^%s*[^%#].*%#[^%#]+$") then
|
|
let s, c, e = line:match("^(%s*[^%#].*)%#([^%~%#%§]+)(.-)$")
|
|
let exp, rem = expression(parsed, c)
|
|
if rem:match("[^%s]") then
|
|
error("invalid tag decorator expression near %q; at line %s in %s":format(rem, lineno, origin))
|
|
else
|
|
line = s..e
|
|
parsed.tags = exp
|
|
end
|
|
end
|
|
-- Paragraph decorator
|
|
if line:match("^%s*[^%§].*%§[^%§]+$") then
|
|
let s, c, e = line:match("^(%s*[^%§].*)%§%s*([^%~%#%§]+)(.-)$")
|
|
line = s..e
|
|
parsed.paragraph = c
|
|
end
|
|
end
|
|
|
|
-- Comment
|
|
if line:match("^%(") then
|
|
skipChildren = true
|
|
continue
|
|
-- Paragraph
|
|
elseif line:match("^§") then
|
|
parsed.type = "paragraph"
|
|
parsed.paragraph = line:match("^§%s*(.-)%s*$")
|
|
parsed.children = {}
|
|
-- Choice
|
|
elseif line:match("^>") then
|
|
parsed.type = "choice"
|
|
parsed.children = {}
|
|
let lastLine = parsed.parent.children[#parsed.parent.children]
|
|
if not lastLineEmpty and lastLine and (lastLine.type == "choice" or lastLine.type == "elsechoice") then
|
|
parsed.continue = true
|
|
end
|
|
parsed.text = line:match("^>%s*(.-)%s*$")
|
|
-- Variable definition
|
|
elseif line:match("^:") then
|
|
parsed.type = "definition"
|
|
parsed.expression, parsed.address = expression(parsed, line:match("^:%s*(.-)%s*$"))
|
|
let var, rem = expression(parsed, parsed.address)
|
|
if rem:match("[^%s]") or var.type ~= "variable" then
|
|
error("unreasonably invalid variable name (%s); at line %s in %s":format(parsed.address, lineno, origin))
|
|
else
|
|
parsed.address = evalAddress(parsed, var.address)
|
|
end
|
|
-- Define at compile time
|
|
defineVariable(parsed, parsed.address, eval(parsed, parsed.expression))
|
|
-- Variable assignements
|
|
elseif line:match("^[%=%+%-%*%/%%%?%!%^%&%|]") then
|
|
parsed.type = "assignement"
|
|
parsed.operator = line:match("^([%=%+%-%*%/%%%?%!%^%&%|])")
|
|
parsed.expression, parsed.address = expression(parsed, line:match("^[%=%+%-%*%/%%%?%!%^%&%|]%s*(.-)%s*$"))
|
|
let var, rem = expression(parsed, parsed.address)
|
|
if rem:match("[^%s]") or var.type ~= "variable" then
|
|
error("unreasonably invalid variable name (%s); at line %s in %s":format(parsed.address, lineno, origin))
|
|
else
|
|
parsed.address = var.address
|
|
end
|
|
-- Redirection
|
|
elseif line:match("^%~") then
|
|
parsed.type = "redirection"
|
|
parsed.children = {}
|
|
let lastLine = parsed.parent.children[#parsed.parent.children]
|
|
if not lastLineEmpty and lastLine and lastLine.type == "redirection" then
|
|
parsed.continue = true
|
|
end
|
|
let cond = line:match("^%~%s*(.-)%s*$")
|
|
if cond == "" then cond = "1" end
|
|
let exp, rem = expression(parsed, cond)
|
|
if rem:match("[^%s]") then
|
|
error("invalid redirection expression near %q; at line %s in %s":format(rem, lineno, origin))
|
|
else
|
|
parsed.expression = exp
|
|
end
|
|
-- Value return
|
|
elseif line:match("^%@") then
|
|
parsed.type = "return"
|
|
let exp, rem = expression(parsed, line:match("^%@%s*(.-)%s*$"))
|
|
if rem:match("[^%s]") then
|
|
error("invalid return expression near %q; at line %s in %s":format(rem, lineno, origin))
|
|
else
|
|
parsed.expression = exp
|
|
end
|
|
-- Tag
|
|
elseif line:match("^%#") then
|
|
parsed.type = "tag"
|
|
parsed.children = {}
|
|
let exp, rem = expression(parsed, line:match("^%#%s*(.-)%s*$"))
|
|
if rem:match("[^%s]") then
|
|
error("invalid tag expression near %q; at line %s in %s":format(rem, lineno, origin))
|
|
else
|
|
parsed.expression = exp
|
|
end
|
|
-- Presumption of text
|
|
else
|
|
parsed.type = "text"
|
|
parsed.text = line:match("^%s*(.-)%s*$")
|
|
end
|
|
|
|
-- Setup paragraph
|
|
if parsed.paragraph then
|
|
let name, parameters = parsed.paragraph:match("^%s*([^%(]+)(.-)%s*$")
|
|
-- Parse address
|
|
if name:match("%s") or name:match("^%{") then
|
|
let var, rem = expression(parsed, name)
|
|
if rem:match("[^%s]") or var.type ~= "variable" then
|
|
error("unreasonably invalid paragraph name (%s); at line %s in %s":format(parsed.address, lineno, origin))
|
|
else
|
|
parsed.address = evalAddress(parsed, var.address)
|
|
end
|
|
else -- allow special caracters for simple addresses (for _+_ methods, etc.)
|
|
parsed.address = { name }
|
|
end
|
|
-- Define at compile time
|
|
let pvar = defineVariable(parent, parsed.address, parsed)
|
|
-- Parse parameters
|
|
parsed.parameters = {}
|
|
parsed.variables = pvar
|
|
if parameters:match("^%s*%(.-%)%s*$") then
|
|
let content = parameters:match("^%s*%((.-)%)%s*$")
|
|
for par in content:gmatch("[^,]+") do
|
|
let var = par:match("^%s*(.-)%s*$")
|
|
table.insert(parsed.parameters, var)
|
|
defineVariable(parsed, { var }, eval("0"))
|
|
end
|
|
end
|
|
parsed.value = parsed -- paragraphs are variables themselves
|
|
defineVariable(parsed, {"👁️"}, eval("0"))
|
|
defineVariable(parsed, {"🗨️"}, eval("0"))
|
|
parsed["return"] = { type = "string", value = "" }
|
|
end
|
|
|
|
-- Insert
|
|
parent = parsed.parent
|
|
table.insert(parent.children, parsed)
|
|
|
|
lastParsed = parsed
|
|
lastLineEmpty = false
|
|
end
|
|
end
|
|
|
|
--- Read an expression at the start of a string, returns the expression AST and the remaining string
|
|
expression = (context, str, operatingOn, minPriority=0)
|
|
-- Sweep sweep sweep
|
|
str = str:match("^%s*(.-)$")
|
|
|
|
-- VALUES --
|
|
-- (requiredly) --
|
|
if not operatingOn then
|
|
-- Litteraly nothing
|
|
if str == "" then
|
|
error("unexpected empty expression; at line %s in %s":format(context.line, context.origin))
|
|
-- String
|
|
elseif str:match("^\"") then
|
|
let string, remaining = str:match("^\"([^\"]*)\"(.*)$")
|
|
return expression(context, remaining, {
|
|
type = "string",
|
|
value = string
|
|
}, minPriority)
|
|
-- Float
|
|
elseif str:match("^%d*%.%d+") then
|
|
let number, remaining = str:match("^(%d*%.%d+)(.*)$")
|
|
return expression(context, remaining, {
|
|
type = "number",
|
|
value = tonumber(number)
|
|
}, minPriority)
|
|
-- Integer
|
|
elseif str:match("^%d+") then
|
|
let number, remaining = str:match("^(%d+)(.*)$")
|
|
return expression(context, remaining, {
|
|
type = "number",
|
|
value = tonumber(number)
|
|
}, minPriority)
|
|
-- Unary operators
|
|
elseif str:match("^[%?%!%-]") then
|
|
let op, rem = str:match("^([%?%!%-])(.*)$")
|
|
-- Copy pasted from the binary operators code. Should not be needed since unary operators are supposed to have the highest priority, but you never know!
|
|
-- UPDATE: added exponentiation operator with higher priority. Thanks, past self.
|
|
let exp, remaining, lowerPriorityOp = expression(context, rem, nil, unopPriority[op])
|
|
let unop = {
|
|
type = "u"..op,
|
|
expression = exp
|
|
}
|
|
if lowerPriorityOp then -- lower priority op handling
|
|
return expression(context, remaining, unop, minPriority)
|
|
else
|
|
return unop, remaining
|
|
end
|
|
-- Parantheses
|
|
elseif str:match("^%(") then
|
|
let content, remaining = str:match("^(%b())(.*)$")
|
|
let exp, premaining = expression(context, content:match("^%((.*)%)$"), nil)
|
|
if premaining:match("[^%s]") then
|
|
error("something in parantheses can't be read as an expression; at line %s in %s":format(context.line, context.origin))
|
|
end
|
|
return expression(context, remaining, {
|
|
type = "parantheses",
|
|
expression = exp
|
|
}, minPriority)
|
|
-- Everything with letters in it, ie variable
|
|
elseif str:match("^"..pvariable) then
|
|
let var, remaining = str:match("^("..pvariable..")(.*)$")
|
|
-- Build address
|
|
let address = { { type = "string", value = var } }
|
|
while remaining:match("^%s%s-"..pvariable) or remaining:match("^%s-%b{}") do
|
|
if remaining:match("^%s-%{") then -- expression
|
|
var, remaining = remaining:match("^%s-(%b{})(.*)$")
|
|
var = var:gsub("^%{", ""):gsub("%}$", "")
|
|
let exp, rem = expression(context, var)
|
|
if rem:match("[^%s]") then
|
|
error("something in parantheses can't be read as an expression; in variable address at line %s in %s":format(context.line, context.origin))
|
|
else
|
|
table.insert(address, exp)
|
|
end
|
|
else -- identifier
|
|
var, remaining = remaining:match("^%s%s-("..pvariable..")(.*)$")
|
|
table.insert(address, {
|
|
type = "string",
|
|
value = var
|
|
})
|
|
end
|
|
end
|
|
return expression(context, remaining, {
|
|
type = "variable",
|
|
address = address
|
|
}, minPriority)
|
|
end
|
|
|
|
-- OPERATORS --
|
|
-- (possibly) --
|
|
else
|
|
-- This is the point where I stopped writing code and pondered for a minute what the fuck was I doing. Heh.
|
|
if str:match("^<=") or str:match("^>=") or str:match("^!=") or str:match("^//") or str:match("^[%&%|%+%-%*%/%%%<%>%=%^%:%,]") then
|
|
let op, rem
|
|
if str:match("^<=") or str:match("^>=") or str:match("^!=") then
|
|
op, rem = str:match("^([<>!]=)(.*)$")
|
|
elseif str:match("^//") then
|
|
op, rem = str:match("^(//)(.*)$")
|
|
else
|
|
op, rem = str:match("^([%&%|%+%-%*%/%%%<%>%=%^%:%,])(.*)$")
|
|
end
|
|
-- Higher priority, ie need to be deeper in the AST
|
|
if binopPriority[op] >= minPriority then
|
|
let rightVal, remaining, lowerPriorityOp = expression(context, rem, nil, binopPriority[op])
|
|
let binop = {
|
|
type = "b"..op,
|
|
left = operatingOn,
|
|
right = rightVal
|
|
}
|
|
if lowerPriorityOp then -- lower priority op handling
|
|
return expression(context, remaining, binop, minPriority)
|
|
else
|
|
return binop, remaining
|
|
end
|
|
-- Return but notice there is a lower priority operator remaining (so it's handled higher)
|
|
else
|
|
return operatingOn, str, true
|
|
end
|
|
-- Function call
|
|
elseif str:match("^%(") then
|
|
let content, remaining = str:match("^(%b())(.*)$")
|
|
content = content:match("^%((.*)%)$")
|
|
let args = nil
|
|
if content:match("[^%s]") then
|
|
let exp, rem = expression(context, content)
|
|
if rem:match("[^%s]") then
|
|
error("invalid function parameter expression near %q; at line %s in %s":format(rem, context.line, context.origin))
|
|
end
|
|
args = exp
|
|
end
|
|
return expression(context, remaining, {
|
|
type = "call",
|
|
expression = operatingOn,
|
|
arguments = args
|
|
}, minPriority)
|
|
-- Function call without parathesis (single number or string litteral)
|
|
elseif str:match("^[%d%.\"]") and minPriority < math.huge then
|
|
let exp, rem = expression(context, str, nil, math.huge)
|
|
assert(exp.type == "number" or exp.type == "string", "expected string or number but got a %s; in function call at line %s in %s":format(exp.type, context.line, context.origin))
|
|
return expression(context, rem, {
|
|
type = "call",
|
|
expression = operatingOn,
|
|
arguments = exp
|
|
}, minPriority)
|
|
else
|
|
return operatingOn, str -- no operator apparently
|
|
end
|
|
end
|
|
|
|
-- Yay, we shouldn't be here.
|
|
error("the expression parser just gave up; at line %s in %s":format(context.line, context.origin))
|
|
end
|
|
|
|
--- Define a variable in the closest scope.
|
|
-- Returns the variable parent table.
|
|
defineVariable = (context, address, value)
|
|
let pvar = context.variables or context.parentParagraph.variables
|
|
for _, part in ipairs(address) do
|
|
if not pvar[part] then
|
|
pvar[part] = {}
|
|
end
|
|
pvar = pvar[part]
|
|
end
|
|
if pvar[1] then
|
|
error("variable %q already defined; at line %s in %s":format(table.concat(address, " "), context.line, context.origin))
|
|
end
|
|
pvar[1] = value
|
|
return pvar
|
|
end
|
|
|
|
--- Find a variable in any line. Won't try looking in a higher scope/paragraph.
|
|
-- Return nil if the variable wasn't found.
|
|
-- Return var, variable parent table if it was found.
|
|
findVariable = (context, address)
|
|
let root = context.root
|
|
-- get variable table
|
|
let pvar = context.variables
|
|
if not pvar then
|
|
return nil
|
|
end
|
|
-- Reach last depth
|
|
for _, part in ipairs(address) do
|
|
while true do
|
|
if pvar[part] then
|
|
pvar = pvar[part]
|
|
break
|
|
end
|
|
-- Aliases
|
|
if root.aliases[part] then
|
|
let alias = root.aliases[part]
|
|
if pvar[alias] then
|
|
pvar = pvar[alias]
|
|
break
|
|
end
|
|
end
|
|
-- Try using parent paragraph return value
|
|
let pval = pvar[1]
|
|
if pval and context ~= pval and pval.type == "paragraph" then
|
|
let r = runFunction(context, pval)
|
|
if type(r.value) == "table" and r.value.variables then
|
|
pvar = r.value.variables
|
|
continue
|
|
end
|
|
end
|
|
-- We failed you.
|
|
return nil
|
|
end
|
|
end
|
|
-- Last depth handling
|
|
if pvar[1] then
|
|
return pvar[1], pvar
|
|
end
|
|
-- We failed. A bit later this time.
|
|
return nil
|
|
end
|
|
|
|
--- Find a variable in any line, and will search higher in the AST until it either find it or reach root.
|
|
-- If source directories are defined (see vm:loaddirectory), this function will load scripts from theses directories as needed.
|
|
-- Return nil if the variable parent paragraph could not be found.
|
|
-- Return var, variable parent table if it was found.
|
|
lookupVariable = (context, address)
|
|
-- Search from current level to top level.
|
|
let parentParagraph = context
|
|
while parentParagraph do
|
|
let v, par = findVariable(parentParagraph, address)
|
|
if v then
|
|
return v, par
|
|
end
|
|
parentParagraph = parentParagraph.parentParagraph
|
|
end
|
|
-- Pending scripts (source directories)
|
|
let root = context.root
|
|
for j=#address, 1, -1 do -- search from most specific to less
|
|
let searchingForAddress = table.concat(address, " ", 1, j)
|
|
for i, pending in ipairs(root.pendingScripts) do
|
|
if pending.address == searchingForAddress then
|
|
let code = "§ %s\n":format(pending.address)
|
|
let f = io.open(pending.path, "r")
|
|
for l in f:lines("*l") do
|
|
code ..= "\t%s\n":format(l)
|
|
end
|
|
f:close()
|
|
parse(root, code, pending.path)
|
|
table.remove(root.pendingScripts, i)
|
|
return findVariable(root, address)
|
|
end
|
|
end
|
|
end
|
|
-- We failed you. Sorry.
|
|
return nil
|
|
end
|
|
|
|
-- Run a function, and returns its return value.
|
|
runFunction = (context, fn, args={})
|
|
if fn.type == "paragraph" then
|
|
-- Checks parameters
|
|
let p = fn.value
|
|
if #args ~= #p.parameters then
|
|
error("paragraph (%s; at line %s in %s) expected %s parameters but received %s; at line %s in %s":format(table.concat(p.address, " "), p.line, p.origin, #p.parameters, #args, context.line, context.origin))
|
|
end
|
|
for i, arg in ipairs(args) do
|
|
p.variables[p.parameters[i]][1] = arg
|
|
end
|
|
-- Run function
|
|
p["return"] = { type = "string", value = "" }
|
|
runChildren(p)
|
|
return p["return"]
|
|
elseif fn.type == "luafunction" then
|
|
let luaArgs = {}
|
|
for i, arg in ipairs(args) do
|
|
table.insert(luaArgs, arg.value)
|
|
end
|
|
let ret = fn.value(unpack(luaArgs))
|
|
let val = luaToAns(ret)
|
|
if val then
|
|
return val
|
|
else
|
|
error("invalid return type (%s) for luafunction in expression; at line %s in %s":format(ret, context.line, context.origin))
|
|
end
|
|
else
|
|
error("tried to call a %s variable in an expression; at line %s in %s":format(fn.type, context.line, context.origin))
|
|
end
|
|
end
|
|
|
|
--- Search for a function in a list of potential source variables.
|
|
-- If found, returns its return value.
|
|
-- Returns nil otherwise
|
|
tryPotentialFunction = (context, potentialParentVariables, name, arguments)
|
|
let fn
|
|
-- search function
|
|
for _, p in ipairs(potentialParentVariables) do
|
|
if type(p.value) == "table" and p.value.variables then
|
|
let v = findVariable(p.value, { name })
|
|
if v then
|
|
fn = v
|
|
break
|
|
end
|
|
end
|
|
end
|
|
-- run
|
|
if fn then
|
|
return runFunction(context, fn, arguments)
|
|
end
|
|
end
|
|
|
|
--- Evaluate an expression
|
|
-- exp can be an expression AST or a string
|
|
-- If no context is specified, will create a temporary empty context.
|
|
eval = (context, exp)
|
|
let remain = ""
|
|
if type(context) == "string" and exp == nil then
|
|
context, exp = {}, context
|
|
end
|
|
if type(exp) == "string" then
|
|
exp, remain = expression(context, exp)
|
|
end
|
|
if remain:match("[^%s]") then
|
|
error("unexpected text in expression near %q; at line %s in %s":format(remain, context.line, context.origin))
|
|
end
|
|
|
|
assert(exp.type, "undefined variable type; at line %s in %s":format(context.line, context.origin))
|
|
-- Litterals
|
|
if exp.type == "number" then
|
|
return {
|
|
type = "number",
|
|
value = exp.value
|
|
}
|
|
elseif exp.type == "string" then
|
|
return {
|
|
type = "string",
|
|
value = formatText(context, exp.value)
|
|
}
|
|
elseif exp.type == "parantheses" then
|
|
return eval(context, exp.expression)
|
|
-- Variables
|
|
elseif exp.type == "variable" then
|
|
let addr = evalAddress(context, exp.address)
|
|
let v = lookupVariable(context, addr)
|
|
if v then
|
|
if type(v.value) == "table" and v.value.paragraph then
|
|
return {
|
|
type = "paragraph",
|
|
value = v.value
|
|
}
|
|
else
|
|
return {
|
|
type = v.type,
|
|
value = v.value
|
|
}
|
|
end
|
|
end
|
|
error("can't find the variable (%s); at line %s in %s":format(table.concat(addr, " "), context.line, context.origin))
|
|
-- Arithmetic
|
|
elseif exp.type:match("^b[%+%-%*%/%%%^%>%<%=%!%:%,][%/%=]?$") then
|
|
let op = exp.type:match("^b(.*)$")
|
|
let left, right = eval(context, exp.left), eval(context, exp.right)
|
|
let customBinop = tryPotentialFunction(context, { left, right }, "_%s_":format(op), { left, right })
|
|
if customBinop then return customBinop end
|
|
if op == "+" then
|
|
if (left.type == "string" and (right.type == "string" or right.type == "number")) or
|
|
((left.type == "number" or left.type == "string") and right.type == "string") then
|
|
return {
|
|
type = "string",
|
|
value = tostring(left.value) .. tostring(right.value)
|
|
}
|
|
elseif left.type == "number" and right.type == "number" then
|
|
return {
|
|
type = "number",
|
|
value = tonumber(left.value) + tonumber(right.value)
|
|
}
|
|
end
|
|
elseif op == "-" then
|
|
if left.type == "string" and right.type == "number" then
|
|
return {
|
|
type = "string",
|
|
value = tostring(left.value):sub(1, utf8.offset(left.value, utf8.len(left.value)-tonumber(right.value)))
|
|
}
|
|
elseif left.type == "number" and right.type == "string" then
|
|
return {
|
|
type = "string",
|
|
value = tostring(right.value):sub(utf8.offset(right.value, tonumber(left.value)+1))
|
|
}
|
|
elseif left.type == "number" and right.type == "number" then
|
|
return {
|
|
type = "number",
|
|
value = tonumber(left.value) - tonumber(right.value)
|
|
}
|
|
elseif left.type == "string" and right.type == "string" then
|
|
return {
|
|
type = "string",
|
|
value = tostring(left.value):gsub(tostring(right.value), "")
|
|
}
|
|
end
|
|
elseif op == "*" then
|
|
if left.type == "string" and right.type == "number" then
|
|
return {
|
|
type = "string",
|
|
value = tostring(left.value):rep(tonumber(right.value))
|
|
}
|
|
elseif left.type == "number" and right.type == "string" then
|
|
return {
|
|
type = "string",
|
|
value = tostring(right.value):rep(tonumber(left.value))
|
|
}
|
|
elseif left.type == "number" and right.type == "number" then
|
|
return {
|
|
type = "number",
|
|
value = tonumber(left.value) * tonumber(right.value)
|
|
}
|
|
end
|
|
elseif op == "/" then
|
|
if left.type == "string" and right.type == "number" then
|
|
return {
|
|
type = "string",
|
|
value = tostring(left.value):sub(utf8.offset(left.value, utf8.len(left.value)-tonumber(right.value)+1))
|
|
}
|
|
elseif left.type == "number" and right.type == "string" then
|
|
return {
|
|
type = "string",
|
|
value = tostring(right.value):sub(1, utf8.offset(right.value, tonumber(left.value)))
|
|
}
|
|
elseif left.type == "number" and right.type == "number" then
|
|
return {
|
|
type = "number",
|
|
value = tonumber(left.value) / tonumber(right.value)
|
|
}
|
|
end
|
|
elseif op == "//" then
|
|
if left.type == "number" and right.type == "number" then
|
|
return {
|
|
type = "number",
|
|
value = tonumber(left.value) // tonumber(right.value)
|
|
}
|
|
end
|
|
elseif op == "%" then
|
|
if (left.type == "string" and right.type == "number") or (left.type == "number" and right.type == "string") then
|
|
return {
|
|
type = "number",
|
|
value = tostring(left.value):find(tostring(right.value)) or 0
|
|
}
|
|
elseif left.type == "number" and right.type == "number" then
|
|
return {
|
|
type = "number",
|
|
value = tonumber(left.value) % tonumber(right.value)
|
|
}
|
|
end
|
|
elseif op == "^" then
|
|
if left.type == "string" then
|
|
let s = tostring(left.value)
|
|
if evalBool(context, exp.right).value == 0 then
|
|
s = s:lower()
|
|
else
|
|
s = s:upper()
|
|
end
|
|
return {
|
|
type = "string",
|
|
value = s
|
|
}
|
|
elseif left.type == "number" and right.type == "number" then
|
|
return {
|
|
type = "number",
|
|
value = tonumber(left.value) ^ tonumber(right.value)
|
|
}
|
|
end
|
|
elseif op == ">" then
|
|
if (left.type == "number" or left.type == "string") and (right.type == "number" or right.type == "string") then
|
|
return {
|
|
type = "number",
|
|
value = (left.value > right.value) and 1 or 0
|
|
}
|
|
end
|
|
elseif op == "<" then
|
|
if (left.type == "number" or left.type == "string") and (right.type == "number" or right.type == "string") then
|
|
return {
|
|
type = "number",
|
|
value = (left.value < right.value) and 1 or 0
|
|
}
|
|
end
|
|
elseif op == ">=" then
|
|
if (left.type == "number" or left.type == "string") and (right.type == "number" or right.type == "string") then
|
|
return {
|
|
type = "number",
|
|
value = (left.value >= right.value) and 1 or 0
|
|
}
|
|
end
|
|
elseif op == "<=" then
|
|
if (left.type == "number" or left.type == "string") and (right.type == "number" or right.type == "string") then
|
|
return {
|
|
type = "number",
|
|
value = (left.value <= right.value) and 1 or 0
|
|
}
|
|
end
|
|
elseif op == "=" then
|
|
return {
|
|
type = "number",
|
|
value = (left.value == right.value) and 1 or 0
|
|
}
|
|
elseif op == "!=" then
|
|
return {
|
|
type = "number",
|
|
value = (left.value ~= right.value) and 1 or 0
|
|
}
|
|
elseif op == ":" then
|
|
return {
|
|
type = "pair",
|
|
value = { name = left, value = right }
|
|
}
|
|
elseif op == "," then
|
|
return {
|
|
type = "list",
|
|
value = { head = left, tail = right }
|
|
}
|
|
end
|
|
error("invalid value types for %s operator: %s %s %s; at line %s in %s":format(op, left.type, op, right.type, context.line, context.origin))
|
|
elseif exp.type:match("^u[%-%?%!]$") then
|
|
let op = exp.type:match("^u(.*)$")
|
|
let value = eval(context, exp.expression)
|
|
let customUnop = tryPotentialFunction(context, { value }, "%s_":format(op), { value })
|
|
if customUnop then return customUnop end
|
|
if op == "-" then
|
|
if value.type == "number" then
|
|
return {
|
|
type = "number",
|
|
value = -tonumber(value.value)
|
|
}
|
|
elseif value.type == "string" then
|
|
return {
|
|
type = "string",
|
|
value = value.value:reverse()
|
|
}
|
|
end
|
|
elseif op == "?" then
|
|
if value.type == "paragraph" then
|
|
return evalBool(context, { type = "call", expression = exp.expression })
|
|
else
|
|
return {
|
|
type = "number",
|
|
value = (value.value == 0) and 0 or 1
|
|
}
|
|
end
|
|
elseif op == "!" then
|
|
return {
|
|
type = "number",
|
|
value = evalBool(context, exp.expression).value == 0 and 1 or 0
|
|
}
|
|
end
|
|
error("invalid value types for %s operator: %s; at line %s in %s":format(op, value.type, context.line, context.origin))
|
|
-- Lazy operators
|
|
elseif exp.type == "b&" then
|
|
if evalBool(exp.left).value == 0 then
|
|
return left
|
|
else
|
|
return evalBool(context, exp.right)
|
|
end
|
|
elseif exp.type == "b|" then
|
|
let left = evalBool(context, exp.left)
|
|
if left.value == 0 then
|
|
return evalBool(context, exp.right)
|
|
else
|
|
return left
|
|
end
|
|
-- Call
|
|
elseif exp.type == "call" then
|
|
let fn = eval(context, exp.expression)
|
|
if exp.arguments then
|
|
return runFunction(context, fn, evalList(context, exp.arguments))
|
|
else
|
|
return runFunction(context, fn)
|
|
end
|
|
else
|
|
error("unkown expression (%s) to evaluate; at line %s in %s":format(exp.type, context.line, context.origin))
|
|
end
|
|
end
|
|
|
|
--- Same as eval, but return a Anselme boolean.
|
|
evalBool = (context, expression)
|
|
return eval(context, { type = "u?", expression = expression })
|
|
end
|
|
|
|
--- Same as eval, but run paragraphs.
|
|
evalNoParagraph = (context, exp)
|
|
let t = eval(context, exp)
|
|
while t.type == "paragraph" do -- run paragraph/function
|
|
t = runFunction(context, t)
|
|
end
|
|
return t
|
|
end
|
|
|
|
--- Same as eval, but returns a Lua list of expression.
|
|
evalList = (context, exp)
|
|
let l = {}
|
|
let e = eval(context, exp)
|
|
while e.type == "list" do
|
|
table.insert(l, e.value.head)
|
|
e = e.value.tail
|
|
end
|
|
table.insert(l, e)
|
|
return l
|
|
end
|
|
|
|
--- Same as evalNoParagraph, but returns a Lua list of expression in which nested lists are merged with the full list.
|
|
evalFlatListNoParagraph = (context, exp)
|
|
let l = {}
|
|
let extract = (e)
|
|
while e.type == "list" or e.type == "paragraph" do
|
|
if e.type == "paragraph" then
|
|
e = runFunction(context, e)
|
|
else
|
|
let hd = e.value.head
|
|
if hd.type == "list" or hd.type == "paragraph" then
|
|
extract(hd)
|
|
else
|
|
table.insert(l, hd)
|
|
end
|
|
e = e.value.tail
|
|
end
|
|
end
|
|
table.insert(l, e)
|
|
end
|
|
extract(eval(context, exp))
|
|
return l
|
|
end
|
|
|
|
--- Evaluate a variable address. Returns a list of strings (paragraphs name).
|
|
evalAddress = (context, address)
|
|
return [
|
|
for _, a in ipairs(address) do
|
|
push tostring(evalNoParagraph(context, a).value)
|
|
end
|
|
]
|
|
end
|
|
|
|
--- Convert a Lua variable to a Anselme value.
|
|
-- Returns nil in case of error.
|
|
luaToAns = (var)
|
|
if var == nil or var == false then
|
|
return eval("0")
|
|
elseif var == true then
|
|
return eval("1")
|
|
elseif type(var) == "number" then
|
|
return {
|
|
type = "number",
|
|
value = var
|
|
}
|
|
elseif type(var) == "string" then
|
|
return {
|
|
type = "number",
|
|
value = var
|
|
}
|
|
elseif type(var) == "function" then
|
|
return {
|
|
type = "luafunction",
|
|
value = var
|
|
}
|
|
elseif type(var) == "table" and var.type then
|
|
return {
|
|
type = var.type,
|
|
value = var.value
|
|
}
|
|
else
|
|
error("don't know how to convert a %s to a Anselme value":format(type(var)))
|
|
end
|
|
end
|
|
|
|
--- Returns a readable representation of a variable.
|
|
readable = (var)
|
|
if var.type == "pair" then
|
|
return "%s:%s":format(readable(var.value.name), readable(var.value.value))
|
|
elseif var.type == "list" then
|
|
return "%s,%s":format(readable(var.value.head), readable(var.value.tail))
|
|
else
|
|
return tostring(var.value)
|
|
end
|
|
end
|
|
|
|
--- Format text.
|
|
formatText = (context, text, yield)
|
|
let r = text:gsub("%b{}", (exp)
|
|
return readable(evalNoParagraph(context, exp:match("^{(.*)}$")))
|
|
end)
|
|
return r
|
|
end
|
|
|
|
--- Send event to the engine.
|
|
-- root is the root node or an event
|
|
sendEvent = (root)
|
|
let e = root.event or root
|
|
root.event = nil
|
|
if e[1] == "choice" then
|
|
let vm = coroutine.yield("choice", e[2])
|
|
if not vm.state.chosen then
|
|
error("no choice has been made by the engine, I don't know what to doooooo")
|
|
end
|
|
let c = assert(e[3][vm.state.chosen], "invalid choice %s, expected something in [1,%s]":format(vm.state.chosen, #e[2]))
|
|
vm.state.chosen = nil
|
|
runChildren(c)
|
|
else
|
|
coroutine.yield(e[1], e[2])
|
|
end
|
|
end
|
|
|
|
--- Add a list of tag expressions to the current tag state, and returns the previous state.
|
|
pushTags = (context, tags)
|
|
let root = context.root
|
|
let oldTags = root.tags
|
|
let newTags = [ for k, v in pairs(root.tags) do @[k]=v end ]
|
|
let l = evalFlatListNoParagraph(context, tags)
|
|
for _, t in ipairs(l) do
|
|
if t.type == "pair" then
|
|
let name, value = t.value.name, t.value.value
|
|
while name.type == "paragraph" do
|
|
name = runFunction(context, name)
|
|
end
|
|
while value.type == "paragraph" do
|
|
value = runFunction(context, value)
|
|
end
|
|
newTags[name.value] = value.value
|
|
else
|
|
table.insert(newTags, t.value)
|
|
end
|
|
end
|
|
root.tags = newTags
|
|
return oldTags
|
|
end
|
|
|
|
--- Run a line's children.
|
|
runChildren = (line)
|
|
run(line.children)
|
|
if line.paragraph then line.variables["🗨️"][1].value += 1 end
|
|
end
|
|
|
|
--- Run a list of elements. Must be used in a couroutine.
|
|
-- i is the starting line indice.
|
|
-- Returns the indice of the last line ran.
|
|
run = (lines, i=1)
|
|
-- trivial case
|
|
if #lines == 0 then
|
|
return i
|
|
end
|
|
-- get root
|
|
let root = lines[1].root
|
|
-- run
|
|
while i <= #lines do
|
|
let line = lines[i]
|
|
i += 1
|
|
-- Interrupt events
|
|
if #root.interrupts > 0 then
|
|
for _, e in ipairs(root.interrupts) do
|
|
sendEvent(e)
|
|
end
|
|
root.interrupts = {}
|
|
end
|
|
-- Condition decorator
|
|
if line.condition then
|
|
if evalBool(line, line.condition).value == 0 then
|
|
continue
|
|
end
|
|
end
|
|
-- Tag decorator
|
|
let oldTags
|
|
if line.tags then
|
|
oldTags = pushTags(line, line.tags)
|
|
end
|
|
-- Run line
|
|
if line.type == "paragraph" or line.type == "definition" then
|
|
-- pass
|
|
elseif line.type == "choice" then
|
|
-- send other event on their way
|
|
if root.event and root.event[1] ~= "choice" then
|
|
sendEvent(root)
|
|
end
|
|
-- let us do our own thing now. please
|
|
if not root.event then
|
|
root.event = { "choice", {}, {} }
|
|
end
|
|
-- complete the choice list
|
|
let isBuffered = false
|
|
let t = formatText(line, line.text)
|
|
if t:match("\\$") then
|
|
t = t:gsub("\\$", "")
|
|
isBuffered = true
|
|
end
|
|
table.insert(root.event[2], { text = t, tags = root.tags })
|
|
table.insert(root.event[3], line)
|
|
-- choices remain
|
|
if i <= #lines and lines[i].type == "choice" and lines[i].continue then
|
|
isBuffered = true
|
|
end
|
|
-- send to whatever when done
|
|
if not isBuffered then
|
|
sendEvent(root)
|
|
end
|
|
elseif line.type == "assignement" then
|
|
-- Evaluate address
|
|
let addr = evalAddress(line, line.address)
|
|
-- Search from current to top level
|
|
let v, pvar = lookupVariable(line, addr)
|
|
if not v then
|
|
error("can't find the variable (%s); at line %s in %s":format(table.concat(addr, " "), line.line, line.origin))
|
|
end
|
|
-- Eval expression
|
|
let exp = eval(line, line.expression)
|
|
-- Custom operators
|
|
let cv = tryPotentialFunction(line, { v }, line.operator, { v, exp })
|
|
if cv then
|
|
-- good
|
|
else
|
|
-- Normal operation
|
|
if line.operator ~= "=" then
|
|
exp = eval(line, {
|
|
type = "b"..line.operator,
|
|
left = {
|
|
type = "variable",
|
|
address = line.address
|
|
},
|
|
right = exp
|
|
})
|
|
end
|
|
-- Assign
|
|
if pvar[1] then
|
|
if v.type ~= exp.type then
|
|
error("tried to replace the variable %q of type %s with a %s; at line %s in %s":format(table.concat(addr, " "), v.type, exp.type, line.line, line.origin))
|
|
else
|
|
pvar[1] = {
|
|
type = exp.type,
|
|
value = exp.value
|
|
}
|
|
end
|
|
else
|
|
error("found the variable (%s) to assign, but it's inaccessible; at line %s in %s":format(table.concat(addr, " "), line.line, line.origin))
|
|
end
|
|
end
|
|
elseif line.type == "redirection" then
|
|
if evalBool(line, line.expression).value ~= 0 then
|
|
runChildren(line)
|
|
while i <= #lines and lines[i].type == "redirection" and lines[i].continue do
|
|
i += 1
|
|
end
|
|
end
|
|
elseif line.type == "return" then
|
|
line.parentParagraph["return"] = eval(line, line.expression)
|
|
elseif line.type == "tag" then
|
|
let oldTags = pushTags(line, line.expression)
|
|
run(line.children)
|
|
root.tags = oldTags
|
|
elseif line.type == "text" then
|
|
-- send other event on their way
|
|
if root.event and root.event[1] ~= "text" then
|
|
sendEvent(root)
|
|
end
|
|
-- do our own thing
|
|
if not root.event then
|
|
root.event = { "text", {} }
|
|
end
|
|
let t = formatText(line, line.text)
|
|
if t:match("\\$") then
|
|
table.insert(root.event[2], { text = t:gsub("\\$", ""), tags = root.tags })
|
|
else
|
|
table.insert(root.event[2], { text = t, tags = root.tags })
|
|
sendEvent(root)
|
|
end
|
|
else
|
|
error("element unknown to the runtime (%s); at line %s in %s":format(line.type, line.line, line.origin))
|
|
end
|
|
-- paragraph decorator
|
|
if line.paragraph then
|
|
line.variables["👁️"][1].value += 1
|
|
end
|
|
-- tag decorator, part two
|
|
if line.tags then
|
|
root.tags = oldTags
|
|
end
|
|
-- Temporary line
|
|
if line.temporary then
|
|
i -= 1
|
|
table.remove(lines, i)
|
|
end
|
|
end
|
|
return i
|
|
end
|
|
|
|
--- Step the VM. Use in a coroutine.
|
|
step = :()
|
|
@_coroutine = coroutine.running()
|
|
while true do
|
|
@state.lastLine = run(@state.children, @state.lastLine)
|
|
if @state.event then sendEvent(@state) end
|
|
coroutine.yield("end")
|
|
end
|
|
end
|
|
|
|
--- Various filesystem manipulation compatibility thingies.
|
|
let isFile = (path)
|
|
if love then
|
|
return love.filesystem.getInfo(path, "file")
|
|
else
|
|
return require("lfs").attributes(path, "mode") == "file"
|
|
end
|
|
end
|
|
let isDir = (path)
|
|
if love then
|
|
return love.filesystem.getInfo(path, "directory")
|
|
else
|
|
return require("lfs").attributes(path, "mode") == "directory"
|
|
end
|
|
end
|
|
let listDir = (path)
|
|
if love then
|
|
return love.filesystem.getDirectoryItems(path)
|
|
else
|
|
return [for item in require("lfs").dir(path) do item end]
|
|
end
|
|
end
|
|
--- Search recursively for Anselme scripts in a directory and add them to a list of { path = "file path", address = "text address" }
|
|
insertAnselmsScriptsFromDirectory = (list, dir, addressPrefix="")
|
|
for _, f in ipairs(listDir(dir)) do
|
|
let path = "%s/%s":format(dir, f)
|
|
if isFile(path) then
|
|
if path:match("%.ans$") then
|
|
table.insert(list, { path = path, address = "%s%s":format(addressPrefix, f:match("^(.*)%.ans$")) })
|
|
end
|
|
elseif isDir(path) then
|
|
insertAnselmsScriptsFromDirectory(list, path, "%s ":format(f))
|
|
end
|
|
end
|
|
end
|
|
|
|
--## Public interface ##--
|
|
|
|
--- Anselme VM method and properties.
|
|
let vm_mt = {
|
|
--- The root AST, A.K.A. the state. This is what you want to save and load in your game.
|
|
-- Also, it's probably not a wise idea to edit this variable yourself, but you do you.
|
|
state = nil,
|
|
|
|
--- Add an engine-defined variable, or a table of engine-defined variables.
|
|
-- The variable will be defined from the root of the document.
|
|
-- Name can be any valid variable name, and can contains . (paragraphs will be created as needed).
|
|
-- The variable can be a Lua function. It will be run inside Anselme's coroutine, so you may yield if you want.
|
|
-- Lua | Anselme type casting:
|
|
-- string <-> string
|
|
-- number <-> number
|
|
-- false -> 0
|
|
-- true -> 1
|
|
-- table -> paragraph
|
|
-- table(type) -> custom type
|
|
-- AST <- paragraph
|
|
-- function <-> luafunction
|
|
-- So if you want to check the falseness of a Anselme's value from Lua, variable == 0.
|
|
-- You can define a paragraph behaviour when called by defining a function as the first element of the table (index 1). The function receives the paragraph as its first argument.
|
|
-- You can also define a cutom type by returning a { type = "type name", value = data } table if you really want to.
|
|
-- The AST structure is documented a few lines above, in the root element definition.
|
|
register = :(name, var)
|
|
if type(name) == "table" then
|
|
for k, v in pairs(name) do
|
|
@register(k, v)
|
|
end
|
|
return @
|
|
elseif type(var) == "table" and not var.type then
|
|
for k, v in pairs(var) do
|
|
@register("%s %s":format(name, k), v)
|
|
end
|
|
if var[1] then
|
|
@register(name, var[1])
|
|
end
|
|
return @
|
|
else
|
|
defineVariable(@state, [ for p in name:gmatch("[^%s]+") do p end ], luaToAns(var))
|
|
return @
|
|
end
|
|
end,
|
|
|
|
--- Load a file content into the VM and append it at the end of the root element.
|
|
loadfile = :(path)
|
|
let f = io.open(path, "r")
|
|
if not f then error("can't open file %q":format(path)) end
|
|
parse(@state, f:read("*a"), path)
|
|
f:close()
|
|
return @
|
|
end,
|
|
--- Load a directory.
|
|
-- When a variable is not found, it will be searched in the source directories.
|
|
-- Requires luafilesystem or LÖVE.
|
|
loaddirectory = :(path)
|
|
insertAnselmsScriptsFromDirectory(@state.pendingScripts, path)
|
|
return @
|
|
end,
|
|
--- Load some text into the VM and append it at the end of the root element.
|
|
load = :(code, origin)
|
|
parse(@state, code, origin)
|
|
return @
|
|
end,
|
|
|
|
--- Load a file content into the VM and append it at the end of the root element.
|
|
-- Unlike loadfile, code loaded using executefile will be deleted from the VM's state as soon as it is run,
|
|
-- so it won't be saved and restored with the VM. Typically used to initiate scripts (with a redirection).
|
|
-- In pratice: the code will be run as soon as possible, only once.
|
|
execfile = :(path)
|
|
let f = io.open(path, "r")
|
|
if not f then error("can't open file \""..tostring(path).."\"") end
|
|
parse(@state, f:read("*a"), path, true)
|
|
f:close()
|
|
return @
|
|
end,
|
|
--- Load some text into the VM and append it at the end of the root element.
|
|
-- Unlike loadfile, code loaded using executefile will be deleted from the VM's state as soon as it is run,
|
|
-- so it won't be saved and restored with the VM. Typically used to initiate scripts (with a redirection).
|
|
-- In pratice: the code will be run as soon as possible, only once.
|
|
exec = :(code, origin)
|
|
parse(@state, code, origin, true)
|
|
return @
|
|
end,
|
|
|
|
--- Evaluate an expression in the VM, from the root element.
|
|
-- Returns the associated Lua value. Casting rules are described in the register method.
|
|
eval = :(exp)
|
|
let v = eval(@state, exp)
|
|
return v.value
|
|
end,
|
|
|
|
--- Choose an answer by specifying its option indice (starting at 1).
|
|
-- Should only be called after receiving a "choice" event. See the step method.
|
|
choose = :(i)
|
|
@state.chosen = assert(i, "expected a choice but none was given")
|
|
return @
|
|
end,
|
|
|
|
--- Throw an interrupt event; i.e., pause the VM and send the event as soon as possible.
|
|
-- This can be used to trigger custom events that need to be handled outside of Anselme.
|
|
-- Can be called from an luafunction, or from outside.
|
|
interrupt = :(name, data)
|
|
if coroutine.running() == @_coroutine then
|
|
coroutine.yield(name, data)
|
|
else
|
|
table.insert(@state.interrupts, { name, data })
|
|
end
|
|
end,
|
|
|
|
--- Wrapped coroutine that returns event, data each time it is called.
|
|
-- Will run all the code at the root element, and then wait for more. It never die.
|
|
-- "text", message: text to display
|
|
-- "choice", {message1, message2, ...}: a choice. Anselme will expect an answer to be chosen using the choose method before the next call to step.
|
|
-- "end", nil: end of the script
|
|
-- Messages are tables: { text = "string", tags = { tagName = tagValue, ... } }
|
|
step = nil,
|
|
|
|
--- The VM coroutine. Do not use this directly, use :step.
|
|
_coroutine = nil
|
|
}
|
|
vm_mt.__index = vm_mt
|
|
|
|
--- Create a new Anselme VM. Load an existing state if specified.
|
|
let newVM = (state)
|
|
-- VM state/root element. Also the full element AST structure specification.
|
|
let root = state or {
|
|
-- Stuff which will still be useful later. See the comments in parse() for a description of the fields.
|
|
type = "root",
|
|
children = {},
|
|
variables = {},
|
|
line = 0,
|
|
origin = "root",
|
|
-- Useful stuff that's only on root.
|
|
chosen = nil, -- chosen answer. See the choose method.
|
|
event = nil, -- event buffer - contains { event(str), data, other } if an event is waiting to be sent.
|
|
interrupts = {}, -- list of interrupt events; they will be sent as soon as possible, regardless of the current event buffer
|
|
lastLine = 1, -- The last line run in the root element. Used to always resume at the exact right spot™ in the step method.
|
|
tags = {}, -- Currently active tags.
|
|
aliases = {}, -- Name aliases.
|
|
pendingScripts = {} -- List of files that are waiting to be loaded.
|
|
}
|
|
root.root = root
|
|
|
|
-- Create and return VM.
|
|
let vm = setmetatable({
|
|
state = root,
|
|
step = coroutine.wrap(step)
|
|
}, vm_mt)
|
|
|
|
-- Defaults functions
|
|
vm:register("↩️", function(dest, source)
|
|
root.aliases[source] = dest
|
|
end)
|
|
|
|
return vm
|
|
end
|
|
|
|
return setmetatable({
|
|
--- Anselme's version string.
|
|
VERSION = VERSION,
|
|
--- Create a new VM.
|
|
new = newVM
|
|
}, {
|
|
--- Create a new VM.
|
|
__call = ()
|
|
return newVM()
|
|
end
|
|
})
|