创造最短的图灵完备解释器

30

我刚尝试着创建最小的语言解释器,你想加入并尝试吗?

游戏规则:

  • 您应指定要解释的编程语言。如果这是您发明的语言,则应在注释中列出命令列表。
  • 您的代码应以分配给代码和数据变量的示例程序和数据开始。
  • 您的代码应以结果输出结束。最好每个中间步骤都有调试语句。
  • 您的代码应按原样运行。
  • 您可以假设数据为0和1(整数、字符串或布尔值,由您选择),而输出是单个位。
  • 该语言应在图灵完备的意义上运行,即对于任何在标准模型上编写的算法,例如图灵机、马尔可夫链或您选择的其他算法,都可以合理地显式或解释如何编写一个程序,在您的解释器执行后执行该算法。
  • 代码长度定义为删除输入部分、输出部分、调试语句和不必要空格后的代码长度。请将结果代码及其长度添加到帖子中。
  • 您不能使用使编译器为您执行代码的函数,例如eval()exec()或类似函数。

这是一个社区维基,意味着问题和答案都不会因投票而获得声望分数。但请投票!


1
抱歉要回复,但是“golf”标签比“code-golf”更广泛使用。 - ilya n.
现在没有代码高尔夫问题了。我只是合并了标签。 - Ólafur Waage
顺便问一下,什么是“图灵沼泽”? - ilya n.
1
@ilya:http://en.wikipedia.org/wiki/Turing_tarpit。这也是我新写的一个。 - dmckee --- ex-moderator kitten
这可能更适合于http://codegolf.stackexchange.com/。 - phuclv
显示剩余2条评论
11个回答

15

Python, 250字节。

这个比ilya n.的Python解决方案长一些,但语言更容易掌握。这种语言中的每个命令(我称其为 Kaputt,德语单词“broken”)都是一个字节。定义了以下6个命令:

  • 0: 将零压入堆栈
  • 1: 将一压入堆栈
  • I: (if) 弹出堆栈上的值(必须为零或一)。如果是一,则运行以下代码块(直到"i");如果是零,则跳过该块。
  • i: (endif) 结束 if 块,并在块未被运行时(因为 "I" 弹出了零)将一推送到堆栈。有关后者的说明,请参见下文。
  • D: (def) 从堆栈中弹出要定义函数的名称(请参见下文),并绑定以下块(直到 "d")以使用该名称。
  • d: (enddef) 结束函数定义。
  • 任何其他字节: 检查是否存在此(一个字节; 但请参见下面的编辑)名称的函数。如果是,则运行此函数;如果不是,则将此字节推送到堆栈上以供 D 立即使用。

嵌套的 ifs 是合法的; 嵌套的函数定义不可行。i (endif) 仅在相应的 if 块未运行时推送一,这使得以下习惯用语类似于一个 if/else/endif 结构:

# [code that left a one or zero on the stack]
I    # abbreviated "(" below
# [code to be run if it was a one]
0iI  # abbreviated "/" below
# [code to be run if it was a zero]
1iIi # abbreviated ")" below
# [continuing...]

请注意,注释、换行、空格等实际上是不被允许的。

以下是一些常见函数的示例。它们使用了前面提到的缩写符号( / )

<D(/)d

定义了函数<(弹出),该函数从栈中弹出一个值,但不对其进行任何操作。

&D((1/0)/<0)d

定义了函数&(与),它从堆栈中弹出两个值,并在这两个值都为1时推入1,在其他情况下推入0。

~D((11/10)/(01/00))d

该函数定义了~(交换)函数,用于交换栈顶的两个值。

RD(R/<)d

定义了函数R(删除),该函数递归地从栈顶删除所有的1,然后再删除两个值(其中顶部一个应为0)。

以下解释器函数期望程序p是一个字符串(或任何其他字节序列),输入堆栈S是一个可能为空的字节列表。解释器运行后,此列表包含输出堆栈。

def i(p,S,F=0):
 A=S.append
 F=F or{}
 C=0
 k=[]
 for c in p:
  h=0in k
  if c=="d":C=0
  elif C!=0:C+=[c]
  elif c=="I":k+=[int(h or S.pop())]
  elif c=="i":k.pop()or A("1")
  elif 1-h:
   if c=="D":C=F[S.pop()]=[]
   else:i(F.get(c)or A(c)or"",S,F)

显然,没有空间进行错误检查,因此i()要求合法的Kaputt代码。测试用例:

