- 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>
1388 lines
40 KiB
Markdown
1388 lines
40 KiB
Markdown
# 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) | 동적 비즈니스 규칙 엔진 | 런타임 미니 컴파일러 |
|
||
| 코드 블록 ({||...}) | 함수형 표현의 핵심 | 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 이식 전략 |
|