Skip to content

Latest commit

 

History

History
487 lines (405 loc) · 39.1 KB

spec_sol.md

File metadata and controls

487 lines (405 loc) · 39.1 KB

sol (Stack Oriented Language) 仕様書

sol (Stack Oriented Language) は、スタック指向のプログラミング言語であり、SRC32アセンブリにコンパイルされる。 名称は小文字の "sol" で表記される。

スタックには32ビットの符号付き数値が入る。データスタックを持つ。 文はスペースで区切られていて、左から解釈される。 ほとんどのオペレーターは短い (基本的に5文字以内) アルファベットで記述する。 記号類は特殊な修飾子などに使われる。 これらの文は最終的にSRC32のアセンブリにコンパイルされる。

スタック

スタックは、LIFO (Last In First Out) のデータ構造であり、最後にプッシュされた値が最初にポップされる。

関数

関数は fn name (arg1 arg2 ...) : で始まり、関数名と引数名を (...) 内に指定する。関数の本体は ; で終了する。

この仕様では、関数呼び出しのための呼び出し規約(calling convention)とスタックフレームのレイアウトを定める。コンパイラはこの規約に従ってコードを生成し、ランタイム(VM/アセンブリ)はそれを実行する。

重要な前提:

  • データスタックは下方向(アドレスが小さくなる方向)に伸びるものとする(push は R28 を 4 バイト減算して書き込む)。R28 は実行時スタックポインタを指す。

呼び出し規約

solの関数呼び出しは、データスタックを引数受け渡しと式評価に使う。規約は次の通りとする。

  1. 呼び出し側(caller)は、引数を左から右の順でデータスタックに積む。つまり fn f (a b) に対しては a を先に push し、その後に b を push する。
  2. 関数は名前をそのまま単語として書くことで呼び出される。コンパイラは内部的に呼び出し命令を生成し、データスタック最上位から関数の引数個数ぶんを消費してその関数の引数として扱う。関数の下に残っている既存のスタック値は保持される。
  3. 呼び出し時、ランタイムは関数用のフレームを確保し、引数とローカル変数はそのフレーム内の固定オフセットでアクセスされる。
  4. 関数本体は、戻り値をデータスタックのトップに 1 個だけ残して ret を実行する。ret はフレームを解放し、呼び出し元へ戻る。
  5. 戻り値を返さない関数は retn を使う。retn はフレームを解放し、戻り先に復帰するが、データスタック上の戻り値は残さない。

この規約では、関数呼び出しは「スタックの先頭に対する局所的な操作」であり、呼び出し前にスタック下位へ積まれていた値は関数終了後も保持される。したがって、関数呼び出しはスタック全体を丸ごと置き換えるのではなく、引数部分だけを消費して 0 個または 1 個の結果を返す。

スタックフレームのレイアウト

フレームはコンパイル時に必要なローカル変数数(N_locals)と引数数(N_args)が分かればサイズが決定できる。

  • フレームは 4 バイト単位で整列される。
  • フレーム確保時の R28 の変化(スタックが下方向に成長するため):
    • R28 := R28 - 4 * (N_locals + N_args)
  • フレーム内のオフセット(R28 がフレームのベースを指す、すなわち最も低いアドレス):
    • ローカル変数 i (0-based): address = R28 + 4 * i
    • local 0 は R28 + 0、local 1 は R28 + 4、... の位置に置かれる。
    • 引数 j (0-based, 左から右の順に arg0, arg1, ...): address = R28 + 4 * (N_locals + j)
    • arg0 はローカル領域の直後に置かれる。

この設計により、同じ関数内ではローカル変数と引数を固定オフセットでアクセスできる。ネストした呼び出しや再帰呼び出しの際でも、各呼び出しは独立したフレームを持つ。

変数参照と代入のコンパイル方針

  • 変数名を式で使う(例: a)ときは、その変数の値をスタックにプッシュするコードが生成される。具体的には、フレーム内の対応するアドレスを計算して ld を行う。
  • >varname のような代入は、スタックのトップの値を pop して変数のアドレスに st する。引数名への代入は現在の呼び出しフレーム内の引数スロットを書き換え、呼び出し元の値には影響しない。

例: 変数 x を読み出す場合、生成される擬似シーケンスは次のような形になる(アセンブリは実装依存):

  • (push address of x) ; 例: R28 + offset を計算してアドレスをスタックに置く
  • ld ; そのアドレスから値をロードして値をスタックに置く

