Files
five/compiler/analyzer/analyzer.go
CharlesKWON fc1dca9551 feat(rdd): real POSIX file/record locking + gap analysis doc
Replaces the FLOCK/DBRLOCK/DBRUNLOCK no-op stubs with actual
fcntl(F_SETLK) byte-range advisory locks, matching Harbour's
hb_fsLockLarge implementation.

Before: rtlDbRLock always returned .T. regardless of contention.
        Multi-process writers could silently corrupt records.

After:  Non-blocking POSIX byte-range locks per file descriptor.
        Cross-process exclusion verified by a subprocess-spawning
        Go test that witnesses BUSY vs OK transitions.

New files:
  hbrdd/dbf/locks_posix.go    fcntl F_WRLCK/F_UNLCK wrappers
  hbrdd/dbf/locks_windows.go  stub (TODO: LockFileEx)
  hbrdd/dbf/lock_multi_test.go   cross-process verification
  docs/gap-analysis.md        honest Harbour parity assessment

Modified:
  hbrdd/dbf/dbf.go
    - DBFArea gains fileLocked bool + lockedRecs map
    - Close() calls releaseAllLocks() before dropping the fd
  hbrtl/database.go
    - rtlDbRLock / rtlDbRUnlock now delegate to DBFArea.LockRecord /
      UnlockRecord instead of returning fixed .T./NIL
    - New rtlFLock / rtlDbUnlock for FLOCK() / DBUNLOCK()
  hbrtl/register.go
    - FLOCK and DBUNLOCK symbols registered (were missing entirely)
  compiler/analyzer/analyzer.go
    - FLOCK / DBUNLOCK added to RTL known-function set

Lock region layout (non-overlapping on purpose):
  FLOCK region       [0, HeaderLen+1)
  Record N region    [RecordOffset(N), RecordLen)

So a workarea can hold FLOCK and multiple DBRLOCK simultaneously
on the same fd without conflict.

Design rationale (captured in locks_posix.go header):
  * POSIX fcntl, not flock(2) — byte-range + NFS-safe
  * Non-blocking F_SETLK — matches Clipper FLOCK() → .F. semantics
  * Released explicitly on Close to avoid workarea-sharing races
  * Windows falls back to no-op (TODO: LockFileEx)

Verification:
  go test ./hbrdd/dbf/ -run TestFLockBlocksAcrossProcesses  PASS
  go test ./hbrdd/dbf/ -run TestRLockBlocksAcrossProcesses  PASS
  go test ./...                                             ALL PASS
  FiveSql2 43/43                                            100%
  compat_harbour 51/51                                      100%

The gap-analysis doc (docs/gap-analysis.md) is a running inventory
of what works vs what's still missing vs Harbour 3.2, written for
users evaluating Five for production — not a sales pitch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:58:03 +09:00

