Files
five/docs/learning.md
Charles KWON OhJun 59568f3301 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>
2026-03-31 09:41:50 +09:00

6.6 KiB

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 핵심 패턴

// /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 구분

문제

METHOD forceStable() CLASS TBrowse
   DO WHILE !::lStable
      ::stabilize()     // ← 이것이 PushSelfField로 생성됨 (메서드 호출이 아님!)
   ENDDO

gengo가 ::stabilize()t.PushSelfField("STABILIZE")로 생성 → 필드값 push만 하고 메서드 호출 안 됨.

원인

파서에서 ::nameSendExpr{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 누락

문제

FUNCTION Test(a, b)
   IF a = b
      RETURN "PASS"    // ← Go에서 함수가 종료되지 않음!
   ENDIF
   RETURN "FAIL"       // ← 항상 이것이 실행됨

원인

gengo가 t.RetValue()만 생성하고 Go의 return을 안 넣음. Go 함수가 계속 실행되어 마지막 RETURN이 덮어씀.

해결

// 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 처리 추가:

case *ast.ArrayLitExpr:
    if len(e.Items) == 0 {
        return "hbrt.MakeArray(0)"  // 빈 배열
    }

5. LOCAL 변수 init에서 파라미터 참조 불가

문제

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 이름으로 키워드 사용

문제

METHOD end() CLASS TBrowse    // "end"는 token.END 키워드
METHOD home() CLASS TBrowse   // "home"은 키워드 아니지만 유사
METHOD left() CLASS TBrowse

해결

파서의 expectMethodName()이 IDENT뿐 아니라 키워드 토큰도 메서드 이름으로 허용:

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 사용:

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 패턴