- 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>
40 KiB
40 KiB
Five: 컴파일러 설계 관점의 Harbour-Go 융합 분석
컴파일러 설계 전문가 + Go 설계자 관점에서 Harbour와 Go를 비교하고, Go의 강점을 살리면서 Harbour의 문법적 강점과 DBF/Index 엔진의 노하우를 보존하는 방법을 검토
Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) All rights reserved.
목차
- 언어 비교: Harbour vs Go 근본 설계 차이
- Harbour 문법의 진짜 가치
- Go의 진짜 강점
- 융합 설계: 충돌 지점과 해결
- DBF 엔진 이식 전략
- Index 엔진 이식 전략
- RDD 아키텍처의 Go 재설계
- 컴파일러가 생성하는 코드의 품질
- 진화 방향: 무엇을 버리고 무엇을 살릴 것인가
- 종합 판정
1. 언어 비교: Harbour vs Go 근본 설계 차이
설계 철학 대비
Harbour Go
────────────────────────────────────────────────────────────────────
타입 시스템 동적 (런타임 결정) 정적 (컴파일 타임 결정)
메모리 모델 값 복사 + GC + 참조 카운트 값/포인터 명시 + GC
동시성 pthread + 수동 mutex goroutine + channel
에러 처리 BEGIN SEQUENCE (예외 모델) error 값 반환
OOP CLASS 기반 (상속, 다형성) struct + interface (합성)
제네릭 동적 타이핑으로 불필요 Go 1.18+ (제한적)
패러다임 명령형 + 절차적 + OOP 명령형 + 절차적 + CSP
문자열 mutable + COW + refcount immutable + GC
배열 동적 크기 + mixed 타입 고정 타입 slice
컴파일 단위 PRG 파일 (모듈) 패키지 (디렉토리)
실행 모델 바이트코드 VM 또는 C 변환 네이티브 컴파일
성능 특성 대비
Harbour Go
────────────────────────────────────────────────────────────────────
함수 호출 심볼 테이블 조회 (O(log N)) 직접 호출 (O(1))
변수 접근 HB_ITEM 간접 (32바이트) 레지스터/스택 직접
산술 연산 타입 체크 + 분기 매번 네이티브 CPU 명령
문자열 연결 재할당 + 복사 새 string 할당 (GC 처리)
배열 접근 HB_ITEM 인덱싱 (32B 단위) 포인터 산술 (타입별)
디스패치 가상 함수 테이블 (RDD 등) 인터페이스 (itab 캐시)
시작 시간 ~50ms (VM 초기화) ~1ms (네이티브)
핵심 인사이트
Harbour의 동적 타이핑은 표현력의 원천이자 성능의 병목이다.
Go의 정적 타이핑은 성능의 원천이자 표현력의 제약이다.
Five의 과제:
동적 타이핑의 표현력을 유지하면서
가능한 영역에서 정적 최적화의 이점을 취하는 것.
2. Harbour 문법의 진짜 가치
2.1 xBase 명령어: 도메인 특화 언어 (DSL)
xBase 명령어는 단순한 함수 호출이 아니라 데이터 조작 DSL이다. 이것은 SQL과도 다르고 일반 프로그래밍 언어와도 다른 독자적 영역이다.
세 가지 패러다임 비교:
[일반 코드] (Go/Java/Python)
db.Open("customers.dbf")
cursor := db.First()
for cursor != nil {
if cursor.Get("salary") > 50000 {
cursor.Set("salary", cursor.Get("salary") * 1.1)
cursor.Save()
}
cursor = cursor.Next()
}
db.Close()
[SQL]
UPDATE customers SET salary = salary * 1.1 WHERE salary > 50000
[xBase]
USE customers
SET FILTER TO salary > 50000
GO TOP
DO WHILE !EOF()
REPLACE salary WITH salary * 1.1
SKIP
ENDDO
USE
xBase의 장점:
- SQL보다 절차적 제어가 자유로움 (조건부 로직, 중간 계산)
- 일반 코드보다 선언적임 (USE, REPLACE, SEEK 의도가 명확)
- 커서 기반 탐색이 대화형 데이터 작업에 자연스러움
- ALIAS 시스템으로 여러 테이블을 동시에 열고 전환 가능
결론: xBase 명령어는 반드시 보존한다. 이것이 Five의 존재 이유.
2.2 매크로 시스템: 런타임 코드 생성
// 필드 이름이 런타임에 결정되는 경우
cField := GetFieldFromConfig()
REPLACE &cField WITH &cField * 1.1
// 인덱스 식이 런타임에 결정되는 경우
cKey := "UPPER(lastname + firstname)"
INDEX ON &cKey TO temp
// 조건식이 런타임에 결정되는 경우
cFilter := BuildFilterFromUserInput()
SET FILTER TO &cFilter
이것이 가능한 이유: Harbour가 런타임 컴파일러(매크로 컴파일러)를 내장하기 때문.
Go에서는 이런 동적 표현이 원천 불가능하다. Five는 매크로 컴파일러를 Go 런타임에 포함시켜야 한다.
2.3 코드 블록: 일급 함수 + 클로저
// 정렬 기준을 값으로 전달
ASort(aData, {|a,b| a[2] < b[2]})
// 콜백 패턴
AEval(aCustomers, {|c| SendEmail(c:email, cTemplate) })
// 지연 평가
bCondition := {|| nAge > 18 .AND. cCountry == "KR"}
IF Eval(bCondition)
...
ENDIF
Go에도 함수 리터럴이 있지만, Harbour의 코드 블록은 xBase 명령어와 결합할 때 극도로 간결하다:
// 이것을 Go로 표현하려면 장황한 구조체 + 메서드가 필요
dbEval({|r| r:salary > 50000}, {|r| r:salary *= 1.1})
2.4 CLASS: Go에 없는 것
CLASS HttpClient
DATA cBaseUrl
DATA nTimeout INIT 30
DATA oHeaders INIT {=>}
METHOD New(cUrl) CONSTRUCTOR
METHOD Get(cPath)
METHOD Post(cPath, hBody)
// 연산자 오버로딩
OPERATOR "+" ARG oOther INLINE ::Merge(oOther)
OPERATOR "==" ARG oOther INLINE ::IsEqual(oOther)
// 소멸자
DESTRUCTOR Cleanup
ENDCLASS
Go에서 불가능한 것들:
- 상속 (
INHERIT FROM) - 연산자 오버로딩
- 소멸자
- 데이터와 메서드의 응집된 선언
Five는 CLASS를 Go struct+interface로 변환하되, 문법적 편의를 제공한다.
3. Go의 진짜 강점
3.1 goroutine: 구조적 동시성
Harbour의 스레드:
- OS 스레드 1:1 매핑 (무거움, ~1MB 스택)
- 최대 수백 개 실용적
- 글로벌 상태 공유 → 레이스 컨디션
Go의 goroutine:
- M:N 스케줄링 (가벼움, ~4KB 초기 스택)
- 수십만 개 실용적
- channel로 통신 → 구조적 안전
Five에서의 활용:
- DBF 테이블 스캔을 goroutine으로 병렬화
- 여러 인덱스 동시 빌드
- HTTP 요청 처리 per-goroutine
- RDD I/O를 goroutine pool로 비동기화
3.2 interface: 암묵적 구현
// Go의 interface는 명시적 "implements" 선언이 필요 없다
type Reader interface {
Read(p []byte) (n int, err error)
}
// 이 메서드만 있으면 자동으로 Reader 인터페이스 충족
func (f *DBFFile) Read(p []byte) (int, error) { ... }
Five의 RDD에 대한 영향:
Harbour RDD: 100+ 함수 포인터를 가진 거대한 가상 함수 테이블
모든 메서드를 구현해야 함 (사용하지 않더라도)
Go RDD: 필요한 interface만 구현하면 됨
io.Reader, io.Writer, io.Seeker 등 Go 표준 인터페이스 활용
테스트와 목(mock) 작성이 쉬워짐
3.3 크로스 컴파일 + 단일 바이너리
Harbour 배포:
실행파일 + libharbour.so + C 런타임 + 플랫폼별 빌드
Go/Five 배포:
harbour build --target linux/arm64 myapp.prg
→ myapp (단일 파일 ~10MB, 의존성 없음)
→ scp myapp server:/usr/local/bin/
→ 끝.
3.4 생태계 접근
Harbour에서 PostgreSQL 사용:
→ contrib/hbpgsql 빌드 (C 라이브러리 의존)
→ 플랫폼별 설정
→ API가 제한적
Five에서 PostgreSQL 사용:
IMPORT "database/sql"
IMPORT _ "github.com/lib/pq"
→ go mod tidy
→ 끝. (Go의 모든 DB 드라이버 즉시 사용 가능)
4. 융합 설계: 충돌 지점과 해결
4.1 동적 타이핑 vs 정적 타이핑
충돌:
// Harbour: 같은 변수에 다른 타입 할당 가능
LOCAL x := 10
x := "hello" // 타입 변경 가능
x := {1, 2, 3} // 또 변경
// Go: 불가능
var x int = 10
x = "hello" // 컴파일 에러
해결: 생성되는 Go 코드에서 hbrt.Value 사용
// Five 컴파일러가 생성하는 코드
x := hbrt.MakeInt(10) // Value 타입 (Tagged 16B)
x = hbrt.MakeString("hello") // 같은 Value 타입이므로 합법
x = hbrt.MakeArray(1, 2, 3) // 역시 합법
최적화: 타입 힌트가 있을 때 Go 네이티브 타입 사용
// 타입 힌트가 있으면 Go 네이티브로 생성
FUNCTION Add(a AS NUMERIC, b AS NUMERIC) AS NUMERIC
RETURN a + b
// 컴파일러가 생성하는 최적화된 코드
func HB_ADD(a float64, b float64) float64 {
return a + b // hbrt.Value 오버헤드 없음!
}
단계적 타이핑 전략:
Level 1: 완전 동적 (기본, 기존 PRG 호환)
→ 모든 변수가 hbrt.Value
→ Harbour 100% 호환
→ 성능: Harbour과 유사 + Go GC 이점
Level 2: 부분 정적 (타입 힌트 사용 시)
→ 힌트가 있는 변수는 Go 네이티브 타입
→ 함수 경계에서 Value ↔ 네이티브 변환
→ 성능: 핫 루프에서 10-50배 향상
Level 3: 완전 정적 (새 코드, TYPE 선언 사용 시)
→ Go struct와 1:1 매핑
→ Go 생태계와 직접 호환
→ 성능: 순수 Go와 동등
4.2 에러 처리
충돌:
// Harbour: 예외 모델
BEGIN SEQUENCE
result := RiskyOp()
RECOVER USING oErr
? oErr:description
END SEQUENCE
// Go: 값 반환 모델
result, err := RiskyOp()
if err != nil {
log.Println(err)
}
해결: 두 모델 공존
// 기존 코드: BEGIN SEQUENCE 계속 지원 (내부적으로 panic/recover)
BEGIN SEQUENCE
USE customers
RECOVER USING oErr
? oErr:description
END SEQUENCE
// 새 코드: Go 스타일도 지원
result, err := TryOpen("customers")
IF err != NIL
? err:Error()
RETURN NIL
ENDIF
// 생성되는 Go 코드:
// BEGIN SEQUENCE → panic/recover
func() {
defer func() {
if r := recover(); r != nil {
// RECOVER 블록
}
}()
// BEGIN SEQUENCE 블록
}()
// Go 스타일 → 직접 생성
result, err := TryOpen("customers")
if err != nil {
// ...
}
4.3 OOP 모델
충돌:
// Harbour: 클래스 상속
CLASS Manager INHERIT FROM Employee
DATA nBonus
METHOD CalcPay()
ENDCLASS
// Go: 상속 없음, 임베딩으로 합성
type Manager struct {
Employee // 임베딩 (상속 아님)
Bonus float64
}
해결: CLASS를 Go struct+interface로 변환하되 상속 시맨틱 보존
// Five 컴파일러가 생성하는 코드
// Employee 클래스
type HbClass_Employee struct {
hbrt.BaseObject // Five 공통 기반 (클래스 메타, 메서드 디스패치)
FcName hbrt.Value // DATA cName
FnSalary hbrt.Value // DATA nSalary
}
// Manager 클래스 (Employee 임베딩 = 상속 효과)
type HbClass_Manager struct {
HbClass_Employee // Employee 상속
FnBonus hbrt.Value // DATA nBonus
}
// 메서드: Employee.CalcPay
func (o *HbClass_Employee) M_CALCPAY(t *hbrt.Thread) {
t.PushValue(o.FnSalary)
t.RetValue()
}
// 메서드: Manager.CalcPay (오버라이드)
func (o *HbClass_Manager) M_CALCPAY(t *hbrt.Thread) {
// ::Super:CalcPay() + ::nBonus
o.HbClass_Employee.M_CALCPAY(t) // super 호출
t.PushValue(o.FnBonus)
t.Plus()
t.RetValue()
}
// 연산자 오버로딩: Go에는 없지만 Five 런타임이 디스패치
// obj1 + obj2 → hbrt.OperatorPlus(obj1, obj2) → obj1.M__PLUS(obj2)
5. DBF 엔진 이식 전략
5.1 핵심 원칙: 포맷 100% 호환, 구현은 Go 네이티브
기존 Harbour DBF 파일을 Five로 그대로 열 수 있어야 한다.
Five로 만든 DBF 파일을 기존 Harbour/Clipper로 그대로 열 수 있어야 한다.
이것은 협상 불가.
바이트 레벨 포맷 호환:
✓ DBF 헤더 (32바이트) - 모든 필드 동일
✓ 필드 디스크립터 (32바이트×N) - 모든 필드 동일
✓ 레코드 데이터 (고정 폭) - 바이트 동일
✓ 삭제 마크 (첫 바이트 '*' 또는 ' ')
✓ EOF 마크 (0x1A)
✓ NTX 인덱스 (1024바이트 페이지)
✓ CDX 인덱스 (512-8192바이트 페이지)
✓ FPT 메모 (블록 단위)
✓ 락 위치/크기 (모든 스키마)
5.2 DBF 코어: Go 구조체로 정밀 매핑
package hbrdd
import (
"encoding/binary"
"os"
"sync"
"io"
)
// DBF 헤더: Harbour의 DBFHEADER와 바이트 동일
type DBFHeader struct {
Version byte // offset 0
Year byte // offset 1 (YY)
Month byte // offset 2
Day byte // offset 3
RecCount uint32 // offset 4 (LE)
HeaderLen uint16 // offset 8 (LE)
RecordLen uint16 // offset 10 (LE)
Reserved1 [2]byte // offset 12
Transaction byte // offset 14
Encrypted byte // offset 15
Reserved2 [12]byte // offset 16
HasTags byte // offset 28
CodePage byte // offset 29
Reserved3 [2]byte // offset 30
}
// sizeof = 32 bytes (Harbour과 동일)
// 필드 디스크립터: DBFFIELD과 바이트 동일
type DBFField struct {
Name [11]byte // offset 0 (null-terminated)
Type byte // offset 11 (C, N, L, D, M, ...)
Reserved1 [4]byte // offset 12
Len byte // offset 16
Dec byte // offset 17
Flags byte // offset 18
Counter [4]byte // offset 19 (auto-increment, LE)
Step byte // offset 23
Reserved2 [7]byte // offset 24
HasTag byte // offset 31
}
// sizeof = 32 bytes (Harbour과 동일)
5.3 레코드 I/O: Go의 I/O 강점 활용
// Harbour의 단일 레코드 버퍼 → Go의 버퍼 + mmap 하이브리드
type DBFArea struct {
mu sync.RWMutex // per-WorkArea 락 (Harbour의 글로벌 락 대체)
file *os.File
header DBFHeader
fields []DBFField
offsets []uint16 // 필드별 레코드 내 오프셋
// 레코드 버퍼 관리
recBuf []byte // 현재 레코드 (RecordLen 크기)
recNo uint32 // 현재 레코드 번호
dirty bool // 수정 여부
// Harbour에 없는 Go 최적화: 읽기 버퍼링
readBuf *bufio.Reader // 순차 스캔 시 성능 향상
readAhead int // 프리페치 레코드 수
// 락 관리
locks map[uint32]bool // 잠긴 레코드 맵
lockScheme LockScheme // 락 스키마 (Clipper/VFP/HB64)
// 상태
bof, eof bool
found bool
deleted bool
// 필터/관계
filter *Filter
relations []*Relation
alias string
}
// 레코드 읽기: Harbour의 hb_fileReadAt 대응
func (a *DBFArea) readRecord(recNo uint32) error {
offset := int64(a.header.HeaderLen) + int64(recNo-1)*int64(a.header.RecordLen)
_, err := a.file.ReadAt(a.recBuf, offset)
if err != nil {
return err
}
a.recNo = recNo
a.dirty = false
a.deleted = (a.recBuf[0] == '*')
return nil
}
// 레코드 쓰기: Harbour의 hb_fileWriteAt 대응
func (a *DBFArea) writeRecord() error {
if !a.dirty {
return nil
}
offset := int64(a.header.HeaderLen) + int64(a.recNo-1)*int64(a.header.RecordLen)
_, err := a.file.WriteAt(a.recBuf, offset)
if err != nil {
return err
}
a.dirty = false
return nil
}
// 필드 접근: Harbour의 pRecord + pFieldOffset[n] 대응
func (a *DBFArea) GetField(index int) hbrt.Value {
off := a.offsets[index]
fld := &a.fields[index]
raw := a.recBuf[off : off+uint16(fld.Len)]
switch fld.Type {
case 'C': // Character
return hbrt.MakeString(trimRight(raw))
case 'N': // Numeric
return parseNumeric(raw, fld.Dec)
case 'L': // Logical
return hbrt.MakeBool(raw[0] == 'T' || raw[0] == 'Y' || raw[0] == 't' || raw[0] == 'y')
case 'D': // Date
return parseDate(raw)
case 'M': // Memo
blockNo := binary.LittleEndian.Uint32(raw[:4])
return a.readMemo(blockNo)
default:
return hbrt.MakeString(string(raw))
}
}
5.4 Go 최적화: Harbour에서 불가능했던 것들
// 최적화 1: mmap으로 대용량 파일 직접 매핑
// Harbour: 매번 hb_fileReadAt() syscall
// Go/Five: mmap으로 메모리 직접 접근 (OS가 페이지 관리)
type MmapDBF struct {
data []byte // mmap된 전체 파일
header *DBFHeader // data[0:32]를 가리킴
}
func OpenMmap(path string) (*MmapDBF, error) {
f, _ := os.Open(path)
data, _ := syscall.Mmap(int(f.Fd()), 0, size,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
return &MmapDBF{data: data}, nil
}
func (m *MmapDBF) Record(recNo uint32) []byte {
off := int(m.header.HeaderLen) + int(recNo-1)*int(m.header.RecordLen)
return m.data[off : off+int(m.header.RecordLen)]
// syscall 없음! 메모리 접근만으로 레코드 읽기
}
// 최적화 2: goroutine으로 병렬 스캔
// Harbour: 단일 스레드 순차 스캔
// Go/Five: 레코드 범위를 분할하여 병렬 처리
func (a *DBFArea) ParallelScan(filter func([]byte) bool) []uint32 {
total := a.header.RecCount
workers := runtime.NumCPU()
chunk := total / uint32(workers)
results := make(chan []uint32, workers)
for i := 0; i < workers; i++ {
start := uint32(i) * chunk + 1
end := start + chunk
if i == workers-1 { end = total + 1 }
go func(s, e uint32) {
var matches []uint32
for r := s; r < e; r++ {
rec := a.mmapRecord(r)
if rec[0] != '*' && filter(rec) {
matches = append(matches, r)
}
}
results <- matches
}(start, end)
}
var all []uint32
for i := 0; i < workers; i++ {
all = append(all, <-results...)
}
sort.Slice(all, func(i, j int) bool { return all[i] < all[j] })
return all
}
// 최적화 3: 버퍼링된 순차 읽기
// Harbour: 레코드 단위 I/O (small random reads)
// Go/Five: bufio.Reader로 여러 레코드를 한 번에 읽기
func (a *DBFArea) BufferedScan() {
a.readBuf = bufio.NewReaderSize(a.file, 64*1024) // 64KB 버퍼
// SKIP 1 반복 시: 디스크 I/O가 64KB 단위로 감소
// DBF 레코드가 100바이트라면 한 번에 ~640 레코드 읽기
}
// 최적화 4: 필드 접근 시 지연 파싱
// Harbour: 레코드 읽을 때 모든 필드 파싱하지 않음 (이미 효율적)
// Go/Five: 동일하게 지연 파싱 + 추가로 unsafe.Pointer로 zero-copy
func (a *DBFArea) GetFieldFast(index int) string {
off := a.offsets[index]
fld := &a.fields[index]
// unsafe.String: 복사 없이 []byte를 string으로 (Go 1.20+)
return unsafe.String(&a.recBuf[off], int(fld.Len))
}
5.5 락 호환성: 모든 스키마 지원
// Harbour의 6가지 락 스키마를 모두 지원
type LockScheme int
const (
LockClipper LockScheme = iota // DBF_LOCKPOS = 1,000,000,000
LockClipper2 // DBF_LOCKPOS = 4,000,000,000
LockVFP // DBF_LOCKPOS = 0x40000000
LockVFPX // DBF_LOCKPOS = 0x7ffffffeUL
LockHB32 // Harbour 32-bit
LockHB64 // DBF_LOCKPOS = 0x7F00000000000000
)
// 레코드 락: Harbour의 1-byte-per-record 방식 그대로
func (a *DBFArea) LockRecord(recNo uint32) error {
pos := a.lockScheme.RecordLockPos(recNo)
return syscall.Flock(...)
// 또는 fcntl(F_SETLK, ...) for POSIX
}
// Harbour/Clipper 프로세스와 동시 접근 시에도 호환
// → 같은 락 위치/크기를 사용하므로 상호 배타적 접근 보장
6. Index 엔진 이식 전략
6.1 NTX 엔진: B-tree 정밀 이식
// NTX 상수: Harbour과 동일
const (
NTXBlockSize = 1024 // 페이지 크기
NTXHeaderSize = 512 // 헤더 크기
NTXMaxKey = 256 // 최대 키 길이
NTXStackSize = 32 // 최대 트리 깊이
)
// NTX 헤더: Harbour NTXHEADER와 바이트 동일
type NTXHeader struct {
Type uint16 // 0x0401
Version uint16
Root uint32 // 루트 페이지 오프셋
NextPage uint32 // 다음 빈 페이지
ItemSize uint16 // 키 엔트리 크기
KeySize uint16 // 키 값 길이
KeyDec uint16 // 소수점 자릿수
MaxItem uint16 // 페이지당 최대 키 수
HalfPage uint16 // 밸런싱용 절반 크기
KeyExpr [256]byte // 키 식 (null-terminated)
Unique byte // 유니크 플래그
_ byte
Descend byte // 내림차순 플래그
_ byte
ForExpr [256]byte // FOR 조건식
TagName [12]byte // 태그 이름
Custom byte // 커스텀 플래그
_ [473]byte // 예약
}
// B-tree 페이지
type NTXPage struct {
KeyCount uint16
Keys []NTXKey // 정렬된 키 배열
}
type NTXKey struct {
Child uint32 // 하위 페이지 (0이면 리프)
RecNo uint32 // 레코드 번호
Value []byte // 키 값
}
// 탐색 스택: 현재 위치 추적
type NTXStack struct {
Page uint32
Key int16
}
type NTXIndex struct {
file *os.File
header NTXHeader
stack [NTXStackSize]NTXStack // Harbour과 동일한 스택
stackPos int
keySize int
// Go 최적화: 페이지 캐시
cache *lru.Cache[uint32, *NTXPage] // LRU 캐시
}
6.2 NTX SEEK: Harbour 알고리즘 정밀 이식
// Harbour의 hb_ntxTagKeyFind와 동일한 알고리즘
func (idx *NTXIndex) Seek(key []byte, softSeek bool) (uint32, bool) {
idx.stackPos = 0
pageNo := idx.header.Root
for {
page := idx.loadPage(pageNo)
// 페이지 내 이진 검색 (Harbour과 동일)
lo, hi := 0, int(page.KeyCount)-1
found := false
pos := 0
for lo <= hi {
mid := (lo + hi) / 2
cmp := idx.compareKeys(key, page.Keys[mid].Value)
if cmp == 0 {
found = true
pos = mid
break
} else if cmp < 0 {
hi = mid - 1
} else {
lo = mid + 1
}
pos = lo
}
// 스택에 위치 기록
idx.stack[idx.stackPos] = NTXStack{Page: pageNo, Key: int16(pos)}
idx.stackPos++
if found && page.Keys[pos].Child == 0 {
// 리프에서 찾음
return page.Keys[pos].RecNo, true
}
if page.Keys[pos].Child != 0 {
// 브랜치: 하위 페이지로
pageNo = page.Keys[pos].Child
} else {
// 리프인데 못 찾음
if softSeek && pos < int(page.KeyCount) {
return page.Keys[pos].RecNo, false
}
return 0, false // EOF
}
}
}
6.3 CDX 엔진: 압축 알고리즘 보존
// CDX 상수
const (
CDXPageLen = 512 // 기본 페이지 크기
CDXPageLenMax = 8192 // 최대 페이지 크기
CDXHeaderLen = 1024 // 파일 헤더
CDXTagHeaderLen = 512 // 태그 헤더
)
// CDX 리프 노드: 비트 패킹 압축 (Harbour의 핵심 노하우)
type CDXExtNode struct {
Attr uint16
KeyCount uint16
LeftPtr uint32
RightPtr uint32
FreeSpace uint16
RecMask uint32 // 레코드 번호 비트마스크
DupMask byte // 중복 바이트 마스크
TrlMask byte // 후행 바이트 마스크
RecBits byte // 레코드 번호 비트 수
DupBits byte // 중복 카운트 비트 수
TrlBits byte // 후행 카운트 비트 수
KeyBytes byte // 메타데이터 총 바이트
}
// CDX 키 디코딩: Harbour의 비트 패킹 알고리즘 정밀 이식
func (n *CDXExtNode) DecodeKey(index int, prevKey []byte, keyLen int) (recNo uint32, key []byte) {
// 비트 스트림에서 추출
bitPos := uint(index) * uint(n.RecBits+n.DupBits+n.TrlBits)
data := n.keyPool()
recNo = extractBits(data, bitPos, uint(n.RecBits)) & n.RecMask
bitPos += uint(n.RecBits)
dupCount := int(extractBits(data, bitPos, uint(n.DupBits)) & uint32(n.DupMask))
bitPos += uint(n.DupBits)
trlCount := int(extractBits(data, bitPos, uint(n.TrlBits)) & uint32(n.TrlMask))
// 키 복원: 이전 키의 앞부분(dup) + 새 데이터 + 공백(trail)
key = make([]byte, keyLen)
copy(key[:dupCount], prevKey[:dupCount])
uniqueLen := keyLen - dupCount - trlCount
if uniqueLen > 0 {
keyDataOff := n.keyDataOffset(index, keyLen)
copy(key[dupCount:dupCount+uniqueLen], n.rawData[keyDataOff:])
}
// 후행 공백 채우기
for i := keyLen - trlCount; i < keyLen; i++ {
key[i] = ' '
}
return recNo, key
}
6.4 Go 최적화: Harbour에서 불가능했던 인덱스 기능
// 최적화 1: 페이지 캐시 (LRU)
// Harbour: 매번 디스크 읽기 (OS 캐시에 의존)
// Go/Five: 애플리케이션 레벨 LRU 캐시
type PageCache struct {
mu sync.RWMutex
cache *lru.Cache[uint64, []byte] // pageKey → page data
}
func newPageCache(maxPages int) *PageCache {
c, _ := lru.New[uint64, []byte](maxPages) // 기본 1000 페이지
return &PageCache{cache: c}
}
// 최적화 2: 병렬 인덱스 빌드
// Harbour: INDEX ON ... 단일 스레드
// Go/Five: 정렬을 goroutine으로 병렬화
func (idx *NTXIndex) ParallelBuild(area *DBFArea, keyExpr func([]byte) []byte) error {
// Phase 1: 병렬로 키 추출
total := area.header.RecCount
workers := runtime.NumCPU()
chunk := total / uint32(workers)
type keyRec struct {
key []byte
recNo uint32
}
parts := make([][]keyRec, workers)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func(w int) {
defer wg.Done()
start := uint32(w)*chunk + 1
end := start + chunk
if w == workers-1 { end = total + 1 }
for r := start; r < end; r++ {
rec := area.mmapRecord(r)
if rec[0] != '*' { // 삭제되지 않은 레코드만
parts[w] = append(parts[w], keyRec{
key: keyExpr(rec),
recNo: r,
})
}
}
}(i)
}
wg.Wait()
// Phase 2: 머지 소트 (이미 각 파트는 RecNo 순)
// Phase 3: 정렬된 키로 B-tree 바텀업 빌드
all := mergeKeyRecs(parts)
sort.Slice(all, func(i, j int) bool {
return bytes.Compare(all[i].key, all[j].key) < 0
})
return idx.buildFromSorted(all)
}
// 최적화 3: 읽기 시 lock-free
// Harbour: 읽기에도 락 필요 (글로벌 상태)
// Go/Five: 읽기 전용 인덱스 접근은 lock-free
// RWMutex: 여러 goroutine이 동시에 SEEK 가능 (RLock만)
// 쓰기(INDEX 갱신)만 배타적 Lock
7. RDD 아키텍처의 Go 재설계
7.1 Harbour의 RDD: 100+ 메서드 가상 함수 테이블
문제:
Harbour RDDFUNCS는 ~100개 함수 포인터의 단일 거대 구조체.
새 RDD 드라이버를 만들려면 100개 메서드를 모두 구현하거나 부모에서 상속.
대부분은 사용하지 않는 메서드를 형식적으로 채워야 함.
7.2 Go 재설계: interface 분할
// 핵심 인터페이스: 필수 (모든 RDD가 구현)
type Driver interface {
Open(params OpenParams) (Area, error)
Create(params CreateParams) (Area, error)
Name() string
}
type Area interface {
io.Closer
// 레코드 이동
GoTo(recNo uint32) error
GoTop() error
GoBottom() error
Skip(count int64) error
// 레코드 접근
RecNo() uint32
RecCount() uint32
EOF() bool
BOF() bool
Deleted() bool
// 필드 접근
FieldCount() int
FieldInfo(index int) FieldInfo
GetValue(index int) (hbrt.Value, error)
PutValue(index int, val hbrt.Value) error
}
// 선택 인터페이스: 필요한 것만 구현
type Appender interface {
Append() error
}
type Deleter interface {
Delete() error
Recall() error
Pack() error
Zap() error
}
type Locker interface {
LockRecord(recNo uint32) error
UnlockRecord(recNo uint32) error
LockFile() error
UnlockFile() error
}
type Indexer interface {
OrderCreate(params OrderCreateParams) error
OrderListAdd(path string) error
OrderListClear() error
OrderSetFocus(tag string) error
Seek(key hbrt.Value, softSeek bool) (bool, error)
}
type Filterer interface {
SetFilter(expr string, block func() bool) error
ClearFilter() error
}
type Relater interface {
SetRelation(child Area, keyExpr func() hbrt.Value) error
ClearRelation() error
ForceRel() error
}
type Transactor interface {
Begin() error
Commit() error
Rollback() error
}
7.3 드라이버 등록
// Harbour의 hb_rddRegister → Go의 init() + Registry
var drivers = make(map[string]Driver)
func RegisterDriver(name string, d Driver) {
drivers[strings.ToUpper(name)] = d
}
func init() {
RegisterDriver("DBF", &DBFDriver{})
RegisterDriver("DBFNTX", &DBFNTXDriver{})
RegisterDriver("DBFCDX", &DBFCDXDriver{})
}
// SQL RDD: Go의 database/sql 활용
func init() {
RegisterDriver("PGSQL", &SQLDriver{DriverName: "postgres"})
RegisterDriver("MYSQL", &SQLDriver{DriverName: "mysql"})
RegisterDriver("SQLITE", &SQLDriver{DriverName: "sqlite3"})
}
7.4 WorkArea 관리: goroutine-local
// Harbour: 글로벌 워크에어리어 테이블 + 스레드 위험
// Go/Five: Thread별 워크에어리어 (goroutine-local, 락 불필요)
type WorkAreaManager struct {
areas map[uint16]Area // 번호 → Area
aliases map[string]uint16 // 별명 → 번호
current uint16 // 현재 선택된 Area
nextArea uint16 // 다음 할당 번호
}
// 각 Thread가 자기만의 WorkAreaManager를 소유
type Thread struct {
// ...
wa *WorkAreaManager // goroutine-local
}
// USE customers ALIAS cust
func (t *Thread) CmdUse(path, driver, alias string) error {
drv := drivers[driver]
area, err := drv.Open(OpenParams{Path: path})
if err != nil {
return err
}
areaNo := t.wa.nextArea
t.wa.nextArea++
t.wa.areas[areaNo] = area
t.wa.aliases[strings.ToUpper(alias)] = areaNo
t.wa.current = areaNo
return nil
}
// SELECT cust
func (t *Thread) CmdSelect(alias string) error {
areaNo, ok := t.wa.aliases[strings.ToUpper(alias)]
if !ok {
return fmt.Errorf("alias not found: %s", alias)
}
t.wa.current = areaNo
return nil
}
8. 컴파일러가 생성하는 코드의 품질
8.1 Go 컴파일러가 최적화할 수 있는 코드 생성
핵심: Five 컴파일러가 생성한 Go 코드는
Go 컴파일러(gc)가 추가 최적화할 수 있어야 한다.
Go 컴파일러의 최적화:
- 인라이닝 (함수 크기 < 80 노드)
- 이스케이프 분석 (힙 vs 스택 결정)
- 데드 코드 제거
- 경계 검사 제거 (BCE)
- SSA 최적화
인라이닝을 위한 설계:
// 나쁜 패턴: 거대한 메서드
func (t *Thread) Plus() {
b := t.stack[t.sp-1]
a := t.stack[t.sp-2]
// ... 100줄의 타입 체크 + 연산 ...
t.sp--
}
// → Go 컴파일러가 인라인하지 않음
// 좋은 패턴: fast path를 분리
func (t *Thread) Plus() {
b := t.stack[t.sp-1]
a := &t.stack[t.sp-2]
// fast path: int + int (가장 빈번한 경우)
if a.IsInt() && b.IsInt() {
*a = addIntFast(a.AsInt(), b.AsInt())
t.sp--
return
}
// slow path: 별도 함수로 (인라인 대상에서 제외)
t.plusSlow(a, b)
}
//go:noinline
func (t *Thread) plusSlow(a *hbrt.Value, b hbrt.Value) {
// 모든 타입 조합 처리
}
// addIntFast는 매우 작으므로 인라인됨
func addIntFast(a, b int64) hbrt.Value {
r := a + b
if (b >= 0 && r >= a) || (b < 0 && r < a) {
return hbrt.MakeInt(r)
}
return hbrt.MakeDouble(float64(a) + float64(b))
}
이스케이프 분석을 위한 설계:
// 나쁜 패턴: Value가 힙으로 이스케이프
func (t *Thread) PushLocal(n int) {
val := t.locals[n] // Value 복사 (16바이트, 스택)
t.push(&val) // 포인터 전달 → 이스케이프 가능!
}
// 좋은 패턴: 값 복사로 전달
func (t *Thread) PushLocal(n int) {
t.stack[t.sp] = t.locals[n] // 값 복사 (16바이트)
t.sp++
// 포인터 없음 → 이스케이프 없음 → GC 부담 없음
}
8.2 타입 힌트 활용 시 코드 품질 도약
// 타입 힌트 없는 코드 (Level 1)
FUNCTION CalcTotal(aItems)
LOCAL nTotal := 0
FOR EACH item IN aItems
nTotal += item:price * item:qty
NEXT
RETURN nTotal
// 생성되는 Go 코드 (Level 1: 동적)
func HB_CALCTOTAL(t *hbrt.Thread) {
t.Frame(1, 1)
defer t.EndProc()
t.LocalSetInt(2, 0)
// FOR EACH → 반복문
arr := t.Local(1)
for i := 0; i < arr.Len(); i++ {
t.PushValue(arr.Index(i))
t.Send0("PRICE") // 동적 메서드 호출
t.PushValue(arr.Index(i))
t.Send0("QTY") // 동적 메서드 호출
t.Mult() // Value * Value
t.LocalAdd(2) // nTotal += result
}
t.PushLocal(2)
t.RetValue()
}
// 타입 힌트 있는 코드 (Level 2)
TYPE OrderItem
DATA price AS NUMERIC
DATA qty AS INTEGER
END TYPE
FUNCTION CalcTotal(aItems AS ARRAY OF OrderItem) AS NUMERIC
LOCAL nTotal AS NUMERIC := 0
FOR EACH item AS OrderItem IN aItems
nTotal += item:price * item:qty
NEXT
RETURN nTotal
// 생성되는 Go 코드 (Level 2: 정적 최적화)
type OrderItem struct {
Price float64
Qty int64
}
func HB_CALCTOTAL(items []OrderItem) float64 {
total := 0.0
for _, item := range items {
total += item.Price * float64(item.Qty)
}
return total
// hbrt.Value 오버헤드 완전 제거!
// 순수 Go와 동일한 성능
}
9. 진화 방향: 무엇을 버리고 무엇을 살릴 것인가
보존 (변경 불가)
| 요소 | 이유 | 보존 방법 |
|---|---|---|
| xBase 명령어 (USE, SEEK, REPLACE...) | Five의 존재 이유, 핵심 가치 | Five 문법으로 유지 |
| DBF 파일 포맷 | 기존 데이터 호환 필수 | 바이트 레벨 정밀 이식 |
| NTX/CDX 인덱스 포맷 | 기존 인덱스 호환 필수 | B-tree 알고리즘 정밀 이식 |
| 락 스키마 (6종) | 기존 앱과 동시 실행 | 모든 스키마 구현 |
| 매크로 시스템 (&variable) | 동적 비즈니스 규칙 엔진 | 런타임 미니 컴파일러 |
| 코드 블록 ({||...}) | 함수형 표현의 핵심 | Go 클로저로 변환 |
| CLASS 문법 | Go에 없는 OOP 표현 | struct+interface로 변환 |
| ALIAS 시스템 | 다중 테이블 작업의 핵심 | Thread-local WorkArea |
진화 (개선)
| 요소 | 현재 문제 | Five의 개선 |
|---|---|---|
| 스레딩 | pthread + 수동 mutex | goroutine + channel |
| GC | 자체 mark-sweep + suspend | Go GC에 위임 |
| 문자열 | mutable + COW + refcount | Go immutable string + 필요시 []byte |
| 에러 처리 | BEGIN SEQUENCE만 | + Go 스타일 error 반환 |
| 타입 시스템 | 완전 동적만 | + 선택적 타입 힌트 (단계적) |
| 패키지 관리 | 없음 | Go modules 기반 |
| 빌드/배포 | C 컴파일러 + 라이브러리 | go build, 단일 바이너리 |
| 개발 도구 | 없음 | LSP, DAP, fmt, lint |
| 네트워크 | contrib만 | Go 표준 라이브러리 직접 |
| RDD 확장 | C 플러그인 | Go interface (SQL, REST, ...) |
제거 (정리)
| 요소 | 제거 이유 | 대안 |
|---|---|---|
| dlmalloc (자체 메모리 할당) | Go 런타임이 처리 | Go GC |
| Harbour 자체 GC | Go GC가 우수 | Go GC |
| C 인라인 (#pragma BEGINDUMP) | Go 생태계 사용 | CGo 또는 Go 네이티브 |
| GT 드라이버 (gtwin, gtcrs...) | 터미널 UI는 Go 라이브러리 | tview, bubbletea 등 |
| OS별 분기 코드 | Go가 크로스플랫폼 | Go 표준 라이브러리 |
| hb_xgrab/hb_xfree (메모리 API) | Go가 관리 | make/new + GC |
| STRING refcount/COW | Go string이 immutable | Go string |
| 180개 pcode opcode | Go 네이티브 코드 생성 | 직접 Go 함수 호출 |
호환 모드 (선택적)
| 요소 | 동작 | 모드 |
|---|---|---|
| STRING - STRING 패딩 | Clipper quirk | #pragma compatibility(clipper) |
| DATE + DATE 줄리안 합산 | Clipper quirk | #pragma compatibility(clipper) |
| SET EXACT OFF 기본 | Clipper 기본 | #pragma compatibility(clipper) |
| 63자 심볼 제한 | Clipper 제한 | #pragma compatibility(clipper) |
10. 종합 판정
컴파일러 설계 관점
1. PRG → Go 트랜스파일 방식은 올바른 선택이다.
- VM 해석 실행 대비 Go 네이티브 컴파일의 성능 이점
- Go 컴파일러의 추가 최적화(인라이닝, BCE, SSA) 활용
- Go 생태계와의 자연스러운 통합
2. Tagged Value 16B는 적절한 타협점이다.
- 동적 타이핑 보존 (호환성)
- NaN-boxing보다 안전하고 메타데이터 보존
- 타입 힌트 시 네이티브 타입으로 전환 가능 (점진적 최적화)
3. DBF/Index 엔진은 정밀 이식이 맞다.
- 포맷 호환성은 협상 불가
- 알고리즘(B-tree, 비트 패킹)은 Harbour의 핵심 노하우
- Go의 mmap, goroutine, bufio로 성능 향상 가능
4. RDD interface 분할은 Go 철학에 부합한다.
- 100+ 메서드 vtable → 작은 interface 조합
- SQL/REST 등 새 드라이버 작성이 쉬워짐
- 테스트 용이성 향상
Go 설계자 관점
1. CLASS 문법은 Go 생태계에서 차별화 요소가 된다.
- Go 개발자들이 가장 아쉬워하는 것 중 하나
- Five가 "Go with classes" 포지션을 가질 수 있음
2. xBase DSL은 niche하지만 강력한 포지션이다.
- 데이터 조작에서 SQL의 대안
- Go의 database/sql보다 절차적 제어가 자유로움
- DBF뿐 아니라 SQL/REST에도 xBase 문법 적용 가능 → 파괴력
3. 단계적 타이핑(gradual typing)이 핵심 전략이다.
- Level 1 (동적): 기존 PRG 100% 호환 → 진입장벽 제거
- Level 2 (힌트): 성능 최적화 → 프로덕션 준비
- Level 3 (정적): Go struct 직접 매핑 → Go 생태계 완전 통합
4. goroutine + xBase 조합은 독보적이다.
- 병렬 테이블 스캔, 병렬 인덱스 빌드
- HTTP 서버에서 xBase 데이터 처리
- 이것은 어떤 기존 도구로도 할 수 없는 것
최종 요약
Five = Harbour의 문법(xBase DSL + CLASS + 매크로)
+ Harbour의 데이터 엔진(DBF + NTX/CDX, 포맷 100% 호환)
+ Go의 플랫폼(goroutine + 생태계 + 단일 바이너리 + 크로스컴파일)
+ 단계적 타이핑(동적 → 정적 점진 전환)
+ 현대적 도구(LSP, DAP, fmt, 패키지 매니저)
이것은 포팅이 아니라
두 언어의 강점만을 결합한 새로운 플랫폼이다.
변경 이력
| 날짜 | 변경 내용 |
|---|---|
| 2026-03-27 | 초기 작성. 컴파일러 설계 관점 Harbour-Go 융합 분석, DBF/Index 이식 전략 |