65.9K
CodeProject 正在变化。 阅读更多。
Home

BrainFix,一种能翻译成流畅的Brainfuck的语言

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (6投票s)

2013年3月8日

CPOL

22分钟阅读

viewsIcon

35900

本文介绍了如何在BrainFix语言中编程以及如何将您的程序编译成Brainfuck。

 

2022年更新

注意:这个项目已经被赋予了新的生命。尽管本文的大部分内容仍然相关且真实(尽管并非所有格式都经过了过去10年的考验),但在我的Github页面上提供了更新、更好、改进、更新、更棒的软件版本。自述文件包含对该语言的描述(许多破坏性的语法更改)。下面的文章可以作为已实现的BF算法的参考。

回到2013年...

BrainFuck简介

BrainFuck是Urban Müller于1993年设计的一种深奥的编程语言。它是一种图灵完备的语言,因为它提供了解决任何计算任务的足够工具。然而,它也可以被称为一个真正的图灵泥潭,尽管它易于学习,但没有任何实际的实用价值。它易于学习的原因主要在于其简单性:该语言只定义了8种操作,类似于图灵机的工作原理。假定有一个(无限)数组,以及指向数组某个元素的指针。现在可以执行的操作如下(括号中显示了等效的C代码)

  • > 将指针向右移动一个位置。(++ptr;)
  • < 将指针向左移动一个位置。(--ptr;)
  • + 将指针指向的值增加1。(++*ptr;)
  • - 将指针指向的值减少1。(--*ptr;)
  • . 将当前单元格中的值打印到输出。(putchar(*ptr);)
  • , 从输入读取一个值并将其存储在当前单元格中。(*ptr = getchar();)
  • [ 如果当前值为零,程序将跳过[和]之间的代码。(while (*ptr) {)
  • ] 程序流程将返回到相应的] (})。

背景

下面描述的语言是使用Frank B. Brokken的Bisonc++Flexc++定义的。

BrainFix简介

使用BrainFuck提供的有限工具集编写复杂算法是一项艰巨的任务,因此设计了一种中间语言,能够将可读的源代码翻译成(几乎)不可读的BrainFuck代码。我将这种语言命名为BrainFix,因为它解决了BrainFuck的不实用性,但仍然生成有效的代码。该语言的语法继承自Matlab、C和Python。一个完整的程序必须至少在一个源文件中定义一个main()函数。最小的、什么都不做的BrainFix程序如下所示:

function main() {}

函数体可能包含由变量、关键字、常量、运算符和函数调用组成的表达式,并以分号(;)结尾。这些特性将在接下来的章节中讨论。如果本手册随我分发的软件包一起提供,则示例将以.bfx文件形式包含。如果没有,您可以轻松地自行复制粘贴代码!

Hello World

让我们从经典的“Hello World!”示例开始。BrainFix支持字符串文字。这使我们能够使用prints关键字将字符串打印到标准输出,如下所示。为了打印换行符,转义序列\n可以嵌入到字符串中(从0.41版开始)。

/* hello.bfx */  
 
function main()  
{  
    prints "Hello World!\n";  
}

打印

BrainFix中有四种不同的打印关键字:printprintcprintdprints。在上一节中,我们已经看到了如何使用prints关键字打印字符串,以及如何使用转义字符打印换行符。printprintc命令打印传递给它的表达式的字符表示(根据您系统的区域设置)。因此,也可以使用printc 10;来打印换行符。printd命令将打印表达式的十进制值,但它只支持3位数字。BrainFuck语言假定一个单字节数组,因此3位数字应该足以打印所有输出。

扫描

使用scan关键字,从标准输入读取一个值,并将其存储在传递给scan命令的变量中(例如,scan x;)。目前只支持扫描十进制值。

变量和关键字

BrainFix中的变量以字母(小写或大写)开头,后跟任意数量的字母数字(无下划线)。它们不需要声明或分配,其类型由编译器推断。BrainFix可以处理三种类型的变量:整数/字符、字符串文字和数组。整数和字符(单引号之间,例如'a')被完全相同地处理,字符串文字只是数组的一种特殊情况:一个以0字符结尾的字符数组。只支持正整数值。负值将不被解析器识别,否则会导致负值的减法将导致0。当值出现在双引号中时(如“Hello World!”示例所示),它被解析为字符串。当然,下面列出的BrainFix关键字不能用作变量。

