qwertie / ecsharp

Home of LoycCore, the LES language of Loyc trees, the Enhanced C# parser, the LeMP macro preprocessor, and the LLLPG parser generator.
http://ecsharp.net
Other
172 stars 25 forks source link

[Question] Using LeMP and .ecs as T4 replacement #112

Closed dadhi closed 3 years ago

dadhi commented 4 years ago

Hi there and thanks for the interesting project(s).

I have read the documentation regarding using LeMP as C# generator and installed VS 2019 extension (btw, thanks for still supporting it). But maybe I am lacking the essential understanding of how things should work or am I chasing something else completely.

What I want is mostly C# code as the output of my .ecs file sprinkled with the dynamically generated methods and fields. Similar to this: https://github.com/jbogard/MediatR/blob/d916697159a1f1270d704c6fca95dab7dc274efa/samples/MediatR.Examples.DryIocZero/Container.Generated.tt#L131

Does it mean I need to construct LNode for the output methods and print it as a plain CSharp, or should I do an intermediate quote step in some other place (what place?) and then use it LNode output for my purposes?

Basically is the LeMP alternative to T4 <# /* insert generated C# */ #>?

qwertie commented 4 years ago

I think you are asking "what is the LeMP alternative to <#= arbitrary C# code #>?", is that right?

dadhi commented 4 years ago

yes, kind of

qwertie commented 4 years ago

Well... give me the weekend and I'll have a proper answer for you. For a long time, I've been meaning to add a way to run arbitrary C# code at compile-time, but no one has actually asked for it. So, currently, the only way to run arbitrary code at compile-time is to create a separate C# project to define macros, compile them as normal, then import them into LeMP.

I think I can add a more convenient feature quickly, but first I have to produce an initial 2.8.0.0 (semver 28.0.0) release, which I'll try to do immediately. After that I will add a macro that allows you to run code at compile time via Roslyn (Microsoft.CodeAnalysis.CSharp.Scripting).

Here's my current thinking:

  1. In the initial version, the compile-time environment will reference standard Loyc libraries. However, any assemblies that your project references will not be available. There should be some mechanism for adding references, but I'm not sure what syntax to use. Perhaps I should add a new preprocessor directive #r "C:\Path\Assembly.dll" like C# interactive uses.

    The LeMP extension is implemented as a single-file generator. As far as I know, it does not have access to information about the project configuration (including references). Also, since a single-file generator processes files individually, any classes that you define at compile time in one ecs file will not be visible in any other ecs file. However, it is still possible to share code between files using code like includeFile("Shared.ecs");.

  2. A compileTime macro will allow you to define stuff at compile time. It will be possible to use macros inside the compileTime block, but not to define macros.

    If I implement this naively, using directives and namespace blocks outside the compileTime block will have no effect inside the compileTime block. I'm not comfortable with this, but arbitrary using statements cannot work at compile-time because the runtime references are not actually being referenced at compile time. So, in the following example, Foo is not in the RuntimeNS namespace, nor is it part of the RuntimeClass class, and it does not have access to System.Linq either.

    using System.Linq;
    namespace RuntimeNS {
        class RuntimeClass {
            compileTime {
                using System.Collections.Generic;
                class Foo { public static int X = 5; }
            }
        }
    }
  3. compileTimeAndRuntime macro will allow you to define dual-use stuff. It's compiled at compile time, but also emits the same code into the output file.

    compileTimeAndRuntime {
        class Foo { public static int X = 5; }
    }
  4. precompute macro will preprocess a C# expression or block with LeMP, run the output code, and insert the result into the code. The expression can return an LNode if it wants to produce to produce a syntax tree, otherwise the expression is treated as a literal.

    const string example1 = precompute((++Foo.X).ToString());
    const string example2 = precompute(LNode.Id("example1"));
    
    // Output of LeMP:
    // const string example1 = "6";
    // const string example2 = example1;
  5. rawPrecompute will be the same as precompute except that it does not preprocess the code with LeMP. (But note: other macros such as replace can potentially change the code before rawPrecompute sees it)

  6. There is already a define macro for creating simple macros. As a MVP (minimum viable product), you could combine this with compileTime to define macros that produce syntax trees at compile-time.

For example, this code:

