analysis

package
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Index

Constants

View Source
const (
	RadShebang = "#!/usr/bin/env rad"
)

Variables

View Source
var ErrInvalidRenameName = errors.New("not a valid Rad identifier")

ErrInvalidRenameName signals the requested new name isn't a legal Rad identifier. Wrapped by errInvalidRenameName(reason, name) at the call site so the LSP client surfaces *which* rule the input violated (digit start, bad character, reserved word).

View Source
var ErrInvalidRenameTarget = errors.New("the symbol under the cursor cannot be renamed")

ErrInvalidRenameTarget signals the cursor isn't on a renameable symbol - e.g. on whitespace, on a builtin (no source decl), or on a literal. The LSP client renders this as "Cannot rename" in the rename popup. The message is generic because the cursor's "what's wrong" depends on context (whitespace vs builtin vs unresolved); callers that need precision should wrap.

View Source
var ErrRenameWouldCollide = errors.New("name is already in scope")

ErrRenameWouldCollide signals the new name is already in scope. Wrapped by errRenameCollision(name, ownerHint) so the message tells the user what they'd be colliding with - a same-scope local, a parent-scope binding, or an unloaded builtin.

Functions

func SemanticTokensLegend added in v0.11.0

func SemanticTokensLegend() lsp.SemanticTokensLegend

SemanticTokensLegend is the shared client/server vocabulary. Exposed via the initialize response so the editor knows how to decode the indices in our emitted token data.

We deliberately keep this list small. The LSP standard names run to 22 token types and 10 modifiers; emitting only the distinctions Rad's analyzer actually understands keeps the theming honest. We can grow this list as the analyzer's view gets richer (e.g. constants, enum members, type names).

Types

type Document added in v0.11.0

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

Document owns the current snapshot of one LSP document. The snapshot pointer is read lock-free; writers serialize through `mu` to ensure a coherent prev->next chain (tree-sitter parsing today is wholesale, but a future incremental-parse path would still want this invariant).

The FileID is fixed at construction; it stays stable across every version of the document so internal code can hold a FileID and always reach the latest snapshot via the State's lookup tables.

func (*Document) FileID added in v0.11.0

func (d *Document) FileID() FileID

func (*Document) Snapshot added in v0.11.0

func (d *Document) Snapshot() *DocumentVersion

Snapshot returns the current immutable version of this document. Lock-free; safe to call from any goroutine.

func (*Document) Update added in v0.11.0

func (d *Document) Update(produce func(prev *DocumentVersion) *DocumentVersion) *DocumentVersion

Update runs `produce` under the writer lock to compute the next version from the previous (nil on first open), then atomically swaps it into place. The new version arrives with refs=1 (held by us); after the store we Release the previous version, dropping Document's reference to it. Any reader that had already Acquired the old version keeps it alive via the refcount.

type DocumentVersion added in v0.11.0

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

DocumentVersion is an immutable snapshot of a single parse of a document. Multiple readers can hold the same *DocumentVersion concurrently without coordination: the data inside is guaranteed not to mutate. The next didChange produces a NEW DocumentVersion (built off the old) and atomically swaps the owning Document's current pointer.

This is the load-bearing piece of Phase 8: it lets LSP request handlers (hover, goto-def, completion, etc.) grab a snapshot once and reason about a frozen world for the duration of the request, rather than racing against the next keystroke.

Lifetime: the underlying tree-sitter tree owns C-heap memory that the Go GC can't reclaim. We refcount accordingly: the Document holds one reference; each call to State.Snapshot bumps the count and returns the snapshot to the caller, who MUST call Release() when done. When the count reaches zero, the tree is Close()d.

Why refcounting and not a finalizer: tree-sitter Nodes carry references into the C tree that the Go GC doesn't see. A finalizer can fire on a tree while another goroutine is walking nodes that point into it - the documented cgo hazard. Explicit release gives us deterministic free at the moment we know no reader is touching the tree.

func (*DocumentVersion) AST added in v0.11.0

func (v *DocumentVersion) AST() *rl.SourceFile

func (*DocumentVersion) Diagnostics added in v0.11.0

func (v *DocumentVersion) Diagnostics() []lsp.Diagnostic

func (*DocumentVersion) Encoding added in v0.11.0

func (v *DocumentVersion) Encoding() PositionEncoding

func (*DocumentVersion) FileID added in v0.11.0

func (v *DocumentVersion) FileID() FileID

func (*DocumentVersion) GetLine added in v0.11.0

func (v *DocumentVersion) GetLine(line int) string

GetLine returns the source of the line at the given index, or "" if out of range. Kept on DocumentVersion (not LineIndex) because callers usually want both the text and the index together.

func (*DocumentVersion) LastGood added in v0.11.0

