编写80年代风格的BASIC解释器





几年来,我一直在从事一个个人项目,该项目旨在创建(并实际研究)“伪仿真器”,即用JavaScript编写的针对不存在的计算机的仿真器。该机器被认为是为了纪念1980年代和90年代的八位和十六位计算机。



但是,我喜欢这种复杂性:该机器还使用了一组新的指令。它类似于那个时代使用的工具包,但是使用起来稍微容易一些。追溯者出生了。多年来,仿真器一直在扩展和改进,但很可能永远不会“完成”(毕竟,这是一个个人研究项目)。@bbcmicrobot



出现时,我想为Retroputer创建类似的内容。我的JS开发技能主要限于前端,因此这是获得一些后端经验的绝佳借口。只有一个问题:Retroputer只能理解其自己的汇编语言。它还没有BASIC支持。



因此,我想出了80年代风格的BASIC解释器,即在编写时完全使用汇编语言。我认为值得分享我的工作,因为我们不必经常去研究与通常的抽象相去甚远的领域。我的日常工具(JavaScript)使许多方面变得微不足道,有时甚至看起来像魔术。了解最低级别的流程通常有助于理解这些抽象。



因此,让我们开始吧。



倒数机特性



  • 16 ROM (KERNEL) c 1 (scratch space)
  • 512 , 64
  • 16- 6516, 512 4 64
  • 4025, ,
  • 1125
  • 9800




当我为Retroputer编写汇编程序时,可以使用非常方便的Pegjs工具。它提供了快速的本机汇编语法,但不幸的是,Retroputer ASM没有这样的东西。也就是说,我们将不得不走艰难的道路。



解析本身是分几个阶段执行的。使用编译器的语言将代码解析为抽象语法树(AST)(或其他表示形式),然后可以使用该树生成完成的本机代码。因此,该程序在语法上必须正确才能成功编译。



一些现代的解释器也有此概念,因为生成中间AST并执行它(而不是源代码)通常很有用。



但是对于资源有限的机器上的BASIC解释器,分几个阶段进行解析将是最有效的,其中一些阶段是在运行时进行的。但是,这意味着直到您运行程序并导航到带有错误的代码区域,才能检测到语法错误。



Retroputer BASIC解析包括三个阶段:



  1. 转换字符串
  2. 代币化
  3. 运行时语法检查


前两个阶段发生在用户输入程序(或下载程序)时。后者在程序执行期间发生。基本上,前两个创建了飞机的机身,但不保证它会起飞。最后一站是试飞员-我们希望他能起飞,但是直到他尝试之前我们不确定。



注意: Retroputer BASIC源代码(正在开发中)可在GitHub上获得



转换字符串



这是整个过程中最简单的部分。用户输入的字符串将转换为大写,以简化(并加快)后续过程。BASIC不区分大小写,因此我们可以利用它。



<pre>print 2+2
' becomes:
PRINT 2+2</pre>


用JavaScript做到这一点非常容易,对吧?



theLine = theLine.toUpperCase();


但是在汇编语言中,此过程更为详细。我们需要读取字符,将其转换为大写字母,然后将其存储在某处。



           ld  y,  0                # y is our index
_loop:     ld  al, [d, x, y]        # [d,x] is pointer to string
           cmp al, 97               # is al (char) in range?
           brs n   _continue        # Not a lowercase char; continue
           cmp al, 123              # high portion
           brs !n  _continue        # not a lowercase char; continue
           and al, 0b1101_1111      # uppercase!
           st  [d, x, y], al        # store back (we modify the original)
_continue: inc y                    # move our index along
           cmp al, 0                # check for NULL
           brs !z _loop             # No? Go back for more.


上面的代码与JavaScript版本的代码的语义不太匹配。一个重要的区别是,今天我们使用Unicode来处理文本,因此将输入从小写转换为大写通常会更加困难,有时甚至是不可能的(取决于语言)。 Retroputer生活在ASCII世界中(更确切地说,在它自己的ASCII变体RetSCII中),也就是说,所有受支持的字符都以8位编码。对于许多语言来说,这是完全不够的,但是可以满足当时的现实。



这也意味着我们可以使用可爱的ASCII功能将小写转换为大写。大写字母“ A”用ASCII代码表示,65小写字母“ a”用代码表示97...如果您熟悉两个的幂,那么您应该已经注意到了这种差异。



