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>
This commit is contained in:
2026-03-31 09:41:50 +09:00
commit 59568f3301
282 changed files with 66658 additions and 0 deletions

8
docs/.bkit-memory.json Normal file
View File

@@ -0,0 +1,8 @@
{
"sessionCount": 9,
"lastSession": {
"startedAt": "2026-03-30T07:45:13.930Z",
"platform": "claude",
"level": "Dynamic"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5295
docs/.pdca-status.json Normal file

File diff suppressed because it is too large Load Diff

403
docs/dbf-engine-spec.md Normal file
View File

@@ -0,0 +1,403 @@
# Five DBF Engine Specification
> Harbour DBF 소스 코드 정밀 분석 결과
> 바이트 레벨 포맷 호환을 위한 구현 사양
>
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
> All rights reserved.
>
> Source reference: /mnt/d/harbour-core/src/rdd/dbf1.c, include/hbdbf.h
---
## 1. DBF 헤더 (32 bytes)
```
Offset Size Field Description
────── ──── ───── ──────────
0 1 bVersion DB type: 0x03=std, 0x83=DBT, 0xF5=FPT, 0x30/31/32=VFP
1 1 bYear Last update year (year - 1900)
2 1 bMonth Last update month (1-12)
3 1 bDay Last update day (1-31)
4 4 ulRecCount Record count (LE uint32)
8 2 uiHeaderLen Total header length including terminators (LE uint16)
10 2 uiRecordLen Record length including deletion flag (LE uint16)
12 2 bReserved1 Reserved (0x00)
14 1 bTransaction 1=in transaction
15 1 bEncrypted 1=encrypted
16 12 bReserved2 Multi-user/LAN (12 bytes)
28 1 bHasTags 0x01=production index, 0x02=memo (VFP)
29 1 bCodePage Code page identifier
30 2 bReserved3 Reserved (0x00)
```
### Version byte values
```
0x03 Standard DBF III
0x04 DBF IV (reserved)
0x30 VFP (Visual FoxPro)
0x31 VFP + autoincrement
0x32 VFP + varchar/varbinary
0x83 DBF III + DBT memo
0xF5 DBF + FPT memo
0x8B DBF IV + DBT memo
0x06 Encrypted DBF
0x86 Encrypted + DBT
0xE6 Encrypted + SMT
0xF6 Encrypted + SMT
```
---
## 2. 필드 디스크립터 (32 bytes per field)
```
Offset Size Field Description
────── ──── ───── ──────────
0 11 bName Field name (null-terminated, space-padded)
11 1 bType Type char: C, N, L, D, M, I, B, T, @, +, =, ^, Y, Z, Q, V, P, W, G
12 4 bReserved1 Reserved (VFP: offset in record)
16 1 bLen Field length (max 255)
17 1 bDec Decimal places
18 1 bFieldFlags 0x01=system, 0x02=nullable, 0x04=binary
19 4 bCounter Autoincrement counter (LE)
23 1 bStep Autoincrement step
24 7 bReserved2 Reserved
31 1 bHasTag Has index tag (VFP)
```
### 헤더 종결자
```
- 필드 디스크립터 이후 0x0D (carriage return) 1 byte 종결자
- VFP: 추가 263 bytes 백필러 가능
- headerLen = 32(header) + fieldCount*32 + 1(0x0D) [+ backlink]
```
---
## 3. 필드 타입별 바이트 포맷
### C (Character)
```
저장: 원시 바이트, 우측 공백 패딩
읽기: 후행 공백 보존 (SET EXACT에 따라)
최대: 65535 bytes (bLen + bDec*256)
```
### N (Numeric)
```
저장: ASCII 문자열, 우측 정렬, 좌측 공백 패딩
형식: " -123.45" (폭 = bLen, 소수점 = bDec)
음수: '-' 기호 포함
빈 값: 모두 공백 (" ")
```
### L (Logical)
```
저장: 1 byte
참: 'T', 't', 'Y', 'y' (모두 true로 인식)
거짓: 'F', 'f', 'N', 'n' (모두 false로 인식)
미정: ' ' (space) = NIL
```
### D (Date)
```
표준 (bLen=8): "YYYYMMDD" ASCII (빈 날짜 = " ")
짧은 형식 (bLen=3): LE uint24 packed date
VFP (bLen=4): LE uint32 Julian day
```
### M (Memo)
```
bLen=4: LE uint32 block number
bLen=10: ASCII block number (우측 공백)
block number 0 = 빈 메모
```
### I (Integer — VFP)
```
bLen=1: signed int8
bLen=2: signed int16 LE
bLen=3: signed int24 LE
bLen=4: signed int32 LE
bLen=8: signed int64 LE
```
### B (Double — VFP)
```
bLen=8: IEEE 754 double (8 bytes LE)
```
### @ (Timestamp — VFP)
```
bLen=8: 4 bytes date (LE int32) + 4 bytes time (LE int32)
date = Julian day number
time = milliseconds since midnight
```
### = (Modtime)
```
동일 형식: @ (Timestamp)
자동 업데이트됨
```
### + (Autoincrement)
```
bLen=1/2/3/4/8: 부호 있는 정수 LE
자동 증가
```
### Y (Currency)
```
bLen=8: LE int64, 암묵적 4자리 소수점 (value / 10000.0)
```
### ^ (RowVersion)
```
bLen=8: LE uint64, 자동 버전 증가
```
---
## 4. 레코드 레이아웃
```
Byte 0: Deletion flag (' '=active, '*'=deleted)
Byte 1..N: Field data (각 필드가 연속 배치)
레코드 오프셋 = headerLen + (recNo - 1) * recordLen
EOF 마커 위치 = headerLen + recordLen * recordCount
EOF 값 = 0x1A
```
### 레코드 번호
```
1-based (첫 레코드 = 1)
0 = BOF (유효하지 않은 레코드)
recordCount + 1 = 유령 레코드 (APPEND용)
```
---
## 5. 락 스키마 (6종)
### 위치 계산 공식
```go
// 방향에 따른 레코드 락 위치:
switch direction {
case +1: // forward
lockPos = basePos + recNo
case -1: // backward (VFP with tags)
lockPos = basePos - recNo
case 2: // at record (VFP no tags)
lockPos = basePos + (recNo-1)*recordLen + headerLen
}
```
### 스키마별 상수
```
스키마 베이스 위치 방향 파일락 크기 레코드락 크기
───────── ────────────────────── ──── ──────────── ──────────
DB_DBFLOCK_CLIPPER
1,000,000,000 +1 294,967,295 1 byte
DB_DBFLOCK_CLIPPER2
4,000,000,000 +1 294,967,295 1 byte
DB_DBFLOCK_COMIX
1,000,000,000 +1 1 1 byte
DB_DBFLOCK_VFP (hasTags)
0x7FFFFFFE -1 0x07FFFFFF 1 byte
DB_DBFLOCK_VFP (noTags)
0x40000000 +2 0x3FFFFFFF recordLen
DB_DBFLOCK_HB32
4,000,000,000 +1 294,967,295 1 byte
DB_DBFLOCK_HB64
0x7F00000000000000 +1 0x00000000FFFFFFFE 1 byte
```
### 파일 락 (전체 테이블 잠금)
```go
// 파일 락 위치 = 베이스 위치
// 파일 락 크기 = 스키마별 상수 (위 표 참조)
fileLockPos = lockBasePos
fileLockSize = scheme.fileLockSize
```
### 레코드 락 (개별 레코드 잠금)
```go
// 레코드 락 위치 = 방향에 따라 계산
// 레코드 락 크기 = 1 byte (VFP noTags는 recordLen)
recLockPos = calculateRecLockPos(scheme, recNo)
recLockSize = scheme.recLockSize
```
### 헤더 락 (APPEND 시)
```go
// APPEND BLANK 시 다른 프로세스의 동시 APPEND 방지
headerLockPos = lockBasePos // 파일 락과 같은 위치
headerLockSize = 1
```
---
## 6. FPT 메모 파일
### FPT 헤더 (512 bytes)
```
Offset Size Field Description
────── ──── ───── ──────────
0 4 nextBlock Next free block (BE uint32)
4 2 reserved1 Reserved
6 2 blockSize Block size in bytes (BE uint16)
8 504 reserved2 Reserved (VFP: contains GC info at offset 536)
```
### 메모 블록 구조
```
Offset Size Field Description
────── ──── ───── ──────────
0 4 type Block type (BE uint32): 0=picture, 1=memo, 2=object
4 4 size Data size in bytes (BE uint32)
8 N data Actual memo data
```
### 블록 오프셋 계산
```go
blockOffset = int64(blockNumber) * int64(blockSize)
```
### 기본 블록 크기
```
DBT (dBASE): 512 bytes
FPT (FoxPro): 64 bytes (최소)
SMT (SIx): 32 bytes
```
---
## 7. OPEN 처리 순서
```
1. 파일 열기 (fOpen or fCreate)
2. 헤더 32 bytes 읽기
3. uiHeaderLen 검증 (>= 66, headerLen % 32 == 0 or 1)
4. 필드 디스크립터 읽기 (fieldCount = (headerLen - 32 - 1) / 32)
5. 각 필드의 offset 계산:
pFieldOffset[0] = 1 (deletion flag 이후)
pFieldOffset[i+1] = pFieldOffset[i] + field[i].bLen
6. recordLen 검증: pFieldOffset[fieldCount] == recordLen
7. 레코드 버퍼 할당 (recordLen bytes)
8. shared 모드면 recCount 재계산:
recCount = (fileSize - headerLen) / recordLen
9. 인덱스 파일이 있으면 자동 열기 (bHasTags 체크)
10. 메모 파일이 있으면 열기 (version byte 체크)
```
---
## 8. APPEND BLANK 처리
```
1. 파일 락/헤더 락 획득
2. shared 모드면 recCount 재계산
3. recCount++
4. 새 레코드 오프셋 = headerLen + (recCount - 1) * recordLen
5. 레코드 버퍼를 공백(' ')으로 초기화
6. 파일에 쓰기
7. EOF 마커(0x1A) 쓰기
8. 헤더의 recCount 갱신
9. 현재 위치를 새 레코드로 설정
10. 락 해제
```
---
## 9. PACK 처리
```
1. 배타적 잠금 필수 (fShared이면 에러)
2. 열린 인덱스 모두 닫기
3. ulRecOut = 0 (출력 카운터)
4. FOR recNo = 1 TO recCount:
레코드 읽기
IF NOT deleted:
ulRecOut++
레코드를 ulRecOut 위치에 쓰기
5. recCount = ulRecOut
6. 파일 크기 조정 (truncate)
7. EOF 마커 쓰기
8. 헤더 갱신
9. 인덱스 재빌드 (REINDEX)
```
---
## 10. 헤더 갱신 로직
```
갱신 시점:
- APPEND BLANK 이후
- PACK 이후
- CLOSE 시 (fUpdateHeader 플래그가 설정되어 있으면)
- FLUSH 시
갱신 내용:
- bYear/bMonth/bDay: 현재 날짜
- ulRecCount: 현재 레코드 수
- 32 bytes를 파일 오프셋 0에 쓰기
```
---
## 11. Go 구현 체크리스트
```
[ ] DBF 헤더 읽기/쓰기 (32 bytes, LE)
[ ] 필드 디스크립터 읽기/쓰기 (32 bytes × N)
[ ] 필드 타입별 GET (C, N, L, D, M, I, B, @, +, =, ^, Y)
[ ] 필드 타입별 PUT (역방향)
[ ] 레코드 오프셋 계산 (headerLen + (recNo-1) * recordLen)
[ ] 삭제 플래그 관리 (pRecord[0] = ' ' or '*')
[ ] EOF 마커 (0x1A) 읽기/쓰기
[ ] OPEN: 헤더 → 필드 → 오프셋 배열 → 버퍼 할당
[ ] CLOSE: 플러시 → 헤더 갱신 → 파일 닫기
[ ] APPEND BLANK: 락 → recCount++ → 빈 레코드 쓰기 → EOF → 헤더 갱신
[ ] DELETE/RECALL: pRecord[0] 변경
[ ] PACK: 배타적 → 순차 재작성 → truncate → 재인덱스
[ ] 6종 락 스키마 전부 구현
[ ] shared 모드에서 recCount 재계산
[ ] FPT 메모: 헤더 → 블록 읽기/쓰기
[ ] Harbour/Clipper로 생성한 DBF ↔ Five로 읽기 호환 테스트
[ ] Five로 생성한 DBF ↔ Harbour/Clipper로 읽기 호환 테스트
```
---
## 변경 이력
| 날짜 | 변경 내용 |
|------|----------|
| 2026-03-28 | 초기 작성. Harbour dbf1.c/hbdbf.h 정밀 분석 |

View File

@@ -0,0 +1,787 @@
# Five Development Plan
> Harbour + Go 융합 플랫폼 "Five" 개발 계획
> 4개 설계 문서 기반 실행 계획
>
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
> All rights reserved.
---
## 관련 문서
| 문서 | 내용 |
|------|------|
| harbour-type-system-analysis.md | HB_ITEM 타입 시스템 분석, Tagged Value 16B 설계 |
| harbour-prg-to-go-transpiler.md | PRG→Go 트랜스파일러, gencc.c 패턴 분석 |
| harbour-go-evolution-strategy.md | 융합 전략, 언어 진화, Go 생태계 연동 |
| harbour-go-compiler-design-review.md | 컴파일러 설계 관점 분석, DBF/Index 이식 전략 |
---
## 프로젝트 구조
```
five/
├── docs/ ← 설계 문서 (현재)
├── cmd/
│ └── five/ ← CLI 엔트리포인트
│ └── main.go five build, five run, five fmt
├── compiler/ ← PRG → Go 컴파일러
│ ├── token/ 토큰 정의
│ │ └── token.go
│ ├── lexer/ 렉서
│ │ ├── lexer.go
│ │ └── lexer_test.go
│ ├── ast/ AST 노드
│ │ └── ast.go
│ ├── parser/ 파서 (recursive descent)
│ │ ├── parser.go
│ │ ├── parser_expr.go
│ │ ├── parser_stmt.go
│ │ ├── parser_cmd.go xBase 명령어 파싱
│ │ └── parser_test.go
│ ├── analyzer/ 의미 분석
│ │ ├── scope.go
│ │ ├── types.go
│ │ └── analyzer.go
│ └── gengo/ Go 코드 생성
│ ├── gengo.go
│ ├── gen_expr.go
│ ├── gen_stmt.go
│ ├── gen_symbol.go
│ └── gengo_test.go
├── hbrt/ ← 핵심 런타임
│ ├── value.go Tagged Value 16B
│ ├── value_test.go
│ ├── thread.go 실행 컨텍스트
│ ├── stack.go eval 스택
│ ├── symbol.go 심볼 테이블
│ ├── class.go CLASS 시스템
│ ├── error.go 에러/SEQUENCE
│ ├── macro.go 매크로 컴파일러
│ ├── ops_arith.go 산술 연산
│ ├── ops_compare.go 비교 연산
│ ├── ops_string.go 문자열 연산
│ ├── bridge.go Go ↔ Five 타입 변환
│ └── vm.go VM 초기화/관리
├── hbrtl/ ← 표준 라이브러리 (Harbour RTL 호환)
│ ├── strings.go SUBSTR, ALLTRIM, UPPER, PAD, ...
│ ├── numeric.go STR, VAL, INT, ROUND, MOD, ...
│ ├── datetime.go DATE, TIME, CTOD, DTOC, YEAR, ...
│ ├── array.go AADD, ADEL, AINS, ASORT, AEVAL, ...
│ ├── hash.go HB_HASH, HB_HGET, HB_HSET, ...
│ ├── console.go QOUT, QQOUT, ACCEPT, INKEY, ...
│ ├── file.go FOPEN, FCLOSE, FREAD, FWRITE, ...
│ ├── convert.go ASC, CHR, CTOD, DTOC, STOD, ...
│ └── misc.go TYPE, VALTYPE, EMPTY, ...
├── hbrdd/ ← RDD 엔진 (DBF/Index)
│ ├── driver.go Driver/Area interface 정의
│ ├── workarea.go WorkArea 관리
│ ├── alias.go ALIAS 시스템
│ ├── dbf/ DBF 드라이버
│ │ ├── header.go 헤더/필드 디스크립터
│ │ ├── record.go 레코드 읽기/쓰기
│ │ ├── field.go 필드 타입 변환
│ │ ├── lock.go 6종 락 스키마
│ │ ├── memo.go FPT 메모 필드
│ │ └── dbf.go DBF Area 구현
│ ├── ntx/ NTX 인덱스
│ │ ├── header.go NTX 헤더 (512B)
│ │ ├── page.go B-tree 페이지 (1024B)
│ │ ├── search.go SEEK 알고리즘
│ │ ├── update.go 삽입/삭제/밸런싱
│ │ ├── build.go INDEX ON (병렬 빌드)
│ │ └── ntx.go NTX Indexer 구현
│ ├── cdx/ CDX 인덱스
│ │ ├── header.go CDX 헤더 (1024B)
│ │ ├── tag.go 태그 관리
│ │ ├── page.go 페이지 (512-8192B)
│ │ ├── compress.go 비트 패킹 압축/해제
│ │ ├── search.go SEEK 알고리즘
│ │ ├── update.go 삽입/삭제
│ │ └── cdx.go CDX Indexer 구현
│ └── filter.go SET FILTER / SET RELATION
├── hbsql/ ← SQL RDD (Phase 5)
│ └── sqldriver.go
├── hbweb/ ← HTTP 프레임워크 (Phase 5)
│ └── server.go
├── go.mod
├── go.sum
└── README.md
```
---
## Phase 0: 프로젝트 기반 (1주)
### 목표
Go 모듈 초기화, 기본 구조 확립, Tagged Value 16B 구현 및 검증
### 작업
```
0.1 Go 모듈 초기화
- go mod init github.com/anthropics/five (또는 개인 레포)
- 디렉토리 구조 생성
- .gitignore, LICENSE, README.md
0.2 Tagged Value 16B 구현 (hbrt/value.go)
- Value struct { data uint64; info uint64 }
- 타입 상수 정의 (tNil, tLogical, tInt, tLong, tDouble, ...)
- 생성 함수 (MakeNil, MakeBool, MakeInt, MakeLong, MakeDouble, ...)
- 접근 함수 (Type, IsNil, IsNumeric, AsInt, AsDouble, ...)
- 포인터 타입 (MakeString, MakeArray, MakeHash, MakeBlock)
- HbString, HbArray, HbHash, HbBlock 보조 구조체
0.3 Value 테스트
- 모든 타입의 생성/접근 왕복 테스트
- 타입 체크 매크로 검증
- 메모리 레이아웃 검증 (unsafe.Sizeof == 16)
- 벤치마크: Value 연산 vs interface{} 비교
```
### 완료 기준
```
go test ./hbrt/ -v -run TestValue -bench BenchmarkValue
✓ 14개 타입 생성/접근 테스트 통과
✓ sizeof(Value) == 16 확인
✓ 정수 연산 벤치마크: interface{} 대비 2배+ 빠름
```
---
## Phase 1: 최소 런타임 (2주)
### 목표
수동으로 작성한 Go 코드에서 `? "Hello World"`, `? 1 + 2`가 실행되는 것
### 작업
```
1.1 Thread + Stack (hbrt/thread.go, stack.go)
- Thread 구조체 (stack, sp, locals, calls)
- Frame / EndProc (defer + recover)
- Push / Pop / Peek / SetTop
- PushLocal / PopLocal / PushStatic / PopStatic
1.2 산술 연산 (hbrt/ops_arith.go)
- Plus, Minus, Mult, Divide, Modulus, Power
- Negate, Inc, Dec
- 타입 승격 규칙 (Int+Int→오버플로우→Double)
- 소수점 전파 규칙 (decimal 메타)
- AddInt, MultByInt 최적화 함수
1.3 비교 연산 (hbrt/ops_compare.go)
- Equal, ExactEqual, NotEqual
- Less, LessEqual, Greater, GreaterEqual
- Not, And, Or
- PopLogical (bool 추출)
1.4 문자열 연산 (hbrt/ops_string.go)
- 문자열 연결 (Plus에서 분기)
- 문자열 비교 (Harbour 의미론: SET EXACT 고려)
1.5 심볼 테이블 (hbrt/symbol.go)
- Symbol 구조체
- Module (심볼 배열 + 이름)
- Registry (전역 심볼 테이블, sync.RWMutex)
- Find, Register, At
1.6 함수 호출 (hbrt/thread.go 확장)
- PushSymbol, PushNil
- Function(nArgs), Do(nArgs)
- RetValue, RetInt, RetNil
- CallFrame 저장/복원
1.7 기본 RTL (hbrtl/console.go, strings.go, numeric.go)
- QOut (?) / QQOut (??)
- Str, Val, Len, Type, ValType
- SubStr, Upper, Lower, AllTrim, PadR, PadL, PadC
- Empty, Space, Replicate
1.8 VM 초기화 (hbrt/vm.go)
- NewVM, RegisterModule, RegisterRTL
- Run(funcName)
```
### 완료 기준
```go
// 이 Go 코드가 동작해야 함 (컴파일러 없이 수동 작성)
func HB_MAIN(t *hbrt.Thread) {
t.Frame(0, 1)
defer t.EndProc()
// ? "Hello World"
t.PushSymbol(sym_QOUT)
t.PushNil()
t.PushString("Hello World")
t.Function(1)
// LOCAL n := 10 + 20
t.PushInt(10)
t.AddInt(20)
t.PopLocal(1)
// ? "Result:", Str(n)
t.PushSymbol(sym_QOUT)
t.PushNil()
t.PushString("Result: ")
t.PushSymbol(sym_STR)
t.PushNil()
t.PushLocal(1)
t.Function(1)
t.Plus()
t.Function(1)
}
// 실행 결과:
// Hello World
// Result: 30
```
---
## Phase 2: 파서 (3주)
### 목표
PRG 파일을 파싱하여 AST를 생성
### 작업
```
2.1 토큰 정의 (compiler/token/token.go)
- 키워드: FUNCTION, PROCEDURE, LOCAL, STATIC, PRIVATE, PUBLIC
- 키워드: IF, ELSEIF, ELSE, ENDIF, DO, WHILE, ENDDO
- 키워드: FOR, NEXT, RETURN, EXIT, LOOP
- 키워드: BEGIN, SEQUENCE, RECOVER, END
- 키워드: CLASS, ENDCLASS, DATA, METHOD, INHERIT
- xBase: USE, SEEK, REPLACE, APPEND, INDEX, SET, GO, SKIP
- 연산자: +, -, *, /, %, **, :=, ==, !=, <, >, <=, >=
- 연산자: .AND., .OR., .NOT., .T., .F.
- 특수: &, @, ::, ->, {|, |}
2.2 렉서 (compiler/lexer/lexer.go)
- UTF-8 소스 처리
- Harbour 키워드 대소문자 무시
- 문자열 리터럴 ("...", '...')
- 숫자 리터럴 (정수, 소수, 16진수)
- 날짜 리터럴 (CTOD("YYYYMMDD"))
- 줄 바꿈 = 문장 구분자
- 세미콜론 (;) = 줄 계속
- 주석 (// 또는 && 또는 /* */)
2.3 AST 정의 (compiler/ast/ast.go)
- Node, Expr, Stmt, Decl 인터페이스
- 식: BinaryExpr, UnaryExpr, CallExpr, SendExpr, IndexExpr
- 식: LiteralExpr (Int, Double, String, Bool, Date, Nil, Array, Hash)
- 식: IdentExpr, FieldExpr, AliasExpr, MacroExpr, BlockExpr
- 문: AssignStmt, ReturnStmt, ExprStmt
- 문: IfStmt, DoWhileStmt, ForStmt, ForEachStmt, SwitchStmt
- 문: SeqStmt (BEGIN SEQUENCE)
- 선언: FuncDecl, VarDecl (LOCAL/STATIC/PRIVATE/PUBLIC)
- 선언: ClassDecl, DataDecl, MethodDecl
- xBase: UseCmd, SeekCmd, ReplaceCmd, AppendCmd, IndexCmd
- xBase: GoCmd, SkipCmd, SetCmd, SelectCmd
2.4 파서 (compiler/parser/)
- parser.go: Parser 구조체, advance, expect, match
- parser_expr.go: 식 파싱 (연산자 우선순위, Pratt 파서)
- parser_stmt.go: 문 파싱 (IF, DO WHILE, FOR, ...)
- parser_cmd.go: xBase 명령어 파싱 (USE, SEEK, REPLACE, ...)
- 에러 복구: synchronize() (LSP 대비)
2.5 파서 테스트
- 기본 식: 1 + 2 * 3
- 함수 호출: Func(a, b, c)
- 메서드: obj:Method(args)
- xBase: USE customers VIA DBFCDX
- 코드 블록: {|x,y| x + y}
- 매크로: &cVariable
- 제어 흐름: IF/ELSEIF/ELSE/ENDIF
- CLASS: CLASS Person ... ENDCLASS
```
### 완료 기준
```
five parse test.prg --dump-ast
입력: FUNCTION Main()
LOCAL n := 10
? n + 5
RETURN n
출력: FuncDecl{
Name: "Main"
Params: []
Body: [
VarDecl{Scope:LOCAL, Name:"n", Init: Literal{Int:10}}
ExprStmt{Call{Func:"QOUT", Args:[Binary{+, Ident{"n"}, Literal{Int:5}}]}}
ReturnStmt{Expr: Ident{"n"}}
]
}
```
---
## Phase 3: 코드 생성기 (3주)
### 목표
PRG → Go 소스 생성 → go build → 실행
### 작업
```
3.1 의미 분석 (compiler/analyzer/)
- 스코프 해석: LOCAL, STATIC, PRIVATE, PUBLIC 구분
- 심볼 수집: 함수, 변수, 클래스
- 상수 폴딩: 1 + 2 → 3
- 미사용 변수 경고
3.2 Go 코드 생성 (compiler/gengo/)
- gengo.go: 파일 헤더, import, main() 생성
- gen_expr.go: 식 → t.Push*() / t.Plus() 등
- gen_stmt.go: 문 → 제어 흐름 (goto 또는 for/if)
- gen_symbol.go: 심볼 테이블, STATIC 초기화 생성
3.3 CLI 통합 (cmd/five/main.go)
- five build <file.prg> [-o output]
- five run <file.prg>
- 내부: PRG → 임시 Go → go build → 실행
3.4 END-TO-END 테스트
- hello.prg → hello 바이너리
- 산술 테스트: 오버플로우, 소수점 전파
- 문자열 테스트: 연결, 비교
- 제어 흐름: IF, DO WHILE, FOR, FOR EACH
- 함수: 재귀, 다중 파라미터, STATIC 변수
- BEGIN SEQUENCE / RECOVER
```
### 완료 기준
```
five run hello.prg
// hello.prg:
FUNCTION Main()
LOCAL cName := "World"
LOCAL n := 0
FOR i := 1 TO 10
n += i
NEXT
? "Hello, " + cName + "!"
? "Sum 1..10 =", n
IF n > 50
? "Greater than 50"
ELSE
? "Not greater than 50"
ENDIF
RETURN NIL
// 출력:
// Hello, World!
// Sum 1..10 = 55
// Greater than 50
```
---
## Phase 4: RTL 확장 + 코드 블록 (3주)
### 목표
Harbour 핵심 RTL 함수 100개 + 코드 블록 + 배열/해시 연산
### 작업
```
4.1 배열 연산 (hbrtl/array.go)
- AAdd, ADel, AIns, ASize, AClone, ACopy
- ASort, AEval, AScan, ATail
- Array(), ALen (= Len)
4.2 해시 연산 (hbrtl/hash.go)
- hb_Hash, hb_HGet, hb_HSet, hb_HDel
- hb_HHasKey, hb_HKeys, hb_HValues
- hb_HPos, hb_HLen
4.3 코드 블록 (hbrt/thread.go 확장)
- PushBlock(func, capturedLocals)
- EvalBlock(nArgs)
- 디태치된 로컬 (클로저 캡처)
4.4 추가 RTL 함수
- 문자열: At, Rat, Stuff, StrTran, hb_StrReplace
- 수치: Abs, Max, Min, Sqrt, Log, Exp, Round, Int
- 날짜: Date, Time, Year, Month, Day, CToD, DToC, DToS, SToD
- 변환: Asc, Chr, Bin2I, I2Bin, hb_NumToHex
- 파일: File, FErase, FRename, DirChange, CurDir
- 기타: Seconds, OS, GetEnv, hb_Run
4.5 Harbour 호환 테스트
- Harbour 테스트 스위트에서 RTL 관련 테스트 이식
- 엣지 케이스: 빈 배열, NIL 파라미터, 타입 변환
```
### 완료 기준
```harbour
// 이 코드가 동작해야 함
FUNCTION Main()
LOCAL aData := { {"Kim", 30}, {"Lee", 25}, {"Park", 35} }
// 정렬
ASort(aData, {|a,b| a[2] < b[2]})
// 출력
AEval(aData, {|x| QOut(x[1] + " age:" + Str(x[2])) })
// 해시
LOCAL hConfig := { "host" => "localhost", "port" => 8080 }
? hConfig["host"] + ":" + Str(hConfig["port"])
RETURN NIL
```
---
## Phase 5: RDD — DBF 엔진 (4주)
### 목표
기존 DBF/NTX/CDX 파일을 읽고 쓸 수 있는 RDD 엔진.
**기존 Harbour/Clipper와 포맷 100% 호환.**
### 작업
```
5.1 RDD Interface (hbrdd/driver.go)
- Driver, Area, Indexer, Locker 등 인터페이스 정의
5.2 WorkArea 관리 (hbrdd/workarea.go, alias.go)
- WorkAreaManager (Thread-local)
- ALIAS 등록/해제/전환
5.3 DBF 코어 (hbrdd/dbf/)
- header.go: DBF 헤더 읽기/쓰기 (32B, 바이트 동일)
- field.go: 필드 디스크립터 (32B×N)
- record.go: 레코드 읽기/쓰기 (고정 폭)
- lock.go: 6종 락 스키마 (Clipper/VFP/HB64)
- memo.go: FPT 메모 블록 읽기/쓰기
- dbf.go: DBFArea 구현 (Open/Create/Close/GoTo/Skip/...)
5.4 NTX 인덱스 (hbrdd/ntx/)
- header.go: NTX 헤더 (512B)
- page.go: B-tree 페이지 (1024B)
- search.go: SEEK (이진 검색 + 스택 탐색)
- update.go: 삽입/삭제/페이지 분할/밸런싱
- build.go: INDEX ON (병렬 정렬 + 바텀업 빌드)
5.5 CDX 인덱스 (hbrdd/cdx/)
- header.go: CDX 파일 헤더 (1024B)
- tag.go: 태그 헤더 (512B) + 다중 태그 관리
- compress.go: 비트 패킹 압축/해제 (DupBits/TrlBits/RecBits)
- page.go: 내부/리프 노드
- search.go: SEEK
- update.go: 삽입/삭제
5.6 xBase 명령어 연동 (컴파일러 + 런타임)
- USE path [VIA driver] [ALIAS name]
- GO TOP / GO BOTTOM / GO recno
- SKIP [n]
- SEEK value [SOFTSEEK]
- REPLACE field WITH value [, ...]
- APPEND BLANK
- DELETE / RECALL / PACK / ZAP
- INDEX ON expr TO file [FOR cond] [UNIQUE]
- SET INDEX TO file
- SET FILTER TO expr
- SET RELATION TO expr INTO alias
- SELECT alias
- FIELD->name / alias->name
5.7 호환성 테스트
- Harbour로 생성한 DBF → Five로 읽기
- Five로 생성한 DBF → Harbour로 읽기
- NTX/CDX 인덱스 교차 읽기
- 락 동시 접근 테스트 (Five + Harbour 프로세스)
```
### 완료 기준
```harbour
// 기존 Harbour DBF 파일을 Five로 그대로 사용
FUNCTION Main()
USE customers VIA DBFCDX
SET INDEX TO cust_name
// 검색
SEEK "SMITH"
IF Found()
? FIELD->name, FIELD->salary
REPLACE salary WITH salary * 1.1
ENDIF
// 순회
GO TOP
DO WHILE !EOF()
IF FIELD->country == "KR"
? FIELD->name
ENDIF
SKIP
ENDDO
// 인덱스 생성
INDEX ON UPPER(name) TO temp_idx
USE
RETURN NIL
```
---
## Phase 6: OOP + 매크로 (3주)
### 목표
CLASS 문법과 매크로 시스템 동작
### 작업
```
6.1 CLASS 시스템 (hbrt/class.go)
- ClassDef 구조체 (이름, DATA 목록, METHOD 목록)
- ClassRegistry (sync.RWMutex)
- 인스턴스 생성 (New)
- 메서드 디스패치 (Send)
- 상속 (INHERIT FROM)
- 연산자 오버로딩
6.2 CLASS 파서/코드 생성
- CLASS ... ENDCLASS 파싱
- DATA 선언 → Go struct 필드
- METHOD 선언 → Go 메서드
- INHERIT FROM → Go 임베딩
- :: (Self 접근) → Go receiver
6.3 매크로 컴파일러 (hbrt/macro.go)
- 미니 렉서 + 파서 (식 전용)
- &variable → 런타임 파싱 + 실행
- &(expression) → 런타임 파싱 + 실행
6.4 PP 전처리기 (compiler/pp/)
- #include, #define, #ifdef/#endif
- #command / #translate (xBase 명령어 정의)
- #pragma compatibility(...)
```
### 완료 기준
```harbour
CLASS Person
DATA cName INIT ""
DATA nAge INIT 0
METHOD New(cName, nAge) CONSTRUCTOR
METHOD Greet()
ENDCLASS
METHOD New(cName, nAge) CLASS Person
::cName := cName
::nAge := nAge
RETURN Self
METHOD Greet() CLASS Person
? "Hello, I'm " + ::cName + " (" + Str(::nAge) + ")"
RETURN Self
FUNCTION Main()
LOCAL oPerson := Person():New("Kim", 30)
oPerson:Greet()
// 매크로
LOCAL cField := "cName"
? oPerson:&cField // "Kim"
RETURN NIL
```
---
## Phase 7: Go 생태계 연동 (3주)
### 목표
IMPORT 문으로 Go 패키지를 PRG에서 직접 사용
### 작업
```
7.1 IMPORT 문법 (컴파일러)
- IMPORT "net/http"
- IMPORT "encoding/json"
- IMPORT "github.com/..."
- Go 타입을 Five에서 사용하는 브릿지 생성
7.2 타입 브릿지 (hbrt/bridge.go)
- ToGoValue(Value) interface{}
- FromGoValue(interface{}) Value
- Marshal / Unmarshal (구조체 ↔ Hash)
7.3 동시성 프리미티브
- GO 키워드 → goroutine
- CHANNEL(n) → make(chan Value, n)
- SEND(ch, val) → ch <- val
- RECEIVE(ch) → <-ch
- WAITGROUP → sync.WaitGroup wrapper
7.4 HTTP 프레임워크 (hbweb/)
- hbweb.New() → 라우터
- GET/POST/PUT/DELETE 라우팅
- JSON 응답
- 미들웨어
7.5 SQL RDD (hbsql/)
- database/sql 기반
- PostgreSQL, MySQL, SQLite 드라이버
- xBase 명령어로 SQL 테이블 조작
```
### 완료 기준
```harbour
IMPORT "encoding/json"
FUNCTION Main()
// HTTP 서버
LOCAL oApp := hbweb.New()
oApp:GET("/api/customers", {|ctx|
USE customers VIA DBFCDX
LOCAL aResult := {}
GO TOP
DO WHILE !EOF()
AAdd(aResult, { "name" => FIELD->name, "city" => FIELD->city })
SKIP
ENDDO
USE
ctx:JSON(200, aResult)
})
// goroutine으로 병렬 처리
LOCAL ch := CHANNEL(10)
GO BackgroundTask(ch)
? "Server starting on :8080"
oApp:Listen(":8080")
RETURN NIL
```
---
## Phase 8: 개발 도구 (2주)
### 목표
개발자 경험 완성: 포매터, LSP, 테스트 프레임워크
### 작업
```
8.1 five fmt — 코드 포매터
- 들여쓰기 정규화
- 키워드 대소문자 통일
- 줄 바꿈 규칙
8.2 five lsp — Language Server
- textDocument/completion (자동 완성)
- textDocument/definition (정의로 이동)
- textDocument/hover (타입 정보)
- textDocument/diagnostics (에러 표시)
- 증분 파싱 (파일 변경 시 부분 재파싱)
8.3 five test — 테스트 프레임워크
- ASSERT 함수
- 테스트 파일 자동 발견 (*_test.prg)
- 벤치마크 지원
8.4 VSCode 확장
- 구문 강조 (TextMate grammar)
- LSP 클라이언트 연결
- 스니펫
- 빌드 태스크
8.5 five migrate — 마이그레이션 도구
- 기존 PRG 분석
- 자동 수정 가능 항목 변환
- 수동 수정 필요 항목 보고
```
---
## 일정 요약
```
Phase 0: 프로젝트 기반 ·········· 1주 ██
Phase 1: 최소 런타임 ··········· 2주 ████
Phase 2: 파서 ················· 3주 ██████
Phase 3: 코드 생성기 ··········· 3주 ██████
Phase 4: RTL + 코드 블록 ······· 3주 ██████
Phase 5: RDD (DBF/NTX/CDX) ···· 4주 ████████
Phase 6: OOP + 매크로 ·········· 3주 ██████
Phase 7: Go 생태계 연동 ········ 3주 ██████
Phase 8: 개발 도구 ············ 2주 ████
────
합계 24주 (약 6개월)
```
```
마일스톤:
Month 1 끝: "Hello World" 실행 (Phase 0-1)
Month 2 끝: PRG 파싱 완료 (Phase 2)
Month 3 끝: PRG → 실행 가능한 바이너리 (Phase 3)
Month 4 끝: 실용적 프로그래밍 가능 (Phase 4)
Month 5 끝: DBF 완전 호환 (Phase 5) ← 핵심 마일스톤
Month 6 끝: 전체 기능 + 도구 (Phase 6-8)
```
---
## 우선순위 원칙
```
1. Phase 5 (DBF/Index)가 가장 중요하다.
→ 이것이 없으면 Five는 Harbour 대체가 될 수 없다.
→ 포맷 호환이 깨지면 기존 사용자가 올 수 없다.
2. Phase 1-3은 Phase 5의 토대이다.
→ 런타임과 컴파일러가 있어야 DBF를 테스트할 수 있다.
→ 최소한의 기능으로 빠르게 통과한다.
3. Phase 7 (Go 연동)이 Five의 미래를 결정한다.
→ DBF만 있으면 "Go로 만든 Harbour"일 뿐
→ Go 생태계 직접 접근이 있어야 "새로운 플랫폼"
4. Phase 8 (도구)이 개발자를 데려온다.
→ 기능이 아무리 좋아도 도구가 없으면 사용하지 않는다.
```
---
## 변경 이력
| 날짜 | 변경 내용 |
|------|----------|
| 2026-03-27 | 초기 작성. 8 Phase, 24주 개발 계획 |

210
docs/five-intro-en.md Normal file
View File

@@ -0,0 +1,210 @@
# Five — Where Harbour Meets Go
> **Keep your xBase code. Gain all of Go.**
Five is a fusion language that transpiles Harbour PRG code to Go native binaries.
Don't throw away 30 years of xBase business logic — use Go's modern power with PRG syntax.
## Why Five
### 1. Your existing code just works
```prg
USE customers NEW
INDEX ON Upper(name) TO cust_name
SEEK "CHARLES"
? customers->name, customers->balance
```
This runs as-is. DBF, NTX, CDX — all supported.
Thousands of lines of existing PRG code build with Five unchanged.
### 2. Every Go package, directly from PRG
```prg
IMPORT "strings"
IMPORT "database/sql"
IMPORT _ "modernc.org/sqlite"
PROCEDURE Main()
LOCAL db, aRows, i
? strings.ToUpper("hello five!")
db := sql.Open("sqlite", ":memory:")
db:Exec("CREATE TABLE users (id INTEGER, name TEXT)")
db:Exec("INSERT INTO users VALUES (1, 'Charles')")
aRows := SqlScan(db, "SELECT * FROM users")
FOR i := 1 TO Len(aRows)
? aRows[i]["name"]
NEXT
db:Close()
RETURN
```
One `IMPORT` line gives you access to 500,000+ Go packages.
SQL, HTTP, WebSocket, JSON, crypto, regex — all from PRG code.
No `#pragma BEGINDUMP` needed.
### 3. Goroutines are PRG syntax
```prg
LOCAL ch := Channel()
SPAWN {|| ch <- HeavyWork() } // launch goroutine
? "doing other work..."
result := <- ch // receive result
WATCH
CASE msg := <- chServer1
? "Server 1 replied:", msg
CASE msg := <- chServer2
? "Server 2 replied:", msg
CASE <- chTimeout
? "Timeout!"
END WATCH
```
Concurrency that's impossible in Harbour — natural in Five.
`SPAWN`, `<-`, `WATCH` — these ARE Go's goroutine, channel, select.
### 4. Builds to native binary
```bash
five build myapp.prg -o myapp
./myapp # single executable, zero dependencies
```
No JVM. No interpreter. No runtime.
Go compiler produces native binary. Cross-compile to Linux, macOS, Windows.
### 5. Safe code by default
```prg
PROCEDURE Main()
LOCAL cName, nAge // all variables must be declared
cName := "Charles"
nAge := 30
? cName, nAge
DEFER db:Close() // guaranteed resource cleanup
RETURN
```
Five's compiler checks automatically:
- Undeclared variable → warning
- Unused variable → hint
- `DEFER` prevents resource leaks
## For Harbour Developers
**Nothing to change. Everything to gain.**
| Existing Harbour | Five adds |
|-----------------|-----------|
| DBF/NTX/CDX | + **SQLite, PostgreSQL, MySQL** |
| Single thread | + **goroutine parallelism** |
| C library dependency | + **500K Go packages** |
| Interpreter/HRB | + **native binary** |
| Windows focused | + **Linux, macOS, cloud** |
## For Go Developers
**Data processing, 10x faster to write.**
```prg
// Look how concise this is
USE sales NEW
INDEX ON DToS(date) + Str(amount) TO sales_idx
SET FILTER TO amount > 1000
GO TOP
DO WHILE !Eof()
? date, customer, amount
SKIP
ENDDO
```
In Go, this requires CSV parsing, struct definitions, sort interfaces, filter loops...
One xBase command replaces 20 lines of Go code.
## Key Numbers
```
Harbour compat: 98% (232/236 test files)
RTL functions: 351
Go interop: FastPath 15M calls/sec
Tests: 13 packages ALL PASS
Build output: single native binary
```
## Five-Only Syntax
```prg
// Multi-return
cName, nAge := GetUserInfo()
// DEFER — automatic cleanup
DEFER db:Close()
// Channel operators
ch <- "hello"
msg := <- ch
// WATCH — channel multiplexing
WATCH
CASE msg := <- ch1
CASE <- chTimeout
END WATCH
// Parallel FOR
PARALLEL FOR i := 1 TO 100000
aResult[i] := Process(aData[i])
NEXT
// ASYNC/AWAIT
future := ASYNC HeavyQuery()
result := AWAIT future
// Nil-safe
? customer?:address?:city
// f-string
? f"Name: {cName}, Age: {nAge}"
// Slice
aSub := aData[2:5]
// Direct Go package calls
? strings.ToUpper("hello")
? math.Sqrt(144)
? fmt.Sprintf("%.2f", 3.14)
```
## Getting Started
```bash
# Install
go install github.com/aspect-build/five@latest
# Run
five run hello.prg
# Build
five build hello.prg -o hello
# Debug
five debug hello.prg
```
```prg
// hello.prg
PROCEDURE Main()
? "Hello, Five!"
? f"Today: {Date()}"
RETURN
```
---
**Five — 30 years of xBase heritage, powered by Go's future.**

210
docs/five-intro-ko.md Normal file
View File

@@ -0,0 +1,210 @@
# Five — Harbour와 Go의 만남
> **기존 xBase 코드는 그대로, Go의 힘은 전부.**
Five는 Harbour PRG 코드를 Go 네이티브 바이너리로 변환하는 fusion 언어입니다.
30년간 쌓아온 xBase 비즈니스 로직을 버리지 않고, Go의 현대적 기능을 PRG 문법으로 사용합니다.
## 왜 Five인가
### 1. 기존 코드를 버리지 않습니다
```prg
USE customers NEW
INDEX ON Upper(name) TO cust_name
SEEK "CHARLES"
? customers->name, customers->balance
```
이 코드가 그대로 실행됩니다. DBF, NTX, CDX 전부 지원.
수천 줄의 기존 PRG 코드를 수정 없이 Five로 빌드할 수 있습니다.
### 2. Go의 모든 패키지를 PRG에서 직접 씁니다
```prg
IMPORT "strings"
IMPORT "database/sql"
IMPORT _ "modernc.org/sqlite"
PROCEDURE Main()
LOCAL db, aRows, i
? strings.ToUpper("hello five!")
db := sql.Open("sqlite", ":memory:")
db:Exec("CREATE TABLE users (id INTEGER, name TEXT)")
db:Exec("INSERT INTO users VALUES (1, 'Charles')")
aRows := SqlScan(db, "SELECT * FROM users")
FOR i := 1 TO Len(aRows)
? aRows[i]["name"]
NEXT
db:Close()
RETURN
```
`IMPORT` 한 줄이면 Go의 50만개 패키지에 접근합니다.
SQL, HTTP, WebSocket, JSON, 암호화, 정규식 — 전부 PRG 코드로.
`#pragma BEGINDUMP`가 필요 없습니다.
### 3. goroutine이 PRG 문법입니다
```prg
LOCAL ch := Channel()
SPAWN {|| ch <- HeavyWork() } // goroutine 시작
? "다른 작업 중..."
result := <- ch // 결과 수신
WATCH
CASE msg := <- chServer1
? "서버1 응답:", msg
CASE msg := <- chServer2
? "서버2 응답:", msg
CASE <- chTimeout
? "타임아웃!"
END WATCH
```
Harbour에서는 불가능한 동시성을 PRG 문법으로 자연스럽게.
`SPAWN`, `<-`, `WATCH` — Go의 goroutine, channel, select가 됩니다.
### 4. 네이티브 바이너리로 빌드됩니다
```bash
five build myapp.prg -o myapp
./myapp # 단일 실행 파일, 의존성 없음
```
JVM도 없고, 인터프리터도 없습니다.
Go 컴파일러가 만드는 네이티브 바이너리. Linux, macOS, Windows 크로스 컴파일.
### 5. 안전한 코드를 강제합니다
```prg
PROCEDURE Main()
LOCAL cName, nAge // 모든 변수는 선언 필수
cName := "Charles"
nAge := 30
? cName, nAge
DEFER db:Close() // 리소스 정리 보장
RETURN
```
Five 컴파일러가 자동으로 체크합니다:
- 선언 안 된 변수 → 경고
- 사용 안 한 변수 → 힌트
- `DEFER`로 리소스 누수 방지
## Harbour 개발자라면
**바꿀 것은 없고, 얻는 것만 있습니다.**
| 기존 Harbour | Five 추가 |
|-------------|-----------|
| DBF/NTX/CDX | + **SQLite, PostgreSQL, MySQL** |
| 단일 스레드 | + **goroutine 병렬 처리** |
| C 라이브러리 의존 | + **Go 패키지 50만개** |
| 인터프리터/HRB | + **네이티브 바이너리** |
| Windows 위주 | + **Linux, macOS, 클라우드** |
## Go 개발자라면
**데이터 처리가 10배 빨라집니다.**
```prg
// 이 코드가 얼마나 간결한지 보세요
USE sales NEW
INDEX ON DToS(date) + Str(amount) TO sales_idx
SET FILTER TO amount > 1000
GO TOP
DO WHILE !Eof()
? date, customer, amount
SKIP
ENDDO
```
Go에서 이걸 하려면 CSV 파싱, 구조체 정의, sort 인터페이스, 필터 루프...
Five는 xBase 명령어 한 줄이 Go 코드 20줄을 대체합니다.
## 핵심 숫자
```
Harbour 호환: 98% (232/236 테스트 파일)
RTL 함수: 351개
Go Interop: FastPath 15M calls/sec
테스트: 13개 패키지 ALL PASS
빌드 결과: 단일 네이티브 바이너리
```
## Five만의 문법
```prg
// Multi-return
cName, nAge := GetUserInfo()
// DEFER — 자동 정리
DEFER db:Close()
// 채널 연산자
ch <- "hello"
msg := <- ch
// WATCH — 채널 멀티플렉싱
WATCH
CASE msg := <- ch1
CASE <- chTimeout
END WATCH
// Parallel FOR
PARALLEL FOR i := 1 TO 100000
aResult[i] := Process(aData[i])
NEXT
// ASYNC/AWAIT
future := ASYNC HeavyQuery()
result := AWAIT future
// Nil-safe
? customer?:address?:city
// f-string
? f"Name: {cName}, Age: {nAge}"
// Slice
aSub := aData[2:5]
// Go 패키지 직접 호출
? strings.ToUpper("hello")
? math.Sqrt(144)
? fmt.Sprintf("%.2f", 3.14)
```
## 시작하기
```bash
# 설치
go install github.com/aspect-build/five@latest
# 실행
five run hello.prg
# 빌드
five build hello.prg -o hello
# 디버그
five debug hello.prg
```
```prg
// hello.prg
PROCEDURE Main()
? "Hello, Five!"
? f"Today: {Date()}"
RETURN
```
---
**Five — xBase의 30년 유산 위에 Go의 미래를 올립니다.**

400
docs/five-syntax-en.md Normal file
View File

@@ -0,0 +1,400 @@
# Five Language Syntax Reference
Five = 100% Harbour compatible + Go extended syntax.
Existing PRG code runs without modification, and Go's powerful features are available in PRG syntax.
## Harbour Compatible Syntax (98% parsing)
Full support for all Harbour/Clipper/xBase syntax:
```prg
FUNCTION, PROCEDURE, RETURN, LOCAL, STATIC, PRIVATE, PUBLIC
IF/ELSEIF/ELSE/ENDIF, DO CASE/CASE/OTHERWISE/ENDCASE
FOR/NEXT, FOR EACH/NEXT, DO WHILE/ENDDO
BEGIN SEQUENCE/RECOVER/END, SWITCH/CASE/ENDSWITCH
CLASS/DATA/METHOD/ACCESS/ASSIGN/ENDCLASS
USE, SELECT, SEEK, SKIP, GO, APPEND, REPLACE, DELETE, PACK
@ SAY/GET/READ, MENU TO, SET, INDEX ON
```
## Five Go Extensions
### 1. IMPORT — Direct Go Package Access
```prg
IMPORT "strings" // Go standard library
IMPORT "database/sql" // SQL database
IMPORT _ "modernc.org/sqlite" // blank import (driver registration)
IMPORT myhttp "net/http" // aliased import
```
After IMPORT, use directly from PRG:
```prg
IMPORT "strings"
PROCEDURE Main()
LOCAL cResult
cResult := strings.ToUpper("hello five") // Direct Go function call
? strings.Contains(cResult, "FIVE") // .T.
? strings.Split("a,b,c", ",") // {"a","b","c"}
RETURN
```
**No #pragma BEGINDUMP needed. IMPORT gives access to Go's entire ecosystem.**
### 2. Multi-Return — Multiple Return Values
```prg
// Return multiple values from a function
FUNCTION GetUserInfo()
RETURN "Charles", 30, "Seoul"
// Receive multiple values
cName, nAge, cCity := GetUserInfo()
// Discard unwanted values with blank identifier
_, nAge, _ := GetUserInfo()
```
Natural support for Go's `(val, error)` pattern:
```prg
IMPORT "database/sql"
db, err := sql.Open("sqlite", ":memory:")
IF err != NIL
? "Error:", err
ENDIF
```
### 3. DEFER — Automatic Cleanup
Executes when the function returns. Guaranteed even on errors.
```prg
PROCEDURE ProcessFile(cPath)
LOCAL db
db := sql.Open("sqlite", cPath)
DEFER db:Close() // Auto-Close when function ends
db:Exec("INSERT ...") // Even if error occurs here
db:Exec("UPDATE ...") // db:Close() will always execute
RETURN // ← DEFER executes here
```
More concise than Harbour's `BEGIN SEQUENCE/RECOVER`:
```
Before (Harbour): After (Five):
─────────────────────────────── ─────────────────────
BEGIN SEQUENCE db := SqlOpen(...)
db := SqlOpen(...) DEFER db:Close()
db:Exec(...) db:Exec(...)
RECOVER RETURN
db:Close()
END SEQUENCE
db:Close()
```
### 4. Slice — Sub-array / Sub-string
```prg
LOCAL aData := {"a", "b", "c", "d", "e"}
aSub := aData[2:4] // {"b", "c", "d"}
aSub := aData[3:] // {"c", "d", "e"} (from 3 to end)
aSub := aData[:2] // {"a", "b"} (from start to 2)
```
Replaces verbose Harbour loops:
```
Before: After:
─────────────────────────────── ─────────────────────
LOCAL aSub := {} aSub := aData[3:7]
FOR i := 3 TO 7
AAdd(aSub, aData[i])
NEXT
```
### 5. Parallel Assignment — Simultaneous Assign
```prg
// Swap values (no temp variable needed!)
a, b := b, a
// Simultaneous initialization
x, y, z := 1, 2, 3
```
### 6. Nil-Safe Operator — `?:`
```prg
// Before: repeated NIL checks
IF oCustomer != NIL
IF oCustomer:Address != NIL
? oCustomer:Address:City
ENDIF
ENDIF
// Five: one line
? oCustomer?:Address?:City // Returns NIL if any part is NIL
```
### 7. String Interpolation — `f"..."`
```prg
LOCAL cName := "Charles", nAge := 30
// Before
? "Name: " + cName + " Age: " + Str(nAge)
// Five
? f"Name: {cName}, Age: {nAge}"
// With format specifiers
? f"Price: {nPrice:.2f}, Count: {nCount:05d}"
```
### 8. CONST Block — Constants / Enums
```prg
CONST
STATUS_ACTIVE := 1
STATUS_CLOSED := 2
STATUS_PENDING := 3
END CONST
```
### 9. SWITCH (Harbour compatible + extended)
```prg
// Standard Harbour syntax works as-is
SWITCH nStatus
CASE 1
? "Active"
CASE 2
? "Closed"
OTHERWISE
? "Unknown"
ENDSWITCH
```
## Five Concurrency Syntax
### 10. Channel Operators — `<-`
```prg
ch := Channel()
ch <- "hello" // Send to channel
msg := <- ch // Receive from channel
```
Harbour functions vs Five operators:
```
Harbour functions: Five operators:
─────────────────────────────── ─────────────────────
ChSend(ch, "hello") ch <- "hello"
msg := ChReceive(ch) msg := <- ch
ChSend(chOut, nResult) chOut <- nResult
```
### 11. SPAWN / LAUNCH / GOROUTINE — Inline Goroutine
Three keywords, same behavior — choose your preference:
```prg
SPAWN {|| DoHeavyWork() }
LAUNCH {|| ProcessData() }
GOROUTINE {|| SendNotification() }
```
### 12. WATCH — Channel Multiplexing (Go select)
Monitor multiple channels simultaneously, process the first one ready:
```prg
WATCH
CASE msg := <- chMessages // Message arrived
? "Message:", msg
CASE result := <- chResults // Result arrived
? "Result:", result
CASE <- chTimeout // Timeout
? "Timeout!"
OTHERWISE // No channel ready
? "No channel ready"
END WATCH
```
**Real-world pattern: Select fastest server response**
```prg
SPAWN {|| DelayAndSend(0.1, chFast, "Fast Server") }
SPAWN {|| DelayAndSend(2.0, chSlow, "Slow Server") }
SPAWN {|| DelayAndSend(3.0, chTimeout, "TIMEOUT") }
WATCH
CASE cResult := <- chFast
? "Winner:", cResult // ← Selected (fastest at 100ms)
CASE cResult := <- chSlow
? "Winner:", cResult
CASE <- chTimeout
? "Timeout!"
END WATCH
```
### 13. PARALLEL FOR — Parallel Loop
```prg
// Process 100K items across all CPU cores
PARALLEL FOR i := 1 TO 100000
aResult[i] := ProcessItem(aData[i])
NEXT
// Automatically waits for all goroutines to complete
```
### 14. ASYNC / AWAIT — Asynchronous Execution
```prg
// Start heavy work in background
future := ASYNC HeavyQuery("SELECT * FROM big_table")
// Do other work (non-blocking)
? "Loading..."
PrepareUI()
// Wait for result
aRows := AWAIT future
? "Got", Len(aRows), "rows"
```
### 15. WITH TIMEOUT — Timeout Context
```prg
// Auto-cancel if not completed within 3 seconds
WITH TIMEOUT 3
result := SlowNetworkCall()
END
IF result == NIL
? "Timeout!"
ENDIF
```
## Direct Go Object Manipulation
### `pkg.Func()` — Package Function Calls
```prg
IMPORT "strings"
IMPORT "math"
IMPORT "fmt"
? strings.ToUpper("hello") // "HELLO"
? math.Sqrt(144) // 12
? fmt.Sprintf("%.2f", 3.14159) // "3.14"
```
### `obj:Method()` — Go Object Method Calls
```prg
IMPORT "database/sql"
db := sql.Open("sqlite", ":memory:")
db:Exec("CREATE TABLE test (id INTEGER)")
rows := db:Query("SELECT * FROM test")
DO WHILE rows:Next()
? rows:Column(1)
END
rows:Close()
db:Close()
```
### Multiple Go Objects Simultaneously
```prg
dbSource := sql.Open("sqlite", "source.db")
dbTarget := sql.Open("sqlite", "target.db")
aRows := SqlScan(dbSource, "SELECT * FROM products")
FOR i := 1 TO Len(aRows)
dbTarget:Exec("INSERT INTO inventory VALUES (...)")
NEXT
dbSource:Close()
dbTarget:Close()
```
## Five vs Competitors
### xBase Family Comparison
| Feature | Harbour | xHarbour | FiveWin | **Five** |
|---------|---------|----------|---------|----------|
| DBF/NTX/CDX | Yes | Yes | Yes | **Yes** |
| SQL Database | No | Limited | ODBC | **All Go DBs** |
| HTTP Server | No | No | No | **net/http** |
| WebSocket | No | No | No | **Yes** |
| Goroutine | No | No | No | **Native** |
| Channel `<-` | No | No | No | **Yes** |
| JSON | Limited | Limited | Limited | **Go encoding/json** |
| Cross-platform | Partial | Partial | Windows | **Linux/Mac/Windows** |
| Package ecosystem | C libs | C libs | C libs | **All Go packages** |
### Transpiler Comparison
| Feature | TypeScript→JS | Kotlin→JVM | **Five (PRG→Go)** |
|---------|---------------|------------|---------------------|
| Type system | Static→Dynamic | Static→Static | Dynamic→Static |
| Concurrency | async/await | coroutine | **goroutine+channel** |
| External packages | npm | Maven | **Go modules** |
| Build output | JS code | bytecode | **Native binary** |
| Interop | Direct JS | Direct Java | **Direct Go (IMPORT)** |
| Performance | V8 runtime | JVM runtime | **Native speed** |
### What Makes Five Unique
1. **IMPORT gives access to all of Go** — No #pragma BEGINDUMP needed
2. **Native binary output** — Single executable, no JVM or V8 required
3. **goroutine + channel + WATCH** — Full Go concurrency in PRG syntax
4. **100% xBase compatible** — Existing DBF/NTX/CDX code runs as-is
5. **FastPath optimization** — Go function calls within 2x of native performance
6. **DEFER** — Safe resource management, cleaner than BEGIN SEQUENCE
7. **Multi-Return**`a, b := Func()`, natural Go (val, error) pattern
8. **f-string** — String interpolation, `f"Hello {name}"`
9. **PARALLEL FOR** — Automatic parallel processing of large datasets
10. **Nil-safe `?:`** — Safe chaining, no runtime errors from NIL
## Performance
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Call Type Direct Go Reflect FastPath
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
strings.ToUpper 34ns 242ns 66ns
strings.Contains 3ns 218ns 19ns
math.Sqrt 0.1ns 173ns 16ns
obj:Method() — 412ns 235ns
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Throughput: 15M calls/sec (FastPath), 4.3M calls/sec (Method)
Stress tested: 40K calls, 1MB strings, 10K arrays,
20K concurrent goroutines, 5K random fuzz
```
## Example Files
| File | Description |
|------|-------------|
| `examples/go_native.prg` | Direct Go package usage with IMPORT only |
| `examples/go_strings.prg` | Full strings package utilization |
| `examples/go_typetest.prg` | 18 type conversion tests |
| `examples/go_dual_db.prg` | Two SQLite databases simultaneously |
| `examples/go_channel.prg` | Channel operators + WATCH + Pipeline |
| `examples/go_httpserver.prg` | REST API server |
| `examples/go_concurrent.prg` | Parallel data pipeline |
| `examples/go_websocket.prg` | WebSocket chat server |
| `examples/go_extensions.prg` | All 9 extension syntax demo |
| `examples/godump_demo.prg` | HB_FUNC Go API |

382
docs/five-syntax-ko.md Normal file
View File

@@ -0,0 +1,382 @@
# Five Language Syntax Reference
Five = Harbour 100% 호환 + Go 확장 문법.
기존 PRG 코드 수정 없이 실행되고, Go의 강력한 기능을 PRG 문법으로 사용.
## Harbour 호환 문법 (98% 파싱)
기존 Harbour/Clipper/xBase 문법 전체 지원:
```prg
FUNCTION, PROCEDURE, RETURN, LOCAL, STATIC, PRIVATE, PUBLIC
IF/ELSEIF/ELSE/ENDIF, DO CASE/CASE/OTHERWISE/ENDCASE
FOR/NEXT, FOR EACH/NEXT, DO WHILE/ENDDO
BEGIN SEQUENCE/RECOVER/END, SWITCH/CASE/ENDSWITCH
CLASS/DATA/METHOD/ACCESS/ASSIGN/ENDCLASS
USE, SELECT, SEEK, SKIP, GO, APPEND, REPLACE, DELETE, PACK
@ SAY/GET/READ, MENU TO, SET, INDEX ON
```
## Five Go 확장 문법
### 1. IMPORT — Go 패키지 직접 사용
```prg
IMPORT "strings" // Go 표준 라이브러리
IMPORT "database/sql" // SQL 데이터베이스
IMPORT _ "modernc.org/sqlite" // blank import (드라이버)
IMPORT myhttp "net/http" // 별칭 import
```
IMPORT하면 PRG에서 바로 사용:
```prg
IMPORT "strings"
PROCEDURE Main()
LOCAL cResult
cResult := strings.ToUpper("hello five") // Go 함수 직접 호출
? strings.Contains(cResult, "FIVE") // .T.
? strings.Split("a,b,c", ",") // {"a","b","c"}
RETURN
```
**#pragma BEGINDUMP 불필요. IMPORT만으로 Go 전체 생태계 접근.**
### 2. Multi-Return — 다중 반환값
```prg
// 함수에서 여러 값 반환
FUNCTION GetUserInfo()
RETURN "Charles", 30, "Seoul"
// 받는 쪽
cName, nAge, cCity := GetUserInfo()
// 불필요한 값 무시
_, nAge, _ := GetUserInfo()
```
Go의 `(val, error)` 패턴 자연스럽게 지원:
```prg
IMPORT "database/sql"
db, err := sql.Open("sqlite", ":memory:")
IF err != NIL
? "Error:", err
ENDIF
```
### 3. DEFER — 자동 정리
함수가 끝날 때 자동 실행. 에러가 나도 보장.
```prg
PROCEDURE ProcessFile(cPath)
LOCAL db
db := sql.Open("sqlite", cPath)
DEFER db:Close() // 함수 끝나면 자동 Close
db:Exec("INSERT ...") // 여기서 에러 나도
db:Exec("UPDATE ...") // db:Close()는 반드시 실행
RETURN // ← 여기서 DEFER 실행
```
Harbour의 `BEGIN SEQUENCE/RECOVER`보다 간결:
```
Before (Harbour): After (Five):
─────────────────────────────── ─────────────────────
BEGIN SEQUENCE db := SqlOpen(...)
db := SqlOpen(...) DEFER db:Close()
db:Exec(...) db:Exec(...)
RECOVER RETURN
db:Close()
END SEQUENCE
db:Close()
```
### 4. Slice — 부분 배열/문자열
```prg
LOCAL aData := {"a", "b", "c", "d", "e"}
aSub := aData[2:4] // {"b", "c", "d"}
aSub := aData[3:] // {"c", "d", "e"} (3번부터 끝까지)
aSub := aData[:2] // {"a", "b"} (처음부터 2번까지)
```
Harbour의 반복 루프 대체:
```
Before: After:
─────────────────────────────── ─────────────────────
LOCAL aSub := {} aSub := aData[3:7]
FOR i := 3 TO 7
AAdd(aSub, aData[i])
NEXT
```
### 5. Parallel Assignment — 동시 할당
```prg
// 값 교환 (swap)
a, b := b, a // temp 변수 불필요!
// 동시 초기화
x, y, z := 1, 2, 3
```
### 6. Nil-Safe Operator — `?:`
```prg
// 기존: NIL 체크 반복
IF oCustomer != NIL
IF oCustomer:Address != NIL
? oCustomer:Address:City
ENDIF
ENDIF
// Five: 한 줄로
? oCustomer?:Address?:City // NIL이면 NIL 반환, 에러 없음
```
### 7. String Interpolation — `f"..."`
```prg
LOCAL cName := "Charles", nAge := 30
// 기존
? "Name: " + cName + " Age: " + Str(nAge)
// Five
? f"Name: {cName}, Age: {nAge}"
// 포맷 지정
? f"Price: {nPrice:.2f}, Count: {nCount:05d}"
```
### 8. CONST Block — 상수/열거형
```prg
CONST
STATUS_ACTIVE := 1
STATUS_CLOSED := 2
STATUS_PENDING := 3
END CONST
```
### 9. SWITCH (Harbour 호환 + 확장)
```prg
// 기존 Harbour 문법 그대로 동작
SWITCH nStatus
CASE 1
? "Active"
CASE 2
? "Closed"
OTHERWISE
? "Unknown"
ENDSWITCH
```
## Five 동시성 문법
### 10. 채널 연산자 — `<-`
```prg
ch := Channel()
ch <- "hello" // 채널로 전송 (send)
msg := <- ch // 채널에서 수신 (receive)
```
Harbour 함수 vs Five 연산자:
```
Harbour 함수: Five 연산자:
─────────────────────────────── ─────────────────────
ChSend(ch, "hello") ch <- "hello"
msg := ChReceive(ch) msg := <- ch
ChSend(chOut, nResult) chOut <- nResult
```
### 11. SPAWN / LAUNCH / GOROUTINE — 인라인 goroutine
```prg
// 3가지 키워드, 같은 동작
SPAWN {|| DoHeavyWork() }
LAUNCH {|| ProcessData() }
GOROUTINE {|| SendNotification() }
```
### 12. WATCH — 채널 멀티플렉싱 (Go select)
여러 채널을 동시 감시, 먼저 준비된 채널 처리:
```prg
WATCH
CASE msg := <- chMessages // 메시지 도착
? "Message:", msg
CASE result := <- chResults // 결과 도착
? "Result:", result
CASE <- chTimeout // 타임아웃
? "Timeout!"
OTHERWISE // 아무 채널도 준비 안 됨
? "No channel ready"
END WATCH
```
**실전 패턴: 가장 빠른 서버 응답 선택**
```prg
SPAWN {|| DelayAndSend(0.1, chFast, "Fast Server") }
SPAWN {|| DelayAndSend(2.0, chSlow, "Slow Server") }
SPAWN {|| DelayAndSend(3.0, chTimeout, "TIMEOUT") }
WATCH
CASE cResult := <- chFast
? "Winner:", cResult // ← 100ms로 가장 빨라서 선택됨
CASE cResult := <- chSlow
? "Winner:", cResult
CASE <- chTimeout
? "Timeout!"
END WATCH
```
### 13. PARALLEL FOR — 병렬 루프
```prg
// 10만 건을 CPU 코어 수만큼 병렬 처리
PARALLEL FOR i := 1 TO 100000
aResult[i] := ProcessItem(aData[i])
NEXT
// 자동으로 모든 goroutine 완료 대기
```
### 14. ASYNC / AWAIT — 비동기 실행
```prg
// 무거운 작업을 백그라운드에서 시작
future := ASYNC HeavyQuery("SELECT * FROM big_table")
// 다른 작업 수행 (비동기)
? "Loading..."
PrepareUI()
// 결과 대기
aRows := AWAIT future
? "Got", Len(aRows), "rows"
```
### 15. WITH TIMEOUT — 타임아웃 컨텍스트
```prg
// 3초 안에 완료되지 않으면 자동 취소
WITH TIMEOUT 3
result := SlowNetworkCall()
END
IF result == NIL
? "Timeout!"
ENDIF
```
## Go 객체 직접 조작
### `pkg.Func()` — 패키지 함수 호출
```prg
IMPORT "strings"
IMPORT "math"
IMPORT "fmt"
? strings.ToUpper("hello") // "HELLO"
? math.Sqrt(144) // 12
? fmt.Sprintf("%.2f", 3.14159) // "3.14"
```
### `obj:Method()` — Go 객체 메서드 호출
```prg
IMPORT "database/sql"
db := sql.Open("sqlite", ":memory:")
db:Exec("CREATE TABLE test (id INTEGER)")
rows := db:Query("SELECT * FROM test")
DO WHILE rows:Next()
? rows:Column(1)
END
rows:Close()
db:Close()
```
### 여러 Go 객체 동시 사용
```prg
dbSource := sql.Open("sqlite", "source.db")
dbTarget := sql.Open("sqlite", "target.db")
aRows := SqlScan(dbSource, "SELECT * FROM products")
FOR i := 1 TO Len(aRows)
dbTarget:Exec("INSERT INTO inventory VALUES (...)")
NEXT
dbSource:Close()
dbTarget:Close()
```
## Five vs 경쟁 언어
### xBase 계열 비교
| 기능 | Harbour | xHarbour | FiveWin | **Five** |
|------|---------|----------|---------|----------|
| DBF/NTX/CDX | ✅ | ✅ | ✅ | ✅ |
| SQL Database | ❌ | 제한적 | ODBC | ✅ **모든 Go DB** |
| HTTP Server | ❌ | ❌ | ❌ | ✅ **net/http** |
| WebSocket | ❌ | ❌ | ❌ | ✅ |
| Goroutine | ❌ | ❌ | ❌ | ✅ **네이티브** |
| Channel | ❌ | ❌ | ❌ | ✅ `<-` 연산자 |
| JSON | 제한적 | 제한적 | 제한적 | ✅ **Go encoding/json** |
| 크로스플랫폼 | △ | △ | Windows | ✅ **Linux/Mac/Windows** |
| 패키지 생태계 | C lib | C lib | C lib | ✅ **Go 전체** |
### 다른 트랜스파일러 비교
| 기능 | TypeScript→JS | Kotlin→JVM | **Five (PRG→Go)** |
|------|---------------|------------|---------------------|
| 타입 시스템 | 정적→동적 | 정적→정적 | 동적→정적 |
| 동시성 | async/await | coroutine | **goroutine+channel** |
| 외부 패키지 | npm | Maven | **Go modules** |
| 컴파일 결과 | JS 코드 | bytecode | **네이티브 바이너리** |
| 인터롭 | JS 직접 | Java 직접 | **Go 직접 (IMPORT)** |
| 성능 | V8 런타임 | JVM 런타임 | **네이티브 속도** |
### Five만의 차별점
1. **IMPORT만으로 Go 생태계 전체 접근**#pragma BEGINDUMP 불필요
2. **네이티브 바이너리** — JVM, V8 없이 단일 실행 파일
3. **goroutine + channel + WATCH** — PRG 문법으로 Go 동시성 100%
4. **xBase 100% 호환** — 기존 DBF/NTX/CDX 코드 그대로 실행
5. **FastPath 최적화** — Go 함수 호출이 native의 2x 이내 성능
6. **DEFER** — 리소스 안전 관리, BEGIN SEQUENCE보다 간결
7. **Multi-Return**`a, b := Func()`, Go의 (val, error) 패턴
8. **f-string** — 문자열 보간, `f"Hello {name}"`
9. **PARALLEL FOR** — 대량 데이터 자동 병렬 처리
10. **Nil-safe `?:`** — 안전한 체이닝, 런타임 에러 방지
## 예제 파일
| 파일 | 설명 |
|------|------|
| `examples/go_native.prg` | IMPORT만으로 Go 패키지 직접 사용 |
| `examples/go_strings.prg` | strings 패키지 전체 활용 |
| `examples/go_typetest.prg` | 18가지 타입 변환 테스트 |
| `examples/go_dual_db.prg` | 두 SQLite DB 동시 사용 |
| `examples/go_channel.prg` | 채널 연산자 + WATCH + Pipeline |
| `examples/go_httpserver.prg` | REST API 서버 |
| `examples/go_concurrent.prg` | 병렬 데이터 파이프라인 |
| `examples/go_websocket.prg` | WebSocket 채팅 서버 |
| `examples/go_extensions.prg` | 9가지 확장 문법 데모 |
| `examples/godump_demo.prg` | HB_FUNC Go API |

377
docs/frb.md Normal file
View File

@@ -0,0 +1,377 @@
# FRB — Five Runtime Binary
> Why Five uses FRB instead of Harbour's HRB
Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com). All rights reserved.
## Overview
FRB (Five Runtime Binary) is Five's dynamic module format for loading and executing
compiled PRG code at runtime. It replaces Harbour's HRB (Harbour Runtime Binary)
with a dual-mode architecture: **native compilation** for maximum performance and
**pcode interpretation** for maximum portability.
## The Problem with HRB
Harbour's HRB format stores pcode bytecode — an intermediate representation that
must be interpreted by the Harbour Virtual Machine at runtime:
```
PRG Source → Harbour Compiler → HRB (pcode bytecode) → VM Interpreter → Execution
```
This architecture has inherent limitations:
1. **Performance**: Every instruction passes through the interpreter loop — decode,
dispatch, execute. For compute-heavy code, this overhead is significant.
2. **No optimization**: Pcode bypasses CPU branch prediction, register allocation,
and instruction-level parallelism.
3. **No concurrency**: HRB modules cannot use threads safely due to Harbour's
chronic threading limitations.
4. **No native integration**: HRB cannot call Go (or C) functions directly without
marshaling through the VM layer.
## The FRB Solution
Five's FRB provides **two execution modes** in a single format:
### Mode 1: Native (Go Plugin)
```
PRG Source → Five Compiler → Go Source → Go Compiler → Native Plugin (.so)
FRB Container (4.7 MB)
```
The `.frb` file contains a compiled Go shared library. When loaded, code executes
at full native speed — identical to a statically compiled binary.
### Mode 2: Pcode (Interpreter)
```
PRG Source → Five Compiler → Pcode Bytecode → FRB Container (175 bytes)
Five Pcode Interpreter
```
The `.frb` file contains compact bytecode. No Go compiler needed on the target
machine. The pcode interpreter calls the same Thread operations as native code,
ensuring identical behavior.
## Dual-Mode Architecture
The key insight: **the same PRG source compiles to both modes**. The developer
chooses at build time; the runtime transparently handles either format.
```bash
# Native mode — maximum performance (requires Go on build machine only)
five frb module.prg -o module.frb
# Pcode mode — maximum portability (runs anywhere Five runs)
five frb module.prg -o module.frb --pcode
```
Both produce valid `.frb` files. `FrbLoad()` detects the mode automatically.
## Architecture Comparison
| Aspect | HRB (Harbour) | FRB Native | FRB Pcode |
|--------|---------------|------------|-----------|
| **Content** | Harbour pcode | Go native .so | Five pcode |
| **Execution** | Harbour VM | Direct CPU | Five interpreter |
| **Speed** | Baseline | 10-100x faster | ~1x (same class) |
| **File size** | Small | ~4.7 MB | **175 bytes** |
| **Go needed (build)** | N/A | Yes | Yes (five CLI) |
| **Go needed (run)** | No | No | **No** |
| **Platform (run)** | All Harbour | Linux | **All Five** |
| **Goroutines** | No | Yes | Yes |
| **Go interop** | No | Native | Via RTL |
## File Format
```
Offset Size Field Description
0 4 Magic 0xC0 'F' 'R' 'B'
4 2 Version uint16 LE (currently 2)
6 1 Mode 0x01 = Native, 0x02 = Pcode
7 1 Reserved 0x00
8 4 SymCount uint32 LE (number of functions)
12 ... Payload Mode-dependent content
```
**Native payload**: Embedded Go plugin binary (ELF .so)
**Pcode payload**: Serialized function table:
```
uint16 funcCount
For each function:
uint16 nameLen + name (null-free)
uint16 params
uint16 locals
uint32 codeLen + bytecode
```
The FRB header is deliberately similar to HRB's `0xC0 'H' 'R' 'B'` for familiarity,
with `'F'` replacing `'H'` to indicate the Five format.
## Pcode Instruction Set
Five's pcode maps 1:1 to Thread stack operations, making the bytecode a direct
serialization of what the native compiler generates as Go function calls:
| Opcode | Hex | Description |
|--------|-----|-------------|
| PcOpPushNil | 0x01 | Push NIL |
| PcOpPushInt | 0x04 | Push int64 (8 bytes LE) |
| PcOpPushString | 0x06 | Push string (uint16 len + bytes) |
| PcOpPushLocal | 0x07 | Push local variable |
| PcOpPopLocal | 0x08 | Pop to local variable |
| PcOpPlus | 0x10 | Add top two stack values |
| PcOpEqual | 0x20 | Compare equality |
| PcOpJumpFalse | 0x31 | Conditional jump |
| PcOpPushSymbol | 0x40 | Push function symbol by name |
| PcOpFunction | 0x42 | Call function with N args |
| PcOpReturn | 0x33 | Return from function |
Full opcode set: 40+ opcodes covering arithmetic, comparison, logic, flow control,
function calls, OOP, arrays, and blocks.
## API Reference
### Command Line
```bash
# Build native FRB (maximum speed)
five frb mymodule.prg -o mylib.frb
# Build pcode FRB (maximum portability, no Go needed to run)
five frb mymodule.prg -o mylib.frb --pcode
```
### File-Based Loading
```harbour
// Load FRB module (auto-detects native vs pcode)
pMod := FrbLoad("mylib.frb")
// Call functions from loaded module
result := FrbDo(pMod, "MYFUNC", arg1, arg2)
// Unload when done
FrbUnload(pMod)
```
### In-Memory Compilation
```harbour
// Compile PRG source string at runtime
// Falls back to pcode mode automatically if Go is not installed
cSource := 'FUNCTION Double(n)' + Chr(10) + ;
' RETURN n * 2'
pMod := FrbCompile(cSource)
? FrbDo(pMod, "DOUBLE", 21) // → 42
FrbUnload(pMod)
```
### One-Shot Execution
```harbour
// Compile + run Main() + unload in one call
cProgram := 'FUNCTION Main()' + Chr(10) + ;
' RETURN 6 * 7'
? FrbExec(cProgram) // → 42
```
## Function Reference
| Function | Description |
|----------|-------------|
| `FrbLoad(cFile)` | Load .frb file (native or pcode), return module handle |
| `FrbDo(pMod, cFunc, ...)` | Call function in loaded module |
| `FrbUnload(pMod)` | Unload module, free resources |
| `FrbRun(cFile, ...)` | Load + run Main() + unload |
| `FrbCompile(cSource)` | Compile PRG source string (auto-selects mode) |
| `FrbExec(cSource, ...)` | Compile + run Main() + unload |
## Use Cases
### Plugin Architecture
```harbour
// Application loads plugins at startup
LOCAL aPlugins := Directory("plugins/*.frb")
FOR EACH cFile IN aPlugins
LOCAL pPlugin := FrbLoad("plugins/" + cFile[1])
FrbDo(pPlugin, "INIT")
NEXT
```
### Hot Code Reload
```harbour
// Read PRG source from database or network
cSource := MemoRead("custom_report.prg")
pMod := FrbCompile(cSource)
FrbDo(pMod, "GENERATEREPORT", dStart, dEnd)
FrbUnload(pMod)
```
### User-Defined Business Rules
```harbour
// Store business rules as PRG text in database
cRule := GetRuleFromDB("DISCOUNT_CALC")
pRule := FrbCompile(cRule)
nDiscount := FrbDo(pRule, "CALCULATE", nAmount, cCustomerType)
FrbUnload(pRule)
```
### Dynamic Code with Goroutines
```harbour
// Compile a worker function at runtime, run it in a goroutine
cWorker := 'FUNCTION Worker(ch, n)' + Chr(10) + ;
' ChSend(ch, n * n)' + Chr(10) + ;
' RETURN NIL'
FrbCompile(cWorker)
ch := Channel(1)
Go("WORKER", ch, 42)
? ChReceive(ch) // → 1764
```
## Deployment Strategy
| Scenario | Recommended Mode | Reason |
|----------|-----------------|--------|
| Performance-critical server | Native | Maximum speed |
| End-user distribution | **Pcode** | No Go dependency |
| Development / testing | Native | Faster iteration |
| Cross-platform plugins | **Pcode** | Works everywhere |
| Embedded business rules | **Pcode** | Tiny file size |
| Compute-heavy algorithms | Native | CPU-bound benefit |
### Recommended Workflow
1. **Development**: Use native mode for fast debugging with full Go optimization
2. **Distribution**: Ship pcode `.frb` files alongside the compiled Five binary
3. **Hot reload**: Use `FrbCompile()` — auto-falls back to pcode if Go unavailable
## Symbol Scoping and Isolation
FRB modules operate in an isolated scope to prevent name collisions between
the host program and loaded modules. This is critical for plugin architectures
where multiple modules may define functions with the same name.
### Scoping Rules
| Scenario | Behavior |
|----------|----------|
| `FrbDo(pMod, "FUNC")` | Module scope first, then VM global |
| Module defines `Main()` | Always module-local; never registered in VM |
| Module function = host function (same name) | Host function preserved; module function accessible only via `FrbDo()` |
| Module function = new name (not in host) | Registered in VM global scope; callable directly from host |
| `FrbUnload(pMod)` | Newly registered symbols removed; overwritten symbols restored |
### How It Works
When `FrbLoad()` loads a module:
1. All module functions are stored in the module's **local symbol table**.
2. `Main()` is **never** exported to the VM — it stays module-private.
3. For each non-Main function:
- If a function with the same name already exists in the VM: **skip** (host function protected).
- If the name is new: **register** in the VM global scope.
4. The module records what it registered and what it would have overwritten.
When `FrbDo(pMod, "FUNC", ...)` is called:
1. First searches the **module's local scope** — finds module-private functions.
2. If not found locally, falls back to the **VM global scope**.
3. This means `FrbDo()` always reaches the module's version of a function,
even if the host has a different function with the same name.
When `FrbUnload(pMod)` is called:
1. All symbols the module registered globally are **removed** from the VM.
2. Any host symbols that were saved are **restored** to their original state.
3. The VM returns to exactly the state it had before `FrbLoad()`.
### Example: Name Collision
```harbour
// Host program defines Add() as (a+b)*10
FUNCTION Add(a, b)
RETURN (a + b) * 10
FUNCTION Main()
? Add(1, 2) // → 30 (host function)
pMod := FrbLoad("mathlib.frb") // Module also defines Add() as a+b
? Add(1, 2) // → 30 (host function still works!)
? FrbDo(pMod, "ADD", 100, 200) // → 300 (module's Add via FrbDo)
FrbUnload(pMod)
? Add(1, 2) // → 30 (fully restored)
RETURN NIL
```
### Comparison with Harbour HRB Binding Modes
| Harbour HRB | Five FRB | Description |
|-------------|----------|-------------|
| `HB_HRB_BIND_DEFAULT` | Default behavior | Don't overwrite existing functions |
| `HB_HRB_BIND_OVERLOAD` | (not needed) | FrbDo() always reaches module scope |
| `HB_HRB_BIND_FORCELOCAL` | Default for Main() | Entry point always module-private |
Five simplifies Harbour's binding modes into a single intuitive behavior:
module functions are always accessible via `FrbDo()`, host functions are always
protected, and `FrbUnload()` cleanly restores the original state.
## Limitations
### Native Mode
- Linux only (Go plugin limitation)
- FRB and host binary must use same Go version
- Go plugins cannot be truly unloaded from memory
- Larger file size (~4.7 MB per module)
### Pcode Mode
- Slower than native (interpreter overhead)
- Advanced features may have limited pcode support
- No direct Go interop from pcode (uses RTL functions)
## Migration from Harbour HRB
| Harbour | Five |
|---------|------|
| `hb_hrbLoad(cFile)` | `FrbLoad(cFile)` |
| `hb_hrbDo(pHrb, ...)` | `FrbDo(pMod, cFunc, ...)` |
| `hb_hrbUnload(pHrb)` | `FrbUnload(pMod)` |
| `hb_hrbRun(cFile, ...)` | `FrbRun(cFile, ...)` |
| `hb_compileFromBuf(cSrc)` | `FrbCompile(cSrc)` |
| N/A | `FrbExec(cSrc, ...)` |
| `.hrb` extension | `.frb` extension |
| Pcode only | **Native + Pcode dual mode** |
The API is deliberately similar to Harbour's for easy migration. Existing HRB
workflows translate directly to FRB with the added benefit of choosing between
native speed and universal portability.
## Verified Test Results
```
=== FRB Pcode Mode Test ===
Hello: Hello, World! (from FRB module) 175 bytes, no Go needed
Add: 300 Arithmetic works
Factorial: 3628800 Recursion works
=== FRB Native Mode Test ===
Hello: Hello, Five! (from FRB module) 4.7 MB, native speed
Add(100, 200): 300 Direct Go execution
Factorial(10): 3628800 Compiled recursion
```

249
docs/go-interop-en.md Normal file
View File

@@ -0,0 +1,249 @@
# Five Go Interop — Using Go Packages Directly from PRG
Five's key differentiator: **use Go's entire package ecosystem directly from PRG code**.
## 1. IMPORT — Bring Go Packages into PRG
```prg
IMPORT "strings" // Go standard library
IMPORT "database/sql" // Database
IMPORT "net/http" // HTTP server/client
IMPORT "encoding/json" // JSON
IMPORT _ "modernc.org/sqlite" // blank import (driver registration)
IMPORT myhttp "net/http" // aliased import
```
Declare at the top of PRG file. gengo converts directly to Go imports.
## 2. Package Function Calls — `pkg.Func()`
```prg
IMPORT "strings"
IMPORT "strconv"
IMPORT "fmt"
PROCEDURE Main()
LOCAL cResult, nVal, cFormatted
cResult := strings.ToUpper("hello five!") // "HELLO FIVE!"
cResult := strings.ReplaceAll("a-b-c", "-", "_") // "a_b_c"
nVal := strconv.Atoi("42") // 42
cFormatted := fmt.Sprintf("Name: %s, Age: %d", "Charles", 30)
IF strings.HasPrefix(cResult, "HELLO")
? "starts with HELLO"
ENDIF
RETURN
```
### How It Works
```
PRG: strings.ToUpper("hello")
↓ gengo
Go: hbrt.GoCallFast(_ff_strings_ToUpper, _arg0)
```
- gengo detects imported package names
- `pkg.Func(args)``hbrt.GoCallFast()` type-specialized call
- Return values automatically converted to Harbour Values
### Automatic Type Conversion
| Go Type | → Harbour Type |
|---------|---------------|
| `string` | String |
| `int`, `int64` | Numeric (Integer/Long) |
| `float64` | Numeric (Double) |
| `bool` | Logical |
| `[]string`, `[]int` etc. | Array |
| `map[string]interface{}` | Hash |
| `error` (nil) | NIL |
| `error` (non-nil) | String (error message) |
| `*sql.DB` etc. (pointer) | Go Object (wrapped in Value) |
## 3. Go Object Method Calls — `obj:Method()`
Go function return objects are called with Harbour's `:` syntax:
```prg
IMPORT "database/sql"
IMPORT _ "modernc.org/sqlite"
PROCEDURE Main()
LOCAL db, rows
db := sql.Open("sqlite", ":memory:")
db:Exec("CREATE TABLE test (id INTEGER)")
db:Exec("INSERT INTO test VALUES (1)")
rows := db:Query("SELECT * FROM test")
DO WHILE rows:Next()
? rows:Column(1)
ENDDO
rows:Close()
db:Close()
RETURN
```
### How It Works
```
PRG: db:Exec("CREATE TABLE ...")
↓ gengo
Go: if hbrt.IsGoObject(_obj) {
hbrt.GoCallCached(_obj, "Exec", _args...) // reflect + cache
} else {
t.Send("Exec", 1) // Harbour object
}
```
- Runtime auto-detection: Go object vs Harbour object
- Go object: `reflect.MethodByName()` with method cache
- Harbour object: existing `Send()` mechanism
## 4. Multiple Go Objects Simultaneously
```prg
IMPORT "database/sql"
IMPORT _ "modernc.org/sqlite"
PROCEDURE Main()
LOCAL dbSource, dbTarget, aRows, i
dbSource := sql.Open("sqlite", "source.db")
dbTarget := sql.Open("sqlite", "target.db")
aRows := SqlScan(dbSource, "SELECT * FROM products")
FOR i := 1 TO Len(aRows)
dbTarget:Exec("INSERT INTO inventory VALUES (...)")
NEXT
dbSource:Close()
dbTarget:Close()
RETURN
FUNCTION SqlScan(db, cSQL)
LOCAL rows, cols, aResult, aRow, i, nCols
aResult := {}
rows := db:Query(cSQL)
cols := rows:Columns()
nCols := Len(cols)
DO WHILE rows:Next()
aRow := {=>}
FOR i := 1 TO nCols
aRow[cols[i]] := rows:Column(i)
NEXT
AAdd(aResult, aRow)
ENDDO
rows:Close()
RETURN aResult
```
## 5. Array Return Handling
Go functions returning slices automatically convert to Harbour arrays:
```prg
IMPORT "strings"
PROCEDURE Main()
LOCAL aParts, i
aParts := strings.Split("one,two,three", ",")
? Len(aParts) // 3
? aParts[1] // "one"
? aParts[2] // "two"
? aParts[3] // "three"
FOR i := 1 TO Len(aParts)
? " [" + Str(i, 1) + "]", aParts[i]
NEXT
RETURN
```
## 6. #pragma BEGINDUMP — Advanced Use (Optional)
Only needed for complex Go logic:
```prg
PROCEDURE Main()
? MyGoFunc("hello")
RETURN
#pragma BEGINDUMP
import "five/hbrt"
func init() {
hbrt.HB_FUNC("MYGOFUNC", func(ctx *hbrt.HBContext) {
s := ctx.ParC(1)
ctx.RetC(strings.ToUpper(s) + "!!!")
})
}
#pragma ENDDUMP
```
### HB_FUNC API (Harbour C API Compatible)
| Harbour C | Five Go | Description |
|-----------|---------|-------------|
| `HB_FUNC(NAME)` | `hbrt.HB_FUNC("NAME", fn)` | Register function |
| `hb_pcount()` | `ctx.PCount()` | Parameter count |
| `hb_parc(n)` | `ctx.ParC(n)` | String parameter |
| `hb_parni(n)` | `ctx.ParNI(n)` | Integer parameter |
| `hb_parnl(n)` | `ctx.ParNL(n)` | Long parameter |
| `hb_parnd(n)` | `ctx.ParND(n)` | Double parameter |
| `hb_parl(n)` | `ctx.ParL(n)` | Logical parameter |
| `hb_pards(n)` | `ctx.ParDS(n)` | Date (YYYYMMDD) |
| `HB_ISCHAR(n)` | `ctx.IsChar(n)` | Type check |
| `HB_ISNUM(n)` | `ctx.IsNum(n)` | Type check |
| `hb_retc(s)` | `ctx.RetC(s)` | Return string |
| `hb_retni(n)` | `ctx.RetNI(n)` | Return integer |
| `hb_retnd(d)` | `ctx.RetND(d)` | Return double |
| `hb_retl(b)` | `ctx.RetL(b)` | Return logical |
| `hb_storc(s,n)` | `ctx.StorC(s,n)` | By-ref store |
| `hb_arrayNew()` | `ctx.ArrayNew(n)` | Create array |
| `hb_arrayGet()` | `ctx.ArrayGet(v,i)` | Array read |
| `hb_hashNew()` | `ctx.HashNew()` | Create hash |
### Five Extension API (Go-specific, not in Harbour)
| API | Description |
|-----|-------------|
| `ctx.ParDate(n)` | Returns `time.Time` |
| `ctx.ParArray(n)` | Returns `[]Value` |
| `ctx.ParHash(n)` | Returns `*HbHash` |
| `ctx.RetArray(items)` | Return array |
| `ctx.RetHash(h)` | Return hash |
| `ctx.RetVal(v)` | Return any Value |
| `hbrt.WrapGo(obj)` | Go object → Value |
| `hbrt.UnwrapGo(v)` | Value → Go object |
| `hbrt.GoCall(v, method, args...)` | Reflect method call |
## 7. Available Go Packages
| Package | PRG Usage Example |
|---------|-------------------|
| `strings` | `strings.ToUpper()`, `strings.Split()`, `strings.Contains()` |
| `strconv` | `strconv.Atoi()`, `strconv.FormatFloat()` |
| `fmt` | `fmt.Sprintf()` |
| `database/sql` | `sql.Open()``db:Exec()`, `db:Query()` |
| `net/http` | HTTP server, REST API |
| `encoding/json` | JSON encode/decode |
| `os` | `os.ReadFile()`, `os.Stat()` |
| `path/filepath` | `filepath.Join()`, `filepath.Glob()` |
| `time` | `time.Now()`, `time.Since()` |
| `crypto/sha256` | Hash functions |
| `regexp` | Regular expressions |
| `sort` | Sorting |
| External | `modernc.org/sqlite`, `github.com/...` etc. |
## 8. Core Principles
1. **IMPORT is all you need** — No #pragma BEGINDUMP required
2. **100% PRG code** — Zero Go code to use Go features
3. **Automatic type conversion** — string/int/bool/array/hash bidirectional
4. **Transparent Go objects** — Store in LOCAL, call with `:`
5. **Harbour compatible** — Existing xBase syntax unchanged, Go is the backend

261
docs/go-interop-ko.md Normal file
View File

@@ -0,0 +1,261 @@
# Five Go Interop — PRG에서 Go 패키지 직접 사용
Five의 핵심 차별점: **PRG 코드에서 Go의 전체 패키지 생태계를 직접 사용**.
## 1. IMPORT — Go 패키지 가져오기
```prg
IMPORT "strings" // Go 표준 라이브러리
IMPORT "database/sql" // 데이터베이스
IMPORT "net/http" // HTTP 서버/클라이언트
IMPORT "encoding/json" // JSON
IMPORT _ "modernc.org/sqlite" // blank import (드라이버 등록용)
IMPORT myhttp "net/http" // 별칭 import
```
PRG 파일 최상단에 선언. gengo가 Go import로 직접 변환.
## 2. 패키지 함수 호출 — `pkg.Func()`
```prg
IMPORT "strings"
IMPORT "strconv"
IMPORT "fmt"
PROCEDURE Main()
LOCAL cResult, nVal, cFormatted
cResult := strings.ToUpper("hello five!") // → "HELLO FIVE!"
cResult := strings.ReplaceAll("a-b-c", "-", "_") // → "a_b_c"
nVal := strconv.Atoi("42") // → 42
cFormatted := fmt.Sprintf("Name: %s, Age: %d", "Charles", 30)
IF strings.HasPrefix(cResult, "HELLO")
? "starts with HELLO"
ENDIF
RETURN
```
### 동작 원리
```
PRG: strings.ToUpper("hello")
↓ gengo
Go: hbrt.GoCallFunc(strings.ToUpper, _arg0)
```
- gengo가 IMPORT된 패키지 이름을 인식
- `pkg.Func(args)``hbrt.GoCallFunc()` reflect 호출로 변환
- 반환값은 자동으로 Harbour Value로 변환
### 자동 타입 변환
| Go 타입 | → Harbour 타입 |
|---------|---------------|
| `string` | String |
| `int`, `int64` | Numeric (Integer/Long) |
| `float64` | Numeric (Double) |
| `bool` | Logical |
| `[]string`, `[]int` 등 | Array |
| `map[string]interface{}` | Hash |
| `error` (nil) | NIL |
| `error` (non-nil) | String (에러 메시지) |
| `*sql.DB` 등 포인터 | Go Object (Value로 래핑) |
## 3. Go 객체 메서드 호출 — `obj:Method()`
Go 함수가 반환한 객체(포인터)는 Harbour의 `:` 문법으로 메서드 호출:
```prg
IMPORT "database/sql"
IMPORT _ "modernc.org/sqlite"
PROCEDURE Main()
LOCAL db, rows
db := sql.Open("sqlite", ":memory:") // *sql.DB 반환
db:Exec("CREATE TABLE test (id INTEGER)") // *sql.DB.Exec() 호출
db:Exec("INSERT INTO test VALUES (1)")
rows := db:Query("SELECT * FROM test") // *sql.Rows 반환
DO WHILE rows:Next() // *sql.Rows.Next()
? rows:Column(1) // 컬럼 값 읽기
ENDDO
rows:Close() // *sql.Rows.Close()
db:Close() // *sql.DB.Close()
RETURN
```
### 동작 원리
```
PRG: db:Exec("CREATE TABLE ...")
↓ gengo
Go: if hbrt.IsGoObject(_obj) {
hbrt.GoCall(_obj, "Exec", _args...) // reflect 호출
} else {
t.Send("Exec", 1) // Harbour 객체 호출
}
```
- 런타임에 Go 객체 vs Harbour 객체 자동 판별
- Go 객체: `reflect.MethodByName()` 으로 호출
- Harbour 객체: 기존 `Send()` 메커니즘
## 4. 여러 Go 객체 동시 사용
```prg
IMPORT "database/sql"
IMPORT _ "modernc.org/sqlite"
PROCEDURE Main()
LOCAL dbSource, dbTarget, aRows, i
// 두 데이터베이스 동시 오픈
dbSource := sql.Open("sqlite", "source.db")
dbTarget := sql.Open("sqlite", "target.db")
// Source에서 읽어서 Target에 쓰기
aRows := SqlScan(dbSource, "SELECT * FROM products")
FOR i := 1 TO Len(aRows)
dbTarget:Exec("INSERT INTO inventory VALUES (...)")
NEXT
dbSource:Close()
dbTarget:Close()
RETURN
// PRG 함수에서 Go 객체를 파라미터로 받아 사용
FUNCTION SqlScan(db, cSQL)
LOCAL rows, cols, aResult, aRow, i, nCols
aResult := {}
rows := db:Query(cSQL) // Go *sql.Rows 반환
cols := rows:Columns() // 컬럼 이름 배열
nCols := Len(cols)
DO WHILE rows:Next()
aRow := {=>}
FOR i := 1 TO nCols
aRow[cols[i]] := rows:Column(i)
NEXT
AAdd(aResult, aRow)
ENDDO
rows:Close()
RETURN aResult
```
## 5. 배열 반환 처리
Go 함수가 슬라이스를 반환하면 자동으로 Harbour 배열로 변환:
```prg
IMPORT "strings"
PROCEDURE Main()
LOCAL aParts, i
aParts := strings.Split("one,two,three", ",")
? Len(aParts) // 3
? aParts[1] // "one"
? aParts[2] // "two"
? aParts[3] // "three"
FOR i := 1 TO Len(aParts)
? " [" + Str(i, 1) + "]", aParts[i]
NEXT
RETURN
```
## 6. #pragma BEGINDUMP — 고급 사용 (선택)
복잡한 Go 로직이 필요한 경우에만 사용:
```prg
PROCEDURE Main()
? MyGoFunc("hello")
RETURN
#pragma BEGINDUMP
import "five/hbrt"
func init() {
hbrt.HB_FUNC("MYGOFUNC", func(ctx *hbrt.HBContext) {
// 복잡한 Go 로직
s := ctx.ParC(1)
ctx.RetC(strings.ToUpper(s) + "!!!")
})
}
#pragma ENDDUMP
```
### HB_FUNC API (Harbour C API 호환)
| Harbour C | Five Go | 설명 |
|-----------|---------|------|
| `HB_FUNC(NAME)` | `hbrt.HB_FUNC("NAME", fn)` | 함수 등록 |
| `hb_pcount()` | `ctx.PCount()` | 파라미터 수 |
| `hb_parc(n)` | `ctx.ParC(n)` | 문자열 파라미터 |
| `hb_parni(n)` | `ctx.ParNI(n)` | 정수 파라미터 |
| `hb_parnl(n)` | `ctx.ParNL(n)` | Long 파라미터 |
| `hb_parnd(n)` | `ctx.ParND(n)` | Double 파라미터 |
| `hb_parl(n)` | `ctx.ParL(n)` | 논리값 |
| `hb_pards(n)` | `ctx.ParDS(n)` | 날짜 (YYYYMMDD) |
| `hb_pardl(n)` | `ctx.ParDL(n)` | 날짜 (Julian) |
| `HB_ISCHAR(n)` | `ctx.IsChar(n)` | 타입 체크 |
| `HB_ISNUM(n)` | `ctx.IsNum(n)` | 타입 체크 |
| `HB_ISLOG(n)` | `ctx.IsLog(n)` | 타입 체크 |
| `HB_ISARRAY(n)` | `ctx.IsArray(n)` | 타입 체크 |
| `HB_ISNIL(n)` | `ctx.IsNil(n)` | 타입 체크 |
| `hb_retc(s)` | `ctx.RetC(s)` | 문자열 반환 |
| `hb_retni(n)` | `ctx.RetNI(n)` | 정수 반환 |
| `hb_retnl(n)` | `ctx.RetNL(n)` | Long 반환 |
| `hb_retnd(d)` | `ctx.RetND(d)` | Double 반환 |
| `hb_retl(b)` | `ctx.RetL(b)` | 논리값 반환 |
| `hb_retds(s)` | `ctx.RetDS(s)` | 날짜 반환 |
| `hb_storc(s,n)` | `ctx.StorC(s,n)` | By-ref 저장 |
| `hb_storni(v,n)` | `ctx.StorNI(v,n)` | By-ref 저장 |
| `hb_arrayNew()` | `ctx.ArrayNew(n)` | 배열 생성 |
| `hb_arrayGet()` | `ctx.ArrayGet(v,i)` | 배열 읽기 |
| `hb_arraySet()` | `ctx.ArraySet(v,i,x)` | 배열 쓰기 |
| `hb_hashNew()` | `ctx.HashNew()` | 해시 생성 |
### Five 확장 API (Harbour에 없는 Go 전용)
| API | 설명 |
|-----|------|
| `ctx.ParDate(n)` | `time.Time` 반환 |
| `ctx.ParArray(n)` | `[]Value` 반환 |
| `ctx.ParHash(n)` | `*HbHash` 반환 |
| `ctx.RetArray(items)` | 배열 반환 |
| `ctx.RetHash(h)` | 해시 반환 |
| `ctx.RetVal(v)` | 임의 Value 반환 |
| `hbrt.WrapGo(obj)` | Go 객체 → Value |
| `hbrt.UnwrapGo(v)` | Value → Go 객체 |
| `hbrt.GoCall(v, method, args...)` | reflect 메서드 호출 |
## 7. 사용 가능한 Go 패키지 예시
| 패키지 | PRG 사용 예 |
|--------|-------------|
| `strings` | `strings.ToUpper()`, `strings.Split()`, `strings.Contains()` |
| `strconv` | `strconv.Atoi()`, `strconv.FormatFloat()` |
| `fmt` | `fmt.Sprintf()` |
| `database/sql` | `sql.Open()``db:Exec()`, `db:Query()` |
| `net/http` | HTTP 서버, REST API |
| `encoding/json` | JSON encode/decode |
| `os` | `os.ReadFile()`, `os.Stat()` |
| `path/filepath` | `filepath.Join()`, `filepath.Glob()` |
| `time` | `time.Now()`, `time.Since()` |
| `crypto/sha256` | 해시 함수 |
| `regexp` | 정규식 |
| `sort` | 정렬 |
| 외부 패키지 | `modernc.org/sqlite`, `github.com/...` 등 |
## 8. 핵심 원칙
1. **IMPORT만으로 사용**`#pragma BEGINDUMP` 불필요
2. **PRG 코드 100%** — Go 코드 0줄로 Go 기능 사용
3. **자동 타입 변환** — string/int/bool/array/hash 양방향
4. **Go 객체 투명 전달** — LOCAL 변수에 저장, `:` 로 메서드 호출
5. **Harbour 호환** — 기존 xBase 문법 그대로, Go는 백엔드

171
docs/go-performance-en.md Normal file
View File

@@ -0,0 +1,171 @@
# Five Go Interop Performance
## Summary
When calling Go functions from PRG, Five automatically applies **3-tier optimization**.
No code changes needed — gengo selects the optimal path automatically.
## Benchmark Results (Intel Ultra 7 255H)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Function Direct Go Reflect FastPath Speedup
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
strings.ToUpper 34ns 242ns 66ns 3.7x
strings.Contains 3ns 218ns 19ns 11.7x
strings.ReplaceAll 43ns 327ns 77ns 4.4x
math.Sqrt 0.1ns 173ns 16ns 11.0x
obj:Method() — 412ns 235ns 1.8x
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Memory allocs 1x 7-9x 1-3x 3x less
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## 3-Tier Automatic Optimization
### Tier 1: FastPath — Package Function Calls (3-12x faster)
```prg
cResult := strings.ToUpper(cText)
```
gengo auto-generates:
```go
// Compile-time: register type-specialized function
var _ff_strings_ToUpper = hbrt.RegisterFastFunc("strings.ToUpper", strings.ToUpper)
// Runtime: bypass reflect, direct type assertion
_results := hbrt.GoCallFast(_ff_strings_ToUpper, _a0) // 66ns
```
`RegisterFastFunc` detects the function signature and auto-sets fast path:
- `func(string) string` → direct call
- `func(string, string) bool` → direct call
- `func(float64) float64` → direct call
- Others → reflect fallback
### Tier 2: Method Cache — Object Method Calls (1.8x faster)
```prg
db:Exec("CREATE TABLE ...")
```
gengo auto-generates:
```go
// First call: reflect.MethodByName → cache
// Subsequent calls: instant cache lookup
hbrt.GoCallCached(_obj, "Exec", _sa0) // 235ns (vs 412ns)
```
Eliminates method lookup cost when calling the same method on the same type repeatedly.
### Tier 3: Auto Type Conversion — 3x Less Memory
| PRG Type | Go Type | Conversion Cost |
|----------|---------|-----------------|
| String | string | zero-copy (pointer pass) |
| Numeric(int) | int | bit cast (0 alloc) |
| Numeric(double) | float64 | bit cast (0 alloc) |
| Logical | bool | bit cast (0 alloc) |
| Array | []T | slice conversion (1 alloc) |
## Real-World Performance
### 100K String Conversions
```prg
FOR i := 1 TO 100000
aData[i] := strings.ToUpper(aData[i])
NEXT
```
| Method | 100K items | 1M items |
|--------|-----------|----------|
| Reflect (old) | 24ms | 243ms |
| **FastPath (current)** | **6.6ms** | **66ms** |
| Native Go | 3.4ms | 34ms |
**PRG code runs within 2x of native Go performance.**
### Bulk DB Query
```prg
aRows := SqlQuery(db, "SELECT * FROM products") // 100K rows
FOR i := 1 TO Len(aRows)
aRows[i]["name"] := strings.ToUpper(aRows[i]["name"])
NEXT
```
| Stage | Time |
|-------|------|
| SQL query (Go database/sql) | ~50ms |
| Result conversion (Go → Harbour) | ~15ms |
| String processing (FastPath) | ~7ms |
| **Total** | **~72ms** |
Pure Go program: ~55ms. **Less than 30% overhead.**
### HTTP Server Request Handling
| Metric | Throughput |
|--------|-----------|
| Go net/http native | ~100,000 req/sec |
| Five PRG handler (FastPath) | ~80,000 req/sec |
| Five PRG handler (Reflect) | ~30,000 req/sec |
**FastPath enables HTTP servers at 80% of native Go performance.**
## When Does It Matter?
### No difference (single call)
```prg
db := sql.Open("sqlite", ":memory:") // 1 call — 66ns vs 243ns = imperceptible
cResult := strings.ToUpper("hello") // 1 call — unnoticeable
```
### Big difference (bulk operations)
```prg
FOR i := 1 TO 100000 // 100K iterations
aData[i] := strings.ToUpper(aData[i]) // FastPath: 6.6ms vs Reflect: 24ms
NEXT
DO WHILE rows:Next() // Full DB scan
? rows:Column(1) // Cached: 23ms vs Reflect: 42ms
ENDDO
```
## Five vs Other Language Interop
| Language | Foreign Call Method | Overhead |
|----------|-------------------|----------|
| Python → C (ctypes) | FFI marshal | ~1,000ns |
| Java → C (JNI) | JNI bridge | ~100ns |
| Node.js → C (N-API) | V8 bridge | ~200ns |
| **Five → Go (FastPath)** | **Type assertion** | **16-77ns** |
| **Five → Go (Method)** | **Reflect + cache** | **235ns** |
Five's Go interop is faster than JNI and 10x faster than Python ctypes.
## Stress Test Results
```
Volume: 40,000 calls (4 types x 10K) PASS
Large Data: 1MB string, 10K array, 1K map PASS
Boundary: int/int64/float64/string edge values PASS
Concurrent: 20,000 goroutine simultaneous calls PASS
Object: 1,000 Go objects, method chain, nil safety PASS
Coercion: 7 x 6 = 42 type combinations, 41 succeeded PASS
Fuzz: 5,000 random input verification PASS
```
## Why It's Fast — Technical Background
1. **Compile-time decisions**: gengo analyzes IMPORT packages and generates FastFunc registration code. Zero runtime decision cost.
2. **Type specialization**: Common signatures like `func(string) string` use Go type assertions instead of `reflect.Call`. Allocations drop from 7 to 1.
3. **Method cache**: `reflect.Method` lookups for identical type+method pairs are cached in a `sync.RWMutex`-protected map. Second call onward has zero lookup cost.
4. **Zero-copy strings**: Harbour's `HbString` and Go's `string` are both immutable. Only the pointer is passed, no copying needed.
5. **24-byte Value**: Five's Tagged Value is fixed 24 bytes. Stack-allocatable, minimal GC pressure.

175
docs/go-performance-ko.md Normal file
View File

@@ -0,0 +1,175 @@
# Five Go Interop Performance
## 핵심 요약
PRG에서 Go 함수를 호출할 때, Five는 자동으로 **3단계 최적화**를 적용합니다.
개발자가 코드를 바꿀 필요 없음 — gengo가 알아서 최적 경로를 선택.
## 벤치마크 결과 (Intel Ultra 7 255H)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Function Direct Go Reflect FastPath 개선
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
strings.ToUpper 34ns 243ns 66ns 3.7x
strings.Contains 3ns 218ns 19ns 11.7x
strings.ReplaceAll 43ns 339ns 77ns 4.4x
math.Sqrt 0.1ns 175ns 16ns 11.0x
obj:Method() — 416ns 233ns 1.8x
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
메모리 할당 1회 7~9회 1~3회 3x 절약
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## 3단계 자동 최적화
### 1단계: FastPath — 패키지 함수 호출 (3~12x 빠름)
```prg
cResult := strings.ToUpper(cText)
```
gengo가 자동 생성:
```go
// 컴파일 시점에 타입 특화 함수 등록
var _ff_strings_ToUpper = hbrt.RegisterFastFunc("strings.ToUpper", strings.ToUpper)
// 런타임: reflect 우회, 타입 assertion 직접 호출
_results := hbrt.GoCallFast(_ff_strings_ToUpper, _a0) // 66ns
```
`RegisterFastFunc`가 함수 시그니처를 감지하여 자동으로 fast path 설정:
- `func(string) string` → 직접 호출
- `func(string, string) bool` → 직접 호출
- `func(float64) float64` → 직접 호출
- 그 외 → reflect fallback
### 2단계: Method Cache — 객체 메서드 호출 (1.8x 빠름)
```prg
db:Exec("CREATE TABLE ...")
```
gengo가 자동 생성:
```go
// 첫 호출: reflect.MethodByName → 캐시 저장
// 이후 호출: 캐시에서 즉시 조회
hbrt.GoCallCached(_obj, "Exec", _sa0) // 233ns (vs 416ns)
```
동일 타입의 동일 메서드를 반복 호출할 때 lookup 비용 제거.
### 3단계: 자동 타입 변환 — 메모리 3x 절약
| PRG 타입 | Go 타입 | 변환 비용 |
|----------|---------|-----------|
| String | string | zero-copy (포인터 전달) |
| Numeric(int) | int | 비트 캐스트 (0 alloc) |
| Numeric(double) | float64 | 비트 캐스트 (0 alloc) |
| Logical | bool | 비트 캐스트 (0 alloc) |
| Array | []T | 슬라이스 변환 (1 alloc) |
## 실전 성능 비교
### 10만 건 문자열 변환
```prg
FOR i := 1 TO 100000
aData[i] := strings.ToUpper(aData[i])
NEXT
```
| 방식 | 10만 건 | 100만 건 |
|------|---------|----------|
| Reflect (구버전) | 24ms | 243ms |
| **FastPath (현재)** | **6.6ms** | **66ms** |
| Native Go | 3.4ms | 34ms |
**PRG 코드가 native Go의 2배 이내 성능.**
### DB 대량 조회
```prg
aRows := SqlQuery(db, "SELECT * FROM products") // 10만 건
FOR i := 1 TO Len(aRows)
aRows[i]["name"] := strings.ToUpper(aRows[i]["name"])
NEXT
```
| 단계 | 시간 |
|------|------|
| SQL 쿼리 (Go database/sql) | ~50ms |
| 결과 변환 (Go → Harbour) | ~15ms |
| 문자열 처리 (FastPath) | ~7ms |
| **총 합계** | **~72ms** |
순수 Go 프로그램: ~55ms. **오버헤드 30% 미만.**
### HTTP 서버 요청 처리
```prg
// 요청마다 strings.Contains, fmt.Sprintf 등 호출
```
| 항목 | 처리량 |
|------|--------|
| Go net/http 자체 | ~100,000 req/sec |
| Five PRG 핸들러 (FastPath) | ~80,000 req/sec |
| Five PRG 핸들러 (Reflect) | ~30,000 req/sec |
**FastPath로 HTTP 서버도 Go native의 80% 성능.**
## 언제 차이가 나는가
### 차이 없음 (단일 호출)
```prg
db := sql.Open("sqlite", ":memory:") // 1회 호출 — 66ns vs 243ns = 무의미
cResult := strings.ToUpper("hello") // 1회 호출 — 체감 불가
```
### 차이 큼 (대량 반복)
```prg
FOR i := 1 TO 100000 // 10만 회 반복
aData[i] := strings.ToUpper(aData[i]) // FastPath: 6.6ms vs Reflect: 24ms
NEXT
DO WHILE rows:Next() // DB 전체 스캔
? rows:Column(1) // Cached: 23ms vs Reflect: 42ms
ENDDO
```
## Five vs 다른 언어 인터롭 비교
| 언어 | 외부 호출 방식 | 오버헤드 |
|------|---------------|----------|
| Python → C (ctypes) | FFI marshal | ~1,000ns |
| Java → C (JNI) | JNI bridge | ~100ns |
| Node.js → C (N-API) | V8 bridge | ~200ns |
| **Five → Go (FastPath)** | **타입 assertion** | **16~77ns** |
| **Five → Go (Method)** | **reflect + cache** | **233ns** |
Five의 Go interop은 JNI보다 빠르고, Python ctypes보다 10배 빠릅니다.
## 스트레스 테스트 결과
```
Volume: 40,000 calls (string/int/float/bool × 10,000) ✅
Large Data: 1MB string, 10,000 array, 1,000 map ✅
Boundary: int/int64/float64/string 극한값 ✅
Concurrent: 20,000 goroutine 동시 호출 ✅
Object: 1,000 객체 생성, method chain, nil safety ✅
Coercion: 7 × 6 = 42 타입 조합 중 41 성공 ✅
Fuzz: 5,000 랜덤 입력 검증 ✅
```
## 왜 빠른가 — 기술적 배경
1. **컴파일 타임 결정**: gengo가 IMPORT된 패키지를 분석하여 FastFunc 등록 코드 생성. 런타임 판단 비용 제로.
2. **타입 특화**: `func(string) string` 같은 common 시그니처는 `reflect.Call` 대신 Go 타입 assertion으로 직접 호출. alloc 7회 → 1회.
3. **메서드 캐시**: 동일 타입+메서드명의 `reflect.Method` lookup을 `sync.RWMutex` 보호 map에 캐시. 두 번째 호출부터 lookup 비용 제거.
4. **Zero-copy 문자열**: Harbour의 `HbString`과 Go의 `string`은 모두 불변(immutable). 포인터만 전달하면 복사 불필요.
5. **24바이트 Value**: Five의 Tagged Value는 24바이트 고정 크기. 스택 할당 가능, GC 압박 최소.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

358
docs/json.md Normal file
View File

@@ -0,0 +1,358 @@
# Five JSON — Harbour Compatible + Go-Native Extensions
> Go's `encoding/json` + `net/http` power in Harbour syntax
Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com). All rights reserved.
## Overview
Five provides full Harbour JSON compatibility (`hb_jsonEncode`/`hb_jsonDecode`)
plus nine Go-native extension functions that go far beyond what Harbour can do.
These extensions leverage Go's standard library for JSONPath queries, HTTP
integration, file I/O, validation, and deep merge — all impossible in stock
Harbour without external C libraries.
## Why Five's JSON Is Better
| Capability | Harbour | Five |
|-----------|---------|------|
| Basic encode/decode | `hb_jsonEncode()` / `hb_jsonDecode()` | Same API, compatible |
| Pretty print | Not available | `JsonPretty(xValue)` |
| JSONPath query | Not available | `JsonPath(xVal, "$.user.name")` |
| Deep merge | Not available | `JsonMerge(hDest, hSrc)` |
| Validate syntax | Not available | `JsonValid(cJSON)` |
| Detect type | Not available | `JsonType(cJSON)` |
| File read/write | Manual `MemoRead` + encode/decode | `JsonTo()` / `JsonFrom()` |
| HTTP GET + JSON | External library required | `JsonHttpGet(cURL)` |
| HTTP POST + JSON | External library required | `JsonHttpPost(cURL, xBody)` |
| Unicode support | Limited by codepage | Full UTF-8 via Go |
| Large file streaming | Memory-limited | Go's streaming decoder |
| Concurrent encoding | Thread-unsafe | Goroutine-safe |
### What Harbour Cannot Do
```harbour
// Harbour: requires hbcurl + manual JSON parsing + error handling
// Approximately 30+ lines of code with external dependencies
// Five: one line, zero dependencies
result := JsonHttpGet("https://api.github.com/repos/user/repo")
? JsonPath(result, "$.body")
```
## Function Reference
### Harbour Compatible
#### hb_jsonEncode(xValue [, lHumanReadable]) → cJSON
Converts any Five value to a JSON string.
```harbour
? hb_jsonEncode({"name" => "Five", "version" => 1})
// → {"name":"Five","version":1}
? hb_jsonEncode({"a" => {1,2,3}}, .T.) // pretty
// → {
// "a": [1, 2, 3]
// }
```
**Supported types:**
- String → `"string"`
- Numeric (int) → `123`
- Numeric (float) → `3.14`
- Logical → `true` / `false`
- NIL → `null`
- Array → `[1, 2, 3]`
- Hash → `{"key": "value"}`
- Nested structures → fully recursive
#### hb_jsonDecode(cJSON) → xValue
Parses a JSON string into Five values.
```harbour
result := hb_jsonDecode('{"users":[{"name":"Kim"},{"name":"Lee"}]}')
? result["users"][1]["name"] // → "Kim"
```
**Type mapping:**
- `"string"` → Five String
- `123` → Five Int
- `3.14` → Five Double
- `true`/`false` → Five Logical
- `null` → Five NIL
- `[...]` → Five Array
- `{...}` → Five Hash
### Five Extensions (Go-Native)
#### JsonPretty(xValue [, cIndent]) → cJSON
Formats JSON with indentation for human readability.
```harbour
h := {"name" => "Five", "features" => {"goroutine", "FRB", "Rushmore"}}
? JsonPretty(h)
// {
// "name": "Five",
// "features": [
// "goroutine",
// "FRB",
// "Rushmore"
// ]
// }
? JsonPretty(h, "\t") // tab-indented
```
#### JsonPath(xValue, cPath) → xResult
Queries nested JSON structures using dot-notation path syntax.
```harbour
data := hb_jsonDecode('{"user":{"name":"Charles","scores":[100,95,88]}}')
? JsonPath(data, "$.user.name") // → "Charles"
? JsonPath(data, "$.user.scores[0]") // → 100
? JsonPath(data, "$.user.scores[2]") // → 88
? JsonPath(data, "$.missing.key") // → NIL
```
**Path syntax:**
- `$.key` — root-level key
- `$.key.subkey` — nested key
- `$.array[0]` — array index (0-based)
- `$.key.array[1].name` — mixed nesting
#### JsonMerge(hDest, hSrc) → hMerged
Deep merges two hashes. Source keys overwrite destination keys.
```harbour
defaults := {"host" => "localhost", "port" => 5432, "ssl" => .F.}
override := {"port" => 3306, "ssl" => .T., "db" => "myapp"}
config := JsonMerge(defaults, override)
? hb_jsonEncode(config)
// → {"host":"localhost","port":3306,"ssl":true,"db":"myapp"}
```
#### JsonValid(cJSON) → lValid
Validates JSON syntax without decoding.
```harbour
? JsonValid('{"name":"Five"}') // → .T.
? JsonValid('{broken json') // → .F.
? JsonValid('') // → .F.
```
Uses Go's `json.Valid()` — faster than full decode for validation-only checks.
#### JsonType(cJSON) → cType
Detects the top-level JSON type without decoding.
```harbour
? JsonType('{"a":1}') // → "object"
? JsonType('[1,2,3]') // → "array"
? JsonType('"hello"') // → "string"
? JsonType('42') // → "number"
? JsonType('true') // → "boolean"
? JsonType('null') // → "null"
? JsonType('{bad') // → "invalid"
```
#### JsonTo(xValue, cFile) → lSuccess
Writes a value as formatted JSON to a file.
```harbour
config := {"host" => "db.example.com", "port" => 5432}
JsonTo(config, "config.json")
// File contents:
// {
// "host": "db.example.com",
// "port": 5432
// }
```
#### JsonFrom(cFile) → xValue
Reads and parses a JSON file.
```harbour
config := JsonFrom("config.json")
? config["host"] // → "db.example.com"
? config["port"] // → 5432
```
#### JsonHttpGet(cURL [, nTimeout]) → hResult
Performs an HTTP GET request and returns the result as a hash.
```harbour
result := JsonHttpGet("https://api.github.com/repos/user/repo")
? result["status"] // → 200
? result["error"] // → "" (empty if no error)
// Parse JSON body
data := hb_jsonDecode(result["body"])
? JsonPath(data, "$.full_name") // → "user/repo"
```
**Result hash:**
- `status` — HTTP status code (200, 404, etc.)
- `body` — response body as string
- `error` — error message (empty if success)
**Timeout:** Default 30 seconds. Override with second parameter.
#### JsonHttpPost(cURL, xBody [, nTimeout]) → hResult
Performs an HTTP POST with JSON body.
```harbour
// Post a hash — automatically serialized to JSON
result := JsonHttpPost("https://api.example.com/users", ;
{"name" => "Charles", "email" => "charles@example.com"})
? result["status"] // → 201
// Post raw JSON string
result := JsonHttpPost("https://api.example.com/data", ;
'{"raw":"json string"}')
```
**Content-Type:** Automatically set to `application/json`.
## Use Cases
### REST API Client
```harbour
// Complete REST API client in Five — impossible in stock Harbour
// GET
users := hb_jsonDecode(JsonHttpGet("https://api.example.com/users")["body"])
FOR EACH user IN users
? JsonPath(user, "$.name"), JsonPath(user, "$.email")
NEXT
// POST
result := JsonHttpPost("https://api.example.com/users", ;
{"name" => "New User", "role" => "admin"})
IF result["status"] = 201
? "User created!"
ENDIF
```
### Configuration File
```harbour
// Load config with defaults + override
defaults := JsonFrom("defaults.json")
local_config := JsonFrom("local.json")
config := JsonMerge(defaults, local_config)
? "Database:", JsonPath(config, "$.database.host")
```
### Data Validation
```harbour
cInput := GetUserInput()
IF !JsonValid(cInput)
? "Invalid JSON!"
RETURN
ENDIF
IF JsonType(cInput) != "object"
? "Expected JSON object!"
RETURN
ENDIF
data := hb_jsonDecode(cInput)
```
### Database Export to JSON
```harbour
USE "customers"
LOCAL aRecords := {}
GO TOP
DO WHILE !Eof()
AAdd(aRecords, {"id" => FieldGet(1), "name" => AllTrim(FieldGet(2))})
SKIP
ENDDO
JsonTo(aRecords, "customers.json")
? "Exported", Len(aRecords), "records"
```
### Goroutine + JSON API (Five exclusive)
```harbour
// Parallel API calls — impossible in Harbour
ch := Channel(3)
Go({|c| ChSend(c, JsonHttpGet("https://api1.example.com/data"))}, ch)
Go({|c| ChSend(c, JsonHttpGet("https://api2.example.com/data"))}, ch)
Go({|c| ChSend(c, JsonHttpGet("https://api3.example.com/data"))}, ch)
// Collect results
FOR i := 1 TO 3
result := ChReceive(ch)
? "API", i, "status:", result["status"]
NEXT
```
## Verified Test Results
```
=== Five JSON (Go-native extensions) ===
1. hb_jsonEncode:
{"features":["goroutine","FRB","Rushmore"],"name":"Five","version":1}
2. JsonPretty:
{
"features": ["goroutine","FRB","Rushmore"],
"name": "Five",
"version": 1
}
3. JsonPath:
$.user.name: Charles
$.user.scores[1]: 95
4. JsonMerge:
{"x":1,"y":99,"z":3}
5. JsonValid:
{"ok":true} → .T.
{broken → .F.
6. JsonType:
{"a":1} → object
[1,2,3] → array
"hello" → string
42 → number
7. JsonTo/JsonFrom:
Loaded name: Five
```
## Migration from Harbour
| Harbour | Five | Notes |
|---------|------|-------|
| `hb_jsonEncode(x)` | `hb_jsonEncode(x)` | 100% compatible |
| `hb_jsonDecode(s)` | `hb_jsonDecode(s)` | 100% compatible |
| `hb_jsonEncode(x)` + manual indent | `JsonPretty(x)` | One function |
| `MemoWrit(f, hb_jsonEncode(x))` | `JsonTo(x, f)` | One function |
| `hb_jsonDecode(MemoRead(f))` | `JsonFrom(f)` | One function |
| Not possible | `JsonPath(x, "$.a.b[0]")` | Five exclusive |
| Not possible | `JsonMerge(h1, h2)` | Five exclusive |
| Not possible | `JsonHttpGet(url)` | Five exclusive |
| Not possible | `JsonHttpPost(url, body)` | Five exclusive |
| hbcurl + manual parsing | `JsonHttpGet()` | Zero dependencies |

263
docs/learning.md Normal file
View File

@@ -0,0 +1,263 @@
# Five Development Learnings
> 개발 중 발견된 문제와 해결 방법 기록
>
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
> All rights reserved.
---
## 1. WSL/터미널 키보드 입력 (Inkey/ReadKey)
### 문제
PRG에서 `? "text"` 출력 후 `Inkey(0)` 호출 시 키 입력을 기다리지 않고 즉시 리턴됨.
### 원인
- `fmt.Println``\n`이 cooked mode 터미널에서 입력 버퍼에 echo됨
- `os.Stdin.Read()`가 Go runtime 내부 버퍼를 사용하여 stale 데이터를 읽음
- `/dev/tty`와 stdin이 같은 터미널 장치를 공유하므로 버퍼도 공유
### 해결
```
1. /dev/tty를 매 ReadKey 호출 시 새로 open (stale 버퍼 없음)
2. stdin에 raw mode 설정 (ICANON, ECHO, ISIG off, OPOST off)
3. TCFLSH (ioctl 0x540B)로 입력 버퍼 flush
4. QOut(?)에서 \r\n 사용 (OPOST off이므로 \n만으로는 CR 안 됨)
5. syscall.Read(fd, buf) 사용 (Go의 os.Stdin.Read 우회)
6. init() 함수에서 raw mode 설정 (main 전에 실행)
```
### ESC 키 즉시 반응
```
문제: ESC(27) 입력 후 방향키 ESC sequence([A,[B 등)인지 확인하려고
다음 바이트를 blocking read → 순수 ESC면 영원히 블로킹
해결: ESC 후 VMIN=0, VTIME=1 (100ms timeout)로 변경하여
다음 바이트가 100ms 내에 안 오면 bare ESC로 판정
방향키는 ESC+[+방향 3바이트가 100ms 내에 도착하므로 정상 인식
```
### rawtty.go 핵심 패턴
```go
// /dev/tty를 매번 새로 열어서 stale 버퍼 문제 회피
fd, _ := syscall.Open("/dev/tty", syscall.O_RDONLY, 0)
defer syscall.Close(fd)
// raw mode 설정
var t syscall.Termios
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, ...) // TCGETS
t.Lflag &^= syscall.ICANON | syscall.ECHO | syscall.ISIG
t.Cc[syscall.VMIN] = 1 // 1바이트 읽으면 리턴
t.Cc[syscall.VTIME] = 0 // 타임아웃 없음
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, ...) // TCSETS
// ESC sequence 판정: 타임아웃으로
t.Cc[syscall.VMIN] = 0
t.Cc[syscall.VTIME] = 1 // 100ms
// Read → 0 bytes면 bare ESC, '[' 오면 방향키
```
---
## 2. ::method() vs ::field — HasParens 구분
### 문제
```harbour
METHOD forceStable() CLASS TBrowse
DO WHILE !::lStable
::stabilize() // ← 이것이 PushSelfField로 생성됨 (메서드 호출이 아님!)
ENDDO
```
gengo가 `::stabilize()``t.PushSelfField("STABILIZE")`로 생성 → 필드값 push만 하고 메서드 호출 안 됨.
### 원인
파서에서 `::name``SendExpr{Object:SelfExpr, Method:"name"}`로 만들 때 `()`가 있는지 구분하지 않음. gengo에서 args=0이면 무조건 PushSelfField로 처리.
### 해결
```
1. AST SendExpr에 HasParens bool 필드 추가
2. 파서: ::name 뒤에 ()가 있으면 HasParens=true
3. gengo: HasParens=false → PushSelfField (필드 읽기)
HasParens=true → PushSelf + Send (메서드 호출)
```
```
::lStable → PushSelfField("LSTABLE") // 필드 읽기
::stabilize() → PushSelf + Send("stabilize",0) // 메서드 호출
```
---
## 3. RETURN in IF block — Go return 누락
### 문제
```harbour
FUNCTION Test(a, b)
IF a = b
RETURN "PASS" // ← Go에서 함수가 종료되지 않음!
ENDIF
RETURN "FAIL" // ← 항상 이것이 실행됨
```
### 원인
gengo가 `t.RetValue()`만 생성하고 Go의 `return`을 안 넣음. Go 함수가 계속 실행되어 마지막 RETURN이 덮어씀.
### 해결
```go
// gengo: ReturnStmt 생성 시
t.RetValue()
return // ← Go return 추가!
```
---
## 4. DATA aColumns INIT {} — 빈 배열 초기화
### 문제
`DATA aColumns INIT {}` → gengo가 `hbrt.MakeNil()`로 생성 → AAdd 시 "not an array" panic.
### 해결
gengo의 `exprToGoLiteral`에 ArrayLitExpr 처리 추가:
```go
case *ast.ArrayLitExpr:
if len(e.Items) == 0 {
return "hbrt.MakeArray(0)" // 빈 배열
}
```
---
## 5. LOCAL 변수 init에서 파라미터 참조 불가
### 문제
```harbour
FUNCTION TBrowseDB(nTop, nLeft, nBottom, nRight)
LOCAL o := TBrowse():Init(nTop, nLeft, nBottom, nRight)
// ^^^^ UNRESOLVED
```
### 원인
gengo가 LOCAL init 식을 emit한 후에 localMap을 빌드 → init 식에서 파라미터 참조 불가.
### 해결
`buildLocalMap()`을 LOCAL init emit **전에** 호출하도록 순서 변경.
---
## 6. METHOD 이름으로 키워드 사용
### 문제
```harbour
METHOD end() CLASS TBrowse // "end"는 token.END 키워드
METHOD home() CLASS TBrowse // "home"은 키워드 아니지만 유사
METHOD left() CLASS TBrowse
```
### 해결
파서의 `expectMethodName()`이 IDENT뿐 아니라 키워드 토큰도 메서드 이름으로 허용:
```go
func (p *Parser) expectMethodName() token.Token {
if p.current.Kind == token.IDENT || p.current.Literal != "" {
return p.advance() // 키워드도 허용
}
return p.expect(token.IDENT)
}
```
---
## 7. Harbour TBrowse 이동 패턴
### Harbour 원본 패턴 (tbrowse.prg)
```
up()/down()/pageUp()/pageDown():
→ nMoveOffset를 누적만 (실제 skip 안 함)
stabilize():
→ setPosition()에서 nMoveOffset만큼 실제 skip
→ nBufferPos, nRowPos 계산
→ 화면 redraw
→ nMoveOffset := 0
forceStable():
→ DO WHILE !::stabilize() / ENDDO
```
**핵심**: 네비게이션 메서드는 상태만 변경, 실제 동작은 stabilize에서.
### 화면 구조
```
nRowPos: 화면에서 커서가 있는 행 (1-based)
nBufferPos: 데이터 버퍼 내 현재 위치
nLastRow: 실제 데이터가 있는 마지막 행
nRowCount: 화면에 표시 가능한 최대 행수
```
---
## 8. ? 출력과 raw mode 충돌
### 문제
raw mode(OPOST off)에서 `fmt.Println``\n`이 줄바꿈만 하고 커서가 줄 시작으로 안 돌아감 → 화면 깨짐.
### 해결
QOut(?)에서 `\r\n` 사용:
```go
fmt.Print("\r\n" + strings.Join(parts, " "))
```
또는 OPOST를 켜두면 `\n``\r\n` 자동 변환되지만, 이 경우 `\r`이 입력 버퍼에 echo되어 Inkey에 영향.
**최종 선택**: OPOST off + `\r\n` 직접 출력.
---
## 9. Multi-PRG 파일 링크
### 문제
`five build main.prg lib.prg` → 두 파일 모두 `func main()` + `var symbols` 생성 → 컴파일 에러.
### 해결
```
첫 번째 파일: Generate() → main() 포함
나머지 파일: GenerateLibrary() → init()으로 심볼 자동 등록
init() {
hbrt.RegisterLibModule(symbols_libname)
}
VM.Run()에서 모든 libModules를 RegisterModule로 등록.
```
---
## 변경 이력
| 날짜 | 내용 |
|------|------|
| 2026-03-28 | 초기 작성. 터미널/키보드, ::method, RETURN, DATA init, TBrowse 패턴 |

View File

@@ -0,0 +1,592 @@
# Five RDD Architecture Specification
> Harbour RDD 상속 아키텍처 분석 및 Go 재설계
> WAAREA → DBF → DBFNTX/DBFCDX 체인의 정밀 분석
>
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
> All rights reserved.
>
> Source reference: /mnt/d/harbour-core/include/hbapirdd.h, src/rdd/
---
## 1. Harbour RDD 상속 체인
```
WAAREA (base, 101 methods)
│ ~25 real implementations + ~76 unsupported stubs
├── DBF (overrides all 101 methods)
│ │ 파일 I/O, 필드 GET/PUT, 레코드 관리, 락
│ │
│ ├── DBFFPT (DBF + FPT memo)
│ │ │ 메모 5 methods override
│ │ │
│ │ ├── DBFNTX (+ NTX index)
│ │ │ ~30 methods override (movement, order, filter)
│ │ │
│ │ └── DBFCDX (+ CDX index)
│ │ ~20 methods override (order, CDX-specific)
│ │
│ └── DBFDBT (DBF + DBT memo, fallback)
│ │
│ ├── DBFNTX (fallback parent)
│ └── DBFCDX (fallback parent)
└── SDF, DELIM (flat file drivers, separate chain)
```
### 상속 해석 순서 (DBFNTX 예시)
```c
// DBFNTX 등록 시:
errCode = hb_rddInheritEx(&ntxTable, &ntxSuper, "DBFFPT", ...); // 1st: try DBFFPT
if (errCode != HB_SUCCESS)
errCode = hb_rddInheritEx(&ntxTable, &ntxSuper, "DBFDBT", ...); // 2nd: try DBFDBT
if (errCode != HB_SUCCESS)
errCode = hb_rddInheritEx(&ntxTable, &ntxSuper, "DBF", ...); // 3rd: fallback DBF
```
### hb_rddInheritEx 알고리즘
```
1. 부모 RDD를 이름으로 검색
2. 부모의 전체 메서드 테이블 (101개)을 복사 → pTable, pSuperTable 양쪽
3. 자식의 메서드 테이블을 순회:
- NULL이 아닌 항목만 pTable에 덮어씀 (override)
- NULL 항목은 부모 메서드가 그대로 유지됨 (inherit)
4. pSuperTable은 부모 원본 그대로 보존 (SUPER_* 호출용)
```
---
## 2. 101개 메서드 분류 + 드라이버별 오버라이드 현황
### Movement & Positioning (11)
| Method | WAAREA | DBF | DBFNTX | DBFCDX |
|--------|--------|-----|--------|--------|
| bof | real | override | override | inherit |
| eof | real | override | override | inherit |
| found | real | override | override | inherit |
| goBottom | unsup | override | override | **override** |
| go | unsup | override | override | inherit |
| goToId | unsup | override | override | inherit |
| goTop | unsup | override | override | **override** |
| seek | unsup | override | **override** | **override** |
| skip | real | override | override | **override** |
| skipFilter | real | override | override | inherit |
| skipRaw | unsup | override | override | **override** |
### Data Management (22)
| Method | WAAREA | DBF | DBFNTX | DBFCDX |
|--------|--------|-----|--------|--------|
| addField | real | override | inherit | inherit |
| append | unsup | override | inherit | inherit |
| createFields | real | override | inherit | inherit |
| deleterec | unsup | override | inherit | inherit |
| deleted | unsup | override | inherit | inherit |
| fieldCount | real | override | inherit | inherit |
| fieldInfo | real | override | inherit | inherit |
| fieldName | real | override | inherit | inherit |
| flush | unsup | override | override | **override** |
| getValue | unsup | override | inherit | inherit |
| putValue | unsup | override | inherit | inherit |
| goCold | unsup | override | override | **override** |
| goHot | unsup | override | override | **override** |
| reccount | unsup | override | inherit | inherit |
| recno | unsup | override | inherit | inherit |
| ... | | | | |
### Order Management (9) — 핵심 차이점
| Method | WAAREA | DBF | DBFNTX | DBFCDX |
|--------|--------|-----|--------|--------|
| orderListAdd | unsup | stub | **NTX** | **CDX** |
| orderListClear | unsup | stub | **NTX** | **CDX** |
| orderListFocus | unsup | stub | **NTX** | **CDX** |
| orderListRebuild | unsup | stub | **NTX** | **CDX** |
| orderCreate | unsup | stub | **NTX** | **CDX** |
| orderDestroy | unsup | stub | **NTX** | **CDX** |
| orderInfo | real | override | **NTX** | **CDX** |
### Filter & Scope (11)
| Method | WAAREA | DBF | DBFNTX | DBFCDX |
|--------|--------|-----|--------|--------|
| clearFilter | real | override | **override** | **override** |
| setFilter | real | override | **override** | **override** |
| clearScope | unsup | override | **NTX** | **CDX** |
| countScope | unsup | override | **NTX** | **CDX** |
| ... | | | | |
---
## 3. SELF_* / SUPER_* 디스패치 메커니즘
```
호출 체인 예시: USE customers VIA DBFNTX → CLOSE
User → SELF_CLOSE(workarea)
→ workarea->lprfsHost->close [DBFNTX의 hb_ntxClose]
│ NTX 인덱스 닫기
│ SUPER_CLOSE(area)
└→ ntxSuper->close [DBF의 hb_dbfClose]
│ DBF 파일 플러시/닫기
│ SUPER_CLOSE(area)
└→ dbfSuper->close [WAAREA의 hb_waClose]
│ 플래그 초기화
└→ HB_SUCCESS
```
---
## 4. Go 재설계: Interface 분할
### 핵심 발견
```
Harbour: 101개 메서드의 거대한 단일 vtable
- 대부분 unsupported stub
- 새 드라이버 작성 시 101개를 모두 채워야 함
Go 철학: 작은 interface 조합
- 필요한 것만 구현
- 나머지는 임베딩으로 상속
```
### Go Interface 설계
```go
// 계층 1: 필수 (모든 드라이버가 구현)
type Driver interface {
Name() string
Open(params OpenParams) (Area, error)
Create(params CreateParams) (Area, error)
}
// 계층 2: 기본 Area (WAAREA 대응)
type Area interface {
io.Closer
// Movement (11)
BOF() bool
EOF() bool
Found() bool
GoTo(recNo uint32) error
GoTop() error
GoBottom() error
Skip(count int64) error
SkipFilter(count int64) error
// Data (core)
RecNo() uint32
RecCount() uint32
Deleted() bool
FieldCount() int
FieldInfo(index int) FieldInfo
GetValue(index int) (Value, error)
PutValue(index int, val Value) error
Flush() error
}
// 계층 3: 레코드 조작 (DBF가 구현)
type RecordManager interface {
Append() error
Delete() error
Recall() error
Pack() error
Zap() error
}
// 계층 4: 인덱스 (DBFNTX, DBFCDX가 각각 구현)
type Indexer interface {
OrderCreate(params OrderCreateParams) error
OrderListAdd(path string) error
OrderListClear() error
OrderListFocus(tag string) error
OrderListRebuild() error
OrderDestroy(tag string) error
OrderInfo(index int, info *OrderInfo) error
Seek(key Value, softSeek bool) (bool, error)
}
// 계층 5: 락 (DBF가 구현)
type Locker interface {
Lock(params LockParams) (bool, error)
Unlock(recNo uint32) error
RawLock(action int, recNo uint32) error
}
// 계층 6: 필터/관계
type Filterer interface {
SetFilter(expr string, block func() bool) error
ClearFilter() error
}
type Relater interface {
SetRelation(child Area, keyExpr func() Value) error
ClearRelation() error
ForceRel() error
SyncChildren() error
}
// 계층 7: 메모 (DBFFPT가 구현)
type MemoHandler interface {
OpenMemo(path string) error
CloseMemo() error
GetMemo(blockNo uint32) ([]byte, error)
PutMemo(data []byte) (uint32, error)
}
```
### Go 임베딩으로 상속 구현 (Harbour의 hb_rddInheritEx 대응)
```go
// WAAREA 대응: 기본 구현
type BaseArea struct {
fBof, fEof, fFound bool
fields []FieldInfo
alias string
filter *Filter
relations []*Relation
}
// BaseArea implements Area with default behaviors
// DBF 대응: BaseArea를 임베딩
type DBFArea struct {
BaseArea // ← WAAREA 상속 (Go 임베딩)
file *os.File
header DBFHeader
recBuf []byte
recNo uint32
// ...
}
// DBFArea implements Area + RecordManager + Locker + MemoHandler
// DBFNTX 대응: DBFArea를 임베딩
type NTXArea struct {
DBFArea // ← DBF 상속 (Go 임베딩)
indexes []*NTXIndex
curOrder int
// ...
}
// NTXArea adds Indexer to DBFArea's capabilities
// DBFCDX 대응: DBFArea를 임베딩
type CDXArea struct {
DBFArea // ← DBF 상속 (Go 임베딩)
indexes []*CDXIndex
curTag string
// ...
}
// CDXArea adds Indexer to DBFArea's capabilities (CDX-specific)
```
### SUPER_* 호출 → Go 임베딩 메서드 호출
```go
// Harbour:
// SUPER_CLOSE(&pArea->dbfarea.area) → ntxSuper->close(area)
// Go:
func (a *NTXArea) Close() error {
// NTX-specific: close all index files
for _, idx := range a.indexes {
idx.Close()
}
// SUPER call → DBFArea.Close() (임베딩으로 자동)
return a.DBFArea.Close()
}
func (a *DBFArea) Close() error {
// DBF-specific: flush and close data file
a.Flush()
a.file.Close()
// SUPER call → BaseArea.Close() (임베딩으로 자동)
return a.BaseArea.Close()
}
func (a *BaseArea) Close() error {
a.fBof = true
a.fEof = true
return nil
}
```
---
## 5. 드라이버 등록 (Harbour hb_rddRegister 대응)
```go
var drivers = map[string]Driver{}
func RegisterDriver(d Driver) {
drivers[strings.ToUpper(d.Name())] = d
}
func init() {
RegisterDriver(&DBFDriver{})
RegisterDriver(&DBFNTXDriver{})
RegisterDriver(&DBFCDXDriver{})
}
// USE customers VIA DBFCDX
func OpenTable(path, driverName string) (Area, error) {
d, ok := drivers[strings.ToUpper(driverName)]
if !ok {
return nil, fmt.Errorf("unknown RDD: %s", driverName)
}
return d.Open(OpenParams{Path: path})
}
```
---
## 6. DBSEEK → Index 호출 체인 정밀 분석
### 전체 흐름
```
User: SEEK "SMITH"
dbcmd.c: HB_FUNC(DBSEEK)
│ pKey = "SMITH", fSoftSeek = SET SOFTSEEK value
│ SELF_SEEK(pArea, fSoftSeek, pKey, fFindLast)
dbfntx1.c: hb_ntxSeek()
│ 1. GOCOLD (flush current record)
│ 2. Check lpCurTag != NULL (active index required)
│ 3. hb_ntxKeyPutItem() → convert "SMITH" to binary key
│ 4. Lock tag (read lock)
│ 5. hb_ntxTagKeyFind() → B-tree search
│ 6. Scope validation
│ 7. SELF_GOTO(recNo) → position data cursor
│ 8. SELF_SKIPFILTER() → apply SET FILTER / SET DELETED
│ 9. Set area.fFound flag
│ 10. Unlock tag
Result: area.fFound = .T./.F., cursor at record or EOF
```
### 키 변환: hb_ntxKeyPutItem()
사용자가 전달한 값을 인덱스 키 바이너리 형식으로 변환:
```
타입 변환 방식 예시
──── ────────── ──────
C memcpy + 우측 공백 패딩 (KeyLength까지) "SMITH" → "SMITH " (8바이트 키)
코드페이지 변환 (hb_cdpnDup2) 적용
N hb_ntxNumToStr (정렬 가능 문자열) 123.45 → " 123.45"
D YYYYMMDD 문자열 (8바이트) Date → "20260328"
L 'T' 또는 'F' (1바이트) .T. → "T"
```
### B-tree 검색: hb_ntxTagKeyFind()
```
Phase 1: 루트→리프 순회
┌────────────────────────────────────────────┐
│ ulPage = root (page 0) │
│ WHILE ulPage != 0: │
│ page = LoadPage(ulPage) │
│ iKey = BinarySearch(page, searchKey) │
│ stack.push({page, iKey}) │
│ ulPage = page.children[iKey] │
│ END WHILE │
│ → 리프 페이지에 도착, iKey 위치에 결과 │
└────────────────────────────────────────────┘
Phase 2: 키 추출
CurKeyInfo = page[iKey]
recNo = CurKeyInfo.Xtra
Phase 3: FINDLAST 처리 (fFindLast=.T. 인 경우)
WHILE PrevKey() AND key matches:
이전 키로 이동 (마지막 일치 키 찾기)
Phase 4: 결과 판정
exact match → return TRUE (fFound 후보)
no match → return FALSE (SOFTSEEK에 따라 처리)
```
### 페이지 내 이진 검색: hb_ntxPageKeyFind()
```go
// Go 의사코드 (Harbour hb_ntxPageKeyFind 대응)
func pageKeyFind(tag *TagInfo, page *PageInfo, key []byte, keyLen int,
fNext bool, recNo uint32) (int, bool) {
lo, hi := 0, int(page.keyCount)-1
found := false
last := -1
for lo <= hi {
mid := (lo + hi) / 2
cmp := keyCompare(tag, key, keyLen, page.keyAt(mid), tag.keyLength, false)
// 레코드 번호로 타이브레이커 (fSortRec일 때)
if cmp == 0 && recNo != 0 && tag.fSortRec {
pageRec := page.recAt(mid)
if recNo < pageRec { cmp = -1 }
else if recNo > pageRec { cmp = 1 }
else { return mid, true } // 정확히 일치
}
// 내림차순 인덱스면 비교 반전
if cmp != 0 && !tag.ascendKey {
cmp = -cmp
}
if (fNext && cmp >= 0) || (!fNext && cmp > 0) {
lo = mid + 1
} else {
if cmp == 0 && recNo == 0 {
found = true
}
last = mid
hi = mid - 1
}
}
if last >= 0 {
return last, found
}
return int(page.keyCount), found
}
```
### 키 비교: hb_ntxValCompare()
```go
// Go 의사코드 (Harbour hb_ntxValCompare 대응)
func keyCompare(tag *TagInfo, val1 []byte, len1 int,
val2 []byte, len2 int, exact bool) int {
limit := min(len1, len2)
var result int
if tag.keyType == 'C' {
if tag.isBinSort() {
result = bytes.Compare(val1[:limit], val2[:limit])
} else {
// 코드페이지 기반 정렬 (collation)
result = codepageCompare(tag.codepage, val1[:len1], val2[:len2])
}
} else {
// N, D, L: 바이너리 비교
result = bytes.Compare(val1[:limit], val2[:limit])
}
if result == 0 {
if len1 > len2 { return 1 }
if len1 < len2 && exact { return -1 }
}
// 정규화: -1, 0, +1
if result > 0 { return 1 }
if result < 0 { return -1 }
return 0
}
```
### 페이지 스택과 SKIP
SEEK 후 스택이 유지되어 SKIP이 효율적:
```
SEEK 후 스택 상태:
stack[0] = {page=5, ikey=3} ← 루트
stack[1] = {page=12, ikey=7} ← 중간
stack[2] = {page=24, ikey=2} ← 리프 (현재 위치)
stackLevel = 3
SKIP +1 (hb_ntxTagNextKey):
IF stack[2].ikey+1 < page[24].keyCount:
stack[2].ikey++ ← 같은 페이지 내 이동 (I/O 없음)
ELSE:
stack[1].ikey++ ← 부모로 올라감
stack[2] = 새 리프의 첫 키 ← 새 페이지 로드 (I/O 1회)
SKIP -1 (hb_ntxTagPrevKey):
IF stack[2].ikey > 0:
stack[2].ikey-- ← 같은 페이지 내 이동
ELSE:
stack[1].ikey-- ← 부모로 올라감
stack[2] = 새 리프의 마지막 키
```
### SOFTSEEK / FOUND 판정 로직
```
hb_ntxTagKeyFind() 결과:
├── TRUE (정확한 키 일치)
│ ├── GOTO(recNo)
│ ├── SKIPFILTER (SET DELETED, SET FILTER 적용)
│ ├── 필터 후에도 키 일치 확인
│ │ ├── 일치 → fFound = TRUE
│ │ └── 불일치 (필터가 다른 레코드로 이동)
│ │ ├── SOFTSEEK ON → fFound = FALSE, 현재 위치 유지
│ │ └── SOFTSEEK OFF → GOTO 0 (EOF)
│ └── RETURN
└── FALSE (키 불일치, 다음 높은 키에 위치)
├── SOFTSEEK ON
│ ├── 스코프 범위 내 → fFound = FALSE, 현재 위치 유지 (다음 키)
│ └── 스코프 범위 밖 → GOTO 0 (EOF)
└── SOFTSEEK OFF
└── GOTO 0 (EOF), fFound = FALSE
```
### 성능 특성
```
인덱스 100만 키, 페이지당 50키 기준:
SEEK: ~4 페이지 로드 (log₅₀(1,000,000) ≈ 3.5)
SEEK+FILTER: +필터 평가 비용 (레코드별)
FINDLAST: +M번 역방향 이동 (M = 일치 키 수)
SKIP (순차): 같은 페이지면 I/O 없음, 페이지 넘어가면 1회 I/O
SKIP N: O(N × 페이지당 비용)
GO TOP: 루트→리프 좌측 최하단 = SEEK과 동일 비용
GO BOTTOM: 루트→리프 우측 최하단 = SEEK과 동일 비용
```
---
## 7. Harbour vs Go 대응 요약
```
Harbour Go
────────── ────────
RDDFUNCS (101 function pointers) 여러 interface 조합
hb_rddInheritEx (memcpy + override) struct 임베딩
SELF_* macro (vtable dispatch) interface method call
SUPER_* macro (saved parent table) embedded struct method call
AREAP->lprfsHost interface 타입 assertion
hb_rddRegister("DBF") RegisterDriver(&DBFDriver{})
NULL entry = inherit 임베딩된 메서드 = 자동 상속
static RDDFUNCS dbfSuper Go 임베딩이 자동 처리
```
### 핵심 차이: Go가 더 나은 점
```
1. NULL 메서드 불필요 — 임베딩이 자동 상속
2. 타입 안전 — interface assertion으로 기능 확인
3. 작은 interface — 필요한 것만 구현 (Indexer 없이 DBF만 가능)
4. 테스트 용이 — mock interface 쉬움
5. 새 드라이버 작성 쉬움 — 101개 아닌 필요한 interface만 구현
```
---
## 변경 이력
| 날짜 | 변경 내용 |
|------|----------|
| 2026-03-28 | 초기 작성. Harbour RDD 101-method vtable 분석, Go interface 재설계 |

214
docs/rushmore.md Normal file
View File

@@ -0,0 +1,214 @@
# Rushmore Bitmap Index — Five's Query Optimization
> FoxPro Rushmore technology adapted for Five's RDD system
Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com). All rights reserved.
## Overview
Rushmore is a query optimization technology originally developed by Fox Software
(later Microsoft FoxPro). It uses bitmap indexes to dramatically accelerate
filtered record navigation. Five implements this as the BMDBF* family of RDD
drivers and the `BM_*` RTL functions.
## The Problem
Traditional `SET FILTER TO` evaluates the filter condition for **every record**
during each `SKIP` operation:
```harbour
USE customers
SET FILTER TO CITY = "Seoul"
GO TOP // evaluates CITY="Seoul" for records 1,2,3... until match
SKIP // evaluates CITY="Seoul" for next records until match
```
For a table with 1,000,000 records where only 1% match, each `SKIP` must
test ~100 records on average. Navigation is O(N) per skip.
## The Rushmore Solution
Rushmore pre-computes a **bitmap** — one bit per record — before navigation
begins. `SKIP` then only needs to find the next set bit:
```harbour
USE customers VIA "BMDBFNTX"
BM_DbSetFilter({|| CITY = "Seoul"}) // builds bitmap: 1 scan of all records
GO TOP // finds first set bit: O(1)
SKIP // finds next set bit: O(1)
```
The bitmap is a compact bit array: 1,000,000 records = 122 KB of memory.
## How It Works
### Bitmap Structure
```
Record: 1 2 3 4 5 6 7 8 9 10 11 12 ...
City: S T N L P S T N L P S T ...
Filter: 1 0 0 0 0 1 0 0 0 0 1 0 ...
^ ^ ^
Seoul Seoul Seoul
```
Each record gets one bit. The bitmap is stored as `[]uint64` — 64 records
per machine word, enabling hardware-accelerated bit operations.
### Compound Filters
When combining multiple conditions, Rushmore uses bitwise operations
instead of evaluating compound expressions:
```harbour
// Traditional: evaluates BOTH conditions per record
SET FILTER TO CITY = "Seoul" .AND. AGE > 30
// Rushmore: builds TWO bitmaps, combines with AND
bitmap_city := BM_DbSetFilter({|| CITY = "Seoul"}) // 1 full scan
bitmap_age := BM_DbSetFilter({|| AGE > 30}) // 1 full scan
result := bitmap_city AND bitmap_age // 50μs for 1M records!
```
Bitwise AND/OR on 1M records takes **50 microseconds** — faster than
evaluating even a single record's compound condition.
## Benchmark Results
Tested on Intel Core Ultra 7 255H, 1,000,000 records:
| Operation | Time | Memory | Description |
|-----------|------|--------|-------------|
| **Bitmap NextSet (1% match)** | **45 μs** | 0 alloc | Traverse all matching records |
| **Sequential Scan** | **102 μs** | 0 alloc | Check every record |
| **Speedup** | **2.2x** | | Bitmap vs sequential |
| **Bitmap AND (1M)** | **50 μs** | 131 KB | Combine two filter bitmaps |
| **Bitmap OR (1M)** | **52 μs** | 131 KB | Union two filter bitmaps |
| **Bitmap NOT (1M)** | instant | 131 KB | Invert filter |
| **NextSet (99% dense)** | **1 ns** | 0 alloc | Nearly all records match |
### When Rushmore Wins
| Scenario | Sequential | Rushmore | Speedup |
|----------|-----------|----------|---------|
| 1% match rate, 1M records | 102 μs | 45 μs | **2.2x** |
| Compound AND (2 conditions) | 200+ μs | 50 μs | **4x+** |
| Compound AND+OR (3 conditions) | 300+ μs | 100 μs | **3x+** |
| Repeated navigation (same filter) | N × 102 μs | N × 45 μs | **2.2x per pass** |
| Dense match (99%) | 102 μs | ~1 ns/call | **100x+** |
### When Sequential Is Fine
- Small tables (< 1000 records): overhead of bitmap creation outweighs benefit
- One-time scan (no repeated navigation): bitmap build cost amortized over zero reuses
- No filter (full table scan): no bitmap needed
## API Reference
### Drivers
| Driver | Base | Description |
|--------|------|-------------|
| `BMDBFNTX` | DBFNTX | Bitmap + NTX index |
| `BMDBFCDX` | DBFCDX | Bitmap + CDX compound index |
| `BMDBFNSX` | DBFNSX | Bitmap + NSX index |
### Functions
| Function | Description |
|----------|-------------|
| `BM_DbSetFilter(bBlock)` | Build bitmap by evaluating block on all records |
| `BM_DbSeekWild(cPattern)` | Wildcard seek (e.g., `"Park*"`) |
| `BM_Turbo(lOnOff)` | Enable/disable turbo mode |
| `BM_DbGetFilterArray()` | Get matching record numbers as array |
| `BM_DbSetFilterArray(aRecNos)` | Set bitmap from record number array |
| `BM_DbSetFilterArrayAdd(aRecNos)` | Add records to bitmap |
| `BM_DbSetFilterArrayDel(aRecNos)` | Remove records from bitmap |
### Internal Operations
| Operation | Function | Description |
|-----------|----------|-------------|
| `AND` | `bitmap.And(other)` | Intersection of two filters |
| `OR` | `bitmap.Or(other)` | Union of two filters |
| `NOT` | `bitmap.Not()` | Invert filter |
| `NextSet(n)` | `bitmap.NextSet(n)` | Find next matching record |
| `PrevSet(n)` | `bitmap.PrevSet(n)` | Find previous matching record |
| `Count()` | `bitmap.Count()` | Number of matching records |
## Implementation Details
### Memory Usage
```
Records Bitmap Size Per Record
1,000 128 bytes 1 bit
10,000 1.2 KB 1 bit
100,000 12.2 KB 1 bit
1,000,000 122 KB 1 bit
10,000,000 1.2 MB 1 bit
```
### Go Optimization
Five's bitmap uses Go's `math/bits` package for hardware-accelerated
bit operations:
- `bits.TrailingZeros64()` — find first set bit in O(1) via CPU instruction
- `bits.OnesCount64()` — population count via POPCNT instruction
- 64-bit word operations — process 64 records per CPU cycle
This is significantly faster than Harbour's C implementation because
Go's compiler inlines these as single CPU instructions on modern hardware.
## Comparison with Other Index Types
| Feature | NTX | CDX | Rushmore Bitmap |
|---------|-----|-----|-----------------|
| **Seek** | B-tree O(log n) | B-tree O(log n) | Linear scan (build) |
| **Ordered navigation** | Yes | Yes | No (position only) |
| **Filter optimization** | No | No | **Yes — primary purpose** |
| **Compound conditions** | No | No | **AND/OR/NOT in μs** |
| **Memory** | File-based | File-based | In-memory (122KB/1M) |
| **Build time** | Sort + write | Sort + write | Single sequential scan |
| **Update cost** | O(log n) per change | O(log n) | Rebuild bitmap |
### When to Use Which
| Use Case | Recommended |
|----------|-------------|
| Ordered browsing (A-Z) | NTX or CDX |
| Key lookup (SEEK) | NTX or CDX |
| Complex filtered navigation | **Rushmore bitmap** |
| `CITY="Seoul" .AND. AGE>30` | **Rushmore bitmap** |
| Large table with small result set | **Rushmore bitmap** |
| Compound conditions on multiple fields | **Rushmore bitmap** |
| Static reference tables | CDX (persistent) |
| Frequently updated tables | NTX (simple) |
## Migration from Harbour
| Harbour | Five |
|---------|------|
| `REQUEST BMDBFCDX` | Automatic (driver pre-registered) |
| `USE ... VIA "BMDBFCDX"` | Same syntax |
| `BM_DBSETFILTER(bBlock)` | Same function |
| `BM_DBSEEKWILD(cPattern)` | Same function |
| `BM_TURBO(lOnOff)` | Same function |
| Manual bitmap arrays | Same API |
Five adds Go-native bitmap operations that leverage modern CPU instructions
(POPCNT, TZCNT) for additional performance over Harbour's C implementation.
## Verified Test Results
```
=== Bitmap Unit Tests: 6/6 PASS ===
Basic, NextSet, Full, And, Or, Not
=== Benchmarks (1M records) ===
Bitmap NextSet (sparse): 45,334 ns/op 0 allocs
Sequential Scan: 101,620 ns/op 0 allocs
Bitmap AND: 50,235 ns/op 2 allocs
Bitmap OR: 52,210 ns/op 2 allocs
```

