check

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: 8 Imported by: 1

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DumpForSnapshot added in v0.11.0

func DumpForSnapshot(file *rl.SourceFile, info *TypeInfo, resolved *Resolved, diagnostics []Diagnostic) string

DumpForSnapshot produces a deterministic textual summary of the checker's output suitable for snapshot testing. It records, in source order:

  • Each Identifier reference, its source position, and the type the checker synthesized for it at that point. This is the load-bearing signal for narrowing: the same identifier name can have different types at different positions, and the dump captures that explicitly so a regression in narrowing surfaces immediately.

  • Each declared Symbol with its final SymbolTypes entry. This covers symbols that may not have a use site (cleanly-declared locals, params).

  • Each diagnostic (severity, code, message). Sorted by source position so the order is stable across runs.

The format is plain text - no JSON - because the goal is for a human to diff snapshot changes and confirm they reflect intentional behavior changes. Each section is prefixed with a header so a future reader can read top-to-bottom without context.

func GenerateErrorMessage added in v0.6.26

func GenerateErrorMessage(node *ts.Node, src string) (msg string, code rl.Error, suggestion *string)

GenerateErrorMessage creates a specific error message for an invalid node. Returns the message, error code, and optional suggestion.

func UnionJoinForTest added in v0.11.0

func UnionJoinForTest(types []rl.TypingT) rl.TypingT

UnionJoinForTest exposes unionTypesForJoin for testing. Tests in the check_test package can'\”t reach the unexported helper; this thin wrapper keeps the test interface explicit (not "everything in the package is exported"). Not for production use.

Types

type BindIssue added in v0.11.0

type BindIssue struct {
	Span       rl.Span
	Severity   IssueSeverity
	Code       rl.Error
	Message    string
	Suggestion string
}

BindIssue is a problem detected during name resolution. It carries just the bare information - span, severity, message, error code - so that the binder doesn't depend on the source text or the wider Diagnostic type. Callers turn each issue into their preferred diagnostic shape (e.g. check.Diagnostic with src-derived range info).

Suggestion is an optional "= help: ..." line. When non-empty, the rendered diagnostic shows it after the source-context block, the same way the runtime renders its `emitErrorWithHint` calls. Used to give the static check parity with runtime diagnostics that already provide actionable follow-up (the v0.9 `+` migration hint is the canonical example).

type Diagnostic

type Diagnostic struct {
	OriginalSrc string // Complete original src
	Range       Range
	RangedSrc   string // Src for just the Range
	LineSrc     string // Src for the line at the start of Range
	Severity    Severity
	Message     string
	Code        *rl.Error
	Suggestion  *string // Optional suggestion for fixing the error (rendered as "Try: ...")
}

Diagnostic represents a static analysis diagnostic. This is the canonical diagnostic type for the check system; both lsp.Diagnostic and core.Diagnostic are derived from it. Severity levels align with the LSP protocol (4 levels); core collapses Hint/Info into Note for its 3-level Rust-style CLI output.

func NewDiagnosticError

func NewDiagnosticError(node *ts.Node, originalSrc string, msg string, code rl.Error) Diagnostic

func NewDiagnosticErrorFromSpan added in v0.9.0

func NewDiagnosticErrorFromSpan(span rl.Span, originalSrc string, msg string, code rl.Error) Diagnostic

func NewDiagnosticErrorWithSuggestion added in v0.6.26

func NewDiagnosticErrorWithSuggestion(node *ts.Node, originalSrc string, msg string, code rl.Error, suggestion *string) Diagnostic

func NewDiagnosticFromNode

func NewDiagnosticFromNode(
	node *ts.Node,
	originalSrc string,
	severity Severity,
	msg string,
	code *rl.Error,
) Diagnostic

func NewDiagnosticFromSpan added in v0.9.0

func NewDiagnosticFromSpan(
	span rl.Span,
	originalSrc string,
	severity Severity,
	msg string,
	code *rl.Error,
) Diagnostic

NewDiagnosticFromSpan creates a Diagnostic from an AST span instead of a CST node.

func NewDiagnosticHint added in v0.6.15

func NewDiagnosticHint(node *ts.Node, originalSrc string, msg string, code rl.Error) Diagnostic

func NewDiagnosticHintFromSpan added in v0.9.0

func NewDiagnosticHintFromSpan(span rl.Span, originalSrc string, msg string, code rl.Error) Diagnostic

func NewDiagnosticWarn added in v0.6.17

func NewDiagnosticWarn(node *ts.Node, originalSrc string, msg string, code rl.Error) Diagnostic

func NewDiagnosticWarnFromSpan added in v0.9.0

func NewDiagnosticWarnFromSpan(span rl.Span, originalSrc string, msg string, code rl.Error) Diagnostic

type Frame added in v0.11.0

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

