ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

SCR1の解析 - ecall命令の処理とriscv-testsの終了条件の確認

昨日の記事の結びで以下のように書いた。

ということはecall命令がうまく処理されていないことになるが、そもそもecall命令って何??な状態なので次回はecall命令の中身を把握するところからやっていきたい。

www.tech-diningyo.info

ということでecall命令の確認を行い、その後にaddi.Sがどのように最終アドレスである0xf4に辿り着くのかを確認していく。

ecall命令

callとつくくらいなので、サブルーチンへのジャンプ的な動作だとは思っているのだけど、改めて確認しておこう。

ecall命令の説明はISAの3.2に説明が記載されている。

f:id:diningyo-kpuku-jougeki:20180924222916p:plain

The ECALL instruction is used to make a request to the supporting execution environment. When executed in U-mode, S-mode, or M-mode, it generates an environment-call-from-U-mode exception, environment-call-from-S-mode exception, or environment-call-from-M-mode exception, respectively, and performs no other operation. ECALL generates a different exception for each originating privilege mode so that environment call exceptions can be selectively delegated. A typical use case for Unix-like operating systems is to delegate to S-mode the environment-call-from-U-mode exception but not the others.

とりあえずざっくり言うと"現在のモードから見た特権モードに遷移して例外を発生させる"になりそう。

他にも探してみたらmsyksphinzさんのFPGA開発日記にもエントリがあった。

msyksphinz.hatenablog.com

自作されているISSの実装に関してのエントリで、修正された部分のコードが載せられている↓。

case PrivMachine    : {
    m_env->DebugPrint ("ECALL from Machine\n");
    m_env->CSRWrite (SYSREG_ADDR_MCAUSE, Except_EcallFromMMode);

    m_env->SetPrivMode (PrivMachine);
    jmp_addr = 0x1c0;
    break;
}

上記はMachineモードの処理だが

  • MCAUSEレジスタへ要因のライト
  • 動作モードをPrivMachineへ
  • jmp_addr=0x1c0にセット

となっている。

ここで出てくるjmp_addr=0x1c0は初期状態におけるMachineモードのハンドラのベクタアドレスなので例外発生させた後ハンドラにジャンプという動作で良さそう。

addi.Sのテストの終わり方

前項のecall命令の動作を踏まえてaddi.Sの処理がどのように終了まで実行されるのかを確認していく。

addi.Sにおけるecall発行後の動作

前項で書いたとおり、ecallを発行すると例外テーブルに飛ぶはず、ということがわかった。従って前回記載したaddi命令のPASS/FAIL判定(以下のコード)のあとは、例外テーブルにジャンプすることになる。

#define RVTEST_PASS                                                     \
        fence;                                                          \
        mv a1, TESTNUM;                                                 \
        li  a0, 0x0;                                                    \
        ecall /* 0x1c0にジャンプ */

とうことでriscv-testsをSCR1の環境で実行した場合の例外ハンドラのコードをチェックしていこう。

trap_vector:                                                            \
        /* test whether the test came from pass/fail */                 \
        csrr t5, mcause;                                                \
        li t6, CAUSE_USER_ECALL;                                        \
        beq t5, t6, _report;                                       \
        li t6, CAUSE_SUPERVISOR_ECALL;                                  \
        beq t5, t6, _report;                                       \
        li t6, CAUSE_MACHINE_ECALL;                                     \
        beq t5, t6, _report;                                       \
        /* if an mtvec_handler is defined, jump to it */                \
        la t5, mtvec_handler;                                           \
        beqz t5, 1f;                                                    \
        jr t5;                                                          \
        /* was it an interrupt or an exception? */                      \
1:      csrr t5, mcause;                                                \
        bgez t5, handle_exception;                                      \
        INTERRUPT_HANDLER;                                              \
handle_exception:                                                       \
        /* we don't know how to handle whatever the exception was */    \
other_exception:                                                        \
        /* some unhandlable exception occurred */                       \
        li   a0, 0x1;                                                   \
_report:                                                                \
        j sc_exit;                                                      \
        .align  6;                                                      \
        .globl _start;   

上記を見るとここでの処理はmcauseレジスタをリードして、その中身に応じて各種処理をしていく、、という感じになる。

先ほどecall命令の動作を確認した際に、msyksphinzさんのエントリから引用させてもらった内容にもあったように、mcauseレジスタにはなぜ例外が発生したかの要因が入っていて、SCR1の場合はMachineモードのみサポートのため、テストの最後に実行されたecall命令が正常に動作すれば以下の部分が実行されることになる。

        li t6, CAUSE_MACHINE_ECALL;                                     \
        beq t5, t6, _report;  
        
_report:                                                                \
        j sc_exit;                                                      \

dumpファイルを使ってアドレスも確認

これをELFファイルから生成したdumpファイル上でもう一度見てみよう。以下がdumpファイルから必要な部分を抜粋したコードになる。

000000f4 <SIM_EXIT>:
  f4:    00000013           nop

~略~

000001c0 <trap_vector>:
 1c0:   34202f73            csrr t5,mcause
 1c4:   4fa1                    li   t6,8
 1c6:   03ff0563            beq  t5,t6,1f0 <_report>
 1ca:   4fa5                    li   t6,9
 1cc:   03ff0263            beq  t5,t6,1f0 <_report>
 1d0:   4fad                    li   t6,11             # CAUSE_MACHINE_ECALL
 1d2:   01ff0f63            beq  t5,t6,1f0 <_report>   # <_report>へジャンプ
 1d6:   00000f17            auipc    t5,0x0
 1da:   e2af0f13             addi t5,t5,-470 # 0 <CL_SIZE-0x20>
 1de:   000f0363            beqz t5,1e4 <trap_vector+0x24>
 1e2:   8f02                    jr   t5
 1e4:   34202f73            csrr t5,mcause
 1e8:   000f5363            bgez t5,1ee <handle_exception>
 1ec:   a009                 j    1ee <handle_exception>

000001ee <handle_exception>:
 1ee:   4505                   li   a0,1

000001f0 <_report>:
 1f0:   4700006f            j    660 <sc_exit> # <sc_exit> へジャンプ
 1f4:   00000013           nop

~略~

00000660 <sc_exit>:
 660:  a95ff06f             j    f4 <SIM_EXIT> # <SIM_EXIT>へジャンプ
 664:  00000013           nop

trap_vector直後からの処理を追っていくと以下のようになるはずだ。

  • 0x1c0 : mcauseリード
  • 0x1c4 - 0x1cc : 別のモードの要因チェック
  • 0x1d0 : Machineモードの要因チェック
  • 0x1d2 : <_report>にジャンプ
  • 0x1ee : <sc_exit>にジャンプ
  • 0x660 : <SIM_EXIT>にジャンプ
  • 0xf4 : 実行終了

以前のエントリで書いたがこの0xf4はSCR1のシミュレーション環境における内側のforeverループの終了条件である。

        forever begin
            @(posedge clk)
            if (i_top.i_core_top.i_pipe_top.curr_pc == SCR1_EXIT_ADDR) begin
                bit test_pass;
                ~~
                break;

