diff --git a/docs/.pdca-status.json b/docs/.pdca-status.json index 825e643..0eadc99 100644 --- a/docs/.pdca-status.json +++ b/docs/.pdca-status.json @@ -1,6 +1,6 @@ { "version": "2.0", - "lastUpdated": "2026-04-01T01:30:25.114Z", + "lastUpdated": "2026-04-01T01:50:29.561Z", "activeFeatures": [ "hbrt", "hbrtl", @@ -33,7 +33,7 @@ "documents": {}, "timestamps": { "started": "2026-03-27T09:33:04.512Z", - "lastUpdated": "2026-04-01T01:30:25.114Z" + "lastUpdated": "2026-04-01T01:50:29.560Z" }, "lastFile": "/mnt/d/charles/five/hbrt/macroeval.go" }, @@ -280,7 +280,7 @@ "session": { "startedAt": "2026-03-27T06:06:49.620Z", "onboardingCompleted": false, - "lastActivity": "2026-04-01T01:30:25.114Z" + "lastActivity": "2026-04-01T01:50:29.561Z" }, "history": [ { @@ -5568,6 +5568,66 @@ "feature": "hbrt", "phase": "do", "action": "updated" + }, + { + "timestamp": "2026-04-01T01:36:41.982Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-04-01T01:36:51.948Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-04-01T01:37:05.337Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-04-01T01:39:24.247Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-04-01T01:40:12.339Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-04-01T01:40:25.832Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-04-01T01:42:45.339Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-04-01T01:43:04.161Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-04-01T01:50:06.804Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-04-01T01:50:29.561Z", + "feature": "hbrt", + "phase": "do", + "action": "updated" } ] } \ No newline at end of file diff --git a/hbrt/class.go b/hbrt/class.go index 620dbbb..66897c8 100644 --- a/hbrt/class.go +++ b/hbrt/class.go @@ -76,9 +76,9 @@ const ( // --- Class Registry --- var ( - classRegMu sync.RWMutex + classRegMu sync.Mutex // full mutex (not RW) — prevents slice reallocation race on classList classReg = map[string]*ClassDef{} - classList []*ClassDef // index = classID - 1 + classList []*ClassDef // index = classID - 1 ) // RegisterClass registers a class definition. @@ -102,15 +102,15 @@ func RegisterClass(cls *ClassDef) uint16 { // FindClass looks up a class by name. func FindClass(name string) *ClassDef { - classRegMu.RLock() - defer classRegMu.RUnlock() + classRegMu.Lock() + defer classRegMu.Unlock() return classReg[strings.ToUpper(name)] } // GetClass looks up a class by ID. func GetClass(id uint16) *ClassDef { - classRegMu.RLock() - defer classRegMu.RUnlock() + classRegMu.Lock() + defer classRegMu.Unlock() if int(id) > 0 && int(id) <= len(classList) { return classList[id-1] } diff --git a/hbrt/goroutine.go b/hbrt/goroutine.go index a869d83..994a038 100644 --- a/hbrt/goroutine.go +++ b/hbrt/goroutine.go @@ -17,7 +17,9 @@ import ( // HbChannel wraps Go's chan Value for use in PRG code. type HbChannel struct { - Ch chan Value + Ch chan Value + closeOnce sync.Once + closed bool } // MakeChannel creates a channel Value with optional buffer size. @@ -36,29 +38,49 @@ func (v Value) AsChannel() *HbChannel { return nil } -// Send sends a value into the channel. +// 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 { - return <-ch.Ch + 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 := <-ch.Ch: + case v, ok := <-ch.Ch: + if !ok { + return MakeNil(), false + } return v, true default: return MakeNil(), false } } -// Close closes the channel. +// Close closes the channel. Safe to call multiple times. func (ch *HbChannel) Close() { - close(ch.Ch) + 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 --- diff --git a/hbrt/macro.go b/hbrt/macro.go index 37c9229..a8b9165 100644 --- a/hbrt/macro.go +++ b/hbrt/macro.go @@ -18,7 +18,7 @@ package hbrt import ( - "fmt" + "strconv" "strings" ) @@ -39,9 +39,7 @@ func (t *Thread) MacroCompile(expr string) Value { // Simple case: expression is a variable name // Look up in memvars first, then try as function call if isSimpleIdent(expr) { - // Try PUBLIC/PRIVATE memvar - // TODO: full memvar system - // For now, try calling it as a function + // Try calling as a function (memvar lookup deferred to MacroEval) sym := t.vm.FindSymbol(strings.ToUpper(expr)) if sym != nil && sym.Func != nil { t.PushSymbol(sym) @@ -56,15 +54,14 @@ func (t *Thread) MacroCompile(expr string) Value { // Full runtime expression parser would be needed for complete macro support. // This handles common patterns: &("literal"), &(numericExpr) - // Try numeric - expr = strings.TrimSpace(expr) + // Try numeric (use stdlib strconv) if len(expr) > 0 && (expr[0] >= '0' && expr[0] <= '9' || expr[0] == '-' || expr[0] == '+') { if strings.Contains(expr, ".") { - if f, err := parseFloat(expr); err == nil { + if f, err := strconv.ParseFloat(expr, 64); err == nil { return MakeDoubleAuto(f) } } else { - if n, err := parseInt64(expr); err == nil { + if n, err := strconv.ParseInt(expr, 10, 64); err == nil { return MakeNumInt(n) } } @@ -96,57 +93,7 @@ func (t *Thread) MacroPush() { t.push(result) } -func parseFloat(s string) (float64, error) { - var result float64 - var sign float64 = 1 - i := 0 - if i < len(s) && s[i] == '-' { - sign = -1 - i++ - } else if i < len(s) && s[i] == '+' { - i++ - } - for i < len(s) && s[i] >= '0' && s[i] <= '9' { - result = result*10 + float64(s[i]-'0') - i++ - } - if i < len(s) && s[i] == '.' { - i++ - frac := 0.1 - for i < len(s) && s[i] >= '0' && s[i] <= '9' { - result += float64(s[i]-'0') * frac - frac /= 10 - i++ - } - } - if i != len(s) { - return 0, fmt.Errorf("invalid float") - } - return sign * result, nil -} - -func parseInt64(s string) (int64, error) { - var result int64 - var sign int64 = 1 - i := 0 - if i < len(s) && s[i] == '-' { - sign = -1 - i++ - } else if i < len(s) && s[i] == '+' { - i++ - } - if i >= len(s) { - return 0, fmt.Errorf("empty") - } - for i < len(s) { - if s[i] < '0' || s[i] > '9' { - return 0, fmt.Errorf("invalid int") - } - result = result*10 + int64(s[i]-'0') - i++ - } - return sign * result, nil -} +// parseFloat and parseInt64 removed — using strconv.ParseFloat/ParseInt instead. // isSimpleIdent checks if string is a valid simple identifier. func isSimpleIdent(s string) bool { diff --git a/hbrt/macroeval.go b/hbrt/macroeval.go index a31aa95..3e9723d 100644 --- a/hbrt/macroeval.go +++ b/hbrt/macroeval.go @@ -21,6 +21,7 @@ import ( "five/compiler/ast" "five/compiler/parser" "five/compiler/token" + "strconv" "strings" ) @@ -176,13 +177,13 @@ func (t *Thread) evalLiteral(e *ast.LiteralExpr) Value { case token.FALSE: return MakeBool(false) case token.INT: - n, _ := parseInt64(e.Value) + n, _ := strconv.ParseInt(e.Value, 10, 64) return MakeNumInt(n) case token.LONG: - n, _ := parseInt64(e.Value) + n, _ := strconv.ParseInt(e.Value, 10, 64) return MakeLong(n) case token.DOUBLE: - f, _ := parseFloat(e.Value) + f, _ := strconv.ParseFloat(e.Value, 64) return MakeDoubleAuto(f) case token.STRING: return MakeString(e.Value)