1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-27 16:49:31 +00:00
anselme/anselme.can
2019-12-25 15:53:32 +01:00

1299 lines
40 KiB
Text

let VERSION = "0.11.0"
--## 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
--- 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
-- Source directories
let root = context.root
for _, dir in ipairs(root.directories) do
for j=#address, 1, -1 do
let filename = table.concat(address, "/", 1, j)
let f = io.open("%s/%s.ans":format(dir, filename), "r")
if f then
let code = "§ %s\n":format(table.concat(address, " ", 1, j))
for l in f:lines("*l") do
code ..= "\t%s\n":format(l)
end
f:close()
parse(root, code, filename)
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.
sendEvent = (root)
let e = root.event
root.event = nil
if e[1] == "text" then
coroutine.yield("text", e[2])
elseif 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)
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
-- 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 = :()
while true do
@state.lastLine = run(@state.children, @state.lastLine)
if @state.event then sendEvent(@state) end
coroutine.yield("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.
loaddirectory = :(path)
table.insert(@state.directories, 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,
--- 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
}
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.
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
directories = {} -- Source directories
}
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
})