analysis

package
v1.55.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: BSD-3-Clause Imports: 15 Imported by: 0

Documentation

Overview

Package analysis provides scope-aware semantic analysis for ELPS lisp source.

The analyzer builds a scope tree from parsed expressions, resolves symbol references, and identifies unresolved symbols. It is designed to be used by lint analyzers for semantic checks like undefined-symbol and unused-variable.

Index

Constants

View Source
const (
	// DefaultMaxWorkspaceFiles limits the number of .lisp files scanned.
	DefaultMaxWorkspaceFiles = 5000
	// DefaultMaxFileBytes limits individual file size (5 MB).
	DefaultMaxFileBytes = 5 * 1024 * 1024
)

Default limits for workspace scanning.

Variables

This section is empty.

Functions

func ExtractPackageExports

func ExtractPackageExports(reg *lisp.PackageRegistry) map[string][]ExternalSymbol

ExtractPackageExports creates a map of package name to exported symbols from a loaded runtime's package registry. This is used to resolve use-package imports for packages defined in Go (stdlib) that cannot be found by workspace scanning.

func LoadWorkspaceMacros added in v1.47.0

func LoadWorkspaceMacros(env *lisp.LEnv, preamble []*lisp.LVal) []error

LoadWorkspaceMacros replays workspace preamble forms (in-package, use-package, export, defmacro, defun, set) into the given environment. This mirrors what the runtime's (load) does — forms are eval'd in source order so package context, imports, and definitions build up naturally.

Workspace packages that don't exist in the boot env are auto-created. The env's active package is saved and restored after loading.

Malformed forms are skipped — the returned errors slice contains one entry per failure. Callers should log these for visibility.

func MatchesExclude added in v1.40.0

func MatchesExclude(path string, patterns []string) bool

MatchesExclude returns true if the given path matches any of the exclude patterns. Each pattern is matched against the full path and each path component (directories + filename), using filepath.Match semantics.

func NormalizePath added in v1.47.1

func NormalizePath(path string) string

NormalizePath returns a cleaned absolute path and resolves symlinks when possible. If the path cannot be resolved, it falls back to a cleaned absolute-or-original path for stable equality checks.

func ScanWorkspaceAll added in v1.39.0

func ScanWorkspaceAll(root string) (globals []ExternalSymbol, pkgs map[string][]ExternalSymbol, allDefs []ExternalSymbol, err error)

ScanWorkspaceAll combines ScanWorkspaceFull and ScanWorkspaceDefinitions into a single pass: each file is parsed once and all three results are extracted from the same AST.

func ScanWorkspaceAllWithConfig added in v1.40.0

func ScanWorkspaceAllWithConfig(root string, scanCfg *ScanConfig) (globals []ExternalSymbol, pkgs map[string][]ExternalSymbol, allDefs []ExternalSymbol, truncated bool, err error)

ScanWorkspaceAllWithConfig is like ScanWorkspaceAll but accepts a ScanConfig for configurable limits. It also returns whether the file list was truncated due to hitting the MaxFiles limit.

Delegates to PrescanWorkspace internally to avoid duplicating the concurrent worker pool logic.

func ScanWorkspacePackages

func ScanWorkspacePackages(root string) (map[string][]ExternalSymbol, error)

ScanWorkspacePackages is like ScanWorkspace but returns a map of package name to exported symbols, suitable for use as Config.PackageExports.

func ScanWorkspaceRefs added in v1.31.0

func ScanWorkspaceRefs(root string, cfg *Config, scanCfg *ScanConfig) map[string][]FileReference

ScanWorkspaceRefs walks the workspace directory tree, performs full analysis on each .lisp file, and extracts cross-file references. The cfg should have ExtraGlobals and PackageExports populated from a prior ScanWorkspaceFull call. Parsing is done concurrently.

If cfg.MacroExpander is set it is used for every file, so the index agrees with per-document analysis about symbols reachable only through a macro expansion. Expansion is only attempted for heads that are user macros or unresolved, so the added cost is small; leave the field nil to skip it. The optional scanCfg controls file collection limits and excludes.

Returns a map from SymbolKey.String() to FileReference slices.

func ShouldSkipDir added in v1.32.0

func ShouldSkipDir(name string) bool

ShouldSkipDir returns true for directories that should not be walked. It skips hidden directories (e.g. .git, .vscode), underscore-prefixed directories (e.g. _archive, _old), and common dependency/build output directories — but not "." or ".." which represent the current/parent directory.

