From 98f346ab6c6bfddc8c1214a3fce170a7f597e55b Mon Sep 17 00:00:00 2001 From: esrrhs Date: Sun, 11 Oct 2026 20:31:53 +0800 Subject: [PATCH 1/2] fix: deoptimize math-specialized functions on non-numeric arguments File-level math-specialized functions hard-failed in the TCC/GCC entry dispatcher when a math-position argument was non-numeric (string/bool/ nil/table, or nil from a missing argument). The exception escaped the dylib with no handler and aborted the process; pcall could not catch it, while the INTERP backend ran the function body normally (e.g. a type() guard returning early). - emit a __dyn full-dynamic variant for each specialized function (CompileFuncBody with spec_bitmask=-1, the mature non-specialized path); the entry dispatcher routes non-numeric math arguments to it instead of throwing, so body-level errors stay pcall-able Lua errors - add FakeluaSpecMissException: tentative native numeric compilation (CompileNumericExp etc., 7 sites) now falls back silently as normal control flow; escaping Build() still becomes the same hard compile error text as before - forward-declare __dyn in the decls section - tests: add test_spec_deopt (type guard/multi-arg/heterogeneous return/pcall/recursion across all backends and debug modes); update math_param_non_numeric_error to the unified body-level message and run on AllJitTypes(); InferGetCCode strips __dyn bodies by default so existing shape assertions target the specialized variants --- src/compile/c_gen.cpp | 77 +++++++++++++++----- src/compile/c_gen.h | 6 +- src/util/exception.h | 10 +++ test/lua/infer/test_spec_deopt.lua | 92 +++++++++++++++++++++++ test/test_exception.cpp | 7 +- test/test_infer.cpp | 113 +++++++++++++++++++++++++++-- 6 files changed, 280 insertions(+), 25 deletions(-) create mode 100644 test/lua/infer/test_spec_deopt.lua diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index 3166058..f3d80ce 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -186,8 +186,14 @@ GenResult CGen::Generate(const ParseResult &pr, const InferResult &ir, const Ana ir_ = ir; ar_ = ar; - // 运行构建主流程 - GenResult gr = Build(pr, cfg); + // 运行构建主流程。逃逸出 Build 的 spec-miss 说明该处数值特化无可回退路径, + // 仍是与历史一致的硬编译错误(含同样的 ERROR 日志与报错文本)。 + GenResult gr; + try { + gr = Build(pr, cfg); + } catch (const FakeluaSpecMissException &e) { + ThrowFakeluaException(std::format("Code generate failed, {}", e.what())); + } LOG_DEBUG(s_, "engine", "CGen::Build complete: {} functions, {} bytes of C code", gr.function_names.size(), gr.c_code.size()); @@ -574,6 +580,12 @@ void CGen::GenerateDecls(const SyntaxTreeInterfacePtr &chunk, GenResult &gr) { // 本地调用(同文件直接调用)。 gr.function_names[spec_name] = JitFunctionInfo{static_cast(func->params.size()), is_vararg, spec_name}; } + // 动态去优化变体(签名与入口分发器一致),前向声明与定义保持同步。 + Out() << "CVar " << name << "__dyn(VarClosure *_CL"; + for (size_t i = 0; i < cparams.size(); ++i) { + Out() << ", CVar " << cparams[i]; + } + Out() << ");\n"; } } } @@ -602,6 +614,12 @@ std::string CGen::CompileFuncName(const SyntaxTreeInterfacePtr &ptr) { ThrowFakeluaException(std::format("Code generate failed, {} at {}", msg, SyntaxTreeLocationStr(file_name_, ptr))); } +[[noreturn]] void CGen::ThrowSpecMiss(const std::string &msg, const SyntaxTreeInterfacePtr &ptr) { + // 与 ThrowError 文本保持一致,仅省去 BuildFakeluaErrorMessage 的 ERROR 日志: + // 这是「试不了原生就回退动态」的常规信号,每个多态函数都可能触发,不应刷错误日志。 + throw FakeluaSpecMissException(std::format("{} at {}", msg, SyntaxTreeLocationStr(file_name_, ptr))); +} + bool CGen::BlockEndsWithReturn(const SyntaxTreeInterfacePtr &block) { const auto block_ptr = std::dynamic_pointer_cast(block); const auto &stmts = block_ptr->Stmts(); @@ -687,7 +705,22 @@ void CGen::GenerateImpl(const SyntaxTreeInterfacePtr &chunk, GenResult &gr) { } Out() << "}\n"; } - GenerateEntryDispatcher(name, func->name, c_func_params, math_params); + // 动态去优化变体:与未特化函数同样按 CVar 动态编译。入口分发器在 + // 数学参数为非数值(string/bool/nil 等)时分派到这里,由函数体内的 + // Lua 层 type() 守卫/动态运算决定语义(与 INTERP 后端行为一致), + // 而不是在函数入口直接抛错导致整个进程 abort。 + const auto dyn_name = name + "__dyn"; + Out() << "CVar " << dyn_name << "(VarClosure *_CL"; + for (size_t i = 0; i < c_func_params.size(); ++i) { + Out() << ", CVar " << c_func_params[i]; + } + Out() << ") {\n"; + CompileFuncBody(func->name, func_block, -1, sections_[static_cast(Section::Impls)]); + if (!BlockEndsWithReturn(func_block)) { + Out() << " return kNil;\n"; + } + Out() << "}\n"; + GenerateEntryDispatcher(name, func->name, c_func_params, math_params, dyn_name); } else { Out() << "CVar " << name << "(VarClosure *_CL"; for (size_t i = 0; i < c_func_params.size(); ++i) { @@ -720,7 +753,8 @@ void CGen::GenerateImpl(const SyntaxTreeInterfacePtr &chunk, GenResult &gr) { } } -void CGen::GenerateEntryDispatcher(const std::string &c_name, const std::string &lua_name, const std::vector &func_params, const std::vector &math_param_indices) { +void CGen::GenerateEntryDispatcher(const std::string &c_name, const std::string &lua_name, const std::vector &func_params, const std::vector &math_param_indices, + const std::string &dyn_name) { const int k = static_cast(math_param_indices.size()); const int num_specs = 1 << k; @@ -730,13 +764,22 @@ void CGen::GenerateEntryDispatcher(const std::string &c_name, const std::string } Out() << ") {\n"; + // 任一数学参数不是数值时(string/bool/nil/table 等),spec miss → 去优化到 + // 动态变体执行,对齐 INTERP 后端:函数体内可能存在 type() 守卫提前 return, + // 或 tonumber() 转换等合法 Lua 语义,入口无权直接判错。 + Out() << " if (UNLIKELY("; for (int i = 0; i < k; ++i) { - const int param_idx = math_param_indices[i]; - const auto &mp_name = func_params[static_cast(param_idx)]; - Out() << std::format(" if (UNLIKELY({0}.type_ != VAR_INT && {0}.type_ != VAR_FLOAT)) {{ FakeluaThrowError(_S, \"bad argument #{1} ({2}): " - "attempt to perform arithmetic on non-numeric value\"); }}\n", - mp_name, param_idx + 1, mp_name); + if (i > 0) { + Out() << " || "; + } + const auto &mp_name = func_params[static_cast(math_param_indices[i])]; + Out() << std::format("({0}.type_ != VAR_INT && {0}.type_ != VAR_FLOAT)", mp_name); + } + Out() << ")) { return " << dyn_name << "(_CL"; + for (size_t i = 0; i < func_params.size(); ++i) { + Out() << ", " << func_params[i]; } + Out() << "); }\n"; Out() << " int flua_spec_idx = "; for (int i = 0; i < k; ++i) { @@ -2716,7 +2759,7 @@ std::string CGen::CompileRawNativeArithBinop(const SyntaxTreeInterfacePtr &left, Out() << GenTab() << std::format("FlToIntChecked(({}), {});\n", native_operand, itmp); return itmp; } - ThrowError("bitwise operand is not numeric", operand_node); + ThrowSpecMiss("bitwise operand is not numeric", operand_node); return {}; }; @@ -3084,7 +3127,7 @@ std::string CGen::CompileNumericExp(const SyntaxTreeInterfacePtr &exp) { if (LookupNodeType(e.get()) == T_FLOAT) { return std::format("{}", ToFloat(e->ExpValue())); } - ThrowError("number node is not inferred as numeric", exp); + ThrowSpecMiss("number node is not inferred as numeric", exp); } else if (exp_kind == ExpKind::kPrefixExp) { const auto pe = std::dynamic_pointer_cast(e->Right()); DEBUG_ASSERT(pe); @@ -3116,12 +3159,12 @@ std::string CGen::CompileNumericExp(const SyntaxTreeInterfacePtr &exp) { if (pe->GetPrefixKind() == PrefixExpKind::kFunctionCall) { const auto inferred = GetType(exp); if (inferred == T_DYNAMIC) { - ThrowError("function call cannot be specialized as numeric", exp); + ThrowSpecMiss("function call cannot be specialized as numeric", exp); } if (const auto native_result = TryCompileNativeSpecCallExpr(pe->GetValue()); !native_result.empty()) { return native_result; } - ThrowError("function call cannot be specialized as numeric", exp); + ThrowSpecMiss("function call cannot be specialized as numeric", exp); } } else if (exp_kind == ExpKind::kBinop) { const auto op = std::dynamic_pointer_cast(e->Op()); @@ -3161,7 +3204,7 @@ std::string CGen::CompileNumericExp(const SyntaxTreeInterfacePtr &exp) { if (!res.empty()) { return res; } - ThrowError("operator is not supported in numeric specialization", exp); + ThrowSpecMiss("operator is not supported in numeric specialization", exp); } else if (exp_kind == ExpKind::kUnop) { const auto op = std::dynamic_pointer_cast(e->Op()); DEBUG_ASSERT(op); @@ -3179,12 +3222,12 @@ std::string CGen::CompileNumericExp(const SyntaxTreeInterfacePtr &exp) { return res; } if (op_kind == UnOpKind::kBitNot) { - ThrowError("bitwise operand is not numeric", e->Right()); + ThrowSpecMiss("bitwise operand is not numeric", e->Right()); } - ThrowError("unary operator is not supported in numeric specialization", exp); + ThrowSpecMiss("unary operator is not supported in numeric specialization", exp); } - ThrowError("unsupported numeric-specialized expression", exp); + ThrowSpecMiss("unsupported numeric-specialized expression", exp); } // TryCompileNativeSpecCallExpr —— 将调用特化函数的结果编译为原生数值临时变量 diff --git a/src/compile/c_gen.h b/src/compile/c_gen.h index b5a0c52..d5be6c3 100644 --- a/src/compile/c_gen.h +++ b/src/compile/c_gen.h @@ -67,7 +67,9 @@ class CGen { void CompileFuncBody(const std::string &func_name, const SyntaxTreeInterfacePtr &func_block, int spec_bitmask, std::ostream &out); // 为特定函数生成分发入口(用于处理动态调用到特化强类型调用的转发) // c_name 是 C 符号(unique_c_name),lua_name 用于查 specialization_return_types。 - void GenerateEntryDispatcher(const std::string &c_name, const std::string &lua_name, const std::vector &func_params, const std::vector &math_param_indices); + // dyn_name 是动态去优化变体的 C 符号:数学参数非数值时分派到它而非抛错。 + void GenerateEntryDispatcher(const std::string &c_name, const std::string &lua_name, const std::vector &func_params, const std::vector &math_param_indices, + const std::string &dyn_name); // 辅助工具:判断一个基本块(Block)的结尾是否显式包含 return 语句 [[nodiscard]] static bool BlockEndsWithReturn(const SyntaxTreeInterfacePtr &block); // 获取特定特化签名下函数的返回类型 @@ -262,6 +264,8 @@ class CGen { // ========================================== // 代码生成阶段抛出带有上下文树节点的异常错误 [[noreturn]] void ThrowError(const std::string &msg, const SyntaxTreeInterfacePtr &ptr); + // 试探性原生数值编译失败的静默信号(不打 ERROR 日志),供回退路径捕获 + [[noreturn]] void ThrowSpecMiss(const std::string &msg, const SyntaxTreeInterfacePtr &ptr); // 根据当前缩进深度生成 C 代码缩进空白字符 [[nodiscard]] std::string GenTab() const; diff --git a/src/util/exception.h b/src/util/exception.h index 99c61c1..0f27779 100644 --- a/src/util/exception.h +++ b/src/util/exception.h @@ -11,6 +11,16 @@ class FakeluaException : public std::runtime_error { } }; +// 静默的「无法按数值特化编译」信号:CGen 试探性地把表达式编译为原生数值失败时抛出, +// 由 TryCompileNativeExpr 等回退路径捕获并改走动态编译,属于正常控制流,不记录 ERROR。 +// 若它逃逸到 CGen 顶层(调用点不允许回退),会在边界处转换为普通 FakeluaException +// 编译错误,保持与历史行为一致的对外报错。 +class FakeluaSpecMissException : public FakeluaException { +public: + explicit FakeluaSpecMissException(const std::string &msg) : FakeluaException(msg) { + } +}; + [[noreturn]] void ThrowFakeluaException(const std::string &msg); // 构造 FakeluaException 的错误文本(追加当前堆栈)并记录日志,但不抛出。 diff --git a/test/lua/infer/test_spec_deopt.lua b/test/lua/infer/test_spec_deopt.lua new file mode 100644 index 0000000..baccb53 --- /dev/null +++ b/test/lua/infer/test_spec_deopt.lua @@ -0,0 +1,92 @@ +-- Math-specialized functions must deoptimize to a dynamic variant when a math +-- parameter receives a non-numeric value (string/bool/nil/table), instead of +-- throwing a hard error at the entry dispatcher. This keeps JIT (TCC/GCC) +-- behavior consistent with the INTERP backend and with Lua semantics: the +-- function body may contain a type() guard, and real arithmetic errors must +-- remain pcall-catchable rather than aborting the whole process. + +-- type() guard: non-numeric argument takes the fallback branch. +function deopt_guard(x) + if type(x) ~= "number" then + return -1 + end + return x + 1 +end + +-- Two math parameters: any non-numeric one triggers deoptimization. +function deopt_multi(a, b) + if type(a) ~= "number" or type(b) ~= "number" then + return 0 + end + return a * 2 + b +end + +-- Dynamic variant must be allowed to return a value of a different kind +-- (string) than the numeric specialized variants. +function deopt_kind(x) + if type(x) == "number" then + return x + 100 + end + return "kind" +end + +-- No guard: arithmetic on a non-numeric value must surface as a Lua error +-- (pcall catches it), not crash the process. +function deopt_arith(x) + return x + 1 +end + +-- Recursive specialized function reached with a non-numeric argument. +function deopt_fact(n) + if type(n) ~= "number" then + return -1 + end + if n <= 1 then + return 1 + end + return n * deopt_fact(n - 1) +end + +-- In-Lua verification entry: returns 0 only when every expectation holds. +function test_deopt() + if deopt_guard(10) ~= 11 then + return 1 + end + if deopt_guard("abc") ~= -1 then + return 2 + end + if deopt_guard(true) ~= -1 then + return 3 + end + if deopt_multi(1, 2) ~= 4 then + return 4 + end + if deopt_multi("x", 2) ~= 0 then + return 5 + end + if deopt_multi(2, "y") ~= 0 then + return 6 + end + if deopt_kind(5) ~= 105 then + return 7 + end + if deopt_kind("z") ~= "kind" then + return 8 + end + if deopt_fact(5) ~= 120 then + return 9 + end + if deopt_fact("z") ~= -1 then + return 10 + end + if pcall(deopt_arith, "abc") then + return 11 + end + if pcall(deopt_arith, {}) then + return 12 + end + if deopt_arith(41) ~= 42 then + return 13 + end + return 0 +end diff --git a/test/test_exception.cpp b/test/test_exception.cpp index 7257197..bcbe66f 100644 --- a/test/test_exception.cpp +++ b/test/test_exception.cpp @@ -1156,6 +1156,9 @@ TEST(exception, function_too_many_params) { } } +// 非数值参数传入数学特化函数:入口不再直接硬失败,而是去优化到 __dyn 动态 +// 变体执行函数体;函数体内的算术仍抛错(可 pcall),三后端消息一致, +// 对齐 INTERP 后端历来行为。 TEST(exception, math_param_non_numeric_error) { FakeluaStateGuard sg; auto s = sg.GetState(); @@ -1163,14 +1166,14 @@ TEST(exception, math_param_non_numeric_error) { SetDebugLogLevel(s, 0); CompileFile(s, "./exception/test_math_param_non_numeric_error.lua", {}); - for (auto jit_type: CCodeJitTypes()) { + for (auto jit_type: AllJitTypes()) { try { CVar ret; Call(s, jit_type, "test", ret, "hello", 1); ASSERT_TRUE(false); } catch (const std::exception &e) { std::cout << e.what() << std::endl; - ASSERT_TRUE(std::string(e.what()).find("bad argument #1 (a): attempt to perform arithmetic on non-numeric value") != std::string::npos); + ASSERT_TRUE(std::string(e.what()).find("attempt to perform arithmetic on non-numeric value") != std::string::npos); } } } diff --git a/test/test_infer.cpp b/test/test_infer.cpp index 0bfda70..1a65519 100644 --- a/test/test_infer.cpp +++ b/test/test_infer.cpp @@ -16,11 +16,52 @@ static void InferRunHelper(const std::function &f) FakeluaDeleteState(s); } +// Every math-specialized function is emitted together with a dynamic +// deoptimization variant named `__dyn` (compiled with full CVar dynamics). +// Shape assertions target the specialized/regular variants ("no IsTrue", +// "no CVar local"), while the __dyn body legitimately contains that dynamic +// machinery; strip the whole __dyn definition before scanning. Forward +// declarations (lines ending with ';') are kept. Top-level function closing +// braces start at column 0 in generated code. +static std::string StripDynVariantBodies(const std::string &code) { + std::string out; + size_t pos = 0; + while (pos < code.size()) { + const size_t line_end = code.find('\n', pos); + const size_t stop = (line_end == std::string::npos) ? code.size() : line_end; + const std::string line = code.substr(pos, stop - pos); + if (line.find("__dyn(VarClosure") != std::string::npos && line.find('{') != std::string::npos) { + size_t p = (line_end == std::string::npos) ? code.size() : line_end + 1; + while (p < code.size()) { + const size_t e = code.find('\n', p); + const size_t estop = (e == std::string::npos) ? code.size() : e; + const std::string body_line = code.substr(p, estop - p); + const size_t first = body_line.find_first_not_of(" \t"); + p = (e == std::string::npos) ? code.size() : e + 1; + if (first == 0 && !body_line.empty() && body_line[0] == '}') { + break; + } + } + pos = p; + continue; + } + out += line; + out.push_back('\n'); + pos = (line_end == std::string::npos) ? code.size() : line_end + 1; + } + return out; +} + // Compile a Lua file with record_c_code=true (both JIT backends disabled so the // step is cheap) and return the recorded globals+decls+impls portion. The // returned string is also printed to stdout so that CI logs show the actual C // output for every infer test case. -static std::string InferGetCCode(const std::string &lua_file) { +// +// By default the emitted __dyn deoptimization bodies are stripped: shape +// assertions almost always target the specialized/regular variants, while the +// fully-dynamic __dyn bodies legitimately contain IsTrue/CVar locals etc. +// Pass keep_dyn_variants=true when asserting on the deopt machinery itself. +static std::string InferGetCCode(const std::string &lua_file, bool keep_dyn_variants = false) { const auto s = FakeluaNewState(); EXPECT_NE(s, nullptr); CompileConfig cfg; @@ -28,11 +69,11 @@ static std::string InferGetCCode(const std::string &lua_file) { cfg.record_c_code = true; DisableAllJit(cfg); CompileFile(s, lua_file, cfg); - const auto code = GetLastRecordedCCode(s); + const auto raw_code = GetLastRecordedCCode(s); FakeluaDeleteState(s); // Print so CI logs can be inspected even without a debugger. - std::cout << "\n=== C code for " << lua_file << " ===\n" << code << "=== end ===\n"; - return code; + std::cout << "\n=== C code for " << lua_file << " ===\n" << raw_code << "=== end ===\n"; + return keep_dyn_variants ? raw_code : StripDynVariantBodies(raw_code); } // Full inference success: all ints, constant for-loop bounds give T_INT loop var. @@ -2876,7 +2917,8 @@ TEST(infer, test_spec_repeat_local_until) { // test(5) == 6 (n+1 path), test(-1) == 0.5 (0.5 path). TEST(infer, test_spec_local_degraded_binop) { const auto code = InferGetCCode("./infer/test_spec_local_degraded_binop.lua"); - // x becomes double. + // x becomes double in the specialized variants (CVar x only exists in the + // dynamic deopt variant, which is stripped before scanning). ASSERT_NE(code.find("double x = "), std::string::npos); ASSERT_EQ(code.find("CVar x"), std::string::npos); @@ -4560,3 +4602,64 @@ TEST(infer, test_spec_literal_keys) { ASSERT_EQ(ret, 1); }); } + +// A math-specialized function must deoptimize to a dynamic variant when a math +// parameter is non-numeric, matching the INTERP backend instead of hard-failing +// at the entry dispatcher (previously TCC/GCC aborted the process here). +TEST(infer, test_spec_deopt) { + const auto code = InferGetCCode("./infer/test_spec_deopt.lua", true); + // A dynamic (__dyn) variant is declared and defined for each specialized + // function, with the same CVar signature as the entry dispatcher. + ASSERT_NE(code.find("CVar flua_fn_deopt_guard__dyn(VarClosure *_CL, CVar x);"), std::string::npos); + ASSERT_NE(code.find("CVar flua_fn_deopt_multi__dyn(VarClosure *_CL, CVar a, CVar b);"), std::string::npos); + // Dispatcher routes non-numeric math arguments to the dynamic variant. + ASSERT_NE(code.find(".type_ != VAR_INT"), std::string::npos); + ASSERT_NE(code.find("flua_fn_deopt_guard__dyn(_CL"), std::string::npos); + // The dynamic body itself is emitted (not only forward-declared). + ASSERT_NE(code.find("CVar flua_fn_deopt_guard__dyn(VarClosure *_CL, CVar x) {"), std::string::npos); + + InferRunHelper([](State *s, JITType type, bool debug_mode) { + CompileFile(s, "./infer/test_spec_deopt.lua", {.debug_mode = debug_mode}); + + int64_t ret = 0; + Call(s, type, "test_deopt", ret); + ASSERT_EQ(ret, 0); + + // Numeric path still goes through specialized variants. + Call(s, type, "deopt_guard", ret, 10); + ASSERT_EQ(ret, 11); + Call(s, type, "deopt_multi", ret, 1, 2); + ASSERT_EQ(ret, 4); + + // Non-numeric path deoptimizes into the function body (type() guards). + Call(s, type, "deopt_guard", ret, "abc"); + ASSERT_EQ(ret, -1); + Call(s, type, "deopt_guard", ret, true); + ASSERT_EQ(ret, -1); + Call(s, type, "deopt_multi", ret, "x", 2); + ASSERT_EQ(ret, 0); + Call(s, type, "deopt_multi", ret, 2, "y"); + ASSERT_EQ(ret, 0); + + // Dynamic variant may return a different kind of value (string). + ret = 0; + Call(s, type, "deopt_kind", ret, 5); + ASSERT_EQ(ret, 105); + std::string sret; + Call(s, type, "deopt_kind", sret, "z"); + ASSERT_EQ(sret, "kind"); + + // Real arithmetic errors stay pcall-catchable Lua errors instead of + // aborting the process (test_deopt already asserts pcall() == false + // in Lua; here verify the C++ boundary catches the same failure). + try { + int64_t bad = 0; + Call(s, type, "deopt_arith", bad, "abc"); + FAIL() << "expected arithmetic error for non-numeric argument"; + } catch (const std::exception &) { + // expected + } + Call(s, type, "deopt_arith", ret, 41); + ASSERT_EQ(ret, 42); + }); +} From 30f7ba188d3d0eef117b506ee99d7a53d02cab45 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Sun, 11 Oct 2026 23:21:29 +0800 Subject: [PATCH 2/2] test: expand spec deopt test coverage and update comment --- src/compile/c_gen.cpp | 2 +- test/lua/infer/test_spec_deopt.lua | 70 +++++++++++++++++++++++++----- test/test_infer.cpp | 21 +++++++++ 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index effa1a7..150a45c 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -970,7 +970,7 @@ bool CGen::TryInferMathCallBitmask(const std::string &callee_name, const std::ve for (int i = 0; i < static_cast(math_params.size()); ++i) { const int param_pos = math_params[i]; if (param_pos >= static_cast(raw_args.size())) { - return false;// fewer args than math params: let slow-path ThrowError handle it + return false;// fewer args than math params: let slow-path deopt handle it } const auto &arg = raw_args[static_cast(param_pos)]; DEBUG_ASSERT(arg && arg->Type() == SyntaxTreeType::Exp); diff --git a/test/lua/infer/test_spec_deopt.lua b/test/lua/infer/test_spec_deopt.lua index baccb53..405bbe9 100644 --- a/test/lua/infer/test_spec_deopt.lua +++ b/test/lua/infer/test_spec_deopt.lua @@ -47,6 +47,36 @@ function deopt_fact(n) return n * deopt_fact(n - 1) end +-- Specialized function without explicit return at block end: +-- exercise !BlockEndsWithReturn(func_block) in __dyn emitting "return kNil;" +function deopt_noreturn(x) + if type(x) ~= "number" then + -- no return + else + x = x + 1 + end +end + +local function dyn_callee() + return "dyn_str" +end + +-- Expressions with unsupported operators or dynamic calls trigger +-- ThrowSpecMiss during tentative numeric compilation and fall back cleanly. +function deopt_unsupported_ops(x) + if type(x) ~= "number" then + return -1 + end + if x < 0 then + local a = x + (~"abc") + local b = x + (#"abc") + local c = x + ("a" .. "b") + local d = x + dyn_callee() + return a + b + c + d + end + return x * 2 +end + -- In-Lua verification entry: returns 0 only when every expectation holds. function test_deopt() if deopt_guard(10) ~= 11 then @@ -58,35 +88,55 @@ function test_deopt() if deopt_guard(true) ~= -1 then return 3 end - if deopt_multi(1, 2) ~= 4 then + local ok4, res4 = pcall(deopt_guard) + if not ok4 or res4 ~= -1 then return 4 end - if deopt_multi("x", 2) ~= 0 then + if deopt_multi(1, 2) ~= 4 then return 5 end - if deopt_multi(2, "y") ~= 0 then + if deopt_multi("x", 2) ~= 0 then return 6 end - if deopt_kind(5) ~= 105 then + if deopt_multi(2, "y") ~= 0 then return 7 end - if deopt_kind("z") ~= "kind" then + local ok8, res8 = pcall(deopt_multi, 1) + if not ok8 or res8 ~= 0 then return 8 end - if deopt_fact(5) ~= 120 then + if deopt_kind(5) ~= 105 then return 9 end - if deopt_fact("z") ~= -1 then + if deopt_kind("z") ~= "kind" then return 10 end - if pcall(deopt_arith, "abc") then + if deopt_fact(5) ~= 120 then return 11 end - if pcall(deopt_arith, {}) then + if deopt_fact("z") ~= -1 then return 12 end - if deopt_arith(41) ~= 42 then + if pcall(deopt_arith, "abc") then return 13 end + if pcall(deopt_arith, {}) then + return 14 + end + if deopt_arith(41) ~= 42 then + return 15 + end + if deopt_noreturn(10) ~= nil then + return 16 + end + if deopt_noreturn("abc") ~= nil then + return 17 + end + if deopt_unsupported_ops(5) ~= 10 then + return 18 + end + if deopt_unsupported_ops("z") ~= -1 then + return 19 + end return 0 end diff --git a/test/test_infer.cpp b/test/test_infer.cpp index 1a65519..cced7e5 100644 --- a/test/test_infer.cpp +++ b/test/test_infer.cpp @@ -4661,5 +4661,26 @@ TEST(infer, test_spec_deopt) { } Call(s, type, "deopt_arith", ret, 41); ASSERT_EQ(ret, 42); + + // Fallback for unsupported operations inside trial numeric expressions + Call(s, type, "deopt_unsupported_ops", ret, 5); + ASSERT_EQ(ret, 10); + Call(s, type, "deopt_unsupported_ops", ret, "z"); + ASSERT_EQ(ret, -1); + + // Specialized function with no explicit return at block end + CVar nil_ret; + Call(s, type, "deopt_noreturn", nil_ret, 10); + ASSERT_EQ(nil_ret.type_, static_cast(VarType::Nil)); + Call(s, type, "deopt_noreturn", nil_ret, "abc"); + ASSERT_EQ(nil_ret.type_, static_cast(VarType::Nil)); }); } + +TEST(infer, test_spec_miss_exception) { + try { + throw FakeluaSpecMissException("test spec miss signal"); + } catch (const FakeluaSpecMissException &e) { + ASSERT_TRUE(std::string(e.what()).find("test spec miss signal") != std::string::npos); + } +}