From 2f54707a6327b5009a07a5e8f5d97de324ecf4e8 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Tue, 16 Jun 2026 23:49:30 +0100 Subject: [PATCH] =?UTF-8?q?feat(strutils):=20clean=20literal=20replace=20A?= =?UTF-8?q?PI=20=E2=80=94=20Replace=20(first)=20+=20ReplaceAll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../src/test/pascal/cp.test.e2e.strutils.pas | 67 +++++++++++-------- compiler/src/test/pascal/cp.test.strutils.pas | 32 ++++----- docs/future-improvements.adoc | 26 +++++++ stdlib/src/main/pascal/strutils.pas | 47 ++++++------- 4 files changed, 104 insertions(+), 68 deletions(-) diff --git a/compiler/src/test/pascal/cp.test.e2e.strutils.pas b/compiler/src/test/pascal/cp.test.e2e.strutils.pas index 132f2d9..0eb3bc0 100644 --- a/compiler/src/test/pascal/cp.test.e2e.strutils.pas +++ b/compiler/src/test/pascal/cp.test.e2e.strutils.pas @@ -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; diff --git a/compiler/src/test/pascal/cp.test.strutils.pas b/compiler/src/test/pascal/cp.test.strutils.pas index 2048937..03760c8 100644 --- a/compiler/src/test/pascal/cp.test.strutils.pas +++ b/compiler/src/test/pascal/cp.test.strutils.pas @@ -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; diff --git a/docs/future-improvements.adoc b/docs/future-improvements.adoc index 8c01741..cecb836 100644 --- a/docs/future-improvements.adoc +++ b/docs/future-improvements.adoc @@ -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 diff --git a/stdlib/src/main/pascal/strutils.pas b/stdlib/src/main/pascal/strutils.pas index dfb067f..05dfb18 100644 --- a/stdlib/src/main/pascal/strutils.pas +++ b/stdlib/src/main/pascal/strutils.pas @@ -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; { ------------------------------------------------------------------ }