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이나 폭 변환이 필요합니까?
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결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
Byte | 200입니다 |
Bit#(8) | 같은 200입니다. 두 이름이 같은 타입이므로 대입에서 아무 변환도 일어나지 않았습니다 |
Wider#(4) | 5비트 10011입니다. Wider#(4)가 Bit#(5)로 전개되었습니다 |
SizeOf Byte | 8입니다. 동의어의 폭은 원래 타입의 폭과 같습니다 |
직접 바꿔 보세요.
| 바꿀 곳 | 예상 결과 |
|---|---|
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과 unpack | Reg#(Stage)와 pack을 쓸 수 없습니다 |
Eq | ==, != | 두 값을 비교할 수 없습니다 |
FShow | fshow로 enum label 이름 출력 | fshow 호출에서 T0031 오류가 발생합니다 |
3.3 인코딩
인코딩을 적지 않으면 첫 enum label이 0이고 다음부터 1씩 커집니다. 폭은 가장 큰 인코딩을 담을
수 있는 최소 비트 수입니다.
enum label 수 | 기본 인코딩 | 폭 |
|---|---|---|
| 2 | 0, 1 | 1 |
| 3–4 | 0 … 3 | 2 |
| 5–8 | 0 … 7 | 3 |
외부 규격이 인코딩을 정해 두었다면 =로 값을 직접 적습니다.
형식
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)는 어떤 인코딩을 출력합니까?
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 문제에 이 개념을 적용해 보세요.
결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
label | FShow 인스턴스가 enum label 이름 Decode를 출력합니다 |
encoding | Bits 인스턴스가 배정한 2비트 인코딩 01입니다. 두 번째 enum label이므로 1입니다 |
is Decode | Eq 인스턴스의 == 결과 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의 폭은 몇 비트입니까?
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 문제에 이 개념을 적용해 보세요.
결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
addr | req.addr가 16진수 40입니다 |
write | fshow가 Bool 값 True를 출력합니다 |
whole | 타입 이름, 필드 이름, 값이 함께 출력됩니다. packed 비트를 손으로 해석할 필요가 없습니다 |
width | 필드 폭의 합 8 + 1 = 9입니다 |
직접 바꿔 보세요.
- 생성식을
Request { write: True, addr: 8'h40 }로 바꿔 보세요. 출력은 하나도 바뀌지 않습니다. 생성과 사용은 적는 순서가 아니라 필드 이름으로 연결됩니다. - 생성 다음 줄에
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의 폭은 지금 담긴 대안에 따라 달라집니까?
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결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
register | tagged Register와 값 'h7이 출력됩니다. 값만 보고도 어느 대안인지 알 수 있습니다 |
immediate | tagged Immediate와 200의 16진수 'hc8이 출력됩니다 |
width | 9입니다. 두 값 모두 같은 타입이므로 폭도 같습니다 |
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를 출력하는 줄은 어느 타입입니까?
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 : 문장;
endcasecase (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의 결과는 얼마입니까?
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 200 | 200입니다. Immediate의 값은 이미 8비트이므로 그대로 반환됩니다 |
Register 7 | 7입니다. 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 분기는 어느 쪽입니까?
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 7 | op가 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. 확인 문제
-
Byte는Bit#(8)에 변환 없이 대입할 수 있는데, 모양이 같은 두 구조체는 서로 대입할 수 없는 이유는 무엇일까요?정답 보기
타입 동의어는 새 타입을 만들지 않으므로
Byte와Bit#(8)은 같은 타입입니다. 구조체를 정의하는typedef는 매번 다른 모든 타입과 구별되는 새 타입을 만듭니다. -
enum label이 다섯 개인enum에는 몇 비트가 필요할까요?정답 보기
3비트입니다. 기본 인코딩은 0부터 4까지이고, 4를 담으려면 3비트가 필요합니다.
-
Request와Operand가 둘 다 9비트인 이유를 각각 설명해 보세요.정답 보기
Request는 필드 폭의 합으로addr8비트와write1비트를 더한 9비트입니다.Operand는 태그 1비트와 가장 큰 멤버Immediate의 8비트를 더한 9비트입니다. -
tagged union을 읽을 때op.Immediate보다 패턴 매칭이 안전한 이유는 무엇일까요?정답 보기
직접 선택은 태그를 확인하지 않으므로 다른 대안이 담겨 있으면 정의되지 않은 값을 읽습니다. 패턴 매칭은 태그가 일치하는 분기에서만 값을 이름에 결합합니다.
-
구조체 필드의 선언 순서를 바꾸면
req.addr의 값과pack(req)의 결과는 각각 어떻게 될까요?정답 보기
req.addr는 이름으로 읽으므로 값이 같습니다.pack(req)는 먼저 선언한 필드가 상위 비트에 놓이므로 비트 배치가 바뀝니다.
아래 실습 문제에서는 파이프라인 단계를 나타내는 enum을 인코딩이 아니라 enum label 기준으로
다음 단계로 옮기고, 요청 구조체를 packed 비트로 읽어 엉뚱한 필드를 읽는 코드를 고치고, Maybe
값에 담긴 값을 꺼내거나 값이 없으면 0을 반환합니다. 각 문제에서 값을 이름으로 다루는지, 비트
위치나 인코딩에 의존하는지 먼저 확인해 보세요. 다음 장은 enum을 상태 기계의 상태로 씁니다.