在Conway的《人生游戏》中制作俄罗斯方块的工作游戏


992

这是一个理论上的问题-在任何情况下都无法给出简单的答案,即使是琐碎的问题也无法解决。

在Conway的《生命游戏》中,存在诸如metapixel之类的结构,这些结构使“生命游戏”也可以模拟任何其他“生命游戏”规则系统。另外,众所周知,生命游戏是图灵完成的。

您的任务是使用Conway的生活游戏规则构建一个细胞自动机,该规则将允许您玩俄罗斯方块游戏。

您的程序将通过手动更改特定世代的自动机状态来表示中断(例如,向左或向右移动,放下,旋转或随机生成新的片段以放置到网格上),计数来接收输入特定数量的世代作为等待时间,并将结果显示在自动机上的某个位置。显示的结果必须明显类似于真实的俄罗斯方块网格。

您的程序将按以下顺序进行评分(较低的标准充当较高标准的决胜局):

  • 边界框大小-面积最小且完全包含给定解的矩形框将获胜。

  • 对输入的更改较小-需要手动调整的最少单元格(对于自动机中最坏的情况)会获胜。

  • 最快的执行速度-最少的一代可以在模拟中获胜。

  • 初始活细胞计数-较小的计数获胜。

  • 先发布-早先发布。


95
“示范性工作实例”是指可以在几个小时内运行的事物,还是可以被证明是正确的事物,即使它需要花费到宇宙的热死才能发挥出来?
彼得·泰勒

34
我很确定这样的事情是可能并且可以玩的。仅有极少数的人具有能够编程可能是世界上更深奥的“汇编语言”之一的专业知识。
贾斯汀L.13年

58
这个挑战正在努力中! 聊天室 | 进展 | 博客
mbomb007 '16

49
截至今天上午5:10(世界标准时间9:10),此问题是PPCG历史上第一个获得100票却没有得到答案的问题!大家做得好。
Joe Z.

76
我正在尝试解决这个问题...现在,当我上床睡觉时,到处都可以看到滑翔机,碰撞成一个巨大的烂摊子。我的睡眠充满噩梦,搏动的十项全能运动阻碍了我的步伐,而赫歇尔人也在不断发展以吸引我。约翰·康威,请为我祈祷...
昏暗

Answers:


937

这开始是一个探索,但结束时是一次冒险。

寻求Tetris处理器,2,940,928 x 10,295,296

模式文件的所有荣耀都可以在这里找到,可以在浏览器中查看

该项目是过去1和1/2年中许多用户努力的结晶。尽管团队的组成随时间变化,但撰写本文时的参与者如下:

我们还要感谢7H3_H4CK3R,Conor O'Brien和其他为解决这一难题而付出努力的用户。

由于此次合作的规模空前,因此该答案分为多个部分,由该团队的成员撰写。每个成员将撰写有关特定子主题的文章,大致对应于他们最参与的项目领域。

请在团队所有成员之间分配所有赞扬或赏金。

目录

  1. 总览
  2. 元像素和VarLife
  3. 硬件
  4. QFTASM和Cogol
  5. 汇编,翻译与未来
  6. 新语言和编译器

还可以考虑查看我们的GitHub组织,在该组织中,我们已将所有编写的代码作为解决方案的一部分。有关问题,请联系我们的开发聊天室


第1部分:概述

该项目的基本思想是抽象。与其直接开发Life中的俄罗斯方块游戏,不如通过一系列步骤逐步提高抽象性。在每一层,我们都远离生活的困难,更接近于像其他任何程序一样容易编程的计算机的构造。

首先,我们使用OTCA元像素作为计算机的基础。这些元像素能够模拟任何“栩栩如生”的规则。 WireworldWireworld计算机是该项目的重要灵感来源,因此我们试图用元像素创建类似的构造。尽管不可能用OTCA元像素来模拟Wireworld,但可以为不同的元像素分配不同的规则,并建立与导线功能相似的元像素排列。

下一步是构建各种基本逻辑门,以作为计算机的基础。在这个阶段,我们已经在处理类似于现实世界处理器设计的概念。这是“或”门的示例,此图像中的每个单元实际上都是整个OTCA元像素。您可以看到“电子”(每个代表一个数据位)进入和离开门。您还可以看到我们在计算机中使用的所有不同的元像素类型:B / S为黑色背景,B1 / S为蓝色,B2 / S为绿色,B12 / S1为红色。

图片

从这里开始,我们为处理器开发了一种体系结构。我们花了很大的精力来设计一种既不深奥又尽可能容易实现的体系结构。Wireworld计算机使用了基本的传输触发体系结构,而该项目使用的是更加灵活的RISC体系结构,其中包含多种操作码和寻址模式。我们创建了一种汇编语言,称为QFTASM(俄罗斯方块装配问题),它指导了处理器的构造。

我们的计算机也是异步的,这意味着没有全局时钟控制计算机。相反,数据在计算机周围流动时带有时钟信号,这意味着我们只需要关注计算机的本地而不是全局时序。

这是我们处理器架构的说明:

图片

从这里开始,只需在计算机上实现俄罗斯方块即可。为了帮助完成此任务,我们研究了将高级语言编译为QFTASM的多种方法。我们有一种称为Cogol的基本语言,这是第二种更高级的语言正在开发中,最后我们还有一个正在建设中的GCC后端。当前的Tetris程序是用Cogol编写的。

最终的Tetris QFTASM代码生成后,最后的步骤是从该代码组装到相应的ROM,然后从元像素组装到底层的生命游戏,从而完成我们的构建。

运行俄罗斯方块

对于那些希望在不干扰计算机的情况下玩俄罗斯方块的人,可以在QFTASM解释器上运行俄罗斯方块源代码。将RAM显示地址设置为3-32,以查看整个游戏。这是方便的固定链接:QFTASM中的Tetris

游戏特色:

  • 所有7个Tetrominoes
  • 运动,旋转,软滴
  • 行清除和计分
  • 预览片
  • 玩家输入注入随机性

显示

我们的计算机将Tetris板表示为内存中的网格。地址10-31显示面板,地址5-8显示预览片段,地址3包含乐谱。

输入项

通过手动编辑RAM地址1的内容来执行游戏输入。使用QFTASM解释器,这意味着直接写入地址1。在解释器页面上查找“直接写入RAM”。每次移动仅需要编辑RAM的单个位,并且在读取输入事件后将自动清除此输入寄存器。

value     motion
   1      counterclockwise rotation
   2      left
   4      down (soft drop)
   8      right
  16      clockwise rotation

计分系统

通过单回合清除多行即可获得奖励。

1 row    =  1 point
2 rows   =  2 points
3 rows   =  4 points
4 rows   =  8 points

14
@ Christopher2EZ4RTZ此概述文章详细介绍了许多项目成员所做的工作(包括概述文章的实际撰写)。因此,适合作为CW。我们还试图避免一个人有两个职位,因为那会导致他们获得不公平的代表数量,因为我们试图使代表保持一致。
Mego

28
首先+1,因为这是一个了不起的成就(尤其是因为您是在生活游戏中构建了一台计算机,而不仅仅是俄罗斯方块)。其次,计算机有多快?俄罗斯方块游戏有多快?它甚至可以远程播放吗?(再次:这太棒了)
苏格拉底凤凰城

18
真是疯了 立即为所有答案+1。
scottinet

28
对于任何希望在答案上分配小额赏金的人来说,这是一个警告:您每次必须将赏金数额加倍(直到您达到500),因此,除非该数额是500代表,否则一个人不能给每个答案相同的数额。
Martin Ender

23
这是我曾经很少了解的最伟大的事情。
Engineer Toast

678

第2部分:OTCA元像素和VarLife

OTCA元像素

OTCA元像素
来源

