Senior-engineer / QA audit landed 13 silent-miscompile and data-
integrity fixes spanning the whole compiler+runtime+storage stack.
Each fix is paired with either an integration test in the suite or
a focused regression check; all 6 release gates stay green:
go test ./..., FiveSql2 43/43, Harbour compat 56/56, std.ch 17/17,
FRB 7/7, examples 65/71.
Compiler
--------
* genpc IF/ELSEIF jumpEnd2 patching (compiler/genpc/genpc.go).
Per-ELSEIF branch terminators were stashed into `_ = jumpEnd2`
and never patched — the relative offset stayed 0 and the runtime
walked the next ELSEIF's PcOpJumpFalse opcode as if it were
jump-offset data. Bytecode-level corruption in pcode mode. Now
collected into a slice and patched at end-of-IF. Verified via
Grade(95..50) cases 11a-e added to tests/frb/test_frb_pcode_sweep.
* countLocalsInStmts / scanBodyLocals missing bodies
(compiler/gengo/gen_util.go, compiler/gengo/gengo.go). Frame-size
counter skipped WATCH/TIMEOUT/PARALLEL FOR bodies, so a LOCAL
declared inside one of those constructs got a slot index past
the runtime's allocated count — silent NIL reads or out-of-range
stomps.
* emitMethodDeclStandalone nested LOCAL (compiler/gengo/gen_class.go).
Same bug class but on the *method* side. Pre-fix repro:
METHOD Stomp(n) CLASS T
LOCAL a := 1, b := 2
IF n > 0
LOCAL c := 30, d := 40, e := 50, f := 60
Inner( n )
IF c != 30 .OR. d != 40 .OR. e != 50 .OR. f != 60 ...
printed `c, d, e, f = 5, NIL, NIL, NIL` because Inner's frame
collided with Stomp's underallocated slot range. Now counts
body-nested LOCALs into the frame and pre-allocates indices via
scanBodyLocals.
* genpc unsupported-AST diagnostic surface (compiler/genpc/genpc.go,
hbrt/pcode.go, cmd/five/main.go, hbrtl/frb.go). The `default`
cases in emitStmt / emitExpr silently emitted PushNil / no-op
for nodes the pcode generator doesn't implement (ClassDecl,
MethodDecl, xBase commands, concurrency primitives, …). Added
`PcodeModule.Warnings []string` populated by noteUnsupported,
surfaced on stderr from the build pipeline. Users now see
"pcode: AST node not supported in --pcode/FRB-pcode mode: stmt
*ast.GoBlockStmt" instead of getting a silently broken module.
Runtime
-------
* class.go Send/tryBinaryOp t.self defer-restore (hbrt/class.go).
Restoration was a plain `t.self = oldSelf` after `fn(t)`. Any
panic in the method body skipped the line, so the next BEGIN
SEQUENCE / RECOVER handler ran with the THROWING object's Self
— `::field` resolved against the wrong receiver. Wrapped both
restore sites in `defer func() { t.self = oldSelf }()`.
Verified: pre-fix RECOVER saw "THROWER", post-fix "OUTER".
* hbfunc.go HB_FUNC parameter Frame() (hbrt/hbfunc.go). The
RegisterDynamicFunc wrapper called `fn(ctx)` without ever
calling Frame, so `ctx.ParC(1)` / `ctx.Local(n)` read through
`t.curFrame.localBase + n - 1` against the *caller's* frame.
Every #pragma BEGINDUMP HB_FUNC taking parameters silently
returned "" / 0 / "" for them — masked by ParNIDef-style
defaults. Wrapper now does `t.Frame(t.pendingParams, 0); defer
t.EndProc()` before dispatch.
* pcode codeblock closure capture (hbrt/pcinterp.go, hbrt/pcode.go,
hbrt/thread.go, compiler/genpc/genpc.go). PcOpPushBlock recorded
`nDetached` but never copied enclosing locals; free vars in the
block body fell through to memvar lookup → NIL. Wired full
capture pipeline:
- New opcodes PcOpPushDetached (0x59) / PcOpPopDetached (0x5A).
- PushBlock now reads per-slot source-local indices and
snapshots into bb.Detached at construction time.
- New detachedMap in genpc auto-promotes any free var that
resolves to an enclosing-frame local into a capture slot.
- emitAssignAsExpr leaves the assigned value on the eval stack
so SeqExpr items like `{|v| acc += v, acc }` work.
- Thread tracks curBlock with paired Set/restore in the block's
Fn wrapper for nested-block evaluation.
Mutating capture (acc += v across successive Evals) now works.
* vm.NewThread statics + waFactory propagation (hbrt/vm.go).
GoLaunch / GoLaunchBlock call NewThread directly. Previously
the statics map and WA factory were applied only in Run(), so
goroutine-spawned PRG code panicked on STATIC access ("static
index out of range") and crashed dereferencing nil WA on any
DB call. Both now happen inside NewThread under the same lock
as TID assignment.
Data layer
----------
* dbf concurrent Append lock (hbrdd/dbf/dbf.go,
hbrdd/dbf/locks_posix.go, hbrdd/dbf/locks_windows.go). Append
bumped a local recCount with no file-system serialization. Two
shared-mode processes both wrote at the same RecordOffset; one
record silently overwrote the other. Added an append-intent
byte-range lock at offset 0x7FFFFFFE + bounded retry, on-disk
header refresh inside the locked region, and immediate header
write so peers refresh past our slot.
* indexer negative numeric key encoding (hbrdd/dbf/indexer.go +
new hbrdd/dbf/encode_numeric_test.go). `%20.10f` formats `-100`
as `" -100.0000000000"` and `99` as `" 99.0000000000"`.
ASCII ' ' (0x20) < '-' (0x2D), so `99` lex-compared LESS than
`-100` — every NTX/CDX index over a column that ever held a
negative number returned wrong rows for SEEK / range scans.
Replaced with a 1-byte sign prefix + 21-byte zero-padded
magnitude (negatives use digit-complement) so byte order
matches numeric order across signs and magnitudes. Format
change: existing indexes built with the old encoding must be
REINDEXed. Three unit tests pin the order.
* dbf Append index maintenance hooks (hbrdd/dbf/dbf.go,
hbrdd/dbf/indexer.go). Append never inserted into open NTX/CDX
indexes — the audit's canonical scenario `SET INDEX TO …;
APPEND BLANK; REPLACE …; dbSeek …` silently missed the new
record. Added optional IndexWriter interface, queue the new
recNo in pendingIdxInserts, drain after flushRecord by calling
InsertKey on every open writer-supporting engine. NTX
participates (its existing rebuild-on-insert is correct);
CDX online maintenance is deferred to a follow-up — those
indexes still need REINDEX. Verified: post-fix SEEK("Charlie")
after APPEND BLANK + REPLACE finds the new record.
* dbf PACK crash-safety (hbrdd/dbf/dbf.go). The old in-place
rewrite read record N, overwrote slot M<N, then truncated.
Power loss after partial loop left a file with overwritten
prefix and no original copies of the records already advanced
past — silent data loss. Rewrote to:
1) drop mmap, build `<file>.pack.tmp` with all surviving
records,
2) Sync(),
3) close original handle + os.Rename(tmp, orig) (atomic on
same FS),
4) reopen + re-mmap.
TestComp_Pack passes; readers always see either the pre-PACK
or post-PACK contents, never a half-state.
* mem RDD torn reads (hbrdd/mem/memrdd.go). The comment claimed
in-place PutValue was safe because hbrt.Value "fits in a
single machine word + pointer". hbrt.Value is 24 bytes (3
words) — a concurrent reader could observe new type tag with
stale scalar/ptr and type-confuse on the next AsXxx() call.
Switched mu to sync.RWMutex; GetValue takes RLock,
Append/PutValue/Delete/Recall take Lock. `go test -race
./hbrdd/mem/` clean.
Files touched
-------------
compiler/gengo/gen_class.go, gen_util.go, gengo.go
compiler/genpc/genpc.go
hbrt/class.go, hbfunc.go, pcinterp.go, pcode.go, thread.go, vm.go
hbrdd/dbf/dbf.go, indexer.go, locks_posix.go, locks_windows.go
hbrdd/dbf/encode_numeric_test.go (new)
hbrdd/mem/memrdd.go
cmd/five/main.go
hbrtl/frb.go
tests/frb/test_frb_pcode_sweep.prg
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
456 lines
12 KiB
Go
456 lines
12 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// CLASS system for the Five runtime.
|
|
// Harbour: classes.c — object = array with uiClass, methods in class registry.
|
|
//
|
|
// Key concepts:
|
|
// - Class = definition (name, DATA fields, METHODs, parent)
|
|
// - Object = HbArray with Class > 0 (fields stored in Items[])
|
|
// - Send = method dispatch: obj:method(args) → lookup class → call func
|
|
// - :: = Self access (current object in method context)
|
|
// - INHERIT FROM = parent class embedding
|
|
// - Operator overloading: HB_OO_OP_PLUS etc.
|
|
//
|
|
// Reference:
|
|
// /mnt/d/harbour-core/include/hbapicls.h
|
|
// /mnt/d/harbour-core/src/vm/classes.c
|
|
package hbrt
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// ClassDef defines a class (DATA fields + METHODs).
|
|
// Harbour: internal class structure in classes.c
|
|
type ClassDef struct {
|
|
ID uint16
|
|
Name string
|
|
Parent *ClassDef // INHERIT FROM
|
|
Fields []ClassField // DATA declarations (ordered)
|
|
Methods map[string]MethodFunc // method name → function
|
|
Operators [MaxOperator + 1]MethodFunc // operator overloading
|
|
fieldMap map[string]int // field name → index
|
|
}
|
|
|
|
// ClassField describes a DATA member.
|
|
type ClassField struct {
|
|
Name string
|
|
Init Value // default INIT value
|
|
AsType string // optional type hint
|
|
}
|
|
|
|
// MethodFunc is the signature for class methods.
|
|
// The method receives the thread; Self is available via thread context.
|
|
type MethodFunc func(t *Thread)
|
|
|
|
// Operator IDs matching Harbour's HB_OO_OP_*
|
|
const (
|
|
OpPlus = 0
|
|
OpMinus = 1
|
|
OpMult = 2
|
|
OpDivide = 3
|
|
OpMod = 4
|
|
OpPower = 5
|
|
OpInc = 6
|
|
OpDec = 7
|
|
OpEqual = 8
|
|
OpExactEqual = 9
|
|
OpNotEqual = 10
|
|
OpLess = 11
|
|
OpLessEqual = 12
|
|
OpGreater = 13
|
|
OpGreaterEqual = 14
|
|
OpAssign = 15
|
|
OpInString = 16
|
|
OpInclude = 17
|
|
OpNot = 18
|
|
OpAnd = 19
|
|
OpOr = 20
|
|
OpArrayIndex = 21
|
|
MaxOperator = 21
|
|
)
|
|
|
|
// --- Class Registry ---
|
|
|
|
var (
|
|
classRegMu sync.Mutex // full mutex (not RW) — prevents slice reallocation race on classList
|
|
classReg = map[string]*ClassDef{}
|
|
classList []*ClassDef // index = classID - 1
|
|
)
|
|
|
|
// RegisterClass registers a class definition.
|
|
// Returns the assigned class ID (1-based).
|
|
func RegisterClass(cls *ClassDef) uint16 {
|
|
classRegMu.Lock()
|
|
defer classRegMu.Unlock()
|
|
|
|
cls.ID = uint16(len(classList) + 1)
|
|
classList = append(classList, cls)
|
|
classReg[strings.ToUpper(cls.Name)] = cls
|
|
|
|
// Build field index map
|
|
cls.fieldMap = make(map[string]int, len(cls.Fields))
|
|
for i, f := range cls.Fields {
|
|
cls.fieldMap[strings.ToUpper(f.Name)] = i
|
|
}
|
|
|
|
return cls.ID
|
|
}
|
|
|
|
// ListClassNames returns all registered class names, sorted by registration
|
|
// order (1-based class IDs). Used by the diagnostic ErrorLog writer.
|
|
func ListClassNames() []string {
|
|
classRegMu.Lock()
|
|
defer classRegMu.Unlock()
|
|
out := make([]string, 0, len(classList))
|
|
for _, c := range classList {
|
|
if c != nil {
|
|
out = append(out, c.Name)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// FindClass looks up a class by name.
|
|
func FindClass(name string) *ClassDef {
|
|
classRegMu.Lock()
|
|
defer classRegMu.Unlock()
|
|
return classReg[strings.ToUpper(name)]
|
|
}
|
|
|
|
// GetClass looks up a class by ID.
|
|
func GetClass(id uint16) *ClassDef {
|
|
classRegMu.Lock()
|
|
defer classRegMu.Unlock()
|
|
if int(id) > 0 && int(id) <= len(classList) {
|
|
return classList[id-1]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Class builder (fluent API for generated code) ---
|
|
|
|
// NewClassDef creates a new class definition builder.
|
|
func NewClassDef(name string) *ClassDef {
|
|
return &ClassDef{
|
|
Name: name,
|
|
Methods: make(map[string]MethodFunc),
|
|
}
|
|
}
|
|
|
|
// InheritFrom sets the parent class.
|
|
func (c *ClassDef) InheritFrom(parentName string) *ClassDef {
|
|
parent := FindClass(parentName)
|
|
if parent != nil {
|
|
c.Parent = parent
|
|
// Copy parent fields first
|
|
c.Fields = append(append([]ClassField{}, parent.Fields...), c.Fields...)
|
|
// Copy parent methods (child can override)
|
|
for name, fn := range parent.Methods {
|
|
if _, exists := c.Methods[name]; !exists {
|
|
c.Methods[name] = fn
|
|
}
|
|
}
|
|
// Copy parent operators
|
|
for i, fn := range parent.Operators {
|
|
if c.Operators[i] == nil {
|
|
c.Operators[i] = fn
|
|
}
|
|
}
|
|
}
|
|
return c
|
|
}
|
|
|
|
// AddData adds a DATA field to the class.
|
|
func (c *ClassDef) AddData(name string, init Value) *ClassDef {
|
|
c.Fields = append(c.Fields, ClassField{Name: name, Init: init})
|
|
return c
|
|
}
|
|
|
|
// AddMethod adds a METHOD to the class.
|
|
func (c *ClassDef) AddMethod(name string, fn MethodFunc) *ClassDef {
|
|
c.Methods[strings.ToUpper(name)] = fn
|
|
return c
|
|
}
|
|
|
|
// AddOperator sets an operator overload.
|
|
func (c *ClassDef) AddOperator(op int, fn MethodFunc) *ClassDef {
|
|
if op >= 0 && op <= MaxOperator {
|
|
c.Operators[op] = fn
|
|
}
|
|
return c
|
|
}
|
|
|
|
// Register registers this class and returns the class ID.
|
|
func (c *ClassDef) Register() uint16 {
|
|
return RegisterClass(c)
|
|
}
|
|
|
|
// FieldIndex returns the 0-based field index by name, or -1.
|
|
func (c *ClassDef) FieldIndex(name string) int {
|
|
if c.fieldMap == nil {
|
|
return -1
|
|
}
|
|
if idx, ok := c.fieldMap[strings.ToUpper(name)]; ok {
|
|
return idx
|
|
}
|
|
// Check parent
|
|
if c.Parent != nil {
|
|
return c.Parent.FieldIndex(name)
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// --- Object creation ---
|
|
|
|
// NewObject creates a new object instance of a class.
|
|
// Harbour: object = array with uiClass set.
|
|
func NewObject(classID uint16) Value {
|
|
cls := GetClass(classID)
|
|
if cls == nil {
|
|
return MakeNil()
|
|
}
|
|
|
|
obj := MakeObject(classID, len(cls.Fields))
|
|
arr := obj.AsArray()
|
|
|
|
// Initialize fields with default values
|
|
for i, f := range cls.Fields {
|
|
arr.Items[i] = f.Init
|
|
}
|
|
|
|
return obj
|
|
}
|
|
|
|
// --- Method dispatch ---
|
|
|
|
// Send dispatches a method call on an object.
|
|
// Harbour: hb_objGetMethod + call
|
|
// Stack: [object] [arg1] ... [argN] → call method → [result]
|
|
func (t *Thread) Send(methodName string, nArgs int) {
|
|
// Collect args
|
|
args := make([]Value, nArgs)
|
|
for i := nArgs - 1; i >= 0; i-- {
|
|
args[i] = t.pop()
|
|
}
|
|
objVal := t.pop() // object
|
|
|
|
if !objVal.IsObject() {
|
|
// Not a class object — try built-in Value methods (String:Upper, Array:Sort, etc.)
|
|
if result, ok := SendBuiltin(t, objVal, methodName, args); ok {
|
|
t.push(result)
|
|
return
|
|
}
|
|
panic(t.runtimeError(fmt.Sprintf("not an object for method %s", methodName)))
|
|
}
|
|
|
|
arr := objVal.AsArray()
|
|
cls := GetClass(arr.Class)
|
|
if cls == nil {
|
|
panic(t.runtimeError(fmt.Sprintf("unknown class ID %d", arr.Class)))
|
|
}
|
|
|
|
upperMethod := strings.ToUpper(methodName)
|
|
|
|
// Check for data field access (getter)
|
|
if nArgs == 0 {
|
|
if idx := cls.FieldIndex(methodName); idx >= 0 {
|
|
t.push(arr.Items[idx])
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check for data field setter: _FIELDNAME convention
|
|
if nArgs == 1 && strings.HasPrefix(upperMethod, "_") {
|
|
fieldName := upperMethod[1:]
|
|
if idx := cls.FieldIndex(fieldName); idx >= 0 {
|
|
arr.Items[idx] = args[0]
|
|
t.push(args[0])
|
|
return
|
|
}
|
|
}
|
|
|
|
// Look up method
|
|
fn, ok := cls.Methods[upperMethod]
|
|
if !ok {
|
|
panic(t.runtimeError(fmt.Sprintf("unknown method %s in class %s", methodName, cls.Name)))
|
|
}
|
|
|
|
// Set up Self context. Restore via defer so a panic in the
|
|
// method body (HbError, BreakValue from `Break(…)`, runtime
|
|
// panic) unwinds with `t.self` pointing at the caller's
|
|
// receiver — not at this method's. Without defer, a RECOVER
|
|
// USING handler that runs after the panic still saw the stale
|
|
// `t.self`, so `::field` / `::method()` resolved against the
|
|
// wrong object — silent data corruption in the recovery path.
|
|
oldSelf := t.self
|
|
t.self = objVal
|
|
defer func() { t.self = oldSelf }()
|
|
|
|
// Push args for Frame
|
|
for _, arg := range args {
|
|
t.push(arg)
|
|
}
|
|
|
|
t.pendingParams = nArgs
|
|
fn(t)
|
|
|
|
// Push return value
|
|
t.push(t.retVal)
|
|
}
|
|
|
|
// tryBinaryOp checks whether the LHS of a pending binary operation is
|
|
// an object whose class overloads the given operator slot. If so, it
|
|
// dispatches the overload (Self=LHS, one positional arg = RHS) and
|
|
// returns true with the result pushed in place of the two operands.
|
|
// Returns false for non-object LHS or classes without an overload,
|
|
// letting the caller fall through to the built-in op.
|
|
func (t *Thread) tryBinaryOp(op int) bool {
|
|
if t.sp < 2 {
|
|
return false
|
|
}
|
|
a := t.stack[t.sp-2]
|
|
if !a.IsObject() {
|
|
return false
|
|
}
|
|
// AsArray can return nil if the ptr field is unset despite an object
|
|
// tag — a corrupted Value that would otherwise crash at `.Class`.
|
|
// Guard defensively; correct construction paths never hit this.
|
|
arr := a.AsArray()
|
|
if arr == nil {
|
|
return false
|
|
}
|
|
cls := GetClass(arr.Class)
|
|
if cls == nil || cls.Operators[op] == nil {
|
|
return false
|
|
}
|
|
fn := cls.Operators[op]
|
|
|
|
// Stack layout: [a] [b] → caller expects [result] after return.
|
|
b := t.pop()
|
|
t.pop() // discard a (Self takes over)
|
|
oldSelf := t.self
|
|
t.self = a
|
|
// defer restore — see comment in Send.
|
|
defer func() { t.self = oldSelf }()
|
|
t.push(b)
|
|
t.pendingParams = 1
|
|
fn(t)
|
|
t.push(t.retVal)
|
|
return true
|
|
}
|
|
|
|
// SendSuper dispatches a method call on Self, but starting the method
|
|
// lookup from the parent of the class that defined the currently-
|
|
// executing method. Implements `::super:Method(args)`.
|
|
//
|
|
// fromClassName is the class whose method body contains the ::super
|
|
// call — gengo emits it at compile time from the `METHOD ... CLASS X`
|
|
// declaration. Using Self's runtime class here would infinite-loop on
|
|
// 3-level hierarchies: Grand:New calls ::super:New → runs Child:New →
|
|
// Child:New calls ::super:New → would look up Grand.Parent = Child
|
|
// again, not Child.Parent = Base. Binding to the defining class is
|
|
// the same technique Harbour uses (method slot carries its origin
|
|
// class in the vtable).
|
|
//
|
|
// Stack: [arg1] ... [argN] → [result].
|
|
func (t *Thread) SendSuper(fromClassName, methodName string, nArgs int) {
|
|
args := make([]Value, nArgs)
|
|
for i := nArgs - 1; i >= 0; i-- {
|
|
args[i] = t.pop()
|
|
}
|
|
|
|
if !t.self.IsObject() {
|
|
panic(t.runtimeError("::super: outside method context"))
|
|
}
|
|
from := FindClass(fromClassName)
|
|
if from == nil {
|
|
panic(t.runtimeError(fmt.Sprintf("::super: unknown defining class %s", fromClassName)))
|
|
}
|
|
if from.Parent == nil {
|
|
panic(t.runtimeError(fmt.Sprintf("class %s has no parent for ::super", from.Name)))
|
|
}
|
|
|
|
parent := from.Parent
|
|
upper := strings.ToUpper(methodName)
|
|
fn, ok := parent.Methods[upper]
|
|
if !ok {
|
|
panic(t.runtimeError(fmt.Sprintf("unknown method %s in parent class %s", methodName, parent.Name)))
|
|
}
|
|
|
|
// Self unchanged — push args and invoke parent's slot.
|
|
for _, a := range args {
|
|
t.push(a)
|
|
}
|
|
t.pendingParams = nArgs
|
|
fn(t)
|
|
t.push(t.retVal)
|
|
}
|
|
|
|
// SendAssign dispatches a setter: obj:prop := value
|
|
// Generated for ::fieldName := value
|
|
func (t *Thread) SendAssign(fieldName string) {
|
|
val := t.pop()
|
|
objVal := t.pop()
|
|
|
|
if !objVal.IsObject() {
|
|
panic(t.runtimeError("not an object for assignment"))
|
|
}
|
|
|
|
arr := objVal.AsArray()
|
|
cls := GetClass(arr.Class)
|
|
if cls == nil {
|
|
return
|
|
}
|
|
|
|
if idx := cls.FieldIndex(fieldName); idx >= 0 {
|
|
arr.Items[idx] = val
|
|
}
|
|
}
|
|
|
|
// Send0 dispatches a no-arg method (getter).
|
|
func (t *Thread) Send0(methodName string) {
|
|
t.Send(methodName, 0)
|
|
}
|
|
|
|
// PushSelfField pushes a field from the current Self object.
|
|
// Used by :: access in methods.
|
|
func (t *Thread) PushSelfField(fieldName string) {
|
|
if t.self.IsNil() {
|
|
t.push(MakeNil())
|
|
return
|
|
}
|
|
arr := t.self.AsArray()
|
|
cls := GetClass(arr.Class)
|
|
if cls != nil {
|
|
if idx := cls.FieldIndex(fieldName); idx >= 0 {
|
|
t.push(arr.Items[idx])
|
|
return
|
|
}
|
|
}
|
|
t.push(MakeNil())
|
|
}
|
|
|
|
// SetSelfField sets a field on the current Self object.
|
|
func (t *Thread) SetSelfField(fieldName string) {
|
|
val := t.pop()
|
|
if t.self.IsNil() {
|
|
return
|
|
}
|
|
arr := t.self.AsArray()
|
|
cls := GetClass(arr.Class)
|
|
if cls != nil {
|
|
if idx := cls.FieldIndex(fieldName); idx >= 0 {
|
|
arr.Items[idx] = val
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetSelf returns the current Self value.
|
|
func (t *Thread) GetSelf() Value {
|
|
return t.self
|
|
}
|