dotnet / vblang

The home for design of the Visual Basic .NET programming language and runtime library.
290 stars 64 forks source link

Inferred Type- and Method-Declarations ("Top-Level Code") #446

Open AnthonyDGreen opened 5 years ago

AnthonyDGreen commented 5 years ago

This proposal is a follow-up to the idea first introduced in #155 and #102

The goal of the proposal is to provide a new tool in VB.NET through which VB abstraction authors, e.g. library authors, educators/documentation writers, senior-developers/project-leads can create powerful yet streamlined experiences for end-developers requiring minimal or no boilerplate (ceremony) beyond use-case specific code. By removing the requirement for explicit type declarations, method declarations, and the like abstraction consumers (end-developers) can focus their attention on precisely the code that matters making those scenarios more approachable, more productive, and/or simply more enjoyable.

Here's a simple example of the syntax, with motivating scenarios to follow. The code depicted represents the entire code content of the program; nothing has been omitted.

Example 1 - Simple program

Console.Clear()
Console.Write("Enter your name: ")
Dim name = Console.ReadLine()
Console.WriteLine("Hello, " & name)

A note on "VB Interactive", "script" files (.vbx), and the scripting engine

There have been prior proposals from circa 2012 for supporting top-level code related to the VB Interactive window, .vbx (script) files, and the Roslyn-based scripting engine. This proposal is unrelated to those proposals and the prototype does not build off of them. The key difference between what's proposed here and those proposals is that this is not a separate scripting dialect of VB.NET. It doesn't require a different extension or parsing options. It's just an extension to vanilla VB. In several other places the designs diverge, e.g. as there are no scripts, there are no "script locals"--there are fields, which can be referenced by top-level methods and plain locals, which can't--and all of the top-level code in a project isn't stitched together implicitly to form one large method. I was not involved in the majority of the design for the proposed .vbx dialect, which happened ~2011 and this proposal reflects my current thinking on a lighter-weight alternative. I do not believe that integrating top-level code directly into the language prohibits application scripting or interactive at a later time.

Motivating Scenarios

The above shows syntax but the feature is motivated far beyond "Hello, World!" programs. Using a prototype I have constructed 5 example scenarios and recorded a series of videos demonstrating them (~5-minutes each) showing a breadth of applications from educational to professional and from traditional desktop applications to the web. Each video has a companion blog post which expands on the scenario depicted and why it matters. They are as follows:

The first four are built solely using components of the proposal while the last combines it with the JSON Literals proposal. This is not an exhaustive list and other potential examples not explored include (but are not limited to):

I. Inferred Type Declarations ("Top-Level Declarations")

The proposed feature would allow type member declarations (methods, properties, fields, etc.) as well as Inherits and Implements statements to appear directly in a compilation-unit (source file) without an explicit enclosing type declaration (class, module, etc). In such case a type declaration will be inferred, having a name inferred from the (file)name of the containing source file.

Any member declaration at the compilation-unit level would be declared as a child of this inferred type declaration and any Inherits or Implements statement would be considered to specify the base type and/or implemented interfaces of this type. In all other respects this is an ordinary VB type indistinguishable from one with an explicit declaration and may be combined with other partial definitions!

Example - VB6-style "classless" code-behinds

Sub Button1_Click()

End Sub

Sub TextBox1_TextChanged()

End Sub

Design Issues

If all top-level members in the file become members of the inferred type declaration, does that mean that top-level type declarations are now implicitly nested once a top-level Sub is added?

Example - CodeSnippet.vb

' Is this Global.SomeUtilityThing or CodeSnippet.SomeUtilityThing?
Class SomeUtilityThing
End Class

Shared Sub Main()
    Console.WriteLine(New SomeUtilityThing)
End Sub 

ADG: I recommend nesting. Given the idea of self-contained documentation snippets I'm kinda inclined to prefer the nested scope. It's very much the same as if all those members were just dropped into a Module. If you wanted to "escape" that behavior the way to do it would be to either move the type definition to another file or potentially be to wrap it in an explicit namespace declaration. If the decision goes the other way forcing nesting would be much harder to express: moving the type to another file AND wrapping it in a partial class.

What about Modules? They can't be nested today so if a module appears in an inferred type declaration what happens?

