ConwayのGame of Lifeでテトリスの実用的なゲームを構築する


994

ここに理論的な質問があります-どんな場合でも簡単な答えを与える余裕はなく、些細な質問でさえありません。

ConwayのGame of Lifeには、Game of Lifeが他のGame-of-Lifeルールシステムをシミュレートできるようにするメタピクセルなどの構造が存在します。さらに、Game of Lifeはチューリング完全であることが知られています。

あなたのタスクは、テトリスのゲームのプレイを可能にするConwayのライフゲームのルールを使用してセルラーオートマトンを構築することです。

プログラムは、割り込みを表すために特定の世代でオートマトンの状態を手動で変更することにより入力を受け取ります(例えば、ピースを左右に移動、ドロップ、回転、またはグリッドに配置する新しいピースをランダムに生成)。待機時間として特定の世代数を生成し、オートマトンのどこかに結果を表示します。表示される結果は、実際のテトリスグリッドに視覚的に似ている必要があります。

プログラムは、次の項目で順番にスコアが付けられます(低い基準が高い基準のタイブレーカーとして機能します)。

  • 境界ボックスサイズ—与えられたソリューションを完全に含む最小の領域を持つ長方形のボックスが優先されます。

  • 入力への小さな変更—割り込みに勝つために手動で調整する必要のある最小のセル(オートマトンの最悪の場合)。

  • 最速の実行—シミュレーションで1ティック進む最小の世代が勝ちます。

  • 初期の生細胞数—小さい数が勝ちます。

  • 最初に投稿—早い投稿が優先されます。


95
「実証的に機能する例」とは、数時間で実行されるもの、またはプレイするのに宇宙の熱死まで時間がかかる場合でも正しいと証明できるものを意味しますか?
ピーターテイラー

34
このようなものが可能であり、プレイ可能であると確信しています。おそらく世界で最も難解な「アセンブリ言語」の1つであるプログラムを作成できる専門知識を持つ人はごくわずかです。
ジャスティンL。13年

58
この課題に取り組んでいます! チャットルーム | 進捗 | ブログ
-mbomb007

49
今朝5時10分(UTC 9時10分)の時点で、この質問はPPCGの歴史の中で最初の質問であり、回答を得ることなく100票に達しました!みんなよくやった。
ジョーZ.

76
私はこれを解決しようとしています...今、私が寝るとき、どこでもグライダーが巨大な混乱で衝突しているのを見ます。私の眠りは悪夢に満ちており、脈動する五十体が私を邪魔し、ハーシェルが私を吸収するように進化しています。ジョン・コンウェイ、私のために祈ってください
薄暗い

回答:


938

これはクエストとして始まりましたが、オデッセイとして終わりました。

Quest for Tetrisプロセッサ、2,940,928 x 10,295,296

パターンファイルは、その栄光がすべてここにあり、ブラウザ内で表示できます

このプロジェクトは、過去1年と1年半にわたる多くのユーザーの努力の集大成です。チームの構成は時とともに変化していますが、執筆時点での参加者は次のとおりです。

また、7H3_H4CK3R、Conor O'Brien、およびこの課題の解決に力を注いできた他の多くのユーザーにも感謝します。

このコラボレーションの前例のない範囲のため、この回答は、このチームのメンバーによって書かれた複数の回答に分割されています。各メンバーは、特定のサブトピックについて、最も関与しているプロジェクトの分野にほぼ対応するように書きます。

チームのすべてのメンバーに賛成票または賞金を分配してください。

目次

  1. 概要
  2. メタピクセルとVarLife
  3. ハードウェア
  4. QFTASMおよびCogol
  5. 組み立て、翻訳、そして未来
  6. 新しい言語とコンパイラ

また、作成したすべてのコードをソリューションの一部として配置したGitHub組織をチェックアウトすることも検討してください。質問は開発チャットルームに送信できます。


パート1:概要

このプロジェクトの基本的な考え方は抽象化です。Lifeでテトリスゲームを直接開発するのではなく、一連の手順で抽象化をゆっくりとラチェットアップしました。各層で、私たちは人生の困難からさらに離れ、他のプログラムと同じくらい簡単にプログラミングできるコンピューターの構築に近づきます。

まず、OTCAメタピクセルをコンピューターの基盤として使用しました。これらのメタピクセルは、「リアル」なルールをエミュレートできます。 WireworldWireworldコンピュータは、このプロジェクトのためのインスピレーションのような重要な情報源を務めたので、我々はmetapixelsと同様のconstuctionを作成しようとしました。OTCAメタピクセルを使用してWireworldをエミュレートすることはできませんが、異なるメタピクセルに異なるルールを割り当て、ワイヤと同様に機能するメタピクセル配列を作成することができます。

次のステップは、コンピューターの基礎として機能するさまざまな基本的な論理ゲートを構築することでした。すでにこの段階で、実際のプロセッサ設計に似た概念を扱っています。ORゲートの例を次に示します。この画像の各セルは、実際にはOTCAメタピクセル全体です。「電子」(それぞれが1ビットのデータを表す)がゲートを出入りするのを見ることができます。また、コンピューターで使用したさまざまなメタピクセルタイプ(黒の背景としてのB / S、青のB1 / S、緑のB2 / S、および赤のB12 / S1)も確認できます。

画像

ここから、プロセッサのアーキテクチャを開発しました。難解ではなく、可能な限り簡単に実装できるアーキテクチャの設計に多大な労力を費やしました。Wireworldコンピュータは基本的なトランスポートトリガーアーキテクチャを使用していましたが、このプロジェクトでは、複数のオペコードとアドレス指定モードを備えたはるかに柔軟なRISCアーキテクチャを使用しています。QFTASM(Quest for Tetris Assembly)として知られるアセンブリ言語を作成し、プロセッサの構築をガイドしました。

私たちのコンピューターも非同期です。つまり、コンピューターを制御するグローバルクロックはありません。むしろ、データはコンピューターの周りを流れるクロック信号を伴います。つまり、コンピューターのグローバルなタイミングではなくローカルなタイミングに焦点を合わせる必要があるだけです。

プロセッサアーキテクチャの図を以下に示します。

画像

ここからは、コンピューターにテトリスを実装するだけです。これを実現するために、高レベル言語をQFTASMにコンパイルする複数の方法に取り組みました。開発中の2番目のより高度な言語であるCogolという基本言語があり、最終的にGCCバックエンドが構築中です。現在のTetrisプログラムは、Cogolで作成/コンパイルされています。

最終的なTetris QFTASMコードが生成されたら、最後の手順として、このコードから対応するROMに、そしてメタピクセルから基礎となるGame of Lifeにアセンブルして、構築を完了しました。

