Interpret /// (pronounced 'slashes')

31

4

Because we can't get enough of esoteric language golfs, can we?

///—pronounced slashes—is a fun little language based on the s/// regex-replacement function of Perl fame. It contains only two special characters, slash / and backslash \. You can find a full article on it at the esolangs wiki, but I will reproduce a description of the language below, as well as some examples.

In short, it works by identifying /pattern/repl/rest in the program and making the substitution as many times as possible. No characters are special except / and \: / demarcates patterns and replacements in the program, while \ allows you to insert literal / or \ characters into your code. Notably, these are not regular expressions, just plain string substitutions.

Your challenge is to produce an interpreter for the /// language, as either a program reading STDIN or a function taking a string argument, in as few characters as possible.

You may use any language except for /// itself. You may not use any libraries that interpret ///; you may, however, use regexes, regex libraries, or string-matching libraries.


Execution

There are four states, print, pattern, replacement, and substitution. In every state except substitution:

  • If the program is empty, execution halts.
  • Else, if the first character is \, do something with the next character (if present) and remove both from the program.
  • Else, if the first character is /, remove it, and change to the next state.
  • Else, do something with the first character and remove it from the program.
  • Repeat.

The states cycle through print, pattern, replacement, and substitution in order.

  • In print mode, 'do something' means output the character.
  • In pattern mode, 'do something' means add the character to the current Pattern.
  • In replacement mode, 'do something' means add the character to the current Replacement.

In substitution mode, you follow a different set of rules. Repeatedly substitute the first occurrence of the current Pattern with the current Replacement in the program, until no more substitutions are possible. At that point, clear the Pattern and Replacement and return to print mode.

In the program /foo/foobar/foo foo foo, the following happens:

/foo/foobar/foo foo foo
foo foo foo
foobar foo foo
foobarbar foo foo
foobarbarbar foo foo
...

This loops forever and never exits substitution mode. Similarly, if the Pattern is empty, then the first occurrence of the empty string—at the beginning of the program—always matches, so substitution mode loops forever, never halting.


Examples

no

Output: no.

/ world! world!/Hello,/ world! world! world!

Output: Hello, world!.

/foo/Hello, world!//B\/\\R/foo/B/\R

Output: Hello, world!.

a/ab/bbaa/abb

Output: a. Program does not halt.

//

Output: none.

///

Output: none. Program does not halt.

/\\/good/\/

Output: good.

There is also a quine on the wiki you can try.

algorithmshark

Posted 2014-08-29T00:10:16.413

Reputation: 8 144

@Loovjo The \ character escapes any character that follows it, including /, which can later be used as normal. While this doesn't look like much, this makes /// Turing-complete.

– algorithmshark – 2015-07-28T19:37:31.503

I think this is a better explanation of the language than the esolangs wiki article. Will use this info in my /// IDE that I'm making! – clabe45 – 2017-10-14T03:01:37.233

/-/World//--/Hello//--W/--, w/---! What's not to love? (Try removing dashes from the end) – seequ – 2014-08-31T12:06:37.853

Answers

7

APL (133)

