diff --git a/docs/.pdca-status.json b/docs/.pdca-status.json index 43a9d8c..e5647a4 100644 --- a/docs/.pdca-status.json +++ b/docs/.pdca-status.json @@ -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" } ] } \ No newline at end of file diff --git a/docs/five-readability-en.md b/docs/five-readability-en.md index 79b8ee3..c5cf6e6 100644 --- a/docs/five-readability-en.md +++ b/docs/five-readability-en.md @@ -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 diff --git a/hbrt/goroutine.go b/hbrt/goroutine.go index 5c4c87c..a869d83 100644 --- a/hbrt/goroutine.go +++ b/hbrt/goroutine.go @@ -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) diff --git a/hbrtl/fileio.go b/hbrtl/fileio.go index 984aa62..27aefee 100644 --- a/hbrtl/fileio.go +++ b/hbrtl/fileio.go @@ -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) diff --git a/hbrtl/register.go b/hbrtl/register.go index 03bdb51..5924342 100644 --- a/hbrtl/register.go +++ b/hbrtl/register.go @@ -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), diff --git a/hbrtl/strings.go b/hbrtl/strings.go index 5cd9fca..b30e4a3 100644 --- a/hbrtl/strings.go +++ b/hbrtl/strings.go @@ -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) {