|
| 1 | +# Farkle's precompiler |
| 2 | + |
| 3 | +Every time an app using Farkle starts, it generates the parser tables. This process takes some time, and it take even more, the app does not reuse the runtime Farkles it creates. |
| 4 | + |
| 5 | +Most apps need to parse a static grammar whose specifications never change between program executions. For example, a compiler or a JSON parser will parse text from the same language every time you use it. Farkle would spend time generating the parsing tables that do not depend on user input and will always be the same. It wouldn't hurt a program like a REST server parsing hundreds of input strings, but a compiler that parses only one file, building the grammar every time it is run would take some time, maybe more than the time spent for the parser, if the grammar is big. |
| 6 | + |
| 7 | +What is more, Farkle does not report any grammar error (such as an LALR conflict) until it's too late: text was attempted to be parsed with a faulty grammar. Wouldn't it be better if these errors were caught earlier in the app's life cycle? |
| 8 | + |
| 9 | +One of Farkle's new features that came with version 6 is called _the precompiler_. The precompiler addresses this inherent limitation of how Farkle works with grammars. Instead of generating it every time, the grammar's parser tables are built ahead of time and stored in the program's assembly, when it gets compiled. This significantly boosts the program's startup time by orders of magnitude. |
| 10 | + |
| 11 | +## How to use it |
| 12 | + |
| 13 | +Using the precompiler is a very easy procedure and does not differ very much from regularly using Farkle. |
| 14 | + |
| 15 | +### Preparing the your code |
| 16 | + |
| 17 | +Let's say you have a very complicated designtime Farkle that you want its grammar to be precompiled. The first thing to do is to use the `RuntimeFarkle.markForPrecompile` function. Here's an example in both F# and C#: |
| 18 | + |
| 19 | +``` fsharp |
| 20 | +open Farkle; |
| 21 | +open Farkle.Builder; |
| 22 | +let designtime = |
| 23 | + "My complicated language" |
| 24 | + ||= [!@ beginning .>>. middle .>>. ``end`` => (fun b m e -> b + m + e)] |
| 25 | + |> DesigntimeFarkle.addLineComment "//" |
| 26 | + |> DesigntimeFarkle.addBlockComment "/*" "*/" |
| 27 | + |> RuntimeFarkle.markForPrecompile |
| 28 | +
|
| 29 | +let runtime = RuntimeFarkle.build designtime |
| 30 | +``` |
| 31 | + |
| 32 | +``` csharp |
| 33 | +using Farkle; |
| 34 | +using Farkle.Builder; |
| 35 | + |
| 36 | +public class MyLanguage { |
| 37 | + public static readonly DesigntimeFarkle<int> Designtime; |
| 38 | + public static readonly RuntimeFarkle<int> Runtime; |
| 39 | + |
| 40 | + static MyLanguage() { |
| 41 | + Designtime = |
| 42 | + Nonterminal.Create("My complicated language", |
| 43 | + beginning.Extended().Extend(middle).Extend(end).Finish((b, m, e) => b + m + e)) |
| 44 | + .AddLineComment("//") |
| 45 | + .AddBlockComment("/*", "*/") |
| 46 | + .MarkForPrecompile(); |
| 47 | + |
| 48 | + Runtime = Designtime.Build(); |
| 49 | + } |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +With this simple function (or extension method), Farkle will be able to discover this designtime Farkle in your assembly and precompile it. It will be able to actually find it if you follow these rules: |
| 54 | + |
| 55 | +* The designtime Farkle must be declared in a `static readonly` _field_ (not property). For F#, a let-bound value in a module is equivalent, but it __must not__ be mutable. |
| 56 | + |
| 57 | +* The designtime Farkle's field can be of any visibility (public, internal, private, it doesn't matter). It will be detected even in nested types. Just remember the rule above. |
| 58 | + |
| 59 | +* The `markForPrecompile` function must be the absolute last function to be applied in the designtime Farkle. If you don't apply it to the starting symbol, or applied any other function on top of it, Farkle will not precompile the designtime Farkle, but it will still be perfectly functional. |
| 60 | + |
| 61 | +* The `markForPrecompile` function must be called in the assembly the designtime Farkle was created and should better not used from inside another function. |
| 62 | + |
| 63 | +Say you have the following function in assembly A: |
| 64 | + |
| 65 | +``` fsharp |
| 66 | +let markForPrecompileAndRename (df: DesigntimeFarkle<_>) = |
| 67 | + df |
| 68 | + |> DesigntimeFarkle.rename (df.Name + " Precompiled") |
| 69 | + |> DesigntimeFarkle.markForPrecompile |
| 70 | +``` |
| 71 | + |
| 72 | +And say you use the function above in assembly B. It won't work; Farkle won't be able to precompile the designtime Farkle. A solution would have been to make the function inline, but don't be 100% sure it will work. |
| 73 | + |
| 74 | +* The field of the precompilable designtime Farkle must be either a typed or untyped designtime Farkle: |
| 75 | + |
| 76 | +``` csharp |
| 77 | +public class MyLanguage { |
| 78 | + // This will work. |
| 79 | + public static readonly DesigntimeFarkle Foo = MySimpleDesigntimeFarkle.MarkForPrecompile(); |
| 80 | + // But not this. |
| 81 | + public static readonly object Foo = MyComplicatedDesigntimeFarkle.MarkForPrecompile(); |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +* All precompilable designtime Farkles within an assembly must have different names, or an error will be raised during precompiling. That's actually why the `DesigntimeFarkle.rename` function was created. |
| 86 | + |
| 87 | +* Multiple references to the same precompilable designtime Farkle do not pose a problem. The following example will work: |
| 88 | + |
| 89 | +``` fsharp |
| 90 | +let designtime1 = myComplicatedDesigntimeFarkle |> RuntimeFarkle.markForPrecompile |
| 91 | +let designtime2 = designtime1 |
| 92 | +``` |
| 93 | + |
| 94 | +### Preparing your project |
| 95 | + |
| 96 | +With our designtime Farkles being ready to be precompiled, it's time to prepare our project files. Add a reference to [the `Farkle.Tools.MSBuild` package][msbuild] like that: |
| 97 | + |
| 98 | +``` xml |
| 99 | +<!-- The following properties are optional. --> |
| 100 | +<PropertyGroup> |
| 101 | + <!-- Set it to false to disable the precompiler. Your app |
| 102 | + will still work, but without the performance boost the precompiler offers. --> |
| 103 | + <FarkleEnablePrecompiler>false</FarkleEnablePrecompiler> |
| 104 | + <!-- If it is true, grammar errors will raise a warning |
| 105 | + instead of an error and not fail the entire build. --> |
| 106 | + <FarkleSuppressGrammarErrors>true</FarkleSuppressGrammarErrors> |
| 107 | +</PropertyGroup> |
| 108 | +<ItemGroup> |
| 109 | + <PackageReference Include="Farkle.Tools.MSBuild" Version="6.*" PrivateAssets="all" /> |
| 110 | +</ItemGroup> |
| 111 | +``` |
| 112 | +If you compile your program now, you should get a message that your designtime Farkles' grammars got precompiled. Hooray! With our grammars being precompiled, calling `RuntimeFarkle.build designtime` (or `buildUntyped`) is now much, much faster. |
| 113 | + |
| 114 | +## Some final notes |
| 115 | + |
| 116 | +### Beware of non-determinism |
| 117 | + |
| 118 | +Farkle's precompiler was made for grammars that are static, that's the reason it only works on static readonly fields: once you created it in your code, you cannot change it. Otherwise, what good would the precompiler be? |
| 119 | + |
| 120 | +You can always call a non-deterministic function like `DateTime.Now()` that will make your designtime Farkle parse integers in the hexadecimal format in your birthday, and in the decimal format in all other days. If you build your app on your birthday, it will produce bizarre results on all the other days, and if you build it on a day other than your birthday, it will work every time, except of your birthday (the worst birthday present) __Just don't do it.__ Farkle cannot be made to detect such things, and you are not getting any smarter by doing it. |
| 121 | + |
| 122 | +### Building from an IDE |
| 123 | + |
| 124 | +And last but not least, the precompiler will not work when running a .NET Framework-based edition of MSBuild. This includes building from IDEs such as Visual Studio. The recommended way to build an app that uses the precompiler is through `dotnet build` and its friends. This _doesn't mean_ that the precompiler won't work on .NET Framework assemblies; you have to use the new project format and build with the .NET Core SDK; it will normally work. |
| 125 | + |
| 126 | +Rider however can use the precompiler with a simple workaround. Open its settings, go to "Build, Execution, Deployment", "Toolset and Build", "Use MSBuild version", and select an MSBuild executable from the .NET Core SDK (it typically has a `.dll` extension). |
| 127 | + |
| 128 | + |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +So I hope you enjoyed this little tutorial. If you did, don't forget to give Farkle a try, and maybe you feel especially precompiled today, and want to hit the star button as well. I hope that all of you have an wonderful day, and to see you soon. Goodbye! |
| 133 | + |
| 134 | +[msbuild]: https://www.nuget.org/packages/Farkle.Tools.MSBuild |
0 commit comments