代入 >x の生成:

  • (push address of x)
  • (push value)
  • st ; アドレスと値を使って書き込みを行う

呼び出し命令と戻り命令

  • 呼び出し: 関数名
    • 作用:
      1. 戻りアドレスを内部的に保存する。
      2. 関数が必要とする引数個数ぶんをデータスタック最上位から消費する。
      3. フレーム領域を確保し、引数とローカル変数のアクセス先を決める。
      4. 関数本体の先頭へ移動する。
  • 戻り(戻り値あり): ret
    • 作用:
      1. 関数は戻り値をデータスタックのトップに 1 個だけ残しておく。
      2. フレームを解放する。
      3. 保存していた戻り先へジャンプする。
      4. 呼び出し前にデータスタック下位にあった値は保持する。
  • 戻り(戻り値なし): retn
    • 作用:
      1. 関数は戻り値を返さない。
      2. フレームを解放してリターン先にジャンプする。
      3. 呼び出し前にデータスタック下位にあった値は保持し、callee が積んだ値は破棄する。

補足:

  • 関数本体が明示的な ret または retn を含まずに終了した場合、コンパイラは関数の終端に暗黙の retn を挿入する(戻り値なしでの復帰を行う)。
  • 再帰呼び出しやネストされた呼び出しに対応するため、フレームサイズは呼び出しごとに独立であること。

IRQ ハンドラー

irq0〜irq15 という名前の引数なし関数は、通常の関数呼び出しではなく IRQ ハンドラーとして扱われる。起動コードは CPU の IRQ ベクター 0xFFFF0100 + 4*n に間接ジャンプ命令を設定し、R30 に共通ディスパッチャーのアドレスを保持する。R30 は IRQ 対応プログラムでは予約される。ディスパッチャーは CAUSE から番号を読み、割り込み前のレジスターを保存したうえで該当関数を通常の sol フレーム規約で呼び出す。ハンドラーは戻り値を返さず、ディスパッチャーが IRET で復帰する。

fn irq5 () :
    # IRQ 5 の処理
    retn
;

sol/cpt32/irqc.sol を !include すると irqc.irqc_irq_number を呼び出せる。これは IRQC の有効な保留ビットを調べ、最も番号の小さい IRQ 番号を返す。該当する IRQ がない場合は -1 を返す。

例:

fn add_two_numbers (a b) :
    local sum
    a b add >sum   # sum = a + b
    sum            # 戻り値として sum をスタックトップに残す
;

# 呼び出し側
1 2 add_two_numbers
print.prn  # 出力: 3

スタックへの値のプッシュ

スタックに値をプッシュするには、数値を直接書く。 例:

1 2 3

オペレーターの使用

オペレーターはスペースで区切られた文として記述される。各オペレーターは指定された数のオペランドをスタックから取り出し、結果をスタックに置く。 例:

1 2 add  # 1と2を加算し、結果の3をスタックに置く
3 2 sub 2 add  # 3から2を減算し、結果の1をスタックに置き、次に1と2を加算し、結果の3をスタックに置く

ラベル(内部実装)

ソースコードにはラベルを書かない。if / else / while / end の構造化構文は、コンパイラが内部的にラベルへ変換する。 内部ラベルは予約済みであり、ユーザーコードから参照したり定義したりすることはできない。

コメント

コメントは # で始まり、行の終わりまで続く。

数値リテラル

10進数の整数リテラルは、符号付き32ビット整数として解釈される。負の数は - 記号で表される。 例:

42 -17 0

符号なし整数リテラルは, u 接尾辞を付けて表される。符号なし整数は、0から2^32-1までの範囲である。 例:

42u 17u 0u

16進数の整数リテラルは、0x 接頭辞を付けて表される。16進数の整数は、0から2^32-1までの範囲である。 例:

0x2A 0x11 0x0

32ビット浮動小数点数リテラルは、小数点または指数表記の末尾に f を付けて表す。値は IEEE 754 binary32 に丸められ、その32ビット表現を整数と同じ値としてスタックに置く。solc は浮動小数点型や演算を扱わず、リテラルの変換のみを行う。 例:

1.0f  # 0x3F800000 をスタックに置く

文字列リテラル