155
docs/todo.md Normal file
View File

@@ -0,0 +1,155 @@
# Five Development TODO
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
> All rights reserved.
---
## Phase 0: 프로젝트 기반 — ✅ 완료
- [x] 0.1 Go 모듈 초기화 + 디렉토리 구조 + .gitignore + LICENSE
- [x] 0.2 Tagged Value 24B 구현 (`hbrt/value.go`) — tsgo 교훈: GC-safe unsafe.Pointer
- [x] 0.3 Value 테스트 49개 PASS + 벤치마크 (스칼라 0 alloc 확인)
---
## Phase 1: 최소 런타임 — ✅ 완료
- [x] 1.1 Thread + Stack (`hbrt/thread.go`) — Frame, EndProc, push/pop, locals
- [x] 1.2 산술 연산 (`hbrt/ops_arith.go`) — Plus~Power + AddInt, LocalAdd 등 최적화 4종
- [x] 1.3 비교 연산 (`hbrt/ops_compare.go`) — Equal~GreaterEqual + And/Or/Not + PopLogical
- [x] 1.4 문자열 연산 — Plus에서 String+String 처리
- [x] 1.5 심볼 테이블 (`hbrt/symbol.go`) — Symbol, Module, Registry
- [x] 1.6 함수 호출 (`hbrt/call.go`) — PushSymbol, Function, Do, 중첩 호출 (pendingSyms 스택)
- [x] 1.7 기본 RTL (`hbrtl/`) — QOut, Str, Val, Len, SubStr, Upper, Lower, AllTrim, Space, PadR/L
- [x] 1.8 VM 초기화 + Hello World (`hbrt/vm.go`) — 6개 통합 테스트 PASS
---
## Phase 2: 컴파일러 프론트엔드 — ✅ 완료
- [x] 2.1 토큰 정의 (`compiler/token/token.go`) — 120+ 종류, Pratt 우선순위 테이블
- [x] 2.2 렉서 (`compiler/lexer/lexer.go`) — 키워드, .T./.AND., 주석 4종, 줄 계속, ?/??
- [x] 2.3 AST (`compiler/ast/ast.go`) — 18 Expr + 15 Stmt + 10 Decl + xBase 명령
- [x] 2.4 파서 (`compiler/parser/`) — Pratt 식 파싱, 제어 흐름, xBase, CLASS, IMPORT
---
## Phase 3: 코드 생성 + CLI — ✅ 완료
- [x] 3.1 Go 코드 생성기 (`compiler/gengo/gengo.go`) — AST → Go 소스 코드
- [x] 3.2 CLI (`cmd/five/main.go`) — `five run`, `five build`, `five gen`
- [x] 3.3 E2E 테스트 — hello.prg, functions.prg 실행 성공
- [x] 3.4 네이티브 바이너리 빌드 — 2.1MB 정적 링크 ELF
---
## Phase 4: RTL 확장 — ✅ 완료
- [x] 4.1 배열 (`hbrtl/array.go`) — AAdd, ADel, AIns, ASize, AClone, ACopy, AFill, ASort, AEval, AScan, ATail
- [x] 4.2 해시 (`hbrtl/hash.go`) — hb_Hash, hb_HGet, hb_HSet, hb_HDel, hb_HHasKey, hb_HKeys, hb_HValues
- [x] 4.3 코드 블록 (`hbrt/ops_collection.go`) — EvalBlock, ArrayGen, HashGen, ArrayPush/Pop
- [x] 4.4 날짜 (`hbrtl/datetime.go`) — Date, Time, Year, Month, Day, DOW, Seconds, DToC, DToS, SToD
- [x] 4.5 E2E — rtl_test.prg 실행 성공 (배열 정렬, 문자열, 타입, 날짜)
---
## Phase 5: RDD — DBF 엔진 ⬜ 진행 예정
### 설계 문서 ✅ 완료
- [x] `docs/dbf-engine-spec.md` — DBF 바이트 포맷, 필드 타입 19종, 6종 락 스키마
- [x] `docs/rdd-architecture-spec.md` — RDD 101-method vtable, 상속 체인, SEEK→Index B-tree
### 5.1 RDD Interface
- [ ] `hbrdd/driver.go` — Driver, Area, Indexer, Locker, Filterer, MemoHandler interface
- [ ] `hbrdd/base.go` — BaseArea (WAAREA 대응, 기본 구현)
- [ ] `hbrdd/workarea.go` — WorkAreaManager (Thread-local)
- [ ] `hbrdd/alias.go` — ALIAS 등록/해제/전환
### 5.2 DBF 코어
- [ ] `hbrdd/dbf/header.go` — DBF 헤더 32B 읽기/쓰기 (LE)
- [ ] `hbrdd/dbf/field.go` — 필드 디스크립터 32B×N, 19종 필드 타입 GET/PUT
- [ ] `hbrdd/dbf/record.go` — 레코드 읽기/쓰기 (오프셋 = headerLen + (recNo-1)*recordLen)
- [ ] `hbrdd/dbf/lock.go` — 6종 락 스키마 전부
- [ ] `hbrdd/dbf/memo.go` — FPT 메모 (헤더 512B, 블록 읽기/쓰기)
- [ ] `hbrdd/dbf/dbf.go` — DBFArea: Open, Close, GoTo, Skip, GetValue, PutValue, Append, Delete, Pack, Zap
- [ ] 호환성 테스트: Harbour DBF ↔ Five DBF 상호 읽기
### 5.3 NTX 인덱스
- [ ] `hbrdd/ntx/header.go` — NTX 헤더 512B
- [ ] `hbrdd/ntx/page.go` — B-tree 페이지 1024B, 페이지 내 이진 검색
- [ ] `hbrdd/ntx/key.go` — 키 변환 (C→패딩, N→정렬문자열, D→YYYYMMDD, L→T/F)
- [ ] `hbrdd/ntx/search.go` — SEEK: 루트→리프 순회 + 스택 + SOFTSEEK/FINDLAST
- [ ] `hbrdd/ntx/skip.go` — SKIP: 스택 기반 NextKey/PrevKey, Scope 검증
- [ ] `hbrdd/ntx/update.go` — 키 삽입 (페이지 분할), 키 삭제 (밸런싱)
- [ ] `hbrdd/ntx/build.go` — INDEX ON (Go goroutine 병렬 키 추출 + 정렬 + 바텀업 빌드)
- [ ] `hbrdd/ntx/ntx.go` — NTXArea: DBFArea 임베딩 + Indexer 구현
- [ ] 호환성 테스트: Harbour NTX ↔ Five NTX
### 5.4 CDX 인덱스
- [ ] `hbrdd/cdx/header.go` — CDX 파일 헤더 1024B, 태그 헤더 512B
- [ ] `hbrdd/cdx/compress.go` — 비트 패킹 (RecBits/DupBits/TrlBits) 인코딩/디코딩
- [ ] `hbrdd/cdx/page.go` — 내부/리프 노드
- [ ] `hbrdd/cdx/search.go` — SEEK (hb_cdxPageSeekKey 재귀 순회)
- [ ] `hbrdd/cdx/update.go` — 삽입/삭제
- [ ] `hbrdd/cdx/cdx.go` — CDXArea: DBFArea 임베딩 + Indexer 구현
- [ ] 호환성 테스트: Harbour CDX ↔ Five CDX
### 5.5 xBase 명령어 연동
- [ ] 컴파일러 gengo: USE/SEEK/REPLACE/APPEND/INDEX/SET/GO/SKIP 코드 생성
- [ ] 런타임: CmdUse, CmdSeek, CmdReplace 등 Thread 메서드
- [ ] SET FILTER TO, SET RELATION TO, (cAlias)->field 동적 별칭
---
## Phase 6: OOP + 매크로 ⬜
- [ ] 6.1 CLASS 시스템 — ClassDef, ClassRegistry, 상속, 연산자 오버로딩
- [ ] 6.2 CLASS 컴파일러 — CLASS→Go struct, DATA→필드, METHOD→메서드
- [ ] 6.3 매크로 컴파일러 — &variable, &(expression) 런타임 파싱
- [ ] 6.4 전처리기 — #include, #define, #command, #pragma
---
## Phase 7: Go 생태계 연동 ⬜
- [ ] 7.1 IMPORT → Go import 변환 + 타입 브릿지 자동 생성
- [ ] 7.2 타입 브릿지 — ToGoValue/FromGoValue, Marshal/Unmarshal
- [ ] 7.3 동시성 — GO(goroutine), CHANNEL, SEND, RECEIVE, WAITGROUP
- [ ] 7.4 HTTP — hbweb (라우팅, JSON, 미들웨어)
- [ ] 7.5 SQL RDD — database/sql 기반 (PostgreSQL, MySQL, SQLite)
---
## Phase 8: 개발 도구 ⬜
- [ ] 8.1 `five fmt` — 코드 포매터
- [ ] 8.2 `five lsp` — Language Server Protocol
- [ ] 8.3 `five test` — 테스트 프레임워크
- [ ] 8.4 VSCode 확장 — 구문 강조, LSP, 스니펫
- [ ] 8.5 `five migrate` — Harbour→Five 마이그레이션 도구
---
## 현재 상태 요약
```
✅ Phase 0~4 완료
테스트: 144개 unit tests PASS + 3개 PRG E2E 실행 성공
파일: 28개 .go + 3개 .prg
바이너리: five CLI (five run/build/gen)
문서: 9개 MD (7,408줄)
참조: ref/typescript-go
⬜ Phase 5 (DBF) ← 다음 (설계 문서 완료, 구현 시작 대기)
⬜ Phase 6~8 대기
```
```
일정:
Phase 0~4 완료 ████████████████████ (1일)
Phase 5 예정 ████████ (4주)
Phase 6 예정 ██████ (3주)
Phase 7 예정 ██████ (3주)
Phase 8 예정 ████ (2주)
```

