dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
19.04k stars 4.03k forks source link

Interactive Design Meeting 4/17/15 #2167

Closed kuhlenh closed 2 years ago

kuhlenh commented 9 years ago

Interactive Design Meeting 4/17/15

Design Team

The following individuals are members of the dream team: Anthony D. Green Chuck Stoner Dustin Campbell Kasey Uhlenhuth Kevin Halverson Kevin Pilch-Bisson Lincoln Atkinson Manish Jayaswal Matt Warren Tomas Matousek

Special Guests Mads Torgersen


Agenda: Async/await, scripting scenarios continued

The purpose of this meeting was to clarify an async/await design issue for our Interactive team as well as continuing our discussion on scripting scenarios from the last couple weeks.

Quotes of the Day

"#seed is right up there with #impregnate" "scripts manipulating other scripts?...it's incestuous really"

Async/Await in the REPL

Previously, we discussed that a user would not get the next prompt in the REPL while awaiting a Task. There was no technical reason why we couldn't let a user continuing typing, we just thought that it would fit the mental model of a REPL best if we used this design.

Note: this means that if a user types await f(); await g(); as a single submission, he will have a different experience than if he had typed them as separate submissions.

However, what if a user types an await, decides it's taking too long to complete, and tries to cancel the process? Currently, typing Ctrl+C actually kills the entire process, causing the user to lose all his previous context. There may be some solution around allowing a user to add a Cancellation Token as an argument, but we have decided to not prioritize this scenario right now (we'd rather have functional use of async/await and launch Interactive than wait to release until get this design right).

Scripting Scenarios continued...

init.csx file

In the last meeting we discussed allowing NuGet authors to include a init.csx file that could set up references and usings as well as provide some initialization code. However, we realized that our proposed method would require each init.csx-esque file to be language-specific. To remedy the pain of an author having to write 3 different scripts for C#, F#, and VB, we decided to tweak our design. NuGets can have a language-agnostic script (we will call it init.x for now) which will only contain references and usings. Authors can then additionally choose to write some initialization code in an init.csx, init.fsx, or init.vbx script. A developer's script will ignore #load-ed init scripts that are of a different language.

If a NuGet only has a C# init script (or no script at all), other authors can create separate NuGet packages that #load the C#-based NuGet and write their own init.vbx or init.fsx script to set up initialization code.

u global directive

In our last notes there seemed to be some general confusion around the global directive #u. We started off by taking another look at our solution and we've made some changes:

To avoid having a different directive in every language, we are changing the name of #u to #import. It's use it also more intuitive this way.

The purpose of the #import directive is to allow script and NuGet authors to "export" usings. For example,

Given a .dll "MyLibrary" with namespace N1 with a class C1 and a script S1 that references "MyLibrary": The author of S1 exports his using via "#import N1". The author of a new script S12 uses "#load S1" to seed his script with context from S1. S2 can now directly access class C1 without declaring any using directives.

This is especially helpful if a NuGet (e.g., WPF) has a lot of usings that will be difficult for users to remember and manually add at the top of each script.

public by default

We readdressed an old issue of whether or not everything in a script should be treated as public. Doing this allows users to #load scripts and directly access all the classes and variables. However, this means any 'temporary' variables a script author created to generate context will litter a developer's IntelliSense if he #load-ed that script.

We discussed several workarounds for this (e.g., placing temporary variables in a block, prefixing temporary variables with "_", allowing private), but in the end decided that this is not a top priority scenario at this time. Should problems come up in the future, we will revisit this problem. For now, using blocks to limit the scope of temp variables seems to be a good enough solution.

'into' keyword

A user can use the keyword into if he wants to isolate things from each other or wants to "discover" methods from a #load via IntelliSense.

For example, a user can do

#load a.csx into Foo
Foo.MethodA();

Instead of having into generate a "wrapper class," we think we should take the "wrapper identifier" provided by the user and essentially pre-pend it to every variable and class within the script. This would prevent creating a lot of duplicated code from arbitrary (transitive) #load-ing. A side-effect of this design is that a variable can be accessed via multiple qualified names (but we decided this is better than having multiple instances of the same variable).