转义序列可用于打印在BrainFix语言中具有特殊含义的字符。例如,为了在字符串中打印双引号,可以执行以下操作

prints "Hello \"World\"!\n";

当转义字符没有特殊含义时,使用其常规表示(例如,\j 被解析为 j)。

关键词

print   printc  printd
prints  scan    if
else    for     array

函数

一个BrainFix程序由函数组成,其中一个函数必须命名为main。这是程序启动时执行的函数。从main()中,可以调用在同一源文件或另一个源文件中定义的其他函数。当其他函数在与main()不同的源文件中定义时,请务必将此文件传递给BrainFix编译器(bfx)。函数可以接收参数(按值传递),并能够通过以下语法返回计算值(方括号中的表达式是可选的)

function [return-variable =] functionName([arg1, arg2, ...])  
{  
    // ... body  
}

函数调用本质上由编译器内联,因此不支持递归调用函数。当一个函数在函数调用栈中出现不止一次时,编译器将报告错误。在下面的示例中,之前的“Hello World!”程序被修改以包含一个函数调用(没有返回值)

/* hellofunction.bfx */  
 
function main()  
{  
    hello("World");  
}  
 
function hello(who)  
{  
    prints "Hello ";  
    prints who;  
    prints "!\n";  
}

注释

BrainFix支持两种类型的注释:行尾注释和C风格注释。EOL注释从//开始,当然,到行尾结束。C风格注释分别由/**/分隔。所有注释的文本都将被编译器忽略。

运算符

目前,该语言支持以下运算符:=, +, -, *, /, %及其对应的赋值运算符(例如+=)。它还包含常见的比较运算符<, >, ==, !=, <=, >=以及逻辑AND (&&)、OR (||)和NOT (!),以允许更复杂的条件。下面是一个示例,说明了常用算术运算符的使用。条件运算符的使用将在下一节(流构造)中显示。

/* arithmetic.bfx */  
 
function main()  
{  
    scan x; // read an integer value from stdin into the variable x  
    scan y; // same for y  
 
    z = 2 * (x + y) * (x % y); // compute the value and assign to z  
    printd z;   // print the decimal value of z  
    print ’\n’; // print a newline  
}

流控制

BrainFix在一定程度上通过实现for循环和if-else条件来支持流控制。这些构造的语法类似于MATLAB,因为if条件周围不需要括号,for的范围使用start:step:stop表示。step值的规范是可选的,如果省略,它将设置为1。for、if或else的主体只能包含一个表达式,或者{}之间的一个复合语句。

/* flow.bfx */  
 
function main()  
{  
    // Get x and y from input  
    x = get("x");  
    y = get("y");  
 
    // Compare the two  
    compare(x, y);  
 
    // See if one or both were 0  
    zero(x, y);  
 
    // Print the alphabet  
    printSequence(’a’, ’z’);  
}  
 
function x = get(name)  
{  
    prints "enter ";  
    prints name;  
    prints ": ";  
    scan x;  
}  
 
function compare(x, y)  
{  
    more = "x is greater than y\n";  
    less = "x is less than y\n";  
    same = "x is equal to y\n";  
 
    if x < y  
        prints less;  
    else if x == y  
        prints same;  
    else if x > y  
        prints more;  
}  
 
function zero(x, y)  
{  
    if x == 0 && y == 0  
        prints "Both x and y are zero.\n";  
    else if x != 0 && y == 0  
        prints "x was not zero, but y was.\n";  
    else if x == 0 && y != 0  
        prints "y was not zero, but x was.\n";  
    else  
        prints "neither x nor y was zero.\n";  
}  
 
function printSequence(start, stop)  
{  
    for i = start:stop  
        print i;  
    print ’\n’;  
}

数组

从0.32版本开始,该语言支持在编译时已知大小的数组(静态分配)。创建数组有两种方式:使用`new array`关键字,或者通过用方括号内逗号分隔的列表表示的数组初始化变量。字符串已被重新实现,现在与数组的行为完全相同,这意味着它们也可以被索引。然而,字符串文字保证以零结尾。下面的示例说明了如何使用数组

/* arrays.bfx */  
 
function main()  
{  
    x = [0, 1, 2, 3, 4];  
    zeros1 = array 5;    // array of 5 zeros  
    zeros2 = array 5 0;  // the 0 at the end is optional  
    ones   = array 5 1;  // array of 5 ones  
 
    for i = 0:4  
    {  
        zeros1[i] = x[i];  
        printd zeros1[i];  
        print ’ ’;  
    }  
    print ’\n’;  
 
    str = "Hello World!\n";  
    str[1] = ’o’;  
    str[7] = ’e’;  
    prints str;  
}

