- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
338 lines
11 KiB
Markdown
338 lines
11 KiB
Markdown
# tsgo (typescript-go) Reference Analysis for Five
|
|
|
|
> microsoft/typescript-go 프로젝트에서 배운 교훈과 Five에 적용한 내용
|
|
>
|
|
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
> All rights reserved.
|
|
|
|
---
|
|
|
|
## 1. tsgo 프로젝트 개요
|
|
|
|
| 항목 | 내용 |
|
|
|------|------|
|
|
| 저장소 | https://github.com/microsoft/typescript-go |
|
|
| 목적 | TypeScript 컴파일러를 Go로 재작성 |
|
|
| 성과 | **10배 빠른 컴파일**, 메모리 2.9배 절감 |
|
|
| 핵심 선택 | Go를 선택한 이유: 코드 구조가 1:1 매핑 가능, GC 제어력, 병렬화 용이 |
|
|
|
|
---
|
|
|
|
## 2. 핵심 교훈 7가지
|
|
|
|
### 교훈 1: GC와 싸우지 마라, 설계로 맞춰라
|
|
|
|
```
|
|
tsgo 접근:
|
|
- 배치 컴파일: GC를 아예 실행하지 않아도 됨 (프로세스 종료 = 정리)
|
|
- 서버 모드: AST는 장수명, "논리적 GC 시점"을 도메인 지식으로 판단
|
|
- Arena 할당자를 사용하지 않음 — 관용적 Go 코드로 충분
|
|
|
|
Five에 적용:
|
|
✅ ptrStore(글로벌 map+mutex) 제거 → Value.ptr(unsafe.Pointer) GC 직접 추적
|
|
✅ 스칼라 타입은 힙 할당 없음 (Value struct에 인라인)
|
|
→ 향후: DBF 배치 스캔 시 GOGC=off 옵션 제공 가능
|
|
```
|
|
|
|
### 교훈 2: Value Semantics가 최대 승리
|
|
|
|
```
|
|
tsgo 수치:
|
|
- JS: 배열의 각 요소가 별도 힙 객체 (N+1 할당)
|
|
- Go: 구조체 배열 = 단일 연속 할당 (1 할당)
|
|
- boolean: JS 8+ bytes → Go 1 byte
|
|
→ 이것만으로 메모리 2.9배 절감의 주 원인
|
|
|
|
Five에 적용:
|
|
✅ Value = 24바이트 struct (Harbour HB_ITEM 32바이트 대비 25% 절감)
|
|
✅ []Value = 연속 메모리 (캐시 효율)
|
|
✅ 스칼라 값은 복사 전달 (포인터 추적 없음)
|
|
→ 1000개 Value 배열: 24KB (Harbour: 32KB)
|
|
```
|
|
|
|
### 교훈 3: 핫 타입(30%)만 풀링하면 충분
|
|
|
|
```
|
|
tsgo 접근:
|
|
- Identifier 노드가 전체 AST의 ~30% 차지
|
|
- 256개씩 청크 할당하여 개별 힙 할당 대폭 감소
|
|
- 나머지 70%는 일반 Go 할당 사용
|
|
- 모든 것을 풀링하지 않음 — ROI가 없음
|
|
|
|
Five에 적용 (향후):
|
|
→ DBF 스캔 시 반복 생성되는 String Value를 sync.Pool로 풀링
|
|
→ FOR/NEXT 루프의 임시 Value를 재사용
|
|
→ 전체가 아닌 프로파일링으로 확인된 핫 경로만 최적화
|
|
```
|
|
|
|
### 교훈 4: 불변성으로 병렬화
|
|
|
|
```
|
|
tsgo 접근:
|
|
- 파싱 결과 AST = 불변 (immutable)
|
|
- 4개 타입 체커가 같은 AST를 동시 읽기 (락 없음)
|
|
- 각 체커는 자기만의 mutable 상태 보유
|
|
- Link Store: AST 수정 없이 노드별 메타데이터 부착
|
|
|
|
Five에 적용:
|
|
✅ Thread별 독립 Stack/Locals (goroutine-local, 락 없음)
|
|
✅ 심볼 테이블은 공유 읽기 전용 (sync.RWMutex)
|
|
→ 향후: 파싱된 AST 불변화, 여러 goroutine에서 병렬 코드 생성
|
|
→ 향후: DBF 읽기 전용 모드에서 여러 goroutine 동시 스캔 (lock-free)
|
|
```
|
|
|
|
### 교훈 5: UTF-8이 공짜 메모리 절감
|
|
|
|
```
|
|
tsgo 수치:
|
|
- JS UTF-16 → Go UTF-8: 문자열 메모리 50% 절감
|
|
- 서브스트링이 원본 메모리를 공유 (복사 없음)
|
|
- 파서가 소스에서 식별자 추출 시 거의 할당 없음
|
|
|
|
Five에 적용:
|
|
✅ Go string = UTF-8 (Harbour의 바이트 문자열보다 유니코드 지원 우수)
|
|
✅ HbString.Data = Go string (immutable, 서브스트링 공유 가능)
|
|
→ 향후 파서: 소스 코드 서브스트링으로 토큰 추출 (할당 최소화)
|
|
```
|
|
|
|
### 교훈 6: 구조적 유사성이 이론적 성능보다 중요
|
|
|
|
```
|
|
tsgo 선택:
|
|
- Rust/C++가 이론적으로 더 빠를 수 있었음
|
|
- 하지만 Go를 선택: TypeScript 코드와 1:1 구조 매핑이 가능
|
|
- 유지보수성 + 포팅 용이성 > 극한 최적화
|
|
- "최고의 메모리 관리 언어도 코드를 재작성해야 하면 쓸모없다"
|
|
|
|
Five에 적용:
|
|
✅ gencc.c 패턴을 Go로 1:1 매핑 (hb_xvm* → Thread 메서드)
|
|
✅ Harbour RDD 가상 함수 테이블 → Go interface (구조 유사)
|
|
✅ Harbour HB_ITEM → Value (필드 의미 보존, 크기만 축소)
|
|
→ 기존 Harbour 소스를 읽으면서 Go 코드를 작성할 수 있음
|
|
```
|
|
|
|
### 교훈 7: 조기 off-heap 트릭을 피하라
|
|
|
|
```
|
|
tsgo:
|
|
- CockroachDB처럼 C.malloc으로 off-heap 이동하지 않음
|
|
- Go GC 범위 안에서 해결
|
|
- 작업 세트가 Go GC 모델에 잘 맞으므로 불필요
|
|
|
|
CockroachDB (대조):
|
|
- 수십 GB 블록 캐시 → C.malloc으로 off-heap
|
|
- GC 스캔 오버헤드가 심각한 경우에만 정당화
|
|
|
|
Five에 적용:
|
|
✅ unsafe.Pointer만 사용, C 할당/mmap은 사용하지 않음
|
|
→ 향후: DBF mmap은 Go의 syscall.Mmap 사용 (GC와 무관한 파일 매핑)
|
|
→ 성능 병목이 확인되기 전까지 Go GC 범위 안에서 해결
|
|
```
|
|
|
|
---
|
|
|
|
## 3. tsgo 아키텍처 패턴과 Five 매핑
|
|
|
|
### 3.1 노드 표현: Kind + Interface
|
|
|
|
```
|
|
tsgo:
|
|
Node struct { Kind SyntaxKind, data nodeData(interface) }
|
|
→ Kind로 빠른 switch 디스패치
|
|
→ data interface로 다형성
|
|
→ 외부 구현 방지 (unexported method)
|
|
|
|
Five:
|
|
Value struct { scalar uint64, info uint64, ptr unsafe.Pointer }
|
|
→ info 상위 8비트로 빠른 타입 체크
|
|
→ scalar로 스칼라 값 직접 접근
|
|
→ ptr로 포인터 타입 접근
|
|
→ 동일 원리: 판별자(discriminant) + 데이터
|
|
```
|
|
|
|
### 3.2 팩토리 패턴
|
|
|
|
```
|
|
tsgo:
|
|
NodeFactory가 모든 AST 노드 생성을 중앙화
|
|
→ 풀링, 캐싱, 통계 수집의 단일 지점
|
|
→ factory.NewIdentifier(), factory.NewBinaryExpression()
|
|
|
|
Five (향후 적용):
|
|
ValueFactory가 빈번한 Value 생성을 최적화
|
|
→ MakeString(""): 빈 문자열 싱글턴
|
|
→ MakeInt(0), MakeInt(1): 자주 쓰는 정수 캐싱
|
|
→ sync.Pool for 임시 HbString 재사용
|
|
```
|
|
|
|
### 3.3 CheckerPool → ThreadPool
|
|
|
|
```
|
|
tsgo:
|
|
- 4개 타입 체커를 병렬 실행
|
|
- 각 체커가 자기만의 캐시/상태 보유
|
|
- 불변 AST를 공유 읽기
|
|
- WorkGroup으로 작업 분배
|
|
|
|
Five (향후 적용):
|
|
- goroutine pool로 병렬 DBF 스캔
|
|
- 각 goroutine이 자기만의 Thread 보유
|
|
- 불변 인덱스를 공유 읽기 (RWMutex)
|
|
- channel로 결과 수집
|
|
```
|
|
|
|
### 3.4 Link Store → Thread-local State
|
|
|
|
```
|
|
tsgo:
|
|
- AST를 수정하지 않고 노드별 메타데이터를 별도 저장
|
|
- 여러 체커가 같은 AST에 다른 메타데이터 부착 가능
|
|
|
|
Five:
|
|
- WorkArea를 수정하지 않고 Thread별 커서 위치/필터를 별도 관리
|
|
- 여러 goroutine이 같은 DBF 파일에 다른 필터/커서 보유
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Five Value 리팩터링 결과
|
|
|
|
### 변경 전 (ptrStore 방식)
|
|
|
|
```go
|
|
// Value = 16 bytes
|
|
type Value struct {
|
|
data uint64 // scalar OR uintptr (GC 불가!)
|
|
info uint64
|
|
}
|
|
|
|
// 글로벌 포인터 저장소 (GC와 싸우는 안티패턴)
|
|
var ptrStore = &pointerStore{
|
|
items: make(map[uintptr]interface{}), // 메모리 누수!
|
|
}
|
|
|
|
func MakeString(s string) Value {
|
|
hs := &HbString{Data: s}
|
|
ptrStore.keep(hs) // 글로벌 mutex 잠금!
|
|
return Value{
|
|
data: uint64(uintptr(unsafe.Pointer(hs))), // GC가 추적 불가
|
|
info: makeInfo(tString, 0, uint32(len(s))),
|
|
}
|
|
}
|
|
```
|
|
|
|
**문제:**
|
|
- `ptrStore.mu.Lock()` — 모든 문자열/배열 생성 시 글로벌 mutex 경합
|
|
- `map[uintptr]interface{}` — 해제 시점 불명, 사실상 메모리 누수
|
|
- GC가 `uintptr`을 추적할 수 없어 조기 수거 위험
|
|
|
|
### 변경 후 (tsgo 방식)
|
|
|
|
```go
|
|
// Value = 24 bytes (Harbour 32B 대비 25% 절감)
|
|
type Value struct {
|
|
scalar uint64 // numeric/date/bool raw bits
|
|
info uint64 // type tag + metadata
|
|
ptr unsafe.Pointer // GC-traced pointer (nil for scalars)
|
|
}
|
|
|
|
func MakeString(s string) Value {
|
|
hs := &HbString{Data: s}
|
|
return Value{
|
|
info: makeInfo(tString, 0, uint32(len(s))),
|
|
ptr: unsafe.Pointer(hs), // GC가 직접 추적!
|
|
}
|
|
}
|
|
```
|
|
|
|
**개선:**
|
|
- 글로벌 mutex 제거 → 무잠금 (lock-free)
|
|
- 메모리 누수 제거 → GC가 자연스럽게 수거
|
|
- `unsafe.Pointer` 필드는 Go GC가 직접 스캔
|
|
|
|
### 벤치마크 비교
|
|
|
|
| 연산 | 16B+ptrStore | 24B+GC-safe | 차이 |
|
|
|------|-------------|------------|------|
|
|
| MakeInt | 4.9ns, 0 alloc | 5.0ns, 0 alloc | 동일 |
|
|
| AddInt | 11.8ns, 0 alloc | 11.7ns, 0 alloc | 동일 |
|
|
| TypeCheck | 0.11ns | 0.11ns | 동일 |
|
|
|
|
스칼라 연산은 성능 동일 (ptr 필드가 nil이므로 GC 스캔 비용 없음).
|
|
|
|
---
|
|
|
|
## 5. 추가 참조 프로젝트
|
|
|
|
### esbuild (Evan Wallace)
|
|
|
|
| 항목 | 내용 |
|
|
|------|------|
|
|
| 관련성 | Go 기반 번들러, tsgo에 영향을 줌 |
|
|
| 핵심 패턴 | AST 전체를 3번만 순회 (캐시 지역성 최대화) |
|
|
| Value semantics | boolean 1바이트, struct 임베딩으로 할당 최소화 |
|
|
| 병렬화 | 파싱/코드 생성을 완전 병렬화 |
|
|
| Five 적용 | 컴파일러 패스 수 최소화, 파싱과 코드 생성 병렬화 |
|
|
|
|
### CockroachDB / Pebble
|
|
|
|
| 항목 | 내용 |
|
|
|------|------|
|
|
| 관련성 | Go 기반 대규모 데이터 처리 |
|
|
| 핵심 패턴 | 블록 캐시를 C.malloc으로 off-heap 이동 |
|
|
| 참조 카운트 | 캐시 값에 refcount 사용 (GC 대신) |
|
|
| Five 적용 | DBF 페이지 캐시에 LRU + off-heap 검토 (향후, 필요 시) |
|
|
|
|
### Goja (JS engine in Go)
|
|
|
|
| 항목 | 내용 |
|
|
|------|------|
|
|
| 관련성 | Go에서 동적 타입 언어 런타임 구현 |
|
|
| 핵심 결정 | goroutine-safe 하지 않음 (단일 스레드 per runtime) |
|
|
| Value 표현 | interface{} 사용 (boxing 비용 수용) |
|
|
| Five 적용 | Thread별 독립 실행 (Goja와 동일), Value는 struct로 boxing 회피 |
|
|
|
|
### Go 1.23 unique 패키지
|
|
|
|
| 항목 | 내용 |
|
|
|------|------|
|
|
| 관련성 | 문자열 인터닝 (중복 제거) |
|
|
| 핵심 | `unique.Make[string]()` → 동일 문자열은 같은 포인터 |
|
|
| 내부 | concurrent hash-trie + weak pointer (GC 자동 정리) |
|
|
| Five 적용 | 향후 심볼 이름, 필드 이름 인터닝에 활용 가능 (Go 1.23+) |
|
|
|
|
### Go Arena 제안 (issue #51317)
|
|
|
|
| 항목 | 내용 |
|
|
|------|------|
|
|
| 관련성 | 동일 수명의 객체를 한 번에 할당/해제 |
|
|
| 상태 | 실험적 (Go 1.20 arena 패키지, 이후 제거) |
|
|
| tsgo 결정 | 사용하지 않음 — 관용적 Go로 충분 |
|
|
| Five 적용 | DBF 배치 스캔의 임시 객체에 자체 Arena 패턴 적용 가능 (향후) |
|
|
|
|
---
|
|
|
|
## 6. 정리: Five가 tsgo에서 가져간 것
|
|
|
|
| tsgo 교훈 | Five 적용 | 상태 |
|
|
|-----------|----------|------|
|
|
| GC와 싸우지 마라 | ptrStore 제거, unsafe.Pointer 사용 | ✅ 완료 |
|
|
| Value semantics | 24B Value struct, 스칼라 인라인 | ✅ 완료 |
|
|
| 핫 타입 풀링 | sync.Pool for 빈번한 Value | ⬜ Phase 4 |
|
|
| 불변성→병렬화 | Thread-local Stack/Locals | ✅ 완료 |
|
|
| UTF-8 활용 | Go string 사용 | ✅ 완료 |
|
|
| 구조 유사성 우선 | gencc.c → Thread 메서드 1:1 매핑 | ✅ 설계 완료 |
|
|
| 조기 off-heap 금지 | Go GC 범위 안에서 해결 | ✅ 완료 |
|
|
| Kind+Interface | Value.info(type tag) + Value.ptr | ✅ 완료 |
|
|
| NodeFactory | ValueFactory (싱글턴 캐싱) | ⬜ Phase 4 |
|
|
| CheckerPool | goroutine pool for DBF 스캔 | ⬜ Phase 5 |
|
|
| Link Store | Thread-local WorkArea 상태 | ⬜ Phase 5 |
|
|
|
|
---
|
|
|
|
## 변경 이력
|
|
|
|
| 날짜 | 변경 내용 |
|
|
|------|----------|
|
|
| 2026-03-27 | 초기 작성. tsgo 분석, Value 리팩터링 (16B→24B), 7대 교훈 정리 |
|