lsp-bjolang/Expansion.fs

219 lines
8.3 KiB
Forth
Raw Normal View History

/// The macro expansions of the last check of each file, and the expansion of
/// one call, printed: one step, or all the way down.
///
/// The compiler hands each expansion to `Macro.observe` while it checks. A
/// call is found again by its range, which is its call site; a template's own
/// nodes carry that range too, so a call whose expansion is another macro
/// call expands again at the same site, and the form expanded is what tells
/// those steps apart.
module BjolangLsp.Expansion
open System
open System.Text
open Bjolang
open Bjolang.Lexer
open Bjolang.Ast
type Record =
{ Macro: string
Site: Range
Input: SExpr
Output: SExpr }
// ---------------------------------------------------------------------------
// Printing
// ---------------------------------------------------------------------------
let private punctuation =
Map.ofList
[ Colon, ":"
Dot, "."
Comma, ","
CommaAt, ",@"
Quote, "'"
SynQuote, "#'"
Spread, "..."
Hash, "#" ]
let private escaped (s: string) =
let b = StringBuilder("\"")
for c in s do
match c with
| '"' -> b.Append "\\\"" |> ignore
| '\\' -> b.Append "\\\\" |> ignore
| '\n' -> b.Append "\\n" |> ignore
| '\t' -> b.Append "\\t" |> ignore
| c -> b.Append c |> ignore
b.Append('"').ToString()
let private character (c: int) =
match c with
| 32 -> @"#\space"
| 10 -> @"#\newline"
| 9 -> @"#\tab"
| c -> @"#\" + Char.ConvertFromUtf32 c
/// An atom as source. `name` spells a symbol, so that a hygiene renaming can
/// be shown as the template wrote it. A keyword at the head of a list is a
/// clause's, `:for`; anywhere else an argument's, `#:when`.
let private atom (name: string -> string) (atHead: bool) (t: Token) =
match t with
| Symbol s
| ResolvedSymbol s -> name s
| Keyword k -> if atHead then ":" + k else "#:" + k
| QuotedSymbol s -> "'" + s
| TypeVar v -> "%" + v.TrimStart '%'
| NumberLit n -> n
| StringLit s -> escaped s
| CharLit c -> character c
| BoolLit b -> if b then "#t" else "#f"
| other -> Map.tryFind other punctuation |> Option.defaultValue "?"
/// A form on one line.
let rec private flat (name: string -> string) (atHead: bool) (s: SExpr) : string =
match s with
| SAtom t -> atom name atHead t.Token
| SList(items, _) -> "(" + (items |> List.mapi (fun i x -> flat name (i = 0) x) |> String.concat " ") + ")"
/// The heads whose first argument stays on their line and whose body is
/// indented by two: a binding form or a definition, as Emacs indents them.
let private bodyForms =
set
[ "let"; "let*"; "letrec"; "let/mono"; "when"; "unless"; "fun"; "bjoroutine"; "defun"; "defbjo"
"def"; "match"; "case"; "with-open"; "with-return"; "loop"; "seql"; "if"; "begin"; "parameterize"
"try"; "def/macro" ]
let private width = 78
/// A form, indented as a reader of Lisp expects: on one line if it fits, else
/// its head and first argument on the first line and the rest beneath, two in
/// for a body form and under the first argument for a call.
let rec private pretty (name: string -> string) (indent: int) (atHead: bool) (s: SExpr) : string =
let one = flat name atHead s
match s with
| SList(head :: args, _) when indent + one.Length > width && not args.IsEmpty ->
let headText = flat name true head
// A template's `match` arrives as `match__12`, which is still a match.
let isBody =
match head with
| SAtom { Token = Symbol h } -> bodyForms.Contains(RegularExpressions.Regex.Replace(h, @"__\d+$", ""))
| _ -> false
let first = pretty name (indent + headText.Length + 2) false args.Head
let restIndent =
match head with
| SAtom _ when not isBody -> indent + headText.Length + 2
| _ -> indent + 2
let rest =
args.Tail
|> List.map (fun a -> "\n" + String(' ', restIndent) + pretty name restIndent false a)
|> String.concat ""
// A list headed by a list, such as a `let`'s bindings or a `match`
// arm, keeps its items in a column.
match head with
| SList _ ->
let inner = indent + 1
"("
+ pretty name inner true head
+ (args |> List.map (fun a -> "\n" + String(' ', inner) + pretty name inner false a) |> String.concat "")
+ ")"
| _ -> "(" + headText + " " + first + rest + ")"
| _ -> one
// ---------------------------------------------------------------------------
// What the last checks expanded
// ---------------------------------------------------------------------------
/// Each file's expansions, from its last check, in the order they were made.
let private byFile = Collections.Generic.Dictionary<string, Record list>()
/// Each name hygiene made, and the spelling the template wrote.
let private renamed = Collections.Generic.Dictionary<string, string>()
/// Runs `check` with the compiler reporting its expansions, and keeps those
/// made in `path` as the file's.
let recording (path: string) (check: unit -> 'a) : 'a =
let seen = ResizeArray<Record>()
Macro.observe <-
Some(fun macro site input output names ->
for KeyValue(fresh, original) in names do
renamed[fresh] <- original
seen.Add
{ Macro = macro
Site = site
Input = input
Output = output })
try
check ()
finally
Macro.observe <- None
let full = IO.Path.GetFullPath path
// A form can be expanded more than once in one check, as a
// declaration is checked again in a copy; the first is kept.
byFile[full] <-
seen
|> Seq.filter (fun r -> r.Site.File <> "" && IO.Path.GetFullPath r.Site.File = full)
|> Seq.distinctBy (fun r -> r.Site, flat id false r.Input)
|> List.ofSeq
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 size (r: Range) = (r.End.Line - r.Start.Line) * 100000 + (r.End.Column - r.Start.Column)
/// The expansion of the innermost macro call at `line` (1-based) and
/// `column` (0-based) of `path`, from its last check: the macro's name and
/// the expansion as text. One step is what the macro made; `full` expands
/// every macro call in that too, as the compiler went on to.
///
/// `hygienic` keeps the names hygiene made, `tmp__37`, rather than showing
/// them as the template wrote them, which is what tells a template's name
/// from the caller's of the same spelling.
let at (path: string) (line: int) (column: int) (full: bool) (hygienic: bool) : (string * string) option =
match byFile.TryGetValue(IO.Path.GetFullPath path) with
| false, _ -> None
| true, records ->
let calls = records |> List.filter (fun r -> holds line column r.Site)
match calls |> List.sortBy (fun r -> size r.Site) |> List.tryHead with
| None -> None
| Some innermost ->
// The first expansion at a site is of the call as written; any
// after it are of what that made.
let call = records |> List.find (fun r -> r.Site = innermost.Site)
let name (s: string) =
if hygienic then s
else
match renamed.TryGetValue s with
| true, original -> original
| _ -> s
/// Every macro call in `s` replaced by its expansion, again and
/// again, as far as the check went.
let rec expand (depth: int) (s: SExpr) : SExpr =
match s with
| SList(items, r) when depth < 200 ->
let key = flat id false s
match records |> List.tryFind (fun x -> x.Site = r && flat id false x.Input = key) with
| Some x -> expand (depth + 1) x.Output
| None -> SList(items |> List.map (expand depth), r)
| _ -> s
let result = if full then expand 0 call.Output else call.Output
Some(call.Macro, pretty name 0 false result)