テトリスの実行

コンピューターをいじらずにテトリスをプレイしたい場合は、QFTASMインタープリターでテトリスのソースコードを実行できます。ゲーム全体を表示するには、RAMディスプレイアドレスを3-32に設定します。便利なパーマリンクを次に示します。QFTASMのテトリス

ゲームの特徴:

  • 7つのテトロミノすべて
  • 動き、回転、ソフトドロップ
  • ラインクリアとスコアリング
  • プレビュー作品
  • プレイヤー入力はランダム性を注入します

表示

私たちのコンピューターは、メモリ内のグリッドとしてテトリスボードを表します。アドレス10〜31はボードを表示し、アドレス5〜8はプレビューピースを表示し、アドレス3はスコアを含みます。

入力

ゲームへの入力は、RAMアドレス1の内容を手動で編集することにより実行されます。QFTASMインタープリターを使用して、これはアドレス1への直接書き込みを実行することを意味します。各移動には、RAMの1ビットの編集のみが必要です。この入力レジスタは、入力イベントが読み取られた後に自動的にクリアされます。

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

スコアリングシステム

1ターンで複数の行をクリアするとボーナスがもらえます。

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

14
@ Christopher2EZ4RTZこの概要投稿では、プロジェクトメンバーの多くが行った作業の詳細を説明しています(概要投稿の実際の執筆を含む)。そのため、CWであることが適切です。また、1人が2つの投稿を持つことを避けようとしました。これにより、不公平な担当者を受け取ることになります。
メゴ

28
まず第一に、これは非常に素晴らしい成果であるためです(特に、テトリスではなく、人生のゲームでコンピューターを構築したためです)。第二に、コンピューターの速度とテトリスのゲームの速度はどれくらいですか?リモートでもプレイできますか?(再び:これは素晴らしいです)
ソクラテスフェニックス

18
これ...これは完全に狂っている。すぐにすべての回答に+1します。
スコチネット

28
小さな賞金を回答に分配したい人への警告:毎回(500を打つまで)賞金の額を2倍にしなければならないので、1人の人がすべての回答に同じ額を与えることはできません。
マーティンエンダー

23
これは、私がこれまでスクロールした中で、ほとんど理解していない中で最も素晴らしいものです。
エンジニアトースト

678

パート2:OTCAメタピクセルとVarLife

OTCAメタピクセル

OTCAメタピクセル
ソース

OTCA Metapixelはどんな生命のようなセルオートマトンをシミュレートするために使用することができますライフゲームにおける構造です。LifeWiki(上記リンク)が言うように、

OTCAメタピクセルは、Brice Dueによって構築された2048×2048周期の35328ユニットセルです。これには多くの利点があります。ライフライクセルラーオートマトンをエミュレートする機能や、ズームアウトするとON OFFセルは簡単に区別できます...

どのような人生のようなセルオートマトンは、細胞が生まれていることを本質的に、細胞が生きているどのように多くの彼らの8つの隣接セルのに従って生き残るここを意味します。これらのルールの構文は次のとおりです。Bに続いて出生を引き起こすライブネイバーの数、次にスラッシュ、Sに続いてセルを生かし続けるライブネイバーの数が続きます。少し冗長なので、例が役立つと思います。標準的なGame of Lifeは、ルールB3 / S23で表すことができます。ルールB3 / S23は、3つのライブネイバーを持つ死んだセルは生き残り、2つまたは3つのライブネイバーを持つ生きたセルは生き続けると言います。そうでなければ、セルは死にます。

OTCAメタピクセルには2048 x 2048のセルがありますが、実際には2058 x 2058のセルの境界ボックスがあります。これは、斜めの隣接ピクセルと全方向に5つのセルが重なっているためです。重なり合うセルは、グライダーを傍受するのに役立ちます-グライダーは、他のメタピクセルと干渉したり、無期限に飛び去ったりしないように、隣接するメタセルに信号を送るために放出されます。出生規則と生存規則は、2つの列に沿った特定の位置(1つは出生用、もう1つは生存用)の食べる人の有無によって、メタピクセルの左側にあるセルの特別なセクションにエンコードされます。隣接セルの状態を検出する場合、次のようになります。

次に、9-LWSSストリームがセルを時計回りに回り、ハニービット反応を引き起こした隣接する「オン」セルごとにLWSSを失います。失われたLWSSの数は、別のLWSSを反対方向から衝突させて前面LWSSの位置を検出することでカウントされます。この衝突によりグライダーが放出され、出生/生存状態を示す食べる人がいない場合、別の1つまたは2つのハニービット反応がトリガーされます。

OTCAメタピクセルの各側面のより詳細な図は、元のWebサイトで確認できます

VarLife

