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>
21 KiB
Five 1.0 구현 계획서
Date: 2026-04-08 Author: Charles KWON OhJun Prerequisite: FiveSql2 Porting Report Goal: FiveSql2에서 발견된 구조적 문제 해결 + Five 1.0 릴리스 완성
목차
- 우선순위 요약
- Phase 1: @byref 구현 (P0)
- Phase 2: LOCAL 의미론 정리 (P0)
- Phase 3: 런타임 안정화 (P1)
- Phase 4: 성능 최적화 (P2)
- Phase 5: 호환성 검증 (P2)
- 위험 요소 및 대응
- 검증 기준
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의 실제 동작은:
emitFuncDecl에서fn.Body의 VarDecl도 카운트 → Frame에 슬롯 할당emitMidVarDecl에서 LOCAL을 만나면 매번 초기화 코드 실행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를 스캔하지 않으므로:
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()
}
참고: 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 예측 적중
- 벤치마크 기준선:
BenchmarkValueAddInt26.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.'"