1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-27 08:39:30 +00:00
anselme/interpreter/expression.lua
Étienne Reuh Fildadut 64bc85741a Changed a few things
- Bumped to 0.15.0
- Add boot script
- Change variable definition syntax, using a = to distinguish more cleary between identifier and value
- Variables initial values are evaluated on first use instead of at parsing time
- Error on variable redefinition. Means you should make sure to load saves after your scripts.
- Improve string parsing, support for escape codes
- Remove support for number literals with empty decimal part (42. for 42.0) as there's no distinction in Anselme and it conflicts with .function call suffix
- Changed priority of : pair operator
- Add type type, and type annotations to variables and function parameters
- Change Lua function system to use regular Anselme functions
  - Defining a function from Lua is now way simpler and require providing a full Anselme function signature
- Change Anselme function system
  - Dynamic dispatch, based on arity, type annotation and parameter names. Will select the most specific function at runtime.
  - Define way to overload most operators
  - Allow custom type to text formatters
  - Allow assignment to custom functions
  - Index operator ( renamed to ()
  - Functions with parameters each have their own private namespace (scoping ersatz)
  - Internal: "custom"-mode operators now have their own expression AST type instead of cluttering the function system
- Remove static type checker as it is barely useful with new function system. May or may not rewrite one in the future.
- Improve error messages here and there
- Internal: cleaning
2021-06-03 15:29:25 +02:00

396 lines
13 KiB
Lua

local expression
local to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable
local run
local unpack = table.unpack or unpack
--- evaluate an expression
-- returns evaluated value if success
-- returns nil, error if error
local function eval(state, exp)
-- nil
if exp.type == "nil" then
return {
type = "nil",
value = nil
}
-- number
elseif exp.type == "number" then
return {
type = "number",
value = exp.value
}
-- string
elseif exp.type == "string" then
local t, e = eval_text(state, exp.value)
if not t then return t, e end
return {
type = "string",
value = t
}
-- parentheses
elseif exp.type == "parentheses" then
return eval(state, exp.expression)
-- list parentheses
elseif exp.type == "list_brackets" then
if exp.expression then
local v, e = eval(state, exp.expression)
if not v then return v, e end
if exp.expression.type == "list" then
return v
-- contained a single element, wrap in list manually
else
return {
type = "list",
value = { v }
}
end
else
return {
type = "list",
value = {}
}
end
-- list
elseif exp.type == "list" then
local l = {}
local ast = exp
while ast.type == "list" do
local left, lefte = eval(state, ast.left)
if not left then return left, lefte end
table.insert(l, left)
ast = ast.right
end
local right, righte = eval(state, ast)
if not right then return right, righte end
table.insert(l, right)
return {
type = "list",
value = l
}
-- assignment
elseif exp.type == ":=" then
if exp.left.type == "variable" then
local name = exp.left.name
local val, vale = eval(state, exp.right)
if not val then return val, vale end
state.variables[name] = val
return val
else
return nil, ("don't know how to perform assignment on %s expression"):format(exp.left.type)
end
-- lazy boolean operators
elseif exp.type == "&" then
local left, lefte = eval(state, exp.left)
if not left then return left, lefte end
if truthy(left) then
local right, righte = eval(state, exp.right)
if not right then return right, righte end
if truthy(right) then
return {
type = "number",
value = 1
}
end
end
return {
type = "number",
value = 0
}
elseif exp.type == "|" then
local left, lefte = eval(state, exp.left)
if not left then return left, lefte end
if truthy(left) then
return {
type = "number",
value = 1
}
end
local right, righte = eval(state, exp.right)
if not right then return right, righte end
return {
type = "number",
value = truthy(right) and 1 or 0
}
-- variable
elseif exp.type == "variable" then
return get_variable(state, exp.name)
-- function
elseif exp.type == "function" then
-- eval args: list_brackets
local args = {}
if exp.argument then
local arg, arge = eval(state, exp.argument)
if not arg then return arg, arge end
args = arg.value
end
-- map named arguments
local named_args = {}
for i, arg in ipairs(args) do
if arg.type == "pair" and arg.value[1].type == "string" then
named_args[arg.value[1].value] = { i, arg.value[2] }
end
end
-- eval assignment arg
local assignment
if exp.assignment then
local arge
assignment, arge = eval(state, exp.assignment)
if not assignment then return assignment, arge end
end
-- try to select a function
local tried_function_error_messages = {}
local selected_variant = { depths = { assignment = nil }, variant = nil }
for _, fn in ipairs(exp.variants) do
-- checkpoint: no args, nothing to select on
if fn.type == "checkpoint" then
if not selected_variant.variant then
selected_variant.depths = {}
selected_variant.variant = fn
else
return nil, ("checkpoint call %q is ambigous; may be at least either:\n\t%s\n\t%s"):format(exp.called_name, fn.pretty_signature, selected_variant.variant.pretty_signature)
end
-- function
elseif fn.type == "function" then
if not fn.assignment or exp.assignment then
local ok = true
-- get and set args
local used_args = {}
local depths = { assignment = nil }
for j, param in ipairs(fn.params) do
local val
-- named
if param.alias and named_args[param.alias] then
val = named_args[param.alias][2]
used_args[named_args[param.alias][1]] = true
elseif named_args[param.name] then
val = named_args[param.name][2]
used_args[named_args[param.name][1]] = true
-- vararg
elseif param.vararg then
val = { type = "list", value = {} }
for k=j, #args do
table.insert(val.value, args[k])
used_args[k] = true
end
-- positional
elseif args[j] and args[j].type ~= "pair" then
val = args[j]
used_args[j] = true
end
if val then
-- check type annotation
if param.type_annotation then
local v, e = eval(state, param.type_annotation)
if not v then return v, e end
local depth = is_of_type(val, v)
if not depth then
ok = false
table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v))
break
end
depths[j] = depth
else
depths[j] = math.huge
end
-- set
state.variables[param.full_name] = val
-- default: evaluate once function is selected
-- there's no need to type check because the type annotation is already the default value's type, because of syntax
elseif param.default then
state.variables[param.full_name] = { type = "pending definition", value = { expression = param.default, source = fn.source } }
else
ok = false
table.insert(tried_function_error_messages, ("%s: missing mandatory argument %q in function %q call"):format(fn.pretty_signature, param.name, fn.name))
break
end
end
-- check for unused arguments
if ok then
for i, arg in ipairs(args) do
if not used_args[i] then
ok = false
if arg.type == "pair" and arg.value[1].type == "string" then
table.insert(tried_function_error_messages, ("%s: unexpected %s argument"):format(fn.pretty_signature, arg.value[1].value))
else
table.insert(tried_function_error_messages, ("%s: unexpected argument in position %s"):format(fn.pretty_signature, i))
end
break
end
end
end
-- assignment arg
if ok and exp.assignment then
-- check type annotation
local param = fn.assignment
if param.type_annotation then
local v, e = eval(state, param.type_annotation)
if not v then return v, e end
local depth = is_of_type(assignment, v)
if not depth then
ok = false
table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v))
else
depths.assignment = depth
end
else
depths.assignment = math.huge
end
-- set
state.variables[param.full_name] = assignment
end
if ok then
if not selected_variant.variant then
selected_variant.depths = depths
selected_variant.variant = fn
else
-- check specificity order
local lower
for j, d in ipairs(depths) do
local current_depth = selected_variant.depths[j] or math.huge -- not every arg may be set on every variant (varargs)
if d < current_depth then -- stricly lower, i.e. more specific function
lower = true
break
elseif d > current_depth then -- stricly greater, i.e. less specific function
lower = false
break
end
end
if lower == nil and exp.assignment then -- use assignment if still ambigous
local current_depth = selected_variant.depths.assignment
if depths.assignment < current_depth then -- stricly lower, i.e. more specific function
lower = true
elseif depths.assignment > current_depth then -- stricly greater, i.e. less specific function
lower = false
end
end
if lower then
selected_variant.depths = depths
selected_variant.variant = fn
elseif lower == nil then -- equal, ambigous dispatch
return nil, ("function call %q is ambigous; may be at least either:\n\t%s\n\t%s"):format(exp.called_name, fn.pretty_signature, selected_variant.variant.pretty_signature)
end
end
end
end
else
return nil, ("unknown function type %q"):format(fn.type)
end
end
-- function successfully selected
if selected_variant.variant then
local fn = selected_variant.variant
if fn.type == "checkpoint" then
local r, e = run(state, fn.child, not exp.explicit_call)
if not r then return r, e end
return r
elseif fn.type == "function" then
local ret
-- get function vars
local checkpoint, checkpointe = get_variable(state, fn.namespace.."🔖")
if not checkpoint then return nil, checkpointe end
local seen, seene = get_variable(state, fn.namespace.."👁️")
if not seen then return nil, seene end
-- execute lua functions
-- I guess we could technically skip getting & updating the seen and checkpoints vars since they can't be used from Anselme
-- but it's also kinda fun to known how many time a function was ran
if fn.lua_function then
local lua_fn = fn.lua_function
-- get args
local final_args = {}
for j, param in ipairs(fn.params) do
local v, e = get_variable(state, param.full_name)
if not v then return v, e end
final_args[j] = v
end
if fn.assignment then
local v, e = get_variable(state, fn.assignment.full_name)
if not v then return v, e end
final_args[#final_args+1] = v
end
-- execute function
-- raw mode: pass raw anselme values to the Lua function
if lua_fn.mode == "raw" then
ret = lua_fn.value(unpack(final_args))
-- untyped raw mode: same as raw, but strips custom types from the arguments
elseif lua_fn.mode == "untyped raw" then
-- extract value from custom types
for i, arg in ipairs(final_args) do
if arg.type == "type" then
final_args[i] = arg.value[1]
end
end
ret = lua_fn.value(unpack(final_args))
-- normal mode: convert args to Lua and convert back Lua value to Anselme
elseif lua_fn.mode == nil then
local l_lua = {}
for _, v in ipairs(final_args) do
table.insert(l_lua, to_lua(v))
end
local r, e
if _VERSION == "Lua 5.1" and not jit then -- PUC Lua 5.1 doesn't allow yield from a pcall
r, e = true, lua_fn.value(unpack(l_lua))
else
r, e = pcall(lua_fn.value, unpack(l_lua)) -- pcall to produce a more informative error message (instead of full coroutine crash)
end
if r then
ret = from_lua(e)
else
return nil, ("%s; in Lua function %q"):format(e, exp.called_name)
end
else
return nil, ("unknown Lua function mode %q"):format(lua_fn.mode)
end
-- execute anselme functions
else
local e
-- eval function from start
if exp.explicit_call or checkpoint.value == "" then
ret, e = run(state, fn.child)
-- resume at last checkpoint
else
local expr, err = expression(checkpoint.value, state, "")
if not expr then return expr, err end
ret, e = eval(state, expr)
end
if not ret then return ret, e end
end
-- update function vars
state.variables[fn.namespace.."👁️"] = {
type = "number",
value = seen.value + 1
}
-- return value
if not ret then return nil, ("function %q didn't return a value"):format(exp.called_name) end
return ret
end
end
-- no matching function found
local args_txt = {}
for _, arg in ipairs(args) do
local s = ""
if arg.type == "pair" and arg.value[1].type == "string" then
s = s .. ("%s="):format(arg.value[1].value)
arg = arg.value[2]
end
s = s .. pretty_type(arg)
table.insert(args_txt, s)
end
local called_name = ("%s(%s)"):format(exp.called_name, table.concat(args_txt, ", "))
if assignment then
called_name = called_name .. " := " .. pretty_type(assignment)
end
return nil, ("no compatible function found for call to %s; potential candidates were:\n\t%s"):format(called_name, table.concat(tried_function_error_messages, "\n\t"))
else
return nil, ("unknown expression %q"):format(tostring(exp.type))
end
end
package.loaded[...] = eval
run = require((...):gsub("expression$", "interpreter")).run
expression = require((...):gsub("interpreter%.expression$", "parser.expression"))
local common = require((...):gsub("expression$", "common"))
to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable = common.to_lua, common.from_lua, common.eval_text, common.is_of_type, common.truthy, common.format, common.pretty_type, common.get_variable
return eval