任意のセルを任意の実物そっくりのルールに従って動作させることができる実物そっくりのルールのオンラインシミュレータを構築し、それを「Variations of Life」と呼びました。この名前は、より簡潔にするために「VarLife」に短縮されています。そのスクリーンショットを次に示します(ここへのリンク:http : //play.starmaninnovations.com/varlife/BeeHkfCpNR):

VarLifeスクリーンショット

注目すべき機能:

  • セルのライブ/デッドを切り替えて、異なるルールでボードをペイントします。
  • シミュレーションを開始および停止し、一度に1つのステップを実行する機能。また、ティック/秒およびミリ秒/ティックボックスで設定されたレートで、指定された数のステップを可能な限り高速またはより低速で実行することもできます。
  • すべてのライブセルをクリアするか、ボードを完全に空白状態にリセットします。
  • セルとボードのサイズを変更したり、水平および/または垂直にトロイダル包装を可能にしたりできます。
  • パーマリンク(URL内のすべての情報をエンコードします)および短いURL(情報が多すぎる場合もありますが、とにかくいいので)。
  • B / S仕様、色、およびオプションのランダム性を備えたルールセット。
  • そして最後になりますが、間違いなく、GIFをレンダリングします!

render-to-gif機能は、実装に膨大な作業を要したため、朝の7時にようやくクラックできたのと、VarLifeコンストラクトを他の人と共有するのが非常に簡単になったため、私のお気に入りです。

基本的なVarLife回路

全体として、VarLifeコンピューターは4つのセルタイプのみを必要とします!デッド/アライブ状態をすべてカウントする8つの状態。彼らです:

  • B / S(黒/白)。B/ Sセルは決して生きることができないため、すべてのコンポーネント間のバッファーとして機能します。
  • B1 / S(青/シアン)、信号の伝播に使用されるメインセルタイプ。
  • B2 / S(緑/黄)。主に信号制御に使用され、逆伝播しないようにします。
  • B12 / S1(赤/オレンジ)。信号の交差やデータのビットの保存など、いくつかの特殊な状況で使用されます。

既に符号化これらの規則でVarLifeを開くために、この短いURLを使用します。http://play.starmaninnovations.com/varlife/BeeHkfCpNR

ワイヤー

さまざまな特性を持ついくつかの異なるワイヤ設計があります。

これは、VarLifeで最も簡単で最も基本的なワイヤーで、青のストリップが緑のストリップに隣接しています。

基本的なワイヤー
短いURL:http : //play.starmaninnovations.com/varlife/WcsGmjLiBF

このワイヤは単方向です。つまり、反対方向に移動しようとする信号をすべて殺します。また、基本的なワイヤよりも1セル狭いです。

一方向ワイヤー
短いURL:http : //play.starmaninnovations.com/varlife/ARWgUgPTEJ

対角線も存在しますが、あまり使用されていません。

対角線
短いURL:http : //play.starmaninnovations.com/varlife/kJotsdSXIj

ゲイツ

実際には、個々のゲートを構築する方法はたくさんあるので、各種類の例を1つだけ示します。この最初のgifは、それぞれAND、XOR、およびORゲートを示しています。ここでの基本的な考え方は、緑のセルがANDのように機能し、青のセルがXORのように機能し、赤のセルがORのように機能し、周囲の他のすべてのセルがフローを適切に制御するためだけにあるということです。

AND、XOR、OR論理ゲート
短いURL:http : //play.starmaninnovations.com/varlife/EGTlKktmeI

「ANTゲート」と略されるAND-NOTゲートは、重要なコンポーネントであることが判明しました。Bからの信号がない場合に限り、Aからの信号を渡すゲートです。したがって、「A AND NOT B」です。

AND-NOTゲート
短いURL:http : //play.starmaninnovations.com/varlife/RsZBiNqIUy

正確にはゲートではありませんが、ワイヤクロスタイルは非常に重要で有用です。

ワイヤークロッシング
短いURL:http : //play.starmaninnovations.com/varlife/OXMsPyaNTC

ちなみに、ここにはNOTゲートはありません。これは、着信信号がない場合、一定の出力を生成する必要があるためです。これは、現在のコンピューターハードウェアが必要とするさまざまなタイミングではうまく機能しません。とにかくそれなしでうまくいきました。

また、多くのコンポーネントは、11 x 11の境界ボックス(タイル)内に収まるように意図的に設計されており、タイルからタイルを離れるまでに11ティックの信号を受け取ります。これにより、コンポーネントをよりモジュール化して、間隔やタイミングの調整を心配することなく、必要に応じて一緒に平手打ちすることが容易になります。

回路コンポーネントを探索する過程で発見/構築されたゲートをもっと見るには、PhiNotPiのこのブログ投稿をチェックしてください:Building Blocks:Logic Gates

遅延コンポーネント

コンピューターのハードウェアを設計する過程で、KZhangは以下に示す複数の遅延コンポーネントを考案しました。

4ティックの遅延: 短いURL:http : //play.starmaninnovations.com/varlife/gebOMIXxdh
4ティック遅延

5ティックの遅延: 短いURL:http : //play.starmaninnovations.com/varlife/JItNjJvnUB
5ティック遅延

8ティックの遅延(3つの異なるエントリポイント): 短いURL:http : //play.starmaninnovations.com/varlife/nSTRaVEDvA
8ティック遅延

11ティックの遅延: 短いURL:http : //play.starmaninnovations.com/varlife/kfoADussXA
11ティック遅延

12ティックの遅延: 短いURL:http : //play.starmaninnovations.com/varlife/bkamAfUfud
12ティック遅延

14ティックの遅延: 短いURL:http : //play.starmaninnovations.com/varlife/TkwzYIBWln
14ティック遅延

15ティックの遅延(これと比較して検証): 短いURL:http : //play.starmaninnovations.com/varlife/jmgpehYlpT
15ティック遅延

それで、VarLifeの基本的な回路コンポーネントについては以上です!コンピューターの主要な回路については、KZhangのハードウェアポストを参照してください!


4
VarLifeは、このプロジェクトの最も印象的な部分の1つです。それは例えば、に比べて汎用性とシンプルだWireworldは驚異的です。OTCAメタピクセルは必要以上に大きいようですが、ゴルフをしようとする試みはありますか?
プリモ

@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)。ズームアウトしたときにセルの状態を示す「ピクセル」領域の大きさに応じて、メタセルを多少小さくすることができます。ただし、GollyのHashLifeアルゴリズムはコンピューターをはるかに高速に実行できるため、正確に2 ^ Nサイズのタイルで停止する価値があるようです。
デイブグリーン

2
ワイヤーとゲートを「無駄」の少ない方法で実装することはできませんか?電子はグライダーまたは宇宙船で表されます(方向によって異なります)。それらをリダイレクトする(必要に応じて一方から他方に変更する)アレンジメントと、グライダーで動作するいくつかのゲートを見てきました。はい、彼らはより多くのスペースを取り、設計はより複雑であり、タイミングが正確である必要があります。しかし、これらの基本的なビルディングブロックがあれば、簡単に組み立てることができ、OTCAを使用して実装されたVarLifeよりもはるかに少ないスペースしか必要ありません。それも高速に実行されます。
ハイムダル

@Heimdallそれはうまくいきますが、テトリスをプレイしている間はうまく表示されません。
MilkyWay90

649

パート3:ハードウェア

論理ゲートとプロセッサの一般的な構造に関する知識があれば、コンピューターのすべてのコンポーネントの設計を開始できます。

デマルチプレクサ

デマルチプレクサ、つまりデマルチプレクサは、ROM、RAM、およびALUにとって重要なコンポーネントです。特定のセレクタデータに基づいて、入力信号を多数の出力信号のいずれかにルーティングします。シリアル/パラレルコンバーター、信号チェッカー、クロック信号スプリッターの3つの主要部分で構成されています。

まず、シリアルセレクターデータを「パラレル」に変換します。これは、データの左端のビットが左端の11x11スクエアでクロック信号と交差し、データの次のビットが次の11x11スクエアでクロック信号と交差するように、データを戦略的に分割および遅延することによって行われます。データのすべてのビットは11x11の正方形ごとに出力されますが、データのすべてのビットはクロック信号と1回だけ交差します。

シリアルパラレルコンバーター

次に、パラレルデータがプリセットアドレスと一致するかどうかを確認します。これを行うには、クロックおよびパラレルデータでANDおよびANTゲートを使用します。ただし、並列データも出力されるようにして、再度比較できるようにする必要があります。これらは私が思いついた門です:

