fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
344 stars 21 forks source link

Fsc can manipulate AST at compile time (wanna approve) #697

Closed kekyo closed 1 year ago

kekyo commented 6 years ago

Fsc can manipulate AST at compile time (wanna approve)

Overview

I propose "the AST translator" is usable and easier metaprogramming at F# world.

It's handle manipulation for untyped AST at compile time, these AST nodes are: ParsedInput, SynExpr, SynPat... etc.

For example, a basic F# code fragment:

let addFunc a b =
    a + b
let subFunc a b =
    a - b

[<EntryPoint>]
let main argv =
    let r1 = addFunc 1 2
    let r2 = subFunc 1 2
    printfn "addFunc=%d, subFunc=%d" r1 r2
    0

We will be able to compile by fsc with the AST translator. Uses sample "FunctionLoggingTranslator" translator (complete code, following link)

type FunctionLoggingTranslatorImpl() =
    interface ITranslator<ICompilerConfig, ErrorLogger, ParsedInput> with
        member __.Name = "FunctionLoggingTranslator"
        member __.Translate config errorLogger input =
            match input with
            | ParsedInput.ImplFile(ParsedImplFileInput(fileName, isScript, qualifiedNameOfFile, scopedPragmas, hashDirectives, modules, (isLastCompiland, isExe))) ->
                ParsedInput.ImplFile(ParsedImplFileInput(fileName, isScript, qualifiedNameOfFile, scopedPragmas, hashDirectives, modules |> List.map Utilities.traverseModule, (isLastCompiland, isExe)))
            | ParsedInput.SigFile (ParsedSigFileInput(fileName, qualifiedNameOfFile, scopedPragmas, hashDirectives, modules)) ->
                input

[<assembly: Translator(typeof<FunctionLoggingTranslatorImpl>)>]
do ()

Made result same as:

let addFunc a b =
    System.Diagnostics.Debug.WriteLine("Enter function: addFunc")
    a + b
let subFunc a b =
    System.Diagnostics.Debug.WriteLine("Enter function: subFunc")
    a - b

[<EntryPoint>]
let main argv =
    System.Diagnostics.Debug.WriteLine("Enter function: main")
    let r1 = addFunc 1 2
    let r2 = subFunc 1 2
    printfn "addFunc=%d, subFunc=%d" r1 r2
    0

I forked and implemented it from visualfsharp repo. See my forked repo 'ast-translator-test' branch.

I wanna approve it and finally merge main repo. Of course, there is preparation to correct the pointed out problem.

Description

The AST translator is full-customizable for untyped AST at compilation time. Therefore we can handle additional features with no runtime costs (compare to quotation expressions and monadic structures)

We'll receive better way:

We'll be able to use for:

Details

How to use this way

We can easy to use AST translators:

Overall architecture

The AST translator uses F# untyped AST node types at FSharp.Compiler.Private.

  1. Fsc parses and aggregates command line option with --translator:<file>. Load these translator assemblies.
  2. Fsc finds translator by the indicator for TranslatorAttribute attribute type applied at translator assembly.
  3. Instantiate translator type from attribute information (TranslatorAttribute.TargetType.)
  4. Call to translator type's Translate function.
  5. The translator can anything AST manipulation and return new AST. The AST node is ParsedInput.
  6. If avaliable another translators, repeats between 4 and 5.
  7. Fsc continues for compilation with translated AST.
     +---------------+   * TranslatorAttribute
     |  Fsharp.Core  |   * ITranslator<_, _, _>
     +-------+-------+
             ^
             |
+------------+--------------+   * ICompilerConfig (derived to TcConfig)
|  FSharp.Compiler.Private  |   * ErrorLogger
+--------+-------------+----+   * ParsedInput (etc...)
         ^             ^
         |             |      +-----------------------------+
         |             +------+  FunctionLoggingTranslator  |
         |                    +-----------------------------+
         |                                 ^  __.Translate ... -> parsedInput
   +-----+-----+    Finding and calling    |
   |  fsc.exe  | <-------------------------+
   +-----------+

Featuring middle ware

