引导40行Lua代码中的四分之一(2008)

2020-09-14 11:49:58

快速索引:本文的真正要点是提出一种实现第四虚拟机的特定方法;让我们称这种新方法为“基于模式的”。基于模式的Forth的主循环是这样的:

在我们用Lua实现的基于模式的Forth中,我们将其称为“MINFORTH”,可以非常容易地动态添加新的模式。我们将从一个只“知道”一种模式的虚拟机开始--“解释”,这相当于(不到)传统四分之四外部解释器的一半--以及一个字典,它最初只包含一个单词,意思是“读取该行的最末端,并将其解释为Lua代码”;最小的虚拟机可以容纳40行Lua代码,并且足以引导批发式系统。

但是,“为什么是福克斯?”读者会问--“福斯既古老又怪异,为什么我们不能坚持使用现代文明语言,而不是高贵的福斯呢?”你在Forth中还喜欢什么?“-我在这里的感觉是,Forth是两种典型的可扩展语言之一,另一种是Lisp。LISP非常容易扩展和修改,但仅限于一定的限制:它的语法由“read”给出,很难更改(1)。如果我们想在Lisp之上实现一种自由语法的小型语言(如[Bentley]),并且我们知道Forth,我们可能会想,也许正确的工具必须同时具有Lisp和Forth的特征。这就是Lua的用武之地--作为构建可扩展语言的基础语言。

