1. 결과 타입이 method의 종류를 정합니다
첫 장에서는 value method 하나만 썼습니다.
인터페이스는 모듈의 상태를 바꾸는 method도 제공할 수 있고, 원형의 결과 타입이 어떤 종류인지
알려 줍니다.
| 결과 타입 | 종류 | 상태 변경 | 반환 | rule에서 호출하는 법 |
|---|---|---|---|---|
Bit#(8), Bool 등 값 타입 | value method | 없음 | 그 값 | 식이 올 수 있는 모든 곳: c.value |
Action | Action method | 가능 | 없음 | 문장으로: c.bump; |
ActionValue#(t) | ActionValue method | 가능 | t 값 | <-로: let x <- t.take; |
종류는 계약에서 고릅니다. 두 번 호출해도 아무 영향이 없어야 하면 value method입니다. 호출자가
명령을 내리는 것이면 Action method입니다. 반환하는 값과 상태 변경이 떨어질 수 없어야 하면, 즉
누구도 티켓 하나를 읽고 다른 티켓을 예약할 수 없어야 하면 ActionValue method입니다.
인터페이스 하나에 세 종류가 함께 있는 경우가 많습니다. 큐가 대표적입니다.
interface Queue;
method Action enq(Bit#(8) x); // 넣기: 상태만 바꿉니다
method ActionValue#(Bit#(8)) take; // 꺼내기: 상태를 바꾸고 값을 반환합니다
method Bool notEmpty; // 관찰: 값만 반환합니다
endinterfacetake를 value method 하나와 Action method 하나로 나누면, 읽은 값과 실제로 꺼낸 값이 같다는
보장을 호출하는 쪽이 떠맡게 됩니다.
확인 문제
어느 모듈이 번호표를 나눠 줍니다. 요청할 때마다 현재 번호를 돌려주고 카운터를 하나 올려서, 두 요청이 같은 번호를 받는 일이 없어야 합니다. request는 어떤 종류의 method입니까?
형식
// value method
method 반환_타입 이름(인자) = 식;
// Action method
method Action 이름(인자);
문장;
endmethod
// ActionValue method
method ActionValue#(t) 이름(인자);
문장;
return 식;
endmethodmethod Bit#(8) total = sum;
method Action add(Bit#(8) x);
sum <= sum + x;
endmethod
method ActionValue#(Bit#(8)) drain;
sum <= 0;
return sum;
endmethod| 정의 | 동작 |
|---|---|
total | sum의 현재 값을 반환합니다. 상태는 바뀌지 않습니다 |
add | sum의 다음 값을 예약합니다. 반환값은 없습니다 |
drain | sum을 0으로 예약하고, 같은 호출에서 갱신 전 값을 반환합니다 |
Action method 본문은 action 블록의 축약형이고, ActionValue method 본문은 actionvalue
블록의 축약형입니다. 두 블록은 다음 절들에서 소개합니다. method 정의에 다시 적은 타입은
인터페이스 선언의 타입과 정확히 같아야 합니다.
주의. drain의 return sum은 0이 아니라 갱신 전 값을 반환합니다. <=는 레지스터의 다음
값을 예약할 뿐이고, 같은 호출 안에서 레지스터를 읽으면 언제나 호출이 시작될 때의 값이 보입니다.
2. action … endaction으로 작업 기술하기
타입이 Action인 식은 작업 그 자체가 아니라 수행할 작업의 기술입니다. action 블록은 레지스터
쓰기와 Action method 호출 같은 기본 Action을 원자적인 Action 하나로 묶습니다.
형식
action
문장;
문장;
endactionAction clearBoth = action
left <= 0;
right <= 0;
endaction;
rule clear (step == 1);
clearBoth;
endrule| 코드 | 일어나는 일 |
|---|---|
Action clearBoth = action ... endaction; | 쓰기 두 개의 기술에 이름을 붙입니다. 아직 아무것도 지워지지 않습니다 |
rule 안의 clearBoth; | Action을 실행합니다. 두 쓰기가 같은 클록 에지에 반영됩니다 |
noAction은 아무 일도 하지 않는 내장 Action입니다. Action이 필요한 위치에서 상태를 바꾸지
않아야 할 때 사용합니다.
원자적 Action의 규칙은 앞 장의 규칙이며, 이제 모든 method 호출에도 그대로 적용됩니다.
- Action 안의 레지스터 읽기는 모두
rule이 시작될 때의 값을 봅니다. - 쓰기는 모두 클록 에지에서 함께 반영됩니다. 같은 Action 안에서 앞서 쓴 값을 뒤 문장이 읽는 일은 없습니다.
- Action에서 호출한 method의 준비 조건은
rule의implicit condition에 더해집니다. method가 준비되지 않으면 명시적 조건과 관계없이rule은 실행되지 않습니다(9절).
3. rule에서 호출한 method는 그 실행의 일부입니다
rule이 c.bump를 호출하면 bump 안의 쓰기는 rule 자신의 쓰기와 같은 에지에서 반영되고,
method 안의 모든 읽기는 사이클 시작 상태를 봅니다. method 본문은 먼저 실행되는 절차가 아닙니다.
rule의 전이 하나에 더해지는 회로입니다.
ActionMethod.bsv의 Counter에는 상태를 바꾸는 bump와 상태를 읽는 value가 있습니다.
rule drive는 step < 3인 동안 매 사이클 c.value를 출력한 뒤 c.bump를 호출합니다.
rule done은 step이 3일 때 최종 값을 출력합니다. drive 안에서 출력문이 c.bump보다 먼저
적혀 있다는 점에 주목하세요. 이 순서가 결과에 영향을 주는지가 이 예제의 질문입니다.
실행하기 전에 예측해 보세요. drive가 세 번 실행되는 동안 출력되는 value는 각각
얼마일까요? done이 출력하는 최종 값은 얼마일까요?
확인 문제
drive의 첫 실행에서 value는 무엇을 출력합니까?
package ActionMethod;
// Action method는 상태를 변경하고 아무 값도 반환하지 않는다.
interface Counter;
method Action bump;
method Bit#(8) value;
endinterface
module mkCounter(Counter);
Reg#(Bit#(8)) count <- mkReg(0);
method Action bump;
count <= count + 1;
endmethod
method Bit#(8) value = count;
endmodule
(* synthesize *)
module mkTop(Empty);
Counter c <- mkCounter;
Reg#(Bit#(4)) step <- mkReg(0);
rule drive (step < 3);
// value method는 rule이 시작될 때의 상태를 읽는다. 따라서 bump가 반영되기 전의
// 값을 출력한다.
$display("step %0d: value = %0d", step, c.value);
c.bump;
step <= step + 1;
endrule
rule done (step == 3);
$display("final value = %0d", c.value);
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인. 사이클마다 상태를 추적하면 다음과 같습니다.
| 사이클 | 시작할 때 count | 실행되는 rule | value가 읽는 값 | 에지 뒤 count |
|---|---|---|---|---|
| 0 | 0 | drive | 0 | 1 |
| 1 | 1 | drive | 1 | 2 |
| 2 | 2 | drive | 2 | 3 |
| 3 | 3 | done | 3 | 3 |
step 줄 세 개는 0, 1, 2를 보여 줍니다. drive 안의 c.value는 같은 rule의 c.bump가
예약한 쓰기를 보지 못하고 사이클이 시작될 때의 상태를 읽습니다. 마지막 줄의 3은 세 번째 쓰기가
반영된 다음 사이클에 done이 읽은 값입니다.
직접 바꿔 보세요.
확인 문제
drive에서 $display 줄을 c.bump; 아래로 옮기면 무엇이 바뀝니까?
이어서 bump의 count + 1을 count + 2로 바꿔 보세요. step 줄은 0, 2, 4를, 마지막 줄은 6을
출력합니다.
4. actionvalue와 <-
ActionValue#(t)는 원자적 Action이면서 타입 t의 값을 반환합니다. actionvalue 블록 안에서는
return으로 결과를 지정합니다.
형식
actionvalue
문장;
return 식;
endactionvalue
타입 이름 <- actionvalue_호출;
let 이름 <- actionvalue_호출;method ActionValue#(UInt#(8)) issue;
next <= next + 1;
return next;
endmethod
let ticket <- dispenser.issue;<-는 오른쪽 action을 실행하고 그 결과 값을 이름에 결합합니다. 모듈 인스턴스화에 쓰는
연산자와 같습니다. 모듈 인스턴스화도 모듈 계산을 실행해 결과 인터페이스를 받는 동작이기
때문입니다. =를 쓰면 실행되지 않은 식 자체가 결합됩니다. 예를 들어
UInt#(8) ticket = dispenser.issue;는 기대 타입 UInt#(8), 추론된 타입
ActionValue#(UInt#(8))이라는 T0020 오류가 됩니다.
5. ActionValue는 진입 값을 반환하고 다음 값을 예약합니다
ActionValueMethod.bsv의 take는 다음 티켓 번호를 예약하면서 현재 번호를 반환합니다. 한
호출에서 next를 반환하고 next + 1을 씁니다. 호출하는 쪽 drive는 결과를
let issued <- t.take;로 받습니다.
실행하기 전에 예측해 보세요. 호출마다 "지금 반환되는 값"과 "다음 사이클에 next에 보이는
값"을 한 쌍으로 적어 보세요.
확인 문제
next는 1에서 시작합니다. 첫 번째로 발급되는 값은 무엇입니까?
package ActionValueMethod;
// ActionValue method는 상태를 변경하고 값도 반환한다.
interface Ticket;
method ActionValue#(Bit#(8)) take;
endinterface
module mkTicket(Ticket);
Reg#(Bit#(8)) next <- mkReg(1);
method ActionValue#(Bit#(8)) take;
next <= next + 1;
return next;
endmethod
endmodule
(* synthesize *)
module mkTop(Empty);
Ticket t <- mkTicket;
Reg#(Bit#(4)) step <- mkReg(0);
rule drive (step < 3);
// ActionValue method의 결과는 <-로 받는다. 모듈 인스턴스화와 같은 연산자다.
let issued <- t.take;
$display("issued %0d", issued);
step <= step + 1;
endrule
rule done (step == 3);
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인.
| 호출 | 반환값 | 다음 사이클의 next |
|---|---|---|
| 1번째 | 1 | 2 |
| 2번째 | 2 | 3 |
| 3번째 | 3 | 4 |
issued 줄 세 개가 1, 2, 3이면 take가 갱신 전 값을 반환한다는 것이 확인됩니다. 반환과 예약이
한 method 호출 안에 있으므로 8비트 카운터가 한 바퀴 돌기 전에는 두 클라이언트가 같은 번호를
받지 않습니다. 호출이 256번 받아들여지면 번호가 다시 반복됩니다.
직접 바꿔 보세요.
| 바꿀 곳 | 예상 결과 |
|---|---|
mkReg(1)을 mkReg(5)로 | issued 줄이 5, 6, 7이 됩니다 |
let issued <- t.take;를 Bit#(8) issued = t.take;로 | 컴파일 오류 T0020이 발생합니다. 기대한 타입은 Bit#(8)이고 실제 타입은 ActionValue#(Bit#(8))입니다 |
주의. Bit#(8) issued = t.take;가 타입 오류인 이유는 =가 실행되지 않은 동작을 결합하고,
그 타입이 Bit#(8)이 아니라 ActionValue#(Bit#(8))이기 때문입니다. <-만 동작을 실행합니다.
6. 예제: 현재 번호표 발급
ActionValueTicket.bsv의 발급기는 issue method가 번호를 반환하면서 카운터를 증가시킵니다.
이번에는 actionvalue … endactionvalue 블록을 직접 적었습니다. TicketDispenser 인터페이스,
next 레지스터와 issue method, issueTwo와 그 안의 if 순서로 읽으세요.
초기 상태는 next = 40, issued = 0입니다.
| 실행 | 시작 상태 (next, issued) | 반환값 | 예약된 다음 상태 |
|---|---|---|---|
| 첫 번째 | (40, 0) | 40 | (41, 1) |
| 두 번째 | (41, 1) | 41 | (42, 2)와 $finish |
두 번째 실행의 if (issued == 1)도 시작 시점의 값을 읽으므로, 두 번째 번호를 출력한 직후
시뮬레이션이 끝납니다.
실행하기 전에 예측해 보세요. return next;는 바로 위 줄의 next <= next + 1;이 예약한 값을
볼까요?
확인 문제
어떤 번호 두 개가 어떤 순서로 출력됩니까?
package ActionValueTicket;
interface TicketDispenser;
method ActionValue#(UInt#(8)) issue;
endinterface
module mkTicketDispenser(TicketDispenser);
Reg#(UInt#(8)) next <- mkReg(40);
method ActionValue#(UInt#(8)) issue;
actionvalue
next <= next + 1;
// 반환값은 예약된 레지스터 갱신이 반영되기 전에 읽힌다.
return next;
endactionvalue
endmethod
endmodule
(* synthesize *)
module mkTop(Empty);
TicketDispenser dispenser <- mkTicketDispenser;
Reg#(UInt#(2)) issued <- mkReg(0);
rule issueTwo (issued < 2);
let ticket <- dispenser.issue;
$display("ticket=%0d", ticket);
issued <= issued + 1;
if (issued == 1) $finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인.
| 출력 줄 | 확인되는 사실 |
|---|---|
첫 ticket이 40 | return next가 예약된 41이 아니라 시작 상태를 읽었습니다 |
두 번째 ticket이 41 | 예약된 쓰기가 두 번째 실행 전의 에지에서 반영되었습니다 |
반환값과 예약된 다음 상태는 같은 원자적 Action에 속하지만 서로 다른 시점의 값입니다.
직접 바꿔 보세요.
- 계약을 "증가한 뒤 그 번호를 보고"로 바꾸어 보세요. 쓰기는 더하지 않고
return next;만return next + 1;로 바꿉니다. 출력은 41과 42가 되고 레지스터는 여전히 40, 41, 42로 변합니다. 반환 계약만 바뀌었습니다. return next;를next <= next + 1;위로 옮긴다고 생각해 보세요. 결과는 같습니다. Action 안의 문장 순서는 읽기가 보는 값을 바꾸지 않습니다.
7. 모듈 안의 rule
rule은 모듈의 내부 동작을 기술합니다. 이름, 선택적인 명시 조건, action 본문으로 이루어지며,
본문에서 모듈이 가진 하위 인스턴스의 method를 호출할 수 있습니다.
형식
rule 이름 (명시_조건);
action_문장;
endrulerule drain (step == 3 && stack.notEmpty);
let value <- stack.pop;
$display("%0d", value);
endrule| 부분 | 의미 |
|---|---|
drain | 모듈 안에서 고유한 rule 이름입니다 |
(step == 3 && stack.notEmpty) | 명시 조건입니다. 생략하면 항상 참입니다 |
stack.pop | 호출한 method입니다. pop의 implicit condition이 활성화 조건에 더해집니다 |
rule이 실행되면 본문의 모든 상태 갱신이 하나의 원자적 전이로 반영됩니다. 호출이 여러 하위
모듈의 상태를 바꾸더라도 일부만 반영된 상태는 관찰되지 않습니다. 여러 rule이 같은 상태를
갱신할 때 필요한 중재와 멀티플렉서 회로는 bsc가 생성합니다.
8. 타입 매개변수가 있는 인터페이스와 하위 인터페이스
8.1 타입 매개변수가 있는 인터페이스
인터페이스 이름 뒤에 #(type 이름)을 붙이면 원소 타입을 고정하지 않은 인터페이스를 만들 수
있습니다.
형식
interface 인터페이스_이름#(type 타입_변수);
...
endinterfaceinterface Holder#(type t);
method Action put(t x);
method t get;
endinterface
Holder#(Bit#(8)) small; // 원소가 Bit#(8)인 인터페이스 타입
Holder#(Bool) flag; // 원소가 Bool인, 위와 다른 인터페이스 타입인스턴스마다 매개변수에 구체적인 타입 하나가 정해집니다. Holder#(Bit#(8))과 Holder#(Bool)은
서로 다른 타입입니다.
참고. 매개변수가 크기를 나타내는데 사용 형태만으로 그 사실을 알 수 없으면
numeric type depth처럼 numeric type으로 선언합니다.
8.2 하위 인터페이스와 Empty
인터페이스 안에 다른 인터페이스를 멤버로 둘 수 있습니다. 멤버 앞에 interface를 쓰고,
클라이언트는 점 표기로 접근합니다.
interface Channel;
interface Holder#(Bit#(8)) data;
method Action clear;
endinterface
// 사용하는 쪽
ch.data.put(8'h2a);작은 인터페이스를 조합해 큰 인터페이스를 만들면 같은 계약을 여러 모듈에서 재사용할 수
있습니다. Clock과 Reset도 인터페이스 타입이므로 하위 인터페이스로 둘 수 있습니다.
프렐류드의 Empty는 method가 하나도 없는 인터페이스입니다. 외부에 아무 기능도 제공하지 않는
최상위 모듈은 module mkTop(Empty);처럼 Empty를 제공합니다.
9. method는 거절할 수 있습니다: implicit condition
method 정의의 헤더에 if (조건)을 붙일 수 있습니다. 이 조건이 method의 준비 조건이고,
implicit condition이라고도 합니다. 조건이 거짓인 사이클에 method는 준비되지 않고, 그 method를
호출하는 rule은 자기 guard가 무엇이든 전혀 실행되지 않습니다. 호출만 건너뛰고 나머지를
실행하는 일은 없습니다. 실행 전체가 보류됩니다.
형식
method Action push(Bit#(8) x) if (depth < 2);
stored <= x;
depth <= depth + 1;
endmethod호출하는 rule의 guard와 그 rule이 호출하는 모든 method의 준비 조건은 합쳐집니다. 모두 참일 때만
rule이 준비됩니다. 준비 조건은 불변식이 있는 곳인 method에 둡니다. 그러면 모든 호출자가 검사를
반복하지 않고도 보호됩니다.
주의. method 본문 안의 if는 다른 것입니다. push 안에 if (depth < 2) begin … end를 쓰면
method는 항상 준비되어 있고, 호출하는 rule은 계속 실행되며 쓰기만 건너뜁니다. 준비 조건은
헤더에 있습니다.
앞 장은 rule이 준비된 것과 실행된 것을 구분했습니다. method는 rule이 멈춘 듯 보일 때 확인할
세 번째 사실을 더합니다. 그 rule이 호출하는 모든 method가 준비되어 있는가? 디버깅 순서는 호출한
method의 준비 조건, 그 다음 rule 자신의 guard, 그 다음 다른 rule과의 충돌입니다. 어느 단계에서
실패해도 밖에서는 같아 보입니다. 아무것도 바뀌지 않습니다.
다음 장에서는
자기 guard가 없는 rule이 push의 준비 조건 때문에 멈추는 예제를 실행하고, 준비 조건이 참조할
수 있는 이름의 규칙을 다룹니다.
10. 적용: 호출 한 번에 전이 하나
실습 문제가 네 개입니다. 채점 문제에서 bump는 count <= ?;를 쓰고 카운터는 임의의 값으로
끝납니다. don't-care를 현재 count에서 유도한 다음 값으로 바꿔 호출 한 번이 정확히 한 번의
증가가 되게 하세요. 두 번째 문제의 step(clear, enable)은 두 명령을 Bool 인자로 받는
Action method 하나입니다. clear가 이기고 enable은 세고 둘 다 없으면 유지하도록 본문을
쓰세요. 설계 장이 요구사항에서 이 본문을
이끌어 냅니다. 나머지 두 문제에서 take와 issue는 각각 next + 1을 반환해 발급되는 모든
티켓이 하나씩 앞서 있습니다. 쓰기는 그대로 두고 반환 식만 진입 값으로 바꾸세요.
11. 자주 하는 실수
11.1 ActionValue method의 결과를 =로 받기
// 잘못된 코드: 결과 값이 아니라 action 자체를 받습니다
Bit#(8) issued = t.take;
// 바른 코드
Bit#(8) issued <- t.take;=는 오른쪽 식을 실행하지 않고 그대로 이름에 결합합니다. 타입을 적었다면 T0020 오류가
기대한 타입 Bit#(8)과 실제 타입 ActionValue#(Bit#(8))을 함께 보여 줍니다. 이 오류는 Action의
기술과 실행 결과를 혼동했다는 신호입니다. let issued = t.take;처럼 타입을 생략하면 issued가
action 자체가 되어 오류가 늦게 나타납니다. 이 값을 issued + 1처럼 계산에 쓰면
Arith#(ActionValue#(Bit#(8))) 인스턴스가 없다는 T0031 오류가 발생합니다.
11.2 현재 값 대신 예약된 값을 반환하기
// "발급한 뒤 증가" 계약에서는 잘못된 코드: 다음 번호를 반환합니다
next <= next + 1;
return next + 1;
// 바른 코드: 시작 상태를 반환합니다
next <= next + 1;
return next;두 코드 모두 컴파일됩니다. 반환값이 늘 한 단계 앞선다면 return 식과 레지스터의 시작 상태를
비교해 보세요.
11.3 pop에서 상태 일부만 갱신하기
// 잘못된 코드: depth만 줄입니다
method ActionValue#(t) pop if (depth > 0);
depth <= depth - 1;
return items[0];
endmethod
// 바른 코드: 저장된 원소도 함께 옮깁니다
method ActionValue#(t) pop if (depth > 0);
items <= shiftInAtN(items, ?);
depth <= depth - 1;
return items[0];
endmethod잘못된 코드도 타입 검사를 통과합니다. 같은 원소가 계속 반환되는 증상이 보이면 method가
갱신해야 할 상태를 모두 갱신하는지 확인하세요. 이 pop이 속한 스택은
다음 장의 종합
예제입니다.
12. 정리
| 요소 | 뜻 |
|---|---|
value method | 읽기. 식 안에서 호출. 상태를 바꾸지 않음 |
Action method | 상태 변경. rule 안에서 문장으로 호출 |
ActionValue#(t) method | 상태를 바꾸고 t를 반환. <-로 호출 |
action … endaction | 원자적 작업. =로 결합해도 실행되지 않음. noAction은 아무 일도 하지 않음 |
actionvalue … endactionvalue | 원자적 작업과 시작 상태로 계산한 결과. <-로 실행 |
<- | 모듈 계산이나 action을 실행하고 결과를 결합. =는 식을 실행하지 않고 결합 |
| 호출 안 | 읽기는 진입 상태를 봄. 쓰기는 rule의 쓰기와 함께 반영 |
method 헤더의 if (조건) | 준비 조건. 거짓이면 호출한 rule이 실행되지 않음 |
| 준비와 실행 | method 준비 조건, rule의 guard, 충돌은 세 가지 별개의 검사 |
인터페이스#(type t), 하위 인터페이스, Empty | 사용할 때마다 정해지는 원소 타입. 점 표기로 접근하는 중첩 인터페이스. method가 없는 인터페이스 |
13. 확인 문제
-
큐에서 항목을 제거하면서 그 항목을 반환해야 합니다. 어떤 method 종류가 알맞을까요?
정답 보기
ActionValue method입니다. 반환과 제거가 한 호출 안에 있어야 제거한 값과 반환한 값이 언제나 같습니다. -
한
rule안에서c.bump;다음 줄에c.value를 읽으면 갱신된 값을 볼 수 있을까요?정답 보기
볼 수 없습니다. 한
rule의 모든 호출은 레지스터를 사이클 시작 시점의 값으로 읽습니다.bump가 예약한 값은 다음 사이클에 보입니다. -
Action clearBoth = action ... endaction;이 어떤 레지스터도 지우지 않는 이유는 무엇일까요?정답 보기
=는 작업의 기술에 이름을 붙일 뿐입니다. 쓰기는rule본문 같은 Action 문맥에서clearBoth를 문장으로 실행할 때 일어납니다. -
ActionValue method가next <= next + 1을 쓰고next를 반환합니다. 호출한 쪽은 어떤 값을 받을까요?정답 보기
실행이 시작될 때의 값을 받습니다. 쓰기는 클록 에지에서 반영되므로 증가한 값은 다음 실행에서야 보입니다.
다음 장은 method의 준비 조건을 호출자까지 따라가고, 모듈 정의에 매개변수와 인자를 더하며, 합성 경계에서 인터페이스가 지켜야 할 조건을 정합니다.