diff --git a/README.md b/README.md index 0530fb1..8c49398 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,18 @@ To support this positioning, FakeLua **does not implement a complex dynamic garb This design allows FakeLua to fully eliminate GC pause impact on frame rates while maintaining JIT native execution speed, keeping memory overhead at a completely predictable, extremely low level. +### 3. Engineering & Language Philosophy: Deliberate Trade-offs, Not Blind Cloning +FakeLua is not a blind clone of standard Lua's overly permissive dynamic features. Instead, it makes conscious, intentional trade-offs for **execution performance**, **large-scale engineering collaboration**, and **long-term maintainability**: + +* **Strict Function Arity Enforcement**: + In standard Lua, calling a fixed-parameter function with too few arguments silently pads `nil`, and extra arguments are silently discarded. In large-scale, multi-developer projects, this looseness easily conceals bugs caused by signature changes, omitted arguments, or parameter mismatches. FakeLua deliberately tightens this rule: + - Direct calls are strictly validated at **compile time**, rejecting mismatched argument counts with a compile error; + - Dynamic / named calls are strictly intercepted at **runtime**, throwing an exception if argument count differs; + - Functions legitimately expecting variable arguments must explicitly declare varargs (`...`). Strict arity keeps interfaces explicit, prevents silent runtime nil pollution, and enables the JIT compiler to emit direct, zero-overhead native C calls without redundant runtime nil padding or truncation boilerplate. +* **No Implicit Globals**: Variables must be explicitly declared with `local`; undeclared assignments trigger compile errors, preventing accidental global pollution from typos. +* **No Implicit Type Coercion**: Arithmetic operators do not implicitly convert strings to numbers (`"10" + 1` errors), keeping types predictable and enabling aggressive native numeric specialization. +* **Static Native Dispatch**: Built-in C++ functions are statically dispatched by name at compile time rather than wrapped as first-class closure values, maximizing call performance. + ## Core Features ### Three Execution Backends @@ -130,14 +142,16 @@ FL_SPEC(Table_Spec_1, point, x) = NativeAdd(FL_SPEC(Table_Spec_1, point, x), (CV - **Lua 5.4 pattern matching**: `string.find`/`match`/`gmatch`/`gsub` use a self-contained Lua-pattern engine (`%d`/`%w` classes, custom sets, lazy `-`, captures, frontier `%f[set]`, balanced `%bxy`), fully compatible with standard Lua. - **String algorithms**: `string.trim`/`trim_left`/`trim_right`/`split`/`join`/`replace`/`starts_with`/`ends_with`/`contains`/`iequals`/`icontains`/`istarts_with`/`iends_with` via Boost.Algorithm. -### Not Supported +### Not Supported / Differences from Standard Lua +- **Strict function arity**: Fixed-parameter functions require exact argument-to-parameter count matching. Calling with too few or too many arguments triggers compile-time or runtime errors instead of silently padding nil or discarding extras (use `...` for variadic arguments). - **Coroutines**: No `coroutine.create`/`resume`/`yield` support. - **Metatables**: No `__index`, `__newindex`, metamethods, or operator overloading. - **`require`/`module`**: No standard module system (replaced by `package "Name"` mechanism). - **`rawequal`/`rawget`/`rawset`/`rawlen`**: Meaningless without metatables. - **Debug library**: No `debug.*` standard library. - **Implicit type coercion**: No string→number conversion in arithmetic (`"10" + 1` errors). +- **Implicit globals**: Local variables must be declared with `local`; implicit global declarations are rejected. ## Built-in Standard Libraries diff --git a/README.zh.md b/README.zh.md index ee1d9ca..795fa4d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -26,6 +26,18 @@ FakeLua 的设计初衷是为了在**高性能游戏服务器**或类似的实 这种设计使得 FakeLua 在保持 JIT 原生执行速度的同时,能够彻底消除垃圾回收停顿(GC Pause)对帧率的影响,让内存开销保持在一条完全可预测的、极低的水平线上。 +### 3. 工程化与语言设计哲学:严谨取舍,拒绝无脑照搬 +FakeLua 并非无脑照搬标准 Lua 的所有动态弱约束特性,而是在**执行性能**、**大规模工程协同**和**代码易维护性**上做出了有意识的取舍: + +* **定参函数实参个数严格匹配(Strict Arity Enforcement)**: + 标准 Lua 在调用定参函数时,少传参数会自动补 `nil`,多传参数会被默默丢弃。在大规模多人协作项目中,这种过度宽松的特性极易掩盖接口签名变更、遗漏必要传参或参数错位等隐蔽 bug。FakeLua 刻意严格收紧该语义: + - 直接调用在**编译期**严格校验参数个数,不一致直接报编译错误; + - 动态/按名分发在**运行期**严格拦截,实参个数不匹配直接抛出异常; + - 需要变长实参时必须显式声明可变参数(`...`)。严格匹配不仅让接口契约清晰透明、杜绝隐蔽缺陷,也让 JIT 能够生成最纯粹高效的原生 C 调用,省去运行时动态补 nil 和截断的冗余指令。 +* **杜绝隐式全局变量**:变量必须显式使用 `local` 声明,未声明直接赋值会在编译期拦截,彻底消除拼写错误带来的全局污染。 +* **杜绝隐式类型转换**:算术运算不做字符串到数字的隐式转换(如 `"10" + 1` 明确报错),确保计算意图可预测,支撑编译器激进的原生数值特化。 +* **原生函数静态分发**:C++ 原生函数作为高效宿主接口,在编译期按名静态分发而非作为第一类值闭包,大幅提升热点调用性能。 + ## 核心特性 ### 三种执行后端 @@ -130,14 +142,16 @@ FL_SPEC(Table_Spec_1, point, x) = NativeAdd(FL_SPEC(Table_Spec_1, point, x), (CV - **Lua 5.4 模式匹配**:`string.find`/`match`/`gmatch`/`gsub` 使用自包含的 Lua 模式引擎(`%d`/`%w` 字符类、自定义集合、惰性 `-`、捕获、前沿 `%f[set]`、平衡 `%bxy`),与标准 Lua 完全兼容。 - **字符串算法**:`string.trim`/`trim_left`/`trim_right`/`split`/`join`/`replace`/`starts_with`/`ends_with`/`contains`/`iequals`/`icontains`/`istarts_with`/`iends_with` 底层使用 Boost.Algorithm。 -### 未支持 +### 未支持 / 与标准 Lua 的设计差异 +- **函数实参个数严格匹配**:定参函数的实参与形参个数必须严格一致,少传或多传参数会在编译期/运行时直接报错,杜绝标准 Lua 隐式补 nil 或静默截断掩盖的接口 bug(变长参数请显式声明 `...`)。 - **协程(Coroutine)**:不支持 `coroutine.create`/`resume`/`yield`。 - **元表(Metatable)**:不支持 `__index`、`__newindex` 等元方法。 - **`require`/`module`**:无标准模块系统(由 `package "Name"` 机制替代)。 - **`rawequal`/`rawget`/`rawset`/`rawlen`**:因无元表,这些函数无意义。 - **debug 标准库**:不支持 `debug.*`。 - **隐式类型转换**:算术中不做字符串→数字的隐式转换(`"10" + 1` 会报错)。 +- **隐式全局变量**:局部变量必须使用 `local` 声明,不支持隐式全局赋值。 ## 标准内置扩展库 diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index 150a45c..8ca89f1 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -4131,12 +4131,22 @@ std::string CGen::TryCompileSpecDirectCall(const std::shared_ptrsecond; const auto explist_arg = args_ptr->Explist(); DEBUG_ASSERT(explist_arg->Type() == SyntaxTreeType::ExpList); const auto explist_arg_ptr = std::dynamic_pointer_cast(explist_arg); const auto &raw_args = explist_arg_ptr->Exps(); + if (static_cast(raw_args.size()) != fn_it->second.params_count) { + return ""; + } if (int bitmask = 0; TryInferMathCallBitmask(callee_name, raw_args, bitmask)) { std::unordered_map native_exprs; diff --git a/src/compile/c_runtime_header.h b/src/compile/c_runtime_header.h index e8ebeec..66ed557 100644 --- a/src/compile/c_runtime_header.h +++ b/src/compile/c_runtime_header.h @@ -392,6 +392,11 @@ static inline CVar FlCallClosure(State *state, CVar cl_var, int arg_num, ...) { const bool is_vararg = cl->is_vararg; const int expected_arg_count = cl->expected_arg_count; const int fixed_arg_count = is_vararg ? expected_arg_count - 1 : expected_arg_count; + // 形参个数也要限:下面按 expected_arg_count 往 temp_arg_arr(定长 32)里写, + // 声明了超过上限的形参会栈缓冲溢出(与 vm.cpp 的 CallByNameImpl 一致)。 + if (UNLIKELY(expected_arg_count < 0 || expected_arg_count > (int)kMaxFunctionInputParams)) { + FakeluaThrowError(state, "too many arguments"); + } CVar raw_arg_arr[kMaxFunctionInputParams]; va_list args_list; diff --git a/src/compile/semantic_analysis.cpp b/src/compile/semantic_analysis.cpp index 2eb5878..f8e5cdb 100644 --- a/src/compile/semantic_analysis.cpp +++ b/src/compile/semantic_analysis.cpp @@ -1269,10 +1269,10 @@ bool SemanticAnalysis::IsDeclaredSimpleName(const std::string &name, const std:: if (file_level_names.contains(name)) { return true; } - // 宿主用 RegisterNativeFunction 声明的全局原生函数(print/type/pairs 及用户回调名)。 - if (s_->GetVM().FindNativeFunction(name) != nullptr) { - return true; - } + // 宿主用 RegisterNativeFunction 注册的全局原生函数不在此列:裸原生名只有出现在 + // 直接调用位(f(...) / a.b.f(...),整条链已在 exempt_vars 豁免)才合法;出现在 + // 值位置由 check_simple_var 给出「native function cannot be used as a value」 + // 的明确编译期报错(fakelua 按名字派发原生调用,原生函数不是一等值)。 return false; } @@ -1294,6 +1294,16 @@ void SemanticAnalysis::CheckVarScopes(const SyntaxTreeInterfacePtr &node, std::v if (IsDeclaredSimpleName(name, scopes, file_level_names)) { return; } + // 原生函数出现在值位置(local t = type、pcall(print, ...)、return next 等): + // fakelua 在编译期按名字派发原生调用,原生函数不是一等值。此前这里放行, + // 到运行时才以 "attempt to call/index a non-function/table value" 失败, + // 报错位置与原因都有误导性;改为编译期明确报错。调用位已在 exempt_vars 豁免。 + if (s_->GetVM().FindNativeFunction(name) != nullptr) { + ThrowError(std::format("native function '{}' cannot be used as a value; fakelua dispatches native calls " + "by name at compile time, call it directly or wrap it in a Lua function", + name), + vnode); + } 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", @@ -1380,6 +1390,36 @@ void SemanticAnalysis::CheckVarScopes(const SyntaxTreeInterfacePtr &node, std::v } case SyntaxTreeType::Var: { const auto var = std::dynamic_pointer_cast(node); + // 点号原生库函数出现在值位置(local f = table.concat、pcall(table.concat, t)、 + // { cb = string.format } 等):调用位的整条点号链已在 exempt_vars 豁免,能走到 + // 这里的 kDot 链都是值位置。全名命中原生函数注册表时给出与裸原生名一致的明确 + // 编译期报错。常量访问(math.pi、string.charpattern)不在注册表里;根名是局部 + // 变量时(local t = {} 之后的 t.concat)是普通字段访问,两者都不报错。 + if (var->GetVarKind() == VarKind::kDot && !exempt_vars.contains(var.get())) { + std::string full; + std::shared_ptr root; + for (auto cur = var; cur && cur->GetVarKind() == VarKind::kDot;) { + full = cur->GetName() + (full.empty() ? "" : "." + full); + const auto pe = std::dynamic_pointer_cast(cur->GetPrefixexp()); + root = (pe && pe->GetPrefixKind() == PrefixExpKind::kVar) + ? std::dynamic_pointer_cast(pe->GetValue()) + : nullptr; + cur = root; + } + if (root && root->GetVarKind() == VarKind::kSimple && !full.empty()) { + const std::string full_name = root->GetName() + "." + full; + bool root_declared = file_level_names.contains(root->GetName()); + for (auto it = scopes.rbegin(); it != scopes.rend() && !root_declared; ++it) { + root_declared = it->contains(root->GetName()); + } + if (!root_declared && s_->GetVM().FindNativeFunction(full_name) != nullptr) { + ThrowError(std::format("native function '{}' cannot be used as a value; fakelua dispatches native " + "calls by name at compile time, call it directly or wrap it in a Lua function", + full_name), + 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); diff --git a/src/interp/codegen.cpp b/src/interp/codegen.cpp index 00cae67..04bcd76 100644 --- a/src/interp/codegen.cpp +++ b/src/interp/codegen.cpp @@ -1612,7 +1612,12 @@ int InterpCodegen::CompileFunctioncall(const SyntaxTreeInterfacePtr &functioncal var_ptr = var.get(); if (const auto it = var_to_def_map_.find(var_ptr); it != var_to_def_map_.end()) { is_local_callee = true; - file_level_func = it->second->defining_func == nullptr; + // 只有文件级 local function 才能走 CALLNAME:named_protos_ 只登记 + // 函数定义。文件级 local 变量(如 local gv = f)即使 defining_func + // 为 nullptr 也不在 named_protos_ 里,必须按闭包调用,否则运行时报 + // "function 'gv' not found"(C 后端此处按闭包处理,行为一致)。 + file_level_func = it->second->defining_func == nullptr && it->second->def_node != nullptr && + it->second->def_node->Type() == SyntaxTreeType::LocalFunction; } } else if (var && var->GetVarKind() == VarKind::kDot) { const auto base_pe = var->GetPrefixexp(); diff --git a/src/jit/vm.cpp b/src/jit/vm.cpp index a621781..bdf1b69 100644 --- a/src/jit/vm.cpp +++ b/src/jit/vm.cpp @@ -115,6 +115,12 @@ static CVar CallByNameImpl(State *state, int jit_type, const char *name, int arg ThrowFakeluaException(std::format("FakeluaCallByName: too many arguments ({}) passed for function '{}', max is {}", arg_num, name, kMaxFunctionInputParams)); } + // 形参个数也要限:下面按 expected_arg_count 往 temp_arg_arr(定长 32)里写, + // 声明了超过上限的形参会越界(FlCallClosure 里同样有这层保护)。 + if (UNLIKELY(expected_arg_count < 0 || expected_arg_count > static_cast(kMaxFunctionInputParams))) { + ThrowFakeluaException(std::format("FakeluaCallByName: function '{}' declares too many parameters ({}), max is {}", name, expected_arg_count, kMaxFunctionInputParams)); + } + // 展开 Multi / 补齐参数 const CVar *arg_arr = nullptr; const bool last_is_multi = (arg_num > 0 && raw_arg_arr[arg_num - 1].type_ == static_cast(VarType::Multi)); diff --git a/src/native/basic/native_basic.cpp b/src/native/basic/native_basic.cpp index 4164d30..474a7c7 100644 --- a/src/native/basic/native_basic.cpp +++ b/src/native/basic/native_basic.cpp @@ -238,7 +238,10 @@ void RegisterBasicLibraryApi(State *s) { }); // type(v) + // 与 Lua 5.4 的 luaL_checkany 对齐:实参缺失必须与显式传 nil 区分, + // type() 报 "bad argument #1 to 'type' (value expected)"。 RegisterNativeFunction(s, "type", 1, false, [](State *state, CVar *args, int n) -> CVar { + if (n < 1) ThrowBadArgument(1, "type", "value expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); switch (static_cast(a0.type_)) { case VarType::Nil: @@ -262,6 +265,7 @@ void RegisterBasicLibraryApi(State *s) { // tostring(v) RegisterNativeFunction(s, "tostring", 1, false, [](State *state, CVar *args, int n) -> CVar { + if (n < 1) ThrowBadArgument(1, "tostring", "value expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); const auto &v = AsVar(a0); if (v.Type() == VarType::String || v.Type() == VarType::StringId) { @@ -272,7 +276,9 @@ void RegisterBasicLibraryApi(State *s) { }); // tonumber(e [, base]) + // e 必填:tonumber() 与 tonumber(nil) 语义不同(前者报错,后者返回 nil)。 RegisterNativeFunction(s, "tonumber", 1, true, [](State *state, CVar *args, int n) -> CVar { + if (n < 1) ThrowBadArgument(1, "tonumber", "value expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); const auto &v = AsVar(a0); diff --git a/src/native/math/native_math.cpp b/src/native/math/native_math.cpp index 8954e8c..b542401 100644 --- a/src/native/math/native_math.cpp +++ b/src/native/math/native_math.cpp @@ -131,7 +131,7 @@ void RegisterMathLibraryApi(State *s) { }); RegisterNativeFunction(s, "math.max", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "math.max", "number expected"); CVar max_cvar = inter::GetNativeArg(state, args, n, 0); double max_v = CheckMathNumberArg(max_cvar, 1, "math.max"); for (int i = 1; i < n; ++i) { @@ -153,7 +153,7 @@ void RegisterMathLibraryApi(State *s) { }); RegisterNativeFunction(s, "math.min", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "math.min", "number expected"); CVar min_cvar = inter::GetNativeArg(state, args, n, 0); double min_v = CheckMathNumberArg(min_cvar, 1, "math.min"); for (int i = 1; i < n; ++i) { diff --git a/src/native/string/native_string.cpp b/src/native/string/native_string.cpp index b7e867a..6634de0 100644 --- a/src/native/string/native_string.cpp +++ b/src/native/string/native_string.cpp @@ -646,7 +646,7 @@ void RegisterStringLibraryApi(State *s) { if (!s) return; RegisterNativeFunction(s, "string.len", 1, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaInt(state, 0); + if (n < 1) ThrowBadArgument(1, "string.len", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.len"); std::string temp; @@ -665,9 +665,14 @@ void RegisterStringLibraryApi(State *s) { int64_t len = static_cast(sv.size()); int64_t start_pos = CheckIntegerArg(inter::GetNativeArg(state, args, n, 1), 2, "string.sub"); + // Lua 5.4:j 是 luaL_optinteger(getendpos),显式 nil 与省略等价,默认 -1(末尾)。 + // i 是 luaL_checkinteger,nil 必须报错——与上面 n<2 的缺参报错一致。 int64_t end_pos = len; if (n >= 3) { - end_pos = CheckIntegerArg(inter::GetNativeArg(state, args, n, 2), 3, "string.sub"); + CVar j_var = inter::GetNativeArg(state, args, n, 2); + if (j_var.type_ != static_cast(VarType::Nil)) { + end_pos = CheckIntegerArg(j_var, 3, "string.sub"); + } } start_pos = NormalizePos(start_pos, len); @@ -685,7 +690,9 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.rep", 2, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaStringView(state, ""); + // Lua 5.4:实参缺失与显式 nil 不同,缺失必报 bad argument(rep 的 s/n 都必填)。 + if (n < 1) ThrowBadArgument(1, "string.rep", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.rep", "number expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.rep"); std::string temp; @@ -696,12 +703,15 @@ void RegisterStringLibraryApi(State *s) { int64_t rep_cnt = CheckIntegerArg(rep_var, 2, "string.rep"); if (rep_cnt <= 0) return inter::NativeToFakeluaStringView(state, ""); + // Lua 5.4:sep 是 luaL_optlstring,显式 nil 与省略等价,默认 ""。 std::string sep = ""; if (n >= 3) { CVar a2 = inter::GetNativeArg(state, args, n, 2); - CheckStringArg(a2, 3, "string.rep"); - std::string temp_sep; - sep = std::string(GetStringArgView(a2, temp_sep)); + if (a2.type_ != static_cast(VarType::Nil)) { + CheckStringArg(a2, 3, "string.rep"); + std::string temp_sep; + sep = std::string(GetStringArgView(a2, temp_sep)); + } } if (sv.empty() && sep.empty()) { @@ -723,7 +733,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.reverse", 1, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.reverse", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.reverse"); std::string temp; @@ -733,7 +743,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.lower", 1, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.lower", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.lower"); std::string temp; @@ -751,7 +761,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.upper", 1, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.upper", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.upper"); std::string temp; @@ -969,7 +979,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.byte", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "string.byte", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.byte"); std::string temp; @@ -977,17 +987,23 @@ void RegisterStringLibraryApi(State *s) { int64_t len = static_cast(sv.size()); if (len == 0) return inter::NativeToFakeluaNil(state); + // Lua 5.4 str_byte 的 i/j 是 luaL_optinteger:显式 nil 与省略等价(i 默认 1, + // j 默认 i)。包装函数转发的显式 nil 不能误报 number expected。 int64_t start_pos = 1; if (n >= 2) { CVar a1 = inter::GetNativeArg(state, args, n, 1); - // 与 Lua 5.4 对齐:超大 float 报 "number has no integer representation" - start_pos = CheckIntegerArg(a1, 2, "string.byte"); + if (a1.type_ != static_cast(VarType::Nil)) { + // 与 Lua 5.4 对齐:超大 float 报 "number has no integer representation" + start_pos = CheckIntegerArg(a1, 2, "string.byte"); + } } int64_t end_pos = start_pos; if (n >= 3) { CVar a2 = inter::GetNativeArg(state, args, n, 2); - end_pos = CheckIntegerArg(a2, 3, "string.byte"); + if (a2.type_ != static_cast(VarType::Nil)) { + end_pos = CheckIntegerArg(a2, 3, "string.byte"); + } } start_pos = NormalizePos(start_pos, len); @@ -1059,7 +1075,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.format", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.format", "string expected"); CVar fmt_var = inter::GetNativeArg(state, args, n, 0); std::string temp_fmt; std::string_view fmt = GetStringArgView(fmt_var, temp_fmt); @@ -1254,7 +1270,9 @@ void RegisterStringLibraryApi(State *s) { // 若 plain 为 true,则退化为纯子串查找(忽略模式元字符)。 // 找不到时返回 nil。 RegisterNativeFunction(s, "string.find", 2, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaNil(state); + // s 与 pattern 都必填:实参缺失报 bad argument(与显式 nil 区分)。 + if (n < 1) ThrowBadArgument(1, "string.find", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.find", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CVar a1 = inter::GetNativeArg(state, args, n, 1); CheckStringArg(a0, 1, "string.find"); @@ -1267,8 +1285,11 @@ void RegisterStringLibraryApi(State *s) { int64_t init_pos = 1; if (n >= 3) { CVar a2 = inter::GetNativeArg(state, args, n, 2); - CheckNumberArg(a2, 3, "string.find"); - init_pos = inter::CVarToInteger(a2, 1); + // Lua 5.4:init 是 luaL_optinteger,显式 nil 与省略等价,默认 1。 + if (a2.type_ != static_cast(VarType::Nil)) { + CheckNumberArg(a2, 3, "string.find"); + init_pos = inter::CVarToInteger(a2, 1); + } } init_pos = NormalizePos(init_pos, len); if (init_pos < 1) init_pos = 1; @@ -1310,7 +1331,8 @@ void RegisterStringLibraryApi(State *s) { // 与 find 一样从 init 起搜索,只是不返回位置;前导 '^' 锚定到 init 点。 // 有捕获返回所有捕获(位置捕获为整数),无捕获返回整个匹配。 RegisterNativeFunction(s, "string.match", 2, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "string.match", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.match", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CVar a1 = inter::GetNativeArg(state, args, n, 1); CheckStringArg(a0, 1, "string.match"); @@ -1324,8 +1346,11 @@ void RegisterStringLibraryApi(State *s) { int64_t init_pos = 1; if (n >= 3) { CVar a2 = inter::GetNativeArg(state, args, n, 2); - CheckNumberArg(a2, 3, "string.match"); - init_pos = inter::CVarToInteger(a2, 1); + // Lua 5.4:init 是 luaL_optinteger,显式 nil 与省略等价,默认 1。 + if (a2.type_ != static_cast(VarType::Nil)) { + CheckNumberArg(a2, 3, "string.match"); + init_pos = inter::CVarToInteger(a2, 1); + } } init_pos = NormalizePos(init_pos, len); if (init_pos < 1) init_pos = 1; @@ -1354,7 +1379,8 @@ void RegisterStringLibraryApi(State *s) { // 返回一个迭代器闭包;每次调用返回下一个匹配(有捕获时返回捕获值)。 // 模式惰性解析,非法模式在首次迭代时抛错(与 Lua 一致)。 RegisterNativeFunction(s, "string.gmatch", 2, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "string.gmatch", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.gmatch", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CVar a1 = inter::GetNativeArg(state, args, n, 1); CheckStringArg(a0, 1, "string.gmatch"); @@ -1379,7 +1405,10 @@ void RegisterStringLibraryApi(State *s) { // 匹配;返回 nil/false 保留原文)/ table(以首个捕获或整个匹配为键查询,nil/false // 保留原文)。前导 '^' 表示只在起点尝试一次。 RegisterNativeFunction(s, "string.gsub", 3, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 3) return inter::NativeToFakeluaNil(state); + // s / pattern / repl 三个都必填 + if (n < 1) ThrowBadArgument(1, "string.gsub", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.gsub", "string expected"); + if (n < 3) ThrowBadArgument(3, "string.gsub", "string/function/table expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CVar a1 = inter::GetNativeArg(state, args, n, 1); CheckStringArg(a0, 1, "string.gsub"); @@ -1566,7 +1595,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.dump", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "string.dump", "function expected"); CVar fn_var = inter::GetNativeArg(state, args, n, 0); if (fn_var.type_ != static_cast(VarType::Closure) || !fn_var.data_.cl) { ThrowFakeluaException("bad argument #1 to 'string.dump' (function expected)"); @@ -1612,7 +1641,7 @@ void RegisterStringLibraryApi(State *s) { }); auto load_impl = [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "load", "string expected"); CVar code_var = inter::GetNativeArg(state, args, n, 0); CheckStringArg(code_var, 1, "load"); if (code_var.type_ != static_cast(VarType::String) && code_var.type_ != static_cast(VarType::StringId) && code_var.type_ != static_cast(VarType::Int) && @@ -1768,7 +1797,7 @@ void RegisterStringLibraryApi(State *s) { // 注册的签名是 (fmt, ...) 即 arg_count=1, is_vararg=true // 调用时:args[0]=fmt, args[1]=Multi(剩余参数) RegisterNativeFunction(s, "string.pack", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.pack", "string expected"); CVar fmt_var = inter::GetNativeArg(state, args, n, 0); CheckStringArg(fmt_var, 1, "string.pack"); std::string temp_fmt; @@ -1993,7 +2022,7 @@ void RegisterStringLibraryApi(State *s) { // string.packsize // 签名: (fmt, ...) packsize 主要需要 fmt,但 z 格式需要字符串长度 RegisterNativeFunction(s, "string.packsize", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaInt(state, 0); + if (n < 1) ThrowBadArgument(1, "string.packsize", "string expected"); CVar fmt_var = inter::GetNativeArg(state, args, n, 0); CheckStringArg(fmt_var, 1, "string.packsize"); std::string temp_fmt; @@ -2123,7 +2152,9 @@ void RegisterStringLibraryApi(State *s) { // string.unpack RegisterNativeFunction(s, "string.unpack", 2, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaNil(state); + // fmt 与 s 都必填 + if (n < 1) ThrowBadArgument(1, "string.unpack", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.unpack", "string expected"); CVar fmt_var = inter::GetNativeArg(state, args, n, 0); CVar str_var = inter::GetNativeArg(state, args, n, 1); CheckStringArg(fmt_var, 1, "string.unpack"); diff --git a/src/native/table/native_table.cpp b/src/native/table/native_table.cpp index 863fde1..e7cd5d6 100644 --- a/src/native/table/native_table.cpp +++ b/src/native/table/native_table.cpp @@ -871,7 +871,7 @@ void RegisterTableLibraryApi(State *s) { if (!s) return; RegisterNativeFunction(s, "table.insert", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "table.insert", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { ThrowFakeluaException("bad argument #1 to 'table.insert' (table expected)"); @@ -901,7 +901,7 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.remove", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "table.remove", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { ThrowFakeluaException("bad argument #1 to 'table.remove' (table expected)"); @@ -937,7 +937,7 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.concat", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "table.concat", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); // 标准 Lua:table.concat 第一个参数必须是 table,否则抛出异常 if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { @@ -946,20 +946,30 @@ void RegisterTableLibraryApi(State *s) { std::string sep = ""; if (n >= 2) { CVar sep_var = inter::GetNativeArg(state, args, n, 1); - // 标准 Lua:table.concat 的 sep 必须是 string(数字会被转换为字符串,这是标准行为) - CheckStringArg(sep_var, 2, "table.concat"); - std::string temp_sep; - sep = std::string(GetStringArgView(sep_var, temp_sep)); + // Lua 5.4 tconcat:sep 是 luaL_optlstring,显式 nil 与省略等价,默认 "" + if (sep_var.type_ != static_cast(VarType::Nil)) { + // 标准 Lua:table.concat 的 sep 必须是 string(数字会被转换为字符串,这是标准行为) + CheckStringArg(sep_var, 2, "table.concat"); + std::string temp_sep; + sep = std::string(GetStringArgView(sep_var, temp_sep)); + } } + // Lua 5.4 tconcat 的 i/j 是 luaL_optinteger:显式 nil 与省略等价,取默认值 + // (i 默认 1,j 默认 #t)。包装函数转发(f(t, sep, i, j) 少传实参)会传入显式 + // nil,此处必须与 table.remove 的 pos 同等对待,否则误报 number expected。 int64_t start_i = 1; if (n >= 3) { CVar start_var = inter::GetNativeArg(state, args, n, 2); - start_i = CheckIntegerArg(start_var, 3, "table.concat"); + if (start_var.type_ != static_cast(VarType::Nil)) { + start_i = CheckIntegerArg(start_var, 3, "table.concat"); + } } int64_t end_j = TableHelper::GetTableLen(tbl); if (n >= 4) { CVar end_var = inter::GetNativeArg(state, args, n, 3); - end_j = CheckIntegerArg(end_var, 4, "table.concat"); + if (end_var.type_ != static_cast(VarType::Nil)) { + end_j = CheckIntegerArg(end_var, 4, "table.concat"); + } } if (end_j < start_i) { @@ -1017,20 +1027,26 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.unpack", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "table.unpack", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { ThrowFakeluaException("bad argument #1 to 'unpack' (table expected)"); } + // Lua 5.4 tunpack 的 i/j 是 luaL_optinteger:显式 nil 与省略等价,取默认值 + // (i 默认 1,j 默认 #t)。与 table.concat / table.remove 的 nil 处理一致。 int64_t start_i = 1; if (n >= 2) { CVar start_var = inter::GetNativeArg(state, args, n, 1); - start_i = CheckIntegerArg(start_var, 2, "table.unpack"); + if (start_var.type_ != static_cast(VarType::Nil)) { + start_i = CheckIntegerArg(start_var, 2, "table.unpack"); + } } int64_t end_j = TableHelper::GetTableLen(tbl); if (n >= 3) { CVar end_var = inter::GetNativeArg(state, args, n, 2); - end_j = CheckIntegerArg(end_var, 3, "table.unpack"); + if (end_var.type_ != static_cast(VarType::Nil)) { + end_j = CheckIntegerArg(end_var, 3, "table.unpack"); + } } if (start_i > end_j) return inter::AllocMultiCVar(state, 0); @@ -1058,7 +1074,11 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.move", 4, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 4) return inter::NativeToFakeluaNil(state); + // a1/f/e/t 四个都必填 + if (n < 1) ThrowBadArgument(1, "table.move", "table expected"); + if (n < 2) ThrowBadArgument(2, "table.move", "number expected"); + if (n < 3) ThrowBadArgument(3, "table.move", "number expected"); + if (n < 4) ThrowBadArgument(4, "table.move", "number expected"); CVar a1 = inter::GetNativeArg(state, args, n, 0); CVar f_var = inter::GetNativeArg(state, args, n, 1); CVar e_var = inter::GetNativeArg(state, args, n, 2); @@ -1099,7 +1119,7 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.sort", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "table.sort", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { ThrowFakeluaException("bad argument #1 to 'table.sort' (table expected)"); diff --git a/src/native/utf8/native_utf8.cpp b/src/native/utf8/native_utf8.cpp index c919479..56ca705 100644 --- a/src/native/utf8/native_utf8.cpp +++ b/src/native/utf8/native_utf8.cpp @@ -132,7 +132,7 @@ static CVar Utf8Char(State *state, CVar *args, int n) { // utf8.codepoint(s [, i [, j]]) // Returns code points for all characters between positions i and j (inclusive). static CVar Utf8Codepoint(State *state, CVar *args, int n) { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "utf8.codepoint", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "utf8.codepoint"); @@ -142,14 +142,14 @@ static CVar Utf8Codepoint(State *state, CVar *args, int n) { int64_t len = static_cast(sv.size()); - // Parse i (default 1) + // Parse i (default 1)。Lua 5.4 codepoint 的 i/j 是 luaL_optinteger:显式 nil 与省略等价。 CVar i_var = (n >= 2) ? inter::GetNativeArg(state, args, n, 1) : CVar{static_cast(VarType::Nil)}; - if (n >= 2) CheckNumberArg(i_var, 2, "utf8.codepoint"); - int64_t i = (n >= 2) ? inter::CVarToInteger(i_var, 1) : 1; + if (n >= 2 && i_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(i_var, 2, "utf8.codepoint"); + int64_t i = (n >= 2 && i_var.type_ != static_cast(VarType::Nil)) ? inter::CVarToInteger(i_var, 1) : 1; // Parse j (default i) CVar j_var = (n >= 3) ? inter::GetNativeArg(state, args, n, 2) : CVar{static_cast(VarType::Nil)}; - if (n >= 3) CheckNumberArg(j_var, 3, "utf8.codepoint"); - int64_t j = (n >= 3) ? inter::CVarToInteger(j_var, i) : i; + if (n >= 3 && j_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(j_var, 3, "utf8.codepoint"); + int64_t j = (n >= 3 && j_var.type_ != static_cast(VarType::Nil)) ? inter::CVarToInteger(j_var, i) : i; // Handle negative indices (relative to end). Index 0 is not from-end. if (i < 0) i = len + i + 1; @@ -264,7 +264,7 @@ extern "C" CVar Utf8CodesIterator(VarClosure *cl, CVar /*s*/, CVar /*var*/) { } static CVar Utf8Codes(State *state, CVar *args, int n) { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "utf8.codes", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "utf8.codes"); @@ -280,7 +280,7 @@ static CVar Utf8Codes(State *state, CVar *args, int n) { // Returns the number of UTF-8 characters in s between positions i and j. // If it finds an invalid byte, returns nil + position of the invalid byte. static CVar Utf8Len(State *state, CVar *args, int n) { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "utf8.len", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "utf8.len"); @@ -290,14 +290,14 @@ static CVar Utf8Len(State *state, CVar *args, int n) { int64_t byte_len = static_cast(sv.size()); - // Parse i (default 1) + // Parse i (default 1)。Lua 5.4 utflen 的 i/j 是 luaL_optinteger:显式 nil 与省略等价。 CVar i_var = (n >= 2) ? inter::GetNativeArg(state, args, n, 1) : CVar{static_cast(VarType::Nil)}; - if (n >= 2) CheckNumberArg(i_var, 2, "utf8.len"); - int64_t i = (n >= 2) ? inter::CVarToInteger(i_var, 1) : 1; + if (n >= 2 && i_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(i_var, 2, "utf8.len"); + int64_t i = (n >= 2 && i_var.type_ != static_cast(VarType::Nil)) ? inter::CVarToInteger(i_var, 1) : 1; // Parse j (default -1, meaning end of string) CVar j_var = (n >= 3) ? inter::GetNativeArg(state, args, n, 2) : CVar{static_cast(VarType::Nil)}; - if (n >= 3) CheckNumberArg(j_var, 3, "utf8.len"); - int64_t j = (n >= 3) ? inter::CVarToInteger(j_var, -1) : -1; + if (n >= 3 && j_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(j_var, 3, "utf8.len"); + int64_t j = (n >= 3 && j_var.type_ != static_cast(VarType::Nil)) ? inter::CVarToInteger(j_var, -1) : -1; // Handle negative indices (relative to end). Index 0 is not from-end (Lua u_posrelat). if (i < 0) i = byte_len + i + 1; @@ -337,7 +337,9 @@ static CVar Utf8Len(State *state, CVar *args, int n) { // Returns the byte position of the n-th character in s, starting at position i. // n can be negative (count backward from i, matching Lua 5.4). static CVar Utf8Offset(State *state, CVar *args, int n) { - if (n < 2) return inter::NativeToFakeluaNil(state); + // s 与 n 都必填(i 可选,默认 1) + if (n < 1) ThrowBadArgument(1, "utf8.offset", "string expected"); + if (n < 2) ThrowBadArgument(2, "utf8.offset", "number expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "utf8.offset"); @@ -351,10 +353,13 @@ static CVar Utf8Offset(State *state, CVar *args, int n) { CheckNumberArg(target_n_var, 2, "utf8.offset"); int64_t target_n = inter::CVarToInteger(target_n_var, 0); - // Parse i (starting byte position, default 1 if n>=0, len+1 if n<0) + // Parse i (starting byte position, default 1 if n>=0, len+1 if n<0)。 + // Lua 5.4 byteoffset 的 i 是 luaL_optinteger:显式 nil 与省略等价;n 是 checkinteger。 CVar i_var = (n >= 3) ? inter::GetNativeArg(state, args, n, 2) : CVar{static_cast(VarType::Nil)}; - if (n >= 3) CheckNumberArg(i_var, 3, "utf8.offset"); - int64_t start_i = (n >= 3) ? inter::CVarToInteger(i_var, 1) : ((target_n >= 0) ? 1 : byte_len + 1); + if (n >= 3 && i_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(i_var, 3, "utf8.offset"); + int64_t start_i = (n >= 3 && i_var.type_ != static_cast(VarType::Nil)) + ? inter::CVarToInteger(i_var, 1) + : ((target_n >= 0) ? 1 : byte_len + 1); // Lua: 1 <= posi <= len+1 (relative negative i counts from the end; 0 is not from-end) if (start_i < 0) start_i = byte_len + start_i + 1; diff --git a/test/lua/string/test_string_optnil.lua b/test/lua/string/test_string_optnil.lua new file mode 100644 index 0000000..82418e2 --- /dev/null +++ b/test/lua/string/test_string_optnil.lua @@ -0,0 +1,60 @@ +-- 可选参数显式传 nil 必须与省略等价(Lua 5.4 luaL_optinteger/luaL_optlstring 语义)。 +-- 注意逐函数差异(以 5.4.9 源码为准): +-- string.sub i 是 checkinteger(nil 报错),j 是 optinteger(nil 取默认 -1) +-- string.byte i/j 均 optinteger;string.rep sep 是 optlstring(nil 取 "") +-- string.find/match init 是 optinteger;string.gsub n 缺省/nil 不限次数 +local function sub_wrap(s, i, j) + return #string.sub(s, i, j) +end + +local function byte_wrap(s, i, j) + local a, b = string.byte(s, i, j) + return (a or 0) + (b or 0) +end + +local function rep_wrap(s, n, sep) + return #string.rep(s, n, sep) +end + +local function find_wrap(s, pat, init) + local a, b = string.find(s, pat, init) + if a == nil then return 0 end + return a * 100 + b +end + +local function match_wrap(s, pat, init) + local m = string.match(s, pat, init) + if m == nil then return 0 end + return #m +end + +function test_string_optnil() + local total = 0 + + -- string.sub:j 传 nil 取默认(末尾);i 传 nil 必须报错(与 Lua 一致) + total = total + sub_wrap("hello", 2, nil) -- "ello" = 4 + total = total + sub_wrap("hello", 2, 4) -- "ell" = 3 + local ok = pcall(function() string.sub("hello", nil) end) + if ok then total = total + 1000 end -- 不应发生:i 为 nil 必须报错 + + -- string.byte:i 默认 1,j 默认 i(j 给定时返回两个字节) + total = total + byte_wrap("hello", nil, nil) -- 104 + total = total + byte_wrap("hello", 2, nil) -- 101 + total = total + byte_wrap("hello", nil, 2) -- 104+101 = 205 + + -- string.rep:sep 传 nil 取默认 "" + total = total + rep_wrap("ab", 3, nil) -- "ababab" = 6 + total = total + rep_wrap("ab", 3, "-") -- "ab-ab-ab" = 8 + + -- string.find/match:init 默认 1 + total = total + find_wrap("hello", "l", nil) -- 3,3 -> 303 + total = total + find_wrap("hello", "l", 4) -- 4,4 -> 404 + total = total + match_wrap("hello", "l+", nil)-- "ll" = 2 + total = total + match_wrap("hello", "xyz", nil)-- 0 + + -- string.gsub:n 缺省/nil 不限次数 + local s, cnt = string.gsub("aaa", "a", "b", nil) + total = total + #s + cnt -- "bbb" = 3 + 3 + + return total +end diff --git a/test/lua/table/test_table_optnil.lua b/test/lua/table/test_table_optnil.lua new file mode 100644 index 0000000..59d6ebc --- /dev/null +++ b/test/lua/table/test_table_optnil.lua @@ -0,0 +1,35 @@ +-- 可选参数显式传 nil 必须与省略等价(Lua 5.4 luaL_optinteger/luaL_optlstring 语义)。 +-- 覆盖包装函数少传实参时形参以显式 nil 进入被调用库函数的常见转发写法。 +local function concat_wrap(t, sep, i, j) + return table.concat(t, sep, i, j) +end + +local function unpack_wrap(t, i, j) + local a, b, c = table.unpack(t, i, j) + return a + b + c +end + +function test_table_optnil() + local total = 0 + local t = { 1, 2, 3, 4, 5 } + + -- table.concat:i 默认 1,j 默认 #t + total = total + #concat_wrap(t, "-", nil, nil) -- "1-2-3-4-5" = 9 + total = total + #concat_wrap(t, "-", 2, nil) -- "2-3-4-5" = 7 + total = total + #concat_wrap(t, "-", nil, 3) -- "1-2-3" = 5 + total = total + #table.concat(t, "-", nil, nil) -- 同上直接调用 = 9 + + -- table.concat:sep 显式 nil 与省略等价,默认 ""(luaL_optlstring 语义) + total = total + #concat_wrap(t, nil, nil, nil) -- "12345" = 5 + total = total + #table.concat(t, nil, 2, 4) -- "234" = 3 + + -- table.unpack:i 默认 1,j 默认 #t(取前三个值相加) + total = total + unpack_wrap(t, nil, nil) -- 1+2+3 = 6 + total = total + unpack_wrap(t, 2, nil) -- 2+3+4 = 9 + + -- table.remove:pos 省略/nil 取 #t + local r = { 10, 20, 30 } + total = total + table.remove(r, nil) -- 30 + + return total +end diff --git a/test/lua/utf8/test_utf8_optnil.lua b/test/lua/utf8/test_utf8_optnil.lua new file mode 100644 index 0000000..becf6dc --- /dev/null +++ b/test/lua/utf8/test_utf8_optnil.lua @@ -0,0 +1,45 @@ +-- 可选参数显式传 nil 必须与省略等价(Lua 5.4 luaL_optinteger 语义)。 +-- utf8.codepoint/len 的 i/j、utf8.offset 的 i 均为 optinteger; +-- utf8.offset 的 n 是 checkinteger(nil 报错,与 Lua 一致)。 +-- "héllo" 的字节布局:h(1) é(2-3) l(4) l(5) o(6),共 6 字节 5 字符。 +local function codepoint_wrap(s, i, j) + local a, b = utf8.codepoint(s, i, j) + return (a or 0) + (b or 0) +end + +local function len_wrap(s, i, j) + local n = utf8.len(s, i, j) + if n == nil then return -1 end + return n +end + +local function offset_wrap(s, n, i) + return utf8.offset(s, n, i) +end + +function test_utf8_optnil() + local total = 0 + local s = "héllo" + + -- utf8.len:i 默认 1,j 默认 -1(末尾) + total = total + len_wrap(s, nil, nil) -- 5 + total = total + len_wrap(s, 2, nil) -- 从第 2 字节起:é,l,l,o = 4 + total = total + len_wrap(s, nil, 6) -- 到第 6 字节 = 5 + total = total + len_wrap(s, 3, 6) -- 第 3 字节是 é 的续字节(非法)-> -1 + + -- utf8.codepoint:i 默认 1,j 默认 i(j>i 时返回多个码点,取前两个相加) + total = total + codepoint_wrap(s, nil, nil) -- 104 (h) + total = total + codepoint_wrap(s, 2, nil) -- 233 (é) + total = total + codepoint_wrap(s, nil, 3) -- 104+233 = 337 + + -- utf8.offset:n 必填;i 默认 1(n>=0) + total = total + offset_wrap(s, 2, nil) -- 第 2 字符起始字节 -> 2 + total = total + offset_wrap(s, 1, nil) -- 1 + total = total + offset_wrap(s, 0, nil) -- 1 + + -- n 传 nil 必须报错(checkinteger),不能静默取默认 + local ok = pcall(function() utf8.offset(s, nil) end) + if ok then total = total + 1000 end -- 不应发生 + + return total +end diff --git a/test/test_basic.cpp b/test/test_basic.cpp index dcc631b..ed03507 100644 --- a/test/test_basic.cpp +++ b/test/test_basic.cpp @@ -1347,3 +1347,4 @@ TEST(test_basic, xpcall_coverage) { EXPECT_EQ(ret, 1); FakeluaDeleteState(s); } + diff --git a/test/test_string.cpp b/test/test_string.cpp index 0977171..2b70a08 100644 --- a/test/test_string.cpp +++ b/test/test_string.cpp @@ -817,3 +817,18 @@ TEST(test_string, replace_empty_from) { CallThrow(s, "test_replace_empty_from", ret); FakeluaDeleteState(s); } + +TEST(test_string, test_string_optnil) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./string/test_string_optnil.lua", config); + int64_t res = 0; + Call(s, jit_type, "test_string_optnil", res); + EXPECT_EQ(res, 1146LL); + } + + FakeluaDeleteState(s); +} diff --git a/test/test_syntax_tree.cpp b/test/test_syntax_tree.cpp index 680c49b..bfa610c 100644 --- a/test/test_syntax_tree.cpp +++ b/test/test_syntax_tree.cpp @@ -1907,6 +1907,73 @@ TEST(syntax_tree, preprocess_multipart_funcname_unsupported) { EXPECT_THROW(c.CompileString("function a.b.c() end", {}), std::exception); } +// 原生函数出现在值位置(非直接调用位)必须在编译期明确报错。 +// fakelua 按名字派发原生调用,原生函数不是一等值;此前这类代码放行到运行时才以 +// "attempt to call/index a non-function/table value" 失败,位置与原因都有误导性。 +namespace { + +void ExpectNativeValueError(Compiler &c, const std::string &script, const std::string &expect_substr) { + try { + c.CompileString(script, {}); + FAIL() << "expected CompileString to throw for: " << script; + } catch (const std::exception &e) { + const std::string msg = e.what(); + EXPECT_NE(msg.find(expect_substr), std::string::npos) << msg; + EXPECT_NE(msg.find("cannot be used as a value"), std::string::npos) << msg; + } +} + +}// namespace + +TEST(syntax_tree, semantic_native_function_as_value_simple_name) { + const FakeluaStateGuard guard; + const auto s = guard.GetState(); + ASSERT_NE(s, nullptr); + + Compiler c(s); + // 裸原生名(type/print/pairs/next ...) + ExpectNativeValueError(c, "function f()\n local t = type\n return t(1)\nend\n", "native function 'type'"); + ExpectNativeValueError(c, "function f()\n return next\nend\n", "native function 'next'"); + // 传给 pcall 的常见写法 + ExpectNativeValueError(c, "function f()\n return pcall(print, \"x\")\nend\n", "native function 'print'"); +} + +TEST(syntax_tree, semantic_native_function_as_value_dotted) { + const FakeluaStateGuard guard; + const auto s = guard.GetState(); + ASSERT_NE(s, nullptr); + + Compiler c(s); + // 点号原生库函数:local f = table.concat / pcall(table.concat, t) / { cb = string.format } + ExpectNativeValueError(c, "function f()\n local g = table.concat\n return g\nend\n", "native function 'table.concat'"); + ExpectNativeValueError(c, "function f()\n local t = { 1, 2 }\n return pcall(table.concat, t, \"-\")\nend\n", + "native function 'table.concat'"); + ExpectNativeValueError(c, "function f()\n return { cb = string.format }\nend\n", "native function 'string.format'"); + ExpectNativeValueError(c, "function f()\n return math.floor\nend\n", "native function 'math.floor'"); +} + +// 值位置检查不能误伤合法写法:局部表字段、局部遮蔽原生名、模块常量、直接调用、冒号调用。 +TEST(syntax_tree, semantic_native_function_value_position_allows_legit_uses) { + const FakeluaStateGuard guard; + const auto s = guard.GetState(); + ASSERT_NE(s, nullptr); + + Compiler c(s); + // 局部表的字段函数(根名是局部变量,走普通字段访问) + EXPECT_NO_THROW(c.CompileString("function f()\n local t = {}\n t.concat = function(a, b) return a .. b end\n" + " local g = t.concat\n return g(\"x\", \"y\")\nend\n", + {})); + // local 遮蔽原生函数名 + EXPECT_NO_THROW(c.CompileString("function f()\n local type = 5\n return type + 1\nend\n", {})); + // 模块常量(math.pi / string.charpattern 等不在原生函数注册表里) + EXPECT_NO_THROW(c.CompileString("function f()\n return math.pi\nend\n", {})); + EXPECT_NO_THROW(c.CompileString("function f()\n return string.charpattern\nend\n", {})); + // 直接调用位(含点号链与字符串冒号方法) + EXPECT_NO_THROW(c.CompileString("function f()\n return table.concat({ 1, 2 }, \"-\")\nend\n", {})); + EXPECT_NO_THROW(c.CompileString("function f()\n local s = \"hi\"\n return s:upper()\nend\n", {})); + EXPECT_NO_THROW(c.CompileString("function f()\n print(\"x\")\n return 0\nend\n", {})); +} + // Covers SyntaxTreeTypeToString for all enum values (syntax_tree.h lines 55-128). TEST(syntax_tree, SyntaxTreeTypeToString) { ASSERT_EQ(SyntaxTreeTypeToString(SyntaxTreeType::None), "None"); diff --git a/test/test_table.cpp b/test/test_table.cpp index 1d60de6..b1861c3 100644 --- a/test/test_table.cpp +++ b/test/test_table.cpp @@ -37,6 +37,24 @@ TEST(test_table, test_table_array_part) { FakeluaDeleteState(s); } +// 可选参数显式传 nil 与省略等价(Lua 5.4 luaL_optinteger 语义): +// table.concat 的 i/j、table.unpack 的 i/j、table.remove 的 pos。 +// 期望值由 Lua 5.4.9 参考实现校准。 +TEST(test_table, test_table_optnil) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./table/test_table_optnil.lua", config); + int64_t res = 0; + Call(s, jit_type, "test_table_optnil", res); + EXPECT_EQ(res, 83LL); + } + + FakeluaDeleteState(s); +} + TEST(test_table, test_table_native_array_part) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); diff --git a/test/test_utf8.cpp b/test/test_utf8.cpp index 781bcb6..0d6911d 100644 --- a/test/test_utf8.cpp +++ b/test/test_utf8.cpp @@ -186,3 +186,18 @@ TEST(test_utf8, test_utf8_boundary_error) { FakeluaDeleteState(s); } + +TEST(test_utf8, test_utf8_optnil) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./utf8/test_utf8_optnil.lua", config); + double res = 0; + Call(s, jit_type, "test_utf8_optnil", res); + EXPECT_NEAR(res, 691, 0.5); + } + + FakeluaDeleteState(s); +}