Ocala 概説
Ocala は 8 bit CPU/MPU をターゲットとしたアセンブラです。 各種の構造化機構と対象文脈式を特徴とします。 現在 Z80 および 6502 のバイナリ出力に対応しています。
手続きとコード
Ocala ではコードは手続きの中に記述します。
proc <proc-name>(<signature>) { ... }
コードは以下の 2 種類の記法で記述できます。
- 機械語命令記法(直接記法)
- 対象文脈記法(中置記法)
機械語命令記法では、CPU 依存の機械語命令を直接記述します。
ただし、(...)
は定数の記述となるため、メモリ参照には [...]
を利用します。
また、オペランドの区切りの,
は省略できます。
LD A 10
LD A [HL]
INC A
対象文脈記法では、対象となるオペランドを指定し、その文脈での操作を列記します。
A // Specify the target operand.
<- 10 // LD A 10
+ 20 // ADD A 20
-> [0x1234] // LD [0x1234] A
A <- 10 + 20 -> [0x1234] // Same as above.
手続きのシグネチャ
手続きにはシグネチャを指定できます。 シグネチャは手続き内でのレジスタの用法に関する注記です。 手続きの呼び出し時はこのシグネチャも記述する必要があります。 意図を示す注記であり、実際の用法を検査するものではありません。
proc f(A B HL => A ! BC E) { // In: A, B and HL
... // Out: A
} // Modify: BC and E.
proc g(=> A L !) { // In: none
... // Out: A and L
} // Modify: all others
proc main() {
f(A B HL => A ! BC E) // OK
f() // ?? proc signature mismatch: f.
g(=> A L !) // OK
...
}
手続きの末尾
手続きの末尾は return
/ goto
もしくはその派生である必要があります。
// Valid
proc valid-a() {
return // OK
}
proc valid-b() {
goto valid-c // OK
}
proc valid-c() {
fallthrough // OK: same as `goto valid-d`
}
proc valid-d() {
recur // OK: same as `goto valid-d`
}
proc valid-e() {
never-return loop { NOP } // OK: never return
}
// Invalid
proc invalid-a() {
NOP // ?? the last instruction must be a return/fallthrough within the proc
}
proc invalid-b() {
fallthrough // ?? it is the last proc
}
名前の構成
手続き名など、プログラム上で扱う名前(識別子)には、
英数字(a-zA-Z0-9
)と一部の記号(-^!$%&*+/<=>?|~_
)を使用できます。
+
や -
などの演算子も名前(識別子)として扱われるので、空白などを用いずに詰めて
記述すると別の識別子として扱われます。
例えば、単項マイナス演算子(-
)は括弧を用いる必要があります。
proc f-g() { return } // the procedure named `f-g'
const f+g = 1 // OK: the constant named `f+g'
A + 1 // register `A', operator `+', number `1'
A+1 // ?? unknown form name A+1
L001:
HL <- -(L001) // OK
HL <- -L001 // ?? undefined name -L001
区切り文字
プログラム中では、,
記号は通常空白文字と同様に扱われます。
ただし、2 個以上連続して記述することはできません。
また、行末に記述すると行の継続を示すことができます。
LD A 1 // OK
LD A, 1 // OK
LD A,, 1 // ??
LD A,
1 // OK: LD A 1
インライン手続き
手続きのシグネチャの先頭に -*
を指定すると、インライン手続き定義となります。
インライン手続きは呼び出し側に直接展開されます。
proc i(-* A => !) { // Inline
...
}
proc main() {
i(-* A => !) // OK
i(A => !) // ?? signature mismatch
}
メモリ参照
メモリの参照は [...]
を使用します。
6502 であってもメモリ参照には括弧を使用する必要があります。
LD A 12
LD HL [0xfffe]
C <- [HL@1024] // LD HL, 1024; LD C, (HL)
LDA 1 // LDA #1
LDA [8] // LDA 8
LDA [[8] Y] // LDA (8), Y
対象文脈式演算子
対象文脈では以下の操作を記述できます(_ は文脈、% は引数)。
操作 | Z80 | 6502 |
---|---|---|
<- % |
LD _ % |
T%_ / LD_ % |
-> % |
LD % _ |
T_% / ST_ % |
<-> % |
EX _ % |
- |
+ % |
ADD _ % |
CLC; ADC % |
+$ % |
ADC _ % |
ADC % |
- % |
SUB % /OR A; SBC _ % |
SEC; SBC % |
-$ % |
SBC _ % |
SBC % |
-? % |
CP _ % |
CM_ % |
& % |
AND % |
AND % |
| % |
OR % |
ORA % |
^ % |
XOR % |
EOR % |
<* % |
(RLCA / RLC _ ) * % |
(CMP 0x80; ROL A ) * % |
<*$ % |
(RLA / RL _ ) * % |
(ROL _ ) * % |
>* % |
(RRCA / RRC _ ) * % |
(LSR A; BCC +2; ORA 0x80 ) * % |
>*$ % |
(RRA / RR _ ) * % |
(ROR _ ) * % |
<< % |
(SLA _ ) * % |
(ASL _ ) * % |
>> % |
(SRA _ ) * % |
(CMP 0x80; ROR A ) * % |
>>> % |
(SRL _ ) * % |
(LSR _ ) * % |
-set % |
SET % _ |
- |
-reset % |
RES % _ |
- |
-bit? % |
BIT % _ |
BIT |
-in % |
IN _ % |
- |
-out % |
OUT % _ |
- |
++ |
INC _ |
CLC; ADC 1 / IN_ / INC _ |
-- |
DEC _ |
SEC; SBC 1 / DE_ / DEC _ |
-push |
PUSH _ |
PH_ |
-pop |
POP _ |
PL_ |
-not |
CPL |
EOR 0xff |
-neg |
NEG |
EOR 0xff; CLC; ADC 1 |
-zero? |
AND A /INC _; DEC _ |
- |
@% |
alias of <- |
|
. { ... } |
inline sub context |
制御構造
手続き内では、以下の制御構造を使用できます。
制御構造 | 処理内容 |
---|---|
基本ブロック | |
do { ... } |
ブロック評価 |
<label>: |
ラベル定義 |
分岐 | |
if <cond> { ... } else { ... } |
条件分岐 |
goto <label> |
ジャンプ |
goto-if <cond> <label> |
条件ジャンプ |
goto-rel <label> |
(Z80) 相対ジャンプ |
goto-rel-if <cond> <label> |
(Z80) 相対条件ジャンプ |
ループ制御 | |
loop { ... } |
無限ループ |
loop { ... } while <cond> |
条件ループ |
once { ... } |
1度のみのループ |
redo |
ループ先頭へジャンプ |
redo-if <cond> |
ループ先頭へ条件ジャンプ |
continue |
ループ条件部へジャンプ |
continue-if <cond> |
ループ条件部へ条件ジャンプ |
break |
ループ中断 |
break-if <cond> |
条件付ループ中断 |
手続き制御 | |
return |
リターン |
return-if <cond> |
(Z80) 条件リターン |
recur |
手続き先頭へジャンプ |
fallthrough |
リターンせず次の手続きを評価 |
never-return loop { ... } |
手続き末で無限ループ |
条件 <cond>
として、以下を使用できます。
Z80 | Z80 別名 | 6502 | 6502 別名 |
---|---|---|---|
NZ? |
!=? not-zero? |
NE? |
!=? not-zero? |
Z? |
==? zero? |
EQ? |
==? zero? |
NC? |
>=? not-carry? |
CC? |
<? not-carry? borrow? |
C? |
<? carry? |
CS? |
>=? carry? not-borrow? |
PO? |
odd? not-over? |
VC? |
not-over? |
PE? |
even? over? |
VS? |
over? |
P? |
plus? |
PL? |
plus? |
M? |
minus? |
MI? |
minus? |
定数
Ocala では、定数を定義することができます。
const <const-name> = <constexpr>
定数式では他の定数やラベルを参照できます。また、各種の演算子や括弧も利用可能です。
data ROM_ADDR = 0x4000
data RAM_ADDR = ROM_ADDR + 0x8000
定数式演算子
定数式では、以下の演算子を利用可能です。
operator |
---|
* / % |
+ - |
<< >> >>> |
< <= > >= |
== != |
& |
| |
^ |
&& |
|| |
構造体
Ocala では、構造体を定義することができます。
struct { <field> ... }
struct <struct-name> { <field> ... }
<field>:
<field-name> <type>
構造体は入れ子にできますが、内側の構造体は名前を持てません。
struct point {
x byte
y byte
}
struct point { x byte; y byte } // OK: same as above
struct outer { // named struct
inner struct { // unnamed struct
x byte
}
}
構造体型のデータは .
を利用してフィールドのアドレスを参照できます。
.
の後には空白を入れずにフィールド名を記述します。
struct point {
x byte
y byte
}
data pt = point { 0 0 }
data byte pt.x pt.z // OK
data byte pt. x // ?? scan error
データ
Ocala では、データを定義することができます。
data <type> <data-body> <repeat> <allocation>
data <data-name> = <type> <data-body> <repeat> <allocation>
<data-body>:
[ ... ]
{ ... }
<repeat>:
* <integer>
<allocation>:
: <section-name>
@ <address>
データの定義では、データ本体を含めた定義とデータ本体を持たない領域のみの定義が可能です。
データの名前を指定した場合、ラベルとして参照できます。
また名前のあるデータは、定数式内で sizeof
式でサイズを取得できます。
データには型が必要です。byte(1 バイト単位定義) または word(2 バイト単位定義) を指定できます。
構造体名や struct { ... }
そのものを指定することもできます。
data
ディレクティブはデータの配列を定義します。
配列のサイズは型の前に [<size>]
で指定します。
データ本体がある場合、要素数は省略可能です。
データの型として単独の型を指定した場合、実際にはその型の配列を指定したことと同義です。 この場合、サイズは省略されたものとして、データ本体の要素数から自動で設定されます。 データ本体がない場合は 1 要素の配列として扱われます。
data byte // OK: same as `data [1]byte`
data byte [123] // OK: same as `data [1]byte`
data []byte [123] // OK: same as `data [1]byte`
data [1]byte [123] // OK
データの本体は型の直後に記述します。空の場合は省略できます。
通常は [...]
を指定してデータの配列を記述します。
構造体の場合は {...}
を記述します。
これらを組み合わせて「構造体の配列」なども記述できます。
データの型が byte
の場合、 要素として "..."
の形式で文字列を指定できます。
実態はバイト型データの配列です。 C 言語などとは異なりゼロ終端はされません。
データは繰り返し回数を指定できます。省略時のデフォルトは 1 です。
データは配置セクションまたは配置アドレスを指定できます。
省略時のデフォルトは現在のセクションへの配置です。
配置アドレスとして特別なアドレス <reserved>
を指定すると、データの配置アドレスを予約できます。
予約されたデータ配置アドレスは主に自己書き換えに利用します。
*patch*
ディレクティブで後から配置アドレスを設定できます。
data byte [0 1 2 3]
data []byte [0 1 2 3] @ 0x1000
data struct { x byte; y byte } { 4 5 }
data str = byte [ "hello!" ]
data tab = byte [ 0 1 2 4 8 ] : rodata
data dat = word * 10 : bss
data smc = byte @ <reserved>
A <- 0; *patch* smc byte
モジュール
Ocala ではプログラムは複数の「モジュール」から構成されます。
module <module-name> { ... }
モジュールは以下の 2 つの側面を持ちます。
- 名前空間
- セクション(バイナリの配置区画)の集合
モジュールは名前空間です。 Ocala では定数や手続きなどの名前は字句的に解決されます。ただし、名前空間を指定することで 他のモジュールに属する名前を参照することもできます。
module mod-a {
const c = 1
const d = c + 1
}
module mod-b {
const c = mod-a:d // Use the constant 'd' in the module 'mod-a'
}
セクションはコードやデータの集まりです。 出力バイナリ上ではセクション単位でコードとデータが配置されます。 モジュール定義直下では任意の位置でセクションの開始を宣言できます。
section <section-name>
標準で各モジュールごとに以下のセクションを持ちます。
- text(コード領域)
- rodata(読込専用データ領域)
- bss(未初期化データ領域)
モジュールや手続きは以下の要素から構成されます。
- データ定義文
- 機械語命令文
- 対象文脈式文
- その他の特殊形式文(疑似命令)
リンク
各モジュールの各セクションはコード生成前に内部中間表現レベルで 「リンク」処理され結合されます。
リンク処理の内容は link 特殊形式を利用して指定します。
link { <link-directive>... }
<link-directive>:
org <address> <size> <mode>
merge <section-name> <module-name>...
例えば、以下の様に text セクションと bss セクションをリンクします。
link {
org 0x4000 0x8000 2 // Set the current address to 0x4000.
// Set the region size to 0x8000 bytes.
// Output zero-padded binary(mode 2).
merge text main _ // Merge all text sections.
// Start with the main module.
merge rodata main _ // Merge all rodata sections.
org 0xc000 0x4000 0 // Set the current address to 0xc000.
// Set the region size to 0x4000 bytes.
// Do not output this region(mode 0).
merge bss main _ // Merge all bss sections.
}
一般にリンク処理の内容は対象機種毎にほぼ定型であるため、 ライブラリ側でリンク方法をマクロ定義できます。 そのため、実際にはプログラム側では link 特殊形式を直接記述せずに ライブラリに定義されたマクロを利用します。
include "msx.oc"
msx:link-as-rom main _
マクロ
マクロを定義することで、複数の処理をまとめることができます。
macro <macro-name> ( <param>... ) [ <var>... ] { ... }
マクロではマクロ引数を使用できます。
<name>: <default>
の形式で省略可能な引数を指定できます。
最後の引数に ...
を指定すると可変長引数となります。
また、ラベル等に利用できるマクロ変数を使用できます。 マクロ変数はマクロ外と重複しない名前に展開されます。
macro myloop(body) [beg] {
%=beg: do %=body
goto %=beg
}
myloop { NOP }
引数、変数は以下のプレースホルダーを利用して展開できます。
プレースホルダー | 展開内容 |
---|---|
%= | そのままの値 |
%* | 可変長引数の埋め込み |
%# | 可変長引数の個数 |
%& | 定数式内へ非定数式を埋め込み |
ブロックとスコープ
Ocala ではコードをブロックでまとめることができます。
{ ... }
={ ... }
ブロック単体では妥当な文とはならないため、他の構文と組み合わせる必要があります。
単純な文の並びとして扱うだけであれば do { ... }
構文が使用できます。
その他、条件文などにも使用できます。
ブロックには { ... }
と ={ ... }
の 2 種類があります。
違いはブロック内の可視範囲の扱いです。
{ ... }
の場合、ブロック内は新たな可視範囲となり、ブロック外から
ネストされたものとなります。
ネストされた可視範囲では名前のシャドウイングが可能であり、
ブロックの外のものと同じ名前の定数などを定義できます。
={ ... }
はブロック外と可視範囲を共有し、
新たな可視範囲を構成しません。
proc f() {
const a = 0
do {
const a = 1 // OK
}
do ={
const a = 2 // ?? a is already defined
}
...
}
ブロックの違いはマクロ定義にも適用されます。
={ ... }
で定義されたマクロは展開箇所の可視範囲を共有します。
macro m() { const a = 1 }
macro n() ={ const a = 2 }
proc f() {
const a = 0
m // OK
n // ?? a is already defined
}
ディレクティブ
ソースコード中、以下のディレクティブを利用できます。
ディレクティブ | 機能 |
---|---|
include "<path>" |
他ソースファイルの読込 |
incbin "<path>" |
他ファイルの埋め込み |
section <section-name> |
セクション変更 |
section <section-nmame> { ... } |
ブロック内のセクション指定 |
if <constexpr> { ... } else { ... } |
条件コンパイル |
expand-loop <n> { ... } |
n 回のループ展開 |
alias <to> <from> |
別名定義 |
assert <constexpr> |
リンク時アサーション |
import <namespace> |
名前空間のインポート |
debug-inspect <constexpr> |
式の値の表示(デバッグ用) |
その他の補助機能
その他以下の補助機能を使用可能です。
制御構造 | 処理内容 |
---|---|
スタック | |
push* <reg>... |
一括プッシュ |
pop* <reg>... |
一括ポップ |
push/pop <reg>... { ... } |
レジスタを一時退避してブロックを評価 |
割り込み | |
di/ei { ... } |
(Z80) DI、ブロック評価、EI |
フラグ | |
set-carry |
キャリーフラグセット |
clear-carry |
キャリーフラグクリア |
clear-over |
(6502) オーバーフローフラグクリア |