McBasic™Get McBasic™ ↗
Build 881
Hands-on tutorial · Language

Build a tested language workbench

Write original, executable examples of functions, enums, loops, arrays, collections, dictionaries, classes and structured errors.

1 Form / control designs4 complete BASIC filesOriginal web lesson

Start with Greeting as a pure function, then add each calculation before wiring the Run button. Main.bas owns the public enum; callers in a different module qualify it as Main.TaskState. Counter is a class with independent per-instance state. FillReadings sizes a dynamic array through its ByRef parameter; declaring the caller with fixed bounds now fails Check. TimerSeconds exercises CDbl(Timer()). Use the array reference’s five-step repair exercise to observe MCB2102 and restore the valid declaration. The test module is declared in the manifest and contains nine test procedures; Check alone does not execute those assertions.

Step 1Prepare a safe practice project#

Extract the archive into a new writable folder, separate from your own application. Open LanguageWorkbench.mcproj in McBasic™. Keep every declared source, layout, resource and migration file together.

To reconstruct it yourself, create a DesktopApp project and add the files declared below. In the complete project, follow these steps as a guided rebuild: replace each named source file once and Check after all its procedures are present.

Exact project configuration
LanguageWorkbench.mcproj
{
  "schemaVersion": 1,
  "projectId": "mcbasic-web-language-workbench",
  "name": "LanguageWorkbench",
  "kind": "DesktopApp",
  "language": {
    "dialect": "McBasic1",
    "requireExplicit": true,
    "compare": "Binary",
    "defaultArrayBase": 0
  },
  "application": {
    "bundleIdentifier": "org.mcbasic.web.languageworkbench",
    "version": "1.0.0",
    "buildNumber": "1",
    "startupFormId": "main",
    "shutdownMode": "OnStartupFormClosed"
  },
  "forms": [
    {
      "id": "main",
      "typeName": "frmMain",
      "layout": "frmMain.form.json",
      "code": "frmMain.bas"
    }
  ],
  "modules": [
    {
      "name": "Main",
      "path": "Main.bas"
    }
  ],
  "classes": [
    {
      "name": "Counter",
      "path": "Counter.cls"
    }
  ],
  "tests": [
    {
      "name": "LanguageTests",
      "path": "LanguageTests.bas",
      "kind": "unit"
    }
  ],
  "targets": [
    "osx-arm64"
  ]
}

Step 2Build the Form and set its properties#

frmMain

Select the Form first and set its properties. Add containers before their children. These are initial Designer values, not statements to paste into a procedure.

Form propertyValue
Caption"Language Workbench"
Width500
Height240
TypeNameParent
CommandButtoncmdRunForm
LabellblStatusForm
cmdRun · exact Inspector values
PropertyValue
Left24
Top24
Width200
Height36
Caption"Run demonstrations"
TabIndex0
lblStatus · exact Inspector values
PropertyValue
Left24
Top90
Width450
Height70
Caption"Open the IDE output, then run the demonstrations."
WordWrapTrue

Step 3Connect the events explicitly#

frmMain

ObjectEventProcedureParameters
cmdRunClickcmdRun_ClickClick()

Step 4Enter the complete behavior#

Open the named source file. Enter declarations first, then every procedure. Expand a procedure for its full body, or copy the complete file. Helpers are required: do not stop after entering the first Click handler.

frmMain.bas

Enter these declarations at file scope, before the procedures.

frmMain.bas · declarations
Option Explicit
Private Sub cmdRun_Click()

Enter the complete action, including its input checks and calls to helper procedures.

frmMain.bas · cmdRun_Click
Private Sub cmdRun_Click()
    Main.Main
    lblStatus.Caption = "Demonstrations completed. Read Debug.Print results in the IDE output."
End Sub
Copy the complete frmMain.bas file

Replace the file once; do not append duplicate procedures.

frmMain.bas
Option Explicit

Private Sub cmdRun_Click()
    Main.Main
    lblStatus.Caption = "Demonstrations completed. Read Debug.Print results in the IDE output."
End Sub

Main.bas

Enter these declarations at file scope, before the procedures.

Main.bas · declarations
Option Explicit

Public Enum TaskState
    Ready = 0
    Running = 1
    Complete = 2
End Enum
Public Function Greeting(ByVal name As String) As String

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · Greeting
Public Function Greeting(ByVal name As String) As String
    Dim clean As String
    clean = Trim$(name)
    If Len(clean) = 0 Then
        Greeting = "Enter a name."
        Exit Function
    End If
    Greeting = "Hello, " & clean
