명령을 넷으로 가르고, 두 번 반박당했다

명령을 넷으로 가르고, 두 번 반박당했다

오픈소스 RTL 시뮬레이터 만들기 · 3편 — 4개 명령의 구조

A single bright beam of light entering from the left and splitting into four diverging glowing paths that continue to th

지난 편에서 소스가 파형이 되기까지 일곱 번 모양이 바뀐다고 썼다. 그건 도구 안쪽 이야기였다.

이번엔 바깥쪽이다 — 그래서 사람이 실제로 무엇을 치는가.

명령은 넷이고, 그 배치는 처음부터 계획된 것이었다. 이 글은 그 계획이 만든 사람에게 두 번 반박당한 기록이다.

명령은 처음부터 넷이었다

vitamin에는 명령이 넷 있다.

text

vita    한 번에 전부
vcmp    컴파일만
velab   엘라보레이트만
vrun    시뮬레이션만

이 배치는 나중에 편해서 생긴 게 아니다. 처음부터 넷이었다. 단계별로 하나씩 짓고 그것을 묶는 것이 목적이었으니, 묶는 쪽(vita)도 처음부터 설계에 있었다. 3단을 만들어놓고 불편해서 원샷을 얹은 게 아니라는 뜻이다.

계획은 그랬다.

하나의 실행 파일이 네 개인 척한다

넷은 각각 다른 바이너리가 아니다. 실행 파일은 하나이고, 자기가 어떤 이름으로 불렸는지를 보고 역할을 정한다.

argv[0]의 베이스네임을 읽어 어떤 애플릿인지 고르고, 이름이 vita면 명시적 서브커맨드 형태도 받는다. 그래서 아래 두 줄은 같은 일을 한다.

Bash

vcmp counter.sv --work work        # vcmp라는 이름으로 불렸을 때
vita vcmp counter.sv --work work   # vita가 서브커맨드를 받았을 때

install.sh가 하는 일도 vita 옆에 나머지 셋의 심볼릭 링크를 거는 것뿐이다(파일시스템이 링크를 거부하면 복사로 폴백한다). 배포할 실행 파일은 하나이고, 링크가 없는 환경에서도 서브커맨드 형태로 전부 쓸 수 있다.

xmvlog를 쓰던 손이 그대로 움직이게

이 구성이 어디서 왔는지는 숨길 것이 없다. 나는 Xcelium을 쓰던 사람이었고, 손에 익은 흐름을 그대로 만들고 싶었다.

하는 일 Cadence Synopsys vitamin
컴파일 xmvlog vlogan vcmp
엘라보레이트 xmelab vcs velab
시뮬레이션 xmsim simv vrun
한 번에 xrun vita

다만 흐름만 가져왔고 기능은 덜어냈다. Xcelium에는 훨씬 많은 것이 있다. 스케매틱 뷰어, 코드 추적, 그 밖의 도구들이 붙어 있고 vitamin에는 그것들이 전부 없다. 못 만든 게 아니라 안 만들기로 한 것이다. 경량 시뮬레이터를 지향했으니 그 표면은 처음부터 목표 바깥이었다.

베낀 자리보다 안 베낀 자리가 설계 판단을 더 잘 보여준다. 그리고 이 글 뒤쪽에, 의도적으로 안 베끼겠다고 문서에 적어놓고 결국 가져온 것이 하나 나온다.

단계마다 끝에 같은 줄이 찍힌다

단계를 가른 실용적 이유는 단순하다. 어디까지 통과했는지 각 단계가 스스로 답한다. 셋을 따로 돌리면 이렇게 된다.

text

$ vita vcmp 000_counter.sv --work work
errors=0 warnings=0 notes=0
→ work/units/cu_0ac662b75d1112512fd19fc13faad14b.vu

$ vita velab -L work --top tb
errors=0 warnings=0 notes=0
→ tb.velab (1,074 B)

$ vita vrun tb.velab
t=16  cnt=1 (0x1)
…
done: final cnt=12
simulation ended (Finish) at time 126
errors=0 warnings=0 notes=0

원샷 vita 000_counter.sv의 출력과 같다. 두 경로는 편의가 다르지 그 답이 달라서는 안 된다.

여기서 눈여겨볼 것은 중간 산출물의 이름이다. cu_0ac662b7….vu — 파일 이름 자체가 내용 해시다. 이게 뒤에 나올 이야기의 복선이다.

“이 실행은 대체 뭘 먹었나”에 로그가 답하게

-v를 붙이면 트랜스크립트 맨 위에 실제로 적용된 호출이 찍힌다.

text

$ vita -v 000_counter.sv

invocation: vita -v 000_counter.sv
cwd:        /tmp/vita_run
sources:    000_counter.sv
threads:    8 (auto)

지금은 이 넷(호출·작업 디렉터리·실제로 컴파일된 소스 목록·스레드 수와 그 출처)이고, -vv는 자리만 잡아둔 상태다.

