본문으로 건너뛰기
code0.HDL

사용자 정의 타입

typedef로 설계가 다루는 타입을 정의합니다. 이름만 바꾸는 동의어, 이름 붙은 레이블의 enum, 이름 붙은 필드의 구조체, 태그와 함께 대안 하나를 담는 tagged union을 선언하고, packed 폭을 계산하며, union을 패턴 매칭으로 읽습니다.

이 장에서 배울 내용

  • 패키지 최상위에서 타입 동의어·enum·구조체·tagged union을 필요한 deriving 인스턴스와 함께 선언할 수 있습니다
  • enum의 인코딩과 enum·구조체·tagged union의 packed 폭을 예측할 수 있습니다
  • 구조체와 tagged union 값을 만들고 필드 이름이나 태그 매칭으로 읽을 수 있습니다
  • 서로 다른 typedef와 빠진 인스턴스가 만드는 타입 오류를 알아볼 수 있습니다
이 장에서 배우는 개념
  • 사용자 정의 타입
먼저 알아야 할 개념

1. typedef의 기본 규칙

typedef는 설계에 필요한 타입을 직접 정의하는 선언입니다. 기존 타입에 새 이름을 붙이고, 가능한 상태를 이름으로 나열하고, 여러 필드를 하나로 묶고, 여러 형태 중 하나를 담는 값을 만들 수 있습니다. 비트 벡터 대신 의미가 드러나는 타입을 쓰면 잘못 섞어 쓴 값이 하드웨어가 생성되기 전에 타입 오류로 검출됩니다. 이 장은 타입 동의어, enum, 구조체, tagged union을 다룹니다.

