Files
five/docs/tsgo-reference-analysis.md
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- 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>
2026-03-31 09:41:50 +09:00

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로 결과 수집
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대 교훈 정리