Python中的Befunge编译器

在为四年级学生准备“编译器基础”课程时,我学习了各种深奥的编程语言。这是一篇有关该主题的好文章在我看来,Befunge语言(克里斯出版社,1993年)似乎是最有趣的,我特别注意到它的三个功能:

  1. 程序字段是一个二维圆环,即 实际上,这是符号命令的矩形矩阵,沿着上(下)边界和左(右)列闭合。命令指针在字段中移动(每个命令都是具有x,y坐标的特定字符),执行该命令,然后继续前进。可以在所有4个方向上移动(默认情况下,从0,0点开始),并且当您超出“字段”时,指针将显示在相反的一侧。
  2. 语言中有两个命令(p,g)会更改字段本身,即 该程序在执行过程中是“自我重写”的。开头的程序代码可能不等于结尾的代码。程序“ 123pgpg ## @”启动,程序“ ABC @ 1 @ 2 @ 3.14”(不是正确的示例)完成工作。
  3. 克里斯·普莱斯(Chris Pressy)指出,他想创建一种尽可能复杂的语言来进行编译。事实上,的确如此,创建了一个使exe文件难以执行的编译器,我发现有人可以用C语言完成此操作的信息...最好创建从该语言到Python代码的翻译器,我仍然称其为为简单起见,编译器。


1993年的计划字段包括25行,每行80个字符。该语言有36个小组,每个小组都是一个ASCII表字符。有关更多信息,请参见Wikipedia,我将在此处进行简要说明:

移动命令(9):

>右移
<左移
^向上移
v向下移
_如果堆栈顶部为0,则向右移,否则向左移。
|如果在堆栈0的顶部,则向下移动,否则向上。
?沿随机方向移动
#跳过下一个单元格(“跳板”)
@程序结束

堆栈操作命令(3)

::将顶点副本放在堆栈上
\交换顶部和底部
$删除顶部

对程序代码(2)的命令修改:
p“ PUT”:
从堆栈中提取单元格坐标和位于这些坐标处的字符的ASCII码g“ GET”:从堆栈中提取单元格坐标;这些坐标处的符号的ASCII码被压入堆栈

常量命令(2):

0-9将数字放在
符号模式堆栈的开始/结束位置,在该模式下,所有当前程序符号的ASCII码都被

压入堆栈。算术运算(5):

+顶点和顶点的
加法- 顶点和顶点的减法
* 顶点和顶点的相乘
/整数除法
余数

堆栈命令和逻辑操作(2)

:!否定:将顶点上的零替换为1,将非零值替换为0。
比较“大于”:如果顶点大于顶点,则将1放置在堆栈上,否则使用0

I / O命令(4):

&请求用户输入数字并将其放置在堆栈上
〜向用户询问一个字符并将其ASCII码放在堆栈上
。将堆栈顶部打印为整数
在堆栈顶部打印与ASCII代码相对应的字符

我决定根据1993年的规则用Python编写Befunge编译器(解释器),但有两个限制:1)该字段不是25x80字符,而是文本块的最小宽度和高度; 2)该字段未循环成圆环,即 越过边界跳到另一侧将不被处理。这不是懒惰(尽管,我在跟谁开玩笑吗?),举个小例子,一切都很好,并且要完成一个真正的圆环很简单,就会有一种愿望。

该代码不必要地在“额头上”出现在某些地方,但这是由于它是针对学生的,其任务是尽可能清晰,而不是用中文缩写成两行。

第1部分


该代码从头到尾都提供了一个例外(将被指定),可以将其复制到文件中并运行。全文可在链接rentry.co/ivansedov-befunge上找到,对我来说在GitHub上提供最好的东西为时尚早。顺便说一下,那里大约有20种Befunge语言实现,但是代码要么是C语言(不是我的语言),要么是Python语言,但它是如此复杂,以至于我不敢涉猎。但是,您可以在此处获取用于测试的示例程序,例如,此处github.com/causal-agent/befungee

from sys import *
import time
import random

