1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-27 16:49:31 +00:00
anselme/interpreter/common.lua

699 lines
24 KiB
Lua

local atypes, ltypes
local eval, run_block
local replace_with_copied_values, fix_not_modified_references
local common
local identifier_pattern
local copy
local function random_identifier()
local r = ""
for _=1, 16 do -- that's like 10^31 possibilities, ought to be enough for anyone
r = r .. string.char(math.random(32, 126))
end
return r
end
common = {
--- merge interpreter state with global state
merge_state = function(state)
local mt = getmetatable(state.variables)
-- store current scoped variables before merging them
for fn in pairs(mt.scoped) do
common.scope:store_last_scope(state, fn)
end
-- merge alias state
local global = state.interpreter.global_state
for alias, fqm in pairs(state.aliases) do
global.aliases[alias] = fqm
state.aliases[alias] = nil
end
-- merge modified mutable varables
local copy_cache, modified_tables = mt.copy_cache, mt.modified_tables
replace_with_copied_values(global.variables, copy_cache, modified_tables)
mt.copy_cache = {}
mt.modified_tables = {}
mt.cache = {}
-- merge modified re-assigned variables
for var, value in pairs(state.variables) do
if var:match("^"..identifier_pattern.."$") then -- skip scoped variables
global.variables[var] = value
state.variables[var] = nil
end
end
-- scoping: since merging means we will re-copy every variable from global state again, we need to simulate this
-- behavious for scoped variables (to have consistent references for mutables values in particular), including
-- scopes that aren't currently active
fix_not_modified_references(mt.scoped, copy_cache, modified_tables) -- replace not modified values in scope with original before re-copying to keep consistent references
for _, scopes in pairs(mt.scoped) do
for _, scope in ipairs(scopes) do
for var, value in pairs(scope) do
-- pretend the value for this scope is the global value so the cache system perform the new copy from it
local old_var = global.variables[var]
global.variables[var] = value
state.variables[var] = nil
scope[var] = state.variables[var]
mt.cache[var] = nil
global.variables[var] = old_var
end
end
end
-- restore last scopes
for fn in pairs(mt.scoped) do
common.scope:set_last_scope(state, fn)
end
end,
--- checks if the value is compatible with the variable's (eventual) constraint
-- returns depth, or math.huge if no constraint
-- returns nil, err
check_constraint = function(state, fqm, val)
local constraint = state.variable_metadata[fqm].constraint
if constraint then
if not constraint.value then
local v, e = eval(state, constraint.pending)
if not v then
return nil, ("%s; while evaluating constraint for variable %q"):format(e, fqm)
end
constraint.value = v
end
local depth = common.is_of_type(val, constraint.value)
if not depth then
return nil, ("constraint check failed")
end
return depth
end
return math.huge
end,
--- checks if the variable is mutable
-- returns true
-- returns nil, mutation illegal message
check_mutable = function(state, fqm)
if state.variable_metadata[fqm].constant then
return nil, ("can't change the value of a constant %q"):format(fqm)
end
return true
end,
--- mark a value as constant, recursively affecting all the potentially mutable subvalues
mark_constant = function(v)
return assert(common.traverse(v, function(v)
if v.hash_id then v.hash_id = nil end -- no longer need to compare by id
end, "mark_constant"))
end,
-- traverse v and all the subvalues it contains
-- callback(v) is called on every value after traversing its subvalues
-- if pertype_callback is given, will then call the associated callback(v) in the type table for each value
-- both those callbacks can either returns nil (success) or nil, err (error)
-- returns true
-- return nil, error
traverse = function(v, callback, pertype_callback)
if atypes[v.type] and atypes[v.type].traverse then
local r, e = atypes[v.type].traverse(v.value, callback, pertype_callback)
if not r then return nil, e end
r, e = callback(v)
if e then return nil, e end
if pertype_callback and atypes[v.type][pertype_callback] then
r, e = atypes[v.type][pertype_callback](v)
if e then return nil, e end
end
return true
else
error(("don't know how to traverse type %s"):format(v.type))
end
end,
--- checks if the value can be persisted
-- returns true
-- returns nil, persist illegal message
check_persistable = function(v)
return common.traverse(v, function(v)
if v.nonpersistent then
return nil, ("can't put a non persistable %s into a persistent variable"):format(v.type)
end
end)
end,
--- returns a variable's value, evaluating a pending expression if neccessary
-- if you're sure the variable has already been evaluated, use state.variables[fqm] directly
-- return var
-- return nil, err
get_variable = function(state, fqm)
local var = state.variables[fqm]
if var.type == "pending definition" then
-- evaluate
local v, e = eval(state, var.value.expression)
if not v then
return nil, ("%s; while evaluating default value for variable %q defined at %s"):format(e, fqm, var.value.source)
end
-- make constant if variable is constant
if state.variable_metadata[fqm].constant then
v = copy(v)
common.mark_constant(v)
end
-- set variable
local s, err = common.set_variable(state, fqm, v, state.variable_metadata[fqm].constant)
if not s then return nil, err end
return v
else
return var
end
end,
--- set the value of a variable
-- returns true
-- returns nil, err
set_variable = function(state, name, val, bypass_constant_check)
if val.type ~= "pending definition" then
-- check constant
if not bypass_constant_check then
local s, e = common.check_mutable(state, name)
if not s then
return nil, ("%s; while assigning value to variable %q"):format(e, name)
end
end
-- check persistence
if state.variable_metadata[name].persistent then
local s, e = common.check_persistable(val)
if not s then
return nil, ("%s; while assigning value to variable %q"):format(e, name)
end
end
-- check constraint
local s, e = common.check_constraint(state, name, val)
if not s then
return nil, ("%s; while assigning value to variable %q"):format(e, name)
end
end
state.variables[name] = val
return true
end,
--- handle scoped function
scope = {
init_scope = function(self, state, fn)
local scoped = getmetatable(state.variables).scoped
if not fn.scoped then error("trying to initialize the scope stack for a non-scoped function") end
if not scoped[fn] then scoped[fn] = {} end
-- check scoped variables
for _, name in ipairs(fn.scoped) do
-- put fresh variable from global state in scope
local val = state.interpreter.global_state.variables[name]
if val.type ~= "undefined argument" and val.type ~= "pending definition" then -- only possibilities for scoped variable, and they're immutable
error("invalid scoped variable")
end
end
end,
--- push a new scope for this function
push = function(self, state, fn)
local scoped = getmetatable(state.variables).scoped
self:init_scope(state, fn)
-- preserve current values in last scope
self:store_last_scope(state, fn)
-- add scope
local fn_scope = {}
table.insert(scoped[fn], fn_scope)
self:set_last_scope(state, fn)
end,
--- pop the last scope for this function
pop = function(self, state, fn)
local scoped = getmetatable(state.variables).scoped
if not scoped[fn] then error("trying to pop a scope without any pushed scope") end
-- remove current scope
table.remove(scoped[fn])
-- restore last scope
self:set_last_scope(state, fn)
-- if the stack is empty,
-- we could remove mt.scoped[fn] I guess, but I don't think there's going to be a million different functions in a single game so should be ok
-- (anselme's performance is already bad enough, let's not create tables at each function call...)
end,
--- store the current values of the scoped variables in the last scope of this function
store_last_scope = function(self, state, fn)
local scopes = getmetatable(state.variables).scoped[fn]
local last_scope = scopes[#scopes]
if last_scope then
for _, name in pairs(fn.scoped) do
local val = rawget(state.variables, name)
if val then
last_scope[name] = val
end
end
end
end,
--- set scopped variables to previous scope
set_last_scope = function(self, state, fn)
local scopes = getmetatable(state.variables).scoped[fn]
for _, name in ipairs(fn.scoped) do
state.variables[name] = nil
end
local last_scope = scopes[#scopes]
if last_scope then
for name, val in pairs(last_scope) do
state.variables[name] = val
end
end
end
},
--- mark a table as modified, so it will be merged on the next checkpoint if it appears somewhere in a value
mark_as_modified = function(state, v)
local modified = getmetatable(state.variables).modified_tables
table.insert(modified, v)
end,
--- returns true if a variable should be persisted on save
-- will exclude: variable that have not been evaluated yet and non-persistent variable
-- this will by consequence excludes variable in scoped variables (can be neither persistent not evaluated into global state), constants (can not be persistent), internal anselme variables (not marked persistent), etc.
-- You may want to check afterwards with check_persistable to check if the value can actually be persisted.
should_be_persisted = function(state, name, value)
return value.type ~= "pending definition" and state.variable_metadata[name].persistent
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,
--- compare two anselme values for equality.
-- for immutable values or constants: compare by value
-- for mutable values: compare by reference
compare = function(a, b)
if a.type ~= b.type or a.constant ~= b.constant then
return false
end
if a.type == "pair" or a.type == "annotated" then
return common.compare(a.value[1], b.value[1]) and common.compare(a.value[2], b.value[2])
elseif a.type == "function reference" then
if #a.value ~= #b.value then
return false
end
for _, aname in ipairs(a.value) do
local found = false
for _, bname in ipairs(b.value) do
if aname == bname then
found = true
break
end
end
if not found then
return false
end
end
return true
-- mutable types: need to be constant
elseif a.constant and a.type == "list" then
if #a.value ~= #b.value then
return false
end
for i, v in ipairs(a.value) do
if not common.compare(v, b.value[i]) then
return false
end
end
return true
elseif a.constant and a.type == "map" then
return common.hash(a) == common.hash(b)
elseif a.constant and a.type == "object" then
if a.value.class ~= b.value.class then
return false
end
-- check every attribute redefined in a and b
-- NOTE: comparaison will fail if an attribute has been redefined in only one of the object, even if it was set to the same value as the original class attribute
local compared = {}
for name, v in pairs(a.value.attributes) do
compared[name] = true
if not b.value.attributes[name] or not common.compare(v, b.value.attributes[name]) then
return false
end
end
for name, v in pairs(b.value.attributes) do
if not compared[name] then
if not a.value.attributes[name] or not common.compare(v, a.value.attributes[name]) then
return false
end
end
end
return true
-- the rest
else
return a.value == b.value
end
end,
--- format a anselme value to something printable
-- does not call custom {}() functions, only built-in ones, so it should not be able to fail
-- 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,
--- compute a hash for a value.
-- A hash is a Lua string such as, given two values, they are considered equal by Anselme if and only if their hash are considered equal by Lua.
-- Will generate random identifiers for mutable values (equality test by reference) in order for the identifier to stay the same accross checkpoints and
-- other potential variable copies.
-- str: if success
-- nil, err: if error
hash = function(val)
if atypes[val.type] and atypes[val.type].hash then
if atypes[val.type].mutable and not val.constant then
if not val.hash_id then val.hash_id = random_identifier() end
return ("mut(%s)"):format(val.hash_id)
else
return atypes[val.type].hash(val.value)
end
else
return nil, ("no hasher for type %q"):format(val.type)
end
end,
--- recompute all the hases in a map.
-- str: if success
-- nil, err: if error
update_hashes = function(map)
for k, v in pairs(map.value) do
local hash, e = common.hash(v[1])
if not hash then return nil, e end
map[k] = nil
map[hash] = v
end
end,
--- convert anselme value to lua
-- lua value: if success (may be nil!)
-- nil, err: if error
to_lua = function(val, state)
if atypes[val.type] and atypes[val.type].to_lua then
return atypes[val.type].to_lua(val.value, state)
else
return nil, ("no Lua exporter for type %q"):format(val.type)
end
end,
--- convert lua value to anselme
-- 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,
--- evaluate a text AST into a single Lua string
-- string: if success
-- nil, err: if error
eval_text = function(state, text)
local l = {}
local s, e = common.eval_text_callback(state, text, function(str) table.insert(l, str) end)
if not s then return nil, e end
return table.concat(l)
end,
--- same as eval_text, but instead of building a Lua string, call callback for every evaluated part of the text
-- callback returns nil, err in case of error
-- true: if success
-- nil, err: if error
eval_text_callback = function(state, text, callback)
for _, item in ipairs(text) do
if type(item) == "string" then
callback(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
if v ~= "" then
local r, err = callback(v)
if err then return r, err end
end
end
end
return true
end,
--- check if an anselme value is of a certain type or annotation
-- specificity(number): if var is of type type. lower is more specific
-- false: if not
is_of_type = function(var, type)
local depth = 1
-- var has a custom annotation
if var.type == "annotated" then
-- special case: if we just want to see if a value is annotated
if type.type == "string" and type.value == "annotated" then
return depth
end
-- check annotation
local var_type = var.value[2]
while true do
if common.compare(var_type, type) then -- same type
return depth
elseif var_type.type == "annotated" then -- compare parent type
depth = depth + 1
var_type = var_type.value[2]
else -- no parent, fall back on base type
depth = depth + 1
var = var.value[1]
break
end
end
end
-- var has a base type
return type.type == "string" and type.value == var.type and depth
end,
-- return a pretty printable type value for var
pretty_type = function(var)
if var.type == "annotated" then
return common.format(var.value[2])
else
return var.type
end
end,
--- tag management
tags = {
--- push new tags on top of the stack, from Anselme values. val is expected to be a map.
push = function(self, state, val)
local new = { type = "map", value = {} }
-- copy
local last = self:current(state)
for k, v in pairs(last.value) do new.value[k] = v end
-- append new values
for k, v in pairs(val.value) do new.value[k] = v end
-- add
table.insert(state.interpreter.tags, new)
end,
--- same but do not merge with last stack item
push_no_merge = function(self, state, val)
table.insert(state.interpreter.tags, val)
end,
-- pop tag table on top of the stack
pop = function(self, state)
table.remove(state.interpreter.tags)
end,
--- return current lua tags table
current = function(self, state)
return state.interpreter.tags[#state.interpreter.tags] or { type = "map", value = {} }
end,
--- returns length of tags stack
len = function(self, state)
return #state.interpreter.tags
end,
--- pop item until we reached desired stack length
-- so in case there's a possibility to mess up the stack somehow, it will restore the stack to a good state
trim = function(self, state, len)
while #state.interpreter.tags > len do
self:pop(state)
end
end
},
--- event buffer management
-- i.e. only for text and choice events
events = {
--- add a new element to the last event in the current buffer
-- will create new event if needed
append = function(self, state, type, data)
local buffer = self:current_buffer(state)
local last = buffer[#buffer]
if not last or last.type ~= type then
last = { type = type, value = {} }
table.insert(buffer, last)
end
table.insert(last.value, data)
end,
--- new events will be collected in this event buffer (any table) until the next pop
-- this is handled by a stack so nesting is allowed
push_buffer = function(self, state, buffer)
table.insert(state.interpreter.event_buffer_stack, buffer)
end,
--- stop capturing events of a certain type.
-- must be called after a push_buffer
pop_buffer = function(self, state)
table.remove(state.interpreter.event_buffer_stack)
end,
--- returns the current buffer value
current_buffer = function(self, state)
return state.interpreter.event_buffer_stack[#state.interpreter.event_buffer_stack]
end,
-- flush event buffer if it's neccessary to push an event of the given type
-- returns true in case of success
-- returns nil, err in case of error
make_space_for = function(self, state, type)
if #state.interpreter.event_buffer_stack == 0 and state.interpreter.current_event and state.interpreter.current_event.type ~= type then
return self:manual_flush(state)
end
return true
end,
--- write all the data in a buffer into the current buffer, or to the game is no buffer is currently set
write_buffer = function(self, state, buffer)
for _, event in ipairs(buffer) do
if #state.interpreter.event_buffer_stack == 0 then
if event.type == "flush" then
local r, e = self:manual_flush(state)
if not r then return r, e end
elseif state.interpreter.current_event then
if state.interpreter.current_event.type == event.type then
for _, v in ipairs(event.value) do
table.insert(state.interpreter.current_event.value, v)
end
else
local r, e = self:manual_flush(state)
if not r then return r, e end
state.interpreter.current_event = event
end
else
state.interpreter.current_event = event
end
else
local current_buffer = self:current_buffer(state)
table.insert(current_buffer, event)
end
end
return true
end,
--- same as manual_flush but add the flush to the current buffer if one is set instead of directly to the game
flush = function(self, state)
if #state.interpreter.event_buffer_stack == 0 then
return self:manual_flush(state)
else
local current_buffer = self:current_buffer(state)
table.insert(current_buffer, { type = "flush" })
return true
end
end,
--- flush events and send them to the game if possible
-- returns true in case of success
-- returns nil, err in case of error
manual_flush = function(self, state)
while state.interpreter.current_event do
local event = state.interpreter.current_event
state.interpreter.current_event = nil
state.interpreter.skip_choices_until_flush = nil
local type = event.type
local buffer
local choices
-- copy & process text buffer
if type == "text" then
buffer = common.post_process_text(state, event.value)
-- copy & process choice buffer
elseif type == "choice" then
-- copy & process choice text content into buffer, and needed private state into choices for each choice
buffer = {}
choices = {}
for _, c in ipairs(event.value) do
table.insert(buffer, common.post_process_text(state, c))
table.insert(choices, c._state)
end
-- discard empty choices
for i=#buffer, 1, -1 do
if #buffer[i] == 0 then
table.remove(buffer, i)
table.remove(choices, i)
end
end
-- nervermind
if #choices == 0 then
return true
end
end
-- yield event
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 > #choices then
return nil, "invalid choice"
else
local choice = choices[sel]
-- execute in expected tag & event capture state
local capture_state = state.interpreter.event_capture_stack
state.interpreter.event_capture_stack = {}
common.tags:push_no_merge(state, choice.tags)
local _, e = run_block(state, choice.block)
common.tags:pop(state)
state.interpreter.event_capture_stack = capture_state
if e then return nil, e end
-- we discard return value from choice block as the execution is delayed until an event flush
-- and we don't want to stop the execution of another function unexpectedly
end
end
end
return true
end
},
--- copy some text & process it to be suited to be sent to Lua in an event
post_process_text = function(state, text)
local r = {}
-- copy into r & convert tags to lua
for _, t in ipairs(text) do
local tags = common.to_lua(t.tags, state)
if state.interpreter.base_lua_tags then
for k, v in pairs(state.interpreter.base_lua_tags) do
if tags[k] == nil then tags[k] = v end
end
end
table.insert(r, {
text = t.text,
tags = tags
})
end
-- remove trailing spaces
if state.feature_flags["strip trailing spaces"] then
local final = r[#r]
if final then
final.text = final.text:match("^(.-) *$")
if final.text == "" then
table.remove(r)
end
end
end
-- remove duplicate spaces
if state.feature_flags["strip duplicate spaces"] then
for i=1, #r-1 do
local a, b = r[i], r[i+1]
local na = #a.text:match(" *$")
local nb = #b.text:match("^ *")
if na > 0 and nb > 0 then -- remove duplicated spaces from second element first
b.text = b.text:match("^ *(.-)$")
end
if na > 1 then
a.text = a.text:match("^(.- ) *$")
end
end
end
return r
end
}
package.loaded[...] = common
local types = require((...):gsub("interpreter%.common$", "stdlib.types"))
atypes, ltypes = types.anselme, types.lua
eval = require((...):gsub("common$", "expression"))
run_block = require((...):gsub("common$", "interpreter")).run_block
local acommon = require((...):gsub("interpreter%.common$", "common"))
replace_with_copied_values, fix_not_modified_references = acommon.replace_with_copied_values, acommon.fix_not_modified_references
identifier_pattern = require((...):gsub("interpreter%.common$", "parser.common")).identifier_pattern
copy = require((...):gsub("interpreter%.common$", "common")).copy
return common