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