导入库:

  1. 需要sys库才能获取程序文件的名称。我的编译器名为bbb.py,这是同一目录1.bf中的测试示例,Python版本3.7本身,因此控制台中的程序调用如下所示:python3 bbb.py 1.bf
  2. time , , 0,5-1,0 .
  3. random «?» ( ), . -, Befunge - , « » (1 , 2 , 3 , 4 ). « » .

class Pointer:
    def __init__(self, x=0, y=0, vector=2, value=None):
        self.x = x
        self.y = y
        self.vector = vector
        self.value = value
        self.stack = []
        self.stack_sf = 0

    def __str__(self):
        return 'Point ({},{}) vektor:{} value:{} stack_sf:{} stack:{}'.format(self.x, self.y, self.vector, self.value, self.stack_sf, self.stack)

    def step(self):
        if self.vector == 1:
            self.x -= 1
        elif self.vector == 2:
            self.y += 1
        elif self.vector == 3:
            self.x += 1
        elif self.vector == 4:
            self.y -= 1

    def action(self):
	#   ,   

程序中使用的主要类:指针(pointer),具有6个属性和2个方法。属性:1)x坐标(初始= 0),2)y坐标(初始= 0),3)向量(运动方向,初始= 2向右),4)值(位于x,y坐标处的字段值,初始=无),实际上,这是要执行的命令; 5)堆栈(程序堆栈,初始= [])和6)stack_st(用于输入行的标志,初始= 0)。

该类有两种方法:1)步骤()指针步骤,无需进行任何检查和测试,即可根据向量中的方向更改x,y坐标,以及2)action()是编译器的心脏,执行当前程序命令。我将在第二部分中给出action()代码,以便它不会偏离逻辑。

# ===============================
# =                      =
# ===============================

def open_file(name):
    data = open(name, 'r').read()
    return data

def field_print():
    for row in A:
        for elem in row:
            print(elem, end='')
        print()  

两个辅助功能:1)open_file(名称)按提交的名称打开文件,读取并返回内容,2)field_print()打印数组A的内容,程序字符位于该数组中。创建数组A如下所示。

# ===========================================
# =                       =
# ===========================================

numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
operators = ['+', '-', '*', '/', '%']
point = Pointer()                           #  
text = open_file(argv[1]).split("\n")       #  
n = len(text)                               # n =  
m = 0                                       # m =  

#    (   m)
for line in text:
    if len(line) > m:
        m = len(line)

#   ( n  m )
A = [' '] * n
for i in range(n):
    A[i] = [' '] * m

#    
for i in range(len(text)):
    for j in range(len(text[i])):
        A[i][j] = text[i][j]

基本程序设置。在数字列表中,我们输入了可以在程序中写入的所有数字(从0到9)(10个不再适合,两个字符)。在运算符列表中,我们放置了基本算术运算符。创建一个point = Pointer对象(默认设置),我们将一直使用它...

在变量文本中,我们放入了可执行程序的读取文本,并用“换行”符号将其打断。结果,文本由多行不同长度的文本组成,必须将其放置在矩形矩形空间中。很容易找到行数n = len(文本),但必须根据文本中包含的行的最大长度来计算列数。我没有找到另一种方法来完成此额头:遍历所有行,找到长度最大的行。有了n和m(future字段的行数和列数),您可以创建一个二维数组,在其中填充空格,然后遍历文本以将字符放置在它们的位置。

结果是一个字符矩形,其间有空格,所有这些都是n×m的二维矩阵。完成这些设置后,您可以调用field_print()函数,并确保所有内容看起来都很漂亮,没有任何浮空且没有违反这种情况。

# ==========================================
# =                       =
# ==========================================

# field_print()

while point.value != '@':
    try:
        point.value = A[point.x][point.y]       # 1)    point
        point.action()                          # 2)  
        # print(point)                            # 3) :  
        # time.sleep(0.5)                         # 4) :  0.5 c
    except IndexError:                          #     =  
        print('     ')
        break

# field_print()
print()

