보조 변수
TLA+의 속성(property)은 유연성이 크지만 한계도 있다. 이를테면 “P는 Q가 참이 될 때까지 참이고, 그 뒤로는 P가 거짓이어도 된다”처럼 속성을 “잊을” 수는 없다. 이를 “~P => Q”로 쓴다고 해 보자. 그러면 ~P로 만든 다음 ~Q로 만들 경우, 이미 “잊었어야” 할 속성인데도 불변식(invariant)이 실패한다.
새 변수 Q_was_true를 추가한 다음 속성을 이렇게 쓰면 이 문제를 그럭저럭 고칠 수 있다.
Prop == P => Q \/ Q_was_true
Next ==
/\ \* regular spec
/\ IF Q' THEN Q_was_true' ELSE UNCHANGED Q_was_true
Q_was_true를 보조 변수(auxiliary variable, 줄여서 “aux var”)라고 한다. 스펙에 보조 변수를 덧붙이면 더 다양한 시스템을 표현하고 더 다양한 속성을 검사할 수 있다. 썩 우아한 해법은 아니지만 제 몫은 해낸다.
노트
다른 정형 기법(formal methods) 분야에서는 보조 변수를 “고스트(ghost)” 변수나 “헬퍼(helper)” 변수라고 부르기도 한다.
보조 변수를 쓰는 방법은 아주 다양하다. 여기서는 그중 몇 가지만 소개한다!
보조 변수의 종류
히스토리 변수
이미 일어난 일을 나타내는 변수다. Q_was_true가 바로 히스토리 변수(history variable)다. 히스토리 변수가 한 번 설정된 뒤로는 바뀌지 않는다는 것을 확인하고 싶다면, 이를 액션 속성(action property)으로 만들면 된다:
Prop ==
[][Q_was_true => UNCHANGED Q_was_true]_Q_was_true
히스토리 변수로 시스템에 더는 남아 있지 않은 과거 정보를 추적할 수도 있다. 예를 들어 클라이언트가 데이터베이스에 질의하는데, 요청을 처리하는 도중에 데이터베이스가 갱신될 수 있다고 해 보자. 이때 aux_client_request_value라는 변수를 추가해 클라이언트가 요청을 보내는 즉시 갱신되게 하면, 데이터베이스 값이 갱신되더라도 요청 시점에 그 값이 무엇이었는지에 대한 정보를 잃지 않는다.
팁
경험적으로 말하자면, 스펙의 행동(behavior)은 히스토리 변수에 의존하면 안 된다. 의존한다면 그 정보는 지금 만들고 있는 기계가 접근할 수 있는 상태 정보라는 뜻이므로, 보조 변수에서 진짜 변수로 격상해야 한다.
에러 변수
either 분기가 있는 PlusCal 스펙을 작성한다고 해 보자:
either
\* path 1
or
\* path 2
or
\* ...
에러 트레이스(error trace)에서는 어느 분기를 탔는지 알아보기 어려울 수 있다. 상태 변화를 보고 추론해야 하기 때문이다. 이를 피하려고 레이블(label)을 잔뜩 붙이는 사람들도 있다:
either
Path1:
\* ...
or
Path2:
\* ...
or
\* ...
그러면 pc를 보고 어느 경로를 탔는지 알 수 있다. 하지만 이렇게 하면 스펙에 여분의 동시성(concurrency)이 잔뜩 더해진다 — 잘해야 상태 공간(state space)이 폭발하고, 최악의 경우에는 스펙의 의미론(semantics)이 바뀐다!
우리가 원하는 것은 스펙의 의미론을 바꾸지 않으면서 에러 트레이스를 풍부하게 만드는 것이다. 보조 변수가 활약하기 딱 좋은 자리다.
either
aux_branch := "Path1";
\* ...
or
aux_branch := "Path2";
\* ...
or
\* ...
또 다른 흔한 용도는 언제 무슨 일이 일어났는지 이력 로그를 남기는 것이다:
\E w \in Workers:
/\ \/ Action1(w)
\/ Action2(w)
\/ Action3(w)
/\ aux_log' = Append(aux_log, w)
참고
- ALIAS
상태에서 곧바로 무언가를 계산하고 싶을 뿐이라면.
경계 변수
경계 변수(bounding variable)는 이미 우리의 reader_writer 스펙에서 하나 본 적이 있다. 어떤 프로세스도 큐에 끝없이 쓰게 두지 않고, 항상 최대 N개의 메시지만 쓰게 했다. 끝없이 쓸 수 있다면 경계 없는(unbound) 상태 공간이 되어 버리기 때문이다!
내가 즐겨 쓰는 경계 변수 활용법 하나는 시스템에 작은 오류를 끼워 넣는 것이다. 메시지 유실을 모델링하고 싶다면 이렇게 쓴다.
either
queue := Append(queue, msg);
or
await aux_drops_left > 0;
aux_drops_left := aux_drops_left - 1;
end either;
(유한 상태 기계 장에 either await 패턴의 동작 방식이 설명되어 있다.)
그러면 유실이 전혀 없는 경우나 딱 한 번만 유실되는 경우로 시스템을 테스트할 수 있다. 시스템이 메시지를 하나도 빠짐없이 전부 유실할 수는 없다.
예언 변수
예언 변수(prophecy variable)는 미래에 일어날 일을 미리 정해 둔다. 사실상 비결정성(nondeterminism)을 스펙의 더 앞쪽으로 끌어당기는 방법이다. 예를 들어 계산기 스펙에서 나는 숫자를 비결정적으로 더하는 것을 이렇게 표현했다:
Digits == 0..9
(* --algorithm calculator
variables
i = 0;
sum = 0;
begin
Calculator:
while i < NumInputs do
with x \in Digits do
\* Add
sum := sum + x;
end with;
i := i + 1;
end while;
초기 상태는 하나뿐이지만, 루프를 한 번 돌 때마다 각 상태가 10갈래로 갈라진다. 대신 이렇게 쓸 수도 있다:
variables
i = 1;
sum = 0;
aux_proph_digits \in [1..NumInputs -> Digits];
begin
Calculator:
while i <= NumInputs do
sum := sum + aux_proph_digits[i];
i := i + 1;
end while;
이제 초기 상태는 더 많아졌지만, 각 초기 상태에서 가능한 행동은 하나뿐이다.
예언 값은 꽤 드물게 쓰이는 편이다. 주로 정제(refinement)를 만드는 데 쓰인다.
사용 시 참고 사항
보조 변수를 쓸 때는 어떤 것이 시스템의 일부인 “기계” 변수이고 어떤 것이 시스템의 일부가 아닌 보조 변수인지 분명히 해야 한다. 구현할 수 없는 것이라면 스펙이 거기에 의존해서는 안 된다!
순수 TLA+에서는 보조 변수를 UNCHANGED 문에도 넣어야 해서 다루기가 번거로울 수 있다. 시퀀스(sequence)를 활용하면 도움이 된다.