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

670 lines
21 KiB
Markdown

# 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.'"*