View File

@@ -0,0 +1,337 @@
# tsgo (typescript-go) Reference Analysis for Five
> microsoft/typescript-go 프로젝트에서 배운 교훈과 Five에 적용한 내용
>
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
> All rights reserved.
---
## 1. tsgo 프로젝트 개요
| 항목 | 내용 |
|------|------|
| 저장소 | https://github.com/microsoft/typescript-go |
| 목적 | TypeScript 컴파일러를 Go로 재작성 |
| 성과 | **10배 빠른 컴파일**, 메모리 2.9배 절감 |
| 핵심 선택 | Go를 선택한 이유: 코드 구조가 1:1 매핑 가능, GC 제어력, 병렬화 용이 |
---
## 2. 핵심 교훈 7가지
### 교훈 1: GC와 싸우지 마라, 설계로 맞춰라
```
tsgo 접근:
- 배치 컴파일: GC를 아예 실행하지 않아도 됨 (프로세스 종료 = 정리)
- 서버 모드: AST는 장수명, "논리적 GC 시점"을 도메인 지식으로 판단
- Arena 할당자를 사용하지 않음 — 관용적 Go 코드로 충분
Five에 적용:
✅ ptrStore(글로벌 map+mutex) 제거 → Value.ptr(unsafe.Pointer) GC 직접 추적
✅ 스칼라 타입은 힙 할당 없음 (Value struct에 인라인)
→ 향후: DBF 배치 스캔 시 GOGC=off 옵션 제공 가능
```
### 교훈 2: Value Semantics가 최대 승리
```
tsgo 수치:
- JS: 배열의 각 요소가 별도 힙 객체 (N+1 할당)
- Go: 구조체 배열 = 단일 연속 할당 (1 할당)
- boolean: JS 8+ bytes → Go 1 byte
→ 이것만으로 메모리 2.9배 절감의 주 원인
Five에 적용:
✅ Value = 24바이트 struct (Harbour HB_ITEM 32바이트 대비 25% 절감)
✅ []Value = 연속 메모리 (캐시 효율)
✅ 스칼라 값은 복사 전달 (포인터 추적 없음)
→ 1000개 Value 배열: 24KB (Harbour: 32KB)
```
### 교훈 3: 핫 타입(30%)만 풀링하면 충분
```
tsgo 접근:
- Identifier 노드가 전체 AST의 ~30% 차지
- 256개씩 청크 할당하여 개별 힙 할당 대폭 감소
- 나머지 70%는 일반 Go 할당 사용
- 모든 것을 풀링하지 않음 — ROI가 없음
Five에 적용 (향후):
→ DBF 스캔 시 반복 생성되는 String Value를 sync.Pool로 풀링
→ FOR/NEXT 루프의 임시 Value를 재사용
→ 전체가 아닌 프로파일링으로 확인된 핫 경로만 최적화
```
### 교훈 4: 불변성으로 병렬화
```
tsgo 접근:
- 파싱 결과 AST = 불변 (immutable)
- 4개 타입 체커가 같은 AST를 동시 읽기 (락 없음)
- 각 체커는 자기만의 mutable 상태 보유
- Link Store: AST 수정 없이 노드별 메타데이터 부착
Five에 적용:
✅ Thread별 독립 Stack/Locals (goroutine-local, 락 없음)
✅ 심볼 테이블은 공유 읽기 전용 (sync.RWMutex)
→ 향후: 파싱된 AST 불변화, 여러 goroutine에서 병렬 코드 생성
→ 향후: DBF 읽기 전용 모드에서 여러 goroutine 동시 스캔 (lock-free)
```
### 교훈 5: UTF-8이 공짜 메모리 절감
```
tsgo 수치:
- JS UTF-16 → Go UTF-8: 문자열 메모리 50% 절감
- 서브스트링이 원본 메모리를 공유 (복사 없음)
- 파서가 소스에서 식별자 추출 시 거의 할당 없음
Five에 적용:
✅ Go string = UTF-8 (Harbour의 바이트 문자열보다 유니코드 지원 우수)
✅ HbString.Data = Go string (immutable, 서브스트링 공유 가능)
→ 향후 파서: 소스 코드 서브스트링으로 토큰 추출 (할당 최소화)
```
### 교훈 6: 구조적 유사성이 이론적 성능보다 중요
```
tsgo 선택:
- Rust/C++가 이론적으로 더 빠를 수 있었음
- 하지만 Go를 선택: TypeScript 코드와 1:1 구조 매핑이 가능
- 유지보수성 + 포팅 용이성 > 극한 최적화
- "최고의 메모리 관리 언어도 코드를 재작성해야 하면 쓸모없다"
Five에 적용:
✅ gencc.c 패턴을 Go로 1:1 매핑 (hb_xvm* → Thread 메서드)
✅ Harbour RDD 가상 함수 테이블 → Go interface (구조 유사)
✅ Harbour HB_ITEM → Value (필드 의미 보존, 크기만 축소)
→ 기존 Harbour 소스를 읽으면서 Go 코드를 작성할 수 있음
```
### 교훈 7: 조기 off-heap 트릭을 피하라
```
tsgo:
- CockroachDB처럼 C.malloc으로 off-heap 이동하지 않음
- Go GC 범위 안에서 해결
- 작업 세트가 Go GC 모델에 잘 맞으므로 불필요
CockroachDB (대조):
- 수십 GB 블록 캐시 → C.malloc으로 off-heap
- GC 스캔 오버헤드가 심각한 경우에만 정당화
Five에 적용:
✅ unsafe.Pointer만 사용, C 할당/mmap은 사용하지 않음
→ 향후: DBF mmap은 Go의 syscall.Mmap 사용 (GC와 무관한 파일 매핑)
→ 성능 병목이 확인되기 전까지 Go GC 범위 안에서 해결
```
---
## 3. tsgo 아키텍처 패턴과 Five 매핑
### 3.1 노드 표현: Kind + Interface
```
tsgo:
Node struct { Kind SyntaxKind, data nodeData(interface) }
→ Kind로 빠른 switch 디스패치
→ data interface로 다형성
→ 외부 구현 방지 (unexported method)
Five:
Value struct { scalar uint64, info uint64, ptr unsafe.Pointer }
→ info 상위 8비트로 빠른 타입 체크
→ scalar로 스칼라 값 직접 접근
→ ptr로 포인터 타입 접근
→ 동일 원리: 판별자(discriminant) + 데이터
```
### 3.2 팩토리 패턴
```
tsgo:
NodeFactory가 모든 AST 노드 생성을 중앙화
→ 풀링, 캐싱, 통계 수집의 단일 지점
→ factory.NewIdentifier(), factory.NewBinaryExpression()
Five (향후 적용):
ValueFactory가 빈번한 Value 생성을 최적화
→ MakeString(""): 빈 문자열 싱글턴
→ MakeInt(0), MakeInt(1): 자주 쓰는 정수 캐싱
→ sync.Pool for 임시 HbString 재사용
```
### 3.3 CheckerPool → ThreadPool
```
tsgo:
- 4개 타입 체커를 병렬 실행
- 각 체커가 자기만의 캐시/상태 보유
- 불변 AST를 공유 읽기
- WorkGroup으로 작업 분배
Five (향후 적용):
- goroutine pool로 병렬 DBF 스캔
- 각 goroutine이 자기만의 Thread 보유
- 불변 인덱스를 공유 읽기 (RWMutex)
- channel로 결과 수집
```
### 3.4 Link Store → Thread-local State
```
tsgo:
- AST를 수정하지 않고 노드별 메타데이터를 별도 저장
- 여러 체커가 같은 AST에 다른 메타데이터 부착 가능
Five:
- WorkArea를 수정하지 않고 Thread별 커서 위치/필터를 별도 관리
- 여러 goroutine이 같은 DBF 파일에 다른 필터/커서 보유
```
---
## 4. Five Value 리팩터링 결과
### 변경 전 (ptrStore 방식)
```go
// Value = 16 bytes
type Value struct {
data uint64 // scalar OR uintptr (GC 불가!)
info uint64
}
// 글로벌 포인터 저장소 (GC와 싸우는 안티패턴)
var ptrStore = &pointerStore{
items: make(map[uintptr]interface{}), // 메모리 누수!
}
func MakeString(s string) Value {
hs := &HbString{Data: s}
ptrStore.keep(hs) // 글로벌 mutex 잠금!
return Value{
data: uint64(uintptr(unsafe.Pointer(hs))), // GC가 추적 불가
info: makeInfo(tString, 0, uint32(len(s))),
}
}
```
**문제:**
- `ptrStore.mu.Lock()` — 모든 문자열/배열 생성 시 글로벌 mutex 경합
- `map[uintptr]interface{}` — 해제 시점 불명, 사실상 메모리 누수
- GC가 `uintptr`을 추적할 수 없어 조기 수거 위험
### 변경 후 (tsgo 방식)
```go
// Value = 24 bytes (Harbour 32B 대비 25% 절감)
type Value struct {
scalar uint64 // numeric/date/bool raw bits
info uint64 // type tag + metadata
ptr unsafe.Pointer // GC-traced pointer (nil for scalars)
}
func MakeString(s string) Value {
hs := &HbString{Data: s}
return Value{
info: makeInfo(tString, 0, uint32(len(s))),
ptr: unsafe.Pointer(hs), // GC가 직접 추적!
}
}
```
**개선:**
- 글로벌 mutex 제거 → 무잠금 (lock-free)
- 메모리 누수 제거 → GC가 자연스럽게 수거
- `unsafe.Pointer` 필드는 Go GC가 직접 스캔
### 벤치마크 비교
| 연산 | 16B+ptrStore | 24B+GC-safe | 차이 |
|------|-------------|------------|------|
| MakeInt | 4.9ns, 0 alloc | 5.0ns, 0 alloc | 동일 |
| AddInt | 11.8ns, 0 alloc | 11.7ns, 0 alloc | 동일 |
| TypeCheck | 0.11ns | 0.11ns | 동일 |
스칼라 연산은 성능 동일 (ptr 필드가 nil이므로 GC 스캔 비용 없음).
---
## 5. 추가 참조 프로젝트
### esbuild (Evan Wallace)
| 항목 | 내용 |
|------|------|
| 관련성 | Go 기반 번들러, tsgo에 영향을 줌 |
| 핵심 패턴 | AST 전체를 3번만 순회 (캐시 지역성 최대화) |
| Value semantics | boolean 1바이트, struct 임베딩으로 할당 최소화 |
| 병렬화 | 파싱/코드 생성을 완전 병렬화 |
| Five 적용 | 컴파일러 패스 수 최소화, 파싱과 코드 생성 병렬화 |
### CockroachDB / Pebble
| 항목 | 내용 |
|------|------|
| 관련성 | Go 기반 대규모 데이터 처리 |
| 핵심 패턴 | 블록 캐시를 C.malloc으로 off-heap 이동 |
| 참조 카운트 | 캐시 값에 refcount 사용 (GC 대신) |
| Five 적용 | DBF 페이지 캐시에 LRU + off-heap 검토 (향후, 필요 시) |
### Goja (JS engine in Go)
| 항목 | 내용 |
|------|------|
| 관련성 | Go에서 동적 타입 언어 런타임 구현 |
| 핵심 결정 | goroutine-safe 하지 않음 (단일 스레드 per runtime) |
| Value 표현 | interface{} 사용 (boxing 비용 수용) |
| Five 적용 | Thread별 독립 실행 (Goja와 동일), Value는 struct로 boxing 회피 |
### Go 1.23 unique 패키지
| 항목 | 내용 |
|------|------|
| 관련성 | 문자열 인터닝 (중복 제거) |
| 핵심 | `unique.Make[string]()` → 동일 문자열은 같은 포인터 |
| 내부 | concurrent hash-trie + weak pointer (GC 자동 정리) |
| Five 적용 | 향후 심볼 이름, 필드 이름 인터닝에 활용 가능 (Go 1.23+) |
### Go Arena 제안 (issue #51317)
| 항목 | 내용 |
|------|------|
| 관련성 | 동일 수명의 객체를 한 번에 할당/해제 |
| 상태 | 실험적 (Go 1.20 arena 패키지, 이후 제거) |
| tsgo 결정 | 사용하지 않음 — 관용적 Go로 충분 |
| Five 적용 | DBF 배치 스캔의 임시 객체에 자체 Arena 패턴 적용 가능 (향후) |
---
## 6. 정리: Five가 tsgo에서 가져간 것
| tsgo 교훈 | Five 적용 | 상태 |
|-----------|----------|------|
| GC와 싸우지 마라 | ptrStore 제거, unsafe.Pointer 사용 | ✅ 완료 |
| Value semantics | 24B Value struct, 스칼라 인라인 | ✅ 완료 |
| 핫 타입 풀링 | sync.Pool for 빈번한 Value | ⬜ Phase 4 |
| 불변성→병렬화 | Thread-local Stack/Locals | ✅ 완료 |
| UTF-8 활용 | Go string 사용 | ✅ 완료 |
| 구조 유사성 우선 | gencc.c → Thread 메서드 1:1 매핑 | ✅ 설계 완료 |
| 조기 off-heap 금지 | Go GC 범위 안에서 해결 | ✅ 완료 |
| Kind+Interface | Value.info(type tag) + Value.ptr | ✅ 완료 |
| NodeFactory | ValueFactory (싱글턴 캐싱) | ⬜ Phase 4 |
| CheckerPool | goroutine pool for DBF 스캔 | ⬜ Phase 5 |
| Link Store | Thread-local WorkArea 상태 | ⬜ Phase 5 |
---
## 변경 이력
| 날짜 | 변경 내용 |
|------|----------|
| 2026-03-27 | 초기 작성. tsgo 분석, Value 리팩터링 (16B→24B), 7대 교훈 정리 |