(The concept not evaluate any code fragments, but I'm clear for this idea)

We can use the AST translator now, but untyped ASTs manipulates difficulty for common users. I'm expecting to develop middle ware library by communities.

The middle ware is fully AST translator but it requires additional translation information from source code. To give an example, we wanna insert arbitrary enter-exit code fragment:

[<InsertBefore>]
let beforeFunc (name: string) (args: obj[]) : unit =
    printfn "Enter function: %s %A" name args

[<InsertAfter>]
let afterFunc (name: string) (args: obj[]) (result: 'a) : 'a =
    printfn "Exit function: %s %A, Result=%A" name args result
    result

// ----------------

let addFunc a b =
    a + b
let subFunc a b =
    a - b

[<EntryPoint>]
let main argv =
    let r1 = addFunc 1 2
    let r2 = subFunc 1 2
    printfn "addFunc=%d, subFunc=%d" r1 r2
    0

The (imaginary) middle ware AST translator find InsertBeforeAttribute and InsertAfterAttribute annotated functions and insert debug output to use these functions. We can write it translator (exactly, but difficult) and it'll make results:

[<InsertBefore>]
let beforeFunc (name: string) (args: obj[]) : unit =
    printfn "Enter function: %s %A" name args

[<InsertAfter>]
let afterFunc (name: string) (args: obj[]) (result: 'a) : 'a =
    printfn "Exit function: %s %A, Result=%A" name args result
    result

// ----------------

let addFunc a b =
    let __args = [|a;b|]
    beforeFunc "addFunc" __args
    afterFunc "addFunc" __args (a + b)
let subFunc a b =
    let __args = [|a;b|]
    beforeFunc "subFunc" __args
    afterFunc "subFunc" __args (a - b)

[<EntryPoint>]
let main argv =
    let __args = [|argv|]
    beforeFunc "main" __args
    let r1 = addFunc 1 2
    let r2 = subFunc 1 2
    printfn "addFunc=%d, subFunc=%d" r1 r2
    afterFunc "main" __args 0

The middle ware translator gets additional translation information via:

AST translator assembly loads way

The AST translator assembly will load by fsc's command line option --translator:<file>.

TODOs

     +---------------+                +---------------------------+
     |  Fsharp.Core  |                |  FSharp.Compiler.Service  |
     +-------+-------+                +----------+----------------+
             ^                                   |
             |                                   |
+------------+-----------+  AST node types       |
|  FSharp.Compiler.Core  +<---------+------------+
+------------+-----------+          |
             ^                      |
             |                      |
+------------+--------------+       |
|  FSharp.Compiler.Private  |       |
+--------+-------------+----+       |
         ^             ^            |
         |             |      +-----+-----------------------+
         |             +------+  FunctionLoggingTranslator  |
         |                    +-----------------------------+
         |
   +-----+-----+
   |  fsc.exe  |
   +-----------+

Background

First way

The way was beginning from runtime validation logger using quotation expression (backend by FSharp.Quatation.Compiler)

But it has a problem for very slow at first runtime execution (cause internal compilation process), we can't pay it cost for business.

Second way

We're starting the fscx project. It's first approach for AST manipulation mechanizm using FSharp.Compiler.Service.

It project mades better results for the purpose. But it had these problems:

Now

Finally, fscx project was suspended (for only business reason.) I wanna finish it and I was thinking about what's more appropriate in what form.

I reimplement from scratch in the F#'s repo direct without FCS.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): M

Related suggestions:

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

robkuz commented 6 years ago

Would it be possible to manipulate also types with this approach?

kekyo commented 6 years ago

@robkuz Sure.

We can manipulate all F#'s type declaration.

For example:

7sharp9 commented 6 years ago

I don't think this is flexible enough for a general plugin, what I would want is to transform the AST and splice into type holes, or transform sections of annotated code entirely. Then again maybe I misunderstood the example, maybe a more through example would enlighten me.

kekyo commented 6 years ago

@7sharp9 Hi, thanks reply. Do you need more example for understanding, or need a example for deeper? (Please you should say me if I mistake understand it :)

I'll dig overview a little. I think about what the translator good at:

I can tell it imagine very narrow situation for use. But I feel the translator will be a lot of application possibility. These examples below can fix by the translator. And maybe we make the middle ware for it.


IPropertyChanged auto implementer.

It interface implementation are a lot of methods invented.

The serializer traversal handler.


The translator can fix for general problems, but it's invalid way. I feel it uses:

dsyme commented 1 year ago

This is macros of SyntaxTrees by plugins. It is not something we plan to add to F#, see #210 for example and all the discussions there