Smaug123 / WoofWare.Myriad

Some Myriad source generators for F#
MIT License
5 stars 0 forks source link

Some banter #62

Open nojaf opened 8 months ago

nojaf commented 8 months ago

Hi Patrick, thanks again for the demo.

Some stuff, that came to mind (in random order):

let a

still gets parsed to a tree with errors:

trivia = {
    LeadingKeyword = SynLeadingKeyword.Let(R("(1,0--1,3)"))
    InlineKeyword = None
    EqualsRange = None
}

Incomplete structured construct at or before this point in binding. Expected '=' or other token.

We don't process any tree with errors (and certain warnings), so we can assume in ASTTransformer that stuff is just there. This means that you need to put in Some range0 in certain places. Just to tell Fantomas that the keyword is present.

We transform the ParsedInput to an Oak internally, to only print out the code again. It is possible to print source code from an Oak, this is more convenient than the untyped tree. I hoped that one day https://edgarfgp.github.io/Fabulous.AST/ would have taken off and become the recommended way to generate code.

Did you ever explore other options?

#r "nuget: Fantomas.Core, 6.3.0-alpha-005"

open Fantomas.FCS.Syntax

let cheatABit (e: SynExpr) : SynBinding =
    let source = "let a b = ()"

    let parsedInput =
        CodeFormatter.ParseAsync(false, source)
        |> Async.RunSynchronously
        |> Array.head
        |> fst

    match parsedInput with
    | ParsedInput.ImplFile(ParsedImplFileInput(
        contents = [ SynModuleOrNamespace(decls = [ SynModuleDecl.Let(bindings = [ b ]) ]) ])) ->
        match b with
        | SynBinding(accessibility,
                     kind,
                     isInline,
                     isMutable,
                     attributes,
                     xmlDoc,
                     valData,
                     headPat,
                     returnInfo,
                     _,
                     range,
                     debugPoint,
                     trivia) ->
            SynBinding(
                accessibility,
                kind,
                isInline,
                isMutable,
                attributes,
                xmlDoc,
                valData,
                headPat,
                returnInfo,
                e,
                range,
                debugPoint,
                trivia
            )
    | _ -> failwith "unexpected"

Parsing small strings to grab the AST nodes is a trick I do in Telplin from time to time.

Smaug123 commented 8 months ago

