

파일을 읽는 데서 멈추지 않고, 그 정보를 바탕으로 실제로 한 건을 판정하는 단계까지 나아갔어요.
Rust로 test262를 공부하고 있어요. 1주차에는 테스트 파일 하나를 읽고 frontmatter metadata와 body를 분리하는 분석기를 만들었고, 2주차에는 그 metadata를 실제 실행 흐름과 연결하는 쪽으로 한 걸음 더 나아갔어요.
핵심은 거창한 JavaScript 엔진을 만드는 게 아니었어요. 목표는 아주 작게 잘라서, run <path> 명령 하나로 테스트 한 건을 skip / pass / fail 중 어디에 놓을지 판정할 수 있게 만드는 것이었어요.
cargo run -- run <path> 형태의 최소 runner를 붙였다negative.phase == parse | runtime를 읽어 pass/fail을 나누게 했다includes, module, feature-gated case 등은 보수적으로 skip 처리했다1주차를 마치면 당연히 "이제 JavaScript를 실행해보자"는 쪽으로 마음이 가요. 저도 그랬어요. 그런데 막상 코드를 보면, 실행기 이전에 먼저 정리해야 할 게 하나 있었어요.
test262는 단순히 JavaScript 코드 조각만 들어 있는 게 아니라, 이 테스트를 어떤 조건에서 어떻게 해석해야 하는지도 함께 담고 있어요. 어떤 테스트는 parse 단계에서 실패해야 정상이고, 어떤 테스트는 runtime error가 나야 정상이에요. harness helper를 로드해야 하는 것도 있고, 아예 지금 단계에서는 건너뛰는 편이 맞는 것도 있어요.
그래서 2주차에서는 evaluator를 정교하게 만드는 것보다, metadata를 실제 판정 흐름과 연결하는 작은 runner를 만드는 것에 집중했어요.
run <path> 명령 추가negative.phase == parse | runtime 초안 처리includes, module, feature-gated case에 대한 skip 정책 정리학습 프로젝트에서는 여전히 어디까지 안 할지를 먼저 정하는 게 중요하다는 걸 다시 느꼈어요.
전체 흐름은 아주 작아요.
negative.phase == parse인지 확인한다negative.phase == runtime인지 확인한다Passed / Failed / Skipped 중 하나를 반환한다이 흐름이 들어가면서 각 모듈의 책임 경계가 명확해졌어요.
이렇게 잘라두니 다음 주에 무엇을 어디에 붙여야 할지도 더 잘 보였어요.
현재 parser는 정말 작아요. ; 기준으로 statement를 나누고, literal과 identifier 정도만 다뤄요.
지금 다룰 수 있는 범위는 대략 이 정도예요.
1;
true;
null;
missingValue;여기서 중요한 건, parser가 정교한 JavaScript parser가 아니라는 점이에요. runner와 연결하기 위한 최소 AST 생성기에 더 가까워요.
evaluator도 마찬가지예요. literal은 바로 Value로 바꾸지만, identifier는 아직 lookup을 하지 않아요. 그래서 missingValue; 같은 코드는 일부러 runtime error로 남겨두고 있어요.
처음에는 덜 만든 코드처럼 보였는데, 오히려 좋았어요. 이 placeholder 덕분에 negative.phase == runtime 테스트를 아주 작은 범위에서도 바로 연결해볼 수 있었거든요.
가장 의미 있었던 부분은, 실패를 하나로 뭉개지 않고 단계별로 나눠서 보게 된 점이었어요.
| 조건 | 결과 |
|---|---|
| 지원 범위 밖인 경우 | Skipped |
| parse 단계에서 실패해야 정상인 경우 | Passed |
| runtime error가 나야 정상인 경우 | Passed |
| 정상적으로 평가되는 경우 | Passed |
이렇게 나누고 나니, "실패했다"는 말 하나 안에도 전혀 다른 의미가 들어 있다는 게 선명하게 느껴졌어요.
test262는 이런 구분을 metadata 수준에서 이미 제공하고 있기 때문에, 이를 무시하고 곧바로 evaluator를 키우는 것보다 판정 정책을 먼저 세우는 쪽이 훨씬 자연스러웠어요.
의외로 parser보다 runner 쪽에서 더 많이 배웠어요.
보통 엔진 공부라고 하면 AST나 evaluator 구현에 먼저 눈이 가는데, test262와 함께 보면 오히려 무엇을 지원하지 않는지, 어떤 실패를 정상으로 볼지, 어떤 테스트를 보수적으로 건너뛸지 같은 정책이 굉장히 중요해요.
또 하나 흥미로웠던 건 Expr와 Value를 분리해 둔 구조였어요.
Expr — 아직 계산되지 않은 문법 구조Value — 계산이 끝난 런타임 결과예를 들어 Identifier는 지금 당장은 값이 아니라 이름이에요. evaluator가 나중에 environment에서 lookup해야 비로소 Value가 돼요. 이 차이를 코드에서 분리해 두니, 3주차에 lexical environment를 도입해야 하는 이유가 훨씬 명확해졌어요.
마무리하면서 몇 가지를 같이 정리했어요.
negative.type이 이미 있어서 기대하는 에러 타입을 결과 설명에 드러내도록 다듬었어요. 아직 엄격 비교까지는 아니지만, 적어도 어떤 기대를 가지고 판정했는지는 보이게 했어요.includes, module뿐 아니라 feature-gated case, async/generator 흔적이 보이는 경우도 2주차 범위에서는 우선 skip하는 쪽으로 가져갔어요.구현량은 크게 늘지 않았지만, 지금 어디까지를 지원한다고 말할 수 있는지를 훨씬 분명하게 만들어줬어요.
아래 명령들로 2주차 흐름을 확인할 수 있어요.
cargo run -- run fixtures/sample-test262/week2-pass.js
cargo run -- run fixtures/sample-test262/week2-runtime-error.js
cargo run -- run fixtures/sample-test262/week2-parse-error.js
cargo run -- run fixtures/sample-test262/week2-skip.js
cargo test각각 정상 평가, runtime error 기대, parse error 기대, 범위 밖 skip 케이스를 한 건씩 확인할 수 있어요. 작지만, 이 네 개만으로도 현재 러너의 판정 철학이 꽤 잘 드러나요.
자연스럽게 lexical environment 쪽으로 넘어가게 될 것 같아요.
현재 identifier는 무조건 runtime error예요. 바로 그 지점 때문에 다음 주 목표가 더 선명해졌어요.
let x = 1; x; 같은 흐름을 실제로 통과시키기2주차가 판정 흐름을 붙인 주였다면, 3주차는 이름을 저장하고 다시 찾는 가장 작은 런타임 모델을 붙이는 주가 될 거예요.
겉보기에는 작아요. 아직 binary expression도 없고, 변수 선언도 없고, 함수 호출은 당연히 없어요. 그런데 오히려 그래서 더 좋았어요.
엔진 공부를 할 때 자꾸 evaluator나 parser부터 크게 만들고 싶어지는데, test262와 같이 가다 보니 먼저 필요한 건 실행 정책과 판정 흐름을 설명할 수 있는 구조라는 걸 배웠어요.
1주차가 파일을 읽는 주 였다면, 2주차는 그 정보를 바탕으로 이 테스트를 지금 어떻게 다룰 것인지 결정하는 주 였어요. 덕분에 다음 단계도 훨씬 선명해졌어요.
다음 주에는 여기서 한 걸음 더 나아가, identifier가 더 이상 무조건 실패하지 않도록 작은 environment를 붙여보려고 해요.