内存管理

垃圾回收器会回收未引用的内存并将其用于其他目的。旧变量可以安全地被覆盖和重用。因为每个函数都有自己的作用域,所以同名变量不会在函数作用域之间引起命名冲突。当参数在函数之间传递时,它们将按值传递。也就是说,函数将接收原始变量的副本,即使它是字符串或数组。

编译

使用bfx编译器将BrainFix编译为BrainFuck是一个单步过程,但除非您有一台可以直接执行BrainFuck指令的机器,否则您将需要另一个工具将生成的BrainFuck代码翻译为C(或任何其他主流语言)。我包含了我的小翻译器,名为bf2c,来完成这项工作。我建议您使用这个,尽管它没有优化,并且在各个方面都非常原始。我仍然推荐它的原因是它实现了一些细微之处

使用整数数组以支持超过1字节的数字计算。从输入读取时,读取的是十进制值而不是字符。在递减值之前,首先检查该值是否已为0。如果是,则不执行任何操作。不实现此行为的解释器可能无法生成工作程序。要编译您的BrainFix程序,只需运行

$./bfx file1.bfx file2.bfx ... filen.bfx file.bf

这将生成一个BrainFuck源文件 file.bf,它可以通过 bf2c 翻译成 C

$./bf2c file.bf file.c

在运行代码之前的最后一步是使用您选择的编译器(例如,gcc)编译C源文件

$gcc -o program file.c  
$./program

实现细节

解析输入文件

为了解析输入,使用了两个解析器。每个解析器都有自己的词法扫描器来收集标记,并由Flexc++生成。解析器本身由Bisonc++解析器生成器生成。第一个解析器(Preprocessor::Parser)不解析每个函数的实际主体,而是收集所有函数并将其主体内部存储在std::map<std::string,></std::string,>中,使用函数名作为映射键。第二个解析器(Compiler::Parser)然后通过向预处理器请求主函数体开始,如果不存在则报告错误。函数调用由一个标识符识别,紧接着是一个左括号。发生这种情况时,解析器在栈上初始化一个新的解析器,该解析器将解析此函数的主体。这也是为什么禁止递归调用:它们将导致无限的解析器栈。因此,当解析器初始化时,它将函数名存储在一个静态的std::vector<std::string></std::string>中,该向量可用于检测递归:禁止调用已存储在向量中的函数。最终,当解析器返回时,它会自动死亡(它是在栈上分配的),并且父解析器继续解析,就像什么都没发生过一样。被调用的函数实际上已被内联。除了内联函数调用之外,我没有看到其他将函数调用编译为BrainFuck的方法。用于生成这些扫描器和解析器的语法规范文件包含在软件包中。我不会在这里讨论它们,但如果您对这些规范文件有任何疑问,请随时与我联系!

类层次结构

bfx的类层次结构如下图所示。每个类都由Bisonc++(解析器)或Flexc++(扫描器)生成。在接下来的章节中,我将讨论通过函数调用调用的算法,例如assign()。这些函数是Compiler::Parser类的成员。

内存管理

在编译过程中,编译器在任何时候都不会知道任何内存单元在运行时将具有什么值。只有每个变量的地址在内部注册到一个`std::vector`中。我意识到这严重限制了性能,因为优化常量表达式会显著减少生成的BF代码。然而,我选择不实现常量表达式(由编译器评估),因为这会使解析器过于复杂。这可能是未来的一个功能,但老实说:BrainFuck从未被设计成紧凑的。内存分配通过将字符串分配给向量的一个元素来完成。包含空字符串的元素被认为是可用于分配变量、数组和临时变量的。对于变量,存储标识符(由程序员指定)。临时变量(由编译器分配)被赋予标识符`__temp__`,以便垃圾回收器能够轻松识别临时变量并将其从内存中清除。作为数组一部分并通过其他地方的指针引用的内存单元被标记为`__refd__`。临时但需要在栈上存活直到`if`或`for`构造作用域结束的内存单元被标记为`__stack__`。当调用`popStack()`时,这些变量将被清除。

变量

