- 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>
31 KiB
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.
목차
- 현재 파이프라인: PRG → C
- 목표 파이프라인: PRG → Go
- gencc.c 패턴 분석
- Go 코드 생성 설계
- Go 런타임 라이브러리 설계
- 변환 예시: PRG → C → Go 비교
- 심볼 테이블 생성
- 스코프와 변수 매핑
- 제어 흐름 변환
- OOP 변환
- 매크로와 코드 블록
- 빌드 파이프라인
- 스레딩 문제 해결
- 구현 순서
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
// 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: 산술/비교 연산
// 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)
// 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: 함수 호출
// 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 코드의 함수 전체 구조:
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 함수 호출을 생성한다:
// 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 코드 구조
// 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:
FUNCTION Main()
LOCAL n := 10
? n + 5
RETURN n
C 출력 (gencc.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):
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* 함수 대응
// 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에서는 모든 연산이 에러 시 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에서는 런타임 에러를 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)
}
// 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:
FUNCTION Greet(cName)
LOCAL cMsg
cMsg := "Hello, " + cName + "!"
? cMsg
RETURN cMsg
C (gencc.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 스타일):
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:
FUNCTION SumTo(nMax)
LOCAL nSum := 0, i := 1
DO WHILE i <= nMax
nSum += i
i++
ENDDO
RETURN nSum
C (gencc.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 버전:
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 스타일) — 구조적 버전 (최적화 시):
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:
FUNCTION SafeOpen(cFile)
LOCAL lOk := .F.
BEGIN SEQUENCE
USE (cFile) ALIAS data
lOk := .T.
RECOVER
? "Error opening: " + cFile
END SEQUENCE
RETURN lOk
Go (gengo 스타일):
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)
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 생성
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 변수
// 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] |
변수 접근 코드 생성
// 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의 패턴을 거의 그대로 변환 가능:
// 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로 변환:
// 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 클래스 정의
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 생성 코드
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 런타임에 미니 컴파일러를 포함해야 한다:
// 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)
}
코드 블록 ({|| ...})
LOCAL bBlock := {|x| x * 2}
? Eval(bBlock, 5) // 10
// 코드 블록: 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
}
디태치된 로컬이 있는 코드 블록
FUNCTION MakeCounter()
LOCAL nCount := 0
RETURN {|| nCount++, nCount}
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 사용법
# 단일 파일 컴파일 + 실행
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 (자동 생성)
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 변환 후 스레드 모델
// 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 트랜스파일러 설계 |