+는 Integer, UInt#(8), Int#(16)에 모두 쓸 수 있지만, 세 타입이 같은 덧셈 구현을
공유하지는 않습니다. BSV는 같은 연산 이름에 타입별 구현을 연결하는 데 타입클래스를,
특정 타입의 구현을 제공하는 데 인스턴스를, 다형 정의가 필요로 하는 구현을 선언하는
데 proviso를 씁니다. 세 요소는 모두 타입 검사와 elaboration에서 처리되며, 실행
중인 회로에는 인스턴스를 고르는 논리가 생기지 않습니다. 이 장은 BSV Language Reference
Guide 제8장의 타입클래스와 proviso, 그리고 제4장의 polymorphic type, proviso,
numeric type 함수를 다룹니다. 타입·폭·명시적 변환
장의 폭과 pack, unpack, 사용자 정의 타입 장의
enum, 구조체, deriving, 그리고 변수·반복문·리터럴의 문맥
장에서 본 선언된 타입이 Literal 인스턴스를 고르는 방식을 알고 있어야 합니다.
1. 오버로딩과 타입클래스
1.1 다형성과 오버로딩
두 개념은 비슷해 보이지만 다릅니다.
| 개념 | 구현 수 | 예 |
|---|---|---|
| 다형 함수 | 본문 하나를 여러 타입에서 구체화합니다 | function t pick(Bool c, t a, t b) |
| 오버로드 연산 | 타입마다 구현이 따로 있습니다 | Bit#(8)의 +와 Int#(16)의 + |
타입클래스는 오버로드 연산의 묶음에 붙인 이름입니다. 예를 들어 Eq는 ==와 /=를,
Bits는 pack과 unpack을, Arith는 +, -, * 같은 산술 연산을 묶습니다.
인스턴스는 특정 타입이 그 타입클래스의 연산을 어떻게 구현하는지 정의합니다.
1.2 타입 변수
polymorphic type은 구체적인 타입 대신 타입 변수를 매개변수로 씁니다. 타입 변수는
사용할 때마다 구체적인 타입 하나로 결정됩니다.
Vector#(n, t) // 타입 t의 원소 n개. 모든 원소가 같은 타입입니다
List#(List#(b)) // 원소가 "타입 b의 리스트"인 리스트타입 변수는 Bit#(n)의 n이나 위의 t처럼 소문자로 시작하며, 사용하는 문맥에서
결정됩니다. 회로가 실행되는 동안 정해지는 타입 변수는 없습니다.
1.3 컴파일할 때 일어나는 일
proviso가 붙은 정의를 호출하면 bsc는 다음 순서로 인스턴스를 찾습니다.
- 호출의 인자와 결과 선언에서 타입 변수와 폭을 추론합니다.
- 오버로드 연산과
proviso가 요구하는 조건을 모읍니다. - 각 조건에 맞는 인스턴스나
numeric type관계를 찾습니다. - 찾은 인스턴스가 다른 조건을 요구하면 그 조건도 확인합니다.
- 선택한 구현으로 정의를 구체화하고
elaboration을 계속합니다.
이 과정은 rule의 조건도 시뮬레이션 사건도 아닙니다. 회로에는 태그, 멀티플렉서, 가상
호출이 추가되지 않습니다. 인스턴스를 찾으면 선택된 구현만 하드웨어 생성에 참여하고,
찾지 못하면 시뮬레이션 전에 컴파일 오류가 발생합니다.
2. 타입클래스 선언
2.1 타입클래스 선언 형식
형식
typeclass 클래스_이름#(type 타입_변수);
멤버_선언;
...
endtypeclasstypeclass Describe#(type t);
function Bit#(4) code(t value);
endtypeclass| 부분 | 의미 |
|---|---|
Describe | 타입클래스 이름입니다. 대문자로 시작합니다 |
type t | 이 타입클래스가 설명하는 타입 변수입니다 |
function Bit#(4) code(t value); | 모든 인스턴스가 구현해야 하는 멤버입니다. 선언만 있고 본문은 없습니다 |
멤버는 함수, 모듈, 일반 값일 수 있습니다. 타입클래스 선언 자체에는 구현이 없습니다.
2.2 상위 타입클래스 조건
타입클래스 선언에 provisos를 붙이면 그 타입클래스의 모든 인스턴스가 다른 타입클래스도
만족해야 합니다.
형식
typeclass 클래스_이름#(type t) provisos (상위_클래스#(t));
...
endtypeclasstypeclass Ranked#(type t) provisos (Eq#(t));
function Bit#(2) rank(t x);
endtypeclassRanked 인스턴스가 있는 타입에는 반드시 Eq 인스턴스도 있어야 합니다. 인스턴스를 찾을 때
bsc는 이 조건까지 연쇄적으로 확인합니다.
3. 인스턴스 선언
3.1 인스턴스 선언 형식
인스턴스는 특정 타입에 대해 타입클래스의 모든 멤버를 정의합니다.
형식
instance 클래스_이름#(구체_타입);
멤버_정의;
...
endinstanceinstance Describe#(Bool);
function Bit#(4) code(Bool value);
return value ? 4'h1 : 4'h0;
endfunction
endinstance| 부분 | 의미 |
|---|---|
instance Describe#(Bool); | Bool 타입의 Describe 구현을 시작합니다 |
function ... code(Bool value); | 타입클래스의 멤버를 Bool에 맞게 정의합니다 |
endinstance | 인스턴스를 닫습니다 |
3.2 연산자 멤버 정의하기
== 같은 연산자를 멤버로 정의할 때는 이스케이프 식별자를 씁니다. 백슬래시로 시작하고
공백으로 끝나는 이름입니다. Eq의 두 멤버는 \==와 \/=입니다.
typedef struct { Bit#(4) v; } Code deriving (Bits);
instance Eq#(Code);
function Bool \== (Code a, Code b) = a.v == b.v;
function Bool \/= (Code a, Code b) = a.v != b.v;
endinstance주의. \== 뒤의 공백은 식별자를 끝내는 BSV 문법의 일부입니다. 공백을 지우고
\==(Code a, ...)로 쓰면 괄호까지 식별자에 포함되어 구문 오류 P0005가 발생합니다.
4. proviso로 다형 함수 제약하기
4.1 proviso 형식
타입 변수 t가 무엇인지 모르면 t에 어떤 연산을 쓸 수 있는지도 알 수 없습니다. 그래서
다형 함수가 오버로드 연산을 사용하려면, 그 연산이 있는 타입만 받겠다고 provisos로
선언해야 합니다.
형식
function 반환_타입 이름(인자) provisos (클래스#(타입_변수), ...);
...
endfunctionfunction Bool same(t a, t b) provisos (Eq#(t));
return a == b;
endfunctionEq#(t)는 모든 타입에 비교 연산을 만들어 주지 않습니다. same을 호출할 때마다 Eq
인스턴스가 이미 있는 타입을 넘겨야 한다는 계약입니다. 이 계약이 있으므로 본문은
다형성을 유지하면서 ==를 쓸 수 있습니다.
| 호출 | 필요한 인스턴스 | 결과 |
|---|---|---|
same(8'd3, 8'd3) | Eq#(Bit#(8)) | 프렐류드에 있으므로 컴파일됩니다 |
same(True, False) | Eq#(Bool) | 프렐류드에 있으므로 컴파일됩니다 |
Eq가 없는 타입 | 해당 Eq 인스턴스 | 컴파일 오류 T0031이 발생합니다 |
proviso는 타입 변수에 폭을 연결할 수도 있습니다.
function Bit#(n) bitsOf(t x) provisos (Bits#(t, n));
return pack(x);
endfunctionBits#(t, n)은 "t는 n비트 표현을 가지며 pack과 unpack을 쓸 수 있다"는
조건입니다. 이 proviso를 빼면 pack(x)를 쓸 근거가 없으므로 T0030 오류가 발생하고,
메시지는 Bits#(t, n)이 필요하다고 알려 줍니다.
proviso | 쓸 수 있는 기능 |
|---|---|
Bits#(t, n) | t와 Bit#(n) 사이의 pack, unpack |
Eq#(t), Ord#(t) | 동등 비교, 순서 비교 |
Arith#(t) | 산술 연산 |
Literal#(t) | fromInteger를 통한 정수 리터럴 |
Bounded#(t) | minBound, maxBound |
BitExtend#(n, m, f) | f#(n)에서 f#(m)으로의 폭 확장과 반대 방향의 절단 |
4.2 예제: 여러 타입에 쓰는 함수 하나
Polymorphism.bsv의 bitsOf는 Bits 인스턴스가 있는 어떤 타입이든 받아 비트 표현을
반환합니다. rule에서 Bool, UInt#(3), Bit#(4) 값으로 한 번씩 호출합니다.
실행하기 전에 예측해 보세요. 각 호출에서 t와 n은 무엇으로 결정될까요? 출력되는
비트 수는 몇 개일까요?
확인 문제
UInt#(3) reading = 5; 일 때 bitsOf(reading)은 무엇을 출력할까요?
package Polymorphism;
// 구체적인 타입은 아직 정해지지 않았다. proviso는 pack을 요구하고 타입의 비트 폭을 n과 연결한다.
function Bit#(n) bitsOf(t x) provisos (Bits#(t, n));
return pack(x);
endfunction
(* synthesize *)
module mkTop(Empty);
rule show;
Bool flag = True;
UInt#(3) reading = 5;
Bit#(4) nibble = 4'b1010;
// 각 호출에서 t가 따로 결정되고, 그 타입에 따라 폭이 정해진다.
$display("Bool -> %b", bitsOf(flag));
$display("UInt#(3) -> %b", bitsOf(reading));
$display("Bit#(4) -> %b", bitsOf(nibble));
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인.
| 출력 줄 | t | n | 확인할 내용 |
|---|---|---|---|
Bool | Bool | 1 | True가 1 한 비트로 출력됩니다 |
UInt#(3) | UInt#(3) | 3 | 5가 101로 출력됩니다 |
Bit#(4) | Bit#(4) | 4 | 1010이 그대로 출력됩니다 |
함수 정의는 하나지만 t와 n은 각 호출에서 따로 결정됩니다. 첫 호출에서 정해진 폭이
다음 호출에 이어지지 않습니다.
직접 바꿔 보세요. Integer 값을 bitsOf에 전달해 보세요. Integer에는 Bits
인스턴스가 없으므로 T0031 오류가 발생하고, 메시지에 Bits#(Integer, ...) 인스턴스가
없다고 표시됩니다. 함수 본문을 고쳐서는 해결되지 않습니다. 비트 표현이 있는 타입을
전달해야 합니다.
4.3 예제: 인자 타입이 인스턴스를 고릅니다
PriorityInstance.bsv는 Priority 타입클래스와 두 인스턴스, 그리고 proviso를 가진
다형 함수 requestPriority를 정의합니다. 다음 순서로 읽으세요.
Priority선언과 멤버priorityOfBool인스턴스와RequestKind인스턴스requestPriority의provisos (Priority#(value_type))show의 두 호출
실행하기 전에 예측해 보세요.
requestPriority(Write)와requestPriority(True)는 각각 어느 인스턴스를 사용할까요?- 두 줄에 출력되는 우선순위는 각각 얼마일까요?
- 회로가 실행되는 동안 인스턴스 선택이 바뀔 수 있을까요?
확인 문제
requestPriority(Write)는 어느 인스턴스를 사용하고 무엇을 출력할까요?
package PriorityInstance;
typedef enum { Reset, Read, Refresh, Write } RequestKind
deriving (Bits, Eq, FShow);
// Priority는 오버로드 연산 하나를 묶는다. 각 instance는 자기 타입에 맞는
// priorityOf를 구현해야 한다.
typeclass Priority#(type value_type);
function Bit#(2) priorityOf(value_type value);
endtypeclass
// 두 구체적인 instance가 같은 연산 이름에 타입별로 다른 동작을 제공한다.
instance Priority#(Bool);
function Bit#(2) priorityOf(Bool value);
return value ? 2'd3 : 2'd0;
endfunction
endinstance
instance Priority#(RequestKind);
function Bit#(2) priorityOf(RequestKind value);
case (value)
Refresh: return 2'd0;
Read: return 2'd1;
Write: return 2'd2;
Reset: return 2'd3;
endcase
endfunction
endinstance
// 다형 본문 하나가 인자 타입이 선택한 Priority instance를 사용한다.
function Bit#(2) requestPriority(value_type value)
provisos (Priority#(value_type));
return priorityOf(value);
endfunction
(* synthesize *)
module mkTop(Empty);
rule show;
$display("Write priority = %0d", requestPriority(Write));
$display("True priority = %0d", requestPriority(True));
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인.
| 출력 줄 | 결정된 value_type | 필요한 인스턴스 | 확인할 내용 |
|---|---|---|---|
Write priority | RequestKind | Priority#(RequestKind) | Write에 2가 반환됩니다 |
True priority | Bool | Priority#(Bool) | True에 3이 반환됩니다 |
두 호출은 컴파일할 때 각자 다른 구현으로 구체화되었습니다. 실행할 때 rule show는 이미
정해진 연산을 수행할 뿐이고, 인스턴스를 고르는 회로는 없습니다.
직접 바꿔 보세요.
| 바꿀 곳 | 예상 결과 |
|---|---|
requestPriority(True)를 requestPriority(False)로 | 같은 Bool 인스턴스가 쓰이고 결과만 0으로 바뀝니다 |
True를 UInt#(8)'(5)로 | 컴파일 오류 T0031이 발생합니다. Priority#(UInt#(8)) 인스턴스가 없다는 메시지입니다 |
requestPriority의 provisos 절 삭제 | 컴파일 오류 T0030이 발생합니다. proviso가 너무 일반적이라며 필요한 Priority#(value_type)을 알려 줍니다 |
값을 바꾸면 같은 인스턴스 안에서 결과만 달라지고, 타입을 바꾸면 필요한 인스턴스 자체가 달라집니다.
5. 크기 관계 proviso
5.1 폭 사이의 관계 선언하기
일부 타입클래스는 함수 본문 대신 numeric type 사이의 등식을 나타냅니다. 이런 proviso는
입력 폭과 출력 폭이 어떤 관계인지 함수 시그니처에 적습니다. 실행 중의 산술이 아니라
numeric type 사이의 관계를 선언합니다.
형식
function Bit#(m) 이름(Bit#(n) x) provisos (Add#(n, k, m));proviso | 뜻 | 예 |
|---|---|---|
Add#(a, b, c) | a + b = c | Add#(3, 4, 7) |
Mul#(a, b, c) | a × b = c | Mul#(3, 2, 6) |
Div#(a, b, c) | ⌈a / b⌉ = c | Div#(7, 2, 4) |
Max#(a, b, c) | c = a와 b 중 큰 값 | Max#(3, 5, 5) |
Log#(a, b) | b = ⌈log₂ a⌉ | Log#(5, 3), Log#(8, 3) |
function Bit#(m) twice(Bit#(n) x) provisos (Mul#(n, 2, m));
return {x, x};
endfunctiontwice(3'b101)을 Bit#(6)에 받으면 n = 3, m = 6이고 Mul#(3, 2, 6)이 성립하므로
101101이 됩니다.
function Bit#(m) pad0101(Bit#(n) x) provisos (Add#(n, 4, m));
return { x, 4'b0101 };
endfunction이 시그니처는 "반환 폭 m은 인자 폭 n보다 4 크다"는 폭 방정식입니다.
참고. 반대로 타입 안에서 폭을 계산할 때는 TAdd#(n, 1), TMul#(n, 2) 같은
타입 함수를 씁니다. proviso는 관계를 조건으로 요구하고, 타입 함수는 결과 타입을 직접
만듭니다.
5.2 예제: 인자 폭에 따라 달라지는 반환 폭
types 예제 묶음의 WidthProviso.bsv는 pad0101을 4비트 nibble과 2비트 pair로
한 번씩 호출합니다.
실행하기 전에 예측해 보세요. 두 호출에서 m은 각각 얼마일까요? 0101은 결과의
어느 쪽에 놓일까요?
확인 문제
Bit#(2) pair = 2'b11; 일 때 pad0101(pair)의 m과 결과는 무엇일까요?
package WidthProviso;
// proviso가 m을 n + 4로 제약한다. 반환 폭은 인자 폭에 따라 정해진다.
function Bit#(m) pad0101(Bit#(n) x) provisos (Add#(n, 4, m));
return { x, 4'b0101 };
endfunction
(* synthesize *)
module mkTop(Empty);
rule show;
Bit#(4) nibble = 4'b1100;
Bit#(2) pair = 2'b11;
// 첫 호출에서 n은 4, 둘째에서 2다. 따라서 m은 8, 그다음 6이다.
Bit#(8) widened = pad0101(nibble);
Bit#(6) shorter = pad0101(pair);
$display("pad0101(%b) = %b", nibble, widened);
$display("pad0101(%b) = %b", pair, shorter);
$finish(0);
endrule
endmodule
endpackage결과 확인.
| 호출 | n | m | 결과 |
|---|---|---|---|
pad0101(1100) | 4 | 8 | 11000101 |
pad0101(11) | 2 | 6 | 110101 |
두 결과 모두 입력이 상위 비트를, 0101이 하위 4비트를 차지합니다.
직접 바꿔 보세요.
| 바꿀 곳 | 예상 결과 |
|---|---|
Bit#(6) shorter를 Bit#(8) shorter로 | T0031 오류가 발생하고, 메시지에 Add#(2, 4, 8) 인스턴스가 없다고 표시됩니다 |
Bit#(5) 변수에 pad0101(1'b1) 결과 담기 | n이 1, m이 5로 결정되어 10101이 됩니다 |
함수 정의는 그대로 유효합니다. 오류는 정의가 아니라 Add#(2, 4, m)과 m = 8을 동시에
만족할 수 없는 호출 위치에서 발생합니다.
5.3 예제: 결과 폭이 고정된 호출
typeclasses-and-provisos 예제 묶음의 WidthProviso.bsv에 있는 appendTag는 값 뒤에
4비트 태그를 연결합니다. 시그니처를 연결식보다 먼저 읽고, show에서 결과를 받는 선언
Bit#(7) result를 확인하세요.
실행하기 전에 예측해 보세요.
n, 태그 폭,m은 각각 얼마일까요?bsc가 확인해야 하는 관계는 무엇일까요?101과1010을 연결한 7비트 값은 16진수로 얼마일까요?
확인 문제
Bit#(7) result = appendTag(3'b101, 4'ha); 일 때 tagged 줄은 무엇을 출력할까요?
package WidthProviso;
function Bit#(m) appendTag(Bit#(n) value, Bit#(4) tag)
provisos (Add#(n, 4, m));
return { value, tag };
endfunction
(* synthesize *)
module mkTop(Empty);
rule show;
Bit#(7) result = appendTag(3'b101, 4'ha);
$display("tagged = %h", result);
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인.
| 단계 | 결정되는 것 |
|---|---|
첫 인자 3'b101 | n = 3 |
| 둘째 인자 | 시그니처에 따라 4비트 |
결과 선언 Bit#(7) | m = 7 |
| 확인할 관계 | Add#(3, 4, 7), 성립 |
| 실행 결과 | 상위 101, 하위 1010을 연결한 1011010이 16진수 5a로 출력됩니다 |
직접 바꿔 보세요.
| 바꿀 곳 | 예상 결과 |
|---|---|
결과 선언만 Bit#(8)로 | 컴파일 오류 T0031이 발생합니다. Add#(3, 4, 8) 인스턴스가 없다는 메시지입니다. 이 proviso에는 자동 확장이나 절단이 없습니다 |
Bit#(8) result = appendTag(4'b1101, 4'ha); | Add#(4, 4, 8)이 성립해 컴파일되고 da가 출력됩니다. 8비트 결과가 필요하면 입력 폭을 바꿔야 합니다 |
6. numeric type을 값으로 바꾸기
6.1 SizeOf와 valueOf
형식
SizeOf#(타입) // 타입의 비트 폭을 numeric type으로 계산합니다
valueOf(numeric_type) // numeric type을 Integer 값으로 바꿉니다| 표현 | 입력 | 결과 |
|---|---|---|
SizeOf#(T) | 타입 | 폭을 나타내는 numeric type |
valueOf(n) | numeric type | Integer 값 |
Integer w = valueOf(SizeOf#(Bit#(12))); // 12stringOf는 같은 방식으로 문자열 타입을 String 값으로 바꿉니다. valueof와
valueOf, stringof와 stringOf는 모두 쓸 수 있는 표기입니다.
6.2 예제: 타입의 폭 출력하기
TypeSize.bsv는 세 타입의 폭을 valueOf(SizeOf#(...))로 계산해 출력합니다. 식을
안쪽부터 읽으세요.
실행하기 전에 예측해 보세요. Bool, Bit#(12), UInt#(5)는 각각 몇 비트일까요?
확인 문제
UInt#(5) 줄은 무엇을 출력하며, valueOf는 무엇을 했을까요?
package TypeSize;
(* synthesize *)
module mkTop(Empty);
rule show;
// SizeOf#(T)는 값이 아니라 numeric type이다. valueOf로 Integer 값으로 변환한다.
$display("Bool occupies %0d bit(s)", valueOf(SizeOf#(Bool)));
$display("Bit#(12) occupies %0d bit(s)", valueOf(SizeOf#(Bit#(12))));
$display("UInt#(5) occupies %0d bit(s)", valueOf(SizeOf#(UInt#(5))));
$finish(0);
endrule
endmodule
endpackage결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
Bool | 1비트입니다 |
Bit#(12) | 12비트입니다 |
UInt#(5) | 5비트입니다 |
SizeOf가 계산한 numeric type을 valueOf가 Integer로 바꾸었기 때문에 $display에
전달할 수 있었습니다.
직접 바꿔 보세요. 한 줄에서 valueOf를 지우고 SizeOf#(Bool)을 바로 전달해 보세요.
식이 와야 할 위치에 타입 표기가 왔으므로 P0005 구문 오류가 발생합니다.
7. 타입클래스 진단 읽기
7.1 진단과 빠진 사실
오류 메시지에 나온 타입과 proviso를 먼저 구체적인 조건으로 다시 적어 보세요. 문구가
비슷해도 빠진 사실과 고칠 곳은 다릅니다.
| 오류 코드와 모양 | 빠진 사실 | 고칠 곳 |
|---|---|---|
T0031 구체 타입의 인스턴스가 없음 | 그 타입에 요청한 타입클래스의 구현이 없습니다 | 지원되는 타입을 쓰거나 인스턴스를 정의합니다 |
T0031 크기 관계가 성립하지 않음 | 폭 등식이 거짓입니다 | 인자 폭, 결과 폭, 함수 시그니처를 맞춥니다 |
T0030 proviso가 너무 일반적임 | 다형 본문이 쓰는 연산을 시그니처가 요구하지 않습니다 | 메시지가 알려 주는 proviso를 그 정의에 추가합니다 |
T0035 크기를 알 수 없는 비트 벡터 | 어느 인스턴스나 폭이 필요한지 정할 타입 정보가 없습니다 | 식이나 결과 선언에 타입을 명시하고 불필요한 확장, 절단을 없앱니다 |
7.2 같은 이름, 다른 원인
Priority#(t)처럼 타입 변수가 남은 조건이 보이면 다형 정의 자신의 proviso가 빠졌을
가능성이 큽니다. Priority#(UInt#(8))처럼 구체 타입이 보이면 타입은 이미 결정되었고 그
타입의 인스턴스가 없다는 뜻입니다. Add#(3, 4, 8)은 모호한 것이 아니라 완전히 결정된
거짓 관계입니다.
Bit#(4) y = 3;
Bit#(4) x = truncate(zeroExtend(y)); // T0035: 중간 폭을 정할 수 없습니다위 코드에서 zeroExtend의 결과 폭은 어디에도 적혀 있지 않습니다. truncate와
zeroExtend 사이의 폭을 정할 정보가 없으므로 T0035가 발생합니다.
컴파일하기 전에 위 표를 다형 함수 하나에 적용해 보세요.
function t largerOf(t a, t b);
return (a > b) ? a : b;
endfunction확인 문제
UInt#(4) 값 두 개로 largerOf(p, q)를 호출합니다. bsc는 무엇을 보고하고, 무엇을 고쳐야 할까요?
확인 문제
provisos (Ord#(t))를 넣은 뒤, (Bits, Eq)만 derive한 구조체 Pair로 largerOf를 호출합니다. 이번에는 bsc가 무엇을 보고하고, 어디를 고쳐야 할까요?
8. 직접 작성한 Bits 인스턴스
8.1 Bits 타입클래스
Bits#(value_type, width)는 의미를 나타내는 타입 하나와 비트 폭 하나를 연결하고, 두
방향의 변환을 정의합니다.
형식
instance Bits#(타입, 폭);
function Bit#(폭) pack(타입 value);
...
endfunction
function 타입 unpack(Bit#(폭) bits);
...
endfunction
endinstancevalue_type이 정해지면 width도 정해집니다. 반대로 폭만으로는 원래 타입을 알 수 없습니다.
UInt#(16)의 폭은 16이지만, 16비트로 표현되는 타입은 그 밖에도 많습니다.
8.2 언제 직접 작성하는가
deriving (Bits)는 enum label에 0부터 차례로 인코딩을 배정하고 구조체 필드를
컴파일러가 정한 순서로 배치합니다. 이 배치를 써도 되면 deriving을 쓰고, 외부
프로토콜이나 외부 하드웨어가 다른 코드를 정해 두었다면 인스턴스를 직접 작성합니다.
| 경우 | 방법 |
|---|---|
| 인코딩이 설계 내부에서만 쓰임 | deriving (Bits) |
| 외부 규격이 코드를 고정함 | 직접 작성한 Bits 인스턴스 |
참고. Bits가 없는 타입도 유효합니다. 다만 Reg에 저장하거나 분리 합성한 모듈의
인터페이스로 내보낼 수는 없습니다.
8.3 예제: 프로토콜 인코딩
CustomBits.bsv의 Command는 Bits를 deriving하지 않고, 인스턴스 안에서 pack과
unpack을 직접 정의합니다. 두 case를 하나의 프로토콜 표의 두 방향으로 읽으세요.
Command | 기본 인코딩이었다면 | 프로토콜 코드 |
|---|---|---|
Idle | 00 | 01 |
Start | 01 | 10 |
Stop | 10 | 11 |
실행하기 전에 예측해 보세요.
pack(Stop)은 무엇을 출력할까요?unpack(2'b10)은 어느enum label일까요?
확인 문제
pack(Stop)은 무엇을 출력할까요?
package CustomBits;
// 외부 wire 프로토콜이 다른 코드를 지정하므로 이 enum은 Bits를 derive하지 않는다.
typedef enum { Idle, Start, Stop } Command deriving (Eq, FShow);
instance Bits#(Command, 2);
// pack은 의미 있는 값을 프로토콜 인코딩으로 변환한다.
function Bit#(2) pack(Command value);
case (value)
Idle: return 2'b01;
Start: return 2'b10;
Stop: return 2'b11;
endcase
endfunction
// unpack은 역방향 변환을 정의한다. 사용하지 않는 코드는 Idle로 처리한다.
function Command unpack(Bit#(2) bits);
case (bits)
2'b10: return Start;
2'b11: return Stop;
default: return Idle;
endcase
endfunction
endinstance
(* synthesize *)
module mkTop(Empty);
rule show;
Command decoded = unpack(2'b10);
$display("Stop code = %b", pack(Stop));
$display("10 means = ", fshow(decoded));
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
Stop code | 기본 인코딩 10이 아니라 프로토콜 코드 11입니다. 직접 작성한 pack이 선택되었습니다 |
10 means | unpack이 10을 Start로 디코딩합니다 |
타입 검사는 두 함수가 모두 2비트를 쓴다는 사실만 확인합니다. pack과 unpack의 표가
서로 역함수인지는 확인하지 않습니다. Command의 모든 값 c에 대해
unpack(pack(c)) == c가 성립하는지는 따로 검증해야 합니다.
직접 바꿔 보세요.
unpack(2'b10)을unpack(2'b00)으로 바꿔 보세요. 프로토콜이 쓰지 않는 코드이므로default분기가 선택되어Idle이 출력됩니다.- 쓰지 않는 코드를
Idle이 아니라 오류로 다루고 싶다면Command만으로는 부족합니다. 정상 명령과 디코딩 실패를 함께 나타내려면Maybe#(Command)같은tagged union결과 타입이 필요합니다.
9. deriving과 동형 래퍼
9.1 deriving으로 만들 수 있는 인스턴스
enum, 구조체, tagged union에 언어가 정한 표준 동작을 쓰려면 deriving으로 인스턴스를
생성합니다.
| 절 | 생성되는 동작 |
|---|---|
deriving (Bits) | 표준 packed 표현: pack, unpack |
deriving (Eq) | 구조적 비교: ==, != |
deriving (Bounded) | 구성 타입에 경곗값이 있을 때 minBound, maxBound |
deriving (FShow) | 구조를 보여 주는 fshow 출력 형식 |
여러 항목은 deriving (Bits, Eq, FShow)처럼 함께 씁니다. typedef와 deriving 문법의
실행 예제는 사용자 정의 타입 장에 있습니다.
9.2 동형 래퍼
필드가 하나인 구조체와 멤버가 하나인 tagged union은 내부 타입과 같은 정보를 담으면서도
별개의 타입입니다. 이런 타입을 동형 래퍼라고 합니다. BSV는 래퍼에 내부 타입의
인스턴스를 deriving으로 생성할 수 있습니다.
형식
typedef struct { 내부_타입 필드; } 래퍼_이름 deriving (..., Literal, Arith);Literal은 정수 리터럴을, Arith는 산술 연산을 래퍼에 그대로 제공합니다.
9.3 예제: 단위가 있는 정수
IsomorphicWrapper.bsv의 Credits는 UInt#(8) 필드 하나를 감싼 구조체입니다. 먼저
Credits 선언의 deriving 목록을 읽으세요.
실행하기 전에 예측해 보세요.
Credits first = 5;처럼 리터럴을 바로 대입할 수 있을까요?first + second의 타입과 값은 무엇일까요?
확인 문제
Credits first = 5; Credits second = 7; 일 때 first + second의 타입과 값은 무엇일까요?
package IsomorphicWrapper;
// 필드 하나인 구조체는 필드 타입과 동형이므로 호환되는 instance를 wrapper를 통해
// derive할 수 있다.
typedef struct {
UInt#(8) amount;
} Credits deriving (Bits, Eq, FShow, Literal, Arith);
(* synthesize *)
module mkTop(Empty);
rule show;
// Credits는 UInt#(8)과 구별되지만 literal과 산술 동작은 필드의 구현을 재사용한다.
Credits first = 5;
Credits second = 7;
Credits total = first + second;
$display("total = ", fshow(total));
$finish(0);
endrule
endmodule
endpackage결과 확인. 출력 줄에는 Credits 타입 이름, 필드 이름 amount, 값 12가 함께
보입니다. 두 리터럴 5와 7이 Credits로 만들어졌고, 더한 결과도 Credits 타입을
유지합니다. deriving한 Literal과 Arith가 내부 UInt#(8)의 동작을 그대로 썼기
때문입니다.
직접 바꿔 보세요. Credits first = 5;를 UInt#(8) raw = 5;와
Credits first = raw; 두 줄로 바꿔 보세요. 컴파일 오류 T0020이 발생합니다. 기대한
타입은 Credits이고 실제 타입은 UInt#(8)입니다. 리터럴과 산술은 공유하지만 두 타입은
여전히 섞어 쓸 수 없습니다.
10. 구현 방법 선택 기준
| 설계 의도 | 선택 | 부담 |
|---|---|---|
| 구현 하나를 분명한 이름으로 호출함 | 일반 함수 | 타입별 오버로딩이 없습니다 |
| 언어의 표준 표현과 의미를 그대로 씀 | deriving | 언어가 정한 동작을 따라야 합니다 |
| 여러 타입이 같은 연산 이름을 공유하되 특정 타입의 의미를 직접 정함 | 직접 작성한 인스턴스 | 왕복 변환 같은 의미 계약을 따로 검증해야 합니다 |
| 다형 본문이 특정 연산이나 폭 관계를 씀 | 최소한의 proviso | 호출자도 같은 조건을 만족해야 합니다 |
큰 타입클래스는 관계없는 연산을 한 계약에 묶어, 호출자에게 쓰지 않는 인스턴스까지 요구할
수 있습니다. 작은 타입클래스와 좁은 proviso를 쓰면 다형 함수를 더 많은 타입에 적용할 수
있습니다.
11. 자주 하는 실수
11.1 다형 함수에 proviso를 빠뜨리기
// 잘못된 코드: 본문이 priorityOf를 쓰는데 시그니처가 요구하지 않습니다
function Bit#(2) requestPriority(value_type value);
return priorityOf(value);
endfunction
// 바른 코드
function Bit#(2) requestPriority(value_type value)
provisos (Priority#(value_type));
return priorityOf(value);
endfunction// 잘못된 코드: 타입 변수에 pack을 쓸 근거가 없습니다. T0030
function Bit#(n) bitsOf(t x);
return pack(x);
endfunction
// 바른 코드
function Bit#(n) bitsOf(t x) provisos (Bits#(t, n));
return pack(x);
endfunctionT0030 오류는 proviso가 너무 일반적이라고 보고하고, 필요한 조건
Priority#(value_type) 또는 Bits#(t, n)과 그 조건이 생긴 위치를 함께 알려 줍니다.
타입 변수에 쓰는 연산은 proviso로 선언해야 합니다. 메시지가 필요한 proviso를 그대로
보여 주므로, 그 내용만 시그니처에 추가하세요.
11.2 결과 폭만 바꾸기
// 잘못된 코드: Add#(3, 4, 8)은 거짓입니다
Bit#(8) result = appendTag(3'b101, 4'ha);
// 바른 코드: 관계가 성립하도록 입력 폭을 맞춥니다
Bit#(8) result = appendTag(4'b1101, 4'ha);크기 관계 proviso에는 자동 확장이나 절단 규칙이 없습니다. 결과에 0을 채운 8비트가
필요하다면 appendTag의 결과를 Bit#(7)로 받은 뒤 zeroExtend로 넓히세요.
11.3 인스턴스가 없는 타입 넘기기
// 잘못된 코드: Priority#(UInt#(8)) 인스턴스가 없습니다
$display("%0d", requestPriority(UInt#(8)'(5)));
// 바른 코드: 그 타입이 정말 필요하면 인스턴스를 정의합니다
instance Priority#(UInt#(8));
function Bit#(2) priorityOf(UInt#(8) value) = truncate(pack(value));
endinstanceT0031 오류에 구체 타입이 보이면 함수가 아니라 그 타입의 인스턴스가 없다는 뜻입니다.
관련 없는 proviso를 다형 함수에 추가해도 해결되지 않습니다.
11.4 연산자 멤버 이름을 틀리게 쓰기
// 잘못된 코드: Eq의 멤버 이름은 \!= 가 아닙니다
function Bool \!= (Code a, Code b) = a.v != b.v;
// 바른 코드
function Bool \/= (Code a, Code b) = a.v != b.v;Eq의 부등 멤버는 \/=입니다. \!=로 쓰면 T0016 오류가 !=는 Eq의 필드가 아니라고
보고합니다. 이스케이프 식별자 뒤의 공백을 빠뜨리면 P0005 구문 오류가 발생합니다.
11.5 동형 래퍼에 내부 타입 값을 대입하기
// 잘못된 코드
UInt#(8) raw = 5;
Credits c = raw;
// 바른 코드: 구조체 생성식으로 감쌉니다
Credits c = Credits { amount: raw };래퍼는 내부 타입과 다른 타입이므로 T0020 오류가 발생합니다. 래퍼를 쓰는 목적이 바로
이런 섞어 쓰기를 막는 것입니다.
11.6 valueOf 없이 numeric type 쓰기
// 잘못된 코드: 타입을 값 자리에 썼습니다. P0005
$display("%0d", SizeOf#(Bool));
// 바른 코드
$display("%0d", valueOf(SizeOf#(Bool)));numeric type은 타입이므로 식에 쓰려면 valueOf로 Integer 값으로 바꿔야 합니다.
12. 정리
| 주제 | 핵심 |
|---|---|
| 타입클래스 | 오버로드 연산의 묶음입니다. 선언에는 멤버의 타입만 있습니다 |
| 인스턴스 | 특정 타입이 타입클래스의 멤버를 구현합니다 |
proviso | 다형 정의가 필요로 하는 인스턴스나 폭 관계를 시그니처에 적습니다. 타입 변수는 호출마다 따로 결정됩니다 |
| 인스턴스 선택 | 컴파일할 때 인자 타입으로 결정됩니다. 실행 중 선택 회로는 없습니다 |
| 크기 관계 | Add, Mul, Div, Max, Log가 폭 사이의 등식을 요구합니다 |
numeric type | SizeOf로 계산하고 valueOf로 Integer 값으로 바꿉니다 |
Bits 직접 작성 | 외부 인코딩을 정확히 표현하지만 왕복 변환은 따로 검증해야 합니다 |
| 동형 래퍼 | 별개의 타입을 유지하면서 내부 타입의 리터럴과 산술을 deriving합니다 |
| 진단 | T0031 인스턴스 없음, T0030 proviso 부족, T0035 타입 정보 부족 |
13. 확인 문제
-
Priority#(RequestKind)는 어느 단계에서 선택되며, 실행 중에 인스턴스를 고르는 회로가 생길까요?정답 보기
타입 검사와
elaboration단계에서 인자 타입RequestKind로 선택됩니다. 선택된 구현만 하드웨어로 만들어지므로 실행 중에 인스턴스를 고르는 회로는 없습니다. -
Bits#(t, n)은 연산과 폭을 어떻게 함께 제약할까요?정답 보기
t에pack과unpack을 쓸 수 있게 하고, 동시에t의 비트 표현 폭을n과 연결합니다. 그래서Bit#(n)반환 타입의 폭이 호출할 때 전달한 타입으로 결정됩니다. -
Add#(n, 4, m)은 결과 타입을Bit#(7)로 직접 적는 것과 무엇이 다를까요?정답 보기
Bit#(7)은 한 가지 폭만 허용합니다.Add#(n, 4, m)은 입력 폭n이 무엇이든 결과 폭이n + 4라는 관계를 요구하므로 여러 입력 폭에 같은 함수를 쓸 수 있습니다. -
pad0101(pair)의 결과를Bit#(8)에 담으면 어디에서 어떤 오류가 날까요?정답 보기
함수 정의가 아니라 호출 위치에서
T0031오류가 납니다.n이 2이므로Add#(2, 4, m)은m = 6을 요구하는데, 목적지는m = 8을 요구합니다. -
T0030과 구체 타입이 보이는T0031은 각각 어디를 고쳐야 할까요?정답 보기
T0030은 다형 정의가 본문에서 쓰는 연산을 요구하지 않았다는 뜻이므로 그 정의에proviso를 추가합니다. 구체 타입이 보이는T0031은 그 타입의 인스턴스가 없다는 뜻이므로 다른 타입을 쓰거나 인스턴스를 정의합니다. -
SizeOf#(T)를$display에 바로 전달할 수 없는 이유는 무엇일까요?정답 보기
SizeOf#(T)는 값이 아니라numeric type입니다.$display는 값을 받으므로valueOf로Integer값으로 바꿔서 전달해야 합니다. -
직접 작성한
Bits인스턴스에서 타입 검사가 보장하는 것과 보장하지 않는 것은 무엇일까요?정답 보기
pack과unpack이 선언한 폭을 쓴다는 사실은 보장합니다. 두 함수가 서로 역함수라는 사실, 곧unpack(pack(c)) == c는 보장하지 않으므로 따로 검증해야 합니다. -
동형 래퍼에
Literal과Arith를deriving하는 방법이 알맞은 경우는 언제일까요?정답 보기
단위를 타입으로 구분해 섞어 쓰기를 막고 싶지만, 산술의 의미는 내부 타입과 같아도 될 때입니다. 산술 의미를 바꿔야 한다면 인스턴스를 직접 작성합니다.
이 장의 실습 문제는 다형 함수, 인스턴스 선택, 폭 proviso, Bits 인스턴스 예제를 이어
갑니다. 첫 문제에서는 Bits proviso가 있는 다형 함수의 본문을 완성합니다. 두 번째
문제에서는 다형 Priority proviso가 사용할 RequestKind의 우선순위 동작을 정의해야
합니다. 세 번째 문제에서는 Add proviso가 결과 폭을 정한 연결식을 완성해야 합니다.
네 번째 문제에서는 pack이 정한 프로토콜 인코딩과 어긋나게 디코딩하는 곳이 있습니다. 각
문제에서 오류나 잘못된 출력이 인스턴스, 폭 관계, 의미 계약 중 어디에서 오는지 먼저
판단해 보세요.