func (v *DocumentVersion) LastGood() *DocumentVersion

LastGood returns the most recent version of this document whose resolved/types are non-nil, or nil if no version has ever converted cleanly. Callers that want to fall back to last-good indexes (e.g. UFCS completion ranking mid-edit) should read from this when their primary snapshot's resolved/types are nil. The returned version shares this version's lifetime - do NOT Release it independently.

func (*DocumentVersion) LineIndex added in v0.11.0

func (v *DocumentVersion) LineIndex() *LineIndex

func (*DocumentVersion) Release added in v0.11.0

func (v *DocumentVersion) Release()

Release drops one reference. When the count reaches zero the underlying tree-sitter tree is freed. Each call to State.Snapshot pairs with exactly one Release - callers typically `defer snap.Release()` right after the nil-check.

func (*DocumentVersion) Resolved added in v0.11.0

func (v *DocumentVersion) Resolved() *check.Resolved

func (*DocumentVersion) Text added in v0.11.0

func (v *DocumentVersion) Text() string

func (*DocumentVersion) Tree added in v0.11.0

func (v *DocumentVersion) Tree() *rts.RadTree

func (*DocumentVersion) Types added in v0.11.0

func (v *DocumentVersion) Types() *check.TypeInfo

func (*DocumentVersion) URI added in v0.11.0

func (v *DocumentVersion) URI() string

func (*DocumentVersion) Version added in v0.11.0

func (v *DocumentVersion) Version() int64

type FileID added in v0.11.0

type FileID uint32

FileID is an opaque identifier the analyzer uses to refer to one document. It's separate from the LSP URI on purpose:

  • URIs are strings, expensive to hash and pass around in deep traversals (symbol lookups, cross-file references, etc.)
  • URIs are externally controlled - any uniqueness invariant lives outside our type system, so the compiler can't help us when we accidentally hand the wrong string to a function expecting a "real" document handle
  • URIs survive forever in client memory but a server-side close should be able to recycle/invalidate references; an opaque int ID gives us the seam to do that without changing the wire

FileIDs are minted monotonically per State and never re-used within a session. Comparing FileIDs is O(1) and they fit in a register. The State maintains both URI -> FileID and FileID -> *Document maps so we can translate at the wire boundary and keep everything downstream typed.

const InvalidFileID FileID = 0

InvalidFileID is the sentinel returned when a lookup misses. Real FileIDs are always >= 1 - we start the counter at 1 so zero stays meaningful as "no file" / "not assigned yet."

type LineIndex added in v0.11.0

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

LineIndex translates positions on a fixed text snapshot. Tree-sitter reports positions in utf-8 byte columns; LSP clients want them in the encoding negotiated at initialize. LineIndex is the single conversion point so the rest of the analyzer can stay in byte units (matching tree-sitter) without ever caring about utf-16 surrogate pairs.

Construction is O(n) over the text; per-position queries are O(byteCol) on the affected line because we have to walk the bytes to count code units. That's fine at our scale (Rad files are small and queries are per-diagnostic, not per-keystroke).

func NewLineIndex added in v0.11.0

func NewLineIndex(text string) *LineIndex

NewLineIndex scans text once to record where each line begins.

func (*LineIndex) ByteColumnTo added in v0.11.0

func (l *LineIndex) ByteColumnTo(line, byteCol int, enc PositionEncoding) int

ByteColumnTo converts a utf-8 byte column on `line` into a column in the target encoding. Out-of-range or past-end-of-line inputs are clamped to the line's length in the target encoding so callers can't produce LSP positions that violate the spec.

func (*LineIndex) ColumnToByte added in v0.11.0

func (l *LineIndex) ColumnToByte(line, col int, enc PositionEncoding) int

ColumnToByte is the inverse of ByteColumnTo: given a column reported in the client's encoding, return the utf-8 byte column tree-sitter uses internally. Out-of-range inputs clamp to the line's byte length so we never index past the end of a line.

func (*LineIndex) LineCount added in v0.11.0

func (l *LineIndex) LineCount() int

LineCount returns the number of lines in the indexed text. A document with no trailing newline still counts the partial final line.

type PositionEncoding added in v0.11.0

type PositionEncoding string

PositionEncoding identifies how LSP positions are measured. LSP 3.17 lets a client/server negotiate one of these at the initialize handshake; utf-16 is the default when no negotiation occurs.

const (
	EncodingUTF8  PositionEncoding = "utf-8"
	EncodingUTF16 PositionEncoding = "utf-16"
	EncodingUTF32 PositionEncoding = "utf-32"
)

func NegotiatePositionEncoding added in v0.11.0

func NegotiatePositionEncoding(clientOffered []PositionEncoding) PositionEncoding