compileTime {
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using Loyc.Ecs;

  static List<string> GetFunctionNames(LNodeList input) {
    // Get list of function names
    var functionNames = new List<string>();
    foreach (var part in input) {
      // Check if it's a method
      if (Loyc.Ecs.EcsValidators.MethodDefinitionKind(part, out _, out LNode name, out _, out _) == CodeSymbols.Fn) {
        functionNames.Add(name.ToString());
      }
    }
    return functionNames;
  }

  static LNodeList WithMetadata(LNode input) {
    // quote { } produces a single node if the input was a list. Convert input back to a list.
    var nameList = GetFunctionNames(input.AsList(CodeSymbols.Splice));
    return quote {
      static string[] metadata = new[] { $(..result.Select(name => LNode.Literal(name))) };
      $input;
    }
  }
}

precompute(WithMetadata(quote {
  static int square(int x) => x*x;
  static int cube(int x) => x*x*x;
}));

...should produce output like this:

  static string[] metadata = new[] { "square", "cube" };
  static int square(int x) => x*x;
  static int cube(int x) => x*x*x;

The part that says precompute(WithMetadata(quote { .... })) could then be simplified by defining a macro like this:

define withMetadata({ $(..code); }) {
  precompute(WithMetadata(quote { $code; }));
}
withMetadata {
  static int square(int x) => x*x;
  static int cube(int x) => x*x*x;
}

If I do this in a quick-and-dirty way based on Roslyn's interactive C# mode, the behavior of the macro will be a bit counterintuitive. For example, the input

class X {
  compileTime {
    using System;
    string text = "Text";
    string folder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
  }
}
class Y {
  compileTime {
    File.WriteAllText(Path.Combine(folder, "temp.txt"), text));
  }
}

...would work: it would write a temp.txt file to my "Documents" folder, even though the definitions string text, string folder and using System appear in an entirely different class. To avoid this counterintuitive behavior, I'm thinking about emitting an error unless compileTime appears at the top level of the file. But I can already see that such a restriction does not entirely solve the problem because precompute can be used elsewhere, and precompute itself could define variables, e.g. precompute(new Dictionary<int,int>().TryGetValue(1, out var value)) defines value in the "global" compile-time scope... but maybe I can fix this by just wrapping the code in some kind of block before feeding it to Roslyn.

What do you think of this proposal?

dadhi commented 4 years ago

@qwertie

Thanks for detailed answer and plan, I will digest them slowly..

My general thoughts on code-gen in C#: the valid C# gen with the output available to user inspection currently boils down to T4. Wich is practical (has a small API surface) and tool-friendly (intellisence, coloring, debugging). But MS does not love it (is not fixing the existing problems) and stopped the dev for newer .Net Core targets. Which opens a space of possibility for alternative solutions, given that some features will cover for absense of some T4 features.

Btw, I don't consider Roslyn for the described task (overly complex and lower level) and upcoming source-generators (incomplete moving target).

Also I still see a big demand in the working solution. The latest addition of T4 support in Rider IDE and activity in mono\t4 repo, questions on StackOverflow. My dream is important as well :) for compile-time DI.

As a PL lover I also see a value in C# macro-system and extending the language via user customization not relying on MS offerings. Looking at C#8, C#9 and beyond - IMHO MS does not have a vision and drived by comittee producing all-over-the-place features. The progression is not fast either.

Regarding your points

Perhaps I should add a new preprocessor directive #r "C:\Path\Assembly.dll"

Yes, this is an important feature to not jump over the hoops. Also having the syntax matching the already existing solutions is good for familiarity.

However, it is still possible to share code between files using code like includeFile("Shared.ecs")

This is perfectly fine, and the code sharing is not the first order feature anyway.

A compileTime macro will allow you to define stuff at compile time. It will be possible to use macros inside the compileTime block, but not to define macros.

I am not yet figured out the what's is what here, feeling a bit dumb.. What I want is something like this in my .ecs:

//auto-generated
namespace Blah
{
    partial public class X
    {
         compileTime {
         foreach (var expr in GetExpressions())
         { 
              var methodName = "Get" + expr.Type.Name;
              generate {
                   public ${methodName}() =>
                        ${exp.Body}
               }
          }
          }
    }
}

compileTimeAndRuntime

Looks interesting but I wonder the use cases for it.

precompute

Simple and useful. As I understood this is the closest to T4 <#= #>.

What do you think of this proposal?

Honestly I did not get all the details. Let it seat with me for a couple of days, maybe I'll come with more thoughts.

qwertie commented 4 years ago

In my current plan, compileTime {...} is kind of like <#...#> in T4 but it is not the same, because LeMP is a macro processor, not a templating engine. The basic problem is that <#...#> accepts partial code snippets like while (x < 5) {, but EC# and LeMP do not.

