Files
five/hbrtl/error.go
CharlesKWON 3adc9d7d59 fix: PCount, Break/RECOVER, SET INDEX TO — 3 Harbour compat fixes
Release-blocking compatibility issues discovered during the 258-test
pre-release validation suite (100 syntax + 44 RDD + 114 RTL).

1. PCount() always returned 0 in PRG code

   Root cause: ParamCount() returned t.pendingParams, which is
   overwritten by every nested Function() call. By the time the
   PCount() RTL's Frame() executes, pendingParams is already 0.

   Fix: Frame() now stores pendingParams in frame.paramCount.
   PCount() RTL uses CallerParamCount() which reads callSP-2
   (the PRG caller's frame), while RTL functions still use
   ParamCount() (reads pendingParams before their own Frame).

   Verified: PCount(1,2,3)=3, PCount(1)=1, PCount()=0

2. Break("string") panicked instead of being caught by RECOVER USING

   Root cause: Generated SEQUENCE code only caught *HbError panics.
   Break() panics with BreakValue (a different type), which fell
   through to EndProc's "runtime error" message and re-panic.

   Fix (two parts):
   a) gengo emitBeginSequence: recover closure now catches any
      panic (interface{}), then dispatches via type switch:
      - *HbError → extract .Error() string
      - hasValue interface (BreakValue) → extract .GetValue()
      - other → static "error" string
   b) hbrtl/error.go: BreakValue gets GetValue() method for
      duck-type detection without import cycles
   c) hbrt/thread.go EndProc: BreakValue type name check added
      so it re-panics silently (no stderr noise)

3. SET INDEX TO a, b, c only opened the last file

   Root cause: Parser's parseSet() called parseExpr() once for
   INDEX setting, stopping at the first comma. Remaining file
   names were consumed by the "eat rest of line" loop.

   Fix: Parser now collects comma-separated identifiers into a
   single string literal "a,b,c". gengo splits on comma and
   calls OrderListAdd() for each file.

   Verified: SET INDEX TO si_name, si_city → OrdCount=2

All tests pass:
  go test ./...          14 packages OK
  FiveSql2               43/43  100%
  compat_harbour         51/51
  Syntax test           100/100
  RDD test               44/44
  RTL test              114/114
  Windows cross-compile  OK
  Linux cross-compile    OK

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:06:28 +09:00

