# Deno 4차 오픈소스 외주 후기



## 계약 조건

방학이 2주 남아서 일단 2주 동안 저번과 같은 조건으로 작업하기로 했다. 근데 작업 목록이 좀 많았다. 2주 안에 과연 되려나 싶었지만 해보니까 되더라.

## 계약서의 작업 목록

- [deno_lint][]에 스코프 분석 추가

어렵지 않을 거라 생각했다.

- [deno_lint][]에 `no-unused-vars` lint 구현

[deno][]에 직접 구현하는 것이었다. [swc][]를 통해 [deno][]에 간접적으로 많은 기여를 했지만 직접 기여한 적은 없었다.
코드 읽기 어려울까봐 시간을 좀 길게 잡기는 했는데, 해보니까 별 거 아니었다.

- [deno_lint][]에 Control flow 분석 추가

러스트로 타입스크립트 타입 엔진 만들 때 비슷한 짓을 해봤기에 그리 어렵지 않을 것이라 봤다. 물론 쉬운 일은 아니었지만 한번 해봤으니 괜찮겠지 이런 생각이었다.

- [swc][]에 하이레벨 API 추가

구체적으로는 [deno][]의 `cli/swc_util.rs`를 swc 레포지토리로 옮기면 좋겠다고 얘기했다.

- [swc][]에 의존석 분석용 라이브러리 추가

이건 [swc][]에서 제공하려는 api에서도 필요한 것이었다.

- [swc][] 오류 메시지 개선

[swc][]의 에러 메시지는 안 그래도 언젠가 개선해야되는데 하고 있었던 부분이다. 나로서는 매우 잘된 었다.

- [swc][] 성능 개선

이건 원래도 꾸준히 하고 있었다.

## 실제 작업

### (deno_lint) unused-vars (타입스크립트 지원 X)

3시간 정도 걸려서 호이스팅이나 variable shadowing 지원하는 린터를 만들었다. 쉽네 이러면서 다음 것 구상하고 있었는데 함정이 있었다.

[deno_lint][]는 eslint를 포팅하는 프로젝트인데, eslint 테스트 보니까 겁나게 복잡하고, 변수만 잡는 것도 아니었다. 함수, 파라미터 다 처리하게 바꿨는데, 하나 고칠 때마다 테스트를 컴파일하다보니 너무 시간이 오래 걸렸다. 절대 길게 끌 일이 아니었는데 컴파일 8시간이나 걸렸다.

