Skip to content

Commit

Permalink
bit_and_test: revise
Browse files Browse the repository at this point in the history
Signed-off-by: smallkirby <[email protected]>
  • Loading branch information
smallkirby committed Nov 17, 2024
1 parent 780bcb9 commit ca46e36
Showing 1 changed file with 22 additions and 16 deletions.
38 changes: 22 additions & 16 deletions src/kernel/bit_and_test.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ bits.foobar();

最初に実装するのは、ある整数 `N` を受取り、`N`番目のビットのみを立てた整数値を返す `tobit()` 関数です。
使いみちとしては、IRQ の3番の割り込みを無効化したい場合に `0b0000_1000` というマスクを作成したいような場合です。
愚直に実装すると `1 << N` ですが、`1`の型を明示する必要があったり、`N`がシフトを許されている値の範囲内であるかをチェックするなど想定以上にコード行数が増えてしまいます。
また、`N` が enum である場合には都度 `@intFromEnum()` を呼び出す必要があります。
愚直に実装すると `1 << N` ですが、Zig ではそう簡単には行きません。
`1`の型を明示する必要があったり、`N`がシフトを許されている値の範囲内であるかをチェックする必要があるなど、想定以上にコード行数が増えてしまいます。
また、`N``enum` である場合には都度 `@intFromEnum()` を呼び出す必要があります。
`tobit()` 関数はこれらの煩雑な部分を隠蔽することを目的としています。

`tobits()` は任意の型 `T` に対して呼び出せるようにします:
`tobit()` は任意の型 `T` に対して呼び出せるようにします:

```ymir/bits.zig
pub fn tobit(T: type, nth: anytype) T {
Expand Down Expand Up @@ -80,13 +82,14 @@ pub inline fn isset(val: anytype, nth: anytype) bool {
```

ほとんど `tobit()` と同じです。
ただし、右シフトでは `val` の整数型が何であろうとOKであるため、`T` を引数に取る必要がないです
ただし、左シフトとは異なり右シフトでは `val` の整数型が何であろうとOKであるため、`T` を引数に取る必要がありません

### 2つの整数を連結する

`u32`型の整数 `a``b` を受取り、それらを連結して`u64`型の整数をつくりたいということもしばしばあります。
`u32` 型の整数 `a``b` を受け取り、それらを連結して`u64`型の整数をつくりたいということもしばしばあります。
例として、[WRMSR](https://www.felixcloutier.com/x86/wrmsr) 命令は EDX と EAX を連結した値を MSR に書き込みます。
普通に書くと `@as(u64, a) << 32 | @as(u64, b)` となりますが、これをラップする `concat()` 関数を作成します:
普通に書くと `@as(u64, a) << 32 | @as(u64, b)` となりますが、やはり少々煩雑なコードになります。
`concat()` 関数はこのような操作を隠蔽します:

```zig
pub inline fn concat(T: type, a: anytype, b: @TypeOf(a)) T {
Expand All @@ -103,9 +106,9 @@ pub inline fn concat(T: type, a: anytype, b: @TypeOf(a)) T {
```

今までよりも少しだけ複雑ですね。
引数の `a``b` は同じ型であることを強制します
`anytype` の型は自動的に `comptime` になるため、引数の型として他の引数の型を `@TypeOf()` で取得できます
また、最終的に生成する型 `T`の幅(`width_T`) は `a``b` の型の幅の2倍(`width_U`)である必要があります。
`b` の型を `@TypeOf(a)` とすることで、引数の `a``b` が同じ型を持つことを強制します
`anytype` の型は自動的に `comptime` になるため、引数の型として他の引数の型を `@TypeOf()` で指定できます
また、最終的に生成する型 `T`の幅 (`width_T`) は `a``b` の型の幅の2倍 (`width_U`) である必要があります。
もしもそうでない場合には `@compileError()` でコンパイルエラーを発生させます。
この関数は enum には対応していません。

Expand Down Expand Up @@ -175,7 +178,7 @@ test_step.dependOn(&run_ymir_tests.step);

### テストの定義

Zig において、テストは `test {}` ブロック内に記述します。
Zig において、テストは [`test {}`](https://ziglang.org/documentation/master/#Zig-Test) ブロック内に記述します。
例として、`tobit()` に対するテストを書いてみます:

```ymir/bits.zig
Expand Down Expand Up @@ -204,18 +207,18 @@ test success

それはさておき、**テストが実行されていない理由は `bits.zig` 自体が評価されていないから**です。
Zig では、「あらゆるものは参照されるまで評価されない」という原則があります。
今回の場合、ルートファイルである `ymir.zig` から `bits.zig` が参照されていません。
今回の場合、ユニットテストのルートファイルである `ymir.zig` から `bits.zig` が参照されていません。
`@import("bits.zig")` はされていますが、実際にその中身が利用されていないため、Zig はこのファイルを評価しません。
その証拠に、`bits.zig` の末尾などに `hogehoge` という明らかに不正なコードを追加して `zig build test` してもエラーになりません。
**参照されていないため評価されず、評価されない限りはどんなに不正なコードでも問題ない**ということです。

この原則自体は基本的に有用なものです。
実行バイナリには不要なコードが含まれず、また評価自体されないためコンパイル時間も削減できます
実行バイナリには不要なコードが含まれず、評価自体されないためコンパイル時間も削減できます
しかし、ことテストに限ってはこの原則が邪魔をします。
実装した関数自体はまだ利用する箇所がないけれど、テストは実行したいという場合があるからです。

これに対処するため、Zig には `testing.refAllDecls()` という関数が用意されています。
この関数は、指定された型(Zigではファイルも型のようなものです)で定義されるフィールドを全て評価してくれます。
この関数は、指定された型 (Zigではファイルも型のようなものです) で定義されるフィールドを全て評価してくれます。
評価するということは、そこにあるテストも実行してくれるようになるということです。
ルートファイルである Ymir に以下を追加します:

Expand All @@ -239,6 +242,9 @@ test success

今度は `2 passed` という表示になりました。ちゃんとテストが実行されています。

以上でユニットテストの実装および実行ができるようになりました。
`tobit()` 以外の2つの関数に対してもユニットテストを書いてみてください。
今後、本シリーズではユニットテストの実装については省略しますが、実際に開発する際には Zig のテスト機能を活用するのも良いかもしれません。
## まとめ

本チャプターではビット演算ライブラリを実装し、そのライブラリに対してユニットテストを書く方法を扱いました。
実際にテストを書いたのは `tobit()` だけでしたが、それ以外の2つの関数に対してもぜひユニットテストを書いてみてください。
今後本シリーズではユニットテストの実装を省略しますが、実際に開発する際には Zig のテスト機能を活用するのも良いかもしれません。
以降のチャプターでは今回実装したライブラリを随所で利用していきます。

0 comments on commit ca46e36

Please sign in to comment.