DEC社 TOPS-20のSNOBOL言語で8080アセンブラを作成(コード編)

SNOBOL

SNOBOL

SNOBOL言語で8080アセンブラ

2018年4月、歴史的に貴重なDEC社 TOPS-20のSNOBOL言語で8080アセンブラ(ASM80.SNO)を作成しました。このブログでは、ASM80.SNOのコード編を書きます。

ASM80 By SNOBOL

ASM80 By SNOBOL

ASM80.SNOの設計編は、下記ブログを参照してください。

DEC社 TOPS-20のSNOBOL言語で8080アセンブラを作成(設計編)

ASM80.SNOのSNOBOLコード説明

ASM80.SNOのSNOBOLコードの全体は、このブログの最後に示します。

SNOBOL言語で記述して約400ステップ規模で、8080アセンブラを作成することができました。SNOBOL言語の強力なパターンマッチ能力の力ですね。

入出力デバイス定義

ソースファイル名を入力して、入出力用デバイスの定義を行います。

  • 変数 ISRC : .ASM ソースファイル名(KEY_DEVから入力)
  • 変数 OLST : .LST リスト出力ファイル名 (拡張子.ASMを.LSTに置換)

デバイスの定義を行います。

デバイス名 デバイス
CON_DEV TTY:出力 ターミナルモード
KEY_DEV TTY:入力
INPUT_PS1 DSK:ISRC ソース入力 パス1用
INPUT_PS2 DSK:ISRC ソース入力 パス2用
OUTPUT_LST DSK:OLST リスト出力

関数定義

プログラムで使用するユーザー関数と演算子を定義します。

関数名 機能 概要
ADDLTBL
(LABEL,VAL)
ラベル登録 ラベルテーブルにLABELでVALを登録
REFLTBL
(LABEL)
ラベル参照 ラベルテーブルからLABELを参照
GETIMM
(STR)
直値参照 STRから直値を参照
TOUPPER
(STR)
英大文字変換 STRを英大文字に変換
CVSTR2BIN
(STR,BASE)
文字列基数変換 文字列STRを基数(BASE)で整数に変換
CVBIN2HEX
(BIN,N)
整数ヘキサ変換 整数BINをN桁16進数に変換
CVOCT2HEX
(STR)
8進to16進変換 8進数文字列STRを16進数2桁に変換

演算子「%」を演算子シノニム定義で、剰余を算出する組み込み関数 REMDR()の呼び出しに定義します。すなわち、「N % D」は、REMDR(N,D)を呼び出します。

STARTラベルに分岐して、関数定義本体の実行をスキップします。

関数本体の定義

ラベル登録

ラベルテーブルにLABELをキーとしてVALを登録します。

PASSによりふたつのラベルテーブルTBL_LAB1<○>とTBL_LAB2<○>を使い分けます。

  1. LABLEが登録済みの場合は、二重登録エラーなので FRETURNします。
  2. ラベルテーブル<LABEL>にVALを登録します。

ラベル参照

ラベルテーブルからLABELキーとして参照します。

PASS=1のとき、LABELが未登録なら “0”を返します。
パス1用のラベルテーブルTBL_LAB1<LABEL>を参照します。

直値参照

STRから直値を参照します。

  1. 直値のパターンに一致しない場合は、パターン不一致エラーで FRETURNします。
  2. 直値のパターンに一致した場合は、一致部分のIMMとHB識別を設定します。HB識別(”H” or “B” or “”)によりBASE(16 or 2 or 10)に変換します。
  3. 一致部分 IMMをBASE進数の数値に変換します。
  4. 数値変換が正常なら、数値を戻り値にします。
  5. 数値変換が異常なら、ラベルとみなしてIMMでラベル参照してSTRに設定して直値のパターンチェックを繰り返します。

英大文字変換

STRを英大文字に変換します。

文字列基数変換

文字列STRを基数(BASE)で整数に変換します。

整数ヘキサ変換

整数BINをN桁16進数に変換します。

8進to16進変換

8進数文字列STRを16進数2桁に変換します。

各種の定義

ASM80の開始メッセージを表示します。

ラベルテーブルを定義します。

テーブル名 用途
TBL_LAB1 パス1用ラベルテーブル
TBL_LAB2 パス2用ラベルテーブル

ラベルのパターン(PT_LABEL)を定義します。左端から英字、数字、アンダーラインの組合せの後にコロン(:)がついた文字列をラベルとします。

