217 lines
10 KiB
Forth
217 lines
10 KiB
Forth
|
|
/// 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)
|