These scenarios might help elucidate our design choices:

Scenario 1: Variable of the same name already declared (this will be an error)

Script a.csx

#load b.csx
var x = 1;

Script b.csx

var x = "x";

Scenario 2: Resolve ambiguity with a wrapper identifier

Script a.csx

#load b.csx into Stark
var x = 1;
//I can use my wrapper variable Stark to access b.csx's 'x' without error
var z = Stark.x;

Script b.csx

var x = "x";

Scenario 3: Access variable with nested dependency

Script a.csx

#load b.csx into Stark
var x = 1;
var z = Stark.x;
//I can access c.csx's variable 'y' via b.csx 
var w = Stark.y;

Script b.csx

#load c.csx
var x = "x";

Script c.csx

var y = 1;

Scenario 4: Multiple access points to variable

Script a.csx

#load b.csx into Stark
#load c.csx
var x = 1;
var z = Stark.x;
//I can access c.csx's variable 'y' via b.csx 
var w = Stark.House.y;
//I can access c.csx's variable 'y' via a.csx 
var v = y;

Script b.csx

#load c.csx into House
var x = "x";

Script c.csx

var y = 1;

Scenario 5: Pre-pend wrapper identifiers to prevent duplication

Script a.csx

#load b.csx into Stark
#load c.csx
var x = 1;
//These will reference the same 'x' as opposed to creating multiple instances of 'x'
var z = Stark.x;
var w = Lannister.x;

Script b.csx

var x = "x";

Script c.csx

#load b.csx into Lannister
var y = 1;
RichiCoder1 commented 9 years ago

We readdressed an old issue of whether or not everything in a script should be treated as public. Doing this allows users to #load scripts and directly access all the classes and variables. However, this means any 'temporary' variables a script author created to generate context will litter a developer's IntelliSense if he #load-ed that script.

We discussed several workarounds for this (e.g., placing temporary variables in a block, prefixing temporary variables with "_", allowing private), but in the end decided that this is not a top priority scenario at this time. Should problems come up in the future, we will revisit this problem. For now, using blocks to limit the scope of temp variables seems to be a good enough solution.

Haha, I'm reminded of javscript. Are we going to need to bring over ES6 modules ;-)?

tmat commented 9 years ago

@RichiCoder1 No. We've had much better solution in C# since the beginning - block scopes and classes.

adamralph commented 9 years ago

Nice work guys. I really like the revised into proposal. I have some questions:

  1. In var z = Stark.x; what is Stark? A local variable with an anonymous type?
  2. Will this work for methods? Stark.Foo();?
  3. What about classes defined in the loaded script?
  4. Has the decision been taken to discard return values from scripts?
khellang commented 9 years ago

If Stark is a "wrapper class", I'd assume

  1. To be a static class.
  2. Yes, it'll be a static method.
  3. They'd just be nested classes, so new Stark.Bar() would work.

I'm also curious about return values of submissions :smile:

adamralph commented 9 years ago

Instead of having into generate a "wrapper class," we think we should take the "wrapper identifier" provided by the user and essentially pre-pend it to every variable and class within the script.

glennblock commented 9 years ago

Great stuff, it is exciting to see this convo happening in such a transparent fashion!

Comments....

i.e.

//visible to the caller
export void DoSomething() {
...
}

//not visible
var foo;
khellang commented 9 years ago

in any of scriptcs that are loaded within the REPL

I :heart: that your muscle memory makes you write scriptcs when you really want to write scripts :wink:

glennblock commented 9 years ago

LOL! so true!

glennblock commented 9 years ago

@khellang aren't they synonymous? #ducks

adamralph commented 9 years ago

Ah yes, I also made a note to suggest #export instead of #import but forgot to mention it.

MgSam commented 9 years ago

Agree with #export rather than #import based on the given definition:

The purpose of the #import directive is to allow script and NuGet authors to "export" usings.

Calling it #import makes it seem like it should do the same thing as #load.

kuhlenh commented 9 years ago

