feat(strutils): clean literal replace API — Replace (first) + ReplaceAll

Replace the FPC/Delphi replace surface (ReplaceStr, ReplaceText, and the
proposed StringReplace + TReplaceFlags set) with two self-explanatory
functions:

  Replace(S, Old, New)    — replaces the FIRST occurrence
  ReplaceAll(S, Old, New) — replaces EVERY occurrence

Both are case-sensitive and literal (non-pattern).  This sheds the legacy
redundancy: ReplaceStr/ReplaceText were two names for one operation split by
a 'String vs Text' distinction that is not obvious, and Delphi's StringReplace
encodes the same two booleans (all-vs-first, sensitive-vs-insensitive) as an
awkward flag-set.  Modern languages (Go, Python, Java, Rust) use a plain
first/all split with no case flag.

Case-insensitive replace is expressed by lower-casing the inputs with the
built-in LowerCase/UpperCase before calling Replace/ReplaceAll; a
case-*preserving* insensitive replace and pattern/regex matching are deferred
and recorded in docs/future-improvements.adoc.

The internal worker keeps the existing TStringBuilder-based loop; it gains a
FirstOnly flag and drops the now-unused case-fold path.

No callers of the removed functions existed anywhere in the tree.  IR/semantic
and e2e tests updated to the new names; both fixpoints green (FIXPOINT_OK,
NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK); full suite OK (3346 tests).
This commit is contained in:
Graeme Geldenhuys 2026-06-16 23:49:30 +01:00
parent 712263abd4
commit 2f54707a63
4 changed files with 104 additions and 68 deletions

View file

@ -53,12 +53,13 @@ type
procedure TestRun_IndexStr_NotFound;
procedure TestRun_IndexText_CaseInsensitive;
{ ReplaceStr / ReplaceText }
procedure TestRun_ReplaceStr_SingleOccurrence;
procedure TestRun_ReplaceStr_MultipleOccurrences;
procedure TestRun_ReplaceText_CaseInsensitive;
procedure TestRun_ReplaceStr_EmptyOld;
procedure TestRun_ReplaceStr_EmptyNew;
{ Replace (first) / ReplaceAll }
procedure TestRun_Replace_FirstOnly;
procedure TestRun_Replace_NotFound;
procedure TestRun_ReplaceAll_SingleOccurrence;
procedure TestRun_ReplaceAll_MultipleOccurrences;
procedure TestRun_ReplaceAll_EmptyOld;
procedure TestRun_ReplaceAll_EmptyNew;
{ DupeString / ReverseString / StuffString }
procedure TestRun_DupeString;
@ -407,65 +408,77 @@ begin
end;
{ ------------------------------------------------------------------ }
{ ReplaceStr / ReplaceText }
{ Replace (first) / ReplaceAll }
{ ------------------------------------------------------------------ }
procedure TE2EStrUtilsTests.TestRun_ReplaceStr_SingleOccurrence;
procedure TE2EStrUtilsTests.TestRun_Replace_FirstOnly;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRunWithRTL(
'''
program P; uses StrUtils;
begin WriteLn(ReplaceStr('hello world', 'world', 'Blaise')) end.
begin WriteLn(Replace('aabbaa', 'aa', 'X')) end.
''', Output, RCode));
AssertEquals('only the first match replaced', 'Xbbaa', Trim(Output));
end;
procedure TE2EStrUtilsTests.TestRun_Replace_NotFound;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRunWithRTL(
'''
program P; uses StrUtils;
begin WriteLn(Replace('hello', 'z', 'X')) end.
''', Output, RCode));
AssertEquals('unchanged when not found', 'hello', Trim(Output));
end;
procedure TE2EStrUtilsTests.TestRun_ReplaceAll_SingleOccurrence;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRunWithRTL(
'''
program P; uses StrUtils;
begin WriteLn(ReplaceAll('hello world', 'world', 'Blaise')) end.
''', Output, RCode));
AssertEquals('hello Blaise', 'hello Blaise', Trim(Output));
end;
procedure TE2EStrUtilsTests.TestRun_ReplaceStr_MultipleOccurrences;
procedure TE2EStrUtilsTests.TestRun_ReplaceAll_MultipleOccurrences;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRunWithRTL(
'''
program P; uses StrUtils;
begin WriteLn(ReplaceStr('aabbaa', 'aa', 'X')) end.
begin WriteLn(ReplaceAll('aabbaa', 'aa', 'X')) end.
''', Output, RCode));
AssertEquals('XbbX', 'XbbX', Trim(Output));
end;
procedure TE2EStrUtilsTests.TestRun_ReplaceText_CaseInsensitive;
procedure TE2EStrUtilsTests.TestRun_ReplaceAll_EmptyOld;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRunWithRTL(
'''
program P; uses StrUtils;
begin WriteLn(ReplaceText('Hello World', 'WORLD', 'Blaise')) end.
''', Output, RCode));
AssertEquals('Hello Blaise', 'Hello Blaise', Trim(Output));
end;
procedure TE2EStrUtilsTests.TestRun_ReplaceStr_EmptyOld;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRunWithRTL(
'''
program P; uses StrUtils;
begin WriteLn(ReplaceStr('hello', '', 'X')) end.
begin WriteLn(ReplaceAll('hello', '', 'X')) end.
''', Output, RCode));
AssertEquals('empty old returns unchanged', 'hello', Trim(Output));
end;
procedure TE2EStrUtilsTests.TestRun_ReplaceStr_EmptyNew;
procedure TE2EStrUtilsTests.TestRun_ReplaceAll_EmptyNew;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRunWithRTL(
'''
program P; uses StrUtils;
begin WriteLn(ReplaceStr('hello', 'l', '')) end.
begin WriteLn(ReplaceAll('hello', 'l', '')) end.
''', Output, RCode));
AssertEquals('heo', 'heo', Trim(Output));
end;