Frame is an immutable overlay of flow-sensitive narrowings on top of the base symbol types tracked in TypeInfo.SymbolTypes. Frames chain to a parent so a child frame can shadow or extend without copying.

Used by the narrowing pass: inside `if x != null:`, the frame in effect for the then-body binds x to its non-null component while leaving the surrounding frame untouched.

Frames are immutable; "narrow this symbol" produces a new frame via With. That keeps branch-join trivial - each branch ends with its own frame, and the join walks both to produce a unioned overlay.

func NewFrame added in v0.11.0

func NewFrame() *Frame

NewFrame returns a root frame with no narrowings. Used at the entry of every scope (file body, function body, lambda body) where no outer narrowings are in effect.

func (*Frame) Lookup added in v0.11.0

func (f *Frame) Lookup(sym *Symbol) (rl.TypingT, bool)

Lookup walks the frame chain looking for a narrowing on sym. Returns the narrowed type and true if found in this frame or any ancestor. Callers fall back to TypeInfo.SymbolTypes when no narrowing is recorded.

func (*Frame) Parent added in v0.11.0

func (f *Frame) Parent() *Frame

Parent returns the parent frame, or nil at the root. Exposed so the flow-sensitive logic can walk back to a known point (e.g. unwind a branch scope on early-exit).

func (*Frame) With added in v0.11.0

func (f *Frame) With(sym *Symbol, t rl.TypingT) *Frame

With returns a new frame that narrows sym to t. The receiver is unchanged. O(1) - no map copy.

func (*Frame) WithMany added in v0.11.0

func (f *Frame) WithMany(m map[*Symbol]rl.TypingT) *Frame

WithMany returns a new frame applying every entry in m as a narrowing. Useful when a single predicate refines multiple symbols at once (e.g. a compound condition or a switch peeling several variants in one go). Returns the receiver if m is empty.

type IssueSeverity added in v0.11.0

type IssueSeverity int

IssueSeverity is the binder's severity classification. Kept here rather than on Diagnostic so resolve.go stays src-free.

Severity policy (followed across rts/check and surfaced to the CLI / LSP):

  • IssueError - the static analyzer can prove the code is wrong. Gates runtime: `rad check` exits non-zero, `rad run` refuses to execute, the LSP marks the file as having errors. If you're tempted to add an IssueError that "might be wrong in edge cases," that's not Error - it's Warning.

  • IssueWarning - genuinely uncertain. The analyzer suspects a problem but can't prove it, or the issue is severity- dependent on context the analyzer doesn't have. Does NOT gate runtime. Today's binder doesn't emit any Warnings; reserved for future drift cases (e.g. "this looks like a dead branch but we can't be sure"). The check.Diagnostic layer has its own Warning emitter for rad-options-no-effect style stylistic hints.

  • IssueHint - style nudge, no behavior implication. "You could write this more idiomatically." Never gates anything.

When promoting a Hint to Error: update the call site, update any snapshots, and migrate downstream runtime tests that used the now-static error as a runtime trigger.

const (
	IssueError IssueSeverity = iota
	IssueWarning
	IssueHint
)

type Pos

type Pos struct {
	Line      int `json:"line"`      // Zero-indexed
	Character int `json:"character"` // Zero-indexed
}

type RadChecker

type RadChecker interface {
	UpdateSrc(src string)
	Update(tree *rts.RadTree, src string, ast *rl.SourceFile)
	SetStrict(strict bool)
	Check() (Result, error)
}

func NewChecker

func NewChecker() (RadChecker, error)

func NewCheckerWithTree

func NewCheckerWithTree(tree *rts.RadTree, parser *rts.RadParser, src string, ast *rl.SourceFile) RadChecker

type RadCheckerImpl

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

func (*RadCheckerImpl) Check

func (c *RadCheckerImpl) Check() (Result, error)

func (*RadCheckerImpl) SetStrict added in v0.11.0

func (c *RadCheckerImpl) SetStrict(strict bool)

func (*RadCheckerImpl) Update added in v0.9.0

func (c *RadCheckerImpl) Update(tree *rts.RadTree, src string, ast *rl.SourceFile)

func (*RadCheckerImpl) UpdateSrc

func (c *RadCheckerImpl) UpdateSrc(src string)

type Range

type Range struct {
	Start Pos `json:"start"`
	End   Pos `json:"end"`
}

type Refinement added in v0.11.0

type Refinement struct {
	WhenTrue  map[*Symbol]rl.TypingT
	WhenFalse map[*Symbol]rl.TypingT
}

Refinement is the truthy/falsy decomposition of a predicate: the narrowings that hold when the condition evaluates truthy live in WhenTrue, the ones that hold on the falsy branch live in WhenFalse. Either map may be empty/nil if the predicate doesn't refine that side.