8080命令コードの定義

OPCODEテーブルに命令をキーとして命令コードと命令長を登録します。命令コードは、8進数3桁で8bitを表します。プラス(+)の後ろに命令長を1~3で定義します。


「OPCODE<“JMP” > = “303+3″」は、JMP命令の定義です。
3バイト命令長で、命令コードは8進数で(303)=2進数で(11 000 011)=16進数で(C3)です。


「OPCODE<“MOV” > = “1DS+1″」は、MOV命令の定義です。
1バイト命令長で、命令コードは8進数で(1DS)=2進数で(01 ddd sss)で、dddは転送先レジスタ、sssは転送元レジスタです。


疑似命令の定義

以下の疑似命令をサポートします。

機能 疑似命令
定数定義 ラベル名 EQU 数値
アドレス設定 ORG アドレス
1バイトの定数定義 DB バイト定数
2バイトの定数定義 DW ワード定数
領域定義 DS 領域数

8080命令のオペランド定義

8080命令をオペランドの形式で分類して定義します。

形式 命令 オペランド部
RM_RM MOV Rd,Rs
RM_IMM MVI Rd,Imd8
RM INR/DCR
ADD/ADC/SUB/SBB
ANA/XRA/ORA/CMP
R
IMM ADI/ACI/SUI/SBI
ANI/XRI/ORI/CPI
Imd8
PORT IN /OUT Port
NON HLT/EI /DI /NOP
RLC/RRC/RAL/RAR
RET
RC /RNC/RZ /RNZ
RP /RM /RPE/RPO
XCHG/XTHL/SPHL/PCHL
CMA/STC/CMC/DAA
ADR JMP
JC /JNC/JZ /JNZ
JP /JM /JPE/JPO
CALL
CC /CNC/CZ /CNZ
CP /CM /CPE/CPO
LDA /STA/LHLD/SHLD
Adr
VEC RST Vect
BDHA PUSH/POP Rp
BD LDAX/STAX Rp
BDHS DAD/INX/DCX Rp
BDHS_I LXI Rp,Imd16

命令のパターン P_INST を定義します。

LINEの開始処理

  1. 生成オブジェクトを格納するOBJHEX配列を3個定義します。
  2. PASS←1に設定
  3. PASSにより INPUT_DEVデバイスを設定します。INPUT_PSx (x=1or2)
  4. プログラムカウンタ PC←0に設定します。
  5. 行カウンタ LN←1に設定します。
  6. エラーをリセット(ERSW←0)します。
  7. 命令長 OPLEN←0に設定します。
  8. 変数LINESRCにソースコードを入力します。EOFなら、:F(FILE_END)に分岐します。
  9. 変数LINEに英大文字変換して格納します。
  10. セミコロン(;)から後ろを消して、コメントを除去します。
  11. ラベルパターンに一致した場合は、ラベル部をLABELに設定してラベルテーブルにキーLABELでPCを登録します。
  12. END文なら、OUT_OBJに分岐します。
  13. 行の右端の不要な空白を除去し、空行ならOUT_OBJに分岐します。

疑似命令と命令のパターン一致

疑似命令(P_PSEUDO)にパターン一致した場合は、一致部分を変数PSEUDOに格納して S($PSEUDO)に分岐します。変数PSEUDOの内容に($PSEUDO)で分岐できるのは、SNOBOL言語の凄さです。
ラベル ORG or DB or DW or DS or EQU に分岐します。

P_PSEUDO = “ORG” | “DB” | “DW” | “DS” | “EQU”

命令パターン(P_INST1/P_INST2)に一致した場合は、一致部分を変数INSTに格納します。命令パターンに一致しない場合は、ERR_INSTに分岐して命令エラーとします。

P_INST1 と P_INST2 に分離したのは、意味があります。SNOBOLパターンマッチの記述方法に不備があるのか、ミスマッチが発生しました。

  • LDAX命令が”LDA”に一致
  • STAX命令が”STA”に一致
  • INX命令が”IN”に一致

そこで、最初にP_INST1でパターンマッチし、不一致の場合だけP_INS2のパターンマッチを行います。命令コードを変数 OPCに設定して、 プラス(+)の後ろの命令長を変数 OPLENに設定します。命令 INST の種別により、各オペランド解析形式へ分岐します。

エラー処理