NegotiatePositionEncoding picks the server-preferred encoding from the list a client advertised in its initialize params. Per LSP 3.17, if the client offers nothing we must use utf-16. If the client offers something but we share nothing in common we also fall back to utf-16 (the spec's universal mandatory).

type SemanticTokenType added in v0.11.0

type SemanticTokenType int

SemanticTokenType indexes into the legend below; the LSP wire format encodes token-type as the index, so the legend must stay in sync with these constants. Adding new types only appends to the end of the legend - never insert in the middle.

const (
	TokenTypeFunction SemanticTokenType = iota
	TokenTypeParameter
	TokenTypeVariable
	TokenTypeType
)

type State

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

State is the per-server registry of open documents. It owns the shared tree-sitter parser and the negotiated position encoding. The docs map is guarded by a top-level mutex for membership changes (open/close); per-document write coordination lives on Document itself, and reads of any document's content are lock-free via its snapshot pointer.

Documents are identified two ways: by URI (the LSP wire concern) and by FileID (the internal handle). The URI is stable from the client's perspective, but our internal code prefers FileID since it's cheaper to compare and clearly distinguishes "an opened document" from "an arbitrary string."

func NewState

func NewState() *State

func (*State) AddDoc

func (s *State) AddDoc(uri, text string)

AddDoc opens a document for the first time and produces its initial version. If the URI is already open the existing entry is replaced - LSP shouldn't send didOpen twice for the same URI, but be defensive. A fresh FileID is allocated per AddDoc; reopening a closed URI gets a new id (we don't reuse ids within a session).

func (*State) CodeAction

func (s *State) CodeAction(snap *DocumentVersion, r lsp.Range) (result []lsp.CodeAction, err error)

CodeAction returns the available code actions for the given range against a fixed document snapshot. Same snapshot discipline as Complete: the caller's responsibility to grab a fresh one.

Two sources of actions today:

  1. The always-available shebang insertion, when the file is missing one - this is whole-document, not range-scoped.
  2. Diagnostic quick fixes - per-diagnostic actions for issues whose range overlaps the request range. Structured fixes (machine-applicable edits) come out as quickfix-kinded CodeActions; suggestion-only diagnostics produce refactor- kinded info actions with no edit.

func (*State) Complete

func (s *State) Complete(snap *DocumentVersion, pos lsp.Pos) (result []lsp.CompletionItem, err error)

Complete runs the completion logic against a fixed document snapshot. Callers (server handlers) grab the snapshot themselves and pass it explicitly: the snapshot pin-points which version of the document this completion is meant for, even if the user types more characters before the response is sent.

The result is the union of the shebang stub (only meaningful on line 0) and a scope-aware identifier list built from the AST and the resolved/type indexes. The shebang stays first so users who started a new file still get the "Add #!" suggestion at the top of the list.

func (*State) Definition added in v0.11.0

func (s *State) Definition(snap *DocumentVersion, pos lsp.Pos) (*lsp.Location, error)

Definition answers textDocument/definition: given a cursor on an identifier, return the Location of its declaration. Returns nil when there's nothing to jump to: the cursor isn't on an identifier, the name didn't resolve, or the symbol is a builtin (no source decl span to point at).

Single-file today, single Location: Rad has no imports, so we never need to return multiple Locations. The LSP spec allows Location | Location[] | LocationLink[] | null; staying on Location keeps the wire shape simple and trivially forward-compatible.

Builtins return nil rather than e.g. a synthetic stdlib URL. "Go to definition" on `print` doing nothing is much better than dumping the user into a generated file they can't edit; hover already shows them the signature, which is the actually-useful information.

func (*State) DocumentSymbols added in v0.11.0

func (s *State) DocumentSymbols(snap *DocumentVersion) ([]lsp.DocumentSymbol, error)

DocumentSymbols answers textDocument/documentSymbol with the outline of a file: top-level functions, top-level variable declarations, the args block (with its declared args nested), and each cmd block (with its declared args nested). Returns an empty slice (NOT nil) when the file has no symbols - the LSP wire expects a JSON array.

What counts as "top-level":

  • FnDef in SourceFile.Stmts -> Function.
  • First-assignment of an identifier in SourceFile.Stmts -> Variable. Only the first assignment per name is emitted; later re-assigns of the same name are not new outline entries.
  • SourceFile.Args - the script's args: block - rendered as a Namespace "args" with each ArgDecl as a Variable child.
  • Each SourceFile.Cmds entry - rendered as a Module child group named after the command, with its ArgDecls nested.

We deliberately do NOT recurse into function bodies. Editors that want a deeper outline (per-statement, per-loop) can render local vars on demand via Hover/GotoDef; the outline is meant to be glanceable, not exhaustive.

func (*State) Encoding added in v0.11.0

func (s *State) Encoding() PositionEncoding

Encoding returns the LSP position encoding currently in use.