通常の文字列リテラルは、二重引用符で囲まれたテキストをUTF-8でエンコードし、末尾に1バイトのヌル文字を付ける。u16"..." の形式ではUTF-16BEでエンコードし、末尾に2バイトのヌル文字を付ける。どちらも文字列データ先頭のアドレスをスタックに置く。 例:

"Hello, World!"  # UTF-8文字列へのポインタ
u16"こんにちは"  # UTF-16BE文字列へのポインタ

文

文 (statements) は、スペースで区切られたオペレーターとオペランドの組み合わせで構成される。それぞれのオペレーターとオペランドを単語 (words) と呼ぶ。

文の区切り

単語はスペースで区切られる。文の終わりは、改行またはセミコロン ; で示される。タブ、2文字目以降の連続したスペースなどは無視される。 例:

a     b c ;  # 文の終わりはセミコロンで示される

値の型

solでは、すべての値は32ビットの符号付き整数として扱われる。符号なし整数は、u 接尾辞を付けることで表される。文字列は、ヌル文字で終端された文字列へのアドレスとして扱われる。

オペレーター

1. 算術演算

オペレーター オペランド数 スタック操作 説明
add 2 a b -> a+b スタックのトップ2つの値を加算し、結果をスタックに置く
sub 2 a b -> a-b スタックのトップ2つの値を減算し、結果をスタックに置く
mul 2 a b -> a*b スタックのトップ2つの値を乗算し、結果をスタックに置く
div 2 a b -> a/b スタックのトップ2つの値を除算し、結果をスタックに置く
mod 2 a b -> a%b スタックのトップ2つの値を除算し、余りをスタックに置く
neg 1 a -> -a スタックのトップの値を符号反転し、結果をスタックに置く

2. ビット演算

オペレーター オペランド数 スタック操作 説明
and 2 a b -> a&b スタックのトップ2つの値のビットごとのANDを計算し、結果をスタックに置く
or 2 a b -> a b
xor 2 a b -> a^b スタックのトップ2つの値のビットごとのXORを計算し、結果をスタックに置く
not 1 a -> ~a スタックのトップの値のビットごとのNOTを計算し、結果をスタックに置く
shl 2 a b -> a<<b スタックのトップの値を2つ目の値だけ左にシフトし、結果をスタックに置く
shr 2 a b -> a>>b スタックのトップの値を2つ目の値だけ右にシフトし、結果をスタックに置く

3. スタック操作

オペレーター オペランド数 スタック操作 説明
dup 1 a -> a a スタックのトップの値を複製し、スタックに置く
drop 1 a -> スタックのトップの値を破棄する
swap 2 a b -> b a スタックのトップ2つの値を入れ替える
over 2 a b -> a b a スタックの2つ目の値を複製し、スタックに置く
rot 3 a b c -> b c a スタックのトップ3つの値を回転させる (1つ目の値が3つ目に、2つ目の値が1つ目に、3つ目の値が2つ目に移動する)
nip 2 a b -> b スタックの2つ目の値を破棄し、トップの値を残す
tuck 2 a b -> b a b スタックのトップの値を2つ目の値の下に挿入する

4. 比較演算

比較演算の結果は真偽値として表される。真は 0、偽は 1 とする。

オペレーター オペランド数 スタック操作 説明
eq 2 a b -> a==b スタックのトップ2つの値が等しいかどうかを比較し、結果をスタックに置く (0: 等しい, 1: 等しくない)
neq 2 a b -> a!=b スタックのトップ2つの値が等しくないかどうかを比較し、結果をスタックに置く (0: 等しくない, 1: 等しい)
lt 2 a b -> a<b スタックのトップ2つの値を比較し、1つ目の値が2つ目の値より小さいかどうかを比較し、結果をスタックに置く (0: 小さい, 1: 小さくない)
gt 2 a b -> a>b スタックのトップ2つの値を比較し、1つ目の値が2つ目の値より大きいかどうかを比較し、結果をスタックに置く (0: 大きい, 1: 大きくない)
le 2 a b -> a<=b スタックのトップ2つの値を比較し、1つ目の値が2つ目の値以下かどうかを比較し、結果をスタックに置く (0: 以下, 1: 以下でない)
ge 2 a b -> a>=b スタックのトップ2つの値を比較し、1つ目の値が2つ目の値以上かどうかを比較し、結果をスタックに置く (0: 以上, 1: 以上でない)
sgn 1 a -> a>>31&1 スタックのトップの値の符号ビットを取り出し、結果をスタックに置く (1: 負, 0: 正 (0を含む))