各エラーによりMSG_ERRにエラーメッセージを格納して、命令長 OPLEN←0として、ERSW←1とします。

オブジェクト出力

  1. PASS=2のとき、LSTファイルを作成してデバイスOUTPUT_LSTに出力します。
  2. エラーがある場合(ERSW=1)は、行番号、ソース(LINESRC)、エラーメッセージ(MSG_ERR)を出力します。
  3. プログラムカウンタ PCを命令長 OPLEN進めます。
  4. 行カウンタ LNをインクリメントします。
  5. LINE_STARTに分岐して、次の行へ進みます。

EOFとパスの終了

  1. ソースファイル入力がEOFになれば、PASSを進めます。
  2. パス2が終了すれば、ASM80の終了メッセージを表示しENDに分岐して終了します。

各疑似命令の処理

疑似命令 ORG、DB、DW、DS、EQUの処理を行います。

ORG 疑似命令

  1. LINEからGETIMM()でアドレスを取得して、PCに設定します。

DB 疑似命令

  1. LINEからGETIMM()で直値(IMM)を取得します。1バイトの定数定義なので、255を超える場合はIMMエラーに分岐します。
  2. 命令長 OPLEN←1として、オブジェクトの1バイト目に定数IMMを設定します。

DW 疑似命令

  1. LINEからGETIMM()で直値(AD)を取得します。
  2. 命令長 OPLEN←2として、オブジェクトの1バイト目に定数ADの下位バイト、2バイト目にADの上位バイトを設定します。

DS 疑似命令

  1. LINEからGETIMM()で直値(IMM)を取得します。
  2. 命令長 OPLEN←IMMとして領域を確保します。
  3. オブジェクトの1~3バイト目には、”??”を設定します。

EQU 疑似命令

  1. ラベルの形式でなければ、LABELエラーに分岐します。ラベルの場合は、DEFLABにラベルを設定します。
  2. GETIMM()で、直値(IMM)を取得します。
  3. ラベルDEFLABをキーとしてIMMをラベル登録します。

8080命令の処理

オペランド形式ごとに、オペランドの解析をしてオブジェクトコードをOBJHEX<>配列に設定します。

命令 オペランド RM_RM形式

MOV命令は、オペランドに転送先レジスタと転送元レジスタを指定します。

転送先 転送元
A,B,C,D,E,H,L A,B,C,D,E,H,L
A,B,C,D,E,H,L M
M A,B,C,D,E,H,L

  1. 該当レジスタ以外のときは、レジスタエラーとします。
  2. レジスタ名(B,C,D,E,H,L,M,A)をコード(0,1,2,3,4,5,6,7)に変換します。
  3. 命令コード OPCのレジスタ部分(D S)をコードで置換します。
  4. オブジェクトの1バイト目に命令コード OPCを設定します。

命令 オペランド RM_IMM形式

MVI命令は、オペランドにレジスタ(B,C,D,E,H,L,M,A)とバイト定数を指定します。

  1. 該当レジスタ以外のときは、レジスタエラーとします。
  2. GETIMM()で、カンマ”,”の右側のバイト定数(IMM)を取得します。1バイトの定数定義なので、255を超える場合はIMMエラーとします。
  3. レジスタ名(B,C,D,E,H,L,M,A)をコード(0,1,2,3,4,5,6,7)に変換します。
  4. 命令コード OPCのレジスタ部分(D)をコードで置換します。
  5. オブジェクトの1バイト目に命令コード OPCを設定します。
  6. オブジェクトの2バイト目にIMMを設定します。

命令 オペランド RM形式

下記命令は、オペランドにレジスタ(B,C,D,E,H,L,M,A)とバイト定数を指定します。

INR/DCR
ADD/ADC/SUB/SBB/ANA/XRA/ORA/CMP

  1. 該当レジスタ以外のときは、レジスタエラーとします。
  2. レジスタ名(B,C,D,E,H,L,M,A)をコード(0,1,2,3,4,5,6,7)に変換します。
  3. 命令コード OPCのレジスタ部分(D)をコードで置換します。
  4. オブジェクトの1バイト目に命令コード OPCを設定します。

命令 オペランド IMM形式

下記命令は、オペランドにバイト定数を指定します。

ADI/ACI/SUI/SBI/ANI/XRI/ORI/CPI

  1. GETIMM()で、バイト定数(IMM)を取得します。1バイトの定数定義なので、255を超える場合はIMMエラーとします。
  2. オブジェクトの1バイト目に命令コード OPCを設定します。
  3. オブジェクトの2バイト目にバイト定数 IMMを設定します。

