Files
five/compiler/analyzer/analyzer.go
Charles KWON OhJun 0828d17159 feat: Harbour RTL vs Go math comparison example + analyzer IMPORT fix
- examples/go_math_compare.prg: Side-by-side comparison of
  Harbour RTL (Abs, Sqrt, Round, Int, Max, Min, Log, Exp, Mod)
  vs Go math package (Sin, Cos, Pow, Pi, Floor, Ceil, Hypot, ...)
- Combined usage: normal distribution, compound interest, distance
- Analyzer: recognize IMPORT package names as valid identifiers
- Analyzer: add math RTL functions (ABS, SQRT, etc.) to known list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:35:40 +09:00

458 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
}
// 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
}
}
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,
"ABS": true, "INT": true, "ROUND": true, "SQRT": true, "LOG": true, "EXP": true,
"MAX": true, "MIN": true, "MOD": 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...)
}