Documentation
¶
Index ¶
- func DumpForSnapshot(file *rl.SourceFile, info *TypeInfo, resolved *Resolved, ...) string
- func GenerateErrorMessage(node *ts.Node, src string) (msg string, code rl.Error, suggestion *string)
- func UnionJoinForTest(types []rl.TypingT) rl.TypingT
- type BindIssue
- type Diagnostic
- func NewDiagnosticError(node *ts.Node, originalSrc string, msg string, code rl.Error) Diagnostic
- func NewDiagnosticErrorFromSpan(span rl.Span, originalSrc string, msg string, code rl.Error) Diagnostic
- func NewDiagnosticErrorWithSuggestion(node *ts.Node, originalSrc string, msg string, code rl.Error, ...) Diagnostic
- func NewDiagnosticFromNode(node *ts.Node, originalSrc string, severity Severity, msg string, ...) Diagnostic
- func NewDiagnosticFromSpan(span rl.Span, originalSrc string, severity Severity, msg string, ...) Diagnostic
- func NewDiagnosticHint(node *ts.Node, originalSrc string, msg string, code rl.Error) Diagnostic
- func NewDiagnosticHintFromSpan(span rl.Span, originalSrc string, msg string, code rl.Error) Diagnostic
- func NewDiagnosticWarn(node *ts.Node, originalSrc string, msg string, code rl.Error) Diagnostic
- func NewDiagnosticWarnFromSpan(span rl.Span, originalSrc string, msg string, code rl.Error) Diagnostic
- type Frame
- type IssueSeverity
- type Pos
- type RadChecker
- type RadCheckerImpl
- type Range
- type Refinement
- type Resolved
- type Result
- type Scope
- type ScopeKind
- type Severity
- type Symbol
- type SymbolKind
- type TypeInfo
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
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 NewDiagnosticErrorFromSpan ¶ added in v0.9.0
func NewDiagnosticErrorWithSuggestion ¶ added in v0.6.26
func NewDiagnosticFromNode ¶
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 NewDiagnosticHintFromSpan ¶ added in v0.9.0
func NewDiagnosticWarn ¶ added in v0.6.17
func NewDiagnosticWarnFromSpan ¶ added in v0.9.0
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
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
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
With returns a new frame that narrows sym to t. The receiver is unchanged. O(1) - no map copy.
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 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 Refinement ¶ added in v0.11.0
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.
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).
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.