In LeMP it's not obvious how to support something like generate { } ... but perhaps I can find some kind of solution after writing a minimum viable product (MVP).

So in my current plan you can't write this...

public partial class X {
    compileTime {
        foreach (var expr in GetExpressions()) { 
             var methodName = "Get" + expr.Type.Name;
             generate {
                  public object $methodName() => $(expr.Body);
             }
         }
     }
}

...but you could write this:

compileTime {
    using System.Linq;
    // TODO: write GetExpressions()

    IEnumerable<LNode> XMethods =
        GetExpressions().Select(expr => {
            var methodName = "Get" + expr.Type.Name;
            return quote {
                public object $methodName() => $(expr.Body);
            };
        });
}
public partial class X {
    precompute(XMethods);
}

...or this:

compileTime {
    using System.Linq;
    // TODO: write GetExpressions()
}
public partial class X {
    precompute(GetExpressions().Select(expr => {
            var methodName = "Get" + expr.Type.Name;
            return quote {
                public object $methodName() => $(expr.Body);
            };
        }));
}
dadhi commented 3 years ago

@qwertie I understand that LeMP is not a templating engine so it should have its own ideomatic way to do things. What I care is simplicity and flexibility. So those sample looks good to me.

qwertie commented 3 years ago

Success! 😁 Input:

compileTime {
    #r "C:\Dev\ecsharp\Bin\Debug\Loyc.Utilities.dll"

    using System.Collections.Generic;
    var d = new Dictionary<int,string> { [2] = "Two" };

    var stat = new Loyc.Utilities.Statistic();
    stat.Add(5);
    stat.Add(7);
}
public class Class
{
    // Yeah baby.
    const string Two = precompute(d[2]);
    const int Six = precompute((int) stat.Avg());
    double Talk() { 
        precompute(new List<LNode> {
            quote(Console.WriteLine("hello")),
            quote { return $(LNode.Literal(Math.PI)); }
        });
    }
}

Output:

public class Class
{
    // Yeah baby.
    const string Two = "Two";
    const int Six = 6;
    double Talk() {
        Console.WriteLine("hello");
        return 3.14159265358979;
    }
}
dadhi commented 3 years ago

This is cool and congrats!

r "C:\Dev\ecsharp\Bin\Debug\Loyc.Utilities.dll"

This is must have and the next immediate step is resolving the path from the "known stable" location, either from the current directory or project directory or something. Actually it can be anything fixed but not an absolute path.

qwertie commented 3 years ago

I've just added relative-path resolution (relative to #get(#inputFolder) which is the folder containing the input file - when using includeFile, I don't think this path changes inside the included file.) I still have to add unit tests.

One significant issue with this new feature is that you'll be able to crash Visual Studio by writing code like this:

compileTime {
    int Foo() => Foo();
    Foo();
}

This is because StackOverflowException is an unrecoverable error, and LeMP runs in VS's address space. This isn't too horrible for an MVP except that Visual Studio is too aggressive about running single-file generators. Not only will your code run every time you save it, it will ALSO run if you change tabs in Visual Studio away from your .ecs file.

So of course, you might write some code that is unfinished. If it's an infinite loop, not a huge deal, VS will freeze up for 10 seconds and then LeMP will automatically terminate the macro. But in case of stack overflow, or if it's an infinite loop that allocates more and more memory, VS will crash.

So I was thinking about how to fix this - it's not easy to set up a separate process with the necessary interprocess communication just for compileTime {...}, so the easiest thing is to move LeMP as a whole into a separate process, or just invoke LeMP.exe as a command-line tool which already works anyway. But then it occurred to me to try this code in a T4 template. Guess what, T4 templates have the exact same problem! They will totally crash VS.

In fact, T4 is worse. <# while(true) {} #> will freeze VS forever. So... if the problem is small enough for MS not to fix for 10+ years, I guess I can accept it in my MVP.

LeMP lets you run arbitrarily evil code at compile time (e.g. "reformat drive D:"). Maybe this could be avoided with a tool like Sandboxie but again, T4 has the same problem.

dadhi commented 3 years ago

I've just added relative-path resolution (relative to #get(#inputFolder) which is the folder containing the input file - when using includeFile

Cool :)

I wonder what happens when you Link ecs file to project instead of adding it. This is a common thing for content files NuGet packages and a major PITA. Many questions arise: where transform will put the output file, what to consider a containing folder.

