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)
- The Basics
- Notebooks
- QBasic-style Game Loop
- Web Page
- Web Controls/Components
- Web API
- XAML/Xamarin.Forms
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
)andAsinFunctionstatements
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
Mainmethods 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
Optionalkeyword … 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
valueparameters
' 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
Customkeyword in for customEventdeclarations not required
See above.
- Optional parentheses for
NameOfexpressions?
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
NameOfexpressions
' 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
ByRefstructure story.Optionalin delegates.System.Rangeinterop.- C# “Extension everything” story/interop.
- Interpolated string handlers.
DelegateandEnumgeneric constraints.Unmanaged(?) Constraint.- “extern alias” scenarios.
- Recognize overloaded
^(exponentiation) operator from F#. - Recognize overloaded
^(exponentiation) operator fromPow.
(e.g.BigInteger). - Recognize overloaded
=/<>operators fromIEquatable. - Recognize overloaded
- Recognize overloaded
++and--forForloops? - Optimized
Select Case TypeOffor classes using Visitor Pattern. - LINQ optimization using value tuples?
- Optimized imperative generation of
For Each ... Where ....
(When appropriate, i.e. in-memory, notIQueryable, well-known methods.
used on in-memory collections. - Generalize
For Each->Forcode-gen forIList(Of T).
(Where appropriate) - Generalize delegate
Invokesyntax.
(May just beDefaultmethod 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
Myhelpers (e.g.Asyncmethods–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 Caseblocks.
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