Options

  1. The module obviously can't be nested so it just have it fall out.
  2. Make it an error.
  3. Lift the restriction on modules being nested.

ADG: My preferences on risk/cost are 2, 3, 1. If it's an error now it's consistent with the existing language and 3 can still happen later. 3 is interesting and there's probably a proposal for it somewhere because it would allow localized extension methods. Option 1 is the worst because it either precludes 3 from happening later or if 3 does happen later the behavior still can't be changed and is now inconsistent with the decision of the previous issue.

What if a namespace appears in a file with an inferred type declaration?

Options

  1. The namespace obviously can't be nested so it just have it fall out.
  2. Make it an error.
  3. Invent class "inner namespaces".

ADG: I really like the idea that a single file can be as self-contained as desired. If you consider Scenario B that kind of documentation is broken if it requires an namespace for some reason, and namespaces are disallowed. I prefer Option 1. Unlike in the previous issue I think 3 will likely never happen.

How do you parse top-level fields vs top-level local declarations?

It's a field if has field modifier, local otherwise, Dim and Const prefer local.

Note that this still allows you to declare type-level constants, you just have to explicitly add a class-level only modifier like Public.

ADG: The native compiler actually parsed local and field declarations the same way with a single type representing both. When the Roslyn VB parser was written I made the decision to have two different types for the two concepts even though structurally they are 100% identical. This briefly created a problem because after parsing a method or property header it's unknown whether the declaration was one-line like an auto-prop or MustOverride member and the next Const is a "field" or if you're in the body now and that should be parsed as a local. David (Schach) just made the parser more context aware here around MustOverride members. In the case of properties there is no keyword to signal user intent but we decided that since a local declaration would still be illegal even in an expanded property that it was fine to always parse it as a field declaration.

My point is, there's nothing new here. It would mean not re-using the ReadOnly modifier for read-only locals if that ever happens in VB (which I kinda hope it won't). But if you want to defend against that just require ReadOnly fields in inferred type declarations to also use another unambiguous modifier like Shared or Private.

Should it always be a class?

Almost certainly

I went with a class because of the inheritance Scenarios C, D, & E. But someone noted it does produce awkwardness if you're not doing any inheritance because you have to explicitly mark things as Shared to be able to call them from other places in your program or to just explicitly define Main. What's most important for the "no ceremony" thing is that the top-level code and the top-level members both be either instance or Shared so they can interact without qualification.

Could say the inferred type declaration has no kind so that it could be a partial part to a structure/module/etc (maybe).

ADG: I don't think there's any scenario for an inferred type declaration becoming an Interface, Enum, or Delegate, and the Structure scenario is very weak. But the module case is arguable. One could go with the design that in the absence of either an explicit base class (or project default) that the code should just be a Module.

This isn't necessary for an inferred type declaration to be the start-up type in a project so the only reason to do it is if it's anticipated/desired for inferred type declarations to be the way most modules are declared and a lot of them have a magic Shared Execute method on them for unknown reasons. I don't think the inferred module scenario is as valuable as it seems at first in the long run and it's nothing compared to the inferred class scenarios.

Can I define extension methods in an inferred type?

No special rules here. If it's a class, then no, until such time as extension methods are allowed in classes.

Inferred type attributes?

See "VI. Type and Method Attribute Target" below.

Inferred type modifiers?

In VB types are Friend by default, heritable, and Partial isn't required on every definition. How will that effect the utility of this feature?

Another way of putting the question is, should making the type Public sacrifice all of the brevity of the feature at large? I tend to think no. In fact, in Scenario E I ran into an issue where my inferred controller wasn't being recognized by MVC routing because it wasn't Public. It was straightforward to work-around it by amending the routing rules to find non-public controllers but there could be other scenarios where this doesn't work. WinRT as I recall requires types to be NotInheritable, for example.

One pretty egregious work-around is to simply say that the inferred type declaration is like a partial declaration with no modifier. This means if you want to override the behavior you just declare a partial class elsewhere with the modifiers (and attributes) you need.

Another solution is some alternate syntax for specifying modifiers such as a pseudo-attribute:

<Type: Public>

' All of my logic goes here

ADG: I don't hate this idea but I need to stare at it for a long time.