func SortDefinitions added in v1.40.0

func SortDefinitions(syms []ExternalSymbol)

SortDefinitions sorts external symbols in-place by deterministic priority: lexicographically smallest Source.File, then earliest Source.Line, then earliest Source.Col. Symbols with nil Source are pushed to the end.

Types

type Config

type Config struct {
	// ExtraGlobals are symbols from other files (e.g. workspace scanning).
	ExtraGlobals []ExternalSymbol

	// PackageExports maps package names to their exported symbols.
	// Used to resolve use-package imports from stdlib and workspace packages.
	PackageExports map[string][]ExternalSymbol

	// PackageSymbols maps package names to ALL symbols (exported and
	// non-exported). Used by resolveQualifiedSymbol as a fallback when
	// a qualified reference (pkg:sym) targets a non-exported symbol.
	// ELPS runtime allows qualified access to any symbol in a package,
	// not just exported ones.
	PackageSymbols map[string][]ExternalSymbol

	// DefForms declares additional definition-form heads for embedding programs.
	// Head is matched exactly against the form head symbol. FormalsIndex must
	// point at the parameter list cell. If BindsName is true, NameIndex points
	// at the defined symbol cell and the analyzer registers it using NameKind.
	DefForms []DefFormSpec

	// PackageImports maps package name to imported package names collected
	// from cross-file use-package declarations during workspace prescan.
	// Per-file analysis applies these imports so symbols from packages
	// imported in other files (e.g. main.lisp) are available.
	PackageImports map[string][]string

	// DefaultPackage overrides the default "user" package for bare files
	// (no in-package declaration). Derived from main.lisp's in-package.
	DefaultPackage string

	// WorkspaceRefs maps SymbolKey.String() to cross-file references.
	// When set, analyzers can check whether a symbol is referenced from
	// other files in the workspace.
	WorkspaceRefs map[string][]FileReference

	// MacroExpander optionally expands user-macro calls at analysis time.
	// When set, the analyzer expands macro calls and analyzes the expanded
	// code, resolving symbols introduced by the macro (e.g. lambda params).
	// If expansion fails, the analyzer falls back to opaque macro handling.
	MacroExpander MacroExpander

	// Filename is the source file being analyzed.
	Filename string
}

Config controls the behavior of the analyzer.

func ConfigForFile added in v1.50.0

func ConfigForFile(cfg *Config, filename string) *Config

ConfigForFile returns a copy of cfg with Filename set to filename, leaving the caller's Config untouched. A nil cfg yields an empty Config.

