astutil

package
v1.74.0 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

Documentation

Overview

Package astutil provides shared AST walking utilities for ELPS lisp values.

These helpers are used by both the lint and analysis packages for traversing parsed ELPS expressions.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ArgCount

func ArgCount(sexpr *lisp.LVal) int

ArgCount returns the number of arguments in an s-expression (excluding the head). A nil sexpr yields 0, so it is safe to call on the nil parent Walk passes for top-level expressions.

func CollectFormals

func CollectFormals(formals *lisp.LVal, defs map[string]bool)

CollectFormals extracts symbol names from a formals list, skipping &rest, &optional, and &key markers.

func ExpandAll added in v1.73.0

func ExpandAll(form *lisp.LVal, exp MacroExpander, pkg string, visit lisp.CodeVisitor) *lisp.LVal

ExpandAll returns form with every macro call exp can expand replaced by its full expansion, in code position only, calling visit (which may be nil) for every event of the expanded code. pkg is the package form is read in, passed to exp.

A call whose head is lexically bound in form (a local function, a let variable, a macrolet name) is not expanded. Calls to macrolet macros are reported as opaque forms: without an environment they cannot be expanded. A macro exp cannot expand is left as a call, and so is one whose expansion does not terminate; a form nested too deeply is reported as opaque. The walk always covers the whole form.

form is never modified. Nodes the expansion did not touch are returned as the same values, and every rebuilt list keeps the source location of the list it replaces, so positions still point into the original file; nodes a macro synthesized carry whatever location the expander gave them. lisp.LEnv.MacroExpandAll is the variant that resolves heads in a live environment and expands local macros too.

func ExportNames added in v1.62.0

func ExportNames(args []*lisp.LVal) []string

ExportNames extracts candidate export names from argument syntax: strings, reader-quoted symbols and nested lists, and (quote x) / (lisp:quote x) calls interpreted as standard quoting. Other expressions contribute no names, so the result may be incomplete. It does not resolve operators or expand macros.

Preservation callers may always use these candidates as names worth keeping. Analysis callers marking symbols Exported or populating cross-file export metadata may use them only as a syntactic approximation under the assumption of standard, unshadowed operators and directly evaluated export forms, not as proof of runtime exports. A caller requiring proof must independently establish that context, the export/quote operator semantics, and that every argument is known. Unqualified quote may be shadowed; lisp:quote resolves in its package but an embedder may register a nonstandard lisp package before sealing it. Macro bodies and quasiquote templates do not establish directly evaluated exports, even when their argument syntax looks literal.

func HeadSymbol

func HeadSymbol(sexpr *lisp.LVal) string

HeadSymbol returns the symbol name at the head of an s-expression, or "". A nil sexpr yields "", so it is safe to call on the nil parent Walk passes for top-level expressions.

func PackageForms added in v1.62.0

func PackageForms(exprs []*lisp.LVal) []*lisp.LVal

PackageForms returns top-level forms plus nested defun, defmacro, set and export forms in source order. These forms affect package bindings even when enclosed by lexical scopes. Quoted data and quasiquote templates are omitted. The returned nodes belong to the input tree; no syntax is moved or copied.

func PackageNameArg added in v1.39.0

func PackageNameArg(arg *lisp.LVal) string

PackageNameArg extracts a package name from a use-package or in-package argument. Handles quoted symbols ('testing), bare symbols (testing), and strings ("testing").

func SourceLoc added in v1.52.0

func SourceLoc(v *lisp.LVal) *token.Location

SourceLoc returns v's source location as a pointer, or nil when v is nil or carries no location. The pointer refers to a private copy — mutating it never affects v or any other LVal (lisp.LVal exposes locations by value only; see issue #362).

func SourceOf

func SourceOf(v *lisp.LVal) *lisp.LVal

SourceOf returns the best source location for a node. Prefers the node's own source, falls back to first child's source. Returns nil for a nil node, so it is safe to call on the nil parent Walk passes for top-level expressions.

func SymbolLoc added in v1.61.0

func SymbolLoc(v *lisp.LVal) *token.Location

SymbolLoc returns the location of the NAME a node is written with, which is not always the node's own span.

Two shapes carry a name inside a wider form. The first is a quoted symbol:

rdparser gives the whole 'x form a single node: lisp.Quote copies the symbol and sets its quoted flag rather than wrapping it, so no node stands for the quote, and applyPrefixLocation then moves the surviving node's start back onto the ' so that the form reports the position a reader would point at. That start is right for the FORM and wrong for the NAME, and a consumer that wants to point at, highlight, or REPLACE the identifier needs the latter: textDocument/rename built its edit ranges from the form span and so replaced the quote along with the name, turning (set 'x 1) into (set new 1) -- a different program, applied to the user's file unread (elps#577).

The end of the span is the name's end already (ParseQuote inherits it from the operand), so the name is recovered by measuring len(v.Str) BACK from it rather than by counting ' characters forward. That is exact whatever sits in the gap -- "' x", a newline, a preserved comment -- and it is a no-op on an unquoted symbol, whose span is its name.

It never widens a span and never moves one it cannot account for: a node whose recorded end is missing, or whose name does not fit inside its own span, is returned untouched.