一切都以主程序(循环)结尾,在此之前和之后您可以显示该字段(有时很有用)。循环旋转直到指针指向“ @”符号(“&”,“ dog”,程序结尾)为止。在周期内,每次执行4次操作:

  1. 在point.value属性中,读取数组A中坐标为point.x,point.y的字符。
  2. 调用point.action()方法,在该方法中执行当前(仅读取)命令
  3. 指针显示在屏幕上(所有属性)
  4. 在下一次迭代之前进行延迟(0.1秒-0.5秒)

项目3和4完全是可选的(甚至被注释掉了),但是我建议使用它们进行测试。循环中的所有操作都会捕获IndexError错误(错误超出索引限制),这使您可以截获两个主要的编译器错误:

  1. 我们转向堆栈,没有任何值
  2. 我们不小心超出了程序(数组)的宽度或高度

需要最后一个空print(),以便在显示输出后,控制台可以从新行开始工作。

第2部分


现在是时候编写最重要的代码了-Pointer类的point.action()方法的内容。下面的所有内容都应插入其编写位置:

    def action(self):
	#   ,   

注意缩进:

        if self.value == '"' and self.stack_sf == 0:                # ,  "
            self.stack_sf = 1
        elif self.value == '"' and self.stack_sf == 1:
            self.stack_sf = 0
        elif self.stack_sf == 1:                                    # "Hello"   
            self.stack.append(ord(self.value))
        elif self.value in numbers and self.stack_sf == 0:
            # 123   
            self.stack.append(int(self.value))

实际上,action()中的代码有很多条件,当此命令位于指针下方时,将执行每个条件。一切都始于条件“如果当前命令=引号并且行stack_sf的开头的标志= 0”,在这种情况下,标志升至1。我们进入了行。

(否则)如果当前命令=引号,并且该标志升至1,则意味着第二次找到引号,并且必须停止输入字符串(stack_sf标志降低为0)。我们不合时宜。

(否则)如果前两个条件不起作用并且标志stack_sf = 1,则我们“位于行内”,需要将当前符号的代码添加到堆栈中。不是字符本身,而是其ASCII代码。

(否则)如果当前字符在数字元素之间并且标志为stack_sf = 0,则这首先是一个数字,其次我们不在行内,我们需要将当前字符=数字添加到堆栈中。不添加字符代码,而是添加字符本身。仍然记住,有一个数字1,并且她的代码= 49。因此,如果我们在行内,则需要将49添加到堆栈中,如果它只是在程序中,则应将命令1添加到堆栈1中。

此后,所有条件都是省略号(否则,如果...),所以我只写它们“如果”。另外,所有条件都是双重的,您需要检查当前字符是否与命令相等,以及我们不在字符串内的事实(在字符串内,所有字符的处理方式均不同)。您可以以更优化的方式编写所有这些内容,但是此解决方案使您可以将注意力集中在此额头上。

        elif self.value in operators and self.stack_sf == 0:
            b = self.stack.pop()
            a = self.stack.pop()
            if self.value == '+':
                res = a + b                                         # a+b  
            elif self.value == '-':
                res = a - b                                         # a-b  
            elif self.value == '*':
                res = a * b                                         # a*b  
            elif self.value == '/':
                if b == 0:
                    res = 0
                else:
                    res = a // b                                    # a//b  
            elif self.value == '%':
                res = a % b                                         # a%b  
            self.stack.append(res)

如果当前字符在运算符之间(并且stack_sf = 0),则意味着我们进入了算术运算。它们都是完全相同的:1)将数字b删除(删除),2)将数字a删除(删除),3)res = a和b之间的运算值,4)res被压入堆栈。除以0时,答案为0,尽管该语言的作者提供了选择0或1的语言。

        elif self.value == '!' and self.stack_sf == 0:
            a = self.stack.pop()
            if a == 0:
                a = 1
            else:
                a = 0
            # 0->1, 1->0
            self.stack.append(a)

        elif self.value == '`' and self.stack_sf == 0:
            a = self.stack.pop()        # 
            b = self.stack.pop()        # 
            if b > a:
                res = 1
            else:
                res = 0
            # b>a -> 1|0
            self.stack.append(res)