script = "<D(/)d"                 # < = pop
script += "&D((1/0)/<0)d"         # & = and
script += "~D((11/10)/(01/00))d"  # ~ = swap
script += "RD(R/<)d"              # R = remove
script += "|D(<1/(1/0))d"         # | = or
script=script.replace("(","I")   
script=script.replace("/","0iI")
script=script.replace(")","1iIi") # ( and / and ) are no real commands of the language.

S=[]
i(script+"1111010111111111R",S)
assert S == ["1","1","1","1","0"]

S=[]
i(script+"00&",S)
assert S == ["0"]

S=[]
i(script+"01~",S)
assert S == ["1","0"]

S=[]
i(script+"00|",S)
assert S == ["0"]

S=[]
i(script+"01|",S)
assert S == ["1"]

愉快的编程 :-)

编辑:解释器中没有强制令标记只能有一个字节的特点。要求这样做更多是为了与内置命令保持一致(它们是一个字节,因为这会使解释器变得更短)。如果你传递一个字符串列表而不是一个字符串,你可以编写更易读的Kaputt代码,就像这样:

script = """
inc D
    (
        (
            0 0
        /
            1 0
        )
    /
        1
    )
d
""".replace("(","I").replace("/","0 i I").replace(")","1 i I i").split()

这定义了一个名为inc的函数,它将栈顶上的两位数(最低有效位在上面)加一。

测试:

for n in xrange(4):
    S=[]
    i(script + [str((n & 2)/2), str(n & 1), "inc"], S)
    assert S == [str(((n+1) & 2)/2), str((n+1) & 1)]

我们把多字节函数名称作语言扩展 :-)


不错!特别是使用'(','/'和')'的想法。 - ilya n.

9

我创建了一种语言,现在需要一个Python程序来解释它:

from random import randint
z = [randint(0,1), None]  # example: input data is 0 or 1
x = '_b_ed_a_i____d_ai'   # this program will set p = data[1] = not data[0]

# input x[i], data z[k]
# jumper j
# p is +1 or -1 each step
# commands:
#    a   set data = 0 or 1
#    b   j += 0 or +-9 depending on data
#    d   move data left or right
#    e   perform jump left or right
#    j   set jumper to 0
#    i   end program
# output: p or some data[x], both possible

g = globals()
p = i = 1
a = b = d = j = e = k = 0
while i:
    h = x[i]
    g[h] = p = -p
    i += 1 + j*e
    if a: z[k] = i % 2
    if z[k]: j += 9*b
    k += d
    g[h] = 0        
#    print(a, b, d, e, i, j, k, h, z)

print('Input:', z, 'Output:', p > 0)

优化版本:

g=globals()
p=i=1
a=b=d=j=e=k=0
while i:
 h=x[i]
 g[h]=p=-p
 i+=1+j*e
 if a:z[k]=i%2
 if z[k]:j+=9*b
 k+=d
 g[h]=0

114字节

更新:我想补充一下,我的程序的目的不是创建任何实用的语言,甚至不是为了拥有“最好”的(=='最短')解释器,而是为了展示有趣的编程技巧。例如,我依赖于通过 globals() 直接访问全局变量,因此我甚至从未测试过j命令,以节省宝贵的字节 :)


9
#include "/dev/tty"

1
+1 对于非常有趣的回答,尽管“您不能使用使编译器为您执行代码的函数,例如eval(),exec()或类似函数。” - ilya n.
这并不会让编译器执行代码,是shell在执行代码。 ;) - Joe D

7
使用A86汇编以下代码,即可获得一个只有150字节的Brainfuck解释器!
    add dh,10h
    push dx
    add dh,10h
    push dx
    mov bl,80h
    lea dx,[bx+2]
    add bl,byte ptr [bx]
    mov byte ptr [bx+1],al
    mov ah,3dh
    int 21h
    pop ds,es
    jc l14
    mov bx,ax
    mov ah,3fh  
    mov cx,di
    xor dx,dx
    int 21h
    jc l14
    mov bx,ax
    xor ax,ax
    rep stosw
    xor di,di
    xor si,si
    inc ch
l1:
    cmp si,bx
    jae l14
    lodsb
    mov bp,8
    push l1
l2: 
    dec bp
    js ret
    cmp al,[bp+l15]
    jne l2
l3:
    mov cl,[bp+8+l15]
    jmp cx
l4:
    inc di  
    ret
l5:
    dec di
    ret
l6:
    es inc byte ptr [di]
    ret
l7:
    es dec byte ptr [di]
    ret
l8:
    mov ah,2
    es mov dl,[di]
    int 21h
    ret
l9:
    mov ah,8
    int 21h
    es mov [di],al
    ret
