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






4.96/5 (6投票s)
本文介绍了如何在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中有四种不同的打印关键字:print
、printc
、printd
、prints
。在上一节中,我们已经看到了如何使用prints关键字打印字符串,以及如何使用转义字符打印换行符。print
和printc
命令打印传递给它的表达式的字符表示(根据您系统的区域设置)。因此,也可以使用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
变量
当解析器遇到变量时,它会调用函数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()
)
联系方式
欢迎联系我报告错误,或表达您对本软件的感受。