因此,事实证明,小写字母由比小写字母的数量多32个数字的数字指定。如果我们知道该值在正确的范围内,则只需减去32!



这种方法有效,但是我们可以稍微修改一下位。对于Retroputer,这不会比减法更快,但是,在没有减法的情况下,在计算过程中我们不必注意进位/借位标志。事实证明,我们可以使用逐位and关闭位来代替32值。



and al, 0b1101_1111         # turn off bit in 32-place
# versus
clr c                       # clear carry
sub al, 32                  # subtract 32


但是有一个微妙之处:并非所有内容都可以转换为大写。例如,如果用户添加了字符串文字,那么我们需要格外小心,因为我们不希望Retroputer BASIC不断大声喊叫用户。(虽然那个时代的许多计算机都没有小写字母,但回溯器没有。)



例如:



print "Hello, World!"
' should become:
PRINT "Hello, World!"
' and not
PRINT "HELLO, WORLD!"


这意味着我们需要跟踪我们是否处于字符串文字中间。在BASIC中,对此只有一个名称:双引号。如果字符是双引号,我们可以设置标志,并根据标志的值将其转换为大写字母或将其保留不变。



事实证明JavaScript对此没有内置功能,但是我们可以自己创建它:



const len = theLine.length;
let insideString = false;
for (let i = 0; i < len; i++) {
    const ch = theLine[i];
    if (ch === `"`) insideString = !insideString;
    if (!insideString) {
        const newCh = ch.toUpperCase();
        if (ch !== newCh) theLine[i] = newCh;
    }
}


现在,尽管我们再次使用了JS中的Unicode支持,但是JS中的逻辑与汇编程序版本更加匹配。



汇编器版本如下所示:



           ld  y,  0                # y is our index
           ld  bl, 0                # === insideString (false)
_loop:     ld  al, [d, x, y]        # [d,x] is pointer to string
           cmp al, 34               # is al a double quote?
           brs !z  check_char       # no? should we uppercase it?
           xor bl, 0xFF             # yes? toggle insideString
_check_char:
           cmp bl, 0xFF             # inside a string?
           brs z   _continue        # yes? don't modify it
           cmp al, 97               # is al (char) in range? "a"
           brs n   _continue        # Not a lowercase char; continue
           cmp al, 123              # high portion           "z"
           brs !n  _continue        # not a lowercase char; continue
           and al, 0b1101_1111      # uppercase!
           st  [d, x, y], al        # store back (we modify the original)
_continue: inc y                    # move our index along
           cmp al, 0                # check for NULL
           brs !z _loop             # No? Go back for more.


到目前为止,我们仅完成了将输入文本转换为大写字母的操作,但是可以将字符串文字的定义用于另一种可能性。我们可以再进行一次语法检查!



如果在处理结束时找出inString仍然是true(bl = 0xFF)的内容,则可以返回错误,因为这意味着字符串中某处存在不完整的字符串文字。



注意:事实证明,许多BASIC版本都比较宽容,因为缺少字符串的右引号。我在创建自己的解释器时发现了这一点。即使是这样,在我看来也不对,所以Retroputer BASIC不允许这样做。



代币化



解析的下一步是将输入字符串转换为更有效的内容,以供Retroputer BASIC解释程序执行。我们正在尝试尽可能接近抽象语法树的概念,但是结果仍然不是树。但这将是我们可以在运行时快速处理的事情。



早期微型计算机的一个共同特征是内存非常有限。 Retroputer当时的内存比大多数标准计算机都要大,但仍然比现代计算机要少得多。因此,如果长时间的BASIC程序在用户输入期间保存,很容易占用过多的内存。



为了节省空间,在程序进入内存期间对关键字进行标记...此过程将关键字转换为单字节令牌。关键字总是至少两个字节长,因此键入时可以节省更多。这也意味着我们可以在运行时使用查找表来调用适当的汇编语言例程。



但是,Retroputer BASIC比当时的大多数BASIC版本走得更远。此外,它将数字转换为二进制形式,标记字符串,计算变量引用等等。老实说,这浪费了更多的空间,但是速度(和易于实现)的好处远大于此。



因此,此处使用以下步骤:







  1. , , . , , , , .




  2. , , , . , PRINT "Hello, World" «Hello, World» , .



    , .




  3. , , , . JavaScript, !



    ( ). , PRINT !




  4. Retroputer BASIC ( ). . , , .



    Retroputer BASIC . , . , Retroputer BASIC.