Thanks for this! (In theory I'm testing everything I have in this repo; I did actually consider just making my own AstExtensions entirely. If necessary I can just make my own versions of everything I currently use from AstExtensions.)

A tool that gives access to type-checking information would be super cool and would fix so many of my hacks. I probably don't have much spare energy soon, but I will definitely keep that in the back of my mind for when I next become inspired. I need to work out how the analysers actually work. I didn't really bother exploring anything other than Myriad.

Neat idea with parsing strings. That's definitely cheating ;)


I guess there's a possible world where the AST had a tighter set of types, so that an "incomplete" AST had the different possible incompletenesses modelled precisely (in such a way that the default were valid, and you had to add extra information to represent an invalid node), and so that a "complete" AST were always valid if you managed to construct a node of the right type. Like, instead of the current optional type, trivia is instead a three-case DU:

type Trivia =
    /// this is "strict mode", currently not really modelled in the AST
    | Absent
    /// this is a subset of the current `Some trivia` cases
    | Present of (data which specifies node layout)
    /// currently implicit as "anything that isn't Present"
    | Incomplete of (data which specifies the fragment)

type SynExpr =
    | SynPat of (_ * {anything which is currently in trivia that is necessary for semantics, e.g. "inline"} * Trivia)

That way, tools such as Fantomas could know whether a node was intended to carry layout information or not.

This would, of course, be a bunch of work in both FCS and Fantomas, and would be a very API-breaking change; I don't have the intuition of working with the compiler to know whether it would be worthwhile, but I bet it would be too much work. It has the nice property that it makes explicit some stuff which is currently implicit in the treatment of range0 as a sort of Some(None) whose interpretation is based on context.

Smaug123 commented 7 months ago

I started looking at this, using the analyzers as a base, but immediately entered NuGet hell while trying to load a project file. This is the kind of work that is literally physically painful to me, so I will probably not proceed with it :P

The "ProcessFrameworkReferences" task failed unexpectedly.
System.IO.FileLoadException: Could not load file or assembly 'NuGet.Frameworks, Version=6.8.0.122, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. Could not find or load a specific file. (0x80131621)

https://github.com/ionide/FSharp.Analyzers.SDK/compare/main...Smaug123:FSharp.Analyzers.SDK:blah?expand=1

nojaf commented 7 months ago

Hi Patrick, I totally hear you that NuGet hell is no fun. I was actually able to run your test project with this small change:

image

The clash seems to come from using ManagePackageVersionsCentrally I would suspect.

Smaug123 commented 7 months ago

So that didn't work for me, but an explicit dep on NuGet.Frameworks v6.8.0 did work 🤷

nojaf commented 7 months ago

Hmm, yes, that is weird. What SDK and OS are you on?

Smaug123 commented 7 months ago

net8.0.1, osx-arm64. I'm not hugely interested in working out the problem here though because I do now have a TAST!

nojaf commented 7 months ago

Yeah fair enough. Nothing comes to mind anyway.

Smaug123 commented 7 months ago

Things of this level of cursedness happen every time I try and host the compiler - with the debugger attached, typechecking consistently passes; without the debugger attached, typechecking consistently aborts.

Smaug123 commented 7 months ago

TASTCollecting in Analyzers.SDK currently seems to lack what I need, namely "visit the declarations directly" - it currently only visits right-hand sides of expressions? (For example, I can't visit a simple declaration of a record type, I think.) But I'm a bit surprised that nobody's needed to visit left-hand sides for analysers before, so maybe I've misunderstood something.

nojaf commented 7 months ago

Yes, you would probably be the first to attempt to find that information via the type tree collector. Most tooling will likely attempt to use the symbols API. I've hacked a quick example of how you could do it: https://github.com/G-Research/fsharp-analyzers/compare/main...record-type

Or a more direct approach would be to filter all the symbols of a file: ctx.CheckFileResults.GetAllUsesOfAllSymbolsInFile() |> // ... filter them out

Smaug123 commented 7 months ago

Do you happen to know where the attributes have gone on the FSharpImplementationFileDeclaration list in the TypedTree? I can't find any declaration that contains a nonempty Attributes. Or did they get thrown away at some point before now? (I've updated https://github.com/ionide/FSharp.Analyzers.SDK/compare/main...Smaug123:blah?expand=1 if interested.)

nojaf commented 7 months ago

Hmm, not sure. This comes to mind: https://github.com/dotnet/fsharp/issues/13786

Smaug123 commented 4 days ago

@nojaf I've started getting a little annoyed with how many versions behind Myriad is with its FCS dependency, so I'm now more interested than I was in rearchitecting things. Do you know if there's a tool which I can reference in an fsproj file like I can with Myriad, which uses Fabulous.AST already?

nojaf commented 4 days ago

I'm not entirely sure if I understand what you are looking for. An example project of Fabulous.AST would be https://github.com/ionide/LanguageServerProtocol/pull/49 maybe. Could you briefly describe again what you are after?

Smaug123 commented 4 days ago

Just a reasonably drop-in "here (WoofWare.Myriad.Plugins) is some F# assembly which exposes methods to construct a syntax tree; here (MyNewProject.fsproj) is an fsproj file which contains some inputs (MyInput.fs) to that construction; run the Plugins on MyInput.fs, creating this resulting output MyOutput.fs file which gets compiled into MyNewProject.fsproj".

Smaug123 commented 4 days ago

Thanks for that PR link; there's rather more manual faff in there than I'd like (which Myriad exists to abstract away from us). I can probably write something Myriad-like which accepts arbitrary plugins.