ということで以前に、以下のように書いたとおりの動きをするのが正しいことになる。

本来なら、この0x4acecall命令を経て、命令読み出しアドレスがSCR1_EXIT_ADDR=0xF8に到達しなければならないのだが、今のVivadoシミュレーション上はそうなっていないためシミュレーションが正常に終了しないことがわかった。

このコメントを書いた際にはecall命令の動作を特にきちんと把握もせず、0xf4のアドレスから逆に辿っていただけであった。そのため、SCR1の内部で追うべき点が絞りきれていない状況であったが、今回の確認を経てSCR1のecall命令発行時の動作がVivadoシミュレータ上でどのように処理されているかに焦点をおいて解析をかけていけば良さそう。

続きは次のエントリにて。そろそろ動くといいなぁ。。

SCR1の解析 - riscv-tests処理内容の確認

昨日の記事でVivadoシミュレータ上で実行したriscv-testsのaddiテストが正常に終了しない件についての調査を行い、テストベンチ上の$finishに辿り着く条件を満たしていないことがわかった。

www.tech-diningyo.info

結構SCR1の中身を真面目に追っていかないとわからない感じがしたが、そもそもこのriscv-testsで何が実行されているのかをちゃんとは把握していないのでriscv-testsの中身の確認をしていく。ということもありタイトルもそれっぽく変更。

addi.Sの中身の確認

現在実行中のaddi命令の試験についての大まかな流れを確認していく。

現在Vivadoシミュレータ上にインスタンスされているSCR1は32bitCPUなのでrv32uiのディレクトリの下のコードがビルドされているが、中身はrv64uiへのインクルードパスが記載されており実際にコードとしては使用されるのはrv64uiのaddiテストになる。

# See LICENSE for license details.

#*****************************************************************************
# addi.S
#-----------------------------------------------------------------------------
#
# Test addi instruction.
#

#include "riscv_test.h"
#include "test_macros.h"

RVTEST_RV64U
RVTEST_CODE_BEGIN

  #-------------------------------------------------------------
  # Arithmetic tests
  #-------------------------------------------------------------

  TEST_IMM_OP( 2,  addi, 0x00000000, 0x00000000, 0x000 );
  TEST_IMM_OP( 3,  addi, 0x00000002, 0x00000001, 0x001 );
  TEST_IMM_OP( 4,  addi, 0x0000000a, 0x00000003, 0x007 );

  TEST_IMM_OP( 5,  addi, 0xfffffffffffff800, 0x0000000000000000, 0x800 );
  TEST_IMM_OP( 6,  addi, 0xffffffff80000000, 0xffffffff80000000, 0x000 );
  TEST_IMM_OP( 7,  addi, 0xffffffff7ffff800, 0xffffffff80000000, 0x800 );

  TEST_IMM_OP( 8,  addi, 0x00000000000007ff, 0x00000000, 0x7ff );
  TEST_IMM_OP( 9,  addi, 0x000000007fffffff, 0x7fffffff, 0x000 );
  TEST_IMM_OP( 10, addi, 0x00000000800007fe, 0x7fffffff, 0x7ff );

  TEST_IMM_OP( 11, addi, 0xffffffff800007ff, 0xffffffff80000000, 0x7ff );
  TEST_IMM_OP( 12, addi, 0x000000007ffff7ff, 0x000000007fffffff, 0x800 );

  TEST_IMM_OP( 13, addi, 0xffffffffffffffff, 0x0000000000000000, 0xfff );
  TEST_IMM_OP( 14, addi, 0x0000000000000000, 0xffffffffffffffff, 0x001 );
  TEST_IMM_OP( 15, addi, 0xfffffffffffffffe, 0xffffffffffffffff, 0xfff );

  TEST_IMM_OP( 16, addi, 0x0000000080000000, 0x7fffffff, 0x001 );

  #-------------------------------------------------------------
  # Source/Destination tests
  #-------------------------------------------------------------

  TEST_IMM_SRC1_EQ_DEST( 17, addi, 24, 13, 11 );

  #-------------------------------------------------------------
  # Bypassing tests
  #-------------------------------------------------------------

  TEST_IMM_DEST_BYPASS( 18, 0, addi, 24, 13, 11 );
  TEST_IMM_DEST_BYPASS( 19, 1, addi, 23, 13, 10 );
  TEST_IMM_DEST_BYPASS( 20, 2, addi, 22, 13,  9 );

  TEST_IMM_SRC1_BYPASS( 21, 0, addi, 24, 13, 11 );
  TEST_IMM_SRC1_BYPASS( 22, 1, addi, 23, 13, 10 );
  TEST_IMM_SRC1_BYPASS( 23, 2, addi, 22, 13,  9 );

  TEST_IMM_ZEROSRC1( 24, addi, 32, 32 );
  TEST_IMM_ZERODEST( 25, addi, 33, 50 );

  TEST_PASSFAIL

RVTEST_CODE_END

  .data
RVTEST_DATA_BEGIN

  TEST_DATA

RVTEST_DATA_END

上記のようにRVTEST_CODE_BEGINで各種初期化を実行した後、addi命令の基本的な処理をTEST_IMM_OPTEST_IMM_DEST_BYPASSといったマクロに渡して命令のテストを行っていく。

RVTEST_CODE_BEGINで各種初期化

まずはRVTEST_CODE_BEGINから。オリジナルのriscv-testsのgithubではこのマクロはsubmoduleとして登録されているriscv-envに登録されているriscv_test.hに実装されているが、SCR1の環境ではこのファイルに実装されている各種テスト用マクロと、riscv-tests/macros/scalar/test_macros.hに登録されている各テスト実行用のマクロをまとめたファイルがtests/common/riscv_macros.hに用意されている。

#define RVTEST_CODE_BEGIN                                               \
        .section .text.init;                                            \
        .org 0xC0, 0x00;                                                \
        .align  6;                                                      \
        .weak stvec_handler;                                            \
        .weak mtvec_handler;                                            \
trap_vector:                                                            \
        /* test whether the test came from pass/fail */                 \
        csrr t5, mcause;                                                \
        li t6, CAUSE_USER_ECALL;                                        \
        beq t5, t6, _report;                                       \
        li t6, CAUSE_SUPERVISOR_ECALL;                                  \
        beq t5, t6, _report;                                       \
        li t6, CAUSE_MACHINE_ECALL;                                     \
        beq t5, t6, _report;                                       \
        /* if an mtvec_handler is defined, jump to it */                \
        la t5, mtvec_handler;                                           \
        beqz t5, 1f;                                                    \
        jr t5;                                                          \
        /* was it an interrupt or an exception? */                      \
1:      csrr t5, mcause;                                                \
        bgez t5, handle_exception;                                      \
        INTERRUPT_HANDLER;                                              \
handle_exception:                                                       \
        /* we don't know how to handle whatever the exception was */    \
other_exception:                                                        \
        /* some unhandlable exception occurred */                       \
        li   a0, 0x1;                                                   \