5. ロード/ストア

オペレーター オペランド数 スタック操作 説明
ld 1 a -> a スタックのトップの値をアドレスとして、そこから32ビットの値をロードし、スタックに置く
st 2 a b -> スタックのトップの値をアドレスとして、そこへ2番目の32ビットの値をストアする
ldb 1 a -> a スタックのトップの値をアドレスとして、そこから8ビットの値をロードし、スタックに置く
stb 2 a b -> スタックのトップの値をアドレスとして、そこへ2番目の8ビットの値をストアする
ldh 1 a -> a スタックのトップの値をアドレスとして、そこから16ビットの値をロードし、スタックに置く
sth 2 a b -> スタックのトップの値をアドレスとして、そこへ2番目の16ビットの値をストアする

6. 制御フロー

sol の制御フローは構造化構文で記述する。if / else / while / end はソース上で使う公開構文であり、ラベルや raw jump は使わない。

構文 スタック操作 説明
if a -> 直前のスタックトップを消費する。値が 0 なら then ブロックを実行し、1 なら else ブロックへ進む。else がなければ end までスキップする。
else -- if の then ブロックと else ブロックを分ける。if の内部でのみ使用できる。
while -- while と end に囲まれた本体を実行し、end の時点でスタックトップを消費する。値が 0 ならループ先頭へ戻り、1 ならループを抜ける。
end -- if または while の終端を示す。

コンパイラは、これらの構文を実装詳細として内部ラベルと jump 命令に変換する。ユーザーはラベル名を意識しなくてよい。

99. 特殊

TODO: 番号を変更する

オペレーター オペランド数 スタック操作 説明
stacksize 0 -- スタックの最大サイズをスタックに置く
halt 0 -- プログラムの実行を終了する

コンパイラ・ディレクティブ

ディレクティブの先頭には!が付く。ディレクティブは、コンパイラに対して特定の指示を与えるために使用される。

ディレクティブ 説明
!include "filename" [as namespace] 指定されたファイルをインクルードする。as namespaceを付けると、含まれる関数・定数・変数を名前空間に配置する。
!define MACRO_NAME value マクロを定義する
!undef MACRO_NAME マクロを未定義にする
!const CONST_NAME value 定数を定義する
!var VAR_NAME [value] 変数を定義する。初期値は省略可能で、省略時は 0 で初期化される。
!end 現在のディレクティブのブロックを終了する
!required_stack_size size 必要なスタック容量をbyte単位で指定する(1回のみ)。指定サイズが実装上限を超えるか実効容量を満たさない場合、VM実行時またはSRC32生成時にエラーとなる。
!force_stack_size size 実効スタック容量をbyte単位で指定する(1回のみ。既定値は1MiB)。両指令を指定する場合は required <= force とする。実装上限を超える値はVM実行時またはSRC32生成時にエラーとなる。
!asm !endまでの以降の行をアセンブリコードとして扱う
!data !endまでの以降の行をデータセクションとして扱う
!db !endまでの以降の行をバイトデータとして扱う
!keepfn function_name 指定された関数を削除しないようにする(デッドコード削除の対象外とする)
!stack_top address 実行時スタックの初期位置を指定する(1回のみ。既定値は0x000FFFFC)。
!var_base address グローバル変数の割当ベースアドレスを指定する(1回のみ。既定値は0x00100000)。
!rodata_base address 読み取り専用データの割当ベースアドレスを指定する(1回のみ。既定値は0x00020000)。
!namespace name - 名前空間ブロックを開始する。ブロック内の関数・定数・変数は名前空間 name に属する。名前空間ブロックは入れ子にでき、内側の名前空間の完全名は outer.inner のように . で連結される。ブロックは !end で終了する。
!abi_compat "name" [major] プログラムが前提とする ABI をコンパイラに指示する。"bare"(既定)と "Foundation" を定義する。仕様は Foundation §13。

これらの値はコンパイル時の配置に使われる。solcの--stack-top、--var-base、--read-only-data-baseオプションで同じ値を指定した場合は、コマンドラインオプションがソース内のディレクティブより優先される。指定したアドレスは0..0xFFFFFFFFの範囲とし、ディレクティブは対応するスタック・変数・読み取り専用データを使う前に記述する。