End Function
Public Function StateCaption(ByVal state As TaskState) As String

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · StateCaption
Public Function StateCaption(ByVal state As TaskState) As String
    Select Case state
        Case TaskState.Ready
            StateCaption = "Ready"
        Case TaskState.Running
            StateCaption = "Working"
        Case Else
            StateCaption = "Complete"
    End Select
End Function
Public Function SumReadings() As Long

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · SumReadings
Public Function SumReadings() As Long
    Dim readings(-1 To 1) As Long
    Dim index As Long
    Dim total As Long
    readings(-1) = 10
    readings(0) = 20
    readings(1) = 30
    For index = LBound(readings) To UBound(readings)
        total = total + readings(index)
    Next index
    SumReadings = total
End Function
Public Function GrowReadings() As Long

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · GrowReadings
Public Function GrowReadings() As Long
    Dim readings() As Long
    ReDim readings(0 To 1)
    readings(0) = 4
    readings(1) = 5
    ReDim Preserve readings(0 To 2)
    readings(2) = 6
    GrowReadings = readings(0) + readings(1) + readings(2)
End Function
Public Sub FillReadings(ByRef readings() As Long)

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · FillReadings
Public Sub FillReadings(ByRef readings() As Long)
    ReDim readings(0 To 2)
    readings(0) = 20
    readings(1) = 30
    readings(2) = 40
End Sub
Public Function ReadingsFromProcedure() As Long

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · ReadingsFromProcedure
Public Function ReadingsFromProcedure() As Long
    Dim readings() As Long
    FillReadings readings
    ReadingsFromProcedure = readings(0) + readings(1) + readings(2)
End Function
Public Function TimerSeconds() As Double

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · TimerSeconds
Public Function TimerSeconds() As Double
    TimerSeconds = CDbl(Timer())
End Function
Public Function CollectionCount() As Long

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · CollectionCount
Public Function CollectionCount() As Long
    Dim names As Collection
    Set names = New Collection
    names.Add "Ada", "first"
    names.Add "Grace", "second"
    names.Remove "first"
    CollectionCount = names.Count
End Function
Public Function LookupName() As String

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · LookupName
Public Function LookupName() As String
    Dim names As Dictionary
    Set names = New Dictionary
    names.Add "lead", "Ada"
    If names.Exists("lead") Then
        LookupName = CStr(names.Item("lead"))
    Else
        LookupName = "Unknown"
    End If
End Function
Public Function CountDown(ByVal start As Long) As Long

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · CountDown
Public Function CountDown(ByVal start As Long) As Long
    Dim value As Long
    value = start
    Do While value > 0
        value = value - 1
    Loop
    CountDown = value
End Function
Public Function CatchFailure() As Long

Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.

Main.bas · CatchFailure
Public Function CatchFailure() As Long
    Try
        Err.Raise 513, "LanguageWorkbench", "A deliberate practice error."
    Catch failure As McException
        CatchFailure = failure.Number
    Finally
        Debug.Print "Error cleanup completed"
    End Try
End Function
Public Sub Main()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · Main
Public Sub Main()
    Dim counter As Counter
    Set counter = New Counter
    counter.Add 3
    counter.Add 2
    Debug.Print Greeting("  Ada  ")
    Debug.Print StateCaption(TaskState.Running)
    Debug.Print "Array total: " & CStr(SumReadings())
    Debug.Print "Grown array total: " & CStr(GrowReadings())
    Debug.Print "Callee-sized array total: " & CStr(ReadingsFromProcedure())
    Debug.Print "Seconds since midnight: " & CStr(TimerSeconds())
    Debug.Print "Collection count: " & CStr(CollectionCount())
    Debug.Print "Lookup: " & LookupName()
    Debug.Print "Counter: " & CStr(counter.Value)
    Debug.Print "Countdown: " & CStr(CountDown(3))
    Debug.Print "Caught error: " & CStr(CatchFailure())
End Sub
Public Function LineTotal(ByVal quantity As Long, ByVal unitPrice As Currency) As Currency

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · LineTotal
Public Function LineTotal(ByVal quantity As Long, ByVal unitPrice As Currency) As Currency
    Dim total As Currency
    total = quantity * unitPrice
    LineTotal = total
End Function
Public Function Welcome(ByVal name As String, Optional ByVal prefix As String = "Hello") As String

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · Welcome
Public Function Welcome(ByVal name As String, Optional ByVal prefix As String = "Hello") As String
    Welcome = prefix & ", " & name