이 기능은 사고가 나서 붙인 게 아니다. 엔지니어로 일하면서 필요하다고 느껴 미리 넣었다. 파일 리스트를 만들 수 있으면 좋기 때문이다.

RTL을 규모 있게 다루면 소스는 filelist로 쪼개져 있고, 특히 합성 흐름으로 넘길 때는 목록만으로 부족하다. 각각에 대해 타입 같은 것들을 따로 세팅해줘야 한다. 그때 “이 실행이 실제로 무엇을 먹었나”를 도구가 스스로 답해주면 그 목록을 거기서 만들 수 있다.

시뮬레이터가 자기 입력을 말하게 하는 건 시뮬레이션을 위한 기능이 아니라 그 다음 공정을 위한 기능이다.

재사용이 편하려면 쉬워야 하고, 건전하려면 의심해야 한다

단계를 가르면 곧바로 문제가 하나 생긴다. 앞 두 단계의 결과가 디스크에 남으니 vrun만 다시 돌리고 싶어진다. 그런데 그 사이에 소스가 바뀌었다면?

낡은 스냅샷을 그대로 시뮬레이션하면 최악의 사고가 난다. 에러 없이, 지금 소스와 무관한 결과가 나온다. 지난 편에서 이 프로젝트가 가장 무서워하는 게 조용히 틀리는 것이라고 썼는데, 여기가 정확히 그 자리다.

그래서 설계 문서에 규칙을 하나 박아뒀다.

RULE V (vrun 건전성) — vrun은 매 실행마다 상류 체인 전체를 라이브 소스에 대해 재검증한다. 소비한 각 단위의 전처리-소스 다이제스트를 디스크에서 다시 계산해 스냅샷에 박힌 값과 대조한다. 하나라도 다르면 vrun은 stale 스냅샷을 시뮬레이션하지 않고 실패한다. mtime은 절대 쓰지 않는다 — 내용 해시만이 건전한 신선도 신호다. (Xcelium -R/-r의 검사-생략 패스트패스는 의도적으로 복제하지 않는다.)

문장이 단호하다. 파일이 언제 만져졌는지는 내용이 바뀌었다는 증거가 못 되니까 — 되돌린 편집, 건드리기만 한 파일, 복사로 옮겨온 트리에서 mtime은 거짓말을 한다.

그리고 앞 절의 cu_0ac662b7….vu가 이 원칙의 눈에 보이는 형태다. 산출물이 자기 내용의 해시로 이름 붙어 있다.

첫 번째 펀치 — 그 원칙을 내가 깎았다

문제는 이 규칙이 정직한 만큼 비쌌다는 것이다.

시뮬레이션을 돌릴 때마다 소비한 소스를 전부 다시 읽고 다시 해싱한다. 사소한 수정 하나에도 그 값을 매번 치른다. 프로젝트가 커질수록 그 비용이 쌓였고, 후반부로 갈수록 개발 속도가 눈에 띄게 느려졌다.

2026년 6월, 결국 mtime 패스트패스를 넣었다. 안 베끼겠다고 문서에 적어둔 바로 그 최적화를. 다만 그냥 넣지는 않았다.

graphviz diagram

📊 다이어그램 요약: velab이 각 경로의 내용을 기록된 해시와 대조해 확인한 뒤에야 (mtime, size)를 스탬프로 남기고, vrun은 stat 결과가 그 스탬프와 일치할 때만 읽기와 해싱을 건너뛴다. 어긋나거나 스탬프가 없으면 원래대로 전부 다시 해싱한다.

핵심은 스탬프를 언제 찍느냐다. 파일을 쓰는 시점에 mtime을 아무렇게나 캡처해두면 앞 단계와 뒤 단계 사이의 창이 다시 열린다 — 그 사이에 바뀐 파일이 검증받지 않은 채 스탬프를 얻는다. 그래서 내용이 기록된 해시와 같다는 걸 확인한 뒤에만 스탬프를 찍는다. 그러면 스탬프의 mtime이 그 내용에 묶인다.

적용 범위도 좁혔다. work 라이브러리 모드에서만 동작하고, 경로를 명시하는 방식에는 애초에 재해싱이 없어 해당이 없다. 스탬프가 어긋나거나 없으면(옛 산출물이면) 예전처럼 전부 다시 해싱한다.

그리고 남은 구멍을 문서에 적었다. mtime에는 표준적으로 알려진 허점이 있다 — 파일시스템 타임스탬프 해상도보다 짧은 간격의 변경, mtime을 고정한 채 같은 길이로 덮어쓰는 경우. 이런 건 이 패스트패스를 통과한다. 없앤 척하지 않고 아는 만큼 적어뒀다.

대가도 있다. 이 (mtime, size) 8바이트 때문에 .velab은 서로 다른 컴파일 실행 사이에 바이트 단위로 재현되지 않는다. 이 프로젝트는 “리눅스와 맥에서 바이트까지 같은 출력”을 계약으로 내거는데, 여기에 명세된 예외가 하나 생긴 것이다. 그 이야기는 결정성을 다루는 편에서 따로 하겠다.