当解析器遇到变量时,它会调用函数Compiler::Parser::allocate(std::string),其中参数与变量名(标识符)匹配。然后,此函数会检查此变量是否已存在,如果不存在,它将为其查找一些空闲内存以存储它。在两种情况下都返回变量的地址,这种行为几乎是解析器将调用的每个函数的特征。

常量

一个常量被编译器当作任何变量来处理,只是它的值存储在一个被标记为`__temp__`的内存位置,以便一旦遇到分隔分号就被释放。同时,该值可以用于计算,而最终用户甚至不知道它曾被存储在某个地方。

数组和字符串

 

当需要分配多个内存单元来存储字符串或数组时,标识符存储在一个单元中,并另外分配一个单独的空单元数组,并用__refd__标记,表示此内存应由另一个变量引用。为了跟踪哪些变量是指针,编译器将它们存储在std::map<int,int>中。键是指针的内存位置,int对包含数组的起始位置和与其关联的元素数量。此外,另一个map用于仅存储数组地址及其关联的元素数量。这个map是多余的,因为这些信息已经包含在前面讨论的map中,但它简化了用于垃圾回收的算法。

垃圾回收

当遇到终止分号时,所有临时变量都可以被释放。它们占用的内存可以在下一个表达式中重用。这意味着向量会搜索包含__temp__的条目,然后将其清除。如果临时变量恰好是指针,它也会从指针映射中移除。当所有临时变量都被释放后,也会找到并释放未引用的内存(标记为__refd__)。

赋值与算术

每个操作(由解析器识别)都会导致一个函数调用,该函数调用会生成BF代码。本节将给出一些示例和BF算法来阐明这个过程。为此,我将使用<expr></expr>来表示一个表达式。一个表达式可以是任何东西,从单个变量或常量到变量、常量和运算符的复杂组合。

赋值

赋值的形式为<expr> = <expr> ’;’ ,编译器知道每个表达式的值在内存中的存储位置。我们现在需要将右侧(rhs)的值复制到左侧(lhs)的地址。当这两个地址相邻时,可以使用以下BF算法(假设指针指向rhs元素,并将其值复制到右侧一个单元格)

Memory contents:
| rhs (x) | lhs (y) | tmp (z) |
    * (pointer)

>[-]>[-]<<    // CLEAR the lhs cell and tmp cell
[->+>+<<]     // MOVE the value in rhs to both lhs and tmp
>>[-<<+>>]    // MOVE the value back to rhs

| rhs (x) | lhs (x) | tmp (0) |

当然,在实践中,可用的内存单元并不总是相邻的。它们可能相距很远,并且移动方向也可能不同。幸运的是,由于编译器知道每个变量的存储位置,这可以通过成员函数Compiler::Parser::movePtr(idx)轻松计算出来,它将生成代码以将指针移动到所需的索引。然而,这要求编译器跟踪当前索引,该索引存储在(静态)数据成员中。使用此成员,算法转换为

Function call: assign(lhs, rhs)

movePtr(lhs) [-]
movePtr(tmp) [-]
movePtr(rhs)
[-
  movePtr(lhs)
  +
  movePtr(tmp)
  +
  movePtr(rhs)
]

movePtr(tmp)
[-
  movePtr(rhs)
  +
  movePtr(tmp)
]

加法

对于每个算术运算符,都实现了一个赋值变体,它直接修改其左侧操作数(例如x += 5将5加到x)。常规运算符可以很容易地通过其赋值变体来实现。

<expr> ’+=’ <expr>
Function call: addTo(lhs, rhs)
Memory: | lhs | rhs | tmp |
           *

assign(tmp, rhs) // make a copy of rhs and store it in the temporary
>>[-<<+>>]       // add the value in tmp to the value in lhs

常规加法返回一个临时变量,其中包含两个操作数的和,利用上述addTo()算法。

<expr> ’+’ <expr>
Function call: add(lhs, rhs)
Memory: | lhs | rhs | tmp |

assign(tmp, lhs) // store a copy of the lhs in tmp
addTo(tmp, rhs)  // add the rhs to this temporary
return tmp       // return the result

减法

减法算法是从加法算法中简单地推导出来的,只需将+替换为减号(-)。原则上,可以实现负数,但我的许多实现依赖于使用[- ...]将单元格递减到0。当一个数字已经是负数时,这将导致无限循环。因此,我假设将生成的BF代码翻译成C(或其他语言)的程序在递减单元格之前实现了一个检查,即if (*ptr) --*ptr;。生成的算法可以通过subtractFrom() (-=)和subtract() (-)分别调用。