LeMP.exe as a command-line tool which already works anyway

I am planning to do that too as a part of build process outside of VS - in VSCode and on CI. For further convenience it can wrapped as a dotnet CLI tool. Then it can be installed similar to NuGet package, local to the project and called from the MSBuild task.

So... if the problem is small enough for MS not to fix for 10+ years, I guess I can accept it in my MVP.

Exactly!!!

Maybe this could be avoided with a tool like Sandboxie but again, T4 has the same problem.

I think educating the user via docs and readme will be enough and already far exceeding current T4 experience.

Update:

Couple of links with T4 struggles on the topic:

qwertie commented 3 years ago

I don't know what you mean by "a common thing for content files NuGet packages". If I choose "Add as Link" when adding an existing file from a different folder in Visual Studio, then... let's see...

var x = #get(#inputFolder);

In this case, the input folder is still the folder that contains the ecs file, not the project folder. And Visual Studio puts the transformed file into the input folder.

Please tell me more about this NuGet scenario you're thinking about. I don't use the command-line dotnet and msbuild tools, but if you use LeMP with those tools I would be interested to learn about what you did and how well it worked.

qwertie commented 3 years ago

About compileTimeAndRuntime, this is used to share code between compile time and runtime. Here's an example where you have an Order class you want to share:

// You might have `class Order` in a Order.cs file that is part of your project 
// and use compileTime { includeFile("Order.cs"); } to use it at compile time, 
// but if it's a small class (or if it uses EC# features) you might want to use 
// compileTimeAndRuntime instead:
compileTimeAndRuntime {
    namespace Company {
        public partial class Order {
            public string ProductCode { get; set; }
            public string ProductName { get; set; }
        }
    }
}
compileTime {
    using System.Linq;

    // (This is already provided as an extension method of Loyc.Essentials btw)
    public static string WithoutPrefix(string s, string prefix) =>
        s.StartsWith(prefix) ? s.Substring(prefix.Length) : s;

    // In real life you might read a file with includeFileText("FileName.csv")
    // and parse it at compile time, to produce a list or dictionary of objects.
    Order[] CannedOrders = new[] { 
        new Order { ProductName = "Tire", ProductCode = "#1234" },
        new Order { ProductName = "XL Tire", ProductCode = "#1236" },
        new Order { ProductName = "Black Rim", ProductCode = "#1238" },
        new Order { ProductName = "Red Rim", ProductCode = "#1240" },
    };
}
namespace Company {
    public partial class Order {
        precompute(CannedOrders
            .Select(o => quote {
            public static Order $(LNode.Id("New" + o.ProductName.Replace(" ", "")))() => 
                new Order {
                    ProductName = $(LNode.Literal(o.ProductName)),
                    ProductCode = $(LNode.Literal(WithoutPrefix(o.ProductCode, "#"))),
                };
        }));
    }
}

Output:

namespace Company {
    public partial class Order {
        public string ProductCode { get; set; }
        public string ProductName { get; set; }
    }
}
namespace Company {
    public partial class Order {
        public static Order NewTire() => new Order { 
            ProductName = "Tire", ProductCode = "1234"
        };
        public static Order NewXLTire() => new Order { 
            ProductName = "XL Tire", ProductCode = "1236"
        };
        public static Order NewBlackRim() => new Order { 
            ProductName = "Black Rim", ProductCode = "1238"
        };
        public static Order NewRedRim() => new Order { 
            ProductName = "Red Rim", ProductCode = "1240"
        };
    }
}

But while testing this, I discovered a couple of significant limitations of the C# scripting engine:

  1. Namespaces are not supported.
  2. Extension methods are not supported.

So I've added code to strip out namespaces and variable declarations marked with this. A warning is printed if you use a namespace, and an error is printed if you use an extension method, but the altered code is executed anyway.

dadhi commented 3 years ago

I don't know what you mean by "a common thing for content files NuGet packages"

When installed the content of NuGet package is put into "%USERPROFILE%\.nuget\libX\versionY\contentFiles\cs\netstandard\MyFile.tt"

Then it is included into the project via link. That's means the output will also go to the machine wide cache, plus it is a pain to include the ".ttinclude" files. Ideally it is something for user to add, so include files should be part of the project in version control, etc.

I don't use the command-line dotnet and msbuild tools, but if you use LeMP with those tools I would be interested to learn about what you did and how well it worked.

I will try to create a reference minimal example with T4 to illustrate the mess.