信号チェックゲート

最後に、クロック信号を分割し、多数の信号チェッカー(アドレス/出力ごとに1つ)をスタックするだけで、マルチプレクサができます!

マルチプレクサー

ROM

ROMはアドレスを入力として受け取り、そのアドレスで命令を出力として送信することになっています。まず、マルチプレクサを使用して、クロック信号を命令の1つに向けます。次に、いくつかのワイヤ交差とORゲートを使用して信号を生成する必要があります。ワイヤの交差により、クロック信号が命令の58ビットすべてを下に移動できるようになり、生成された信号(現在並列)がROMを下に移動して出力できるようになります。

ROMビット

次に、パラレル信号をシリアルデータに変換するだけで、ROMが完成します。

パラレルシリアルコンバーター

ROM

ROMは現在、クリップボードからROMにアセンブリコードを変換するGollyでスクリプトを実行して生成されます。

SRL、SL、SRA

これら3つの論理ゲートはビットシフトに使用され、通常のAND、OR、XORなどよりも複雑です。これらのゲートを機能させるために、最初にクロック信号を適切な時間遅延させて「シフト」を発生させます。データ内。これらのゲートに与えられる2番目の引数は、シフトするビット数を決定します。

SLとSRLについては、

  1. 12個の最上位ビットがオンになっていないことを確認してください(そうでない場合、出力は単に0です)。
  2. 4つの最下位ビットに基づいて、データを正しい量だけ遅延させます。

これは、AND / ANTゲートの束とマルチプレクサで実行可能です。

SRL

シフト中に符号ビットをコピーする必要があるため、SRAはわずかに異なります。これを行うには、クロック信号と符号ビットをAND演算してから、その出力をワイヤースプリッターとORゲートで大量にコピーします。

SRA

セットリセット(SR)ラッチ

プロセッサの機能の多くの部分は、データを保存する機能に依存しています。2つの赤いB12 / S1セルを使用して、それを行うことができます。2つのセルは互いにオンのままにすることができ、一緒にオフのままにすることもできます。追加のセット、リセット、読み取り回路を使用して、単純なSRラッチを作成できます。

SRラッチ

シンクロナイザー

シリアルデータをパラレルデータに変換し、SRラッチのセットを設定することにより、1ワードのデータを保存できます。その後、データを再度出力するには、すべてのラッチを読み取ってリセットし、それに応じてデータを遅延させるだけです。これにより、1つ(またはそれ以上)のデータワードを別のデータを待機しながら保存することができ、異なる時間に到着する2ワードのデータを同期させることができます。

シンクロナイザー

カウンターを読む

このデバイスは、RAMからアドレスする必要がある回数を追跡します。SRラッチに似たデバイス、Tフリップフロップを使用してこれを行います。Tフリップフロップは入力を受け取るたびに状態を変更します。オンの場合はオフになり、逆の場合も同様です。Tフリップフロップがオンからオフに切り替えられると、出力パルスが送信され、このパルスを別のTフリップフロップに入力して2ビットカウンターを形成できます。

2ビットカウンター

読み取りカウンターを作成するには、2つのANTゲートを使用してカウンターを適切なアドレッシングモードに設定し、カウンターの出力信号を使用して、クロック信号をALUまたはRAMに送る場所を決定する必要があります。

カウンターを読む

読み取りキュー

読み取りキューは、RAMの出力を正しい場所に送信できるように、どの読み取りカウンターが入力をRAMに送信したかを追跡する必要があります。そのために、いくつかのSRラッチを使用します。各入力に1つのラッチがあります。読み取りカウンターからRAMに信号が送信されると、クロック信号が分割され、カウンターのSRラッチが設定されます。RAMの出力はSRラッチとANDされ、RAMからのクロック信号がSRラッチをリセットします。

読み取りキュー

ALU

ALUは読み取りキューと同様に機能し、SRラッチを使用して信号の送信先を追跡します。最初に、命令のオペコードに対応する論理回路のSRラッチがマルチプレクサを使用して設定されます。次に、1番目と2番目の引数の値がSRラッチとAND演算された後、論理回路に渡されます。クロック信号は、ALUを再び使用できるように、通過するときにラッチをリセットします。(ほとんどの回路はゴルフダウンされ、膨大な遅延管理が押し込まれているため、少し混乱しているように見えます)

ALU

RAMは、このプロジェクトの最も複雑な部分でした。データを保存した各SRラッチを非常に具体的に制御する必要がありました。読み取りの場合、アドレスはマルチプレクサに送信され、RAMユニットに送信されます。RAMユニットは、保存したデータを並列に出力し、シリアルに変換して出力します。書き込みの場合、アドレスは別のマルチプレクサに送信され、書き込まれるデータはシリアルからパラレルに変換され、RAMユニットはRAM全体に信号を伝播します。

各22x22メタピクセルRAMユニットの基本構造は次のとおりです。

RAMユニット

RAM全体をまとめると、次のようになります。

羊

すべてをまとめる

これらすべてのコンポーネントと「概要」で説明した一般的なコンピューターアーキテクチャを使用して、機能するコンピューターを構築できます。

ダウンロード: - 完成テトリスコンピュータ - ROMの作成スクリプト、空、コンピュータ、およびプライム発見コンピュータ

コンピュータ


49
この投稿の画像は、何らかの理由で、非常に美しいと思います。:P +1
HyperNeutrino

7
これは私が今まで見た中で最も驚くべきことです....できれば+20になります
FantaC

3
@tfbninjaそれは賞金と呼ばれ、200の評判を与えることができます。
ファビアンレーリング

10
このプロセッサは、Spectre and Meltdown攻撃に対して脆弱ですか?:)
フェリービッグ

5
@Ferrybig分岐予測がないので、私はそれを疑います。
JAD

621

パート4:QFTASMとCogol

アーキテクチャの概要

つまり、コンピューターには16ビットの非同期RISCハーバードアーキテクチャがあります。プロセッサを手作業で構築する場合、RISC(縮小命令セットコンピュータ)アーキテクチャは実際には要件です。私たちの場合、これは、オペコードの数が少なく、さらに重要なことには、すべての命令が非常に類似した方法で処理されることを意味します。

参考までに、Wireworldコンピューターはトランスポートトリガーアーキテクチャーを使用しました。このアーキテクチャーでは、MOV特殊レジスターの書き込み/読み取りによって命令のみが実行され、計算が実行されました。このパラダイムは実装が非常に簡単なアーキテクチャになりますが、結果は使用できない境界線でもあります。すべての算術/論理/条件付き演算には3つの命令が必要です。難解なアーキテクチャを作成したかったのは明らかでした。