(免责声明:我在本文中使用了松散意义上的术语。(我将在最后一节详细说明这一点。)。

任何“普通的”Forth都有一个交互界面,用户输入一行,然后按“Return”键,然后Forth逐字执行该行,并显示一些输出;我们的MiniForth没有交互界面,但大多数想法仍在继续。这是一个非常简单的程序;普通文本是用户输入,背景较暗的部分是Forth系统的输出。请注意,“单词”是非空格字符上的序列,由空格分隔。

上面的程序可以这样“朗读”:“将5放到堆栈上;运行‘dup’,即复制堆栈顶部的元素;将堆栈顶部的两个元素相乘,用它们的乘积替换它们;打印堆栈顶部的元素并将其从堆栈中移除。”

下面是一个程序,它定义了两个新函数(在Forth行话中称为“word”):

:正方形DUP*;OK:立方体DUP正方形*;OK 5立方体。125 OK(节目2)

可以这样朗读:定义一些新单词:先定义一些新词:先运行DUP,然后乘以;再定义“CUBE”:运行DUP,然后运行平方,然后乘以;现在将5放在堆栈上,将其立方,然后打印结果。

单词Square和Cube在内存中表示为某种字节码;不同的Forth使用不同类型的字节码。在这里,我们更感兴趣的是“间接线程”(参见[ertl]),它将词典存储在内存的单独区域中。一些可能的表示如图1、2和3所示;在这些框图中,所有数字都是十六进制的,为简单起见,我们假定使用大端机器。图4显示了我们将在MiniForth中使用的“字节码”表示。它不完全是字节码,因为存储单元可以容纳任意的LuaObject,而不仅仅是字节-但我们无论如何都会称它为“字节码”,因为滥用了语言。

下面是我们运行CUBE(在程序2和图4中)时发生的情况的跟踪:

RS={5}MODE=HEAD DS={5}HEAD=";DOCOL";RS={7}MODE=FORTH DS={5}INTR=";DUP";RS={8}MODE=FORTH DS={5 5}INTR=1RS={8 1}MODE=HEAD DS={5 5}HEAD=";DOCOL";RS={8 3}MODE=FORTH DS={5}INTR="。退出";RS={9}模式=FORTH DS={5 25}instr=";*";RS={10}MODE=FORTH DS={125}instr=";exit";图5:多维数据集的跟踪(程序2,图4)。

请注意,我们没有单独的指令指针(IP)变量;我们使用返回堆栈的顶部(RS)作为IP。

跟踪的最右边部分总是描述将要执行的内容,而其余部分描述当前状态。因此,在第六行中,我们有RS={8,4},我们将在模式";FORTH";中执行内存[4]中的指令,即“*”。

程序3a是我们引导迷你前进所需的全部程序。它定义了主循环(“run”)、一种模式(“解释”)、字典(“_F”)和字典中的一个单词:“%L”,意思是“将当前行的其余部分求值为Lua代码”。

--保存输入的全局变量:SUBJ=";5 DUP*.";--我们正在解释的(示例)pos=1--其中是(1=";在开头";)--从";pos";和Advance";pos&34;中读取内容的低级函数。--注意:";parseypattern中的";pat";参数。捕获然后是一个空的capture.parseypattern=function(Pat)local capture,newpos=string。Match(subj,pat,pos)if newpos则pos=newpos;return Capture end endparsespaces=function()return parseypattern(";^([\t]*)()";)endparseword=function()return parseypattern(";;^([^\t\n]+)。^([^\n]*)()()";)endparsewordornewline=function()return parseword()或parsenewline()end--A";word";是一个或多个非空格字符的序列。--外部解释器一次读取一个单词并执行它。--请注意,`getwordornewline()或";";';返回一个单词、换行符或&。返回parsewordornewline()end--字典。--值为函数的条目为基元。_F={}_F[";%L";]=function()eval(parserestofline())end--处理器";。它可以是几种模式中的任何一种。--它的初始行为是运行模式[mode]()-即--modes.Interprete()-直到`mode';变成";stop";.mode=";mode={}run=function()而mode~=";stop";do mode[mode]()end--最初处理器只知道这个模式,";解释&##。...--请注意,";Word&34;是一个全局变量。Interpretprimitive=function()if type(_F[word])==";function";Then_F[word]();return true end endInterpretnon primitive=function()return false end--stubInterpretnumber=function()返回false end--stubp_s_i=function()end--打印状态,对于";解释器";(存根)模式。P_s_i()local_=解释原语()或解释非原语()或解释编号()或错误(";无法解释:";..Word)结束--(程序3a:MiniForth的核心)

节目3b是未来的第一个节目。它只定义了“%L”,并且定义了几个新词:在行尾、文本末尾和“[L”,它计算可能跨越多行的Lua代码块;然后它创建一个数据堆栈,并定义对其进行操作的单词“dup”、“*”、“5”和“.”。

--MiniForth中的第一个程序。--它定义换行符的含义(它们是无操作的;继续),--对于";";(";在文本末尾,将模式更改为';停止";),--对于";[L";-读取所有内容,直到";L]";,并解释--";[L";]之间的内容。L]作为Lua代码,使用eval。--然后它创建一个数据堆栈(";DS";)和四个单词--";5";,";DUP";,--";*";,";.";。--subj=[=[%L_F[";\n";]=function()end%L_F[";]=function()mode=";stop";end%L_F[";[L";]=function()eval(parseypattern(";^(.-)%SL]()";))end[L ds={n=0}Push=function(stack,x)stack.n=stack.n+1;stack[stack.n]=x end POP=function(Stack)local x=stack[stack.n。Stack.n=stack.n-1;return x END_F[";5";]=Function()PUSH(DS,5)END_F[";DUP";]=Function()PUSH(DS,DS[DS.n])END_F[";*";]=Function()PUSH(DS,POP(DS)*POP(DS))END_F[";.";]=Function()。..POP(DS)Endl]]=]--现在运行它。没有可见的输出。pos=1mode=";Interprete";run()--此时词典(_F)有八个单词。--(节目3b:MINFORDS中的第一个节目)。

在运行程序3b之后,系统已经足够强大,例如,

这就好像我们手动设置内存(这里是“subj”)和原始机器的寄存器,然后按下它的“Run”按钮;显然,这可以做得更好,但在这里我们有其他优先事项。

程序3a和3b不支持非原语;这将在稍后添加。请看图4;像“square”这样的非原语在字节码中表示为数字-内存[]中的头的地址-我们还没有引入内存,也没有引入状态“头”或“前”。

请注意,非基元的名称不会出现在内存中,只会出现在字典_F中。为了方便起见,在这样的内存图中,我们会将非基元的名称画在它们相应的头下面;在图4中,_F[";Square";]=1 and_F[";cube";]=5。

当内部解释器运行时-即,当模式是";HEAD";或";FORTH";;参见图5-时,处理器在每个步骤读取IP处的存储器的内容并对其进行处理。当外部解释器运行时,在每个步骤中,它从subj读取一个从pos开始的单词,并对其进行处理。这些行为之间有相似之处。

在过去的文学作品中,我从未见过任何关于模式的提及。在FORTH内部解释器的通常描述中,HEAD模式不是独立的;它只是一种短暂的状态,是执行FORTH字的语义的一部分。并且也不存在";解释";和";编译";模式-外部解释器被实现为包含循环的第四个字;它一次读取一个字,并且根据变量STATE的值,它或者解释";或者";编译该字。因此,从某种意义上讲,解释和编译都是虚拟模式。

让我来解释一下我是如何产生这个想法的--以及我试图做什么让我有了这样的想法。

有些词会干扰外部口译员的变量。例如,";:";读取pos所指向的单词-例如,";square";-,将该单词(";square";)的定义添加到字典中,并在控件返回到模式时前进pos;。解释器()变量pos指向square之后的位置-而modes.prestrate()从不尝试处理单词square。显然,这可以用来在Forth之上实现具有任意语法的新语言。

有些字会干扰内部解释器的变量-它们会修改返回堆栈。让我们使用一个更丰富的术语:我们将谈论“吃文本”和“吃字节码”的词。正如我们已经看到的,“:”是一个吞噬文本的单词;数字文字在Forth代码中使用一个吞噬字节码的单词LIT来实现。在下面的节目中,

Memory={";DOCOL";,";LIT";,12,";*";,";EXIT";}--1 2 3 4 5--几十个图6:几十个字节码(程序4)。

当几十个LIT执行时,它会读取紧随其后的12个,并将其放到数据堆栈中;然后,它会更改返回堆栈,以便在主循环的下一步中,IP将是4,而不是3。这是它执行的痕迹;请注意,这里有一个新模式,";LIT";。在MODE";LIT";中执行";Memory[3]中的12的效果是将12放入DS。

RS={1}MODE=HEAD DS={5}HEAD=";DOCOL";RS={2}MODE=FORTH DS={5}instr=";LIT";RS={3}MODE=LEIT DS={5}DATA=12RS={4}MODE=FORTH DS={5 12}INTR=";*";RS={5}MODE=FORTH DS={60}INTR=";EXIT";图7。

LIT基元和LIT模式的Lua中的代码可以从跟踪合成。通过分析第2步和第3步以及第3步和第4步之间发生的情况,我们可以看到LIT和LIT必须是:

_F[";LIT";]=Function()mode=";LIT";endmodes.light=Function()PUSH(DS,内存[RS[RS.n]])RS[RS.n]=RS[RS.n]+1 mode=";FORTH";END

因此,从这一点开始,我们将认为轨迹提供了足够的信息,我们不会显示相应的代码。

请注意,不同的模式从不同的位置读取它们将执行的内容:HEAD、FORFER和LIT从内存读取[RS[RS.n]](它们读取字节码);解释和编译从subj读取的内容,从pos开始(它们读取文本)。我们这里的重点将放在读取字节码的模式和单词上。

在图8中的程序中,单词TESTLITS首先调用LIT,然后调用VLIT;VLIT的行为应该类似于LIT,但是LIT是一个原语,而VLIT不是。

内存={";DOCOL";,";R&>;P";,";P&>;R";,";P&>;R";,";退出";,--1 2 3 4 5--VLIT<;-|";DOCOL";,";LIT&#。Exit";,}--6 7 8 9 10 11--TESTLITS图8:同时使用LIT和VLIT的单词(TESTLITS。

T=0 RS={6}模式=磁头PS={}DS={}Head=";DOCOL";t=1 RS={7}模式=FORTH PS={}DS={}instr=";t=2 RS={8}MODE=LIT PS={}DS={}data=123t=3 RS={9}MODE=FORTH PS={}DS={123}instr=1t=4 RS={10 1}模式。T=6 RS={3}MODE=FORTH PS={10}DS={123}instr=";pCell";t=7 RS={4}Mode=pCell PS={10}DS={123}PDATA=234t=8 RS={4}mode=FORTH PS={11}DS={123 234}instr=";P>;R";t=9 RS={115}Mode=FORTH PS={}DS={。

图8和图9包含完整的解决方案,因此首先忽略内存单元2、3和4,以及TheTrace的t=5到t=8行。从t=5到t=9,我们需要做的是。

其中-1是一个幻数:粗略地说,就是调用VLIT和将读取其文字数据的代码之间堆栈中的“调用帧”数被求反。在其他情况下,这可能是-2,-3...。消除这个神奇数字的一种方法是创建一个新的堆栈-“解析堆栈”(“ps”)-并让“解析词”从ps顶部指向的位置解析字节码;然后,像VLIT这样的单词变成单词pCell的变体,它从内存[ps[ps.n]]读取一个单元并推进ps[ps.n]。图8中的VLIT代码显示已经完成-我们将pCell包装为“R>;P pCell P>;R”-从图9中的跟踪可以推断如何定义这些单词。

注意,从t=2到t=3的转变对应于从t=4到t=10的转变;模式对应于将VLIT的头部地址放在RS的顶部,并且模式是头部,使用此思想,我们可以在Forth中实现虚拟模式。更好的是:如果我们认为模式是一个始终位于RS顶部之上的不可见元素,那么一切都会变得简单一些。因此,一个虚构的模式将被翻译、扩展为1(VLIT的头部),再加上一个模式,或者类似于VLIT的另一个单词,只需将模式切换到#34;VLIT,而该单词的作用将是将其扩展到VLIT的头部,再加上模式#34;Head&34;。(注:VLIT的头部是VLIT的头部),该词的作用是将其扩展到VLIT的头部,加上模式(HEAD&34;34;);或者,类似于VLIT的另一个词,将只是将模式切换到VLIT的头部,并且该词的作用将是将其扩展到VLIT的头部,再加上模式。

具有固定数值系数的多项式在内存中可以先表示这些系数的个数,然后表示每个系数的值;例如,P(X)=2x^3+3x^2+4x+5.5表示为{...,4,2,3,4,5.5,...}。我们称它为系数的表示数,然后称系数为“多项式的数据”。让我们从一个像pCell一样工作的基元PPOLY开始,因为它从内存中读取多项式的数据,从位置Ps[PS.n]开始,每一步前进Ps[PS.n]。这个PPOLY从数据堆栈的顶部获取一个值-在我们的示例中是10个-并将其替换为对其应用P的结果-P(10),即2345.5。

我们得到一个吞噬字节码的单词;对Poly的调用应该后跟多项式的数据,就像LIT后面跟数字一样。我们还可以做其他的事情:我们可以创建新的头部,DOPOLY和DOADDR,并将多项式表示为两个头部,后面跟着多项式的数据。图10中测试此想法的程序具有图11中的跟踪。

内存={";DOPOLY";,";DOADDR";,4,2,3,4,5.5,--1 2 3 4 5 6 7--P(X)&;P(X)--^。}--8 9 10 11 12--TESTDOPOLY(图10:将10放入堆栈,调用P(X))。

RS={8}模式=HEAD PS={}DS={}HEAD=";DOCOL";RS={9}MODE=FORTH PS={}DS={}instr=";RS={10}MODE=LIT PS={}DS={}DATA=10RS={11}MODE=FORTH PS={}DS={10}INTR=1RS={12 1}MODE=HEAD PS={}DS={10}HEAD="。RS={12 FORTH}MODE=pPolyn PS={3}DS={10}n=4RS={12 FORTH}MODE=PDERC PS={4}DS={10}n=4 acc=0 coef=2RS={12 forth}mode=ppolyc ps={5}ds={10}n=3 acc=2 coef=3Rs={12 th}mode=ppolyc PS={6}ds={10}n=2 acc=23 coef=4。(图11:图10中程序的跟踪)。

上面的跟踪没有显示&;P(X)的作用;运行&;P(X)的效果是将多项式的数据起始地址(即3)放入数据堆栈。请注意,在Forth中,一个多项式(在大多数其他语言中是一段被动数据)是如何表示为共享其数据的两个程序P(X)和&;P(X)的。与Lua中的闭包情况进行比较-由同一母函数创建的两个闭包,并引用该母函数的本地变量,共享向上的值。

这是另一个例子。让&39;s写“=>;”表示“暗示”,写“&;”表示“and”。然后这个,

在命题微积分中是一个“公式”或“命题”;顺便说一句,它是一个重言式,即总是正确的。

在某些情况下,例如,如果我们想要找到该命题的证明,或者如果我们想要评估其真值,以便将真值分配给P、Q和R,我们需要参考该公式的子公式。如果我们用波兰语符号(而不是反向波兰语符号)用字节码表示它!你明白为什么吗?)。然后这就变得微不足道了:

内存={";=&>;";,";=";Q";,";R";,";=&>;";,";&;&34;,";P";,";Q";,";&;&34;,";P";,&。R";}--1 2 3 4 5 6 7 8 9 10 11(图12b:图11中的命题用波兰语表示。)。

子公式现在可以用数字来指代--它们在内存中的起始位置。我们可以编写一个单词来解析一个命题,从内存中的某个位置开始;如果该位置包含二进制连接词,如“=>;”或“&;”,则该词调用自身两次来解析连接词的";左侧&34;和";右侧的子公式。如果单词通过将其存储在表公式中来记住结果结构,那么重新解析从位置开始的公式(比方说,6)会变得非常迅速:结果是公式[6],并且。

.