End Function
Public Sub TryWelcome()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · TryWelcome
Public Sub TryWelcome()
    Debug.Print Welcome("Ada")
    Debug.Print Welcome(prefix:="Welcome", name:="Ada")
End Sub
Public Sub TryCounter()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · TryCounter
Public Sub TryCounter()
    Dim counter As Counter
    Set counter = New Counter
    counter.Add 3
    counter.Add 2
    Debug.Print counter.Value
End Sub
Public Sub ReadLocalJson()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Main.bas · ReadLocalJson
Public Sub ReadLocalJson()
    Dim document As JsonValue
    Dim name As String
    document = Json.Parse("{""name"":""Ada""}")
    If Json.Has(document, "name") Then
        name = CStr(Json.Value(Json.Get(document, "name")))
        Debug.Print name
    End If
End Sub
Copy the complete Main.bas file

Replace the file once; do not append duplicate procedures.

Main.bas
Option Explicit

Public Enum TaskState
    Ready = 0
    Running = 1
    Complete = 2
End Enum

Public Function Greeting(ByVal name As String) As String
    Dim clean As String
    clean = Trim$(name)
    If Len(clean) = 0 Then
        Greeting = "Enter a name."
        Exit Function
    End If
    Greeting = "Hello, " & clean
End Function

Public Function StateCaption(ByVal state As TaskState) As String
    Select Case state
        Case TaskState.Ready
            StateCaption = "Ready"
        Case TaskState.Running
            StateCaption = "Working"
        Case Else
            StateCaption = "Complete"
    End Select
End Function

Public Function SumReadings() As Long
    Dim readings(-1 To 1) As Long
    Dim index As Long
    Dim total As Long
    readings(-1) = 10
    readings(0) = 20
    readings(1) = 30
    For index = LBound(readings) To UBound(readings)
        total = total + readings(index)
    Next index
    SumReadings = total
End Function

Public Function GrowReadings() As Long
    Dim readings() As Long
    ReDim readings(0 To 1)
    readings(0) = 4
    readings(1) = 5
    ReDim Preserve readings(0 To 2)
    readings(2) = 6
    GrowReadings = readings(0) + readings(1) + readings(2)
End Function

Public Sub FillReadings(ByRef readings() As Long)
    ReDim readings(0 To 2)
    readings(0) = 20
    readings(1) = 30
    readings(2) = 40
End Sub

Public Function ReadingsFromProcedure() As Long
    Dim readings() As Long
    FillReadings readings
    ReadingsFromProcedure = readings(0) + readings(1) + readings(2)
End Function

Public Function TimerSeconds() As Double
    TimerSeconds = CDbl(Timer())
End Function

Public Function CollectionCount() As Long
    Dim names As Collection
    Set names = New Collection
    names.Add "Ada", "first"
    names.Add "Grace", "second"
    names.Remove "first"
    CollectionCount = names.Count
End Function

Public Function LookupName() As String
    Dim names As Dictionary
    Set names = New Dictionary
    names.Add "lead", "Ada"
    If names.Exists("lead") Then
        LookupName = CStr(names.Item("lead"))
    Else
        LookupName = "Unknown"
    End If
End Function

Public Function CountDown(ByVal start As Long) As Long
    Dim value As Long
    value = start
    Do While value > 0
        value = value - 1
    Loop
    CountDown = value
End Function

Public Function CatchFailure() As Long
    Try
        Err.Raise 513, "LanguageWorkbench", "A deliberate practice error."
    Catch failure As McException
        CatchFailure = failure.Number
    Finally
        Debug.Print "Error cleanup completed"
    End Try
End Function

Public Sub Main()
    Dim counter As Counter
    Set counter = New Counter
    counter.Add 3
    counter.Add 2
    Debug.Print Greeting("  Ada  ")
    Debug.Print StateCaption(TaskState.Running)
    Debug.Print "Array total: " & CStr(SumReadings())
    Debug.Print "Grown array total: " & CStr(GrowReadings())
    Debug.Print "Callee-sized array total: " & CStr(ReadingsFromProcedure())
    Debug.Print "Seconds since midnight: " & CStr(TimerSeconds())
    Debug.Print "Collection count: " & CStr(CollectionCount())
    Debug.Print "Lookup: " & LookupName()
    Debug.Print "Counter: " & CStr(counter.Value)
    Debug.Print "Countdown: " & CStr(CountDown(3))
    Debug.Print "Caught error: " & CStr(CatchFailure())
End Sub