그래서 이걸 “원칙을 어겼다”고 쓰고 싶지는 않다. 실제로 한 일은 금지를 조건부 예외로 좁히고, 그 예외의 경계와 비용을 문서에 남긴 것이다. 다만 문장이 무뎌진 건 사실이다. “절대 쓰지 않는다”였던 것이 “이러이러한 조건에서만 믿는다”가 됐다.

기록이 기억과 조금 다르다. 내 기억은 위에 쓴 대로 “재검증 비용 때문에 개발이 느려졌다”인데, 로드맵에 남은 형태는 결이 조금 다르다. 그 항목은 18건짜리 성능 백로그에 “매 실행마다 전체 소스를 다시 읽고 다시 해싱함(패스트패스 없음)” 이라는 한 줄로, 우선순위 낮음으로 올라가 있었고 순서가 돌아왔을 때 처리됐다. 어느 쪽이 더 정확한 서술인지 지금은 단정하지 못한다. 다만 어긋난다는 사실 자체를 지우지 않는 편이 맞다고 생각해서 남긴다.

두 번째 펀치 — 만들어놓고 내가 안 쓴다

여기까지가 설계 이야기다. 이제 사용 이야기를 해야 한다.

나는 사실상 vita만 쓴다.

원래 의도는 3단 흐름을 각각 독립적으로 구현하고 그것을 한 번에 실행한다는 개념이었는데, 어느 순간부터 그렇지 않게 됐다. 단계를 나눠 하나씩 확인하는 대신 원샷 한 줄을 친다.

이 문장은 보통 이런 글에서 지워진다. 도구를 만든 사람이 자기가 만든 표면을 안 쓴다는 건 좋게 들리지 않으니까. 그런데 지난 편에서 나는 두 경로에 대해 이렇게 말했다.

둘 중 하나만이 진짜라는 건 없다고 생각한다. 둘 다 진짜다. 틀리지 않아. 그저 조금 다를 뿐이다. 그리고 지금도 합칠 생각은 없다.

이 둘은 모순이 아니다. 앞의 것은 유효성에 대한 말이고, 뒤의 것은 일상 사용에 대한 말이다. 합쳐서 읽으면 결론이 하나 나온다.

단계별 경로는 매일 쓰라고 있는 게 아니라, 필요한 그 순간에 있으라고 있다.

그리고 안 쓰면서도 안 없앤다는 점에서 “합칠 생각 없다”는 앞 편의 결론은 오히려 더 단단해진다. 쓰지 않는 것을 유지하는 데는 이유가 필요하고, 여기서 그 이유는 셋이다. 단계가 갈려 있어야 각 단계가 독립적인 모듈로 기능하고, 어디서 깨졌는지를 단계가 스스로 답하고, 무엇보다 도구를 처음 보는 사람에게 중간에 무슨 일이 벌어지는지 보여줄 수 있다.

마지막 이유는 이 글 자체이기도 하다.

그래도 넷이다

정리하면 이렇다.

  • 계획 — 처음부터 명령 넷. 단계별로 짓고 묶는다.
  • 맞은 것 ① — “mtime은 절대 쓰지 않는다”고 적어놓고, 개발이 느려져서 조건을 붙여 되가져왔다.
  • 맞은 것 ② — 단계별 경로를 만들어놓고 정작 나는 원샷만 쓴다.

누구나 계획은 있다. 맞기 전까지는.

— 원래 마이크 타이슨의 말이다.

그런데 맞은 뒤에 남은 것을 보면, 계획이 틀렸다기보다 계획이 맞으면서 형태가 바뀌었다는 쪽에 가깝다. 명령은 여전히 넷이고, mtime은 조건부로만 들어갔고, 그 조건과 대가는 문서에 적혀 있다. 안 쓰는 경로도 지우지 않았다. 버리지 않고 깎았다. 지금도 그러고 있다.

다음 편 예고 — 단계를 가르면 “이 산출물이 아직 쓸 만한가”를 판단해야 한다. 이 글에서는 그 판단을 mtime으로 하지 않는다고 했다가 조건부로 열었다. 판단의 전체 그림 — 어떤 플래그가 어느 단계의 출력을 교란하는지로 분류하는 방식 — 은 결정성과 산출물을 다루는 편에서 이어간다.

📚 시리즈← 2편 · 소스에서 파형까지, 일곱 번 모양이 바뀐다 · 시리즈 목차

🔗 코드github.com/tjddnr0912/vitamin-rtl-simulator (MIT / Apache-2.0 듀얼 라이선스). 본문의 실행 기록은 저장소의 examples/000_counter.sv를 릴리즈 빌드로 실제로 돌린 결과 그대로다.

개발 중인 개인 프로젝트의 기록입니다. 상용 검증 도구를 대체하지 않으며, 본문의 수치와 구현 범위는 집필 시점(2026-08-25 · 0.1.0 · format_version 29 · 테스트 5,987 green) 기준으로 이후 변경됩니다. 이 프로젝트는 AI와 협업해 개발하고 있습니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