g | x | w | all
Bytes Lang Time Link
026Haskell + hgl240724T135452ZWheat Wi
016sed 4.2.2240725T052036ZDigital
043JavaScript ES6240724T081140ZArnauld
082Python 3.8 prerelease240724T095658ZJitse
030Charcoal240725T083936ZNeil
105Setanta240725T071331Zbb94
029Wolfram Language Mathematica240724T225458Zatt
008Nekomata240725T021308Zalephalp
009QuadR ≡240724T085533ZAdá
014Brachylog240724T223135ZUnrelate
018Perl 5 + pl240724T205349ZDom Hast
015Brachylog240724T203657ZDLosc
011Retina 0.8.2240724T173832ZNeil
048R240724T103921Zpajonk
010Japt240724T093257ZShaggy
00805AB1E legacy240724T083148ZKevin Cr

Haskell + hgl, 26 bytes

yyc$sk$h_>~l2 aT(mY<xys)ʃ

Attempt This Online!

This uses the adapted rule from Nitrodon, which has not been proven correct, but at least has not been proven incorrect.

Explanation

This finds occurrences of xyx where y is made up of a subset of the characters of x, and replaces them with x until a fixed point is reached. y can potentially be empty here.

Reflection

This is ok. I'm not too happy with it but I initially expected much worse. This really has more of a glue problem than anything else.

The above are minor fixes, but I think the thing that would really fix the glue problem is having a version of (>~) which just returns the result from the left-hand side instead of the right hand-side. So:

(<~) =
  f' $ fb < ap pM

In which case this could look like:

yyc$sk$h_<~(mY<xys)>~ʃ

Although this wouldn't make this answer shorter than implementing either of the above, this does seem like the solution lightest on the glue and an operation which has a large reuse potential

Old versions

Here are some old versions which use the incorrect algorithm given in the question and currently used by all the other answers:

Parser, 21 13 bytes

yyc$sk$h_>~ʃ

Attempt This Online!

Explanation

No parser, 21 bytes

yyc$mBl<(he~<<gr)<<pt

Attempt This Online!

Explanation

Reflection

Although I only have two versions of this answer I am splitting the reflection into three based on three potential solutions to this challenge.

Parser version