名前空間内の関数・定数・変数は同じ名前空間に属する。名前空間内の非修飾名は、まず現在の名前空間を検索し、次に外側の名前空間を内側から順に検索し、最後にグローバルな関数・定数・変数を検索する。名前空間外からは ns.name と修飾して参照し、代入は >ns.name と書く。名前空間を入れ子にした場合の完全名は ns1.ns2.name のように . で連結し、修飾名はその完全名が指す名前空間だけを検索して外側の名前空間へはフォールバックしない。名前空間付き !include も同じ規則で関数・定数・変数を配置し、名前空間ブロックの内側でインクルードした場合はその名前空間の内側に入れ子になる。 名前解決と未定義参照の検証はコンパイル時に行い、名前空間自体は実行時の状態や呼び出し規約を追加しない。グローバル名と各名前空間の名前は独立に定義できるため、同じ名前を別の名前空間で再利用しても衝突しない。同じスコープでの重複定義(関数・定数・変数)はエラーになる。マクロ (!define) はテキスト置換であり名前空間の対象外で、常にグローバルである。名前空間ブロックの入れ子は任意の深さで可能で、!end は直近に開始したブロックを 1 つだけ閉じる。同じ完全名の名前空間は複数のブロックに分けて再宣言でき、その場合は 1 つの名前空間として扱われる。入れ子の例:

!namespace outer -
!const BASE 10
fn outer_value () : 100 ret ;
!namespace inner -
!const BASE 20
fn read_base () : BASE ret ;        # outer.inner.BASE(20)
fn read_outer () : outer_value ret ; # 外側の名前空間を検索して outer.outer_value
!end
fn read_base () : BASE ret ;         # outer.BASE(10)
!end
outer.inner.read_base outer.inner.read_outer outer.read_base

!abi_compat

!abi_compat "Foundation"(または !abi_compat "Foundation" 1)を書いたプログラムは、Foundation OS の U モードプロセスとしてコンパイルされる。言語の意味と関数の呼び出し規約は変わらず、起動コード・終了処理・配置・出力形式・irqN 関数と !asm 内の特権命令の扱いだけが変わる。ディレクティブの置き場所、ファイル間で宣言が食い違った場合の規則、コード生成の差分は Foundation カーネル ABI 仕様 §13 を参照。Status: 実装済み(solc、sol VM、LSP/VS Code 拡張)。Foundation カーネル本体はまだないため、生成物の実行はテスト用の S モード ハーネス上でのみ確認している。

  • 省略可能な major は、ディレクティブと同じ行に書く。次の行の先頭にある数値はトップレベルのコードとして扱う(!abi_compat "bare" の次の行の 1 は push される)。
  • "Foundation" のプログラムは sol VM(solc.py run、REPL)では実行できず、エラーになる。solc.py compile で得たアセンブリを tools/asm/asm.py prog.a -o prog.wax でアセンブルすると、PROTECTED WAX になる(コード RX、rodata R、変数 RW、スタックは WAX の stack 欄)。solc は .PROTECTED と .PERM のアセンブラ指令(CPU 仕様 §7.1)を出力する。
  • 既定の配置は origin 0x00800000(スロット 5)、rodata はコード直後、変数は rodata 直後(いずれも 4-byte 境界)、スタック頂上 0x00C00000、容量 256 KiB。--org/--read-only-data-base/--var-base/--stack-top とソース内の !rodata_base/!var_base/!stack_top/!force_stack_size は既定より優先する。origin を含むスロットに、コード・rodata・変数・スタックが重ならずにこの順で収まらなければコンパイルエラーになる。
  • !var の初期値は従来どおり起動時のコードが書き込む。RW ブロックはその領域をゼロで確保するだけである。
  • システムコールのラッパーは sol/foundation/fnd.sol(fnd_ で始まる名前、名前空間なし)。
  • C libfnd の stream/file/cwd/console/process 操作は sol/foundation/libfnd.sol を include し、tools/solc/build_foundation.py でリンクする。C と同じ引数順、出力引数、負の status を使う。構造体の配置、64-bit 値とミリ秒、対応範囲は ライブラリの契約 を参照。静的 WAX v2 と、--import-wal で fnd を使う WAX v3 を生成できる。実行例 は実サービスの未実装結果も区別する。

!asm

