mirror of
https://github.com/Reuh/anselme.git
synced 2025-10-27 16:49:31 +00:00
528 lines
17 KiB
Lua
528 lines
17 KiB
Lua
local atypes, ltypes
|
|
local eval, run_block
|
|
local replace_with_copied_values, fix_not_modified_references
|
|
local common
|
|
local identifier_pattern
|
|
|
|
--- copy some text & process it to be suited to be sent to Lua in an event
|
|
local function post_process_text(state, text)
|
|
local r = {}
|
|
-- copy into r & convert tags to lua
|
|
for _, t in ipairs(text) do
|
|
local tags = common.to_lua(t.tags)
|
|
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
|
|
|
|
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,
|
|
--- 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
|
|
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
|
|
state.variables[fqm] = v
|
|
return v
|
|
else
|
|
return var
|
|
end
|
|
end,
|
|
--- set the value of a variable
|
|
set_variable = function(state, name, val)
|
|
state.variables[name] = val
|
|
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: undefined variables, variables in scoped functions, internal anselme variables
|
|
should_keep_variable = function(state, name, value)
|
|
return value.type ~= "undefined argument" and value.type ~= "pending definition" and name:match("^"..identifier_pattern.."$") and not name:match("^anselme%.")
|
|
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 value for equality
|
|
compare = function(a, b)
|
|
if a.type ~= b.type then
|
|
return false
|
|
end
|
|
if a.type == "pair" or a.type == "type" then
|
|
return common.compare(a.value[1], b.value[1]) and common.compare(a.value[2], b.value[2])
|
|
elseif 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.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
|
|
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,
|
|
--- convert anselme value to lua
|
|
-- lua value: if success (may be nil!)
|
|
-- nil, err: if error
|
|
to_lua = function(val)
|
|
if atypes[val.type] and atypes[val.type].to_lua then
|
|
return atypes[val.type].to_lua(val.value)
|
|
else
|
|
return nil, ("no Lua exporter for type %q"):format(val.type)
|
|
end
|
|
end,
|
|
--- 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 = {}
|
|
common.eval_text_callback(state, text, function(str) table.insert(l, str) 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
|
|
-- 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 type
|
|
if var.type == "type" then
|
|
local var_type = var.value[2]
|
|
while true do
|
|
if common.compare(var_type, type) then -- same type
|
|
return depth
|
|
elseif var_type.type == "type" 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 == "type" 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
|
|
push = function(self, state, val)
|
|
local new = { type = "list", value = {} }
|
|
-- copy
|
|
local last = self:current(state)
|
|
for _, v in ipairs(last.value) do table.insert(new.value, v) end
|
|
-- append new values
|
|
if val.type ~= "list" then val = { type = "list", value = { val } } end
|
|
for _, v in ipairs(val.value) do table.insert(new.value, 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 = "list", 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 -- FIXME useful?
|
|
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 = 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, 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
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
return common
|