# Five 1.0 구현 계획서 **Date:** 2026-04-08 **Author:** Charles KWON OhJun **Prerequisite:** [FiveSql2 Porting Report](FiveSql2-Porting-Report.md) **Goal:** FiveSql2에서 발견된 구조적 문제 해결 + Five 1.0 릴리스 완성 --- ## 목차 1. [우선순위 요약](#1-우선순위-요약) 2. [Phase 1: @byref 구현 (P0)](#2-phase-1-byref-구현) 3. [Phase 2: LOCAL 의미론 정리 (P0)](#3-phase-2-local-의미론-정리) 4. [Phase 3: 런타임 안정화 (P1)](#4-phase-3-런타임-안정화) 5. [Phase 4: 성능 최적화 (P2)](#5-phase-4-성능-최적화) 6. [Phase 5: 호환성 검증 (P2)](#6-phase-5-호환성-검증) 7. [위험 요소 및 대응](#7-위험-요소-및-대응) 8. [검증 기준](#8-검증-기준) --- ## 1. 우선순위 요약 | Phase | 항목 | 우선순위 | 영향 범위 | 난이도 | |-------|------|---------|----------|--------| | 1 | @byref 구현 | **P0** | 모든 @변수 사용 코드 | 높음 | | 2 | LOCAL 의미론 정리 | **P0** | 루프 내 LOCAL 선언 | 중간 | | 3 | 런타임 안정화 | **P1** | 타입 비교, 에러 처리 | 낮음 | | 4 | 성능 최적화 | **P2** | JOIN, CTE, INDEX | 중간 | | 5 | 호환성 검증 | **P2** | 전체 언어 스펙 | 낮음 | --- ## 2. Phase 1: @byref 구현 ### 2.1 문제 정의 현재 `@variable` (pass-by-reference)가 **pass-by-value로 동작**한다. ```go // hbrt/thread.go:350-351 — 현재 상태 func (t *Thread) PushLocalRef(n int) { t.push(t.Local(n)) // 값 복사. 원본 수정 불가. } ``` **피해 범위:** - `FRead(h, @cBuf, n)` — 파일 읽기 결과가 cBuf에 반영 안 됨 - `ParseExpr(tokens, @nPos)` — 위치 추적 불가 - `ASort(aArray, , , {|x,y| ...})` — 배열 원소 교환 불가 (일부 패턴) - Harbour 표준 라이브러리의 30%+ 가 @를 사용 ### 2.2 설계: RefCell 패턴 Harbour의 `hb_struRefer`를 단순화하여 Go에 맞게 설계한다. ``` ┌─────────────────────────────────────────────────┐ │ Harbour @byref: 4가지 참조 대상 │ │ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌───────┐ │ │ │ Local │ │ Static │ │ Block │ │ Item │ │ │ │ variable │ │ variable │ │ local │ │ ptr │ │ │ └──────────┘ └──────────┘ └────────┘ └───────┘ │ │ ↓ ↓ ↓ ↓ │ │ └──────────────┴───────────┴─────────┘ │ │ ↓ │ │ Five RefCell (단일 구조) │ │ │ │ type HbRefCell struct { │ │ V *Value // 공유 포인터 │ │ } │ │ │ │ caller.locals[n] ──→ RefCell.V ←── callee.local │ │ │ │ │ 양쪽 모두 같은 *Value를 가리킴 │ └─────────────────────────────────────────────────┘ ``` ### 2.3 구현 단계 #### Step 1: HbRefCell 구조체 정의 **파일:** `hbrt/value.go` ```go // HbRefCell holds a shared pointer to a Value. // Both caller and callee's local slots point to the same RefCell. type HbRefCell struct { V Value } ``` 이미 `tByref = 12` 타입 상수가 정의되어 있고 `IsByref()` 메서드도 존재한다. 추가할 것: ```go func MakeByref(cell *HbRefCell) Value { return Value{info: uint64(tByref) << 56, ptr: unsafe.Pointer(cell)} } func (v Value) AsRefCell() *HbRefCell { return (*HbRefCell)(v.ptr) } // UnRef follows the reference chain to get the actual value. func (v Value) UnRef() Value { for v.IsByref() { v = v.AsRefCell().V } return v } ``` #### Step 2: PushLocalRef 수정 **파일:** `hbrt/thread.go` ```go func (t *Thread) PushLocalRef(n int) { idx := t.localIndex(n) v := t.locals[idx] // 이미 RefCell이면 그대로 push if v.IsByref() { t.push(v) return } // 새 RefCell 생성, 원본 local을 RefCell로 교체 cell := &HbRefCell{V: v} ref := MakeByref(cell) t.locals[idx] = ref // caller의 local도 RefCell로 바뀜 t.push(ref) // callee에게 같은 RefCell 전달 } ``` #### Step 3: Local/SetLocal에 UnRef 적용 **파일:** `hbrt/thread.go` ```go func (t *Thread) Local(n int) Value { v := t.locals[t.localIndex(n)] if v.IsByref() { return v.AsRefCell().V // 투명하게 역참조 } return v } func (t *Thread) SetLocal(n int, v Value) { idx := t.localIndex(n) existing := t.locals[idx] if existing.IsByref() { existing.AsRefCell().V = v // RefCell 통해 원본 수정 return } t.locals[idx] = v } ``` **주의:** `LocalFast`, `SetLocalFast`, `PushLocalFast`, `PopLocalFast`에도 동일 적용. #### Step 4: RTL 함수 수정 @byref를 받는 RTL 함수들이 callee 측에서 local을 수정해야 한다. **FRead 수정 (`hbrtl/fileio.go`):** ```go func FRead(t *hbrt.Thread) { t.Frame(3, 0) defer t.EndProc() h := t.Local(1).AsInt() nBytes := t.Local(3).AsInt() f, ok := getHandle(h) if !ok { ... } buf := make([]byte, nBytes) n, err := f.Read(buf) if err != nil && n == 0 { ... } // @byref: local(2)에 읽은 데이터 기록 t.SetLocal(2, hbrt.MakeString(string(buf[:n]))) SetFError(0) t.RetInt(int64(n)) } ``` #### Step 5: 컴파일러 — callee 측 처리 현재 gengo는 @param을 받는 **callee** 함수에서 특별한 처리를 하지 않는다. Harbour에서는 callee가 param을 수정하면 caller에 반영된다. **핵심:** Step 3에서 `Local()`/`SetLocal()`이 RefCell을 투명하게 처리하므로, callee의 코드 생성은 변경 불필요. 이것이 RefCell 패턴의 장점이다. ### 2.4 영향 분석 | 파일 | 변경 내용 | 위험도 | |------|----------|--------| | `hbrt/value.go` | HbRefCell, MakeByref, UnRef 추가 | 낮음 — 신규 추가 | | `hbrt/thread.go` | PushLocalRef, Local, SetLocal, +Fast 변형 | **높음** — 핫 패스 | | `hbrtl/fileio.go` | FRead에서 SetLocal(2, ...) | 낮음 | | 기타 RTL | @param 사용하는 함수들 점검 | 중간 | ### 2.5 성능 고려 `Local()`과 `SetLocal()`에 `IsByref()` 분기가 추가된다. 이것은 **모든 local 접근**에 영향을 미치므로 벤치마크 필수. **완화 전략:** ```go // 인라인 가능하도록 hot path를 짧게 유지 func (t *Thread) Local(n int) Value { v := t.locals[t.curFrame.localBase+n-1] if v.Type() != tByref { // 99%의 경우 여기서 리턴 return v } return v.AsRefCell().V } ``` `v.Type()`는 비트 시프트 한 번이므로 ~0.3ns 추가 (벤치마크 기준). @byref가 아닌 경우 branch prediction이 fast path를 학습하므로 실질 영향 미미. ### 2.6 테스트 계획 ```go // hbrt/byref_test.go func TestByrefBasic(t *testing.T) { // @nVal 전달 → callee에서 수정 → caller에서 변경 확인 } func TestByrefChain(t *testing.T) { // f(@x) → g(@x) → g에서 수정 → f, caller 모두 반영 } func TestByrefFRead(t *testing.T) { // FRead(h, @cBuf, n) → cBuf에 파일 내용 반영 } func TestByrefNoRegression(t *testing.T) { // @없는 일반 호출이 기존과 동일하게 동작 } ``` **FiveSql2 회귀 테스트:** - `SqlLoadConstraints`를 원래 `FOpen/FRead/FClose` 방식으로 복원 - 43/43 ALL PASS 유지 확인 --- ## 3. Phase 2: LOCAL 의미론 정리 ### 3.1 문제 정의 ```harbour WHILE condition LOCAL x := {} // Harbour: 매 반복마다 {} 로 재초기화 // Five: Frame() 시점에 NIL로 1회 초기화, := {}는 매번 실행 AAdd(x, item) // Harbour: 항상 1개 원소 // Five: 누적 (현재 동작은 사실 동일하게 매번 실행됨) ENDDO ``` ### 3.2 현재 동작 분석 탐색 결과, Five의 실제 동작은: 1. `emitFuncDecl`에서 `fn.Body`의 VarDecl도 카운트 → Frame에 슬롯 할당 2. `emitMidVarDecl`에서 LOCAL을 만나면 **매번** 초기화 코드 실행 3. `buildLocalMap`은 `fn.Decls`만 스캔 → **mid-function LOCAL이 맵에 없음** ``` 문제의 핵심: ┌────────────────────────────────────────────┐ │ buildLocalMap: fn.Decls만 스캔 (버그) │ │ emitFuncDecl: fn.Decls + fn.Body 카운트 │ │ emitMidVarDecl: 동적으로 맵에 추가 │ │ │ │ → 순서: Frame 호출 → top-level init → │ │ body 실행 중 mid-LOCAL 발견 시 │ │ 동적으로 idx 할당 + 초기화 │ │ │ │ 문제: 루프 안의 LOCAL은 매번 재초기화되지만, │ │ idx가 동적 할당이라 첫 반복에서만 │ │ 맵에 추가됨. 이후 반복은 동일 idx 재사용. │ │ → 초기화 코드는 매번 실행 ✓ │ │ → Harbour와 동일 동작 ✓ │ └────────────────────────────────────────────┘ ``` ### 3.3 실제 위험: buildLocalMap 불일치 `buildLocalMap`이 `fn.Body`를 스캔하지 않으므로: ```harbour FUNCTION Test() LOCAL a := 1 // fn.Decls → buildLocalMap에 있음 ✓ IF condition LOCAL b := 2 // fn.Body → buildLocalMap에 없음 ✗ ? a + b // a는 idx 찾음, b는 emitMidVarDecl에서 동적 할당 ENDIF RETURN ``` `b`를 참조하는 코드가 `emitMidVarDecl` **이전에** 나오면 identifier로 인식 못함. Harbour에서는 LOCAL 선언 이전에 변수를 사용하는 것이 합법이다 (PRIVATE로 처리). 하지만 Five에서는 undeclared warning을 내고 MEMVAR로 처리한다. ### 3.4 수정 계획 #### Step 1: buildLocalMap에 fn.Body 스캔 추가 **파일:** `compiler/gengo/gengo.go:399-415` ```go func (g *Generator) buildLocalMap(fn *ast.FuncDecl) localMap { m := make(localMap) idx := 1 // 1. Parameters for _, p := range fn.Params { m[strings.ToUpper(p.Name)] = idx idx++ } // 2. Top-level declarations for _, d := range fn.Decls { if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal { for _, v := range vd.Vars { m[strings.ToUpper(v.Name)] = idx idx++ } } } // 3. Mid-function LOCALs (NEW: scan fn.Body recursively) g.scanBodyLocals(fn.Body, m, &idx) return m } func (g *Generator) scanBodyLocals(stmts []ast.Stmt, m localMap, idx *int) { for _, s := range stmts { switch st := s.(type) { case *ast.VarDecl: if st.Scope == ast.ScopeLocal { for _, v := range st.Vars { name := strings.ToUpper(v.Name) if _, exists := m[name]; !exists { m[name] = *idx (*idx)++ } } } case *ast.IfStmt: g.scanBodyLocals(st.Body, m, idx) for _, ei := range st.ElseIfs { g.scanBodyLocals(ei.Body, m, idx) } g.scanBodyLocals(st.ElseBody, m, idx) case *ast.DoWhileStmt: g.scanBodyLocals(st.Body, m, idx) case *ast.ForStmt: g.scanBodyLocals(st.Body, m, idx) case *ast.DoCaseStmt: for _, c := range st.Cases { g.scanBodyLocals(c.Body, m, idx) } g.scanBodyLocals(st.Otherwise, m, idx) case *ast.SwitchStmt: for _, c := range st.Cases { g.scanBodyLocals(c.Body, m, idx) } g.scanBodyLocals(st.Default, m, idx) case *ast.BeginSeqStmt: g.scanBodyLocals(st.Body, m, idx) g.scanBodyLocals(st.Recover, m, idx) case *ast.WithObjectStmt: g.scanBodyLocals(st.Body, m, idx) } } } ``` #### Step 2: emitMidVarDecl 단순화 buildLocalMap이 모든 LOCAL을 사전 등록하므로, emitMidVarDecl은 **초기화 코드 실행만** 담당한다. 동적 idx 할당 제거. ```go func (g *Generator) emitMidVarDecl(s *ast.VarDecl, locals localMap) { for _, v := range s.Vars { idx := locals[strings.ToUpper(v.Name)] // 반드시 존재 if v.Init != nil { g.emitExpr(v.Init) g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx)) } } } ``` **효과:** 루프 안 `LOCAL x := {}`는 매 반복마다 `{}` 초기화 실행. 이는 Harbour 동작과 일치한다. ### 3.5 테스트 ```harbour // test_local_loop.prg PROCEDURE Main() LOCAL i, aResult := {} FOR i := 1 TO 3 LOCAL x := {} AAdd(x, i) AAdd(aResult, Len(x)) // 매번 1이어야 함 NEXT // aResult = {1, 1, 1} ✓ (not {1, 2, 3}) Assert(aResult[1] == 1 .AND. aResult[2] == 1 .AND. aResult[3] == 1) RETURN ``` --- ## 4. Phase 3: 런타임 안정화 ### 4.1 TestLessTypeMismatch 수정 **파일:** `hbrt/ops_compare.go:312-320` 현재 string vs numeric 비교가 `Len(string) vs number`로 동작한다. Harbour 원본 동작 확인 필요: ``` Harbour에서: 1 < "hello" → 런타임 에러 (type mismatch) "hello" < 1 → 런타임 에러 Clipper에서: 1 < "hello" → .T. (string이 항상 "큰" 타입) ``` **수정:** Harbour 호환으로 통일 — type mismatch 시 panic 복원. ```go if at != bt { return 0, false // type mismatch → error } ``` ### 4.2 ErrorBlock 전파 개선 현재 `EndProc()`에서 `*HbError`만 re-panic하고 나머지는 stderr 출력 후 re-panic. `BreakValue`도 처리해야 한다: ```go func (t *Thread) EndProc() { if r := recover(); r != nil { t.endFrame() switch r.(type) { case *HbError: panic(r) // BEGIN SEQUENCE가 잡음 case BreakValue: panic(r) // Break()도 전파 default: fmt.Fprintf(os.Stderr, "Five runtime error: %v\n", r) panic(r) } } t.endFrame() } ``` **참고:** `BreakValue`는 `hbrtl/error.go`에 정의되어 있으므로 import cycle 확인 필요. `hbrt` 패키지에 `BreakValue` 타입을 이동하거나 interface assertion 사용. --- ## 5. Phase 4: 성능 최적화 ### 5.1 SQL JOIN: Nested-Loop → Hash Join **현재:** 108ms/query (100 x 200 = 20,000 비교) ``` 현재 알고리즘: FOR each row in left_table // 100 FOR each row in right_table // 200 IF join_condition THEN // 비교 add to result ENDIF NEXT NEXT → O(n * m) = O(20,000) ``` **개선:** Hash Join ``` Hash Join 알고리즘: 1. Build phase: right_table의 join key → hash map // O(m) 2. Probe phase: left_table 각 row에서 hash lookup // O(n) → O(n + m) = O(300) ``` **파일:** `_FiveSql2/src/TSqlExecutor.prg` — `RunSelect` 내 JOIN 처리 부분 **예상 개선:** 108ms → ~15ms (7x) ### 5.2 CTE: Temp DBF → In-Memory Table **현재:** 46ms/query (파일 생성 → 기록 → 닫기 → 재열기 → 삭제) ``` 현재 흐름: dbCreate("__cte_name.dbf") → USE → APPEND → CLOSE → USE → query → CLOSE → FErase ↑ 디스크 I/O 4회 ``` **개선:** RECURSIVE CTE처럼 배열 기반 in-memory 처리 ``` 개선 흐름: aFN := {"COL1", "COL2"} aRows := {{val1, val2}, {val3, val4}} → 디스크 I/O 0회 ``` **파일:** `_FiveSql2/src/TSqlExecutor.prg` — `MaterializeCTE` 메서드 **예상 개선:** 46ms → ~5ms (9x) ### 5.3 NTX INDEX: 단건 삽입 → Bulk Load **현재:** 5,536ms (10K records, 건별 B-tree insert) ``` 현재: record 1개 추가 → B-tree 검색 → 삽입 → 분할 (x10,000) 개선: 전체 정렬 → 정렬된 데이터로 B-tree 일괄 구성 ``` **파일:** `hbrdd/ntx/build.go` **예상 개선:** 5,536ms → ~500ms (10x) ### 5.4 우선순위 | 항목 | 현재 | 예상 | 개선률 | 구현 난이도 | 우선순위 | |------|------|------|--------|-----------|---------| | CTE in-memory | 46ms | 5ms | 9x | 중간 | 1순위 | | Hash JOIN | 108ms | 15ms | 7x | 높음 | 2순위 | | NTX bulk load | 5,536ms | 500ms | 10x | 높음 | 3순위 | | PACK 최적화 | 9,149ms | 1,000ms | 9x | 중간 | 4순위 | --- ## 6. Phase 5: 호환성 검증 ### 6.1 미검증 언어 기능 | 기능 | 검증 방법 | 위험도 | |------|----------|--------| | `@byref` (Phase 1 후) | FRead/@nPos 복원 테스트 | 높음 | | `&cMacro` 매크로 컴파일 | 동적 SQL 표현식 평가 | 중간 | | Multi-goroutine WA | 동시 테이블 접근 테스트 | 중간 | | SWITCH 완전 분기 | 복잡한 CASE 패턴 | 낮음 | | CDX compound index | 멀티태그 인덱스 | 낮음 | | FRB 동적 로딩 | 런타임 심볼 해석 | 낮음 | | GET/READ TUI | 콘솔 입력 처리 | 낮음 | ### 6.2 Harbour 호환성 테스트 스위트 `/mnt/d/harbour-core` 소스에서 핵심 테스트 벡터를 추출하여 Five 전용 호환성 테스트를 구축한다. ``` tests/ ├── compat_byref.prg # @variable 동작 ├── compat_local.prg # LOCAL 의미론 ├── compat_shortcircuit.prg # .AND./.OR. 단축 평가 ├── compat_for_loop.prg # FOR..NEXT LOOP/EXIT ├── compat_sequence.prg # BEGIN SEQUENCE/RECOVER/Break ├── compat_closure.prg # 코드 블록 변수 캡처 ├── compat_workarea.prg # (alias)->(expr) 컨텍스트 ├── compat_types.prg # 타입 비교/변환 규칙 └── compat_static.prg # STATIC 변수 동작 ``` ### 6.3 차이점 문서화 `docs/harbour-compat.md` — 의도적으로 다른 동작과 알려진 제한사항 문서화. --- ## 7. 위험 요소 및 대응 ### 7.1 @byref 성능 리그레션 **위험:** `Local()`/`SetLocal()`에 `IsByref()` 분기 추가 → 전체 성능 저하 **대응:** - `IsByref()` = `v.Type() == tByref` = 비트 시프트 1회 (~0.3ns) - Branch prediction: @byref 미사용 시 99% fast path 예측 적중 - 벤치마크 기준선: `BenchmarkValueAddInt` 26.45 ns → 27ns 이내 허용 - **만약 3% 이상 저하 시:** `LocalFast`/`SetLocalFast`는 byref 체크 생략 (gengo가 @param이 없는 함수에만 Fast 버전 사용하도록 분기) ### 7.2 @byref GC 영향 **위험:** `HbRefCell`이 힙에 할당되어 GC 부담 증가 **대응:** - @byref는 전체 호출의 ~5% 미만 - `HbRefCell`은 24바이트 — Go의 소형 객체 할당기 최적 크기 - 필요시 sync.Pool 사용 가능 (대부분의 RefCell은 수명이 짧음) ### 7.3 LOCAL 스캔 컴파일 시간 증가 **위험:** `scanBodyLocals`가 전체 AST를 재귀 순회 **대응:** - 컴파일 시간에만 영향, 런타임 무관 - FiveSql2 전체 (10,458 lines) 컴파일이 현재 ~2초 — 10% 증가 허용 - 최적화: 함수당 1회만 스캔, 결과 캐시 ### 7.4 Hash JOIN 정확성 **위험:** 부동소수점 key, NULL key, 복합 key 처리 **대응:** - 단계적 도입: 단일 정수/문자열 key만 먼저 구현 - 복합 key는 문자열 직렬화 (`Str(id) + "|" + name`) - NULL key는 별도 버킷 - 기존 nested-loop를 fallback으로 유지 --- ## 8. 검증 기준 ### 8.1 Phase별 완료 조건 | Phase | 완료 조건 | |-------|----------| | Phase 1 | @byref 테스트 통과 + FiveSql2 FRead 복원 + 43/43 유지 + 벤치마크 3% 이내 | | Phase 2 | LOCAL-in-loop 테스트 통과 + buildLocalMap 일치 + 43/43 유지 | | Phase 3 | TestLessTypeMismatch 통과 + go test ./... ALL PASS | | Phase 4 | JOIN < 20ms, CTE < 10ms (100행 기준, 1000회 반복) | | Phase 5 | compat_*.prg 9개 테스트 ALL PASS | ### 8.2 회귀 테스트 매트릭스 모든 Phase에서 다음을 반드시 통과: ``` ✓ go test ./... (Go 유닛 테스트) ✓ five build + test_basic_sql.prg (15/15) ✓ five build + test_sql1999.prg (43/43) ✓ five build + bench_rdd.prg (RDD 정상 동작) ✓ five build + bench_sql.prg (성능 리그레션 없음) ``` ### 8.3 1.0 릴리스 최종 체크리스트 - [ ] Phase 1 완료: @byref 동작 - [ ] Phase 2 완료: LOCAL 의미론 정확 - [ ] Phase 3 완료: go test ALL PASS - [ ] FiveSql2 43/43 ALL PASS - [ ] FiveSql2 Basic 15/15 ALL PASS - [ ] 벤치마크 리그레션 없음 - [ ] `docs/harbour-compat.md` 작성 - [ ] `docs/FiveSql2-Porting-Report.md` 최종 업데이트 - [ ] git tag v1.0.0 --- *"FiveSql2 proves Five is ready. This plan turns 'ready' into 'released.'"*