{T←''∘{(0=≢⍵)∨'/'=⊃⍵:(⊂⍺),⊂⍵⋄(⍺,N⌷⍵)∇⍵↓⍨N←1+'\'=⊃⍵}⋄⍞N←T⍵⋄p N←T 1↓N⋄r N←T 1↓N⋄''≡N:→⋄∇{⍵≡p:∇r⋄∨/Z←p⍷⍵:∇(r,⍵↓⍨N+≢p),⍨⍵↑⍨N←1-⍨Z⍳1⋄⍵}1↓N}

This is a function that takes the /// code as its right argument.

Ungolfed, with explanation:

slashes←{
   ⍝ a function to split the input string into 'current' and 'next' parts,
   ⍝ and unescape the 'current' bit
   split←''∘{
       ⍝ if the string is empty, or '/' is reached,
       ⍝ return both strings (⍺=accumulator ⍵=unprocessed)
       (0=≢⍵)∨'/'=⊃⍵:(⊂⍺),⊂⍵
       ⍝ otherwise, add current character to accumulator,
       ⍝ skipping over '\'s. (so if '\/' is reached, it skips '\',
       ⍝ adds '/' and then processes the character *after* that.)
       idx←1+'\'=⊃⍵
       (⍺,idx⌷⍵)∇idx↓⍵
   }

   ⍞   next ← split ⍵      ⍝ output stage
   pat next ← split 1↓next ⍝ pattern stage, and eat the '/'
   rpl next ← split 1↓next ⍝ replacement stage, and eat the '/'

   ⍝ if there are no characters left, halt.
   ''≡next:⍬

   ⍝ otherwise, replace and continue.
   ∇{  ⍝ if the input string equals the pattern, return the replacement and loop
       ⍵≡pat:∇rpl

       ⍝ otherwise, find occurences, if there are, replace the first and loop
       ∨/occ←pat⍷⍵:∇(rpl, (idx+≢pat)↓⍵),⍨ (idx←(occ⍳1)-1)↑⍵

       ⍝ if no occurences, return string
       ⍵

   }1↓next
}

marinus

Posted 2014-08-29T00:10:16.413

Reputation: 30 224

"if there are no characters left, halt." Does this work correctly on /// and //foo/ (i.e. loops forever)? – algorithmshark – 2014-09-07T16:14:26.220

@algorithmshark: yes, in that situation the / would still be left at that point. – marinus – 2014-09-07T17:36:45.720

11

J - 181 190 170 char

This was a nightmare. I rewrote it from scratch, twice, because it just kept bugging me. This is a function taking a single string argument, outputting to STDOUT.

(0&$`((2{.{:@>&.>)((j{.]),-i@=`p@.~:~/@[,]}.~#@p+j=.0{p I.@E.])i 5;@}.&,'/';"0;&.>)@.(2<#)@}.[4:1!:2~{:@>@p=.>@{.@[)@((0;(0,:~1 0,.2);'\';&<1 0)<;._1@;:'/'&,)i=. ::](^:_)

To explain, I will break it up into subexpressions.

i =. ::](^:_))
parse =: ((0;(0,:~1 0,.2);'\';&<1 0)<;._1@;:'/'&,)
print =: 4:1!:2~{:@>@p=.>@{.@[
eval  =: 0&$`((2{.{:@>&.>)sub 5;@}.&,'/';"0;&.>)@.(2<#)@}.
sub   =: ((j{.]),-i@=`p@.~:~/@[,]}.~#@p+j=.0{p I.@E.])i

interp =: (eval [ print) @ parse i
  • i (short for iterate) is an adverb. It takes a verb argument on the left and returns a verb (f)i, which when applied to an argument, applies f repeatedly to the argument until one of two things happens: it finds a fixed point (y = f y), or it throws an error. The fixed-point behaviour is inherent to ^:_, and ::] does the error handling.

  • parse tokenizes the input into what I call half-parsed form, and then cuts it up at the unescaped '/'. It binds escaping backslashes to their characters, but doesn't get rid of the backslashes—so we can either revert it or finish it depending on which we want.

    The bulk of the interesting work occurs in ;:. This is a sequential-machine interpreter primitive, taking a description of the machine ((0;(0,:~1 0,.2);'\';&<1 0)) on the left and something to parse on the right. This does the tokenizing. I will note that this specific machine actually treats the first character unspecial, even if it's a \ and should bind. I do this for a few reasons: (1) the state table is simpler, so it can be golfed further; (2) we can easily just add a dummy character to the front to dodge the problem; and (3) that dummy-character gets half-parsed at no extra cost, so I can use it to set up for the cutting phase, next.

    We also use <;._1 to cut the tokenized result on unescaped / (which is what I choose to be the first char). This is handy for pulling out the output, pattern, and replacement from out/patt/repl/rest all in one step, but unfortunately also cuts up the rest of the program, where we need those / to stay untouched. I splice these back in during eval, because making <;._1 leave them alone ends up costing a lot more.

  • The fork (eval [ print) executes print on the result from parse for its side-effects, and then runs eval. print is a simple verb that opens up the first box (the one we know for sure is output), finishes parsing it, and sends it to STDOUT. However, we also take the chance to define a utility verb p.

    p is defined as >@{.@[, so it takes its left arg (acts like the identity if given only one arg), takes the first item of that (identity when given a scalar), and unboxes it (identity if already unboxed). This will come in very handy in sub.

  • eval evaluates the remainder of the processed program. If we don't have a full pattern or a full replacement, eval throws it out and just returns an empty list, which terminates evaluation by making ;: (from parse) error out on the next iteration. Else, eval fully parses the pattern and replacement, corrects the remainder of the source, and then passes both to sub. By explosion:

                                                  @}.  NB. throw out printed part
                                           @.(2<#)     NB. if we have a pattern and repl:
          2{.                                          NB.  take the first two cuts:
                 &.>                                   NB.   in each cut:
             {:@>                                      NB.    drop escaping \ from chars
         (          )                                  NB.  (these are pattern and repl)
                                       &.>             NB.  in each cut:
                                      ;                NB.   revert to source form
                                '/';"0                 NB.  attach a / to each cut
                              &,                       NB.  linearize (/ before each cut)
                         5  }.                         NB.  drop '/pattern/repl/'
                          ;@                           NB.  splice together
        (            sub                  )            NB.  feed these into sub
       `                                               NB. else:
    0&$                                                NB.  truncate to an empty list
    
  • sub is where one (possibly infinite) round of substitutions happens. Because of the way we set up eval, the source is the right argument, and the pattern and replacement are bundled together in the left. Since the arguments are ordered like this and we know the pattern and replacement don't change within a round of substitutions, we can use another feature of i—the fact that it modifies only the right argument and keeps passing in the same left—to delegate to J the need to worry about keeping track of the state.

    There are two spots of trouble, though. The first is that J verbs can have at most two arguments, so we don't have an easy way to access any that are bundled together, like pattern and replacement, here. Through clever use of the p utility we defined, this isn't that big of a problem. In fact, we can access the pattern in one character, just by using p, because of its >@{.@[ definition: the Unbox of the First item of the Left arg. Getting the replacement is tricker, but the shortest way would be p&|., 2 chars shorter than manually getting it out.

    The second problem is that i exits on fixed points instead of looping forever, and if the pattern and replacement are equal and you make a substitution, that looks like a fixed point to J. We handle this by entering an infinite loop of negating 1 over and over if we detect they are equal: this is the -i@=`p@.~:~/ portion, replacing p&|..

                                        p    E.]    NB. string search, patt in src
                                          I.@       NB. indices of matches
                                      0{            NB. take the first (error if none)
                                   j=.              NB. assign to j for later use
                               #@p+                 NB. add length of pattern
                           ]}.~                     NB. drop that many chars from src
                       /@[                          NB. between patt and repl:
                      ~                             NB.  patt as right arg, repl as left
                  @.~:                              NB.  if equal:
            -i@=                                    NB.   loop forever
                `p                                  NB.  else: return repl
     (j{.])                                         NB. first j chars of src
           ,              ,                         NB. append all together
    (                                           )i  NB. iterate
    
  • This cycle repeats due to the use of i, until something outside of sub errors out. As far as I'm aware, this can only happen when we are out of characters, of when we throw out an incomplete set of pattern-and-replacement.

Fun facts about this golf:

  • For once, using ;: is shorter than manually iterating through the string.
  • 0{ should have a chance to error out before sub goes into an infinite loop, so this it should work fine if the pattern matches the replacement but never shows up in the remainder of the source. However, this may or may not be unspecified behaviour, since I can't find a citation either way in the docs. Whoopsie.
  • Keyboard interrupts are processed as spontaneous errors inside running functions. However, due to the nature of i, those errors get trapped too. Depending on when you hit Ctrl+C, you might:
    • Exit out of the negate-forever loop, error out of the sub loop by trying to concatenate a number to a string, and then go on interpreting /// as if you finished substituting a string with itself an infinite number of times.
    • Leave sub halfway through and go on interpreting a half-subbed /// expression.
    • Break out of the interpreter and return an unevaluated /// program to the REPL (not STDOUT, though).

Example usage:

   f=:(0&$`((2{.{:@>&.>)((j{.]),-i@=`p@.~:~/@[,]}.~#@p+j=.0{p I.@E.])i 5;@}.&,'/';"0;&.>)@.(2<#)@}.[4:1!:2~{:@>@p=.>@{.@[)@((0;(0,:~1 0,.2);'\';&<1 0)<;._1@;:'/'&,)i=. ::](^:_)
   f 'no'
no
   f '/ world! world!/Hello,/ world! world! world!'
Hello, world!
   f '/foo/Hello, world!//B\/\\R/foo/B/\R'
Hello, world!
   f '//'  NB. empty string

   f '/\\/good/\/'
good

algorithmshark

Posted 2014-08-29T00:10:16.413

Reputation: 8 144

When I run this, I get the empty string from every test case. I'm using jqt64, what are you using to run this? – bcsb1001 – 2015-04-09T16:04:59.200

@bcsb1001 I've been using the (64-bit) jconsole binary directly. Checking jqt now, I am getting actually getting intended results except for the /\\/good/\/ test case; debugging tells me the issue is my use of 1!:2&4, since jqt does not have stdin/out. Will investigate. What are your 9!:12'' and 9!:14''? – algorithmshark – 2015-04-09T18:24:59.697

@algorithmshark My 9!:12'' is 6, and 9!:14'' is j701/2011-01-10/11:25. – bcsb1001 – 2015-04-09T18:43:40.323

Wow. I would call this masochistic. +1 – seequ – 2014-09-03T10:17:37.390

4

Perl - 190

$|=1;$/=undef;$_=<>;while($_){($d,$_)=/(.)(.*)/;eval(!$e&&({'/','$a++','\\','$e=1'}->{$d})||('print$d','$b.=$d','$c.=$d')[$a].';$e=0');if($a==3){while($b?s/\Q$b/$c/:s/^/$c/){}$a=0;$b=$c=''}}

Reads /// program from stdin until EOF.

faubi

Posted 2014-08-29T00:10:16.413

Reputation: 2 599

I believe this fails with /a/\0/a – Asone Tuhid – 2018-03-09T12:53:34.797

Would an approach along the lines of m/^(.*?)(?<!\\)\/(.*?)(?<!\\)\/(.*?)(?<!\\)\/(.*)$/s--i.e. match output, pattern, and replacement all at once--make for a shorter golf? I don't know any Perl, myself. – algorithmshark – 2014-09-03T17:28:23.930

3

C 393

Edits:

  • Thanks to @algorithmshark for lots of help making the original shorter
  • Thanks to @ceilingcat for some very nice pieces of golfing - now even shorter

Here is the code:

#import<bits/stdc++.h>
#define M(x)bzero(x,99);
#define P o[i])
#define N(x)P;else if(n<x)(P==92?
#define O (o[++i]):(P==47?n++:
using S=std::string;char p[99],*q=p,r[99],*s=r;int i,t;main(int n,char**m){for(S o=m[1];i<=o.size();++i){if(!N(3)putchar O putchar(N(4)*q++=O(*q++=N(5)*s++=O(*s++=P;if(n>4){for(;(t=o.find(p,i+1))-S::npos;)o=o.substr(0,t)+r+o.substr(t+strlen(p));M(q=p)M(s=r)n=2;}}}

Try it online!

Jerry Jeremiah

Posted 2014-08-29T00:10:16.413

Reputation: 1 217

I know this is about four years old, but rewriting N(x) as P;else if(n<x)(P==92? and changing calls to N accordingly could save a few bytes. – Zacharý – 2018-05-28T21:37:32.900

@Zacharý That's awesome. Thank you very much. – Jerry Jeremiah – 2018-05-29T05:51:16.260

1Can you rewrite if(!o[i]); as if(P to save chars, or am I misunderstanding how #define works? – algorithmshark – 2014-09-24T06:45:08.693

@algorithmshark how did I miss that?! if(!P is perfect. I'll change it. – Jerry Jeremiah – 2014-09-24T10:17:18.033

Every instance of P in main has a space after it, so you can save a character by replacing those spaces with semicolons and removing it from #define. Then, if you can use #defines inside other ones, you can save some more by rewriting N(x) as (92==P instead of o[i]==92 and O likewise. – algorithmshark – 2014-09-26T05:44:23.737

@algorithmshark you are obviously much better at this than I. Thanks for the help. – Jerry Jeremiah – 2014-09-26T10:40:43.703

3

Pip, 100 102 bytes

I hadn't ever proven Pip to be Turing-complete (though it's pretty obviously so), and instead of going the usual route of BF I thought /// would be interesting. Once I had the solution, I figured I'd golf it and post it here.

101 bytes of code, +1 for -r flag:

i:gJnf:{a:xW#i&'/NE YPOia.:yQ'\?POiya}W#iI'\Q YPOiOPOiEIyQ'/{p:VfY0s:VfIyQ'/WpNi&YviR:Xp{++y?ps}}E Oy

Here's my ungolfed version with copious comments:

; Use the -r flag to read the /// program from stdin
; Stdin is read into g as a list of lines; join them on newline and assign to c for code
c : gJn

; Loop while c is nonempty
W #c {
 ; Pop the first character of c and yank into y
 Y POc
 ; If y equals "\"
 I yQ'\
  ; Pop c again and output
  O POc
 ; Else if y equals "/"
 EI yQ'/ {
  ; Build up pattern p from empty string
  p : ""
  ; Pop c, yank into y, loop while that is not equal to "/" and c is nonempty
  W #c & '/ NE Y POc {
   ; If y equals "\"
   I yQ'\
    ; Pop c again and add that character to p
    p .: POc
   ; Else, add y to p
   E p .: y
  }

  ; Yank 0 so we can reliably tell whether the /// construct was completed or not
  Y0
  ; Build up substitution s from empty string
  s : ""
  ; Pop c, yank into y, loop while that is not equal to "/" and c is nonempty
  W #c & '/ NE Y POc {
   ; If y equals "\"
   I yQ'\
    ; Pop c again and add that character to s
    s .: POc
   ; Else, add y to s
   E s .: y
  }

  ; If the last value yanked was "/", then we have a complete substitution
  ; If not, the code must have run out; skip this branch, and then the outer loop
  ; will terminate
  I yQ'/ {
   ; While pattern is found in code:
   W pNc {
    ; Set flag so only one replacement gets done
    i : 0
    ; Convert p to a regex; replace it using a callback function: if ++i is 1,
    ; replace with s; otherwise, leave unchanged
    c R: Xp {++i=1 ? s p}
   }
  }
 }
 ; Else, output y
 E Oy
}

Try it online! (Note that TIO doesn't give any output when the program is non-terminating, and it also has a time limit. For larger examples and infinite loops, running Pip from the command line is recommended.)

DLosc

Posted 2014-08-29T00:10:16.413

Reputation: 21 213

I think this should be pip + -r, 101 bytes – Asone Tuhid – 2018-03-09T13:31:16.197

2

Ruby, 119 110 bytes

Terminates with exception

r=->s,o=$>{s[k=s[0]]='';k==?/?o==$>?s.gsub!([r[s,''],e=r[s,'']][0]){e}:t=o:o<<(k==?\\?s[0]+s[0]='':k);t||redo}

Try it online!

Terminates cleanly (116 bytes)

r=->s,o=$>{s[k=s[0]||exit]='';k==?/?o==$>?s.gsub!([r[s,''],e=r[s,'']][0]){e}:t=o:o<<(k==?\\?s[0]+s[0]='':k);t||redo}

Try it online!

Asone Tuhid

Posted 2014-08-29T00:10:16.413

Reputation: 1 944

2

Python 2 (236), Python 3 (198?)

from __future__ import print_function
def d(i):
 t=0;p=['']*3+[1]
 while i:
  if'/'==i[0]:t+=1
  else:
   if'\\'==i[0]:i=i[1:]
   p[t]+=i[0]
  i=i[1:]
  print(end=p[0]);p[0]=''
  if t>2:
   while p[1]in i:i=i.replace(*p[1:])
   d(i);i=0

Called as d(r"""/foo/Hello, world!//B\/\\R/foo/B/\R"""). The triple quotes are only needed if the /// program contains newlines: otherwise simple quotes are ok.

EDIT: This interpreter now prints stuff as expected (previously it only printed at the very end, cf. comments). For Python 3, remove the first line (but I don't have Python 3 on my ancient install, so cannot be sure there is no other change).

Bruno Le Floch

Posted 2014-08-29T00:10:16.413

Reputation: 1 181

the interpreter not printing anything until termination is problematic. writing an infinite loop in /// is possible, so your interpreter fails on non-terminating-but-still-printing-something programs. – proud haskeller – 2014-08-30T22:31:23.213

@proudhaskeller Fixed. – Bruno Le Floch – 2014-08-31T09:46:21.370

Actually, this isn't fixed, it doesn't print anything for /a/ab/bbaa/abb. – Beta Decay – 2014-09-02T06:57:48.187

@BetaDecay /a/ab/bbaa/abb will get stuck in an endless loop without printing anything, because the first substitution is a=>ab. The correct a/ab/bbaa/abb works as advertised. – algorithmshark – 2014-09-02T07:59:52.950

@BetaDecay: besides the change suggested by algorithmshark, you may need to include the command line option -u to force the output buffer to be unbuffered. – Bruno Le Floch – 2014-09-02T15:02:31.763

2

Cobra - 226

sig Z as String
def f(l='')
    m=Z(do=[l[:1],l=l[1:]][0])
    n as Z=do
        if'/'<>(a=m())>'',return if(a=='\\',m(),a)+n()
        else,return''
    print n()stop
    p,s=n(),n()
    if''<l
        while p in l,l=l[:l.indexOf(p)+1]+s+l[p.length:]
        .f(l)

Οurous

Posted 2014-08-29T00:10:16.413

Reputation: 7 916

1

Python 2/3 (211 bytes)

The following code, based on Bruno Le Floch's answer, is Python 2 and Python 3 compatible.

Moreover, being iterative rather than recursive it does not risk hitting the maximum recursion depth of Python.

def S(c):
 while c:
  B=["","",1]
  for m in 0,1,2:
   while c:
    if"/"==c[0]:c=c[1:];break
    if"\\"==c[0]:c=c[1:]
    if m:B[m-1]+=c[0]
    else:yield c[0]
    c=c[1:]
  while c and B[0]in c:c=c.replace(*B)

Carlos Luna

Posted 2014-08-29T00:10:16.413

Reputation: 11

Hello and welcome to PPCG. You can golf in(0,1,2) to in 0,1,2 and [""]*2+[1] to ["","",1], resulting in 211 bytes.

– Jonathan Frech – 2019-09-19T20:07:36.697

I have linked to the referred answer and added the word "bytes". If you disagree with my edit, feel free to roll back. – Jonathan Frech – 2019-09-19T20:08:02.173

Thanks Jonathan, your suggestions are very welcome! – Carlos Luna – 2019-09-21T07:28:24.747

0

BaCon, 391 387 395 bytes

From the contributions on this page I only got the Python program to work. The others work for some /// samples, or do not work at all. Therefore, I decided to add my version, which is an implementation in BASIC.

To compete in a CodeGolf contest with BASIC is not easy, as BASIC uses long words as statements. The only abbreviation commonly found in BASIC is the '?' sign, which means PRINT.

So the below program may never win, but at least it works with all demonstration code on this Codegolf page and on the Esolangs Wiki. Including all versions of the "99 bottles of beer".

p$=""
r$=""
INPUT i$
WHILE LEN(i$)
t$=LEFT$(i$,1)
i$=MID$(i$,2)
IF NOT(e) THEN
IF t$="\\" THEN
e=1
CONTINUE
ELIF t$="/" THEN
o=IIF(o<2,o+1,0)
IF o>0 THEN CONTINUE
FI
FI
IF o=1 THEN
p$=p$&t$
ELIF o=2 THEN
r$=r$&t$
ELIF o=0 THEN
IF LEN(p$) THEN i$=REPLACE$(i$,p$,r$)
IF NOT(INSTR(t$&i$,"/")) THEN
?t$;
BREAK
ELSE
?LEFT$(i$,INSTR(i$,"/")-1);
i$=MID$(i$,INSTR(i$,"/"))
FI
p$=""
r$=""
FI
e=0
WEND
?i$

Peter

Posted 2014-08-29T00:10:16.413

Reputation: 119

Added INPUT statement to get input from user. – Peter – 2016-10-27T06:30:15.707