The last option, which gets more compelling by the end of the document is described in "VII. A Magic Attribute That Lets The Abstract Author Prescribe Various Things"

How is the inferred type name inferred?

The compiler starts out with a full path. Any algorithm can be decided from that and it's a matter of deciding which parts to care about and which parts not to care about. But consider a few things:

  1. Most files will have a .vb extension
  2. Eventually someone may decide that .vbxhtml, or .vbdoc or whatever is an extension that helps VS out even if the compiler processes it normally (the compiler does not care about extensions).
  3. It may be the case, similar to Scenario D and Scenario E, that there's a future where these inferred method declarations are where markup is mostly kept and there are still code-behind files. In which case they may be named MainWindow.designer.vb or MainPage.markup.vb.
  4. Some people use dotted names as a convention for nested types, MyCollection.Enumerator.vb.
  5. Filenames may not be valid identifiers for various reasons.
  6. Source files can be linked (e.g. Shared Projects) and so its path may have no relation to its place in the current project.

With all that said what I prototyped is simple, take the path, get the filename, split it on dots, take the first part. Nothing clever to try and figure out a namespace from the folder structure, no inferring a nested type from dots, no special treatment of the .vb extension. If the name isn't a valid identifier it can either be an error or the compiler can do substitutions within reason like spaces with underscores or non-id characters with their unicode value or something. I'm open to suggestions.

Can there by multiple partial inferred type declarations per type?

Sure!

Actually, there's just nothing to stop this. If the name is inferred from the path but without using every bit of the filename then both Index.vb and Index.markup.vb would infer the same name and be considered two parts to the same class. That's nothing special, it just "falls out" of how types are built. What will fail is if both parts define members with the same name/signature (like with explicit type declarations). This means that only one file gets to have an inferred method declaration since, as currently proposed they would both declare a method with the same name and signature. But as long as it's just non-conflicting top-level member declarations explicit top-level member declarations in one file and an inferred method declaration in another there's no problem here.

Can an Inherits or Implements statement appear anywhere in the file or must it be at the top?

As prototyped, this is not a requirement and it's not technically problematic or expensive to go either way. It does make it harder to locate the Inherits or Implements statement for API consumers, including the IDE.

Given that inheritance affects name lookup it makes sense for the Inherits statement to precede any members or executable code. But if I did define any types in a separate namespace I could easily see myself putting such declarations before the Inherits statement or waaaaaaaaaaaaaaaay at the bottom of the file:

Namespace Global.MyApp.Utilities
    Class StringUtils : End Class
End Namespace

Inherits StringProcessor

Return StringUtils.M(arg)

Maybe we split the difference, a compilation-unit becomes:

ADG: I feel like "at the top" is best but I don't have a strong case on whether that should be a rule or a style thing. I would prefer CompilationUnitSyntax to have explicit properties for these. Because of Scenario B I'm inclined to let the needs of the documentation writer dictate what goes where, though obviously that argument could as easily be made for Imports and Option statements so...

II. Inferred Method Declarations ("Top-Level (Executable) Statements")

The proposal feature would allow executable statements (code which traditionally may only appear within method/accessor bodies) to appear directly in a compilation-unit (source file) without an explicit enclosing method declaration. In such case a method declaration will be inferred. Note that this inferred method declaration still constitutes a member declaration and as such its existence may itself cause an inferred type declaration. It is NOT proposed to allow executable statements to appear directly within a explicit type declaration.

The base class of the containing type of such a declaration may designate a particular Overridable method to be overridden by this method, in which case the inferred method declaration will infer the same name, signature, and accessibility as the method being overridden. Otherwise the name, signature, and accessibility shall be some set of defaults to be determined.

Design Issues

Can top-level members reference top-level (non-field) locals?

No

Dim startTime = Date.Now

Sub M()
    ' ERROR: 'startTime' not defined.
    Console.WriteLine(startTime)
End Sub

Fields are fields and locals are locals.

What about Field/Property initializer execution order when interleaved with executable code?

Field and Property initializers execute normally (during construction) rather than when any inferred method declaration is executed

We could rewrite field initializers to execute inside of inferred method declarations so that this code works.

Imports System.Console

Write("Enter a number: ")

' This statement will run when the object is created, not after the previous statement runs.
Private response As String = ReadLine()