l10:
    es cmp byte ptr [di],dh
    jne ret
l11:    
    cmp si,bx
    jae l14
    lodsb
    cmp al,']'
    jne l11
    ret
l12:
    es cmp byte ptr [di],dh
    je ret
l13:    
    dec si
    jz l14
    cmp byte ptr [si-1],'['
    jne l13
l14:
    ret
l15:
    db '>'
    db '<'
    db '+'
    db '-'
    db '.'
    db ','
    db '['
    db ']'
    db offset l4-100h
    db offset l5-100h
    db offset l6-100h
    db offset l7-100h
    db offset l8-100h
    db offset l9-100h
    db offset l10-100h
    db offset l12-100h

在命令行上指定文件名,不需要双引号,作为BF源。


1
内置的Windows汇编器可以汇编这个吗? - erjiang
如果您指的是调试命令,只要计算出标签的地址就可以实现。 - Skizz
除了上述内容外,帖子中还有一个链接指向一个拥有优秀汇编器的网站。16位版本是共享软件,而32位版本需要付费。对于上述内容,您只需要16位版本即可。 - Skizz

6

这不是我的作品,但你可以看一下这个210的二进制λ演算自解释器,链接在这里


4

这段代码使用Perl编写,总共包括shell调用和标志在内只有140个字符:

perl -ne'push@a,split;if(eof){$i=0;while($i>=0){($a,$b,$c)=@a[$i..$i+2];$b>-1?$a[$b]-=$a[$a]:print chr$a[$a];$i=$b>-1&&$a[$b]<=0?$c:$i+3;}}'

易读版本:

#!/usr/bin/perl -n
push @prog, split /\s+/, $_;
if(eof) {
  $i = 0;
  while($i >= 0) {
    ($a, $b, $c) = @prog[$i .. $i + 2];
    if($b > -1) {
      $prog[$b] -= $prog[$a];
    } else {
      print chr $prog[$a];
    }
    if($b > -1 and $prog[$b] <= 0) {
      $i = $c;
    } else {
      $i + 3;
    }
  }
}

上面是一个解释器,用于伪机器码的单指令集计算机,使用subleq指令。基本上,源代码应该只是由空格分隔的一堆数字。一个简单的测试程序来验证我的结果(应该打印出“Hi”和Unix换行符):
0 0 6 72 105 10 3 -1 9 4 -1 12 5 -1 15 0 0 -1

可读版本的测试输入(同样有效):

0    0     6
72   105   10
3   -1     9
4   -1     12
5   -1     15
0    0    -1

4

我自己编写了一个OISC实现,使用Ruby语言。代码长度为85字节,我相信通过一些巧妙的技巧还可以进一步压缩。程序必须在代码中包含数据(一行以空格分隔的数字)。目前我无法提供使用OISC编写的可工作程序,但稍后我会提供。

p,m=0,gets.chomp.split.map(:to_i)
while p>0
p=(m[m[b]]-=m[m[a]])>0?p+3:c
end
$><<m[0]

这段代码非常直观。变量m是一个包含程序和数据的“内存”。第一行初始化m为提供的代码,p则是内存指针。主循环是使用三元运算符编写的subleq操作。最后一行是输出内存中包含数字的聪明方式。


已经确立的惯例(对于这种事情而言,尽可能确立了惯例)是打印a中包含的值,您可以使用命令subleq a, -1或此语法中的a -1 x(其中x是您想要转到的任何位置)。 - Chris Lutz
2
当我的Fortran实现比其他紧凑的语言少一个数量级时,我总是感觉不错。 - dmckee --- ex-moderator kitten

2

我之前的代码高尔夫比赛作品的基础上,这里是一个(稍微一般化了IO)OISC模拟器,使用的语言是Fortran77。

Fortran77

混淆过且没有加载脚手架的版本(655个字符):

  subroutine o(n,m)
  integer m(n)              
  l=1;                      
  do while (l.ne.0)
     i=m(l)
     j=m(l+1)
     k=m(l+2)
     mi=mg(n,m,i)
     mj=mg(n,m,j)
     if(j.eq.(n+2)) then
        write(6,*)mj-mi
     else
        m(l+1)=mj-mi
     endif
     if (m(l+1).lt.0) then
        l=mg(n,m,k)
     else
        l=l+3
     endif
  end do
  return
  end
  function mg(n,m,i)
  integer m(n)
  if (i.eq.n+2) then
     read(5,*)mg
  elseif (i.eq.n+1) then
     mg=0
  else
     mg=m(i)
  endif
  return
  end