func (*State) FileIDFor added in v0.11.0

func (s *State) FileIDFor(uri string) FileID

FileIDFor returns the FileID assigned to the named URI, or InvalidFileID if the document isn't open. Internal callers that already have a *Document should just call doc.FileID() directly.

func (*State) Hover added in v0.11.0

func (s *State) Hover(snap *DocumentVersion, pos lsp.Pos) (*lsp.Hover, error)

Hover answers textDocument/hover against a fixed document snapshot. We find the smallest Identifier whose span covers the cursor, look up its Symbol in the resolved view, and format the symbol's type (declared annotation if any, else what the type checker inferred). Returns nil when the cursor isn't on a hoverable thing - the LSP spec lets us return null and the client will simply show nothing.

Why identifier-only for v1: the value gradient is steep. Hovering a name is what users want 90% of the time; expression-result hover (e.g. on a binop or call) is nice-to-have and lights up trivially once we route through TypeInfo.ExprTypes here. We'll grow it as users notice it's missing.

func (*State) References added in v0.11.0

func (s *State) References(snap *DocumentVersion, pos lsp.Pos, includeDecl bool) ([]lsp.Location, error)

References answers textDocument/references: given a cursor on an identifier, return every Location where that symbol is used in the document. The LSP context flag IncludeDeclaration toggles whether the declaration site itself is included.

We compute on-demand from resolved.Uses rather than maintaining a pre-built reverse index. References is a click-driven feature (not background-streaming like diagnostics), so the cost of walking the Uses map once per request is fine - and it avoids growing every snapshot with a map most snapshots will never use. If the access pattern changes, the snapshot can grow a cache later without breaking callers.

Returns an empty slice (not nil) when there are no references - the LSP wire expects an array.

func (*State) Rename added in v0.11.0

func (s *State) Rename(snap *DocumentVersion, pos lsp.Pos, newName string) (*lsp.WorkspaceEdit, error)

Rename answers textDocument/rename: produce a WorkspaceEdit covering every site that needs to change so the symbol under the cursor becomes `newName`. Single-file scope - Rad has no imports today, so cross-file renames don't apply.

Returns:

  • ErrInvalidRenameTarget when the cursor isn't on a renameable symbol (whitespace, builtin, unresolved name).
  • ErrInvalidRenameName when newName isn't a legal identifier.
  • ErrRenameWouldCollide when the new name already binds something in the target symbol's scope.
  • A WorkspaceEdit with one TextEdit per site otherwise.

func (*State) SemanticTokens added in v0.11.0

func (s *State) SemanticTokens(snap *DocumentVersion) (*lsp.SemanticTokens, error)

SemanticTokens answers textDocument/semanticTokens/full: walk the resolved view and tag each identifier with its kind (function vs parameter vs variable). The editor uses this for finer-grained syntax highlighting than tree-sitter's CST-level coloring alone provides - e.g. distinguishing a builtin call from a local-variable read.

Returns an empty data slice (not nil) when there's nothing to emit. The LSP spec lets us return null, but always-array makes the client's decode path simpler.

Encoding: per LSP 3.17, tokens are sorted by position then delta-encoded as quintuples of uint. We do the sort and encode in one pass at the end so the AST walk can stay shape-blind.

func (*State) SetEncoding added in v0.11.0

func (s *State) SetEncoding(enc PositionEncoding)

SetEncoding installs the encoding negotiated at initialize. Must be called before any didOpen so the first document's diagnostics use the right encoding. Initialize happens exactly once per session, so we don't bother guarding against a second call.

func (*State) Snapshot added in v0.11.0

func (s *State) Snapshot(uri string) *DocumentVersion

Snapshot returns the current version of the named document with its refcount incremented. The caller MUST call Release() when done (typically `defer snap.Release()` right after the nil-check). Returns nil if the document isn't open.

The acquire-after-load loop handles a small race window: between loading the atomic pointer and bumping the refcount, the writer could have Released the version we observed. In that case acquire returns false and we retry; the Document.snapshot pointer has already been updated to a newer version by the time we get here.

func (*State) SnapshotByID added in v0.11.0

func (s *State) SnapshotByID(id FileID) *DocumentVersion

SnapshotByID returns the current snapshot for a FileID, or nil if the id isn't known. Same Acquire/Release contract as Snapshot: caller MUST Release() what they get back.

func (*State) UpdateDoc

func (s *State) UpdateDoc(uri string, changes []lsp.TextDocumentContentChangeEvent)

UpdateDoc applies a sequence of content changes and produces a fresh version for each. Today the protocol delivers full-document text (TextDocumentSync = 1), so each change spawns a wholesale reparse; the per-change loop is preserved so a future move to incremental edits is a one-spot change.

Jump to

Keyboard shortcuts

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