Five v0.9 — Harbour + Go fusion language

- 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>
This commit is contained in:
2026-03-31 09:41:50 +09:00
commit 59568f3301
282 changed files with 66658 additions and 0 deletions

263
docs/learning.md Normal file
View File

@@ -0,0 +1,263 @@
# Five Development Learnings
> 개발 중 발견된 문제와 해결 방법 기록
>
> Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
> All rights reserved.
---
## 1. WSL/터미널 키보드 입력 (Inkey/ReadKey)
### 문제
PRG에서 `? "text"` 출력 후 `Inkey(0)` 호출 시 키 입력을 기다리지 않고 즉시 리턴됨.
### 원인
- `fmt.Println``\n`이 cooked mode 터미널에서 입력 버퍼에 echo됨
- `os.Stdin.Read()`가 Go runtime 내부 버퍼를 사용하여 stale 데이터를 읽음
- `/dev/tty`와 stdin이 같은 터미널 장치를 공유하므로 버퍼도 공유
### 해결
```
1. /dev/tty를 매 ReadKey 호출 시 새로 open (stale 버퍼 없음)
2. stdin에 raw mode 설정 (ICANON, ECHO, ISIG off, OPOST off)
3. TCFLSH (ioctl 0x540B)로 입력 버퍼 flush
4. QOut(?)에서 \r\n 사용 (OPOST off이므로 \n만으로는 CR 안 됨)
5. syscall.Read(fd, buf) 사용 (Go의 os.Stdin.Read 우회)
6. init() 함수에서 raw mode 설정 (main 전에 실행)
```
### ESC 키 즉시 반응
```
문제: ESC(27) 입력 후 방향키 ESC sequence([A,[B 등)인지 확인하려고
다음 바이트를 blocking read → 순수 ESC면 영원히 블로킹
해결: ESC 후 VMIN=0, VTIME=1 (100ms timeout)로 변경하여
다음 바이트가 100ms 내에 안 오면 bare ESC로 판정
방향키는 ESC+[+방향 3바이트가 100ms 내에 도착하므로 정상 인식
```
### rawtty.go 핵심 패턴
```go
// /dev/tty를 매번 새로 열어서 stale 버퍼 문제 회피
fd, _ := syscall.Open("/dev/tty", syscall.O_RDONLY, 0)
defer syscall.Close(fd)
// raw mode 설정
var t syscall.Termios
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, ...) // TCGETS
t.Lflag &^= syscall.ICANON | syscall.ECHO | syscall.ISIG
t.Cc[syscall.VMIN] = 1 // 1바이트 읽으면 리턴
t.Cc[syscall.VTIME] = 0 // 타임아웃 없음
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, ...) // TCSETS
// ESC sequence 판정: 타임아웃으로
t.Cc[syscall.VMIN] = 0
t.Cc[syscall.VTIME] = 1 // 100ms
// Read → 0 bytes면 bare ESC, '[' 오면 방향키
```
---
## 2. ::method() vs ::field — HasParens 구분
### 문제
```harbour
METHOD forceStable() CLASS TBrowse
DO WHILE !::lStable
::stabilize() // ← 이것이 PushSelfField로 생성됨 (메서드 호출이 아님!)
ENDDO
```
gengo가 `::stabilize()``t.PushSelfField("STABILIZE")`로 생성 → 필드값 push만 하고 메서드 호출 안 됨.
### 원인
파서에서 `::name``SendExpr{Object:SelfExpr, Method:"name"}`로 만들 때 `()`가 있는지 구분하지 않음. gengo에서 args=0이면 무조건 PushSelfField로 처리.
### 해결
```
1. AST SendExpr에 HasParens bool 필드 추가
2. 파서: ::name 뒤에 ()가 있으면 HasParens=true
3. gengo: HasParens=false → PushSelfField (필드 읽기)
HasParens=true → PushSelf + Send (메서드 호출)
```
```
::lStable → PushSelfField("LSTABLE") // 필드 읽기
::stabilize() → PushSelf + Send("stabilize",0) // 메서드 호출
```
---
## 3. RETURN in IF block — Go return 누락
### 문제
```harbour
FUNCTION Test(a, b)
IF a = b
RETURN "PASS" // ← Go에서 함수가 종료되지 않음!
ENDIF
RETURN "FAIL" // ← 항상 이것이 실행됨
```
### 원인
gengo가 `t.RetValue()`만 생성하고 Go의 `return`을 안 넣음. Go 함수가 계속 실행되어 마지막 RETURN이 덮어씀.
### 해결
```go
// gengo: ReturnStmt 생성 시
t.RetValue()
return // ← Go return 추가!
```
---
## 4. DATA aColumns INIT {} — 빈 배열 초기화
### 문제
`DATA aColumns INIT {}` → gengo가 `hbrt.MakeNil()`로 생성 → AAdd 시 "not an array" panic.
### 해결
gengo의 `exprToGoLiteral`에 ArrayLitExpr 처리 추가:
```go
case *ast.ArrayLitExpr:
if len(e.Items) == 0 {
return "hbrt.MakeArray(0)" // 빈 배열
}
```
---
## 5. LOCAL 변수 init에서 파라미터 참조 불가
### 문제
```harbour
FUNCTION TBrowseDB(nTop, nLeft, nBottom, nRight)
LOCAL o := TBrowse():Init(nTop, nLeft, nBottom, nRight)
// ^^^^ UNRESOLVED
```
### 원인
gengo가 LOCAL init 식을 emit한 후에 localMap을 빌드 → init 식에서 파라미터 참조 불가.
### 해결
`buildLocalMap()`을 LOCAL init emit **전에** 호출하도록 순서 변경.
---
## 6. METHOD 이름으로 키워드 사용
### 문제
```harbour
METHOD end() CLASS TBrowse // "end"는 token.END 키워드
METHOD home() CLASS TBrowse // "home"은 키워드 아니지만 유사
METHOD left() CLASS TBrowse
```
### 해결
파서의 `expectMethodName()`이 IDENT뿐 아니라 키워드 토큰도 메서드 이름으로 허용:
```go
func (p *Parser) expectMethodName() token.Token {
if p.current.Kind == token.IDENT || p.current.Literal != "" {
return p.advance() // 키워드도 허용
}
return p.expect(token.IDENT)
}
```
---
## 7. Harbour TBrowse 이동 패턴
### Harbour 원본 패턴 (tbrowse.prg)
```
up()/down()/pageUp()/pageDown():
→ nMoveOffset를 누적만 (실제 skip 안 함)
stabilize():
→ setPosition()에서 nMoveOffset만큼 실제 skip
→ nBufferPos, nRowPos 계산
→ 화면 redraw
→ nMoveOffset := 0
forceStable():
→ DO WHILE !::stabilize() / ENDDO
```
**핵심**: 네비게이션 메서드는 상태만 변경, 실제 동작은 stabilize에서.
### 화면 구조
```
nRowPos: 화면에서 커서가 있는 행 (1-based)
nBufferPos: 데이터 버퍼 내 현재 위치
nLastRow: 실제 데이터가 있는 마지막 행
nRowCount: 화면에 표시 가능한 최대 행수
```
---
## 8. ? 출력과 raw mode 충돌
### 문제
raw mode(OPOST off)에서 `fmt.Println``\n`이 줄바꿈만 하고 커서가 줄 시작으로 안 돌아감 → 화면 깨짐.
### 해결
QOut(?)에서 `\r\n` 사용:
```go
fmt.Print("\r\n" + strings.Join(parts, " "))
```
또는 OPOST를 켜두면 `\n``\r\n` 자동 변환되지만, 이 경우 `\r`이 입력 버퍼에 echo되어 Inkey에 영향.
**최종 선택**: OPOST off + `\r\n` 직접 출력.
---
## 9. Multi-PRG 파일 링크
### 문제
`five build main.prg lib.prg` → 두 파일 모두 `func main()` + `var symbols` 생성 → 컴파일 에러.
### 해결
```
첫 번째 파일: Generate() → main() 포함
나머지 파일: GenerateLibrary() → init()으로 심볼 자동 등록
init() {
hbrt.RegisterLibModule(symbols_libname)
}
VM.Run()에서 모든 libModules를 RegisterModule로 등록.
```
---
## 변경 이력
| 날짜 | 내용 |
|------|------|
| 2026-03-28 | 초기 작성. 터미널/키보드, ::method, RETURN, DATA init, TBrowse 패턴 |