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);