在这篇文章中,我不会详细介绍该阶段用汇编语言实现的过程,我会留待以后使用。但是不用担心,那里有很多代码



在运行时检查语法



最后但并非最不重要的一点是,在运行时进行语法检查。将代码转换为标记形式后,实现起来非常容易。



首先,在运行时, BASIC检查它当前是否正在处理令牌。所有令牌均启用了最高有效位(即,它们的值大于等于128)。如果找到了令牌,则可以通过在向量表中找到它来确定要调用的子例程。这也使显示语法错误变得很简单-有些关键字作为运算符没有意义,因此向量表仅指向生成语法错误的过程。



调用运算符令牌处理程序后,该处理程序将承担所有其他解析工作。对于令牌,它可以利用它们之间的过渡gettokgettok-rawpeektok等。如果令牌不是该过程所期望的,则该过程仅返回错误代码。正是在这个阶段,同时捕获语法错误和类型匹配错误。



如果操作员必须表达式求值,则执行另一阶段的解析。解析表达式时,将使用另一个向量查找表,也就是说,我们可以拦截与数学表达式无关的关键字并返回相应的错误。例如,如果您尝试输入PRINT 2+CLS,则在上出现语法错误CLSCLS-这是“清晰屏幕”的简写)。



注意:从此表中,我们还可以确定数学运算符的优先级和所需参数的数量。这对于表达式的实际求值很重要,但是它也可以用于捕获用户未提供足够参数的情况。



由于令牌直接映射到向量查找表条目,因此可以相当快速且省力地执行程序。解析每种类型的运算符的任务都委托给处理程序本身,通常,这不会引起任何特定的问题。也许最困难的解析是PRINTINPUT,但在每个步骤中一次只能获取一个令牌。



由于许多检查直到程序运行时才执行,因此这意味着在错误发生之前我们可能会得到部分结果。例如:



PRINT "Hello";CLS
Hello
?Syntax Error


另外,这意味着如果程序在屏幕上无法看到文本的状态下出现,则可能在消除错误方面存在问题。如果显示了语法错误,但我们没有看到它,该怎么办?



这种检查语法的方法当然有其缺点,但是它允许使用相当简单的解释器。



标记用户输入



如前所述,该时代的BASIC版本通常使用标记化策略。为了节省空间并提高执行速度,将关键字“压缩”为单字节标记(如果需要更多关键字,则压缩为双字节)。



假设我们有一行简单的代码可以清除屏幕并打印标准问候语:



CLS: PRINT "Hello, World"


尽管这本身并不会占用太多内存,但是如果您编写一个长程序,则该程序中的许多单词将成为关键字。如果压缩它们(标记化),则可以节省一部分空间。例如,对上面显示的行进行标记化之后,内存中将出现以下内容:



8A E3 B5 FC Hello, World 00 00


将其转换回原始结构应该不太困难:



字节 关键词 笔记
8A

CLS

E3 “:” 施工结束
32 我们最多存储一个空间
B5

打印



FB 输入代码 接下来是字符串文字
你好,世界,00 字符串以null结尾
00 代码行尾 代码行也以null终止


您可能会认为这是一项繁重的工作,但是节省的空间可能非常可观。在示例中这并不是特别明显,但是即使从中您也可以想象节省的空间可以累积多快。在这种情况下,压缩结果为19个字节。源文本由26个字节组成(包括终止的空字节)。



当BASIC解释器在用户程序内存很少的机器上运行时,节省空间就变得很重要。因此,即使没有额外的好处,这种压缩也是非常重要和有吸引力的。



那么我们如何标记这样的东西呢?刚开始,JavaScript似乎微不足道。使用字符串数组,您可以将每个关键字快速替换为相应的标记。真正?



似乎这是一份工作String#replace使用幼稚的方法,您可以尝试如下操作:



const tokens = ["CLS", "PRINT", ":" ];
function tokenize (inStr) {
    const newStr = inStr;
    tokens.forEach((token, idx) => newStr = newStr.replace(
        new RegExp(token, "g"), String.fromCharCode(128+idx)
    );
    return newStr;
}


不幸的是,我们很快意识到我们不能用标记代替字符串文字。这意味着您需要在考虑上下文的​​情况下逐个字符地进行处理,以免压缩不是关键字的元素。



这个新算法更接近Retroputer中的汇编语言代码,但是JS仍然使事情变得容易得多。这是JS代码的开头(我们将在本文中逐步进行扩展):



const tokens = ["CLS", "PRINT", ":" ];

function tokenize(inStr) {
    let out = [];                    // return value is "crunched" array
    let idx = 0;                     // index into inStr
    let ch = "";                     // current character
    while (idx < inStr.length) {
        ch = inStr.charCodeAt(idx);  // get the current character code
        
        // our logic is going to go here

        out.push(ch);                // for now push the character
        idx++;                       // and keep going (next char)
    }
    out.push(0);                     // we end with NUL
    return out;
}


我们从一个非常简化的令牌列表开始,因为没有人希望以这种格式查看整个表(该表很长,而Retroputer实际上是从JS文件创建令牌表的!),但是就我们的目的而言,这就足够了。此处的原理是,当我们看到标记时,会将其索引写入输出数组。



至此,我们有了一个函数,目前,该函数仅将字符串转换为其等效的字符代码。尽管它不是特别有用,但可以用作方便的框架。



汇编语言版本与上面显示的非常相似。



  do {
    call _get-source-index     # get next character index (in y)
    dl := <BP+source>,y        # read next char from our source string (ch)
    call _adv-source-index     # advance our source index
    call _get-target-index     # get position in target   ("out" in JS)
    <BP+target>,y := dl        # store to target          (out[y] = ch)
    call _adv-target-index     # move it along
    cmp dl, 0                  # was char 0? if so, break
  } while !z


我没有包含在上面的示例_get-source-index或其他函数中,因为它们的任务在名称上很清楚:它们只是获取,设置源索引和目标索引,以及在它们之间导航。值得注意的是,与JS不同,汇编语言中没有动态分配的数组,因此此算法分配了足够大小的缓冲区。在输入行中导航时,我们需要知道在目标缓冲区中将下一个标记写入哪里,以及目标索引在上面做什么。以上每个函数都返回中的索引Y例如,该函数_adv-target-index将目标索引增加一个(等于y++)。



小心文字



我们需要注意:字符串文字可能会使令牌生成器感到困惑-我们不能简单地将每个与关键字匹配的字符串替换为令牌值。如果我们看到其中包含“ CLS”的字符串文字,则我们不应该寻求对其进行标记化。它不应该是可执行的,如果我们输出它,那么我们将输出对应于令牌的字节而不是它。这很可能不是开发人员的意思。



另一方面,数字文字不应该出现此问题,因为BASIC没有数字关键字。即便如此,面对数字时搜索关键词毫无意义-为什么要浪费时间呢?



标记字符串文字



因此,让我们首先检查行是否开始-在JS中,这并不是特别困难:



if (ch === 34) {
  out.push (0xFB);             // string-in-code token
  idx++;
  ch = inStr.charCodeAt(idx);  // get next character after the quote
  while (ch !== 34 && idx < inStr.length) {
    out.push(ch);
    idx++;
    ch = inStr.charCodeAt(idx);
  };
  idx++;                       // go past the quote
  out.push(0);                 // end of string
  continue;
}


双引号由字符代码34表示。其他编程语言可以识别更多样式的双引号(例如JS或C),但使用BASIC则很简单:双引号或什么都不是。



当我们看到一个字符串开始时,我们可以简单地获取其余字符并添加到输出数组中,直到遇到另一个引号为止。



读取整个字符串后,由于Retroputer BASIC使用C风格的字符串,因此需要添加一个零字节;如果要使用Pascal风格的字符串,则可以存储一个计数器并插入字符串文字的长度。无论如何,这不会引起任何特殊问题。我使用以null终止的字符串的唯一原因是它们很容易在汇编语言中使用-我们只需要比较一个null字节,而不存储计数器。



因此JavaScript并不难。我想你们大多数人都会决定使用更抽象的东西,例如正则表达式。我认为这段代码将使我们的意图更加明显。



if (ch === 34) {
  out.push (0xFB);             // string-in-code token
  const stringLiteral = inStr.substr(idx+1).match(/^[^"]*/)[0];
  idx += stringLiteral.length+1;
  out.push(...Array.from(stringLiteral, ch => ch.charCodeAt(0)));
  idx++;                       // go past the quote
  out.push(0);                 // end of string
  continue;
}


上面的代码大致相同,但是我们让JS只是执行,而不是逐字符验证match。我们知道会得到一个匹配项(我们在一个字符串中),因此我们甚至不需要检查匹配是否成功。然后,我们将索引增加字符串的长度,并将字符复制到输出数组中。在我看来,此代码更容易理解(但是,它意味着对正则表达式以及ES6语法的特殊性以及箭头函数的理解)。



当然,在汇编语言中,我们必须手动完成JS所做的所有工作。结果与我们第一次编写JS代码的尝试非常相似:



  cmp dl, constants.QUOTE         # is dl a quote?
  brs !Z not-a-string             # nope; not a string

  call _get-target-index          # get next position in crunch target
  dl := brodata.TOK_CODE_STRING   # "String" token
  <bp+target>,y := dl             # Store it into the crunch target
  call _adv-target-index

still-a-string:
  call _get-source-index
  dl := <bp+source>,y             # get string character
  call _adv-source-index
  cmp dl, constants.QUOTE         # if it's a quote, we can zero it
  if Z { 
    dl := 0 
  }
  call _get-target-index
  <bp+target>,y := dl             # write to target
  call _adv-target-index
  cmp dl, 0                       # are we done?
  brs !Z still-a-string           # no, keep going
  continue                        # next!
not-a-string:


值得添加有关Retroputer程序集解析器的注释-它具有对块和循环等高级构造的基本支持。因此,if Z {…}当设置了零标志时,它将在块内执行内容,并且continue可以用于分支回到块的开头(break也用于退出块)。汇编程序将其转换为各种比较和分支指令,因此CPU看不到代码的高级部分,但是使代码的编写更加容易。



标记数字



另外,尝试在关键字列表中搜索数字也没有意义,因此我们可以跳过它们。 BASIC的大多数版本都执行与上面显示的字符串处理类似的操作-如果读取的字符是数字,它将被连接到输出,然后压缩程序将接管输出。



Retroputer BASIC(以及某些其他BASIC版本,例如Atari BASIC)进一步迈进了一步:它将数字转换为适当的二进制格式。这很容易做到-如果我们遇到一个数字,那么我们将累加器乘以10并加一个数字,只要这种数字继续出现,就以这种方式继续。 (不过,值得注意的是,尽管Retroputer BASIC仅适用于整数值,但在我的待办事项列表中已经添加了浮点数。)



(我必须说,当整数溢出发生时,无论是有符号整数还是无符号整数,当前Retroputer BASIC均不执行任何操作。当我添加浮点数时,Retroputer也会转换为浮点数。)



Retroputer BASIC向前迈了一步。 :它还识别十六进制数并将其转换为等效的二进制数。它用作此类数字的名称0x(如JS中的语言),并且语言本身具有其他逻辑,以确保如果指定了多个字符,则返回错误x



在汇编语言中,检查字符是否为数字并不难,但是需要进行两次比较:您需要确保字符代码在0x30之间。0x39... (这些是字符代码“ 0”和“ 9”。)



接收到字符数字后,我们可以使用字符集的另一个便捷属性。0x30-这是字符代码“ 0”,0x31-代码“ 1”,依此类推。如果需要的话0x30我们可以减去,但是有一个更简单的方法:只删除四个最高有效位:



and dl, 0b0000_1111


不幸的是,如果我们需要处理十六进制数字,这无法正常工作。在这种情况下,您可以减去然后与10进行比较,如果代码大于10(假定十六进制数字为大写“ A”-“ Z”),则可以再将其减7。



在JS中,您可以再次使用正则表达式,然后当我们获得数字的匹配项时,可以调用Number(),这给我们带来了另一个好处:十六进制数字也可以进行简单转换,因为Number()如果数字以开头,十六进制数字将自动转换0x



它在JavaScript中的外观如何?



if (ch >= 48 && ch <= 57) {
  out.push (0xFD);           // number token
  const numberLiteralStr = inStr.substr(idx).match(/^[\d|A-F|a-f|x|X]+/)[0];
  idx += numberLiteralStr.length;
  const numberLiteral = Number(numberLiteralStr);
  const bytes = new Uint8Array(new Uint16Array([numberLiteral]).buffer);
  out.push(...bytes)
  continue;
}


正则表达式允许您使用数字或十六进制数字的任意组合(加号x可切换到十六进制模式)。该表达式不是很理想,例如,它允许您使用几个x,但是现在就足够了。



在上面的代码中,将数字转换为字节可能是有问题的。Number()完成了将数字字符串转换为JavaScript可以使用的数字的所有艰苦工作,但是现在我们需要将其表示为字节序列。您可以使用数学来做到这一点:



const hiByte = (numberLiteral & 0xFF00) >> 8;
const loByte = (numberLiteral & 0x00FF);


...并且很容易对整数进行运算。但是,由于使用了JS类型的数组,您可以避免所有这些计算,但是同时可以为将来处理浮点数做准备(我们将简单地替换Uint16ArrayFloat64Array)。



此任务的汇编语言代码稍长一些,但功能更多。Retroputer还使用了另一种优化方法:如果数量较小,则以较小的格式保存。这意味着0-255可以存储在一个字节中,而较长的数字则占用两个字节。



搜索关键词



因此,我们已经做了很多工作,但是还没有压缩单个关键字。处理数字和字符串文字后,我们可以确保其他所有内容都是关键字或变量名。 (或空格,但这很容易检查。)



在BASIC中,关键字并不总是以字母字符开头;运算符和定界符也被视为标记。但是变量也以字母字符开头。因此,我们无法立即确定是否要压缩关键字或变量。这适合我们-如果在令牌列表中找不到匹配项,我们将假定这是一个变量。



那么,我们如何验证潜在关键字确实是关键字?如果我们使用JavaScript编写,则可以使用Array#findIndex



const tokenMatch = tokens.findIndex(token => 
  inStr.substr(idx).startsWith(token));
if (tokenMatch > -1) {
  out.push(tokenMatch + 128);
  idx += tokens[tokenMatch].length;
  continue;
}


该方法Array#findIndex迭代遍历数组tokens,我们可以检查数组是否以要检查的标记开始inStr(由当前数组开始idx)。使用简化的令牌列表,我们将执行以下操作(假设inStr.substr(idx)===”PRINT”):



代币 .startsWith(令牌)? 指数
CLS 0
打印 真正 1个


注意:indexOf和JS一样,如果什么也没找到,我们得到-1



如果找到匹配项,则可以将其索引存储在返回的数组中。但是,我们以后如何区分令牌和符号呢?简单:打开最显著位,这可以通过添加128的标记值来完成



(注:如果我们需要超过128多个符号,那么对于一些标记,我们将不得不使用两个字节这复杂的事情一点点,但没有这么多。某些版本的BASIC会执行此操作,例如,各种Microsoft BASIC。)



我们已经完成了使用JavaScript的工作,但是如何用汇编语言来完成呢?



事实证明几乎是一样的,但是代码会更长。



search-keywords:
  bl := [d, x]                 # get first character of current token
  cmp bl, constants.NUL        # if it's NUL, we've exhausted the list
  brs Z exit-keyword-search    # so we're clearly not a keyword
  clr Z                        # compare strings, but with partial equality
  call [vectors.STRCMP]        # so that our source doesn't need NUL between
                               # tokens; c will now be how many chars got 
                               # compared
  if !Z {
    do {                       # no match; advance past our token in the list
      inc x                    # character by character
      bl := [d, x]             # tokens are terminated with NULs
      cmp bl, constants.NUL    # so if we see it, we can stop
    } while !z
    inc x                      # go past the token #
    inc x                      # in the lookup table
    brs search-keywords        # and keep looking for a match
  }
  clr c                        # found the match!
  add x, c                     # c is the length of the match
  inc x                        # past the NUL
  bl := [d, x]                 # bl is now the token #
  call _get-target-index
  <bp+target>,y := bl          # write the token #
  call _adv-target-index
  call _get-source-index       # advance past the token in the source
  clr c
  add y, c                     # by adding the character count
  dec y                        # back off by one (we'll advance again later)
  call _set-source-index
  continue


好吧,看起来还不错。算法几乎相同,只是汇编语言令牌表的结构略有不同。看起来像这样:



CLS   00 80
PRINT 00 81
:     00 82


每个关键字以一个空字节结尾,后跟一个令牌号。



但是,我们在这里缺少一些重要的东西-完全如何进行字符串比较?在Retroputer的核心处有一个过程STRCMP可以使用,但是它看起来像什么?



strcmp: {
  enter 0x00
  push a
  push b
  push d
  push y
  pushf
  if Z {
    bl := 0x00                  # Checking for full equality
  } else {
    bl := 0x01                  # only checking for partial equality
  }
_main:
  y := 0                        # start of string
top:
  cl := [d, x, y]               # character in string A
  al := <bp+4>,y                # character in string B
  cmp bl, 0x01                  # check if we're doing full equality
  if Z {
    cmp cl, 0                   # we're not, so check for an early nul
                                # in string b
    brs Z strings-are-equal     # if it's NUL, we calling them equal
  }
  cmp cl, al                    # check character
  if Z {
    cmp cl, 0                   # equal, but check for NUL
    brs Z strings-are-equal     # NUL reached, strings are equal
    inc y                       # next character
    brs top                     # not NUL, so keep going...
  }
 # if here, the strings aren't equal
  if N {
    popf                        # string is less than
    set N
    clr Z
    brs _out
  } else {
    popf                        # string is greater than
    clr N
    clr Z
    brs _out
  }
strings-are-equal:
  popf
  clr N                         # Not less than
  set Z                         # but Equal
_out:
  c := y                        # make sure we know how many chars
                                # were compared
  pop y
  pop d
  pop b
  pop a
  exit 0x00
  ret
}


我不了解您,但是我开始越来越喜欢String#startsWithJS中的方法是的,它做同样的事情,但是我们不必编写所有内部逻辑!



变量处理



密钥压缩现已完成,因此我们可以完成。但是Retroputer BASIC向前迈出了又一步-标记化变量。在我看来,只有80年代和90年代的BASIC版本执行了此操作,因为在内存有限的情况下,它可能是有害的。但是,如果有很多内存,则变量的标记化有助于提高性能。



这是Retroputer BASIC的作用:



  • 它最多读取变量名的前两个字符。(由于内存限制,这是该时代的BASIC版本的标准不便。)
  • 根据这两个字符,它确定变量的索引。“ A”是变量0,“ A0”是变量53,依此类推。这个方程很简单,但是现在我们不感兴趣。
  • BASIC . , $ BASIC . .
  • BASIC , . !


(注意:当Retroputer BASIC学习动态地为变量分配内存时,我们将用指向变量的指针来替换索引。待办事项列表中的另一项!)



这使得在运行时查找变量的速度变得异常快:我们不必每次都解析变量名并计算索引当我们遇到一个变量时。在连续的周期中,节省的费用可观。但这付出了高昂的代价:我们需要将指针和变量名称存储在一起,因此对于遇到的每个变量,我们需要在输出中添加四个额外的字节。



由您决定是否值得。



即便如此,在JavaScript中也很容易确定字符流的其余部分是否为变量:



const variableMatch = inStr.substr(idx).match(/^[A-Z]+[A-Z0-9]*[\$]*/);
if (variableMatch) {
  const variableName = variableMatch[0];
  idx += variableName.length;
  // tokenize it (omitted; nothing new here)
  continue;
}


我不会详细描述Retroputer当前用于标记变量代码,它太冗长。当我为变量添加动态内存分配时,我们将回到它。



令牌化完成



如果现在使用JS测试代码,我们将得到一个字节数组,该字节数组与Retroputer BASIC内部使用的字节数组类似:



console.log(toHex(tokenize(`CLS: PRINT "Hello, World"`)));
// 80 82 20 81 20 FB 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 00 00 


哇,节省几个字节需要做很多工作。但是,当只有几千字节的可用内存时,这是值得的!但这不是压缩用户输入的文本唯一优点,我们还可以加快程序执行速度。



为了解释为什么会发生这种情况,我们需要了解Retroputer如何执行代码。我现在不会详细介绍,但简而言之,执行代码需要以下内容:



  • 从内存中获取一个字节
  • 如果一个字节的最高有效位已打开,则它是一个令牌,否则是语法错误(或NUL;无论如何,这是我们在程序行中结束的地方)
  • 我们正在数组中寻找令牌处理程序-该数组包含指向处理令牌的函数本身的指针
  • 我们称为处理程序(它负责接收参数等)
  • 我们再重复一遍


这是标记关键字的另一个原因-执行关键字逻辑变得微不足道,只需在数组中找到地址并调用它即可。



我想强调一点,尽管令牌化对于节省空间很重要,但它也可以提高执行速度。



例如,JS运行循环可能看起来像这样:



const handlers = [ cls, print, endOfStmt ];
bytes.forEach(byte => handlers[byte] && handlers[byte]());


当然,实际上并不是所有的事情都那么简单,而是更多,但这已经是另一篇文章的主题!



资源资源








广告



具有DDoS保护和最新硬件的强大VPS所有这些都与我们的史诗服务器有关最大配置为128个CPU内核,512 GB RAM,4000 GB NVMe。






All Articles