[Pull request](https://github.com/denoland/deno_lint/pull/267)

---

이거 만들 때 매우 재밌는 방식을 사용했다. [swc][]의 `resolver` 패스를 거치고 나면 모든 식별자(Identifier)가 유일해진다. 무슨 말이나면, 아래의 코드에서

```js
let a = 1;
{
  let a = 2;
}
```

두개의 a를 구분할 수 있게 된다는 것이다. 물론 최종 사용자한테는 보이지 않는다. 이는 내가 만든 알고리즘인데, span의 hygiene 정보를 사용해서 유일성을 만들어내므로 span hygiene라고 아름 붙였다.

이 특성을 이용해서 `unused-vars` 린터를 아래 코드처럼 nested scope가 없는 형태로 구현했다.

```rust
struct Collector {
  used_vars: HashSet<Id>,
}
```

`Id`는 유일하므로, 이걸로 충분했다. 이름이 `a`인 변수가 많아도`resolver`가 처리하므로 상관 없었다.

### (swc) type-resolver

위에서는 유일성을 이용해서 빠르게 끝냈는데, 그 유일성을 만들어주는 컴파일러 패스인 `resolver`가 타입을 지원하지 않았다.
그래서 resolver에 타입 시스템 지원을 추가했는데, 한 5시간 정도 걸렸던 것 같다.
각 ast 노드를 처리하는 규칙은 정해져있었고, 그에 맞게 구현만 하면 되는거라 금방 된 것 같다.

### (swc) 에러 메시지 개선

기간을 길게 부르긴 했는데, 오류 메시지 바꾸는 건 오래 걸릴 이유가 없었다.

첫번째로 `unexpected` 호출하는 코드 주변 보고 무슨 토큰을 처리할 수 있는지 살펴보고 그 토큰 목록을 인자로 넘겨주었다. 그런 다음 토큰의 `Debug` 구현을 사용자가 알아볼 수 있게 고쳤다.

그리고 마지막으로 에러 메시지를 다 추가했는데,

```
error: TS1009
 --> $DIR/tests/test262-parser/fail/0053737b6145994c.js:1:6
  |
1 | var x, ;
```

를

```
error: Trailing comma is not allowed
 --> $DIR/tests/test262-parser/fail/0053737b6145994c.js:1:6
  |
1 | var x, ;
  |      ^
```

처럼 바꿨다. 사실 내가 한 건 `fomrat!("{:?}", kind)` 를 `SyntaxError::TS1109 => "Trailing comma is not allowed".into()` 처럼 바꿔주는 것 뿐이다.
참고로 기존에 에러 메시지가 이상했던 건 에러 메시지 적기 귀찮아서 대충 기본 `Debug`구현으로 때웠기 때문이다.

### (swc) 성능 개선

당장 할 수 있는 작업 중에서 딱히 땡기는 게 없어서 이걸 골랐다.

계약서상에 적혀있는 이슈는 [swc][] 자체보단 `spack`에 관한 이슈들이었는데, `spack` 성능 문제의 원인은 3가지였다.

- 모든 모듈 소스 코드가 여러 번 복사된다
- 병렬 처리가 제대로 안 되어있다.
- LocalMarker이 너무 자주 호출된다.

셋 다 빠르게 짜고 천천히 패치하겠단 생각으로 위해 넘어갔던 것들이라 어떻게 하면 빨라지는지 알고 있얶다. 참고로 [swc][]는 변수에 표시를 해서 다른 모듈에 같은 이름의 무언가가 있어도 충돌하지 않고 자연스럽게 합쳐지도록 한다. (span hygiene가 그것이다). 그 과정에서 쓰이는 게 `LocalMarker` 였는데, 의존성 라이브러리를 병합할 때마다 호출됐기에 성능 문제가 있었다.

우선 `LocalMarker` 사용 횟수를 줄이기로 했다. `LocalMarker`을 모듈당 1번씩만 실행되게 바꿨는데, common js import와 es6 import는 문제가 없었고, cyclic import 쪽에 버그가 생겼다.

---

그런데 맥북 키가 또 빠졌다. 이번에도 커맨드 키... 수리 받으러 가서 한참 기다렸다.
이동 시간까지 합해서 3시간 넘게 걸렸던 것 같다.

---

맥북 맡기고 집에 와서 데스크탑으로 열심히 작업해서 `swc_bundler`의 병렬 처리를 거의 끝냈는데, 실제로 모듈을 병합하기 전에 모든 의존성을 검사해서 계획을 짠 뒤 그 계획에 맞춰 병렬 처리를 하도록 변경했다. 역시 이번에도 cyclic import에 문제가 생겼다.

그래서 코드를 살펴보니까 cyclic import 감지 코드에 문제가 있었다. 그 부분은 변수 하나 더 써서 고쳤다.

남은 건 clone 없애는 거였는데, 간단해 보였다. Arc만 잘 관리하면 되는 문제라고 생각인데, 프로파일링을 해보니 파서하고 hygiene이 문제고 모듈 데이터 clone하는 건 잡히지도 않길래 넘어갔다.

### (deno_lint) unused_vars 완성

이것저것 다른 이슈 작업하다가 [deno_lint][]가 사용하는 [swc][]의 버전이 업데이트했다는 말을 들었다. 그래서 다시 작업에 들어갔다.
cargo 의 patch 기능을 이용해서 `ts-resolver`와 `unused-vars`를 동시에 작업했다.

오래 안 걸릴 일이었는데 컴파일 시간 때문에 오래 걸렸다.

### (deno_lint) Scope 분석

Scope analysis / Control flow analysis가 필요한 일부 린트 빼고 작업이 끝났고, Scope 분석기를 만들기 위해 어떤 린트를 스코프 기반으로 만들 건지 물어봤다.

- no-class-assign
- no-const-assign
- no-ex-assign
- no-func-assign
- no-global-assign
- no-import-assign
- no-undef
- no-redeclare

`no-class-assign`, `no-const-assign`, `no-ex-assign`, `no-func-assign`, `no-import-assign` 은 한 종류로 볼 수 있다.
Scope에 바인딩의 종류를 제공하는 API를 만들면 해결될 문제들이니까 별 거 없을 것이라 같았는데, `no-undef`, `no-global-assign`,의 경우 `None`이나 비어있는 `&[BindingKind]`을 반환하면 될 것이라 생각했다.

하나씩 생각 해보면서 상단의 lint rule들을 다 구현할 수 있다는 걸 확인한 뒤에 작업을 마무리했다. 논리적으로 증명한 뒤 방법을 pr에 적어놓았다. 그리고 짧은 코드였지만 (아주 작은) 테스트도 넣었다.

### (deno lint) Control flow 분석

완전한 Control flow 분석기를 만들 생각은 없었고, 그럴 필요도 없었다. 그래서 이번에도 이 분석기로 무슨 rule을 만들건지 물어봤다.

- `no-unreachable`
- `no-fallthrough`

이건 좀 막막했다. 구현해본적은 있는데 내가 만들었던 건 2만줄 넘는 완전한 분석기였다. 그래서 한참 고민했다.
생각만 하면 심심하니까 겸사겸사 [swc][]의 성능을 최적화했다.

중간에 그냥 그래프 써서 쉽게 갈까 싶은 순간도 있었는데, 그냥 머리 쓰기로 했다.
머리 쓴다고 해봐야 거창한 건 아니고 이 구문이 나왔을 때 이 변수가 어떻고 저 변수가 어떻고 하는 수준이긴 하지만 효과가 있었다.

---

생각해보면 스코프의 종류가 다양하고 구문 실행이 끝나는 이유도 다양한 상황이었다.

예를 들어

```js
while (true) return;
```

```js
do return;
while (true);
```

이 둘은 같은 코드를 이용해서 분석할 수 없다.
그래서 각 루프가 무한히 반복하는 경우데 대한 코드를 각 반복문의 visitor로 옮겼다.

또한, break로 끝난 것과 return으로 끝난 건 완전히 다르다.
그래서 `Done` 이라는 enum을 만들고, 그 상태를 추적했다. 예를 들어, `switch` 처리가 끝났을 때 끝난 이유가 break이면 done을 초기화시키고, return이면 아래의 줄을 unreachable로 표시하는 방법을 사용했다.

그리고 `no-reachable` 린트도 직접 구현했다.
계약 조건상으로 보면 내가 할 일은 아니었지만, 내가 작성한 게 스코프를 제대로 분석하는지 알려면 뭔가 테스트가 필요했고, 검증할 겸 구현했다.

### (swc) 성능 개선

deno 쪽에서 가져다 쓰는 타입스크립트 제거 pass의 설계가 잘못된 상태였다.

기존 설계에서는 아래 코드의 `a`를 2^4회 방문한다. 이를 모든 노드를 한 번만 방문하도록 바꾸니까 5배 빨라졌다.

```js
{
  {
    {
      a;
    }
  }
}
```

### (deno_lint) no-unreachable

[Pull request](https://github.com/denoland/deno_lint/pull/288)

너무 간단해서 찜찜했었는데 [deno][]의 std에 돌려보니까 에러가 잔뜩 나와서 고쳤다.
4시간 정도 걸렸는데 첫 컴파일에 1시간 반 정도를 썼다.

규칙이 엄청 복잡했다. 특정 구문은 `return`을 상속하고, `break`는 또 어디 있냐에 따라서 처리 규칙이 확연히 달라지고...
근데 사실 프로그래밍 해보면 이 정도는 별 것 아니다. 난 컴파일 기다리는 게 제일 어렵다.
[swc][]를 만들 정도로 기다리는 걸 싫어한다.

### (swc) High-level api

디노 팀 멤버가 더 좋은 추상화를 찾았다고 PR 보내겠다고해서 그건 천천히 하기로 하고 계약을 끝냈다.

### (swc) 의존성 분석

간단하게 만들어봤는데 위의 디노 팀 멤버가 더 좋은 방식으로 만들어서 PR 보내줬다.

---

[swc]: https://swc-project.github.io
[deno]: https://deno.land
[deno_lint]: https://github.com/denoland/deno_lint

