From 9f34f350d3cb40674344f9712eec0fc4c19c2329 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Sat, 3 Oct 2026 11:20:30 +0800 Subject: [PATCH] fix: reject undeclared variables at semantic analysis (bug5) Undeclared simple names inside function bodies were emitted as the static const kNil singleton: reads happened to yield nil, while writes became 'kNil = rhs', which fails compilation on the GCC backend (and silently mutated the shared nil singleton under TCC). Since CompileFile precompiles the GCC variant, such scripts were unusable on every backend. fakelua intentionally does not support Lua implicit globals, so reject them at semantic analysis with a locatable message: - read -> "unknown variable 'X'; fakelua has no implicit globals, declare it with 'local' first at file:line:col" - write -> "undeclared variable 'X' on the left side of assignment..." Exemptions (names that are not script variables): - the whole dot call chain at direct non-colon call sites f(...) and a.b.f(...), so late-registered natives and package runtime names (Player.AddItem()) keep runtime "not found" errors; chains stop at square indexing (a[1].f() still requires a declared 'a') and colon receivers a:m() are still checked as variables; - dotted native-module roots (math/string/utf8/...) recognized via the new Vm::HasNativeFunctionWithPrefix, covering both dotted calls and non-call module constants (math.pi, string.charpattern); bare natives (print/type/...) and _VERSION are also accepted; - the first-statement package declaration in both forms (package "X" and package = "X", which preprocessing moves into __fakelua_init). Scope resolution follows CGen: until conditions resolve inside the repeat body scope and can see locals declared in that body. Tests: add compile-fail regression Lua files (undeclared read / write-then-read / undeclared dotted base) with exception tests that check message and location, a positive module-name test, and update test_string_sub_undeclared_var to expect the compile-time rejection contract instead of a runtime nil error. --- src/compile/semantic_analysis.cpp | 348 ++++++++++++++++++ src/compile/semantic_analysis.h | 16 + src/jit/vm.h | 13 +- .../test_undeclared_field_base_error.lua | 4 + .../test_undeclared_var_read_error.lua | 4 + .../test_undeclared_var_write_read_error.lua | 5 + test/test_exception.cpp | 58 +++ test/test_string.cpp | 13 +- 8 files changed, 455 insertions(+), 6 deletions(-) create mode 100644 test/lua/exception/test_undeclared_field_base_error.lua create mode 100644 test/lua/exception/test_undeclared_var_read_error.lua create mode 100644 test/lua/exception/test_undeclared_var_write_read_error.lua diff --git a/src/compile/semantic_analysis.cpp b/src/compile/semantic_analysis.cpp index a7a4d1c..2eb5878 100644 --- a/src/compile/semantic_analysis.cpp +++ b/src/compile/semantic_analysis.cpp @@ -45,6 +45,7 @@ AnalysisResult SemanticAnalysis::Analyze(const ParseResult &pr, const CompileCon AnalysisResult ar; AnalyzeGlobalConstNames(pr.chunk, ar); CheckUnsupportedSyntax(pr.chunk, ar); + CheckUndeclaredVars(pr.chunk, ar); AnalyzeFunctionReturnCounts(pr.chunk, ar); WalkSyntaxTree(pr.chunk, [&](const SyntaxTreeInterfacePtr &node) { @@ -1147,4 +1148,351 @@ void SemanticAnalysis::CheckGlobalConstExp(const SyntaxTreeInterfacePtr &exp) { ThrowFakeluaException(std::format("SemanticAnalysis check failed, {} at {}", msg, SyntaxTreeLocationStr(file_name_, ptr))); } +void SemanticAnalysis::CheckUndeclaredVars(const SyntaxTreeInterfacePtr &chunk, const AnalysisResult &ar) { + DEBUG_ASSERT(chunk->Type() == SyntaxTreeType::Block); + const auto block = std::dynamic_pointer_cast(chunk); + + // 文件级声明名:预处理后文件级 local 仍留在顶层(复杂初始化被搬进 __fakelua_init + // 的赋值语句,右侧名字也都来自这些声明),文件级 function/local function 同样在顶层 + // (含编译器合成的 __fakelua_init)。函数体内对这些名字的引用是合法的文件级符号。 + std::unordered_set file_level_names = ar.global_const_names; + for (const auto &stmt: block->Stmts()) { + if (stmt->Type() == SyntaxTreeType::LocalFunction) { + file_level_names.insert(std::dynamic_pointer_cast(stmt)->Name()); + } else if (stmt->Type() == SyntaxTreeType::Function) { + const auto func = std::dynamic_pointer_cast(stmt); + const auto fname = std::dynamic_pointer_cast(func->Funcname()); + if (fname && fname->ColonName().empty()) { + if (const auto fnl = std::dynamic_pointer_cast(fname->FuncNameList()); + fnl && fnl->Funcnames().size() == 1) { + file_level_names.insert(fnl->Funcnames()[0]); + } + } + } + } + + // 沿 Var 的 kVar 前缀取上一级变量(a.b / a.b.c 的基变量),前缀不是 kVar 时返回空。 + auto base_var_of = [](const std::shared_ptr &v) -> std::shared_ptr { + const auto pe = std::dynamic_pointer_cast(v->GetPrefixexp()); + if (!pe || pe->GetPrefixKind() != PrefixExpKind::kVar) { + return nullptr; + } + return std::dynamic_pointer_cast(pe->GetValue()); + }; + + // 豁免变量集合(不按"必须声明"检查),来源有两类: + // 1. 直接调用位 f(...) / a.b.f(...) 的整条点号调用链——不含冒号方法调用 a:m(), + // 其接收者 a 仍是普通变量引用。同文件函数已在 file_level_names;宿主原生函数 + // 允许编译后再注册;其余未知名字维持运行时 FakeluaCallByName 的 "not found" 报错。 + // 链上遇到 kSquare 即停止:a[1].f() 里的 a 是必须声明的表变量。 + // 2. 点号原生库模块根名(math/string/utf8 ...),覆盖非调用位的模块常量访问 + // (math.pi、string.charpattern);模块名由已注册的 "math.xxx" 原生函数前缀识别, + // 它本身不是脚本变量。 + std::unordered_set exempt_vars; + // 赋值左值集合,用于给出「先 local 再赋值」的定向提示。 + std::unordered_set lvalue_vars; + WalkSyntaxTree(chunk, [&](const SyntaxTreeInterfacePtr &n) { + if (n->Type() == SyntaxTreeType::FunctionCall) { + const auto fc = std::dynamic_pointer_cast(n); + if (!fc->Name().empty()) { + return; + } + const auto pe = std::dynamic_pointer_cast(fc->prefixexp()); + if (!pe || pe->GetPrefixKind() != PrefixExpKind::kVar) { + return; + } + for (auto cur = std::dynamic_pointer_cast(pe->GetValue()); cur;) { + exempt_vars.insert(cur.get()); + if (cur->GetVarKind() != VarKind::kDot) { + break; + } + cur = base_var_of(cur); + } + } else if (n->Type() == SyntaxTreeType::Assign) { + // package = "X" 形式的 package 声明(CheckFileLevelStmts 已限定其只能出现在 + // 文件首行;预处理会把它搬进 __fakelua_init):左值 package 是语法关键字位, + // 不是脚本变量。package("X") 调用形式已由上面的调用链豁免覆盖。 + if (std::string pkg_name; ExtractPackageName(n, pkg_name)) { + if (const auto vl = std::dynamic_pointer_cast( + std::dynamic_pointer_cast(n)->Varlist())) { + for (const auto &var_node: vl->Vars()) { + exempt_vars.insert(var_node.get()); + } + } + return; + } + const auto assign = std::dynamic_pointer_cast(n); + const auto vl = std::dynamic_pointer_cast(assign->Varlist()); + if (!vl) { + return; + } + for (const auto &var_node: vl->Vars()) { + const auto v = std::dynamic_pointer_cast(var_node); + if (v && v->GetVarKind() == VarKind::kSimple) { + lvalue_vars.insert(v.get()); + } + } + } + }); + WalkSyntaxTree(chunk, [&](const SyntaxTreeInterfacePtr &n) { + if (n->Type() != SyntaxTreeType::Var) { + return; + } + auto cur = std::dynamic_pointer_cast(n); + if (!cur || cur->GetVarKind() != VarKind::kDot) { + return; + } + // 只沿点号链下行到根;链中夹 kSquare(a[1].b)时根 a 仍是普通表变量引用。 + while (cur && cur->GetVarKind() == VarKind::kDot) { + cur = base_var_of(cur); + } + if (cur && cur->GetVarKind() == VarKind::kSimple && + s_->GetVM().HasNativeFunctionWithPrefix(std::string(cur->GetName()) + ".")) { + exempt_vars.insert(cur.get()); + } + }); + + std::vector> scopes; + CheckVarScopes(chunk, scopes, file_level_names, exempt_vars, lvalue_vars); +} + +bool SemanticAnalysis::IsDeclaredSimpleName(const std::string &name, const std::vector> &scopes, + const std::unordered_set &file_level_names) const { + if (name == "_VERSION") { + return true; + } + for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) { + if (it->contains(name)) { + return true; + } + } + if (file_level_names.contains(name)) { + return true; + } + // 宿主用 RegisterNativeFunction 声明的全局原生函数(print/type/pairs 及用户回调名)。 + if (s_->GetVM().FindNativeFunction(name) != nullptr) { + return true; + } + return false; +} + +void SemanticAnalysis::CheckVarScopes(const SyntaxTreeInterfacePtr &node, std::vector> &scopes, + const std::unordered_set &file_level_names, + const std::unordered_set &exempt_vars, + const std::unordered_set &lvalue_vars) { + if (!node) { + return; + } + + // 检查 kSimple 变量引用。kDot/kSquare 的基表达式随递归继续检查。 + auto check_simple_var = [&](const SyntaxTreeInterfacePtr &vnode) { + const auto var = std::dynamic_pointer_cast(vnode); + if (!var || var->GetVarKind() != VarKind::kSimple || exempt_vars.contains(var.get())) { + return; + } + const std::string &name = var->GetName(); + if (IsDeclaredSimpleName(name, scopes, file_level_names)) { + return; + } + if (lvalue_vars.contains(var.get())) { + ThrowError(std::format("undeclared variable '{}' on the left side of assignment; fakelua has no implicit " + "globals, declare it with 'local' before assigning", + name), + vnode); + } + ThrowError(std::format("unknown variable '{}'; fakelua has no implicit globals, declare it with 'local' first", name), vnode); + }; + + switch (node->Type()) { + case SyntaxTreeType::Block: { + const auto block = std::dynamic_pointer_cast(node); + scopes.emplace_back(); + for (const auto &stmt: block->Stmts()) { + CheckVarScopes(stmt, scopes, file_level_names, exempt_vars, lvalue_vars); + } + scopes.pop_back(); + break; + } + case SyntaxTreeType::LocalVar: { + const auto lv = std::dynamic_pointer_cast(node); + // 先用外层作用域解析初始化表达式(local x = x 右边的 x 是外层同名变量) + CheckVarScopes(lv->Explist(), scopes, file_level_names, exempt_vars, lvalue_vars); + if (const auto nl = std::dynamic_pointer_cast(lv->Namelist())) { + for (const auto &name: nl->Names()) { + scopes.back().insert(name); + } + } + break; + } + case SyntaxTreeType::ForLoop: { + const auto fl = std::dynamic_pointer_cast(node); + CheckVarScopes(fl->ExpBegin(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(fl->ExpEnd(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(fl->ExpStep(), scopes, file_level_names, exempt_vars, lvalue_vars); + scopes.emplace_back(); + scopes.back().insert(fl->Name()); + CheckVarScopes(fl->Block(), scopes, file_level_names, exempt_vars, lvalue_vars); + scopes.pop_back(); + break; + } + case SyntaxTreeType::ForIn: { + const auto fi = std::dynamic_pointer_cast(node); + CheckVarScopes(fi->Explist(), scopes, file_level_names, exempt_vars, lvalue_vars); + scopes.emplace_back(); + if (const auto nl = std::dynamic_pointer_cast(fi->Namelist())) { + for (const auto &name: nl->Names()) { + scopes.back().insert(name); + } + } + CheckVarScopes(fi->Block(), scopes, file_level_names, exempt_vars, lvalue_vars); + scopes.pop_back(); + break; + } + case SyntaxTreeType::Function: + case SyntaxTreeType::LocalFunction: + case SyntaxTreeType::FunctionDef: { + // local function f 对自身函数体可见(支持递归),在开新作用域前登记。 + if (node->Type() == SyntaxTreeType::LocalFunction) { + scopes.back().insert(std::dynamic_pointer_cast(node)->Name()); + } + SyntaxTreeInterfacePtr funcbody; + if (node->Type() == SyntaxTreeType::Function) { + funcbody = std::dynamic_pointer_cast(node)->Funcbody(); + } else if (node->Type() == SyntaxTreeType::LocalFunction) { + funcbody = std::dynamic_pointer_cast(node)->Funcbody(); + } else { + funcbody = std::dynamic_pointer_cast(node)->Funcbody(); + } + scopes.emplace_back(); + if (funcbody) { + const auto fb = std::dynamic_pointer_cast(funcbody); + if (const auto parlist = std::dynamic_pointer_cast(fb->Parlist())) { + if (const auto namelist = std::dynamic_pointer_cast(parlist->Namelist())) { + for (const auto &pname: namelist->Names()) { + scopes.back().insert(pname); + } + } + } + CheckVarScopes(fb->Block(), scopes, file_level_names, exempt_vars, lvalue_vars); + } + scopes.pop_back(); + break; + } + case SyntaxTreeType::Var: { + const auto var = std::dynamic_pointer_cast(node); + check_simple_var(node); + CheckVarScopes(var->GetPrefixexp(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(var->GetExp(), scopes, file_level_names, exempt_vars, lvalue_vars); + break; + } + case SyntaxTreeType::Return: { + CheckVarScopes(std::dynamic_pointer_cast(node)->Explist(), scopes, file_level_names, exempt_vars, lvalue_vars); + break; + } + case SyntaxTreeType::VarList: { + const auto vl = std::dynamic_pointer_cast(node); + for (const auto &v: vl->Vars()) { + CheckVarScopes(v, scopes, file_level_names, exempt_vars, lvalue_vars); + } + break; + } + case SyntaxTreeType::ExpList: { + const auto el = std::dynamic_pointer_cast(node); + for (const auto &exp: el->Exps()) { + CheckVarScopes(exp, scopes, file_level_names, exempt_vars, lvalue_vars); + } + break; + } + case SyntaxTreeType::Assign: { + const auto assign = std::dynamic_pointer_cast(node); + CheckVarScopes(assign->Varlist(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(assign->Explist(), scopes, file_level_names, exempt_vars, lvalue_vars); + break; + } + case SyntaxTreeType::FunctionCall: { + const auto fc = std::dynamic_pointer_cast(node); + CheckVarScopes(fc->prefixexp(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(fc->Args(), scopes, file_level_names, exempt_vars, lvalue_vars); + break; + } + case SyntaxTreeType::Args: { + const auto args = std::dynamic_pointer_cast(node); + CheckVarScopes(args->Explist(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(args->Tableconstructor(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(args->String(), scopes, file_level_names, exempt_vars, lvalue_vars); + break; + } + case SyntaxTreeType::TableConstructor: { + CheckVarScopes(std::dynamic_pointer_cast(node)->Fieldlist(), scopes, file_level_names, exempt_vars, + lvalue_vars); + break; + } + case SyntaxTreeType::FieldList: { + const auto fl = std::dynamic_pointer_cast(node); + for (const auto &field: fl->Fields()) { + CheckVarScopes(field, scopes, file_level_names, exempt_vars, lvalue_vars); + } + break; + } + case SyntaxTreeType::Field: { + const auto field = std::dynamic_pointer_cast(node); + CheckVarScopes(field->Key(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(field->Value(), scopes, file_level_names, exempt_vars, lvalue_vars); + break; + } + case SyntaxTreeType::While: { + const auto while_node = std::dynamic_pointer_cast(node); + CheckVarScopes(while_node->Exp(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(while_node->Block(), scopes, file_level_names, exempt_vars, lvalue_vars); + break; + } + case SyntaxTreeType::Repeat: { + // Lua 语义(与 CGen::CompileStmtRepeat 一致):until 条件在 repeat body 的 + // 作用域内解析,可以引用 body 中声明的 local(repeat local x ... until x > 0)。 + const auto rep = std::dynamic_pointer_cast(node); + const auto rep_body = std::dynamic_pointer_cast(rep->Block()); + scopes.emplace_back(); + if (rep_body) { + for (const auto &stmt: rep_body->Stmts()) { + CheckVarScopes(stmt, scopes, file_level_names, exempt_vars, lvalue_vars); + } + } + CheckVarScopes(rep->Exp(), scopes, file_level_names, exempt_vars, lvalue_vars); + scopes.pop_back(); + break; + } + case SyntaxTreeType::If: { + const auto if_node = std::dynamic_pointer_cast(node); + CheckVarScopes(if_node->Exp(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(if_node->Block(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(if_node->ElseIfs(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(if_node->ElseBlock(), scopes, file_level_names, exempt_vars, lvalue_vars); + break; + } + case SyntaxTreeType::ElseIfList: { + const auto eil = std::dynamic_pointer_cast(node); + for (size_t i = 0; i < eil->ElseifSize(); ++i) { + CheckVarScopes(eil->ElseifExp(i), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(eil->ElseifBlock(i), scopes, file_level_names, exempt_vars, lvalue_vars); + } + break; + } + case SyntaxTreeType::Exp: { + const auto exp = std::dynamic_pointer_cast(node); + CheckVarScopes(exp->Left(), scopes, file_level_names, exempt_vars, lvalue_vars); + CheckVarScopes(exp->Right(), scopes, file_level_names, exempt_vars, lvalue_vars); + break; + } + case SyntaxTreeType::PrefixExp: { + CheckVarScopes(std::dynamic_pointer_cast(node)->GetValue(), scopes, file_level_names, exempt_vars, + lvalue_vars); + break; + } + default: { + // 其余节点(break/continue/goto/label/空语句/名字列表等)不含变量引用。 + break; + } + } +} + }// namespace fakelua diff --git a/src/compile/semantic_analysis.h b/src/compile/semantic_analysis.h index e9b9f17..44727e1 100644 --- a/src/compile/semantic_analysis.h +++ b/src/compile/semantic_analysis.h @@ -4,6 +4,7 @@ #include "compile/syntax_tree.h" #include "fakelua.h" #include +#include #include namespace fakelua { @@ -41,6 +42,21 @@ class SemanticAnalysis { void CheckForIn(const SyntaxTreeInterfacePtr &node); void CheckExp(const SyntaxTreeInterfacePtr &node); void CheckGlobalConstExp(const SyntaxTreeInterfacePtr &exp); + // fakelua 刻意不支持 Lua 的隐式全局:每个简单名必须能解析到 local/形参/upvalue、 + // 文件级 local 或文件级函数,或宿主注册的原生函数。未声明名字在编译期直接报错 + // (读返回 nil、写改写 const kNil 的旧行为都是隐患)。豁免:直接调用位 f(...) 的 + // 整条点号调用链(未知名维持运行时 FakeluaCallByName 的 "not found" 报错,且原生 + // 函数允许编译后注册)、math/string 等点号原生库的模块根名,以及文件首行 package + // 声明(package "X" / package = "X")的名字。 + void CheckUndeclaredVars(const SyntaxTreeInterfacePtr &chunk, const AnalysisResult &ar); + // CheckUndeclaredVars 的词法作用域递归,作用域栈语义与 CGen::ResolveScopes 对齐。 + void CheckVarScopes(const SyntaxTreeInterfacePtr &node, std::vector> &scopes, + const std::unordered_set &file_level_names, + const std::unordered_set &exempt_vars, + const std::unordered_set &lvalue_vars); + [[nodiscard]] bool IsDeclaredSimpleName(const std::string &name, + const std::vector> &scopes, + const std::unordered_set &file_level_names) const; [[noreturn]] void ThrowError(const std::string &msg, const SyntaxTreeInterfacePtr &ptr); void AnalyzeFunctionReturnCounts(const SyntaxTreeInterfacePtr &chunk, AnalysisResult &ar); diff --git a/src/jit/vm.h b/src/jit/vm.h index bfc4639..ef5b6fb 100644 --- a/src/jit/vm.h +++ b/src/jit/vm.h @@ -51,12 +51,23 @@ class Vm { native_functions_[name] = NativeFuncEntry{arg_count, is_vararg, std::move(callback)}; } - // 查找原生函数条目(供 FakeluaCallByName 使用) + // 查找原生函数条目(供 FakeluaCallByName 分发用) [[nodiscard]] const NativeFuncEntry *FindNativeFunction(std::string_view name) const { const auto it = native_functions_.find(name); return it != native_functions_.end() ? &it->second : nullptr; } + // 是否存在以 prefix 开头的注册原生函数名(语义分析用它识别 math.xxx / string.xxx + // 这类点号原生库的模块根名,模块名本身不是脚本变量) + [[nodiscard]] bool HasNativeFunctionWithPrefix(std::string_view prefix) const { + for (const auto &[name, entry]: native_functions_) { + if (name.starts_with(prefix)) { + return true; + } + } + return false; + } + // 分配一个唯一的全局变量名 std::string AllocGlobalName() { return std::format("__fakelua_global_{}__", global_name_++); diff --git a/test/lua/exception/test_undeclared_field_base_error.lua b/test/lua/exception/test_undeclared_field_base_error.lua new file mode 100644 index 0000000..3981335 --- /dev/null +++ b/test/lua/exception/test_undeclared_field_base_error.lua @@ -0,0 +1,4 @@ +function test(a) + local x = unknown_object.field + return x +end diff --git a/test/lua/exception/test_undeclared_var_read_error.lua b/test/lua/exception/test_undeclared_var_read_error.lua new file mode 100644 index 0000000..dd850e7 --- /dev/null +++ b/test/lua/exception/test_undeclared_var_read_error.lua @@ -0,0 +1,4 @@ +function test(a) + local x = unknown_global + return x +end diff --git a/test/lua/exception/test_undeclared_var_write_read_error.lua b/test/lua/exception/test_undeclared_var_write_read_error.lua new file mode 100644 index 0000000..3f3b8b4 --- /dev/null +++ b/test/lua/exception/test_undeclared_var_write_read_error.lua @@ -0,0 +1,5 @@ +function test(a) + if a < 0 then flag = 1 end + if flag == 1 then flag = 0 end + return 0 +end diff --git a/test/test_exception.cpp b/test/test_exception.cpp index 307e62d..7257197 100644 --- a/test/test_exception.cpp +++ b/test/test_exception.cpp @@ -1030,6 +1030,64 @@ TEST(exception, no_define_lvalue_error) { } } +// fakelua 刻意不支持 Lua 隐式全局:函数体内未声明的简单名(读 / 写 / 先写后读 / +// 点号字段的未声明根名)必须在语义分析阶段直接报错,并带上文件名、行、列, +// 而不是把读编成 kNil、把写编成对 const kNil 的赋值。 +TEST(exception, undeclared_var_error) { + struct Case { + const char *file; + const char *name; + int line; + }; + const Case cases[] = { + {"./exception/test_undeclared_var_read_error.lua", "unknown_global", 2}, + {"./exception/test_undeclared_var_write_read_error.lua", "flag", 2}, + {"./exception/test_undeclared_field_base_error.lua", "unknown_object", 2}, + {"./exception/test_no_define_lvalue_error.lua", "b", 2}, + }; + for (const auto &c: cases) { + SCOPED_TRACE(c.file); + FakeluaStateGuard sg; + auto s = sg.GetState(); + ASSERT_NE(s, nullptr); + SetDebugLogLevel(s, 0); + + try { + CompileFile(s, c.file, {}); + FAIL() << "expected CompileFile to throw for " << c.file; + } catch (const std::exception &e) { + const std::string msg = e.what(); + EXPECT_NE(msg.find("no implicit globals"), std::string::npos) << msg; + EXPECT_NE(msg.find(c.name), std::string::npos) << msg; + EXPECT_NE(msg.find(std::string(c.file) + ":" + std::to_string(c.line) + ":"), std::string::npos) << msg; + } + } +} + +// 模块名不是脚本变量:点号原生库调用与模块常量必须正常编译, +// 点号链 / 调用链上的模块根名不应被误判成未声明变量。 +TEST(exception, undeclared_var_check_allows_module_names) { + FakeluaStateGuard sg; + auto s = sg.GetState(); + ASSERT_NE(s, nullptr); + SetDebugLogLevel(s, 0); + + const std::string script = R"( +function f(x) + local t = {} + table.insert(t, math.floor(x)) + local p = math.pi + local c = string.charpattern + local s = tostring(math.maxinteger) + if t[1] == 3 and p > 3.0 and #c == 1 and #s > 0 then + return s + end + return "bad" +end +)"; + ASSERT_NO_THROW(CompileString(s, script, {})); +} + TEST(exception, global_duplicate_lvalue_error) { FakeluaStateGuard sg; auto s = sg.GetState(); diff --git a/test/test_string.cpp b/test/test_string.cpp index 5a52cc5..9e1d799 100644 --- a/test/test_string.cpp +++ b/test/test_string.cpp @@ -587,8 +587,8 @@ TEST(test_string, test_string_sub_undeclared_var) { ASSERT_NE(s, nullptr); CompileConfig config; - // fmod 与 C 标准库 math.h 中的 fmod 同名;未在 Lua 中声明时应求值为 nil, - // 传给 string.sub 必须抛出异常(与 Lua 5.4 行为一致),不得被当成 C 函数指针调用。 + // fmod 与 C 标准库 math.h 中的 fmod 同名。fakelua 不支持隐式全局:未声明的简单名 + // 在编译期直接报错——既不会静默求值为 nil,更不可能被当成 C 函数指针传给 string.sub。 const std::string script = R"( function test_fmod_sub() local suA_ub = string.sub(fmod, 3) @@ -596,10 +596,13 @@ TEST(test_string, test_string_sub_undeclared_var) { end )"; - for (auto jit_type: AllJitTypes()) { + try { CompileString(s, script, config); - int64_t res = 0; - EXPECT_THROW(Call(s, jit_type, "test_fmod_sub", res), std::exception); + FAIL() << "expected CompileString to throw for undeclared variable fmod"; + } catch (const std::exception &e) { + const std::string msg = e.what(); + EXPECT_NE(msg.find("unknown variable 'fmod'"), std::string::npos) << msg; + EXPECT_NE(msg.find("no implicit globals"), std::string::npos) << msg; } FakeluaDeleteState(s);