Built by interpretCondition when the type checker walks an if/while/ternary condition. Consumers layer the chosen side onto the current frame via Frame.WithMany.

func EmptyRefinement added in v0.11.0

func EmptyRefinement() Refinement

EmptyRefinement is the no-op refinement used for conditions the narrower doesn't know how to interpret. Returned in preference to a zero-value Refinement so call sites can rely on the maps being non-nil if needed without re-checking.

func (Refinement) Negate added in v0.11.0

func (r Refinement) Negate() Refinement

Negate swaps WhenTrue and WhenFalse. Used for `not <cond>` where the truthy / falsy branches of the inner predicate are inverted relative to the surrounding flow.

type Resolved added in v0.11.0

type Resolved struct {
	// Builtin is the ambient scope holding lazily-synthesized symbols
	// for runtime-provided names.
	Builtin *Scope
	// File is the top-level script scope.
	File *Scope
	// Uses maps an identifier-reference node to the Symbol it resolves to.
	// Identifiers that fail to resolve are absent from this map.
	Uses map[rl.Node]*Symbol
	// Decls maps a declaring node to the Symbol it introduced. Useful for
	// goto-def (jump to decl span) and for hover (show declared type).
	Decls map[rl.Node]*Symbol
	// ForLoopVars holds the per-variable Symbol list for each `for` loop,
	// in source order. The header `for k, v in xs:` produces two symbols
	// for the same ForLoop AST node; Decls can only key one of them, so
	// callers that want both (the type checker, for map iteration k/v
	// binding) read from here. ListComp uses the same Vars[]string shape
	// but is single-var in practice today, so we don't track it.
	ForLoopVars map[*rl.ForLoop][]*Symbol
	// ParamSymbols maps a function-like owner (FnDef or Lambda) to its
	// parameter Symbols in source order. SymParam bindings live in the
	// function's body scope, so a name-only lookup needs the scope to
	// be in hand; this index lets LSP click-at-decl-site features
	// reach the symbol without re-walking scope chains.
	ParamSymbols map[rl.Node][]*Symbol
	// Issues are problems the binder detected during resolution
	// (undefined references, duplicate parameters, etc.). Callers
	// convert these to whatever diagnostic shape they need; the binder
	// stays src-free.
	Issues []BindIssue
}

Resolved is the output of name resolution: a scope tree plus indexes from AST nodes to the symbols they refer to or declare.

All maps key on AST node pointer identity, so a Resolved is safe to pass to readers that hold the same AST.

func Resolve added in v0.11.0

func Resolve(file *rl.SourceFile) *Resolved

Resolve performs name resolution on a parsed script and returns a Resolved view: a scope tree plus per-node maps from uses to symbols and from declarations to symbols.

The result is a pure value over the input AST. It holds no references to source text and never mutates the AST, so callers can share it freely (the LSP relies on this for snapshot-based reads).

This is the *binder* phase in the Pyright sense: eager, single-pass, no type information. The type checker (Phase 2) consumes Resolved and populates Declared/Inferred on each Symbol.

NOTE: Phase 1a establishes the data structures and the file-level binding pass (hoisted functions, args block, top-level assignments, identifier resolution). Per-construct scoping for function bodies, lambdas, loops, switch, and defer lands in Phase 1b.

type Result

type Result struct {
	Diagnostics []Diagnostic
	Resolved    *Resolved
	Types       *TypeInfo
}

Result is the output of a check pass. Diagnostics is the user-visible product; Resolved and Types are the supporting indexes the LSP layer leans on for hover, goto-def, find-refs, etc. They're nil when the source failed to convert to an AST (so the checker only ran the CST-based passes). Callers that only want diagnostics can ignore them.

type Scope added in v0.11.0

type Scope struct {
	Parent  *Scope
	Kind    ScopeKind
	Owner   rl.Node // node that introduced the scope; nil for file/builtin
	Symbols map[string]*Symbol
}

Scope is a lexical name -> Symbol table chained to its parent. Lookup walks the parent chain; declaration is local.

func (*Scope) Lookup added in v0.11.0

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

Lookup walks this scope and its parents for a symbol named `name`. Returns nil if the name is not in scope.

type ScopeKind added in v0.11.0

type ScopeKind int

ScopeKind tracks why a scope exists.

