模組:PythonScript/Evaluator/Core/Stmt
外观
-- Module:PythonScript/Evaluator/Core/Stmt
-- AST 敘述與區塊執行器 (負責 If, While, For, Try, Match, With, FunctionDef, ClassDef, Import, Goto 等)
local Stmt = {}
local PyClass = require("Module:PythonScript/Class")
local Runtime = require("Module:PythonScript/Runtime")
local Utils = require("Module:PythonScript/Evaluator/Utils")
local SIGNAL_BREAK = Utils.SIGNAL_BREAK
local SIGNAL_CONTINUE = Utils.SIGNAL_CONTINUE
local make_return = Utils.make_return
local create_scope = Utils.create_scope
local set_var = Utils.set_var
local del_var = Utils.del_var
local is_truthy = Utils.is_truthy
local resolve_index = Utils.resolve_index
local resolve_slice = Utils.resolve_slice
local py_eq = Utils.py_eq
local match_pattern = Utils.match_pattern
local make_iter_state = Utils.make_iter_state
local get_next_item = Utils.get_next_item
local assign_target = Utils.assign_target
local bind_and_exec_func = nil
function Stmt.bind_func_executor(baef)
bind_and_exec_func = baef
end
function Stmt.create(eval_expr)
local invoke = nil
local exec_stmt, exec_block
exec_block = function(stmts, scope)
local pc = 1
while pc <= #stmts do
local stmt = stmts[pc]
local sig = exec_stmt(stmt, scope)
if sig then
if sig.type == "goto" and sig.target_block == stmts then
pc = sig.target_pc
else
return sig
end
else
pc = pc + 1
end
end
return nil
end
exec_stmt = function(node, scope)
if node.line then Runtime.Traceback.current_line = node.line end
if node.type == "ExprStatement" then
if node.expr.type == "Yield" then
local val = Runtime.builtins.None
if node.expr.value then val = eval_expr(node.expr.value, scope) end
return { type = "return", value = val, is_yield = true }
elseif node.expr.type == "YieldFrom" then
local val = eval_expr(node.expr.value, scope)
return { type = "return", value = val, is_yield_from = true }
end
eval_expr(node.expr, scope)
elseif node.type == "AnnAssign" then
local ann_val = eval_expr(node.annotation, scope)
if node.target.type == "Identifier" then
local ann_dict = scope.vars["__annotations__"]
if not ann_dict then
ann_dict = { __type = "dict", pairs = {} }
scope.vars["__annotations__"] = ann_dict
end
local found = false
for _, p in ipairs(ann_dict.pairs) do
if p.key == node.target.name then
p.value = ann_val; found = true; break
end
end
if not found then
table.insert(ann_dict.pairs, { key = node.target.name, value = ann_val })
end
end
if node.value then
local val = eval_expr(node.value, scope)
assign_target(node.target, val, scope)
end
elseif node.type == "If" then
if is_truthy(eval_expr(node.test, scope)) then return exec_block(node.body, scope)
elseif node.orelse then return exec_block(node.orelse, scope) end
elseif node.type == "While" then
local break_hit = false
while is_truthy(eval_expr(node.test, scope)) do
local sig = exec_block(node.body, scope)
if sig then
if sig.type == "break" then break_hit = true; break end
if sig.type == "return" or sig.type == "goto" then return sig end
end
end
if not break_hit and node.orelse then return exec_block(node.orelse, scope) end
elseif node.type == "For" then
local iter_state = make_iter_state(eval_expr(node.iter, scope)); local break_hit = false
while true do
local has_next, v = get_next_item(iter_state); if not has_next then break end
assign_target(node.target, v, scope); local sig = exec_block(node.body, scope)
if sig then
if sig.type == "break" then break_hit = true; break end
if sig.type == "return" or sig.type == "goto" then return sig end
end
end
if not break_hit and node.orelse then return exec_block(node.orelse, scope) end
elseif node.type == "Try" then
local prev_stack_len = #Runtime.Traceback.stack
local prev_depth = Runtime.Traceback.eval_depth
local prev_file_idx = Runtime.Traceback.current_file_idx
local prev_func_name = Runtime.Traceback.current_func_name
local pending_sig, uncaught_err = nil, nil
local success, err_or_sig = pcall(function() return exec_block(node.body, scope) end)
Runtime.Traceback.eval_depth = prev_depth
if success then
if err_or_sig then
pending_sig = err_or_sig
elseif node.orelse and #node.orelse > 0 then
local else_ok, else_res = pcall(function() return exec_block(node.orelse, scope) end)
Runtime.Traceback.eval_depth = prev_depth
if else_ok then pending_sig = else_res else uncaught_err = else_res end
end
else
while #Runtime.Traceback.stack > prev_stack_len do table.remove(Runtime.Traceback.stack) end
Runtime.Traceback.current_file_idx = prev_file_idx
Runtime.Traceback.current_func_name = prev_func_name
if type(err_or_sig) == "string" and err_or_sig:lower():find("stack overflow", 1, true) then
local rec_inst = PyClass.make_instance(Runtime.builtins.RecursionError)
PyClass.set_attr(rec_inst, "msg", "maximum recursion depth exceeded")
PyClass.set_attr(rec_inst, "args", { __type = "tuple", elements = { "maximum recursion depth exceeded" } })
err_or_sig = rec_inst
end
if type(err_or_sig) ~= "table" or not err_or_sig.__class then
uncaught_err = err_or_sig
else
local handled = false
for _, handler in ipairs(node.handlers) do
local is_match = false
if not handler.type_expr then is_match = true else
local target_types = eval_expr(handler.type_expr, scope)
local function check_match(target) return PyClass.is_instance(err_or_sig, target) end
if type(target_types) == "table" and target_types.__type == "tuple" then
for _, t in ipairs(target_types.elements) do if check_match(t) then is_match = true; break end end
else is_match = check_match(target_types) end
end
if is_match then
handled = true
if handler.name then set_var(handler.name, err_or_sig, scope) end
local prev_active = Runtime.active_exception
Runtime.active_exception = err_or_sig
local h_ok, h_res = pcall(function() return exec_block(handler.body, scope) end)
Runtime.Traceback.eval_depth = prev_depth
Runtime.active_exception = prev_active
if h_ok then pending_sig = h_res else uncaught_err = h_res end
break
end
end
if not handled then uncaught_err = err_or_sig end
end
end
if node.finalbody and #node.finalbody > 0 then
local fin_sig = exec_block(node.finalbody, scope)
if fin_sig then return fin_sig end
end
if uncaught_err ~= nil then error(uncaught_err, 0) end
return pending_sig
elseif node.type == "Match" then
local subject_val = eval_expr(node.subject, scope)
for _, case_node in ipairs(node.cases) do
local case_scope = create_scope(scope)
if match_pattern(case_node.pattern, subject_val, case_scope, scope) then
local guard_pass = true
if case_node.guard then guard_pass = is_truthy(eval_expr(case_node.guard, case_scope)) end
if guard_pass then
for k, v in pairs(case_scope.vars) do scope.vars[k] = v end
local sig = exec_block(case_node.body, scope); if sig then return sig end; break
end
end
end
elseif node.type == "Global" then for _, name in ipairs(node.names) do scope.globals[name] = true end
elseif node.type == "Nonlocal" then for _, name in ipairs(node.names) do scope.nonlocals[name] = true end
elseif node.type == "Assert" then
if not is_truthy(eval_expr(node.test, scope)) then
local msg_str = ""; if node.msg then msg_str = Runtime.builtins.str.__call({eval_expr(node.msg, scope)}) end
Runtime.raise("AssertionError", msg_str)
end
elseif node.type == "Delete" then
for _, target in ipairs(node.targets) do
if target.type == "Identifier" then del_var(target.name, scope)
elseif target.type == "Index" then
local t_obj = eval_expr(target.target, scope)
if target.index.type == "Slice" then
if type(t_obj) == "table" and t_obj.__type == "instance" and t_obj.__builtin_value ~= nil then
t_obj = t_obj.__builtin_value
end
if type(t_obj) == "table" and t_obj.__type == "bytearray" then
local lower = target.index.lower and eval_expr(target.index.lower, scope) or nil
local upper = target.index.upper and eval_expr(target.index.upper, scope) or nil
local step = target.index.step and eval_expr(target.index.step, scope) or nil
local p_start, p_stop, p_step = resolve_slice(#t_obj._data, lower, upper, step)
if p_step == 1 then
t_obj._data = t_obj._data:sub(1, p_start) .. t_obj._data:sub(p_stop + 1)
else
local del_map = {}
if p_step > 0 then for i = p_start, p_stop - 1, p_step do del_map[i + 1] = true end
else for i = p_start, p_stop + 1, p_step do del_map[i + 1] = true end end
local kept = {}
for i = 1, #t_obj._data do if not del_map[i] then table.insert(kept, t_obj._data:sub(i, i)) end end
t_obj._data = table.concat(kept)
end
elseif type(t_obj) == "table" and t_obj.__type == "list" then
local lower = target.index.lower and eval_expr(target.index.lower, scope) or nil
local upper = target.index.upper and eval_expr(target.index.upper, scope) or nil
local step = target.index.step and eval_expr(target.index.step, scope) or nil
local p_start, p_stop, p_step = resolve_slice(#t_obj.elements, lower, upper, step)
local to_delete = {}
if p_step > 0 then for i = p_start, p_stop - 1, p_step do table.insert(to_delete, i + 1) end
else for i = p_start, p_stop + 1, p_step do table.insert(to_delete, i + 1) end end
table.sort(to_delete, function(a, b) return a > b end)
for _, idx in ipairs(to_delete) do table.remove(t_obj.elements, idx) end
else
Runtime.raise("TypeError", "object doesn't support slice deletion")
end
else
local idx = eval_expr(target.index, scope)
local handled_delitem = false
if type(t_obj) == "table" and t_obj.__type == "instance" then
local prev_depth = Runtime.Traceback.eval_depth
local success, attr = pcall(PyClass.get_attr, t_obj, "__delitem__")
Runtime.Traceback.eval_depth = prev_depth
if success and attr and attr.__type == "bound_method" then
bind_and_exec_func(attr.__func, {idx}, {}, attr.__self)
handled_delitem = true
elseif t_obj.__builtin_value ~= nil then
t_obj = t_obj.__builtin_value
end
end
if not handled_delitem then
if type(t_obj) == "table" and t_obj.__type == "dict" then
local found = false
for i, p in ipairs(t_obj.pairs) do
if py_eq(p.key, idx) then
table.remove(t_obj.pairs, i)
if t_obj.__scope_ref and type(idx) == "string" then t_obj.__scope_ref[idx] = nil end
found = true; break
end
end
if not found then Runtime.raise("KeyError", tostring(idx)) end
else
local lua_idx = resolve_index(t_obj, idx)
if type(t_obj) == "table" and t_obj.__type == "list" then table.remove(t_obj.elements, lua_idx)
elseif type(t_obj) == "table" and t_obj.__type == "bytearray" then t_obj._data = t_obj._data:sub(1, lua_idx - 1) .. t_obj._data:sub(lua_idx + 1)
else Runtime.raise("TypeError", "object doesn't support item deletion") end
end
end
end
elseif target.type == "Attribute" then PyClass.set_attr(eval_expr(target.target, scope), target.attr, nil) end
end
elseif node.type == "Raise" then
local exc = nil
if node.exc then
exc = eval_expr(node.exc, scope)
if type(exc) == "table" and exc.__type == "class" then exc = PyClass.make_instance(exc) end
else
exc = Runtime.active_exception
if not exc then Runtime.raise("RuntimeError", "No active exception to reraise") end
end
if node.cause then
local cause = eval_expr(node.cause, scope)
PyClass.set_attr(exc, "__cause__", cause)
PyClass.set_attr(exc, "__suppress_context__", true)
end
Runtime.raise(exc)
elseif node.type == "With" then
local managers = {}
for _, item in ipairs(node.items) do
local ctx = eval_expr(item.context_expr, scope)
local enter_func = PyClass.get_attr(ctx, "__enter__")
local exit_func = PyClass.get_attr(ctx, "__exit__")
if type(invoke) ~= type(function()end) then invoke = require("Module:PythonScript/Evaluator").invoke end
local enter_res = invoke(enter_func, {}, {})
table.insert(managers, { exit_func = exit_func, ctx = ctx })
if item.optional_vars then assign_target(item.optional_vars, enter_res, scope) end
end
local prev_stack_len = #Runtime.Traceback.stack
local prev_depth = Runtime.Traceback.eval_depth
local prev_file_idx = Runtime.Traceback.current_file_idx
local prev_func_name = Runtime.Traceback.current_func_name
local success, sig = pcall(function() return exec_block(node.body, scope) end)
Runtime.Traceback.eval_depth = prev_depth
if not success then
while #Runtime.Traceback.stack > prev_stack_len do table.remove(Runtime.Traceback.stack) end
Runtime.Traceback.current_file_idx = prev_file_idx
Runtime.Traceback.current_func_name = prev_func_name
if type(sig) == "string" and sig:lower():find("stack overflow", 1, true) then
local rec_inst = PyClass.make_instance(Runtime.builtins.RecursionError)
PyClass.set_attr(rec_inst, "msg", "maximum recursion depth exceeded")
PyClass.set_attr(rec_inst, "args", { __type = "tuple", elements = { "maximum recursion depth exceeded" } })
sig = rec_inst
end
end
local None = Runtime.builtins.None
if type(invoke) ~= type(function()end) then invoke = require("Module:PythonScript/Evaluator").invoke end
for i = #managers, 1, -1 do
local m = managers[i]
local exc_type, exc_val, tb = None, None, None
if not success then
exc_val = sig
exc_type = (type(exc_val) == "table" and exc_val.__class) or None
tb = (type(exc_val) == "table" and exc_val.__dict and exc_val.__dict.__traceback_info__) or None
end
local suppress = invoke(m.exit_func, {exc_type, exc_val, tb}, {})
if not success and is_truthy(suppress) then success = true; sig = nil end
end
if not success then error(sig, 0) end
return sig
elseif node.type == "FunctionDef" then
local defaults_val = {}; if node.defaults then for arg_name, expr_node in pairs(node.defaults) do defaults_val[arg_name] = eval_expr(expr_node, scope) end end
local ann_pairs = {}
if node.annotations then
for _, arg_name in ipairs(node.args or {}) do
if node.annotations[arg_name] then
table.insert(ann_pairs, { key = arg_name, value = eval_expr(node.annotations[arg_name], scope) })
end
end
if node.vararg and node.annotations[node.vararg] then
table.insert(ann_pairs, { key = node.vararg, value = eval_expr(node.annotations[node.vararg], scope) })
end
for _, kw_name in ipairs(node.kwonlyargs or {}) do
if node.annotations[kw_name] then
table.insert(ann_pairs, { key = kw_name, value = eval_expr(node.annotations[kw_name], scope) })
end
end
if node.kwarg and node.annotations[node.kwarg] then
table.insert(ann_pairs, { key = node.kwarg, value = eval_expr(node.annotations[node.kwarg], scope) })
end
end
if node.returns then
table.insert(ann_pairs, { key = "return", value = eval_expr(node.returns, scope) })
end
local curr = scope; while curr.parent do curr = curr.parent end; local mod_name = curr.vars["__name__"] or "__main__"
local qname = node.name; if scope.is_class_scope then qname = scope.class_name .. "." .. node.name end
local bind_scope = scope
if scope.is_class_scope then bind_scope = scope.parent end
local func_obj = {
__type = "function", __name = node.name, __qualname__ = qname,
__node = node, __scope = bind_scope, __defaults = defaults_val,
__module__ = mod_name,
__dict = { __annotations__ = { __type = "dict", pairs = ann_pairs } }
}
local final_obj = func_obj
if node.decorator_list and #node.decorator_list > 0 then
for i = #node.decorator_list, 1, -1 do
local deco = eval_expr(node.decorator_list[i], scope)
final_obj = eval_expr({ type = "Call", callee = { type = "DirectValue", value = deco }, args = { { type = "DirectValue", value = final_obj } }, kwargs = {} }, scope)
end
end
set_var(node.name, final_obj, scope)
elseif node.type == "ClassDef" then
local bases = {}
for _, b_node in ipairs(node.bases or {}) do
if b_node.type == "Starred" then
local iter_state = make_iter_state(eval_expr(b_node.value, scope))
while true do
local has_next, v = get_next_item(iter_state); if not has_next then break end
table.insert(bases, v)
end
else
table.insert(bases, eval_expr(b_node, scope))
end
end
local passed_kwargs = {}
for _, kw_node in ipairs(node.kwargs or {}) do
if kw_node.key then
local v = eval_expr(kw_node.value, scope)
if passed_kwargs[kw_node.key] ~= nil then
Runtime.raise("TypeError", "got multiple values for keyword argument '" .. tostring(kw_node.key) .. "'")
end
passed_kwargs[kw_node.key] = v
else
local dict_obj = eval_expr(kw_node.value, scope)
if type(dict_obj) == "table" and dict_obj.__type == "dict" then
for _, pair in ipairs(dict_obj.pairs) do
if passed_kwargs[pair.key] ~= nil then
Runtime.raise("TypeError", "got multiple values for keyword argument '" .. tostring(pair.key) .. "'")
end
passed_kwargs[pair.key] = pair.value
end
else Runtime.raise("TypeError", "argument after ** must be a mapping") end
end
end
local explicit_meta = passed_kwargs["metaclass"]
passed_kwargs["metaclass"] = nil
local meta = PyClass.resolve_metaclass(explicit_meta, bases)
local prep_ns = PyClass.prepare_namespace(meta, node.name, bases, passed_kwargs)
local curr = scope; while curr.parent do curr = curr.parent end
local mod_name = curr.vars["__name__"] or "__main__"
local class_scope = create_scope(scope)
class_scope.is_class_scope = true
class_scope.class_name = node.name
class_scope.custom_ns = prep_ns
if type(prep_ns) == "table" and prep_ns.__type == "dict" then
for _, p in ipairs(prep_ns.pairs or {}) do
if type(p.key) == "string" then class_scope.vars[p.key] = p.value end
end
end
set_var("__module__", mod_name, class_scope)
set_var("__qualname__", node.name, class_scope)
local prev_func_name = Runtime.Traceback.current_func_name or "<module>"
local prev_line = Runtime.Traceback.current_line
table.insert(Runtime.Traceback.stack, {Runtime.Traceback.current_file_idx, prev_func_name, node.line or prev_line})
Runtime.Traceback.current_func_name = node.name
exec_block(node.body, class_scope)
table.remove(Runtime.Traceback.stack)
Runtime.Traceback.current_func_name = prev_func_name
local class_obj
if meta == Runtime.builtins.type then
class_obj = PyClass.make_class(node.name, bases, class_scope.vars)
PyClass.init_class_hooks(class_obj, meta, passed_kwargs)
else
if type(invoke) ~= type(function()end) then invoke = require("Module:PythonScript/Evaluator").invoke end
local bases_tup = { __type = "tuple", elements = bases }
local ns_arg = prep_ns
if type(ns_arg) == "table" and ns_arg.__type == "dict" then
ns_arg.__scope_ref = class_scope.vars
elseif type(ns_arg) == "table" then
ns_arg._class_vars = class_scope.vars
end
class_obj = invoke(meta, { node.name, bases_tup, ns_arg }, passed_kwargs)
end
local final_obj = class_obj
if node.decorator_list and #node.decorator_list > 0 then
for i = #node.decorator_list, 1, -1 do
local deco = eval_expr(node.decorator_list[i], scope)
final_obj = eval_expr({ type = "Call", callee = { type = "DirectValue", value = deco }, args = { { type = "DirectValue", value = final_obj } } }, scope)
end
end
set_var(node.name, final_obj, scope)
elseif node.type == "Import" then
local ImportMod = require("Module:PythonScript/Import")
local root_scope = scope.parent or scope
local function resolve_sub_import(mod_name)
local ok, mod = pcall(ImportMod.execute_import, mod_name, root_scope)
if ok then return mod end
local root_pkg, rest = mod_name:match("^([^%.]+)%.(.+)$")
if root_pkg and rest then
local ok_r, pkg = pcall(ImportMod.execute_import, root_pkg, root_scope)
if ok_r and pkg then
local cur = pkg
for part in rest:gmatch("[^%.]+") do
local ok_a, nxt = pcall(PyClass.get_attr, cur, part)
if not ok_a or nxt == nil then error(mod, 0) end
cur = nxt
end
return cur
end
end
error(mod, 0)
end
local items = node.names or { { name = node.name, alias = node.alias } }
for _, item in ipairs(items) do
local imported_obj = resolve_sub_import(item.name)
local target_name = item.alias or item.name:match("([^/%.%:-]+)$") or item.name
set_var(target_name, imported_obj, scope)
if item.name == "goto" and scope.vars["with_goto"] == nil then
local ok_wg, wg = pcall(PyClass.get_attr, imported_obj, "with_goto")
if ok_wg and wg then set_var("with_goto", wg, scope) end
end
end
elseif node.type == "FromImport" then
local ImportMod = require("Module:PythonScript/Import")
local root_scope = scope; while root_scope.parent do root_scope = root_scope.parent end
local function resolve_sub_import(mod_name)
local d_save = Runtime.Traceback.eval_depth
local ok, mod = pcall(ImportMod.execute_import, mod_name, root_scope)
Runtime.Traceback.eval_depth = d_save
if ok then return true, mod end
local root_pkg, rest = mod_name:match("^([^%.]+)%.(.+)$")
if root_pkg and rest then
local ok_r, pkg = pcall(ImportMod.execute_import, root_pkg, root_scope)
Runtime.Traceback.eval_depth = d_save
if ok_r and pkg then
local cur = pkg
local all_ok = true
for part in rest:gmatch("[^%.]+") do
local ok_a, nxt = pcall(PyClass.get_attr, cur, part)
Runtime.Traceback.eval_depth = d_save
if ok_a and nxt ~= nil then cur = nxt else all_ok = false; break end
end
if all_ok then return true, cur end
end
end
return false, mod
end
local ok_pkg, imported_obj = resolve_sub_import(node.module)
for _, item in ipairs(node.names) do
if item.name == "*" then
if not ok_pkg then error(imported_obj, 0) end
scope.star_imports = scope.star_imports or {}
table.insert(scope.star_imports, imported_obj)
else
local val
local found = false
if ok_pkg then
local prev_depth = Runtime.Traceback.eval_depth
local success, res = pcall(PyClass.get_attr, imported_obj, item.name)
Runtime.Traceback.eval_depth = prev_depth
if success then val = res; found = true end
end
if not found then
local sub_module_name = (node.module:match("^%.+$") and (node.module .. item.name)) or (node.module .. "." .. item.name)
local success2, sub_mod = resolve_sub_import(sub_module_name)
if success2 then
val = sub_mod
elseif not ok_pkg then
error(imported_obj, 0)
else
Runtime.raise("ImportError", "cannot import name '" .. item.name .. "' from '" .. node.module .. "'")
end
end
local target_name = item.alias or item.name; set_var(target_name, val, scope)
end
end
elseif node.type == "Return" then
local val = Runtime.builtins.None
if node.value then val = eval_expr(node.value, scope) end
return make_return(val)
elseif node.type == "Break" then return SIGNAL_BREAK
elseif node.type == "Continue" then return SIGNAL_CONTINUE
elseif node.type == "Pass" then
elseif node.type == "Label" then
if not scope.__goto_ctx then
Runtime.raise("NameError", "name 'label' is not defined")
end
elseif node.type == "Goto" then
local gctx = scope.__goto_ctx
if not gctx then
Runtime.raise("NameError", "name 'goto' is not defined")
end
local target_info = gctx.labels[node.target]
if not target_info then
Runtime.raise("NameError", "name '" .. tostring(node.target) .. "' is not defined")
end
gctx.jumps = gctx.jumps + 1
if gctx.jumps > 100000 then
Runtime.raise("RuntimeError", "maximum goto jump limit (100000) exceeded")
end
return {
type = "goto",
target = node.target,
target_block = target_info.block,
target_pc = target_info.pc,
line = node.line
}
end
end
return exec_stmt, exec_block
end
return Stmt