_report:                                                                \
        j sc_exit;                                                      \
        .align  6;                                                      \
        .globl _start;                                                  \
_start:                                                                 \
        RISCV_MULTICORE_DISABLE;                                        \
        /*INIT_SPTBR;*/                                                 \
        /*INIT_PMP;*/                                                   \
        DELEGATE_NO_TRAPS;                                              \
        li TESTNUM, 0;                                                  \
        la t0, trap_vector;                                             \
        csrw mtvec, t0;                                                 \
        CHECK_XLEN;                                                     \
        /* if an stvec_handler is defined, delegate exceptions to it */ \
        la t0, stvec_handler;                                           \
        beqz t0, 1f;                                                    \
        csrw stvec, t0;                                                 \
        li t0, (1 << CAUSE_LOAD_PAGE_FAULT) |                           \
               (1 << CAUSE_STORE_PAGE_FAULT) |                          \
               (1 << CAUSE_FETCH_PAGE_FAULT) |                          \
               (1 << CAUSE_MISALIGNED_FETCH) |                          \
               (1 << CAUSE_USER_ECALL) |                                \
               (1 << CAUSE_BREAKPOINT);                                 \
        csrw medeleg, t0;                                               \
        csrr t1, medeleg;                                               \
        bne t0, t1, other_exception;                                    \
1:      csrwi mstatus, 0;                                               \
        init;                                                           \
        EXTRA_INIT;                                                     \
        EXTRA_INIT_TIMER;                                               \
        la t0, _run_test;                                               \
        csrw mepc, t0;                                                  \
        csrr a0, mhartid;                                               \
        mret;                                                           \
        .section .text;                                                 \
_run_test:

やってることをざっくり書くと以下の通り。

  • trap_vectorの実装
    • .org 0xc0, 0x00に従いtrap_vector`は0x1c0を先頭に配置される。(0x100はリンカスクリプトの指定)
    • 各モードごとにエントリが定義されており、ecall実行時のモードに対応したエントリが実行される
    • _reportではsc_exitへジャンプして処理終了。
    • sc_exit自体はCRTに定義されているが、アドレス自体はが前回の記事で書いたシミュレーション終了条件となるアドレスに一致する。
  • _startの宣言と実装

因みに上記のアセンブラコード中で使用されているa0やらt0やらはRISC-Vの持っているレジスタ(x0-x31とか)を指しており、コンパイラでどのように使用するかによって名前が割り振られている。最初、何も知らずにアセンブラみて混乱したので一応。

対応は以下の通りで、ここではSCR1に関係しているx0-x31のみ記載(この表自体はuser-level ISAのchapter 20に記載されている)。

Register ABI Name(※) Description Saver
x0 zero Hard-wired zero -
x1 ra Return address Caller
x2 sp Stack pointer Callee
x3 gp Global pointer -
x4 tp Thread pointer -
x5 t0 Temporary/alternate link register Caller
x6-7 t1-2 Temporaries Caller
x8 s0/fp Saved register/frame pointer Callee
x9 s1 Saved register Callee
x10-11 a0-1 Function arguments/return values Caller
x12-17 a2-7 Function arguments Caller
x18-27 s2-11 Saved registers Callee
x28-31 t3-6 Temporaries Caller

※ABI(Application Binary Interface)の略らしい。

因みに以下にこの辺のアセンブラのことがまとめてられているのでこちらも参照するとわかりやすいはず。

github.com

各命令のテスト

tests/common/riscv_macros.hに定義されているテスト用マクロを使って命令のテストを実行する。

一例としてTEST_IMM_OPの中身は以下のようになっている。(ファイルはsim/tests/common/riscv_macros.h

#define TEST_IMM_OP( testnum, inst, result, val1, imm ) \
    TEST_CASE( testnum, x30, result, \
      li  x1, MASK_XLEN(val1); \
      inst x30, x1, SEXT_IMM(imm); \
)

マクロの中ではTEST_CASEマクロを呼んでおり、この中でテストが実行される↓

#define TEST_CASE( testnum, testreg, correctval, code... ) \
test_ ## testnum: \
    code; \
    li  x29, MASK_XLEN(correctval); \
    li  TESTNUM, testnum; \
bne testreg, x29, fail;

実際の処理としてはTEST_IMM_OPinst引数に設定した命令(今回はaddi)を実行した結果を格納したx30と期待値が格納されているx29を比較して値が不一致の場合にfailラベルにジャンプする。

テストのPASS/FAIL判定

このfailラベルは各テストの最後に実行されるTEST_PASSFAILマクロに定義されている。

#define TEST_PASSFAIL \
        bne x0, TESTNUM, pass; \
fail: \
        RVTEST_FAIL; \
pass: \
        RVTEST_PASS \

fail/passラベルで実行されるのはriscv_test.hにて定義されるRVTEST_FAIL/RVTEST_PASSマクロでa1レジスタに最後に実行された試験番号(TEST_NUM)、a0レジスタにテスト結果(0x0:PASS/0x1:FAIL)を格納して対象命令の試験が終了となる。

#define RVTEST_PASS                                                     \
        fence;                                                          \
        mv a1, TESTNUM;                                                 \
        li  a0, 0x0;                                                    \
        ecall

#define TESTNUM x28
#define RVTEST_FAIL                                                     \
        fence;                                                          \
        mv a1, TESTNUM;                                                 \
        li  a0, 0x1;                                                    \
        ecall

ということで流れをまとめると

  1. RVTEST_CODE_BEGINでテストの準備
  2. TEST_IMM_OP等のテスト用マクロを使ってテストを実行
    1. 各テスト結果がPASSならそのまま次のテストへ
    2. FAILの場合には、failラベルにジャンプして試験終了
  3. テストの結果自体は最終状態におけるa0レジスタの値をチェックすればわかるようになっている。
  4. PASS or FAILにかかわらず最終的にはecallが発行される

これで大体内容は把握できたかな。やっぱり流れ的には最後のRVTEST_PASS/RVTEST_FAILからtrap_handlerへジャンプした後にsc_exitを経て最終アドレスな気がする。

ということはecall命令がうまく処理されていないことになるが、そもそもecall命令って何??な状態なので次回はecall命令の中身を把握するところからやっていきたい。

RISC-Vの実装の1つ - SCR1の解析 - シミュレーション環境の疑問点の確認(2)

前回のSCR1ネタではVivadoシミュレータを使用した際に$readmemhがうまく行かない問題を解析していった。

今回はもうひとつ残っているシミュレーションが正常に終わらない問題について解析していく。

これね↓。

実はこのシミュレーションも正常に終わってなかったりしているので、引き続きもう少しシミュレーション環境の調査を行っていくつもり

tech-diningyo.hatenablog.com

シミュレーションが終わらない問題

現状の状態でシミュレーションを実行すると、Vivadoのシミュレータで設定できる最大シミュレーション時間に到達したためシミュレーションが終了している状態にある。

現在のシミュレーションに関する設定は以下の画像の通りで、シミュレーションの最大実行時間は3usになっている。

f:id:diningyo-kpuku-jougeki:20180922231015p:plain

シミュレーションの波形は以下のような感じ。

f:id:diningyo-kpuku-jougeki:20180922231034p:plain

黄色のフラグが立っている2295ns以降にAXIバスの動きが一切止まっている状態なのに、シミュレーション自体はタイムアウトの3usまで実行されている状態にある。

シミュレーションの終了条件

この「3usで終了する」という動作が意図した動作の可能性もあるので、先にこのSCR1のシミュレーション環境の終了条件について確認しておく。

scr1_top_tb_axi.svに以下のように明示的な$finishが存在しており、ここに到達すればシミュレーションが終了するはずである。

    // scr1_top_tb_axi.sv - L.180 - L.213
    forever begin
        if ($feof(f_info)) break;
        ret_val = $fscanf(f_info, "%s\n", i_memory_tb.stuff_file);
        i_top.i_core_top.i_pipe_top.i_tracelog.test_name = i_memory_tb.stuff_file;
        $write("\033[0;34m---Test: %s\033[0m\n", i_memory_tb.stuff_file);
        reset();
        forever begin
            @(posedge clk)
            if (i_top.i_core_top.i_pipe_top.curr_pc == SCR1_EXIT_ADDR) begin
                bit test_pass;

                test_pass =
                    (i_top.i_core_top.i_pipe_top.i_pipe_mprf.mprf_int[10] == 0);

                tests_total  += 1;
                tests_passed += test_pass;

                $fwrite( f_results,
                         "%s\t\t%s\n",
                         i_memory_tb.stuff_file,
                         (test_pass ? "PASS" : "__FAIL") );

                if (test_pass) $write("\033[0;32mTest passed\033[0m\n");
                else $write("\033[0;31mTest failed\033[0m\n");
                break;
            end
        end
    end
    $display("\n#--------------------------------------");
    $display("# Summary: %0d/%0d tests passed", tests_passed, tests_total);
    $display("#--------------------------------------\n");
    $fclose(f_info);
    $fclose(f_results);
    $finish();

この$finishに到達するためには以下の2つのforeverループを抜けなければならないことがわかる

  1. 外側のforever ループ : f_infoファイルのEOFに到達すること
  2. 内側のforever ループ : SCR1内部のcurr_pcSCR1_EXIT_ADDRに到達すること

現在のシミュレーションの状態

ここで先程のシミュレーション波形をもう一度確認してみる。AXIバスの動きが無くなった部分を拡大したすると以下のようになっていた。

f:id:diningyo-kpuku-jougeki:20180922231050p:plain

波形中の各信号io_axi_imem_***はSCR1から出力されるRISC-Vの命令リード用のAXI I/Fとなっており、この中のio_axi_imem_araddrがSCR1から出力される次の命令のアドレスとなっている。

この信号が0x0になる前の最後の値は

  • io_axi_imem_araddr = 0x4ac

となっている。

先に確認した内側のforeverループの脱出条件となるがSCR1_EXIT_ADDRは同じファイルに定義されており、以下の通りである。

// scr1_top_tb_axi.sv - L.13 - 16
//------------------------------------------------------------------------------
// Local parameters
//------------------------------------------------------------------------------
localparam logic [`SCR1_XLEN-1:0]   SCR1_EXIT_ADDR      = 32'h000000F8;