202 lines
5.3 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Error handling functions: ERRORBLOCK, ERRORNEW, DOSERROR, FERROR
// Harbour error system: Error object + ErrorBlock callback chain.
package hbrtl
import (
"five/hbrt"
)
var (
errorBlock hbrt.Value // current error handler block
lastDosErr int // last OS error code
lastFErr int // last file error code
)
// ERRORBLOCK([bNewBlock]) → bOldBlock
// Gets/sets the error handler code block.
func ErrorBlock(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
old := errorBlock
if old.IsNil() {
old = hbrt.MakeNil()
}
if nParams >= 1 && !t.Local(1).IsNil() {
errorBlock = t.Local(1)
}
t.RetVal(old)
}
// ERRORNEW() → oError
// Creates a new Error object as a hash with standard Harbour error properties.
func ErrorNew(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
keys := []hbrt.Value{
hbrt.MakeString("ARGS"), hbrt.MakeString("CANDEFAULT"),
hbrt.MakeString("CANRETRY"), hbrt.MakeString("CANSUBSTITUTE"),
hbrt.MakeString("CARGO"), hbrt.MakeString("DESCRIPTION"),
hbrt.MakeString("FILENAME"), hbrt.MakeString("GENCODE"),
hbrt.MakeString("OPERATION"), hbrt.MakeString("OSCODE"),
hbrt.MakeString("SEVERITY"), hbrt.MakeString("SUBCODE"),
hbrt.MakeString("SUBSYSTEM"), hbrt.MakeString("TRIES"),
}
vals := []hbrt.Value{
hbrt.MakeNil(), hbrt.MakeBool(false),
hbrt.MakeBool(false), hbrt.MakeBool(false),
hbrt.MakeNil(), hbrt.MakeString(""),
hbrt.MakeString(""), hbrt.MakeInt(0),
hbrt.MakeString(""), hbrt.MakeInt(0),
hbrt.MakeInt(2), hbrt.MakeInt(0),
hbrt.MakeString(""), hbrt.MakeInt(0),
}
h := &hbrt.HbHash{Keys: keys, Values: vals}
order := make([]int, len(keys))
for i := range order {
order[i] = i
}
h.Order = order
t.RetVal(hbrt.MakeHashFrom(h))
}
// DOSERROR([nNewCode]) → nOldCode
func DosError(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
old := lastDosErr
if nParams >= 1 && !t.Local(1).IsNil() {
lastDosErr = t.Local(1).AsInt()
}
t.RetInt(int64(old))
}
// FERROR() → nLastFileError
func FError(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
t.RetInt(int64(lastFErr))
}
// SetFError sets the file error code (called internally by file I/O functions).
func SetFError(code int) {
lastFErr = code
}
// --- Break / Error dispatch ---
// BreakValue is a special panic value for Break().
// Harbour: HB_BREAK — unwinds to nearest BEGIN SEQUENCE.
type BreakValue struct {
Value hbrt.Value
}
// GetValue returns the Break() argument. Used by RECOVER USING via duck typing
// (the generated code checks for a `hasValue` interface to avoid import cycles).
func (bv BreakValue) GetValue() hbrt.Value {
return bv.Value
}
// Break(xValue) → panics with BreakValue, caught by BEGIN SEQUENCE/RECOVER.
func Break(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
// Don't defer EndProc — we're panicking
var val hbrt.Value
if nParams >= 1 {
val = t.Local(1)
} else {
val = hbrt.MakeNil()
}
panic(BreakValue{Value: val})
}
// BreakBlock returns a block that calls Break(). Harbour: {|e| Break(e)}
func BreakBlock(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
t.PushBlock(func(bt *hbrt.Thread) {
bt.Frame(1, 0)
val := bt.Local(1)
panic(BreakValue{Value: val})
}, 0)
t.RetValue()
}
// LaunchError creates and dispatches an error through ErrorBlock.
// Harbour: hb_errLaunch — calls error handler, returns action.
// Actions: 0=default, 1=retry, 0xFFFF=break
func LaunchError(t *hbrt.Thread, oErr hbrt.Value) hbrt.Value {
if errorBlock.IsNil() || !errorBlock.IsBlock() {
// No error handler — Break with error object
panic(BreakValue{Value: oErr})
}
// Call error handler block: errorBlock:Eval(oErr)
blk := errorBlock.AsBlock()
t.PushValue(oErr)
t.PendingParams2(1)
blk.Fn(t)
return t.Pop2()
}
// RuntimeError creates and launches a runtime error.
// Harbour: hb_errRT_BASE
func RuntimeError(t *hbrt.Thread, subSystem string, genCode, subCode int,
description, operation string) {
// Create error object
oErr := createErrorHash(subSystem, genCode, subCode, description, operation)
LaunchError(t, oErr)
}
func createErrorHash(subSystem string, genCode, subCode int,
description, operation string) hbrt.Value {
h := &hbrt.HbHash{}
addKV := func(k string, v hbrt.Value) {
h.Keys = append(h.Keys, hbrt.MakeString(k))
h.Values = append(h.Values, v)
}
addKV("SUBSYSTEM", hbrt.MakeString(subSystem))
addKV("GENCODE", hbrt.MakeInt(genCode))
addKV("SUBCODE", hbrt.MakeInt(subCode))
addKV("DESCRIPTION", hbrt.MakeString(description))
addKV("OPERATION", hbrt.MakeString(operation))
addKV("SEVERITY", hbrt.MakeInt(2)) // ES_ERROR
addKV("CANRETRY", hbrt.MakeBool(false))
addKV("CANDEFAULT", hbrt.MakeBool(false))
addKV("CANSUBSTITUTE", hbrt.MakeBool(false))
addKV("TRIES", hbrt.MakeInt(0))
addKV("CARGO", hbrt.MakeNil())
addKV("ARGS", hbrt.MakeNil())
addKV("FILENAME", hbrt.MakeString(""))
addKV("OSCODE", hbrt.MakeInt(0))
return hbrt.MakeHashFrom(h)
}
// GetErrorBlock returns the current error block (for internal use).
func GetErrorBlock() hbrt.Value {
return errorBlock
}
// IsBreak checks if a recovered panic is a BreakValue.
func IsBreak(r interface{}) (hbrt.Value, bool) {
if bv, ok := r.(BreakValue); ok {
return bv.Value, true
}
return hbrt.MakeNil(), false
}