Rad opens new scopes only at function-like boundaries - named functions, lambdas, and the implicit top-level file scope. Loops, switch cases, defer bodies, list comprehensions, and cmd blocks do NOT open a scope; they share the enclosing env (matching the runtime's runBlock behavior, where loop variables and body-locals persist after the construct ends).

const (
	ScopeBuiltin  ScopeKind = iota // ambient runtime names
	ScopeFile                      // script body
	ScopeFunction                  // named function body
	ScopeLambda                    // anonymous function body
)

type Severity

type Severity int
const (
	Hint Severity = iota
	Warning
	Info
	Error
)

func (Severity) String

func (s Severity) String() string

type Symbol added in v0.11.0

type Symbol struct {
	Name     string
	Kind     SymbolKind
	DeclSpan rl.Span // location of the declaration in source; zero for builtins
	DefNode  rl.Node // the AST node that declared the symbol; nil for builtins
	Scope    *Scope  // scope this symbol lives in; the builtin scope for SymBuiltin
	// Declared is the user-written type annotation pinned to this
	// binding (e.g. the `int` in `x: int = 5`). Once set it never
	// changes; subsequent reassignments must remain assignable to it.
	// nil for unannotated locals - those carry only an Inferred type
	// that the type checker derives from the RHS.
	Declared rl.TypingT
}

Symbol is the declaration record for a name in some scope.

Each *use* of a name resolves to exactly one Symbol via Resolved.Uses. The Symbol is shared across all uses so later passes (type checker, goto-def, find-refs) can route through one identity per binding.

Parameter and loop-variable symbols carry name-precision DeclSpans: TypingFnParam.NameSpan and ForLoop.VarSpans (parallel to Vars) feed the binder, which plants those on the symbol. Goto-def, find-refs, and rename therefore land on the name token rather than the owner. (Synthesised params with no source token - e.g. fn_type entries - still fall back to the owner span; affects no real call site today.)

type SymbolKind added in v0.11.0

type SymbolKind int

SymbolKind classifies what introduced a symbol.

const (
	// SymBuiltin is an ambient name supplied by the runtime (e.g. `print`).
	// These symbols are synthesized on first reference; they have no decl
	// span in the user's source.
	SymBuiltin SymbolKind = iota + 1
	// SymHoistedFn is a top-level named function. Visible across the file
	// regardless of textual order; this is how callers reference a function
	// defined further down.
	SymHoistedFn
	// SymArg is declared in the script-level `args:` block. These act as
	// ambient locals in the file scope: the runtime populates them from
	// CLI flags before the body executes.
	SymArg
	// SymCmdArg is declared inside a `cmd_block` args section. The
	// binding lives in the enclosing (file) scope because the runtime
	// populates it as a global before the command's callback runs;
	// the kind distinguishes it from SymLocal so LSP hover and
	// goto-def can point users at the cmd block's decl.
	SymCmdArg
	// SymParam is a function/lambda parameter.
	SymParam
	// SymLocal is anything else assigned in normal statement flow.
	SymLocal
	// SymLoopVar is the binding introduced by `for x in ...`.
	SymLoopVar
	// SymWith is the `with` context binding on a `for` loop.
	SymWith
)

type TypeInfo added in v0.11.0

type TypeInfo struct {
	// SymbolTypes maps a symbol to the type the checker has decided
	// it currently holds. For an annotated binding this is the
	// declared type; for an unannotated binding it's the type
	// synthesized from the most-recent assignment. Absent entries
	// mean "no information yet" (callers should treat as Dynamic).
	SymbolTypes map[*Symbol]rl.TypingT
	// ExprTypes maps an expression AST node to its synthesized type.
	// Useful for hover, and for narrowing passes that need to ask
	// "what was the static type of this sub-expression?"
	ExprTypes map[rl.Node]rl.TypingT
	// Issues are type-related findings: type mismatches, arg count
	// errors, and so on. The checker layer converts these to
	// Diagnostic. Empty in Phase 2a; populated as later sub-commits
	// add the real checks.
	Issues []BindIssue
}

TypeInfo is the output of bidirectional type checking. It carries the type each symbol settled on, the type each expression node synthesized to, and any type-related diagnostics the checker recorded along the way.

Like Resolved, it's a pure value over the (ast, resolved) inputs - no mutation of Symbol records, no dependence on source text. The LSP snapshot model can hold one alongside the matching Resolved and serve hover/goto-type queries lock-free.

Phase 2a establishes the data model and handles literal + identifier synthesis only. Calls, operators, struct/list/map literals, and flow-sensitive narrowing come in subsequent commits.

func TypeCheck added in v0.11.0

func TypeCheck(file *rl.SourceFile, resolved *Resolved) *TypeInfo

TypeCheck runs bidirectional type checking over the file using the pre-computed Resolved view. Returns nil if either input is nil.

The pass is single-pass and AST-order: when it sees `x = expr`, it synthesizes the type of `expr`, then records that as the type of the symbol for `x`. Later references to `x` synth to that recorded type. Forward references (e.g. inside a lambda that's stored before it's called) get Dynamic - that's the right answer for a gradual system, and Phase 2e will refine it with Tarjan SCC ordering for genuine mutual recursion.

Jump to

Keyboard shortcuts

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