このことから、2つあるforeverループのうち内側のループの終了条件に到達していないことが原因で$finishに辿り着かないため、シミュレーションが終了しないことがわかった。

ここからは実際のテストコードを見ていこう。

現在実行しているのはriscv-testsの中から適当に選択したaddi命令のテストになっている。波形デバッグの際には実際のソースではなく、テストのビルド時に生成される*.dumpを見るほうがいいのでそちらを確認していく。なお、*.dumpファイルはSCR1の環境においてはビルド時に"build"ディレクトリ以下にELFファイルと一緒に生成されるようにMakefileに定義されている。

# Makefile - L.36 - 37
$(bld_dir)/%.dump: $(bld_dir)/%.elf
  $(RISCV_OBJDUMP) $^ > $@

ではaddi.dumpを確認していく。先ほど波形で確認したアドレスをaddi.bumpから探すと以下の部分になる。

00000494 <fail>:
 494:  0ff0000f            fence
 498:  85f2                    mv  a1,t3
 49a:   4505                   li  a0,1
 49c:   00000073           ecall

000004a0 <pass>:
 4a0:   0ff0000f            fence
 4a4:   85f2                    mv  a1,t3
 4a6:   4501                   li  a0,0
 4a8:   00000073           ecall

000004ac <ecall>:
 4ac:   00000073           ecall

上記のようにaddi試験における試験のPASS/FAIL判定部分のコードになる。更にシミュレーション上で0x4ac以前の命令読み出しアドレスを確認すると0x4a0 - 0x4a8を通過しているので、このテスト自体の判定はPASS判定になっていることもわかった。

本来なら、この0x4acecall命令を経て、命令読み出しアドレスがSCR1_EXIT_ADDR=0xF8に到達しなければならないのだが、今のVivadoシミュレーション上はそうなっていないためシミュレーションが正常に終了しないことがわかった。

うーーーむ、とりあえず終了しない理由はわかったけど、これ以上はSCR1の中の解析が必要そう。

引き続き調査が必要、、、ということがわかったので今日はここまで。

RISC-Vの実装の1つ - SCR1の解析 - シミュレーション環境の疑問点の確認(1)

前回のSCR1ネタ最後に以下のように書いた。

ここまでに記載したようにとりあえず...の対策をいくつか施して動かした部分もあるし、実はこのシミュレーションも正常に終わってなかったりしているので、引き続きもう少しシミュレーション環境の調査を行っていくつもりだが、今日はここまで。

tech-diningyo.hatenablog.com

ということで今日は、Vivado+SCR1+riscv-testsシミュレーション環境についてももう少し調べていく。今日は$readmemhがうまく動かないという話題について。

$readmemhうまく行かない問題

表題の通りだが、前回の記事では読み込むRISC-VバイナリのHEXデータファイルを以下のようにして、直書き&フルパス指定することによって、実行していた。

always @(negedge rst_n) begin
    //memory = '{SIZE{'0}};
    $readmemh("./scr1/build/addi.hex", memory); // 修正後
    //if(stuff_file.len()>0) $readmemh(stuff_file,memory);
end

これについて少し調べてみる。

いろいろ試してみたが、どうもSystem Verilogstringに入れると解釈できていないように見える。

試したのは以下の3つのケース

  • SCR1オリジナル
  • (すでにやってるけど)$readmemhに直接ファイル名を指定
  • reg宣言した変数にファイルパスを設定して、その変数を$readmemhに与える

ソースコード的には以下のような物を用意して、コメントアウトする部分を切り替えて試してみた。

reg[1023:0] tmp;