如果当前字符为“!”,则需要替换堆栈的头(顶点):它为0-将变为1,与0有所不同-将为1。如果当前字符为“»”(撇号),则需要检查顶部和书脊:1)如果顶点大于顶点,则为1,2)如果顶点小于(或等于)顶点,则将0放置在堆栈上。请注意,在删除比较对象时,它们将被删除(删除),而不是被复制。

        elif self.value == '?' and self.stack_sf == 0:
            # ? ( )
            a = random.randint(1, 4)
            self.vector = a

        elif self.value == ':' and self.stack_sf == 0:              #  
            last = self.stack.pop()
            self.stack.append(last)
            self.stack.append(last)

        elif self.value == '\\' and self.stack_sf == 0:             # ab => ba
            a = self.stack.pop()
            b = self.stack.pop()
            self.stack.append(a)
            self.stack.append(b)

如果当前字符是“?”,则需要选择一个随机方向并沿其前进。我们使用random.randint(1,4)函数,该函数生成数字1,2,3,4并将新值放入point.vector中。

如果当前字符为“:”,则将堆栈顶部的副本(即,读取它,然后将其添加到堆栈两次。

如果当前字符为“ \\”(反斜杠),则需要交换顶点和子顶点。我们得到两个数字,以相反的顺序将它们放在堆栈中。

        elif self.value == '#' and self.stack_sf == 0:              #  ""
            self.step()

        elif self.value == ',' and self.stack_sf == 0:              # =65=A
            value = self.stack.pop()
            print(chr(value), end='')

        elif self.value == '.' and self.stack_sf == 0:              # Print 
            a = self.stack.pop()
            print(a, end='')

如果当前符号为“#”(磅),则必须跳过下一个(方向)命令。请注意,在动作()的末尾有一个无条件的跳转self.step(),它使您可以前进到下一个命令。将self.step()写入“#”处理后,我们实际上进行了两次跳转,并在“#”之后的下一个命令“跳过”。

如果当前字符为“,”(逗号),则需要打印其ASCII码位于堆栈顶部的字符。如果存在数字65,则应显示“ A”。

如果当前字符为“。” (点),然后您需要打印位于堆栈顶部的数字,就像一个数字一样。如果存在65,则需要显示“ 65”。在这两种情况下,在输出期间都将设置end =''参数,以便不存在新的换行符。

        elif self.value == '_' and self.stack_sf == 0:              #  "_"
            test = self.stack.pop()
            if test == 0:
                #  = 0, (2)
                self.vector = 2
            else:
                #  !=0, (4)
                self.vector = 4

        elif self.value == '|' and self.stack_sf == 0:              #  "|"
            test = self.stack.pop()
            if test == 0:
                self.vector = 3
            else:
                self.vector = 1

如果当前字符为“ _”(下划线),则水平检查。我们从堆栈中删除要检查(测试)的数字,如果它等于0,那么我们向右移动(向量= 2),如果它等于0,那么我们向左移动(向量= 4)。

如果当前字符=“ |” (垂直条),那么您需要进行垂直检查。我们从堆栈中删除数字(测试),如果它等于0,则向下移动(向量= 3),否则向上移动(向量= 1)。

        elif self.value == '$' and self.stack_sf == 0:              #  
            self.stack.pop()

        elif self.value == '~' and self.stack_sf == 0:              # Input: A => 65
            val = input(' : ')
            self.stack.append(ord(val[0]))

        elif self.value == '&' and self.stack_sf == 0:              # Input: 65 => 65
            val = int(input(' : '))
            self.stack.append((val))

        elif self.value == 'p' and self.stack_sf == 0:              # x, y, symcode
            x = self.stack.pop()                                    # A(x,y) = symcode
            y = self.stack.pop()
            symcode = self.stack.pop()
            A[x][y] = chr(symcode)

        # x, y, value=A(x,y)
        elif self.value == 'g' and self.stack_sf == 0:
            x = self.stack.pop()                                    # ord(value) => 
            y = self.stack.pop()
            value = A[x][y]
            self.stack.append(ord(value))

如果当前字符=“ $”,则需要删除顶点。制作简单的pop()。

如果当前字符=“〜”(波浪号),那么我们要求用户输入一个字符并将其ASCII码放在堆栈上。发送用户“ A”(英语)时,必须将65放在堆栈中,以防万一,我们将val [0]放入,否则用户可以输入“ Apple”并将其转换为代码无效。

如果当前字符=“&”(“&”号),那么我们要求用户输入一个数字并将该数字放在堆栈中。您输入65,需要将65放入堆栈,

现在是两个最困难的团队。

如果当前字符=“ p”,则需要从堆栈中提取单元格坐标和该字符的ASCII代码,然后将此字符放在字段A的这些坐标中。假设堆栈上有1.2.65,我们得到(1.2)和65,则应在单元格(1.2)中放入符号“ A”。我再次指出:我们得到了三个数字,并在坐标中放置了一个符号。

如果当前字符=“ g”,则从堆栈中检索单元格坐标,在字段上搜索该单元格,从那里获取该字符并将其ASCII码压入堆栈中。假设符号“ B”躺在单元格(2,3)中的字段上,当前团队获得“ g”,而我们从堆栈中获得2,3。在这种情况下,我们沿着坐标(2,3),从那里得到符号“ B”,将其转换为数字66(符号代码B),然后将66放入堆栈。

        elif self.value == '>' and self.stack_sf == 0:              # >
            self.vector = 2
        elif self.value == '<' and self.stack_sf == 0:              # <
            self.vector = 4
        elif self.value == '^' and self.stack_sf == 0:              # ^
            self.vector = 1
        elif self.value == 'v' and self.stack_sf == 0:              # v
            self.vector = 3
        self.step()                                                 #  

好吧,这是最后几行代码:跨字段移动指针的组织。这里的一切都很简单:我们查看符号并更改方向(矢量)。action()函数的最后是self.step(),它在当前方向上迈出了一步。因此,动作()既是动作的执行,也是下一个字符的步骤。

结论


编写此编译器是一种乐趣。当您向程序中添加特定代码并执行(正确执行)时,带来了多少欢乐。befungius.aurlien.net网站在工作时提供了很多帮助,作者在该网站上发布了Befunge在线解释器(使用JavaScript)。这是他在一些会议www.youtube.com/watch?v=oCPT3L33848上的视频,其中他谈到了这种语言。

对于测试,您可以使用Befunge上几乎任何程序,除了需要将字段作为圆环的程序,即朝着世界的不同方向过渡。对于某些程序,您需要查看字段本身(例如,计数器程序会更改坐标A [0] [1]),因此请在屏幕上插入此坐标的输出或在每个步骤之后显示整个矩阵A。

无论如何,这不是一个让人眼花shine乱的程序,而是可以添加和编辑的培训代码。打字错误是可能的,尽管我已经测试了很多次。批评是不受欢迎的,但也不被禁止。祝大家好运,并提供良好的代码。

你好,世界!

0"!dlrow olleH">:#,_@

巴兹

0> 1+:3%v
>^  v%5:_:5% v
,v.:_v     v0_0"zzub"v
"v         #
     >0"zzub"v
"   v"fizz"<         <
^<         $<>:#,_v
    >      #^^#   <

斐波那契

62*1+v>01p001>+v>\:02p\:02gv
     0       ^             <
     .         :p
     "         .1
        v 0," "<0
     "  >1g12-+:|
     ,          @
     >^

计数

>91+:9`v
p   v  _v
    >$0 v
^ 01+*68<

但这是行不通的

0>:00p58*`#@_0>:01p78vv$$<
@^+1g00,+55_v# !`\+*9<>4v$
@v30p20"?~^"< ^+1g10,+*8<$
@>p0\>\::*::882**02g*0v >^
`*:*" d":+*:-*"[Z"+g3 < |<
v-*"[Z"+g30*g20**288\--\<#
>2**5#>8*:*/00g"P"*58*:*v^
v*288 p20/**288:+*"[Z"+-<:
>*%03 p58*:*/01g"3"* v>::^
   \_^#!:-1\+-*2*:*85<^

链接和其他材料:

  1. 完整代码
  2. 在线版
  3. 她在用JavaScript
  4. 文档(英文)
  5. Wiki文章(英语)

All Articles