Files
five/docs/harbour-go-compiler-design-review.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

1388 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Five: 컴파일러 설계 관점의 Harbour-Go 융합 분석
> 컴파일러 설계 전문가 + Go 설계자 관점에서
> Harbour와 Go를 비교하고, Go의 강점을 살리면서
> Harbour의 문법적 강점과 DBF/Index 엔진의 노하우를 보존하는 방법을 검토
>
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
> All rights reserved.
---
## 목차
1. [언어 비교: Harbour vs Go 근본 설계 차이](#1-언어-비교-harbour-vs-go-근본-설계-차이)
2. [Harbour 문법의 진짜 가치](#2-harbour-문법의-진짜-가치)
3. [Go의 진짜 강점](#3-go의-진짜-강점)
4. [융합 설계: 충돌 지점과 해결](#4-융합-설계-충돌-지점과-해결)
5. [DBF 엔진 이식 전략](#5-dbf-엔진-이식-전략)
6. [Index 엔진 이식 전략](#6-index-엔진-이식-전략)
7. [RDD 아키텍처의 Go 재설계](#7-rdd-아키텍처의-go-재설계)
8. [컴파일러가 생성하는 코드의 품질](#8-컴파일러가-생성하는-코드의-품질)
9. [진화 방향: 무엇을 버리고 무엇을 살릴 것인가](#9-진화-방향-무엇을-버리고-무엇을-살릴-것인가)
10. [종합 판정](#10-종합-판정)
---
## 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 매크로 시스템: 런타임 코드 생성
```harbour
// 필드 이름이 런타임에 결정되는 경우
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 코드 블록: 일급 함수 + 클로저
```harbour
// 정렬 기준을 값으로 전달
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 명령어와 결합할 때 극도로 간결하다:
```harbour
// 이것을 Go로 표현하려면 장황한 구조체 + 메서드가 필요
dbEval({|r| r:salary > 50000}, {|r| r:salary *= 1.1})
```
### 2.4 CLASS: Go에 없는 것
```harbour
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
// 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
// Harbour: 같은 변수에 다른 타입 할당 가능
LOCAL x := 10
x := "hello" // 타입 변경 가능
x := {1, 2, 3} // 또 변경
```
```go
// Go: 불가능
var x int = 10
x = "hello" // 컴파일 에러
```
**해결: 생성되는 Go 코드에서 hbrt.Value 사용**
```go
// Five 컴파일러가 생성하는 코드
x := hbrt.MakeInt(10) // Value 타입 (Tagged 16B)
x = hbrt.MakeString("hello") // 같은 Value 타입이므로 합법
x = hbrt.MakeArray(1, 2, 3) // 역시 합법
```
**최적화: 타입 힌트가 있을 때 Go 네이티브 타입 사용**
```harbour
// 타입 힌트가 있으면 Go 네이티브로 생성
FUNCTION Add(a AS NUMERIC, b AS NUMERIC) AS NUMERIC
RETURN a + b
```
```go
// 컴파일러가 생성하는 최적화된 코드
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
// Harbour: 예외 모델
BEGIN SEQUENCE
result := RiskyOp()
RECOVER USING oErr
? oErr:description
END SEQUENCE
```
```go
// Go: 값 반환 모델
result, err := RiskyOp()
if err != nil {
log.Println(err)
}
```
**해결: 두 모델 공존**
```harbour
// 기존 코드: 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
// 생성되는 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
// Harbour: 클래스 상속
CLASS Manager INHERIT FROM Employee
DATA nBonus
METHOD CalcPay()
ENDCLASS
```
```go
// Go: 상속 없음, 임베딩으로 합성
type Manager struct {
Employee // 임베딩 (상속 아님)
Bonus float64
}
```
**해결: CLASS를 Go struct+interface로 변환하되 상속 시맨틱 보존**
```go
// 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 구조체로 정밀 매핑
```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 강점 활용
```go
// 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에서 불가능했던 것들
```go
// 최적화 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 락 호환성: 모든 스키마 지원
```go
// 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 정밀 이식
```go
// 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 알고리즘 정밀 이식
```go
// 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 엔진: 압축 알고리즘 보존
```go
// 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에서 불가능했던 인덱스 기능
```go
// 최적화 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 분할
```go
// 핵심 인터페이스: 필수 (모든 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 드라이버 등록
```go
// 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
```go
// 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 최적화
```
**인라이닝을 위한 설계:**
```go
// 나쁜 패턴: 거대한 메서드
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))
}
```
**이스케이프 분석을 위한 설계:**
```go
// 나쁜 패턴: 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 타입 힌트 활용 시 코드 품질 도약
```harbour
// 타입 힌트 없는 코드 (Level 1)
FUNCTION CalcTotal(aItems)
LOCAL nTotal := 0
FOR EACH item IN aItems
nTotal += item:price * item:qty
NEXT
RETURN nTotal
```
```go
// 생성되는 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()
}
```
```harbour
// 타입 힌트 있는 코드 (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
// 생성되는 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) | 동적 비즈니스 규칙 엔진 | 런타임 미니 컴파일러 |
| 코드 블록 ({&#124;&#124;...}) | 함수형 표현의 핵심 | 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 이식 전략 |