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>
670 lines
21 KiB
Go
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...)
|
|
}
|