- 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>
11 KiB
11 KiB
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 방식)
// 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 방식)
// 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대 교훈 정리 |