I am happy with the parser version. I was annoyed by the operator precedence between (~<) and (#|)/(++) in the original version of the answer, but I'm not sure it's actually wrong in general, and now it's no longer a problem.

In the interest in coming up with something to improve here:

Regex version

I don't think I could reasonably do this with regex right now, since I haven't implemented back-referencing yet. This is just yet another push to do that.

Non-parser version

I am rather frustrated with trying to build a non-parser version here. There is a lot to improve on this front.

sed 4.2.2, 16

:
s/(.+)\1/\1/
t

Try it online!

JavaScript (ES6), 43 bytes

-2 thanks to @l4m2

f=s=>s==(s=s.replace(/(.+)\1/,"$1"))?s:f(s)

Try it online!

Python 3.8 (pre-release), 82 bytes

lambda s:(n:=len(s))==[s:=s.replace((x:=s[i%n:i%~n])+x,x)for i in range(n**3)]or s

Try it online!

-14 bytes thanks to xnor

Charcoal, 30 bytes

W⌈ΦEθ⌈ΦEλ✂θμλ¹№θײμκ≔⪫⪪θײιιθθ

Try it online! Link is to verbose version of code. Explanation:

W⌈ΦEθ⌈ΦEλ✂θμλ¹№θײμκ

While there are substrings of the input that appear doubled in the input, take the maximum, and...

≔⪫⪪θײιιθ

... deduplicate that substring from the input.

θ

Output the finally reduced string.

Setanta, 105 bytes

gniomh(s){le t idir(0,fad@s)le j idir(1,fad@s+1)le i idir(0,j){t=cuid@s(i,j)s=athchuir@s(t+t,t)}toradh s}

Try on try-setanta.ie

Wolfram Language (Mathematica), 31 29 bytes

f[a___,b__,b__,c___]=f[a,b,c]

Try it online!

Input [characters...]. Return the characters of the reduced string, wrapped in f.

NonCommutativeMultiply (**) is a conveniently Flat undefined operator. Unfortunately, it needs to be Unprotected before we can add a definition. Try it online!

Nekomata, 8 bytes

ʷ{JYᵗƶ¿j

Attempt This Online!

ʷ{JYᵗƶ¿j
ʷ{          Loop until failure:
  J             Nondeterministically split the input into parts
                e.g. "QUAQUAUAL" may become ["QUA", "QUA", "UA", "L"]
   Y            Run length encode
                e.g. ["QUA", "QUA", "UA", "L"] -> ["QUA", "UA", "L"], [2, 1, 1]
    ᵗƶ          Check that at least one count is greater than 1
                e.g. [2, 1, 1] passes
      ¿         If the check passes, drop the counts; otherwise fail
                e.g. ["QUA", "UA", "L"], [2, 1, 1] -> ["QUA", "UA", "L"]
       j        Join
                e.g. ["QUA", "UA", "L"] -> "QUAUAL"

This may output the same result multiple times. You can add the -1 flag to only output the first result.

QuadR , 9 bytes

Blatant rip-off of l4m2's golf of Arnauld's JavaScript solution — go upvote!

(.+)\1
\1

Try it online!

Brachylog, 15 14 bytes

{c~c₂ᶠo∋~jᵗc}ˡ

Try it online!

I wouldn't be surprised if there's a better way to do this.

...But, I mostly just took Jitse's comment about requiring recursion as a challenge.

{           }ˡ    Left reduce by:
 c                Concatenate the new element to the accumulator,
  ~c₂             then partition that result into two possibly-empty slices,
     ᶠo∋          trying the partitions in lexicographic order.
        ~jᵗ       Un-double the last slice,
           c      and concatenate the slices back together.

Perl 5 + -pl, 18 bytes

1while s;(.+)\K\1;

Try it online!

Brachylog, 16 15 bytes

~c₃↺{h&~j}ʰ↻c↰|

Try it online!

Explanation

I wouldn't be surprised if there's a better way to do this.

~c₃↺{h&~j}ʰ↻c↰|
~c₃               "Unconcatenate" the input string into three substrings
   ↺              Rotate the first element to the end
    {    }ʰ       Apply this predicate to the new first element:
     h&             Assert that it is nonempty
       ~j           Assert that it consists of a substring repeated twice,
                    and return that substring
           ↻      Rotate the final element back to the beginning
             c    Concatenate the list back together
              ↰   Call the main predicate again on the result
               |  If there was no way to partition the input string that
                  satisfied the assertions, return the input unchanged

Retina 0.8.2, 11 bytes

+`(.+)\1
$1

Try it online! Link includes test cases. Explanation: Blatant rip-off of @Adám's rip-off of @l4m2's golf of @Arnauld's answer.

R, 48 bytes

\(s){while(s!=(s=sub("(.+)\\1","\\1",s,,T)))0;s}

Attempt This Online!

Japt, 10 bytes

I didn't understand the spec at all so this is based on the worked example & test cases.

e"(.+)%1"Ï

Try it (includes all test cases)

05AB1E (legacy), 8 bytes

Δ.œ€ÔJéн

Try it online or verify all test cases.

Explanation:

Δ         # Loop until the result no longer changes:
 .œ       #  Get all partitions of the current string
          #  (which will use the implicit input-string in the first iteration)
   €      #  Map over each partition:
    Ô     #   Connected uniquify all parts in this partition
     J    #  Join each connected uniquified partition back together
      é   #  Sort these strings by length (shortest to longest)
       н  #  Pop and keep the first/shortest one
          # (after the loop, the result is output implicitly)

Uses the legacy version of 05AB1E instead of the latest version, since the connected uniquify builtin Ô will interpret stringified numbers as numbers instead of strings. E.g. with input 11.0+1+1e1, the legacy program correctly connected uniqifies ["1","1",".0","+1","+1","e1"] to ["1",".0","+1","e1"]"1.0+1e1", whereas the new version of 05AB1E will connected uniquify ["1","1.0","+1","+1e1"] to ["1"]"1", since it interprets them all as the same number (1): try it online.