@adamralph We are still working through implementation details on this (tbc in future notes)!

RichiCoder1 commented 9 years ago

RichiCoder1 No. We've had much better solution in C# since the beginning - block scopes and classes.

Was more referring to #import and #export. Very reminicient of ES6's modules proposal. Javascript rough equivalents would look like:

#load Start from "b"
var x = 1;
//I can use my wrapper variable Stark to access b.csx's 'x' without error
var z = Stark.x;
#load Stark from "b"
#load "c"
var x = 1;
//These will reference the same 'x' as opposed to creating multiple instances of 'x'
var z = Stark.x;
var w = Lannister.x;

and to build on @glennblock's example

//visible to the caller
export void DoSomething() {
...
}

//not visible
var foo;

which might be also:

//visible to the caller
void DoSomething() {
...
}

//not visible
var foo;
#export { DoSomething } // This would probably choke on overloads
khellang commented 9 years ago

I think the F# approach (pretty much what's mentioned in the issue) is nice:

If no access specifier is used, the default is public, except for let bindings in a type, which are always private to the type.

So top-level variables etc. will be public by default, but you can specify private on declaration.

paulomorgado commented 9 years ago

Regarding #import I'd like to reinforce what others have already said. It makes no sense having an #import that exports. #export (or even export) makes a lot more sense.

I'm also in favor of explicit exports. That way producers of scripts don't need to be worried with polluting the caller script and the caller script doesn't need to be aware of the loaded script - just the public façade.

It was not clear to me if the package author needs to author a script for each supported language or if C# scripts can import VB scripts and vice versa.

tmat commented 9 years ago

@paulomorgado #import does not export. It imports the members of the target namespace or type into the current scope, just like using does. The only difference is that the scope is the compilation not the file. Very much like VB project-level imports.

paulomorgado commented 9 years ago

That's what I had understood before and was confused now.

Nevertheless, it makes sense to me to export only what I want to export. And in that sense, it might make sense to export those namespaces instead of importing them into the compilation.

In the end, it's the same thing looked at from different perspectives.

paulomorgado commented 9 years ago

Regarding async-await, what is the execution context of the REPL?

Is it the UI thread? Can one create forms and interact with them?

Is there any type of synchronization context?

Since this is an interactive environment, if the task is not completed, the user could be questioned on what to do. (a)wait or continue.

If the user chooses not to (a)wait, then the whole line will be "ignored". await is supposed to work with hot tasks, but what if they aren't?

adamralph commented 9 years ago

Another thing we discussed in our regular scriptcs core team meeting recently was multi-language capability. I.e. a way for host to allow:

// a.csx
#load b.vbx into Foo

This would require resolution from a file name extension to a language specific implementation, unless some other language feature were introduced to allow specification of the language implementation in code.

.csx and .vbx could have support out of the box, with pluggable support for other languages.

paulomorgado commented 9 years ago

Or have a convention on the first line of the script file to specify the scripting engine.

Or have an argument to #load to specify the scripting engine.

adamralph commented 9 years ago

Or have an argument to #load to specify the scripting engine.

That was what I had in mind with the 'language feature' suggestion. I can't really think of an elegant way of doing it though.

tmat commented 9 years ago

We don't plan on supporting multiple languages in a single script. Note that #load-ing of a script is similar to #include in C++, the target language has to be the same.

adamralph commented 9 years ago

Note that #load-ing of a script is similar to #include, the target language has to be the same.

I understand that that is the case with a plain old fashioned #load, but in the case of #load .. into (perhaps the directive should be something other than #load) we have an opportunity to supply and consume scripts as packages (effectively obsoleting scriptcs script libraries) and build something analogous to other script based platforms, e.g. node, Ruby, etc. This would be way smoother if each package has to be written only once in its language of choice, rather than having to include a version for each target language.

tmat commented 9 years ago

@adamralph #load into as currently designed isn't much different from #load. All it does is prefix the names of the members declared on top-level of the target script with the identifier specified in the into clause.

Correct me if I'm wrong, but I don't think you can import Ruby script from Python and vice versa (unless of course on DLR :))