670 lines
21 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// analyzer.go — Semantic analysis pass for Five AST.
//
// Runs AFTER parsing, BEFORE code generation.
// Checks:
// 1. Variable declaration: all LOCAL vars declared before use
// 2. Scope analysis: LOCAL vs PRIVATE vs PUBLIC vs FIELD
// 3. Undeclared variable warnings
// 4. Unused variable warnings
// 5. Function signature validation
// 6. Type hints (when available)
package analyzer
import (
"five/compiler/ast"
"five/compiler/token"
"fmt"
"strings"
)
// Diagnostic represents an analysis warning or error.
type Diagnostic struct {
Pos token.Position
Message string
Severity Severity
}
type Severity int
const (
SevError Severity = iota // Must fix
SevWarning // Should fix
SevHint // Optional improvement
)
func (d Diagnostic) String() string {
prefix := "HINT"
switch d.Severity {
case SevError:
prefix = "ERROR"
case SevWarning:
prefix = "WARN"
}
return fmt.Sprintf("%s:%d:%d: %s: %s", d.Pos.File, d.Pos.Line, d.Pos.Col, prefix, d.Message)
}
// Scope tracks declared variables in a function.
type Scope struct {
Name string // function name
Declared map[string]VarInfo // upper(name) → info
Used map[string]bool // upper(name) → was used
Parent *Scope // outer scope (for blocks)
}
// VarInfo holds info about a declared variable.
type VarInfo struct {
Name string
Pos token.Position
Kind ast.VarScope // LOCAL, STATIC, FIELD, etc.
IsParam bool
}
// Analyzer performs semantic analysis on a parsed AST file.
type Analyzer struct {
file *ast.File
diagnostics []Diagnostic
scope *Scope
funcNames map[string]bool // declared function names (this file + external)
moduleStatics map[string]VarInfo // module-level STATIC variables
}
// Analyze runs semantic analysis and returns diagnostics.
// externalFuncs (optional) provides function names from other files in multi-file builds.
func Analyze(file *ast.File, externalFuncs ...map[string]bool) []Diagnostic {
a := &Analyzer{
file: file,
funcNames: make(map[string]bool),
moduleStatics: make(map[string]VarInfo),
}
// Phase 1: Collect all function names from this file
for _, d := range file.Decls {
switch decl := d.(type) {
case *ast.FuncDecl:
a.funcNames[strings.ToUpper(decl.Name)] = true
case *ast.ClassDecl:
a.funcNames[strings.ToUpper(decl.Name)] = true
}
}
// Merge external function names (from other PRG files in multi-file build)
for _, ext := range externalFuncs {
for name := range ext {
a.funcNames[name] = true
}
}
// Phase 1.5: Collect module-level STATIC variables
for _, d := range file.Decls {
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic {
for _, v := range vd.Vars {
a.moduleStatics[strings.ToUpper(v.Name)] = VarInfo{
Name: v.Name,
Pos: v.NamePos,
Kind: ast.ScopeStatic,
}
}
}
}
// Phase 2: Analyze each function
for _, d := range file.Decls {
switch decl := d.(type) {
case *ast.FuncDecl:
a.analyzeFunc(decl)
}
}
return a.diagnostics
}
func (a *Analyzer) analyzeFunc(fn *ast.FuncDecl) {
a.scope = &Scope{
Name: fn.Name,
Declared: make(map[string]VarInfo),
Used: make(map[string]bool),
}
// Register module-level STATIC variables (visible to all functions in this file)
for name, info := range a.moduleStatics {
a.scope.Declared[name] = info
}
// Register parameters as declared
for _, p := range fn.Params {
a.scope.Declared[strings.ToUpper(p.Name)] = VarInfo{
Name: p.Name,
Pos: p.NamePos,
IsParam: true,
}
}
// Register LOCAL/STATIC declarations
for _, d := range fn.Decls {
if vd, ok := d.(*ast.VarDecl); ok {
for _, v := range vd.Vars {
a.scope.Declared[strings.ToUpper(v.Name)] = VarInfo{
Name: v.Name,
Pos: v.NamePos,
Kind: vd.Scope,
}
}
}
}
// Analyze body statements
for _, stmt := range fn.Body {
a.analyzeStmt(stmt)
}
// Check for unused variables
for name, info := range a.scope.Declared {
if !a.scope.Used[name] && !info.IsParam {
// Skip common patterns: loop vars, error vars
lower := strings.ToLower(info.Name)
if lower == "i" || lower == "j" || lower == "k" || lower == "n" ||
lower == "err" || lower == "_" {
continue
}
a.hint(info.Pos, "unused variable '%s'", info.Name)
}
}
}
func (a *Analyzer) analyzeStmt(stmt ast.Stmt) {
if stmt == nil {
return
}
switch s := stmt.(type) {
case *ast.ExprStmt:
a.analyzeExpr(s.X)
case *ast.ReturnStmt:
if s.Value != nil {
a.analyzeExpr(s.Value)
}
for _, v := range s.Values {
a.analyzeExpr(v)
}
case *ast.IfStmt:
a.analyzeExpr(s.Cond)
for _, st := range s.Body {
a.analyzeStmt(st)
}
for _, ei := range s.ElseIfs {
a.analyzeExpr(ei.Cond)
for _, st := range ei.Body {
a.analyzeStmt(st)
}
}
for _, st := range s.ElseBody {
a.analyzeStmt(st)
}
case *ast.DoWhileStmt:
a.analyzeExpr(s.Cond)
for _, st := range s.Body {
a.analyzeStmt(st)
}
case *ast.ForStmt:
a.markUsed(s.Var)
a.analyzeExpr(s.Start)
a.analyzeExpr(s.To)
if s.Step != nil {
a.analyzeExpr(s.Step)
}
for _, st := range s.Body {
a.analyzeStmt(st)
}
case *ast.ForEachStmt:
a.markUsed(s.Var)
a.analyzeExpr(s.Collection)
for _, st := range s.Body {
a.analyzeStmt(st)
}
case *ast.SwitchStmt:
a.analyzeExpr(s.Expr)
for _, c := range s.Cases {
a.analyzeExpr(c.Value)
for _, st := range c.Body {
a.analyzeStmt(st)
}
}
for _, st := range s.Otherwise {
a.analyzeStmt(st)
}
case *ast.SeqStmt:
for _, st := range s.Body {
a.analyzeStmt(st)
}
// RECOVER USING var — declare the variable in scope
if s.RecoverVar != "" {
a.scope.Declared[strings.ToUpper(s.RecoverVar)] = VarInfo{
Name: s.RecoverVar,
Pos: s.BeginPos,
Kind: ast.ScopeLocal,
}
}
for _, st := range s.RecoverBody {
a.analyzeStmt(st)
}
case *ast.QOutStmt:
for _, e := range s.Exprs {
a.analyzeExpr(e)
}
case *ast.VarDecl:
// Mid-function LOCAL — register
for _, v := range s.Vars {
a.scope.Declared[strings.ToUpper(v.Name)] = VarInfo{
Name: v.Name,
Pos: v.NamePos,
Kind: s.Scope,
}
if v.Init != nil {
a.analyzeExpr(v.Init)
}
}
case *ast.MultiAssignStmt:
for _, name := range s.Targets {
if name != "_" {
a.markUsed(name)
}
}
for _, v := range s.Values {
a.analyzeExpr(v)
}
case *ast.DeferStmt:
a.analyzeExpr(s.Call)
case *ast.ChanSendStmt:
a.analyzeExpr(s.Chan)
a.analyzeExpr(s.Value)
case *ast.WatchStmt:
for _, c := range s.Cases {
if c.RecvChan != nil {
a.analyzeExpr(c.RecvChan)
}
if c.SendChan != nil {
a.analyzeExpr(c.SendChan)
}
if c.SendVal != nil {
a.analyzeExpr(c.SendVal)
}
if c.RecvVar != "" {
a.markUsed(c.RecvVar)
}
for _, st := range c.Body {
a.analyzeStmt(st)
}
}
for _, st := range s.Otherwise {
a.analyzeStmt(st)
}
case *ast.ParallelForStmt:
a.markUsed(s.Var)
a.analyzeExpr(s.Start)
a.analyzeExpr(s.To)
for _, st := range s.Body {
a.analyzeStmt(st)
}
case *ast.TimeoutStmt:
a.analyzeExpr(s.Duration)
for _, st := range s.Body {
a.analyzeStmt(st)
}
}
}
func (a *Analyzer) analyzeExpr(expr ast.Expr) {
if expr == nil {
return
}
switch e := expr.(type) {
case *ast.IdentExpr:
a.checkVarUsage(e.Name, e.NamePos)
case *ast.BinaryExpr:
a.analyzeExpr(e.Left)
a.analyzeExpr(e.Right)
case *ast.UnaryExpr:
a.analyzeExpr(e.X)
case *ast.PostfixExpr:
a.analyzeExpr(e.X)
case *ast.AssignExpr:
a.analyzeExpr(e.Left)
a.analyzeExpr(e.Right)
case *ast.CallExpr:
a.analyzeExpr(e.Func)
for _, arg := range e.Args {
a.analyzeExpr(arg)
}
case *ast.SendExpr:
a.analyzeExpr(e.Object)
for _, arg := range e.Args {
a.analyzeExpr(arg)
}
case *ast.IndexExpr:
a.analyzeExpr(e.X)
a.analyzeExpr(e.Index)
case *ast.SliceExpr:
a.analyzeExpr(e.X)
if e.Low != nil {
a.analyzeExpr(e.Low)
}
if e.High != nil {
a.analyzeExpr(e.High)
}
case *ast.DotExpr:
a.analyzeExpr(e.X)
case *ast.ArrayLitExpr:
for _, item := range e.Items {
a.analyzeExpr(item)
}
case *ast.HashLitExpr:
for i := range e.Keys {
a.analyzeExpr(e.Keys[i])
a.analyzeExpr(e.Values[i])
}
case *ast.BlockExpr:
// Register block parameters (e.g., {|x,y| x + y})
for _, p := range e.Params {
a.scope.Declared[strings.ToUpper(p)] = VarInfo{
Name: p,
Pos: e.LBrace,
IsParam: true,
}
}
a.analyzeExpr(e.Body)
case *ast.AliasExpr:
a.analyzeExpr(e.Alias)
a.analyzeExpr(e.Field)
case *ast.MacroExpr:
a.analyzeExpr(e.Expr)
case *ast.RefExpr:
a.analyzeExpr(e.X)
case *ast.NilSafeExpr:
a.analyzeExpr(e.X)
for _, arg := range e.Args {
a.analyzeExpr(arg)
}
case *ast.ChanRecvExpr:
a.analyzeExpr(e.Chan)
case *ast.AsyncExpr:
a.analyzeExpr(e.Call)
case *ast.AwaitExpr:
a.analyzeExpr(e.Future)
}
}
// checkVarUsage verifies a variable is declared and marks it used.
func (a *Analyzer) checkVarUsage(name string, pos token.Position) {
upper := strings.ToUpper(name)
// Skip well-known RTL functions and constants
if a.isKnownFunction(upper) || a.isBuiltinConstant(upper) {
return
}
// Mark as used
a.markUsed(name)
// Check if declared in current scope
if _, ok := a.scope.Declared[upper]; ok {
return
}
// Check if it's an IMPORT package name
for _, imp := range a.file.Imports {
parts := strings.Split(imp.Path, "/")
pkgName := parts[len(parts)-1]
if strings.EqualFold(pkgName, name) || (imp.Alias != "" && imp.Alias == name) {
return
}
}
// Not declared — warn (could be MEMVAR, FIELD, or typo)
a.warn(pos, "undeclared variable '%s' (missing LOCAL?)", name)
}
func (a *Analyzer) markUsed(name string) {
if a.scope != nil {
a.scope.Used[strings.ToUpper(name)] = true
}
}
// rtlFunctions contains all 479 RTL functions registered in hbrtl/register.go.
// Generated from: grep -o 'hbrt.Sym("[^"]*"' hbrtl/register.go
var rtlFunctions = map[string]bool{
// Console
"QOUT": true, "QQOUT": true,
// String/Conversion
"STR": true, "VAL": true, "LEN": true, "SUBSTR": true, "UPPER": true, "LOWER": true,
"ALLTRIM": true, "LTRIM": true, "RTRIM": true, "TRIM": true, "SPACE": true,
"PADR": true, "PADL": true, "PADC": true, "REPLICATE": true,
// Type/Conversion
"VALTYPE": true, "EMPTY": true, "ABS": true, "INT": true,
// Array
"AADD": true, "ADEL": true, "AINS": true, "ASIZE": true, "ACLONE": true,
"ACOPY": true, "AFILL": true, "ASORT": true, "AEVAL": true, "ASCAN": true, "ATAIL": true,
// Hash
"HB_HASH": true, "HB_HGET": true, "HB_HSET": true, "HB_HDEL": true,
"HB_HHASKEY": true, "HB_HKEYS": true, "HB_HVALUES": true,
"HB_HPOS": true, "HB_HKEYAT": true, "HB_HVALUEAT": true, "HB_HCLONE": true,
// Date/Time
"DATE": true, "TIME": true, "YEAR": true, "MONTH": true, "DAY": true, "DOW": true,
"SECONDS": true, "DTOC": true, "DTOS": true, "STOD": true, "CTOD": true,
"CDOW": true, "CMONTH": true, "DAYS": true, "ELAPTIME": true, "AMPM": true, "SECS": true,
// Eval
"EVAL": true,
// String Extended
"AT": true, "LEFT": true, "RIGHT": true, "ASC": true, "CHR": true,
"STRTRAN": true, "STUFF": true, "RAT": true, "HARDCR": true,
"HB_STRREPLACE": true, "HB_NTOS": true, "DESCEND": true,
"HB_VALTOSTR": true, "HB_VALTOEXP": true, "HB_CSTR": true,
// Math
"ROUND": true, "MAX": true, "MIN": true, "SQRT": true, "LOG": true, "EXP": true, "MOD": true,
// Misc
"TYPE": true, "PCOUNT": true, "BREAK": true, "ARRAY": true, "FCOUNT": true,
"FIELDNAME": true, "SELECT": true, "FILE": true, "INKEY": true, "TRANSFORM": true,
"SETDATEFORMAT": true, "SETEPOCH": true, "SETCENTURY": true,
"IIF": true, "IF": true, "STRZERO": true, "OUTSTD": true, "OUTERR": true,
"CENTER": true, "SOUNDEX": true, "TONE": true,
// Terminal
"SETPOS": true, "ROW": true, "COL": true, "DEVPOS": true, "DEVOUT": true,
"DISPOUT": true, "DEVOUTPICT": true, "DISPBOX": true, "CLS": true, "SCROLL": true,
"SETCOLOR": true, "SETCURSOR": true, "MAXROW": true, "MAXCOL": true,
// dbEdit/Browse
"DBEDIT": true, "TBROWSEDB": true, "TBROWSENEW": true, "TBCOLUMNNEW": true,
// RDD
"EOF": true, "BOF": true, "FOUND": true, "RECNO": true, "RECCOUNT": true,
"LASTREC": true, "DELETED": true, "FIELDGET": true, "FIELDPUT": true,
"FIELDPOS": true, "FIELDBLOCK": true, "FIELDWBLOCK": true, "AFIELDS": true,
"DBSTRUCT": true,
// Database
"ALIAS": true, "DBEVAL": true, "USED": true, "DBUSEAREA": true,
"DBCLOSEAREA": true, "DBCLOSEALL": true, "DBGOTO": true, "DBSKIP": true,
"DBGOTOP": true, "DBGOBOTTOM": true, "DBAPPEND": true, "DBDELETE": true,
"DBRECALL": true, "DBCOMMIT": true, "DBRLOCK": true, "DBRUNLOCK": true,
"DBSEEK": true, "DBSELECTAREA": true, "DBPACK": true, "DBZAP": true,
"DBCREATE": true, "DBINFO": true, "DBORDERINFO": true, "DBSETINDEX": true,
"RECALL": true, "PACK": true, "ZAP": true,
"FLOCK": true, "DBUNLOCK": true,
"__DBPACK": true, "__DBZAP": true,
// Locate/Filter
"DBLOCATE": true, "__DBLOCATE": true, "__DBCONTINUE": true,
"DBSETFILTER": true, "DBCLEARFILTER": true, "DBFILTER": true,
// Encoding/Hashing
"HB_MD5": true, "HB_SHA256": true, "HB_BASE64ENCODE": true,
"HB_BASE64DECODE": true, "HB_CRC32": true,
// Bit Operations
"HB_BITAND": true, "HB_BITOR": true, "HB_BITXOR": true, "HB_BITNOT": true,
"HB_BITSHIFT": true, "HB_BITTEST": true, "HB_BITSET": true, "HB_BITRESET": true,
// Regex
"HB_REGEXCOMP": true, "HB_REGEXMATCH": true, "HB_REGEXSPLIT": true,
"HB_REGEXALL": true, "HB_REGEXREPLACE": true,
// Memo
"MEMOREAD": true, "MEMOWRIT": true, "MEMOTRAN": true, "MEMOLINE": true, "MLCOUNT": true,
// Binary Conversion
"BIN2I": true, "BIN2L": true, "BIN2W": true, "I2BIN": true, "L2BIN": true, "W2BIN": true,
// Keyboard
"LASTKEY": true, "NEXTKEY": true, "READKEY": true, "SETKEY": true,
"KEYBOARD": true, "HB_KEYPUT": true, "HB_KEYCHAR": true, "HB_KEYINS": true,
// Display
"DISPBEGIN": true, "DISPEND": true, "DISPCOUNT": true,
"SAVESCREEN": true, "RESTSCREEN": true, "ALERT": true,
// Error Handling
"ERRORBLOCK": true, "ERRORNEW": true, "DOSERROR": true, "FERROR": true, "ERRORSYS": true,
// SET Commands
"SET": true, "__SETDATEFORMAT": true, "__SETDECIMALS": true, "__SETEPOCH": true,
"SETDELETED": true, "SETEXACT": true, "SETSOFTSEEK": true, "SETEXCLUSIVE": true,
"SETFIXED": true, "SETCANCEL": true, "SETBELL": true, "SETCONFIRM": true,
"SETINSERT": true, "SETESCAPE": true, "SETWRAP": true,
"_SET_EXACT": true, "_SET_DELETED": true, "_SET_SOFTSEEK": true,
"_SET_EXCLUSIVE": true, "_SET_DATEFORMAT": true, "_SET_DECIMALS": true, "_SET_EPOCH": true,
// File I/O
"FOPEN": true, "FCREATE": true, "FCLOSE": true, "FREAD": true, "FWRITE": true,
"FSEEK": true, "FERASE": true, "FRENAME": true, "HB_FILEEXISTS": true,
// Directory/Disk
"CURDIR": true, "DIRCHANGE": true, "DIRECTORY": true, "DIRMAKE": true,
"DIRREMOVE": true, "DISKSPACE": true,
// Type Checking
"HB_ISARRAY": true, "HB_ISBLOCK": true, "HB_ISCHAR": true, "HB_ISSTRING": true,
"HB_ISDATE": true, "HB_ISDATETIME": true, "HB_ISLOGICAL": true, "HB_ISNUMERIC": true,
"HB_ISOBJECT": true, "HB_ISHASH": true, "HB_ISNIL": true, "HB_ISPOINTER": true,
"HB_ISEVALITEM": true, "HB_ISNULL": true,
// OS/Environment
"GETENV": true, "HB_GETENV": true, "SETENV": true, "HB_SETENV": true,
"OS": true, "VERSION": true, "HB_RUN": true,
"HB_FNAMEDIR": true, "HB_FNAMEEXT": true, "HB_FNAMENAME": true, "HB_FNAMEMERGE": true,
"HB_FNAMESPLIT": true, "HB_FNAMEEXISTS": true, "HB_FNAMEEXTSET": true, "HB_FNAMENAMEEXT": true,
// Character Classification
"ISDIGIT": true, "ISALPHA": true, "ISALNUM": true,
"ISUPPER": true, "ISLOWER": true, "ISSPACE": true,
// Harbour Extensions
"HB_ASCIIUPPER": true, "HB_ASCIILOWER": true,
"HB_DEFAULT": true, "HB_DEFAULTVALUE": true,
"HB_DISPOUTAT": true, "HB_DISPOUTATBOX": true, "HB_DISPBOX": true,
"HB_COLORINDEX": true, "HB_LEFTEQ": true, "HB_LEFTEQI": true,
"HB_VAL": true, "HB_TOKENGET": true, "HB_TOKENCOUNT": true,
"__DEFAULTNIL": true, "MEMVARBLOCK": true,
// Bitmap/Rushmore
"BM_DBSETFILTER": true, "BM_DBSEEKWILD": true, "BM_TURBO": true,
"BM_DBGETFILTERARRAY": true, "BM_DBSETFILTERARRAY": true,
"BM_DBSETFILTERARRAYADD": true, "BM_DBSETFILTERARRAYDEL": true,
// HBSIX Compatibility
"SX_SETTAG": true, "SX_INDEXTAG": true, "SX_TAGORDER": true,
"SX_TAGCOUNT": true, "SX_TAGS": true, "SX_SETFILEORD": true,
"SX_ISDBT": true, "SX_ISFPT": true, "SX_ISSMT": true,
"SX_AUTOOPEN": true, "SX_AUTOSHARE": true, "SX_BLOB2FILE": true,
"SX_FILE2BLOB": true, "SX_SETTRIGGER": true, "SX_VFGET": true,
"SX_DBFENCRYPT": true, "SX_DBFDECRYPT": true, "SX_COMPRESS": true,
"SX_DECOMPRESS": true, "RDDINFO": true, "RDDNAME": true, "RDDLIST": true,
// Timestamp
"HB_DATETIME": true, "HB_HOUR": true, "HB_MINUTE": true, "HB_SEC": true,
"HB_TTOC": true, "HB_CTOT": true, "HB_SECOND": true, "HB_ATOKENS": true,
"HB_CDPSELECT": true, "HB_TTOS": true, "HB_STOT": true, "HB_MILLISECONDS": true,
"HB_DATE": true, "HB_CTOD": true, "HB_DTOC": true, "HB_STOD": true,
"HB_DTOT": true, "HB_TTOD": true, "HB_TTOHOUR": true, "HB_TTOMIN": true,
"HB_TTOSEC": true, "HB_TTOMSEC": true, "HB_TTON": true, "HB_NTOT": true,
"HB_NTOHOUR": true, "HB_NTOMIN": true, "HB_NTOSEC": true,
"HB_WEEK": true, "HB_CDAY": true,
// Index/DB Introspection
"INDEXORD": true, "INDEXKEY": true, "ORDSETFOCUS": true, "ORDCOUNT": true,
"ORDNAME": true, "ORDKEY": true, "ORDFOR": true, "ORDINFO": true,
"ORDSCOPE": true, "RDDSETDEFAULT": true,
// Directory/Temp
"HB_DIREXISTS": true, "HB_DIRCREATE": true, "HB_FTEMPCREATE": true, "HB_DIRTEMP": true,
// File Extended
"HB_FSIZE": true, "HB_FCOPY": true, "HB_FEOF": true, "HB_FCOMMIT": true,
"HB_FREADLEN": true, "HB_FGETATTR": true, "HB_FSETATTR": true,
"HB_FGETDATETIME": true, "HB_FSETDATETIME": true, "HB_FLOCK": true, "HB_FUNLOCK": true,
"HB_FILEDELETE": true, "HB_FILEMATCH": true,
"HB_MEMOREAD": true, "HB_MEMOWRIT": true, "HB_DISKSPACE": true,
// String Extended 2
"HB_AT": true, "HB_RAT": true, "HB_ATI": true, "HB_ATX": true,
"HB_ASCIIISALPHA": true, "HB_ASCIIISDIGIT": true,
"HB_ASCIIISLOWER": true, "HB_ASCIIISUPPER": true,
"HB_STRISUTF8": true, "HB_STRDECODESCAPE": true, "HB_STRXOR": true,
"HB_WILDMATCH": true, "HB_WILDMATCHI": true,
"HB_STRTOHEX": true, "HB_HEXTOSTR": true, "HB_STRFORMAT": true,
// Hex Conversion
"HB_NUMTOHEX": true, "HB_HEXTONUM": true,
// Token
"TOKEN": true, "NUMTOKEN": true,
// Stack Introspection
"PROCNAME": true, "PROCLINE": true, "PROCFILE": true, "ERRORLEVEL": true,
// JSON
"HB_JSONENCODE": true, "HB_JSONDECODE": true,
"JSONPRETTY": true, "JSONTO": true, "JSONFROM": true, "JSONPATH": true,
"JSONMERGE": true, "JSONTYPE": true, "JSONVALID": true,
"JSONHTTPGET": true, "JSONHTTPPOST": true,
// Random
"HB_RANDOM": true, "HB_RANDOMINT": true, "HB_RANDOMSEED": true, "HB_RANDSTR": true,
// OS Info
"HB_VERSION": true, "HB_COMPILER": true, "HB_OSNEWLINE": true,
"HB_OSPATHSEPARATOR": true, "HB_CWD": true, "HB_DIRBASE": true,
"HB_PROGNAME": true, "HB_USERNAME": true, "HB_GETHOSTNAME": true,
// Process
"HB_PROCESSRUN": true, "WAIT": true,
// UTF-8
"HB_UTF8TOSTR": true, "HB_STRTOUTF8": true, "HB_UTF8LEN": true,
"HB_UTF8SUBSTR": true, "HB_UTF8LEFT": true, "HB_UTF8RIGHT": true, "HB_UTF8AT": true,
// FRB
"FRBLOAD": true, "FRBDO": true, "FRBUNLOAD": true, "FRBRUN": true,
"FRBCOMPILE": true, "FRBEXEC": true,
// Concurrency
"GO": true, "CHANNEL": true, "CHSEND": true, "CHRECEIVE": true, "CHCLOSE": true,
"WAITGROUP": true, "WGDONE": true, "WGWAIT": true, "WGADD": true,
"MUTEX": true, "LOCK": true, "UNLOCK": true, "SLEEP": true,
// Harbour compat aliases
"HB_SYMBOL_UNUSED": true, "HB_IDLEADD": true, "HB_IDLESLEEP": true,
"HB_PS": true, "HB_EOL": true,
}
func (a *Analyzer) isKnownFunction(name string) bool {
// Check declared functions in this file (and external files)
if a.funcNames[name] {
return true
}
return rtlFunctions[name]
}
func (a *Analyzer) isBuiltinConstant(name string) bool {
constants := map[string]bool{
"NIL": true, "TRUE": true, "FALSE": true,
"SELF": true, "SUPER": true,
// Harbour commands treated as identifiers
"QUIT": true, "ERRORLEVEL": true,
// Field/Memvar alias prefixes
"FIELD": true, "_FIELD": true, "M": true, "MEMVAR": true,
// Keyboard constants
"K_ESC": true, "K_ENTER": true, "K_UP": true, "K_DOWN": true,
"K_LEFT": true, "K_RIGHT": true, "K_PGUP": true, "K_PGDN": true,
// Alternate source (SET ALTERNATE)
"ALTSRC": true,
}
return constants[name]
}
// --- Diagnostic helpers ---
func (a *Analyzer) diag(sev Severity, pos token.Position, format string, args ...interface{}) {
a.diagnostics = append(a.diagnostics, Diagnostic{
Pos: pos,
Message: fmt.Sprintf(format, args...),
Severity: sev,
})
}
func (a *Analyzer) errorf(pos token.Position, format string, args ...interface{}) {
a.diag(SevError, pos, format, args...)
}
func (a *Analyzer) warn(pos token.Position, format string, args ...interface{}) {
a.diag(SevWarning, pos, format, args...)
}
func (a *Analyzer) hint(pos token.Position, format string, args ...interface{}) {
a.diag(SevHint, pos, format, args...)
}