// 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, "SQLEACH": true, "SQLHASHBUILD": true, "SQLHASHJOIN": true, "SQLORDERBY": true, "SQLGROUPBY": 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...) }