Files
five/docs/Five-1.0-Implementation-Plan.md
Charles KWON OhJun 486e466592 feat: FiveSql2 43/43, @byref, mutable closure, RTL 479, DateTime fix
Major changes since last commit:
- FiveSql2 SQL:1999 engine (10,458 LOC) — 43/43 ALL PASS
- 21 compiler/runtime bugs fixed (short-circuit AND/OR, FOR LOOP, etc.)
- @byref pass-by-reference via RefCell pattern
- Mutable closure capture (EnsureLocalRef + RefCell sharing)
- RTL: 400 → 479 functions (+79: file, string, datetime, hash, UTF-8)
- DateTime/Timestamp fully working (hb_DateTime, hb_Hour/Min/Sec, display)
- Reserved word guard (39 keywords blocked from function calls)
- AEval arg order fix (element before index)
- Closure capture redecl fix (unique _cap_ names per block)
- Hash/string indexing in ArrayPush/ArrayPop
- Harbour compat test suite: 51/51
- 4 docs: Porting Report, Implementation Plan, Optimization Plan, Commercialization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:35:37 +09:00

21 KiB

Five 1.0 구현 계획서

Date: 2026-04-08 Author: Charles KWON OhJun Prerequisite: FiveSql2 Porting Report Goal: FiveSql2에서 발견된 구조적 문제 해결 + Five 1.0 릴리스 완성


목차

  1. 우선순위 요약
  2. Phase 1: @byref 구현 (P0)
  3. Phase 2: LOCAL 의미론 정리 (P0)
  4. Phase 3: 런타임 안정화 (P1)
  5. Phase 4: 성능 최적화 (P2)
  6. Phase 5: 호환성 검증 (P2)
  7. 위험 요소 및 대응
  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로 동작한다.

// 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

// 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() 메서드도 존재한다. 추가할 것:

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

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

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):

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 접근에 영향을 미치므로 벤치마크 필수.

완화 전략:

// 인라인 가능하도록 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 테스트 계획

// 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 문제 정의

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. buildLocalMapfn.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 불일치

buildLocalMapfn.Body를 스캔하지 않으므로:

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

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 할당 제거.

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 테스트

// 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 복원.

if at != bt {
    return 0, false  // type mismatch → error
}

4.2 ErrorBlock 전파 개선

현재 EndProc()에서 *HbError만 re-panic하고 나머지는 stderr 출력 후 re-panic. BreakValue도 처리해야 한다:

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()
}

참고: BreakValuehbrtl/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.prgRunSelect 내 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.prgMaterializeCTE 메서드

예상 개선: 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.'"