From e2e730b950e610dc09ea290f9aa92fc1c08553c3 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Sat, 3 Oct 2026 17:28:27 +0800 Subject: [PATCH] fix: implement Lua 5.4 patterns (P1-5b) and while pure cond re-evaluation (P1-9) - P1-9: While condition purity check (IsPureNativeNumericExp): Prevent hoisting non-pure numeric expressions (e.g. #t table length which requires emitting FlLenInt statements) into single-evaluation outer while statements. Pure numeric expressions (literals, local variables, pure binary ops) continue to use native fast-path while loops. Add regression test test_jitbug_while_cond_reval.lua. - P1-5b: Standard Lua 5.4 pattern matching for string.find/match/gmatch/gsub: Replace boost::regex ECMAScript engine with a self-contained byte-pattern matcher ported from Lua 5.4 lstrlib.c. Supports % escapes, character classes (%a, %d, %w, etc.), custom sets/ranges, quantifiers (*, +, -, ?), captures (), backreferences %1-%9, %b(), %f[set], and gsub Lua replacement templates (%0-%9, %%, function/table dispatch). Update tests and docs accordingly. --- README.md | 39 +- README.zh.md | 38 +- src/compile/c_gen.cpp | 163 +++++- src/compile/c_gen.h | 13 +- src/interp/codegen.cpp | 32 +- src/native/README.md | 12 +- src/native/README.zh.md | 12 +- src/native/string/lua_pattern.cpp | 428 ++++++++++++++++ src/native/string/lua_pattern.h | 43 ++ src/native/string/native_string.cpp | 462 ++++++++++-------- test/lua/crypto/test_crypto_uuid.lua | 11 +- .../infer/test_jitbug_while_cond_reval.lua | 94 ++++ test/lua/json/test_json_edge.lua | 4 +- .../string/test_gsub_bad_func_return_bool.lua | 23 +- .../string/test_gsub_bad_table_value_bool.lua | 21 +- test/lua/string/test_string_find.lua | 6 +- test/lua/string/test_string_find_cases.lua | 4 +- test/lua/string/test_string_gmatch.lua | 8 +- test/lua/string/test_string_gsub.lua | 12 +- test/lua/string/test_string_gsub_cases.lua | 6 +- test/lua/string/test_string_lua_pattern.lua | 247 ++++++++++ test/lua/string/test_string_match.lua | 12 +- test/lua/string/test_string_match_cases.lua | 4 +- test/test_infer.cpp | 19 + test/test_string.cpp | 36 +- 25 files changed, 1429 insertions(+), 320 deletions(-) create mode 100644 src/native/string/lua_pattern.cpp create mode 100644 src/native/string/lua_pattern.h create mode 100644 test/lua/infer/test_jitbug_while_cond_reval.lua create mode 100644 test/lua/string/test_string_lua_pattern.lua diff --git a/README.md b/README.md index ac8748d..0530fb1 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ FL_SPEC(Table_Spec_1, point, x) = NativeAdd(FL_SPEC(Table_Spec_1, point, x), (CV - **Package modules**: `package "Name"` for namespace isolation, zero-`require` cross-module calls. - **Complex global initialization**: Arbitrary expressions as file-level variable initializers, executed in generated `__fakelua_init()`. - **NativeObject & C++ interop**: Host-side object mapping with group arena batch release, C++ member method binding via `RegisterMethod`, colon-syntax calls from Lua. -- **ECMAScript regex**: `string.find`/`match`/`gmatch`/`gsub` via Boost.Regex (supports lookahead, alternation, non-greedy quantifiers — more powerful than Lua patterns). +- **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 @@ -157,30 +157,29 @@ FakeLua provides 30+ independent C++ native modules under `src/native/` (registe | Logging | `log` (levels, tagged output, file rotation) | | Object | `object` (NativeObject Lua-side API) | -**Regex note:** `string.find`/`match`/`gmatch`/`gsub` use **ECMAScript regex** (`boost::regex::ECMAScript`), not Lua patterns. See [Regex Guide](#regex-matching-ecmascript-syntax-not-lua-patterns) below for migration tips. +**Pattern note:** `string.find`/`match`/`gmatch`/`gsub` use **Lua 5.4 patterns** (a self-contained byte-pattern engine in `src/native/string/lua_pattern.*`), not ECMAScript/POSIX regex. See [Lua Pattern Matching](#lua-pattern-matching) below. -### Regex Matching: ECMAScript Syntax, Not Lua Patterns +### Lua Pattern Matching -| Purpose | Lua Pattern | FakeLua (ECMAScript Regex) | -|---|---|---| -| Digits | `%d` | `\\d` | -| Letters | `%a` | `[A-Za-z]` | -| Alphanumeric | `%w` | `[A-Za-z0-9]` (note `\\w` additionally includes `_`) | -| Whitespace | `%s` | `\\s` | -| Escape literal | `%.`, `%%` | `\\.`、`%` | -| Lazy repeat | `-` (e.g. `.-`) | `?` (e.g. `.*?`) | -| Backreference in replacement | `%1`, `%0` | `$1`, `$&` | +`string.find`/`match`/`gmatch`/`gsub` follow PUC-Rio Lua 5.4 semantics exactly, including: -> Since `\d` is not a valid escape in Lua string literals, backslashes in regex patterns must be written as `"\\d+"`. FakeLua does not support `[[...]]` long strings as a workaround. -> -> For scripts that need to be compatible with both standard Lua and FakeLua, use syntax that has the same semantics in both engines, e.g. `[0-9]+` instead of `%d+`. +- **Escapes**: `%.` `%(` `%)` `%%` `%+` ... for punctuation; `.` matches any byte. +- **Classes**: `%a %c %d %g %l %p %s %u %w %x` and their uppercase complements; `%z` matches the zero byte. +- **Sets/ranges**: `[set]`, `[^set]`, `[a-z]`, classes inside sets (`[%d_]`), leading `]`/`-` as literals. +- **Quantifiers**: `* + - ?` — including Lua's lazy `-` (`a.-b`). +- **Anchors**: `^` at pattern start, `$` at pattern end. +- **Captures**: nested `(...)`, position captures `()`, back-references `%1`… in patterns and `%0`…`%9` + `%%` in `gsub` replacement strings. +- **Frontier** `%f[set]` and **balanced match** `%bxy`. +- **`gsub`**: string/function/table replacements; a function/table result of `nil`/`false` keeps the original match; `plain=true` on `find` bypasses the pattern engine. +- **Malformed patterns raise an error** (caught by `pcall`) instead of silently returning no match. -Key differences: +```lua +string.match("limit=15", "%d+") --> "15" +string.gsub("hello world", "(%w+) (%w+)", "%2 %1") --> "world hello", 1 +string.match("a(b(c)d)e", "%b()") --> "(b(c)d)" +``` -- **`gsub` replacement strings** use JS-style notation: `$1`…`$9` (capture groups), `$&` (entire match), `` $` `` (text before match), `$'` (text after match), `$$` (literal `$`). Lua's `%1` / `%0` are treated as literal characters here. -- **Invalid patterns don't throw**: `boost::regex_error` is caught and returns `nil`, so the script doesn't interrupt. -- **`string.find`'s `plain` parameter** has the same semantics as Lua: passing `true` degrades to pure substring search, completely bypassing the regex engine — also the fastest path. -- **Performance**: The regex path is significantly slower than Lua's native pattern engine; prefer `plain` search or `string.sub` / `string.byte` basic operations on hot paths. +> Lua patterns are *not* regular expressions: there is no alternation (`a|b`), and the escape prefix is `%`, not `\`. Scripts written for the old ECMAScript behavior (e.g. `"\\d+"`, `$1` replacements) must be updated to Lua form (`"%d+"`, `"%1"`). ## Quick Start diff --git a/README.zh.md b/README.zh.md index e1e1d9a..ee1d9ca 100644 --- a/README.zh.md +++ b/README.zh.md @@ -127,7 +127,7 @@ FL_SPEC(Table_Spec_1, point, x) = NativeAdd(FL_SPEC(Table_Spec_1, point, x), (CV - **Package 包管理**:`package "Name"` 命名空间隔离,零 `require` 跨模块互调。 - **全局变量复杂初始化**:文件级变量支持任意复杂表达式初始化器,在生成的 `__fakelua_init()` 中执行。 - **NativeObject 与 C++ 互操作**:支持组粒度 Arena 批量释放、C++ 成员方法 `RegisterMethod` 绑定、冒号语法调用。 -- **ECMAScript 正则**:`string.find`/`match`/`gmatch`/`gsub` 底层使用 Boost.Regex(支持前瞻、交替、非贪婪等能力,强于 Lua pattern)。 +- **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。 ### 未支持 @@ -157,29 +157,29 @@ FakeLua 在 `src/native/` 下提供 30+ 个独立 C++ 原生模块(每个 `Sta | 日志 | `log`(级别、分类标签输出、文件滚动) | | 对象 | `object`(NativeObject Lua 侧 API) | -> ⚠️ `string.find`/`match`/`gmatch`/`gsub` 底层使用 **ECMAScript 正则**(`boost::regex::ECMAScript`),而非 Lua pattern。从标准 Lua 迁移时需改写模式串。 +> `string.find`/`match`/`gmatch`/`gsub` 使用 **Lua 5.4 模式**(自包含字节模式引擎,位于 `src/native/string/lua_pattern.*`),不是 ECMAScript/POSIX 正则。 -### 正则匹配:ECMAScript 语法 +### Lua 模式匹配 -| 用途 | Lua pattern | FakeLua(ECMAScript 正则) | -|---|---|---| -| 数字 | `%d` | `\\d` | -| 字母 | `%a` | `[A-Za-z]` | -| 字母或数字 | `%w` | `[A-Za-z0-9]`(注意 `\\w` 额外包含 `_`) | -| 空白 | `%s` | `\\s` | -| 惰性重复 | `-`(如 `.-`) | `?`(如 `.*?`) | -| 替换串捕获引用 | `%1`、`%0` | `$1`、`$&` | +`string.find`/`match`/`gmatch`/`gsub` 严格遵循 PUC-Rio Lua 5.4 语义,包括: -> Lua 字符串中 `\d` 不是合法转义,正则里的反斜杠需写成 `"\\d+"`。FakeLua 不支持 `[[...]]` 长字符串。 -> -> 兼容写法:用 `[0-9]+` 代替 `%d+`,`[A-Za-z]+` 代替 `%a+`,两种引擎语义一致。 +- **转义**:标点用 `%.` `%(` `%)` `%%` `%+` 等;`.` 匹配任意单字节。 +- **字符类**:`%a %c %d %g %l %p %s %u %w %x`,大写取反;`%z` 匹配零字节。 +- **集合/区间**:`[set]`、`[^set]`、`[a-z]`、集合内嵌字符类(`[%d_]`)、开头的 `]`/`-` 按字面量处理。 +- **量词**:`* + - ?`,含 Lua 独有的惰性 `-`(`a.-b`)。 +- **锚点**:模式开头的 `^` 与末尾的 `$`。 +- **捕获**:嵌套 `(...)`、位置捕获 `()`、模式内反向引用 `%1`…,gsub 替换串 `%0`…`%9` 与 `%%`。 +- **前沿模式** `%f[set]` 与**平衡匹配** `%bxy`。 +- **gsub**:替换值可为字符串/函数/表;函数或表返回 `nil`/`false` 时保留原匹配;`find` 的 `plain=true` 跳过模式引擎。 +- **非法模式直接报错**(可被 `pcall` 捕获),而不是静默返回不匹配。 -主要差异: +```lua +string.match("limit=15", "%d+") --> "15" +string.gsub("hello world", "(%w+) (%w+)", "%2 %1") --> "world hello", 1 +string.match("a(b(c)d)e", "%b()") --> "(b(c)d)" +``` -- **`gsub` 替换串**使用 JS 风格:`$1`…`$9`、`$&`、`` $` ``、`$'`、`$$` -- **非法模式串不抛异常**:`boost::regex_error` 被捕获后返回 `nil` -- **`plain` 参数**:传 `true` 退化为纯子串查找,绕过正则引擎,是最快路径 -- **性能**:正则路径慢于 Lua 原生 pattern,热路径建议优先用 `plain` 查找 +> Lua 模式**不是**正则:没有分支交替(`a|b`),转义前缀是 `%` 而非 `\`。为旧 ECMAScript 行为写的脚本(如 `"\\d+"`、`$1` 替换)需改写为 Lua 写法(`"%d+"`、`"%1"`)。 ## 快速上手 diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index cccc39a..56fe719 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -999,7 +999,7 @@ std::string CGen::TryCompileNativeExpr(const SyntaxTreeInterfacePtr &exp) { static const std::unordered_map kCmpOpMap = {{BinOpKind::kLess, "<"}, {BinOpKind::kLessEqual, "<="}, {BinOpKind::kMore, ">"}, {BinOpKind::kMoreEqual, ">="}, {BinOpKind::kEqual, "=="}, {BinOpKind::kNotEqual, "!="}}; -std::string CGen::TryCompileNativeBoolExpr(const SyntaxTreeInterfacePtr &exp) { +std::string CGen::TryCompileNativeBoolExpr(const SyntaxTreeInterfacePtr &exp, bool require_pure) { // 只处理 Exp 节点。 DEBUG_ASSERT(exp && exp->Type() == SyntaxTreeType::Exp); const auto e = std::dynamic_pointer_cast(exp); @@ -1010,7 +1010,7 @@ std::string CGen::TryCompileNativeBoolExpr(const SyntaxTreeInterfacePtr &exp) { if (!pexp || pexp->GetPrefixKind() != PrefixExpKind::kExp) { return {}; } - return TryCompileNativeBoolExpr(pexp->GetValue()); + return TryCompileNativeBoolExpr(pexp->GetValue(), require_pure); } // 处理 not 一元逻辑取反:将 not 编译为 !()。 @@ -1018,7 +1018,7 @@ std::string CGen::TryCompileNativeBoolExpr(const SyntaxTreeInterfacePtr &exp) { if (const auto unop = std::dynamic_pointer_cast(e->Op()); !unop || unop->GetOpKind() != UnOpKind::kNot) { return {}; } - const auto inner = TryCompileNativeBoolExpr(e->Right()); + const auto inner = TryCompileNativeBoolExpr(e->Right(), require_pure); if (inner.empty()) { return {}; } @@ -1044,6 +1044,12 @@ std::string CGen::TryCompileNativeBoolExpr(const SyntaxTreeInterfacePtr &exp) { if ((left_type != T_INT && left_type != T_FLOAT) || (right_type != T_INT && right_type != T_FLOAT)) { return {}; } + // while 条件:含「语句 + 临时变量」形态的操作数(#t、函数调用、表索引、 + // 整数整除/取模/移位等)会被提升到 while 之外只求值一次,必须回退通用路径, + // 由生成的 while(1) 每轮重新 CompileExp 整个条件。 + if (require_pure && (!IsPureNativeNumericExp(e->Left()) || !IsPureNativeNumericExp(e->Right()))) { + return {}; + } const auto left_native = TryCompileNativeExpr(e->Left()); const auto right_native = TryCompileNativeExpr(e->Right()); DEBUG_ASSERT(!left_native.empty() && !right_native.empty()); @@ -1052,6 +1058,117 @@ std::string CGen::TryCompileNativeBoolExpr(const SyntaxTreeInterfacePtr &exp) { return {}; } +// 判定数值表达式能否编译为「不输出任何语句」的纯 C 表达式。 +// 口径必须与 CompileNumericExp / CompileRawNativeArithBinop / CompileRawNativeUnop +// 中每一处向 Out() 写语句(FlLenInt / FlFloorDivInt / FlModInt / FlLShiftInt / +// FlToIntChecked / 特化函数调用 …)的分支保持一致:这里判纯的表达式,那边只能 +// 返回一个随 C 变量读取自然重新求值的表达式子串,否则就是漏判。 +bool CGen::IsPureNativeNumericExp(const SyntaxTreeInterfacePtr &exp) const { + if (!exp || exp->Type() != SyntaxTreeType::Exp) { + return false; + } + const auto e = std::dynamic_pointer_cast(exp); + + // 数值常量(字面量、文件级只读常量折叠)→ C 字面量。 + TableKeyKind const_kind = TableKeyKind::kInt; + std::string canonical; + int64_t int_value = 0; + double float_value = 0; + if (ClassifyConstNumberExp(exp, const_kind, canonical, int_value, float_value)) { + return true; + } + + switch (e->GetExpKind()) { + case ExpKind::kNumber: + // 数值字面量;非数值的情形在比较分支类型检查处已被拒绝,走不到这里。 + return true; + case ExpKind::kPrefixExp: { + const auto pe = std::dynamic_pointer_cast(e->Right()); + if (!pe) return false; + switch (pe->GetPrefixKind()) { + case PrefixExpKind::kExp: + // 括号表达式:看内层。 + return IsPureNativeNumericExp(pe->GetValue()); + case PrefixExpKind::kVar: { + const auto var = std::dynamic_pointer_cast(pe->GetValue()); + // 简单 local / 参数 / 文件级常量读的是 C 变量,嵌在 while 条件里 + // 每轮重新读,语义正确。表索引 t[i] / t.x 在 CompileNumericExp 中 + // 不支持(会抛异常回退),统一判不纯。 + return var && var->GetVarKind() == VarKind::kSimple; + } + case PrefixExpKind::kFunctionCall: + // TryCompileNativeSpecCallExpr:先输出调用语句再读临时变量。 + return false; + } + return false; + } + case ExpKind::kBinop: { + const auto op = std::dynamic_pointer_cast(e->Op()); + if (!op) return false; + const auto op_kind = op->GetOpKind(); + // 数值路径下 and/or 会编译「丢弃左值副作用语句 / 三元」,且本身是短路语义, + // 保守判不纯。 + if (op_kind == BinOpKind::kAnd || op_kind == BinOpKind::kOr) { + return false; + } + const auto result_type = LookupNodeType(e.get()); + switch (op_kind) { + // 只产生表达式(含 FL_INT_* 宏与强转),递归要求两侧纯。 + case BinOpKind::kPlus: + case BinOpKind::kMinus: + case BinOpKind::kStar: + case BinOpKind::kSlash: + case BinOpKind::kPow: + return IsPureNativeNumericExp(e->Left()) && IsPureNativeNumericExp(e->Right()); + // 浮点整除是 floor(a/b) 表达式;整数整除发 FlFloorDivInt 语句。 + case BinOpKind::kDoubleSlash: + return result_type != T_INT && IsPureNativeNumericExp(e->Left()) && IsPureNativeNumericExp(e->Right()); + // 整数/浮点取模都发 FlModInt / FlModFloat 语句。 + case BinOpKind::kMod: + return false; + // 位运算:T_INT 操作数只有强转表达式;T_FLOAT 操作数发 FlToIntChecked + // 语句。结果与操作数都为整型时才纯。 + case BinOpKind::kBitAnd: + case BinOpKind::kXor: + case BinOpKind::kBitOr: { + const auto lt = e->Left() ? GetType(e->Left()) : T_DYNAMIC; + const auto rt = e->Right() ? GetType(e->Right()) : T_DYNAMIC; + return lt == T_INT && rt == T_INT && IsPureNativeNumericExp(e->Left()) && IsPureNativeNumericExp(e->Right()); + } + // 左/右移位固定发 FlLShiftInt / FlRShiftInt 语句。 + case BinOpKind::kLeftShift: + case BinOpKind::kRightShift: + return false; + default: + // 比较/连接等不出现在 CompileNumericExp 成功路径中。 + return false; + } + } + case ExpKind::kUnop: { + const auto op = std::dynamic_pointer_cast(e->Op()); + if (!op) return false; + const auto op_kind = op->GetOpKind(); + switch (op_kind) { + case UnOpKind::kMinus: + // FL_INT_SUB(0, x) / (-(x)),纯表达式。 + return IsPureNativeNumericExp(e->Right()); + case UnOpKind::kBitNot: { + // 整型操作数是 ~(int64_t)x;浮点操作数先发 FlToIntChecked 语句。 + const auto rt = e->Right() ? GetType(e->Right()) : T_DYNAMIC; + return rt == T_INT && IsPureNativeNumericExp(e->Right()); + } + case UnOpKind::kNumberSign: + // #x:FlLenInt 语句 + 临时变量,while 条件陈旧问题的头号来源。 + return false; + default: + return false; + } + } + default: + return false; + } +} + void CGen::EmitSpecParamList(const std::vector ¶ms, const std::vector &math_params, int bitmask) { for (size_t i = 0; i < params.size(); ++i) { if (i > 0) { @@ -1471,7 +1588,10 @@ void CGen::CompileStmtWhile(const SyntaxTreeInterfacePtr &stmt) { auto saved_for_cont = for_cont_stack_; for_cont_stack_.clear(); - if (const auto native_cond = TryCompileNativeBoolExpr(while_stmt->Exp()); !native_cond.empty()) { + // require_pure:原生 while 条件是生成代码里的一个 C 表达式,每轮只重新读取 C + // 变量;条件编译过程中输出的语句位于 while 之外、只执行一次。因此只接受纯表达式 + // 操作数,#t / 函数调用 / 表索引 / 整数整除取模等一律走下方每轮重新求值的通用路径。 + if (const auto native_cond = TryCompileNativeBoolExpr(while_stmt->Exp(), true); !native_cond.empty()) { Out() << GenTab() << "while (" << native_cond << ") {\n"; cur_tab_++; CompileStmtBlock(while_stmt->Block()); @@ -2869,14 +2989,43 @@ std::string CGen::CompileVar(const SyntaxTreeInterfacePtr &v) { // 拦截 string 静态库常量访问 (string.charpattern) if (base_var->GetName() == "string") { if (name == "charpattern") { - return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", s_->GetConstString().Alloc("[^%z]")); + // Lua 5.4:[\0-\255],匹配任意单字节(pattern 长度按字节算)。 + static const std::string kCharPattern = [] { + std::string p = "["; + p.push_back('\0'); + p.push_back('-'); + p.push_back(static_cast(0xFF)); + p.push_back(']'); + return p; + }(); + return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", s_->GetConstString().Alloc(kCharPattern)); } } // 拦截 utf8 静态库常量访问 (utf8.charpattern) if (base_var->GetName() == "utf8") { if (name == "charpattern") { - // Lua utf8.charpattern: [\0-\x7F\xC2-\xF4][\x80-\xBF]* - return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", s_->GetConstString().Alloc("[\\x00-\\x7F\\xC2-\\xF4][\\x80-\\xBF]*")); + // Lua 5.4 utf8.charpattern:由原始字节构成的 Lua 模式 + // [\0-\x7F\xC2-\xF4][\x80-\xBF]*(不能是字面 \xHH, + // 那是 ECMAScript 写法,Lua 模式下无效)。 + static const std::string kUtf8Pattern = [] { + std::string p = "["; + p.push_back('\0'); + p.push_back('-'); + p.push_back(static_cast(0x7F)); + p.push_back(static_cast(0xC2)); + p.push_back('-'); + p.push_back(static_cast(0xF4)); + p.push_back(']'); + p.push_back('['); + p.push_back(static_cast(0x80)); + p.push_back('-'); + p.push_back(static_cast(0xBF)); + p.push_back(']'); + p.push_back('*'); + return p; + }(); + return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", + s_->GetConstString().Alloc(kUtf8Pattern)); } } } diff --git a/src/compile/c_gen.h b/src/compile/c_gen.h index 15664b1..b5a0c52 100644 --- a/src/compile/c_gen.h +++ b/src/compile/c_gen.h @@ -178,8 +178,17 @@ class CGen { std::string CompileNumericExp(const SyntaxTreeInterfacePtr &exp); // 尝试将指定表达式编译为高效的 C 原生运算符直接计算(如直接输出 `a + b`,规避动态重载) std::string TryCompileNativeExpr(const SyntaxTreeInterfacePtr &exp); - // 尝试将表达式直接编译为原生的布尔值比较/运算 - std::string TryCompileNativeBoolExpr(const SyntaxTreeInterfacePtr &exp); + // 尝试将表达式直接编译为原生的布尔值比较/运算。 + // require_pure=true 用于 while 循环条件:两侧操作数必须能编译为不输出任何语句的 + // 纯 C 表达式。#t、函数调用、整数整除/取模等会被 CompileNumericExp 编译成 + // 「语句 + 临时变量」,而这些语句落在生成的 while 之外、只求值一次,导致条件 + // 读取陈旧值(违反 Lua 每轮重新求值条件的语义);此时返回空,由调用方回退到 + // while(1){ 重新 CompileExp 条件 } 的通用路径。 + std::string TryCompileNativeBoolExpr(const SyntaxTreeInterfacePtr &exp, bool require_pure = false); + // 判定表达式能否被 CompileNumericExp 编译为不输出语句、可直接嵌入循环条件的纯 C + // 表达式(每轮读取 C 变量/字面量即重新求值)。判定口径必须与 CompileNumericExp、 + // CompileRawNativeArithBinop、CompileRawNativeUnop 的语句发射点严格一致。 + [[nodiscard]] bool IsPureNativeNumericExp(const SyntaxTreeInterfacePtr &exp) const; // 尝试对强类型数学特化库调用(如 math.sin, math.cos)进行原生直接映射优化 std::string TryCompileNativeSpecCallExpr(const SyntaxTreeInterfacePtr &functioncall_node); diff --git a/src/interp/codegen.cpp b/src/interp/codegen.cpp index 086ed6f..9e5356a 100644 --- a/src/interp/codegen.cpp +++ b/src/interp/codegen.cpp @@ -1445,18 +1445,46 @@ int InterpCodegen::CompileVar(const SyntaxTreeInterfacePtr &v) { } } if (base_var->GetName() == "string" && name == "charpattern") { + // Lua 5.4:[\0-\255],匹配任意单字节。 + static const std::string kCharPattern = [] { + std::string p = "["; + p.push_back('\0'); + p.push_back('-'); + p.push_back(static_cast(0xFF)); + p.push_back(']'); + return p; + }(); const int r = AllocReg(); CVar s{}; s.type_ = static_cast(VarType::StringId); - s.data_.i = s_->GetConstString().Alloc("[^%z]"); + s.data_.i = s_->GetConstString().Alloc(kCharPattern); LoadConstTo(r, s); return r; } if (base_var->GetName() == "utf8" && name == "charpattern") { + // Lua 5.4 utf8.charpattern:由原始字节构成的 Lua 模式 + // [\0-\x7F\xC2-\xF4][\x80-\xBF]*(非字面 \xHH)。 + static const std::string kUtf8Pattern = [] { + std::string p = "["; + p.push_back('\0'); + p.push_back('-'); + p.push_back(static_cast(0x7F)); + p.push_back(static_cast(0xC2)); + p.push_back('-'); + p.push_back(static_cast(0xF4)); + p.push_back(']'); + p.push_back('['); + p.push_back(static_cast(0x80)); + p.push_back('-'); + p.push_back(static_cast(0xBF)); + p.push_back(']'); + p.push_back('*'); + return p; + }(); const int r = AllocReg(); CVar s{}; s.type_ = static_cast(VarType::StringId); - s.data_.i = s_->GetConstString().Alloc("[\\x00-\\x7F\\xC2-\\xF4][\\x80-\\xBF]*"); + s.data_.i = s_->GetConstString().Alloc(kUtf8Pattern); LoadConstTo(r, s); return r; } diff --git a/src/native/README.md b/src/native/README.md index 4fcc883..eabbc43 100644 --- a/src/native/README.md +++ b/src/native/README.md @@ -13,7 +13,7 @@ Detailed API reference for all built-in native libraries. Each module lives in i | basic | `basic/` | Global functions: `print`, `type`, `tostring`, `tonumber`, `select`, `error`, `assert`, `pcall`, `xpcall`, `next`, `pairs`, `ipairs`, `collectgarbage` | | math | `math/` | Math functions: arithmetic, trigonometry, exponential/logarithm, random, constants, special functions | | table | `table/` | Table operations: `insert`, `remove`, `concat`, `sort`, `pack`, `unpack`, `move`, `create` | -| string | `string/` | String operations: substring, case, trim/split/join/replace, pattern matching (ECMAScript regex), formatting, binary pack/unpack, serialization | +| string | `string/` | String operations: substring, case, trim/split/join/replace, Lua pattern matching, formatting, binary pack/unpack, serialization | | os | `os/` | OS interface: time, date, environment, file operations, process execution (UTF-8 paths via Boost.Nowide on Windows) | | utf8 | `utf8/` | UTF-8 encoding/decoding: `char`, `codepoint`, `codes`, `len`, `offset` | | io | `io/` | File I/O: open, close, read, write, seek, popen, standard streams | @@ -184,10 +184,10 @@ There are no `thread_local` variables left. | `string.byte(s, [i, [j]])` | vararg | Byte values in range | | `string.char(...)` | vararg | Characters from code points 0-255 | | `string.format(fmt, ...)` | vararg | Formatted output (supports `%s %d %i %u %x %X %o %f %e %E %g %G %c %q %p`) | -| `string.find(s, pattern, [init, [plain]])` | vararg | Regex or plain substring search; returns positions + captures | -| `string.match(s, pattern, [init])` | vararg | Regex match; returns captures or full match | -| `string.gmatch(s, pattern)` | 2 | Iterator for regex matches | -| `string.gsub(s, pattern, repl, [n])` | vararg | Regex substitution; supports string/function/table replacements | +| `string.find(s, pattern, [init, [plain]])` | vararg | Lua pattern or plain substring search; returns positions + captures | +| `string.match(s, pattern, [init])` | vararg | Lua pattern match; returns captures or full match | +| `string.gmatch(s, pattern)` | 2 | Iterator for Lua pattern matches | +| `string.gsub(s, pattern, repl, [n])` | vararg | Lua pattern substitution; supports string (`%0`-`%9`, `%%`)/function/table replacements | | `string.dump(f, [strip])` | vararg | Serialize closure to binary string | | `load(source, ...)` | vararg | Compile Lua source string into closure | | `loadstring(s, ...)` | vararg | Alias for `load` | @@ -196,7 +196,7 @@ There are no `thread_local` variables left. | `string.packsize(fmt)` | 1 | Compute packed size for format | | `string.unpack(fmt, s, [pos])` | vararg | Binary unpack (Lua 5.3 format; `<`/`>`/`=` via Boost.Endian) | -> ⚠️ `string.find`/`match`/`gmatch`/`gsub` use **ECMAScript regex** (`boost::regex::ECMAScript`), not Lua patterns. See [Regex Matching](../README.md#regex-matching-uses-ecmascript-syntax-not-lua-patterns) in the main README. +> `string.find`/`match`/`gmatch`/`gsub` follow **Lua 5.4 patterns** (self-contained matcher in `string/lua_pattern.*`): escapes with `%` (`%.`, `%d`, `%w`, ...), classes, custom sets, `* + - ?` quantifiers, `^`/`$` anchors, nested/position captures with back-references, frontier `%f[set]`, balanced match `%bxy`, and `gsub` replacement strings with `%0`-`%9` / `%%`. A `function`/`table` replacement returning `nil`/`false` keeps the original match. Malformed patterns raise an error (caught by `pcall`) instead of silently returning no match. These are *not* POSIX/ECMAScript regexes: there is no alternation and escapes use `%`, not `\`. --- diff --git a/src/native/README.zh.md b/src/native/README.zh.md index f288c8b..3198bc8 100644 --- a/src/native/README.zh.md +++ b/src/native/README.zh.md @@ -11,7 +11,7 @@ | basic | `basic/` | 全局函数:`print`、`type`、`tostring`、`tonumber`、`select`、`error`、`assert`、`pcall`、`xpcall`、`next`、`pairs`、`ipairs`、`collectgarbage` | | math | `math/` | 数学函数:算术、三角、指数/对数、随机数、常量、特殊函数 | | table | `table/` | 表操作:`insert`、`remove`、`concat`、`sort`、`pack`、`unpack`、`move`、`create` | -| string | `string/` | 字符串操作:子串、大小写、trim/split/join/replace、模式匹配(ECMAScript 正则)、格式化、二进制 pack/unpack、序列化 | +| string | `string/` | 字符串操作:子串、大小写、trim/split/join/replace、Lua 模式匹配、格式化、二进制 pack/unpack、序列化 | | os | `os/` | 系统接口:时间、日期、环境变量、文件操作、进程执行(Windows 上路径走 Boost.Nowide UTF-8) | | utf8 | `utf8/` | UTF-8 编解码:`char`、`codepoint`、`codes`、`len`、`offset` | | io | `io/` | 文件 IO:open、close、read、write、seek、popen、标准流 | @@ -180,10 +180,10 @@ JIT 的错误边界链(`jit_error_boundary.h`)也挂在 `State` 上:链顶 | `string.byte(s, [i, [j]])` | vararg | 范围内的字节值 | | `string.char(...)` | vararg | 编码点 0-255 转字符 | | `string.format(fmt, ...)` | vararg | 格式化输出(支持 `%s %d %i %u %x %X %o %f %e %E %g %G %c %q %p`) | -| `string.find(s, pattern, [init, [plain]])` | vararg | 正则或纯子串查找;返回位置 + 捕获 | -| `string.match(s, pattern, [init])` | vararg | 正则匹配;返回捕获或完整匹配 | -| `string.gmatch(s, pattern)` | 2 | 正则匹配迭代器 | -| `string.gsub(s, pattern, repl, [n])` | vararg | 正则替换;支持字符串/函数/表替换 | +| `string.find(s, pattern, [init, [plain]])` | vararg | Lua 模式或纯子串查找;返回位置 + 捕获 | +| `string.match(s, pattern, [init])` | vararg | Lua 模式匹配;返回捕获或完整匹配 | +| `string.gmatch(s, pattern)` | 2 | Lua 模式匹配迭代器 | +| `string.gsub(s, pattern, repl, [n])` | vararg | Lua 模式替换;支持字符串(`%0`-`%9`、`%%`)/函数/表替换 | | `string.dump(f, [strip])` | vararg | 将闭包序列化为二进制字符串 | | `load(source, ...)` | vararg | 编译 Lua 源码为闭包 | | `loadstring(s, ...)` | vararg | `load` 别名 | @@ -192,7 +192,7 @@ JIT 的错误边界链(`jit_error_boundary.h`)也挂在 `State` 上:链顶 | `string.packsize(fmt)` | 1 | 计算格式打包后大小 | | `string.unpack(fmt, s, [pos])` | vararg | 二进制解包(Lua 5.3 格式;`<`/`>`/`=` 由 Boost.Endian 处理) | -> ⚠️ `string.find`/`match`/`gmatch`/`gsub` 底层使用 **ECMAScript 正则**(`boost::regex::ECMAScript`),而非 Lua pattern。参见主 README 的[正则匹配](../README.md#regex-matching-uses-ecmascript-syntax-not-lua-patterns)章节。 +> `string.find`/`match`/`gmatch`/`gsub` 遵循 **Lua 5.4 模式**(自包含匹配器在 `string/lua_pattern.*`):`%` 转义(`%.`、`%d`、`%w` 等)、字符类、自定义集合、`* + - ?` 量词、`^`/`$` 锚点、嵌套/位置捕获与反向引用、前沿模式 `%f[set]`、平衡匹配 `%bxy`,gsub 替换串支持 `%0`-`%9` / `%%`。函数/表替换返回 `nil`/`false` 时保留原匹配;非法模式直接报错(可被 `pcall` 捕获),而不是静默返回不匹配。它**不是** POSIX/ECMAScript 正则:没有分支交替,转义用 `%` 而非 `\`。 --- diff --git a/src/native/string/lua_pattern.cpp b/src/native/string/lua_pattern.cpp new file mode 100644 index 0000000..817fcee --- /dev/null +++ b/src/native/string/lua_pattern.cpp @@ -0,0 +1,428 @@ +#include "native/string/lua_pattern.h" +#include "util/exception.h" + +#include +#include +#include + +namespace fakelua::string::lua_pattern { + +namespace { + +constexpr int kMaxCallDepth = 200;// 与 lstrlib.c 的 MAXCCALLS 一致,防止病态模式打爆 C 栈 + +static inline unsigned char U(char c) { + return static_cast(c); +} + +// Lua 5.4 lstrlib.c 模式匹配器的 C++ 移植。所有边界都用显式尾指针表示, +// subject / pattern 中允许出现内嵌 '\0'。 +// +// 关键边界(均已用 PUC-Rio Lua 5.4/5.5 实跑核对): +// - '*' item 匹配失败时允许零次重复("b*" 在非 b 位置 / 串尾都匹配空串); +// - '?' item 失败(含串尾)时回退零次("a?" 在串尾也产生空匹配); +// - item 零宽匹配成功时,贪婪重复整体判定失败("()*" 不产生匹配); +// - '-' 惰性量词零宽合法,从「零次 + suffix」开始试。 +class PatternMatcher { +public: + PatternMatcher(const char *src, size_t slen, const char *pat, size_t plen) + : src_(src), se_(src + slen), pstart_(pat), pe_(pat + plen) { + } + + bool RunAt(size_t start, MatchResult &out) { + if (start > static_cast(se_ - src_)) return false; + level_ = 0; + depth_ = 0; + const char *s = src_ + start; + const char *ep = Match(s, pstart_); + if (ep == nullptr) return false; + out.begin = s; + out.end = ep; + out.level = level_; + for (int i = 0; i < level_; ++i) { + out.caps[i] = caps_[i]; + } + return true; + } + +private: + const char *src_; + const char *se_; + const char *pstart_; + const char *pe_; + int depth_ = 0; + int level_ = 0; + Capture caps_[kMaxCaptures]{}; + + [[noreturn]] static void Fail(const std::string &msg) { + ThrowFakeluaException(msg); + } + + // 量词回溯时,一次 suffix 尝试可能开闭/回填捕获;失败后把全部捕获槽与 + // level 恢复到尝试前(end_capture 回填的是 caps_[level-1],必须一并回滚, + // 才能在新位置重新回填——与 Lua start/end_capture + max/min_expand 一致)。 + void RestoreCaps(const Capture *snap, int snap_level) { + std::memcpy(caps_, snap, sizeof(caps_)); + level_ = snap_level; + } + + // 匹配单个「可量化」模式 item(不含其后的量词):字面量、'.'、字符类、 + // 集合、%b/%f/%d 转义、'$'。捕获括号 '(' ')' 由 Match 直接处理——Lua 语法里 + // 捕获组后面不跟量词(其后的 * + - ? 是普通字面量字符)。 + const char *MatchOneItem(const char *s, const char *p, const char *&item_end) { + if (p >= pe_) { + item_end = p; + return s; + } + switch (*p) { + case '$': + item_end = p + 1; + // '$' 只在模式末尾是锚点,其他位置是普通字符。 + if (p + 1 == pe_) return (s == se_) ? s : nullptr; + return SingleChar(s, p); + case '%': { + if (p + 1 >= pe_) Fail("malformed pattern (ends with '%')"); + const char c1 = p[1]; + if (c1 == 'b') { + if (p + 3 >= pe_) Fail("malformed pattern (missing arguments to '%b')");// 需要 p[2], p[3] + item_end = p + 4; + // 只返回平衡段尾后位置,剩余模式由调用方续接(量词亦可跟在 %b 后)。 + return MatchBalance(s, U(p[2]), U(p[3])); + } + if (c1 == 'f') { + if (p + 2 >= pe_ || p[2] != '[') Fail("missing '[' after '%f' in pattern"); + const char *set_after = ClassEnd(p + 2);// ']' 之后 + item_end = set_after; + const char prev = (s == src_) ? '\0' : s[-1]; + const bool cur_in = (s < se_) && SingleMatch(s, p + 2); + const bool prev_in = SingleMatchByte(U(prev), p + 2); + // 前沿模式:从前一字符不在集合中过渡到当前字符在集合中 + // (串首视作 '\0')。零宽 item:ep=s 或 nullptr,续接交给调用方。 + return (cur_in && !prev_in) ? s : nullptr; + } + if (std::isdigit(U(c1))) { + const int idx = U(c1) - '1';// %1..%9 → 0..8(%0 非法) + if (idx < 0) Fail("invalid capture index %0"); + item_end = p + 2; + return MatchCapture(s, idx); + } + item_end = p + 2; + return SingleChar(s, p); + } + default: + // '[' 集合或普通字面量;item 尾后位置统一用 ClassEnd(集合跳过 ']')。 + item_end = ClassEnd(p); + return SingleChar(s, p); + } + } + + // 递归匹配主入口:p 指向剩余模式(绝对位置)。 + const char *Match(const char *s, const char *p) { + if (++depth_ > kMaxCallDepth) { + ThrowFakeluaException("stack overflow"); + } + // 捕获括号:开闭捕获后直接续接剩余模式,绕过量词分发(Lua 里捕获组 + // 不可量化;括号后面的量词字符按普通字面量匹配)。 + if (p < pe_ && *p == '(') { + const char *r = (p + 1 < pe_ && p[1] == ')') ? StartCapture(s, p + 2, kCapPosition) + : StartCapture(s, p + 1, kCapUnfinished); + --depth_; + return r; + } + if (p < pe_ && *p == ')') { + const char *r = EndCapture(s, p + 1); + --depth_; + return r; + } + const char *after; + const char *ep = MatchOneItem(s, p, after); + if (after >= pe_) { + // 模式结束:成功/失败直接返回(goto init 式尾调用在这里就是返回值)。 + --depth_; + return ep; + } + const char q = *after; + const char *suffix = after + 1; + const char *ret = nullptr; + switch (q) { + case '?': { + // 先试一次,再回退零次;失败尝试整体回滚捕获状态。 + if (ep != nullptr) { + Capture snap[kMaxCaptures]; + const int snap_level = level_; + std::memcpy(snap, caps_, sizeof(caps_)); + ret = Match(ep, suffix); + if (ret == nullptr) RestoreCaps(snap, snap_level); + } + if (ret == nullptr) { + Capture snap[kMaxCaptures]; + const int snap_level = level_; + std::memcpy(snap, caps_, sizeof(caps_)); + ret = Match(s, suffix); + if (ret == nullptr) RestoreCaps(snap, snap_level); + } + break; + } + case '*': + // 零次或多次;item 失败也算零次匹配成功。 + ret = Greedy(s, ep, p, suffix); + break; + case '+': { + // 一次或多次:item 必须先成功;随后等价于从 ep 起的 '*'。 + if (ep == nullptr) { + ret = nullptr; + } else { + const char *next_after; + const char *more = MatchOneItem(ep, p, next_after); + ret = Greedy(ep, more, p, suffix); + } + break; + } + case '-': + ret = Lazy(s, p, suffix); + break; + default: + // 不是量词:after 本身就是下一个 item 的起点(不是 after+1)。 + ret = ep ? Match(ep, after) : nullptr; + break; + } + --depth_; + return ret; + } + + // 贪婪重复。cur 为下一次 item 匹配的起点,ep 为该起点上 item 的结果 + // (nullptr 表示 item 失败)。所有失败的 suffix 回溯尝试都必须把捕获栈恢复 + // 到进入本次重复时的状态(Lua end_capture/max_expand 语义)。 + const char *Greedy(const char *cur, const char *ep, const char *ip, const char *suffix) { + const int entry_level = level_; + if (ep == nullptr) { + // 无法再多消费一个 item:在当前位置接 suffix(零次进一步重复合法)。 + Capture snap[kMaxCaptures]; + std::memcpy(snap, caps_, sizeof(caps_)); + const char *r = Match(cur, suffix); + if (r == nullptr) RestoreCaps(snap, entry_level); + return r; + } + if (ep == cur) { + // item 零宽匹配成功:Lua 判定整条贪婪路径失败,不回退到零次 suffix。 + return nullptr; + } + const char *next_after; + const char *nxt = MatchOneItem(ep, ip, next_after); + if (const char *deeper = Greedy(ep, nxt, ip, suffix); deeper != nullptr) { + return deeper; + } + // 后续重复失败:整体回滚捕获后在当前 item 尾后试 suffix。 + Capture snap[kMaxCaptures]; + std::memcpy(snap, caps_, sizeof(caps_)); + const char *r = Match(ep, suffix); + if (r == nullptr) RestoreCaps(snap, entry_level); + return r; + } + + // 惰性重复:始终先在当前位置试 suffix,失败才再消费一个 item(必须非零宽)。 + // 每次 suffix 尝试前后做捕获整体快照/恢复,使 end_capture 能在新位置重新回填。 + const char *Lazy(const char *cur, const char *ip, const char *suffix) { + const int entry_level = level_; + const char *s = cur; + for (;;) { + Capture snap[kMaxCaptures]; + const int snap_level = level_; + std::memcpy(snap, caps_, sizeof(caps_)); + if (const char *r = Match(s, suffix); r != nullptr) { + return r; + } + RestoreCaps(snap, snap_level); + const char *after; + const char *nxt = MatchOneItem(s, ip, after); + if (nxt == nullptr || nxt == s) { + level_ = entry_level; + return nullptr; + } + s = nxt;// 已消费 item 的捕获保留给下一轮 suffix + } + } + + const char *StartCapture(const char *s, const char *after, ptrdiff_t what) { + if (level_ >= kMaxCaptures) Fail("too many captures"); + caps_[level_].init = s; + caps_[level_].len = what; + ++level_; + return Match(s, after); + } + + const char *EndCapture(const char *s, const char *after) { + int i = level_ - 1; + while (i >= 0 && caps_[i].len != kCapUnfinished) { + --i; + } + if (i < 0) Fail("invalid pattern capture"); + caps_[i].len = s - caps_[i].init; + return Match(s, after); + } + + const char *MatchCapture(const char *s, int i) { + if (i >= level_) { + Fail(std::string("invalid capture index %") + std::to_string(i + 1)); + } + const ptrdiff_t l = caps_[i].len; + if (l == kCapUnfinished) { + Fail(std::string("invalid capture index %") + std::to_string(i + 1)); + } + if (l == kCapPosition) { + return (s == caps_[i].init) ? s : nullptr; + } + if (se_ - s < l) return nullptr; + return (std::memcmp(caps_[i].init, s, static_cast(l)) == 0) ? s + l : nullptr; + } + + // %bxy 平衡匹配。 + const char *MatchBalance(const char *s, int b, int e) { + if (b == e || s >= se_ || U(*s) != b) return nullptr; + const char *q = s + 1; + int cont = 1; + int steps = 0; + while (q < se_) { + if (U(*q) == b) { + ++cont; + } else if (U(*q) == e) { + --cont; + if (cont == 0) return q + 1; + } + ++q; + if (++steps > kMaxCallDepth) Fail("stack overflow"); + } + return nullptr; + } + + // 字符类(%a/%d/... 大写取反);非字母类名按字面量匹配(%e 匹配 'e')。 + static bool MatchClass(int c, int cl) { + bool res; + switch (std::tolower(cl)) { + case 'a': res = std::isalpha(c) != 0; break; + case 'c': res = std::iscntrl(c) != 0; break; + case 'd': res = std::isdigit(c) != 0; break; + case 'g': res = std::isgraph(c) != 0; break; + case 'l': res = std::islower(c) != 0; break; + case 'p': res = std::ispunct(c) != 0; break; + case 's': res = std::isspace(c) != 0; break; + case 'u': res = std::isupper(c) != 0; break; + case 'w': res = std::isalnum(c) != 0; break; + case 'x': res = std::isxdigit(c) != 0; break; + case 'z': res = (c == 0); break;// Lua 5.1/5.5 保留类:零字节 + default: return cl == c; + } + return std::islower(cl) ? res : !res; + } + + bool SingleMatchByte(int c, const char *p) const { + switch (*p) { + case '%': + return MatchClass(c, U(p[1])); + case '.': + return true; + case '[': { + bool sig = true; + const char *q = p + 1; + if (q < pe_ && *q == '^') { + sig = false; + ++q; + } + // do-while 语义:先测成员再看是否落在结束 ']' 上——因此紧跟 '[' 或 + // '[^' 的 ']' 会被当成普通成员测一次([]x] 里的首 ']')。 + do { + bool in; + if (*q == '%') { + // '%' 转义成员(含集合内嵌字符类如 [%d_])。 + in = (q + 1 < pe_) && MatchClass(c, U(q[1])); + q += 2; + } else { + const unsigned char lo = U(*q); + // 区间:普通字符 '-' 普通字符;紧贴 ']' 的 '-' 是字面量。 + if (q + 2 < pe_ && q[1] == '-' && q[2] != ']') { + in = (lo <= c && c <= U(q[2])); + q += 3; + } else { + in = (c == lo); + q += 1; + } + } + if (in) return sig; + } while (q < pe_ && *q != ']'); + return !sig; + } + default: + return c == U(*p); + } + } + + bool SingleMatch(const char *s, const char *p) const { + return s < se_ && SingleMatchByte(U(*s), p); + } + + const char *SingleChar(const char *s, const char *p) const { + if (SingleMatch(s, p)) return s + 1; + return nullptr; + } + + // 返回模式 item 的尾后位置;校验悬空 '%' 与缺失 ']'。 + const char *ClassEnd(const char *p) const { + if (*p == '%') { + if (p + 1 < pe_) return p + 2; + Fail("malformed pattern (ends with '%')"); + } + if (*p == '[') { + const char *q = p + 1; + if (q < pe_ && *q == '^') ++q; + if (q < pe_ && *q == ']') ++q;// 集合开头的 ']' 是字面量 + while (q < pe_ && *q != ']') { + if (*q == '%') { + ++q; + if (q >= pe_) Fail("malformed pattern (ends with '%')"); + } + ++q; + if (q > pe_) Fail("malformed pattern (missing ']')"); + } + if (q >= pe_) Fail("malformed pattern (missing ']')"); + return q + 1; + } + if (p < pe_) return p + 1; + Fail("malformed pattern (missing ']')"); + } +}; + +}// namespace + +bool MatchAt(const char *src, size_t slen, const char *pat, size_t plen, size_t start, MatchResult &out, bool leading_caret_is_anchor) { + // 前导 '^' 只表示锚定(MatchAt 本身就是锚定),剥除;其余位置是普通字符。 + const char *p = pat; + size_t use_len = plen; + if (leading_caret_is_anchor && plen > 0 && pat[0] == '^') { + ++p; + --use_len; + } + PatternMatcher ms(src, slen, p, use_len); + return ms.RunAt(start, out); +} + +bool Search(const char *src, size_t slen, const char *pat, size_t plen, size_t start, MatchResult &out) { + const char *p = pat; + size_t use_len = plen; + bool anchored = false; + if (plen > 0 && pat[0] == '^') { + anchored = true; + ++p; + --use_len; + } + if (start > slen) return false; + PatternMatcher ms(src, slen, p, use_len); + if (anchored) { + return ms.RunAt(start, out); + } + for (size_t pos = start; pos <= slen; ++pos) { + if (ms.RunAt(pos, out)) return true; + } + return false; +} + +}// namespace fakelua::string::lua_pattern diff --git a/src/native/string/lua_pattern.h b/src/native/string/lua_pattern.h new file mode 100644 index 0000000..0d8d367 --- /dev/null +++ b/src/native/string/lua_pattern.h @@ -0,0 +1,43 @@ +#pragma once + +// lua_pattern.h — PUC-Rio Lua 5.4 风格的模式匹配(pattern matching)。 +// +// string.find/match/gmatch/gsub 的模式不是正则:Lua 模式不支持分组交替、量词不 +// 可嵌套、转义用 '%' 而非 '\',但额外支持平衡匹配 %bxy、前沿模式 %f[set]、惰性 +// 量词 '-' 等正则无法表达的结构。这里提供自包含的字节模式匹配器,语义以 +// Lua 5.4 lstrlib.c 为准(显式长度,subject/pattern 均可包含内嵌 '\0')。 +// +// 非法模式(悬空 '%'、缺失 ']'、捕获越界等)抛 FakeluaException,与 Lua 的 +// "malformed pattern ..." 报错一致,而不是静默返回不匹配。 + +#include +#include + +namespace fakelua::string::lua_pattern { + +inline constexpr int kMaxCaptures = 32; +// 捕获长度哨兵(与 lstrlib.c 的 CAP_UNFINISHED / CAP_POSITION 对应)。 +inline constexpr ptrdiff_t kCapUnfinished = -1; +inline constexpr ptrdiff_t kCapPosition = -2; + +struct Capture { + const char *init = nullptr;// 捕获起点(绝对指针;位置捕获也用它定位) + ptrdiff_t len = 0; // >=0:字节长度;否则为 kCap* 哨兵 +}; + +struct MatchResult { + const char *begin = nullptr; + const char *end = nullptr; // 尾后指针(零宽匹配时 end == begin) + int level = 0; // 显式捕获个数 + Capture caps[kMaxCaptures]{}; +}; + +// 在 src[start..slen) 的起点处做锚定匹配(Lua 的 string.match 语义)。 +// start 可以等于 slen(允许在尾后位置匹配空串)。成功返回 true 并填 out。 +// leading_caret_is_anchor=false 时不剥除前导 '^'(gmatch 把 '^' 当普通字符)。 +bool MatchAt(const char *src, size_t slen, const char *pat, size_t plen, size_t start, MatchResult &out, bool leading_caret_is_anchor = true); + +// 从 src[start] 起逐位扫描(string.find 语义),含尾后位置;成功返回 true。 +bool Search(const char *src, size_t slen, const char *pat, size_t plen, size_t start, MatchResult &out); + +}// namespace fakelua::string::lua_pattern diff --git a/src/native/string/native_string.cpp b/src/native/string/native_string.cpp index b8ca3d5..80400e6 100644 --- a/src/native/string/native_string.cpp +++ b/src/native/string/native_string.cpp @@ -3,6 +3,7 @@ #include "jit/jit_error_boundary.h" #include "native/native_common.h" #include "native/object/native_object.h" +#include "native/string/lua_pattern.h" #include "native/table/native_table.h" #include "state/state.h" #include "util/utf8_io.h" @@ -12,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -39,40 +40,55 @@ static inline int64_t NormalizePos(int64_t pos, int64_t len) { return len + pos + 1; } -// ECMAScript 正则编译缓存 -// Boost.Regex 构造仍有成本;find/match/gmatch/gsub 的热点是重复编译同一 pattern。 -// 进程级缓存:key=pattern 原文,value=编译结果(含 optimize)。 -// 返回的指针由缓存永久持有(不淘汰,避免 gmatch 状态中的裸指针悬空)。 +// Lua 模式(pattern matching,见 lua_pattern.h)辅助。 +// find/match/gmatch/gsub 使用的是 Lua 5.4 语义的模式,不是正则。 namespace { -std::shared_mutex g_regex_cache_mu; -std::unordered_map> g_regex_cache; +namespace lp = lua_pattern; -// 成功返回非空指针;pattern 非法返回 nullptr(与原先 catch regex_error → nil 一致)。 -const boost::regex *GetCachedRegex(std::string_view pattern) { - std::string key(pattern); - { - std::shared_lock lock(g_regex_cache_mu); - auto it = g_regex_cache.find(key); - if (it != g_regex_cache.end()) { - return it->second.get(); - } +// 把一个捕获值转成 Lua 值:位置捕获返回整数,其余返回(可能为空的)字符串。 +CVar CaptureToCVar(State *state, const char *src_base, const lp::MatchResult &m, int i) { + const auto &cap = m.caps[i]; + if (cap.len == lp::kCapPosition) { + return inter::NativeToFakeluaInt(state, static_cast(cap.init - src_base) + 1); } + return inter::NativeToFakeluaStringView(state, std::string_view(cap.init, static_cast(cap.len))); +} - std::unique_ptr compiled; - try { - compiled = std::make_unique(key, boost::regex::ECMAScript | boost::regex::optimize); - } catch (const boost::regex_error &) { - return nullptr; +// gsub 字符串替换中 %1-%9 引用捕获:位置捕获转成十进制数字串,其余为捕获切片。 +static std::string CaptureToLuaString(const char *src_base, const lp::MatchResult &m, int i) { + const auto &cap = m.caps[i]; + if (cap.len == lp::kCapPosition) { + return std::to_string(static_cast(cap.init - src_base) + 1); } + if (cap.len == lp::kCapUnfinished) { + ThrowFakeluaException("unfinished capture"); + } + return std::string(cap.init, static_cast(cap.len)); +} - std::unique_lock lock(g_regex_cache_mu); - auto it = g_regex_cache.find(key); - if (it != g_regex_cache.end()) { - return it->second.get(); +// 组装 find 的返回:start, end(1-based,零宽匹配允许 end == start-1),后接捕获。 +// 匹配成功但存在未闭合捕获时与 Lua 一样报 "unfinished capture"。 +static void CheckCapturesFinished(const lp::MatchResult &m) { + for (int i = 0; i < m.level; ++i) { + if (m.caps[i].len == lp::kCapUnfinished) { + ThrowFakeluaException("unfinished capture"); + } } - auto [ins, _] = g_regex_cache.emplace(std::move(key), std::move(compiled)); - return ins->second.get(); +} + +CVar BuildFindResult(State *state, const char *src_base, const lp::MatchResult &m) { + CheckCapturesFinished(m); + const int64_t start = m.begin - src_base + 1; + const int64_t finish = m.end - src_base;// 1-based 闭区间尾(零宽时为 start-1) + const int total = 2 + m.level; + CVar multi = inter::AllocMultiCVar(state, total); + inter::SetMultiCVarElement(multi, 0, inter::NativeToFakeluaInt(state, start)); + inter::SetMultiCVarElement(multi, 1, inter::NativeToFakeluaInt(state, finish)); + for (int i = 0; i < m.level; ++i) { + inter::SetMultiCVarElement(multi, i + 2, CaptureToCVar(state, src_base, m, i)); + } + return multi; } // Lua integer widths are 1..16; we pack through uint64 so cap at 8. @@ -133,12 +149,14 @@ static void CheckFormatItemSize(std::string_view spec) { }// namespace -// gmatch 迭代器状态(存储在闭包 upvalue 中) -// re 指向全局缓存中的编译结果,不在此处持有所有权(arena 不跑析构也安全)。 +// gmatch 迭代器状态(存储在闭包 upvalue 中,arena 分配,无需手动释放)。 +// text/pattern 自有副本,跨 tick / arena reset 都安全。 +// prev 是上一次产出匹配的【尾后位置】;首轮标记为 SIZE_MAX,使位置 0 的零宽 +// 匹配也能产出(与 PUC-Rio gmatch_iter 一致)。 struct GMatchState { std::string text; - const boost::regex *re = nullptr; - size_t pos = 0; + std::string pattern; + size_t prev = std::numeric_limits::max(); }; // string.pack / packsize / unpack 二进制序列化辅助 @@ -584,38 +602,34 @@ extern "C" CVar GMatchIterator(VarClosure *cl, CVar /*s*/, CVar /*var*/) { return inter::NativeToFakeluaNil(iter_state); } - if (!gs->re || gs->pos >= gs->text.size()) { - return inter::NativeToFakeluaNil(iter_state); - } - - try { - // 在原串上从 pos 起搜,避免每次 substr 拷贝 - auto first = gs->text.cbegin() + static_cast(gs->pos); - auto last = gs->text.cend(); - boost::smatch match; - if (!boost::regex_search(first, last, match, *gs->re)) { - gs->pos = gs->text.size(); - return inter::NativeToFakeluaNil(iter_state); - } - - gs->pos += static_cast(match.position() + match.length()); - if (match.length() == 0) { - // 零宽匹配:前进一位避免死循环 - gs->pos += 1; - } - - if (match.size() > 1) { - int groups = static_cast(match.size()) - 1; - CVar multi = inter::AllocMultiCVar(iter_state, groups); - for (int i = 0; i < groups; ++i) { - inter::SetMultiCVarElement(multi, i, inter::NativeToFakeluaStringView(iter_state, match[i + 1].str())); + const size_t slen = gs->text.size(); + size_t pos = (gs->prev == std::numeric_limits::max()) ? 0 : gs->prev; + // gmatch 不像 gsub/find 有锚定概念:模式里的 '^' 是普通字符。 + const char *src = gs->text.data(); + const char *pat = gs->pattern.c_str(); + while (pos <= slen) { + lua_pattern::MatchResult m; + if (lua_pattern::MatchAt(src, slen, pat, gs->pattern.size(), pos, m, /*leading_caret_is_anchor=*/false)) { + const size_t mend = static_cast(m.end - src); + // 零宽匹配只在「不是紧跟上一个产出位置」时产出,否则跳过一位, + // 避免重复空匹配(PUC-Rio gmatch_iter 语义)。 + if (mend != pos || pos != gs->prev) { + gs->prev = mend; + CheckCapturesFinished(m); + if (m.level > 0) { + CVar multi = inter::AllocMultiCVar(iter_state, m.level); + for (int i = 0; i < m.level; ++i) { + inter::SetMultiCVarElement(multi, i, CaptureToCVar(iter_state, src, m, i)); + } + return multi; + } + return inter::NativeToFakeluaStringView(iter_state, std::string_view(m.begin, static_cast(m.end - m.begin))); } - return multi; } - return inter::NativeToFakeluaStringView(iter_state, match[0].str()); - } catch (const boost::regex_error &) { - return inter::NativeToFakeluaNil(iter_state); + ++pos; } + gs->prev = std::numeric_limits::max();// 标记迭代结束 + return inter::NativeToFakeluaNil(iter_state); } std::string_view GetStringArgView(CVar a, std::string &temp) { @@ -1247,9 +1261,9 @@ void RegisterStringLibraryApi(State *s) { }); // string.find(s, pattern [, init [, plain]]) - // 在 s 中查找 pattern(ECMAScript 正则),返回起始位置与结束位置(1-based)。 - // 若 pattern 含捕获组,则后续返回值依次为各捕获。 - // 若 plain 为 true,则退化为纯子串查找(忽略正则元字符)。 + // 在 s 中查找 Lua 模式 pattern,返回起始位置与结束位置(1-based)。 + // 若 pattern 含捕获,则后续返回值依次为各捕获。 + // 若 plain 为 true,则退化为纯子串查找(忽略模式元字符)。 // 找不到时返回 nil。 RegisterNativeFunction(s, "string.find", 2, true, [](State *state, CVar *args, int n) -> CVar { if (n < 2) return inter::NativeToFakeluaNil(state); @@ -1294,31 +1308,19 @@ void RegisterStringLibraryApi(State *s) { return multi; } - const boost::regex *re = GetCachedRegex(pat_view); - if (!re) return inter::NativeToFakeluaNil(state); - - try { - boost::smatch match; - if (!boost::regex_search(sub, match, *re)) return inter::NativeToFakeluaNil(state); - - int64_t start = init_pos + static_cast(match.position()); - int64_t end = start + static_cast(match.length()) - 1; - int captures = static_cast(match.size()) - 1;// 捕获组数 - int total = 2 + captures; // start, end, + 捕获 - CVar multi = inter::AllocMultiCVar(state, total); - inter::SetMultiCVarElement(multi, 0, inter::NativeToFakeluaInt(state, start)); - inter::SetMultiCVarElement(multi, 1, inter::NativeToFakeluaInt(state, end)); - for (int i = 0; i < captures; ++i) { - inter::SetMultiCVarElement(multi, i + 2, inter::NativeToFakeluaStringView(state, match[i + 1].str())); - } - return multi; - } catch (const boost::regex_error &) { + // Lua 模式匹配:在原串上从 init_pos-1 起扫描(位置 1-based 由 BuildFindResult 处理)。 + // 非法模式直接抛错(与 Lua 的 malformed pattern 一致)。 + lua_pattern::MatchResult m; + if (!lua_pattern::Search(sv.data(), static_cast(len), pat_view.data(), pat_view.size(), + static_cast(init_pos - 1), m)) { return inter::NativeToFakeluaNil(state); } + return BuildFindResult(state, sv.data(), m); }); // string.match(s, pattern [, init]) - // 与 string.find 相似,但不返回位置;仅返回捕获(或整个匹配,若无捕获组)。 + // 与 find 一样从 init 起搜索,只是不返回位置;前导 '^' 锚定到 init 点。 + // 有捕获返回所有捕获(位置捕获为整数),无捕获返回整个匹配。 RegisterNativeFunction(s, "string.match", 2, true, [](State *state, CVar *args, int n) -> CVar { if (n < 2) return inter::NativeToFakeluaNil(state); CVar a0 = inter::GetNativeArg(state, args, n, 0); @@ -1342,33 +1344,27 @@ void RegisterStringLibraryApi(State *s) { if (init_pos > len + 1) { return inter::NativeToFakeluaNil(state); } - std::string sub = std::string(sv.substr(static_cast(init_pos - 1))); - - const boost::regex *re = GetCachedRegex(pat_view); - if (!re) return inter::NativeToFakeluaNil(state); - - try { - boost::smatch match; - if (!boost::regex_search(sub, match, *re)) return inter::NativeToFakeluaNil(state); - if (match.size() > 1) { - // 有捕获组:返回所有捕获 - int groups = static_cast(match.size()) - 1; - CVar multi = inter::AllocMultiCVar(state, groups); - for (int i = 0; i < groups; ++i) { - inter::SetMultiCVarElement(multi, i, inter::NativeToFakeluaStringView(state, match[i + 1].str())); - } - return multi; - } - // 无捕获组:返回整个匹配 - return inter::NativeToFakeluaStringView(state, match[0].str()); - } catch (const boost::regex_error &) { + lua_pattern::MatchResult m; + // Search 内部会处理前导 '^' 的锚定;无 '^' 时从 init 起逐位搜索。 + if (!lua_pattern::Search(sv.data(), static_cast(len), pat_view.data(), pat_view.size(), + static_cast(init_pos - 1), m)) { return inter::NativeToFakeluaNil(state); } + if (m.level > 0) { + CheckCapturesFinished(m); + CVar multi = inter::AllocMultiCVar(state, m.level); + for (int i = 0; i < m.level; ++i) { + inter::SetMultiCVarElement(multi, i, CaptureToCVar(state, sv.data(), m, i)); + } + return multi; + } + return inter::NativeToFakeluaStringView(state, std::string_view(m.begin, static_cast(m.end - m.begin))); }); // string.gmatch(s, pattern) - // 返回一个迭代器闭包;每次调用返回下一个匹配(或捕获)。 + // 返回一个迭代器闭包;每次调用返回下一个匹配(有捕获时返回捕获值)。 + // 模式惰性解析,非法模式在首次迭代时抛错(与 Lua 一致)。 RegisterNativeFunction(s, "string.gmatch", 2, false, [](State *state, CVar *args, int n) -> CVar { if (n < 2) return inter::NativeToFakeluaNil(state); CVar a0 = inter::GetNativeArg(state, args, n, 0); @@ -1379,18 +1375,21 @@ void RegisterStringLibraryApi(State *s) { std::string text(GetStringArgView(a0, temp0)); std::string pattern(GetStringArgView(a1, temp1)); - const boost::regex *re = GetCachedRegex(pattern); - if (!re) return inter::NativeToFakeluaNil(state); - - // 使用 arena 分配器分配迭代器状态(re 由全局缓存持有) + // 模式语法惰性校验(与 Lua 一致:迭代时才解析),这里只持有副本。 + // arena 分配迭代器状态(text/pattern 自有副本)。 auto &alloc = state->GetValueAllocator(); - GMatchState *gs = alloc.New(std::move(text), re, 0); + GMatchState *gs = alloc.New(); + gs->text = std::move(text); + gs->pattern = std::move(pattern); // 使用共享辅助函数创建迭代器闭包 return MakeIteratorClosure(state, reinterpret_cast(GMatchIterator), gs); }); // string.gsub(s, pattern, repl [, n]) + // Lua 模式替换。repl 为 string(%0-%9/%% 引用捕获)/ function(收到捕获或整个 + // 匹配;返回 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); CVar a0 = inter::GetNativeArg(state, args, n, 0); @@ -1402,6 +1401,7 @@ void RegisterStringLibraryApi(State *s) { std::string temp0, temp1; std::string_view sv = GetStringArgView(a0, temp0); std::string_view pat_view = GetStringArgView(a1, temp1); + const size_t slen = sv.size(); int64_t max_replace = -1; if (n >= 4) { @@ -1416,123 +1416,161 @@ void RegisterStringLibraryApi(State *s) { if (repl_var.type_ == static_cast(VarType::Bool)) { ThrowFakeluaException("bad argument #3 to 'string.gsub' (string/function/table expected, got boolean)"); } - bool repl_is_table = (repl_var.type_ == static_cast(VarType::Table) && repl_var.data_.t); - bool repl_is_closure = (repl_var.type_ == static_cast(VarType::Closure) && repl_var.data_.cl); + const bool repl_is_table = (repl_var.type_ == static_cast(VarType::Table) && repl_var.data_.t); + const bool repl_is_closure = (repl_var.type_ == static_cast(VarType::Closure) && repl_var.data_.cl); + const std::string repl_str = (repl_is_table || repl_is_closure) ? std::string() : std::string(KeyToStringView(repl_var)); - const boost::regex *re = GetCachedRegex(pat_view); - if (!re) return inter::NativeToFakeluaNil(state); + const bool anchored = !pat_view.empty() && pat_view[0] == '^'; + const char *eff_pat = pat_view.data() + (anchored ? 1 : 0); + const size_t eff_len = pat_view.size() - (anchored ? 1 : 0); - try { - std::string input(sv); - std::string result; - result.reserve(input.size()); - int64_t count = 0; - - auto it = boost::sregex_iterator(input.begin(), input.end(), *re); - auto end = boost::sregex_iterator(); - size_t last_pos = 0; - - for (; it != end; ++it) { - if (max_replace >= 0 && count >= max_replace) break; - const boost::smatch &match = *it; - result.append(input, last_pos, match.position() - last_pos); - - std::string replacement; - if (repl_is_closure) { - VarClosure *cl = repl_var.data_.cl; - void *addr = cl->func_ptr; - if (match.size() > 1) { - int call_arg_count = static_cast(match.size()) - 1; - if (call_arg_count > static_cast(kMaxFunctionInputParams)) { - ThrowFakeluaException(std::format("string.gsub: too many capture arguments ({}), max is {}", - call_arg_count, kMaxFunctionInputParams)); - } - std::vector call_args(static_cast(call_arg_count)); - for (int i = 0; i < call_arg_count; ++i) { - call_args[static_cast(i)] = inter::NativeToFakeluaStringView(state, match[i + 1].str()); - } - CVar fn_res = - (addr != nullptr) ? inter::DispatchCallClosure(state, cl, call_args.data(), call_arg_count, JIT_TCC) : FlEvalLoadClosure(state, cl, call_arg_count, call_args.data()); - if (fn_res.type_ == static_cast(VarType::Bool) || fn_res.type_ == static_cast(VarType::Table)) { - ThrowFakeluaException("invalid replacement value (boolean)"); - } - if (fn_res.type_ == static_cast(VarType::Nil)) { - replacement = match[0].str(); - } else { - replacement = std::string(KeyToStringView(fn_res)); - } - } else { - CVar call_arg = inter::NativeToFakeluaStringView(state, match[0].str()); - CVar fn_res = (addr != nullptr) ? inter::DispatchCallClosure(state, cl, &call_arg, 1, JIT_TCC) : FlEvalLoadClosure(state, cl, 1, &call_arg); - if (fn_res.type_ == static_cast(VarType::Bool) || fn_res.type_ == static_cast(VarType::Table)) { - ThrowFakeluaException("invalid replacement value (boolean)"); - } - if (fn_res.type_ == static_cast(VarType::Nil)) { - replacement = match[0].str(); - } else { - replacement = std::string(KeyToStringView(fn_res)); - } + std::string result; + result.reserve(slen); + size_t copied = 0; // 已拷进 result 的原文位置 + size_t src = 0; // 下一次搜索起点 + size_t prev_end = std::numeric_limits::max();// 上一次产出匹配的尾后位置 + int64_t count = 0; + + while ((max_replace < 0 || count < max_replace) && src <= slen) { + // 从 src 起逐位尝试;与 gmatch 相同的零宽规则:起点处的零宽匹配若紧跟上 + // 一次产出匹配的尾后位置,则跳过该位继续找(避免相邻空匹配)。 + lua_pattern::MatchResult m; + bool found = false; + size_t trial = src; + const size_t trial_end = anchored ? src : slen; + for (; trial <= trial_end; ++trial) { + if (!lua_pattern::MatchAt(sv.data(), slen, eff_pat, eff_len, trial, m)) { + if (anchored) break; + continue; + } + const size_t mst = static_cast(m.begin - sv.data()); + const size_t men = static_cast(m.end - sv.data()); + if (men == mst && mst == prev_end) { + if (anchored) break; + continue; + } + found = true; + break; + } + if (!found) break; + CheckCapturesFinished(m); + const size_t st = static_cast(m.begin - sv.data()); + const size_t en = static_cast(m.end - sv.data()); + src = st; + + result.append(sv.data() + copied, st - copied); + + const std::string_view whole(m.begin, en - st); + std::string replacement; + bool keep_original = false; + + if (repl_is_closure) { + VarClosure *cl = repl_var.data_.cl; + const int call_arg_count = (m.level > 0) ? m.level : 1; + if (call_arg_count > static_cast(kMaxFunctionInputParams)) { + ThrowFakeluaException(std::format("string.gsub: too many capture arguments ({}), max is {}", + call_arg_count, kMaxFunctionInputParams)); + } + std::vector call_args(static_cast(call_arg_count)); + if (m.level > 0) { + for (int i = 0; i < call_arg_count; ++i) { + call_args[static_cast(i)] = CaptureToCVar(state, sv.data(), m, i); } - } else if (repl_is_table) { - std::string gsub_key = (match.size() > 1) ? match[1].str() : match[0].str(); - // 必须走完整表查找(spec + quick XOR buckets)。只扫 quick_data_ - // 会在 rehash 后漏掉第 9 个及之后的键,整段匹配原样留下。 - CVar val = table::TableHelper::GetTableStrId(state, repl_var, gsub_key.c_str()); - - if (val.type_ == static_cast(VarType::Nil)) { - replacement = match[0].str(); - } else if (val.type_ == static_cast(VarType::Bool) || val.type_ == static_cast(VarType::Table)) { - ThrowFakeluaException("invalid replacement value (boolean)"); + } else { + call_args[0] = inter::NativeToFakeluaStringView(state, whole); + } + // 在发起 gsub 的引擎里同步调用替换函数(与 pool:with 同理)。 + CVar fn_res = (cl->func_ptr != nullptr) + ? inter::DispatchCallClosure(state, cl, call_args.data(), call_arg_count, state->CurrentJit()) + : FlEvalLoadClosure(state, cl, call_arg_count, call_args.data()); + if (fn_res.type_ == static_cast(VarType::Nil) || + (fn_res.type_ == static_cast(VarType::Bool) && !fn_res.data_.b)) { + keep_original = true; + } else if (fn_res.type_ == static_cast(VarType::Bool)) { + ThrowFakeluaException("invalid replacement value (a boolean)"); + } else if (fn_res.type_ == static_cast(VarType::Table)) { + ThrowFakeluaException("invalid replacement value (a table)"); + } else { + replacement = std::string(KeyToStringView(fn_res)); + } + } else if (repl_is_table) { + CVar val; + if (m.level > 0) { + const auto &cap0 = m.caps[0]; + if (cap0.len == lua_pattern::kCapPosition) { + val = table::TableHelper::GetTableInt(state, repl_var, static_cast(cap0.init - sv.data()) + 1); } else { - replacement = std::string(KeyToStringView(val)); + std::string key(cap0.init, static_cast(cap0.len)); + val = table::TableHelper::GetTableStrId(state, repl_var, key.c_str()); } } else { - // 字符串替换:支持 $1 $2 ... $& $` $' $$ - std::string repl_str(KeyToStringView(repl_var)); - replacement.clear(); - for (size_t i = 0; i < repl_str.size(); ++i) { - if (repl_str[i] == '$' && i + 1 < repl_str.size()) { - char next = repl_str[i + 1]; - if (next == '$') { - replacement.push_back('$'); - i++; - } else if (next == '&') { - replacement += match[0].str(); - i++; - } else if (next == '`') { - replacement += match.prefix().str(); - i++; - } else if (next == '\'') { - replacement += match.suffix().str(); - i++; - } else if (next >= '1' && next <= '9') { - int idx = next - '1' + 1; - if (idx < static_cast(match.size())) { - replacement += match[idx].str(); - } - i++; - } else { - replacement.push_back(repl_str[i]); - } - } else { - replacement.push_back(repl_str[i]); + std::string key(whole); + val = table::TableHelper::GetTableStrId(state, repl_var, key.c_str()); + } + if (val.type_ == static_cast(VarType::Nil) || + (val.type_ == static_cast(VarType::Bool) && !val.data_.b)) { + keep_original = true; + } else if (val.type_ == static_cast(VarType::Bool) || val.type_ == static_cast(VarType::Table)) { + ThrowFakeluaException("invalid replacement value (a boolean)"); + } else { + replacement = std::string(KeyToStringView(val)); + } + } else { + // 字符串替换:%0=整个匹配,%1-%9=捕获,%%=百分号,其余 %x 报错。 + for (size_t i = 0; i < repl_str.size(); ++i) { + if (repl_str[i] != '%') { + replacement.push_back(repl_str[i]); + continue; + } + if (i + 1 >= repl_str.size()) { + ThrowFakeluaException("invalid use of '%' in replacement string"); + } + const char next = repl_str[++i]; + if (next == '%') { + replacement.push_back('%'); + } else if (next == '0') { + replacement.append(whole.data(), whole.size()); + } else if (next >= '1' && next <= '9') { + const int idx = next - '1'; + if (idx >= m.level) { + ThrowFakeluaException(std::format("invalid capture index %{}", idx + 1)); } + replacement.append(CaptureToLuaString(sv.data(), m, idx)); + } else { + ThrowFakeluaException(std::format("invalid use of '%{}' in a replacement string", next)); } } + } + if (keep_original) { + result.append(whole.data(), whole.size()); + } else { result += replacement; - last_pos = match.position() + match.length(); - count++; } - result.append(input, last_pos, std::string::npos); + ++count; + prev_end = en; - CVar multi = inter::AllocMultiCVar(state, 2); - inter::SetMultiCVarElement(multi, 0, inter::NativeToFakeluaStringView(state, result)); - inter::SetMultiCVarElement(multi, 1, inter::NativeToFakeluaInt(state, count)); - return multi; - } catch (const boost::regex_error &) { - return inter::NativeToFakeluaNil(state); + if (en == st) { + // 零宽匹配:保留当前字节(若有),下一轮从后一位置继续,避免死循环。 + if (st < slen) { + result.push_back(sv[st]); + copied = st + 1; + } else { + copied = en; + } + src = en + 1; + } else { + copied = en; + src = en; + } + if (anchored) break; } + result.append(sv.data() + copied, slen - copied); + + CVar multi = inter::AllocMultiCVar(state, 2); + inter::SetMultiCVarElement(multi, 0, inter::NativeToFakeluaStringView(state, result)); + inter::SetMultiCVarElement(multi, 1, inter::NativeToFakeluaInt(state, count)); + return multi; }); RegisterNativeFunction(s, "string.dump", 1, true, [](State *state, CVar *args, int n) -> CVar { diff --git a/test/lua/crypto/test_crypto_uuid.lua b/test/lua/crypto/test_crypto_uuid.lua index 4b7c75c..cd52264 100644 --- a/test/lua/crypto/test_crypto_uuid.lua +++ b/test/lua/crypto/test_crypto_uuid.lua @@ -12,11 +12,18 @@ function test_uuid() print("uuid not unique:", a) return 0 end - if not string.find(a, "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") then + -- Lua 模式没有 {n} 量词,按 RFC 段长重复字符类;段间连字符是元字符 '-' + -- (惰性量词),字面连字符必须写 %-。 + local hex8 = "%x%x%x%x%x%x%x%x" + local hex4 = "%x%x%x%x" + local hex3 = "%x%x%x" + local hex12 = "%x%x%x%x%x%x%x%x%x%x%x%x" + local uuid_pat = "^" .. hex8 .. "%-" .. hex4 .. "%-4" .. hex3 .. "%-[89ab]" .. hex3 .. "%-" .. hex12 .. "$" + if not string.find(a, uuid_pat) then print("uuid format:", a) return 0 end - if not string.find(b, "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") then + if not string.find(b, uuid_pat) then print("uuid format:", b) return 0 end diff --git a/test/lua/infer/test_jitbug_while_cond_reval.lua b/test/lua/infer/test_jitbug_while_cond_reval.lua new file mode 100644 index 0000000..7bf8ad3 --- /dev/null +++ b/test/lua/infer/test_jitbug_while_cond_reval.lua @@ -0,0 +1,94 @@ +-- P1-9 回归:while 条件必须每轮重新求值。 +-- 原生 C while 快速路径只能嵌入「不输出语句」的纯表达式条件;#t、函数调用、 +-- 表索引、整数整除/取模等经 CompileNumericExp 会编译成 while 之外只求值一次的 +-- 「语句 + 临时变量」,造成条件读陈旧值(死循环或越界)。这些条件必须回退到 +-- while(1){ 每轮重新 CompileExp 条件 } 的通用路径。 +-- 预期值以 PUC-Rio Lua 5.4/5.5 实跑结果为准。 + +-- #t 每轮重新求值:陈旧绑定会死循环(100 次保护返回 -1),正确则删空返回 10。 +function test_len_in_cond() + local t = {} + for i = 1, 10 do t[i] = i end + local removed = 0 + while #t > 0 do + if removed > 100 then return -1 end + table.remove(t, #t) + removed = removed + 1 + end + return removed +end + +-- 条件里的函数调用每轮重新执行。 +function test_call_in_cond() + local n = 3 + local function dec() + n = n - 1 + return n + end + local hits = 0 + while dec() > 0 do + hits = hits + 1 + end + return hits +end + +-- 条件里的表索引每轮重新读取。 +function test_index_in_cond() + local t = { x = 3 } + local hits = 0 + while t.x > 0 do + t.x = t.x - 1 + hits = hits + 1 + end + return hits +end + +-- 条件里的整数取模 / 整除每轮重新计算。 +function test_arith_in_cond() + local v = 10 + local hits = 0 + while v % 3 ~= 0 do + v = v - 1 + hits = hits + 1 + end + local v2 = 10 + local hits2 = 0 + while v2 // 4 > 1 do + v2 = v2 - 1 + hits2 = hits2 + 1 + end + return v * 1000 + hits * 100 + v2 * 10 + hits2 +end + +-- not 包住的不纯比较同样必须回退。 +function test_not_len_in_cond() + local t = { 1, 2, 3 } + local hits = 0 + while not (#t == 0) do + if hits > 10 then return -1 end + table.remove(t, 1) + hits = hits + 1 + end + return hits +end + +-- 纯简单变量条件仍走原生 while 快速路径(结果也必须正确)。 +function test_pure_cond() + local x = 0 + local hits = 0 + while x < 10 do + x = x + 2 + hits = hits + 1 + end + return hits * 100 + x +end + +function test() + if test_len_in_cond() ~= 10 then return 1 end + if test_call_in_cond() ~= 2 then return 2 end + if test_index_in_cond() ~= 3 then return 3 end + if test_arith_in_cond() ~= 9173 then return 4 end + if test_not_len_in_cond() ~= 3 then return 5 end + if test_pure_cond() ~= 510 then return 6 end + return 0 +end diff --git a/test/lua/json/test_json_edge.lua b/test/lua/json/test_json_edge.lua index 807d1c0..c99dd58 100644 --- a/test/lua/json/test_json_edge.lua +++ b/test/lua/json/test_json_edge.lua @@ -260,8 +260,8 @@ end -- 测试 JSON 编码:空数组字段嵌在对象里 function test_encode_empty_array_field() local s = json.encode({ type = "rank", list = {} }) - -- 空 list 字段应为 "list":[] 而不是 "list":{}(string.find 为 ECMAScript 正则,[ 需转义) - if not string.find(s, '"list":\\[\\]') then return 0 end + -- 空 list 字段应为 "list":[] 而不是 "list":{}(Lua 模式下 [ ] 用 % 转义) + if not string.find(s, '"list":%[%]') then return 0 end return 1 end diff --git a/test/lua/string/test_gsub_bad_func_return_bool.lua b/test/lua/string/test_gsub_bad_func_return_bool.lua index 03662da..1323508 100644 --- a/test/lua/string/test_gsub_bad_func_return_bool.lua +++ b/test/lua/string/test_gsub_bad_func_return_bool.lua @@ -1,7 +1,22 @@ function test_gsub_bad_func_return_bool() - -- string.gsub 函数替换时,函数返回值不能是 Bool,标准 Lua 会报错 - string.gsub("hello", "l", function() - return true + -- Lua 5.4 语义:替换函数返回 nil 或 false 时保留原匹配(不报错、不删除)。 + local r1, c1 = string.gsub("hello", "l", function() + return false end) - return 0 + if r1 ~= "hello" or c1 ~= 2 then return 0 end + + local r2, c2 = string.gsub("abc", "b", function() + return nil + end) + if r2 ~= "abc" or c2 ~= 1 then return 0 end + + -- 返回 true(非 nil/false 的布尔)仍然是非法替换值 + local ok = pcall(function() + string.gsub("a", "a", function() + return true + end) + end) + if ok then return 0 end + + return 1 end diff --git a/test/lua/string/test_gsub_bad_table_value_bool.lua b/test/lua/string/test_gsub_bad_table_value_bool.lua index f9afd87..8a4c563 100644 --- a/test/lua/string/test_gsub_bad_table_value_bool.lua +++ b/test/lua/string/test_gsub_bad_table_value_bool.lua @@ -1,6 +1,19 @@ function test_gsub_bad_table_value_bool() - -- string.gsub 表替换时,表值不能是 Bool,标准 Lua 会报错 - local t = { l = true } - string.gsub("hello", "l", t) - return 0 + -- Lua 5.4 语义:表值为 nil 或 false 时保留原匹配(不报错、不删除)。 + local keep = { l = false } + local r1, c1 = string.gsub("hello", "l", keep) + if r1 ~= "hello" or c1 ~= 2 then return 0 end + + local missing = {} + local r2, c2 = string.gsub("abc", "b", missing) + if r2 ~= "abc" or c2 ~= 1 then return 0 end + + -- 表值为 true(非 nil/false 的布尔)仍然是非法替换值 + local bad = { a = true } + local ok = pcall(function() + string.gsub("a", "a", bad) + end) + if ok then return 0 end + + return 1 end diff --git a/test/lua/string/test_string_find.lua b/test/lua/string/test_string_find.lua index 8892ab3..eeee7f4 100644 --- a/test/lua/string/test_string_find.lua +++ b/test/lua/string/test_string_find.lua @@ -20,12 +20,12 @@ function test_string_find() local h, i = string.find(s, "o", 1, true) if h ~= 5 or i ~= 5 then return 3 end - -- 正则捕获组 (ECMAScript 语法) - local j, k, cap = string.find(s, "([a-zA-Z]+) ([a-zA-Z]+)") + -- Lua 模式捕获组 + local j, k, cap = string.find(s, "(%a+) (%a+)") if j ~= 1 or k ~= 11 or cap ~= "hello" then return 4 end -- 多个捕获组 - local l, m, c1, c2 = string.find(s, "([a-zA-Z]+) ([a-zA-Z]+)") + local l, m, c1, c2 = string.find(s, "(%a+) (%a+)") if l ~= 1 or m ~= 11 or c1 ~= "hello" or c2 ~= "world" then return 5 end -- 数字参数隐式转换 (Lua 标准规范) diff --git a/test/lua/string/test_string_find_cases.lua b/test/lua/string/test_string_find_cases.lua index 3634a7c..6569d55 100644 --- a/test/lua/string/test_string_find_cases.lua +++ b/test/lua/string/test_string_find_cases.lua @@ -1,9 +1,9 @@ package "StringFindCases" --- 测试 string.find 正则捕获(fakelua 使用 ECMAScript 正则语法) +-- 测试 string.find Lua 模式捕获 function test_find_capture() local s = "hello world 123" - local start, finish, cap = string.find(s, "(\\d+)") + local start, finish, cap = string.find(s, "(%d+)") if start == nil then return 0 end if cap ~= "123" then return 0 end return 1 diff --git a/test/lua/string/test_string_gmatch.lua b/test/lua/string/test_string_gmatch.lua index 230acd6..cf7fcea 100644 --- a/test/lua/string/test_string_gmatch.lua +++ b/test/lua/string/test_string_gmatch.lua @@ -1,7 +1,7 @@ function test_string_gmatch() local s = "hello world from fakelua" local words = {} - for w in string.gmatch(s, "[a-zA-Z]+") do + for w in string.gmatch(s, "%a+") do words[#words + 1] = w end if #words ~= 4 then return 1 end @@ -10,11 +10,11 @@ function test_string_gmatch() if words[3] ~= "from" then return 4 end if words[4] ~= "fakelua" then return 5 end - -- 捕获组迭代 (ECMAScript 语法) + -- 捕获组迭代(Lua 模式 %a/%d) local s2 = "a=1 b=2 c=3" local keys = {} local vals = {} - for k, v in string.gmatch(s2, "([a-z])=(\\d)") do + for k, v in string.gmatch(s2, "(%a)=(%d)") do keys[#keys + 1] = k vals[#vals + 1] = v end @@ -33,7 +33,7 @@ function test_string_gmatch() -- 空串也必须返回迭代器,不能直接 nil(for-in 会 attempt to call a nil value) local empty_n = 0 - for _ in string.gmatch("", "[a-z]+") do + for _ in string.gmatch("", "%a+") do empty_n = empty_n + 1 end if empty_n ~= 0 then return 11 end diff --git a/test/lua/string/test_string_gsub.lua b/test/lua/string/test_string_gsub.lua index ed8444c..7891e0a 100644 --- a/test/lua/string/test_string_gsub.lua +++ b/test/lua/string/test_string_gsub.lua @@ -15,17 +15,17 @@ function test_string_gsub() local s4, cnt4 = string.gsub("hello", "xyz", "abc") if s4 ~= "hello" or cnt4 ~= 0 then return 0 end - -- 正则捕获 + $1 引用 (ECMAScript 语法) - local s5 = string.gsub("hello world", "([a-zA-Z]+) ([a-zA-Z]+)", "$2 $1") + -- Lua 模式捕获 + %1/%2 引用 + local s5 = string.gsub("hello world", "(%a+) (%a+)", "%2 %1") if s5 ~= "world hello" then return 0 end - -- 表替换 (ECMAScript 语法) + -- 表替换(Lua 模式 %a) local t = { a = "A", b = "B" } - local s6 = string.gsub("a b c", "[a-z]", t) + local s6 = string.gsub("a b c", "%a", t) if s6 ~= "A B c" then return 0 end -- 函数替换 + 捕获组正确传递测试 - local s7 = string.gsub("hello 123 world", "(\\d+)", function(cap1) + local s7 = string.gsub("hello 123 world", "(%d+)", function(cap1) return "[" .. cap1 .. "]" end) if s7 ~= "hello [123] world" then return 0 end @@ -37,7 +37,7 @@ function test_string_gsub() gsub_big["k" .. gsub_i] = "v" .. gsub_i gsub_i = gsub_i + 1 end - local s8 = string.gsub("k1 k9 k15 kx", "k\\d+", gsub_big) + local s8 = string.gsub("k1 k9 k15 kx", "k%d+", gsub_big) if s8 ~= "v1 v9 v15 kx" then return 0 end -- 捕获组超过旧的 16 槽上限时,第 17 个参数会被丢掉 diff --git a/test/lua/string/test_string_gsub_cases.lua b/test/lua/string/test_string_gsub_cases.lua index ea948e4..da65556 100644 --- a/test/lua/string/test_string_gsub_cases.lua +++ b/test/lua/string/test_string_gsub_cases.lua @@ -1,9 +1,9 @@ package "StringGsubCases" --- 测试 string.gsub 函数替换 +-- 测试 string.gsub 函数替换(Lua 模式 %w+ 字母数字序列) function test_gsub_function() local s = "hello world" - local r = string.gsub(s, "\\w+", function(w) + local r = string.gsub(s, "%w+", function(w) return string.upper(w) end) if r ~= "HELLO WORLD" then return 0 end @@ -14,7 +14,7 @@ end function test_gsub_table() local s = "a b" local t = { a = "x", b = "y" } - local r = string.gsub(s, "(\\w+)", t) + local r = string.gsub(s, "(%w+)", t) if r ~= "x y" then return 0 end return 1 end diff --git a/test/lua/string/test_string_lua_pattern.lua b/test/lua/string/test_string_lua_pattern.lua new file mode 100644 index 0000000..472f431 --- /dev/null +++ b/test/lua/string/test_string_lua_pattern.lua @@ -0,0 +1,247 @@ +-- P1-5b 回归:string.find/match/gmatch/gsub 使用 Lua 5.4 模式(pattern), +-- 而不是 ECMAScript 正则。覆盖转义/字符类/集合/量词/锚点/捕获/%b/%f/gsub 替换。 +-- 所有预期值以 PUC-Rio Lua 5.4/5.5 实跑为准。 + +local function check(cond, code) + if not cond then return code end +end + +function test_lua_pattern_escape() + -- 标点转义:%. 匹配点号,%% 匹配百分号,%( %) %+ 等 + local a, b = string.find("../etc/passwd", "%.%.") + if a ~= 1 or b ~= 2 then return 1 end + if string.match("100%", "%%") ~= "%" then return 2 end + if string.find("a+b", "%+") ~= 2 then return 3 end + if string.match("x.y", "x%.y") ~= "x.y" then return 4 end + -- 点号是任意单字节 + if string.match("a1b", "a.b") ~= "a1b" then return 5 end + return 0 +end + +function test_lua_pattern_classes() + if string.match("abc123", "%d+") ~= "123" then return 1 end + if string.match("AbC", "%l+") ~= "b" then return 2 end + if string.match("AbC", "%u+") ~= "A" then return 3 end + if string.match("AbC", "%a+") ~= "AbC" then return 4 end + -- %W 取反:非字母数字 + if string.match("ab1!cd", "%W") ~= "!" then return 5 end + -- %x 十六进制 + if string.match("f0oBAR", "%x+") ~= "f0" then return 6 end + if string.match("ZZZ9", "%X+") ~= "ZZZ" then return 7 end + -- %s 空白 / %p 标点 / %c 控制符 / %g 可打印(不含空格) + if string.match("a b", "%s") ~= " " then return 8 end + if string.match("a,b", "%p") ~= "," then return 9 end + if string.match("a\1b", "%c") ~= "\1" then return 10 end + if string.match(" a", "%g") ~= "a" then return 11 end + -- %z 零字节(Lua 5.1/5.5 保留类) + if select(2, string.find("a\0b", "%z")) ~= 2 then return 12 end + return 0 +end + +function test_lua_pattern_sets() + if string.match("a-", "[-a]") ~= "a" then return 1 end + if string.match("]x", "[]x]") ~= "]" then return 2 end + if string.match("9a", "[%d_]") ~= "9" then return 3 end + if string.match("a1_", "[%d_]+") ~= "1_" then return 4 end + if string.match("9a", "[^%d]") ~= "a" then return 5 end + if string.match("m", "[a-z]") ~= "m" then return 6 end + if string.match("M", "[a-z]") ~= nil then return 7 end + if string.match("7", "[0-9-]") ~= "7" then return 8 end + -- 区间外的 '-' 在末尾是字面量 + if string.match("-", "[a-]") ~= "-" then return 9 end + return 0 +end + +function test_lua_pattern_quantifiers() + -- 惰性 '-' + if string.match("a12b", "a.-b") ~= "a12b" then return 1 end + if string.match("a12b34", "a(.-)b") ~= "12" then return 2 end + -- '?' 可选 + if string.match("abc", "ab?c") ~= "abc" then return 3 end + if string.match("ac", "ab?c") ~= "ac" then return 4 end + -- 零宽重复的逐位行为(gsub/gmatch 不产生相邻重复空匹配) + local r1, n1 = string.gsub("aaa", "b*", "x") + if r1 ~= "xaxaxax" or n1 ~= 4 then return 5 end + local r2, n2 = string.gsub("aaa", "a*", "x") + if r2 ~= "x" or n2 ~= 1 then return 6 end + local r3, n3 = string.gsub("abc", "a?", "x") + if r3 ~= "xbxcx" or n3 ~= 3 then return 7 end + local r4, n4 = string.gsub("", "", "x") + if r4 ~= "x" or n4 ~= 1 then return 8 end + -- 捕获组后面不含量词语义:(a)* 里的 '*' 是字面量 + if string.match("aaa", "(a)*") ~= nil then return 9 end + return 0 +end + +function test_lua_pattern_anchors() + if string.match("hello", "^h") ~= "h" then return 1 end + if string.match("hello", "o$") ~= "o" then return 2 end + if string.match("hello", "^o") ~= nil then return 3 end + if string.match("hello", "h$") ~= nil then return 4 end + -- 不在末尾的 '$' 是普通字符 + if string.match("a$b", "a$b") ~= "a$b" then return 5 end + -- gsub 锚定只替换一次 + local r, n = string.gsub("aaa", "^a", "x") + if r ~= "xaa" or n ~= 1 then return 6 end + return 0 +end + +function test_lua_pattern_captures() + -- 位置捕获 () + local a, b = string.match("hello", "()l()") + if a ~= 3 or b ~= 4 then return 1 end + -- 嵌套捕获 + local x, y, z = string.match("key=val", "((%w+)=(%w+))") + if x ~= "key=val" or y ~= "key" or z ~= "val" then return 2 end + -- 捕获反向引用 %1 + if string.match("abab", "(%w+)%1") ~= "ab" then return 3 end + if string.match("abac", "(%w+)%1") ~= nil then return 4 end + -- find 同时返回位置和捕获 + local s, e, cap = string.find("hello world", "(%w+)$") + if s ~= 7 or e ~= 11 or cap ~= "world" then return 5 end + -- match 无捕获返回整个匹配 + if string.match("2024-01-15", "%d+-%d+-%d+") ~= "2024-01-15" then return 6 end + -- match 有多个捕获返回多值 + local cy, cm, cd = string.match("2024-01-15", "(%d+)-(%d+)-(%d+)") + if cy ~= "2024" or cm ~= "01" or cd ~= "15" then return 7 end + -- 空捕获串:组内可选项未消费时返回空串 + if string.match("b", "(a?)b") ~= "" then return 8 end + return 0 +end + +function test_lua_pattern_frontier_balance() + -- %f[set] 前沿模式(串首视作 '\0',而 '\0' 属于 %S,所以首词前沿不触发) + if string.match(" hello", "%f[%w]%w+") ~= "hello" then return 1 end + local words = {} + for w in string.gmatch("a b c", "%f[%S]%S+") do words[#words + 1] = w end + if #words ~= 2 or words[1] ~= "b" or words[2] ~= "c" then return 2 end + -- %b() 平衡匹配 + if string.match("a(b(c)d)e", "%b()") ~= "(b(c)d)" then return 3 end + local s, e = string.find("x(..(.).)y", "%b()") + if s ~= 2 or e ~= 9 then return 4 end + -- 自定义平衡字符 + if string.match("a[x[y]z]b", "%b[]") ~= "[x[y]z]" then return 5 end + return 0 +end + +function test_lua_pattern_init_plain() + -- init 起始位置(1-based,支持负数) + local a = string.find("hello world", "o", 5) + if a ~= 5 then return 1 end + local b = string.find("hello world", "o", -5) + if b ~= 8 then return 2 end + -- match 的 init + if string.match("hello world", "%w+", 7) ~= "world" then return 3 end + -- plain=true 纯子串查找,元字符无特殊含义 + local s, e = string.find("a+b+c", "+", 1, true) + if s ~= 2 or e ~= 2 then return 4 end + return 0 +end + +function test_lua_pattern_gsub() + -- 字符串替换:%0 整个匹配,%% 百分号,%1-%9 捕获 + local r1 = string.gsub("abc", "b", "[%0]") + if r1 ~= "a[b]c" then return 1 end + local r2 = string.gsub("a%b", "%%", "pct") + if r2 ~= "apctb" then return 2 end + local r3 = string.gsub("a1b2", "(%a)(%d)", "%2%1") + if r3 ~= "1a2b" then return 3 end + -- 函数替换:收捕获;nil/false 保留原文 + local r4 = string.gsub("hello 123", "%d+", function(d) return "[" .. d .. "]" end) + if r4 ~= "hello [123]" then return 4 end + local r5, c5 = string.gsub("aaa", "a", function() return nil end) + if r5 ~= "aaa" or c5 ~= 3 then return 5 end + local r6, c6 = string.gsub("aaa", "a", function() return false end) + if r6 ~= "aaa" or c6 ~= 3 then return 6 end + -- 表替换 + local r7 = string.gsub("a b c", "%a", { a = "X", c = "Z" }) + if r7 ~= "X b Z" then return 7 end + local r8, c8 = string.gsub("k1 k9", "k(%d)", { ["1"] = "one", ["9"] = "nine" }) + if r8 ~= "one nine" or c8 ~= 2 then return 8 end + -- 替换次数上限 + local r9, c9 = string.gsub("aaa aaa aaa", "aaa", "b", 2) + if r9 ~= "b b aaa" or c9 ~= 2 then return 9 end + return 0 +end + +function test_lua_pattern_gmatch() + local out = {} + for w in string.gmatch("a,b,,c", "[^,]*") do out[#out + 1] = w end + if #out ~= 4 or out[1] ~= "a" or out[2] ~= "b" or out[3] ~= "" or out[4] ~= "c" then return 1 end + local keys, vals = {}, {} + for k, v in string.gmatch("a=1, b=2", "(%w)=(%d)") do + keys[#keys + 1] = k + vals[#vals + 1] = v + end + if #keys ~= 2 or keys[1] ~= "a" or vals[1] ~= "1" or keys[2] ~= "b" or vals[2] ~= "2" then return 2 end + -- 空主体 + local n = 0 + for _ in string.gmatch("", "%w+") do n = n + 1 end + if n ~= 0 then return 3 end + return 0 +end + +function test_lua_pattern_charpattern() + local pat = string.charpattern + if type(pat) ~= "string" then return 1 end + -- Lua 5.4 charpattern 匹配任意单字节,包括 '\0' 和 0xFF + if string.match("hello", pat) ~= "h" then return 2 end + if string.match("\0\255", pat) ~= "\0" then return 3 end + local n = 0 + for _ in string.gmatch("12345", pat) do n = n + 1 end + if n ~= 5 then return 4 end + if string.match("", pat) ~= nil then return 5 end + return 0 +end + +function test_lua_pattern_errors() + -- 非法模式必须响亮报错(pcall 包一层 Lua 闭包) + local cases = { + function() return string.match("abc", "%") end, + function() return string.find("abc", "[a") end, + function() return string.gsub("abc", "%1", "x") end, + function() return string.gsub("abc", ".", "%x") end, + function() return string.gsub("abc", ".", "%") end, + function() return string.match("abc", "%0") end + } + for i = 1, #cases do + local ok_each = pcall(cases[i]) + if ok_each then return i end + end + -- 未闭合捕获在匹配成功提取时报 unfinished capture + local ok_open = pcall(function() return string.match("ab", "(ab") end) + if ok_open then return 6 end + -- 合法的未匹配畸形片段不应报错(惰性解析,与 Lua 一致) + local ok_lazy, ret = pcall(function() return string.match("zzz", "a[") end) + if not ok_lazy or ret ~= nil then return 7 end + return 0 +end + +function test() + local rc + rc = test_lua_pattern_escape() + if rc ~= 0 then return rc + 100 end + rc = test_lua_pattern_classes() + if rc ~= 0 then return rc + 200 end + rc = test_lua_pattern_sets() + if rc ~= 0 then return rc + 300 end + rc = test_lua_pattern_quantifiers() + if rc ~= 0 then return rc + 400 end + rc = test_lua_pattern_anchors() + if rc ~= 0 then return rc + 500 end + rc = test_lua_pattern_captures() + if rc ~= 0 then return rc + 600 end + rc = test_lua_pattern_frontier_balance() + if rc ~= 0 then return rc + 700 end + rc = test_lua_pattern_init_plain() + if rc ~= 0 then return rc + 800 end + rc = test_lua_pattern_gsub() + if rc ~= 0 then return rc + 900 end + rc = test_lua_pattern_gmatch() + if rc ~= 0 then return rc + 1000 end + rc = test_lua_pattern_charpattern() + if rc ~= 0 then return rc + 1100 end + rc = test_lua_pattern_errors() + if rc ~= 0 then return rc + 1200 end + return 0 +end diff --git a/test/lua/string/test_string_match.lua b/test/lua/string/test_string_match.lua index 70f667f..1a32d63 100644 --- a/test/lua/string/test_string_match.lua +++ b/test/lua/string/test_string_match.lua @@ -2,23 +2,23 @@ function test_string_match() local s = "hello world 123" -- 无捕获组:返回整个匹配 - local m1 = string.match(s, "[a-zA-Z]+") + local m1 = string.match(s, "%a+") if m1 ~= "hello" then return 1 end -- 单个捕获组 - local m2 = string.match(s, "([a-zA-Z]+) ([a-zA-Z]+)") + local m2 = string.match(s, "(%a+) (%a+)") if m2 ~= "hello" then return 2 end -- 多个捕获组 - local a, b = string.match(s, "([a-zA-Z]+) ([a-zA-Z]+)") + local a, b = string.match(s, "(%a+) (%a+)") if a ~= "hello" or b ~= "world" then return 3 end - -- 数字捕获 (ECMAScript 语法用 \d) - local num = string.match(s, "\\d+") + -- 数字捕获(Lua 模式用 %d) + local num = string.match(s, "%d+") if num ~= "123" then return 4 end -- 带起始位置 - local m3 = string.match(s, "[a-zA-Z]+", 7) + local m3 = string.match(s, "%a+", 7) if m3 ~= "world" then return 5 end -- 找不到 diff --git a/test/lua/string/test_string_match_cases.lua b/test/lua/string/test_string_match_cases.lua index 7dca779..77dca00 100644 --- a/test/lua/string/test_string_match_cases.lua +++ b/test/lua/string/test_string_match_cases.lua @@ -1,9 +1,9 @@ package "StringMatchCases" --- 测试 string.match 捕获 +-- 测试 string.match 捕获(Lua 模式 %d 数字类) function test_match_capture() local s = "date: 2024-01-15" - local y, m, d = string.match(s, "(\\d+)-(\\d+)-(\\d+)") + local y, m, d = string.match(s, "(%d+)-(%d+)-(%d+)") if y ~= "2024" then return 0 end if m ~= "01" then return 0 end if d ~= "15" then return 0 end diff --git a/test/test_infer.cpp b/test/test_infer.cpp index 9394625..0bfda70 100644 --- a/test/test_infer.cpp +++ b/test/test_infer.cpp @@ -4521,6 +4521,25 @@ TEST(infer, test_jitbug_reserved_func_name) { }); } +// P1-9:while 条件里的 #t / 函数调用 / 表索引 / 整数整除取模必须每轮重新求值。 +// 这些操作数经 CompileNumericExp 会输出「while 之外只求值一次的语句 + 临时变量」, +// 必须回退到每轮重新 CompileExp 条件的通用 while(1) 路径;纯简单变量条件则仍保留 +// 原生 while 快速路径(检查生成 C 代码)。 +TEST(infer, test_jitbug_while_cond_reval) { + const auto code = InferGetCCode("./infer/test_jitbug_while_cond_reval.lua"); + // 不纯条件走通用路径。 + ASSERT_NE(code.find("while (1) {"), std::string::npos); + // 纯简单变量条件仍是原生 while 比较(无 IsTrue 临时 bool)。 + ASSERT_NE(code.find("while ((x) < (10)) {"), std::string::npos); + + InferRunHelper([](State *s, JITType type, bool debug_mode) { + CompileFile(s, "./infer/test_jitbug_while_cond_reval.lua", {.debug_mode = debug_mode}); + int64_t ret = 0; + Call(s, type, "test", ret); + ASSERT_EQ(ret, 0); + }); +} + TEST(infer, test_spec_literal_keys) { InferRunHelper([](State *s, JITType type, bool debug_mode) { CompileFile(s, "./infer/test_spec_literal_keys.lua", {.debug_mode = debug_mode}); diff --git a/test/test_string.cpp b/test/test_string.cpp index 9e1d799..505e902 100644 --- a/test/test_string.cpp +++ b/test/test_string.cpp @@ -110,6 +110,25 @@ TEST(test_string, test_string_dump) { FakeluaDeleteState(s); } +// P1-5b:find/match/gmatch/gsub 使用 Lua 5.4 模式而非 ECMAScript 正则。 +TEST(test_string, test_string_lua_pattern) { + // 用例内含 pcall 捕获 native 异常;异常后各引擎用独立 state,避免跨引擎状态污染。 + for (auto jit_type: AllJitTypes()) { + SCOPED_TRACE(::testing::Message() << "lua-pattern jit=" << JitTypeName(jit_type)); + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + // 用例用 pcall 捕获 native 抛错;debug 模式下 C++ 异常穿过调试调用帧不被 + // Lua pcall 捕获(既有引擎限制),release 模式三后端行为一致。 + config.debug_mode = false; + CompileFile(s, "./string/test_string_lua_pattern.lua", config); + int64_t res = -1; + Call(s, jit_type, "test", res); + EXPECT_EQ(res, 0); + FakeluaDeleteState(s); + } +} + TEST(test_string, test_string_find) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); @@ -386,12 +405,13 @@ TEST(test_string, test_gsub_bad_table_value_bool) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); CompileConfig config; - CompileFile(s, "./string/test_gsub_bad_table_value_bool.lua", config); - // TCC 是 C 编译器,不支持 C++ 异常传播,只测试 GCC 后端 - double res = 0; - CallThrow(s, "test_gsub_bad_table_value_bool", res); + // Lua 5.4:表值 false/nil 保留原匹配;true 仍是非法替换值(脚本内 pcall 校验)。 + // 三后端都跑:pcall 包 Lua 闭包可以捕获 native 异常。 + int64_t ret = 0; + CallAll(s, "test_gsub_bad_table_value_bool", ret); + EXPECT_EQ(ret, 1); FakeluaDeleteState(s); } @@ -400,12 +420,12 @@ TEST(test_string, test_gsub_bad_func_return_bool) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); CompileConfig config; - CompileFile(s, "./string/test_gsub_bad_func_return_bool.lua", config); - // TCC 是 C 编译器,不支持 C++ 异常传播,只测试 GCC 后端 - double res = 0; - CallThrow(s, "test_gsub_bad_func_return_bool", res); + // Lua 5.4:替换函数返回 false/nil 保留原匹配;返回 true 仍报错。 + int64_t ret = 0; + CallAll(s, "test_gsub_bad_func_return_bool", ret); + EXPECT_EQ(ret, 1); FakeluaDeleteState(s); }