osdev-jpでは、OS開発に有用な情報を収集し公開しています

View My GitHub Profile

OpeLa 言語仕様

OpeLa メインページ

プログラム例:割り込み

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();
  }
}

組み込み型

OpeLa では char は組み込み型名ではない。

たとえ両辺が整数であっても、異なる型同士の計算はコンパイルエラー。

アドレス型

他の言語にない特徴的な型として address がある。アドレスはポインタから型情報を取り除いたものである。C の void* に近い。

任意のポインタと address は相互に暗黙的に変換可能。 address から整数へは暗黙的に変換可能。 整数から address へは明示的なキャスト address(整数) が必要。

定数リテラルの型は int

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 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) と書いてしまうと、Addint の比較式(Add < int)なのかが分からないため、型演算子 @ を使って、右辺が型であることを明示する。

参考:OpeLa のジェネリクスに登場する演算子の考察

ジェネリクスの実現方式

ジェネリクスの実現方法は C++ の方式と同様、実装(コード)を型ごとに自動生成する方法とする。

これは Java のジェネリクスとは異なる。 Java では、型 T に関するデータ構造を定義しても、コンパイル時に T が検査されるだけで、コンパイル後は T が Object に置き換わっただけの、ただ 1 つの実装が提供される(はず。uchan は Java の仕様には詳しくないため、細かい説明が違うかも)。

形式定義

hsjoihs さんが EBNF で定義を書いてくださいました。 将来的には、これに修正を加えて OpeLa の仕様として使いたいですが、今はとりあえずリンクするだけ。

https://gist.github.com/sozysozbot/b973b6f592eb3e990f904f56584a43a7

言語仕様のヒントとなる情報群