使いやすさを向上させながらプロセッサーをシンプルに保つために、いくつかの重要な設計上の決定を下しました。

  • レジスタなし。RAM内のすべてのアドレスは等しく扱われ、任意の操作の任意の引数として使用できます。ある意味では、これはすべてのRAMをレジスタのように扱うことができることを意味します。これは、特別なロード/ストア命令がないことを意味します。
  • 同様に、メモリマッピング。読み書きできるものはすべて、統一されたアドレス体系を共有しています。これは、プログラムカウンター(PC)がアドレス0であり、通常の命令と制御フロー命令の唯一の違いは、制御フロー命令がアドレス0を使用することです。
  • データは送信ではシリアル、ストレージではパラレルです。コンピューターの「電子」ベースの性質により、データがシリアルリトルエンディアン(最下位ビットが最初)の形式で送信される場合、加算と減算は非常に簡単に実装できます。さらに、シリアルデータにより、面倒なデータバスの必要性がなくなります。データバスは、幅が広く、適切に時間を調整するのが面倒です(データをまとめるには、バスのすべての「レーン」で同じ移動遅延が発生する必要があります)。
  • ハーバードアーキテクチャ。プログラムメモリ(ROM)とデータメモリ(RAM)の分割を意味します。これはプロセッサの柔軟性を低下させますが、これはサイズの最適化に役立ちます。プログラムの長さは必要なRAMの量よりもはるかに大きいため、プログラムをROMに分割し、ROMの圧縮に集中できます。 、読み取り専用の場合ははるかに簡単です。
  • 16ビットのデータ幅。これは、標準のテトリスボード(10ブロック)よりも広い2の最小乗数です。これにより、データ範囲は-32768〜+32767、最大プログラム長は65536命令になります。(2 ^ 8 = 256の命令で、おもちゃのプロセッサにしたい最も単純なことには十分ですが、テトリスにはできません。)
  • 非同期設計。すべてのデータには、コンピューターのタイミングを指示する中央のクロック(または同等のいくつかのクロック)が存在するのではなく、コンピューターの周りを流れるデータと並行して移動する「クロック信号」が付随します。特定のパスは他のパスよりも短くなる可能性があり、これにより中央クロックの設計では困難が生じますが、非同期設計では可変時間の操作を簡単に処理できます。
  • すべての命令は同じサイズです。各命令が3つのオペランド(値の値の宛先)を持つ1つのオペコードを持つアーキテクチャが最も柔軟なオプションだと感じました。これには、条件付き移動だけでなく、バ​​イナリデータ操作も含まれます。
  • シンプルなアドレッシングモードシステム。さまざまなアドレス指定モードを持つことは、配列や再帰などをサポートするのに非常に役立ちます。比較的単純なシステムで、いくつかの重要なアドレス指定モードを実装することができました。

アーキテクチャの図は、概要の投稿に含まれています。

機能とALU操作

ここからは、プロセッサに必要な機能を決定することでした。実装の容易さと各コマンドの汎用性に特別な注意が払われました。

条件付き移動

条件付きの動きは非常に重要であり、小規模および大規模の両方の制御フローとして機能します。「小規模」は特定のデータ移動の実行を制御する能力を指し、「大規模」は制御フローを任意のコードに転送する条件付きジャンプ操作としての使用を指します。メモリマッピングのため、条件付きの移動ではデータを通常のRAMにコピーし、宛先アドレスをPCにコピーできるため、専用のジャンプ操作はありません。同様の理由で、無条件移動と無条件ジャンプの両方を放棄することも選択しました。どちらもTRUEにハードコードされた条件を持つ条件付き移動として実装できます。

2つの異なるタイプの条件付き移動を選択しました:「ゼロでない場合は移動」(MNZ)および「ゼロ未満の場合は移動」(MLZ)。機能的にMNZは、データのいずれかのビットが1であるかどうかをチェックすることになりますがMLZ、符号ビットが1 であるかどうかをチェックすることになります。これらはそれぞれ、等価および比較に役立ちます。その理由は、我々は、「ゼロならば移動」などの他のものの上にこれら二つを選んだ(MEZ(「ゼロより大きい場合に移動」)又はMGZそれがあった)MEZが、空の信号から真の信号を作成必要とするMGZ必要、より複雑な検査であります符号ビットは0で、少なくとも1つのビットは1です。

算術

プロセッサ設計のガイドという点で次に重要な命令は、基本的な算術演算です。前に述べたように、リトルエンディアンのシリアルデータを使用します。エンディアンの選択は、加算/減算操作の容易さによって決まります。最下位ビットを最初に到着させることにより、算術ユニットはキャリービットを簡単に追跡できます。

負の数には2の補数表現を使用することにしました。これにより、加算と減算の一貫性が向上するためです。Wireworldコンピュータが1の補数を使用したことは注目に値します。

加算と減算は、コンピューターのネイティブの算術サポートの範囲です(後述のビットシフトを除く)。乗算などの他の演算は、アーキテクチャで処理するには複雑すぎるため、ソフトウェアで実装する必要があります。

ビット演算

私たちのプロセッサがありANDORXORあなたが期待する何をすべきかの指示。持っているのではなくNOT、命令を、私たちは「と-ない」(持っていることを選んだANT)命令を。NOT命令の難しさは、信号の欠如から信号を作成する必要があることです。これは、セルオートマトンでは困難です。ANT最初の引数のビットが1であり、二番目の引数のビットは、このように0である場合にのみ、命令は1を返すNOT xと等価であるANT -1 x(同様にXOR -1 x)。さらに、ANT汎用性があり、マスキングでその主な利点があります。テトリスプログラムの場合、テトロミノを消去するために使用します。

ビットシフト

ビットシフト操作は、ALUによって処理される最も複雑な操作です。シフトする値とシフトする量の2つのデータ入力を受け取ります。(可変量のシフトのため)複雑であるにもかかわらず、これらの操作は、テトリスに関係する多くの「グラフィカル」操作を含む多くの重要なタスクにとって重要です。ビットシフトは、効率的な乗算/除算アルゴリズムの基盤としても機能します。

プロセッサには、「左シフト」(SL)、「右シフト論理」(SRL)、「右シフト算術」(SRA)の3ビットのシフト操作があります。最初の2ビットのシフト(SLおよびSRL)は、新しいビットをすべてゼロで埋めます(つまり、右にシフトした負の数は負ではなくなります)。シフトの2番目の引数が0〜15の範囲外の場合、予想どおり、結果はすべてゼロになります。最後のビットシフトについてはSRA、ビットシフトは入力の符号を保持するため、真の2除算として機能します。

命令パイプライン