OTCA Metapixel是康威生命游戏一个结构,它可以用来模拟任何生命般元胞自动机。正如LifeWiki(上面链接)所说,

OTCA元像素是由Brice Due构建的2048×2048周期35328单位单元。它具有许多优点...包括能够模拟任何栩栩如生的细胞自动机,以及缩小时打开的事实。和OFF电池很容易区分...

什么生命般元胞自动机在这里意味着本质是细胞的诞生和细胞存活根据多少他们的八个相邻小区的活着。这些规则的语法如下:B后面是将导致出生的活动邻居的数量,然后是斜杠,然后是S后面是将使该单元存活的活动邻居的数量。有点罗word,所以我认为一个例子会有所帮助。可以用规则B3 / S23表示经典的生命游戏,该规则说,具有三个活邻居的任何死细胞都将存活,而具有两个或三个活邻居的任何活细胞都可以存活。否则,细胞死亡。

尽管是2048 x 2048单元,但OTCA元像素实际上具有2058 x 2058单元的边界框,原因是它在每个方向上都与对角邻域重叠五个单元。重叠的细胞用于拦截滑翔机-发出滑翔机是为了向相邻的元细胞发出信号,以防止它们干扰其他元像素或无限期地飞翔。出生和生存规则是通过在两列的特定位置(一个用于出生,另一个用于生存)中特定位置存在或不存在食者的情况下,在元像素左侧的特殊单元格中编码的。至于检测相邻小区的状态,这是如何发生的:

然后,一个9-LWSS流围绕该单元顺时针方向移动,从而为触发蜂蜜反应的每个相邻“上”单元丢失了LWSS。丢失的LWSS的数量是通过将另一个LWSS从相反方向撞入前LWSS的位置来检测其位置来计算的。这种碰撞会释放滑翔机,如果食者没有出生/生存状况,则会引发另一或两个蜜糖反应。

可以在其原始网站上找到有关OTCA元像素各方面的详细信息:它是如何工作的?

VarLife