!asmディレクティブは、以降の行をアセンブリコードとして扱うことを指定する。!endディレクティブが現れるまで、すべての行はアセンブリコードとして解釈される。 コンパイラは、!asmディレクティブの後に続く行をそのままアセンブリコードとして出力する。これにより、特定のアセンブリ命令を直接使用することができる。 ブロック内の;と#を含む各行はトークン化せず出力する。ブロックはsolデータスタックを変更せず、R28(スタックポインタ)とR26(フレームベース)を保持しなければならない。静的スタック解析では効果を0として扱う。 例:

!asm
; ここからアセンブリコード
LDIH R1, 0x1234 ; Load VDP Mode Register Address
LDIL R1, 0x1234
LDIH R2, 0x0000
LDIL R2, 0x0001 ; Load value to set VDP to text mode
STB [R1], R2 ; Set VDP to text mode
!end

!data, !db

!dataディレクティブは、以降の行をデータセクションとして扱うことを指定する。!endディレクティブが現れるまで、すべての行はデータとして解釈される。 !dbディレクティブは、以降の行をバイトデータとして扱うことを指定する。!endディレクティブが現れるまで、すべての行はバイトデータとして解釈される。 データを記述するには、数値リテラルをスペース区切りで並べる。数値リテラルは10進数、16進数(0xプレフィックス)、または2進数(0bプレフィックス)で表すことができる。 !dataでは、各数値リテラルは4バイトのデータとして扱われる。それ以下の数値はパディングされ、4バイトに拡張される。上位ビットは符号拡張される。 !dbでは、各数値リテラルは1バイトのデータとして扱われる。上位ビットは無視され、下位8ビットのみが使用される。 例:

!data
# ここからデータセクション
0x12345678 -12345 # 4バイトのデータ
!end

!db
# ここからバイトデータセクション
0x01 0x02 0xFF 0x00 # 1バイトのデータ
!end

コンパイラオプション

  • グローバル変数の割当ベースアドレスを指定するにはコンパイラの --var-base オプションを使用する(デフォルト: 0x00100000)。
  • 実行時スタック(R28)の初期位置を指定するにはコンパイラの --stack-top オプションを使用する(デフォルト: 0x000FFFFC)。
  • 生成コードのロードオリジンを指定するには --org オプションを使用する(デフォルト: 0x00000000)。BIOS ROM 等の非ゼロオリジン向けで、指定時は先頭に .ORG <org> を出力する。ROM 起動では RAM 上の rodata は事前ロードされないため、--read-only-data-base も ROM ウィンドウ内(例: BIOS は --org 0xFF000000 と --read-only-data-base 0xFF008000)に置く。アセンブル時は asm.py --strip で先頭のゼロ埋めを省略し、ファイル先頭がオリジンに対応するイメージを得る(区間間のギャップは引き続き埋められる)。JMP は 16-bit PC 相対のためウィンドウ外へのジャンプには使えず、LDIH/LDIL + JR でレジスタ経由ジャンプする。
  • solc compile は -O size と -O speed を選択できる。既定値は size。
  • size は Short Mode 命令を自動選択し、命令列のバイナリサイズ(アセンブル後のbyte数)を小さくする。既存のShort Mode計画とtrampoline coalescingを使用する。
  • speed はShort Mode遷移を使わない通常命令列を生成し、SRC32 CPUの動的実行サイクル数を減らす。サイズとサイクル数は固定workloadを実行して測定する。
  • --no-short-mode は -O size より優先し、Short Modeを無効化する。-O speed もShort Modeを使用しない。
  • solc compile は既定で、トップレベルから到達できない関数をバイナリへ出力しない。すべての関数を出力するには --keep-unused-functions オプションを使用する。
  • ソース中で !keepfn function_name を指定すると、トップレベルから参照されていない関数もデッドコード削除の対象外になる。指定する関数は関数定義より前に記述する。

solc のレジスタスタックキャッシュ

solc の SRC32 コード生成では、データスタックのトップ付近を R1〜R12 にキャッシュできる。
この最適化は実装上の都合であり、sol 言語のスタック意味論(演算結果やスタック効果)は従来と同一である。

  • キャッシュ対象レジスタ: R1〜R12(12要素)
  • 管理方式: リングバッファ(top を head として管理)
  • キャッシュに空きがある push はメモリアクセスなしでレジスタに積む
  • キャッシュが満杯の push は、最下位(最古)の 1 要素のみをメモリへ spill してから新しい top を積む
  • キャッシュ要素が不足した参照(例: dup/swap/二項演算入力)は、必要なぶんだけメモリから fill して復元する