乘法

乘法通过重复加法实现。要将一个值乘以另一个值,我们只需不断将此值加到自身上,同时递减另一个值,直到它达到零。算法变为

<expr> ’*=’ <expr>
Function call: multiplyBy(lhs, rhs)
Memory: | lhs | rhs | tmp1 | tmp2|

assign(tmp1, lhs)
assign(tmp2, rhs)
movePtr(lhs)
[-]                  // set the lhs to 0
movePtr(tmp2)
[-                   // keep decrementing tmp2
  addTo(rhs, tmp1)     // while adding tmp1 to rhs
  movePtr(tmp2)        // and move back to tmp2
]

常规乘法(返回一个临时值)与我们在加法示例中看到的完全一样,只是将`addTo()`调用替换为`multiplyBy()`。从现在开始,我将省略这些实现。

除法

除法算法使用了前面描述的减法和乘法算法。它首先从分子中减去分母,直到分子变为0,同时计算可以执行此操作的次数。例如,在计算`24/7`时,我们会发现我们可以从`24`中减去`7` 4次,然后才能达到零。然而,由于我们进行的是整数除法,我们希望结果是向下取整而不是向上取整,因此我们通过从分母和计数器的乘积中减去分子(24)来得出余数(`rem == 4 * 7 - 24`)。当余数非零时,计数器会递减以获得向下取整的结果。为了检查变量是否非零,我们将一个临时标志初始化为0,该标志将在条件语句中设置为1。然后将该标志从计数器中减去以获得所需的结果

<expr> ’/=’ <expr>
Function call: divideBy(lhs, rhs)
Memory: | lhs | rhs | cpy | cnt | rem | flag |

assign(cpy, lhs)         // make a copy of the left hand side
movePtr(cpy)
[
  subtractFrom(cpy, rhs)   // keep subtracting the right hand side from this copy
  movePtr(cnt)             // while counting how many times this can be done
  +
  movePtr(cpy)
]

rem = multiply(cnt, rhs) // calculate the product
subtractFrom(rem, lhs)   // and subtract the left-hand side to obtain the remainder
movePtr(rem)
[[-]                     // only if the remainder was nonzero will this loop be entered
  movePtr(flag)
  [-]+                     // set the flag to 1
  movePtr(rem)             // rem is now 0 so the loop is broken
]                        

subtractFrom(div, flag)  // subtract 1 from the answer when the remainder was nonzero
assignTo(lhs, div)       // update the left-hand-side

模运算完全通过现有算法实现。它可能不是最有效的算法,但易于实现

<expr> ’%=’ <expr>
Function call: moduloBy(lhs, rhs)
Memory: | lhs | rhs | tmp |

assign(tmp, lhs)
divideBy(tmp, rhs)
subtractFrom(lhs, multiply(tmp, rhs))

比较和逻辑

在支持条件结构的编程语言中,比较是一个重要的特性。因此,BrainFix支持众所周知的逻辑运算符(&&, ||)以及比较运算符(<, >, <=, >=, ==, !=)。

小于:<

要检查一个表达式是否小于另一个表达式,我们让它们都变为0。当左侧实际达到零时,条件中断。在这种情况下,如果左侧实际上小于右侧,则当循环中断时,右侧尚未为零。这可以使用我们在除法实现中已经看到的标志方法来检查。

<expr> ’<’ <expr>
Function call: lt(lhs, rhs)
Memory: | lhs | rhs | tmp1 | tmp2 | ret |

assign(tmp1, lhs)   // copy both sides to temporaries
assign(tmp2, rhs)

movePtr(tmp1)
[-                  // keep decrementing the lhs
  movePtr(tmp2)     // while also decrementing the rhs
  -                   
  movePtr(tmp1)     // quit the loop when the lhs is 0
]

movePtr(tmp2)       // if the rhs is still nonzero, it was larger
[[-]
  movePtr(ret)
  [-]+              // and ret becomes 1 (otherwise remains 0)
  movePtr(tmp2)
]

大于 (>)

大于号的实现与小于操作相同。唯一的区别是,这次我们当右侧达到零时退出循环,并使用左侧的余数来设置标志。结果调用是gt(lhs, rhs)

(不)相等:(==, !=)

等式检查可以通过前面几节讨论的不等式检查来实现:如果两个表达式既不大于也不小于彼此,则它们相等。因此,我们可以使用这些运算符的结果来在其中任何一个为真时设置标志。请记住,如果设置了此标志,则表示它们不相等,因此我们将标志从1中减去以获得最终结果。

