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>
734 lines
23 KiB
Go
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...)
|
|
}
|