Public Function LineTotal(ByVal quantity As Long, ByVal unitPrice As Currency) As Currency
    Dim total As Currency
    total = quantity * unitPrice
    LineTotal = total
End Function

Public Function Welcome(ByVal name As String, Optional ByVal prefix As String = "Hello") As String
    Welcome = prefix & ", " & name
End Function

Public Sub TryWelcome()
    Debug.Print Welcome("Ada")
    Debug.Print Welcome(prefix:="Welcome", name:="Ada")
End Sub

Public Sub TryCounter()
    Dim counter As Counter
    Set counter = New Counter
    counter.Add 3
    counter.Add 2
    Debug.Print counter.Value
End Sub

Public Sub ReadLocalJson()
    Dim document As JsonValue
    Dim name As String
    document = Json.Parse("{""name"":""Ada""}")
    If Json.Has(document, "name") Then
        name = CStr(Json.Value(Json.Get(document, "name")))
        Debug.Print name
    End If
End Sub

Counter.cls

Enter these declarations at file scope, before the procedures.

Counter.cls · declarations
Option Explicit

Private mValue As Long
Public Property Get Value() As Long

Keep this public property accessor with its complementary accessor and the fields or controls it uses.

Counter.cls · Value
Public Property Get Value() As Long
    Value = mValue
End Property
Public Sub Add(ByVal amount As Long)

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Counter.cls · Add
Public Sub Add(ByVal amount As Long)
    mValue = mValue + amount
End Sub
Copy the complete Counter.cls file

Replace the file once; do not append duplicate procedures.

Counter.cls
Option Explicit

Private mValue As Long

Public Property Get Value() As Long
    Value = mValue
End Property

Public Sub Add(ByVal amount As Long)
    mValue = mValue + amount
End Sub

LanguageTests.bas

Enter these declarations at file scope, before the procedures.

LanguageTests.bas · declarations
Option Explicit
Public Sub Test_Greeting()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

LanguageTests.bas · Test_Greeting
Public Sub Test_Greeting()
    Assert.AreEqual "Hello, Ada", Greeting("  Ada  ")
    Assert.AreEqual "Enter a name.", Greeting("   ")
End Sub
Public Sub Test_ReferenceFunctions()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

LanguageTests.bas · Test_ReferenceFunctions
Public Sub Test_ReferenceFunctions()
    Assert.AreEqual CCur(15), LineTotal(3, CCur(5))
    Assert.AreEqual "Hello, Ada", Welcome("Ada")
    Assert.AreEqual "Welcome, Ada", Welcome(prefix:="Welcome", name:="Ada")
End Sub
Public Sub Test_JsonValues()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

LanguageTests.bas · Test_JsonValues
Public Sub Test_JsonValues()
    Dim document As JsonValue
    document = Json.Parse("{""name"":""Ada""}")
    Assert.IsTrue Json.Has(document, "name")
    Assert.AreEqual "Ada", CStr(Json.Value(Json.Get(document, "name")))
End Sub
Public Sub Test_Arrays()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

LanguageTests.bas · Test_Arrays
Public Sub Test_Arrays()
    Assert.AreEqual 60, SumReadings()
    Assert.AreEqual 15, GrowReadings()
End Sub
Public Sub Test_ArrayParameters()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

LanguageTests.bas · Test_ArrayParameters
Public Sub Test_ArrayParameters()
    Dim readings() As Long
    FillReadings readings
    Assert.AreEqual 0, LBound(readings)
    Assert.AreEqual 2, UBound(readings)
    Assert.AreEqual 90, ReadingsFromProcedure()
End Sub
Public Sub Test_TimerValue()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

LanguageTests.bas · Test_TimerValue
Public Sub Test_TimerValue()
    Dim seconds As Double
    seconds = TimerSeconds()
    Assert.IsTrue seconds >= 0
    Assert.IsTrue seconds < 86400
End Sub
Public Sub Test_Containers()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

LanguageTests.bas · Test_Containers
Public Sub Test_Containers()
    Assert.AreEqual 1, CollectionCount()
    Assert.AreEqual "Ada", LookupName()
End Sub
Public Sub Test_ControlFlow()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

LanguageTests.bas · Test_ControlFlow
Public Sub Test_ControlFlow()
    Assert.AreEqual "Working", StateCaption(Main.TaskState.Running)
    Assert.AreEqual 0, CountDown(3)
    Assert.AreEqual 513, CatchFailure()
End Sub
Public Sub Test_InstanceState()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