dadhi commented 3 years ago

So as I understood compileTimeAndRuntime is to define the entity for compile time in a single file, which both used by the compile time computation and as part of output. Ok, useful.

qwertie commented 3 years ago

Hmm, when I install NuGet packages I normally get a "packages" folder associated with the solution, instead of (or in addition to) the packages being put in %USERPROFILE%.nuget.

Unfortunately, the Visual Studio extension is using the ancient "Single-File Generator" a.k.a. "Custom Tool" APIs, which do not provide access to information about the current project (such as its location). No doubt there is a way to upgrade it with more modern amenities, but ... ugh, doing anything with VSIXs is horrible. Case in point, yesterday and today I spent several hours figuring out how to force Visual Studio to include DLLs in the VSIX that it was randomly excluding for no apparent reason so that the new compileTime feature would work in VS. And like, everything is controlled with MEF and other magic attributes, so when things go wrong - and things WILL go wrong - there's no way to debug it. Oh and you're supposed to easily run your extension in the Experimental Instance, but on my old PC that stopped working one day... even uninstalling and reinstalling VS completely did not help. So then the only way to debug was to laboriously uninstall the extension, quit VS, click Modify, wait for uninstall, install the experimental copy, restart VS... you get the idea. I assume this new PC will be able to debug again though. But I am not a happy camper.

But... VS/MSBuild variable expansions don't seem to work in the "Custom Tool Namespace" field where I have been shoehorning command-line arguments until now. For example --set:dir="$(OutDir)" just sets #get(dir) to the useless literal string "$(OutDir)". Perhaps it would work, though, if it was set up as an msbuild task...

qwertie commented 3 years ago

I just published a new version here.

The source code of the feature is CompileTimeMacro.cs.

dadhi commented 3 years ago

Great, that's something to experiment with.

Regarding the NuGet, since v4.0 and for modern sdk-based projects the packages are installed into the Windows user ".nuget" folder. Maybe some other things can influence this behavior (like legacy solution or existence of packages folder) but if you try to scaffold the new .NET Core 3.1 or .NET Standard project you will get what I get.

dadhi commented 3 years ago

@qwertie

I have started to put together a sample here https://github.com/dadhi/LempTest

and stuck with exception while using any of compileTime, precompute, etc. macros when doing lemp LibWithEcs\CompileTimeDI.ecs