WriteLine("You've entered " & response) 

But why? This code could have just as easily used a local and gotten the correct behavior. If we change the behavior of fields it weakens their utility for the use cases they are appropriate for. Scenario C makes use of a lot of fields and properties that would be broken by this idea.

ADG: Today conventionally we put fields and properties at the top of the declaration rather than intermixing them with methods in part to signify that they're special. I think this is more of a style thing.

Does the inferred method have the Async and/or Iterator modifiers?

Syntactically Await and Yield are always keywords inside of an inferred method though semantically they may be invalid.

This is completely new code so there's no reason for Await to be an identifier. Yield is arguable for finance apps but for now it's the same way. The question isn't whether those parse as keywords, they always will, but rather whether it's legal to use those keywords inside such methods.

VB does not at this time support Async Sub Main. In part I initially thought of this proposal as a possible alternative to making approaching async easier. While this proposal would do so I don't think it necessarily precludes supporting a proper Async Sub Main. Certainly, the documentation scenario says there will be cases where Await is valuable in these methods. Scenarios B, D, and E all could have used Await.

In the case that the inferred method overrides a base method this is easy enough. The method just has to return the correct type:

If base method returns...

Alternately, see "VII. A Magic Attribute That Lets The Abstract Author Prescribe Various Things".

In the case that there is no base method there are a few options:

  1. Always generate an <Async Function() As Task(Of Object)> and suppress diagnostics if the function doesn't return a value or fails to use Await.
  2. Infer the signature based on the content of the method.
  3. Always generate a synchronous Sub (or <Function() As Integer>).

Option 1 gives good flexibility in most cases because whoever calls this code can use or not use the return value if any and/or wait for the method to execute synchronously.

Option 2 is the most flexible but is problematic since the method signature, specifically its return type can change based on its content. That's unprecedented in the compiler and I'm not enthusiastic about breaking new ground here.

Option 3 is the simplest and means in the very simple case, a simple program won't be able to use Await which is important when learning about I/O in .NET.

Another way of looking at the problem is this. If Async Main style tutorials are the key scenario to consider, we can get that for free with appropriate base-types in a library (e.g. the VB runtime). Just set the default base type to e.g. Microsoft.VisualBasic.ApplicationServices.AsyncConsoleApp and those programs will work fine even if we go with Option 3. The question is whether to require that or whether the compiler does the work that that base class would have.

ADG: When I consider that possibility--shipping pre-built base types for inferred classes, I wonder if the case where there's just a Execute method with no base type is worth pursuing at all because all of those scenarios can be hit with the right base-classes.

Are Exit Sub, Exit Function, Return statements valid in an inferred method?

As long as they are of the right kind for that method, yes, it's an ordinary method.

If the inferred method is a function, is the return variable valid to use inside?

Yes, it's an ordinary method.

The method has an utter-able name; it's not like an Operator or a lambda. It's arbitrary to disallow this in this one case and the compiler has a lot of assumptions about functions always having one.

Does an inferred method constitute user-code for the purpose of CallerInfo attributes?

Yes, it's an ordinary method..

This isn't a compiler-generated method-body (like an auto-prop) invoking a method; the code that causes the inferred method to come into being is user-code. Crippling CallerInfo in this case serves no one.

Can you overload the inferred method?

Yes, it's an ordinary method.

How does GetDeclaredSymbol work?

In order to get the Nav bar working here's what I implemented. The inferred method uses the CompilationUnitSyntax as its declaring syntax because there is no other SyntaxNode that would work. The inferred type also uses the CompilationUnitSyntax as one of its declaring syntax references. So, obviously consumers will pass those nodes to the SemanticModel to get back to the ISymbol for those entities. When passing a compilation-unit node to GetDeclaredSymbol here's what you get back:

All of these situations are new and some invariants have been stretched or broken. As I recall, GetDeclaredSymbol either always returns null for a given SyntaxNode type or always returns non-null. In places like For loops where the i in For i = 1 To 10 could be declaring an i or referring to an existing one we opted to just say it never declares a variable but rather always refers to one (though that one may also be implicitly declared) rather than sometimes returning something and sometimes not.

