The Agenda – A Visual Guide

A few years back I tried to find a way to communicate the scope of the ModVB project. After repeated attempts to edit the length down from hundreds of pages I decided to try a depth-first summary. This proved too expensive so I’m back going for a breadth-first approach. You know the adage “A picture is worth a thousand words”? I’m hoping that works with code samples. My goal here is for you to “see” the scale not necessarily the precision. I cannot condense 20 years of study into an elevator pitch, I’m sorry.

So, I’m going to go through the outline and try to write as little prose as possible to illustrate what the change is. I’ll resist the urge to give context or backstory or motivation or exhaustively cover every bell and whistle but I will not if an idea feels less baked or there have been changes since I last publicly spoke on a topic. A few quick notes before we start to hopefully limit confusion in the comments.

  • This is not a priority list; don’t @ me about where I should be focusing.
  • This is a list accumulated over 20 years of using VB.NET and/or 8 years developing it and investigating thousands of customer complaints and requests, etc. There is not much here that I haven’t thought about deeply so if it doesn’t make sense the fault is in the description, not the design. Back-compat, tooling, performance, etc., are all part of my process.
  • I don’t follow C# or F# language design, intentionally. I want to see VB actualized as the best it can be from its own intrinsic motivation, not copying the new shiny object from another language. There will be overlap in scenarios because they’re both general purpose languages and because I worked on both and had awareness of problems we wanted to solve since 2010 but I haven’t been keeping up with C# or F# since 2018 and have only a “feature name and snippet on Twittter” level of awareness of the depths of C# and F# additions. That said in the places where the features are analogous, of course there would be some reconciliation process to make for an acceptable interop story. It’s unlikely that I would accidentally overlook a chance to work well with the other languages.
  • This is not a release schedule. It’s not a project plan. It doesn’t include costs. That’s outside the scope of this document and will evolve on GitHub based on factors.
  • This doesn’t include the many IDE improvements I want to make.
  • Every section and most subsections have links for each referencing/sharing.

The Agenda Illustrated

1. Type-Inference Enhancements

Examples excerpted from summary here.

If TypeOf animal Is Duck Then
    animal.Quack()
End If
Select Case TypeOf animal
    Case Mammal
        animal.SecreteMilk()
        
    Case Bird
        animal.LayEggs()
        
    Case Fish
        animal.Swim()
        
    Case Insect
        animal.Creep()
        
    Case Else
        Throw New NotImplementedException()
        
End Select
If TypeOf animal Is Bird AndAlso animal.WingspanInMeters > 2 Then
    ' Big Bird
End If

Let holders = If(TypeOf quadruped Is Bird,
                 quadruped.Hindlimbs, 
                 quadruped.Forelimbs)
If TypeOf animal Is Vertebrate Then
    animal.FlexBackbone()
    
    If TypeOf animal Is Reptile Then
        animal.ShedScales()
        
        If TypeOf animal Is Snake Then
            animal.Coil()
        End If
    End If
End If
If TypeOf animal Is Mammal AndAlso TypeOf animal Is ICanFly Then
    ' Members of both types are available.
    animal.SecreteMilk()
    animal.Fly()
End If
Let animal As Animal? = SomeFunction()

' (Actual error text subject to change)
' Error: `Move` is not a member of `Animal?`.
' -OR-
' Error: Cannot call member `Animal.Move` from nullable value.
animal.Move()

If animal IsNot Null Then
    ' No error.
    animal.Move()
End If
' Works with guard statements (Return, Exit, Continue, Throw).
If TypeOf animal IsNot Mammal Then Return
    
animal.ShedHair()
' Monotremes are egg-laying mammals, including the Platypus and the Echidna.
If TypeOf animal IsNot Mammal OrElse animal.IsMonotreme Then
    mayLayEggs = True
End If
Let animal As Animal = GetDolphin()

Do Until TypeOf animal Is ILandDwelling
    ' Or maybe I should have said:
    ' Do While TypeOf animal IsNot ILandDwelling?
    animal = animal.GetImmediateAncestor()
Loop

animal.Walk()
Select Case TypeOf dinosaur
    Case IsNot Bird
        
        dinosaur.GoExtinct()
        
    Case Else
        ' This metaphor is straining.
        If dinosaur.IsDodo Then
            dinosaur.GoExtinct()
        Else
            ' Expensive O(n) lookup on Wikipedia.
            
        End If
        
End Select
Let usStateBearsByHibernationPeriod =
    From
        state In usStates
    Let
        mammal = state.StateMammal
    Where
        TypeOf mammal Is Bear AndAlso Not mammal.IsExtinct
    Order By
        mammal.HibernationPeriod Descending
' In vanilla VB, type inference requires the 2nd & 3rd operands
' to convert to the type of one or the other or type inference
' fails, resulting in type 'Object'.
Dim animal = If(someCondition, New Cat, New Dog)
' In ModVB, 'animal' would have type 'Carnivora',  which is the taxonomical
' order common to cat-like and dog-like placental mammals, but that's not
' actually useful because not all carnivorans are carnivores, e.g. bears
' are often omnivores except for the Giant Panda which mostly eats plants.
' So, even though it's inferred that type, the only members I can use are
' defined on 'Mammal' in this example, which is still more than I could
' use if the type were 'Object', so... 
Let animal = If(someCondition, New Cat, New Dog)
animal.SecreteMilk()
' This should work.
Let snapshot = Snapshot.Create()

' This should work correcly (infer the type of `cache`).
Static cache = New Dictionary(Of Integer, Integer)

' Normally type-inference can't be done for a variable which
' refers to itself (e.g. a recursive lambda expression).
' But, since the types are explicit on the lambda,
' technically type-inference is unnecessary; this should work.
Let factorial =
      Function(n As Integer) As Integer
          If n < 0 Then Throw
          
          Return If(n = 0, 1, n * factorial(n - 1))
      End Function

' Normally it's an error to refer to a variable before it's
' declared (and also a definite assignment error).
' Can these rules be relaxed in lambdas that aren't invoked
' until all variables are declared and initialized to
' enable mutual recursion?
Let isEven = Function(n As Integer) As Boolean
                 Return n = 0 OrElse isOdd(n - 1)
             End Function
             
Let isOdd = Function(n As Integer) As Boolean
                Return n <> 0 OrElse isEven(n - 1)
            End Function

2. Streamlining and Boilerplate Reduction

Examples excerpted summary here.

2.1 Immersive Files (“Top-Level Code”)

FirstProgram.vb

Console.WriteLine("Hello, World!")

SecondProgram.vb

' This is an entire program.
Imports System.Console

Function Prompt(message As String) As String
    WriteLine(message)
    Return ReadLine()
End Function

Let name = Prompt("What is your name?")

WriteLine($"Hello, {name}โ€‹.")

Starfield.vb

buffer.Clear(Color.Black)

' Draw 10 yellow 5x5 squares randomly across a 600x400 screen.
For i = 1 To 10
    buffer.FillRectangle(Brushes.Yellow, Rnd() * 600, Rnd() * 400, 5, 5)
Next

Index.vbxhtml

<?xml version="1.0" encoding="UTF-8"?>
<html>
  <head>
    <title>Welcome to my site!</title>
  </head>
  <body>
    <h1><{ ViewData!Message }></h1>
    <h2>This is a header.</h2>
    <p>Today is <{ Date.Today.ToString("MMM d, yyyy") }>.</p>
    
    <a href="/">Home</a>
    <a href="/home/about">About</a>
    <a href="/home/contact">Contact</a>
  </body>
</html>

Form1.vb

Sub Button1_Click() Handles Button1.Click
    MsgBox("Hi")
End Sub

Sub Button2_Click() Handles Button2.Click
    MsgBox("Bye")
End Sub

Past Demos (Early Prototypes)

2.2 Key Fields & Properties, and Auto-Constructors

Class AppointmentBookingService
    
    Private Key CalendarService As ICalendarService,
                PaymentService As IPaymentService,
                EmailService As IEmailService

    ' Look at this code I'm never going to have to type again!
    ''Public Sub New(calendarService As ICalendarService,
    ''               paymentService As IPaymentService,
    ''               emailService As IEmailService)
    ''               
    ''    Me.CalendarService = calendarService
    ''    Me.PaymentService = paymentService
    ''    Me.EmailService = emailService
    ''End Sub

End Class
Structure Money
    Implements IEquatable(Of Money)
    
    Public Key ReadOnly Property Value As Decimal
    
    Public Key ReadOnly Property Currency As Currency
    
    ' Sub New(value, currency), Equals, and GetHashCode provided by compiler.
    
End Structure

2.3 Wildcard Lambda Expressions

<TextBox Text={*.PrimaryContact.FirstName}/>
' Vanilla VB
modelBuilder.Entity(Of Blog) _
            .Property(Function(b) b.Url) _
            .IsRequired()
            
modelBuilder.Entity(Of Blog) _
            .HasMany(Function(e) e.Posts) _
            .WithOne(Function(e) e.Blog) _
            .HasForeignKey(Function(e) e.ContainingBlogId) _
            .IsRequired()
            
' ModVB
modelBuilder.Entity(Of Blog) _
            .Property(*.Url) _
            .IsRequired()
            
modelBuilder.Entity(Of Blog) _
            .HasMany(*.Posts) _
            .WithOne(*.Blog) _
            .HasForeignKey(*.ContainingBlogId) _
            .IsRequired()
Let users = context.Users.Include(*.Profile.Avatar).ToList()

2.4 New since summary published

' Computed `ReadOnly` Properties.
' Now 2 lines instead of 5.
ReadOnly Property DiscountedPrice As Decimal
    Return Price - (Price * DiscountRate)
    
' Accessor `End` Statements can be omitted.
Property Age As Integer
    Get
        Return _Age
        
    Set(value)
        If value < 0 Then Throw
            
        _Age = value
End Property

Event AgeChanged As EventHandler
    AddHandler(value)
        _AgeChanged += value
        
    RemoveHandler(value)
        _AgeChanged -= value
    
    RaiseEvent(sender, e)
        _AgeChanged?(sender, e)
    
End Event

' Pre-processing conditional attributes.
#If PLATFORM = "NetFX" Then
    <Serializable>
#Else
    <DataContract>
#End If
Class Message
    ...
End Class

2.5 Markdown Documentation Comment Syntax