ここで、アーキテクチャのざらざらした詳細について説明します。各CPUサイクルは、次の5つのステップで構成されます。

1. ROMから現在の命令を取得する

PCの現在の値は、ROMから対応する命令をフェッチするために使用されます。各命令には、1つのオペコードと3つのオペランドがあります。各オペランドは、1つのデータワードと1つのアドレッシングモードで構成されます。これらのパーツは、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に書き込みます。

前の命令の条件(条件付き移動の最初の引数の値など)に応じて、書き込みが実行されます。書き込みのアドレスは、前の命令の第3オペランドによって決定されます。

命令フェッチ後に書き込みが発生することに注意することが重要です。これにより、分岐遅延スロットが作成されます。このスロットでは、分岐先の最初の命令の代わりに、分岐命令(PCに書き込む操作)の直後の命令が実行されます。

特定のインスタンス(無条件ジャンプなど)では、分岐遅延スロットを最適化して削除できます。それ以外の場合は不可能であり、分岐後の命令は空のままにしておく必要があります。さらに、このタイプの遅延スロットは、発生するPC増分を考慮して、実際のターゲット命令より1アドレス少ない分岐ターゲットを使用する必要があることを意味します。

つまり、次の命令がフェッチされた後に前の命令の出力がRAMに書き込まれるため、条件付きジャンプの後に空白の命令が必要です。そうしないと、ジャンプのためにPCが正しく更新されません。

3.現在の命令の引数のデータをRAMから読み取ります

前述のように、3つのオペランドはそれぞれ、データワードとアドレッシングモードの両方で構成されています。データワードは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)

この逆参照が実行された後、命令の3つのオペランドは異なる役割を持ちます。通常、最初のオペランドは2項演算子の最初の引数ですが、現在の命令が条件付き移動である場合の条件としても機能します。2番目のオペランドは、2項演算子の2番目の引数として機能します。3番目のオペランドは、命令の結果の宛先アドレスとして機能します。

最初の2つの命令はデータとして機能し、3番目の命令はアドレスとして機能するため、アドレス指定モードは、使用される位置に応じて解釈が若干異なります。たとえば、固定モードのRAMアドレスからデータを読み取るには1回のRAM読み取りが必要です)が、即時モードは固定RAMアドレスへのデータの書き込みに使用されます(RAM読み取りが不要なため)。

4.結果を計算する

オペコードと最初の2つのオペランドがALUに送信され、バイナリ演算が実行されます。算術演算、ビット演算、およびシフト演算の場合、これは関連する演算を実行することを意味します。条件付き移動の場合、これは単に第2オペランドを返すことを意味します。

オペコードと第1オペランドを使用して条件を計算し、結果をメモリに書き込むかどうかを決定します。条件付き移動の場合、これは、オペランドのいずれかのビットが1(for MNZ)かどうかを判別するか、符号ビットが1(for MLZ)かどうかを判別することを意味します。オペコードが条件付き移動でない場合、書き込みは常に実行されます(条件は常に真です)。

5.プログラムカウンターをインクリメントする

最後に、プログラムカウンターが読み取られ、インクリメントされ、書き込まれます。

読み取られた命令と書き込まれた命令の間のPCインクリメントの位置により、これは、PCを1インクリメントする命令がノーオペレーションであることを意味します。PCをそれ自体にコピーする命令により、次の命令が連続して2回実行されます。ただし、命令パイプラインに注意を払わないと、連続した複数のPC命令が無限ループなどの複雑な影響を引き起こす可能性があることに注意してください。

テトリスアセンブリの探求

プロセッサ用にQFTASMという名前の新しいアセンブリ言語を作成しました。このアセンブリ言語は、コンピューターのROMにあるマシンコードと1対1で対応しています。

QFTASMプログラムは、1行に1つずつ、一連​​の命令として記述されます。各行の形式は次のとおりです。