Further, the type of symbol returned for a given syntax type has always been one kind and in this case it's one of two. But, CompilationUnitSyntax is/can be pretty special. We'd have to look at the impact on IDE code (and others) to understand whether this design is elegant or a nightmare for API consumers.

ADG: Open to feedback and suggestions on this.

How does the language determine which method an inferred method declaration will override?

Options

  1. All inferred method declarations have the same name, e.g. Execute, and will override a base method by name-only. If more than one method named Execute exists in the base classes it is ambiguous.
  2. Methods intended to be overridden by an inferred method declaration can be marked by an attribute.
  3. Base types with methods intended to be the default override can be marked with an attribute specifying the name of the method.

It's important that the compiler check each level of the hierarchy in turn so that a more derived type can change the default override method. Scenario D does this when on file inherits Layout and defines another placeholder for content.

I started out with Option 1 but the name appearing in the nav bar is intuitively the most informative piece of information you can have when dropping code into one of these overrides. Requiring all of them to use the generic name Execute instead of meaningful names seems wrong.

Then I moved to Option 2 with the DefaultOverrideMethodAttribute idea, which is better. The only problem is that it requires enumerating all methods and realizing their attributes to determine what to override.

Option 3 is more inline with how indexers work. There's an attribute applied at the type level that tells you the name of the property that is the default property for that type. This would simplify the code to find such a method a lot and there are other things which might go into such an attribute, see "VII. A Magic Attribute That Lets The Abstract Author Prescribe Defaults"

ADG: I prototyped Option 2 but my inclination is toward Option 3, at this time.

III. Pattern-Based Non-Form-Derived Start-up Types

Startable non-Form types is demonstrated in Scenario B video and in it different types are specified as the start-up type at different points in the video to show why you might use a different type at different times. Other scenarios might include test runners/harnesses, etc., in non-documentation projects.

Today the entry-point to an executable VB program is determined one of several ways:

In the case that it is specified with /main: the type specified or one of its base-types must declare a valid Main method OR if the type inherits System.Windows.Forms.Form (directly or indirectly) an entry point will be generated, roughly equivalent to the following:

Class SomeFormInMyProject : Inherits Form
    Shared Sub Main()
        #If <My.Forms is available> Then  
            Application.Run(My.Forms.SomeFormInMyProject)
        #Else
            Application.Run(New SomeFormInMyProject)
        #End If
    End Sub
End Class

I propose generalizing this special case further:

This indirection is necessary for several reasons. First, it allows the Run method to do any scenario-specific setup and control exactly when the top-level code gets run and what happens before or after and what arguments, if any get passed to it.

In Scenario C the Execute method isn't run once at the start of the program, it's actually run every frame and "starting" the app is independent of invoking that method, though in that scenario this is accomplished by the fact that the game loop is implemented as a type deriving from Form so the existing start-up code that calls Application.Run is called.

Another scenario that would benefit from this is Async Main. The compiler would only need to call (e.g.) AsyncConsoleApp.Run(New Program) and the Run method would have the smarts to call ExecuteAsync().GetAwaiter().GetResult(). It also means that a different base class could use a different scheduling strategy and could provide its own message pump, if so inclined.

Secondary Proposals

IV. Default inferred type base class compilation option

Default base types for inferred type declarations is demonstrated in Scenario D above.

I propose a new compiler option allowing one to specify the fully-qualified name of a type to be used by default as the base type of an inferred type declaration if and only if no other base type is explicitly specified either within the inferred type declaration or any of its other partial definitions.

Note: That just as with project-level imports this name can specify a generic instantiation so /defaultinferredtypebaseclass:System.Collections.Generic.Dictionary(Of String, Object) is perfectly valid.

While one could always specify a base type explicitly, that constitutes boilerplate; especially in projects where a significant number of files use inferred type declarations. In Scenario D potentially dozens of types might all inherit from View. In WinForms the user-file is kept uncluttered like this by putting the Inherits statement in the Partial declaration in the designer-generated file by default.

ADG: For a feature that aims to reduce boilerplate, mandating a second file with a partial declaration for every inferred type would defeat the purpose. Further, it would actually be pretty confusing in situations like Scenario B where the content of the file benefits most from omitting setup code (like namespace declarations, Imports statements, and Inherits statements) which would otherwise appear out of place in the "document".

