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

40 KiB
Raw Permalink Blame History

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 근본 설계 차이
  2. Harbour 문법의 진짜 가치
  3. Go의 진짜 강점
  4. 융합 설계: 충돌 지점과 해결
  5. DBF 엔진 이식 전략
  6. Index 엔진 이식 전략
  7. RDD 아키텍처의 Go 재설계
  8. 컴파일러가 생성하는 코드의 품질
  9. 진화 방향: 무엇을 버리고 무엇을 살릴 것인가
  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 매크로 시스템: 런타임 코드 생성

// 필드 이름이 런타임에 결정되는 경우
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 이식 전략