<expr> ’==’ <expr>
Function call: eq(lhs, rhs)
Memory: | lhs | rhs | less | more | flag | ret |

less = lt(lhs, rhs)
more = gt(lhs, rhs)
movePtr(less)             // check if lhs < rhs
[[-]
  movePtr(flag)
  [-]+
  movePtr(less)
]

movePtr(more)             // check if lhs > rhs
[[-]
  movePtr(flag)
  [-]+
  movePtr(more)
]

movePtr(ret)
[-]+                      // set ret to 1
subtractFrom(ret, flag)   // and subtract the flag

不等于运算符的实现与等于运算符相同,只是结果(flag)不需要反转。

小于/大于或等于 (<=, >=)

我们在上一节中看到的原理可以再次用于实现小于/等于和大于/等于运算符。这次唯一的区别是我们可以直接使用标志作为返回值。我将给出小于/等于运算符的实现,并省略其对应部分,它与以下内容类似

<expr> ’<=’ <expr>
Function call: le(lhs, rhs)
Memory: | lhs | rhs | less | same | ret |

less = lt(lhs, rhs)
same = eq(lhs, rhs)

movePtr(less)
[[-]
  movePtr(ret)    // check if lhs < rhs
  [-]+
  movePtr(less)
]

movePtr(same)     // check if lhs == rhs
[[-]
  movePtr(ret)
  [-]+
  movePtr(same)
]

逻辑与 (&&)

逻辑运算符(非位运算符!)可以与任何比较运算符结合,形成复杂的条件。它们的实现再次依赖于设置标志并组合这些标志以获得所需的结果。对于逻辑AND,BF实现如下

<expr> ’&&’ <expr>
Function call: logicAnd(lhs, rhs)
Memory: | lhs | rhs | cpy1 | cpy2 | flag1 | flag2 |

assign(cpy1, lhs)          // copy both operands
assign(cpy2, rhs)

movePtr(cpy1)              // see if the lhs is nonzero
[[-]
  movePtr(flag1)
  [-]+
  movePtr(cpy1)
]

movePtr(cpy2)              // see if the rhs is nonzero
[[-]
  movePtr(flag2)
  [-]+
  movePtr(cpy2)
]

multiplyBy(flag1, flag2)   // multiply the flags

// flag1 == 1 if and only if both lhs and rhs were nonzero

逻辑或 (||)

逻辑或与AND运算符类似,但它将两个标志相加而不是相乘。然而,这可能导致值不是0或1,因此使用第三种算法来获得最终结果。

<expr> ’||’ <expr>
Function call: logicOr(lhs, rhs)
Memory: | lhs | rhs | cpy1 | cpy2 | flag1 | flag2 | ret |

assign(cpy1, lhs)                  // copy the operands
assign(cpy2, rhs)

movePtr(cpy1)                      // see if the lhs is nonzero
[[-]
  movePtr(flag1)
  [-]+
  movePtr(cpy1)
]

movePtr(cpy2)                      // see if the rhs is nonzero
[[-]
  movePtr(flag2)
  [-]+
  movePtr(cpy2)
]

addTo(flag1, flag2)                // add them together

movePtr(flag1)
[[-]
  movePtr(ret)                     // ret becomes 1 if flag1 != 0
  [-]+
  movePtr(flag1)
]

逻辑非 (!)

逻辑非运算符已经隐式地用于相等检查算法的实现中,在那里我们需要将标志从1中减去以获得反转结果。算法是

’!’ <expr>
Function call: logicNot(rhs)
Memory: | rhs | cpy | flag | ret |

assign(cpy, rhs)          // copy the operand
movePtr(cpy)
[[-]
  movePtr(flag)   
  [-]+                    // flag becomes 1 if rhs != 0
  movePtr(cpy)
]

movePtr(ret)
[-]+                      // set the return value to 1
subtractFrom(ret,flag)    // and subtract the flag

流程

BrainFix语言只支持几种流程控制结构:if(-else)for。这足以编写复杂的程序,因此我选择不费心实现更困难的while或switch语句。在未来的程序版本中,它可能仍然会成为一个功能,如果您有兴趣贡献,我将不胜感激!

if-else