[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の範囲の10進数で記述されます。アドレス指定モードは、データ値の1文字のプレフィックスで記述されます。

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

サンプルコード

5行のフィボナッチ数列:

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をディスプレイとして視覚化できます。

コゴル

アーキテクチャとアセンブリ言語が定義されたら、プロジェクトの「ソフトウェア」側の次のステップは、テトリスに適した高レベル言語の作成でした。したがって、私はCogolを作成しました。この名前は「COBOL」の省略語であり、「C of Game of Life」の頭字語でもありますが、CogolがCであり、実際のコンピューターに対するCであることに注意する価値があります。

Cogolは、アセンブリ言語のすぐ上のレベルに存在します。一般に、Cogolプログラムのほとんどの行はそれぞれ単一のアセンブリ行に対応していますが、言語の重要な機能がいくつかあります。

  • 基本的な機能には、より読みやすい構文を持つ割り当てと演算子を持つ名前付き変数が含まれます。例えば、ADD A1 A2 3となりz = x + y;のアドレスへのコンパイラマッピング変数を使用して、。
  • 以下のようなループ構造if(){}while(){}およびdo{}while();そのコンパイラが分岐を処理します。
  • Tetrisボードに使用される1次元配列(ポインター演算付き)。
  • サブルーチンと呼び出しスタック。これらは、大量のコードの重複を防ぎ、再帰をサポートするのに役立ちます。

コンパイラ(最初から書いた)は非常に基本的で素朴ですが、いくつかの言語構成を手作業で最適化して、コンパイルされたプログラムの長さを短くしようとしました。

さまざまな言語機能がどのように機能するかの簡単な概要を次に示します。

トークン化

ソースコードは、トークン内で隣接する文字を許可する単純なルールを使用して、線形的にトークン化されます(シングルパス)。現在のトークンの最後の文字に隣接できない文字に遭遇すると、現在のトークンは完全であると見なされ、新しい文字は新しいトークンを開始します。一部の文字(例えば、{または,)は、他の文字に隣接し、従って、自分のトークンであることはできません。その他(のような>または=)は、そのクラス内の他の文字に隣接するように許可されているので、のようなトークンを形成することができ>>>==または>=好きではなく=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)

最初の本体が実行されると、2番目の本体はスキップされます。最初の本文がスキップされると、2番目の本文が実行されます。

アセンブリでは、通常、条件テストは単なる減算であり、結果の符号によってジャンプするか本体を実行するかが決まります。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を返す場合、本体は繰り返されず、ループは終了します。ループ実行の開始時に、制御フローはループ本体を飛び越えて条件コードにジャンプするため、条件が初めてfalseの場合、本体は実行されません。

MLZ命令は、次のような不平等を処理するために使用されています><=。during ifステートメントとは異なり、差がゼロでない場合(したがって、引数が等しくない場合)に本体にジャンプするため、MNZ命令を処理!=するために使用されます。

DO-WHILE 声明

唯一の違いwhileとは、do-whileということですdo-while、それは常に少なくとも一度実行されるように、ループ本体が最初にスキップされていません。通常do-while、ループを完全にスキップする必要がないことがわかっている場合は、ステートメントを使用してアセンブリコードを数行保存します。

配列

1次元配列は、連続したメモリブロックとして実装されます。すべての配列は、宣言に基づいて固定長です。配列は次のように宣言されます。

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コードの最終パスとしていくつかの本当に基本的な遅延スロット最適化を実行します。空の分岐遅延スロットを使用した無条件ジャンプの場合、遅延スロットはジャンプ先の最初の命令で埋められ、ジャンプ先は次の命令を指すように1ずつ増加します。これにより、通常、無条件ジャンプが実行されるたびに1サイクル節約されます。

Cogolでテトリスコードを書く

最終的なTetrisプログラムはCogolで作成されており、ソースコードはこちらから入手できます。コンパイルされたQFTASMコードは、ここから入手できます。便宜上、パーマリンクがここに提供されています:QFTASMのテトリス。目標はアセンブリコード(Cogolコードではなく)をゴルフすることであったため、結果のCogolコードは扱いにくいです。通常、プログラムの多くの部分はサブルーチンに配置されますが、これらのサブルーチンは実際にはコードを複製して命令を保存するのに十分なほど短いものでしたcallステートメント。最終コードには、メインコードに加えて1つのサブルーチンしかありません。さらに、多くの配列が削除され、同等の長さの個々の変数のリストに置き換えられるか、プログラム内の多数のハードコードされた数値に置き換えられました。最終的にコンパイルされたQFTASMコードは300命令未満ですが、Cogolソース自体よりもわずかに長いだけです。


22
アセンブリ言語命令の選択は、サブストレートハードウェアによって定義されることが大好きです(2つのfalseからtrueを組み立てるのは難しいため、MEZはありません)。素晴らしい読み。
AlexC

1
あなたはそれ=がそれ自身の隣にしか立つことができないと言いました、しかし、そこがあり!=ます。
ファビアンRöling17年

@Fabian +=
Oliphaunt

@Oliphauntええ、私の説明はあまり正確ではありませんでした。それは、特定のクラスのキャラクターが互いに隣接できるキャラクタークラスのものです。
PhiNotPi

606

パート5:アセンブリ、翻訳、および将来

コンパイラからのアセンブリプログラムを使用して、Varlifeコンピュータ用のROMを組み立て、すべてを大きなGoLパターンに変換します。

アセンブリ

アセンブリプログラムのROMへのアセンブルは、従来のプログラミングとほぼ同じ方法で行われます。各命令は同等のバイナリに変換され、実行可能ファイルと呼ばれる大きなバイナリblobに連結されます。私たちにとって唯一の違いは、バイナリblobをVarlife回路に変換してコンピューターに接続する必要があることです。

K Zhang は、アセンブリと翻訳を行うGolly用のPythonスクリプトCreateROM.pyを作成しました。かなり簡単です。クリップボードからアセンブリプログラムを取得し、バイナリにアセンブルし、そのバイナリを回路に変換します。スクリプトに含まれる単純な素数性テスターの例を次に示します。

#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

ROMはコンピューターとリンクされ、Varlifeで完全に機能するプログラムを形成します。しかし、まだ完了していません...

Game of Lifeへの翻訳

これまでずっと、Game of Lifeの基盤の上にあるさまざまな抽象化レイヤーで作業してきました。しかし、今は抽象化のカーテンを引き戻し、私たちの仕事をGame of Lifeパターンに変換する時です。前述のように、OTCAメタピクセルをVarlifeのベースとして使用しています。したがって、最後のステップは、Varlifeの各セルをGame of Lifeのメタピクセルに変換することです。

ありがたいことに、Gollyには、OTCAメタピクセルを介して異なるルールセットのパターンをGame of Lifeパターンに変換できるスクリプト(metafier.py)が付属しています。残念ながら、単一のグローバルルールセットでパターンを変換するように設計されているため、Varlifeでは機能しません。Varlifeのセルごとに各メタピクセルのルールが生成されるように、その問題に対処する修正版を作成しました。

そのため、コンピューター(テトリスROM搭載)には1,436 x 5,082の境界ボックスがあります。そのボックスの7,297,752個のセルのうち、6,075,811個は空のスペースであり、実際の人口カウントは1,221,941個です。これらのセルはそれぞれ、2048x2048の境界ボックスと64,691(ONメタピクセルの場合)または23,920(OFFメタピクセルの場合)の人口を持つOTCAメタピクセルに変換する必要があります。つまり、最終製品には、29,228,828,720から79,048,585,231の間の人口で、2,940,928 x 10,407,936のバウンディングボックス(およびメタピクセルの境界に数千の追加)が含まれることになります。ライブセルごとに1ビットの場合、コンピューターとROM全体を表すには27〜74 GiBが必要です。

スクリプトを開始する前にそれらを実行することを怠り、コンピューターのメモリがすぐに不足していたため、ここにこれらの計算を含めました。killコマンドがパニックになった後、metafierスクリプトに変更を加えました。メタピクセルが10行ごとに、パターンが(gzip圧縮されたRLEファイルとして)ディスクに保存され、グリッドがフラッシュされます。これにより、翻訳に余分なランタイムが追加され、より多くのディスク容量が使用されますが、メモリ使用量は許容範囲内に維持されます。Gollyはパターンの場所を含む拡張RLE形式を使用するため、パターンの読み込みに複雑さを追加することはなく、同じレイヤー上のすべてのパターンファイルを開くだけです。

K Zhangはこの作業に基づいて構築し、MacroCellファイル形式を使用するより効率的なメタファイラースクリプトを作成しました。これは、大きなパターンに対してRLEよりも効率的にロードされます。このスクリプトは非常に高速に実行され(元のメタファイラースクリプトの数時間に比べて数秒)、非常に小さな出力(121 KB対1.7 GB)を作成し、大量を使用せずにコンピューターとROM全体を一気にメタ化できますメモリの。MacroCellファイルは、パターンを記述するツリーをエンコードするという事実を利用します。カスタムテンプレートファイルを使用すると、メタピクセルがツリーにプリロードされ、近隣検出のための計算と修正を行った後、Varlifeパターンを簡単に追加できます。

コンピューター全体のパターンファイルとGame of LifeのROMは、ここにあります


プロジェクトの未来

テトリスを作成したので、完了しましたか?程遠い。このプロジェクトには、さらにいくつかの目標があります。

  • muddyfishとKritixi Lithosは、QFTASMにコンパイルされる高レベル言語の作業を継続しています。
  • El'endia Starmanは、オンラインQFTASMインタープリターのアップグレードに取り組んでいます。
  • quartataはGCCバックエンドに取り組んでおり、GCCを介して、独立したCおよびC ++コード(およびFortran、D、Objective-Cなどの他の言語)をQFTASMにコンパイルできます。これにより、標準ライブラリがなくても、より洗練されたプログラムをより使い慣れた言語で作成できます。
  • さらなる進歩を遂げる前に克服しなければならない最大のハードルの1つは、ツールが位置に依存しないコード(相対ジャンプなど)を出力できないという事実です。PICがないと、リンクを作成できません。そのため、既存のライブラリにリンクできることから得られる利点を逃します。私たちは、PICを正しく実行する方法を見つけることに取り組んでいます。
  • QFTコンピュータ用に作成する次のプログラムについて話し合っています。現在、ポンは素晴らしい目標のように見えます。

2
将来のサブセクションを見ると、相対的なジャンプだけではありませんADD PC offset PCか?これが正しくない場合、私の素朴な言い訳、アセンブリプログラミングは私の得意ではなかった。
MBraedley

3
@Timmmmはい、しかし非常にゆっくりです。(HashLifeも使用する必要があります)。
spaghetto

75
次に作成するプログラムは、ConwayのGame of Lifeでなければなりません。
ACK_stoverflow

13
@ACK_stoverflowそれはある時点で行われます。
メゴ

13
実行中のビデオはありますか?
PyRulez

583

パート6:QFTASMの新しいコンパイラー

Cogolは基本的なTetrisの実装には十分ですが、簡単に読めるレベルでの汎用プログラミングには単純すぎて低レベルです。2016年9月に新しい言語の作業を開始しました。実際の生活だけでなくバグも理解しにくいため、言語の進歩は遅かったです。

単純な型システム、再帰をサポートするサブルーチン、インライン演算子など、Pythonと同様の構文を持つ低レベル言語を構築しました。テキストからQFTASMへのコンパイラーは、トークナイザー、文法ツリー、高レベルコンパイラー、低レベルコンパイラーの4つのステップで作成されました。

トークナイザー

開発は組み込みのトークナイザーライブラリを使用してPythonを使用して開始されました。つまり、この手順は非常に簡単でした。コメントの除去(#includes は除く)を含む、デフォルト出力へのわずかな変更のみが必要でした。

文法ツリー

文法ツリーは、ソースコードを変更せずに簡単に拡張できるように作成されました。

ツリー構造は、ツリーを構成できるノードの構造と、他のノードとトークンでどのように構成されているかを含むXMLファイルに保存されます。

文法では、オプションのノードだけでなく、繰り返されるノードもサポートする必要がありました。これは、トークンの読み取り方法を記述するメタタグを導入することで達成されました。

生成されたトークンは、出力がsubsやなどの文法要素のツリーを形成するように、文法のルールを介して解析されますgeneric_variables。これには、他の文法要素とトークンが含まれます。

高レベルコードへのコンパイル

言語の各機能は、高レベルの構成にコンパイルできる必要があります。これらにはassign(a, 12) およびが含まれcall_subroutine(is_prime, call_variable=12, return_variable=temp_var)ます。要素のインライン化などの機能は、このセグメントで実行されます。これらはoperators として定義され、+or などの演算子%が使用されるたびにインライン化されるという点で特別です。このため、通常のコードよりも制限されています。独自の演算子も、定義されている演算子に依存する演算子も使用できません。

インライン化プロセス中に、内部変数は呼び出されている変数に置き換えられます。これは実質的に変わります

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の最後にジャンプします)。

if そして while

どちらwhileif低レベルの通訳は非常に単純です:彼らはそれらに応じて、その条件とジャンプへのポインタを取得します。whileループは次のようにコンパイルされるという点でわずかに異なります

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

call_sub そして return

ほとんどのアーキテクチャとは異なり、コンパイル対象のコンピューターは、スタックからプッシュおよびポップするためのハードウェアサポートを備えていません。これは、スタックのプッシュとポップの両方に2つの命令が必要であることを意味します。ポッピングの場合、スタックポインターをデクリメントし、値をアドレスにコピーします。プッシュの場合、アドレスから現在のスタックポインターのアドレスに値をコピーしてからインクリメントします。

サブルーチンのすべてのローカルは、コンパイル時に決定されたRAMの固定位置に保存されます。再帰を機能させるために、関数のすべてのローカルは呼び出しの開始時にスタックに配置されます。次に、サブルーチンへの引数がローカルストア内の位置にコピーされます。戻りアドレスの値がスタックに入れられ、サブルーチンが実行されます。

ときreturn文が検出され、スタックの最上位はオフにポップされ、プログラムカウンタがその値に設定されています。呼び出し側サブルーチンのローカルの値は、スタックからポップされ、以前の位置に配置されます。

assign

変数の割り当ては、最も簡単にコンパイルできます。変数と値を受け取り、1行にコンパイルします。 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のようにインデントを使用して、論理ブロックと制御フローを分割しています。これは、プログラムにとって空白が重要であることを意味します。すべての完全なプログラムには、Cに似た言語の関数のmainようにmain()機能するサブルーチンがあります。この関数は、プログラムの開始時に実行されます。

変数と型

変数が初めて定義されるとき、変数はそれらに関連付けられた型を持つ必要があります。現在定義されている型はintbool定義された配列の構文であり、コンパイラではありません。

ライブラリと演算子

stdint.txtという基本的な演算子を定義するライブラリが利用可能です。これが含まれていない場合、単純な演算子でさえ定義されません。このライブラリはで使用できます#include stdintstdintオペレーターなどの定義+>>さらには*%直接QFTASMオペコードであるどちらもが、。

この言語では、QFTASMオペコードをで直接呼び出すこともできます__OPCODENAME__

加算stdintは次のように定義されます

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

これは、+2が与えられたときにオペレーターが行うことを定義しますint


1
このような既存のcgolユニバーサルコンピューターを再利用/改造するのではなく、Conwayの生活のゲームでワイヤーワールドのようなCAを作成し、この回路を使用して新しいプロセッサを作成することに決めたのはなぜですか?
eaglgenes101

4
@ eaglgenes101まず第一に、私たちのほとんどは他の使用可能なユニバーサルコンピューターの存在を認識していなかったと思います。複数のルールが混在するワイヤーワールドのようなCAのアイデアは、メタセルをいじった結果として生まれました(Phiはアイデアを思いついた人だと思います)。そこから、私たちが作成したものへの論理的な進歩でした。
メゴ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.