adamralph commented 9 years ago

@tmat I asked for an explanation of the 'wrapper identifier' some time ago (https://github.com/dotnet/roslyn/issues/2167#issuecomment-95040213) but no-one has answered yet.

The Ruby/Python case is completely different. Those are two distinct runtimes. In the .NET case we are dealing with a single runtime but multiple languages, a design tenet of .NET from day one. This is illustrated by C# and VB and has encouraged a huge number of other language implementations. For scriptcs we already have several other language engines, some serious (e.g. https://github.com/scriptcs-contrib/scriptcs-fsharp) and some fun (e.g. https://github.com/filipw/ScriptCs.Engine.Brainfuck).

I'm not suggesting that the scripting runtime necessarily needs to support this out of the box, but it would be of enormous benefit if we could hook into the language services (i.e. #load .. into) and provide it ourselves.

tmat commented 9 years ago

@adamralph I'm confused, The design notes say exactly what I said above: "Instead of having into generate a "wrapper class," we think we should take the "wrapper identifier" provided by the user and essentially pre-pend it to every variable and class within the script. This would prevent creating a lot of duplicated code from arbitrary (transitive) #load -ing."

Yes, .NET is a single runtime and the common target for languages is IL/metadata and common type system. We don't support creating projects that mix languages because that requires that a) the languages involved have semantically similar top-level declarations that mix well with each other b) there is a common interfaces for all compilers thru which they can cooperate.

That doesn't mean you can't interop between scripts written in different languages. The means for interoperability are going to be metadata references. The current extensibility enables host to interpret the content of #r directive as it wishes to. I can imagine the host having some registry of supported compilers/languages and then allow scripts to reference scripts written in another language via #r. For example, #r foo.vbx from C# script would compile foo.vbx into an in-memory assembly using VB compiler and pass it to the C# script as a metadata reference.

This is different from #load since #load provides source "merging".

glennblock commented 9 years ago

If you are pre-appending, this will mean all references will be pre-appended as well right?

Ie if I have Foo() and Bar() and Foo calls Bar and is loaded into Baz then the call to Bar will become Baz.Bar()? Is '.' even valid in a member name?

One thing I dislike about this that the wrapper brings is automatic encapsulation of loose variables. Sure the variables are pre-appended so they should not conflict, but there are still able to be accessed and mucked around with. That is unless you explicitly create your own class that you include the members in.

Glenn On Sun, May 17, 2015 at 5:01 PM Tomáš Matoušek notifications@github.com wrote:

@adamralph https://github.com/adamralph I'm confused, The design notes say exactly what I said above: "Instead of having into generate a "wrapper class," we think we should take the "wrapper identifier" provided by the user and essentially pre-pend it to every variable and class within the script. This would prevent creating a lot of duplicated code from arbitrary (transitive) #load -ing."

Yes, .NET is a single runtime and the common target for languages is IL/metadata and common type system. We don't support creating projects that mix languages because that requires a) that the languages have structurally and behaviorally similar top-level declarations that mix well with the other languages b) there is a common interfaces for all compilers thru which they can cooperate.

That doesn't mean you can't interop between scripts written in different languages. The means for interoperability are going to be metadata references. The current extensibility enables host to interpret the content of #r directive as it wishes to. I can imagine the host having some registry of supported compilers/languages and then allow scripts to reference scripts written in another language via #r. For example, #r foo.vbx from C# script would compile foo.vbx into an in-memory assembly using VB compiler and pass it to the C# script as a metadata reference.

This is different from #load since #load provides source "merging".

— Reply to this email directly or view it on GitHub https://github.com/dotnet/roslyn/issues/2167#issuecomment-102871813.

tmat commented 9 years ago

@glennblock Yes. '.' is valid in metadata member name. We can also mangle the metadata name arbitrarily if needed.

Correct, top-level variables are also accessible thru dotted name. There are several options how to make them truly local to the script - declare them in a block, method, class, lambda, etc.

tmat commented 9 years ago

@glennblock If the above options for script variable encapsulation turn out to be insufficient we can add another one (e.g. some modifier). For now we think they are good enough.

glennblock commented 9 years ago

Thanks. The lack of encapsulation may not be an issue as if one needs to they can have a class. On Sun, May 17, 2015 at 7:38 PM Tomáš Matoušek notifications@github.com wrote:

@glennblock https://github.com/glennblock If the above options for script variable encapsulation turn out to be insufficient we can add another one (e.g. some modifier). For now we think they are good enough.

— Reply to this email directly or view it on GitHub https://github.com/dotnet/roslyn/issues/2167#issuecomment-102899728.

glennblock commented 9 years ago

Class, lambda etc On Sun, May 17, 2015 at 10:35 PM Glenn Block glenn.block@gmail.com wrote:

Thanks. The lack of encapsulation may not be an issue as if one needs to they can have a class. On Sun, May 17, 2015 at 7:38 PM Tomáš Matoušek notifications@github.com wrote:

@glennblock https://github.com/glennblock If the above options for script variable encapsulation turn out to be insufficient we can add another one (e.g. some modifier). For now we think they are good enough.

— Reply to this email directly or view it on GitHub https://github.com/dotnet/roslyn/issues/2167#issuecomment-102899728.

adamralph commented 9 years ago

@tmat thanks for the info. I hope you understand why some explanation was/is still required. The comment history shows I'm not the only one who requires it. :wink:

I still don't understand how this is going to work. Although . is valid in a member name, the only way to refer to the member in C# is by using a unicode escape:

Foo\u002EBar();
tmat commented 9 years ago

@adamralph The name in source does not necessarily need to be the same as metadata name. In source you'll refer to it as Foo.Bar(); in metadata it will be a method Foo.Bar on type <Script>.

adamralph commented 9 years ago

But I can't write Foo.Bar(); in C# where Foo.Bar is a member name. It won't compile. The best I can do is Foo\u002EBar(). Am I missing something?

glennblock commented 9 years ago

@admaralph they own the compiler, they can make it do whatever they want. :-) On Sun, May 17, 2015 at 11:56 PM Adam Ralph notifications@github.com wrote:

But I can't write Foo.Bar(); in C# where Foo.Bar is a member name. It won't compile. The best I can do is Foo\u002EBar(). Am I missing something?

— Reply to this email directly or view it on GitHub https://github.com/dotnet/roslyn/issues/2167#issuecomment-102940653.

paulomorgado commented 9 years ago

@glennblock, just because they can, doesn't mean they should.

I can understand avoiding the collision of a #load with existing classes and thus not generating a wrapper class.

But if the collision would exist, then a name collision will still exist.

Given a s.csx script file with:

int GetMeAnInt()
{
    ....
}

loaded with:

#load s.csx into X

and a class:

public static class X
{
    public static int GetMeAnInt()
    {
         ...
    }
}

which one would X.GetMeAnInt() in the script refer to?

tmat commented 9 years ago

@paulomorgado You'd get an error that X has already been declared at the declaration of class X.

adamralph commented 9 years ago

Joking aside, I did wonder if a new language feature was being proposed here. I can't imagine that allowing . in an identifier would be an easy, if at all possible. let alone desirable, thing to achieve.

Indeed, from the original proposal above:

Scenario 2: Resolve ambiguity with a wrapper identifier

//I can use my wrapper variable Stark to access b.csx's 'x' without error
var z = Stark.x;

Wrapper identifier, wrapper variable - what is this thing?

Assuming the compiler isn't going to allow Foo.Bar any time soon, where Foo.Bar is an identifier, we're back to my original question:

  1. In var z = Stark.x; what is Stark? A local variable with an anonymous type?
paulomorgado commented 9 years ago

@tmat, the class X could have been declare before. But that would only result in a different error.

I'm still missing the point of not having a wrapper class instead.

glennblock commented 9 years ago

Agreed the identifier gives me an uneasy feeling / feels a big hacky.

CyrusNajmabadi commented 2 years ago

Closing out. We don't need active issues for meeting nodes.