'''
''' Returns a function from a description of a line in slope-intercept form.
'''
''' # Parameters
''' - @m: The slope of the line. Must not be @Double.PositiveInfinity,
'''       @Double.NegativeInfinity, or @Double.NaN.
''' - @b: The y-intercept of the line. Must not be @Double.PositiveInfinity,
'''       @Double.NegativeInfinity, or @Double.NaN.
'''
''' # Returns
''' An instance of `Func(Of Double, Double)` delegate type which
''' returns the y-coordinate given an x-coordinate.
'''
''' # Exceptions
''' - @ArgumentOutOfRangeException: Either @m or @b is
'''     @Double.PositiveInfinity, @Double.NegativeInfinity, or @Double.NaN.
'''
''' # Remarks
''' A line can be of one of 3 forms:
''' 1. Horizontal,
''' 2. Vertical, or
''' 3. Diagonal.
'''
''' This API only supports forms 1 and 3.
'''
''' # Examples
''' ## Normal usage
''' ``` vb.net
''' Let y = F(3 / 2, -5)
''' Graph(y, 0 To 100)
''' ```
'''
''' ## Horizontal line
''' ``` vb.net
''' Let y = F(0, -5)
''' Graph(y, 0 To 100)
''' ```
Function F(m As Double, b As Double) As Func(Of Double, Double)
    If Double.IsInfinity(m) OrElse Double.IsNaN(m) Then
        Throw New ArgumentOutOfRangeException(NameOf m)
    ElseIf Double.IsInfinity(b) OrElse Double.IsNaN(b) Then
        Throw New ArgumentOutOfRangeException(NameOf b)
    End If
    
    Return Function(x) (m * x) + b
End Function

2.6 #Ignore Warning Directive

' Before.
#Disable Warning BC42356 ' No `Awaits`; don't care.
Async Function SayHiAsync() As Task
#Enable Warning BC42356

    Console.WriteLine("Hello, World!")

End Function
    
' After.
#Ignore Warning BC42356 ' No `Awaits`; don't care.
Async Function SayHiAsync() As Task

    Console.WriteLine("Hello, World!")

End Function

2.7 New Implicit Line Continuations

  • Before Then
If someComplexConditionExpression1 AndAlso
   (someComplexConditionExpression2 OrElse
    someComplexConditionExpression3)
Then
    ' Do stuff.

End If
  • Before Handles
Sub Button1_Click(sender As Object, e As MouseMoveEventArgs)
    Handles Button1.MouseMove

    ' Do stuff.
End If
  • Before Implements
Function GetEnumerator() As IEnumerator(Of T)
    Implements IEnumerable(Of T).GetEnumerator
    
    ' Get thing.
End Function
  • Between ) and As in Function statements
Function BuyCar(
           make As String,
           model As String,
           year As Integer,
           Optional trim As String
         )
         As Car
  • Maybe allowing comments between query clauses?
' Getting customers
From customer In db.Customers
' in Illinois
Where customer.Address.State = "IL"
' with orders
Join order In db.Orders On order.CustomerId = customer.Id
' made last year
Where order.OrderDate.Year = 2023
' from most expensive to least
Order By order.Total Descending
' getting the top 10
Take 10

2.8 Bug Fixes & Other Minutiae

  • Async Main methods will be legal
Async Function Main(args As String()) As Task(Of Integer)

    Await Console.Out.WriteLineAsync("This is an example.")

    Return 0
End Sub
  • Optional parameter default value inference
Function BuyCar(
           make As String,
           model As String,
           year As Integer,
           Optional trim As String ' <-- No default here.
         )
         As Car
  • Optional keyword โ€ฆ optional after first optional parameter?
Function MakeDateTime(
           year As Integer,
           month As Integer,
           day As Integer,
           Optional
             hour As Integer,
             minute As Integer,
             second As Integer,
             millisecond As Integer,
             microsecond As Integer
         )
         As Date
  • Type inference for accessor value parameters
' Before
Property Name As String
    Get

    End Get
    Set(value As String)

    End Set
End Property

' We shall never know the delegate type of this event.
Custom Event NameChanged As EventHandler(Of EventArgs)
    AddHandler(value As EventHandler(Of EventArgs))

    End AddHandler
    RemoveHandler(value As EventHandler(Of EventArgs))

    End RemoveHandler
    RaiseEvent(sender As Object, e As EventArgs)

    End RaiseEvent
End Event

' After
Property Name As String
    Get

    End Get
    Set(value)

    End Set
End Property

Event NameChanged As EventHandler(Of EventArgs)
    AddHandler(value)

    End AddHandler
    RemoveHandler(value)

    End RemoveHandler
    RaiseEvent(sender, e)

    End RaiseEvent
End Event
  • Custom keyword in for custom Event declarations not required

See above.

  • Optional parentheses for NameOf expressions?
Public Shared ReadOnly HeaderTextProperty As DependencyProperty =
                         DependencyProperty.Register(
                           NameOf HeaderText, ' <-- No parentheses.
                           GetType(String),
                           GetType(MyControl)
                         )
                                            
Public Property HeaderText As String
    Get
        ...
  • Open generic types/methods(?) allowed in NameOf expressions
' Vanilla VB
' This is legal:
Shared ReadOnly DictionaryType As Type = GetType(Dictionary(Of,))

' But you're required to write this:
Shared ReadOnly DictionaryName As String = NameOf(Dictionary(Of Object, Object))

' ModVB
' Now this is also legal:
Shared ReadOnly DictionaryName As String = NameOf Dictionary(Of,)

3. General Modernization and Evolution I (Statements & Expressions)

3.1 Local variables

' Tuple deconstruction.
Let suit, rank = GetCard()

' Fix: `As New` for arrays and anonymous types.
Dim buffer As New Byte(0 To 1023) {}

Let foreignKey As New With {emailAddress, userId}

3.2 Assignment

' Explicit `Set` statement.
Set obj.Position += acceleration
    
Set left = Null,
    right = Null    
            
Set suit, rank = GetNextCard()

3.3 Select Case

' `Select Case` on type.
Select Case TypeOf obj
    Case AppDomain
        ...

' `Select Case` on shape.
Select Case ShapeOf coordinate
    Case (latitude, longitude)
        ...

' `Select Case` on identity.
Select Case sender
    Case Is CloseButton
        ...
        
    Case IsNot MainForm
        ...
        
' `Select Case` with `Like`.
Select Case str
    Case Is Like "?*@?*.?*"
        ...

    Case IsNot Like "#.#.#.#"
        ...
        
' `Select Case` on collection membership.
Select Case str
    Case In bannedWords
        ...

    Case NotIn bannedWords
        ...

3.4 For

' Multiple nested counter variables and ranges.
For z = 0 To maxDepth,
    y = 0 To maxHeight,
    x = 0 To maxWidth
    
    ....   
    
Next x, y, z

' Explicit variable in `Exit For` and `Continue For` statements.
Continue For y

Exit For z
' Fix: Capture `For` variable per iteration to avoid
' reporting warning BC42324 AND throwing 
' 'IndexOutOfRangeException' in cases like this:
Let arr = {"Peaches", "Pears", "Plums"}
Let elementPrintActions = New List(Of Action)

For i = 0 To arr.Length - 1
    ' Warning BC42324: Using the iteration variable in
    ' a lambda expression may have unexpected results.
    ' Instead, create a local variable within the loop
    ' and assign it the value of the iteration variable.
    elementPrintActions.Add(Sub() Console.WriteLine(arr(i)))
    '                                                   ~
Next

For Each printAction In elementPrintActions
    printAction()
Next

3.5 For Each

' Tuple deconstruction.
For Each key, value In dictionary
    ...

' Explicit variable in `Exit For` and `Continue For` statements.
Continue For child

Exit For parent


' Query comprehensions.
For Each ch In str Where ch -> Char.IsDigit()
    ...
    
' IAsyncEnumerable support.
Await Each item In sequence
    ...
Next

Past Demo (Early Prototype)

3.6 Do

' Variable declaration/assignment in header BEFORE condition is evaluated.
Do line = Console.ReadLine() Until line Is Null
    ...

Do bytesRead = stream.ReadAsync(buffer, 0, buffer.Length) While bytesRead > -1
    ...

3.7 With

' Named `With` variable.
With parameter = command.CreateParameter()
    .ParameterName = "@id"
    .DbType = DbType.Guid
    .Value = id
    
    command.Parameters.Add(parameter)
End With

' `.Me` pseudo-member.
With command.CreateParameter()
    .ParameterName = "@id"
    .DbType = DbType.Guid
    .Value = id
    
    command.Parameters.Add(.Me)
End With

' Compound-assignment operators.
With builder
    &= item.Header
    &= vbCrLf
    &= item.Description
    &= vbCrLf
End With

3.8 Throw

' Exception type inference.

' Throws ArgumentNullException.
If name Is Null Then Throw

' Throws ArgumentOutOfRangeException.
If count < 0 Then Throw
    
' Throws InvalidOperationException.
If IsClosed Then Throw

3.9 Try

' `Await` in `Catch` and `Finally` blocks.
Try
    ...
Catch ex As Exception
    Await logger.LogAsync(ex)
    
Finally
    Await resource.DisposeAsync()
End Try

' Local declarations scoped to entire `Try`/`Catch`/`Finally` block.
Try resource1 = GetResource(),
    resource2 As ResourceHandle
    
    resource1.Open()
    resource2 = resource1.GetChildResourceHandle()
    
Catch ex As Exception
    resource2?.Revert()
    resource1?.Revert()
    
Finally
    resource2?.Dispose()
    resource1?.Dispose()
End Try

' `Catch` and/or `Finally` in any block.
For Each p In Process.GetProcesses() Where p.Name = "chrome.exe"
    ...
    
Finally
    p.Kill()
Next

3.10 Using

' `Catch` and/or `Finally` in `Using` block.
Using reader = File.OpenText(filename)
    ...

Catch ex As IOException
    logger.Log("Failed to read configuration.")

Finally
    Await reader.DisposeAsync()

End Using

' Tuple deconstruction.
Using one, two, three = GetTriplet()
    ...
    
' Recognize `Dispose` method by name (maybe even an extension method?).

3.11 SyncLock

Basically the same stuff as Using. Maybe some enhancements for additional synchronization methods other than Monitor.Enter/Exit.

3.12 Casting

' Post-fix casting syntax makes it easier to read
' and write chained conversions and member accesses.
Let rowId = control.Tag(As DataRow).RowId

? jsonObject!receivedDate(As Date)

3.13 Return

' `Return` statement may now also assign to `ByRef`/`Out` parameters.
Return True, value:=result

4. UI, XML, XAML

' XAML Literals.
Let button = <?xaml?>
             <Button Content="{Binding Command.Name}"
                     Clicked="OnButtonClicked"/>
             
' Embedded VB Parsing Mode (No <%= %> or XML escapes required).
'
' Not Shown: Full-Fidelity VB Code-Trees to support inspection, execution, and
' translation of embedded VB code to target DSLs such as JavaScript.
<?vb?>
<html>
  <head><title>document.Title</title></head>
  <body>
  <h1>$"โ€‹{document.Titleโ€‹} >> โ€‹{document.LastUpdatedโ€‹}"</h1>
  
  If includeDisclaimer Then
      <div>
          ...
      </div>
  End If
  
  For Each p In document.Paragraphs
      <p>p.Text</p>
  Next
  </body>
</html>

' Implicit `Return` or `Yield` for XML expression statements in
' function and property bodies.
'
' Not Shwon: Implicit self-application inside `InitializeComponent` methods.
' Not Shown: Builder-pattern to support streaming writes of XML documents.
Function GetResponse() As <ResponseMessage>
    <ResponseMessage>
        ...
    </ResponseMessage>
End Function

' XML Schema "types" to re-enable XML IntelliSense.
Let address As <geo:Address> = ...
? address.<Country>.Value

Past Demos (Early Prototypes)

5. JSON and JSON Pattern Matching

Past Demo

Additions since demo

' Target-typed object creation from JSON.

' Intialize test data.
Let dbContext As InMemoryDbContext =
    {
      "employees": [
        {"firstName":"John", "lastName":"Doe"},
        {"firstName":"Anna", "lastName":"Smith"},
        {"firstName":"Peter", "lastName":"Jones"}
      ]
    }
    
Let service = New HrService(dbContext)

Let results = Await service.FindEmployeesByNameAsync("John Doe")
    
Assert.Equal(1, results.Count)

Let newEmployee = Await service.AddEmployeeAsync("Jack", "Sparrow")

' Copied base-line from postman.
Assert.IsTrue(ShapeOf newEmployee Is {
                "firstName": "Jack",
                "lastName": "Sparrow",
                "emailAddress": "jack.sparrow@company.com"
              })
              
' Pattern to support &= for writers w/o realizing/allocating
' objects (e.g. JsonWriter/Serialization).
For Each e In Employees
    response &= {
                  "firstName": e.FirstName,
                  "lastName": e.LastName,
                  "emailAddress": e.EmailAddress
                }
Next

' JSON schema types for IntelliSense/analyzers.
Let employee As {"http://.../employee"} = ...
? employee!emergencyContact

6. Strings and String Pattern Matching

' This should only call String.Format once.
? $"Greetings, โ€‹{firstNameโ€‹}. " &
  $"The current time is โ€‹{Date.Nowโ€‹}, " &
  $"on โ€‹{Date.Today.DayOfWeekโ€‹}."

' This shouldn't call String.Format at all.
Throw New ArgumentNullException(
            description, 
            $"Argument 'โ€‹{NameOf(description)}' may not be null."
          )

' `&=` should work whether concatenating a string variable or a StringBuilder.
Let builder As StringBuilder = ...
builder &= "Line" & vbCrLf

' String pattern matching (inverse of interpolation).
Select Case ShapeOf command
    Case $"/echo โ€‹{messageโ€‹}"
        
        ? message
        
    Case $"/beep โ€‹{frequency As Integer} โ€‹{duration As Integer}"
        
        Console.Beep(frequency, duration \ 1000)
        
    Case "/clear"
        
        Console.Clear()

    Case Else
        ? "Unrecognized command: " & command

End Select
' Narrowing conversion operator from String if Parse exists.
Let nil As Guid = "00000000-0000-0000-0000-000000000000"

' Pattern/Shape operator from String if TryParse exists.
Let str = "20.0.255.255:23"

If ShapeOf str Is ip As IPEndpoint Then
    ? ip.Port
End If

' Narrowing conversion operator from String to Enum types.
Enum PhoneKind
    Home
    Office
    Mobile
End Enum

Let kind As PhoneKind = "mobile"

7. General Pattern Matching

' Explicit and implicit `ShapeOf` operators.
Select Case ShapeOf input
  Case pn As USPhoneNumber
    Return New DomesticContact(pn)
  
  Case pn As InternationalPhoneNumber 
    Return New GlobalContact(pn)
    
End Select

' User-defined pattern methods.
Let hasAnotherChar =
      Function(reader As TextStream, Out ch As Char) As Boolean
          If reader.EndOfFile() Then
              Return False, ch:=Nothing
          Else
              Return True, ch:=reader.PeekChar(0)
          End If
      End Function

If ShapeOf stream Is hasAnotherChar(c) AndAlso c <> Nothing Then
    buffer.Append(c)
    stream.EatNextChar()
End If
' Named patterns.
Function ConstructorCall(
           node As ExecutableStatementSyntax,
           Out kind As VBSyntaxKind,
           Out arguments As SeparatedSyntaxList(Of ArgumentSyntax)
         )
         As Boolean

    ' Bring shared pattern functions into scope.
    Imports SyntaxPatterns

    ' Deconstruction syntax matches/inverts factory syntax.
    Select Case ShapeOf node
      ' MyBase.New(...)
      Case ExpressionStatement(
             InvocationExpression(
               MemberAccess(
                 MeExpression(),
                 IdentifierName("New")
               ),
               argList
             )
           )
       
        Return True, kind:=VBSyntaxKind.MeExpression,
                     arguments:=argList.Arguments

      ' Me.New(...)
      Case ExpressionStatement(
             InvocationExpression(
               MemberAccess(
                 MyBaseExpression(),
                 IdentifierName("New")
               ),
               argList
             )
           )

        Return True, kind:=VBSyntaxKind.MyBaseExpression,
                     arguments:=argList.Arguments

      Case Else

        Return False, kind:=Nothing, arguments:=Nothing
      
    End Select
End Function

' Check if first statement of constructor is
' a call to another constructor.
Let firstStatement As ExecutableStatementSyntax = ...

If ShapeOf firstStatement Is ConstructorCall(kind, args) Then
    If kind = VBSyntaxKind.MeExpression Then
        ProcessChainedConstructorCall(args)
        
    ElseIf kind = VBSyntaxKind.MyBaseExpression Then
        ProcessBaseConstructorCall(args)
        
    End If
Else
    ProcessOtherStatement(firstStatement)
   
End If

8. General Modernization and Evolution II (Declarations)

' `Out` arguments avoid null-reference warnings, and
' allow for inference and implicit declaration of variables.
If cache.TryGetValue(name, Out value) Then
    Return value
End If

' Explicit `ByRef` argument syntax for clarity, stricter checking,
' and disambiguation when needed.
ReportErrors(ByRef diagnostics)

' `Out` parameters for idiomatic multiple returns (e.g. pattern methods).
Function IsPerfectSquare(number As Integer, Out root As Integer) As Boolean
    
    Select Case number
        Case 1
            ' Simplified single-statement return with `Out` assigns.
            Return True, root:=1
            
        Case 4
            Return True, root:=2
            
        Case 9        
            Return True, root:=3

        Case Else
            Let intRoot As Integer = Math.Sqrt(number)

            If (intRoot * intRoot) = number Then
                Return True, root:=intRoot
            Else
                Return False, root:=Nothing
            End If
    End Select
End Function    

' Modules may be generic, nested, and won't hoist members
' into containing namespace by default.
Module Utils(Of T)
    ...
    Module Strings
        ...

' Classic behavior can be opted into with attribute.
<StandardModule>
Module IOHelpers
    ...

' Namespaces, Shared type members, and XML/JSON namespaces can
' be imported at the method level.
Sub WriteHelp()
    Imports System.Console
    Imports <xmlns:xs="http://www.w3.org/2001/XMLSchema">

    Clear()    
    WriteLine("...")
    WriteLine("...")

End Sub

' Bit modifier for <Flags> Enums.
Bit Enum MyFlags As Byte
    None
    
    ' Correct initialization by default
    First  ' = 1
    Second ' = 2
    Third  ' = 4
    Fourth ' = 8
    
    ' Simpler masks.
    Odds = First, Third
    Evens = Second, Fourth
    All = Odds, Evens

    ' Named numeric sections.    
    Kind = Bit 4 To Bit 7
End Enum

Let flags As MyFlags = &B_0111_1010

' Simpler checking.
? flags(Bits MyFlags.First)                  ' False.
? flags(Bits MyFlags.Second)                 ' True.

' Simpler settting.
flags(Bit MyFlags.Third) = True

' I have been coding for 30 years and don't need to
' do manual bit operations to prove myself.
? flags(Bit MyFlags.Second, MyFlags.Fourth) ' True.

? flags(Bit MyFlags.Kind)                   ' 7

' Strongly typed [Delegate].Combine/Remove with + and -.
Let handlers As EventHandler = Null
handlers += AddressOf Button_Click
handlers -= AddressOf Button_Click

' Explicit syntax for anonymous delegate types.
Let binOp As <Function(Double, Double) As Double>

' Not shown: Relaxed delegates can be removed.

' Pipeline `->` operator.
' Instead of:
Return Third(Second(First(x)))
' Write this:
Return First(x) -> Second() -> Third()

9. Language Integrated Query (LINQ) Enhancements

' Range expressions.
Let odds = 1 To 10 Step 2

For Each n In 0 To 60
    ...
    
Let guess = Rnd(1 To 100)

' Tuple deconstruction.
? From
    x, y In EnumerateCoordinates()
  Order By
    y Descending

' Smarter syntax for aggregate functions.
' Less need for Aggregate clause.
Let firstMatch = From   c In db.Contacts
                 Where  c.Id = contactId
                 Select FirstOrDefault()
                     
' New Query comprehensions:
' Shown: Include
' Not Shown:
' Skip Until, Take Until,
' Skip Last, Take Last,
' Left Join, Right Join
For Each blog In context.Blogs
 Include post In blog.Posts,
         post.Author.Photo,
         blog.Owner.Photo
    ...
Next


' Insert expression.
Let data = [
      {"name": "Leo"},
      {"name": "Donny"},
      {"name": "Raph"},
      {"name": "Mikey"}
    ]

Let newStudentIds =
      Insert s In db.Students
        From dto In data
         Set s.Name = dto!name

' Update expression.
? Update
    t In db.Teachers
  Where
    t.Id = teacherId
  Set
    t.EmailAddress = $"โ€‹{t.FirstNameโ€‹}.โ€‹{t.LastNameโ€‹}@university.edu"

' Delete expression.
? Delete c In db.Classes Where c.IsDeleted

' Nested initializers call `New` when no matching `Add`.
Let usFederalHolidays =
      New List(Of DateOnly) From {
            {2026, 1, 1},
            {2026, 1, 19},
            {2026, 2, 16},
            {2026, 5, 25},
            {2026, 6, 19},
            {2026, 7, 3},
            {2026, 9, 7},
            {2026, 10, 12},
            {2026, 11, 11},
            {2026, 11, 26},
            {2026, 12, 24},
            {2026, 12, 25}
          }

' Nested member and collection initializers.
? New MessageBoxFrom With {
        .Caption = "Select one",
        .AcceptButton With {
           .Text = "Proceed"
         },
        .Choices From {
           "Option 1",
           "Option 2",
           "Option 3"
         }
      }

' Combine `With` and `From`.
? New List(Of Object) With { .Capacity = 8 } From { ... }

' Dictionary access in `With`
? New Dictionary(Of String, Integer) With {
        !One   = 1,
        !Two   = 2,
        !Three = 3
      }
      
' Query collection initializers.
Let digits = New Hashset(Of Integer) From n In 0 To 9

' In and NotIn operators map to `Contains`.
If input In bannedWords Then Throw

' Works with range expressions.
If actScore NotIn 1 To 36 Then Throw
    
' Target-typing into `Select` clause.
Let actions As IEnumerable(Of Action) =
      From   obj In objects
      Select AddressOf obj.M ' <- Typed to `Action`.
' Fix: Stop reporting this error in cases like this:
' BC36606: Range variable name cannot match the name of 
' a member of the 'Object' class.
Let strings = From n In 1 To 5
              Select n.ToString()
'                      ~~~~~~~~

10. Dynamic Programming Enhancements

' New (or back from VB6/VBA) `Any` pseudotype for
' dynamic dispatch instead of `Object`.
' `Object` always compile-time checked.
Let obj As Any = New ExpandoObject
obj.CreateTime = Date.Now
obj.Id = 1
obj.Description = "[Untitled]"

' Late-bind for a single expression if you want.
? someObject(As Any).X.Y().Z

' New default type for "typeless" declarations.
' Great for RAD/prototypes with gradual typing.
Function Add(left, right)
'Function Add(left As Any, right As Any) As Any
    
Sub New(ParamArray args())
    
Property Description
    
Async Function Fetch(objectId)

Private State

' Default Methods to enable both early- and late-
' bound invocable objects/functors.
Class StoredProcedureInfo
    
    Default Sub Invoke(ParamArray args As Object())
        ...
    End Sub
End Class

Let sp_Users_Initialize As StoredProcedureInfo = ...
sp_Users_Initialize()

' And dynamic language like capabilities:
obj.Multiply = Function(left, right) left * right
? obj.Multiply(3, 5)

' Better support for pseudo-dynamic APIs w/o runtime
' binding.
' Argument list info passed to method w/ names, etc.
sp_Users_SelectById(id:=Guid.Empty, role:="admin")

' Late-bound support for the following:
' Not shown: Late-bound AddressOf bug fix from Dev10.
AddHandler
RemoveHandler
AddressOf

' Maybe
' Await
' For Each
' Queries

11. Async Programming Enhancements

' New syntax for true "Async Sub"s.
' No more "Function As Task" weirdness.
Async Sub Flush() As Task
    ...
End Sub

' Default awaitable type for lightweight
Async Sub MkDir() As ValueTask

' Lightweight Async syntax with configurable default async
' types and name-suffixing. e.g.
' Public methods use Task, Private methods use ValueTask.

' Same as `Async Sub Flush() As Task` (configurable).
' Same as `Function FlushAsync() As Task.
Async Sub Flush()

' Same as `Async Function Pull(...) As Task(Of JsonObject).
' Same as `Function PullAsync(...) As Task(Of JsonObject).
Async Function Pull(...) As JsonObject

' (Thread) `Agile` `Async` methods.
' Don't capture sync context w/o `ConfigureAwait(False)`.
Agile Async Sub FeelFreeToUseThreadPool()

' Elision of intermediate `Await` operations.
' Same as `(Await (Await obj.MAsync()).NAsync()).P`
Let result = Await obj.MAsync().NAsync().P

' Works with new conversion syntax.
' Same as `CType(CType(Await client.HttpGetAsync("..."), JsonObject)("requestId"), As Guid)`
? Await client.HttpGetAsync("...")(As JsonObject)!requestId(As Guid)

' Not shown: `Await obj?.MAsync()` actually working.

' "Conversions" to task objects still allowed.
Let t As Task = MkDirAsync("...")

' Async calls must be awaited or task objects assigned.
MkDirAsync() ' <- Not allowed.

' Explicit syntax (required) for not awaiting.
Call FireAndForgetAsync()

' Support for consuming IAsyncEnumerable.
Await Each chunk In steam.FetchChunksAsync(cancellationToken)
    ...
    
' Support for constructing IAsyncEnumerable.
Async Iterator Function FetchChunksAsync(...) As IAsyncEnumerable
    Await ...
    
    Yield ...
    Yield ...
End Function

' `Async` events?
Async Event E(sender As Object, e As EventArgs)

12. Null and Nothing

Note: I have received feedback that the “Nullable Reference Types” feature design, which would otherwise be the highlight of this section, needs further iteration to be less confusing and less annoying. I am currently working on this.

' `Null` literal for null-references and values.
' Never means CType(Nothing, T) for value types.
? Null

' Forces nullable type inference.
' `endDate` is typed as `DateTime?` not `Date`.
Let endDate = If(HasFinished, EndDatePicker.Value, Null)

