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

View My GitHub Profile

OpeLa 言語仕様

OpeLa メインページ

プログラム例:割り込み

IDT の 21 に割り込みハンドラを登録し,割り込み許可(sti)する例です。

// 構造体の定義
// foo[n] は特別な構文で,接頭辞 foo が共通するメンバがグループ化される
type idtEntry packed_struct {
  Offset[0]        address16;
  SegmentSelector  uint16;
  IST              uint3;
  _                uint5;
  Type             uint4;
  _                uint1;
  DPL              uint2;
  P                uint1;
  Offset[1]        address16;
  Offset[2]        address32;
  _                uint32;
};

// OpeLa の構造体にはパディング機能がないので明示的にパディングする
type stackFrame packed_struct {
  SS    uint16;
  _     uint48;
  SP    address;
  Flags uint64;
  CS    uint16;
  _     uint48;
  IP    address;
};

// グローバル変数
var (
  idt idtEntry[256];
)

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 address64[63:12];
}

func f() {
  var e PageEntry
  e.Addr = 0x12345
  assert(e.Addr == 0x12000)
}

型変換

当初は 型 ( 値 ) を型変換として扱うつもりだったが、ポインタへの変換と間接演算子の区別が付かないことから、今は 値 @ 型 と書くことになっている。

レジスタ差分定義

文字列

文字列は 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 の先頭位置と要素数を持つ

値付き列挙型

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 言語は今のところテンプレート、あるいは型を抽象化する機能を提供しない(将来的には提供したいと考えているが)。 その代わりに組み込みで型を抽象化したコンテナを提供する。

a := [4]myType{1, 2, 3};   // 配列 {1, 2, 3, 0}
assert(a.Length() == 4);
sl := a[1:3];              // スライス(sl is {2, 3})
assert(sl.Length() == 2 && sl.Capacity() == 3 && sl.Offset() == 1);
var sl2 []myType = a;      // スライス(sl2 is {1, 2, 3, 0})
m := HashMap[[]byte, int]; // ハッシュマップ
q := ArrayQueue[int];      // バックエンドに配列を使ったキュー

スライス

配列を参照するポインタのようなもの。スライス自身は長さと容量を持つが、データ本体は持たない。

スライスは、元になる配列から 配列[start : end] として作成する。start を含み、end を含まない区間を指すスライスとなる。

スライスは長さ(要素数)と容量(要素数の限界値)を持つ。加えて、元の配列に対するオフセット位置を得ることもできる。

              -------
slice sl     | 2 | 3 |     Length = 2
              -------
             | Capacity  |
      Offset .
          ---------------
array a  | 1 | 2 | 3 | 0 | Length = 4
          ---------------

形式定義

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

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

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