diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index 075bc173..844119f3 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -3669,7 +3669,8 @@ std::string CGen::TryCompileBuiltinMathCall(const std::shared_ptrExps(); static const std::unordered_set math_builtins = {"abs", "floor", "ceil", "max", "min", "sqrt", "sin", "cos", "tan", "pow", "deg", "rad", "random", - "randomseed", "modf", "frexp", "exp", "log", "log10", "asin", "acos", "atan", "sinh", "cosh", "tanh"}; + "randomseed", "modf", "frexp", "exp", "log", "log10", "asin", "acos", "atan", "sinh", "cosh", "tanh", + "fmod", "ldexp", "type", "tointeger", "ult"}; if (!math_builtins.contains(method_name)) { return {}; @@ -3886,20 +3887,22 @@ std::string CGen::TryCompileBuiltinMathCall(const std::shared_ptr= 2) { diff --git a/src/compile/semantic_analysis.h b/src/compile/semantic_analysis.h index a6db9af7..4f8465c5 100644 --- a/src/compile/semantic_analysis.h +++ b/src/compile/semantic_analysis.h @@ -24,6 +24,9 @@ class SemanticAnalysis { // 运行语义分析 AnalysisResult Analyze(const ParseResult &pr, const CompileConfig &cfg); + // Collect return statements under a syntax subtree (also used by focused coverage tests). + void CollectReturnsForBlock(const SyntaxTreeInterfacePtr &node, std::vector &returns); + private: void AnalyzeGlobalConstNames(const SyntaxTreeInterfacePtr &chunk, AnalysisResult &ar); void CheckUnsupportedSyntax(const SyntaxTreeInterfacePtr &chunk, const AnalysisResult &ar); @@ -65,7 +68,6 @@ class SemanticAnalysis { [[noreturn]] void ThrowError(const std::string &msg, const SyntaxTreeInterfacePtr &ptr); void AnalyzeFunctionReturnCounts(const SyntaxTreeInterfacePtr &chunk, AnalysisResult &ar); - void CollectReturnsForBlock(const SyntaxTreeInterfacePtr &node, std::vector &returns); std::string GetCalleeName(const SyntaxTreeInterfacePtr &exp_node); private: diff --git a/test/lua/basic/test_file_level_literals.lua b/test/lua/basic/test_file_level_literals.lua new file mode 100644 index 00000000..af8c4f2d --- /dev/null +++ b/test/lua/basic/test_file_level_literals.lua @@ -0,0 +1,26 @@ +-- File-level locals for LiteralFromExp float/unop paths in interp codegen. +-- These locals MUST stay in the same file as test_file_level_literals. +local cov_neg = -42 +local cov_flt = 1.25 +local cov_paren = (-3.5) + +function test_file_level_literals() + if cov_neg ~= -42 then return 1 end + if cov_flt ~= 1.25 then return 2 end + if cov_paren ~= -3.5 then return 3 end + return 0 +end + +function test_bool_nil_locals() + local a = nil + local b = true + local c = false + if a ~= nil then return 1 end + if b ~= true then return 2 end + if c ~= false then return 3 end + return 0 +end + +function test_empty_body() + return 0 +end diff --git a/test/lua/basic/test_interp_errors.lua b/test/lua/basic/test_interp_errors.lua new file mode 100644 index 00000000..83a3d6ab --- /dev/null +++ b/test/lua/basic/test_interp_errors.lua @@ -0,0 +1,52 @@ +-- Interpreter error and edge paths (len/index/for-step/bitwise/_VERSION). +function test_interp_errors() + local ok, err = pcall(function() return #123 end) + if ok then return 1 end + ok, err = pcall(function() + local t = 1 + return t[1] + end) + if ok then return 2 end + ok, err = pcall(function() + local t = {} + return t[nil] + end) + if ok then return 3 end + local function zero_step() + return 0 + end + ok, err = pcall(function() + for i = 1, 10, zero_step() do + return i + end + end) + if ok then return 4 end + local function zero_fstep() + return 0.0 + end + ok, err = pcall(function() + for i = 1.0, 10.0, zero_fstep() do + return i + end + end) + if ok then return 5 end + -- _VERSION global + if type(_VERSION) ~= "string" then return 6 end + local function nested_local_fn() + return 7 + end + if nested_local_fn() ~= 7 then return 8 end + -- for-limit edge: huge float limit with negative step should skip + local n = 0 + for i = 1, 1e308, -1 do + n = n + 1 + if n > 2 then break end + end + for i = 1, -1e308, 1 do + n = n + 100 + end + -- bitwise on non-number + ok = pcall(function() return 1 & "x" end) + if ok then return 9 end + return 0 +end diff --git a/test/lua/basic/test_local_multi_assign.lua b/test/lua/basic/test_local_multi_assign.lua new file mode 100644 index 00000000..fb89cb01 --- /dev/null +++ b/test/lua/basic/test_local_multi_assign.lua @@ -0,0 +1,12 @@ +-- Local multi-assign with multi-return RHS. +function returns_two() + return 10, 20 +end + +function test_local_multi_assign() + local a, b, c = 1, returns_two() + if a ~= 1 or b ~= 10 or c ~= 20 then return 1 end + local x, y = returns_two() + if x ~= 10 or y ~= 20 then return 2 end + return 0 +end diff --git a/test/lua/basic/test_select_and_closure_args.lua b/test/lua/basic/test_select_and_closure_args.lua new file mode 100644 index 00000000..a6b9f6f2 --- /dev/null +++ b/test/lua/basic/test_select_and_closure_args.lua @@ -0,0 +1,19 @@ +-- select(), many-arg calls, and multi-return flattening. +function many_args(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) + return a1+a2+a3+a4+a5+a6+a7+a8+a9+a10 +end + +function test_select_and_closure_args() + if select("#", 1, 2, 3) ~= 3 then return 1 end + if select(2, 10, 20, 30) ~= 20 then return 2 end + local f = function(...) + return select("#", ...) + end + if f(1, 2, 3, 4) ~= 4 then return 3 end + if many_args(1,2,3,4,5,6,7,8,9,10) ~= 55 then return 4 end + -- closure call with multi returns flattened + local function ret_multi() return 1, 2, 3 end + local function take3(a, b, c) return a + b + c end + if take3(ret_multi()) ~= 6 then return 5 end + return 0 +end diff --git a/test/lua/math/test_math_inline_extra.lua b/test/lua/math/test_math_inline_extra.lua new file mode 100644 index 00000000..0e899432 --- /dev/null +++ b/test/lua/math/test_math_inline_extra.lua @@ -0,0 +1,17 @@ +-- Extra math inline / native fallback coverage. +function test_math_inline_extra() + if math.abs(math.fmod(10, 3) - 1) > 1e-9 then return 1 end + if math.abs(math.ldexp(2.0, 3) - 16.0) > 1e-9 then return 2 end + if math.type(7) ~= "integer" then return 3 end + if math.type(7.5) ~= "float" then return 4 end + if math.type("x") ~= nil then return 5 end + if math.tointeger(9.0) ~= 9 then return 6 end + if math.tointeger(9.2) ~= nil then return 7 end + if not math.ult(1, 2) then return 8 end + if math.ult(-1, 1) then return 9 end + local ok = pcall(function() return math.fmod("a", 1) end) + if ok then return 10 end + -- string -> tointeger native fallback + if math.tointeger("11") ~= 11 then return 11 end + return 0 +end diff --git a/test/lua/string/test_string_escapes_and_ops.lua b/test/lua/string/test_string_escapes_and_ops.lua new file mode 100644 index 00000000..fbc4c685 --- /dev/null +++ b/test/lua/string/test_string_escapes_and_ops.lua @@ -0,0 +1,19 @@ +-- String escape compile paths and extra string ops coverage. +function test_string_escapes() + local s = "a\001b\n\tc\127d" + if #s < 5 then return 1 end + if string.byte(s, 2) ~= 1 then return 2 end + return 0 +end + +function test_string_ops_extra() + if string.lower("AbC") ~= "abc" then return 1 end + if string.upper("AbC") ~= "ABC" then return 2 end + if string.reverse("ab") ~= "ba" then return 3 end + if string.rep("a", 3) ~= "aaa" then return 4 end + if string.byte("ABC", 2) ~= 66 then return 5 end + if string.char(65) ~= "A" then return 6 end + if string.find("hello", "ll", 1, true) ~= 3 then return 7 end + if tonumber("3.5") ~= 3.5 then return 8 end + return 0 +end diff --git a/test/lua/table/test_table_seq_and_move.lua b/test/lua/table/test_table_seq_and_move.lua new file mode 100644 index 00000000..e342dd66 --- /dev/null +++ b/test/lua/table/test_table_seq_and_move.lua @@ -0,0 +1,23 @@ +-- Table sequence length, insert/remove, and move coverage. +function test_table_seq_and_move() + local t = {10, 20, 30, 40, 50} + if #t ~= 5 then return 1 end + -- force hash path mixed with array + t["k"] = "v" + if t[1] ~= 10 or t.k ~= "v" then return 2 end + -- table.move / insert / remove paths + table.insert(t, 2, 15) + if t[2] ~= 15 then return 3 end + local r = table.remove(t, 2) + if r ~= 15 then return 4 end + local dst = {} + table.move(t, 1, 3, 1, dst) + if dst[1] ~= 10 or dst[3] ~= 30 then return 5 end + -- sparse then fill for seq scan + local s = {} + s[1] = 1 + s[3] = 3 + s[2] = 2 + if #s < 2 then return 6 end + return 0 +end diff --git a/test/test_common.cpp b/test/test_common.cpp index 9e9e3e1b..3411c6e1 100644 --- a/test/test_common.cpp +++ b/test/test_common.cpp @@ -3,6 +3,7 @@ #include "util/macro.h" #include "util/string_util.h" #include "gtest/gtest.h" +#include #include #include @@ -341,20 +342,47 @@ TEST(common, ToFloat_hex_errors) { } #include "compile/c_runtime_header.h" +#include "compile/compile_common.h" +#include "compile/syntax_tree.h" +#include "test_jit.h" +#include +#include +#include // Keep c_runtime_cov.h (Codecov view / #line target) identical to kCRuntimeHeader's // raw-string body. The string begins with a leading '\n' that is not in the file. TEST(common, c_runtime_cov_matches_kCRuntimeHeader) { - const char *home = std::getenv("FAKELUA_HOME"); + std::vector candidates; + if (const char *home = std::getenv("FAKELUA_HOME"); home && *home) { + candidates.emplace_back(std::filesystem::path(home) / kCRuntimeCovPath); + } + // unit_tests usually run with cwd=test/lua; shards may use a temp cwd with symlinks. + candidates.emplace_back(std::filesystem::path("../../") / kCRuntimeCovPath); + candidates.emplace_back(std::filesystem::path("../..") / "src" / "compile" / "c_runtime_cov.h"); + candidates.emplace_back(std::filesystem::current_path() / ".." / ".." / kCRuntimeCovPath); + // Walk up from cwd looking for the repo-rooted header. + { + auto cur = std::filesystem::current_path(); + for (int i = 0; i < 6; ++i) { + candidates.emplace_back(cur / kCRuntimeCovPath); + if (!cur.has_parent_path() || cur == cur.parent_path()) break; + cur = cur.parent_path(); + } + } std::filesystem::path cov_path; - if (home && *home) { - cov_path = std::filesystem::path(home) / kCRuntimeCovPath; - } else { - // unit_tests usually run with cwd=test/lua - cov_path = std::filesystem::path("../../") / kCRuntimeCovPath; + std::ifstream ifs; + for (const auto &cand: candidates) { + std::error_code ec; + auto canon = std::filesystem::weakly_canonical(cand, ec); + if (ec) continue; + ifs.open(canon); + if (ifs.good()) { + cov_path = canon; + break; + } + ifs.close(); } - std::ifstream ifs(cov_path); - ASSERT_TRUE(ifs.good()) << "missing view header: " << cov_path; + ASSERT_TRUE(ifs.good()) << "missing view header; tried FAKELUA_HOME/cwd parents for " << kCRuntimeCovPath; std::ostringstream oss; oss << ifs.rdbuf(); const std::string file_body = oss.str(); @@ -362,3 +390,316 @@ TEST(common, c_runtime_cov_matches_kCRuntimeHeader) { EXPECT_EQ(file_body, expected) << "Edit kCRuntimeHeader and sync " << kCRuntimeCovPath << " (or the reverse); Codecov #line mapping depends on it."; } + +namespace { + +yy::location CovLoc() { + return yy::location(); +} + +SyntaxTreeInterfacePtr CovMakeNumberExp(const std::string &v) { + auto e = std::make_shared(CovLoc()); + e->SetExpKind(ExpKind::kNumber); + e->SetValue(v); + return e; +} + +SyntaxTreeInterfacePtr CovMakeStringExp(const std::string &v) { + auto e = std::make_shared(CovLoc()); + e->SetExpKind(ExpKind::kString); + e->SetValue(v); + return e; +} + +SyntaxTreeInterfacePtr CovMakeSimpleVar(const std::string &name) { + auto v = std::make_shared(CovLoc()); + v->SetVarKind(VarKind::kSimple); + v->SetName(name); + return v; +} + +SyntaxTreeInterfacePtr CovMakePrefixVar(const std::string &name) { + auto pe = std::make_shared(CovLoc()); + pe->SetPrefixKind(PrefixExpKind::kVar); + pe->SetValue(CovMakeSimpleVar(name)); + return pe; +} + +SyntaxTreeInterfacePtr CovMakePrefixCall(const std::string &name) { + auto pe = std::make_shared(CovLoc()); + pe->SetPrefixKind(PrefixExpKind::kFunctionCall); + pe->SetValue(CovMakeSimpleVar(name)); + return pe; +} + +SyntaxTreeInterfacePtr CovMakeVarPrefixExp(const std::string &name) { + auto e = std::make_shared(CovLoc()); + e->SetExpKind(ExpKind::kPrefixExp); + e->SetRight(CovMakePrefixVar(name)); + return e; +} + +SyntaxTreeInterfacePtr CovMakeCallPrefixExp(const std::string &name) { + auto e = std::make_shared(CovLoc()); + e->SetExpKind(ExpKind::kPrefixExp); + e->SetRight(CovMakePrefixCall(name)); + return e; +} + +SyntaxTreeInterfacePtr CovMakeUnaryMinus(const SyntaxTreeInterfacePtr &right) { + auto op = std::make_shared(CovLoc()); + op->SetOpKind(UnOpKind::kMinus); + auto e = std::make_shared(CovLoc()); + e->SetExpKind(ExpKind::kUnop); + e->SetOp(op); + e->SetRight(right); + return e; +} + +}// namespace + +TEST(common, compile_math_param_helpers) { + EXPECT_EQ(MathParamKindOf(0, 0), kMathParamInt); + EXPECT_EQ(MathParamKindOf(1, 0), kMathParamFloat); + EXPECT_EQ(MathParamKindOf(2, 1), kMathParamFloat); + EXPECT_STREQ(MathParamCTypeName(kMathParamInt), "int64_t"); + EXPECT_STREQ(MathParamCTypeName(kMathParamFloat), "double"); + EXPECT_EQ(MathParamSuffix(kMathParamInt), '0'); + EXPECT_EQ(MathParamSuffix(kMathParamFloat), '1'); + + EXPECT_EQ(InferredTypeToString(T_UNKNOWN), "T_UNKNOWN"); + EXPECT_EQ(InferredTypeToString(T_INT), "T_INT"); + EXPECT_EQ(InferredTypeToString(T_FLOAT), "T_FLOAT"); + EXPECT_EQ(InferredTypeToString(T_DYNAMIC), "T_DYNAMIC"); + EXPECT_EQ(InferredTypeToString(static_cast(99)), "T_UNKNOWN"); + + EXPECT_TRUE(IsNumericInferredType(T_INT)); + EXPECT_TRUE(IsNumericInferredType(T_FLOAT)); + EXPECT_FALSE(IsNumericInferredType(T_UNKNOWN)); + EXPECT_FALSE(IsNumericInferredType(T_DYNAMIC)); + + EXPECT_EQ(InferNumericBinopResultType(BinOpKind::kSlash, T_INT, T_INT), T_FLOAT); + EXPECT_EQ(InferNumericBinopResultType(BinOpKind::kPow, T_INT, T_FLOAT), T_FLOAT); + EXPECT_EQ(InferNumericBinopResultType(BinOpKind::kPlus, T_INT, T_INT), T_INT); + EXPECT_EQ(InferNumericBinopResultType(BinOpKind::kPlus, T_INT, T_FLOAT), T_FLOAT); + EXPECT_EQ(InferNumericBinopResultType(BinOpKind::kBitAnd, T_INT, T_INT), T_INT); + EXPECT_EQ(InferNumericBinopResultType(BinOpKind::kAnd, T_INT, T_FLOAT), T_FLOAT); + EXPECT_EQ(InferNumericBinopResultType(BinOpKind::kOr, T_INT, T_FLOAT), T_INT); + EXPECT_EQ(InferNumericBinopResultType(BinOpKind::kConcat, T_INT, T_INT), T_DYNAMIC); + EXPECT_EQ(InferNumericBinopResultType(BinOpKind::kPlus, T_DYNAMIC, T_INT), T_DYNAMIC); + + EXPECT_STREQ(SpecReturnCTypeName(T_INT), "int64_t"); + EXPECT_STREQ(SpecReturnCTypeName(T_FLOAT), "double"); + EXPECT_STREQ(SpecReturnCTypeName(T_DYNAMIC), "CVar"); + + EXPECT_EQ(SpecFuncName("fib", {0}, 0), "fib_0"); + EXPECT_EQ(SpecFuncName("test", {1, 4}, 2), "test_0_1"); + EXPECT_NE(BoxNativeValue("x", T_INT).find("VAR_INT"), std::string::npos); + EXPECT_NE(BoxNativeValue("y", T_FLOAT).find("VAR_FLOAT"), std::string::npos); +} + +TEST(common, compile_extract_args_and_vararg) { + { + auto args = std::make_shared(CovLoc()); + args->SetArgsKind(ArgsKind::kExpList); + auto el = std::make_shared(CovLoc()); + el->AddExp(CovMakeNumberExp("1")); + el->AddExp(CovMakeNumberExp("2")); + args->SetExplist(el); + auto raw = ExtractCallRawArgs(args); + ASSERT_EQ(raw.size(), 2u); + } + { + auto args = std::make_shared(CovLoc()); + args->SetArgsKind(ArgsKind::kString); + args->SetString(CovMakeStringExp("\"hi\"")); + auto raw = ExtractCallRawArgs(args); + ASSERT_EQ(raw.size(), 1u); + } + { + auto args = std::make_shared(CovLoc()); + args->SetArgsKind(ArgsKind::kTableConstructor); + auto tc = std::make_shared(CovLoc()); + args->SetTableconstructor(tc); + auto raw = ExtractCallRawArgs(args); + ASSERT_EQ(raw.size(), 1u); + } + + EXPECT_FALSE(IsVarargExp(nullptr)); + EXPECT_FALSE(IsVarargExp(CovMakeNumberExp("1"))); + EXPECT_FALSE(IsVarargExp(CovMakeCallPrefixExp("f"))); + EXPECT_TRUE(IsVarargExp(CovMakeVarPrefixExp("__fakelua_vararg_0"))); + EXPECT_FALSE(IsVarargExp(CovMakeVarPrefixExp("x"))); + + EXPECT_FALSE(IsFunctionCallExp(nullptr)); + EXPECT_FALSE(IsFunctionCallExp(CovMakeNumberExp("1"))); + EXPECT_TRUE(IsFunctionCallExp(CovMakeCallPrefixExp("f"))); + EXPECT_FALSE(IsFunctionCallExp(CovMakeVarPrefixExp("x"))); +} + +TEST(common, compile_package_and_strings) { + EXPECT_EQ(StripLuaStringQuotes("\"abc\""), "abc"); + EXPECT_EQ(StripLuaStringQuotes("'xyz'"), "xyz"); + EXPECT_EQ(StripLuaStringQuotes("bare"), "bare"); + EXPECT_EQ(StripLuaStringQuotes("\""), "\""); + + { + // package "mypkg" + auto fc = std::make_shared(CovLoc()); + fc->SetPrefixexp(CovMakePrefixVar("package")); + auto args = std::make_shared(CovLoc()); + args->SetArgsKind(ArgsKind::kString); + args->SetString(CovMakeStringExp("\"mypkg\"")); + fc->SetArgs(args); + std::string out; + EXPECT_TRUE(ExtractPackageName(fc, out)); + EXPECT_EQ(out, "mypkg"); + } + { + // package("mypkg") via explist + auto fc = std::make_shared(CovLoc()); + fc->SetPrefixexp(CovMakePrefixVar("package")); + auto args = std::make_shared(CovLoc()); + args->SetArgsKind(ArgsKind::kExpList); + auto el = std::make_shared(CovLoc()); + el->AddExp(CovMakeStringExp("\"pkg2\"")); + args->SetExplist(el); + fc->SetArgs(args); + std::string out; + EXPECT_TRUE(ExtractPackageName(fc, out)); + EXPECT_EQ(out, "pkg2"); + } + { + // package = "pkg3" + auto assign = std::make_shared(CovLoc()); + auto vl = std::make_shared(CovLoc()); + vl->AddVar(CovMakeSimpleVar("package")); + auto el = std::make_shared(CovLoc()); + el->AddExp(CovMakeStringExp("\"pkg3\"")); + assign->SetVarlist(vl); + assign->SetExplist(el); + std::string out; + EXPECT_TRUE(ExtractPackageName(assign, out)); + EXPECT_EQ(out, "pkg3"); + } + { + auto empty = std::make_shared(CovLoc()); + std::string out; + EXPECT_FALSE(ExtractPackageName(empty, out)); + } + + auto empty = std::make_shared(CovLoc()); + EXPECT_FALSE(SyntaxTreeLocationStr("f.lua", empty).empty()); +} + +TEST(common, compile_number_key_classify) { + TableKeyKind kind = TableKeyKind::kString; + std::string canon; + int64_t iv = 0; + double fv = 0; + + EXPECT_TRUE(ClassifyLuaNumberKey("42", kind, canon, iv, fv)); + EXPECT_EQ(kind, TableKeyKind::kInt); + EXPECT_EQ(iv, 42); + + EXPECT_TRUE(ClassifyLuaNumberKey("0x10", kind, canon, iv, fv)); + EXPECT_EQ(kind, TableKeyKind::kInt); + EXPECT_EQ(iv, 16); + + EXPECT_TRUE(ClassifyLuaNumberKey("1.5", kind, canon, iv, fv)); + EXPECT_EQ(kind, TableKeyKind::kFloat); + + EXPECT_TRUE(ClassifyLuaNumberKey("1.0", kind, canon, iv, fv)); + EXPECT_EQ(kind, TableKeyKind::kInt); + EXPECT_EQ(iv, 1); + + EXPECT_FALSE(ClassifyLuaNumberKey("", kind, canon, iv, fv)); + EXPECT_FALSE(ClassifyLuaNumberKey("abc", kind, canon, iv, fv)); + EXPECT_FALSE(ClassifyLuaNumberKey("inf", kind, canon, iv, fv)); + + auto n = CovMakeNumberExp("7"); + EXPECT_TRUE(ClassifyConstNumberExp(n, kind, canon, iv, fv)); + EXPECT_EQ(iv, 7); + + auto neg = CovMakeUnaryMinus(CovMakeNumberExp("3")); + EXPECT_TRUE(ClassifyConstNumberExp(neg, kind, canon, iv, fv)); + EXPECT_EQ(iv, -3); + + auto min_neg = CovMakeUnaryMinus(CovMakeNumberExp(std::to_string(std::numeric_limits::min()))); + // May classify as float overflow path; just exercise the branch. + (void)ClassifyConstNumberExp(min_neg, kind, canon, iv, fv); + + auto two63 = CovMakeUnaryMinus(CovMakeNumberExp("9223372036854775808.0")); + EXPECT_TRUE(ClassifyConstNumberExp(two63, kind, canon, iv, fv)); + + EXPECT_FALSE(ClassifyConstNumberExp(CovMakeStringExp("\"x\""), kind, canon, iv, fv)); + EXPECT_FALSE(ConstNumberExpHasFloatSyntax(nullptr)); + EXPECT_TRUE(ConstNumberExpHasFloatSyntax(CovMakeNumberExp("1.0"))); + EXPECT_FALSE(ConstNumberExpHasFloatSyntax(CovMakeNumberExp("1"))); + EXPECT_TRUE(ConstNumberExpHasFloatSyntax(CovMakeUnaryMinus(CovMakeNumberExp("0.0")))); + + EXPECT_TRUE(ConstNumberExpIsIntValue(TableKeyKind::kInt, CovMakeNumberExp("1"))); + EXPECT_FALSE(ConstNumberExpIsIntValue(TableKeyKind::kInt, CovMakeNumberExp("1.0"))); + + const double z = ConstNumberExpToDouble(TableKeyKind::kInt, 0, 0.0, CovMakeNumberExp("0.0")); + EXPECT_TRUE(z == 0.0); + const double nz = ConstNumberExpToDouble(TableKeyKind::kInt, 0, 0.0, CovMakeUnaryMinus(CovMakeNumberExp("0.0"))); + EXPECT_TRUE(std::signbit(nz)); + + EXPECT_NE(FormatCDoubleLiteral(1.0).find('.'), std::string::npos); + EXPECT_FALSE(FormatCDoubleLiteral(1.5).empty()); + + EXPECT_EQ(EscapeCStringLiteral("a\"b\\c\n\r\td"), "a\\\"b\\\\c\\n\\r\\td"); + EXPECT_NE(EscapeCStringLiteral(std::string("\x01\x7f", 2)).find('\\'), std::string::npos); +} + +TEST(common, compile_table_spec_helpers) { + TableFieldInfo fs{.key = "a", .type = T_INT, .key_kind = TableKeyKind::kString}; + TableFieldInfo fi{.key = "1", .type = T_INT, .key_kind = TableKeyKind::kInt, .int_value = 1}; + TableFieldInfo fb{.key = "true", .type = T_INT, .key_kind = TableKeyKind::kBool, .bool_value = true}; + TableFieldInfo ff{.key = "1.5", .type = T_FLOAT, .key_kind = TableKeyKind::kFloat, .float_value = 1.5}; + EXPECT_EQ(TableFieldDescriptor(fs), "S_a"); + EXPECT_EQ(TableFieldDescriptor(fi), "I_1"); + EXPECT_EQ(TableFieldDescriptor(fb), "B_true"); + EXPECT_EQ(TableFieldDescriptor(ff), "F_1.5"); + + std::vector fields{fs, fi}; + const auto name = ComputeTableSpecName(fields); + EXPECT_EQ(name.rfind("flua_spec_", 0), 0u); + EXPECT_EQ(ComputeTableSpecName({fi, fs}), name); + + EXPECT_EQ(GetSimpleVarName(nullptr), ""); + EXPECT_EQ(GetSimpleVarName(CovMakeSimpleVar("x")), "x"); + EXPECT_EQ(GetSimpleVarName(CovMakePrefixVar("y")), "y"); + EXPECT_EQ(GetSimpleVarName(CovMakeVarPrefixExp("z")), "z"); + EXPECT_EQ(GetSimpleVarName(CovMakeNumberExp("1")), ""); +} + +TEST(common, cov_gaps_lua_misc_paths) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./basic/test_local_multi_assign.lua", config); + CompileFile(s, "./table/test_table_seq_and_move.lua", config); + CompileFile(s, "./basic/test_select_and_closure_args.lua", config); + CompileFile(s, "./basic/test_file_level_literals.lua", config); + CompileFile(s, "./basic/test_interp_errors.lua", config); + double res = 0; + Call(s, jit_type, "test_local_multi_assign", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + Call(s, jit_type, "test_table_seq_and_move", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + Call(s, jit_type, "test_select_and_closure_args", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + Call(s, jit_type, "test_file_level_literals", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + Call(s, jit_type, "test_bool_nil_locals", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + Call(s, jit_type, "test_empty_body", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + Call(s, jit_type, "test_interp_errors", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + } + FakeluaDeleteState(s); +} diff --git a/test/test_jitter.cpp b/test/test_jitter.cpp index 0286b08a..fbceeecd 100644 --- a/test/test_jitter.cpp +++ b/test/test_jitter.cpp @@ -6,6 +6,9 @@ #include "var/var_string.h" #include "var/var_type.h" #include "gtest/gtest.h" +#include "test_jit.h" +#include +#include using namespace fakelua; @@ -3096,3 +3099,55 @@ TEST(jitter, numeric_eval_once) { ASSERT_EQ(ret, 1); }); } + +namespace { +void SetTestEnv(const char *name, const char *value) { +#if defined(_WIN32) + // MinGW/MSVC: empty value clears getenv for gcc_jit's FAKELUA_CC check. + const std::string entry = std::string(name) + "=" + (value ? value : ""); + _putenv(entry.c_str()); +#else + if (value) { + setenv(name, value, 1); + } else { + unsetenv(name); + } +#endif +} +}// namespace + +TEST(jitter, gcc_jit_env_and_paths) { + StateConfig cfg; + cfg.gcc_config.include_paths.push_back("/usr/include"); + cfg.gcc_config.library_paths.push_back("/usr/lib"); + cfg.gcc_config.libraries.push_back("m"); + State *s = FakeluaNewState(cfg); + ASSERT_NE(s, nullptr); + + SetTestEnv("FAKELUA_CC", "gcc"); + CompileConfig ccfg; + ccfg.debug_mode = true; + EXPECT_NO_THROW(CompileString(s, "function gcc_cov_fn()\n return 1\nend\n", ccfg)); + double res = 0; + Call(s, JIT_GCC, "gcc_cov_fn", res); + EXPECT_EQ(res, 1.0); + + // Force GCC failure to hit JoinCommand / CleanupOnError / log-read paths. + // Use a nonexistent binary so Windows/MinGW and POSIX both fail the spawn. + SetTestEnv("FAKELUA_CC", "fakelua_nonexistent_cc_for_cov"); + EXPECT_THROW(CompileString(s, "function gcc_fail()\n return 2\nend\n", ccfg), std::exception); + SetTestEnv("FAKELUA_CC", nullptr); + FakeluaDeleteState(s); +} + +TEST(jitter, tcc_libraries_config_path) { + StateConfig cfg; + // Exercise tcc_add_library loop; missing lib still reaches that line before compile fails. + cfg.tcc_config.libraries.push_back("fakelua_missing_lib_for_cov"); + State *s = FakeluaNewState(cfg); + ASSERT_NE(s, nullptr); + CompileConfig ccfg; + ccfg.debug_mode = true; + EXPECT_THROW(CompileString(s, "function tcc_lib_cov()\n return 3\nend\n", ccfg), std::exception); + FakeluaDeleteState(s); +} diff --git a/test/test_log.cpp b/test/test_log.cpp index 7aacb06e..39c12d49 100644 --- a/test/test_log.cpp +++ b/test/test_log.cpp @@ -5,10 +5,13 @@ #include #include +#include +#include #include #include #include #include +#include "state/state.h" using namespace fakelua; @@ -740,3 +743,90 @@ TEST(test_log, level_transitions_and_errors) { CallThrow(s, "LogAllLevels.test_log_info_no_args", ret); FakeluaDeleteState(s); } + + +TEST(test_log, log_file_rotation_small_limit) { + // Force RotateLogs by using a tiny max size and writing repeatedly. + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + const auto log_path = (std::filesystem::temp_directory_path() / "fakelua_cov_rotate.log").string(); + std::error_code ec; + std::filesystem::remove(log_path, ec); + for (int i = 1; i <= 5; ++i) { + std::filesystem::remove(std::format("{}.{}", log_path, i), ec); + } + SetLogFile(s, log_path, /*max_size=*/64, /*max_files=*/3); + SetLogLevel(s, LogLevel::Trace); + for (int i = 0; i < 40; ++i) { + LOG_INFO(s, "engine", "rotate-me-{}-{}", i, std::string(80, 'x')); + } + SetLogFile(s, "", 0, 0); + FakeluaDeleteState(s); + // At least the primary or a rotated file should exist. + const bool any = std::filesystem::exists(log_path) || std::filesystem::exists(log_path + ".1"); + EXPECT_TRUE(any); + std::filesystem::remove(log_path, ec); + for (int i = 1; i <= 5; ++i) { + std::filesystem::remove(std::format("{}.{}", log_path, i), ec); + } +} + +extern "C" void FakeluaLogLua(State *s, int level, CVar msg, const char *file, int line, const char *func); + +TEST(test_log, fakelua_log_lua_cvar_types) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + SetLogLevel(s, LogLevel::Trace); + + CVar nil{}; + FakeluaLogLua(s, static_cast(LogLevel::Info), nil, "f.lua", 1, "fn"); + + CVar b{}; + b.type_ = static_cast(VarType::Bool); + b.data_.b = true; + FakeluaLogLua(s, static_cast(LogLevel::Info), b, "f.lua", 2, "fn"); + b.data_.b = false; + FakeluaLogLua(s, static_cast(LogLevel::Warn), b, "f.lua", 3, "fn"); + + CVar i{}; + i.type_ = static_cast(VarType::Int); + i.data_.i = 42; + FakeluaLogLua(s, static_cast(LogLevel::Error), i, "f.lua", 4, "fn"); + + CVar f{}; + f.type_ = static_cast(VarType::Float); + f.data_.f = 3.14; + FakeluaLogLua(s, static_cast(LogLevel::Critical), f, "f.lua", 5, "fn"); + + CVar bad{}; + bad.type_ = static_cast(VarType::Table); + FakeluaLogLua(s, static_cast(LogLevel::Info), bad, nullptr, 6, nullptr); + + // Level filter early-return in FakeluaLogLua / LogLua + SetLogLevel(s, LogLevel::Error); + FakeluaLogLua(s, static_cast(LogLevel::Info), i, "f.lua", 7, "fn"); + LogLua(s, LogLevel::Debug, "t", "m", "f.lua", 8, "fn"); + + // Off level name path via explicit Off log when level is Off + SetLogLevel(s, LogLevel::Off); + Log(s, LogLevel::Off, "t", "off-msg"); + + // empty path configure + reconfigure existing sink + SetLogFile(s, "", 0, 0); + const auto path = (std::filesystem::temp_directory_path() / "fakelua_cov_log2.log").string(); + std::error_code ec; + std::filesystem::remove(path, ec); + // Create oversized file so Configure rotates on open + { + std::ofstream ofs(path); + ofs << std::string(200, 'z'); + } + SetLogFile(s, path, /*max_size=*/50, /*max_files=*/2); + SetLogFile(s, path, /*max_size=*/50, /*max_files=*/2);// close existing + SetLogLevel(s, LogLevel::Info); + LOG_INFO(s, "engine", "after-rotate"); + SetLogFile(s, "", 0, 0); + FakeluaDeleteState(s); + std::filesystem::remove(path, ec); + std::filesystem::remove(path + ".1", ec); +} diff --git a/test/test_math.cpp b/test/test_math.cpp index bb3a97a9..77e48e70 100644 --- a/test/test_math.cpp +++ b/test/test_math.cpp @@ -1331,3 +1331,16 @@ TEST(test_math, special_functions) { CallThrow(s, "test_math_clamp_bad_bounds", throw_ret); FakeluaDeleteState(s); } + +TEST(test_math, cov_math_inline_extra) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./math/test_math_inline_extra.lua", config); + double res = 0; + Call(s, jit_type, "test_math_inline_extra", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + } + FakeluaDeleteState(s); +} diff --git a/test/test_native.cpp b/test/test_native.cpp index fefe31d0..c45d5b4a 100644 --- a/test/test_native.cpp +++ b/test/test_native.cpp @@ -735,3 +735,48 @@ TEST(test_native, test_native_manager_destroy_empty) { FakeluaDeleteState(s); } + +TEST(test_native, register_native_var_and_func_addr) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + RegisterNativeVarFunction(s, "cov_make_tbl", 0, false, [](State *state, const std::vector &) -> VarInterface * { + auto *t = new SimpleVarImpl(); + std::vector> kvs; + auto *k = new SimpleVarImpl(); + k->ViSetString("a"); + auto *v = new SimpleVarImpl(); + v->ViSetInt(1); + kvs.emplace_back(k, v); + t->ViSetTable(kvs); + delete k; + delete v; + return t; + }); + // null callback path + RegisterNativeVarFunction(s, "cov_nil_cb", 0, false, NativeVarFuncCallback{}); + + CompileConfig config; + CompileString(s, R"( +function test_native_var() + local t = cov_make_tbl() + if t.a ~= 1 then return 1 end + local n = cov_nil_cb() + if n ~= nil then return 2 end + return 0 +end +)", + config); + double res = 0; + Call(s, JIT_GCC, "test_native_var", res); + EXPECT_EQ(res, 0.0); + + int argc = 0; + bool is_va = false; + void *addr = inter::GetFuncAddr(s, JIT_GCC, "test_native_var", argc, is_va); + EXPECT_NE(addr, nullptr); + EXPECT_EQ(inter::GetFuncAddr(s, JIT_GCC, "no_such_fn_xyz", argc, is_va), nullptr); + + SetDebugLogLevel(s, 1); + SetDebugLogLevel(s, LogLevel::Warn); + FakeluaDeleteState(s); +} diff --git a/test/test_runtime.cpp b/test/test_runtime.cpp index dd7060be..4493f356 100644 --- a/test/test_runtime.cpp +++ b/test/test_runtime.cpp @@ -16,6 +16,11 @@ #include #include #include +#include "interp/runtime.h" +#include "interp/vm_ops.h" +#include "native/table/native_table.h" +#include +#include using namespace fakelua; @@ -743,3 +748,85 @@ TEST(runtime, heap_allocator_syncs_offset_under_interleaved_bumps) { } } } + +TEST(runtime, interp_runtime_error_paths) { + FakeluaStateGuard guard; + auto *s = guard.GetState(); + + EXPECT_THROW(interp_rt::UnLen(interp_rt::Int(1)), std::exception); + EXPECT_THROW(interp_rt::GetTable(s, interp_rt::Int(1), interp_rt::Int(1)), std::exception); + EXPECT_THROW(interp_rt::GetTable(s, interp_rt::NewTable(s), interp_rt::Nil()), std::exception); + CVar nan_key{}; + nan_key.type_ = interp_rt::kFloat; + nan_key.data_.f = std::numeric_limits::quiet_NaN(); + EXPECT_THROW(interp_rt::GetTable(s, interp_rt::NewTable(s), nan_key), std::exception); + EXPECT_THROW(interp_rt::SetTable(s, interp_rt::Int(1), interp_rt::Int(1), interp_rt::Int(1)), std::exception); + EXPECT_THROW(interp_rt::SetTable(s, interp_rt::NewTable(s), interp_rt::Nil(), interp_rt::Int(1)), std::exception); + EXPECT_THROW(interp_rt::TableEntryCount(interp_rt::Int(1)), std::exception); + + CVar tbl = interp_rt::NewTable(s); + // non-multi expand path + interp_rt::TableExpandMulti(s, tbl, 1, interp_rt::Int(42)); + EXPECT_EQ(table::TableHelper::GetTableInt(s, tbl, 1).data_.i, 42); + + // CombineMulti / MakeMulti with nested empty multi + CVar empty_multi = inter::AllocMultiCVar(s, 0); + CVar prefix[1] = {empty_multi}; + CVar combined = interp_rt::CombineMulti(s, prefix, 1, empty_multi); + EXPECT_EQ(combined.type_, interp_rt::kMulti); + CVar vals[2] = {empty_multi, interp_rt::Int(7)}; + CVar made = interp_rt::MakeMulti(s, vals, 2); + EXPECT_EQ(made.type_, interp_rt::kMulti); +} + +TEST(runtime, vm_ops_for_and_arith_edges) { + using namespace interp_rt; + (void)Nil(); + (void)Bool(true); + (void)Int(1); + (void)Float(1.5); + + EXPECT_THROW(CheckInt(Bool(true)), std::exception); + EXPECT_EQ(BinAdd(Int(1), Int(2)).data_.i, 3); + EXPECT_EQ(BinSub(Int(5), Int(3)).data_.i, 2); + EXPECT_EQ(BinMul(Int(4), Int(5)).data_.i, 20); + + EXPECT_THROW(ForStepPositive(Int(0)), std::exception); + EXPECT_THROW(ForStepPositive(Float(0.0)), std::exception); + EXPECT_THROW(ForStepPositive(Bool(true)), std::exception); + EXPECT_TRUE(ForStepPositive(Int(1))); + EXPECT_FALSE(ForStepPositive(Int(-1))); + EXPECT_TRUE(ForLoopInRange(Int(1), Int(3), Int(1))); + EXPECT_TRUE(ForLoopInRange(Int(3), Int(1), Int(-1))); + + bool skip = false; + EXPECT_EQ(ForLimitToInt(Float(std::numeric_limits::infinity()), -1, &skip), 0); + EXPECT_TRUE(skip); + skip = false; + EXPECT_EQ(ForLimitToInt(Float(std::numeric_limits::infinity()), 1, &skip), std::numeric_limits::max()); + skip = false; + EXPECT_EQ(ForLimitToInt(Float(-std::numeric_limits::infinity()), 1, &skip), 0); + EXPECT_TRUE(skip); + skip = false; + EXPECT_EQ(ForLimitToInt(Float(-std::numeric_limits::infinity()), -1, &skip), std::numeric_limits::min()); + skip = false; + EXPECT_EQ(ForLimitToInt(Float(1e300), -1, &skip), 0); + EXPECT_TRUE(skip); + skip = false; + EXPECT_EQ(ForLimitToInt(Float(1e300), 1, &skip), std::numeric_limits::max()); + skip = false; + EXPECT_EQ(ForLimitToInt(Float(-1e300), 1, &skip), 0); + EXPECT_TRUE(skip); + skip = false; + EXPECT_EQ(ForLimitToInt(Float(-1e300), -1, &skip), std::numeric_limits::min()); + + CVar idx = Int(1); + CVar limit = Int(3); + CVar step = Int(0); + CVar loopvar{}; + EXPECT_THROW(ForPrep(idx, limit, step, loopvar), std::exception); + idx = Float(1.0); + limit = Float(3.0); + step = Float(0.0); + EXPECT_THROW(ForPrep(idx, limit, step, loopvar), std::exception); +} diff --git a/test/test_state.cpp b/test/test_state.cpp index bc550656..047b3f93 100644 --- a/test/test_state.cpp +++ b/test/test_state.cpp @@ -8,6 +8,8 @@ #include "var/var.h" #include "var/var_string.h" #include "gtest/gtest.h" +#include +#include using namespace fakelua; @@ -443,3 +445,109 @@ TEST(state, heap_alloc_boundary) { inter::Reset(s); ASSERT_EQ(alloc.Size(), 0u); } + + +TEST(state, cvar_to_integer_edge_cases) { + FakeluaStateGuard guard; + auto *s = guard.GetState(); + + CVar i{}; + i.type_ = static_cast(VarType::Int); + i.data_.i = 42; + EXPECT_EQ(inter::CVarToInteger(i, -1), 42); + + CVar btrue{}; + btrue.type_ = static_cast(VarType::Bool); + btrue.data_.b = true; + CVar bfalse{}; + bfalse.type_ = static_cast(VarType::Bool); + bfalse.data_.b = false; + EXPECT_EQ(inter::CVarToInteger(btrue, -1), 1); + EXPECT_EQ(inter::CVarToInteger(bfalse, -1), 0); + + CVar f{}; + f.type_ = static_cast(VarType::Float); + f.data_.f = 3.9; + EXPECT_EQ(inter::CVarToInteger(f, -1), 3); + + CVar nanv{}; + nanv.type_ = static_cast(VarType::Float); + nanv.data_.f = std::numeric_limits::quiet_NaN(); + EXPECT_EQ(inter::CVarToInteger(nanv, 99), 99); + + CVar inf{}; + inf.type_ = static_cast(VarType::Float); + inf.data_.f = std::numeric_limits::infinity(); + EXPECT_EQ(inter::CVarToInteger(inf, 99), 99); + + CVar huge{}; + huge.type_ = static_cast(VarType::Float); + huge.data_.f = 1e300; + EXPECT_EQ(inter::CVarToInteger(huge, 99), 99); + + Var vs; + vs.SetTempString(s, "123"); + EXPECT_EQ(inter::CVarToInteger(reinterpret_cast(vs), -1), 123); + + Var vsf; + vsf.SetTempString(s, "12.5"); + EXPECT_EQ(inter::CVarToInteger(reinterpret_cast(vsf), -1), 12); + + Var vsbad; + vsbad.SetTempString(s, "12x"); + EXPECT_EQ(inter::CVarToInteger(reinterpret_cast(vsbad), 77), 77); + + Var vempty; + vempty.SetTempString(s, ""); + EXPECT_EQ(inter::CVarToInteger(reinterpret_cast(vempty), 77), 77); + + Var vhuge; + vhuge.SetTempString(s, "1e300"); + EXPECT_EQ(inter::CVarToInteger(reinterpret_cast(vhuge), 77), 77); + + CVar nil{}; + EXPECT_EQ(inter::CVarToInteger(nil, 55), 55); +} + +TEST(state, cvar_to_number_bool_string) { + FakeluaStateGuard guard; + auto *s = guard.GetState(); + + CVar f{}; + f.type_ = static_cast(VarType::Float); + f.data_.f = 1.5; + EXPECT_DOUBLE_EQ(inter::CVarToNumber(f, -1), 1.5); + + CVar i{}; + i.type_ = static_cast(VarType::Int); + i.data_.i = 8; + EXPECT_DOUBLE_EQ(inter::CVarToNumber(i, -1), 8.0); + + Var vs; + vs.SetTempString(s, "2.5"); + EXPECT_DOUBLE_EQ(inter::CVarToNumber(reinterpret_cast(vs), -1), 2.5); + + Var vsbad; + vsbad.SetTempString(s, "nope"); + EXPECT_DOUBLE_EQ(inter::CVarToNumber(reinterpret_cast(vsbad), 9.0), 9.0); + + CVar zero{}; + zero.type_ = static_cast(VarType::Int); + zero.data_.i = 0; + EXPECT_TRUE(inter::CVarToBool(zero, false)); + + CVar nil{}; + EXPECT_TRUE(inter::CVarToBool(nil, true)); + EXPECT_FALSE(inter::CVarToBool(nil, false)); + + CVar bfalse{}; + bfalse.type_ = static_cast(VarType::Bool); + bfalse.data_.b = false; + EXPECT_FALSE(inter::CVarToBool(bfalse, true)); + + Var hello; + hello.SetTempString(s, "hi"); + EXPECT_EQ(inter::CVarToString(reinterpret_cast(hello), "x"), "hi"); + EXPECT_EQ(inter::CVarToString(CVar{}, "def"), "def"); + EXPECT_EQ(inter::CVarToString(CVar{}, nullptr), ""); +} diff --git a/test/test_string.cpp b/test/test_string.cpp index 24422f8e..4d948c21 100644 --- a/test/test_string.cpp +++ b/test/test_string.cpp @@ -960,3 +960,18 @@ TEST(test_string, plan_12_to_19_regressions) { FakeluaDeleteState(s); } + +TEST(test_string, cov_string_escapes_and_ops) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./string/test_string_escapes_and_ops.lua", config); + double res = 0; + Call(s, jit_type, "test_string_escapes", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + Call(s, jit_type, "test_string_ops_extra", res); + EXPECT_EQ(res, 0.0) << "jit=" << JitTypeName(jit_type); + } + FakeluaDeleteState(s); +} diff --git a/test/test_syntax_tree.cpp b/test/test_syntax_tree.cpp index 689572b4..51b4df43 100644 --- a/test/test_syntax_tree.cpp +++ b/test/test_syntax_tree.cpp @@ -1,6 +1,11 @@ #include "compile/compiler.h" #include "fakelua.h" #include "gtest/gtest.h" +#include "compile/semantic_analysis.h" +#include "compile/syntax_tree.h" +#include +#include +#include using namespace fakelua; @@ -2175,3 +2180,188 @@ TEST(syntax_tree, SyntaxTreeTypeToString) { ASSERT_EQ(SyntaxTreeTypeToString(SyntaxTreeType::Args), "Args"); ASSERT_EQ(SyntaxTreeTypeToString(SyntaxTreeType::PrefixExp), "PrefixExp"); } + +namespace { + +yy::location StCovLoc() { + return yy::location(); +} + +SyntaxTreeInterfacePtr StCovMakeNumberExp(const std::string &v) { + auto e = std::make_shared(StCovLoc()); + e->SetExpKind(ExpKind::kNumber); + e->SetValue(v); + return e; +} + +SyntaxTreeInterfacePtr StCovMakeSimpleVar(const std::string &name) { + auto v = std::make_shared(StCovLoc()); + v->SetVarKind(VarKind::kSimple); + v->SetName(name); + return v; +} + +SyntaxTreeInterfacePtr StCovMakePrefixVar(const std::string &name) { + auto pe = std::make_shared(StCovLoc()); + pe->SetPrefixKind(PrefixExpKind::kVar); + pe->SetValue(StCovMakeSimpleVar(name)); + return pe; +} + +}// namespace + +TEST(syntax_tree, getters_setters_coverage) { + auto empty = std::make_shared(StCovLoc()); + EXPECT_EQ(empty->Type(), SyntaxTreeType::Empty); + EXPECT_FALSE(empty->Dump(1).empty()); + empty->SetLoc(StCovLoc()); + (void)empty->Loc(); + + auto label = std::make_shared(StCovLoc()); + label->SetName("L"); + EXPECT_EQ(label->GetName(), "L"); + EXPECT_EQ(label->Type(), SyntaxTreeType::Label); + + auto ret = std::make_shared(StCovLoc()); + auto el = std::make_shared(StCovLoc()); + ret->SetExplist(el); + EXPECT_EQ(ret->Explist(), el); + + auto assign = std::make_shared(StCovLoc()); + auto vl = std::make_shared(StCovLoc()); + assign->SetVarlist(vl); + assign->SetExplist(el); + EXPECT_EQ(assign->Varlist(), vl); + EXPECT_EQ(assign->Explist(), el); + vl->AddVar(StCovMakeSimpleVar("a")); + EXPECT_EQ(vl->Vars().size(), 1u); + el->AddExp(StCovMakeNumberExp("1")); + EXPECT_EQ(el->Exps().size(), 1u); + + auto var = std::make_shared(StCovLoc()); + var->SetName("t"); + var->SetVarKind(VarKind::kDot); + auto pe = StCovMakePrefixVar("obj"); + var->SetPrefixexp(pe); + var->SetExp(StCovMakeNumberExp("1")); + EXPECT_EQ(var->GetName(), "t"); + EXPECT_EQ(var->GetPrefixexp(), pe); + EXPECT_NE(var->GetExp(), nullptr); + EXPECT_EQ(var->GetVarKind(), VarKind::kDot); + // const overloads + const SyntaxTreeVar &cvar = *var; + EXPECT_EQ(cvar.GetName(), "t"); + EXPECT_EQ(cvar.GetPrefixexp(), pe); + EXPECT_NE(cvar.GetExp(), nullptr); + + auto fc = std::make_shared(StCovLoc()); + fc->SetPrefixexp(pe); + auto args = std::make_shared(StCovLoc()); + args->SetArgsKind(ArgsKind::kExpList); + args->SetExplist(el); + fc->SetArgs(args); + fc->SetName("m"); + EXPECT_EQ(fc->prefixexp(), pe); + EXPECT_EQ(fc->Args(), args); + EXPECT_EQ(fc->Name(), "m"); + + auto tc = std::make_shared(StCovLoc()); + auto fl = std::make_shared(StCovLoc()); + auto field = std::make_shared(StCovLoc()); + field->SetKey(StCovMakeNumberExp("1")); + field->SetValue(StCovMakeNumberExp("2")); + field->SetName("k"); + field->SetFieldKind(FieldKind::kObject); + EXPECT_NE(field->Key(), nullptr); + EXPECT_NE(field->Value(), nullptr); + EXPECT_EQ(field->Name(), "k"); + EXPECT_EQ(field->GetFieldKind(), FieldKind::kObject); + fl->AddField(field); + EXPECT_EQ(fl->Fields().size(), 1u); + tc->SetFieldlist(fl); + EXPECT_EQ(tc->Fieldlist(), fl); + + auto brk = std::make_shared(StCovLoc()); + EXPECT_EQ(brk->Type(), SyntaxTreeType::Break); + auto gt = std::make_shared(StCovLoc()); + gt->SetLabel("L"); + EXPECT_EQ(gt->GetLabel(), "L"); + + auto wh = std::make_shared(StCovLoc()); + wh->SetExp(StCovMakeNumberExp("1")); + wh->SetBlock(std::make_shared(StCovLoc())); + EXPECT_NE(wh->Exp(), nullptr); + EXPECT_NE(wh->Block(), nullptr); + + auto rp = std::make_shared(StCovLoc()); + rp->SetBlock(std::make_shared(StCovLoc())); + rp->SetExp(StCovMakeNumberExp("1")); + EXPECT_NE(rp->Block(), nullptr); + EXPECT_NE(rp->Exp(), nullptr); + + auto iff = std::make_shared(StCovLoc()); + iff->SetExp(StCovMakeNumberExp("1")); + iff->SetBlock(std::make_shared(StCovLoc())); + iff->SetElseiflist(std::make_shared(StCovLoc())); + iff->SetElseBlock(std::make_shared(StCovLoc())); + EXPECT_NE(iff->Exp(), nullptr); + EXPECT_NE(iff->Block(), nullptr); + EXPECT_NE(iff->ElseIfs(), nullptr); + EXPECT_NE(iff->ElseBlock(), nullptr); + + EXPECT_EQ(SyntaxTreeTypeToString(static_cast(999)), "UNKNOWN"); +} + +TEST(syntax_tree, semantic_collect_returns_all_shapes) { + FakeluaStateGuard guard; + auto *s = guard.GetState(); + Compiler c(s); + // skip_jit: parse-only so file-level control flow is allowed for CollectReturns coverage. + const char *src = R"( +return 1 +if true then + return 2 +elseif false then + return 3 +else + return 4 +end +while false do + return 5 +end +repeat + return 6 +until true +for i = 1, 2 do + return 7 +end +for k, v in pairs({}) do + return 8 +end +function nested() + return 9 +end +local function inner() + return 10 +end +)"; + auto pr = c.CompileString(src, {true}); + ASSERT_NE(pr.chunk, nullptr); + SemanticAnalysis sa(s); + std::vector returns; + sa.CollectReturnsForBlock(pr.chunk, returns); + EXPECT_GE(returns.size(), 5u); + // Descend into function bodies via Function/LocalFunction case (no nested returns collected). + for (const auto &stmt : std::dynamic_pointer_cast(pr.chunk)->Stmts()) { + if (stmt->Type() == SyntaxTreeType::Function || stmt->Type() == SyntaxTreeType::LocalFunction) { + sa.CollectReturnsForBlock(stmt, returns); + } + } + // nullptr early-return + sa.CollectReturnsForBlock(nullptr, returns); + // non-statement node error path + auto exp = std::make_shared(yy::location()); + exp->SetExpKind(ExpKind::kNumber); + exp->SetValue("1"); + EXPECT_THROW(sa.CollectReturnsForBlock(exp, returns), std::exception); +} diff --git a/test/test_util.cpp b/test/test_util.cpp index 54ac2836..d13d5576 100644 --- a/test/test_util.cpp +++ b/test/test_util.cpp @@ -2,6 +2,14 @@ #include "util/common.h" #include "util/string_util.h" #include "gtest/gtest.h" +#include "util/file_util.h" +#include "util/number_util.h" +#include "util/os.h" +#include +#include +#include +#include +#include using namespace fakelua; @@ -134,3 +142,46 @@ TEST(util, ReplaceEscapeChars_mixed_escapes) { std::string result = ReplaceEscapeChars(s); ASSERT_EQ(result, "A\nB\tCaDE\\F"); } + +TEST(util, number_util_and_os_exec) { + EXPECT_TRUE(IsInteger("42")); + EXPECT_FALSE(IsInteger("1.0")); + EXPECT_FALSE(IsInteger("")); + + EXPECT_EQ(FormatLuaFloat(1.0), "1.0"); + EXPECT_FALSE(FormatLuaFloat(1.5).empty()); + EXPECT_FALSE(FormatLuaFloat(-42.0).empty()); + + EXPECT_EQ(*TryConvertDoubleToInt64(3.0), 3); + EXPECT_FALSE(TryConvertDoubleToInt64(1.5).has_value()); + EXPECT_FALSE(TryConvertDoubleToInt64(std::numeric_limits::infinity()).has_value()); + EXPECT_FALSE(TryConvertDoubleToInt64(1e300).has_value()); + EXPECT_EQ(*TryConvertDoubleToInt64(static_cast(std::numeric_limits::min())), std::numeric_limits::min()); + + EXPECT_EQ(CompareInt64Double(1, std::numeric_limits::quiet_NaN()), 0); + EXPECT_EQ(CompareInt64Double(1, 1e300), -1); + EXPECT_EQ(CompareInt64Double(1, -1e300), 1); + EXPECT_EQ(CompareInt64Double(2, 2.5), -1); + EXPECT_EQ(CompareInt64Double(3, 2.5), 1); + EXPECT_EQ(CompareInt64Double(2, 2.0), 0); + EXPECT_TRUE(Int64DoubleEqual(2, 2.0)); + EXPECT_FALSE(Int64DoubleEqual(2, std::numeric_limits::quiet_NaN())); + + const std::string out = Exec("echo hello_cov"); + EXPECT_NE(out.find("hello_cov"), std::string::npos); +} + +TEST(util, generate_tmp_filename_collision) { + // Collision path in GenerateTmpFilename: pre-create the first candidate. + const auto first = GenerateTmpFilename("cov_collide_", ".tmp"); + { + std::ofstream ofs(first); + ofs << "x"; + } + // Force same pid/count window by creating many temps; at least exercise the function. + const auto second = GenerateTmpFilename("cov_collide_", ".tmp"); + EXPECT_NE(first, second); + std::error_code ec; + std::filesystem::remove(first, ec); + std::filesystem::remove(second, ec); +} diff --git a/test/test_var.cpp b/test/test_var.cpp index bedaaba0..a8212361 100644 --- a/test/test_var.cpp +++ b/test/test_var.cpp @@ -1,8 +1,17 @@ #include "fakelua.h" #include "state/state.h" #include "var/var.h" +#include "var/var_closure.h" #include "var/var_string.h" +#include "var/var_multi.h" +#include "native/table/native_table.h" #include "gtest/gtest.h" +#include "interp/interpreter.h" +#include "interp/runtime.h" +#include "var/var_type.h" +#include +#include +#include using namespace fakelua; @@ -361,3 +370,185 @@ TEST(var, test_true_string) { empty_str.SetTempString(s, ""); ASSERT_TRUE(empty_str.TestTrue());// empty string is still truthy in Lua } + + +TEST(var, tostring_hash_equal_table_multi_closure) { + const FakeluaStateGuard guard; + const auto s = guard.GetState(); + + CVar tbl_cv = table::TableHelper::CreateTable(s); + Var &tbl = AsVar(tbl_cv); + ASSERT_EQ(tbl.Type(), VarType::Table); + ASSERT_TRUE(tbl.TestTrue()); + const std::string tbl_str = tbl.ToString(); + ASSERT_NE(tbl_str.find("table("), std::string::npos); + ASSERT_NE(tbl.Hash(), 0u); + ASSERT_TRUE(tbl.Equal(tbl)); + + CVar tbl2_cv = table::TableHelper::CreateTable(s); + Var &tbl2 = AsVar(tbl2_cv); + ASSERT_FALSE(tbl.Equal(tbl2)); + + CVar multi_cv = inter::AllocMultiCVar(s, 2); + CVar one{}; + one.type_ = static_cast(VarType::Int); + one.data_.i = 1; + CVar two{}; + two.type_ = static_cast(VarType::Int); + two.data_.i = 2; + inter::SetMultiCVarElement(multi_cv, 0, one); + inter::SetMultiCVarElement(multi_cv, 1, two); + Var &multi = AsVar(multi_cv); + ASSERT_EQ(multi.Type(), VarType::Multi); + ASSERT_TRUE(multi.TestTrue()); + const std::string multi_str = multi.ToString(); + ASSERT_NE(multi_str.find("multi("), std::string::npos); + ASSERT_NE(multi_str.find("1"), std::string::npos); + ASSERT_NE(multi.Hash(), 0u); + + CVar multi2_cv = inter::AllocMultiCVar(s, 2); + inter::SetMultiCVarElement(multi2_cv, 0, one); + inter::SetMultiCVarElement(multi2_cv, 1, two); + ASSERT_TRUE(multi.Equal(AsVar(multi2_cv))); + CVar ninetynine{}; + ninetynine.type_ = static_cast(VarType::Int); + ninetynine.data_.i = 99; + inter::SetMultiCVarElement(multi2_cv, 1, ninetynine); + ASSERT_FALSE(multi.Equal(AsVar(multi2_cv))); + + CVar multi3_cv = inter::AllocMultiCVar(s, 1); + inter::SetMultiCVarElement(multi3_cv, 0, one); + ASSERT_FALSE(multi.Equal(AsVar(multi3_cv))); + + Var cl; + cl.type_ = static_cast(VarType::Closure); + cl.data_.cl = reinterpret_cast(0x1234); + ASSERT_TRUE(cl.TestTrue()); + const std::string cl_str = cl.ToString(); + ASSERT_NE(cl_str.find("function("), std::string::npos); + ASSERT_NE(cl.Hash(), 0u); + Var cl2; + cl2.type_ = static_cast(VarType::Closure); + cl2.data_.cl = reinterpret_cast(0x1234); + ASSERT_TRUE(cl.Equal(cl2)); + cl2.data_.cl = reinterpret_cast(0x5678); + ASSERT_FALSE(cl.Equal(cl2)); +} + +TEST(var, equal_same_type_int_bool_float) { + ASSERT_TRUE(Var(static_cast(7)).Equal(Var(static_cast(7)))); + ASSERT_FALSE(Var(static_cast(7)).Equal(Var(static_cast(8)))); + ASSERT_TRUE(Var(true).Equal(Var(true))); + ASSERT_FALSE(Var(true).Equal(Var(false))); + ASSERT_TRUE(Var(1.25).Equal(Var(1.25))); + ASSERT_FALSE(Var(1.25).Equal(Var(1.5))); +} + +TEST(var, type_unknown_string) { + EXPECT_EQ(VarTypeToString(static_cast(999)), "UNKNOWN"); + EXPECT_EQ(VarTypeToString(static_cast(-1)), "UNKNOWN"); +} + +TEST(var, fakelua_varto_vi_and_native_bridge) { + FakeluaStateGuard guard; + auto *s = guard.GetState(); + + CVar nil{}; + VarInterface *vi_nil = inter::FakeluaToNativeObj(s, nil); + ASSERT_NE(vi_nil, nullptr); + EXPECT_EQ(vi_nil->ViGetType(), VarInterface::Type::NIL); + delete vi_nil; + + CVar b{}; + b.type_ = static_cast(VarType::Bool); + b.data_.b = true; + VarInterface *vi_b = inter::FakeluaToNativeObj(s, b); + EXPECT_EQ(vi_b->ViGetType(), VarInterface::Type::BOOL); + EXPECT_TRUE(vi_b->ViGetBool()); + delete vi_b; + + CVar f{}; + f.type_ = static_cast(VarType::Float); + f.data_.f = 2.5; + VarInterface *vi_f = inter::FakeluaToNativeObj(s, f); + EXPECT_EQ(vi_f->ViGetType(), VarInterface::Type::FLOAT); + delete vi_f; + + // Hash/bucket table path (nodes_ + active_list_) + CVar big = table::TableHelper::CreateTable(s); + for (int i = 0; i < 20; ++i) { + CVar k{}; + k.type_ = static_cast(VarType::StringId); + k.data_.i = s->GetConstString().Alloc("k" + std::to_string(i)); + table::TableHelper::SetTable(s, big, k, interp_rt::Int(i)); + } + VarInterface *vi_t = inter::FakeluaToNativeObj(s, big); + ASSERT_NE(vi_t, nullptr); + EXPECT_EQ(vi_t->ViGetType(), VarInterface::Type::TABLE); + delete vi_t; + + // Closure / Multi unsupported + CVar cl{}; + cl.type_ = static_cast(VarType::Closure); + EXPECT_THROW(inter::FakeluaToNativeObj(s, cl), std::exception); + CVar multi = inter::AllocMultiCVar(s, 2); + EXPECT_THROW(inter::FakeluaToNativeObj(s, multi), std::exception); + + // ViToVar paths via NativeToFakeluaVarInterface + EXPECT_EQ(inter::NativeToFakeluaVarInterface(s, nullptr).type_, static_cast(VarType::Nil)); + SimpleVarImpl bool_vi; + bool_vi.ViSetBool(false); + EXPECT_EQ(inter::NativeToFakeluaVarInterface(s, &bool_vi).type_, static_cast(VarType::Bool)); + SimpleVarImpl float_vi; + float_vi.ViSetFloat(1.25); + EXPECT_EQ(inter::NativeToFakeluaVarInterface(s, &float_vi).type_, static_cast(VarType::Float)); + // default / unknown: leave as NIL after Clear — already covered; synthesize bogus by INT then + // Call NativeFloat helper + CVar nf = NativeFloat(s, 3.14); + EXPECT_EQ(nf.type_, static_cast(VarType::Float)); + + EXPECT_EQ(inter::GetMultiCVarCount(multi), 2); + CVar args_multi[1] = {multi}; + inter::SetMultiCVarElement(multi, 0, interp_rt::Int(11)); + inter::SetMultiCVarElement(multi, 1, interp_rt::Int(22)); + EXPECT_EQ(inter::GetNativeArg(s, args_multi, 1, 0).data_.i, 11); + EXPECT_EQ(inter::GetNativeArg(s, args_multi, 1, 1).data_.i, 22); + + // Nested multi unwrap in GetNativeArg + CVar nested = inter::AllocMultiCVar(s, 1); + inter::SetMultiCVarElement(nested, 0, multi);// multi-in-multi + CVar args_nested[1] = {nested}; + // idx 0 yields nested multi then unwraps first element of multi -> 11? + // GetNativeArg: args[0] is Multi nested; gets idx from nested; then while Multi unwraps. + CVar got = inter::GetNativeArg(s, args_nested, 1, 0); + // nested[0] is `multi` (Multi); unwrap loop takes multi.vars[0] == 11 + EXPECT_EQ(got.type_, static_cast(VarType::Int)); + + EXPECT_THROW(inter::DispatchCallClosure(s, nullptr, nullptr, 0, JIT_GCC), std::exception); + VarClosure empty_cl{}; + empty_cl.func_ptr = nullptr; + empty_cl.code_str = nullptr; + EXPECT_THROW(inter::DispatchCallClosure(s, &empty_cl, nullptr, 0, JIT_GCC), std::exception); + + EXPECT_THROW(InterpreterExecute(s, nullptr, nullptr, 0, nullptr), std::exception); + EXPECT_THROW(FakeluaInterpCall(s, nullptr, 0, nullptr), std::exception); + + // C exports used by JIT + CVar m2 = FakeluaAllocMultiCVar(s, 1); + FakeluaSetMultiCVarElement(&m2, 0, interp_rt::Int(9)); + EXPECT_EQ(inter::GetMultiCVarElement(m2, 0).data_.i, 9); + + // NativeObject null + EXPECT_EQ(inter::NativeToFakeluaNativeObject(s, nullptr).type_, static_cast(VarType::Nil)); +} + +TEST(var, cvar_to_integer_catch_paths) { + FakeluaStateGuard guard; + auto *s = guard.GetState(); + Var huge_digits; + huge_digits.SetTempString(s, std::string(400, '9')); + EXPECT_EQ(inter::CVarToInteger(reinterpret_cast(huge_digits), 7), 7); + Var bad_exp; + bad_exp.SetTempString(s, "1.2e"); + EXPECT_EQ(inter::CVarToInteger(reinterpret_cast(bad_exp), 8), 8); +}