Files modified (5): hbrt/macro.go — Replace hand-rolled parseFloat/parseInt64 with strconv (#50) Remove stale TODO, redundant TrimSpace hbrt/macroeval.go — Use strconv for literal parsing (was using removed functions) hbrt/class.go — CRITICAL #3: Change RWMutex to Mutex on classList Prevents slice reallocation race on concurrent GetClass hbrt/goroutine.go — #36: Channel double-close protection (sync.Once) #37: Send on closed channel recovery (defer/recover) Add IsClosed(), safe Receive (handles closed channel) hbrt/gobridge.go — Already clean (confirmed) hbrt/hbfunc.go — Already clean (confirmed) Issues resolved: #3 (CRITICAL), #36, #37 (MEDIUM), #50 (LOW) Total fixed: 16/53 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
171 lines
3.3 KiB
Go
171 lines
3.3 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Goroutine support for Five runtime.
|
|
// Provides Go's goroutine, channel, and WaitGroup primitives
|
|
// as first-class Harbour values.
|
|
|
|
package hbrt
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
)
|
|
|
|
// --- Channel ---
|
|
|
|
// HbChannel wraps Go's chan Value for use in PRG code.
|
|
type HbChannel struct {
|
|
Ch chan Value
|
|
closeOnce sync.Once
|
|
closed bool
|
|
}
|
|
|
|
// MakeChannel creates a channel Value with optional buffer size.
|
|
func MakeChannel(size int) Value {
|
|
return MakePointer(&HbChannel{Ch: make(chan Value, size)})
|
|
}
|
|
|
|
// AsChannel extracts HbChannel from a Pointer value.
|
|
func (v Value) AsChannel() *HbChannel {
|
|
if !v.IsPointer() {
|
|
return nil
|
|
}
|
|
if ch, ok := v.AsPointer().(*HbChannel); ok {
|
|
return ch
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Send sends a value into the channel. Safe if channel is closed (recovers panic).
|
|
func (ch *HbChannel) Send(val Value) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// send on closed channel — silently ignore
|
|
}
|
|
}()
|
|
ch.Ch <- val
|
|
}
|
|
|
|
// Receive receives a value from the channel.
|
|
func (ch *HbChannel) Receive() Value {
|
|
v, ok := <-ch.Ch
|
|
if !ok {
|
|
return MakeNil() // channel closed
|
|
}
|
|
return v
|
|
}
|
|
|
|
// TryReceive attempts non-blocking receive. Returns (value, true) or (nil, false).
|
|
func (ch *HbChannel) TryReceive() (Value, bool) {
|
|
select {
|
|
case v, ok := <-ch.Ch:
|
|
if !ok {
|
|
return MakeNil(), false
|
|
}
|
|
return v, true
|
|
default:
|
|
return MakeNil(), false
|
|
}
|
|
}
|
|
|
|
// Close closes the channel. Safe to call multiple times.
|
|
func (ch *HbChannel) Close() {
|
|
ch.closeOnce.Do(func() {
|
|
ch.closed = true
|
|
close(ch.Ch)
|
|
})
|
|
}
|
|
|
|
// IsClosed returns true if the channel has been closed.
|
|
func (ch *HbChannel) IsClosed() bool {
|
|
return ch.closed
|
|
}
|
|
|
|
// --- WaitGroup ---
|
|
|
|
// HbWaitGroup wraps sync.WaitGroup.
|
|
type HbWaitGroup struct {
|
|
WG sync.WaitGroup
|
|
}
|
|
|
|
// MakeWaitGroup creates a WaitGroup Value with initial count.
|
|
func MakeWaitGroup(n int) Value {
|
|
wg := &HbWaitGroup{}
|
|
if n > 0 {
|
|
wg.WG.Add(n)
|
|
}
|
|
return MakePointer(wg)
|
|
}
|
|
|
|
// AsWaitGroup extracts HbWaitGroup from a Pointer value.
|
|
func (v Value) AsWaitGroup() *HbWaitGroup {
|
|
if !v.IsPointer() {
|
|
return nil
|
|
}
|
|
if wg, ok := v.AsPointer().(*HbWaitGroup); ok {
|
|
return wg
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Mutex ---
|
|
|
|
// HbMutex wraps sync.Mutex.
|
|
type HbMutex struct {
|
|
Mu sync.Mutex
|
|
}
|
|
|
|
// MakeMutex creates a Mutex Value.
|
|
func MakeMutex() Value {
|
|
return MakePointer(&HbMutex{})
|
|
}
|
|
|
|
// AsMutex extracts HbMutex from a Pointer value.
|
|
func (v Value) AsMutex() *HbMutex {
|
|
if !v.IsPointer() {
|
|
return nil
|
|
}
|
|
if mu, ok := v.AsPointer().(*HbMutex); ok {
|
|
return mu
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- GoRoutine launcher ---
|
|
|
|
// 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)
|
|
}
|
|
t.PendingParams2(len(args))
|
|
fn(t)
|
|
}()
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
t.PendingParams2(len(args))
|
|
blk.Fn(t)
|
|
}()
|
|
}
|