带有注释,加载脚手架等(2435个字符):
      program c
      parameter (n=1024)        ! The size to use for memeory
      integer m(n)              ! represent the memory
c     Load a program into memory
      i=1
 1    read(5,*,end=2)l
c     write (6,*) "Load ",l," into location ",i
      m(i)=l
      i=i+1
      goto 1
c     Run the computer
 2    call o(n,m)
      stop
      end

      subroutine o(n,m)
c     Simulate a simple computer that supports only a single
c     instruction. Memory is a fixed size array of integers.
c
c     The supported instruction is subtract-branch-negative which we
c     give the assembly syntax
c
c     sbn i j k
c
c     and acts by subtracting the value in memeory location i from that
c     in location j and storing the result in j. If the result is
c     negative, the PC is set to k. Because there is only one opcode, it
c     is not represented, so a program consists simply of a series of
c     triplet (i,j,k), and the PC is generally incremented by 3. The
c     program counter starts at 1
c
c     Povisions for IO and halting are by way of special memory
c     location:
c
c     * PC=0 means halt
c     * writes (opcode argument j) to memory location n+1 send
c       output, reads from n+1 evaluate as 0
c     * reads (opcode argument i, j, k) from memory location n+2 fetch
c       input, writes to n+2 are forbidden
c     * All others reads and writes outside of memeory are forbidden

c     n                         ! the size of the computers memory 
      integer m(n)              ! an array representing the computers memory
      l=1;                      ! program counter
      do while (l.ne.0)
         i=m(l)
         j=m(l+1)
         k=m(l+2)
c        write (6,*) "Executing from PC=",l,": (",i,", ",j,", ", k,")"
c        handle the write special case for output
         mi=mg(n,m,i)
         mj=mg(n,m,j)
         if(j.eq.(n+1)) then
            write(6,*)mj-mi
         else
c           write (6,*) "Setting location ",j," to ",mj-mi
            m(l+1)=mj-mi
         endif
         if (m(l+1).lt.0) then
            l=mg(n,m,k)
         else
            l=l+3
         endif
      end do
      return
      end

c     Handle the various read special cases
      function mg(n,m,i)
      integer m(n)
      if (i.eq.n+2) then
         read(5,*)mg
      elseif (i.eq.n+1) then
         mg=0
      else
         mg=m(i)
      endif
c     write (6,*) "Read ",mg," from location ",i
      return
      end

示例程序:

13
1025
0

14 
1025
0

14
15
0

0
0
0

-1
1
0

导致输出结果:
$ cat trivial.oisc | ./a.out 
           1
          -1

正如预期的那样。

这真的是非常简单的代码。重点不在于我能够编写多紧凑的代码,而是为了图灵完备性所需的语言要多么简单。


1

106字节的解决方案发布在codegolf.com竞赛中。它是用Perl编写的,可以解释Brainfuck。据我所知,目前还没有办法审查它。

这不是我的解决方案,但我相信它比当前的条目更短。可能的解决方案应该更短,因为没有必要使用Brainfuck作为图灵完备语言。


你怎么获取那个源代码? - pts

0

URM 解释器使用 CoffeeScript 实现,仅有 143 字节(带有新行共 167 字节)。

此版本的 URM 包含无限数量的寄存器,具备零、后继和跳转运算符。众所周知,这是图灵完备的。

URM 程序编写在数组 c(命令)中,输入位于数组 r(寄存器)中。计算结束后,输出在 r [0] 中,并展示该值。

解释器附带了一个计算 32+13(并且的确输出 45)的示例程序和输入。

# Addition program, similar to http://planetmath.org/examplesofunlimitedregistermachines
c = [['j', 1, 2, 4], ['s', 0], ['s', 2], ['j', 1, 1, 0]]

# Input: 32, 13, thus the desired output is: 45
r = [32, 13]

k = 0
while k < c.length
    t = c[k]
    k += 1
    if t[0] == 'z'
        r[t[1]] = 0
    if t[0] == 's'
        if !r[t[1]]?
            r[t[1]] = 0
        r[t[1]] += 1
    if t[0] == 'j' && r[t[1]] == r[t[2]]
        k = t[3]
alert r[0]

压缩版本:

k=0
while k<c.length
 t=c[k]
 k+=1
 if t[0]=='z'
  r[t[1]]=0
 if t[0]=='s'
  if !r[t[1]]?
   r[t[1]]=0
  r[t[1]]+=1
 if t[0]=='j'&&r[t[1]]==r[t[2]]
  k=t[3]
alert r[0]

我真正喜欢这段代码的地方在于它非常直接明了,易于理解。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接