- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1199 lines
31 KiB
Markdown
1199 lines
31 KiB
Markdown
# Harbour PRG → Go Transpiler Strategy
|
|
|
|
> PRG 소스를 Go 소스 코드로 변환하는 트랜스파일러 설계 문서
|
|
> 기존 PRG → C (gencc.c/genc.c) 패턴을 분석하고 Go 대응 설계를 제시
|
|
>
|
|
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
> All rights reserved.
|
|
|
|
---
|
|
|
|
## 목차
|
|
|
|
1. [현재 파이프라인: PRG → C](#1-현재-파이프라인-prg--c)
|
|
2. [목표 파이프라인: PRG → Go](#2-목표-파이프라인-prg--go)
|
|
3. [gencc.c 패턴 분석](#3-genccc-패턴-분석)
|
|
4. [Go 코드 생성 설계](#4-go-코드-생성-설계)
|
|
5. [Go 런타임 라이브러리 설계](#5-go-런타임-라이브러리-설계)
|
|
6. [변환 예시: PRG → C → Go 비교](#6-변환-예시-prg--c--go-비교)
|
|
7. [심볼 테이블 생성](#7-심볼-테이블-생성)
|
|
8. [스코프와 변수 매핑](#8-스코프와-변수-매핑)
|
|
9. [제어 흐름 변환](#9-제어-흐름-변환)
|
|
10. [OOP 변환](#10-oop-변환)
|
|
11. [매크로와 코드 블록](#11-매크로와-코드-블록)
|
|
12. [빌드 파이프라인](#12-빌드-파이프라인)
|
|
13. [스레딩 문제 해결](#13-스레딩-문제-해결)
|
|
14. [구현 순서](#14-구현-순서)
|
|
|
|
---
|
|
|
|
## 1. 현재 파이프라인: PRG → C
|
|
|
|
### 전체 흐름
|
|
|
|
```
|
|
test.prg
|
|
│
|
|
▼
|
|
┌──────────────────┐
|
|
│ hb_comp_yyparse │ Bison 파서 (harbour.y)
|
|
│ (파싱) │
|
|
└────────┬─────────┘
|
|
▼
|
|
┌──────────────────┐
|
|
│ pcode 생성 │ AST → 바이트코드 (hbpcode.c)
|
|
└────────┬─────────┘
|
|
▼
|
|
┌──────────────────┐
|
|
│ 최적화 │ hb_compOptimizePCode()
|
|
└────────┬─────────┘
|
|
▼
|
|
┌──────────────────┐
|
|
│ genc.c │ pcode → C 소스 (바이트 배열)
|
|
│ 또는 │ 또는
|
|
│ gencc.c │ pcode → C 함수 호출 (실제 코드)
|
|
└────────┬─────────┘
|
|
▼
|
|
test.c 생성된 C 파일
|
|
│
|
|
▼
|
|
┌──────────────────┐
|
|
│ C 컴파일러 │ gcc / msvc / clang
|
|
│ + Harbour 런타임 │ libharbour.a 링크
|
|
└────────┬─────────┘
|
|
▼
|
|
test.exe 네이티브 바이너리
|
|
```
|
|
|
|
### 두 가지 C 생성 모드
|
|
|
|
| 모드 | 파일 | 출력 형태 | 용도 |
|
|
|------|------|----------|------|
|
|
| **Compact** | `genc.c` | pcode를 바이트 배열로 임베드 | 기본 모드, VM이 해석 실행 |
|
|
| **RealCode** | `gencc.c` | pcode를 `hb_xvm*()` 함수 호출로 변환 | 최적화 모드, 직접 실행 |
|
|
|
|
**Go 변환은 RealCode 모드(gencc.c)를 기반으로 설계한다.**
|
|
pcode 바이트 배열을 임베드하는 것이 아니라, 각 opcode를 Go 런타임 함수 호출로 변환한다.
|
|
|
|
---
|
|
|
|
## 2. 목표 파이프라인: PRG → Go
|
|
|
|
### 전체 흐름
|
|
|
|
```
|
|
test.prg
|
|
│
|
|
▼
|
|
┌──────────────────┐
|
|
│ Parser │ hand-written recursive descent (Go)
|
|
│ (파싱) │
|
|
└────────┬─────────┘
|
|
▼
|
|
┌──────────────────┐
|
|
│ AST │ typed AST nodes
|
|
└────────┬─────────┘
|
|
▼
|
|
┌──────────────────┐
|
|
│ Analyzer │ 타입 추론, 스코프 해석, 상수 폴딩
|
|
└────────┬─────────┘
|
|
▼
|
|
┌──────────────────┐
|
|
│ gengo.go │ AST → Go 소스 코드
|
|
│ (Go 코드 생성) │
|
|
└────────┬─────────┘
|
|
▼
|
|
test_hb.go 생성된 Go 파일
|
|
│
|
|
▼
|
|
┌──────────────────┐
|
|
│ go build │ Go 컴파일러
|
|
│ + hbrt 패키지 │ harbour-go 런타임 라이브러리
|
|
└────────┬─────────┘
|
|
▼
|
|
test 네이티브 바이너리 (단일 파일)
|
|
```
|
|
|
|
### C 모드 vs Go 모드 비교
|
|
|
|
| 항목 | PRG → C (gencc.c) | PRG → Go (gengo.go) |
|
|
|------|-------------------|---------------------|
|
|
| 파서 | Bison (harbour.y) | hand-written recursive descent |
|
|
| 중간 표현 | pcode 바이트코드 | typed AST |
|
|
| 코드 생성 | `fprintf(yyc, "hb_xvm*(...)")` | Go AST → `go/format` |
|
|
| 런타임 | libharbour.a (C) | `hbrt` 패키지 (Go) |
|
|
| 컴파일러 | gcc/msvc | `go build` |
|
|
| 실행 파일 | 동적 링크 가능 | 단일 정적 바이너리 |
|
|
| 스레딩 | pthread + 수동 mutex | goroutine + channel |
|
|
| GC | 자체 mark-sweep | Go 런타임 GC |
|
|
|
|
---
|
|
|
|
## 3. gencc.c 패턴 분석
|
|
|
|
### 핵심 패턴: pcode opcode → C 런타임 함수 호출
|
|
|
|
gencc.c는 각 pcode opcode를 대응하는 `hb_xvm*()` C 함수 호출로 변환한다.
|
|
|
|
#### 패턴 1: 값 Push
|
|
|
|
```c
|
|
// pcode: HB_P_PUSHINT (정수 push)
|
|
// 생성되는 C 코드:
|
|
fprintf(yyc, "\tif( hb_xvmPushInt( %dL ) ) break;\n", value);
|
|
|
|
// pcode: HB_P_PUSHSTRCONST (문자열 상수 push)
|
|
// 생성되는 C 코드:
|
|
fprintf(yyc, "\thb_xvmPushStringConst( \"hello\", 5 );\n");
|
|
|
|
// pcode: HB_P_PUSHLOCAL (로컬 변수 push)
|
|
// 생성되는 C 코드:
|
|
fprintf(yyc, "\tif( hb_xvmPushLocal( %d ) ) break;\n", localIndex);
|
|
```
|
|
|
|
#### 패턴 2: 산술/비교 연산
|
|
|
|
```c
|
|
// pcode: HB_P_PLUS
|
|
fprintf(yyc, "\tif( hb_xvmPlus() ) break;\n");
|
|
|
|
// 최적화: PUSH + 연산 결합
|
|
// pcode: HB_P_PUSHINT(5) + HB_P_PLUS
|
|
fprintf(yyc, "\tif( hb_xvmAddInt( 5L ) ) break;\n");
|
|
```
|
|
|
|
#### 패턴 3: 제어 흐름 (goto + label)
|
|
|
|
```c
|
|
// pcode: HB_P_JUMP → C goto
|
|
fprintf(yyc, "\tgoto lab%05u;\n", labelNumber);
|
|
|
|
// pcode: HB_P_JUMPFALSE → C conditional goto
|
|
fprintf(yyc, "\tif( hb_xvmPopLogical(&fValue) ) break;\n");
|
|
fprintf(yyc, "\tif( !fValue )\n\t\tgoto lab%05u;\n", labelNumber);
|
|
|
|
// 점프 대상 → C label
|
|
fprintf(yyc, "lab%05u: ;\n", labelNumber);
|
|
```
|
|
|
|
#### 패턴 4: 함수 호출
|
|
|
|
```c
|
|
// pcode: HB_P_FUNCTION → C 호출
|
|
fprintf(yyc, "\tif( hb_xvmFunction( %hu ) ) break;\n", paramCount);
|
|
|
|
// pcode: HB_P_SEND → 메서드 호출
|
|
fprintf(yyc, "\tif( hb_xvmSend( %hu ) ) break;\n", paramCount);
|
|
```
|
|
|
|
#### 패턴 5: 에러 처리 (do-break 패턴)
|
|
|
|
```c
|
|
// 생성되는 C 코드의 함수 전체 구조:
|
|
HB_FUNC( MYFUNC )
|
|
{
|
|
do {
|
|
// ... 모든 코드가 do { } while(0) 안에 ...
|
|
// hb_xvm*() 함수가 TRUE 반환 = 에러 발생
|
|
// → break로 do 블록 탈출
|
|
if( hb_xvmPlus() ) break;
|
|
if( hb_xvmPopLocal(1) ) break;
|
|
} while( 0 );
|
|
hb_xvmExitProc();
|
|
}
|
|
```
|
|
|
|
### gencc.c의 최적화 패턴
|
|
|
|
gencc.c는 연속된 pcode를 분석하여 융합된 C 함수 호출을 생성한다:
|
|
|
|
```c
|
|
// PUSHINT + POPLOCAL → hb_xvmLocalSetInt()
|
|
// PUSHINT + PLUS → hb_xvmAddInt()
|
|
// PUSHINT + EQUAL + JUMPFALSE → hb_xvmEqualIntIs() + goto
|
|
// PUSHINT + RETVALUE → hb_xvmRetInt()
|
|
// PUSHLOCAL + PLUS + POPLOCAL → hb_xvmLocalAdd()
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Go 코드 생성 설계
|
|
|
|
### 핵심 원칙
|
|
|
|
```
|
|
1. gencc.c가 hb_xvm*() C 함수를 호출하듯,
|
|
gengo는 hbrt.*() Go 함수를 호출하는 Go 코드를 생성한다.
|
|
|
|
2. C의 goto+label → Go의 구조적 제어 흐름으로 변환한다.
|
|
(Go에는 goto가 있으나, 구조적 변환이 가능하면 우선 사용)
|
|
|
|
3. C의 do{...break...}while(0) 에러 패턴 →
|
|
Go의 error 반환 또는 panic/recover로 변환한다.
|
|
```
|
|
|
|
### 생성되는 Go 코드 구조
|
|
|
|
```go
|
|
// test.prg에서 생성된 test_hb.go
|
|
package main
|
|
|
|
import "harbour-go/hbrt"
|
|
|
|
// 심볼 테이블
|
|
var symbols = hbrt.NewSymbolTable(
|
|
hbrt.Symbol{Name: "MAIN", Scope: hbrt.FsPublic | hbrt.FsLocal, Func: HB_MAIN},
|
|
hbrt.Symbol{Name: "HELPER", Scope: hbrt.FsStatic | hbrt.FsLocal, Func: HB_HELPER},
|
|
hbrt.Symbol{Name: "QOUT", Scope: hbrt.FsPublic, Func: nil}, // 외부
|
|
)
|
|
|
|
func HB_MAIN(t *hbrt.Thread) {
|
|
t.Frame(0, 2) // 파라미터 0개, 로컬 2개
|
|
defer t.EndProc()
|
|
|
|
// LOCAL nCount := 10
|
|
t.PushInt(10)
|
|
t.PopLocal(1)
|
|
|
|
// LOCAL cName := "World"
|
|
t.PushString("World")
|
|
t.PopLocal(2)
|
|
|
|
// ? "Hello " + cName
|
|
t.PushSymbol(symbols.Find("QOUT"))
|
|
t.PushNil()
|
|
t.PushString("Hello ")
|
|
t.PushLocal(2)
|
|
t.Plus()
|
|
t.Function(1)
|
|
|
|
// RETURN nCount
|
|
t.PushLocal(1)
|
|
t.RetValue()
|
|
}
|
|
|
|
func HB_HELPER(t *hbrt.Thread) {
|
|
t.Frame(1, 0) // 파라미터 1개, 로컬 0개
|
|
defer t.EndProc()
|
|
|
|
// RETURN param1 * 2
|
|
t.PushLocal(1)
|
|
t.PushInt(2)
|
|
t.Mult()
|
|
t.RetValue()
|
|
}
|
|
|
|
func main() {
|
|
hbrt.Init(symbols)
|
|
hbrt.Run("MAIN")
|
|
}
|
|
```
|
|
|
|
### C gencc.c 출력 vs Go gengo 출력 비교
|
|
|
|
**동일한 PRG:**
|
|
|
|
```harbour
|
|
FUNCTION Main()
|
|
LOCAL n := 10
|
|
? n + 5
|
|
RETURN n
|
|
```
|
|
|
|
**C 출력 (gencc.c):**
|
|
|
|
```c
|
|
HB_FUNC( MAIN )
|
|
{
|
|
do {
|
|
static const HB_BYTE pcode[] = { ... };
|
|
hb_xvmFrame( 0, 1 );
|
|
hb_xvmLocalSetInt( 1, 10L );
|
|
hb_xvmPushSymbol( symbols + 1 ); // QOUT
|
|
hb_xvmPushNil();
|
|
if( hb_xvmPushLocal( 1 ) ) break;
|
|
if( hb_xvmAddInt( 5L ) ) break;
|
|
if( hb_xvmFunction( 1 ) ) break;
|
|
if( hb_xvmPushLocal( 1 ) ) break;
|
|
hb_xvmRetValue();
|
|
} while( 0 );
|
|
hb_xvmExitProc();
|
|
}
|
|
```
|
|
|
|
**Go 출력 (gengo):**
|
|
|
|
```go
|
|
func HB_MAIN(t *hbrt.Thread) {
|
|
t.Frame(0, 1)
|
|
defer t.EndProc()
|
|
|
|
t.LocalSetInt(1, 10)
|
|
t.PushSymbol(symbols.At(1)) // QOUT
|
|
t.PushNil()
|
|
t.PushLocal(1)
|
|
t.AddInt(5)
|
|
t.Function(1)
|
|
t.PushLocal(1)
|
|
t.RetValue()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Go 런타임 라이브러리 설계
|
|
|
|
### 패키지 구조
|
|
|
|
```
|
|
harbour-go/
|
|
├── cmd/
|
|
│ └── harbour/ ← CLI: harbour build, harbour run
|
|
│ └── main.go
|
|
├── hbrt/ ← 핵심 런타임
|
|
│ ├── value.go ← Tagged Value 16B
|
|
│ ├── thread.go ← goroutine별 실행 컨텍스트
|
|
│ ├── stack.go ← eval 스택
|
|
│ ├── symbol.go ← 심볼 테이블
|
|
│ ├── class.go ← OOP 클래스 시스템
|
|
│ ├── error.go ← 에러 처리 (BEGIN SEQUENCE)
|
|
│ ├── ops_arith.go ← 산술 연산 (Plus, Minus, ...)
|
|
│ ├── ops_compare.go ← 비교 연산
|
|
│ ├── ops_string.go ← 문자열 연산
|
|
│ ├── macro.go ← 런타임 매크로 컴파일
|
|
│ └── init.go ← 초기화
|
|
├── hbrtl/ ← 표준 라이브러리 (RTL)
|
|
│ ├── strings.go ← SUBSTR, ALLTRIM, UPPER, ...
|
|
│ ├── numeric.go ← INT, VAL, STR, ROUND, ...
|
|
│ ├── datetime.go ← DATE, TIME, CTOD, DTOC, ...
|
|
│ ├── file.go ← FOPEN, FCLOSE, FREAD, ...
|
|
│ ├── console.go ← QOUT, ACCEPT, INKEY, ...
|
|
│ └── ...
|
|
├── hbrdd/ ← RDD (데이터베이스)
|
|
│ ├── workarea.go
|
|
│ ├── dbf.go
|
|
│ ├── ntx.go
|
|
│ ├── cdx.go
|
|
│ └── ...
|
|
└── compiler/ ← 컴파일러 (PRG → Go)
|
|
├── lexer.go
|
|
├── parser.go
|
|
├── ast.go
|
|
├── analyzer.go
|
|
└── gengo.go ← Go 코드 생성기
|
|
```
|
|
|
|
### hbrt.Thread — gencc.c의 hb_xvm* 함수 대응
|
|
|
|
```go
|
|
// hbrt/thread.go
|
|
type Thread struct {
|
|
stack []Value // eval 스택
|
|
sp int // 스택 포인터
|
|
calls []CallFrame // 호출 스택
|
|
locals []Value // 현재 함수의 로컬 변수
|
|
memvars map[string]*Value // PRIVATE 변수
|
|
statics *[]Value // STATIC 변수 (모듈별)
|
|
vm *VM // 공유 VM 상태
|
|
}
|
|
```
|
|
|
|
### hb_xvm* → Thread 메서드 매핑
|
|
|
|
gencc.c가 생성하는 C 함수 호출과 Go 런타임 메서드의 1:1 매핑:
|
|
|
|
```
|
|
C (gencc.c 출력) Go (Thread 메서드)
|
|
───────────────────────────────── ──────────────────────────────
|
|
hb_xvmFrame(params, locals) t.Frame(params, locals)
|
|
hb_xvmExitProc() t.EndProc()
|
|
hb_xvmPushNil() t.PushNil()
|
|
hb_xvmPushInt(n) t.PushInt(n)
|
|
hb_xvmPushLong(n) t.PushLong(n)
|
|
hb_xvmPushDouble(v, w, d) t.PushDouble(v, w, d)
|
|
hb_xvmPushStringConst(s, len) t.PushString(s)
|
|
hb_xvmPushLogical(b) t.PushBool(b)
|
|
hb_xvmPushDate(julian) t.PushDate(julian)
|
|
hb_xvmPushLocal(n) t.PushLocal(n)
|
|
hb_xvmPushLocalByRef(n) t.PushLocalRef(n)
|
|
hb_xvmPushStatic(n) t.PushStatic(n)
|
|
hb_xvmPushStaticByRef(n) t.PushStaticRef(n)
|
|
hb_xvmPushMemvar(sym) t.PushMemvar(sym)
|
|
hb_xvmPushField(sym) t.PushField(sym)
|
|
hb_xvmPushSymbol(sym) t.PushSymbol(sym)
|
|
hb_xvmPopLocal(n) t.PopLocal(n)
|
|
hb_xvmPopStatic(n) t.PopStatic(n)
|
|
hb_xvmPopMemvar(sym) t.PopMemvar(sym)
|
|
hb_xvmPopField(sym) t.PopField(sym)
|
|
hb_xvmPop() t.Pop()
|
|
hb_xvmDuplicate() t.Dup()
|
|
hb_xvmPlus() t.Plus()
|
|
hb_xvmMinus() t.Minus()
|
|
hb_xvmMult() t.Mult()
|
|
hb_xvmDivide() t.Divide()
|
|
hb_xvmModulus() t.Modulus()
|
|
hb_xvmPower() t.Power()
|
|
hb_xvmNegate() t.Negate()
|
|
hb_xvmInc() t.Inc()
|
|
hb_xvmDec() t.Dec()
|
|
hb_xvmEqual() t.Equal()
|
|
hb_xvmExactlyEqual() t.ExactEqual()
|
|
hb_xvmNotEqual() t.NotEqual()
|
|
hb_xvmLess() t.Less()
|
|
hb_xvmLessEqual() t.LessEqual()
|
|
hb_xvmGreater() t.Greater()
|
|
hb_xvmGreaterEqual() t.GreaterEqual()
|
|
hb_xvmNot() t.Not()
|
|
hb_xvmAnd() t.And()
|
|
hb_xvmOr() t.Or()
|
|
hb_xvmFunction(n) t.Function(n)
|
|
hb_xvmDo(n) t.Do(n)
|
|
hb_xvmSend(n) t.Send(n)
|
|
hb_xvmRetValue() t.RetValue()
|
|
hb_xvmArrayGen(n) t.ArrayGen(n)
|
|
hb_xvmArrayPush() t.ArrayPush()
|
|
hb_xvmArrayPop() t.ArrayPop()
|
|
hb_xvmSeqBegin(offset) t.SeqBegin(recoverFunc)
|
|
hb_xvmSeqEnd() t.SeqEnd()
|
|
hb_xvmSeqRecover() t.SeqRecover()
|
|
hb_xvmPopLogical(&fValue) t.PopLogical() bool
|
|
hb_xvmLocalSetInt(n, val) t.LocalSetInt(n, val)
|
|
hb_xvmAddInt(n) t.AddInt(n)
|
|
hb_xvmRetInt(n) t.RetInt(n)
|
|
hb_xvmLocalAdd(n) t.LocalAdd(n)
|
|
hb_xvmEqualIntIs(n, &fValue) t.EqualIntIs(n) bool
|
|
```
|
|
|
|
### 에러 처리 패턴 변환
|
|
|
|
**C (gencc.c): `if(hb_xvm*()) break` 패턴**
|
|
|
|
```c
|
|
// C에서는 모든 연산이 에러 시 TRUE 반환
|
|
// break로 do{}while(0) 탈출 후 hb_xvmExitProc()
|
|
do {
|
|
if( hb_xvmPlus() ) break;
|
|
if( hb_xvmPopLocal(1) ) break;
|
|
} while( 0 );
|
|
hb_xvmExitProc();
|
|
```
|
|
|
|
**Go: panic/recover 패턴**
|
|
|
|
```go
|
|
// Go에서는 런타임 에러를 panic으로 전파
|
|
// defer t.EndProc()가 recover() 포함
|
|
func HB_MAIN(t *hbrt.Thread) {
|
|
t.Frame(0, 1)
|
|
defer t.EndProc() // recover() + 스택 정리
|
|
|
|
// hb_xvm*()의 break 패턴이 불필요
|
|
// 에러 시 Plus() 내부에서 panic
|
|
t.Plus()
|
|
t.PopLocal(1)
|
|
}
|
|
```
|
|
|
|
```go
|
|
// hbrt/thread.go
|
|
func (t *Thread) EndProc() {
|
|
if r := recover(); r != nil {
|
|
if hbErr, ok := r.(*HbError); ok {
|
|
t.handleError(hbErr) // BEGIN SEQUENCE 처리
|
|
} else {
|
|
panic(r) // 예상치 못한 panic은 전파
|
|
}
|
|
}
|
|
t.restoreFrame()
|
|
}
|
|
|
|
func (t *Thread) Plus() {
|
|
b := t.pop()
|
|
a := t.peek() // top을 교체할 것이므로 peek
|
|
result, err := valueAdd(a, b)
|
|
if err != nil {
|
|
panic(t.runtimeError("EG_ARG", "+", a, b))
|
|
}
|
|
t.setTop(result)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 변환 예시: PRG → C → Go 비교
|
|
|
|
### 예시 1: 기본 함수
|
|
|
|
**PRG:**
|
|
```harbour
|
|
FUNCTION Greet(cName)
|
|
LOCAL cMsg
|
|
cMsg := "Hello, " + cName + "!"
|
|
? cMsg
|
|
RETURN cMsg
|
|
```
|
|
|
|
**C (gencc.c 스타일):**
|
|
```c
|
|
HB_FUNC( GREET )
|
|
{
|
|
do {
|
|
hb_xvmFrame( 1, 1 ); // 파라미터 1, 로컬 1
|
|
hb_xvmPushStringConst("Hello, ", 7);
|
|
if( hb_xvmPushLocal(1) ) break; // cName
|
|
if( hb_xvmPlus() ) break;
|
|
hb_xvmPushStringConst("!", 1);
|
|
if( hb_xvmPlus() ) break;
|
|
if( hb_xvmPopLocal(2) ) break; // cMsg
|
|
hb_xvmPushSymbol(symbols + 1); // QOUT
|
|
hb_xvmPushNil();
|
|
if( hb_xvmPushLocal(2) ) break; // cMsg
|
|
if( hb_xvmFunction(1) ) break;
|
|
if( hb_xvmPushLocal(2) ) break; // cMsg
|
|
hb_xvmRetValue();
|
|
} while(0);
|
|
hb_xvmExitProc();
|
|
}
|
|
```
|
|
|
|
**Go (gengo 스타일):**
|
|
```go
|
|
func HB_GREET(t *hbrt.Thread) {
|
|
t.Frame(1, 1)
|
|
defer t.EndProc()
|
|
|
|
t.PushString("Hello, ")
|
|
t.PushLocal(1) // cName
|
|
t.Plus()
|
|
t.PushString("!")
|
|
t.Plus()
|
|
t.PopLocal(2) // cMsg
|
|
t.PushSymbol(sym_QOUT)
|
|
t.PushNil()
|
|
t.PushLocal(2) // cMsg
|
|
t.Function(1)
|
|
t.PushLocal(2) // cMsg
|
|
t.RetValue()
|
|
}
|
|
```
|
|
|
|
### 예시 2: DO WHILE 루프
|
|
|
|
**PRG:**
|
|
```harbour
|
|
FUNCTION SumTo(nMax)
|
|
LOCAL nSum := 0, i := 1
|
|
DO WHILE i <= nMax
|
|
nSum += i
|
|
i++
|
|
ENDDO
|
|
RETURN nSum
|
|
```
|
|
|
|
**C (gencc.c 스타일):**
|
|
```c
|
|
HB_FUNC( SUMTO )
|
|
{
|
|
do {
|
|
HB_BOOL fValue;
|
|
hb_xvmFrame( 1, 2 );
|
|
hb_xvmLocalSetInt( 2, 0L ); // nSum := 0
|
|
hb_xvmLocalSetInt( 3, 1L ); // i := 1
|
|
lab00001: ;
|
|
if( hb_xvmPushLocal( 3 ) ) break; // i
|
|
if( hb_xvmPushLocal( 1 ) ) break; // nMax
|
|
if( hb_xvmLessEqual() ) break;
|
|
if( hb_xvmPopLogical( &fValue ) ) break;
|
|
if( !fValue )
|
|
goto lab00002;
|
|
if( hb_xvmPushLocal( 3 ) ) break; // i
|
|
hb_xvmLocalAdd( 2 ); // nSum += top
|
|
hb_xvmLocalAddInt( 3, 1 ); // i++
|
|
goto lab00001;
|
|
lab00002: ;
|
|
if( hb_xvmPushLocal( 2 ) ) break; // nSum
|
|
hb_xvmRetValue();
|
|
} while(0);
|
|
hb_xvmExitProc();
|
|
}
|
|
```
|
|
|
|
**Go (gengo 스타일) — goto 버전:**
|
|
```go
|
|
func HB_SUMTO(t *hbrt.Thread) {
|
|
t.Frame(1, 2)
|
|
defer t.EndProc()
|
|
|
|
t.LocalSetInt(2, 0) // nSum := 0
|
|
t.LocalSetInt(3, 1) // i := 1
|
|
|
|
lab00001:
|
|
t.PushLocal(3) // i
|
|
t.PushLocal(1) // nMax
|
|
t.LessEqual()
|
|
if !t.PopLogical() {
|
|
goto lab00002
|
|
}
|
|
t.PushLocal(3) // i
|
|
t.LocalAdd(2) // nSum += top
|
|
t.LocalAddInt(3, 1) // i++
|
|
goto lab00001
|
|
|
|
lab00002:
|
|
t.PushLocal(2) // nSum
|
|
t.RetValue()
|
|
}
|
|
```
|
|
|
|
**Go (gengo 스타일) — 구조적 버전 (최적화 시):**
|
|
```go
|
|
func HB_SUMTO(t *hbrt.Thread) {
|
|
t.Frame(1, 2)
|
|
defer t.EndProc()
|
|
|
|
t.LocalSetInt(2, 0) // nSum := 0
|
|
t.LocalSetInt(3, 1) // i := 1
|
|
|
|
for {
|
|
t.PushLocal(3) // i
|
|
t.PushLocal(1) // nMax
|
|
t.LessEqual()
|
|
if !t.PopLogical() {
|
|
break
|
|
}
|
|
t.PushLocal(3) // i
|
|
t.LocalAdd(2) // nSum += top
|
|
t.LocalAddInt(3, 1) // i++
|
|
}
|
|
|
|
t.PushLocal(2) // nSum
|
|
t.RetValue()
|
|
}
|
|
```
|
|
|
|
### 예시 3: BEGIN SEQUENCE
|
|
|
|
**PRG:**
|
|
```harbour
|
|
FUNCTION SafeOpen(cFile)
|
|
LOCAL lOk := .F.
|
|
BEGIN SEQUENCE
|
|
USE (cFile) ALIAS data
|
|
lOk := .T.
|
|
RECOVER
|
|
? "Error opening: " + cFile
|
|
END SEQUENCE
|
|
RETURN lOk
|
|
```
|
|
|
|
**Go (gengo 스타일):**
|
|
```go
|
|
func HB_SAFEOPEN(t *hbrt.Thread) {
|
|
t.Frame(1, 1)
|
|
defer t.EndProc()
|
|
|
|
t.PushBool(false)
|
|
t.PopLocal(2) // lOk := .F.
|
|
|
|
t.SeqBegin(func(t *hbrt.Thread) {
|
|
// RECOVER 블록
|
|
t.PushSymbol(sym_QOUT)
|
|
t.PushNil()
|
|
t.PushString("Error opening: ")
|
|
t.PushLocal(1) // cFile
|
|
t.Plus()
|
|
t.Function(1)
|
|
})
|
|
|
|
// BEGIN SEQUENCE 블록
|
|
t.PushSymbol(sym_USE)
|
|
t.PushNil()
|
|
t.PushLocal(1) // cFile
|
|
t.Do(1)
|
|
t.PushBool(true)
|
|
t.PopLocal(2) // lOk := .T.
|
|
|
|
t.SeqEnd()
|
|
|
|
t.PushLocal(2) // lOk
|
|
t.RetValue()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 심볼 테이블 생성
|
|
|
|
### C 생성 (genc.c)
|
|
|
|
```c
|
|
HB_INIT_SYMBOLS_BEGIN( hb_vm_SymbolInit_TEST )
|
|
{ "MAIN", {HB_FS_PUBLIC | HB_FS_LOCAL | HB_FS_FIRST}, {HB_FUNCNAME( MAIN )}, NULL },
|
|
{ "QOUT", {HB_FS_PUBLIC}, {HB_FUNCNAME( QOUT )}, NULL },
|
|
{ "HELPER", {HB_FS_STATIC | HB_FS_LOCAL}, {HB_FUNCNAME( HELPER )}, NULL },
|
|
{ "USE", {HB_FS_PUBLIC}, {NULL}, NULL }
|
|
HB_INIT_SYMBOLS_END( hb_vm_SymbolInit_TEST )
|
|
```
|
|
|
|
### Go 생성
|
|
|
|
```go
|
|
package main
|
|
|
|
import "harbour-go/hbrt"
|
|
import "harbour-go/hbrtl"
|
|
|
|
var symbols = hbrt.NewModule("TEST",
|
|
hbrt.Sym("MAIN", hbrt.FsPublic|hbrt.FsLocal|hbrt.FsFirst, HB_MAIN),
|
|
hbrt.Sym("QOUT", hbrt.FsPublic, hbrtl.QOUT),
|
|
hbrt.Sym("HELPER", hbrt.FsStatic|hbrt.FsLocal, HB_HELPER),
|
|
hbrt.Sym("USE", hbrt.FsPublic, nil), // RDD 명령
|
|
)
|
|
|
|
// 컴파일 타임 심볼 참조 (인덱스 캐싱)
|
|
var (
|
|
sym_MAIN = symbols.At(0)
|
|
sym_QOUT = symbols.At(1)
|
|
sym_HELPER = symbols.At(2)
|
|
sym_USE = symbols.At(3)
|
|
)
|
|
```
|
|
|
|
### STATIC 변수
|
|
|
|
```go
|
|
// C: static 배열로 생성
|
|
// HB_FUNC_INITSTATICS() { ... }
|
|
|
|
// Go: 모듈 레벨 변수로 생성
|
|
var statics_TEST = hbrt.NewStatics(3) // 3개의 STATIC 변수
|
|
|
|
// STATIC 초기화 함수
|
|
func init() {
|
|
hbrt.RegisterStatics("TEST", statics_TEST)
|
|
// STATIC nCounter := 0
|
|
statics_TEST.Set(0, hbrt.MakeInt(0))
|
|
// STATIC cPrefix := "LOG_"
|
|
statics_TEST.Set(1, hbrt.MakeString("LOG_"))
|
|
// STATIC aCache := {}
|
|
statics_TEST.Set(2, hbrt.MakeArray(0))
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. 스코프와 변수 매핑
|
|
|
|
### Harbour 변수 스코프 → Go 매핑
|
|
|
|
| Harbour 스코프 | C 매핑 | Go 매핑 |
|
|
|---------------|--------|---------|
|
|
| LOCAL | 스택의 HB_ITEM | `t.locals[]` (Thread-local) |
|
|
| STATIC | 모듈 static 배열 | `statics_MODULE[]` (패키지 변수) |
|
|
| PRIVATE | 동적 심볼의 memvar | `t.memvars[name]` (Thread-local map) |
|
|
| PUBLIC | 동적 심볼의 memvar | `vm.publics[name]` (sync.RWMutex 보호) |
|
|
| FIELD | WorkArea 필드 | `t.currentWA().Field(name)` |
|
|
| PARAMETER | 스택의 HB_ITEM (음수 오프셋) | `t.locals[1..paramCount]` |
|
|
|
|
### 변수 접근 코드 생성
|
|
|
|
```go
|
|
// LOCAL nX → t.PushLocal(n) / t.PopLocal(n)
|
|
// STATIC nY → t.PushStatic(n) / t.PopStatic(n)
|
|
// PRIVATE cZ → t.PushMemvar(sym) / t.PopMemvar(sym)
|
|
// PUBLIC cW → t.PushMemvar(sym) / t.PopMemvar(sym) (PUBLIC도 memvar)
|
|
// FIELD->Name → t.PushField(sym) / t.PopField(sym)
|
|
// alias->Name → t.PushAliasedField(alias, sym)
|
|
```
|
|
|
|
---
|
|
|
|
## 9. 제어 흐름 변환
|
|
|
|
### C의 goto/label → Go 변환 전략
|
|
|
|
**전략 1: 직접 goto (단순, 기본)**
|
|
|
|
Go도 goto를 지원한다. gencc.c의 패턴을 거의 그대로 변환 가능:
|
|
|
|
```go
|
|
// DO WHILE → goto loop
|
|
lab_1:
|
|
t.PushLocal(1)
|
|
t.PushInt(10)
|
|
t.LessEqual()
|
|
if !t.PopLogical() { goto lab_2 }
|
|
// body
|
|
goto lab_1
|
|
lab_2:
|
|
```
|
|
|
|
**전략 2: 구조적 변환 (최적화)**
|
|
|
|
컴파일러가 제어 흐름 그래프를 분석하여 for/if/switch로 변환:
|
|
|
|
```go
|
|
// DO WHILE → for loop (분석 후 변환)
|
|
for {
|
|
t.PushLocal(1)
|
|
t.PushInt(10)
|
|
t.LessEqual()
|
|
if !t.PopLogical() { break }
|
|
// body
|
|
}
|
|
```
|
|
|
|
**권장: Phase 1에서는 goto, Phase 2에서 구조적 변환 추가**
|
|
|
|
### 제어 흐름 매핑
|
|
|
|
| Harbour | C (gencc.c) | Go (gengo) |
|
|
|---------|------------|------------|
|
|
| `DO WHILE ... ENDDO` | label + goto | `for { if !cond { break } }` |
|
|
| `FOR i := 1 TO n` | label + goto + addint | `for { ... t.LocalAddInt() }` |
|
|
| `FOR EACH x IN arr` | label + goto + enum | `for { ... t.EnumNext() }` |
|
|
| `IF ... ELSEIF ... ENDIF` | jumpfalse + label | `if ... { } else if ... { }` |
|
|
| `DO CASE ... ENDCASE` | jumpfalse chain | `switch { case: ... }` 또는 if chain |
|
|
| `BEGIN SEQUENCE` | seqbegin + recover addr | `t.SeqBegin(recoverFunc)` |
|
|
| `SWITCH ... END` | jump table | `switch t.PopInt() { case: }` |
|
|
|
|
---
|
|
|
|
## 10. OOP 변환
|
|
|
|
### PRG 클래스 정의
|
|
|
|
```harbour
|
|
CLASS Person
|
|
DATA cName INIT ""
|
|
DATA nAge INIT 0
|
|
|
|
METHOD New(cName, nAge)
|
|
METHOD Greet()
|
|
ENDCLASS
|
|
|
|
METHOD New(cName, nAge) CLASS Person
|
|
::cName := cName
|
|
::nAge := nAge
|
|
RETURN Self
|
|
|
|
METHOD Greet() CLASS Person
|
|
? "Hi, I'm " + ::cName
|
|
RETURN Self
|
|
```
|
|
|
|
### Go 생성 코드
|
|
|
|
```go
|
|
func init() {
|
|
hbrt.RegisterClass("PERSON", func(cls *hbrt.ClassDef) {
|
|
cls.Data("CNAME", hbrt.MakeString(""))
|
|
cls.Data("NAGE", hbrt.MakeInt(0))
|
|
cls.Method("NEW", HB_PERSON_NEW)
|
|
cls.Method("GREET", HB_PERSON_GREET)
|
|
})
|
|
}
|
|
|
|
func HB_PERSON_NEW(t *hbrt.Thread) {
|
|
t.Frame(2, 0)
|
|
defer t.EndProc()
|
|
|
|
// ::cName := cName
|
|
t.PushLocal(1) // cName 파라미터
|
|
t.PushSelf()
|
|
t.SendAssign("CNAME")
|
|
|
|
// ::nAge := nAge
|
|
t.PushLocal(2) // nAge 파라미터
|
|
t.PushSelf()
|
|
t.SendAssign("NAGE")
|
|
|
|
// RETURN Self
|
|
t.PushSelf()
|
|
t.RetValue()
|
|
}
|
|
|
|
func HB_PERSON_GREET(t *hbrt.Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
|
|
// ? "Hi, I'm " + ::cName
|
|
t.PushSymbol(sym_QOUT)
|
|
t.PushNil()
|
|
t.PushString("Hi, I'm ")
|
|
t.PushSelf()
|
|
t.Send0("CNAME") // ::cName 읽기
|
|
t.Plus()
|
|
t.Function(1)
|
|
|
|
// RETURN Self
|
|
t.PushSelf()
|
|
t.RetValue()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 11. 매크로와 코드 블록
|
|
|
|
### 매크로 (`&variable`)
|
|
|
|
매크로는 **런타임 컴파일**이 필요하다. Go 런타임에 미니 컴파일러를 포함해야 한다:
|
|
|
|
```go
|
|
// PRG: USE &cFile
|
|
// Go 생성 코드:
|
|
t.PushLocal(1) // cFile 값을 스택에
|
|
t.MacroPush() // 런타임 컴파일 + 실행
|
|
|
|
// hbrt/macro.go
|
|
func (t *Thread) MacroPush() {
|
|
str := t.pop().AsString()
|
|
// 미니 파서 + 컴파일러로 str을 파싱
|
|
// 결과 코드 실행하여 값을 스택에 push
|
|
code := t.vm.macroCompiler.Compile(str)
|
|
code.Execute(t)
|
|
}
|
|
```
|
|
|
|
### 코드 블록 (`{|| ...}`)
|
|
|
|
```harbour
|
|
LOCAL bBlock := {|x| x * 2}
|
|
? Eval(bBlock, 5) // 10
|
|
```
|
|
|
|
```go
|
|
// 코드 블록: Go 클로저로 변환
|
|
// 캡처하는 로컬이 없으면 → 순수 함수
|
|
// 캡처하는 로컬이 있으면 → 클로저 + detached locals
|
|
|
|
func HB_MAIN(t *hbrt.Thread) {
|
|
t.Frame(0, 1)
|
|
defer t.EndProc()
|
|
|
|
// LOCAL bBlock := {|x| x * 2}
|
|
t.PushBlock(func(bt *hbrt.Thread) {
|
|
bt.Frame(1, 0)
|
|
defer bt.EndProc()
|
|
bt.PushLocal(1) // x
|
|
bt.PushInt(2)
|
|
bt.Mult()
|
|
bt.RetValue()
|
|
}, 0) // 0 = 캡처하는 로컬 없음
|
|
t.PopLocal(1)
|
|
|
|
// ? Eval(bBlock, 5)
|
|
t.PushSymbol(sym_QOUT)
|
|
t.PushNil()
|
|
t.PushLocal(1) // bBlock
|
|
t.PushInt(5)
|
|
t.EvalBlock(1) // Eval(block, 1 param)
|
|
t.Function(1) // QOut
|
|
}
|
|
```
|
|
|
|
### 디태치된 로컬이 있는 코드 블록
|
|
|
|
```harbour
|
|
FUNCTION MakeCounter()
|
|
LOCAL nCount := 0
|
|
RETURN {|| nCount++, nCount}
|
|
```
|
|
|
|
```go
|
|
func HB_MAKECOUNTER(t *hbrt.Thread) {
|
|
t.Frame(0, 1)
|
|
defer t.EndProc()
|
|
|
|
t.PushInt(0)
|
|
t.PopLocal(1) // nCount := 0
|
|
|
|
// 코드 블록 + 로컬 캡처
|
|
t.PushBlockWithLocals(func(bt *hbrt.Thread) {
|
|
bt.Frame(0, 0)
|
|
defer bt.EndProc()
|
|
bt.PushDetachedLocal(0) // nCount (detached)
|
|
bt.Inc()
|
|
bt.PopDetachedLocal(0)
|
|
bt.PushDetachedLocal(0)
|
|
bt.RetValue()
|
|
}, []int{1}) // 로컬 인덱스 1 (nCount)을 캡처
|
|
t.RetValue()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 12. 빌드 파이프라인
|
|
|
|
### CLI 사용법
|
|
|
|
```bash
|
|
# 단일 파일 컴파일 + 실행
|
|
harbour run test.prg
|
|
|
|
# 컴파일만 (Go 소스 생성)
|
|
harbour build test.prg -o test_hb.go
|
|
|
|
# 프로젝트 빌드 (여러 PRG → Go → 바이너리)
|
|
harbour build ./src/*.prg -o myapp
|
|
|
|
# 내부 동작:
|
|
# 1. *.prg → parser → AST
|
|
# 2. AST → analyzer
|
|
# 3. analyzer → gengo → *_hb.go
|
|
# 4. go build -o myapp ./_harbour_build/
|
|
```
|
|
|
|
### 생성되는 프로젝트 구조
|
|
|
|
```
|
|
_harbour_build/ (임시 빌드 디렉토리)
|
|
├── go.mod
|
|
├── main.go (엔트리포인트)
|
|
├── test_hb.go (test.prg에서 생성)
|
|
├── utils_hb.go (utils.prg에서 생성)
|
|
└── module_init.go (모듈 초기화, 심볼 테이블 결합)
|
|
```
|
|
|
|
### main.go (자동 생성)
|
|
|
|
```go
|
|
package main
|
|
|
|
import "harbour-go/hbrt"
|
|
|
|
func main() {
|
|
vm := hbrt.NewVM()
|
|
vm.RegisterModule(symbols_TEST)
|
|
vm.RegisterModule(symbols_UTILS)
|
|
vm.RegisterRTL() // 표준 라이브러리
|
|
vm.RegisterRDD() // RDD 드라이버
|
|
vm.Run("MAIN")
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 13. 스레딩 문제 해결
|
|
|
|
### PRG → Go 변환이 스레딩 문제를 해결하는 방법
|
|
|
|
| 기존 문제 (C) | Go 변환에서의 해결 |
|
|
|--------------|-------------------|
|
|
| 글로벌 GC suspend 경합 | Go GC에 위임. HB_GARBAGE 계층 완전 제거 |
|
|
| 심볼 테이블 수동 락 | `hbrt.SymbolTable`에 `sync.RWMutex` 캡슐화 |
|
|
| 정적 버퍼 공유 | `Thread` 구조체에 모든 상태 캡슐화 |
|
|
| WorkArea 크로스 스레드 | `Thread.currentWA()` = goroutine-local |
|
|
| STRING 참조 카운트 레이스 | Go string = immutable, 또는 `atomic.Int32` refcount |
|
|
| 클래스 풀 lazy init | `init()` 함수에서 완전 초기화, `sync.Once` 불필요 |
|
|
| `hb_vmThreadRequest` volatile | `context.Context` 취소 신호 |
|
|
|
|
### Go 변환 후 스레드 모델
|
|
|
|
```go
|
|
// Harbour의 hb_threadStart() → Go의 goroutine
|
|
func HB_MYTHREADSTART(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
|
|
// hb_threadStart(@WorkerFunc())
|
|
t.PushLocal(1) // 함수 심볼
|
|
|
|
// Go에서는 goroutine으로 실행
|
|
workerSym := t.pop()
|
|
go func() {
|
|
newThread := t.vm.NewThread()
|
|
defer newThread.Destroy()
|
|
newThread.PushSymbol(workerSym)
|
|
newThread.PushNil()
|
|
newThread.Function(0)
|
|
}()
|
|
}
|
|
```
|
|
|
|
```
|
|
각 goroutine은 독립된 Thread를 소유:
|
|
- stack, locals, memvars: goroutine-local (락 불필요)
|
|
- symbols, classes: 공유 읽기 전용 (RWMutex)
|
|
- publics: sync.RWMutex 보호
|
|
- WorkArea: goroutine별 소유권
|
|
```
|
|
|
|
---
|
|
|
|
## 14. 구현 순서
|
|
|
|
### Phase 0: 기반 타입 (1주)
|
|
|
|
```
|
|
구현: value.go (Tagged Value 16B)
|
|
검증: 모든 타입의 생성, 접근, 변환 단위 테스트
|
|
```
|
|
|
|
### Phase 1: 최소 런타임 (2주)
|
|
|
|
```
|
|
구현: thread.go, stack.go, symbol.go, ops_arith.go, ops_compare.go
|
|
검증: 수동으로 작성한 Go 코드로 "? 1 + 2" 실행
|
|
```
|
|
|
|
### Phase 2: 파서 (3주)
|
|
|
|
```
|
|
구현: lexer.go, parser.go, ast.go
|
|
검증: test.prg → AST 덤프 출력
|
|
```
|
|
|
|
### Phase 3: 코드 생성기 (3주)
|
|
|
|
```
|
|
구현: analyzer.go, gengo.go
|
|
검증: test.prg → test_hb.go 생성 → go build → 실행
|
|
대상: LOCAL, IF, DO WHILE, FUNCTION, RETURN
|
|
```
|
|
|
|
### Phase 4: RTL 기본 (2주)
|
|
|
|
```
|
|
구현: hbrtl/ (QOUT, STR, VAL, SUBSTR, DATE 등 핵심 50개 함수)
|
|
검증: 기본 Harbour 프로그램 실행
|
|
```
|
|
|
|
### Phase 5: OOP + 코드 블록 (2주)
|
|
|
|
```
|
|
구현: class.go, 코드 블록 변환, Eval()
|
|
검증: CLASS 정의 + 메서드 호출 동작
|
|
```
|
|
|
|
### Phase 6: 매크로 + PP (2주)
|
|
|
|
```
|
|
구현: macro.go (미니 컴파일러), 전처리기
|
|
검증: &variable, #command 동작
|
|
```
|
|
|
|
### Phase 7: RDD (4주)
|
|
|
|
```
|
|
구현: hbrdd/ (DBF 읽기/쓰기, NTX/CDX 인덱스)
|
|
검증: USE, APPEND, SEEK, REPLACE, INDEX ON
|
|
```
|
|
|
|
### Phase 8: 도구 체인 (2주)
|
|
|
|
```
|
|
구현: CLI (harbour build/run/fmt), LSP 기본
|
|
검증: VSCode에서 .prg 편집, 빌드, 실행
|
|
```
|
|
|
|
---
|
|
|
|
## 변경 이력
|
|
|
|
| 날짜 | 변경 내용 |
|
|
|------|----------|
|
|
| 2026-03-27 | 초기 작성. PRG→C 패턴 분석 및 PRG→Go 트랜스파일러 설계 |
|