spill / fill と R28

  • spill: R28 := R28 - 4 の後に ST し、キャッシュ外へ退避する
  • fill: LD [R28 + 0] 後に R28 := R28 + 4 し、キャッシュへ復元する
  • したがって R28 は「キャッシュの直下にあるメモリ上スタック先頭」を指す

制御フロー境界の整合ルール

実装の単純性と安全性を優先し、次を採用する。

  • call / jmp / jz / jnz / halt の直前でキャッシュを flush する
  • 内部ラベル到達時は「空キャッシュ状態」から再開する
  • これにより、制御フロー分岐を跨いだときもメモリ上スタック表現が正準となる

関数呼び出し時の規約

  • R1〜R12 は caller-saved として扱い、call 境界では保存/復元せず flush で整合を取る
  • 関数本体の ret / retn でも flush を行った上で既存のフレーム復帰規約に従う

定数・変数

定数は、!const ディレクティブを使用して定義される。定数は、プログラムの実行中に変更することはできない。 変数は、!var ディレクティブを使用して定義される。変数は、プログラムの実行中に変更することができる。 変数への値の格納は、> 接頭辞を使用して行う。 定数と変数も名前空間に属させることができる。名前空間ブロック内で定義した定数・変数は外部から namespace.NAME で参照し、代入は >namespace.NAME と書く。入れ子の名前空間では完全名 ns1.ns2.NAME を使う([コンパイラ・ディレクティブ]の名前空間の項を参照)。 例:

!const PI 3.14159
!var num 3
PI print.prn
num PI mul >num  # numにPIを掛けた値を格納
num print.prn  # 出力: 9.42477

ローカル変数

ローカル変数は、関数の本体内で定義され、関数の呼び出しが終了すると破棄される。ローカル変数は、関数の引数として渡される値を格納するために使用される。 ローカル変数は、関数の本体内で local キーワードを使用して宣言される。local var_name initial_value の形式で、初期値を指定することもできる。初期値が指定されない場合は、ローカル変数は0で初期化される。 ローカル変数は、関数の本体内でのみ有効であり、関数の外部からアクセスすることはできない。 ローカル変数への値の格納は、> 接頭辞を使用して行う。 例:

fn example_function (a b) :
    local c  # ローカル変数cを定義
    a b add >c  # aとbを加算し、結果をローカル変数cに格納
    c 2 mul  # ローカル変数cの値を2倍する
;
1 2 example_function print.prn  # 出力: 6

変数スコープ

名前解決の優先順位:

  1. ローカル変数
  2. 関数引数
  3. グローバル変数
  4. 定数
  5. 未定義エラー

同一スコープ内で同名変数を定義することは禁止する。 例:

fn example_function (a b) :
    local a  # エラー: 同一スコープ内で同名変数を定義することは禁止されている
    a b add
;

コンパイラ、ツールチェイン

solはsolcというコンパイラを使用して、SRC32のアセンブリにコンパイルされる。solcは、ソースコードを解析し、構文エラーや型エラーを検出する。エラーが検出された場合、コンパイルは中止され、エラーメッセージが表示される。

標準ライブラリ

solには、標準ライブラリ(sol/solstd)とCPT32用ライブラリ(sol/cpt32)が含まれ、基本的な入出力操作やコンソール描画、乱数、デバイス操作などが提供される。各ライブラリは!includeディレクティブで取り込み、ファイル自身が名前空間を宣言しているため、関数・定数・変数は 名前空間.名前 で参照する。

ファイル 名前空間 主な内容
sol/solstd/array.sol array aget / aset / arevcp / acopy
sol/solstd/prng_software.sol prng_software rand_lfsr と LFSR 状態 lfsr
sol/solstd/softfloat.sol softfloat binary32 の sf32_* 演算と整数変換
sol/cpt32/print.sol print putc / prn / prnln(UART出力)
sol/cpt32/pcg.sol pcg PCGテキスト画面の initPCG / locate / putcscr / prnscr / prnlnscr / colorfg / colorbg / col40 / col80 / cls と状態変数 pcg_*
sol/cpt32/colors.sol colors tocolor(hsv2color は未実装のスタブ)
sol/cpt32/irqc.sol irqc irqc_irq_number と IRQC_PENDING / IRQC_ENABLE
sol/cpt32/pec_rng.sol pec_rng initPRNG / rand / randT
sol/cpt32/idc.sol idc IDCドライバ(idc_read など)と IDC_* 定数
sol/cpt32/dsfs.sol dsfs DSFS操作(dsfs_mount など)と DSFS_* 定数
sol/cpt32/vpu.sol vpu VPUのFIFOコマンドヘルパ
sol/foundation/fnd.sol なし(fnd_ 接頭辞) Foundation OS のシステムコール ラッパー fnd_* と FND_* 定数。!abi_compat "Foundation" を宣言しているため、Foundation のプログラムからだけ使える