View file

@ -60,10 +60,10 @@ type
procedure TestSemantic_IndexText_OK;
procedure TestSemantic_IndexStr_ReturnsInteger;
{ ReplaceStr / ReplaceText }
procedure TestSemantic_ReplaceStr_OK;
procedure TestSemantic_ReplaceText_OK;
procedure TestSemantic_ReplaceStr_ReturnsString;
{ Replace / ReplaceAll }
procedure TestSemantic_Replace_OK;
procedure TestSemantic_ReplaceAll_OK;
procedure TestSemantic_ReplaceAll_ReturnsString;
{ DupeString / ReverseString / StuffString }
procedure TestSemantic_DupeString_OK;
@ -104,7 +104,7 @@ type
{ Codegen — unit-level function calls appear in IR }
procedure TestCodegen_ContainsStr_InIR;
procedure TestCodegen_ReplaceStr_InIR;
procedure TestCodegen_ReplaceAll_InIR;
procedure TestCodegen_TrimLeft_InIR;
procedure TestCodegen_PosEx_InIR;
procedure TestCodegen_LeftStr_InIR;
@ -542,30 +542,30 @@ begin
end;
{ ------------------------------------------------------------------ }
{ ReplaceStr / ReplaceText }
{ Replace / ReplaceAll }
{ ------------------------------------------------------------------ }
procedure TStrUtilsTests.TestSemantic_ReplaceStr_OK;
procedure TStrUtilsTests.TestSemantic_Replace_OK;
begin
SemanticOK(
'''
program P; uses StrUtils;
var S, T: string;
begin T := ReplaceStr(S, 'x', 'y') end.
begin T := Replace(S, 'x', 'y') end.
''');
end;
procedure TStrUtilsTests.TestSemantic_ReplaceText_OK;
procedure TStrUtilsTests.TestSemantic_ReplaceAll_OK;
begin
SemanticOK(
'''
program P; uses StrUtils;
var S, T: string;
begin T := ReplaceText(S, 'x', 'y') end.
begin T := ReplaceAll(S, 'x', 'y') end.
''');
end;
procedure TStrUtilsTests.TestSemantic_ReplaceStr_ReturnsString;
procedure TStrUtilsTests.TestSemantic_ReplaceAll_ReturnsString;
var
Lexer: TLexer;
Parser: TParser;
@ -580,7 +580,7 @@ begin
Lexer := nil; Parser := nil; Prog := nil; Semantic := nil;
Loader := nil; Units := nil; SearchPaths := nil;
try
Lexer := TLexer.Create('program P; uses StrUtils; var S, T: string; begin T := ReplaceStr(S, ''x'', ''y'') end.');
Lexer := TLexer.Create('program P; uses StrUtils; var S, T: string; begin T := ReplaceAll(S, ''x'', ''y'') end.');
Parser := TParser.Create(Lexer);
Prog := Parser.Parse();
Semantic := TSemanticAnalyser.Create();
@ -593,7 +593,7 @@ begin
Semantic.AnalyseUnitForExport(TUnit(Units.Items[I]));
Semantic.Analyse(Prog);
Assign := TAssignment(Prog.Block.Stmts[0]);
AssertEquals('ReplaceStr returns string',
AssertEquals('ReplaceAll returns string',
Ord(tyString), Ord(Assign.Expr.ResolvedType.Kind));
finally
Semantic.Free();
@ -1042,16 +1042,16 @@ begin
AssertTrue('ContainsStr appears in IR', IRContains(IR, '$StrUtils_ContainsStr'));
end;
procedure TStrUtilsTests.TestCodegen_ReplaceStr_InIR;
procedure TStrUtilsTests.TestCodegen_ReplaceAll_InIR;
var IR: string;
begin
IR := GenIR(
'''
program P; uses StrUtils;
var S, T: string;
begin T := ReplaceStr(S, 'x', 'y') end.
begin T := ReplaceAll(S, 'x', 'y') end.
''');
AssertTrue('ReplaceStr appears in IR', IRContains(IR, '$StrUtils_ReplaceStr'));
AssertTrue('ReplaceAll appears in IR', IRContains(IR, '$StrUtils_ReplaceAll'));
end;
procedure TStrUtilsTests.TestCodegen_TrimLeft_InIR;

View file

@ -246,6 +246,32 @@ replacements. `Extended` maps to `Double` with a precision-loss warning.
*Effort.* Medium — primarily RTL/stdlib work once operator overloading is in place.
No compiler changes needed.
== StdLib — String operations
=== Regular-expression search and replace
`StrUtils` provides literal (non-pattern) replacement only: `Replace` (first
occurrence) and `ReplaceAll` (every occurrence), both case-sensitive. A
pattern-based API — `regex` search, `ReplaceMatches`/`ReplaceFirstMatch`, and
capture-group substitution — is deliberately deferred because it requires a
regular-expression engine (parser + NFA/DFA matcher) that does not yet exist
in the stdlib.
*Why deferred.* A correct, reasonably performant regex engine is a
substantial component in its own right. The literal `Replace`/`ReplaceAll`
functions cover the common cases without it.
*Current workarounds.*
- Case-insensitive replace: lower-case the inputs with the built-in
`LowerCase` (or `UpperCase`) before calling `Replace`/`ReplaceAll`. Note
that this also folds the case of the surrounding text in the result; a
case-*preserving* insensitive replace is a regex-engine feature.
- Fixed multi-pattern replacement: chain `ReplaceAll` calls.
*Effort.* Large — a regex engine is the bulk of the work; the replace API on
top of it is small.
== macOS — Debugging and Code-Signing
=== Debugger entitlement requirements

View file

@ -80,11 +80,15 @@ function IndexText(const Str: string; const Arr: array of string): Integer;
{ Replacement }
{ ------------------------------------------------------------------ }
{ Replaces all occurrences of OldPattern with NewPattern (case-sensitive). }
function ReplaceStr(const S, OldPattern, NewPattern: string): string;
{ Replaces the FIRST occurrence of OldPattern with NewPattern. Returns S
unchanged if OldPattern is empty or not found. Matching is case-sensitive;
for a case-insensitive replace, lower-case the inputs as you pass them in. }
function Replace(const S, OldPattern, NewPattern: string): string;
{ Replaces all occurrences of OldPattern with NewPattern (case-insensitive). }
function ReplaceText(const S, OldPattern, NewPattern: string): string;
{ Replaces ALL occurrences of OldPattern with NewPattern. Matching is
case-sensitive; for a case-insensitive replace, lower-case the inputs as
you pass them in. }
function ReplaceAll(const S, OldPattern, NewPattern: string): string;
{ ------------------------------------------------------------------ }
{ Manipulation }
@ -386,15 +390,15 @@ end;
{ Replacement }
{ ------------------------------------------------------------------ }
{ Internal: replace all occurrences of OldP with NewP; Fold=True for case-insensitive. }
function ReplaceAll(const S, OldP, NewP: string; Fold: Boolean): string;
{ Internal worker for Replace/ReplaceAll. Matching is case-sensitive;
FirstOnly=True stops after the first match (the rest is copied verbatim). }
function DoReplace(const S, OldP, NewP: string; FirstOnly: Boolean): string;
var
SLen, OldLen, NewLen: Integer;
SB: TStringBuilder;
I, J: Integer;
Match: Boolean;
Match, Done: Boolean;
SP, OP: PChar;
C1, C2: Integer;
begin
SLen := Length(S);
OldLen := Length(OldP);
@ -406,29 +410,22 @@ begin
SP := PChar(S);
OP := PChar(OldP);
SB := TStringBuilder.Create();
I := 0;
while I <= SLen - OldLen do
I := 0;
Done := False;
while (not Done) and (I <= SLen - OldLen) do
begin
Match := True;
for J := 0 to OldLen - 1 do
begin
if Fold then
begin
C1 := FoldLower(SP[I + J]);
C2 := FoldLower(OP[J]);
end
else
begin
C1 := SP[I + J];
C2 := OP[J];
end;
if C1 <> C2 then begin Match := False; Break; end;
if SP[I + J] <> OP[J] then begin Match := False; Break; end;
end;
if Match then
begin
if NewLen > 0 then
SB.Append(NewP);
I := I + OldLen;
if FirstOnly then
Done := True;
end
else
begin
@ -446,14 +443,14 @@ begin
SB.Free();
end;
function ReplaceStr(const S, OldPattern, NewPattern: string): string;
function Replace(const S, OldPattern, NewPattern: string): string;
begin
Result := ReplaceAll(S, OldPattern, NewPattern, False);
Result := DoReplace(S, OldPattern, NewPattern, True);
end;
function ReplaceText(const S, OldPattern, NewPattern: string): string;
function ReplaceAll(const S, OldPattern, NewPattern: string): string;
begin
Result := ReplaceAll(S, OldPattern, NewPattern, True);
Result := DoReplace(S, OldPattern, NewPattern, False);
end;
{ ------------------------------------------------------------------ }