为了实现带有可选else子句的if条件,我们必须再次为编译器提供一些额外的成员。问题在于条件表达式只能通过使用方括号在BF中实现,这意味着在闭合方括号处,指针必须位于一个我们知道它为零的单元格。我们可以通过创建一个设置为零的临时变量来做到这一点,但这会浪费内存。相反,充当条件的表达式(ifFlag)的地址存储在std::stack<std::vector<int>>上,以及此条件的反向(elFlag)。在这种情况下,栈是最自然的容器,因为它可以在不费吹灰之力的情况下实现嵌套的if。当解析器遇到if主体结束时,它只需将指针移动到ifFlag(已设置为0)并在一轮“循环”后终止循环。当程序员附加else子句时,编译器会将指针移动到elFlag的位置。为了确保这些标志占用的内存直到if-else作用域结束才被释放,它们的单元格被标记为__stack。一个特殊函数致力于释放与栈顶元素关联的内存:popStack()。当解析器遇到if-else构造的最终闭合大括号时,会调用此函数。

以下示例说明了当解析器遇到if-else语句时会发生什么

if x < 10 && y > 10   // push (ifFlag, elFlag) on the stack
{                     // movePtr(ifFlag) [-
    // body           // parse body as usual
}                     // ]
else                  // movePtr(elFlag)
{                     // [-
    // body           // parse body as usual
}                     // ] popStack()

for

我选择模仿MATLAB的for循环语法,因为它清晰简洁。它也比C语法更容易实现,因为C语法要求反复检查条件。相比之下,MATLAB语法强制程序员为编译器提供起始条件、停止条件和步长,每个都可以像我们在上一节中看到的那样推入栈中。这就是for语句发生的情况

for x = start:step:stop  // start is assigned to x and the addresses of
                            x, step and stop are pushed on the stack. A
                            fourth cell, named flag, is created by calling
                            le(x, stop) to determine if the loop needs to
                            be executed. Start loop: movePtr(flag) [
{
    // body              // parse body as usual
}                        // x, step, and stop are fetched from the stack
                            and step is added to x. The flag is updated
                            by calling le(x, stop) again, and the loop is
                            closed: movePtr(flag) ] popStack()

数组

实现数组被证明是一个挑战,因为我希望程序员能够使用其值在编译时未知变量来索引这些数组,例如scan i; arr[i] = 1;。这成为挑战的原因是,指针必须移动到一个编译器无法知道地址的内存单元格。我需要提出一种算法,该算法可以将指针移动到所需的单元格,在该单元格上执行一些操作,然后移回到某个已知位置。最后一步(移回)至关重要,因为编译器必须始终知道指针指向什么。

获取值

函数调用:arrayValue(array, index)

当元素不需要修改时(即,它用作右值),只需将元素复制到某个已知位置即可。然后,该副本可用于进行进一步的计算(例如,y = 3 * x[i])。为此,最大数组大小设置为256个元素(MAX ARRAY SIZE),并分配一个此大小的缓冲区(buf)。

在此缓冲区中,我们可以通过首先将i复制到第一个(buf[0])和第二个单元格(buf[1]),然后应用以下算法(假设指针最初指向buf[0])来向右移动未知数量的单元格(i

[>[->+<]<[->+<]>-]

此算法本质上是将第二个单元格向前推进,同时递减第一个单元格,导致指针偏移量为i

现在,因为我们知道缓冲区(起始)与实际数组之间的距离,所以我们可以将指针翻译回数组本身,它将精确地停留在所需元素处。我们现在可以应用复制算法将值复制到缓冲区,并使用我们留在最后一个单元格中的值,以相同的量将指针(连同副本)移回。

当算法完成时,我们可以在返回第一个地址的地址之前清除缓冲区的其余部分(不包括第一个元素)。

赋值

函数调用:assignToElement(array, index, value)

同样的策略用于将值赋给数组的一个元素,但这次要赋的值也会在缓冲区中移动(被i量向前推进)。情况将如上图所示,另外一个单元格包含要赋的值。可以使用通常的赋值算法来更改数组中的单元格,并且指针可以移回缓冲区的开头。

算术运算

现在我们有了获取值(arrayValue())和赋值(assignToElement())的工具,对数组元素进行算术运算变得容易。每个算术算法都具有相同的结构

1. 获取元素的副本 (arrayValue())

2. 应用操作(加、减等)

3. 将结果存储在数组中 (assignToElement())

联系方式

欢迎联系我报告错误,或表达您对本软件的感受。

© . All rights reserved.