' 2-Valued Logic equality operators which treat two
' nulls as equivalent. (i.e. T-SQL `IS [NOT] DISTINCT FROM`).
' NOTE: VB `=`/`<>` uses 3-valued logic for NVTs today.

' True if both null, or both not null and equal.
' Otherwise, false.
If left ?= right Then
    ...
    
' True if one or the other (but not both) are null,
' or if neither is null and left <> right.
' False if both are null, or both not null and equal.
If left ?<> right Then
    ...

' Post-fix null-coalescing operator.
Label1.Text = account.Description ?? "n/a".

' `Do Nothing` statement (not a joke).
' https://anthonydgreen.net/2026/04/01/this-feature-intentionally-left-blank/
Do Nothing
    
' Null indicator operator `?` for values.
Let flag = True? ' Infers `Boolean?` type.

' Null indicator `?` for optional reference types.
Property Description As String?

' Documents value "may be null", turns on
' stricter error checking.
Let result = lookup("key")?

' Null-safe behaviors (no-op) and
' null-propagation from `?.` for
' the following statements/operators:

' Doesn't enumerate if collection is null.
For Each item In collection?
For Each item in obj?.Member

' Question: Should querying a null collection evaluate to null or empty?
            
' Won't wait a null task/throw exception.
Await someTask?
Await obj?.MemberAsync()

' Same as `If(obj Is Null, Null, Await obj.MemberAsync())`
Let result = Await obj?.MemberAsync()

' Won't assign to member of null object.
obj?.Member = value

' Won't register/unregister from event on null object.
AddHandler obj?.E, handler
RemoveHandler obj?.E, handler

' Returns a null delegate if receiver is null.
Let d As Action? = AddressOf obj?.Method

13. Declarative Programming and Code-Generation

These strategies make different trade-offs in end-developer experience, tool-author experience, performance, complexity, etc. The same scenario might be addressed using more than one approach or a combination. How different scenarios and responsibilities are covered will very likely shift based on feedback and further attempts at reconciling their designs and implementation approaches.

13.1 Smart Attributes – Declarative w/o Source Generators

This approach prioritizes simplicity.

' Custom attributes inject code into auto-
' properties and events at defined points in
' in defined order. Allows for composing
' multiple behaviors declaratively.

' Example use sites:

<Trim, Notify, Undoable, MaxLength(25)>
Property Title As String = "New Listing"

<ThrowOnNull, Notify, Undoable, MaxLength(50)>
Property Description As String = "No description."

<Notify>
Property LastSaved As Date

<Notify, Undoable,
 Regex("^\d{3}-\d{2}-\d{4}$", "Must be of the form '###-##-####'.")>
Property ListingCode As String = "<None>"
    
<Wrapper>
Public Property Rating As Integer = 3

<NotSupported>
Public ReadOnly Property CanRead As Boolean

<IgnoreExceptions>
Event Closing As EventHandler

<RaiseAsync>
Event Saved As EventHandler

' Example declarations:

Public Class TrimAttribute
    Inherits PropertyHandlerAttribute

    Public Shared Sub OnPropertySet(
                        sender As Object,
                        propertyName As String,
                        ByRef backingField As String,
                        ByRef value As String
                      )

        ' Doesn't set backing field; let's later handlers run.
        value = If(value?.Trim(), "")
    End Sub
End Class

Public Class IdempotentAttribute
    Inherits PropertyHandlerAttribute
    
    ' Causes `Set` to exit early if function returns true.
    Public Shared Function OnPropertySet(Of T)(
                             sender As Object,
                             propertyName As String,
                             ByRef backingField As T,
                             ByRef value As T
                           ) As Boolean

        Return (value ?= backingField)
    End Sub
End Class

Public Class AutoRoundAttribute
    Inherits PropertyHandlerAttribute

    Sub New(digits As Integer)
        ' Because this constructor arguments are copied to the generated callsite,
        ' the 'digits' parameter doesn't actually have to be saved anywhere.
        Do Nothing
    End Sub

    Public Shared Sub OnPropertySet(
                        sender As Object,
                        propertyName As String,
                        ByRef backingField As Decimal,
                        ByRef value As Decimal,
                        digits As Integer
                      )

        value = Math.Round(value, digits)
    End Sub
End Class
#End Region

Public Class ThrowOnNullAttribute
    Inherits PropertyHandlerAttribute

    Public Shared Sub OnPropertySet(sender As Object,
                                    propertyName As String,
                                    backingField As Object,
                                    value As Object)

        If value Is Nothing Then Throw New ArgumentNullException(propertyName)
    End Sub
End Class

Public Class UndoableAttribute
    Inherits PropertyHandlerAttribute

    Public Shared Sub OnPropertySet(obj As Object,
                                    propertyName As String,
                                    previousValue As Object,
                                    ByRef newValue As Object)

        UndoRedo.Remember(Sub() CallByName(obj, propertyName, CallType.Set, previousValue))
    End Sub
End Class

Class NotSupportedAttribute
    Inherits PropertyHandlerAttribute

    Shared Sub OnPropertyGet(Of T)(obj As Object,
                                   propertyName As String,
                                   ByRef backingField As T,
                                   ByRef value As T)

        Throw New NotSupportedException(propertyName)
    End Sub

    Shared Sub OnPropertySet(Of T)(obj As Object,
                                   propertyName As String,
                                   ByRef backingField As T,
                                   ByRef value As T)

        Throw New NotSupportedException(propertyName)
    End Sub
End Class

13.2 New Replacement Modifiers – Declarative w/ Source Generators

This is approach is all about conversational collaboration between an end-developer and source generation tools.

Replaceable and Replaces Modifiers

Analogous to Overridable and Overrides modifiers. Another source member declaration can replace this one.

Human-to-Tool

e.g. Human writes algorithm semantics naturally and tool mutilates optimizes it for performance.

' Handwritten.vb
<Optimize>
Replaceable Function ToCommaSeparated(
                       items As IEnumerable(Of Object)
                     )
                     As String

    Let firstItem = items.FirstOrDefault()?
    
    If firstItem Is Null Then Return ""

    Let result = firstItem.ToString()        
        
    For Each item In items Skip 1
        result &= ", " & item.ToString()
    Next
    
    Return result.ToString()
End Function
' ToolGenerated.vb
Replaces Function ToCommaSeparated(
                    items As IEnumerable(Of Object)
                  )
                  As String
                     
    If TypeOf items Is Object() Then
        ' Array fast-path doesn't allocate enumerator
        ' for array and indexing is faster anyway.
        Select Case items.Length
          Case 0
              ' Don't do anything for an empty collection.
              Return String.Empty
          Case 1
              ' For array of 1, simply return its `ToString()`.
              Return items(0).ToString()
          Case Else
              ' Allocate `StringBuilder` as last resort.
              Let builder = New StringBuilder(items.Length * 4)
              
              builder.Append(items(0).ToString())
              
              For i = 1 To builder.Length - 1
                ' Don't concat then append;
                ' append twice.
                  builder.Append(", ")
                  builder.Append(items(i).ToString())
              Next
              
              Return builder.ToString()
        End Select
    Else
        Using enumerator = items.GetEnumerator()
            ' Don't do anything for an empty collection.
            If Not enumerator.MoveNext() Then Return String.Empty

            Let firstItem = enumerator.Current
            
            ' For collection of 1, simply return its `ToString()`.
            If Not enumerator.MoveNext() Then Return firstItem.ToString()

            ' Allocate `StringBuilder` as last resort.
            Let builder = New StringBuilder(firstItem.ToString())
              
            Do
                ' Don't concat then append;
                ' append twice.
                builder.Append(", ")
                builder.Append(enumerator.Current.ToString())
            Loop While enumerator.MoveNext()
              
            Return builder.ToString()
        End Using
    End If
End Function

Human-to-Tool-to-Human

e.g. Tool provides default implementation which human tweaks for correctness. Note the conversational back-and-forth.

The developer requests boilerplate serialization logic:

' Handwritten.vb - Step 1.
Class Student
    
    Property StudentId As String
    Property LastName As String
    Property FirstName As String
        
End Class

<Xmlifiable>
Class AthleticsTeam
    
    Property Sport As String
    Property School As String
    Property Coach As String
    ReadOnly Property Players As New List(Of Student)
    
End Class

The tool responds with its best guess based on conventions:

' ToolGenerated.vb - Step 2.
Partial Class AthleticsTeam

    Replaceable Function ToXml() As XElement
        <Team>
          <School>School</>
          <Sport>Sport</>
          <Coach>Coach</>
          <Players>
            For Each student In Players
                Yield ToXml(student)
            Next
          </Players>
        </Team>
    End Function
    
    Replaceable Shared Function ToXml(student As Student) As XElement
        <Student>
          <StudentId>student.StudentId</>
          <LastName>student.LastName</>
          <FirstName>student.FirstName</>
        </Student>
    End Function
    
End Class

And the human tweaks only what they need to:

' Handwritten.vb - Step 3.
Class Student
    
    Property StudentId As String
    Property LastName As String
    Property FirstName As String
        
End Class

<Xmlifiable>
Class AthleticsTeam
    
    Property Sport As String
    Property School As String
    Property Coach As String
    ReadOnly Property Players As New List(Of Student)

    ' Tweaks the default (generated) implementation of
    ' serializing a student without losing the benefit
    ' of the generated boilerplate logic for the team.
    Replaces Shared Function ToXml(student As Student) As XElement
        <Player id-number={student.StudentId}>
          <FullName>student.LastName & ", " & student.FirstName</>
        </Player>
    End Function
    
End Class

MustReplace Modifier

Analogous to MustOverride modifier and P/Invoke Declare statements. The specified name and signature must be supplied by another source member declaration.

Human-to-Tool-to-Human

Developer declares a need:

' Handwritten.vb - Step 1.
<StoredProcedure("Customers_SelectByCity")>
MustReplace Function GetCustomersByCity(city As String) As List(Of String)

Tool supplies the need but declares a need of its own:

' ToolGenerated.vb - Step 2.
MustReplace Function CreateConnection() As DbConnection

Replaces Function GetCustomersByCity(city As String) As List(Of String)
    Using connection = CreateConnection()
        With command = connection.CreateCommand()
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Customers_SelectByCity"
            
            With .CreateParameter()
                .ParameterName = "@city"
                .DbType = DbType.String
                .Value = city
                
                command.Add(.Me)
            End With
            
            connection.Open()
            Using reader = .ExecuteReader()
                Let results = New List(Of String)
                        
                Do While reader.Read()
                    results.Add(reader.GetString(0))
                Loop
                connection.Close()
                
                Return results
            End Using
        End With
    End Using
End Function

Developer supplies what tool needs to fulfill initial request:

' Handwritten.vb - Step 3.
<StoredProcedure("Customers_SelectByCity")>
MustReplace Function GetCustomersByCity(city As String) As List(Of String)

Replaces Function CreateConnection() As DbConnection
    Return New SqlConnection(
                 ConfigurationManager.ConnectionStrings!AcmeDb.ConnectionString
               )
End Function

NotReplaceable Modifier

Analogous to NotOverridable modifier. The provided implementation must not be replaced. Only useful if the final design includes that certain kinds of declarations are replaceable by default (i.e. auto-implemented properties and events).

13.3 Partial members

Partial Modifier

Can now be applied to any type member, regardless of access modifiers. All partial metadata merged. Useful for adding attributes, documentation comments, Handles, Implements, Overrides clauses to an existing member in another file.

' `End Sub` no longer required for `Partial` subs.
Partial Sub OnModelLoaded()
' End Sub

13.4 Semantic Pre-Processing

This capability is highly depending on the performance of certain analyzer or source generator techniques. But the versioning/cross-plat scenario has been a pain for a while.

' Conditionally compile statements.

' Enables better code-sharing (e.g. shared projects/linked-files)
' and cross-platform "adaptive light-up".
Let obj As MyType = ...

##If METHOD_EXISTS(obj.BetterMethod) Then
    obj.BetterMethod(...)
##Else
    ' Must call obsolete method in this case.
    #Ignore Warning BC40008
    obj.OldSlowMethod(...)
##End If

' Conditionally compile declarations.

' Enables source generation from syntax w/o requiring semantic
' analysis.
##If TYPE_EXISTS(System.DateTimeOffset) Then
    Sub WriteLine(value As DateTimeOffset)
        ...
    End Sub
##End If

' Enables source generators to provide boilerplate if missing.

' ToolGenerated.vb
Partial Class MyViewModel
    Implements INotifyPropertyChanged

    ' Only declare this if the end-developer didn't.
    ##If Not MEMBER_EXISTS(PropertyChanged) Then
        Event PropertyChanged As PropertyChangedEventHandler
    ##End If

End Class

##If MEMBER_EXISTS(OnNameChanged) Then
    OnNameChanged()

##ElseIf MEMBER_EXISTS(OnPropertyChanged) Then
    OnPropertyChanged("Name")

##Else
    ' Derived types can't raise base class events!
    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Name"))
##End If

' Enables project templates/source generators to detect
' errors or provide warnings.

##If ... Then
    #Warning "Behavior may be different on this platform. Did you mean to target .NET 11?"
##End If

##If Not TYPE_EXISTS(...) Then
    #Warning "Types from 'MyAssembly' required at runtime via reflection. Add a reference to ensure binary is deployed on build."
##End If

##If Not MEMBER_EXISTS(...) Then
    #Error "Must define a deserialization constructor."
#End If

14. Type-System Enhancements

' Ad-hoc intersection types.
Let i As {IDisposable, ICloneable} = ...

' Ad-hoc union types.
Let u As {IDisposable Or ICloneable} = ...

' Array pseudo-type?
Let vector As Array(Of Byte) = {}
Let jagged As Array(Of Array(Of Integer)) = {({})}

' Array instantiation by size (rather than bounds).
Let buffer = New Array(Of Byte)(1024)

' Millisecond in `Date` literals.
? #1/1/2026 10:00:00.200#

' DateTimeKind in `Date` literal?
? #1/1/2026 UTC#
? #1/1/2026 Local#

' Built-in type support for: DateOnly, TimeOnly, TimeSpan, DateTimeOffset
? #7/4/2026#d
? #12:00#t
? #1h 35m#s
? #200ms#s
? #4/2/2007 7:23:57 PM - 4/3/2007 2:23:57 AM = -07:00:00#

' Not shown:
' Built-in type support for: Half, Int128 (^/`LongLong`?), UInt128
' Special-handling of: System.Numerics types?
' Built-in type support for BigInteger?
' Operator inference from interfaces (e.g. IEquatable, IComparable, etc)?

    15. Inheritance, Interface Implementation, and Extension

    ' Signature (and name?) relaxation for member overriding.
    ' (ร  la relaxed delegates).
    Class Base
    
        MustOverride Function Clone() As Base
    
    End Class
    
    Class Derived
        Inherits Base
    
        Function Clone() As Derived Overrides Base.Clone
            ...
        End Function
    End Class
    
    ' Implicit interface implementation.
    ' (Great for code generation!)
    Class ResourceHandle
        Implements IDisposable
        
        Sub Dispose() ' Implements IDisposable.Dispose
            ...
        End Sub
    End Class
    
    ' Signature relaxation for explicit interface implementation.
    Class StudentCollection
        Implements IEnumerable(Of Student)
    
        ' No need for second method.    
        Function GetEnumerator() As IEnumerator(Of Student)
          Implements IEnumerable(Of Student).GetEnumerator,
                     IEnumerable.GetEnumerator
    
            ...
        End Function
    End Class
    
    ' Interface implementation delegation to fields/properties.
    ' (Composition Over Inheritance FTW!)
    
    Interface IContractA
        Sub M()
    End Interface
    
    Interface IContractB
        Sub X()
            
        Sub Y()
    End Interface
    
    Class ContractAProvider
        Implements IContractA
        
        ...
    End Class
    
    Class ContractBProvider
        Implements IContractB
        
        ...
    End Class
    
    Class MyObject
        Implements IContractA, IContractB
        
        ' Whole composition.
        Private WholeProvider As ContractAProvider Implements IContractA
        
        ' Partial composition.
        Private PartialProvider As ContractBProvider Implements IContractB
        
        Private Sub IContractB_Y() Implements IContractB.Y
            ...
        End Sub
    End Class
    
    ' Extension Properties.
    Module MyExtensions
    
        ' Note: Can be generic.
        <Extension>
        ReadOnly Property SecondOrDefault(Of T)(list As List(Of T)) As T
            Return If(list.Count > 1, list(1), Nothing)
    
    End Module

    16. Performance and Interoperability

    ' Fix: Smarter name resolution to avoid `Console` namespace
    ' hiding `System.Console` class, necessitating an explicit
    ' `Imports System`
    Imports Microsoft.Extensions.Logging
    
    ' error BC30456: 'WriteLine' is not a member of 'Microsoft.Extensions.Logging.Console'
    Console.WriteLine("Hello World!")
    '       ~~~~~~~~~
    
    ' `InitOnly` properties.
    Class Account
        
        ReadOnly _Id As Guid
        
        InitOnly Property Id As Guid
            Get
                Return _Id
    
            Set(value)
                If value = Guid.Empty Then Throw
                    
                _Id = value
        End Property
    End Class
    
    ? New Account With {.Id = Guid.NewGuid()}
    
    ' `MustInit` (required in C#) properties.
    Class Person
        MustInit Property Name As String = ""
        MustInit Property Age As Integer
    End Class
    
    ? New Person With {.Name = "Anthony", .Age = 41}
    
    ' Piecewise initialation allowed before using other members?
    Let p = New Person
    p.Name = "Anthony"
    p.Age = 41
    ? p.ToString()

    Not shown

    • ByRef structure story.
    • Optional in delegates.
    • System.Range interop.
    • C# “Extension everything” story/interop.
    • Interpolated string handlers.
    • Delegate and Enum generic constraints.
    • Unmanaged(?) Constraint.
    • “extern alias” scenarios.
    • Recognize overloaded ^ (exponentiation) operator from F#.
    • Recognize overloaded ^ (exponentiation) operator from Pow.
      (e.g. BigInteger).
    • Recognize overloaded =/<> operators from IEquatable.
    • Recognize overloaded
    • Recognize overloaded ++ and -- for For loops?
    • Optimized Select Case TypeOf for classes using Visitor Pattern.
    • LINQ optimization using value tuples?
    • Optimized imperative generation of For Each ... Where ....
      (When appropriate, i.e. in-memory, not IQueryable, well-known methods.
      used on in-memory collections.
    • Generalize For Each -> For code-gen for IList(Of T).
      (Where appropriate)
    • Generalize delegate Invoke syntax.
      (May just be Default method attribution)
    • Value (open) delegates?
    • “Property Delegates”?
    • Syntax for iterator functions that return structures?
    • (Extra items I remember this morning but have forgotten but presume I’ll remember again later)

    17. Runtime Library Enhancements

    There are a host of improvements that need to be made to the “VB runtime”, including but not limited to:

    • Performance of runtime methods particularly around intrinsic conversions.
    • Better (useful/informative) exceptions for conversions.
    • Better (useful/informative) exceptions for late-bound operations.
    • More support for early-bound language semantics (e.g. target-typing) when late-binding.
    • Better factoring for alternate platforms/targets/etc.
    • Bug fixes such as support for binary literal syntax (and separators?) in conversions.
    • Modernization of My helpers (e.g. Async methods–might be done already).
    • Additional functionality.

    18. Needs more bake time

    These ideas haven’t even been put in the oven. There’s awareness or scouting or sketching ideas but they aren’t “approved in principle”. They’re interesting or experimental concepts that I include so that you know I’m aware of things that have been brought up.

    18.1 Target-typed conversions?

    ' For those of us who prefer variable types on the variable.
    
    Let obj As Object = 1S
    
    Let i As Integer = DirectCast obj
    
    Let v As T = Trycast expression
    
    ' I think this will cause fist fights.
    M(CType(x))

    18.2 Guarded Let

    Ok, so one day I hear an F# dev lament the lack of early returns in functional expressions. So, as I do, I thought “How would I write this code in ModVB?” and here’s what I came up with:

    Function GetIVsTextView() As IVsTextView
        
        Let textManager = GetService(Of SVsTextManager)
        If TypeOf textManager IsNot IVsTextManager Then Return Null
            
        Let rdt = GetService(Of SVsRunningDocumentTable)
        If TypeOf rdt IsNot IVsRunningDocumentTable Then Return Null
            
        Let hresult = FindAndLockDocument(rdt, Out docData As IntPtr)
        If Not ErrorHandler.Succeed(hresult) Then Return Null
        If docData = IntPtr.Zero Then Return Null
            
        Let textBuffer = Marshal.GetObjectForIUnknown(docData)
        If textBuffer Is Null Then Return Null
            
        hresult = GetActiveView(textManager, 1, textBuffer, Out textView)
        If Not ErrorHandler.Succeeded(hresult) Then Return Null
            
        Return textView
    
    End Function

    This was oppressively monotonous and I started thinking about simplifying with tuples and nullability or Out variables and anything I could think of to break the boring pattern. This made me realize that repeating the variable name to check for null was annoying. I knew I didn’t like the if let/guard let syntax in other languages (e.g. Swift) because it doesn’t read naturally to me. Eventually I realized I was actually cool with the concept, I just didn’t like that syntax and came up with this to eliminate a lot of duplication:

    ' Declares and initializes a variable (or variables).
    ' If the value of the variable after is null, executes
    ' the specified control flow statement.
    ' (e.g. `Return`, `Throw`, `Exit`, `Continue`, etc)
    Let variable = expression Else <Control Flow Statement>
    
    ' e.g.
    Let v = e Else Continue For
    
    ' Executes specified control flow statement if variable
    ' is not null.
    Let variable = expression Then <Control Flow Statement>
    
    ' e.g.
    Let v = dictionary.TryGetValue("key") Then Return v
    
    ' Executes statements if variable is initialized to
    ' non-null value. Variable loses scope after `End Let`.
    Let variable = expression Then
        ...
    End Let
    
    ' `Then` and `Else`.
    ' e.g.
    Let v As T = TryCast e Then
        ...
    Else
        ...
    End Let
    
    ' Not sure about this:
    Let list = lists.TryGetValue("key") Else
        list = New List(Of Object)
        lists.Add("key", list)
    End Let
    
    ' But `list` shouldn't be in scope here.
    ' Should it have been `End Else`?
    ' This logic should maybe be in a TryGetValueOrCreate?
    list.Add(value)
    
    ' And what about conditional 'Set'?
    Let list As List(Of Object)
    Set list = lists.TryGetValue("key") Else
        list = New List(Of Object)
        lists.Add("key", list)
    End Set
    list.Add(value)

    I’m also interesting in how this kind of construct might be used with a “Result Pattern”, particularly if Case classes make defining such types easier.

    18.3 Case Else variable

    ' Expect the unexpected?
    ' (Great for enums!)
    Select Case expression
        Case value1
            ...
    
        Case value2
            ...
    
        Case Else other
            ' Only needed a variable if the value is unexpected.
            ' This keeps the scope limited to this case.
            Throw New UnexpectedValueException(other)
            
    End Select

    18.4 Exclusive For “upper” bound

    I’m a little tired of arr.Length - 1. It didn’t bother me in QB/VB6 because of the UBound function (which only works with arrays). On the other hand, some APIs take exclusive ranges so this x To < y syntax might be generally useful.

    For i = 0 To < arr.Length
        ...
    Next
    
    Console.WriteLine(String.Join(", ", 0 To < 10)

    18.5 Parallel extensions

    We’ve integrated asynchrony but not parallelism. Worth thinking about eventually. For now Parallel.For is enough.

    18.6 Retry/Resume

    The only scenarios where I can see using On Error today is for Resume Next or retrying with Goto. If we can hit that with Try that kind of unstructured error handling can be deprecated.

    ' This isn't that bad.
    Let retryCount = 0
    Try
        Retry:
        ...
    Catch ex As Exception When retryCount < 3
        retryCount += 1
        Goto Retry
    End Try
    
    ' This is worse but likely uncommon and there are
    ' other ways to do this elegantly.
    Try
        ' Step 1
    Catch
    End Try
    
    Try
        ' Step 2
    Catch
    End Try
    
    Try
        ' Step 3
    Catch
    End Try

    18.7 More robust mapping

    Should there be some “natural join” syntax to map the members of one object to another?

    Should there be a way to indicate that an unmapped member is intentional?

    ' If there's a warning that you forgot to serialize a property,
    ' how do you silence it without turning off the warning entirely?
    jsonWriter &= {
                    "firstName": person.Name.Given,
                    "lastName" : person.Name.Surname,
                    null       : person.EmailAddress
                  }
    
    ' Same question with deserializing.
    ' Pattern matching into existing variables/properties is
    ' still an open question, though.
    Let address = New Address
    If ShapeOf json IsNot {
         "line1": address.Street
         "line2": null,
         "city" : address.City
         "state": address.State
       }
    Then
        Throw New DeserializationFailureException()
    End If
    
    Return address
    
    

    18.8 VB Idiomatic custom JSON serializers

    This goes with the previous example. The only scenario I consistently hear for “ref structs” is custom JSON serializers with Utf8JsonReader. Setting aside the general issue, this assumes that the idiomatic way to write a deserializer in VB is the way it’s done in C#. I could delay, mitigate, or side-step the issue if VB had a natural abstraction for this.

    18.9 More elegant integration of case-insensitivity/collation

    VB has many built-in constructs that may compare strings:

    • Comparison operators =, <>, < , >, <=, >= in binary expressions.
    • The same in Select Case blocks.

    Many applications benefit from case-insensitive string comparisons but this means not using the built-in support for strings, which is a hallmark of BASIC since time immemorial.

    You can use Option Compare Text but it has unexpected null handling and only affects comparison operators and Select Case and certain APIs in the VB runtime library (if you’ve ever wonder how that works–it’s basically <CallerOptionCompareSettingAttribute>). And this doesn’t include the Distinct query operator at all. This means giving up the syntactic sugar and calling Equals and passing ComparisonType or StringComparer.

    This issue becomes worse with String and JSON pattern matching. An elegant solution that integrates with existing .NET APIs would be wonderful. There are some starting places for investigating such as design but it needs more consideration.

    18.10 Lookahead and backtracking in String patterns

    As-is pattern-matching evaluates patterns from the outside to the inside, left to right. This raises several problems:

    • A naรฏve implementation of String pattern matching would allocate many needless intermediate garbage strings.
    • Certain common patterns need backtracking.
    • Inner patterns can’t influence decomposition of the outer pattern and lazy or greedy ways.

    It would be good in the design of the string pattern matching to consider how the feature would apply to a text window/stream or a span.

    18.11 String vs Span/ReadOnlySpan(Of Char/Byte) and UTF-8

    String handling in VB must be awesome! We cannot allow modern problems to introduce ugliness into our codebases.

    18.12 Named pattern inputs

    Some hypothetical patterns would benefit from additional “inputs” (e.g. Regex("\d+", n)) but currently the “argument” list for a named pattern is output only. This should be investigated.

    18.13 Patterns as data?

    Composition is essential to managing complexity but as-is there isn’t yet a way to pass patterns around as data or compose them. Fixing this could open some scenarios.

    18.14 PLINQ, Async queries, more query operators

    Parallel LINQ, queries that use Await, queries against IAsyncEnumerable are areas to investigate. As query languages and data stores evolve we also have to ensure that the query comprehension syntax can smoothly express common use-cases (e.g. Common-Table Expressions, Geospatial data, No-SQL, Temporal Databases) without needing to fallback to extension methods and lambda expressions.

    18.15 New ConfigureAwait options/fire-and-forget exception handling

    Void-returning async methods are my white whale. I must find more ways to limit their spread. Additionally when “forgetting” a Task-returning async method I need to be sure folks can express/configure the various patterns to unobserved exceptions.

    18.16 “Nullable Reference Types”

    Nullable Reference Types has received mix reviews in C#. And VB users have expressed confusion and concern about how it would be done in VB. I consider it a failure if users hate the new feature turn it off or advise others to do so. I must refine the design to consider these observations.

    18.17 Handling duplicate declarations, “File” accessibility, “Appendable” methods?

    Source generator authoring (and performance) could be negatively impacted trying to avoid name clashes, duplicate definitions (or ensuring there aren’t). I’ve thought through some ways to handle this at various layers.

    I personally dislike stitching together statements from across multiple files into one method. I don’t want two VB script files to just run back-to-back or for end-developers to have to think too hard about the order partial files would be combined in. With that said, a source generator analog to a “multicast delegate” could impact how certain scenarios are addressed between my various declarative programming approaches.

    18.18 Generative compiler scripting

    I’m pretty sure this is a bad idea. But at some point I’ll venture into that dragon’s lair.

    18.19 Units of Measure?

    When F# debuted with many cool ideas. I like the feeling of units of measure but I’m not convinced it’s a no-brainer. I’ve gone through several exercises to see how a VB user might implement a unit of measure system in a library without language support at all, or, what the minimal set of features could be added to make the experience of creating and using such a system acceptable. I will continue to investigate but so far my exercises have yielded 1 uncontroversial feature: Generic Operators.

    There is an attractive syntactic sugar that isn’t necessary but would clean up some rough edges, e.g. Weight(in lbs) but exploring the space is absolutely worth a dedicated and detailed in-depth posting.

    18.20 Annotated types/Type flavors

    In Visual Studio 2008 VB introduced XML literals. The VB team had a feature for providing IntelliSense for XML elements and attributes in the code editor based on schema files:

    When building Roslyn we made the painful decision to cut this feature for time and I’ve been stewing over how to gracefully get it back. Additionally, now with a greater emphasis on JSON a similar IntelliSense experience for examining JSON payloads in the editor is a no-brainer. Given some context the IDE should be able to provide a list of completions when you type jsonObject!

    To support this experience I’ve already mentioned a desire to make XML and JSON schema types expressible in code, even if just decoration:

    ' Suggestions should be provided after typing `order.<`.
    Sub ProcessOrder(order As <PurchaseOrder>)
    
    ' Suggestions should be provided after typing `request!`.
    Sub ProcessRequest(request As {"trade-message"})

    These don’t have to be realized as their own types (in fact it’s better if they aren’t) but more of an annotation on e.g. XElement or JsonObject that tools can use.

    And we do something like this today with tuple types–(x As Integer, y As Integer) doesn’t generate a new type but annotates the declaration in such a way as to surface those names on ValueTuple(Of Integer, Integer).

    C# does basically the same thing to represent dynamic which is otherwise represented as System.Object within the .NET runtime. And ModVB will do the same with the Any type.

    What if simple anonymous delegate types e.g. unified with Action and Func types instead of generating their own anonymous ones, e.g. <Function(Object) As Boolean> is a flavor of Func(Of Object, Boolean). VB already does some magic to erase those types if ultimately converted to a nominal delegate type. Then VB users could use Action and Func types while still surfacing valuable information to callers:

    ' Do you know what the two parameters to the delegate mean?
    Function ReplaceNodes(
               nodes As IEnumerable(Of Node),
               replacer As Func(Of Node, Node, Node)
             )
             As Node
             
    ' How about now?
    Function ReplaceNodes(
               nodes As IEnumerable(Of Node),
               replacer As <Function(original As Node, rewritten As Node) As Node>
             )
             As Node

    And then there’s the aforementioned units-of-measure question, what if it were tracked as an annotation on a numeric variable rather than a real type?

    Funny story: Once Erik Meijer (who was a significant influence on VB9) told me that the way the XML IntelliSense was done was actually essentially “type provider”, at least for the purposes of filing a patent on it. Which brings me to my final scenario:

    Type Providers.

    Ok, not the compile-time code generation/injection approach used in F# but something a little less … trusting of 3rd party code running inside the compiler. I haven’t landed on an intuitive syntax but imagine if a developer could annotate any type reference in code with some arbitrary metadata which tools could use to provide suggestions and diagnostics!

    ' Annotated with the key for a connection string.
    Let db = New SqlDbProxy(For "AdventureWorks")()
    
    ' Completions could be provided by an editor extension that
    ' can connect to the database locally.
    For Each p In db.Tables!Products
       Where p!Price(As Decimal) >= 100
    
    ' Remember that thing I said about `Default` methods and invocables?
    db.StoredProcedures!sp_DeleteOldRecords()   
       
    Let api = New RestProxy(For "api.github.com")()
    
    ' REST API URL completions.
    ? api.HttpGet("/repos/...")
    
    ' As easy as late-binding but not actually.
    ' Windows Management Instrumentation.
    Let wmi = New WmiProxy(For "\\.\root\cimv2")()
    wmi.Classes!Win32_Process.Methods!Create(commandLine:="notepad.exe",
                                             returnValue:=Out code As UInt32)
    If code = 0 Then
        ...

    And that’s the real scenario, enabling tooling to provide the safe(er) experience a types while retaining the lightweight footprint of late-bound object models but not paying the runtime cost of true Reflection/DLR machinery at run-time!

    At this point I’m laughing maniacally. But yeah, that’s 6 scenarios and I thought up like 2-3 more after I stopped writing this section. I think that’s enough to justify searching for a unified solution.

    18.21 Case Classes, Structures, and Interfaces

    OK, this is the last section I’m going to write. I’ve known “discriminated unions” were on the “look at this one day” list for dotnet (like many F# features) so I have been watching functional videos and reading articles to appreciate them. And I mostly don’t. But I did see 2 examples that cracked the door and I’ve reframed the issue for myself and that has produced some syntax sketches that I’ll share for illustration purposes.

    I’ll dive into this another day, but sooner than later.

    18.22 Overloading on Structure constraint

    Sometimes when dealing with generics, I wish I could provide different implementations for value types and reference types especially when any kind of nullability is involved. But you can’t overload methods on generic constraints. But there’s a cheat.

    ' (As we all know...) you can't overload on generic constraints.
    ' This is illegal:
    Sub M(Of T As Class)(p As T)
    Sub M(Of T As Structure)(p As T)
    
    ' But this is not:
    Sub M(Of T As Class)(p As T)
    Sub M(Of T As Structure)(p As T, Optional ignored As Object = Nothing)
    
    ' Because VB considers type parameter constraints in determining
    ' which overloads are applicable, these lines do what you'd hope:
    M("") ' Calls reference type overload.
    M(1)  ' Calls value type overload

    So this is a “to-do” to consider how to remove the need for the fake parameter in source even if it needs there in the metadata. That’s it.

    18.23 Platform Native Instruction Helpers

    Tired of being asked to add a one-off keyword for every opcode in the CLR. I wouldn’t go so far as to add “IL Literals” but maybe there’s something that gets the job done without directly bloating the language (similar to P/Invoke), e.g.:

    • Case-sensitive member resolution against poorly written libraries.
    • Checked/Unchecked arithmetic.
    • Volatile
    • Localloc

    VB already recognizes certain method calls and emits them as intrinsic IL instructions. It’s just a matter of expanding the list and defining a well-known module in the runtime with the right shared methods in the right shapes to make more of IL expressible through VB.

    Of course, there will always be instructions so mainstream that they should be surfaced more naturally, e.g. the VB language specification outright says that DirectCast and TryCast were added to make the native unbox and isinst instructions available natively. And maybe there’s some shared infrastructure for easily supporting System.Runtime.Intrinsics for those so inclined.

    18.24 Scripting and Interpreted Code

    Of course.

    18.25 Simplified “Chained” ternary expressions (Case expression lite?)

    ' Something like this.
    ? If(year Mod 400 = 0,
           True,
         year Mod 4 = 0 AndAlso
         year Mod 100 <> 0,
           True,
         Else,
           False)

    18.26 Type Predicates in Flow-Sensitive Typing

    I haven’t given much thought to this at all but it occurred to me when I was writing the type-inference section that it might be simple to generalize things a little further.

    I showed that checking if a variable of type IEnumerable to see if its value is of the type IDisposable will cause in that execution path the type of the variable to become the composite (intersection) type {IEnumerable, IDisposable}.

    Then mentioned that you can treat nullness like this so that a variable’s type could be {String, Not Null}.

    Skipping a long story … effect system … TypeScript … “future-proof?” …

    For the sake of illustration imagine this (conceptually):

    ' Pseudo-code, not proposed syntax.
    Interface Negative
        Shared Function IsNegative(x As Integer) As Boolean
            Return x < 0
        End Function
    End Interface
    
    Interface Positive
        Inherits NonNegative
        
        Shared Function IsPositive(x As Integer) As Boolean
            Return x > 0
        End Function    
    End Interface

    That is to say if I could provide the system I designed a named type which represented some function returning true then it would start plumbing things through like {Integer, Not Negative} or have a declaration like count As {Integer, Positive}. That’s enough that it’s an interesting thing to explore at some point but I haven’t thought too deeply about it yet.

    18.27 More versioning tools to let library authors improve things without binary breaks

    We have TypeForwardedToAttribute to move a type to a different assembly. What about moving it to a different namespace? What about renaming types or methods? Could I design some kind of “virtual” or “meta” namespace concept that let’s authors provide cleaner experiences, e.g. Imports Wpf instead of the 5 or 6 namespaces you actually need. Is there a way to provide a cleaner System namespace for new developers? StringBuilder should be there, _AppDomain probably shouldn’t. What would tooling look like on upgraded projects, looking for help, docs, etc.

    18.28 Arbitrary block scopes?

    Just remembered that. People keep asking for this.

    18.29 Rust-style “ownership” for disposables, etc.?

    Could we? Of course. Should we? Who knows?

    Take, Borrow, Give, Lend

    18.30 Idk, something about AI I guess

    Ugh. (sigh) yeah, I know…

    Wrap-up

    This post took me at least a month to write. It is 59-pages long when printed, which breaks my previous record. That’s much compressed from the original 300-page+ … “depth-first” expansion of my original outline that I estimated I’d need several years ago. To get down to this length I had to cut out a lot of me talking and explaining and justifying and anecdotes. I could specify and prototype and script and record and demo videos on YouTube and edit them down to ~5mins to hold attention. I could write the grammar and talk about back-compat and the debugging and IDE experience and how to represent these changes in the Roslyn Syntax API in a compatible way that vanilla-VB tooling won’t choke on new features. If I wanted to spend the rest of my 40s trying to prove things to some unknown judge my anxiety has created. But this is like years of work and it won’t all come out in one release and there’s a lot of work to do. This “one-pager” represents probably 80% of the compiler/language possibilities that I’m motivated to pursue in ModVB. But that’s only like 65% of the total project. The other 35% is IDE stuff, etc. And on top of that there’s broader OpenVB community/organization/foss stuff that the entire VB community needs to be part of. This is not “a product” or “a release” or “some features”. It’s a living programming language community and everything that entails on an ongoing basis without a defined end-point.

    I hope this document gives people a sense that the future of VB isn’t about “catching up” to some other perceived front-runner. It’s not about feature envy or re-styling the things other communities are focusing on but finding and maintaining our own focus on what our community has to offer and can become.

    And this all lives in my head all the time and I’d like to get it out of there. So, I’m going to trust my audience to trust me. And respect me, and my time, my ideas, and focus on making cool things and stop selling myself and selling the idea that there are cool things worth making. Thanks for reading.

    Regards,

    -Anthony D. Green

    This Feature Intentionally Left Blank

    A few posts ago I put out a bullet outline of the lengthy agenda for ModVB. I intentionally didn’t do much editing to it since the point was to be more comfortable publishing things before they’re in a final, marketing-approved state, as is the habit I developed over 8 years at Microsoft.

    One particularly attentive reader noticed an item I had forgotten I’d left in the outline with a rather curious heading: “10.4 This feature intentionally left blank”. I suspect he thought it was a joke–a eccentric reference to the Intentionally blank pages that sometimes appear in printed books and such. Per Wikipedia, “Such notices typically appear in printed works, such as legal documents, manuals, and exam papers, in which the reader might otherwise suspect that the blank pages are due to aย printingย error and where missing pages might have serious consequences”.

    While it’s definitely a reference to that, it’s not a joke. This is a feature I’ve been mulling over for a couple of years now and I want to walk you through a few scenarios.

    More than a decade ago, while making some code change for Roslyn, I think I was either making a class into a singleton or at least making sure it didn’t have a public constructor. VB and C# are both languages which don’t force you to explicitly declare a default constructor. If you don’t define any constructors, a public one is implicitly defined for you with no arguments. That means if you don’t want a public constructor, you must define one so that you can mark it private, even if it has no other functionality. It must exist to not be called. Leading to an odd looking block of code like this:

    Private Sub New()
    End Sub

    I find an End Sub statement immediately after a Sub New() statement to look weird (It’s almost never the case that a statement block exists with no code inside of it so of course it looks weird. So instead I wrote this to look less weird and make it look less weird.

    Private Sub New()
        Return
    End Sub

    Which I found better but in code review someone commented that seeing a constructor immediately return also looked weird, to them. I assume we compromised on a comment like,

    Private Sub New()
        ' This is not a mistake. This constructor is supposed to be empty.
    End Sub

    That’s a fine solution. And if that were the only niche case I wouldn’t be writing this post, but read on.

    Sometimes when you inherit a class, operations may include certain default behaviors for convenience which you need to turn off for whatever reason. Liskov Substitution aside and without regard to whether this is a good design for a base class author or not, this is a situation that may pop up. When it does it may be that you need to override some protected method simply to make sure that when a operation occurs, this extra behavior is not executed or an otherwise meaningless or inapplicable command is ignored.

    ' For 99% of controls either the default (rectangular) size calculation
    ' is fine, or the logic should be specifically overridden. But, in the
    ' case of this control, all size calculation is handled by the
    ' container and no logic should execute nor `Resized` events raised.
    Protected Overrides Sub OnArrangeCalled()
        '
    End Sub

    The pattern illustrated by these examples is default behavior which is usually correct and convenient to provide by default but that in a minority case needs to be turned off. And reviewers and maintainers should be alerted that the conspicuously empty statement block isn’t there because the rest of the code was forgotten, or a file was hastily checked in without saving, but is empty by design. That is to say that it is intended that in that case the program should do nothing.

    Introducing the Do Nothing Statement

    That’s right. Whenever I’m out in the world reading words and I see the expression “do nothing” I see the two words colored in keyword blue. This is my curse to bear. If VB didn’t already have both of those keywords already I’d have gone on to live a happy and productive life, but here we are.

    In true VB fashion this statement does exactly what it says on the tin–nothing. It is, as we say a no-op. And plenty of languages include some kind of no-op instruction, including x86/64 assembly, Python (pass), and .NET IL. And in many C-like languages sometimes the syntax requires a statement so an empty statement–a lone semicolon (;)–must be used. And the compilers emit nop instructions all the time. Have you ever wondered what happens when you set a breakpoint on an End If statement? Block End statements don’t do anything but for debugging purposes the compiler may emit a nop so the debugger can stop at a line which otherwise wouldn’t have corresponding instruction). So the concept of a no-op “statement” already exists in the compiler, it’s just not exposed explicitly in the high-level language.

    And in a way this is a statement analog of a null value or a sentinel value like Stream.Null, sometimes you need to have a thing that isn’t a thing at all but fits in a thing-shaped space.

    But wait! There’s more!

    I wouldn’t blame you for still being skeptical. This has to be the absolute least … existing feature one could ever implement. It doesn’t do anything, doesn’t add new keywords, but doesn’t cost much at all and if it went 6 inches farther in either direction it wouldn’t even be on the line of consideration. In fact this feature is the line of consideration. A feature that does less and/or costs more is certainly not worth discussing. But there are two more scenarios I want to present that have made me warm up to this idea which would otherwise feel simply whimsical. One is stylist and the other is a potential interaction with a possible future feature.

    I don’t know why but the longer I grew in the craft I became a little less comfortable with certain kinds of implication. It’s really more a nagging sensation. Or perhaps for simplifying the interpretive simulation in my brain I want to state clearly some things which would otherwise “fall out” later. That is to say I’m less comfortable with asking myself or the reader to do the thought exercise to recognize that a certain thing is the eventual logical consequence of the way I’ve written my code. For example:

    Select Case dayOfWeek
        Case DayOfWeek.Monday To DayOfWeek.Thursday
            ...
        Case DayOfWeek.Friday
            ...
        Case DayOfWeek.Saturday
            ...
    End Select

    Ignoring the limitations of enums (for now), “it goes without saying” that if a day isn’t M-Th, Friday, or Saturday that it must be Sunday. Stylistically, sometimes I want to say things that go without saying. And the more more code interleaved between the cases I’m dealing with and the conclusion that I’m not dealing with Sunday the more bothered I personally get remembering that Sunday wasn’t handled earlier.

    It’s a not a correctness thing but a style thing. It depends on the dev, the situation, maybe the day… of the week. I’m not saying what anyone should always do or what I always will do but I think it’s legitimate sometimes to want to expression oneself in that style.

    Now there are at least 3 ways to deal with this.

    Option 1. Handle Sunday

            ...
        Case DayOfWeek.Sunday
            ...
    End Select

    Looks clear. But this contends with another feature many developers love, flow analysis. If you’re one who wants the compiler to raise an error if a variable is never assigned on a code path or perhaps a function doesn’t return a value on all code paths, there is a hidden code path here. You and I know that logically there are only 7 days, or 12 months, or that all integers are one of < 0, 0, or > 0, but the compiler doesn’t, especially those first two. So the compiler will flag the invisible code path where it’s a day other than 7 mentioned and complain. So to shut the compiler up it may be tempting to take…

    Option 2. Handle the last case with Case Else

            ...
        Case Else
            ...
    End Select

    This will definitely satisfy the compiler (which is just annoying enough to have to do that one might turn off the warning entirely). But the problem is that this might hide a bug. What should the program do if it is passed an enum value containing -47 (now unignoring the limitations of enums)? Should invalid parameters be treated like Sunday? Maybe. But often, particularly when dealing with enums the programmer not only assumes but would enforce that only valid (defined) enum values are flowing around the program. Values that haven’t been defined may indicate that some other piece of the system isn’t working as expected.

    So, on Roslyn in the many places we check for what is believed to be the set of all legitimate cases we handle unexpected cases with…

    Option 3. Handle the last defined case explicitly and unexpected cases by throwing

        Case DayOfWeek.Sunday
            ...
        Case Else
            Throw New InvalidOperationException("tf?")
    End Select

    This satisfies the compiler’s need for completeness, the programmer’s need for correctness, and the reader’s need to know what the writer intended.

    You’ll note that none of these 3 options used the proposed Do Nothing statement. This was intentional. I’ve lured you into a trap!

    After years of having to add a Case Else to the thousands of enum checks scattered around the codebase “I’m tired, Boss”. And thinking about adding the ability to Select Case on the type of an object and union types this pain will only grow with time.

    So I’ve been thinking of ways to eliminate this repetitive boiler plate and at the moment my thinking has landed on a compiler option like Integer Overflow/Underflow Checking or Array Bounds Checking in pre-.NET VB. A flag that let’s you type the code like Option 1 but get the precision and safety of Option 3 by default. The design and merit and alternatives to that flag are beyond the scope of this post but for the sake of argument imagine it was added. Now you can write code in a natural way covering the expected code paths and the compiler will spit out a backstop/assert that throws an exception in the hopefully rare occurrence that somehow execution behaves in an unexpected way (like an integer overflowing, or access outside the bounds of an array, or calling an instance method on a null reference).

    This could be a win and it’s basically turning off “implicitly” falling through a Select Case by default. But now what about those cases where you really do want to silently fall through when a value not listed occurs. For example when future proofing; when writing a Roslyn analyzer or other tooling it’s often best to not throw exceptions if you encounter nodes you didn’t expect. If you write a code action for version n of a language that has 2 kinds of parameters ByRef and ByVal and in version n + 1 the language adds a 3rd (e.g. Out) you don’t want your tool to start blowing up because someone updated Visual Studio. Sometimes when you don’t know how to handle a situation, it’s better to… do nothing.

    So, if such a safety check flag were added to ModVB, a Do Nothing statement would become more valuable in the cases where “silently” falling through is still desired.

            ...
        Case Else
            Do Nothing
    End Select

    Which is just a new example of overriding default behavior to turn it off and who knows if more will present themselves.

    Many years ago a fun exercise going around the language teams was to figure the simplest syntax in different programming languages for defining and invoking a lambda expression that does nothing. The most unsettling one I recall was this syntax in C++ :

    [](){}();

    which defines and immediately calls a lambda expression that captures no values, takes no arguments, has no return value, and does nothing.

    I like Call (Sub() Do Nothing)() better, honestly!

    Wrap up

    So, those are the examples that nudged this feature up from “unserious and easily dismissible” to “… there’s an argument to be made for thinking about it seriously”. Did they nudge you? Sound off in the comments!

    Regards,

    -ADG

    Draft: Open VB.NET

    I’ve created a new OpenVBdotnet organization on GitHub. At the moment it is a place for starting some very important conversations within our community–conversations that are bigger than a personal blog and its comments section. I can present options to the rest of the community but I cannot unilaterally speak for us all, whatever our number. Simply follow the discussions to be alerted as I kick off those many conversations as I am able, and add your own.

    Action: At this time the most valuable work we can do is to identify ourselves. We need a focal point that welcomes self-selection without asking any further contribution. We need a headcount.

    Open VB.NET is NOT a fundraising organization. And it is distinct from the ModVB Project, which I consider to be a personal creative endeavor shaped by decades of experience with VB in my life and countless interactions with VB developers during my 8 years at Microsoft, which I will at some point submit as a candidate to the rest of the community for adoption. But it is critical to me that the manifestation of the VB community and its future not be conflated with, or hindered in any way by the perception, feasibility, or execution of ModVB, nor the wellness or worthiness of me personally, or any willingness or ability to contribute to any project, financially or otherwise.

    I think I’ve been putting the cart before the horse. I’m not interested in (or even capable of) being a consistent “content creator”. And I can’t use my own efforts to support my community to bootstrap that community. So I’m going to flip my approach on its head and parallelize. We will build a community together and then look at how that community is supported separately

    Avengers, Assemble!

    -ADG


    Feel free to skip this section to get my current thinking. What follows is a recap of how I got here if you really just like reading behind-the-scenes anecdotes.

    VB Homepage & Community Blog

    Way back before the the Roslyn project went open-source there was discussion on the PM team on whether the individual .NET languages should have their own homepages as a place to really focus on each language individually. Some of us felt that most popular languages out at the time had their own pages and that folks tend to think about their stack and ultimately identifier in a language-first way (“I want to learn Python”, “I am a Python developer”) and that our languages would benefit from having an entry point from that angle.

    Naturally, other folks internally preferred to think about (and advertise) our offerings in a platform-first way (“I want to adopt .NET”, “I am a .NET developer”). Unsurprisingly folks who came from the platform side of .NET thought about the platform and those of us who came up through the languages side thought about the languages as the best foot (feet?) to put forward. Ultimately it was decided to lead with platform.

    Aside: Marketing Brand Consolidation

    Incidentally, you may have noticed a consolidation of top-level Microsoft brands over the last few decades. Microsoft makes hundreds of products and I guess that’s unwieldy from a marketing side. Further, successfully winning mind share with one product won’t necessarily contribute to a better impression of another when they aren’t clearly linked.

    When I was a kid “Microsoft Word” was a brand put forth on its own. On shelves. On box-art. That was the name of a product. But eventually that became the Office suite and nomenclature moved to “Microsoft Office” as the top-level brand with the individual programs being subordinate (Microsoft Office Word, Microsoft Office PowerPoint). A quick Google returns a Microsoft page which reads “The Microsoft Office app is now Microsoft 365 Copilot” so you can see this process has continued.

    At some point in time the big top-level brands were I can remember were Microsoft .NET, Visual Studio, Microsoft Office, Microsoft Dynamics, Xbox (remember Xbox Music?), Surface, etc. There’s a sense to it in the modern marketplace. The more commonly a term appears … everywhere the easier it is to amplify it on social and track it.

    Likewise, “back in the day” there was Visual Basic (and Visual C++, and Visual FoxPro), a product. And you could buy Visual Basic .NET 2003 as a stand-alone package when I started with .NET. But no more.

    End Aside

    Even under the existing Microsoft sites it was a struggle to maintain dedicated spots. We used to have separate Dev Centers for each language, and Insider lists, and blogs too but that all slipped away and I noticed that the more the VB community was mixed into the crowd the quieter we became–the more we disappeared. At one point I tried to bring the VB Team blog back and we even tried to pilot the idea of opening the VB Team blog up to external contributors such as MVPs to give those VB community members who were occasionally blogging more visibility, but we got lost in the swamp of drafting legalese policies for who was allowed to post what and why and the effort died.

    Fast-forward to 2019. I had just finished fasting from my habits as VB PM at Microsoft for an entire year and concluded that I still wanted to engage as an outsider. I thought it would be good to make a community-driven site like I had imagined while at Microsoft. I spent a couple of weeks playing around with different names before I found one that I really liked: learnvb.net. It would be the place for everyone from novices to experts to learn anything related to VB.NET. Learn about the language, learn about community projects, learn about what individual developers and companies were building all over the world with it, learn about the latest in security best practices. I wanted to really normalize the notion that we can and should all be lifelong learners in our craft and that “learning” wasn’t just a dirty word used to marginalize perpetual “beginners”.

    Lofty vision, but nowhere near enough content to justify launching such a site. It wouldn’t really help to send people to a sparsely populated page under construction so I dialed back my initial release plan a lot and decided to start with just a simple personal blog. If I ever got enough general articles/content together I could rebrand. I agonized on a cool name for my own blog until I realized Eric Lippert’s blog URL was just his name and decided to copy a master and that’s how anthonydgreen.net came to be.

    As some of you may remember, my very first post was a massive 56-pager going over all kinds of non-trivial differences between VB and C#. It was very successful in getting views for a first post on a new blog. I’d hoped to save the second part of that exhausting list for a future date when I had more content to benefit from all those eyeballs but I never really got that together.

    Ok, so now I’m publishing posts with prototype language features. Uploading videos to my personal YouTube channel and eventually I decide to make ModVB–a modified version of the open-source VB/Roslyn compiler and Visual Studio tooling with a bunch of old and new dream features–and also now a new “brand” that was implicitly homed under my name.

    But what if I wanted to make content about vanilla VB or just general programming stuff that wasn’t ModVB specific? I was dreaming of being a “content creator” and you wouldn’t want a generic tutorial on VB debugger visualizers to just be under my YouTube channel or conflated with an ambitious but unofficial project like ModVB. So, I would still need the learnvb.net brand eventually. I could put non-ModVB tutorials or samples under that name and pre-release mod related content under my own name and/or ModVB.

    But, sometimes either myself or the community might make tools or libraries–assets–which aren’t tutorials or samples but are in a sense “community owned” (like analyzers, source generators, and project templates). For example, the grammar which is used to colorize VB code on GitHub, or the code converter, or a VSCode compatible LSP for VB aren’t really “learn”-related per se and it didn’t feel right to put them under that heading (think of the namespaces!).

    There’s actually another brand I picked up along the way (I own far too many domain names) but it’s not really important now, if ever, so I’ll skip to the more recent (and immediately relevant) thinking of the past year or so.


    Current thinking

    I’ve been researching and watching various developments in tech, and thinking a lot about power and collective ownership of the technologies we depend on as institutions and how over time different stakeholders gain interests in the future of a technology that goes well beyond the strategic pivots of any single company, or the financial (or other) interest of a single individual or select few who steward or even create those technologies. Those interests are often not explicitly represented.

    Cultural shifts around FOSS in the past 3 decades have been massively influential to creating the modern industry but I feel that further innovation is needed in models of governance and sustainability. Maintainers of massively successful libraries struggle to make ends meet, or successful communities have the rug pulled out from under them by a few with personal motives or caprice. No longer is the acceptance of open source code or platforms by businesses and governments the challenge. Rather, maintainer burnout, license changes, IPOs, and backroom deals are just some of the modern threats we must find ways to overcome.

    We have models of exercising collective power in employee-ownership of businesses and profit sharing, profit and non-profit healthcare co-ops, trade unions, and craft guilds that more equitably balance the interests of the many who are all too often suddenly impacted by the relative few. Now is the time to rethink past practices and explore novel approaches to distribution of power, costs, and benefits of the tech we are building our modern way of life on top of. We must insist upon seats at the table. And if we cannot get them we must build our own better ones.

    Draft: My Mind is an Adirected Dicyclic Graph (ADG) that I am Failing to Serialize

    Yes, I made up that term as a backronym to match my initials. I’ve been trying to write a blog post mostly about GitHub organizations but I got preoccupied trying to describe why I was struggling to write that post and that meta-post is easier to write at this time so here it is.

    Image from Bing search. I don’t own it. Please don’t sue me.

    As of the time of this writing I am 41 years of age and have been programming on and off in various capacities for almost 30 of those years. There’s no shortage of information stored in my brain. Lots of nodes, an insane number of edges trained and optimized over an interesting career for fast random access in both directions.

    And not just about programming, there’s other nerdy topics like fictional timelines, an at times annoying supply of Chicago trivia and navigational data, birthdays of family and friends, and yes a lot of stuff about programming. And it’s all so associative, if I see a thing or hear a thing or think about a thing I quickly jump 3-5 steps away to other “related” things. Things that are, things that were, and “some things which have not yet come to pass”.

    In certain very specific contexts this can be a powerful tool but in the last few years it’s become a massive hindrance to communicating effectively if at all. Because communicating effectively isn’t random, it’s serial. Start to finish, top to bottom, in order of appearance or importance I need to take this multidecade old wiki and walk it from a random entry point, not get stuck in infinite cycles, not stack overflow, not timeout, not grow exponentially grinding to a halt, not start mutating the data while retrieving it, and write all of that information out in a linear order.

    And I have to apply… or at least choose to apply, a good deal of empathy to that process. What must be said as a prerequisite? What counters or confusion or just plain questions are likely to arise. And from ages 25-35 or so this was fairly easy. I often joke about posting “Walls of Text” or nicknaming myself “Option Explicit”. But in the last few years I just can’t seem to brain-dump at will. I can’t find where to start (or stop). This is probably one of those life skills I would have developed if school hadn’t been so easy for me.

    I can respond to prompts pretty well, to lean into the current AI parlance. If someone asks me a question on Discord or some other place I can regurgitate a fair amount “off the top of my head”. If I have a substrate like a source document or outline I can transform it, decompressing information as I go. That’s how I wrote my “Exhausting List of Differences Between VB.NET and C#”, I didn’t go one by one trying stuff or compare the language specifications line by line, I just looked at the existing structure of the spec(s) and traversed it and recalled experiences as a user, experiences reported from customers, bugs seen in triage, etc. The bulk of that information is just cached in my brain and I don’t really have any control over that.

    It does not help that my favorite programming language uses so many keywords that commonly pop up in everyday English sentences that like random signage sends me off, “If And Until… what would that mean?”.

    My 3rd manager at Microsoft, had our team do an exercise called Strengths Finder. It’s a kind of psychological personality test you take and it gives you a report on your top 5 “themes” or strengths. The idea being that if you know what you’re great at you can prioritize investing time and energy into maximizing the benefit of those strengths rather than endlessly trying to patch up every weakness–if you’re The Flash, run fast and don’t try to be as strong as Superman or as smart as Batman. Here are my Top 5 Themes:

    I remember when I first got the report I think I said to the team something like “Oh, great, so nothing practical or immediately useful, yay!”.

    Now, I could fight this or find out how to ride the waves better. I have tremendous context and relevant thought and problem solving and strategy that’s all up here (* points at skull *) I just can’t seem to get a handle on it. What knots to untangle first.

    I’m trying to build systems to support productive use of my knowledge and abilities. At the moment I’m thinking if I can just off load pieces to GitHub issues or discussions I can kinda fill them out lean on that to get me unstuck.

    Anyway, any advice from anyone who’s dealt with this kind of lack of discipline would be appreciated. I’ve never been on Adderall or Ritalin before. They acquired bad reputations in my childhood for abuse and over-prescription. Maybe I’ve let fear keep me from legitimate treatment and am now paying the price.

    Otherwise just consider this more context setting from me. Thanks for listening.

    Regards,

    -ADG

    Draft: Shutting Down My Patreon

    This is a draft so I don’t have to feel bad if it’s not perfect.

    This isn’t a bad thing!

    I originally started this blog with the idea of creating a place for VB community, but I didn’t have enough content to launch my vision so I settled for a personal blog. Over time it’s expanded from a place to just air my thoughts to a place to think about marketing moments.

    First I was prototyping potential contributions to Microsoft’s Roslyn repo. And then, when I decided to build my own mod it kinda served as a project landing page and a crowdfunding engine and I think that’s been bad for both me AND the VB community.

    It’s been bad for me because everything I write or show comes with tremendous perfection to be substantial, complete, polished, regular, consistent, etc. But I’m not really a marketer. Working at Microsoft I developed a sense for the power and pitfalls of communicating from inside a mega corporation but this isn’t the .NET blog and my posts aren’t instantly seen by 100k people.

    Further, I started my Patreon to help me deal with a personal crisis that has mostly passed. In fact, for months (if not the last year) I have been actively pausing my Patreon to prevent charging folks. You can only pause for 1 month at a time and I was late last month and some folks got charged who I’ll refund when I can. But I’m not executing as a full-time or even part-time content creator and while I do make content I don’t think that’s the arena where my skillset is best used.

    But most importantly, I suck am a flawed human being and while I’m learning to be compassionate with myself about that it’s very important to me that my personal shortcomings–whether they be mental health or discipline or whatever–not be the weak link in reaching as many VB enthusiasts as possible as quickly as possible. Last year I concluded that the absolute most important thing the VB community can do for itself right now is to find each other. Not features, not funding, not campaigning, or any specific action. We need our own rolodex as a resource for ourselves and I don’t want consideration of financial support of me or a project to at all be a factor in spreading the word.

    There’s just a lot of information we don’t know about ourselves and we cannot rely on others to figure it out.

    And on a personal note, it would do me a world of good if I could know there was some forward momentum in the community independent of my performance. To know that if I’m sleeping or dealing with a family crisis or changing my medication that there will still be important things happening would take some pressure off and give me some much needed dopamine.

    I’ve been looking into some community platforms/CRMs we could use. It’s critical that we (the community) own our own data. But it might be something extremely simple in the beginning–a mailing list or something. I’ll post more on that in the future though.

    This weekend I’ll figure out how to move my existing patrons to the free tier. I thank them for every drop of their incredible support these last few years while I’ve been losing my mind. It absolutely made a difference when it mattered most–specifically regarding my healthcare. Now it’s time to take some next steps.

    This does NOT mean shutting down the ModVB project (in fact I’ll be moving the center of gravity for that to GitHub where it always belonged). I’m just decoupling components of the system for better scalability!

    With gratitude and hope,

    -ADG

    Draft: The initial ModVB backlog outline

    Like 2 years ago I said “I’ve got like 11 pages of just bullet points of stuff to do for the ModVB and I tried to publish more polished detailed writeups of each to help folks see the vision but that was going to be like 300 pages so then I trimmed it down to somewhere in between and I still will need to unzip all of these in professional detail on github. But for now I just want this out there and not hanging over me every day. This is copy-pasted section 3 direct out of a Word doc I started with. I’d like to post a follow-up which maybe illustrates each bullet point briefly since not everyone will be self-evident. These are old and minimally processed–I cut out most of the prose and code samples and the section on tooling/IDE-focused changes. Chrome says that gets me down to The point is simply to at a glance answer the question of “What would Anthony even add to VB? Surely it’s like 3 things and they’re all just copies of what C# already has” or to illustrate that I have a rather long-term vision. When I tried to summarize this agenda 2 summers ago I believe I got through Sections 2.1 and 5. I’ve demoed bits of other sections at various times in either prototype or pre-release form. This outline is slightly older than what I promised in that blog post because I refactored some sections and some scenarios/features that folks highlighted in response to those posts have been incorporated but I’m trying to put this out without updating it to be perfect or even great. Chrome says this is about 7 pages printed.

    Cheers,

    -ADG

    1        General Modernization and Evolution I

    1.1     Local Declaration Enhancements

    1.1.1    A new keyword for introducing local variable declarations: `Let`

    1.1.2    Tuple deconstruction syntax

    1.1.3    Bug-fix/Consistency

    1.2     Assignment Enhancement

    1.2.1     Unassignment?

    1.3     `Select Case` Enhancements

    1.3.1    `Select Case` on Type (Smart-Casting)

    1.3.2    `Select Case` on Shape (Pattern Matching)

    1.3.3    `Select Case` on Identity (using `Is`/`IsNot` operators)

    1.3.4    `Select Case` using `Like` operator

    1.3.5    `Select Case` using (new) `In`/`NotIn` operators

    1.4     `For` Enhancements

    1.5     `For Each` Enhancements

    1.5.1    Tuple deconstruction

    1.5.2    Query clauses

    1.5.3    `IAsyncEnumerable` support

    1.6     `Do` Enhancements

    1.7     `With` Enhancements

    1.7.1    Named `With` variable

    1.7.2    `.Me` pseudo-member

    1.8     `Try` Enhancements

    1.8.1    `Await` in `Catch` and `Finally`

    1.8.2    Scope?

    1.8.3    Fault blocks?

    1.8.4    Implicit

    1.8.5    `Retry`/`Resume Next`?

    1.9     `Using` Enhancements

    1.9.1    `Catch` and `Finally` Blocks

    1.9.2    Asynchrony?

    1.9.3    Tuple Decomposition

    1.10     `SyncLock` Enhancements

    1.10.1                       `Catch` and `Finally` Blocks

    2        Smart-Casting and Pattern Matching

    2.1     Smart-Casting

    • Intersection types
    • In LINQ
    • Visitor optimization
    • Nullable
    • `Assert` statement?

    2.2     Pattern Matching โ€“ General

    • In LINQ
    • Implicit and explicit ShapeOf methods

    2.3     Pattern Matching โ€“ Interpolated Strings

    2.4     Pattern Matching โ€“ JSON

    2.5     Pattern Matching โ€“ XML/XAML?

    2.6        Summary/Final Thoughts

    3        JSON

    3.1     JSON Object and Array Literals

    • Builder pattern
    • Target-typing to JSON types
    • Target-typing to non-JSON types

    3.2     JSON Schema Types

    3.3     โ€œFluentโ€ Postfix Parenthetical Casting

    3.4     โ€œFluentโ€ Postfix Null Coalescing

    4        UI, XML, and XAML

    4.1     Overview

    4.2     XAML Literals

    4.3     Embedded Block Expressions

    4.4     Wildcard Lambda Expressions

    4.5     Expression Tree Enhancements

    4.6     Expression Tree Alternatives

    4.7     XML Schema Types

    5        Streamlining and Boilerplate Reduction

    5.1     Top-Level Member Declarations, Statements, and Expressions

    • Implicitly-declared types and members
    • Filename patterns

    5.2     Key Fields & Properties, and Auto-Constructors

    5.3     Async Main

    5.4     Type-Inference Enhancements

    • Static local type inference
    • Nearest common ancestor in dominant type (ternary, collection initializer, type arg inference?)
    • Overloading on constraints?
    • Target-typed conversion operators?

    5.5     Lightweight Doc Comment Syntax

    5.6     Implicit Conversions from String Expressions

    5.7     Implicit Comparison Operators

    5.8     #Ignore Statement

    5.9     Implicitly-Escaped Keywords in Declarations?

    5.10     Bit Indexer

    5.11     String comparison extensions?

    5.12     Explicit Anonymous Delegate Type โ€œNamesโ€

    5.13     Miscellaneous

    • Optional parameter default value inference
    • Type inference for accessor `value` parameters
    • Optional parentheses for `NameOf` expressions
    • `?` Statements
    • `Custom` Event keyword
    • Folder-level Imports and Root Namespaces
    • &= StringBuilder

    6        Declarative Programming and Code Generation

    6.1     Smart-Attributes

    6.2     Semantic Pre-processing

    6.3     Replaceable Declarations

    7        Language Integrated Query Enhancements

    7.1     For Each Integration

    7.2     Range Expressions

    7.3     Tuple-Deconstruction

    7.4     Aggregate Functions in Select

    7.5     New Query Operators

    • Take Last
    • Skip Last
    • Intersect?
    • Except?
    • Union?
    • Zip?

    7.6     Asynchrony and Parallelism(?)

    7.7     Collection Initializer Enhancements

    • Nested Member and Collection Initializers
    • Combine With and From
    • Implicit New

    7.8     Query Collection Initializers

    7.9     `In` and `NotIn` Operators

    • IsIn/IsNotIn?

    7.10     Target-Typing

    8        Dynamic Programming Enhancements

    8.1     LateBound/Dynamic Pseudotype

    8.2     Alternate Dynamic Types?

    8.3     Dynamic Interfaces?

    8.4     Annotated Types

    8.5     Default Methods

    • Late-Bound

    8.6     Late-Bound AddressOf

    8.7     Late-Bound AddHandler

    • Handles?

    8.8     Late-Bound Queries?

    8.9     Late-Bound User-Defined Conversions?

    8.10     Unify Early- and Late-Binders

    8.11     ParamArray with Named, Omitted, ByRef/Out parameters, etc.

    9        Asynchronous Programming Enhancements

    9.1     Lightweight Async Syntax

    • Async Sub As Task
    • Async Function As T
    • Implicit suffix
    • Conversion rules/Async expressions
    • Lookup rules?
    • Elision of intermediate Await operators
    • ?. Propagation

    9.2     Async for non-Task Types (e.g. ValueTask)

    9.3     Agile Async Methods

    9.4     Await Each Blocks

    9.5     Async Iterator Methods

    9.6     Async Events

    • Function Delegate Return Types

    10 Null and Nothing

    10.1     Nullable Reference Types

    10.2     Null literal?

    10.3    Null-safe statements (For Each, Await, AddHandler/RemoveHandler, Assignment)

    10.4     This feature intentionally left blank

    11 General Modernization and Evolution II

    11.1     `Out` Parameters & Arguments

    11.2     Module Enhancements

    • Closed by default
    • Generic
    • Nestable

    11.3     Method-Level `Imports` (and `Option`?) Statements

    11.4     Enum Enhancements

    • Flags?
    • Non-Integral?
    • Conversions from String

    11.5     Strongly-typed Delegate Combine and Remove

    11.6     Generalized Block Expressions

    11.7     Pipeline Operator

    • Precedence, Await

    11.8     New Built-In Types

    • Millisecond date literal?
    • DateTimeOffset literal?
    • DateOnly/TimeOnly literals?
    • TimeSpan literals
    • BigInteger/Half/Int128/UInt128 support (LongLong)
    • Byte array literal support?
    • Base64 literal support?

    11.9     Array pseudo-type

    • IEnumerable(Of T) Shorthand?

    11.10                       Unit of measure syntax?

    12 Inheritance, Interface Implementation, and Extension

    12.1     Implicit Interface Implementations

    12.2     Signature Relaxation with `Implements`

    12.3     Signature and Name Relaxation with `Overrides`

    12.4     `Implements` and `Overrides` on Fields and Properties

    12.5     `Overridable` and `Overrides` Classes?

    12.6     Extension Properties and Events

    • Generic
    • Extension Keyword?

    12.7     Extension Operators

    • Generic

    13 Performance and Interoperability

    13.1     Native Instruction Helpers

    13.2     ByRef Structure Consumption

    13.3     Unmanaged Constraint

    13.4     Delegate and Enum Constraints

    13.5     Explicit Iterator Structure Return Types?

    14 Runtime Library Enhancements

    14.1     Performance

    14.2     `My` Modernization

    14.3     Better Run-time Errors?

    15 Deprecations?

    15.1     On Error?

    15.2     Options Strict?

    15.2.1                       Interlude โ€“ Rules vs Guidelines

    15.3     Other Options?

    16 Versioning

    17 Experimental

    Draft: Trying to decouple some stuff

    This post is a work in progress. Maybe all my posts are works in progress. As a human being I certainly am a work in progress.

    When I was a junior in high school I remember my friends and I had a group “weblog”. Weblogging was new then. It was purely public journaling then. Since then it’s become primarily a marketing exercise. A way to build a personal brand or a corporate brand. Years of thinking of myself as a representative of either a big tech company or a small firm have conditioned me to put incredible pressure on everything I say in public. Any muddled messaging can instantly cause chaos. A flood of angry comments and corrections.

    But my blog isn’t a corporate blog and I want to get back to blogging being a low stakes form of expression. At times I consider that maybe my blog should just be a discord transcript, lol.

    I’m so afraid of questions. Or afraid of arguments? Or just tired of convincing people of things. Of anything actually, not just technical things.

    I’m going to restructure this place. I think maybe the time-based labeling makes me embarrassed when there are such long gaps between posts. Maybe this should be a non-deterministic publication of articles and essays.

    I think there’s a bit of confusion between 4 or 5 brands that I need to separate out for clarity.

    1. Anthony <– that’s me. May want to talk about tech, or recreation, or health, or maybe philosophy and religion.
    2. ModVB a personal project I work on primarily to fulfill a creative imperative that I would like to share with other VB lovers for their consideration.
    3. Other tools I want to share that have no dependency on ModVB.
    4. Educational materials about software development.
    5. VB community info.

    This is mostly important for urls and GitHub orgs. I have a bunch and I need to use them more clearly. Believe it or not the original title for this blog wasn’t… my name. It was something loftier but I didn’t think it was a good idea to launch that domain without more content.

    Whatever my productivity problem is, this year I need to find or create systems that let me be productive without a lot of activation costs. For instance, the ModVB nugget/vsix feed never gets updated because the process of building those packages with the appropriate metadata is manual. I never set up a CI/CD process for it and that should happen.

    It’s not that my thinking on these topics is as erratic as my writing. I just can’t figure out how to get the mature revised experience-based stuff inside my head out. I don’t know why. I’ve always been articulate. So pardon the babbling and stream of consciousness. I’m trying to do things differently this year and see if I get different results.

    I want to post again about power as it applies to tech.

    Regards,

    -ADG

    How I Survived My Fortieth Birthday

    OR “On psychological flexibility”

    In this post I’ll talk about my mental health. VB/Tech talk will be back after.

    Last month I turned 41 but what I want to talk about is last year. You’ve probably noticed that I haven’t blogged in over a year and that’s a health thing not an interest thing.

    I’ve broken down a couple times and am trying to build myself back up. Most forms of communication were very difficult for me for the last year or two. Written, verbal, in person, friends, strangers, authority figures. I lost the ability to make almost all decisions.

    I have a not of those neuro-divergences. I’m working with my healthcare time to identify all of them but it’s a lot more than the “clinical depression” that I’ve focused on for the last 15 years. Currently my top of mind is “anxiety”. Not like the general emotion everyone feels before an interview but clinical anxiety. I think I was diagnosed for anxiety back in 2010 but I thought of it as an accent pillow on my depression. I’ve had breakthroughs with the depression but I’m still not ok and I have to find a new definition of normal that doesn’t presuppose a potentially unobtainable goal.

    In group therapy we learned about a lot of terms that didn’t hit with me right away but they’re coming in handy now. I have a lot of … “perfectionism”, “all-or-nothing thinking”, “catastrophizing”, “cognitive distortion”. In a lot of ways I didn’t recognize until THIS spring I live in constant fear. Not like, of physical violence but innumerable undesirable outcomes and I’ve become so psychologically inflexible that I’m basically paralyzed. My home until recently was basically a trash dump for recycling, old meds, shipping containers, batteries, and really anything that would require decision making. I had to pay a decluttering service to help me throw it all way. It took 2 people plus myself 2 days and it was exhausting. So many decisions. But I’m a little better.

    “Anything worth doing, is worth doing poorly”

    There are so many things I don’t want people to think about me and I’m always trying to manage that so the experiment I want to try is to put out more content in what I consider poor condition. I need to write badly. Rushed. Not revised enough. Be ok with sounding erratic. Impulsive. Sloppy. Maybe I can go back, maybe not. But it’s more important that I say anything than that I say it in the ideal way every time.

    Maybe if I keep being reckless I’ll stop being afraid and maybe even get some good stuff out. I have to trust that my audience trusts that even if I sound … unprepared that there’s a lot of thought beneath the surface.

    I think the most important thing to know is that I am now thinking of myself in my second half of life. Which is a LOT more forward looking than I was at 39. I’m not running out of time; I’m just getting started on the the sequel.

    2024 Recap

    • February – I looked into the abyss and the abyss looked back. I reached what I hope is “rock bottom” in terms of apathy. Apathy for everything in the world but terror for myself. I was trying to write to a colleague about my goals and I stared by contrast listing things that I didn’t care about. That list got very very long. I (thought) that I didn’t care about anything really and that’s not a place I wanted to be 8 months before my 40th birthday.
    • I abandoned most of my life. I stopped checking email. My phone was on do not disturb from February to October. I turned off all notifications. I left people and projects hanging without any communication. I was just trying to survive. I owe folks more than just apologies. I gotta do a whole 12-Step amends-type thing.
    • June – Reconnecting with my creative goals on the language design side with ModVB wasn’t exactly a “cry for help” but it was desperate. I told my family and friends that I would be completely self-isolating for 30 days and to not try to contact me until July unless someone was dead or dying. In hindsight I should not have tempted fate with that kind of specificity. No one died but there was a whole thing with mutual acquaintance and the FBI on one side and an urgent double organ transplant emergency within like 2 weeks of my attempted isolation. They’re fine now. I had another terrifying setback in the beginning of July. I couldn’t actualize anything–it seemed.
    • August–I had a bit of a breakthrough about things I’d been carrying since early childhood that were killing me and opened myself up to alternate paths of success.
    • September–the first month I’d had in years where the sky didn’t feel heavy when I looked at it. It was just blue and pretty.
    • October–I wanted to try to do something grand to look forward to my birthday rather than dread it so I’d aspired to go to Miami or something but then like 3 hurricanes hit Florida and that didn’t happen so I just drove around Detroit after midnight blasting the Robocop theme song.

    November 2024-February 2025 was cold, mistakes were made, I tried to get back on my feet and got knocked down hard again but fortunately I was able to get new healthcare coverage and begin the long process of healing. My physical health and mental health were tag-teaming me and I was pretty sure I was just destined to decay for the rest of my life but then I went to physical therapy and learned that the pain I’d been living with wasn’t normal aging or one of a host of inevitable inherited ailments I get from either side of my family.

    For the first time in like a decade I have:

    • A primary care doctor that’s a doctor and at a medical center that appears staffed and equipped above the minimum.
    • A psychiatrist helping me manage my meds. I’m now on 3 different meds and I expect there will be more and I’m optimistic.
    • A therapist I talk to every week thanks to BetterHelp.com
    • A dentist office within a 20 minute drive.

    Then my healthcare premium quadrupled in July but me and the feds are working it out now. I’m not okay. I’m not well. I’m not anywhere near “ready”. I’m still under constant mental assault but I’m learning to recognize when I get stuck and building muscle memory on how to dislodge myself.

    A Beautiful Mind

    2001’s Academy Award winning “A Beautiful Mind” has been morbidly inspirational in my life now in two ways. It’s biographical film about a Nobel Prize winning economist John Forbes Nash Jr. (played by Russel Crowe). It’s supposed to be an inspiration film about (tortured) genius, in the same vein as 2023’s “Oppenheimer” (about J. Robert Oppenheimer) or 2014’s “The Imitation Game” (about Alan Turing) and it does succeed in that but the brilliant economic work of Dr. Nash wasn’t what impacted me the most.

    In 2015 Dr. Nash and his wife died in a car accident. They were riding in a taxi that had a collision and neither of them were wearing seatbelts and were killed as a result. For my whole life I’d been in the (in hindsight weird) habit of not wearing seatbelts in the backs of taxis/Ubers/Lyfts. Not sure why, it was just a kind of common “convention” when I grew up like being back there was magically safer than in a normal vehicle. And then a man lauded as a genius who had this whole big movie made about his life and his work died from not wearing a seatbelt. It was sobering to realize that such a bright light could be extinguished from the world in such a unremarkable way and since then I buckle up every time whether I’m a passenger in a cab or a rideshare or I think I’ll be moving slowly through downtown streets. I share that not to use his death as a rhetorical prop but to beg any readers who might also be in the habit of not wearing seatbelts in back seats or at low speeds or in cabs or limos to drop that habit immediately. Don’t risk that being the end of your story, PLEASE!

    That PSA aside there’s now another influence from that film that I had in the Spring of 2025. There’s this critical moment toward the end of the film with Dr. Nash finally accepts that he’s mentally unwell and has been imagining countless interactions with delusions throughout his life. The hallucinations had been so real and what finally convinces him that he has schizophrenia in the 11th hour as his marriage is falling apart and his wife is about to leave him for good is he notices something that feels real but *can’t be true*. “She never gets old!” he tells his wife. The little girl he’d seen in these episodes for decades can’t be real because she never gets old. His disease overplayed its hand so to speak. That one little detail changed his life.

    Earlier this year I was trying to write a small sample of a VB program or potential extension of string interpolation for someone online to illustrate a point. And I sat at my desk for like an hour or two frozen and unable to implement it. My mind was racing through all of these considerations but I was paralyzed at my keyboard. And the lies and self-deprecation start to pop up. “Am I a fraud?” “Have a really just been skating by all this time?” “Did I just fool everybody until now and I can’t even implement this?”

    And in the midst of that death spiral of doubt I saw the never aging little girl in my delusions. Not only had I implemented this proof of concept sample at least once or twice already in the past (I could remember doing it) but I also implemented the much more robust implementation of actual string interpolation in VB that shipped 10 years ago and has been used by many developers worldwide since (including myself). It simply could not be possible that I was not capable of implementing a thing I’d already implemented like 3 times and shipped once in a wildly available commercial product made by one of the largest tech companies in the history of the world. Those thoughts I was having… couldn’t be real. And so that inability or resistance or obstruction to progress wasn’t from a deficiency of knowledge or skill or qualification but something else. I had definitely been able to do something and wasn’t doing it now–solve for x. And that’s when I recognized it as more than hyperventilating panic attacks–“Oh! THIS IS ANXIETY!!”. You can get to 40 with a shocking lack of self-awareness (I had actually told one of my teammates at my previous consulting company that I was experiencing “cognitive decline”).

    I’d been so focused for the last 15 years on the idea that I couldn’t function because I was sad that I hadn’t properly considered how much I was sad because I couldn’t function for a reason other than sadness.

    That was not a cure

    But it was an invaluable clue and now I’m using this shift in perspective to re-evaluate many blockages in my life and as I am able to try to push myself through. It’s like everything I do throws up big red emergency lights and I have to manually hit “Override” and it’s very tiring but slowly I’m making progress.

    Learning from my past

    Ok, one last anecdote. In September of 2018 I’d just flew back to Chicago from my first visit to Seattle post-Microsoft. I’d met with my friends and old teammates and ate old food favorites and I was ready to try to turn my attention back to tech after months of being in transition. I came home, put my luggage down, hit the power button on my PC and hopped in the shower. I was so inspired I didn’t want to have to wait for it to boot after the shower. I wanted it ready for me when I got out.

    I get out of what I assume was a wonderful hot shower and get dressed and sit at my desk and try to wake the computer and the screen is lit but not showing anything. I didn’t even stop to put on socks so my feet were still a little wet on the wood laminate flooring under my desk, I was making a little puddle–or so I thought. Eventually I found out that the puddle wasn’t from my wet feet but from my liquid cooling system which had ruptured while I was in the shower. One of the tubes had popped off a processor and saturated the video cards and power supply.

    My baby was ruined!

    Who knows how bad it was. Seemingly everything had been hit by neon green coolant that was supposedly non-conductive but was riddled with impurities from years of my neglecting to change the coolant. I’m lucky I didn’t electrocute myself stepping in the puddle while the box still “on”. This was the worst possible time. I wasn’t yet working again, I was budgeting my savings, that computer was so expensive to build–I couldn’t possibly replace it now. I don’t think they even make that dual-socket motherboard anymore.

    And it took a while but slowly I was able to gather myself and take the whole thing apart and assess. To see what could be salvaged. To see if parts could be cleaned. And even though I had built this entire PC from parts myself that was waaaaay back in 2011 and I’d forgotten everything about PC hardware since then. What did all the jumpers do? Where did the cables go? What did you call any of these “things”. Computers had changed so much since when I first learned back in 1996 with IDE cables and Soundblaster sound cards. I’d forgotten about SATA II and all that stuff I had to learn back in November 2011 just to put together a (then) modern PC.

    It took a couple weeks but I did ultimately disassemble, clean, and put humpty dumpty back together again (only having to replace the video cards, power supply, and cooling system). I downloaded the manual PDF and re-read the stuff and switched to a fan-based air-cooled system and didn’t break the bank while I was on sabbatical.

    Somehow I was able to do again that thing I had already done that for some reason I feared I wasn’t able to do anymore cause… I don’t know. And I did it the same way I did it the first time–one small step at a time.

    And aside from one mysterious 2-day hiccup earlier this year that stymied my premature efforts at returning to work, that same machine is still running fine. 14 straight years. I’m going for 15 before I build a new one next year (because I am still psychologically rigid about multiples of 5 but I can only tackle so much of my own “crazy” at one time, give me this).

    One step at a time

    All that to say, I have to remember that very tall mountains that seem impossible can be climbed one step at a time. And that’s how I feel and have felt for the last 6 months or so. Like I’m at the early hours of a very long climb but that eventually I’ll get to the top. And folks will just have to bear with me if I’m a little (or a lot) slower at climbing than I used to be. I finally have a firm belief that I can get there, even if it’s going to take a while. I needed to share that so folks will understand my fumbling forward in the near future and my long and unexpected silence until now. If I owe anyone an apology for my behavior (and I know I do to many), know that it’s coming. If my writing seems a bit out of character or unpolished for a while, try not to hold it against me.

    “I’m all tied up in inside. And if I don’t untie myself inside… the emotional knots… I’m gonna explode!”

    I’m learning to give myself the grace I’ve learned to give others and to embrace a greater degree of self-acceptance of my inescapable human-ness. I’ve let myself become the incarnation of not just “Making The Perfect the Enemy of The Good” but making it the enemy of the “At All” and I really want to try something different now.

    Faithfully,

    -Anthony D. Green

    Streamlining and Boilerplate Reduction

    The theme of streamlining code and reducing boilerplate extends through the entire ModVB agenda. In the previous section I talked about how improvements in type inference can do this and in future sections Iโ€™ll show more examples that also align strongly with broader themes. This section highlights a few investments that donโ€™t align with other themes but rather are purely planned to delete characters from code while still preserving VBs intuitive and self-descriptive nature.

    Immersive Files (incl. top-level code)

    The feature in this section with the farthest reaching impact potential is what I have recently dubbed Immersive Files. This includes โ€œtop-level codeโ€ which appears in many other languages, but as is often the case, ModVB extends this concept much further, so donโ€™t skip the details on this one.

    Of course, it all began with top-level code and trying to get back to the simplicity I started with in QBasicโ€”that a program could be one or more executable lines of code without all the boilerplate of a `Main` method or a module to contain it

    FirstProgram.vb

    Console.WriteLine("Hello, World!")

    SecondProgram.vb

    ' This is an entire program.
    Imports System.Console
    
    Function Prompt(message As String) As String
        WriteLine(message)
        Return ReadLine()
    End Function
    
    Let name = Prompt("What is your name?")
    
    WriteLine($"Hello, {name}โ€‹.")

    These are great demos for educational purposes and there was much discussion of these โ€œsimple programsโ€ and wanting to bring scripting and interactive experiences to VB and C# like what F# has had forever. And creating as minimal an on-ramp for new developers is of course critically important but the immersive files feature is designed such that they can have value for any developer or project regardless of skill level.

    If youโ€™re an experienced VB developer, when you read the above simple programs you probably came to some quick conclusions about what was going on like that the executable statements run in a Sub Main or that the function Prompt was being declared in an implicit module, which are certainly true of how I imagined those files would be compiled but the great thing about immersive files is that they can be useful for an open-ended number of immersive experiences with completely different compilation (maybe) and execution models. More explicitly,

    • The statements will end up in a method but it doesnโ€™t have to be a Main method nor does it have to be Shared and it could be an Async or even Iterator method.
    • Any non-type declarations will end up bundled in a type, whose name is inferred from the filename, but that type doesnโ€™t have to be a module or even a class. This type can be Partial.

    Instead of me as a language designer making these decisions, I have designed the feature so that these decisions can be made by VB developers and communicated to the compiler through attribution from a base class that a file may derive from explicitly through an Inherits statement or implicitly through configuration.

    Let’s look at some examples…


    Starfield.vb

    buffer.Clear(Color.Black)
    
    ' Draw 10 yellow 5x5 squares randomly across a 600x400 screen.
    For i = 1 To 10
        buffer.FillRectangle(Brushes.Yellow, Rnd() * 600, Rnd() * 400, 5, 5)
    Next

    In this video I recorded, I have defined a base class called GameLoop that is attributed so that when an immersive file inherits it, any of its top-level statements go into an Execute method thatโ€™s run on every frame and can draw to a graphics buffer. This allows the developer to be โ€œimmersedโ€ in the animation loop of this starfield.


    Index.vbxhtml

    <?xml version="1.0" encoding="UTF-8"?>
    <html>
      <head>
        <title>Welcome to my site!</title>
      </head>
      <body>
        <h1><{ ViewData!Message }></h1>
        <h2>This is a header.</h2>
        <p>Today is <{ Date.Today.ToString("MMM d, yyyy") }>.</p>
        
        <a href="/">Home</a>
        <a href="/home/about">About</a>
        <a href="/home/contact">Contact</a>
      </body>
    </html>

    And in this video, I use a base class that puts top-level statements inside a Render method that sends HTML from an ASP.NET Core MVC controller to a browser. This example takes advantage of a top-level expression thatโ€™s a single large XML literal which is the return value of the top-level method that the base class then calls when rendering the page to the response stream. This is an extreme though common example where the primary logic of an entire type, in this case a View class, is entirely or almost entirely restricted to a single functionโ€”computing the result markup. This feature allows the developer to be โ€œimmersedโ€ in that all-consuming purpose.


    I will use this same technique in the future to enable VB to easily utilize several of .NET’s many popular XAML-based UI frameworks. But in these case the top-level expression will be a XAML literal (coming in Section 4) forming the body of the implicit InitializeComponent of a Window, UserControl, or ContentPage from these frameworks. This approach will bring cross-platform and mobile UI development to VB, without requiring any support from UI framework authors.


    Supporting top-level LINQ query expressions (which could be compiled or simply interpreted in the IDE) will allow the creation of immersive interactive live data analysis experiences like what you get in SQL Server Management Studio, LINQPad, or other tools for data science (expect a demo one day).


    Or, a VB.NET Script file for automation and management of Windows or other operating systemsโ€ฆ


    But even beyond all that fancy stuff, what if Iโ€™m just tired of Class statements?

    Hear me out.

    In VB6, .frm files were forms I didnโ€™t need an explicit Class statement. The name of the form always matched the name of the file. And in VB.NET we lost that โ€œwhole fileโ€ immersive experience in exchange for the ability to put multiple types in a single file, which is great, but what if we could have it all?

    Form1.vb

    Sub Button1_Click() Handles Button1.Click
        MsgBox("Hi")
    End Sub
    
    Sub Button2_Click() Handles Button2.Click
        MsgBox("Bye")
    End Sub

    Combined with VBโ€™s already great experiences of project root namespaces, and project-level imports, that file is looking pretty clean. And by making it immersive I save 4 spaces of indentation on every line! One step closer to my beloved single-procedure view.


    Which brings us to a conversation about conventions and configuration. I love VBโ€™s project-level imports and root namespaces, but Iโ€™ve long desired the ability to configure them for a group of files in my project with more granularity, e.g. the ability to assign a root (implicit) namespace to all files in a particular folder (usually representing an architectural layer), and/or to add one or more Imports to all source files in a particular layer/folder. And I can definitely imagine wanting to give a particular immersive configuration to groups of source files based on folder, extension, or just naming convention.

    So, ModVB will add the ability to configure these settings and apply them based on a pattern which could describe folders, conventions, and extensions (maybe regex, maybe not):

    • Extensions for type kinds? .vbclass, .vbstructure, .vbinterface, .vbmodule
    • Extensions for base types? .vbxhtml, .vbxaml, .vbsql, .vbform
    • Or conventions for base types *Controller.vb, *Form.vb, *Service.vb

    Beyond top-level for learning how to code (which, again, is very important), Iโ€™d use immersive files extensively on every project and Iโ€™ve been coding in VB.NET for 20 yearsโ€”Iโ€™ve proven I know how to declare a class by now, Iโ€™ve earn this!

    Key Fields & Properties, and Auto-Constructors

    Another thing Iโ€™m a little tired of typing after 20 years is very simple constructors for setting fields and properties. I love parameterized constructors! They really were one of the things I was very excited for in VB.NET coming from VB6โ€”the ability to ensure that an objectโ€™s state was initialized on creation really appealed to me and it still does. Thatโ€™s why I continue to initialize my objects through constructors, even for non-public types, instead of just making fields public or other violations of encapsulation. That said, it’s kind of tedious after thousands of times doing it.

    To that end, ModVB will allow you to apply a modifier to any class or structure instance fields or properties of your choosing, designating them as Key fields/properties. If no other constructor is provided by the developer, the compiler will generate one with parameters matching the keys and initialize them.

    Class AppointmentBookingService
        
        Private Key CalendarService As ICalendarService,
                    PaymentService As IPaymentService,
                    EmailService As IEmailService
    
        ' Look at this code I'm never going to have to type again!
        ''Public Sub New(calendarService As ICalendarService,
        ''               paymentService As IPaymentService,
        ''               emailService As IEmailService)
        ''               
        ''    Me.CalendarService = calendarService
        ''    Me.PaymentService = paymentService
        ''    Me.EmailService = emailService
        ''End Sub
    
    End Class

    And yes, naming conventions are important to me as they are to many .NET developers, so the compiler will camelCase those parameter names appropriately, though this may be configured. This design has great synergy with the Immersive Files feature because it doesnโ€™t require a class statement. So much boilerplate gone!

    Some of you may have noticed that the Key keyword is actually borrowed from VB anonymous types. In vanilla VB, anonymous type properties marked Key contribute to that anonymous typeโ€™s implementation of Equals and GetHashCode and are implicitly ReadOnly. Key fields/properties in regular classes or structures retain this capability, but, to ensure the broadest applicability of the feature, they are not required. Rather, if a class or structure implements IEquatable(Of T), a default override of Equals and GetHashCode will be provided by the compiler using these fields/properties, if the developer does not provide explicit overrides of these members.

    Structure Money
        Implements IEquatable(Of Money)
        
        Public Key ReadOnly Property Value As Decimal
        
        Public Key ReadOnly Property Currency As Currency
        
        ' Sub New(value, currency), Equals, and GetHashCode provided by compiler.
        
    End Structure

    Explicitly marking the fields/properties of a type that are โ€ฆ key to its identity and function also allows the IDE to be more guided in suggestions and code generation for other members, such as operators. This leaves open a question of which members should be generated by the compiler by default and which should be suggested by the IDE if the developer begins defining them. For example:

    • A default implementation of the = and <> operators
    • A default implementation of the Deconstruct method for pattern matching

    Iโ€™m leaning toward the compiler generating the = and <> operators and Iโ€™m leaning toward not making an object implicitly deconstructable because that could break encapsulation. One compromise Iโ€™m considering though is a simple rule: Unlike the generated constructor, which takes all keys, the Deconstruct method(s) will only expose those keys which are Public, with the rationale that if a Key field/property is already part of a typeโ€™s public API, itโ€™s not a violation of encapsulation to also make that value accessible via pattern matching.

    Pattern matching isnโ€™t in VB yet so I donโ€™t know how popular these methods are going to be, so ultimately the decision on whether to adopt this convention-based approach will heavily depend on usage patterns and feedback, so sound off in the comments!

    Wildcard Lambda Expressions

    Idiomatic .NET development has changed massively since v1.0. One of the most impactful shifts came after v3.5 in 2008 with the introduction of lambda expressions. Beyond their necessity to support LINQ, lambda expressions have become fundamental to how all manner of APIs work. One pattern in particular thatโ€™s been extremely popular is passing a lambda expression as a kind of strongly-typed โ€œproperty referenceโ€. Indeed, the origin of this feature was my desire to create a syntactic distinction for values in XAML literals that described data-binding paths:

    <TextBox Text={*.PrimaryContact.FirstName}/>

    Whether that potential use case ever materializes, the feature is generally useful for the many .NET APIs which take Expression(Of Func(Of T1, T2)) lambdas as a declarative property mapping. I felt the need for this feature especially on a recent-ish project using Entity Framework Core, where even C#โ€™s _ => _.Foo syntax very quickly grew tiresome (so many <SHIFT> key presses, itโ€™s like playing piano)

    ' Vanilla VB
    modelBuilder.Entity(Of Blog) _
                .Property(Function(b) b.Url) _
                .IsRequired()
                
    modelBuilder.Entity(Of Blog) _
                .HasMany(Function(e) e.Posts) _
                .WithOne(Function(e) e.Blog) _
                .HasForeignKey(Function(e) e.ContainingBlogId) _
                .IsRequired()
                
    ' ModVB
    modelBuilder.Entity(Of Blog) _
                .Property(*.Url) _
                .IsRequired()
                
    modelBuilder.Entity(Of Blog) _
                .HasMany(*.Posts) _
                .WithOne(*.Blog) _
                .HasForeignKey(*.ContainingBlogId) _
                .IsRequired()

    I feel the wildcard syntax really captures the spirit of how much I donโ€™t care about the variable.

    Of course, these expressions arenโ€™t limited to just property access or just one level so expressions like this will also work

    Let users = context.Users.Include(*.Profile.Avatar).ToList()

    This raises the question about nesting and what the second `*` below should mean?

    Let blogs = context.Blogs.Include(*.Posts.Select(*.Comments))

    Maybe the syntax should it be **.Comments, or maybe *.Posts..Comments?

    Recently I noticed that F# has already added this kind of feature so maybe Iโ€™ll peek at what they do in this case. Sound off in the comments!

    SPOILER: Fortunately, in the LINQ section youโ€™ll discover that Include is being added as a new query operator so maybe this was just a bad example.

    Markdown Documentation Comment Syntax

    While studying the topic of user interaction design (one of my favorite topics), I read somewhere (that I can no longer find) either the statement โ€œThat which is easy will be done often (even if it shouldnโ€™t)โ€ or the corollary โ€œThat which is hard will not be done often (even if it should)โ€. The second one is most important to this section. If you want people to โ€œdo the right thingโ€ you have to make โ€œthe right thingโ€ easy, and certainly not a pain.

    I believe in documenting my declarations and I know many other developers who also believe thisโ€ฆ in their hearts. But in our fingers, itโ€™s just a little too much to do for non-public APIs. I want to encourage both new programmers and veterans to document their declarations. Itโ€™s good for maintainers, itโ€™s good for tooling because documentation is surfaced in IntelliSense. To that end, I intend to allow a dramatically simplified syntax for doc comments. After playing around with a few minimalist designs Iโ€™ve decided that rather than demand anyone remember yet another format and despite my general preference for WYSIWYG editing, it would be best to accept a markdown-based format.

    '''
    ''' Returns a function from a description of a line in slope-intercept form.
    '''
    ''' # Parameters
    ''' - @m: The slope of the line. Must not be @Double.PositiveInfinity,
    '''       @Double.NegativeInfinity, or @Double.NaN.
    ''' - @b: The y-intercept of the line. Must not be @Double.PositiveInfinity,
    '''       @Double.NegativeInfinity, or @Double.NaN.
    '''
    ''' # Returns
    ''' An instance of `Func(Of Double, Double)` delegate type which
    ''' returns the y-coordinate given an x-coordinate.
    '''
    ''' # Exceptions
    ''' - @ArgumentOutOfRangeException: Either @m or @b is
    '''     @Double.PositiveInfinity, @Double.NegativeInfinity, or @Double.NaN.
    '''
    ''' # Remarks
    ''' A line can be of one of 3 forms:
    ''' 1. Horizontal,
    ''' 2. Vertical, or
    ''' 3. Diagonal.
    '''
    ''' This API only supports forms 1 and 3.
    '''
    ''' # Examples
    ''' ## Normal usage
    ''' ``` vb.net
    ''' Let y = F(3 / 2, -5)
    ''' Graph(y, 0 To 100)
    ''' ```
    '''
    ''' ## Horizontal line
    ''' ``` vb.net
    ''' Let y = F(0, -5)
    ''' Graph(y, 0 To 100)
    ''' ```
    Function F(m As Double, b As Double) As Func(Of Double, Double)
        If Double.IsInfinity(m) OrElse Double.IsNaN(m) Then
            Throw New ArgumentOutOfRangeException(NameOf m)
        ElseIf Double.IsInfinity(b) OrElse Double.IsNaN(b) Then
            Throw New ArgumentOutOfRangeException(NameOf b)
        End If
        
        Return Function(x) (m * x) + b
    End Function
    

    The three '‘s are still required (instead of one) to keep devs from accidentally publishing private comments with their public docs.

    This is about as minimal as itโ€™s going to get which is great for getting over the laziness threshold to type it but also great for reading online, e.g. viewing PRs on GitHub. No worries, the output format will still be the same XML and XML in the comments will still be supported. In fact, youโ€™ll be able to mix them. There are some great scenarios out there for the open-ended XML syntax such as the <response code=โ€###โ€> tag used by ASP.NET for Swagger API documentation generation and I donโ€™t want to break any of them.

    #Ignore Warning Directive

    For a syntax that supposedly represents ignoring a warning, it sure takes up a lot of spaceโ€ฆ

    ' Before.
    #Disable Warning BC42356 ' No `Awaits`; don't care.
    Async Function SayHiAsync() As Task
    #Enable Warning BC42356
    
        Console.WriteLine("Hello, World!")
    
    End Function
        
    ' After.
    #Ignore Warning BC42356 ' No `Awaits`; don't care.
    Async Function SayHiAsync() As Task
    
        Console.WriteLine("Hello, World!")
    
    End Function
    

    This new directive will ignore the specified warning for the next statement only. Not the next block, the next statement. Itโ€™s actually pretty rare that I interact with warnings in VB or would choose to ignore them, but with the rise of code analyzers this is sure to become more common, so Iโ€™d like to be able to ignore them in a way that I can mostlyโ€ฆ ignore.

    New Implicit Line Continuations

    VBโ€™s implicit line continuation has been an incredibly impactful addition to the language and an elegant solution to allowing more flexible formatting of code without requiring any explicit line termination. I havenโ€™t heard many requests for new places to allow them over the years but hereโ€™s what I have heard so far

    • Before Then
    If someComplexConditionExpression1 AndAlso
       (someComplexConditionExpression2 OrElse
        someComplexConditionExpression3)
    Then
        ' Do stuff.
    
    End If
    • Before Handles
    Sub Button1_Click(sender As Object, e As MouseMoveEventArgs)
        Handles Button1.MouseMove
    
        ' Do stuff.
    End If
    • Before Implements
    Function GetEnumerator() As IEnumerator(Of T)
        Implements IEnumerable(Of T).GetEnumerator
        
        ' Get thing.
    End Function
    • Between ) and As in Function statements
    Function BuyCar(
               make As String,
               model As String,
               year As Integer,
               Optional trim As String
             )
             As Car

    Of course, any changes in implicit line continuation have to be very carefully considered with regard to the IDE experienceโ€”the IDE has to be pretty smart about whether the user is done typing or not and I canโ€™t regress any muscle memory in common scenarios. With that said, also Iโ€™m considering

    • Maybe allowing comments between query clauses?
    ' Getting customers
    From customer In db.Customers
    ' in Illinois
    Where customer.Address.State = "IL"
    ' with orders
    Join order In db.Orders On order.CustomerId = customer.Id
    ' made last year
    Where order.OrderDate.Year = 2023
    ' from most expensive to least
    Order By order.Total Descending
    ' getting the top 10
    Take 10

    If this would help you, please send me a better example (instead of my contrived one where the comments are completely redundant)!

    • VERY UNLIKELY: Before commas (,) and various binary operators like AndAlso and OrElse?

    I know it’s pretty common in SQL and other languages to put the conjunction on the next line so you can delete or comment out trailing expressions in a list easily but that could be pretty invasive to the typing experience in many scenarios. It would take a lot of analysis to understand the impact of this request, which I’ll happily do at some point in the future. As it stands this is very unlikely.


    Please let me know if you have more scenarios. I know I want to do something about leading .‘s and calling fluent-APIs but right now Iโ€™m leaning more toward an IDE-focused solution for a few reasons, so Iโ€™ll discuss that design in a future IDE-focused post.

    Bug Fixes & Other Minutiae

    Now the lightning-round!

    • Async Main methods will be legal
    Async Function Main(args As String()) As Task(Of Integer)
    
        Await Console.Out.WriteLineAsync("This is an example.")
    
        Return 0
    End Sub

    Thereโ€™s no principled reason it wasnโ€™t before, just never got around to it.

    • Optional parameter default value inference

    Reusing an earlier example

    Function BuyCar(
               make As String,
               model As String,
               year As Integer,
               Optional trim As String ' <-- No default here.
             )
             As Car

    Typing = Null and = Nothing over and over again doesnโ€™t actually help me. Itโ€™s just noise and it obscures the important cases where the default value isnโ€™t just the parameter typeโ€™s default value.

    • Optional keyword โ€ฆ optional after first optional parameter?
    Function MakeDateTime(
               year As Integer,
               month As Integer,
               day As Integer,
               Optional
                 hour As Integer,
                 minute As Integer,
                 second As Integer,
                 millisecond As Integer,
                 microsecond As Integer
             )
             As Date

    Because in VB optional parameters must be at the end of a parameter list, the keyword neednโ€™t be repeated for every subsequent parameter after the first.

    • Type inference for accessor value parameters
    ' Before
    Property Name As String
        Get
    
        End Get
        Set(value As String)
    
        End Set
    End Property
    
    ' We shall never know the delegate type of this event.
    Custom Event NameChanged As EventHandler(Of EventArgs)
        AddHandler(value As EventHandler(Of EventArgs))
    
        End AddHandler
        RemoveHandler(value As EventHandler(Of EventArgs))
    
        End RemoveHandler
        RaiseEvent(sender As Object, e As EventArgs)
    
        End RaiseEvent
    End Event
    
    ' After
    Property Name As String
        Get
    
        End Get
        Set(value)
    
        End Set
    End Property
    
    Event NameChanged As EventHandler(Of EventArgs)
        AddHandler(value)
    
        End AddHandler
        RemoveHandler(value)
    
        End RemoveHandler
        RaiseEvent(sender, e)
    
        End RaiseEvent
    End Event

    I donโ€™t know that these type specifiers help folks, but they do make changing a property or event type more error prone.

    A little-known secret, the value parameter list in Set accessors has always been optional. If you delete it (including the parentheses) the compiler declares an implicit parameter named Value but the IDE always generates it for clarity. AddHandler and RemoveHandler should also do this to be consistent though personally I prefer having an explicit parameter.

    • Custom keyword in for custom Event declarations not required?

    Also did you notice that the second event declaration in the previous example didnโ€™t have a Custom modifier?

    When Custom events were introduced to VB I suspect that the Custom keyword made parsing expanded event declarations easier, but when auto-implemented properties were introduced it became apparent that this keyword wasnโ€™t necessary to distinguish the one-line declaration from the multi-line one. While I do find the modifier useful for establishing the vocabulary of how we VB devs refer to such events, I feel compelled to ensure properties and events are consistently flexible here.

    • Optional parentheses for NameOf expressions
    Public Shared ReadOnly HeaderTextProperty As DependencyProperty =
                             DependencyProperty.Register(
                               NameOf HeaderText, ' <-- No parentheses.
                               GetType(String),
                               GetType(MyControl)
                             )
                                                
    Public Property HeaderText As String
        Get
            ...
    

    Because of the way VB NameOf is designed (itโ€™s a reserved keyword) I donโ€™t think these parentheses were ever required. Theyโ€™re an artifact of thinking of it like GetType() when I should have been thinking of it like AddressOf. What do you thinkโ€”parens or no parens?

    • Open generic types/methods(?) allowed in NameOf expressions
    ' Vanilla VB
    ' This is legal:
    Shared ReadOnly DictionaryType As Type = GetType(Dictionary(Of,))
    
    ' But you're required to write this:
    Shared ReadOnly DictionaryName As String = NameOf(Dictionary(Of Object, Object))
    
    ' ModVB
    ' Now this is also legal:
    Shared ReadOnly DictionaryName As String = NameOf Dictionary(Of,)

    VB allows โ€œopen generic typesโ€ in GetType operands but not NameOf operands; in NameOf we require developers to supply meaningless dummy type arguments. Iโ€™m pretty sure this inconsistency isnโ€™t necessary in VB and is just a historical artifact.

    Wrap up

    Remember, Folks: The more you say it, the less special it becomesโ€ฆ

    I love VBโ€™s declarative and descriptive syntax and will always prefer it over more terse languages with heavy uses of symbols with special meanings. That said, Iโ€™ve been doing this VB.NET thing for a couple decades now and there are a few common patterns of coding that Iโ€™d like to clean up just a little bit so I can focus more on what matters. Hopefully, you agree with these changes and if not or if Iโ€™ve overlooked something that matters to you, donโ€™t hesitate to sound off in the comments!

    Thanks for reading! Please share and discuss everywhere!

    โ† Type-Inference Enhancements | Overview โ†‘ | Next Section Coming Soon! โ†’