LibWithEcs\CompileTimeDI.ecs(17,5,17,41): Error: LeMP.StandardMacros.rawPrecompute: FileLoadException: Could not load file or assembly 'System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
LibWithEcs\CompileTimeDI.ecs(17,5,17,41): LeMP.StandardMacros.rawPrecompute:    at System.Span`1..ctor(T[] array, Int32 start, Int32 length)
   at Roslyn.Utilities.StringTable.Add(Char[] chars, Int32 start, Int32 len)
   at Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.Lexer.ScanIdentifier_FastPath(TokenInfo& info)
   at Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.Lexer.ScanIdentifier(TokenInfo& info)
   at Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.Lexer.ScanIdentifierOrKeyword(TokenInfo& info)
   at Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.Lexer.ScanSyntaxToken(TokenInfo& info)
   at Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.Lexer.LexSyntaxToken()
   at Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.Lexer.Lex(LexerMode mode)
   at Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.SyntaxParser.PreLex()
   at Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.SyntaxParser..ctor(Lexer lexer, LexerMode mode, CSharpSyntaxNode oldTree, IEnumerable`1 changes, Boolean allowModeReset, Boolean preLexIfNotIncremental, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.LanguageParser..ctor(Lexer lexer, CSharpSyntaxNode oldTree, IEnumerable`1 changes, LexerMode lexerMode, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(SourceText text, CSharpParseOptions options, String path, ImmutableDictionary`2 diagnosticOptions, Nullable`1 isGeneratedCode, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScriptCompiler.CreateSubmission(Script script)
   at Microsoft.CodeAnalysis.Scripting.Script.GetCompilation()
   at Microsoft.CodeAnalysis.Scripting.Script`1.GetExecutor(CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunAsync(Object globals, Func`2 catchException, CancellationToken cancellationToken)
   at LeMP.StandardMacros.MaybeStartRoslynSession(LNode codeBlock, IMessageSink sink)
   at LeMP.StandardMacros.WriteHeaderCommentInSessionLog(LNode codeBlock, IMessageSink sink)
   at LeMP.StandardMacros.PrecomputeMacro(String macroName, LNode node, IMacroContext context, Boolean rawMode)
   at LeMP.StandardMacros.rawPrecompute(LNode node, IMacroContext context)
   at LeMP.MacroProcessorTask.ApplyMacrosFound2(CurNodeState s, ListSlice`1 foundMacros)

I tried to install System.Runtime.CompilerServices.Unsafe NuGet package but does not solve the problem.

The LibWithEcs is netstandard2.0.

qwertie commented 3 years ago

Confirmed. So I guess this is why Visual Studio has been adding "app.config" files in my projects without explanation. In the folder with LeMP.exe, create a file called LeMP.exe.config with the following code (courtesy VS):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

It's bizarre: my own code doesn't use this DLL, which implies Roslyn is explicitly depending on the wrong version and expecting every app that uses Roslyn to have a bindingRedirect to fix it.

dadhi commented 3 years ago

Thanks for the tip with the binding redirects, I did not think about adding them to LeMP installation, just tried on my project :/

Regarding Roslyn seems like the common problem, the Span and ValueTuple where added later and somehow MS decided to conform them for the older .net targets - who knew.

qwertie commented 3 years ago

I have updated the zip file with the necessary .config files.

dadhi commented 3 years ago

@dadhi

I will be putting found issues and/or questions here while exploring things. If you think it is better to handle this separately, please say.

Small things first:

image

image

dadhi commented 3 years ago

Could you suppress the warning CS7021 regarding the The C# scripting engine does not support namespaces. in LeMP? I've tried to suppress in the consuming project but without success: https://github.com/dadhi/LempTest/blob/87cf84636e4b4d990c024e376e740d81e08d9b1d/LibWithEcs/LibWithEcs.csproj#L4

qwertie commented 3 years ago

I don't want to overload this thread; please file separate bug(s) for these issues. --o-omit-comments should not affect newlines, and in other circumstances, doesn't. Preserving newlines has proven tricky, and there are often cases where it doesn't work quite right.

dadhi commented 3 years ago

@qwertie

I am stuck on the next code where the method body is generated from the string in compileTime

    var ecs = Loyc.Ecs.EcsLanguageService.Value;
    var getMethods = factories.Select(f => quote {
        object $(LNode.Id("Get_" + f.Key.Name))(IResolver r) =>
            $(..ecs.Parse(f.Value.Body.ToString()));
    });

I want:

        object Get_A(IResolver r) => new A();

But getting

        object Get_A(IResolver r) => _numresult(new A());

Could you please help? Was searching through the docs index, blog articles but without luck. The code is here https://github.com/dadhi/LempTest/blob/263312a7b9e56d42616679abdc71b8defad8a505/LibWithEcs/CompileTimeDI.ecs#L69

qwertie commented 3 years ago

Use expression-parsing mode:

using Loyc.Syntax; // for extension methods
Loyc.Ecs.EcsLanguageService.Value.ParseSingle("new A()", inputType: ParsingMode.Expressions)

Explanation of what happens: I originally designed Enhanced C# to support a Rust-like syntax where you'd return stuff by leaving off the semicolon, e.g.

int Sign(int x) {
    if (x > 0) 1 else if (x < 0) -1 else 0
}

I never actually supported the feature in any way, except that it is still supported in the parser, where an statement expression without a semicolon at the end is encoded in the Loyc tree by #result(expression). When printing in plain C# mode, the hash-sign # is encoded as _num. So, to avoid this you just tell the parser to expect an expression instead of a statement.

qwertie commented 3 years ago

It would be nice to record Q&As like this on StackOverflow, so if you ask the question again on SO - I'm subscribed to the lemp, ec# and loyc tags.

dadhi commented 3 years ago

I'm subscribed to the lemp, ec# and loyc tags

Cool, will post next questions there and thanks for the answer.

qwertie commented 3 years ago

Regarding "The C# scripting engine does not support namespaces.", you can't suppress that because it comes from the macro, not from the scripting engine (the latter produces error 7021 which of course can't be suppressed either, so the macro removes the namespace directive entirely) and LeMP currently has no system for suppressing warnings. So you could make a feature request about that... but I have no idea how warning suppression should be designed... by substring match, perhaps?

dadhi commented 3 years ago

but I have no idea how warning suppression should be designed... by substring match, perhaps?

may be somehow propagate #pragma warning disable csXXXX. No idea honestly.

dadhi commented 3 years ago

@qwertie Ok, seems like the initial question is answered plenty :) Thanks for your work! Closing the issue.