This is a whole-struct copy rather than a field-by-field rebuild on purpose. The rebuild it replaces silently dropped fields twice: DefForms and PackageImports, and then MacroExpander, which did not exist when the rebuild was written and was never added to it (issue #353). Copying carries new Config fields through automatically, so the omission cannot recur. Use this anywhere a workspace-wide Config is specialised for one file.

type DefFormSpec added in v1.38.0

type DefFormSpec struct {
	Head         string
	FormalsIndex int
	BindsName    bool
	NameIndex    int
	NameKind     SymbolKind
}

DefFormSpec describes a custom definition-like form.

type EnvMacroExpander added in v1.47.0

type EnvMacroExpander struct {
	Env *lisp.LEnv
	// contains filtered or unexported fields
}

EnvMacroExpander uses a live LEnv to expand user-defined macros. Expansion errors and panics are caught — the analyzer falls back to opaque analysis when ExpandMacro returns nil.

Thread-safe: a mutex serializes expansion calls since MacroCall mutates shared Runtime state (call stack, package pointer). The mutex is only contended when multiple LSP handlers trigger concurrent analyses.

A recovered panic is COUNTED as well as swallowed; see ExpansionPanics for why the count exists and why nothing outside this file can fake it.

func (*EnvMacroExpander) ExpandMacro added in v1.47.0

func (e *EnvMacroExpander) ExpandMacro(form *lisp.LVal, pkg string) (result *lisp.LVal)

ExpandMacro looks up the head symbol in the environment relative to the given package, verifies it is a macro, and calls MacroCall to expand it. The env is temporarily switched to pkg so that unqualified symbol resolution matches the file's package context — the same way the runtime resolves symbols during eval.

A recovered panic still yields nil — the analyzer's fallback to opaque macro handling is the right behaviour and does not change — but it is now also COUNTED, so the crash is observable even though the return value cannot carry it. See ExpansionPanics.

func (*EnvMacroExpander) ExpansionPanics added in v1.50.0

func (e *EnvMacroExpander) ExpansionPanics() uint64

ExpansionPanics returns how many ExpandMacro calls on this expander did not complete normally — a Go panic that the blanket recover swallowed, or a runtime.Goexit that unwound through it.

Why this is here at all

ExpandMacro's recover sets result = nil, and nil is ALSO the ordinary answer for "that head symbol is not a macro" — by far the most common outcome, since the analyzer consults the expander for every unknown head symbol in the file. A panic in analysis-time macro expansion was therefore not merely unreported, it was unreportABLE: no caller, and no test or fuzz target, could tell the crash apart from the routine case. Merely opening a document runs this code over attacker-chosen source (lsp/), and luthersystems/substrate reaches it through lint/ as well, so the silent class is a real one.

Why the marker cannot be forged

This is the same trick lisp.IsInternalPanic uses, and the same one the LSP fuzz harness uses on buildWorkspaceIndex: key off something only the non-panicking path can produce, rather than off a value the code under test chose.

The marker is `completed`, a bool local to ExpandMacro's own stack frame. It is set by exactly one statement, placed immediately after the call to expand() returns, so it runs if and only if control came back from expand normally. A panic anywhere inside expand — including inside a macro BODY that env.MacroCall evaluates — skips that statement, and the deferred handler observes completed == false and counts the abort.

Nothing reachable from lisp can touch a Go local. A macro cannot expand to a value that sets it, an expansion result cannot be shaped to imitate it, and an embedder cannot set it either: it does not exist outside the frame. The counter it feeds is unexported, is only ever incremented, and the package exports no way to lower it — deliberately, so that the code whose bugs it records cannot clear its own record. That is the difference between this and a `Panicked bool` field, which any holder of the expander could set to either value at any time.

The count is monotonic over the expander's lifetime, so callers that want a per-operation answer should take a snapshot before and compare after.

func (*EnvMacroExpander) LastExpansionPanic added in v1.50.0

func (e *EnvMacroExpander) LastExpansionPanic() *ExpansionPanic

LastExpansionPanic returns the most recently recorded abort, or nil if there has been none. The returned value is a copy; the GoStack slice is shared and must not be mutated.

func (*EnvMacroExpander) LoadWorkspaceMacros added in v1.50.0

func (e *EnvMacroExpander) LoadWorkspaceMacros(preamble []*lisp.LVal) []error

LoadWorkspaceMacros replays preamble forms into this expander's environment while holding the same lock that serializes ExpandMacro, then clears the not-a-macro cache.

Prefer it to the package-level LoadWorkspaceMacros whenever the expander is reachable from another goroutine. The package-level function evaluates the preamble against the env directly, and an expansion running concurrently reads and writes that very env — Runtime.Package, the call stack, the package registry. ExpandMacro's mutex cannot help there: the loader never asked for it. mcpserver reaches exactly that pairing, because one env backs every workspace root it indexes, so replaying one root's preamble overlaps with expanding macros for a document belonging to another (issue #403).

Clearing the cache is why this is a method rather than a mutex taken at the call site: the preamble defines macros, so any "not a macro" answer recorded before it ran may now be wrong. Reset documents the same requirement; this removes the chance to forget it.

func (*EnvMacroExpander) Reset added in v1.47.0

func (e *EnvMacroExpander) Reset()

Reset clears the not-a-macro cache. Call this after loading new macros into the env if the expander is reused. Prefer the expander's own LoadWorkspaceMacros method, which loads and invalidates under one lock and so cannot leave a window where the cache and the env disagree.

Reset does NOT clear the abort count or the last recorded panic. Evidence that host code crashed during analysis outlives a cache invalidation, and a reset that wiped it would give the caller a way to lose the only record of a swallowed panic.

type ExpansionPanic added in v1.50.0

type ExpansionPanic struct {
	// Value is what recover() returned at the aborted call. It is nil in the
	// one case that is not a panic at all: runtime.Goexit unwinding through
	// ExpandMacro (a macro body that reached t.Fatal, say). The abort is
	// counted either way — see EnvMacroExpander.ExpansionPanics.
	Value any

	// GoStack is the Go stack captured inside the recover handler. That
	// handler runs before the panic unwind completes, so the stack points at
	// the panic site rather than at the handler — the same property
	// lisp.(*LEnv).eval relies on when it fills CallStack.GoStack.
	GoStack []byte

	// Macro is the head symbol of the form being expanded and Package the
	// package context it was expanded in, when they could be read. Both are
	// best-effort diagnostics: the form itself may be what was malformed.
	Macro   string
	Package string
}

ExpansionPanic describes one ExpandMacro call that did not complete.

type ExternalSymbol

type ExternalSymbol struct {
	Name      string
	Kind      SymbolKind
	Package   string
	Signature *Signature
	Source    *token.Location
	DocString string
}

ExternalSymbol represents a symbol defined in another file.

func ExtractFileDefinitions added in v1.40.0

func ExtractFileDefinitions(source []byte, filename string) []ExternalSymbol

ExtractFileDefinitions parses source and returns all top-level definitions from a single file. This is the public counterpart of extractDefinitions, suitable for incremental workspace index updates.

func FindExternalSymbol added in v1.46.2

func FindExternalSymbol(pkgMap map[string][]ExternalSymbol, pkgName, symName string) *ExternalSymbol

FindExternalSymbol looks up a symbol by name in a package-to-symbols map.

func PreferredDefinition added in v1.40.0

func PreferredDefinition(syms []ExternalSymbol) *ExternalSymbol

PreferredDefinition returns the preferred definition from a set of symbols with the same name. The winner is chosen by: lexicographically smallest Source.File, then earliest Source.Line, then earliest Source.Col. Symbols with nil Source are ranked last. Returns nil if the slice is empty. Note: sorts the input slice in-place as a side effect.

func ScanWorkspace

func ScanWorkspace(root string) ([]ExternalSymbol, error)

ScanWorkspace walks a directory tree, parsing all .lisp files and extracting exported top-level definitions. The result can be used as Config.ExtraGlobals for cross-file symbol resolution.

Files that fail to parse are silently skipped (fault tolerant).

func ScanWorkspaceDefinitions added in v1.39.0

func ScanWorkspaceDefinitions(root string) ([]ExternalSymbol, error)

ScanWorkspaceDefinitions walks a directory tree and returns all top-level definitions, including non-exported symbols. The result is intended for workspace symbol search, not cross-file resolution.

func ScanWorkspaceFull added in v1.27.0

func ScanWorkspaceFull(root string) ([]ExternalSymbol, map[string][]ExternalSymbol, error)

ScanWorkspaceFull walks a directory tree in a single pass, parsing all .lisp files and extracting both global symbols and package exports. It skips directories matched by ShouldSkipDir (hidden, underscore-prefixed, node_modules, vendor, build). Stops collecting after maxWorkspaceFiles. Parsing is done concurrently using a bounded worker pool.

Files that fail to parse are silently skipped (fault tolerant).

type FileReference added in v1.31.0

type FileReference struct {
	SymbolKey       SymbolKey
	Source          *token.Location
	File            string          // absolute path
	Enclosing       string          // name of enclosing function ("" if top-level)
	EnclosingSource *token.Location // definition site of enclosing function
	EnclosingKind   SymbolKind      // kind of enclosing function
}

FileReference represents a cross-file reference to a symbol.

func ExtractFileRefs added in v1.31.0

func ExtractFileRefs(result *Result, filePath string) []FileReference

ExtractFileRefs extracts cross-file-trackable references from an analysis result. Only references to global-scope, non-builtin symbols are included (builtins, special ops, parameters, and locals are skipped).

File and Source.File of every returned FileReference denote the same file. The workspace index pairs the two when building document edits (lsp/rename.go takes the URI from File and the range from Source), so a reference whose text lives in a different file must not be recorded under filePath.

type MacroExpander added in v1.47.0

type MacroExpander interface {
	ExpandMacro(form *lisp.LVal, pkg string) *lisp.LVal
}

MacroExpander expands a macro call form at analysis time. The form includes the macro name as Cells[0] and arguments as Cells[1:]. pkg is the current package context from the analyzer — the expander should resolve the macro name relative to this package, matching the runtime's package-scoped symbol resolution. Returns the expanded AST, or nil if expansion is not possible (macro not found, expansion error, wrong arity, etc.). Returning nil causes the analyzer to fall back to treating the call as an opaque macro invocation.

type PanicReporter added in v1.50.0

type PanicReporter interface {
	// ExpansionPanics returns the number of ExpandMacro calls that did not
	// complete normally over the lifetime of the expander.
	ExpansionPanics() uint64
}

PanicReporter is implemented by a MacroExpander that recovers Go panics internally and can report having done so.

The problem it solves is that MacroExpander's contract makes a swallowed panic INDISTINGUISHABLE from ordinary failure: an implementation that recovers can only report that recovery by returning nil, and nil is also the answer for "not a macro", "wrong arity" and "expansion errored" — the overwhelmingly common cases. A caller that wants to know whether analysis-time macro expansion crashed cannot learn it from the return value, ever.

So the signal is carried out of band. Callers that need it (tests, fuzz targets, an embedder that wants to log host-code bugs) should type-assert:

if pr, ok := cfg.MacroExpander.(analysis.PanicReporter); ok {
        if n := pr.ExpansionPanics(); n > 0 { ... }
}

ExpansionPanics must be monotonic and must never be resettable through this interface: a count that can go back down is a count that can be cleared by the very code whose bugs it is recording.

type Reference

type Reference struct {
	Symbol *Symbol
	Source *token.Location
	Node   *lisp.LVal
}

Reference records a resolved symbol usage.

type Result

type Result struct {
	RootScope  *Scope
	Symbols    []*Symbol
	References []*Reference
	Unresolved []*UnresolvedRef

	// ExtraGlobals are the external symbols that were provided via Config.
	// Stored here so downstream consumers (e.g. lint analyzers) can check
	// for cross-file duplicates without relying on scope lookups that may
	// have been overwritten by local definitions.
	ExtraGlobals []ExternalSymbol

	// WorkspaceRefs maps SymbolKey.String() to cross-file references.
	// Copied from Config. Used by lint analyzers to check whether a symbol
	// is referenced from other workspace files (e.g. unused-function check).
	WorkspaceRefs map[string][]FileReference
}

Result holds the output of semantic analysis.

func Analyze

func Analyze(exprs []*lisp.LVal, cfg *Config) *Result

Analyze performs semantic analysis on a set of parsed expressions. It builds a scope tree, resolves references, and collects unresolved symbols.

func AnalyzeFile added in v1.31.0

func AnalyzeFile(source []byte, filename string, cfg *Config) *Result

AnalyzeFile parses and performs full semantic analysis on a single file. Returns nil if the file fails to parse.

Every field of cfg is honoured, including MacroExpander: analysis through AnalyzeFile resolves the same symbols as analysis through Analyze. Callers that scan many files and do not want to pay for macro expansion should leave cfg.MacroExpander nil.

func AnalyzeProgram added in v1.52.0

func AnalyzeProgram(p lisp.Program, cfg *Config) *Result

AnalyzeProgram performs semantic analysis on a sealed lisp.Program. It is Analyze for callers that hold a Program instead of raw parser output, and it reads the sealed expressions in place — no detaching, no copying — via the internal/astraw accessor (available here because analysis lives in the elps module; embedders outside the module have no such bypass). Analysis never mutates the expressions it walks, so the read-only contract on the sealed AST is preserved.

type ScanConfig added in v1.40.0

type ScanConfig struct {
	// MaxFiles is the maximum number of .lisp files to collect.
	// 0 means use DefaultMaxWorkspaceFiles.
	MaxFiles int
	// MaxFileBytes is the maximum size in bytes for a single file.
	// Files exceeding this are skipped. 0 means use DefaultMaxFileBytes.
	MaxFileBytes int64
	// Excludes are glob patterns for files to skip during collection.
	// Patterns are matched against the full path, base name, and each
	// directory component using filepath.Match semantics.
	Excludes []string
	// IncludeDirs are directory names that override ShouldSkipDir.
	// If a directory name matches any entry, it will be walked even if
	// ShouldSkipDir would normally skip it (e.g. "_examples").
	IncludeDirs []string
}

ScanConfig controls workspace file collection limits.

type Scope

type Scope struct {
	Kind           ScopeKind
	Parent         *Scope
	Children       []*Scope
	Symbols        map[string]*Symbol
	PackageSymbols map[string]*Symbol
	PackageImports map[string]map[string]*Symbol

	Node *lisp.LVal // the AST node that introduced this scope
	// contains filtered or unexported fields
}

Scope represents a lexical scope in the source.

func NewScope

func NewScope(kind ScopeKind, parent *Scope, node *lisp.LVal) *Scope

NewScope creates a new scope of the given kind with the given parent.

func ScopeAtPosition added in v1.31.0

func ScopeAtPosition(root *Scope, line, col int) *Scope

ScopeAtPosition returns the innermost scope that contains the given 1-based ELPS line and column. It walks the scope tree depth-first.

func (*Scope) Define

func (s *Scope) Define(sym *Symbol)

Define adds a symbol to this scope.

func (*Scope) DefineImported added in v1.39.0

func (s *Scope) DefineImported(sym *Symbol, pkg string)

DefineImported adds a symbol that should be visible both by package-qualified name and as an unqualified import in the current scope.

func (*Scope) DefineQualifiedOnly added in v1.39.0

func (s *Scope) DefineQualifiedOnly(sym *Symbol)

DefineQualifiedOnly adds a package-qualified symbol without adding it to the bare-name Symbols map. The symbol is still reachable through Lookup/LookupLocal via the bareNameIndex for package-agnostic callers (e.g., minifier, lint arity checker).

func (*Scope) Lookup

func (s *Scope) Lookup(name string) *Symbol

Lookup resolves a symbol by walking the parent chain. Returns nil if the symbol is not found.

func (*Scope) LookupAllLocal added in v1.39.0

func (s *Scope) LookupAllLocal(name string) []*Symbol

LookupAllLocal returns all symbols matching a bare name in this scope, across Symbols and all PackageSymbols entries. Used by call hierarchy to find all package variants of a function name.

func (*Scope) LookupInPackage added in v1.39.0

func (s *Scope) LookupInPackage(name, pkg string) *Symbol

LookupInPackage resolves a symbol by preferring a package-qualified match in the current scope chain before falling back to a bare-name lookup. The bare-name fallback into Symbols is intentional: builtins and user-package symbols are registered with Package == "" and must be reachable when no qualified entry matches.

Unlike Lookup, LookupInPackage does not consult bareNameIndex — a symbol registered only via DefineQualifiedOnly is invisible to LookupInPackage unless the correct package is specified.

func (*Scope) LookupLocal

func (s *Scope) LookupLocal(name string) *Symbol

LookupLocal resolves a symbol only in this scope (not parents).

func (*Scope) LookupLocalInPackage added in v1.39.0

func (s *Scope) LookupLocalInPackage(name, pkg string) *Symbol

LookupLocalInPackage resolves a symbol in the current scope, preferring the package-qualified key when a package is provided.

The bare-name fallback into Symbols is restricted to the default user package. This prevents builtins (Package == "") from blocking package-local definitions like (in-package 'foo) (defun set ...). Unlike LookupInPackage, this method does not consult bareNameIndex.

func (*Scope) LookupLocalVisible added in v1.39.0

func (s *Scope) LookupLocalVisible(name, pkg string) *Symbol

LookupLocalVisible resolves a symbol as it would be seen unqualified in the current scope: first the active package, then imported/bare symbols.

type ScopeKind

type ScopeKind int

ScopeKind classifies the kind of scope.

const (
	ScopeGlobal   ScopeKind = iota // file/module level
	ScopeFunction                  // defun/defmacro body
	ScopeLambda                    // lambda body
	ScopeLet                       // let/let* body
	ScopeFlet                      // flet/labels body
	ScopeMacrolet                  // macrolet body
	ScopeDotimes                   // dotimes body
)

func (ScopeKind) String

func (k ScopeKind) String() string

type Signature

type Signature struct {
	Params []lisp.ParamInfo
}

Signature describes the parameter signature of a callable symbol.

func (*Signature) MaxArity

func (sig *Signature) MaxArity() int

MaxArity returns the maximum number of arguments accepted. Returns -1 for variadic functions (those with &rest or &key params).

func (*Signature) MinArity

func (sig *Signature) MinArity() int

MinArity returns the minimum number of arguments required.

type Symbol

type Symbol struct {
	Name       string
	Package    string
	Kind       SymbolKind
	Source     *token.Location // nil for builtins
	Node       *lisp.LVal
	Scope      *Scope
	Signature  *Signature // non-nil for callables
	DocString  string
	References int
	Exported   bool
	External   bool // true for workspace-scanned or package-imported symbols
}

Symbol represents a defined name in a scope.

func FindEnclosingFunction added in v1.31.0

func FindEnclosingFunction(root *Scope, line, col int) *Symbol

FindEnclosingFunction finds the function symbol that contains the given 1-based position by walking the scope tree.

type SymbolKey added in v1.31.0

type SymbolKey struct {
	Package string
	Name    string
	Kind    SymbolKind
}

SymbolKey identifies a symbol across files by name and kind.

func SymbolKeyFromNameKind added in v1.31.0

func SymbolKeyFromNameKind(name string, kind string) SymbolKey

SymbolKeyFromNameKind creates a SymbolKey from raw name and kind strings. The kind string should match SymbolKind.String() output.

func SymbolToKey added in v1.31.0

func SymbolToKey(sym *Symbol) SymbolKey

SymbolToKey derives a SymbolKey from an analysis Symbol.

func (SymbolKey) String added in v1.31.0

func (k SymbolKey) String() string

String returns a lookup key for use in maps.

type SymbolKind

type SymbolKind int

SymbolKind classifies a symbol definition.

const (
	SymVariable  SymbolKind = iota // set, let binding
	SymFunction                    // defun, flet/labels binding
	SymMacro                       // defmacro
	SymParameter                   // function/lambda parameter
	SymSpecialOp                   // special operator (if, cond, etc.)
	SymBuiltin                     // builtin function
	SymType                        // deftype
)

func (SymbolKind) String

func (k SymbolKind) String() string

type UnresolvedRef

type UnresolvedRef struct {
	Name   string
	Source *token.Location
	Node   *lisp.LVal
	// InsideMacroCall is true when the unresolved reference appears inside
	// a user-defined macro call body. Macros may introduce bindings at
	// expansion time that are invisible to static analysis.
	InsideMacroCall bool
}

UnresolvedRef records a symbol usage that could not be resolved.

type WorkspacePrescan added in v1.40.0

type WorkspacePrescan struct {
	// Files is the list of .lisp file paths that were scanned.
	Files []string
	// Truncated is true if the file limit was reached during collection.
	Truncated bool
	// ExportedGlobals are exported top-level definitions only.
	// Used by ScanWorkspaceAllWithConfig for backwards compatibility.
	ExportedGlobals []ExternalSymbol
	// PkgExports maps package name to exported symbols.
	PkgExports map[string][]ExternalSymbol
	// AllDefs are all definitions (exported and non-exported).
	// Typically used as Config.ExtraGlobals for cross-file resolution.
	AllDefs []ExternalSymbol
	// PkgAllSymbols maps package name to ALL symbols (exported and
	// non-exported). Used for qualified symbol resolution since ELPS
	// runtime allows pkg:sym access to any symbol in a package.
	PkgAllSymbols map[string][]ExternalSymbol
	// DefForms are DefFormSpecs derived from defmacro definitions whose
	// names start with "def". These can be injected into Config.DefForms
	// for per-file analysis.
	DefForms []DefFormSpec
	// PackageImports maps package name to the list of packages imported
	// via use-package across all workspace files. This enables per-file
	// analysis to resolve symbols from cross-file use-package declarations.
	PackageImports map[string][]string
	// Preamble contains package-management and macro-definition forms
	// (in-package, use-package, export, defmacro) from all workspace files,
	// in source order per file. Pass these to LoadWorkspaceMacros to replay
	// the package setup and register macros in a runtime environment.
	Preamble []*lisp.LVal
	// DefaultPackage is the package declared in main.lisp (if present).
	// Used as the default for bare files (no in-package) so that cross-file
	// resolution works in projects where files inherit the package from
	// load order.
	DefaultPackage string
}

WorkspacePrescan holds the results of a workspace prescan: file definitions, package exports, and DefFormSpecs extracted from defmacro definitions.

func PrescanWorkspace added in v1.40.0

func PrescanWorkspace(root string, scanCfg *ScanConfig) (*WorkspacePrescan, error)

PrescanWorkspace performs a single-pass workspace scan that collects file definitions and extracts DefFormSpecs from def*-prefixed macros. The result provides everything needed to build a Config for per-file analysis, including macro-derived definition forms.

Directories

Path Synopsis
Package perf provides call-graph-based performance analysis for ELPS source files.
Package perf provides call-graph-based performance analysis for ELPS source files.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL