- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
447 lines
11 KiB
Go
447 lines
11 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
|
|
}
|
|
|
|
// Analyze runs semantic analysis and returns diagnostics.
|
|
func Analyze(file *ast.File) []Diagnostic {
|
|
a := &Analyzer{
|
|
file: file,
|
|
funcNames: make(map[string]bool),
|
|
}
|
|
|
|
// Phase 1: Collect all function names
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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 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)
|
|
}
|
|
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:
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
func (a *Analyzer) isKnownFunction(name string) bool {
|
|
// Check declared functions in this file
|
|
if a.funcNames[name] {
|
|
return true
|
|
}
|
|
// Common RTL functions
|
|
rtl := map[string]bool{
|
|
"LEN": true, "SUBSTR": true, "LEFT": true, "RIGHT": true,
|
|
"UPPER": true, "LOWER": true, "TRIM": true, "LTRIM": true, "RTRIM": true,
|
|
"STR": true, "VAL": true, "STRTRAN": true, "AT": true, "RAT": true,
|
|
"SPACE": true, "REPLICATE": true, "PADR": true, "PADL": true, "PADC": true,
|
|
"VALTYPE": true, "TYPE": true, "EMPTY": true, "HB_ISSTRING": true,
|
|
"EVAL": true, "AEVAL": true, "ASCAN": true, "ASORT": true,
|
|
"AADD": true, "ADEL": true, "AINS": true, "ASIZE": true, "ACOPY": true, "ACLONE": true,
|
|
"ARRAY": true, "HASH": true, "HB_HASH": true,
|
|
"DTOC": true, "CTOD": true, "DTOS": true, "DATE": true, "TIME": true, "YEAR": true, "MONTH": true, "DAY": true,
|
|
"QOUT": true, "QQOUT": true, "OUTSTD": true, "ALERT": true,
|
|
"INKEY": true, "LASTKEY": true, "CHR": true, "ASC": true,
|
|
"FILE": true, "FOPEN": true, "FCLOSE": true, "FREAD": true, "FWRITE": true,
|
|
"IIF": true, "IF": true, "STRZERO": true, "TRANSFORM": true,
|
|
"FIELDNAME": true, "FIELDPUT": true, "FIELDGET": true, "FCOUNT": true,
|
|
"ALIAS": true, "DBAPPEND": true, "DBDELETE": true, "DBSKIP": true,
|
|
"DBGOTO": true, "DBGOTOP": true, "DBGOBOTTOM": true, "DBCOMMIT": true,
|
|
"RECNO": true, "RECCOUNT": true, "EOF": true, "BOF": true, "FOUND": true,
|
|
"CHANNEL": true, "CHSEND": true, "CHRECEIVE": true,
|
|
"SLEEP": true, "HB_IDLEADD": true, "SECONDS": true,
|
|
"ERRORBLOCK": true, "BREAK": true, "PCOUNT": true, "PROCNAME": true,
|
|
"SETPOS": true, "ROW": true, "COL": true, "MAXROW": true, "MAXCOL": true,
|
|
"SETCOLOR": true, "DISPBOX": true, "DISPBEGIN": true, "DISPEND": true,
|
|
"HB_SYMBOL_UNUSED": true, "HB_DEFAULT": true, "HB_NTOS": true,
|
|
}
|
|
return rtl[name]
|
|
}
|
|
|
|
func (a *Analyzer) isBuiltinConstant(name string) bool {
|
|
constants := map[string]bool{
|
|
"NIL": true, "TRUE": true, "FALSE": true,
|
|
"SELF": true, "SUPER": true,
|
|
"K_ESC": true, "K_ENTER": true, "K_UP": true, "K_DOWN": true,
|
|
"K_LEFT": true, "K_RIGHT": true, "K_PGUP": true, "K_PGDN": 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...)
|
|
}
|