always @(negedge rst_n) begin
    //memory = '{SIZE{'0}};

    // case 1 - original, but set file name on this always block directly
    stuff_file = "/home/dnn-admin/workspace/hw/study/1000_scr1/scr1/build/addi.hex";
    $display("case1:%s", stuff_file);
    if(stuff_file.len()>0) $readmemh(stuff_file, memory);
    
    // case 2 - set file name directly to $readmemh task
    $display("case2:%s", "/home/dnn-admin/workspace/hw/study/1000_scr1/scr1/build/addi.hex");
    $readmemh("/home/dnn-admin/workspace/hw/study/1000_scr1/scr1/build/addi.hex", memory);
    
    // case 3 - set file name to reg valuable and feed $readmemh task
    tmp = "/home/dnn-admin/workspace/hw/study/1000_scr1/scr1/build/addi.hex";
    $display("case3:%s", tmp);
    $readmemh(tmp, memory);
end

case1 : SCR1のオリジナルソースコード

以下のようなメッセージが得られた。

case1:/home/dnn-admin/workspace/hw/study/1000_scr1/scr1/build/addi.hex
WARNING: File õ referenced on /home/dnn-admin/workspace/hw/study/fpga-scr1/scr1/src/tb/scr1_memory_tb_axi.sv at line 154 cannot be opened for reading. Please ensure that this file is available in the current working directory.

ここで気になるのは警告文中のファイル名が謎の文字列"õh"に化けているように見えること。。。

なんかやっぱり、string型の変数にファイル名設定するとVivadoでは読み込めてないように見える。$displayの出力は普通なのに。。

上記のようになっているので当然、読み出されるデータも不定になっていた。

case2 : 直接ファイルを指定

こちらも既に試しているとおりではあるが、以下のメッセージが得られた。実行時間の情報を出しているのはcase1のように警告が出ていないことを示すため。

case2:/home/dnn-admin/workspace/hw/study/1000_scr1/scr1/build/addi.hex
relaunch_sim: Time (s): cpu = 00:00:05 ; elapsed = 00:00:07 . Memory (MB): peak = 6431.449 ; gain = 0.000 ; free physical = 10548 ; free virtual = 28752

警告が出ていないことからもわかるが、正常にメモリがデータが読み込まれているため、シミュレーション時の波形においても正しいでデータが読み込まれている。

case3 : reg変数に設定した後、$readmemh

得られた出力は以下のようになった。

case3:                                                                /home/dnn-admin/workspace/hw/study/1000_scr1/scr1/build/addi.hex
relaunch_sim: Time (s): cpu = 00:00:05 ; elapsed = 00:00:06 . Memory (MB): peak = 6447.441 ; gain = 7.988 ; free physical = 10477 ; free virtual = 28689

"case2"という文字の直後にスペースが入っているのは、reg変数のビット幅の関係。

こちららも特に警告は出ておらず、正常に$readmemhにより所望のファイルが読み込まれていた。

結果まとめ

以上の結果からすると、やっぱりVivadoシミュレータにおいてはstring変数に設定したファイルパスが$readmemhの処理の上では認識されていないことになる。

ということで、この挙動についてverilogの仕様を追ってみることにする。

verilogの仕様の確認

現在実行しているシミュレーションでこの$readmemhが実行されているのはテストベンチ上のメモリモデルscr1_memory_tb_axiである。このファイルはVivado上ではSystem Verilogのファイルとして認識されているので、エラボレーション時にもSystem Verilogの文法が適用されている。

そのため、まずはSystem Verilogにおける$readmemhの定義を確認していく。確認したのはこのPDFでこれによると、$readmemhの項目では以下の事項のみが触れられている。

22.14 $readmemb and $readmemh 22.14.1 Reading packed data $readmemb and $readmemh are extended to unpacked arrays of packed data, associative arrays of packed data, and dynamic arrays of packed data. In such cases, the system tasks treat each packed element as the vector equivalent and perform the normal operation.

When working with associative arrays, indexes must be of integral types. When an associative array’s index is of an enumerated type, address entries in the pattern file are in numeric format and correspond to the numeric values associated with the elements of the enumerated type.

22.14.2 Reading 2-state types $readmemb and $readmemh are extended to packed data of 2-state types, such as int or enumerated types. For 2-state integer types, reading proceeds the same as for conventional Verilog variable types (e.g., inte-ger), with the exception that X or Z data are converted to 0. For enumerated types, the file data represents the numeric values associated with each element of the enumerated type (see 4.10). If a numeric value is out of range for a given type, then an error shall be issued and no further reading shall take place.

この22.14で記載されているのはSystem Verilogにおいて拡張された部分のみで、ざっくり以下のことが書いてある。

  • packed dataのunpacked arrayに読み込めるようなった。
  • 2-state typesの型に読み込めるようになった。

実際に知りたいファイル指定部分の扱いは書いていなかったのでVerilog-HDLの方の仕様書(IEEE Std 1364-2001)で確認してみる。

"17.2.8 Loading memory data from a file"によると、以下のようになっている。

load_memory_tasks ::=
$readmemb ( " file_name " , memory_name [ , start_addr [ , finish_addr ] ] ) ;
| $readmemh ( " file_name " , memory_name [ , start_addr [ , finish_addr ] ] ) ;

今回知りたいのは上記の" file_name "が何を指しているのか、、、なのだが、ここには書いていないので、別の項目も探してみると以下の説明が$fopenの項目である17.2.1で見つかる。

file_open_function ::=
integer multi_channel_descriptor = $fopen ( " file_name " );
| integer fd = $fopen ( " file_name ", type );

ここには" file_name "についての説明が記載してあり、それは以下のようになっている。

filename is a character string, or a reg containing a character string that names the file to be opened.

ということで、verilog(IEEE Std 1364-2001)の仕様では" file_name "として許容されるのは

  • a character string → "string"のように""で括られた文字列
  • reg containing a character string → reg変数に"a character string"を格納したもの

ということになりそう。

上記で先に調べたとおりSystem Verilogにおいてはこの" file_name "についての扱いは拡張されていないように見えるので、$readmemhstring型の文字列が扱えないのは一概にバグとは言えない感じに思える。VCSとかでは普通に扱えるからてっきり読めるんだと思ってた。

商用のシミュレータ使ってると結構ベンダ依存の拡張が存在してて、ツール切り替えるときに動いてた記述が動かなくなることがあるけど、これもその一つということなのか。因みに上記で$displaystringを与えると動いていたが、System Verilogの仕様でしっかり拡張されているようで以下の一文が書いてあった。

  • $display , $write , $fdisplay , $fwrite , $swrite , and their variants
    • The argument corresponding to a string % format specifier ( s ) may have the string data type.

ということで、結論としてはstringで読めないのは仕方がなさそうなのでreg変数で動くように書き換える(前項のcase3の対応)、、、ということになりそう。

Scalaの勉強 - トレイト(1)

今日はトレイトの章を。

トレイト

概要

今までどの言語でも見たことのない用語。"trait == 特定とか特色"という意味らしい。

