// 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...) }