Home Back Play Pinja Bobbity flop

World's smallest ETA interpreter

Copyright © Stephen Sykes, 2002 - 2007.

Scroll to the bottom for the latest improvements

It's fun to write really small and compact code in the tradition of the annual obfuscated C contest. Perl also lends itself to this kind of compressed code, and there is an ETA interpreter written in this way.

Ruby, however, is not known for this type of thing. But I present here a compressed Ruby interpreter that is rather shorter than the shortest Perl version made so far:

n=[];p=$<.readlines;$==f=l=c=0;loop{p[l].gsub(/(a)|(i)|(n)|(o)|(h)|(e)|(s)|t/){
f>0?$6?n<<c&&c=f=0:c="htaoins".index($&)+c*7: $1?n<<l+2: $2?n<<STDIN.getc: $3?f=
1:(a=n.pop;$4?$><<a.chr: $5?n<<n[-1-a.abs]&&a>0&&n.slice!(-2-a):(b=n.pop;$6?n<<
b/a&&n<<b%a: $7?n<<b-a: b!=0&&(l=a-2;break)))};p[l+=1]&&l>=0||break}

Actually, this started life as a 5-liner, but harsh competition from mtve and the guys at Perl Monks motivated me to do some more work on it. The most significant optimisations were using gsub as the line level iterator, using negative subscripts instead of using the 'size' method on the stack, extensive use of logical operators rather than 'if' or '?:', and use of the $= variable instead of downcase.

It now stands at 306 characters and I'm particularly pleased with it.

So the challenge has been laid - is is possible to build an ETA interpreter in less than this in a mainstream language?

*** [update 21 Feb 2002] Mtve and the guys ar perl monks have come up with a new strategy that beats this Ruby implementation - 290 bytes is the current record. See the evolution described here. I tried a couple of different Ruby implementations, including this one:

n=[];p=$<.readlines;$==f=l=c=i=0;k=%w(2;n<<l+2 4;n<<STDIN.getc 5;f=1
n<<n[-1-a.abs];a>0&&n.slice!(-2-a) 3;$><<a.chr 6;n<<b-a 1;b!=0&&(l=a-1;i=-1)
n<<b/a;n<<b%a);loop{(g="ainhoste".index p[l][i].chr)&&(f<1?(g>2&&a=n.pop;g>4&&b=
n.pop;eval k[g]):g<7?c=c*7+k[g].to_i: n<<c&&f=c=0);p[l][i+=1]||(i=0;l+=1);p[l]&&
l>=0||break}

But this is 318 chars.

*** [update 22 Jan 2003] Nearly a year later, and I returned to the project. It took just a couple of flashes of inspiration, and Ruby now comes in at 277 characters - comfortably beating Perl once more!

n=[];p=$<.to_a;$==f=l=-1;i=%w{n<<n[-1-a.abs];a>0&&n.slice!(-2-a)
b!=0&&(l=a-2;break) n<<l+2 $><<a.chr n<<STDIN.getc f=0 n<<b-a n<<b/a;n<<b%a}
p[l].scan(/\w/){|x|(y='htaoinse'=~x)&&(f>=0?y>6?n<<f&&f=-1:f=f*7+y:
('those'=~x&&a=n.pop;'set'=~x&&b=n.pop;eval i[y]))}while p[l+=1]&&l>=0

So the challenge has once again been laid before the Perl Monks.

*** [update 23 Jan 2003] Mtve responded splendidly with a 270 character perl masterpiece. However, I have made some improvements. I sent to him a 268 char attempt, but subsequently improved again. I am now at 255 characters, by perl golf rules (newlines count as 1 char). I await the response...

$==f=l=-1;q,i,*n=$<.to_a,%w{won<<(w>0?n.slice!(f-w):n[w+f])
gopop!=0&&(l=g-2;break) n<<l+2 go$><<g.chr n<<getc f=0 roton<<t-r
cowon<<w/c<<w%c}
q[l].scan(/\w/){(y='htaoinse'=~$&)&&(f<0?eval(i[y].gsub /o/,'=n.pop;'):f=y>6?n<<f&&-1:f*7+y)}while
q[l+=1]&&l>=0