プログラムの分割(モジュール化)と組み立て(合成)は、オブジェクト指向プログラミングでも関数型プログラミングにおいても重要な設計の概念になります。そして、Scalaオブジェクト指向プログラミングにおけるモジュール化の中心的な概念になるのがトレイトです。

と書いてあるので、Scalaにおける重要な機能なのだろう。

では定義を。

trait <トレイト名> {
  (<フィールド定義> | <メソッド定義>)*
}

クラスの定義からコンストラクタ部分を除いたものという感じ。

以下のようなことが特徴として挙げられるとのこと。

  • 複数のトレイトを1つのクラスやトレイトにミックスインできる
  • 直接インスタンス化できない
  • クラスパラメータ(コンストラクタの引数)を取ることができない

PerlMooseとかにあるRoleみたいなものっぽい。

上記の3点について見ていく。

複数のトレイトを1つのクラスやトレイトにミックスインできる

Scalaのトレイトはクラスとは異なり、複数のトレイトを1つのクラスやトレイトにミックスイン出来る

trait TraitA

trait TraitB

class ClassA

class ClassB

class ClassC extends ClassA with TraitA with TraitB

object TraitMixin {
  def main(args: Array[String]): Unit = {
  }
}
$ scala 20180916_01_trait_mixin.scala
# エラーなく実行される

次にwith ClassBというふうにクラスを指定した場合を見てみる。

class ClassA

class ClassB

class ClassC extends ClassA with ClassB

object TraitMixinNG {
  def main(args: Array[String]): Unit = {
  }
}

上記のようにwithClassBを指定した場合はエラーになる。

$ scala 20180916_02_trait_mixin_ng.scala
20180916_02_trait_mixin_ng.scala:9: error: class ClassB needs to be a trait to be mixed in
class ClassC extends ClassA with ClassB
                                 ^
one error found

複数のクラスを継承する必要のある場合はクラスをトレイトにすることで実現が可能。

直接インスタンス化できない

そのままなので、試してみる。

trait TraitA

object TraitInstanceNG {
  def main(args: Array[String]): Unit = {
    val a = new Trait
  }
}

上記のコードを実行すると以下のようにエラーになる。

$ scala 20180916_03_trait_instance_ng.scala 
20180916_03_trait_instance_ng.scala:5: error: trait TraitA is abstract; cannot be instantiated
    val a = new TraitA
            ^
one error found

クラスパラメータ(コンストラクタの引数)を取ることができない

これもそのままでクラスでは出来たコンストラクタに引数を持たせるということが出来ない。

trait TraitA(name: String)

object TraitNotHasConstructorMemeberNG {
  def main(args: Array[String]): Unit = {
  }
}

上記のコードを実行すると以下のようにエラーになる。

$ scala 20180916_04_trait_not_has_member_ng.scala
/home/dnn-admin/workspace/hw/study/2000_chisel/200_examples/20180916_04_trait_not_has_member_ng.scala:1: error: traits or objects may not have parameters
trait TraitA(name: String)
            ^
one error found

定義で書いたようにトレイト自体はフィールドを持つことが可能だが、コンストラクタ引数が存在しないのでそれを使った値の設定が出来ないことになるが、これも特に問題にはならないらしく以下のように抽象メンバーを持たせることで値を設定可能にすることが出来る。

trait TraitA {
  val name: String
  def printName(): Unit = println(name)
}

class ClassA(val name: String) extends TraitA

object TraitAbstractMember {
  def main(args: Array[String]): Unit = {
    val a = new ClassA("trait")

    a.printName()

    // nameを上書きするような実装を与えることも可能
    val a2 = new TraitA { val name = "override" }

    a2.printName()
  }
}

上記のように定義したTraitAを継承したクラスを作成しコンストラクタの引数でメンバを上書きするとTraintAnameに値を設定することが出来る。

また、val a2のようにnameを上書きするようにして使用することも可能。このnew Trait {}を使うとTraitを継承した無名のクラスを作ってインスタンスすることが出来る。

$ scala 20180916_05_trait_abstract_member.scala
trait
override

まだこの章は続くのだけど、今日はここまで。

Scalaの勉強 - オブジェクト

今日はオブジェクトの章を。

オブジェクト

概要

ポイントっぽいところをまとめると以下。

  • Scalaでは全ての値がオブジェクト
    • これはpythonと一緒の思想。pythonの場合は全ての値がobjectクラスを継承している
    • 一方でScalaの場合はobjectという構文でそれを実現している(ように見えた)
  • 全てのメソッドは何らかのオブジェクトに属している
  • objectキーワードによって、同名のシングルトンオブジェクトを現在の名前空間の下に1つ定義可能
  • シングルトンオブジェクトにはそのオブジェクト固有のメソッド、フィールドを定義可能
    • 単純に上記の3つを読む限りだと、C++namespaceっぽい扱いに感じる。

構文はclassと似ていて以下の通り。

object <オブジェクト名> extends <クラス名> (with <トレイト名>)* {
  (<フィールド定義> | <メソッド定義>)*
}

クラスを継承できる?ようになってる。。。with <トレイト名>でmix-inが可能と書いてある(traitについては次の記事で)。用途は「オブジェクト名を既存のクラスのサブクラス等として振る舞わせる」ようなケースがあるらしい。

一例として挙げられていたのは以下。

Scalaの標準ライブラリでは、 Nil という object がありますが、これは List の一種として振る舞わせたいため、 List を継承しています。

用途

object構文の主な用途としては以下が紹介されている。

  • ユーティリティメソッドやグローバルな状態の置き場所(Javaで言うstaticメソッドやフィールド)
    • Scalaclassはstaticメソッドやフィールドを持てない。
  • 同名クラスのオブジェクトのファクトリメソッド

2番目の例は、前回のclassの記事でも使ったPointクラスを使って説明されている。

class Point(_x: Int, _y: Int) {
  val x = _x
  val y = _y
}

object Point {
  def apply(x: Int, y: Int): Point = new Point(x, y)
}

object ClassPoint {

  def main(args: Array[String]): Unit = {
    val point = Point(100, 2)

    // pattern 4
    println(point.x, point.y)
  }

上記のようにmainではPoint(100, 2)のようにnewなしでPointクラスを生成している。このケースではobject Pointapplyメソッドを定義したことにより、Point(100, 2)Point.apply(100, 2)と解釈されるようになっている。

これにより以下のメリットが得られる。

  • クラスの実装詳細を内部に隠蔽出来る
  • Pointではなく、そのサブクラスのインスタンスを返すことが出来る

このあとにケースクラスについての言及があるが、ここでは割愛して次に進む(後で使い方含めて説明があるらしいので)。

コンパニオンクラス

クラスと同じファイル内、同じ名前で定義されたシングルトンオブジェクトは、コンパニオンオブジェクトと呼ばれ、対応するクラスに対して特権的なアクセス権をもっているそうだ。

早速、例を見ていく。

NGになるケース

class Person(name: String, age: Int, private val weight: Int)

object Hoge {
  def printWeight(): Unit = {
    val taro = new Person("Taro", 20, 70)
    println(taro.weight)
  }
}

object ClassCompanionNG {