Design Issues

What should it be named?

V. Implicit Return Expression Statements ("Top-Level Expressions")

Implicit return expressions are demonstrated in Scenario D and Scenario E above.

Example - TopBuyers2018.vb

    From o In db.Orders
   Where o.PurchaseDate.Year = 2018
Group By o.Customer
    Into Group, GrandTotal = Sum(o.Total)
Order By o.GrandTotal Descending
    Take 20

When combined with VBs existing MyGroupCollection extensibility this becomes a very fast way to build up suite of queries because the compiler itself already knows how to collect all types of a given base class and manage their instances: My.Queries.TopBuyers2018 could come into existence simply by defining this file.

I propose that some set of expressions in VB are often of sufficient size and complexity to both be the central content of their containing method AND to benefit from having the maximum horizontal space for their use, particularly in an inferred method declaration, and that it should be permissible in an inferred method declaration (at least) to use such expressions in an expression statement and have that statement act as an implicit return statement returning the value of that expression.

Specifically, expressions that I believe fit this criteria at this time include:

I also believe that if that proposal were accepted, JSON literal object and array expressions could fit this criteria when writing web services. This allows for an immersive authoring experience and, in the case of an XML literal may have practical implications on the output because indentation contributes whitespace to the constructed object.

Functional languages like F# lean hard into this and I don't think VB will or should ever go that deep but there could be some measured toe-dipping here.

Design issues

Should it be all expressions?

No.

Practically it can't be because there are some ambiguities between expressions and statements. The most well-known example is a = b which is either an assignment or a comparison depending on how it's interpreted. But also, from a parsing (and reading) perspective, the first token of most valid VB statements is a keyword describing the kind of statement it is or an identifier for assignments and invocations. There are exceptions, such as numeric labels, but it's pretty unknown what ambiguities arise if that convention is dropped entirely.

In the case of LINQ or a hypothetical JSON literal there is no ambiguity, these are safe. But in the case of a top-level XML elements it's ambiguous whether a line starting with < is the beginning of an XML literal or a attribute specification and I haven't yet thought my way out of how to resolve it elegantly. Even with an XML namespace it looks the same as an attribute target specifier and I don't we would want to key the parsing on whether the namespace prefix is a valid target anyway as that would tie our hands in the future if more attribute targets are added. It's not ambiguous with XML documents though, which is good which is why Scenario D specifically uses whole documents.

ADG: I mean, come on, DOCUMENT is in the name; they're supposed to take up the whole file :)

Arguably it could be fine for parenthesized expressions, which would consequently let all the other expressions in. This is something that was discussed for VB Interactive submissions as well as the Immediate Window. It would have to be weighed against any possible target-type-inference/expression-classification issues (e.g. if the return type of the containing method is delegate/array-literal/interpolated string).

ADG: It would make it marginally easy to write a VB expression evaluator but once the tool author has to wrap every expression in parentheses to do it it doesn't matter that they also have to wrap them in a Return statement, plus the scenario is very narrow, so I don't think parenthesized expressions are worth it.

Should this be expanded to cases outside of inferred method declarations?

Maybe.

ADG: It would give a consistent experience in Scenario D-like situations where multiple portions of content are overridden in the same file

Does it have to be the last expression in the compilation-unit and and can it be inside a block?

There's potentially some ambiguity with XML elements because "technically" VB static local variable declarations can have attributes, but they are ignored is provided, aren't actually bound, no tooling, don't even have to refer to real attributes. I believe it's a parsing artifact from the native compilers which was maintained for back-compat. If the attribute appears on a non-static local it's an error, so it could probably be worked around.

ADG: In Scenario E, Scott's original code had all of the I/O wrapped in a Using block to properly dispose of resources. I rewrote it to use an iterator in part to work-around the limitation of the prototype not allowing this. My gut says to wait for more scenarios before pursuing this.

VI. Type and Method Attribute Targets

Because under this proposal, type and method declarations may be inferred there's no place to attach attributes which would otherwise appear on an explicit declaration. Similar to the approach one might take when, say, applying an attribute to the implicitly declared backing field of an auto-prop, this would be a good scenario to extend VBs existing Assembly and Module attribute target specifiers to include some form of Type and Method targets. They would still appear at the file-level but would be taken to apply to the inferred declarations.