我构建了一个类似于生命规则的在线模拟器,您可以在其中使任何单元格按照任何生命规则进行行为,并将其称为“生命变异”。为了更简洁,此名称已缩写为“ VarLife”。这是它的屏幕截图(链接到这里:http : //play.starmaninnovations.com/varlife/BeeHkfCpNR):

VarLife屏幕截图

显着特点:

  • 在生/死之间切换单元,并使用不同的规则对板进行绘制。
  • 开始和停止仿真以及一次执行一步的能力。也可以按照每秒刻度数和每秒毫秒数框中设置的速率,以最快或更慢的速度执行给定数量的步骤。
  • 清除所有活动单元格或将板完全重置为空白状态。
  • 可以更改单元和电路板的尺寸,还可以水平和/或垂直缠绕环形。
  • 永久链接(将所有信息编码在url中)和短url(因为有时信息太多,但无论如何它们还是不错的)。
  • 具有B / S规范,颜色和可选随机性的规则集。
  • 最后但同样重要的是,渲染gif!

render-to-gif功能是我最喜欢的功能,因为它需要花费大量的工作来实现,所以当我终于在早上7点破解它时,它确实令人满足,并且因为它使与他人共享VarLife结构非常容易。

VarLife基本电路

总之,VarLife计算机仅需要四种细胞类型!共计八个状态,分别计算死亡/活动状态。他们是:

  • B / S(黑色/白色),由于B / S单元永远无法存活,因此充当所有组件之间的缓冲区。
  • B1 / S(蓝色/青色),是用于传播信号的主要细胞类型。
  • B2 / S(绿色/黄色),主要用于信号控制,确保其不会反向传播。
  • B12 / S1(红色/橙色),在一些特殊情况下使用,例如交叉信号和存储一点数据。

使用此简短网址打开已经编码的以下规则的VarLife:http ://play.starmaninnovations.com/varlife/BeeHkfCpNR 。

电线

有几种具有不同特性的不同导线设计。

这是VarLife中最简单,最基础的电线,蓝色的条带与绿色的条带相邻。

基本线
简短网址:http : //play.starmaninnovations.com/varlife/WcsGmjLiBF

该导线是单向的。也就是说,它将杀死试图向相反方向传播的任何信号。它也比基本电线窄一格。

单向线
简短网址:http : //play.starmaninnovations.com/varlife/ARWgUgPTEJ

对角线也存在,但根本没用。

对角线
简短网址:http : //play.starmaninnovations.com/varlife/kJotsdSXIj

盖茨

实际上,有很多方法可以构造每个单独的门,因此,我将仅展示每种类型的一个示例。第一个gif分别演示了AND,XOR和OR门。这里的基本思想是,绿色单元格的作用类似于AND,蓝色单元格的作用类似于XOR,红色单元格的作用类似于OR,周围的所有其他单元格都可以正确控制流量。

AND,XOR,OR逻辑门
简短网址:http : //play.starmaninnovations.com/varlife/EGTlKktmeI

AND-NOT门(缩写为“ ANT门”)被证明是至关重要的组件。当且仅当没有来自B的信号时,它才是通过A信号的门。因此,“ A AND NOT B”。

与非门
简短网址:http : //play.starmaninnovations.com/varlife/RsZBiNqIUy

虽然不完全是一扇门,但跨线瓦仍然非常重要且有用。

电线交叉
简短网址:http : //play.starmaninnovations.com/varlife/OXMsPyaNTC

顺便说一下,这里没有NOT门。这是因为没有输入信号,就必须产生恒定的输出,这种输出不能与当前计算机硬件所需的各种时序配合良好。无论如何,我们相处得很好。

同样,有意设计了许多组件以适合11乘11的边界框(图块),从该图块进入图块离开图块需要11个滴答信号。这使组件更具模块化,并且可以根据需要更轻松地连接在一起,而不必担心为间隔或时序调整导线。

要查看在探索电路组件的过程中发现/构造的更多门,请查看PhiNotPi的博客文章:Building Blocks:Logic Gates

延迟组件

在设计计算机硬件的过程中,KZhang设计了多个延迟组件,如下所示。

4点延迟: 简短网址:http : //play.starmaninnovations.com/varlife/gebOMIXxdh
4滴答延迟

5分钟延迟: 短网址:http : //play.starmaninnovations.com/varlife/JItNjJvnUB
5滴答延迟

8点延迟(三个不同的入口点): 短网址:http : //play.starmaninnovations.com/varlife/nSTRaVEDvA
8滴答延迟

11分钟的延迟: 短网址:http : //play.starmaninnovations.com/varlife/kfoADussXA
11滴答延迟

12点延迟: 简短网址:http : //play.starmaninnovations.com/varlife/bkamAfUfud
12滴答延迟

14分钟的延迟: 短网址:http : //play.starmaninnovations.com/varlife/TkwzYIBWln
14滴答延迟

15-TICK延迟(通过比较,验证): 短网址:http://play.starmaninnovations.com/varlife/jmgpehYlpT
15滴答延迟

好吧,这就是VarLife中基本电路组件!有关计算机的主要电路,请参见KZhang的硬件文章


4
VarLife是该项目最令人印象深刻的部分之一;与Wireworld相比,它具有多功能性和简单性。OTCA Metapixel似乎比所需的要大很多,是否有任何尝试将其击倒?
primo

@primo:Dave Greene似乎正在为此工作。chat.stackexchange.com/transcript/message/40106098#40106098
El'endia Starman

6
是的,本周末在512x512 HashLife友好型元单元(conwaylife.com/forums/viewtopic.php?f=&p=51287#p51287)的心脏上取得了不错的进展。可以将元单元做得稍小一些,具体取决于缩小时要用多大的“像素”区域来表示单元状态。不过,绝对值得在一个精确的2 ^ N大小的磁贴处停下来,因为Golly的HashLife算法将能够使计算机运行得更快。
Dave Greene

2
不能以“浪费”较少的方式实施电线和闸门吗?电子将由滑翔机或宇宙飞船(取决于方向)来表示。我已经看到了将它们重定向(并在必要时从一个更改为另一个)的安排,以及一些使用滑翔机的门。是的,它们占用了更多空间,设计更加复杂,并且计时需要精确。但是一旦有了这些基本的构建基块,它们就应该足够容易地组装在一起,并且它们所占用的空间将比使用OTCA实现的VarLife少得多。它也会运行得更快。
Heimdall

@Heimdall虽然可以很好地工作,但在玩俄罗斯方块时却表现不佳。
MilkyWay90

649

第3部分:硬件

利用我们对逻辑门和处理器的一般结构的了解,我们可以开始设计计算机的所有组件。

解复用器

解复用器或解复用器是ROM,RAM和ALU的关键组成部分。它基于某些给定的选择器数据将输入信号路由到多个输出信号之一。它由3个主要部分组成:串行到并行转换器,信号检查器和时钟信号分配器。

我们首先将串行选择器数据转换为“并行”。这是通过策略性地分割和延迟数据来完成的,以使数据的最左位与时钟信号在最左边的11x11正方形相交,数据的下一位与时钟信号在下一个11x11正方形相交,依此类推。尽管每11x11平方将输出每一个数据位,但每一个数据位仅与时钟信号相交一次。

串并转换器

接下来,我们将检查并行数据是否与预设地址匹配。我们通过在时钟和并行数据上使用AND和ANT门来做到这一点。但是,我们需要确保也输出并行数据,以便可以再次进行比较。这些是我想到的门:

信号检查门

最后,我们只是分割时钟信号,堆叠一堆信号检查器(每个地址/输出一个),然后有一个多路复用器!

复用器

只读存储器

ROM应该以地址作为输入,并在该地址处发送指令作为其输出。我们首先使用多路复用器将时钟信号定向到指令之一。接下来,我们需要使用一些导线交叉和或门来生成信号。导线的交叉使时钟信号可以沿指令的所有58位向下传输,并且还允许生成的信号(当前并行)向下通过ROM向下输出。

ROM位

接下来,我们只需要将并行信号转换为串行数据,即可完成ROM。

并行至串行转换器

只读存储器

当前,ROM是通过在Golly中运行脚本来生成的,该脚本会将汇编代码从剪贴板转换为ROM。

SRL,SL,SRA

这三个逻辑门用于移位,它们比典型的AND,OR,XOR等复杂。要使这些门工作,我们将首先延迟时钟信号适当的时间,以引起“移位”。在数据中。提供给这些门的第二个参数指示要移位的位数。

对于SL和SRL,我们需要

  1. 确保12个最高有效位未打开(否则输出仅为0),并且
  2. 根据4个最低有效位将数据延迟正确的数量。

这可以通过一堆AND / ANT门和一个多路复用器来实现。

SRL

SRA稍有不同,因为我们需要在移位期间复制符号位。为此,我们将时钟信号与符号位进行“与”运算,然后使用分线器和“或”门将输出复制一堆。

SRA

置位复位(SR)锁存器

处理器功能的许多部分都依赖于存储数据的能力。使用2个红色B12 / S1电池,我们可以做到这一点。这两个单元可以彼此保持接通,也可以保持在一起。使用一些额外的置位,复位和读取电路,我们可以制作一个简单的SR锁存器。

SR锁存器

同步器

通过将串行数据转换为并行数据,然后设置一堆SR锁存器,我们可以存储整个数据字。然后,要再次获取数据,我们可以读取并重置所有锁存器,并相应地延迟数据。这使我们能够在等待另一个时存储一个(或多个)数据字,从而使在不同时间到达的两个数据字同步。

同步器

读计数器

该设备跟踪需要从RAM寻址多少次。它使用类似于SR锁存器的设备(T触发器)来完成此操作。每当T触发器接收到输入时,它都会更改状态:如果打开,则关闭,反之亦然。当T触发器从开到关翻转时,它会输出一个输出脉冲,该脉冲可被馈送到另一个T触发器中以形成2位计数器。

两位计数器

为了制作读取计数器,我们需要通过两个ANT门将计数器设置为适当的寻址模式,并使用计数器的输出信号来决定将时钟信号定向到何处:ALU或RAM。

读计数器

读队列

读取队列需要跟踪哪个读取计数器向RAM发送了输入,以便可以将RAM的输出发送到正确的位置。为此,我们使用一些SR锁存器:每个输入一个锁存器。当信号从读取计数器发送到RAM时,时钟信号被分频并设置计数器的SR锁存器。然后,RAM的输出与SR锁存器进行“与”运算,来自RAM的时钟信号将SR锁存器复位。

读队列

ALU

ALU的功能类似于读取队列,因为它使用SR锁存器来跟踪发送信号的位置。首先,使用多路复用器设置与指令的操作码相对应的逻辑电路的SR锁存器。接下来,将第一个和第二个自变量的值与SR锁存器进行“与”运算,然后传递到逻辑电路。时钟信号在锁存器通过时将其重置,以便可以再次使用ALU。(大多数电路被打倒了,并且推入了大量的延迟管理,所以看起来有点混乱)

ALU

内存

RAM是该项目中最复杂的部分。它需要对存储数据的每个SR锁存器进行非常具体的控制。为了读取,地址被发送到多路复用器中并发送到RAM单元。RAM单元输出并行存储的数据,然后将其转换为串行并输出。为了进行写入,将地址发送到不同的多路复用器中,将要写入的数据从串行转换为并行,并且RAM单元在整个RAM中传播信号。

每个22x22元像素RAM单元都具有以下基本结构:

RAM单元

将整个RAM放在一起,我们得到的内容如下所示:

内存

放在一起

使用所有这些组件和概述中描述的通用计算机体系结构,我们可以构建一台可运行的计算机!

下载: - 成品俄罗斯方块电脑 - ROM创建脚本,空计算机,主要发现计算机

电脑


49
我只想说,无论出于何种原因,我认为这篇文章中的图片都非常漂亮。:P +1
HyperNeutrino

7
这是我所见过的最神奇的事情。...如果可以的话,我会+20
FantaC

3
@tfbninja可以,那就是赏金,可以给200点声望。
法比安·罗林(FabianRöling)

10
该处理器是否容易受到Spectre and Meltdown攻击?:)
Ferrybig '18

5
@Ferrybig没有分支预测,所以我对此表示怀疑。
JAD

621

第4部分:QFTASM和Cogol

架构概述

简而言之,我们的计算机具有16位异步RISC哈佛体系结构。手动构建处理器时,实际上需要RISC(精简指令集计算机)体系结构。在我们的情况下,这意味着操作码的数量很少,更重要的是,所有指令的处理方式都非常相似。

作为参考,Wireworld计算机使用了传输触发的体系结构,其中唯一的指令就是MOV通过写入/读取特殊寄存器来执行计算。尽管这种范例导致了非常易于实现的体系结构,但结果也是无法使用的边界:所有算术/逻辑/条件运算都需要三个指令。对我们来说很明显,我们希望创建一个不那么神秘的架构。

为了在增加可用性的同时保持处理器简单,我们做出了一些重要的设计决策:

  • 没有寄存器。RAM中的每个地址均被平等对待,并且可用作任何操作的任何参数。从某种意义上讲,这意味着所有RAM都可以像寄存器一样对待。这意味着没有特殊的加载/存储说明。
  • 同样,记忆映射。可以写入或读取的所有内容都共享一个统一的寻址方案。这意味着程序计数器(PC)为地址0,而常规指令和控制流指令之间的唯一区别是控制流指令使用地址0。
  • 数据以串行方式传输,以并行方式存储。由于我们计算机的基于“电子”的性质,当数据以串行小字节序(最低有效位在前)的形式传输时,加减法非常容易实现。此外,串行数据消除了对繁琐的数据总线的需要,而繁琐的数据总线对于正确地计时来说确实很宽且繁琐(为了使数据保持在一起,总线的所有“通道”必须经历相同的传输延迟)。
  • 哈佛架构,意味着程序存储器(ROM)和数据存储器(RAM)之间的划分。尽管这确实降低了处理器的灵活性,但这有助于优化大小:程序的长度比我们所需的RAM量大得多,因此我们可以将程序拆分为ROM,然后集中精力压缩ROM ,这是只读的时要容易得多。
  • 16位数据宽度。这是最小的两个电源,比标准的俄罗斯方块板(10块)宽。这使我们的数据范围为-32768至+32767,最大程序长度为65536个指令。(2 ^ 8 = 256条指令足以完成我们可能希望玩具处理器执行的大多数简单操作,而不是俄罗斯方块。)
  • 异步设计。并非由中央时钟(或等效地,几个时钟)来决定计算机的时序,而是所有数据都带有“时钟信号”,该时钟信号与数据在计算机周围流动时并行传输。某些路径可能比其他路径短,尽管这会给中央时钟设计带来困难,但异步设计可以轻松处理可变时间操作。
  • 所有指令大小相等。我们认为,其中每条指令具有1个操作码和3个操作数(值目标)的体系结构是最灵活的选择。这包括二进制数据操作以及条件移动。
  • 简单的寻址模式系统。具有多种寻址模式对于支持诸如数组或递归之类的功能非常有用。我们设法用一个相对简单的系统实现了几种重要的寻址模式。

概述文章中包含我们架构的说明。

功能和ALU运作

从这里开始,就可以确定处理器应该具有的功能。特别注意了易于执行以及每个命令的多功能性。

有条件的举动

有条件的移动非常重要,既可作为小型控制流程,也可作为大型控制流程。“小规模”是指其控制特定数据移动执行的能力,而“大规模”是指其用作将控制流转移到任意代码段的条件跳转操作。没有专用的跳转操作,因为由于内存映射,有条件的移动既可以将数据复制到常规RAM,又可以将目标地址复制到PC。由于类似的原因,我们还选择了无条件移动和无条件跳转:两者都可以作为条件移动实现,且条件已硬编码为TRUE。

我们选择了两种不同类型的条件移动:“如果不为零则移动”(MNZ)和“ 如果不为零则移动”(MLZ)。从功能上讲,它MNZ等于检查数据中的任何位是否为1,而MLZ等于检查符号位是否为1。它们分别对等值和比较有用。我们之所以选择这两个而不是诸如“如果零则移动”(MEZ)或“如果大于零则移动”(MGZ)之类的原因是,这MEZ将需要从一个空信号中创建一个TRUE信号,而这MGZ是一个更复杂的检查,需要符号位为0,而其他至少一位为1。

算术

就指导处理器设计而言,第二重要的指令是基本的算术运算。正如我前面提到的,我们使用的是低字节序的串行数据,其字节序的选择取决于加/减运算的难易程度。通过使最低有效位先到达,算术单元可以轻松跟踪进位。

我们选择对负数使用2的补码表示法,因为这会使加法和减法更加一致。值得注意的是,Wireworld计算机使用了1的补码。

加法和减法是我们计算机对本机算术支持的范围(除移位外,稍后将进行讨论)。其他运算(例如乘法)过于复杂,无法由我们的体系结构处理,因此必须以软件实现。

按位运算

我们的处理器具有ANDORXOR可以执行您期望的指令。NOT我们选择了一条“非”(ANT)指令,而不是一条指令。NOT指令的困难再次在于,它必须从缺乏信号的情况下产生信号,这对于细胞自动机来说是困难的。ANT仅当第一个参数位为1且第二个参数位为0时,该指令才返回1。因此,NOT x它等效于ANT -1 x(以及XOR -1 x)。此外,ANT它具有通用性,并且在屏蔽方面具有主要优势:在“俄罗斯方块”程序中,我们可以使用它来擦除四聚体。

移位

移位操作是ALU处理的最复杂的操作。它们接受两个数据输入:要移动的值和要移动的值。尽管它们很复杂(由于移动量可变),但是这些操作对于许多重要任务至关重要,包括俄罗斯方块中涉及的许多“图形”操作。位移也将成为高效乘法/除法算法的基础。

我们的处理器具有三个移位操作,“左移”(SL),“逻辑右移”(SRL)和“算术右移”(SRA)。前两个移位(SLSRL)将所有零填充为新的比特(这意味着向右移位的负数将不再为负)。如果移位的第二个参数超出了0到15的范围,则结果将全为零,这可能与您期望的一样。对于最后一个移位,,SRA该移位保留输入的符号,因此用作真正的二分之一。

指令流水线

现在是时候讨论该体系结构的一些具体细节了。每个CPU周期包括以下五个步骤:

1.从ROM获取当前指令

PC的当前值用于从ROM中获取相应的指令。每条指令具有一个操作码和三个操作数。每个操作数由一个数据字和一种寻址模式组成。从ROM读取这些部分时,它们会彼此分开。

操作码是4位,可支持16个唯一的操作码,其中分配了11个:

0000  MNZ    Move if Not Zero
0001  MLZ    Move if Less than Zero
0010  ADD    ADDition
0011  SUB    SUBtraction
0100  AND    bitwise AND
0101  OR     bitwise OR
0110  XOR    bitwise eXclusive OR
0111  ANT    bitwise And-NoT
1000  SL     Shift Left
1001  SRL    Shift Right Logical
1010  SRA    Shift Right Arithmetic
1011  unassigned
1100  unassigned
1101  unassigned
1110  unassigned
1111  unassigned

2.将一条指令的结果(如有必要)写入RAM

根据前一条指令的条件(例如,条件移动的第一个参数的值),执行写操作。写入的地址由上一条指令的第三个操作数确定。

重要的是要注意,在取指令之后进行写操作。这导致创建分支延迟时隙,在该分支延迟时隙中,紧接分支指令之后执行的指令(写入PC的任何操作)将代替分支目标处的第一条指令执行。

在某些情况下(如无条件跳转),可以优化分支延迟时隙。在其他情况下则不能,并且分支后的指令必须保留为空。此外,这种类型的延迟时隙意味着分支必须使用比实际目标指令少1个地址的分支目标,以解决发生的PC增量。

简而言之,由于上一条指令的输出是在提取下一条指令之后写入RAM的,因此条件跳转必须在它们之后有一个空白指令,否则PC不会针对该跳转进行适当的更新。

3.从RAM中读取当前指令参数的数据

如前所述,这三个操作数中的每一个均由数据字和寻址模式组成。数据字为16位,与RAM相同。寻址模式为2位。

由于许多现实世界中的寻址模式都涉及多步计算(例如增加偏移量),因此寻址模式可能是导致此类处理器复杂性很高的原因。同时,通用寻址模式在处理器的可用性中起着重要作用。

我们试图统一使用硬编码数字作为操作数和使用数据地址作为操作数的概念。这导致了基于计数器的寻址模式的创建:操作数的寻址模式只是一个数字,表示应该在RAM读取循环中发送多少次数据。这包括立即,直接,间接和双重间接寻址。

00  Immediate:  A hard-coded value. (no RAM reads)
01  Direct:  Read data from this RAM address. (one RAM read)
10  Indirect:  Read data from the address given at this address. (two RAM reads)
11  Double-indirect: Read data from the address given at the address given by this address. (three RAM reads)

执行此取消引用后,指令的三个操作数具有不同的作用。第一个操作数通常是二进制运算符的第一个参数,但是当当前指令是有条件的移动时,它也用作条件。第二个操作数用作二进制运算符的第二个参数。第三个操作数用作指令结果的目标地址。

由于前两个指令用作数据,而第三个指令用作地址,因此寻址模式根据它们在哪个位置使用而略有不同。例如,直接模式用于从固定RAM地址读取数据(因为需要读取一个RAM),但是立即模式用于将数据写入固定的RAM地址(因为无需读取RAM)。

4.计算结果

操作码和前两个操作数被发送到ALU以执行二进制运算。对于算术,按位和移位运算,这意味着执行相关运算。对于条件移动,这意味着只需返回第二个操作数。

操作码和第一个操作数用于计算条件,该条件确定是否将结果写入内存。在有条件移动的情况下,这意味着要么确定操作数中的任何位是否为1(用于MNZ),要么确定符号位是否为1(针对MLZ)。如果操作码不是条件移动,则始终执行写入操作(条件始终为true)。

5.增加程序计数器

最后,读取,递增和写入程序计数器。

由于PC增量在读指令和写指令之间的位置,这意味着将PC增量1的指令是空操作。将PC复制到自身的指令将使下一条指令连续执行两次。但是请注意,如果您不注意指令流水线,则连续多个PC指令会导致复杂的效果,包括无限循环。

寻求俄罗斯方块大会

我们为处理器创建了一种名为QFTASM的新汇编语言。该汇编语言与计算机ROM中的机器代码一对一对应。

任何QFTASM程序都是按一系列指令编写的,每行一条。每行的格式如下:

[line numbering] [opcode] [arg1] [arg2] [arg3]; [optional comment]

操作码列表

如前所述,计算机支持11个操作码,每个操作码都有3个操作数:

MNZ [test] [value] [dest]  – Move if Not Zero; sets [dest] to [value] if [test] is not zero.
MLZ [test] [value] [dest]  – Move if Less than Zero; sets [dest] to [value] if [test] is less than zero.
ADD [val1] [val2] [dest]   – ADDition; store [val1] + [val2] in [dest].
SUB [val1] [val2] [dest]   – SUBtraction; store [val1] - [val2] in [dest].
AND [val1] [val2] [dest]   – bitwise AND; store [val1] & [val2] in [dest].
OR [val1] [val2] [dest]    – bitwise OR; store [val1] | [val2] in [dest].
XOR [val1] [val2] [dest]   – bitwise XOR; store [val1] ^ [val2] in [dest].
ANT [val1] [val2] [dest]   – bitwise And-NoT; store [val1] & (![val2]) in [dest].
SL [val1] [val2] [dest]    – Shift Left; store [val1] << [val2] in [dest].
SRL [val1] [val2] [dest]   – Shift Right Logical; store [val1] >>> [val2] in [dest]. Doesn't preserve sign.
SRA [val1] [val2] [dest]   – Shift Right Arithmetic; store [val1] >> [val2] in [dest], while preserving sign.

寻址方式

每个操作数都包含一个数据值和一个寻址移动。数据值由-32768到32767范围内的十进制数字描述。寻址模式由数据值的一个字母前缀描述。

mode    name               prefix
0       immediate          (none)
1       direct             A
2       indirect           B
3       double-indirect    C 

范例程式码

斐波那契数列在五行中:

0. MLZ -1 1 1;    initial value
1. MLZ -1 A2 3;   start loop, shift data
2. MLZ -1 A1 2;   shift data
3. MLZ -1 0 0;    end loop
4. ADD A2 A3 1;   branch delay slot, compute next term

此代码计算斐波那契数列,其中RAM地址1包含当前项。它在28657之后迅速溢出。

格雷码:

0. MLZ -1 5 1;      initial value for RAM address to write to
1. SUB A1 5 2;      start loop, determine what binary number to covert to Gray code
2. SRL A2 1 3;      shift right by 1
3. XOR A2 A3 A1;    XOR and store Gray code in destination address
4. SUB B1 42 4;     take the Gray code and subtract 42 (101010)
5. MNZ A4 0 0;      if the result is not zero (Gray code != 101010) repeat loop
6. ADD A1 1 1;      branch delay slot, increment destination address

该程序计算格雷码,并将代码存储在从地址5开始的成功地址中。该程序利用了多个重要功能,例如间接寻址和条件跳转。一旦生成的格雷码为101010,它就会停止,这发生在地址56处的输入51处。

在线口译员

El'endia Starman在这里创建了一个非常有用的在线翻译。您可以单步执行代码,设置断点,对RAM执行手动写入以及将RAM可视化为显示。

古柯

定义了架构和汇编语言后,项目“软件”方面的下一步是创建一种高级语言,该语言适用于Tetris。因此,我创建了Cogol。该名称既是“ COBOL”的双关语,也是“生命游戏的C”的首字母缩写,尽管值得注意的是,Cogol代表的是C代表我们的计算机代表实际的计算机。

Cogol的存在水平仅高于汇编语言。通常,Cogol程序中的大多数行都对应于单个汇编行,但是该语言有一些重要功能:

  • 基本功能包括具有赋值的命名变量和具有更易读语法的运算符。例如,ADD A1 A2 3变为z = x + y;,编译器将变量映射到地址上。
  • 循环结构(例如if(){},)while(){}do{}while();因此编译器处理分支。
  • 一维数组(带有指针算法),用于俄罗斯方块板。
  • 子例程和调用堆栈。这些对于防止大量代码重复和支持递归很有用。

编译器(我从头开始编写)非常基础/天真,但是我尝试手动优化几种语言结构以实现较短的编译程序长度。

以下是各种语言功能如何工作的简短概述:

代币化

使用关于允许哪些字符在令牌中相邻的简单规则,对源代码进行线性令牌化(单次通过)。当遇到不能与当前令牌的最后一个字符相邻的字符时,当前令牌被视为已完成,并且新字符开始一个新令牌。某些字符(例如{,)不能与其他任何字符相邻,因此它们是它们自己的标记。其他(如>=)只允许以邻接于它们的类内的其它字符,并且因此可以形成像令牌>>>==或者>=,但不喜欢=2。空格字符会在标记之间建立边界,但本身不会包含在结果中。标记最困难的字符是- 因为它既可以表示减法,也可以表示一元求反,因此需要一些特殊的框。

解析中

解析也以单遍方式完成。编译器具有用于处理每种不同语言构造的方法,并且当各种编译器方法使用它们时,会将令牌从全局令牌列表中弹出。如果编译器看到了它不期望的令牌,则会引发语法错误。

全局内存分配

编译器为每个全局变量(字或数组)分配自己的指定RAM地址。必须使用关键字声明所有变量,my以便编译器知道为其分配空间。临时地址内存管理比命名全局变量要酷得多。许多指令(特别是条件指令和许多数组访问指令)需要临时的“临时”地址来存储中间计算。在编译过程中,编译器会根据需要分配和取消分配暂存地址。如果编译器需要更多的暂存地址,它将把更多的RAM用作暂存地址。我相信程序通常只需要几个暂存地址,尽管每个暂存地址将被使用很多次。

IF-ELSE 陈述

if-else语句的语法是标准的C形式:

other code
if (cond) {
  first body
} else {
  second body
}
other code

转换为QFTASM时,代码的排列方式如下:

other code
condition test
conditional jump
first body
unconditional jump
second body (conditional jump target)
other code (unconditional jump target)

如果执行第一个主体,则跳过第二个主体。如果跳过了第一个主体,则执行第二个主体。

在装配中,条件测试通常只是减法,结果的符号决定是进行跳跃还是执行车身。一个MLZ指令被用来处理不等式如><=。的MNZ指令是用来处理==,因为它跃过体当所述差不为零(并且因此当参数不相等)。当前不支持多表达式条件。

如果else省略该语句,则也将无条件跳转,并且QFTASM代码如下所示:

other code
condition test
conditional jump
body
other code (conditional jump target)

WHILE 陈述

while语句的语法也是标准的C形式:

other code
while (cond) {
  body
}
other code

转换为QFTASM时,代码的排列方式如下:

other code
unconditional jump
body (conditional jump target)
condition test (unconditional jump target)
conditional jump
other code

条件测试和条件跳转位于该块的末尾,这意味着它们在每次执行该块后都会重新执行。当条件返回false时,主体不再重复,循环结束。在循环执行开始期间,控制流会跳到循环主体上,并跳转到条件代码,因此,如果条件第一次为假,则主体将永远不会执行。

一个MLZ指令被用来处理不等式如><=。与指令期间if语句不同,MNZ指令用于处理!=,因为当差异不为零时(因此参数不相等时),指令会跳转到主体。

DO-WHILE 陈述

while和之间的唯一区别do-while是,do-while循环主体最初不会被跳过,因此它总是至少执行一次。do-while当我知道循环永远不需要完全跳过时,我通常使用语句来保存几行汇编代码。

数组

一维数组被实现为连续的内存块。所有数组都基于其声明为固定长度。数组声明如下:

my alpha[3];               # empty array
my beta[11] = {3,2,7,8};   # first four elements are pre-loaded with those values

对于阵列,这是一个可能的RAM映射,显示如何为阵列保留地址15-18:

15: alpha
16: alpha[0]
17: alpha[1]
18: alpha[2]

标记的地址alpha用指向的位置的指针填充alpha[0],因此在这种情况下,地址15包含值16。该alpha变量可以在Cogol代码内部使用,如果您想将此数组用作堆栈,则可以用作堆栈指针。 。

使用标准array[index]符号完成对数组元素的访问。如果的值index是一个常数,则该引用将自动用该元素的绝对地址填充。否则,它将执行一些指针算术(仅加法)以找到所需的绝对地址。也可以嵌套索引,例如alpha[beta[1]]

子程序和调用

子例程是可以从多个上下文中调用的代码块,从而防止代码重复并允许创建递归程序。这是一个带有递归子程序的程序,用于生成斐波那契数(基本上是最慢的算法):

# recursively calculate the 10th Fibonacci number
call display = fib(10).sum;
sub fib(cur,sum) {
  if (cur <= 2) {
    sum = 1;
    return;
  }
  cur--;
  call sum = fib(cur).sum;
  cur--;
  call sum += fib(cur).sum;
}

子例程用关键字声明,sub子例程可以放在程序内的任何位置。每个子例程可以具有多个局部变量,这些局部变量被声明为其参数列表的一部分。也可以为这些参数指定默认值。

为了处理递归调用,子例程的局部变量存储在堆栈中。RAM中的最后一个静态变量是调用堆栈指针,其后的所有内存都用作调用堆栈。子程序被调用时,它在调用堆栈上创建了一个新帧,其中包括所有局部变量以及返回(ROM)地址。程序中的每个子例程都被赋予一个静态RAM地址,以用作指针。该指针给出了子例程的“当前”调用在调用堆栈中的位置。使用此静态指针的值加上一个偏移量来引用该局部变量,以给出该特定局部变量的地址。调用堆栈中还包含静态指针的先前值。这里'

RAM map:
0: pc
1: display
2: scratch0
3: fib
4: scratch1
5: scratch2
6: scratch3
7: call

fib map:
0: return
1: previous_call
2: cur
3: sum

子例程有趣的一件事是它们不返回任何特定值。而是,子例程执行后可以读取子例程的所有局部变量,因此可以从子例程调用中提取各种数据。这是通过存储该子例程的特定调用的指针来实现的,然后可以使用该指针从(最近已解除分配的)堆栈帧中恢复任何局部变量。

调用子例程有多种方法,所有方法都使用call关键字:

call fib(10);   # subroutine is executed, no return vaue is stored

call pointer = fib(10);   # execute subroutine and return a pointer
display = pointer.sum;    # access a local variable and assign it to a global variable

call display = fib(10).sum;   # immediately store a return value

call display += fib(10).sum;   # other types of assignment operators can also be used with a return value

可以将任意数量的值作为子例程调用的参数。未提供的任何参数将使用其默认值(如果有)填充。未清除没有提供且没有默认值的参数(以节省指令/时间),因此在子例程开始时可能会采用任何值。

指针是访问子例程的多个局部变量的一种方式,尽管要特别注意的是,该指针只是临时的:指针所指向的数据在进行另一个子例程调用时将被破坏。

调试标签

{...}Cogol程序中的任何代码块都可以带有多词描述性标签。此标签作为注释附加在已编译的汇编代码中,并且对于调试非常有用,因为它使查找特定代码块更加容易。

分支延迟时隙优化

为了提高编译代码的速度,Cogol编译器执行了一些真正基本的延迟时隙优化,作为对QFTASM代码的最后一次传递。对于具有空的分支延迟槽的任何无条件跳转,该延迟槽可以由跳转目标处的第一条指令填充,并且跳转目标增加一个以指向下一条指令。每次执行无条件跳转时,通常可以节省一个周期。

用Cogol编写俄罗斯方块代码

最终的Tetris程序是用Cogol编写的,其源代码在此处此处提供已编译的QFTASM代码。为方便起见,此处提供了一个永久链接:QFTASM中的Tetris。由于目标是打高尔夫球的汇编代码(而不是Cogol代码),因此生成的Cogol代码不方便使用。程序的许多部分通常都位于子例程中,但是这些子例程实际上足够短,以至于将代码保存的指令复制到了子例程中。call陈述。最终代码除主代码外仅具有一个子例程。此外,删除了许多数组,并用等长的单个变量列表或程序中的许多硬编码数字替换了它们。最终编译的QFTASM代码在300条指令下,尽管它仅比Cogol源本身稍长。


22
我喜欢汇编语言指令的选择是由您的基板硬件定义的(没有MEZ,因为很难将两个错误组合成一个true)。很棒的阅读。
AlexC

1
您说=只能站在自己旁边,但是有一个!=
法比安·罗林(FabianRöling)

@Fabian 一个+=
Oliphaunt

@Oliphaunt是的,我的描述不是很准确,更多的是字符类的东西,其中某些类的字符可以彼此相邻。
PhiNotPi

606

第五部分:汇编,翻译和未来

利用编译器提供的汇编程序,现在该为Varlife计算机组装ROM了,并将所有内容转换为大的GoL模式!

部件

将汇编程序组装到ROM中的方式与传统编程中几乎相同:将每条指令转换为等效的二进制代码,然后将它们串联到一个大的二进制blob(我们称为可执行文件)中。对我们来说,唯一的区别是,二进制斑点需要转换为Varlife电路并连接到计算机。

K Zhang编写了CreateROM.py,这是Golly的Python脚本,负责汇编和翻译。这非常简单:它从剪贴板中提取一个汇编程序,将其汇编成二进制文件,然后将该二进制文件转换为电路。这是脚本中包含一个简单素数测试器的示例:

#0. MLZ -1 3 3;
#1. MLZ -1 7 6; preloadCallStack
#2. MLZ -1 2 1; beginDoWhile0_infinite_loop
#3. MLZ -1 1 4; beginDoWhile1_trials
#4. ADD A4 2 4;
#5. MLZ -1 A3 5; beginDoWhile2_repeated_subtraction
#6. SUB A5 A4 5;
#7. SUB 0 A5 2;
#8. MLZ A2 5 0;
#9. MLZ 0 0 0; endDoWhile2_repeated_subtraction
#10. MLZ A5 3 0;
#11. MNZ 0 0 0; endDoWhile1_trials
#12. SUB A4 A3 2;
#13. MNZ A2 15 0; beginIf3_prime_found
#14. MNZ 0 0 0;
#15. MLZ -1 A3 1; endIf3_prime_found
#16. ADD A3 2 3;
#17. MLZ -1 3 0;
#18. MLZ -1 1 4; endDoWhile0_infinite_loop

这将产生以下二进制文件:

0000000000000001000000000000000000010011111111111111110001
0000000000000000000000000000000000110011111111111111110001
0000000000000000110000000000000000100100000000000000110010
0000000000000000010100000000000000110011111111111111110001
0000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000011110100000000000000100000
0000000000000000100100000000000000110100000000000001000011
0000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000110100000000000001010001
0000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000001010100000000000000100001
0000000000000000100100000000000001010000000000000000000011
0000000000000001010100000000000001000100000000000001010011
0000000000000001010100000000000000110011111111111111110001
0000000000000001000000000000000000100100000000000001000010
0000000000000001000000000000000000010011111111111111110001
0000000000000000010000000000000000100011111111111111110001
0000000000000001100000000000000001110011111111111111110001
0000000000000000110000000000000000110011111111111111110001

当转换为Varlife电路时,它看起来像这样:

只读存储器

特写ROM

然后将ROM与计算机链接,从而在Varlife中形成一个功能齐全的程序。但是我们还没有完成...

翻译为人生游戏

在整个过程中,我们一直在“生命游戏”基础之上进行各种抽象层次的研究。但是现在,该拉开抽象的帷幕,将我们的工作转变为“生活游戏”模式的时候了。如前所述,我们将OTCA元像素用作Varlife的基础。因此,最后一步是将Varlife中的每个单元转换为Life Game中的一个元像素。

幸运的是,Golly带有一个脚本(metafier.py),该脚本可以通过OTCA Metapixel将不同规则集中的模式转换为Game of Life模式。不幸的是,它仅设计为使用单个全局规则集转换模式,因此在Varlife上不起作用。我写了一个修改后的版本解决了这个问题,因此每个元像素的规则都是基于Varlife逐个单元生成的。

因此,我们的计算机(带有Tetris ROM)的边框为1,436 x 5,082。该框中的7,297,752个单元中有6,075,811个是空白空间,实际人口总数为1,221,941。这些单元格中的每一个都需要转换为OTCA元像素,该像素具有2048x2048的边界框以及64,691(对于ON元像素)或23,920(对于OFF元像素)填充。这意味着最终产品将具有2,940,928 x 10,407,936的边界框(外加几千个用于元像素边界的边界框),人口在29,228,828,720和79,048,585,231之间。每个活动单元只有1位,代表整个计算机和ROM需要27到74 GiB。

我将这些计算包括在这里是因为在启动脚本之前我忽略了运行这些计算,并且很快计算机内存不足。在kill执行紧急命令后,我对metafier脚本进行了修改。每10行元像素,该模式将保存到磁盘(作为压缩的RLE文件)中,并且将刷新网格。这为翻译增加了额外的运行时间,并使用了更多的磁盘空间,但将内存使用率保持在可接受的范围内。由于Golly使用扩展的RLE格式(包括模式的位置),因此不会增加模式加载的复杂性-只需在同一层上打开所有模式文件即可。

K Zhang以此工作为基础,并创建了一个更有效的metafier脚本,该脚本利用了MacroCell文件格式,对于大型模式,该格式的加载效率高于RLE。该脚本运行速度相当快(几秒钟,而原始metafier脚本要花费数小时),产生的输出量要小得多(121 KB与1.7 GB),并且可以一口气将整个计算机和ROM进行元配置,而无需花费大量资源的记忆。它利用了MacroCell文件对描述模式的树进行编码这一事实。通过使用自定义模板文件,将元像素预加载到树中,并且在进行了一些计算和修改以进行邻居检测之后,可以简单地添加Varlife模式。

可以在此处找到《生命游戏》中整个计算机和ROM的特征码文件。


项目的未来

现在我们已经制作了俄罗斯方块,就完成了,对吗?差远了。我们正在努力实现该项目的更多目标:

  • 浑水和Kritixi光刻机正在继续研究可编译为QFTASM的高级语言。
  • El'endia Starman正在致力于升级在线QFTASM解释器。
  • quartata正在GCC后端上工作,该后端将允许通过GCC将独立的C和C ++代码(以及可能的其他语言,如Fortran,D或Objective-C)编译为QFTASM。尽管没有标准库,这将允许使用更熟悉的语言创建更复杂的程序。
  • 要取得更大的进步,我们必须克服的最大障碍之一就是我们的工具无法发出与位置无关的代码(例如,相对跳转)。没有PIC,我们将无法进行任何链接,因此我们错过了能够链接到现有库所带来的优势。我们正在努力寻找正确进行PIC的方法。
  • 我们正在讨论要为QFT计算机编写的下一个程序。现在,Pong看起来是一个不错的目标。

2
只看未来的小节,相对跳就不是ADD PC offset PC吗?如果这不正确,请原谅我,汇编编程从来不是我的专长。
MBraedley

3
@Timmmm是的,但是非常慢。(您还必须使用HashLife)。
意大利面条

75
您为其编写的下一个程序应该是Conway的《人生游戏》。
ACK_stoverflow

13
@ACK_stoverflow这将在某个时候完成。
Mego

13
您有正在运行的视频吗?
PyRulez

583

第6部分:QFTASM的较新编译器

尽管Cogol对于基本的Tetris实现已经足够了,但对于以易于阅读的水平进行通用编程而言,它过于简单且层次太低。我们于2016年9月开始研究一种新语言。由于难以理解的错误以及现实生活,该语言的进展缓慢。

我们使用类似于Python的语法构建了一种低级语言,包括一个简单的类型系统,支持递归的子例程和内联运算符。从文本到QFTASM的编译器由以下四个步骤创建:标记程序,语法树,高级编译器和低级编译器。

代币发行人

使用内置的tokeniser库使用Python开始开发,这意味着这一步非常简单。只需对默认输出进行一些更改,包括删除注释(但不#include删除s)。

语法树

创建语法树是为了易于扩展而无需修改任何源代码。

树结构存储在XML文件中,该文件包含可以构成树的节点的结构以及它们如何与其他节点和令牌一起构成。

语法需要支持重复的节点以及可选的节点。这是通过引入元标记来描述如何读取令牌来实现的。

然后,通过语法规则对生成的标记进行解析,以使输出形成诸如subs和的语法元素树,而语法树generic_variables又包含其他语法元素和标记。

编译成高级代码

语言的每个功能都需要能够被编译成高级结构。这些包括assign(a, 12)call_subroutine(is_prime, call_variable=12, return_variable=temp_var)。在该段中执行诸如元素内联之类的功能。这些被定义为operators,并且特殊之处在于每次使用诸如+或的运算符时都会内联它们%。因此,它们比常规代码受到更多限制-它们不能使用自己的运算符,也不能使用任何依赖于所定义的运算符的运算符。

在内联过程中,内部变量将替换为被调用的内部变量。这实际上变成了

operator(int a + int b) -> int c
    return __ADD__(a, b)
int i = 3+3

进入

int i = __ADD__(3, 3)

但是,如果输入变量和输出变量指向​​内存中的同一位置,则此行为可能是有害的,并且容易出错。为了使用“更安全”的行为,unsafe关键字会调整编译过程,以便根据需要创建其他变量并将其复制到内联中或从内联复制。

临时变量和复杂操作

a += (b + c) * 4如果不使用额外的存储单元,将无法进行数学运算,例如。高级编译器通过将操作分为不同的部分来解决此问题:

scratch_1 = b + c
scratch_1 = scratch_1 * 4
a = a + scratch_1

这引入了暂存变量的概念,该暂存变量用于存储计算的中间信息。它们根据需要分配,并在完成后重新分配到常规池中。这减少了使用所需的暂存存储位置的数量。临时变量被视为全局变量。

每个子例程都有自己的VariableStore,以保留对该子例程使用的所有变量及其类型的引用。在编译结束时,它们被转换为从存储开始处的相对偏移,然后在RAM中给出实际地址。

RAM结构

Program counter
Subroutine locals
Operator locals (reused throughout)
Scratch variables
Result variable
Stack pointer
Stack
...

低级编译

唯一的东西低水平编译器处理的subcall_subreturnassignifwhile。这是一个大大减少的任务列表,可以更轻松地将其转换为QFTASM指令。

sub

这将找到命名子例程的开始和结束。低级编译器添加标签,在main子例程的情况下,添加退出指令(跳转到ROM的末尾)。

ifwhile

无论是whileif低水平解释非常简单:他们得到指向他们的情况,并根据他们的跳跃。while循环略有不同,因为它们被编译为

...
condition
jump to check
code
condition
if condtion: jump to code
...

call_subreturn

与大多数体系结构不同,我们要编译的计算机不具有从堆栈中推送和弹出的硬件支持。这意味着从堆栈中推入和弹出都需要两条指令。在弹出的情况下,我们递减堆栈指针并将值复制到一个地址。在压入的情况下,我们将一个值从地址复制到当前堆栈指针处的地址,然后递增。

子例程的所有本地变量都存储在编译时确定的RAM中的固定位置。为了使递归有效,在调用开始时将函数的所有本地变量都放在堆栈上。然后,将子例程的参数复制到其在本地存储中的位置。返回地址的值被放入堆栈,并执行子例程。

return遇到声明,堆栈的顶部弹出程序计数器被设置为该值。调用子例程的locals的值从堆栈中弹出并移至其先前位置。

assign

变量赋值是最容易编译的东西:它们采用变量和值并编译为单行: MLZ -1 VALUE VARIABLE

分配跳跃目标

最后,编译器为指令附加的标签制定跳转目标。确定标签的绝对位置,然后使用这些值替换对这些标签的引用。将标签本身从代码中删除,最后将指令号添加到已编译的代码中。

逐步编译示例

现在,我们已经完成了所有阶段,让我们逐步完成实际程序的实际编译过程。

#include stdint

sub main
    int a = 8
    int b = 12
    int c = a * b

好的,很简单。它应该是显而易见的,在节目的最后,a = 8b = 12c = 96。首先,让我们包括以下相关部分stdint.txt

operator (int a + int b) -> int
    return __ADD__(a, b)

operator (int a - int b) -> int
    return __SUB__(a, b)

operator (int a < int b) -> bool
    bool rtn = 0
    rtn = __MLZ__(a-b, 1)
    return rtn

unsafe operator (int a * int b) -> int
    int rtn = 0
    for (int i = 0; i < b; i+=1)
        rtn += a
    return rtn

sub main
    int a = 8
    int b = 12
    int c = a * b

好吧,稍微复杂一点。让我们进入令牌生成器,看看结果如何。在这一阶段,我们将只有线性的令牌流,而没有任何形式的结构

NAME NAME operator
LPAR OP (
NAME NAME int
NAME NAME a
PLUS OP +
NAME NAME int
NAME NAME b
RPAR OP )
OP OP ->
NAME NAME int
NEWLINE NEWLINE
INDENT INDENT     
NAME NAME return
NAME NAME __ADD__
LPAR OP (
NAME NAME a
COMMA OP ,
NAME NAME b
RPAR OP )
...

现在,所有标记都经过语法分析器,并输出包含每个部分名称的树。这显示了代码读取的高级结构。

GrammarTree file
 'stmts': [GrammarTree stmts_0
  '_block_name': 'inline'
  'inline': GrammarTree inline
   '_block_name': 'two_op'
   'type_var': GrammarTree type_var
    '_block_name': 'type'
    'type': 'int'
    'name': 'a'
    '_global': False

   'operator': GrammarTree operator
    '_block_name': '+'

   'type_var_2': GrammarTree type_var
    '_block_name': 'type'
    'type': 'int'
    'name': 'b'
    '_global': False
   'rtn_type': 'int'
   'stmts': GrammarTree stmts
    ...

该语法树设置了要由高级编译器解析的信息。它包括诸如结构类型和变量属性之类的信息。然后,语法树将获取此信息,并分配子例程所需的变量。该树还将插入所有内联。

('sub', 'start', 'main')
('assign', int main_a, 8)
('assign', int main_b, 12)
('assign', int op(*:rtn), 0)
('assign', int op(*:i), 0)
('assign', global bool scratch_2, 0)
('call_sub', '__SUB__', [int op(*:i), int main_b], global int scratch_3)
('call_sub', '__MLZ__', [global int scratch_3, 1], global bool scratch_2)
('while', 'start', 1, 'for')
('call_sub', '__ADD__', [int op(*:rtn), int main_a], int op(*:rtn))
('call_sub', '__ADD__', [int op(*:i), 1], int op(*:i))
('assign', global bool scratch_2, 0)
('call_sub', '__SUB__', [int op(*:i), int main_b], global int scratch_3)
('call_sub', '__MLZ__', [global int scratch_3, 1], global bool scratch_2)
('while', 'end', 1, global bool scratch_2)
('assign', int main_c, int op(*:rtn))
('sub', 'end', 'main')

接下来,低级编译器必须将此高级表示形式转换为QFTASM代码。像这样在RAM中为变量分配位置:

int program_counter
int op(*:i)
int main_a
int op(*:rtn)
int main_c
int main_b
global int scratch_1
global bool scratch_2
global int scratch_3
global int scratch_4
global int <result>
global int <stack>

然后编译简单的指令。最后,添加指令号,生成可执行的QFTASM代码。

0. MLZ 0 0 0;
1. MLZ -1 12 11;
2. MLZ -1 8 2;
3. MLZ -1 12 5;
4. MLZ -1 0 3;
5. MLZ -1 0 1;
6. MLZ -1 0 7;
7. SUB A1 A5 8;
8. MLZ A8 1 7;
9. MLZ -1 15 0;
10. MLZ 0 0 0;
11. ADD A3 A2 3;
12. ADD A1 1 1;
13. MLZ -1 0 7;
14. SUB A1 A5 8;
15. MLZ A8 1 7;
16. MNZ A7 10 0;
17. MLZ 0 0 0;
18. MLZ -1 A3 4;
19. MLZ -1 -2 0;
20. MLZ 0 0 0;

语法

既然我们已经有了裸语言,那么我们实际上必须在其中编写一个小程序。我们像Python一样使用缩进,拆分逻辑块和控制流。这意味着空格对于我们的程序很重要。每个完整程序都有一个main子例程,该子例程的作用类似于main()C语言中的函数。该功能在程序开始时运行。

变量和类型

首次定义变量时,它们需要具有与它们关联的类型。当前定义的类型为intbool并定义了数组的语法,但未定义编译器。

图书馆和运营商

提供了一个名为的库,该库stdint.txt定义了基本运算符。如果不包括在内,就不会定义简单的运算符。我们可以将此库与一起使用#include stdintstdint定义运算符,例如和+>>甚至*%,都不是直接的QFTASM操作码。

该语言还允许QFTASM操作码直接通过调用__OPCODENAME__

加法stdint定义为

operator (int a + int b) -> int
    return __ADD__(a, b)

定义+给定两个ints 时运算符的作用。


1
我能问,为什么却决定建立在康威的生命游戏Wireworld线般的CA和使用这种电路,而不是重用创建一个新的处理器/改造现有cgol通用计算机,例如这一个
eaglgenes101 '17

4
@ eaglgenes101首先,我认为我们大多数人都不知道其他可用的通用计算机的存在。创建具有多个混合规则的类似wireworld的CA的想法是通过玩弄元单元而实现的(我相信-Phi是提出这个想法的人)。从那里开始,这是对我们所创造的东西的顺理成章的进步。
Mego
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.