  def main(args: Array[String]): Unit = {
    Hoge.printWeight()
  }
}

上記では、class Person内のprivateなフィールドであるweightを表示するメソッドprintWeightを持つobjectであるHogeが定義されており、それをmainで呼び出している。

実行すると結果は以下のようにエラーになる。

$ scala 20180915_02_object_companion_ng.scala 
/home/dnn-admin/workspace/hw/study/2000_chisel/200_examples/20180915_02_object_companion_ng.scala:6: error: value weight in class Person cannot be accessed in Person
    println(taro.weight)
                 ^
one error found

OKになるケース

次はOKになる場合だ。

先ほどのNGケースとの違いはobject Hogeobject Personに変更されているだけになる。

class Person(name: String, age: Int, private val weight: Int)

object Person {
  def printWeight(): Unit = {
    val taro = new Person("Taro", 20, 70)
    println(taro.weight)
  }
}

object ClassCompanionOK {

  def main(args: Array[String]): Unit = {
    Person.printWeight()
  }
}

では実行してみよう。

$ scala 20180915_03_object_companion_ok.scala
70

今度は正常に実行されて、printWeightメソッド内で生成されたPersonクラスのオブジェクトのweightフィールドの値が表示された。

なおコンパニオンオブジェクトであってもprivate[this]で定義したフィールドにはアクセスが出来ないとのこと。

一応こちらも確認してみる。

class Person(name: String, age: Int, private[this] val weight: Int) {
  def print() = {
    println(this.weight)
  }
}

object Person {
  def printWeight(): Unit = {
    val taro = new Person("Taro", 20, 70)
    println(taro.weight)
    //taro.print()
  }
}

object ClassCompanionOK {

  def main(args: Array[String]): Unit = {
    Person.printWeight()
  }
}

先ほど正常に実行できたソースに対してprivate val weightprivate[this] val weightの修正を入れて、同様にprintWeight()メソッドを呼ぶと以下のようになる。

$ scala 20180915_04_object_companion_ng_private_this.scala 
/home/dnn-admin/workspace/hw/study/2000_chisel/200_examples/20180915_04_object_companion_ng_private_this.scala:6: error: value weight is not a member of Person
    println(taro.weight)
                 ^
one error found

一方で、上記でコメントアウトしたtaro.print()を呼ぶようにすると、正常に実行が可能となりクラス内部からのアクセスが可能なことが確認できた。

$ scala 20180915_04_object_companion_ng_private_this.scala 
70

さて練習問題を、、、と思ったら本章の練習問題は以下の通りで、今確かめたことだった。。。

クラスを定義して、そのクラスのコンパニオンオブジェクトを定義してみましょう。コンパニオンオブジェクトが同名のクラスに対する特権的なアクセス権を持っていることを、クラスのフィールドをprivateにして、そのフィールドへアクセスできることを通じて確認してみましょう。また、クラスのフィールドをprivate[this]にして、そのフィールドへアクセスできないことを確認してみましょう。

というわけでオブジェクトの章はおしまい。

Scalaの勉強 - クラス

今日はクラスの章を。

クラス

クラス定義

記法を除けばJavaのクラスと同等らしい。(Javaはよくわかってない)

構文は以下の通り。

class <クラス名> '(' (<引数名1> : <引数型1>, <引数名2>: <引数型2> ...)? ')' {
  (<フィールド定義> | <メソッド定義> )*
}

例えば点を示すクラスPointScalaでは以下のようになる。

class Point(_x: Int, _y: Int) {
  val x = _x
  val y = _y
}

うん、なんとなくはわかる。上記の定義そのものがコンストラクタになっている感じか。

使うときは、こんな感じでよさそう。

object ClassPoint {

  class Point(_x: Int, _y: Int) {
    val x = _x
    val y = _y
  }

  def main(args: Array[String]): Unit = {
    val point = new Point(100, 2)

    println(point)
    println(point.x)
    println(point.y)
  }
}

実行結果はエラーはなく、以下のようになった。

Main$Point@16b3fc9e
100
2

以下のようにして、コンストラクタの引数にval/varをつけて定義すると、その引数はそのまま公開フィールドになるとのこと。

class Point(val x: Int, val y: Int) {
}

Scalaでは複数のコンストラクタを定義することは出来るが、基本的には1つのコンストラクタのみを使って複数の精製方法を定義する場合にはapplyメソッドを使うことが多い。この1つのコンストラクタはプライマリコンストラクタとして、特別に扱われる。

ちなみにプライマリコンストラクタ以外にコンストラクタを作る場合はthisメソッドを用意する形になる(Scalaのリファレンスの5.3.1より

A class may have additional constructors besides the primary constructor. These

are defined by constructor definitions of the form def this(ps_1). . .(ps_n) = e.

あんまりいい例ではないけど、こんな感じ↓。

class Point3(val x: Int, val y: Int) {
  var z = 0
  def this(x: Int, y: Int, z: Int) = { this(x, y); this.z = z } // 2つめのコンストラクタ
}

プライマリコンストラクタの引数のスコープはクラス定義全体に及ぶので、以下のようにメソッド定義の中からコンストラクタ引数を参照できる。

class Point(val x: Int, val y: Int) {
  def +(p: Point): Point = {
    new Point(x + p.x, y + p.y)
  }
  override def toString(): String = "(" + x + ", " + y + ")"
}

なんかいろいろまだ見たこと無いのがいっぱい。。けど推測は出来る。

  • def +(p: Point): Point演算子オーバーロード的なやつ
  • def toString()String型への変換メソッドをオーバーライドしてる
    • ということはもともとのクラスにはtoString()メソッドがあるということか。pythonobjectみたく大本から派生してるものだと推測。

メソッド定義

すでに上記の例でメソッドを定義しているが一般形は以下のような形になる。

(private([this | <パッケージ名>])? | protected([<パッケージ名>])? def <メソッド名> '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*)?
')': <返り値型> = <本体>
  • <本体>の部分は通常はブロック式になる。
  • <返り値型>は省略可能だが、つけておくのがベター
  • private/protectedをつけない場合はpublic扱い
  • private/protected[this | パッケージ名]thisパッケージ名でスコープを限定できる
    • privateのみの場合:同一クラスからのみアクセス可能
    • protectedののみの場合:派生クラスからのアクセスも可能
    • [this]をつけた場合:同じオブジェクトからのみアクセス可能
    • [パッケージ名]をつけた場合:同一パッケージに所属している場合もアクセス可能

この辺のスコープの話はまたあとで確認が必要そう。。

複数の引数リストを持つメソッド

以下のように複数の引数リストを持つように定義することが可能

(private([this | <パッケージ名>])? | protected[<パッケージ名>]) def <メソッド名> '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*) ?')' ( '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*) ?')' )*
: <返り値型> = <本体式>

資料のサンプルを動かしてみる。

object ClassAdder {

  class Adder {
    def add(x: Int)(y: Int): Int = x + y
  }