At this time I don't have concrete scenarios for this at the top of my mind with the exception of Scenario E. Because ASP.NET Core can also make use of attribution to describe URL routing it would be valuable to be able to specify those attributes on a stand-alone file. In my demo I designed my base classes such that this wasn't necessary but there are certainly other situations out there.

Type and Method attribute targets are orthogonal to this proposal and aren't necessary for it. This is just a note to keep in mind that doing this feature adds another place where such targets would potentially add value should they be needed/added later.

VII. A Magic Attribute That Lets The Abstract Author Prescribe Defaults

As I worked through the scenarios there were an increasing number of knobs an abstraction author might want to configure and it could be beneficial to have a centralized attribute for configuring them all. For example:

Because this feature is a tool for abstraction authors to create particular experiences for end-developers it's conceivable that there might be a richer InferredClassAndMethodConfigurationAttribute which makes all that information available to the compiler so that those decisions can remain in the hands of abstraction authors.

The alternative is to pick a set of defaults with various escape hatches for abstraction consumers (e.g. attribute targets) at the cost of increasing the complexity of the abstractions we're trying to simplify in the first place.

Today attribute inheritance is largely governed by the setting of AttributeUsageAttribute.Inherited which gives power to the developer defining the attribute to decide how it behaves. I think there's a case to be made that there should also be a mechanism for the developer applying an attribute to decide how it will behave for their derivatives.

By analogy, a base class author can mark a method Overridable but another derived base class author can revisit and decide the method is NotOverridable or even MustOverride. It would be interesting to look into that.

The issue of attribute inheritance was entirely motivated by how ASP.NET MVC finds and uses attributes in Scenario E. However, the biggest issue with Scenario E was that, by default, controllers have to be Public and types in VB are Friend by default. To work around this issue I used MVCs extensibility to add a filter for controllers that permits non-public controllers but the documentation indicated there may be similar issues with route attributes that would require similar workarounds.

ADG: More scenarios are needed for attribution inheritance behavior and attribute targets are a pretty general solution for this problem that has been discussed for VB for other issues anyway.

Async and Iterator modifiers are more about the method author than the method definer and I'm more inclined to just deal with whether or not the user writes them than to have the abstraction author prescribe them.

Other modifiers seem reasonable provided they can be explicitly stated in a partial and that takes precedence. These are just defaults, after all. I like it a little better than <Class: Public, NotInheritable> pseudo-attributes (which I don't hate).

I'm at least sold on the name being in a base-type-applied attribute.

AdamSpeight2008 commented 5 years ago

@AnthonyDGreen Why is the first the example?

Console.Clear()
Console.Write("Enter your name: ")
Dim name = Console.ReadLine()
Console.WriteLine("Hello, " & name)

and not?


Clear()
Write("Enter your name: ")
Dim name = ReadLine()
WriteLine("Hello, " & name)
ericmutta commented 5 years ago

I have only just finished looking at scenario B, but after seeing the debugger run code in what looks like a documentation file with code snippets, I am completely sold!

Apologies if this is already covered in the expansive notes above, but is there a way for us to get this prototype and try it in Visual Studio today?

DualBrain commented 5 years ago

@AdamSpeight2008 I suspect that it is simply due to that being a different "feature". I agree with what you are saying from one point of view (mine) since I play a lot with console applications; so having the namespace references already in place to better facilitate this would be great for me. However, what if you are writing applications that are WinForms, WPF or something that we aren't yet aware of... does this defaulting to a particular (additional) set of includes introduce problems that are difficult to overcome in the future? With all of that said, I think you'll find #103 an interesting topic regarding this. Although it's not exactly what you are asking... I think it is approaching the same subject.

ericmutta commented 5 years ago

Can top-level members reference top-level (non-field) locals?

I think this needs more thought. Given code like this:

Dim startTime = Date.Now

Sub M()
End Sub

Sub P()
End Sub

I would expect startTime to behave like a global variable/field and be accessible within both M() and P(), exactly the way it would be if the code was wrapped into something like this:

Public Module Program
   Private startTime As Date = Date.Now

   Sub M()
   End Sub

   Sub P()
   End Sub
End Module

In other words when writing code without an enclosing class/module, any use of Dim outside a method should act as if it declared a global field that can be accessed in all the methods within the file. I think this is how scripting languages work(?).

ericmutta commented 5 years ago

Having scanned most of the notes above and watched all the videos, all I can say is that this is really really cool. I hope that in the final result, there will be a short list of simple rules that describe what is/isn't allowed when writing VB in low-ceremony mode.

For teaching and simple "scripting" purposes, I am hoping that the rules allow equally low-ceremony transitions from code that is just a bunch of statements, to code that groups those statements into functions for clarity, to code that groups those functions into classes/modules.

Thanks a lot for this @AnthonyDGreen, I imagine it has taken a significant amount of your time and it is much appreciated!

AnthonyDGreen commented 5 years ago

@AdamSpeight2008, @DualBrain,

I just didn't think of it when I wrote it. I originally had another example there but swapped it out at the last minute to be slightly more interesting. But that would work 100% fine already by either adding an Imports System.Console statement to the top of the code-snippet or adding System.Console to the project default imports. It's not communicated in the UI in any way but project-level imports have always supported importing a type, you just have to type it in manually. In fact, they also support import aliases. The compiler just parses whatever you wrote as though it came after Imports and then runs with it :)

