StringLibrary.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. using System.Text;
  2. using Lua.Internal;
  3. using Lua.Runtime;
  4. using System.Globalization;
  5. using Lua.Standard.Internal;
  6. using System.Diagnostics;
  7. namespace Lua.Standard;
  8. public sealed class StringLibrary
  9. {
  10. public static readonly StringLibrary Instance = new();
  11. public StringLibrary()
  12. {
  13. var libraryName = "string";
  14. Functions =
  15. [
  16. new(libraryName, "byte", Byte),
  17. new(libraryName, "char", Char),
  18. new(libraryName, "dump", Dump),
  19. new(libraryName, "find", Find),
  20. new(libraryName, "format", Format),
  21. new(libraryName, "gmatch", GMatch),
  22. new(libraryName, "gsub", GSub),
  23. new(libraryName, "len", Len),
  24. new(libraryName, "lower", Lower),
  25. new(libraryName, "match", Match),
  26. new(libraryName, "rep", Rep),
  27. new(libraryName, "reverse", Reverse),
  28. new(libraryName, "sub", Sub),
  29. new(libraryName, "upper", Upper),
  30. ];
  31. }
  32. public readonly LibraryFunction[] Functions;
  33. public ValueTask<int> Byte(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  34. {
  35. var s = context.GetArgument<string>(0);
  36. var i = context.HasArgument(1)
  37. ? context.GetArgument<double>(1)
  38. : 1;
  39. var j = context.HasArgument(2)
  40. ? context.GetArgument<double>(2)
  41. : i;
  42. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.Thread, 2, i);
  43. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.Thread, 3, j);
  44. var span = StringHelper.Slice(s, (int)i, (int)j);
  45. var buffer = context.GetReturnBuffer(span.Length);
  46. for (int k = 0; k < span.Length; k++)
  47. {
  48. buffer[k] = span[k];
  49. }
  50. return new(span.Length);
  51. }
  52. public ValueTask<int> Char(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  53. {
  54. if (context.ArgumentCount == 0)
  55. {
  56. return new(context.Return(""));
  57. }
  58. var builder = new ValueStringBuilder(context.ArgumentCount);
  59. for (int i = 0; i < context.ArgumentCount; i++)
  60. {
  61. var arg = context.GetArgument<double>(i);
  62. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.Thread, i + 1, arg);
  63. builder.Append((char)arg);
  64. }
  65. return new(context.Return(builder.ToString()));
  66. }
  67. public ValueTask<int> Dump(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  68. {
  69. // stirng.dump is not supported (throw exception)
  70. throw new NotSupportedException("stirng.dump is not supported");
  71. }
  72. public ValueTask<int> Find(LuaFunctionExecutionContext context, CancellationToken cancellationToken) =>
  73. FindAux(context, true);
  74. public async ValueTask<int> Format(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  75. {
  76. var format = context.GetArgument<string>(0);
  77. var stack = context.Thread.Stack;
  78. // TODO: pooling StringBuilder
  79. var builder = new StringBuilder(format.Length * 2);
  80. var parameterIndex = 1;
  81. for (int i = 0; i < format.Length; i++)
  82. {
  83. if (format[i] == '%')
  84. {
  85. i++;
  86. // escape
  87. if (format[i] == '%')
  88. {
  89. builder.Append('%');
  90. continue;
  91. }
  92. var leftJustify = false;
  93. var plusSign = false;
  94. var zeroPadding = false;
  95. var alternateForm = false;
  96. var blank = false;
  97. var width = 0;
  98. var precision = -1;
  99. // Process flags
  100. while (true)
  101. {
  102. var c = format[i];
  103. switch (c)
  104. {
  105. case '-':
  106. if (leftJustify) throw new LuaRuntimeException(context.Thread, "invalid format (repeated flags)");
  107. leftJustify = true;
  108. break;
  109. case '+':
  110. if (plusSign) throw new LuaRuntimeException(context.Thread, "invalid format (repeated flags)");
  111. plusSign = true;
  112. break;
  113. case '0':
  114. if (zeroPadding) throw new LuaRuntimeException(context.Thread, "invalid format (repeated flags)");
  115. zeroPadding = true;
  116. break;
  117. case '#':
  118. if (alternateForm) throw new LuaRuntimeException(context.Thread, "invalid format (repeated flags)");
  119. alternateForm = true;
  120. break;
  121. case ' ':
  122. if (blank) throw new LuaRuntimeException(context.Thread, "invalid format (repeated flags)");
  123. blank = true;
  124. break;
  125. default:
  126. goto PROCESS_WIDTH;
  127. }
  128. i++;
  129. }
  130. PROCESS_WIDTH:
  131. // Process width
  132. var start = i;
  133. if (char.IsDigit(format[i]))
  134. {
  135. i++;
  136. if (char.IsDigit(format[i])) i++;
  137. if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.Thread, "invalid format (width or precision too long)");
  138. width = int.Parse(format.AsSpan()[start..i]);
  139. }
  140. // Process precision
  141. if (format[i] == '.')
  142. {
  143. i++;
  144. start = i;
  145. if (char.IsDigit(format[i])) i++;
  146. if (char.IsDigit(format[i])) i++;
  147. if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.Thread, "invalid format (width or precision too long)");
  148. precision = int.Parse(format.AsSpan()[start..i]);
  149. }
  150. // Process conversion specifier
  151. var specifier = format[i];
  152. if (context.ArgumentCount <= parameterIndex)
  153. {
  154. throw new LuaRuntimeException(context.Thread, $"bad argument #{parameterIndex + 1} to 'format' (no value)");
  155. }
  156. var parameter = context.GetArgument(parameterIndex++);
  157. // TODO: reduce allocation
  158. string formattedValue = default!;
  159. switch (specifier)
  160. {
  161. case 'f':
  162. case 'e':
  163. case 'g':
  164. case 'G':
  165. if (!parameter.TryRead<double>(out var f))
  166. {
  167. LuaRuntimeException.BadArgument(context.Thread, parameterIndex + 1, LuaValueType.Number, parameter.Type);
  168. }
  169. switch (specifier)
  170. {
  171. case 'f':
  172. formattedValue = precision < 0
  173. ? f.ToString(CultureInfo.InvariantCulture)
  174. : f.ToString($"F{precision}", CultureInfo.InvariantCulture);
  175. break;
  176. case 'e':
  177. formattedValue = precision < 0
  178. ? f.ToString(CultureInfo.InvariantCulture)
  179. : f.ToString($"E{precision}", CultureInfo.InvariantCulture);
  180. break;
  181. case 'g':
  182. formattedValue = precision < 0
  183. ? f.ToString(CultureInfo.InvariantCulture)
  184. : f.ToString($"G{precision}", CultureInfo.InvariantCulture);
  185. break;
  186. case 'G':
  187. formattedValue = precision < 0
  188. ? f.ToString(CultureInfo.InvariantCulture).ToUpper()
  189. : f.ToString($"G{precision}", CultureInfo.InvariantCulture).ToUpper();
  190. break;
  191. }
  192. if (plusSign && f >= 0)
  193. {
  194. formattedValue = $"+{formattedValue}";
  195. }
  196. break;
  197. case 's':
  198. {
  199. await parameter.CallToStringAsync(context, cancellationToken);
  200. formattedValue = stack.Pop().Read<string>();
  201. }
  202. if (specifier is 's' && precision > 0 && precision <= formattedValue.Length)
  203. {
  204. formattedValue = formattedValue[..precision];
  205. }
  206. break;
  207. case 'q':
  208. switch (parameter.Type)
  209. {
  210. case LuaValueType.Nil:
  211. formattedValue = "nil";
  212. break;
  213. case LuaValueType.Boolean:
  214. formattedValue = parameter.Read<bool>() ? "true" : "false";
  215. break;
  216. case LuaValueType.String:
  217. formattedValue = $"\"{StringHelper.Escape(parameter.Read<string>())}\"";
  218. break;
  219. case LuaValueType.Number:
  220. formattedValue = DoubleToQFormat(parameter.Read<double>());
  221. static string DoubleToQFormat(double value)
  222. {
  223. if (MathEx.IsInteger(value))
  224. {
  225. return value.ToString(CultureInfo.InvariantCulture);
  226. }
  227. return HexConverter.FromDouble(value);
  228. }
  229. break;
  230. default:
  231. {
  232. var top = stack.Count;
  233. stack.Push(default);
  234. await parameter.CallToStringAsync(context with { ReturnFrameBase = top }, cancellationToken);
  235. formattedValue = stack.Pop().Read<string>();
  236. }
  237. break;
  238. }
  239. break;
  240. case 'i':
  241. case 'd':
  242. case 'u':
  243. case 'c':
  244. case 'x':
  245. case 'X':
  246. if (!parameter.TryRead<double>(out var x))
  247. {
  248. LuaRuntimeException.BadArgument(context.Thread, parameterIndex + 1, LuaValueType.Number, parameter.Type);
  249. }
  250. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.Thread, parameterIndex + 1, x);
  251. switch (specifier)
  252. {
  253. case 'i':
  254. case 'd':
  255. {
  256. var integer = checked((long)x);
  257. formattedValue = precision < 0
  258. ? integer.ToString()
  259. : integer.ToString($"D{precision}");
  260. }
  261. break;
  262. case 'u':
  263. {
  264. var integer = checked((ulong)x);
  265. formattedValue = precision < 0
  266. ? integer.ToString()
  267. : integer.ToString($"D{precision}");
  268. }
  269. break;
  270. case 'c':
  271. formattedValue = ((char)(int)x).ToString();
  272. break;
  273. case 'x':
  274. {
  275. var integer = checked((ulong)x);
  276. formattedValue = alternateForm
  277. ? $"0x{integer:x}"
  278. : $"{integer:x}";
  279. }
  280. break;
  281. case 'X':
  282. {
  283. var integer = checked((ulong)x);
  284. formattedValue = alternateForm
  285. ? $"0X{integer:X}"
  286. : $"{integer:X}";
  287. }
  288. break;
  289. case 'o':
  290. {
  291. var integer = checked((long)x);
  292. formattedValue = Convert.ToString(integer, 8);
  293. }
  294. break;
  295. }
  296. if (plusSign && x >= 0)
  297. {
  298. formattedValue = $"+{formattedValue}";
  299. }
  300. break;
  301. default:
  302. throw new LuaRuntimeException(context.Thread, $"invalid option '%{specifier}' to 'format'");
  303. }
  304. // Apply blank (' ') flag for positive numbers
  305. if (specifier is 'd' or 'i' or 'f' or 'g' or 'G')
  306. {
  307. if (blank && !leftJustify && !zeroPadding && parameter.Read<double>() >= 0)
  308. {
  309. formattedValue = $" {formattedValue}";
  310. }
  311. }
  312. // Apply width and padding
  313. if (width > formattedValue.Length)
  314. {
  315. if (leftJustify)
  316. {
  317. formattedValue = formattedValue.PadRight(width);
  318. }
  319. else
  320. {
  321. formattedValue = zeroPadding ? formattedValue.PadLeft(width, '0') : formattedValue.PadLeft(width);
  322. }
  323. }
  324. builder.Append(formattedValue);
  325. }
  326. else
  327. {
  328. builder.Append(format[i]);
  329. }
  330. }
  331. return context.Return(builder.ToString());
  332. }
  333. public ValueTask<int> GMatch(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  334. {
  335. var s = context.GetArgument<string>(0);
  336. var pattern = context.GetArgument<string>(1);
  337. return new(context.Return(new CSharpClosure("gmatch_iterator", [s, pattern, 0], static (context, cancellationToken) =>
  338. {
  339. var upValues = context.GetCsClosure()!.UpValues;
  340. var s = upValues[0].Read<string>();
  341. var pattern = upValues[1].Read<string>();
  342. var start = upValues[2].Read<int>();
  343. var matchState = new MatchState(context.Thread, s, pattern);
  344. var captures = matchState.Captures;
  345. // Check for anchor at start
  346. bool anchor = pattern.Length > 0 && pattern[0] == '^';
  347. int pIdx = anchor ? 1 : 0;
  348. // For empty patterns, we need to match at every position including after the last character
  349. var sEndIdx = s.Length + (pattern.Length == 0 || (anchor && pattern.Length == 1) ? 1 : 0);
  350. for (int sIdx = start; sIdx < sEndIdx; sIdx++)
  351. {
  352. // Reset match state for each attempt
  353. matchState.Level = 0;
  354. matchState.MatchDepth = MatchState.MaxCalls;
  355. // Clear captures to avoid stale data
  356. Array.Clear(captures, 0, captures.Length);
  357. var res = matchState.Match(sIdx, pIdx);
  358. if (res >= 0)
  359. {
  360. // If no captures were made, create one for the whole match
  361. if (matchState.Level == 0)
  362. {
  363. captures[0].Init = sIdx;
  364. captures[0].Len = res - sIdx;
  365. matchState.Level = 1;
  366. }
  367. var resultLength = matchState.Level;
  368. var buffer = context.GetReturnBuffer(resultLength);
  369. for (int i = 0; i < matchState.Level; i++)
  370. {
  371. var capture = captures[i];
  372. if (capture.IsPosition)
  373. {
  374. buffer[i] = capture.Init + 1; // 1-based position
  375. }
  376. else
  377. {
  378. buffer[i] = s.AsSpan(capture.Init, capture.Len).ToString();
  379. }
  380. }
  381. // Update start index for next iteration
  382. // Handle empty matches by advancing at least 1 position
  383. upValues[2] = res > sIdx ? res : sIdx + 1;
  384. return new(resultLength);
  385. }
  386. // For anchored patterns, only try once
  387. if (anchor) break;
  388. }
  389. return new(context.Return(LuaValue.Nil));
  390. })));
  391. }
  392. public async ValueTask<int> GSub(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  393. {
  394. var s = context.GetArgument<string>(0);
  395. var pattern = context.GetArgument<string>(1);
  396. var repl = context.GetArgument(2);
  397. var n_arg = context.HasArgument(3)
  398. ? context.GetArgument<double>(3)
  399. : s.Length + 1;
  400. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.Thread, 4, n_arg);
  401. var n = (int)n_arg;
  402. // Use MatchState instead of regex
  403. var matchState = new MatchState(context.Thread, s, pattern);
  404. var captures = matchState.Captures;
  405. var builder = new StringBuilder();
  406. StringBuilder? replacedBuilder = repl.Type == LuaValueType.String
  407. ? new StringBuilder(repl.UnsafeReadString().Length)
  408. : null;
  409. var lastIndex = 0;
  410. var replaceCount = 0;
  411. // Check for anchor at start
  412. bool anchor = pattern.Length > 0 && pattern[0] == '^';
  413. int sIdx = 0;
  414. // For empty patterns, we need to match at every position including after the last character
  415. var sEndIdx = s.Length + (pattern.Length == 0 || (anchor && pattern.Length == 1) ? 1 : 0);
  416. while ((sIdx < sEndIdx) && replaceCount < n)
  417. {
  418. // Reset match state for each attempt
  419. matchState.Level = 0;
  420. Debug.Assert(matchState.MatchDepth == MatchState.MaxCalls);
  421. // Clear captures array to avoid stale data
  422. for (int i = 0; i < captures.Length; i++)
  423. {
  424. captures[i] = default;
  425. }
  426. // Always start pattern from beginning (0 or 1 if anchored)
  427. int pIdx = anchor ? 1 : 0;
  428. var res = matchState.Match(sIdx, pIdx);
  429. if (res >= 0)
  430. {
  431. // Found a match
  432. builder.Append(s.AsSpan()[lastIndex..sIdx]);
  433. // If no captures were made, create one for the whole match
  434. if (matchState.Level == 0)
  435. {
  436. captures[0].Init = sIdx;
  437. captures[0].Len = res - sIdx;
  438. matchState.Level = 1;
  439. }
  440. LuaValue result;
  441. if (repl.TryRead<string>(out var str))
  442. {
  443. if (!str.Contains("%"))
  444. {
  445. result = str; // No special characters, use as is
  446. }
  447. else
  448. {
  449. // String replacement
  450. replacedBuilder!.Clear();
  451. replacedBuilder.Append(str);
  452. // Replace %% with %
  453. replacedBuilder.Replace("%%", "\0"); // Use null char as temporary marker
  454. // Replace %0 with whole match
  455. var wholeMatch = s.AsSpan(sIdx, res - sIdx).ToString();
  456. replacedBuilder.Replace("%0", wholeMatch);
  457. // Replace %1, %2, etc. with captures
  458. for (int k = 0; k < matchState.Level; k++)
  459. {
  460. var capture = captures[k];
  461. string captureText;
  462. if (capture.IsPosition)
  463. {
  464. captureText = (capture.Init + 1).ToString(); // 1-based position
  465. }
  466. else
  467. {
  468. captureText = s.AsSpan(capture.Init, capture.Len).ToString();
  469. }
  470. replacedBuilder.Replace($"%{k + 1}", captureText);
  471. }
  472. // Replace temporary marker back to %
  473. replacedBuilder.Replace('\0', '%');
  474. result = replacedBuilder.ToString();
  475. }
  476. }
  477. else if (repl.TryRead<LuaTable>(out var table))
  478. {
  479. // Table lookup - use first capture or whole match
  480. string key;
  481. if (matchState.Level > 0 && !captures[0].IsPosition)
  482. {
  483. key = s.AsSpan(captures[0].Init, captures[0].Len).ToString();
  484. }
  485. else
  486. {
  487. key = s.AsSpan(sIdx, res - sIdx).ToString();
  488. }
  489. result = table[key];
  490. }
  491. else if (repl.TryRead<LuaFunction>(out var func))
  492. {
  493. // Function call with captures as arguments
  494. var stack = context.Thread.Stack;
  495. if (matchState.Level == 0)
  496. {
  497. // No captures, pass whole match
  498. stack.Push(s.AsSpan(sIdx, res - sIdx).ToString());
  499. var retCount = await context.Access.RunAsync(func, 1, cancellationToken);
  500. using var results = context.Access.ReadTopValues(retCount);
  501. result = results.Count > 0 ? results[0] : LuaValue.Nil;
  502. }
  503. else
  504. {
  505. // Pass all captures
  506. for (int k = 0; k < matchState.Level; k++)
  507. {
  508. var capture = captures[k];
  509. if (capture.IsPosition)
  510. {
  511. stack.Push(capture.Init + 1); // 1-based position
  512. }
  513. else
  514. {
  515. stack.Push(s.AsSpan(capture.Init, capture.Len).ToString());
  516. }
  517. }
  518. var retCount = await context.Access.RunAsync(func, matchState.Level, cancellationToken);
  519. using var results = context.Access.ReadTopValues(retCount);
  520. result = results.Count > 0 ? results[0] : LuaValue.Nil;
  521. }
  522. }
  523. else
  524. {
  525. throw new LuaRuntimeException(context.Thread, "bad argument #3 to 'gsub' (string/function/table expected)");
  526. }
  527. // Handle replacement result
  528. if (result.TryRead<string>(out var rs))
  529. {
  530. builder.Append(rs);
  531. }
  532. else if (result.TryRead<double>(out var rd))
  533. {
  534. builder.Append(rd);
  535. }
  536. else if (!result.ToBoolean())
  537. {
  538. // False or nil means don't replace
  539. builder.Append(s.AsSpan(sIdx, res - sIdx));
  540. }
  541. else
  542. {
  543. throw new LuaRuntimeException(context.Thread, $"invalid replacement value (a {result.Type})");
  544. }
  545. replaceCount++;
  546. lastIndex = res;
  547. // If empty match, advance by 1 to avoid infinite loop
  548. if (res == sIdx)
  549. {
  550. if (sIdx < s.Length)
  551. {
  552. builder.Append(s[sIdx]);
  553. lastIndex = sIdx + 1;
  554. }
  555. sIdx++;
  556. }
  557. else
  558. {
  559. sIdx = res;
  560. }
  561. }
  562. else
  563. {
  564. // No match at this position
  565. if (anchor)
  566. {
  567. // Anchored pattern only tries at start
  568. break;
  569. }
  570. sIdx++;
  571. }
  572. }
  573. // Append remaining part of string
  574. if (lastIndex < s.Length)
  575. {
  576. builder.Append(s.AsSpan()[lastIndex..]);
  577. }
  578. return context.Return(builder.ToString(), replaceCount);
  579. }
  580. public ValueTask<int> Len(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  581. {
  582. var s = context.GetArgument<string>(0);
  583. return new(context.Return(s.Length));
  584. }
  585. public ValueTask<int> Lower(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  586. {
  587. var s = context.GetArgument<string>(0);
  588. return new(context.Return(s.ToLower()));
  589. }
  590. public ValueTask<int> Match(LuaFunctionExecutionContext context, CancellationToken cancellationToken) =>
  591. FindAux(context, false);
  592. public ValueTask<int> FindAux(LuaFunctionExecutionContext context, bool find)
  593. {
  594. var s = context.GetArgument<string>(0);
  595. var pattern = context.GetArgument<string>(1);
  596. var init = context.HasArgument(2)
  597. ? context.GetArgument<int>(2)
  598. : 1;
  599. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.Thread, 3, init);
  600. // Convert to 0-based index
  601. if (init < 0)
  602. {
  603. init = s.Length + init + 1;
  604. }
  605. init--; // Convert from 1-based to 0-based
  606. // Check if init is beyond string bounds
  607. if (init > s.Length)
  608. {
  609. return new(context.Return(LuaValue.Nil));
  610. }
  611. init = Math.Max(0, init); // Clamp to 0 if negative
  612. // Check for plain search mode (4th parameter = true)
  613. if (find && context.GetArgumentOrDefault(3).ToBoolean())
  614. {
  615. return PlainSearch(context, s, pattern, init);
  616. }
  617. // Fast path for simple patterns without special characters
  618. if (find && MatchState.NoSpecials(pattern))
  619. {
  620. return SimplePatternSearch(context, s, pattern, init);
  621. }
  622. return PatternSearch(context, s, pattern, init, find);
  623. }
  624. private static ValueTask<int> PlainSearch(LuaFunctionExecutionContext context, string s, string pattern, int init)
  625. {
  626. var index = s.AsSpan(init).IndexOf(pattern);
  627. if (index == -1)
  628. {
  629. return new(context.Return(LuaValue.Nil));
  630. }
  631. var actualStart = init + index;
  632. return new(context.Return(actualStart + 1, actualStart + pattern.Length)); // Convert to 1-based
  633. }
  634. private static ValueTask<int> SimplePatternSearch(LuaFunctionExecutionContext context, string s, string pattern, int init)
  635. {
  636. var index = s.AsSpan(init).IndexOf(pattern);
  637. if (index == -1)
  638. {
  639. return new(context.Return(LuaValue.Nil));
  640. }
  641. var actualStart = init + index;
  642. return new(context.Return(actualStart + 1, actualStart + pattern.Length)); // Convert to 1-based
  643. }
  644. private static ValueTask<int> PatternSearch(LuaFunctionExecutionContext context, string s, string pattern, int init, bool find)
  645. {
  646. var matchState = new MatchState(context.Thread, s, pattern);
  647. var captures = matchState.Captures;
  648. // Check for anchor at start
  649. bool anchor = pattern.Length > 0 && pattern[0] == '^';
  650. int pIdx = anchor ? 1 : 0;
  651. // For empty patterns, we need to match at every position including after the last character
  652. var sEndIdx = s.Length + (pattern.Length == 0 ? 1 : 0);
  653. for (int sIdx = init; sIdx < sEndIdx; sIdx++)
  654. {
  655. // Reset match state for each attempt
  656. matchState.Level = 0;
  657. matchState.MatchDepth = MatchState.MaxCalls;
  658. Array.Clear(captures, 0, captures.Length);
  659. var res = matchState.Match(sIdx, pIdx);
  660. if (res >= 0)
  661. {
  662. // If no captures were made for string.match, create one for the whole match
  663. if (!find && matchState.Level == 0)
  664. {
  665. captures[0].Init = sIdx;
  666. captures[0].Len = res - sIdx;
  667. matchState.Level = 1;
  668. }
  669. var resultLength = matchState.Level + (find ? 2 : 0);
  670. var buffer = context.GetReturnBuffer(resultLength);
  671. if (find)
  672. {
  673. // Return start and end positions for string.find
  674. buffer[0] = sIdx + 1; // Convert to 1-based index
  675. buffer[1] = res; // Convert to 1-based index
  676. buffer = buffer[2..];
  677. }
  678. // Return captures
  679. for (int i = 0; i < matchState.Level; i++)
  680. {
  681. var capture = captures[i];
  682. if (capture.IsPosition)
  683. {
  684. buffer[i] = capture.Init + 1; // 1-based position
  685. }
  686. else
  687. {
  688. buffer[i] = s.AsSpan(capture.Init, capture.Len).ToString();
  689. }
  690. }
  691. return new(resultLength);
  692. }
  693. // For anchored patterns, only try once
  694. if (anchor) break;
  695. }
  696. return new(context.Return(LuaValue.Nil));
  697. }
  698. public ValueTask<int> Rep(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  699. {
  700. var s = context.GetArgument<string>(0);
  701. var n_arg = context.GetArgument<double>(1);
  702. var sep = context.HasArgument(2)
  703. ? context.GetArgument<string>(2)
  704. : null;
  705. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.Thread, 2, n_arg);
  706. var n = (int)n_arg;
  707. var builder = new ValueStringBuilder(s.Length * n);
  708. for (int i = 0; i < n; i++)
  709. {
  710. builder.Append(s);
  711. if (i != n - 1 && sep != null)
  712. {
  713. builder.Append(sep);
  714. }
  715. }
  716. return new(context.Return(builder.ToString()));
  717. }
  718. public ValueTask<int> Reverse(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  719. {
  720. var s = context.GetArgument<string>(0);
  721. using var strBuffer = new PooledArray<char>(s.Length);
  722. var span = strBuffer.AsSpan()[..s.Length];
  723. s.AsSpan().CopyTo(span);
  724. span.Reverse();
  725. return new(context.Return(span.ToString()));
  726. }
  727. public ValueTask<int> Sub(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  728. {
  729. var s = context.GetArgument<string>(0);
  730. var i = context.GetArgument<double>(1);
  731. var j = context.HasArgument(2)
  732. ? context.GetArgument<double>(2)
  733. : -1;
  734. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.Thread, 2, i);
  735. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.Thread, 3, j);
  736. return new(context.Return(StringHelper.Slice(s, (int)i, (int)j).ToString()));
  737. }
  738. public ValueTask<int> Upper(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
  739. {
  740. var s = context.GetArgument<string>(0);
  741. return new(context.Return(s.ToUpper()));
  742. }
  743. }