-
Notifications
You must be signed in to change notification settings - Fork 5
OpeLa Language Specification
IDT の 21 に割り込みハンドラを登録し,割り込み許可(sti)する例です。
// 構造体の定義
// foo (msb:lsb) は特別な構文で,接頭辞 foo が共通するメンバがグループ化される
type idtEntry packed_struct {
Offset(15:0) address16;
SegmentSelector uint16;
IST uint3;
_ uint5;
Type uint4;
_ uint1;
DPL uint2;
P uint1;
Offset(31:16) address16;
Offset(63:32) address32;
_ uint32;
};
// OpeLa の構造体にはパディング機能がないので明示的にパディングする
type stackFrame packed_struct {
SS uint16;
_ uint48;
SP address;
Flags uint64;
CS uint16;
_ uint48;
IP address;
};
// グローバル変数
var (
idt [256]idtEntry;
)
func notifyEndOfInterrupt() {
// 組み込みのアトミック読み書き関数を使ってレジスタアクセス
var eoi *uint32 = 0xfee000b0 @ address;
AtomicStore(eoi, 0);
// same as: AtomicStore(0xfee000b0@address@*uint32, 0)
}
// interrupt service routine
isr intHandler21(stackFrame *stackFrame) {
Printk("INT21 CS:RIP = {:02x}:{:08x}\n", stackFrame->CS, stackFrame->IP);
notifyEndOfInterrupt();
}
func main(argc int, argv **byte) {
idt[21] = {
.Offset = &intHandler21, .SegmentSelector = 1 * 8,
.Type = 14, .DPL = 0, .P = 1
};
idtr := packed_struct { _ uint16, _ address }{ sizeof(idt) - 1, &idt };
intrin.Lidt(&idtr);
intrin.Sti();
for {
intrin.Hlt();
}
}
- int/uint: int64/uint64 の別名
- intN/uintN: N ビット幅の符号付き/符号無し整数
- byte: uint8 の別名
- address: ネイティブ幅のアドレス型(Intel 64 モードでは 64 ビット)
- addressN: N ビット幅のアドレス型
OpeLa では char
は組み込み型名ではない。
たとえ両辺が整数であっても、異なる型同士の計算はコンパイルエラー。
他の言語にない特徴的な型として address がある。アドレスはポインタから型情報を取り除いたものである。C の void*
に近い。
任意のポインタと address は相互に暗黙的に変換可能。
address から整数へは暗黙的に変換可能。
整数から address へは明示的なキャスト address(整数)
が必要。
var a uint2 = 1; var b int = 3; a = a + b;
では a は uint2、b は int となる。型が異なるためコンパイルエラー。
正しくは var a uint2 = 1; var b int = 3; a = a + b@uint2;
とする。
ちなみに、a はオーバーフローして 0 となる。
- type Name T: T という型に Name と言う名前を付ける。Name は T とは異なる型となる。
- packed_struct {..}: 構造体型(パディングなし)
type Foo packed_struct { ... }; // 構造体型に名前 "Foo" を付ける
var x packed_struct { ... }; // 構造体型の変数 x の定義
ページエントリのアドレスフィールドのように,下位 N ビットをマスクして読み書きすべきフィールドを定義できる。
type PageEntry struct {
P uint1;
RW uint1;
US uint1;
PWT uint1;
PCD uint1;
A uint1;
D uint1;
PAT uint1;
G uint1;
_ uint3;
Addr(63:12) address64;
}
func f() {
var e PageEntry
e.Addr = 0x12345
assert(e.Addr == 0x12000)
}
値 @ 型
と書くと値の型を変換できる。参考:OpeLa の型変換文法の設計意図
整数同士、整数とポインタ間のキャストは、C 言語と同じ挙動とする。
例:3@int2 == -1
, 3@int2@uint64 == 0xffffffffffffffff
型変換演算子 @
は左結合。例えば a@b@c
は (a@b)@c
と解釈される。
型変換演算子の優先順位は単項演算子より強い。したがって、何らかの演算結果の型を変換するなら (p-q)@int
のように括弧で囲む。
文字列は byte の配列として扱う。
文字列は,将来的には Go と同じように読み取り専用のスライスとして実装したい。 現状では文字列を変数に格納するためには,先頭要素のポインタを取得する必要がある。
p := &"abc"[0];
var x1 [3]int; --> int が 3 つ並んだ配列変数(初期値は不定)
x1[i]; --> 配列の i 番目の要素(i は 0 始まりの整数)
p := &x1[i]; --> 配列の i 番目の要素へのポインタ
初期値付き配列
var x2 [3]int = {1, 2}; --> {1, 2, 0} という初期値を持つ 3 要素の配列
以下,仕様考え中…
p := &x1; --> p は x1 の先頭位置と要素数を持つ
配列を参照するポインタのようなもの。スライス自身は長さと容量を持つが、データ本体は持たない。
スライスは、元になる配列から 配列[start : end]
として作成する。start を含み、end を含まない区間を指すスライスとなる。
スライスは長さ(要素数)と容量(要素数の限界値)を持つ。加えて、元の配列に対するオフセット位置を得ることもできる。
-------
slice sl | 2 | 3 | Length = 2
-------
| Capacity |
Offset .
---------------
array a | 1 | 2 | 3 | 0 | Length = 4
---------------
enum Message {
kInterruptXHCI, // 単純な列挙子
kWindowClose(int), // 整数を値として持つ列挙子
kKeyPush(packed_struct{ // 構造体を値として持つ列挙子
keycode uint8; modifier uint8; press bool;
}),
};
メモリ上は次のような C 言語の構造体で表されるような構造となる。
struct Message {
enum {
kInterruptXHCI,
kWindowClose,
kKeyPush,
} kind;
union {
int window_close;
uint8_t* key_push;
} value;
};
初期化や読み書きは次のようになる。
x := Message::kWindowClose(42); // 初期値付き変数定義
assert(x == kWindowClose); // 列挙子との比較では列挙子のみ比較される
assert(x != kWindowClose(1)); // 値付きの列挙子リテラルとの比較では値も比較される
assert(x.kind == kWindowClose(1).kind); // 値を無視する場合は .kind を使う
kWindowClose(win_id) := x; // 値を取り出す(x が kWindowClose でなければ UB)
if kWindowClose(win_id) := x { // x が kWindowClose のときに真
Printk("window id = {}", win_id);
}
match x {
kInterruptXHCI => Printk("xHCI interrupt");
kKeyPush(arg) => Printk("keycode={}", arg.keycode);
_ => return -1; // Error
}
列挙子のスコープは原則、列挙体に閉じる。つまり Message::
というプレフィクスが必要。
ただ、その列挙体型のインスタンスと列挙子の比較(x == kWindowCLose
)では、インスタンスの型から列挙体を特定できるため、列挙子に Message::
を付けなくて良い。
OpeLa 言語は型を抽象化するための機能、ジェネリクスを提供する。
最も簡単な例
func main() int { return Add@<int>(2, 3); }
func Add<T>(a, b T) T { return a + b; }
ジェネリック関数を定義するには、関数名の後に < 型変数 >
を追加し、その他は通常の関数定義と同じ文法を用いる。
引数リスト、戻り値、関数本体の内部で「型変数」を用いることができる。
ジェネリック関数を使う際は 関数名 @ < 型引数 >
というように型引数を明示してもいいし、引数の型から推論することもできる。
ただし、2021 年 6 月 28 日時点の実装では型の推論機構はまだ備わっていない。
少し複雑な例
type Pair<T> struct {
first T;
second T;
};
func Add<T>(p *Pair<T>) T {
return p->first + p->second;
}
func main() int {
var x Pair<int> = {2, 3};
return Add@<int>(&x);
} // -> 5
型であることが期待される場面(例えば var 変数名 型名
の「型名」の部分など)では @
を使わずに型引数を与える。
値であることが期待される場面(例えば、関数呼び出し式の関数名の部分など)では @ < 型引数 >
とする。
上記の例で Add<int>(&x)
と書いてしまうと、Add
と int
の比較式(Add < int
)なのかが分からないため、型演算子 @
を使って、右辺が型であることを明示する。
ジェネリクスの実現方式
ジェネリクスの実現方法は C++ の方式と同様、実装(コード)を型ごとに自動生成する方法とする。
これは Java のジェネリクスとは異なる。 Java では、型 T に関するデータ構造を定義しても、コンパイル時に T が検査されるだけで、コンパイル後は T が Object に置き換わっただけの、ただ 1 つの実装が提供される(はず。uchan は Java の仕様には詳しくないため、細かい説明が違うかも)。
hsjoihs さんが EBNF で定義を書いてくださいました。 将来的には、これに修正を加えて OpeLa の仕様として使いたいですが、今はとりあえずリンクするだけ。
https://gist.github.com/sozysozbot/b973b6f592eb3e990f904f56584a43a7
- パターンマッチを実装するにはコンストラクタと型を結び付けておく必要がある https://twitter.com/takoeight0821/status/1303531973722304513
- Go の文字列は読み取り専用のスライス https://blog.golang.org/strings
- 「コンパイラ側でMMIOとin/outを隠蔽できると移植性が高い言語になりそう」 https://twitter.com/retrage/status/1413773900920418304