That said, if I were writing such a script I would definitely import Console as suggested!

AnthonyDGreen commented 5 years ago

@ericmutta you can still declare fields that the methods can reference you just have to use Private instead of Dim. You could write Private Dim if you wanted, I guess or even Shared/Shared Dim. But there's always the possibility to make a local that's truly local and can't be referenced by the members by just saying Dim.

As for playing with the bits, everything up to Scenario D is in https://github.com/AnthonyDGreen/roslyn/tree/prototypes/vb-top-level and Scenario E is in https://github.com/AnthonyDGreen/roslyn/tree/prototypes/vb-top-level-json

If you clone my fork, sync to that branch and build and set either Roslyn or "VisualStudioSetup.Next" to the start up project it should launch a new instance of VS with those changes. Sometimes it doesn't deploy the compiler right and so the changes work in the IDE but not on build and to get Scenarios D and E working I had to manually overwrite the compiler in the MSBuild folder because doesn't use the same compiler normal VS builds use by default.

Also all of my code only works on VS15.7 because that's what I have installed and I don't like switching VS updates mid-prototype because of all kinds of unexpected dependencies that can break if the bits on your machine don't exactly match the bits in the branch.

The VS-add-in for the rich documentation file is a separate project I just whipped up for the demo, it's not rigorous by any stretch and I hadn't had any plans to version either it or the demo projects themselves but maybe I should just so people can see behind the curtain a little.

Yes, it has taken up a good chunk of time but not excessively so. It's something I've wanted to deep dive into for a while now and I'm very pleased with the results. I'm already off to the next prototype which is something I've been stewing on since about 2010 :) I think it's even cooler.

AdamSpeight2008 commented 5 years ago

@DualBrain I was thinking, it should assume it is a console application. Unless it detects that some form of UI form is to be created.

AnthonyDGreen commented 5 years ago

You don't have to assume anything. Console apps and WinForms apps are different project templates and already have different sets of project-level imports. WinForms apps have System.Drawing for example while Console apps don't. But my thoughts on importing types is that I'm less confident that every or even most files in a project need direct access to the console. Usually in my apps there are a few types that need Console members a lot and then a lot that don't so I prefer to do the import on a per file basis. Certainly the main code file in the console app project template could include this import explicitly as well.

ericmutta commented 5 years ago

@AnthonyDGreen: I'm already off to the next prototype which is something I've been stewing on since about 2010 :) I think it's even cooler.

Awesome keep 'em coming! :-)

rrvenki commented 5 years ago

Excited about watching only @AnthonyDGreen doing prototypes and did not hear from "Lucian Wischik" for quite sometime from VB desk. But please let us know how to use the prototypes ourselves or contribute to it any form. Is there any tracker for knowing if any of the prototypes have reached public release? For example in most (almost) cases we use webAPIs for all our business app development in workplaces which returns JSON. The JSON literals will be of immense help and will certainly give another life to VB. But how to use JSON Literals? If it makes to public release C# too would have that feature...