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>
670 lines
21 KiB
Markdown
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.'"*
|