이 장의 예제는 대부분 rule 하나에서 값을 만들고 $display로 출력한 뒤 끝납니다. 타입 선언이 주제이므로 사이클 추적보다 출력되는 값과 폭에 주목하세요. 폭을 출력하는 줄은 valueOf(SizeOf#(T))를 씁니다. SizeOf#(T)는 T의 폭을 나타내는 numeric type이고, valueOf가 그 타입을 출력할 수 있는 정수로 바꿉니다. 비트 폭 자체는 타입 장에서 다루었습니다.

1.1 선언 위치

사용자 정의 타입은 패키지 최상위에서만 선언합니다. 모듈이나 함수 안에서는 선언할 수 없으며, 지역 typedef는 없습니다.

package Cpu;

typedef Bit#(8) Byte;        // 가능: 패키지 최상위

module mkCpu(Empty);
  // typedef Bit#(4) Nibble; // 불가능: 모듈 안
endmodule

endpackage

참고. 모듈 안에서 타입에 지역 이름이 필요하면 Alias, NumAlias, StrAlias proviso를 씁니다. 이 proviso가 도입하는 이름은 타입이 아니라 타입 변수이므로 소문자로 시작합니다.

1.2 typedef마다 새 타입이 생깁니다

enum, 구조체, union은 반드시 typedef로 이름을 붙인 뒤에 사용합니다. C와 달리 익명 구조체를 변수나 인자의 타입으로 직접 쓸 수 없습니다.

그리고 enum, 구조체, union을 정의하는 typedef는 각각 다른 모든 타입과 구별되는 새 타입을 만듭니다. 필드 이름과 필드 타입이 완전히 같은 구조체 둘도 서로 다른 타입이며, 한쪽 값을 다른 쪽 변수에 대입하면 타입 오류가 발생합니다. 원래 타입과 바꿔 쓸 수 있는 것은 타입 동의어뿐입니다.

1.3 이름의 대소문자

이름첫 글자예
타입 이름대문자Byte, Stage, Request
enum label대문자Fetch, Decode
tagged union의 태그대문자Immediate, Register
구조체 필드소문자addr, write
타입 변수소문자n, element_type

2. 타입 동의어

2.1 동의어 선언 형식

타입 동의어는 기존 타입에 다른 이름을 붙입니다. 새 타입을 만들지 않습니다.

형식

typedef 기존_타입 새_이름;
typedef Bit#(8)  Byte;
typedef UInt#(16) Address;

Byte와 Bit#(8)은 이름만 다른 같은 타입입니다. 변환 없이 서로 대입할 수 있고, 오류 메시지에도 같은 타입으로 나타납니다. 동의어에는 deriving을 붙이지 않습니다. Bit#(8)에 정의된 인스턴스를 Byte가 그대로 사용합니다.

동의어에 다시 동의어를 정의할 수 있고, 인터페이스도 타입이므로 같은 방법으로 이름을 붙일 수 있습니다.

2.2 매개변수가 있는 동의어

동의어 이름 뒤에 #(...)로 매개변수를 받으면 다형 동의어가 됩니다.

형식

typedef 기존_타입 새_이름#(type 타입_변수);
typedef 기존_타입 새_이름#(numeric type 크기_변수);
typedef Int#(n) MyInt#(numeric type n);
typedef Bit#(TAdd#(n, 1)) Wider#(numeric type n);
사용전개 결과
MyInt#(12)Int#(12)
Wider#(4)Bit#(TAdd#(4, 1)), 곧 Bit#(5)

TAdd#(n, 1)은 타입 수준에서 n + 1을 계산하는 타입 함수입니다.

주의. 매개변수가 크기를 나타내는데 사용 형태만으로 알 수 없으면 numeric type n으로 선언합니다. type n으로 선언하면 bsc는 크기가 아닌 일반 타입으로 간주합니다.

2.3 예제: 동의어는 이름만 바꿉니다

TypeSynonym.bsv는 동의어 두 개를 선언합니다. 두 typedef 줄을 먼저 읽고, rule show의 지역 변수 세 개를 확인하세요. raw = named 대입에는 아무 변환도 적혀 있지 않습니다.

실행하기 전에 예측해 보세요. five는 몇 비트일까요? SizeOf Byte 줄은 무엇을 출력할까요?

확인 문제

named는 Byte이고 raw는 Bit#(8)입니다. 대입 raw = named에 pack이나 폭 변환이 필요합니까?

답

TypeSynonym.bsv
package TypeSynonym;

// 타입 동의어는 기존 타입에 이름을 붙인다. 새 타입을 만들지 않으며 Byte와
// Bit#(8)은 어디서든 서로 대체할 수 있다.
typedef Bit#(8) Byte;

// 동의어는 매개변수를 받을 수 있고, 그때 다형 타입이 된다.
typedef Bit#(TAdd#(n, 1)) Wider#(numeric type n);

(* synthesize *)
module mkTop(Empty);

  rule show;
    Byte    named = 8'd200;
    Bit#(8) raw   = named;      // no conversion: the two types are the same
    Wider#(4) five = 5'b10011;

    $display("Byte        = %0d", named);
    $display("Bit#(8)     = %0d", raw);
    $display("Wider#(4)   = %b", five);
    $display("SizeOf Byte = %0d", valueOf(SizeOf#(Byte)));
    $finish(0);
  endrule

endmodule

endpackage

결과 확인.

출력 줄확인할 내용
Byte200입니다
Bit#(8)같은 200입니다. 두 이름이 같은 타입이므로 대입에서 아무 변환도 일어나지 않았습니다
Wider#(4)5비트 10011입니다. Wider#(4)가 Bit#(5)로 전개되었습니다
SizeOf Byte8입니다. 동의어의 폭은 원래 타입의 폭과 같습니다

직접 바꿔 보세요.

바꿀 곳예상 결과
5'b10011을 4'b1001로컴파일 오류 T0060이 발생합니다. Bit#(5)에 4비트 값이 쓰였다는 폭 불일치입니다. 동의어도 전개된 타입으로 검사됩니다
Wider#(4)를 Wider#(7)로 바꾸고 리터럴을 8'b10011001로Bit#(8)로 전개되어 그대로 컴파일되고 10011001이 출력됩니다

3. enum

3.1 enum 선언 형식

enum은 나열한 enum label만 값으로 갖는 새 타입을 만듭니다.

형식

typedef enum { 레이블1, 레이블2, ... } 타입_이름 deriving (인스턴스_목록);
typedef enum { Fetch, Decode, Execute } Stage deriving (Bits, Eq, FShow);
부분의미
enum { Fetch, Decode, Execute }가능한 값이 이 셋뿐인 타입입니다
Stage새 타입의 이름입니다
deriving (Bits, Eq, FShow)비트 표현, 비교, 출력 인스턴스를 자동으로 만듭니다

Stage 변수에는 숫자를 대입할 수 없고, 정수와 비교할 수도 없습니다. 서로 다른 두 enum이 같은 enum label을 써도 됩니다.

3.2 deriving이 만드는 인스턴스

인스턴스가능해지는 것없으면
Bits레지스터에 저장, pack과 unpackReg#(Stage)와 pack을 쓸 수 없습니다
Eq==, !=두 값을 비교할 수 없습니다
FShowfshow로 enum label 이름 출력fshow 호출에서 T0031 오류가 발생합니다

3.3 인코딩

인코딩을 적지 않으면 첫 enum label이 0이고 다음부터 1씩 커집니다. 폭은 가장 큰 인코딩을 담을 수 있는 최소 비트 수입니다.

enum label 수기본 인코딩폭
20, 11
3–40 … 32
5–80 … 73

외부 규격이 인코딩을 정해 두었다면 =로 값을 직접 적습니다.

형식

typedef enum { 레이블1 = 값, 레이블2 = 값, ... } 타입_이름 deriving (...);
typedef enum { Idle = 0, Busy = 4, Done } State deriving (Bits, Eq);

값을 생략한 enum label은 바로 앞 값보다 1 큰 값을 받으므로 Done은 5입니다. 가장 큰 인코딩이 5이므로 폭은 3비트입니다. 두 enum label에 같은 값을 주면 컴파일 오류 T0069가 발생합니다.

3.4 예제: 이름과 인코딩

EnumType.bsv는 Stage 값 하나를 네 가지 방식으로 출력합니다. deriving 절보다 enum label 목록을 먼저 읽으세요. fshow는 이름을, pack은 비트를 보여 줍니다.

실행하기 전에 예측해 보세요. is Decode 줄은 무엇을 출력할까요? Stage의 폭은 몇 비트일까요?

확인 문제

Stage는 Fetch, Decode, Execute를 나열합니다. pack(Decode)는 어떤 인코딩을 출력합니까?

EnumType.bsv
package EnumType;

// 열거는 나열한 레이블만을 값으로 갖는 새 타입을 만든다. deriving은 하드웨어에
// 저장하는 Bits, 비교하는 Eq, 출력하는 FShow 인스턴스를 생성한다.
typedef enum { Fetch, Decode, Execute } Stage deriving (Bits, Eq, FShow);

(* synthesize *)
module mkTop(Empty);

  rule show;
    Stage current = Decode;

    // fshow는 레이블을, pack은 컴파일러가 배정한 인코딩을 출력한다.
    $display("label    = ", fshow(current));
    $display("encoding = %b", pack(current));
    $display("is Decode = ", fshow(current == Decode));
    $display("width     = %0d", valueOf(SizeOf#(Stage)));
    $finish(0);
  endrule

endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인.

출력 줄확인할 내용
labelFShow 인스턴스가 enum label 이름 Decode를 출력합니다
encodingBits 인스턴스가 배정한 2비트 인코딩 01입니다. 두 번째 enum label이므로 1입니다
is DecodeEq 인스턴스의 == 결과 True입니다
width레이블 셋을 담는 데 필요한 2비트입니다

직접 바꿔 보세요.

바꿀 곳예상 결과
레이블에 Memory 추가(4개)폭은 그대로 2입니다
Memory, Writeback 추가(5개)폭이 3이 되고 인코딩이 001로 출력됩니다
{ Fetch = 1, Decode = 4, Execute }인코딩 100, 폭 3이 출력됩니다. Execute는 5입니다
deriving에서 FShow 제거fshow(current)에서 컴파일 오류 T0031이 발생합니다. FShow#(Stage) 인스턴스가 없다는 메시지입니다

4. 구조체

4.1 구조체 선언 형식

구조체는 이름 붙은 필드를 묶은 타입입니다. 모든 필드가 동시에 존재합니다.

형식

typedef struct {
  필드_타입 필드_이름;
  ...
} 타입_이름 deriving (인스턴스_목록);
typedef struct {
  Byte addr;
  Bool write;
} Request deriving (Bits, Eq, FShow);

4.2 값 만들기와 필드 읽기

형식

타입_이름 { 필드1: 값, 필드2: 값 }   // 생성
변수.필드                            // 읽기
변수.필드 = 새_값;                   // 지역 변수의 필드 바꾸기
Request req = Request { addr: 8'h40, write: True };
Byte a = req.addr;     // 8'h40
req.write = False;     // req의 write 필드만 바꿉니다
코드의미
Request { addr: ..., write: ... }필드 이름을 적어 구조체 값을 만듭니다. 적는 순서는 상관없습니다
req.addr점 표기로 필드를 읽습니다
req.write = False;지역 변수 req의 필드 하나를 바꿉니다. 나머지 필드는 그대로입니다

4.3 비트 배치

deriving (Bits)로 만든 구조체의 비트 표현은 먼저 선언한 필드가 상위 비트에 놓입니다. Request는 addr가 bit 8..1, write가 bit 0입니다. 그러나 소스에서 비트 위치를 계산해 필드를 꺼낼 필요는 없습니다. 필드 이름으로 읽으면 선언 순서가 바뀌어도 같은 값이 나옵니다.

4.4 예제: 필드를 이름으로 읽기

StructType.bsv는 Request 값 하나를 만들고 필드와 전체 값, 폭을 출력합니다. 각 필드 초기화 식이 어느 $display에서 읽히는지 추적하세요.

실행하기 전에 예측해 보세요. addr 줄은 %h로 무엇을 출력할까요? whole 줄의 fshow(req)는 어떤 모양일까요?

확인 문제

Request는 8비트 addr와 Bool write로 이루어집니다. Request의 폭은 몇 비트입니까?

StructType.bsv
package StructType;

// 구조체는 이름 붙은 필드를 묶는다. 필드 이름은 소문자로 시작한다.
typedef struct {
  Bit#(8) addr;
  Bool    write;
} Request deriving (Bits, Eq, FShow);

(* synthesize *)
module mkTop(Empty);

  rule show;
    Request req = Request { addr: 8'h40, write: True };

    // 필드는 점 표기로 읽는다.
    $display("addr    = %h", req.addr);
    $display("write   = ", fshow(req.write));
    $display("whole   = ", fshow(req));
    // 폭은 필드 폭의 합인 8 + 1이다.
    $display("width   = %0d", valueOf(SizeOf#(Request)));
    $finish(0);
  endrule

endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인.

출력 줄확인할 내용
addrreq.addr가 16진수 40입니다
writefshow가 Bool 값 True를 출력합니다
whole타입 이름, 필드 이름, 값이 함께 출력됩니다. packed 비트를 손으로 해석할 필요가 없습니다
width필드 폭의 합 8 + 1 = 9입니다

직접 바꿔 보세요.

  1. 생성식을 Request { write: True, addr: 8'h40 }로 바꿔 보세요. 출력은 하나도 바뀌지 않습니다. 생성과 사용은 적는 순서가 아니라 필드 이름으로 연결됩니다.
  2. 생성 다음 줄에 req.write = False;를 추가해 보세요. write와 whole 줄에만 False가 나타나고 addr는 그대로입니다.

5. tagged union

5.1 tagged union 선언 형식

tagged union은 여러 대안 중 정확히 하나를 담고, 지금 어느 대안인지를 태그로 함께 저장합니다.

형식

typedef union tagged {
  멤버_타입 태그_이름;
  ...
} 타입_이름 deriving (인스턴스_목록);
typedef union tagged {
  Byte    Immediate;
  Bit#(3) Register;
} Operand deriving (Bits, Eq, FShow);
부분의미
Immediate, Register태그 이름입니다. 값이 어느 대안인지 나타냅니다
Byte, Bit#(3)각 대안이 담는 값의 타입입니다

5.2 값 만들기

형식

tagged 태그_이름 값
tagged 태그_이름          // void 멤버
Operand a = tagged Immediate 8'd200;
Operand b = tagged Register 3'd7;

멤버 타입이 void이면 담을 값이 없는 대안이 됩니다. 프렐류드의 Maybe가 이런 형태입니다.

typedef union tagged {
  void Invalid;
  t    Valid;
} Maybe#(type t) deriving (Eq, Bits);

Maybe#(Bit#(8)) m = tagged Valid 8'd5;
Maybe#(Bit#(8)) none = tagged Invalid;

5.3 C의 union과 다른 점

태그가 값과 함께 저장된다는 점이 C의 union과 다릅니다. SystemVerilog에는 태그 없는 union도 있지만 BSV에는 tagged union만 있습니다.

주의. op.Immediate처럼 멤버를 직접 선택하는 문법도 허용됩니다. 그러나 op에 다른 태그가 담겨 있으면 결과는 정의되지 않은 값입니다. 컴파일은 성공하므로 이런 실수는 타입 검사로 검출되지 않습니다. 태그를 확인한 뒤 값을 꺼내려면 7절의 패턴 매칭을 씁니다.

5.4 예제: 대안이 다른 두 값

TaggedUnion.bsv는 같은 Operand 타입의 두 값을 만듭니다. fromRegister는 Register 대안을, fromImmediate는 Immediate 대안을 담습니다.

실행하기 전에 예측해 보세요. fshow는 태그 이름까지 출력할까요? 200은 16진수로 어떻게 출력될까요?

확인 문제

Operand의 폭은 지금 담긴 대안에 따라 달라집니까?

답

TaggedUnion.bsv
package TaggedUnion;

// tagged union은 대안 중 정확히 하나를 담고 어느 것인지 함께 기억한다. 태그
// 이름은 대문자로 시작한다. 두 대안의 폭을 일부러 다르게 두었다.
typedef union tagged {
  Bit#(8) Immediate;
  Bit#(3) Register;
} Operand deriving (Bits, Eq, FShow);

(* synthesize *)
module mkTop(Empty);

  rule show;
    Operand fromRegister  = tagged Register 3'd7;
    Operand fromImmediate = tagged Immediate 8'd200;

    $display("register  = ", fshow(fromRegister));
    $display("immediate = ", fshow(fromImmediate));
    // 태그 1비트에 가장 큰 멤버인 8비트 Immediate를 더한다.
    $display("width     = %0d", valueOf(SizeOf#(Operand)));
    $finish(0);
  endrule

endmodule

endpackage

결과 확인.

출력 줄확인할 내용
registertagged Register와 값 'h7이 출력됩니다. 값만 보고도 어느 대안인지 알 수 있습니다
immediatetagged Immediate와 200의 16진수 'hc8이 출력됩니다
width9입니다. 두 값 모두 같은 타입이므로 폭도 같습니다

6. packed 폭 계산

6.1 폭 계산 규칙

deriving (Bits)로 만든 비트 표현의 폭은 종류마다 다르게 계산합니다.

종류폭Stage, Request, Operand의 경우
enum가장 큰 인코딩을 담는 최소 비트 수레이블 3개, 인코딩 0–2이므로 2
구조체모든 필드 폭의 합8 + 1 = 9
tagged union태그 비트 수 + 가장 큰 멤버의 폭태그 1 + Immediate 8 = 9

Request와 Operand는 둘 다 9비트지만 이유가 다릅니다. Operand에서 Register 대안은 8비트 중 하위 3비트만 쓰고, 그 위의 비트는 정의되지 않습니다. 태그는 최상위 비트에 놓입니다.

주의. 폭이 같아도 비트 배치는 전혀 다르고, 두 타입은 서로 대입할 수 없습니다. 폭은 저장 비용을 알려 줄 뿐 타입의 호환성을 정하지 않습니다.

6.2 예제: 세 타입의 폭

Widths.bsv는 세 타입의 폭만 출력합니다. 실행하기 전에 위 규칙으로 세 값을 모두 계산해 보세요.

확인 문제

세 줄 가운데 2를 출력하는 줄은 어느 타입입니까?

답

Widths.bsv
package Widths;

typedef enum { Fetch, Decode, Execute } Stage deriving (Bits, Eq);

typedef struct {
  Bit#(8) addr;
  Bool    write;
} Request deriving (Bits, Eq);

typedef union tagged {
  Bit#(8) Immediate;
  Bit#(3) Register;
} Operand deriving (Bits, Eq);

(* synthesize *)
module mkTop(Empty);

  rule show;
    // 열거는 레이블 개수가 요구하는 만큼의 비트를 쓴다.
    $display("Stage   = %0d", valueOf(SizeOf#(Stage)));
    // 구조체는 필드 폭의 합이다.
    $display("Request = %0d", valueOf(SizeOf#(Request)));
    // 유니온은 담고 있는 멤버가 아니라 태그와 가장 큰 멤버를 더한 값이다.
    $display("Operand = %0d", valueOf(SizeOf#(Operand)));
    $finish(0);
  endrule

endmodule

endpackage

결과 확인. Stage, Request, Operand 줄이 각각 2, 9, 9이면 위 표와 일치합니다. Operand가 지금 어느 대안을 담는지와 관계없이 9비트라는 점을 확인하세요.

직접 바꿔 보세요.

바꿀 곳예상 결과
Register를 Bit#(9)로가장 큰 멤버가 9비트가 되므로 Operand가 10이 됩니다
Request에 Bool last; 필드 추가필드 폭의 합이 10이 됩니다

7. 패턴 매칭으로 tagged union 읽기

7.1 case ... matches 형식

tagged union의 값은 태그를 확인하면서 꺼냅니다.

형식

case (식) matches
  tagged 태그1 .이름1 : 문장;
  tagged 태그2 .이름2 : 문장;
endcase
case (op) matches
  tagged Immediate .v : $display("immediate %0d", v);
  tagged Register  .r : $display("register %0d", r);
endcase
부분의미
matches이 case가 값 비교가 아니라 패턴 매칭이라는 표시입니다
tagged Immediate .v태그가 Immediate이면 담긴 값을 새 이름 v에 결합합니다
.v, .r미리 선언한 변수가 아니라 패턴이 도입하는 이름입니다. 해당 분기 안에서만 쓸 수 있습니다

중첩 패턴과 패턴이 쓰일 수 있는 다른 자리는 패턴 매칭 장에서 다룹니다.

7.2 모든 대안 처리하기

case 문은 일부 대안을 생략할 수 있고, 일치하는 대안이 없으면 아무 문장도 실행하지 않습니다. case가 값을 반환해야 하는 함수 안에서 쓰일 때는 사정이 다릅니다. 현재 bsc는 대안이 빠진 경우도 컴파일할 수 있으며, 일치하는 분기가 없으면 정해지지 않은 값이 나올 수 있습니다. 컴파일러가 누락을 모두 검출하지는 않으므로 소스에서 모든 대안을 직접 처리하세요.

7.3 예제: 태그에 따라 값 꺼내기

PatternMatching.bsv의 resolve는 Operand를 받아 8비트 값을 반환합니다. 각 입력을 태그 확인, 값 결합, 결과 순서로 추적하세요. 두 대안의 값 타입이 다르다는 점에 주목하세요.

실행하기 전에 예측해 보세요. tagged Immediate 8'd200에는 어느 분기가 선택될까요? Register 분기에만 zeroExtend가 있는 이유는 무엇일까요?

확인 문제

tagged Register 3'd7에 대한 resolve의 결과는 얼마입니까?

PatternMatching.bsv
package PatternMatching;

typedef union tagged {
  Bit#(8) Immediate;
  Bit#(3) Register;
} Operand deriving (Bits, Eq, FShow);

// 패턴이 .value와 .number를 도입한다. 각각 자기 분기 안에서만 보인다.
function Bit#(8) resolve(Operand op);
  case (op) matches
    tagged Immediate .value  : return value;
    tagged Register  .number : return zeroExtend(number);
  endcase
endfunction

(* synthesize *)
module mkTop(Empty);

  rule show;
    $display("Immediate 200 -> %0d", resolve(tagged Immediate 8'd200));
    $display("Register  7   -> %0d", resolve(tagged Register 3'd7));
    $finish(0);
  endrule

endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인.

출력 줄확인할 내용
Immediate 200200입니다. Immediate의 값은 이미 8비트이므로 그대로 반환됩니다
Register 77입니다. 3비트 111이 zeroExtend로 상위에 0을 채운 8비트가 되었습니다

직접 바꿔 보세요.

바꿀 곳예상 결과
zeroExtend를 signExtend로111의 최상위 비트 1이 확장되어 Register 7의 결과가 255가 됩니다
return zeroExtend(number);를 return number;로컴파일 오류 T0020이 발생합니다. 기대한 타입은 Bit#(8), 실제 타입은 Bit#(3)입니다

8. 종합 예제

UserTypes.bsv는 네 종류의 사용자 정의 타입을 한 프로그램에서 함께 사용합니다. 동의어 Byte가 구조체 필드와 union 멤버의 타입으로 다시 쓰인다는 점을 확인하세요.

실행하기 전에 예측해 보세요. 마지막 네 줄이 출력하는 폭은 각각 얼마일까요?

확인 문제

op는 tagged Register 3'd7입니다. op에 대해 실행되는 case 분기는 어느 쪽입니까?

답

UserTypes.bsv
package UserTypes;

// 타입 동의어는 기존 타입에 이름을 붙인다. 새 타입을 만들지 않으며 Byte와
// Bit#(8)은 어디서든 서로 대체할 수 있다.
typedef Bit#(8) Byte;

// 열거는 나열한 레이블만을 값으로 갖는 새 타입을 만든다. deriving은 하드웨어에
// 저장하는 Bits, 비교하는 Eq, 출력하는 FShow 인스턴스를 생성한다.
typedef enum { Fetch, Decode, Execute } Stage deriving (Bits, Eq, FShow);

// 구조체는 이름 붙은 필드를 묶는다. 필드 이름은 소문자로 시작한다.
typedef struct {
  Byte   addr;
  Bool   write;
} Request deriving (Bits, Eq, FShow);

// tagged union은 대안 중 정확히 하나를 담고 어느 것인지 함께 기억한다. 태그
// 이름은 대문자로 시작한다. 두 대안의 폭을 일부러 다르게 두었다. 유니온의 폭은
// 지금 담고 있는 멤버가 아니라 태그와 가장 큰 멤버를 더한 값이다.
typedef union tagged {
  Byte    Immediate;
  Bit#(3) Register;
} Operand deriving (Bits, Eq, FShow);

(* synthesize *)
module mkTop(Empty);

  rule show;
    Byte     b   = 8'd200;
    Stage    st  = Decode;
    Request  req = Request { addr: 8'h40, write: True };
    Operand  op  = tagged Register 3'd7;

    $display("b        = %0d", b);
    $display("stage    = ", fshow(st));
    $display("request  = ", fshow(req));
    $display("operand  = ", fshow(op));

    // tagged union은 태그를 대조해 읽는다.
    case (op) matches
      tagged Immediate .v : $display("immediate value %0d", v);
      tagged Register  .r : $display("register number %0d", r);
    endcase

    // SizeOf#(T)는 numeric type이며 valueOf가 이를 Integer 값으로 변환한다.
    // 이 값으로 타입의 폭을 출력할 수 있다.
    $display("width Byte    = %0d", valueOf(SizeOf#(Byte)));
    $display("width Stage   = %0d", valueOf(SizeOf#(Stage)));
    $display("width Request = %0d", valueOf(SizeOf#(Request)));
    // 태그 1비트에 더 큰 멤버인 Immediate의 8비트를 더한다. Register는 3비트를 쓴다.
    $display("width Operand = %0d", valueOf(SizeOf#(Operand)));
    $finish(0);
  endrule

endmodule

endpackage

결과 확인.

출력확인할 내용
b, stage, request, operand동의어는 숫자로, 나머지는 fshow가 구조를 보여 주는 형태로 출력됩니다
register number 7op가 Register 태그이므로 Register 분기가 실행되었습니다
폭 네 줄Byte 8, Stage 2, Request 9, Operand 9입니다

9. 자주 하는 실수

9.1 모양이 같은 두 구조체를 서로 대입하기

typedef struct { Bit#(8) addr; Bool write; } Request deriving (Bits);
typedef struct { Bit#(8) addr; Bool write; } Other   deriving (Bits);

// 잘못된 코드: 다른 타입입니다
Other copy = req;

// 바른 코드: 변환이 의도라면 필드를 명시적으로 복사합니다
Other copy = Other { addr: req.addr, write: req.write };

typedef마다 별개의 타입이 생기므로 T0020 오류가 기대한 타입 Other와 실제 타입 Request를 보고합니다. 두 타입이 정말 같은 것을 뜻한다면 처음부터 하나의 타입을 쓰세요.

9.2 enum 값에 산술 연산하기

typedef enum { Red, Green, Yellow } Light deriving (Bits, Eq);

// 잘못된 코드: Light에는 Arith 인스턴스가 없습니다
Light next = current + 1;

// 바른 코드: enum label로 다음 상태를 적습니다
function Light nextLight(Light l);
  case (l)
    Red:    return Green;
    Green:  return Yellow;
    Yellow: return Red;
  endcase
endfunction

잘못된 코드는 Arith#(Light) 인스턴스가 없다는 T0031 오류를 발생시킵니다. pack과 unpack으로 우회해 인코딩에 1을 더하면 컴파일은 되지만, 마지막 상태 다음에 이름 없는 비트 패턴이 생길 수 있습니다.

9.3 비트 위치로 구조체 필드 꺼내기

typedef struct { Bit#(4) kind; Bit#(4) len; } Packet deriving (Bits);

// 잘못된 코드: 하위 4비트는 kind가 아니라 len입니다
Bit#(4) k = pack(p)[3:0];

// 바른 코드
Bit#(4) k = p.kind;

먼저 선언한 필드가 상위 비트에 놓이므로 pack(p)[3:0]은 len입니다. 비트 위치로 읽으면 필드 순서나 폭이 바뀔 때마다 조용히 틀린 값을 읽습니다. 필드는 이름으로 읽으세요.

9.4 태그를 확인하지 않고 멤버 읽기

// 잘못된 코드: op가 Register이면 정의되지 않은 값입니다
Bit#(8) v = op.Immediate;

// 바른 코드: 태그를 확인한 분기에서만 값을 씁니다
case (op) matches
  tagged Immediate .imm : v = imm;
  tagged Register  .r   : v = zeroExtend(r);
endcase

직접 선택은 컴파일되므로 오류 메시지가 없습니다. 결과가 가끔 이상한 값이면 union 멤버를 직접 읽는 곳이 있는지 확인하세요.

9.5 필요한 인스턴스를 deriving하지 않기

// 잘못된 코드: fshow를 쓰는데 FShow가 없습니다
typedef enum { Fetch, Decode, Execute } Stage deriving (Bits, Eq);
$display("stage = ", fshow(s));

// 바른 코드
typedef enum { Fetch, Decode, Execute } Stage deriving (Bits, Eq, FShow);

오류는 선언이 아니라 fshow를 호출한 줄에서 T0031로 보고됩니다. 인스턴스가 없다는 오류가 나면 해당 타입의 deriving 목록을 확인하세요.

10. 정리

주제핵심
선언 위치패키지 최상위에서만 선언합니다
타입 동의어새 타입을 만들지 않습니다. 원래 타입과 자유롭게 바꿔 씁니다
enum나열한 enum label만 값으로 갖는 새 타입입니다. 기본 인코딩은 0부터 1씩 증가합니다
구조체이름 붙은 필드를 동시에 담습니다. 필드는 이름으로 만들고 읽습니다
tagged union대안 하나와 그 태그를 함께 담습니다. 패턴 매칭으로 태그를 확인하며 읽습니다
폭enum은 최소 비트 수, 구조체는 필드 폭의 합, tagged union은 태그 + 가장 큰 멤버입니다
타입 구별typedef로 만든 enum, 구조체, union은 모양이 같아도 서로 다른 타입입니다

11. 확인 문제

  1. Byte는 Bit#(8)에 변환 없이 대입할 수 있는데, 모양이 같은 두 구조체는 서로 대입할 수 없는 이유는 무엇일까요?

    정답 보기

    타입 동의어는 새 타입을 만들지 않으므로 Byte와 Bit#(8)은 같은 타입입니다. 구조체를 정의하는 typedef는 매번 다른 모든 타입과 구별되는 새 타입을 만듭니다.

  2. enum label이 다섯 개인 enum에는 몇 비트가 필요할까요?

    정답 보기

    3비트입니다. 기본 인코딩은 0부터 4까지이고, 4를 담으려면 3비트가 필요합니다.

  3. Request와 Operand가 둘 다 9비트인 이유를 각각 설명해 보세요.

    정답 보기

    Request는 필드 폭의 합으로 addr 8비트와 write 1비트를 더한 9비트입니다. Operand는 태그 1비트와 가장 큰 멤버 Immediate의 8비트를 더한 9비트입니다.

  4. tagged union을 읽을 때 op.Immediate보다 패턴 매칭이 안전한 이유는 무엇일까요?

    정답 보기

    직접 선택은 태그를 확인하지 않으므로 다른 대안이 담겨 있으면 정의되지 않은 값을 읽습니다. 패턴 매칭은 태그가 일치하는 분기에서만 값을 이름에 결합합니다.

  5. 구조체 필드의 선언 순서를 바꾸면 req.addr의 값과 pack(req)의 결과는 각각 어떻게 될까요?

    정답 보기

    req.addr는 이름으로 읽으므로 값이 같습니다. pack(req)는 먼저 선언한 필드가 상위 비트에 놓이므로 비트 배치가 바뀝니다.

아래 실습 문제에서는 파이프라인 단계를 나타내는 enum을 인코딩이 아니라 enum label 기준으로 다음 단계로 옮기고, 요청 구조체를 packed 비트로 읽어 엉뚱한 필드를 읽는 코드를 고치고, Maybe 값에 담긴 값을 꺼내거나 값이 없으면 0을 반환합니다. 각 문제에서 값을 이름으로 다루는지, 비트 위치나 인코딩에 의존하는지 먼저 확인해 보세요. 다음 장은 enum을 상태 기계의 상태로 씁니다.

확인 지점

예제를 실행해 예측과 비교합니다

참고 자료

참고한 자료의 라이선스와 고지