LanguageTests.bas · Test_InstanceState
Public Sub Test_InstanceState()
    Dim first As Counter
    Dim second As Counter
    Set first = New Counter
    Set second = New Counter
    first.Add 5
    second.Add 2
    Assert.AreEqual 5, first.Value
    Assert.AreEqual 2, second.Value
    Assert.IsFalse first Is second
End Sub
Copy the complete LanguageTests.bas file

Replace the file once; do not append duplicate procedures.

LanguageTests.bas
Option Explicit

Public Sub Test_Greeting()
    Assert.AreEqual "Hello, Ada", Greeting("  Ada  ")
    Assert.AreEqual "Enter a name.", Greeting("   ")
End Sub

Public Sub Test_ReferenceFunctions()
    Assert.AreEqual CCur(15), LineTotal(3, CCur(5))
    Assert.AreEqual "Hello, Ada", Welcome("Ada")
    Assert.AreEqual "Welcome, Ada", Welcome(prefix:="Welcome", name:="Ada")
End Sub

Public Sub Test_JsonValues()
    Dim document As JsonValue
    document = Json.Parse("{""name"":""Ada""}")
    Assert.IsTrue Json.Has(document, "name")
    Assert.AreEqual "Ada", CStr(Json.Value(Json.Get(document, "name")))
End Sub

Public Sub Test_Arrays()
    Assert.AreEqual 60, SumReadings()
    Assert.AreEqual 15, GrowReadings()
End Sub

Public Sub Test_ArrayParameters()
    Dim readings() As Long
    FillReadings readings
    Assert.AreEqual 0, LBound(readings)
    Assert.AreEqual 2, UBound(readings)
    Assert.AreEqual 90, ReadingsFromProcedure()
End Sub

Public Sub Test_TimerValue()
    Dim seconds As Double
    seconds = TimerSeconds()
    Assert.IsTrue seconds >= 0
    Assert.IsTrue seconds < 86400
End Sub

Public Sub Test_Containers()
    Assert.AreEqual 1, CollectionCount()
    Assert.AreEqual "Ada", LookupName()
End Sub

Public Sub Test_ControlFlow()
    Assert.AreEqual "Working", StateCaption(Main.TaskState.Running)
    Assert.AreEqual 0, CountDown(3)
    Assert.AreEqual 513, CatchFailure()
End Sub

Public Sub Test_InstanceState()
    Dim first As Counter
    Dim second As Counter
    Set first = New Counter
    Set second = New Counter
    first.Add 5
    second.Add 2
    Assert.AreEqual 5, first.Value
    Assert.AreEqual 2, second.Value
    Assert.IsFalse first Is second
End Sub

Step 5Keep resources and storage reproducible#

No external resource or migration files are declared. Any inline migration, seed data or local JSON is present in the code above. Runtime application data and private preferences are not included in the archive.

Step 6Check, run and test the result#

Save all files and run Check. Resolve diagnostics before Run. Developer CLI users can work from the extracted folder:

Developer CLI
mcbasic check LanguageWorkbench.mcproj
mcbasic run LanguageWorkbench.mcproj
mcbasic test LanguageWorkbench.mcproj

Acceptance checklist

  • Run Check on the whole project.
  • Run the demonstrations and inspect the IDE output: array totals 60, 15 and 90, seconds since midnight from 0 up to (but not including) 86400, collection count 1, lookup Ada, counter 5 and countdown 0.
  • Run Test: all nine test procedures must pass.
  • Change the expected greeting in the test to an incorrect value, confirm the test fails, then restore it.

Step 7Make one change and repeat the tests#

Add a pure function that clamps a Long to a minimum and maximum. Add tests for below, inside and above the range.

Save a working Git checkpoint first. Change one behavior, Check again and repeat the acceptance checklist. Compare with the downloaded solution if a regression appears.

Source baseline 20f4bfed6b · Build manifest
Sources & maintenance

Original web explanation, reviewed against these repository sources. API tables and example files follow the implementation.

  • docs/manual/21-language-reference.md
  • docs/manual/11-language-toolkit.md
  • site/examples/LanguageWorkbench/Main.bas
  • src/McBasic.Compiler/ArrayResizeAnalysis.cs
  • src/McBasic.Compiler/Binder.cs
  • src/McBasic.Compiler/CallArguments.cs
  • src/McBasic.Runtime/BasicRuntime.cs
  • site/examples/LanguageWorkbench/LanguageTests.bas
  • site/examples/LanguageWorkbench/LanguageWorkbench.mcproj
  • site/examples/LanguageWorkbench/Counter.cls