Files
five/compiler/analyzer/analyzer.go
CharlesKWON 3a00aa5435 feat(hbrtl): field metadata + index creation RTL — TSqlIndex warnings to zero
TSqlIndex.prg had five undefined identifiers and six undefined
constants that the new CLASS-method analyzer surfaced after the
gengo PushMemvar fallback stopped crashing on them. All real tech
debt, not false positives. This lands the implementations.

New RTL functions (hbrtl/indexrtl.go + register.go):
  - FieldType(n) → "C"/"N"/"L"/"D"/"M"/... one-letter type
  - FieldLen(n)  → length in bytes
  - FieldDec(n)  → decimal places
  - ordCreate(cBag, cTag, cExpr [, bExpr] [, lUnique])
      → DBFArea.OrderCreate with TagName set (CDX tag or NTX tag)
  - dbCreateIndex(cFile, cExpr [, bExpr] [, lUnique])
      → legacy Clipper single-tag NTX without TagName
  - dbClearIndex() → OrderListClear

All pass through the existing Indexer interface; key expressions go
through the MacroEval slow path since callers pass string literals.
When callers are updated to pass compiled key blocks, the existing
KeyFunc fast path kicks in automatically.

New header files (include/):
  - dbinfo.ch  — DBI_* and DBOI_* constants with Harbour-compatible
                 values (FULLPATH=10, SHARED=42, EXPRESSION=2, etc.)
  - dbstruct.ch — DBS_NAME/TYPE/LEN/DEC field descriptor indices

TSqlIndex.prg already did `#include "dbinfo.ch"` and `#include
"dbstruct.ch"` but Five's preprocessor silently ignored the missing
files. Both headers land in include/ where cmd/five's include-dir
chain already looks.

Analyzer RTL allow-list updated with the six new function names so
the warning pipeline stays clean.

Result: FiveSql2 build goes from 17 WARN → 0. Both tracked test
suites still pass.

Note: dbInfo() / dbOrderInfo() themselves remain stubbed (return NIL)
— the constants exist for compile-time resolution and for future use
when the stubs are replaced. Callers that depend on actual dbInfo
values still get NIL at runtime.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

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

734 lines
23 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 and class method body
for _, d := range file.Decls {
switch decl := d.(type) {
case *ast.FuncDecl:
a.analyzeFunc(decl)
case *ast.MethodDecl:
a.analyzeMethod(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)
}
}
}
// analyzeMethod walks a class-method body (`METHOD Foo() CLASS TBar`)
// applying the same undeclared-variable and unused-variable checks
// that analyzeFunc performs on standalone functions. Without this,
// unresolved identifiers inside CLASS methods silently fell through
// to gengo's memvar fallback (NIL at runtime) — e.g. a missing
// `#include "dbinfo.ch"` leaving DBI_FULLPATH undefined in a method.
func (a *Analyzer) analyzeMethod(m *ast.MethodDecl) {
a.scope = &Scope{
Name: m.Name,
Declared: make(map[string]VarInfo),
Used: make(map[string]bool),
}
// Module-level STATICs are visible to class methods too
for name, info := range a.moduleStatics {
a.scope.Declared[name] = info
}
// Parameters
for _, p := range m.Params {
a.scope.Declared[strings.ToUpper(p.Name)] = VarInfo{
Name: p.Name,
Pos: p.NamePos,
IsParam: true,
}
}
// LOCAL / STATIC declarations inside the method
for _, d := range m.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,
}
}
}
}
for _, stmt := range m.Body {
a.analyzeStmt(stmt)
}
// Unused-variable hints (same exclusions as analyzeFunc)
for name, info := range a.scope.Declared {
if !a.scope.Used[name] && !info.IsParam {
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,
// FiveSql2 hybrid hot-path RTL (pcode + Go-native scan)
"PCCOMPILE": true, "PCEVAL": true, "SQLSCAN": true,
// Field metadata + index creation
"FIELDTYPE": true, "FIELDLEN": true, "FIELDDEC": true,
"ORDCREATE": true, "DBCREATEINDEX": true, "DBCLEARINDEX": 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...)
}