  def main(args: Array[String]): Unit = {
    val ad = new Adder()

    println(ad.add(100)(200))
  }
}

うーーむ、なんとも不思議な定義&呼び出しだ。とりあえず実行してみると、以下のようにエラーはなく動作する。

$ scala ./20180909_02_class_adder.scala 
300

資料によれば、、

複数の引数リストを持つメソッドには、Scalaの糖衣構文と組み合わせて流暢なAPIを作ったり、後述するimplicit parameterのために必要になったり、型推論を補助するために使われたりといった用途があります。

とのことだが、このサンプルだけではいまいち便利さが理解できない。そのうちわかるのだろうか。。

とりあえず、続き。以下のようにすることで、最初の引数は適用済みの状態にして新しい関数にすることも出来る(部分適用というらしい)。

object ClassAdder {

  class Adder {
    def add(x: Int)(y: Int): Int = x + y
  }

  def main(args: Array[String]): Unit = {
    val ad = new Adder()
    val ad2 = ad.add(1) _ // addメソッドの最初の引数を適用した状態を作る
    println(ad2(4))
  }
}

なるほど、こうすることで特定のデータを適用済みの状態を作り出せるのか。match式とかと組み合わせると面白そうな気配。

ちなみに、通常通りに1つの引数リストに複数の引数を定義しておき同様に部分適用した状態を作ることも可能。なんかad4の定義が面倒になってるな。。なんだろう、これ。

object ClassAdder {

  class Adder2 {
    def add(x: Int, y: Int): Int = x + y
  }

  def main(args: Array[String]): Unit = {
    val ad3 = new Adder2()

    val ad4: Int => Int = ad3.add(1, _)
    println(ad4(3))
  }
}

フィールド定義

以下のように定義する

(private ([this | <パッケージ名>])? /protected ([<パッケージ名>])? (val/var) <フィールド名>: <フィールド型> = <初期化式>
  • val/var : 他のケースと同様に変更可能かどうか
  • private/protected:メソッドの定義と同様
    • private[this]をつけるとJVMレベルでのフィールドへの直接アクセスになるので若干高速になるらしい。

抽象メンバー

抽象メンバーも定義できる。

まずはメソッドから。要は本体を書かなければいいみたい。

(private([this | <パッケージ名>])? | protected([<パッケージ名>])? def <メソッド名> '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*)?
')': <返り値型>

次にフィールド。こちらもメソッドと感覚的には一緒で、初期化をしなければいい。

(private ([this | <パッケージ名>])? /protected ([<パッケージ名>])? (val/var) <フィールド名>: <フィールド型>

なお、抽象メンバーを持ったクラスを定義する際には抽象クラスとして宣言するためにabstract修飾子が必要になる。

ということでまとめると、以下のようになるはず。

abstract class <クラス名> '(' (<引数名1> : <引数型1>, <引数名2>: <引数型2> ...)? ')' {
  (private ([this | <パッケージ名>])? /protected ([<パッケージ名>])? (val/var) <フィールド名>: <フィールド型>* // 抽象フィールド
  (private([this | <パッケージ名>])? | protected([<パッケージ名>])? def <メソッド名> '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*)?
')': <返り値型>* // 抽象メソッド
}

前項で出てきたAdderを抽象クラスにしてみる。

object ClassAbstract {

  abstract class BaseAdder {
    var x:Int
    var y:Int
    def add(x: Int)(y: Int): Int
  }

  def main(args: Array[String]): Unit = {
    val ad = new BaseAdder()
  }
}

抽象クラスなのでインスタンス使用しようとするとエラーになる。この辺はC++とかと一緒。

$ scala ./20180909_03_class_abstract.scala 
./20180909_03_class_abstract.scala:10: error: class BaseAdder is abstract; cannot be instantiated
    val ad = new BaseAdder()

継承

前項で抽象クラスが出てきたが、他の限度と同様にインスタンス出来ないという挙動だった。ということで次は継承についてが出てくる。

継承の目的は以下の2つ

継承は以下のような構文。

class <クラス名> <クラス引数> (extends <スーパークラス>)? (with <トレイト名>)* {
  (<フィールド定義> | <メソッド定義>)*
}

extendsが継承のキーワード。System Verilogと一緒か。その後に付いているトレイトを使うことで複数の実装を継承することが可能とのことだが、こちらはドワンゴの資料で別で1章解説があるのでその時に確認することにする。

先ほど作成した抽象クラスBaseAdderを継承してインスタンス可能なクラスAdderを作る。

object ClassAbstract {

  abstract class BaseAdder {
    var x:Int
    var y:Int
    def add(x: Int)(y: Int): Int
  }

  class SubAdder extends(BaseAdder) {
    var x:Int = 0
    var y:Int = 0
    override def add(x: Int)(y: Int): Int = x + y
  }

  def main(args: Array[String]): Unit = {
    val ad = new SubAdder()

    println(ad.add(100)(200))
  }
}

上記のように継承して作成したSubAdderインスタンス可能になり、以下のように加算された結果が得られる。

$ scala ./20180909_03_class_abstract.scala 
300

次は抽象クラスではないクラスを継承してメソッドをオーバーライドしてみる。

object ClassDerived {

  class BaseAdder {
    var x:Int = 0
    var y:Int = 0
    def add(x: Int)(y: Int): Int = x + y
  }

  class WrongAdder extends(BaseAdder) {
    x = 50
    override def add(x: Int)(y: Int): Int = x + y + this.x
  }

  def main(args: Array[String]): Unit = {
    val ad = new WrongAdder()

    println(ad.add(100)(200))
  }
}

上記を実行してみると、WrongAdderの方ではthis.xに50を入れており、addメソッドでthis.xが加算される仕組みになっているため値が350になって返ってくる。

$ scala ./20180909_04_class_derived.scala
350

なお先の抽象クラスBaseAdderの例とは異なり、今回のBaseAdderクラスでは既にaddメソッドが実装されているため、SubAdderクラスのadd関数の宣言に付いているoverrideを外すとエラーになる。

$ scala ./20180909_04_class_derived.scala
./20180909_04_class_derived.scala:11: error: overriding method add in class BaseAdder of type (x: Int)(y: Int)Int;
 method add needs `override' modifier
    def add(x: Int)(y: Int): Int = x + y + this.x
        ^
one error found

練習問題

全てが Int 型の xyz という名前を持った、3次元座標を表す Point3D クラスを定義してください。 Point3D クラスは次のようにして使うことができなければいけません。

scala val p = new Point3D(10, 20, 30) println(p.x) // 10 println(p.y) // 20 println(p.z) // 30

解答

object ClassPractice {

  class Point3D(val x: Int, val y:Int, val z:Int)

  def main(args: Array[String]): Unit = {
    val p = new Point3D(10, 20, 30)
    println(p.x) // 10
    println(p.y) // 20
    println(p.z) // 30
  }
}

出力は以下。

$ scala ./20180909_05_class_practice.scala 
10
20
30

これでClassの章はおしまい。一通り終わってからスコープの件はもう少し突っ込んで確認してみよう。