fix: Critical code review fixes — race conditions, panic recovery, LTRIM/RTRIM

CRITICAL fixes:
- fileio.go: Add sync.Mutex to file handle table (race condition #2)
  allocHandle/getHandle/removeHandle thread-safe helpers
- goroutine.go: Add defer/recover to GoLaunch/GoLaunchBlock
  Goroutine panic no longer crashes entire process (#5)

HIGH fixes:
- strings.go: Implement proper LTrim (TrimLeft) and RTrim (TrimRight)
  Previously both aliased to AllTrim — silent semantic bug (#18)
- register.go: TRIM = RTrim (Harbour compatible)

From 53-issue senior code review.
Remaining: 47 issues (HIGH: 10, MEDIUM: 18, LOW: 16, CRITICAL: 3)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 10:17:30 +09:00
parent a2430fa44b
commit 7c61db70c3
6 changed files with 149 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
{
"version": "2.0",
"lastUpdated": "2026-03-31T02:56:59.267Z",
"lastUpdated": "2026-04-01T01:16:33.700Z",
"activeFeatures": [
"hbrt",
"hbrtl",
@@ -33,9 +33,9 @@
"documents": {},
"timestamps": {
"started": "2026-03-27T09:33:04.512Z",
"lastUpdated": "2026-03-31T01:15:54.989Z"
"lastUpdated": "2026-04-01T01:14:37.127Z"
},
"lastFile": "/mnt/d/charles/five/hbrt/dynamic_syntax_test.go"
"lastFile": "/mnt/d/charles/five/hbrt/goroutine.go"
},
"hbrtl": {
"phase": "do",
@@ -46,9 +46,9 @@
"documents": {},
"timestamps": {
"started": "2026-03-27T11:15:10.675Z",
"lastUpdated": "2026-03-31T01:06:49.182Z"
"lastUpdated": "2026-04-01T01:16:33.700Z"
},
"lastFile": "/mnt/d/charles/five/hbrtl/rawtty.go"
"lastFile": "/mnt/d/charles/five/hbrtl/register.go"
},
"tests": {
"phase": "do",
@@ -280,7 +280,7 @@
"session": {
"startedAt": "2026-03-27T06:06:49.620Z",
"onboardingCompleted": false,
"lastActivity": "2026-03-31T02:56:59.267Z"
"lastActivity": "2026-04-01T01:16:33.700Z"
},
"history": [
{
@@ -5418,6 +5418,72 @@
"feature": "mem",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T00:50:44.331Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T00:51:04.728Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T00:51:29.797Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T00:53:27.988Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T00:53:39.882Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T00:53:51.335Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T01:12:51.547Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T01:14:22.341Z",
"feature": "hbrt",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T01:14:37.127Z",
"feature": "hbrt",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T01:15:49.420Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-04-01T01:16:33.700Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
}
]
}

View File

@@ -115,7 +115,7 @@ if condition {
```prg
LOCAL cCustomerName, nTotalBalance, dLastPurchase, lIsActive
cCustomerName := "Charles Kwon" // c = Character
cCustomerName := "Charles KWON" // c = Character
nTotalBalance := 15000.50 // n = Numeric
dLastPurchase := Date() // d = Date
lIsActive := .T. // l = Logical

View File

@@ -8,6 +8,8 @@
package hbrt
import (
"fmt"
"os"
"sync"
)
@@ -114,6 +116,11 @@ func (v Value) AsMutex() *HbMutex {
// GoLaunch spawns a new goroutine that runs a function on a new Thread.
func (vm *VM) GoLaunch(fn func(*Thread), args []Value) {
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "Five goroutine panic: %v\n", r)
}
}()
t := vm.NewThread()
for _, a := range args {
t.push(a)
@@ -126,6 +133,11 @@ func (vm *VM) GoLaunch(fn func(*Thread), args []Value) {
// GoLaunchBlock spawns a goroutine that evaluates a code block.
func (vm *VM) GoLaunchBlock(blk *HbBlock, args []Value) {
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "Five goroutine panic: %v\n", r)
}
}()
t := vm.NewThread()
for _, a := range args {
t.push(a)

View File

@@ -13,14 +13,40 @@ import (
"os"
"path/filepath"
"strings"
"sync"
)
// File handle table — maps Harbour handle (int) to Go *os.File
// Protected by mutex for goroutine safety.
var (
fileHandles = map[int]*os.File{}
nextHandle = 10 // start from 10, avoid 0-2 (stdin/out/err)
fileHandles = map[int]*os.File{}
nextHandle = 10 // start from 10, avoid 0-2 (stdin/out/err)
fileHandlesMu sync.Mutex
)
// File handle helpers (thread-safe)
func allocHandle(f *os.File) int {
fileHandlesMu.Lock()
defer fileHandlesMu.Unlock()
h := nextHandle
nextHandle++
fileHandles[h] = f
return h
}
func getHandle(h int) (*os.File, bool) {
fileHandlesMu.Lock()
defer fileHandlesMu.Unlock()
f, ok := fileHandles[h]
return f, ok
}
func removeHandle(h int) {
fileHandlesMu.Lock()
defer fileHandlesMu.Unlock()
delete(fileHandles, h)
}
// FOPEN(cFileName [, nMode]) → nHandle | -1
// nMode: 0=read, 1=write, 2=readwrite
func FOpen(t *hbrt.Thread) {
@@ -53,9 +79,7 @@ func FOpen(t *hbrt.Thread) {
return
}
h := nextHandle
nextHandle++
fileHandles[h] = f
h := allocHandle(f)
SetFError(0)
t.RetInt(int64(h))
}
@@ -74,9 +98,7 @@ func FCreate(t *hbrt.Thread) {
return
}
h := nextHandle
nextHandle++
fileHandles[h] = f
h := allocHandle(f)
SetFError(0)
t.RetInt(int64(h))
}
@@ -87,14 +109,14 @@ func FClose(t *hbrt.Thread) {
defer t.EndProc()
h := t.Local(1).AsInt()
f, ok := fileHandles[h]
f, ok := getHandle(h)
if !ok {
SetFError(6) // invalid handle
t.RetBool(false)
return
}
err := f.Close()
delete(fileHandles, h)
removeHandle(h)
SetFError(0)
t.RetBool(err == nil)
}
@@ -107,7 +129,7 @@ func FRead(t *hbrt.Thread) {
h := t.Local(1).AsInt()
nBytes := t.Local(3).AsInt()
f, ok := fileHandles[h]
f, ok := getHandle(h)
if !ok {
SetFError(6)
t.RetInt(0)
@@ -139,7 +161,7 @@ func FWrite(t *hbrt.Thread) {
h := t.Local(1).AsInt()
data := t.Local(2).AsString()
f, ok := fileHandles[h]
f, ok := getHandle(h)
if !ok {
SetFError(6)
t.RetInt(0)
@@ -177,7 +199,7 @@ func FSeek(t *hbrt.Thread) {
origin = t.Local(3).AsInt()
}
f, ok := fileHandles[h]
f, ok := getHandle(h)
if !ok {
SetFError(6)
t.RetInt(0)

View File

@@ -33,9 +33,9 @@ func RegisterRTL(vm *hbrt.VM) {
hbrt.Sym("UPPER", hbrt.FsPublic, Upper),
hbrt.Sym("LOWER", hbrt.FsPublic, Lower),
hbrt.Sym("ALLTRIM", hbrt.FsPublic, AllTrim),
hbrt.Sym("LTRIM", hbrt.FsPublic, AllTrim), // simplified alias
hbrt.Sym("RTRIM", hbrt.FsPublic, AllTrim), // simplified alias
hbrt.Sym("TRIM", hbrt.FsPublic, AllTrim), // simplified alias
hbrt.Sym("LTRIM", hbrt.FsPublic, LTrim),
hbrt.Sym("RTRIM", hbrt.FsPublic, RTrim),
hbrt.Sym("TRIM", hbrt.FsPublic, RTrim), // TRIM = RTRIM in Harbour
hbrt.Sym("SPACE", hbrt.FsPublic, Space),
hbrt.Sym("PADR", hbrt.FsPublic, PadR),
hbrt.Sym("PADL", hbrt.FsPublic, PadL),

View File

@@ -202,6 +202,32 @@ func AllTrim(t *hbrt.Thread) {
t.RetValue()
}
// LTrim trims leading spaces only. Harbour: LTRIM(cString) → cString
func LTrim(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
v := t.Local(1)
if v.IsString() {
t.PushString(strings.TrimLeft(v.AsString(), " "))
} else {
t.PushString("")
}
t.RetValue()
}
// RTrim trims trailing spaces only. Harbour: RTRIM(cString) / TRIM(cString)
func RTrim(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
v := t.Local(1)
if v.IsString() {
t.PushString(strings.TrimRight(v.AsString(), " "))
} else {
t.PushString("")
}
t.RetValue()
}
// Space returns a string of n spaces.
// Harbour: Space(nCount) → cString
func Space(t *hbrt.Thread) {