音译老式基础

2021-07-25 23:33:41

偶尔,我会遇到一个 1980 年代的 BASIC 程序,通常是为 Commodore 64、Apple ][ 或 TRS-80 编写的。我想将它音译成现代语言,重构它并探索它。这些旧程序很好地介于重构汇编语言和重构现代语言之间。我们将研究一种作为重构起点的翻译方法。在某些 BASIC 中,可以使用冒号 (:) 将两个或多个语句放在同一行。如果冒号跟在 THEN 子句之后,则这些语句是 THEN 的一部分。常用命令包括:​​assignment、END、FOR、GOSUB、GOTO、IF..THEN、POKE、PRINT、REM(备注,用于注释)、RETURN 不太常用的命令包括:​​DATA(定义常量数据)、DEF(创建单行)函数)、DIM(维度 – 创建数组)、INPUT、ON..GOTO(计算的 GOTO)、READ(来自 DATA 语句)、RESTORE(重置 DATA 语句)、STOP(可重新启动的停止)。为了转换行号,我们可以将整个代码放入一个带有 switch 语句的循环中。这实际上是一个状态机(参见参考资料),其中的线代表状态。 var line = 1 while line != 0 { switch line { case 10: // 第 10 行的代码 goto(20) break case 20: // 第 20 行的代码 goto(30) break : default: line += 1 } }

引用行号的 IF、GOTO 或 GOSUB 等语句可以查询或设置行变量。 END 语句可以将行号强制为 0。其他语句将其设置为最后一步(或失败)。如果冒号前出现 NEXT 或 GOSUB,则“:”后的代码将是目标,需要将行分开。在这些情况下,我会给拆分部分一个唯一的负行号。负行号不是合法的 BASIC,但这适用于我们的 switch 语句。不同的 BASIC 对变量名有不同的规则。所有旧的都至少支持单字母或字母数字不区分大小写的变量名(X,B9)。大多数都支持以 $ 结尾的字符串(A$,与 A 不同)。大多数允许您创建一维数组(使用 DIM 语句)。一些 BASIC 允许您使用两个字母字符作为变量名称。有些(奇怪)让你使用更长的名字,但只保留前两个字符,如果你不小心在名字中形成了一个关键字,它会被使用。 OSI 手册(参见参考资料)给出了一个变量 ANEW 的例子,它在执行时擦除程序,因为这就是“NEW”所做的。一些 BASIC 仅支持整数,其他支持浮点数(并且不区分整数,JavaScript 风格)。在某些情况下,您可以推断类型(int 或 float)或可能的值范围。 10 X = 53520 :100 如果 X > 53500 那么 X=X-1110 如果 X < 53530 那么 X=X+1

由于该值以整数开头,并且只对整数进行加减运算,因此我们知道它是整数,而不是浮点数。由于 if 语句,我们知道 X 的范围是 53500…53530。这对于内存映射 I/O 非常有用——例如,PEEK 和 POKE 访问屏幕内存。了解变量的范围可能会告诉我们它是否仅限于屏幕内存甚至特定行。其中 STEP 子句是可选的,NEXT 语句中的变量也是可选的。变量表示要关闭哪个循环;没有变量意味着关闭最里面的活动循环。循环需要一个堆栈来跟踪嵌套循环。为了模拟这一点,我们将在堆栈上保留一个元组: FOR: If var is in the stack { - // 重新启动循环,从堆栈中删除 var 的条目——某些语义也可能会弹出内部循环 } push tuple set var = start -- 许多 BASIC 至少执行一次循环体 -- (即使 start > end) -- 我不知道其他人如何确定循环结束的位置 goto(loop-body) NEXT X: Pop until variable X is在堆栈顶部 — 如果未找到则未定义 句柄为普通 NEXT

下一步:对于堆栈顶部的变量: — undefined if none var = var + skip if var <= end then { goto(tuple.loop-body) } else { pop ; goto(exit-target) } — NEXT 之后的行 如果循环的文本范围很明确,您可以避免所有这些并将其音译为“常规”for 循环。这是语用学对语义学的胜利。语义上说它没有任何意义,可以删除。实用主义者说“这些 BASIC 没有优化,所以这会造成延迟。”我会将这些转换为对 usleep() 的调用。旧的 BASIC 的子程序不是递归的,它们不传递参数,因此它们在技术上不需要堆栈。也就是说,堆栈仍然是实现它们的简单方法。将 GOSUB 后面的语句的行号放入堆栈。如果 GOSUB 位于“:”行,则它可能必须是生成的行号。请注意,RETURN 不是在文本上匹配的,而是在动态遇到 RETURN 语句时找到的。一旦我们音译了代码,我们通常希望将其重构为现代风格。

虽然最初是作为一个笑话提供的(参见参考资料中的 Lawrence),但分析 COME FROM 很有用——控制从哪里转移? 10 GOTO 50 :50 X=060 GOSUB 500: Y=1070 GOTO 50 :500 Z=10 : 550 RETURN 因为第 60 行包含一个 :,我们将其拆分。 (如果它是“IF”的一部分,我们必须更加小心。) 10 GOTO 50 :50 X=0 *** 来自 10 或 7060 GOSUB 50065 Y=10 *** 来自 55070 GOTO 50 : 500 Z=10 *** 来自 60 : 550 RETURN 从这个分析中我们得到三个有用的东西:单入口代码块、单目标代码块和关于子程序的信息。从一个“来自”到下一个的代码只有一个入口点,尽管它可能有多个出口点。我们只需要保留块的第一行号。因此,我们的翻译可以包括“案例 50”下的两个陈述。

由于我们想要进行的重构之一是使代码“结构化”,因此消除不必要的行号有助于我们专注于更大的结构。 50 GOTO 80 60 line 60 stuff 70 GOTO 100 80 line 80 stuff 90 line 90 stuff 100 etc 如果第80行是第50行的唯一目标,我们可以将第80行和第90行向上移动以替换第50行: 50 line 80 stuff 51 line 90 stuff 52 GOTO 100 — 在 60 line 60 stuff 之前是隐含的 — 大概是某个 GOTO 的目标;省略第 70 行并落入第 100 行等等,也就是说,可以将一个单一目标的代码块上拉到到达它的地方。 (只需确保处理显式目标和语句之间的失败。)我们获得有关子例程调用者的信息。如果只有一个调用者,我们可以通过将 GOSUB 更改为 GOTO,并将 GOSUB 后面的语句更改为 GOTO 来内联它。许多旧代码使用 PEEK 和 POKE 来处理与系统相关的方面:屏幕、键盘、图形模式、偶尔的汇编语言方法。我将在以后的文章中探讨这一点。

旧的 BASIC 并不漂亮,但翻译和现代化一些旧代码很有趣。克拉克,R.劳伦斯。 “对 GOTO-less 编程的语言贡献”,CACM,1984 年 4 月,第 349-350 页。