This is NOT elps#463. That was a WIDTH in the wrong unit (token.TokenEnd counted EndCol one per rune onto a byte-valued Col); this is a start that is one reader-prefix too far left, and it is wrong by the same byte for "'x" as for "'é".

The second shape is a STRING LITERAL used as a name, which some def-like forms take: a qualified call ending in ":deftype" (e.g. libschema's s:deftype, before elps#736 removed it for writing into the caller's package) whose first argument is a string binds a global named by that string, and the node analysis records for it is the literal. Its span covers the quotes, so a rename built from it replaced them too and produced (x:deftype NEW ...) -- a bare symbol where the form requires a string. Here the name is the literal's INTERIOR, so both ends move in by one delimiter.

A string is only handled when its raw span is exactly the decoded value plus two delimiter bytes on one line. Anything else -- an escape, a raw-string form, a line break inside -- means the interior is not recoverable by arithmetic on the length, and the span is returned untouched rather than guessed at.

func UserDefined

func UserDefined(exprs []*lisp.LVal) map[string]bool

UserDefined returns the set of names defined or bound in the source that shadow builtins. This includes:

  • Function/macro names from defun/defmacro
  • Parameter names from defun/defmacro/lambda formals lists
  • Names rebound by (set 'name ...) or (set! 'name ...)

The result is file-global (not scope-aware), which is conservative: it may suppress a valid finding but will never produce a false positive.

func Walk

func Walk(exprs []*lisp.LVal, fn func(node *lisp.LVal, parent *lisp.LVal, depth int))

Walk calls fn for every node in the tree, depth-first. parent is nil for top-level expressions.

func WalkSExprs

func WalkSExprs(exprs []*lisp.LVal, fn func(sexpr *lisp.LVal, depth int))

WalkSExprs calls fn for every unquoted s-expression (potential function call or special form) in the tree.

Types

type CallSite added in v1.73.0

type CallSite struct {
	// Form is the call.
	Form *lisp.LVal
	// Name is the head as written.
	Name string
	// Enclosing lists the special forms and function bodies around the
	// call, outermost first.  Ordinary function calls are not listed.
	Enclosing []Enclosure
}

CallSite is a call FindCalls found.

func FindCallSites added in v1.73.0

func FindCallSites(form *lisp.LVal, names ...string) (sites []CallSite, complete bool)

FindCallSites is FindCalls reporting whether the result is complete. complete is false when the query ran out of work or site budget; a caller checking that no call occurs in some context must then assume one does.

func FindCalls added in v1.73.0

func FindCalls(form *lisp.LVal, names ...string) []CallSite

FindCalls returns every call in form, in code position, whose head is one of names (bare, or qualified by the lisp package) and is not lexically bound where it occurs, with the special forms and function bodies around it. A macro whose body must not contain some call (inside a lambda, a handler, a quasiquote) can reject it by inspecting Enclosing. Calls inside quoted data are not calls and are not returned. Pass expanded code.

Code built by macros can share structure. A shared call is reported once per place it occurs in code, each with its own enclosures and lexical context. The work and the number of sites are bounded; FindCalls drops what does not fit. A check that must not miss a call uses FindCallSites, which says when the result is incomplete.

type Enclosure added in v1.73.0

type Enclosure struct {
	// Form is the special form, or for a function body the form (or
	// flet/labels/macrolet binding) whose body it is.
	Form *lisp.LVal
	// Op is the special form's name: "let", "handler-bind", "quasiquote",
	// "lambda", ...
	Op string
	// Function marks a function body -- code that may run later, from
	// wherever the function is called -- rather than the form itself.  A
	// call inside a lambda has an Enclosure for the lambda form and one,
	// with Function set, for its body.
	Function bool
}

Enclosure is one construct around a call FindCalls found.

type MacroExpander added in v1.73.0

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

MacroExpander expands one macro call. It has the method set of analysis.MacroExpander, so an *analysis.EnvMacroExpander can be passed directly. ExpandMacro returns nil when form is not a macro call or cannot be expanded.

type Role added in v1.73.0

type Role uint8

Role is what a node is in the code around it.

const (

	// RoleData: unevaluated data -- a quoted value or anything inside one,
	// a quasiquote template outside its holes, a condition type.
	RoleData Role
	// RoleSyntax: structure a special form reads but does not evaluate: a
	// binding list, one [name init] binding, a lambda list, a cond clause,
	// a dotimes control list.  A binding written with brackets reads as a
	// quoted list; its role is still syntax, not data.
	RoleSyntax
)

The roles ClassifyNodes assigns. Only RoleData and RoleSyntax have a consumer outside this package; the others are kept unexported.

type Roles added in v1.73.0

type Roles struct {
	// contains filtered or unexported fields
}

Roles is the result of ClassifyNodes.

func ClassifyNodes added in v1.73.0

func ClassifyNodes(form *lisp.LVal) *Roles

ClassifyNodes walks form as code with lisp.CodeWalker (pass expanded code) and assigns every node in it a Role, so a tool can tell a binding position or structural list from quoted data, which the reader represents the same way. Each node is visited once, so data that shares structure, or is cyclic, costs one visit per node.

func (*Roles) Role added in v1.73.0

func (r *Roles) Role(node *lisp.LVal) Role

Role returns the role of node, which must be a node of the classified form (compared by identity).

Jump to

Keyboard shortcuts

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