例:

!include "sol/cpt32/pcg.sol"
!include "sol/cpt32/print.sol"

pcg.initPCG
pcg.col80
"hello" pcg.prnlnscr
0x3f pcg.colorfg
"UART output" print.prnln

1. 出力 (sol/cpt32/print.sol, 名前空間 print)

関数 オペランド数 スタック操作 説明
print.prn 1 a -> スタックのトップの値を文字列へのポインタとして、標準出力に出力する
print.prnln 1 a -> スタックのトップの値をポインタとして、標準出力に出力し、改行する
print.putc 1 a -> スタックのトップの値を文字として標準出力に出力する

PCG画面消去 (sol/cpt32/pcg.sol, 名前空間 pcg)

pcg.cls(color) は40列・80列・80×30の8×16表示に共通で、pcg.pcg_vram_base の文字面4 KiBを0、FG(+0x1000)とBG(+0x2000)の各属性面4 KiBを指定色にします。表示外のring rowも消去し、glyph 0が空でなくても同じFG/BGによって単色になります。消去後、カーソル位置のFG/BGだけは現在の出力色で再設定します。カーソル座標、スクロール、以後の出力色は保持します。ライブラリはゲストへ組み込まれるため、既存のWAXにはソースからの再コンパイルが必要です。

initPCG、locate、文字出力・改行後、colorfg / colorbg と cls は、物理ring rowのカーソル位置に現在のFG/BGを書き込みます。文字コードは変更しません。ハードウェアカーソルはこの2色を交換するため、両色が異なれば空白セルでも表示できます。

tests/pcg_cls.sol は3モードの全plane byteと状態保持を検証するゲストです。次の順でビルドし、config.yaml のないディレクトリーをcwdとして src32_testbench を実行します(パスはリポジトリーからの相対表記)。

python tools/solc/solc.py compile tests/pcg_cls.sol -O speed -o target/pcg-cls.asm
python tools/asm/asm.py target/pcg-cls.asm -o target/pcg-cls.wax
# 空のcwdで、実際の絶対パスを指定する。--cpu pipeline でも実行する。
<repo>/target/release/src32_testbench <repo>/target/pcg-cls.wax 42 --cpu scalar --frames 64 --skip-render-checks

2. 浮動小数点 (sol/solstd/softfloat.sol, 名前空間 softfloat)

IEEE 754 binary32 の値を、浮動小数点リテラル (1.0f など) がスタックに置く32ビット表現のまま整数演算で扱う。softfloat.sf32_add、softfloat.sf32_sub、softfloat.sf32_mul、softfloat.sf32_div、softfloat.sf32_neg、softfloat.sf32_abs、softfloat.sf32_eq、softfloat.sf32_lt と、次の整数変換を提供する。比較は 0 が真、1 が偽である。NaN は順序付けできないため、softfloat.sf32_eq と softfloat.sf32_lt はどちらも偽を返す。

関数 スタック操作 説明
softfloat.sf32_toint x -> n binary32 のビット列を int32 へ0方向への切り捨てで変換する
softfloat.sf32_tofloat n -> x int32 を最近接 binary32 のビット列へ丸める (近接偶数丸め)

変換はすべての入力に対して定義され、トラップも未定義値も生じない。softfloat.sf32_toint は NaN を 0、0x7FFFFFFF を超える値と +∞ を 0x7FFFFFFF、-0x80000000 を下回る値と -∞ を 0x80000000 (-2^31) へ飽和させ、|x| < 1 は 0 にする。丸めは0方向への切り捨てなので、sf32_toint(1.9f) は 1、sf32_toint(-1.9f) は -1 になる。softfloat.sf32_tofloat は 2^24 未満の大きさの整数を厳密に変換し、それ以上は近接偶数丸めで丸める。0 は +0.0 (0x00000000)、0x80000000 (-2^31) は厳密に表現できる。