lsp-bjolang/Scope.fs

217 lines
10 KiB
Forth
Raw Permalink Normal View History

/// The local names in scope at a position: parameters, `let` bindings,
/// pattern variables, loop variables, and definitions inside a body.
///
/// Read off the forms as the reader gives them, as `Symbols` reads the
/// top-level names, with the scoping rules of the forms the parser knows. A
/// macro's binding forms are not known, so a name a macro binds is not seen;
/// the parser's own forms cover most of what is written.
module BjolangLsp.Scope
open System
open Bjolang.Lexer
open Bjolang.Ast
type Binder =
{ Name: string
/// Where the name is written in its binding form.
Range: Range
/// What a hover shows of the binding: the parameter with its type, the
/// `let` binding with its value, the pattern.
Shown: Range
/// What binds it, for a hover: "a parameter of `f`", "bound by `let`".
Description: string }
/// Whether `r` holds the position. The end is included, so a cursor right
/// after a name is in it, as it is while the name is being typed.
let private holds (line: int) (column: int) (r: Range) =
(line > r.Start.Line || (line = r.Start.Line && column >= r.Start.Column))
&& (line < r.End.Line || (line = r.End.Line && column <= r.End.Column))
let private binder name range shown description =
{ Name = name
Range = range
Shown = shown
Description = description }
/// The names a pattern binds: every lowercase symbol that is not the head of
/// a list, as the head is the constructor.
let rec private patternNames (description: string) (shown: Range) (s: SExpr) : Binder list =
match s with
| SAtom { Token = Symbol n; Range = r } when n <> "_" && n <> "else" && not (Char.IsUpper n[0]) ->
[ binder n r shown description ]
| SList(SAtom { Token = Symbol _ } :: args, _) -> args |> List.collect (patternNames description shown)
| SList(items, _) -> items |> List.collect (patternNames description shown)
| _ -> []
/// The parameters of an argument list: `x`, `(: x Type)`, `#:rest xs`, and a
/// keyword parameter `(#:key default)` or `#:key default`, which binds `key`.
let private parameters (owner: string) (args: SExpr list) : Binder list =
let description = if owner = "" then "a parameter" else $"a parameter of `%s{owner}`"
let rec go (args: SExpr list) =
match args with
| [] -> []
| SAtom { Token = Symbol n; Range = r } :: rest -> binder n r r description :: go rest
| (SList([ SAtom { Token = Colon }; SAtom { Token = Symbol n; Range = r }; _ ], whole)) :: rest ->
binder n r whole description :: go rest
| SList([ SAtom { Token = Keyword k; Range = r }; _ ], whole) :: rest ->
binder k r whole (description + ", by keyword") :: go rest
| SAtom { Token = Keyword "rest" } :: SAtom { Token = Symbol n; Range = r } :: rest ->
binder n r r (description + ", the rest of the arguments") :: go rest
| SAtom { Token = Keyword k; Range = r } :: value :: rest ->
binder k r (getRange value) (description + ", by keyword") :: go rest
| _ :: rest -> go rest
go args
/// What a body form defines for the forms after it: `(def x ...)`,
/// `(def (: x T) ...)`, `(defun (f ...) ...)` and their kin.
let private definedBy (form: SExpr) : Binder list =
match form with
| SList(SAtom { Token = Symbol("def" | "def/mutable") } :: target :: _, whole) ->
match target with
| SAtom { Token = Symbol n; Range = r } -> [ binder n r whole "defined in this body" ]
| SList(SAtom { Token = Colon } :: SAtom { Token = Symbol n; Range = r } :: _, _) ->
[ binder n r whole "defined in this body" ]
| SList(names, _) -> names |> List.collect (patternNames "defined in this body" whole)
| _ -> []
| SList(SAtom { Token = Symbol head } :: SList(SAtom { Token = Symbol n; Range = r } :: _, sig') :: _, _) when
head = "defun" || head = "defbjo" || head = "defbjouble"
->
[ binder n r sig' $"a `%s{head}` in this body" ]
| _ -> []
/// The bindings of a `let`'s list, each as its names and its value.
let private letBindings (description: string) (bindings: SExpr list) : (Binder list * SExpr option) list =
bindings
|> List.map (fun b ->
match b with
| SList([ SAtom { Token = Symbol n; Range = r }; value ], whole) -> [ binder n r whole description ], Some value
| SList([ SList(SAtom { Token = Colon } :: SAtom { Token = Symbol n; Range = r } :: _, _); value ], whole) ->
[ binder n r whole description ], Some value
| SList([ pattern; value ], whole) -> patternNames description whole pattern, Some value
| other -> [], Some other)
/// The names a loop's clauses bind, which its other clauses, its `=>` and,
/// in a comprehension, the expression before them see. A loop variable is in
/// scope across the whole form rather than only after its clause; that is
/// more than the loop allows, and enough to find a name by.
let rec private loopNames (items: SExpr list) : Binder list =
items
|> List.collect (fun item ->
match item with
| SList(SAtom { Token = Keyword("for" | "with" | "let" | "when-let" | "finish-let" | "break-let" as k) }
:: pattern
:: _,
whole) -> patternNames $"bound by a `:%s{k}` clause" whole pattern
| SList(SAtom { Token = Keyword "acc" } :: SAtom { Token = Symbol n; Range = r } :: _, whole) ->
[ binder n r whole "a loop accumulator" ]
| SList(SAtom { Token = Keyword "subloop" } :: inner, _) -> loopNames inner
| SList([ SAtom { Token = Symbol n; Range = r }; _ ], whole) when
// A `:subloop`'s `=> (name expr)`, the only bare list among clauses.
not (Char.IsUpper n[0])
->
[ binder n r whole "a subloop's result" ]
| _ -> [])
/// The names in scope at the position, innermost last, inside `s`; `None`
/// if the position is not in it.
let rec private inForm (line: int) (column: int) (env: Binder list) (s: SExpr) : Binder list option =
if not (holds line column (getRange s)) then
None
else
match s with
| SAtom _ -> Some env
| SList(items, _) -> Some(inList line column env items)
and private inList (line: int) (column: int) (env: Binder list) (items: SExpr list) : Binder list =
let body env forms = inBody line column env forms
match items with
| SAtom { Token = Symbol head } :: SList(SAtom { Token = Symbol name } :: args, _) :: _ when
head.StartsWith "def" && head <> "def" && head <> "def/mutable"
->
// `(defun (f x y) body...)`: the parameters over the whole form, the
// head included, so that a parameter is found where it is written.
body (env @ parameters name args) items
| SAtom { Token = Symbol("fun" | "bjoroutine") } :: SList(args, _) :: _ -> body (env @ parameters "" args) items
| SAtom { Token = Symbol("let" | "let*" | "letrec" | "let/mono" | "with-open" as head) } :: rest ->
let named, bindingList, bodyForms =
match rest with
| SAtom { Token = Symbol n; Range = r } :: SList(bs, _) :: forms ->
[ binder n r r "the name of a named `let`, called to loop" ], bs, forms
| SList(bs, _) :: forms -> [], bs, forms
| forms -> [], [], forms
let bindings = letBindings $"bound by `%s{head}`" bindingList
let all = named @ (bindings |> List.collect fst)
// A value sees what is around the `let`, and in a `let*` or a
// `letrec` the bindings before it, or all of them.
let inValue =
bindings
|> List.indexed
|> List.tryPick (fun (i, (_, value)) ->
value
|> Option.bind (fun v ->
let seen =
match head with
| "letrec" -> env @ all
| "let*" -> env @ named @ (bindings |> List.take i |> List.collect fst)
| _ -> env
inForm line column seen v))
match inValue with
| Some found -> found
| None ->
let inBinders = all |> List.exists (fun b -> holds line column b.Range)
if inBinders then env @ all else body (env @ all) bodyForms
| SAtom { Token = Symbol "with-return" } :: SAtom { Token = Symbol n; Range = r } :: forms ->
body (env @ [ binder n r r "the escape of a `with-return`, called to return" ]) forms
| SAtom { Token = Symbol "match" } :: scrutinee :: arms ->
match inForm line column env scrutinee with
| Some found -> found
| None ->
arms
|> List.tryPick (fun arm ->
match arm with
| SList(pattern :: _, _) ->
let bound = patternNames "bound by a `match` pattern" (getRange pattern) pattern
inForm line column (env @ bound) arm
| _ -> inForm line column env arm)
|> Option.defaultValue env
| SAtom { Token = Symbol("loop" | "seql" | "comprehension") } :: rest -> body (env @ loopNames rest) items
| _ -> body env items
/// The names in scope in a body at the position: a definition scopes over
/// the forms after it.
and private inBody (line: int) (column: int) (env: Binder list) (forms: SExpr list) : Binder list =
let rec go (env: Binder list) (forms: SExpr list) =
match forms with
| [] -> env
| form :: rest ->
match inForm line column env form with
| Some found -> found
| None -> go (env @ definedBy form) rest
go env forms
/// The local names in scope at `line` (1-based) and `column` (0-based) among
/// a file's top-level forms, innermost last. A top-level definition is not
/// among them: `Symbols` has those.
let at (forms: SExpr list) (line: int) (column: int) : Binder list =
forms |> List.tryPick (inForm line column []) |> Option.defaultValue []
/// The binding `name` refers to at the position, if it is a local one: the
/// innermost of that name.
let find (forms: SExpr list) (line: int) (column: int) (name: string) : Binder option =
at forms line column |> List.tryFindBack (fun b -> b.Name = name)