命令 オペランド PORT形式

下記命令は、オペランドにバイト定数を指定します。

IN/OUT

  1. GETIMM()で、バイト定数(PORT)を取得します。1バイトの定数定義なので、255を超える場合はIMMエラーとします。
  2. オブジェクトの1バイト目に命令コード OPCを設定します。
  3. オブジェクトの2バイト目にバイト定数 PORTを設定します。

命令 オペランド NON形式

下記命令は、オペランドがありません。

HLT/RLC/RRC/RAL/RAR
RET/RC /RNC/RZ /RNZ/RP /RM /RPE/RPO
XCHG/XTHL/SPHL/PCHL
CMA/STC/CMC/DAA/EI /DI /NOP

  1. オブジェクトの1バイト目に命令コード OPCを設定します。

命令 オペランド ADR形式

下記命令は、オペランドにアドレスを指定します。

JMP /JC /JNC/JZ /JNZ/JP /JM /JPE/JPO
CALL/CC /CNC/CZ /CNZ/CP /CM /CPE/CPO
LDA /STA/LHLD/SHLD

  1. GETIMM()で、ワード定数(ADR)を取得します。
  2. オブジェクトの1バイト目に命令コード OPCを設定します。
  3. オブジェクトの2バイト目にADRの下位バイト、3バイト目にADRの上位バイトを設定します。

命令 オペランド VEC形式

RST命令は、オペランドにベクトルを指定します。

  1. GETIMM()で、定数(VEC)を取得します。0~7の定数定義なので、7を超える場合はVECエラーとします。
  2. ベクトル(0,1,2,3,4,5,6,7)をコード(0,1,2,3,4,5,6,7)に変換します。
  3. 命令コード OPCのベクトル部分(V)をコードで置換します。
  4. オブジェクトの1バイト目に命令コード OPCを設定します。

命令 オペランド BDHA形式

PUSH/POP命令は、オペランドにレジスタ(B,D,H,PSW)を指定します。

  1. 該当レジスタ以外のときは、レジスタエラーとします。
  2. レジスタ名(B,D,H,PSW)をコード(0,2,4,6)に変換します。
  3. 命令コード OPCのレジスタ部分(R)をコードで置換します。
  4. オブジェクトの1バイト目に命令コード OPCを設定します。

命令 オペランド BD形式

LDAX/STAX命令は、オペランドにレジスタ(B,D)を指定します。

  1. 該当レジスタ以外のときは、レジスタエラーとします。
  2. レジスタ名(B,D)をコード(0,2)に変換します。
  3. 命令コード OPCのレジスタ部分(P Q)をコードで置換します。
  4. オブジェクトの1バイト目に命令コード OPCを設定します。

命令 オペランド BDHS形式

DAD/INX/DCX命令は、オペランドにレジスタ(B,D,H,SP)を指定します。

  1. 該当レジスタ以外のときは、レジスタエラーとします。
  2. レジスタ名(B,D,H,SP)をコード(0,2,4,6)に変換します。
  3. 命令コード OPCのレジスタ部分(P Q)をコードで置換します。
  4. オブジェクトの1バイト目に命令コード OPCを設定します。

命令 オペランド BDHS_I形式

LXI命令は、オペランドにレジスタ(B,D,H,PSW)とワード定数 を指定します。

  1. 該当レジスタ以外のときは、レジスタエラーとします。
  2. GETIMM()で、カンマ”,”の右側のワード定数(ADR)を取得します。
  3. レジスタ名(B,D,H,PSW)をコード(0,2,4,6)に変換します。
  4. 命令コード OPCのレジスタ部分(P)をコードで置換します。
  5. オブジェクトの1バイト目に命令コード OPCを設定します。
  6. オブジェクトの2バイト目にADRの下位バイト、3バイト目にADRの上位バイトを設定します。

 

SNOBOLによる8080アセンブラコード完成

SNOBOL言語による8080アセンブラのコードが完成しました。強力なパターンマッチ能力で簡潔にASM80.SNOを記述できました。

ASM80.SNOのテストについては、次回に書きます。

ASM80.SNO ソースコード

ASM80.SNOのSNOBOLコードの全体を示します。
ソースコード

このエントリーをはてなブックマークに追加