*** [update 24 Jan 2003] Well, again Mtve came up with the goods and provided a 261, then 254 char version. Here is the 254 (I have added two newlines in the code for readability, they are not required or counted):

#!perl -n0
s#.#+{qw(H 0splice@n,-2-P,$_>!W$n[-1-abs] T 1P%//+P&&goto"L$'" O 3print+chrP I 4Word(getc)||
-1 N 5W$N++ S 6W-P+P E 7W$==P%//+P/$',$_%$' A 2W$L)}->{uc$&}=~//&&"\$N?$&>6?
\$N=0:W$&+7*P:$';"#eg;s'P'($_=pop@n)'g;s'W'push@n,'g;s/^/L${\$.++}:\$L=$.;/mg;eval

However, I made a couple of improvements, and 252 is the best Ruby version now:

$==f=l=-1;q,i,*n=$<.to_a,%w{on<<(b>0?n.slice!(f-b):n[b+f])
oob!=0&&(l=a-2;break) n<<l+2 o$><<b.chr n<<getc f=0 oon<<b-a
oon<<b/a<<b%a}
q[l].scan(/\w/){(b='htaoinse'=~$&)&&(f<0?eval(i[b].gsub /o/,'a=b;b=n.pop;'):f=b>6?n<<f&&-1:f*7+b)}while
q[l+=1]&&l>=0

Does Ruby win? If Mtve improves again, then no I fear. I can't see any areas that I can improve the Ruby version further.

*** [update 27 Jan 2003] That said, I finally figured out how I could factor the recurrent n<< out, and saved three bytes doing it. Also, in-lining the 'i' array saves another four - this was obvious, and I should have done it earlier. Using putc drops me another two, and moving some code around saved one last byte. So I'm now somewhat miraculously well under the 250 barrier at 242. What's more, just for fun, I made it into a valid ETA program (it just waits for a char then exits, but it is valid).

$=,i,*l=n=e=-1,$<.to_a
i[e].scan(/\w/){eval "n<0?l<<(#{%w{,b>0?l.slice!(n-b):l[b+n]
,,b!=0&&(e=c-2;break), e+2 ,putc(b), getc n=0, ,,b-c ,,l<<b/c;b%c
,}[b='htaoinse'=~$&||8].gsub /,/,';c=b;b=l.pop;'}):n=b>6?l<<n&&-1:n*7+b"}while
i[e+=1]&&e>-1

Mtve has sent me a 233 character perl program. He has altered strategy and used much of the algorithm I am using in Ruby. I would be well beaten by this except for the fact that it is not, at present, a valid interpreter because of a problem handling regex special characters in the input.

I am waiting for him to come up with a fix, but until then Ruby is the leader.

*** [update 28 Jan 2003] Stone the crows, just when I thought nothing further could be done to the program to improve it I found a way to chop another seven characters. I also fixed a little bug in the previous version - quite similar to the one Mtve had!

Here is the best yet at 235 bytes:

$=,i,*l=n=e=-1,$<.to_a
i[e].scan(/\w/){(p='htaoinse'=~$&)&&(n<0?l<<eval(%w^,>0?l.slice!(n-p):l[p+n]
,,!=0&&(e=c-2;break), e+2 ,utc(p), getc n=0, ,,-c
,,;l<<p/c;p%c^[p].gsub',',';c,p=p,l.pop;p'):n=p>6?l<<n&&-1:n*7+p)}while
i[e+=1]&&e>-1

*** [update 2 Feb 2003] I studied hard, and found yet another six bytes that were not required. Here is the 229 character masterpiece:

_,i=a=$==-1,$<.to_a;i[_].scan(/\w/){!p='htaoinse'=~$&or
a<0?i<<eval(%w^,>0?i.slice!(a-p):i[p+a] ,,==0||(_=c-2;break), _+2
,utc(p), getc a=0, ,,-c ,,;i<<p/c;p%c^[p].gsub',',';c,p=p,i.pop
p'):a=p>6?i<<a&&-1:a*7+p}while-2<_&&$.>_+=1

This must, now, be the smallest possible – although I've thought that before!

Note, it has been pointed out that the program does not work correctly if you use it like this:

$ ruby tinyeta.rb <something.eta

This is because of a bug in Ruby – it seems to incorrectly count the input lines from the input when using indirection (matz fixed the bug once I pointed it out, but it will take a while to filter through to a release). The solution in the mean time is to use the program like this:

$ ruby tinyeta.rb something.eta

*** [update 5 Feb 2003] I shaved just one more byte. I'm now at 228:

_,i=a=$==-1,$<.to_a;i[_].scan(/\w/){!p='htaoinse'=~$&or
a<0?i<<eval(%w^,>0?i.slice!(a-p):i[p+a] ,,==0||(_=c-2;break), _+2
,utc(p), getc a=0, ,,-c ,,;i<<p/c;p%c^[p].gsub',',';c,p=p,i.pop
p'):a=p>6?i<<a&&~0:a*7+p}until(_+=1)/$.!=0

*** [update 6 Feb 2003] Mtve has come back with an excellent perl script with 231 characters. He has also pointed out that there is a bug in the way I handle getc - after end of input the I instruction should push -1 onto the stack. Fixing this costs three characters, so I'm now at 231:

_,i=a=$==-1,$<.to_a;i[_].scan(/\w/){!p='htaoinse'=~$&or
a<0?i<<eval(%w^,>0?i.slice!(a-p):i[p+a] ,,==0||(_=c-2;break), _+2
,utc(p), getc||a a=0, ,,-c ,,;i<<p/c;p%c^[p].gsub',',';c,p=p,i.pop
p'):a=p>6?i<<a&&~0:a*7+p}until(_+=1)/$.!=0

Here is Mtve's Perl masterpiece (231 chars):

#!perl -nX0
s#.#htaoinse=~/\Q$&/i&&";\$|?\$|=@-<7&&W@-P*7:".qw(splice@n,-2-P,$_>!W$n[-1-abs]
P%//P&&goto+L.$' W$L print+chrP Word+getc||-1 W$|++ W-PP
WP%//P/$',$_%$')[@-]#eg;s'W'push@n,'g;s'P'+($_=$==pop@n)'g;s/^/;L${\$.++}:\$L=$./mg;eval

*** [update March 2003] It seems that this is the limit, and we are both too exhausted to work on this any more.

So it's officially a tie!

One final note - my program works in recent versions of Ruby, but terse features I am using are becoming deprecated as Ruby moves away from some of its Perlish origins. So there are more and more warning messages, and with Ruby 1.8 these are very intrusive. I advise running with warnings turned off (-W0).

Or is it?

*** [update 1 Apr 2004] A miracle happened - over a year after my last work on this, I just noticed a little improvement - I'm now at 227. Let's see if Perl/Mtve can improve also.

i=*$<;_=a=$==-1;i[_].scan(/\w/){!p='htaoinse'=~$&or
a<0?i<<eval(%w^,>0?i.slice!(a-p):i[p+a] ,,==0||(_=c-2;break), _+2
,utc(p), getc||a a=0, ,,-c ,,;i<<p/c;p%c^[p].gsub',',';c,p=p,i.pop
p'):a=p>6?i<<a&&~0:a*7+p}until(_+=1)/$.!=0

Improvements in 2007

*** [update 21 Sep 2007] More improvements following correspondence with Darren of darrenks.com

I received a series of emails, ending with the following excellent solution (and I thank Darren very much for the insights and time he took to produce this):

i=[*$<]<<a=!_=1
scan(/\w/){a ?a=Q>6?p&i<<a:Q+a*7:i<<eval(%w{,>0?i.slice!(~p):i[p-1]
,,==0||(_=c;break),
_
,utc~~,
getc||-1
a=0,
,,-c
,,;i<<p/c;p%c}[Q].gsub',','
c=p;p=i.pop
p')if Q='htaoinse'=~/#$&/i}while$_=i[-2+_+=1]

This measures at 218 chars, an amazing improvement of 9. Also, one problem was fixed - Ruby disallows having a string on the right hand side of =~ (since 1.8.something), so the program works again now. Wonderful achievement Darren, thanks.

One thing to note is that the warnings with this version are many. -W0 is definitely needed (as it was with my previous versions also).

So while I was thinking about this, and with renewed energy, I had a good look at the code. I found some tiny improvements of my own. I made two versions - one that will not generate any warnings, and one that generates many warnings.

Firstly the clean version:

i=p,*$<;_,a,*s=1;scan(/\w/){q=/#$&/i=~'htaoinse'and
s<<eval(%w{,>0?(s.slice!~p):s[p-1] ,,==0||(break~_=c), _ ,utc~~,
STDIN.getc||-1 a=0 ,,-c ,,;s<<p/c;p%c ,*7+q a=,}[a ?8+q/7:q].gsub(',','
c=p;p=s.pop;p'))}while$_=i[-1+_+=1]
This comes in at 224 chars. Notice I needed to add STDIN. before getc, and parens around the arguments to gsub. These alone account for 8 chars. So I think 224 is a good result given these constraints.

Next to see is the unclean, warnings-but-we-don't-care version:

i=p,*$<;_,a=1;scan(/\w/){i<<eval(%w{,>0?(i.slice!~p):i[p-1]
,,==0||(break~_=c), _ ,utc~~,
getc||-1 a=0 ,,-c ,,;i<<p/c;p%c ,*7+Q a=,}[a ?8+Q/7:Q].gsub',','
c=p;p=i.pop;p')if Q=/#$&/i=~'htaoinse'}while$_=i[-1+_+=1]

This is an incredible 212 chars. It actually generates an error if you exit by falling off the end of the ETA program, and there happens to be items left on the stack. Well, you should exit by transferring to zero of course.

As always, I don't think there are any more improvements, but I've been wrong so many times before. Write to me if you find one!

*** [update 22 Sep 2007] One byte more from Darren

"Your changes look good, I see one minor improvement: ~-_+=1 instead
of -1+_+=1 (don't you wish ruby just had the post increment ++ ?)"

What a nice use of the complement operator. And right, those increment and decrement operators would be very useful for us. Here are the two programs with the improvement:

Clean

i=p,*$<;_,a,*s=1;scan(/\w/){q=/#$&/i=~'htaoinse'and
s<<eval(%w{,>0?(s.slice!~p):s[p-1] ,,==0||(break~_=c), _ ,utc~~,
STDIN.getc||-1 a=0 ,,-c ,,;s<<p/c;p%c ,*7+q a=,}[a ?8+q/7:q].gsub(',','
c=p;p=s.pop;p'))}while$_=i[~-_+=1]

[223 chars]

With warnings

i=p,*$<;_,a=1;scan(/\w/){i<<eval(%w{,>0?(i.slice!~p):i[p-1]
,,==0||(break~_=c), _ ,utc~~,
getc||-1 a=0 ,,-c ,,;i<<p/c;p%c ,*7+Q a=,}[a ?8+Q/7:Q].gsub',','
c=p;p=i.pop;p')if Q=/#$&/i=~'htaoinse'}while$_=i[~-_+=1]

[211 chars]

If you are interested in obfuscated Ruby, then this is a good page - there is a nice maze generator in 10 lines (you need to know it takes two arguments - width and height), and also a BrainF*ck implementation.


S.D.Sykes Sep 2007