From 52d13717ce1f8163932ecebae90a7b702523880f Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 30 Sep 2026 09:57:59 -0500 Subject: [PATCH] Lots of new BASIC features. --- Makefile | 3 +- docs/dvx_basic_reference.html | 1255 ++++++- docs/dvx_system_reference.html | 33 + screenshots/86box/apps-cpanel.png | 4 +- screenshots/86box/cpanel-cp-color.png | 4 +- screenshots/86box/cpanel-cp-desk.png | 4 +- screenshots/86box/cpanel-cp-joy.png | 4 +- screenshots/86box/cpanel-cp-mouse.png | 4 +- screenshots/86box/cpanel-cp-net.png | 4 +- screenshots/86box/cpanel-cp-print.png | 4 +- screenshots/86box/cpanel-cp-types.png | 4 +- screenshots/86box/cpanel-cp-video.png | 4 +- screenshots/86box/f1-h-basdem.png | 4 +- screenshots/86box/f1-h-cpanel.png | 4 +- screenshots/86box/hang-ended.png | 4 +- screenshots/86box/hang-taskmgr.png | 4 +- screenshots/86box/menus-m-altdn.png | 4 +- screenshots/86box/menus-m-altup.png | 4 +- screenshots/86box/menus-m-drop.png | 4 +- screenshots/86box/scrtest-back.png | 3 + screenshots/86box/scrtest-con1.png | 3 + screenshots/86box/scrtest-con2.png | 3 + screenshots/86box/scrtest-full1.png | 3 + screenshots/86box/scrtest-full2.png | 3 + screenshots/86box/scrtest-gfx.png | 3 + screenshots/86box/video-v-800.png | 4 +- screenshots/86box/video-v-pick.png | 4 +- screenshots/dosbox/apps-cpanel.png | 4 +- screenshots/dosbox/cpanel-cp-color.png | 4 +- screenshots/dosbox/cpanel-cp-desk.png | 4 +- screenshots/dosbox/cpanel-cp-joy.png | 4 +- screenshots/dosbox/cpanel-cp-mouse.png | 4 +- screenshots/dosbox/cpanel-cp-net.png | 4 +- screenshots/dosbox/cpanel-cp-print.png | 4 +- screenshots/dosbox/cpanel-cp-types.png | 4 +- screenshots/dosbox/cpanel-cp-video.png | 4 +- screenshots/dosbox/f1-h-basdem.png | 4 +- screenshots/dosbox/f1-h-cpanel.png | 4 +- screenshots/dosbox/hang-ended.png | 4 +- screenshots/dosbox/hang-taskmgr.png | 4 +- screenshots/dosbox/menus-m-altdn.png | 4 +- screenshots/dosbox/menus-m-altup.png | 4 +- screenshots/dosbox/menus-m-drop.png | 4 +- screenshots/dosbox/scrtest-back.png | 3 + screenshots/dosbox/scrtest-con1.png | 3 + screenshots/dosbox/scrtest-con2.png | 3 + screenshots/dosbox/scrtest-full1.png | 3 + screenshots/dosbox/scrtest-full2.png | 3 + screenshots/dosbox/scrtest-gfx.png | 3 + screenshots/dosbox/video-v-800.png | 4 +- screenshots/dosbox/video-v-pick.png | 4 +- src/apps/kpunch/Makefile | 2 +- src/apps/kpunch/basdemo/basdemo.dbp | 4 + src/apps/kpunch/basdemo/basdemo.dhs | 1 + src/apps/kpunch/basdemo/basdemo.frm | 34 + src/apps/kpunch/basdemo/shape.cls | 36 + src/apps/kpunch/cpanel/cpanel.c | 128 +- src/apps/kpunch/cpanel/cpanel.dhs | 4 +- src/apps/kpunch/dvxbasic/Makefile | 6 +- src/apps/kpunch/dvxbasic/basBuild.c | 64 +- src/apps/kpunch/dvxbasic/basBuild.h | 27 +- src/apps/kpunch/dvxbasic/basRes.h | 141 + src/apps/kpunch/dvxbasic/compiler/basEvents.h | 22 +- .../kpunch/dvxbasic/compiler/basKeywords.h | 25 +- src/apps/kpunch/dvxbasic/compiler/codegen.c | 116 +- src/apps/kpunch/dvxbasic/compiler/codegen.h | 5 + src/apps/kpunch/dvxbasic/compiler/compact.c | 21 +- src/apps/kpunch/dvxbasic/compiler/lexer.c | 62 + src/apps/kpunch/dvxbasic/compiler/lexer.h | 8 + src/apps/kpunch/dvxbasic/compiler/obfuscate.c | 12 + src/apps/kpunch/dvxbasic/compiler/obfuscate.h | 5 +- src/apps/kpunch/dvxbasic/compiler/opcodes.h | 387 +- src/apps/kpunch/dvxbasic/compiler/parser.c | 3236 +++++++++++++++-- src/apps/kpunch/dvxbasic/compiler/parser.h | 12 + src/apps/kpunch/dvxbasic/compiler/symtab.c | 150 +- src/apps/kpunch/dvxbasic/compiler/symtab.h | 73 +- src/apps/kpunch/dvxbasic/ctrlover.dhs | 121 + src/apps/kpunch/dvxbasic/form.dhs | 9 + src/apps/kpunch/dvxbasic/formrt/formGfx.c | 1021 ++++++ src/apps/kpunch/dvxbasic/formrt/formrt.c | 605 ++- src/apps/kpunch/dvxbasic/formrt/formrt.h | 51 + src/apps/kpunch/dvxbasic/ide/ideDesigner.c | 3 +- src/apps/kpunch/dvxbasic/ide/ideDesigner.h | 1 + src/apps/kpunch/dvxbasic/ide/ideMain.c | 643 +++- src/apps/kpunch/dvxbasic/ide/ideProject.c | 166 +- src/apps/kpunch/dvxbasic/ide/ideProject.h | 45 +- src/apps/kpunch/dvxbasic/ideguide.dhs | 97 +- src/apps/kpunch/dvxbasic/langref.dhs | 1453 +++++++- src/apps/kpunch/dvxbasic/runtime/basErrors.h | 6 + src/apps/kpunch/dvxbasic/runtime/dates.c | 1244 +++++++ src/apps/kpunch/dvxbasic/runtime/dates.h | 148 + src/apps/kpunch/dvxbasic/runtime/gfx.c | 1172 ++++++ src/apps/kpunch/dvxbasic/runtime/gfx.h | 153 + src/apps/kpunch/dvxbasic/runtime/screen.c | 996 +++++ src/apps/kpunch/dvxbasic/runtime/screen.h | 122 + src/apps/kpunch/dvxbasic/runtime/serialize.c | 145 +- src/apps/kpunch/dvxbasic/runtime/values.c | 311 +- src/apps/kpunch/dvxbasic/runtime/values.h | 91 +- src/apps/kpunch/dvxbasic/runtime/vm.c | 2665 +++++++++++++- src/apps/kpunch/dvxbasic/runtime/vm.h | 99 + src/apps/kpunch/dvxbasic/stub/basstub.c | 7 + src/apps/kpunch/dvxbasic/test_compiler.c | 2145 ++++++++++- src/apps/kpunch/dvxbasic/test_suite.c | 121 +- src/include/basic/commdlg.bas | 9 +- src/libs/kpunch/libdvx/apiref.dhs | 64 + src/libs/kpunch/libdvx/dvxApp.c | 120 + src/libs/kpunch/libdvx/dvxApp.h | 24 +- src/libs/kpunch/libdvx/dvxDialog.c | 240 +- src/libs/kpunch/libdvx/dvxDlg.h | 22 + src/test/audio/Makefile | 2 +- src/test/basic/Makefile | 2 +- src/test/basic/fixtures/classObjects.bas | 64 + src/test/basic/fixtures/classObjects.out | 6 + src/test/dialogs/dialogsTest.c | 189 + src/test/dos/fixtures/scrtest/scrtest.bas | 61 + src/test/dos/fixtures/scrtest/scrtest.dbp | 11 + src/test/dos/scripts/scrtest.scr | 44 + src/test/dvxtest/dvxtest.c | 23 + src/test/dvxtest/dvxtest.h | 5 + src/test/formrt/testFormRt.c | 581 ++- src/test/ide/Makefile | 4 +- src/test/ide/testIde.c | 4 +- src/test/ide/testIdeDebug.c | 2 +- src/test/ide/testIdeDesign.c | 164 +- src/test/ide/testIdeRegress.c | 276 ++ src/test/lint/analyzerBaseline.txt | 16 +- src/test/lint/cppcheckBaseline.txt | 2 + src/test/music/Makefile | 2 +- src/test/samples/golden/basdemo-debug.app | Bin 23431 -> 24906 bytes src/test/samples/golden/basdemo-release.app | Bin 20182 -> 21349 bytes src/test/samples/golden/helpedit-debug.app | Bin 12571 -> 12661 bytes src/test/samples/golden/helpedit-release.app | Bin 11363 -> 11453 bytes src/test/samples/golden/iconed-debug.app | Bin 17371 -> 17461 bytes src/test/samples/golden/iconed-release.app | Bin 14506 -> 14596 bytes src/test/samples/golden/imgview-debug.app | Bin 9536 -> 9626 bytes src/test/samples/golden/imgview-release.app | Bin 9063 -> 9153 bytes src/test/samples/golden/inc-comm-debug.app | Bin 7418 -> 7498 bytes src/test/samples/golden/inc-comm-release.app | Bin 7336 -> 7416 bytes src/test/samples/golden/inc-commdlg-debug.app | Bin 7132 -> 7222 bytes .../samples/golden/inc-commdlg-release.app | Bin 7068 -> 7158 bytes src/test/samples/golden/inc-fileops-debug.app | Bin 7114 -> 7194 bytes .../samples/golden/inc-fileops-release.app | Bin 7059 -> 7139 bytes src/test/samples/golden/inc-game-debug.app | Bin 7098 -> 7178 bytes src/test/samples/golden/inc-game-release.app | Bin 6980 -> 7060 bytes src/test/samples/golden/inc-help-debug.app | Bin 7046 -> 7126 bytes src/test/samples/golden/inc-help-release.app | Bin 6991 -> 7071 bytes .../samples/golden/inc-joystick-debug.app | Bin 7119 -> 7199 bytes .../samples/golden/inc-joystick-release.app | Bin 7055 -> 7135 bytes src/test/samples/golden/inc-music-debug.app | Bin 7263 -> 7343 bytes src/test/samples/golden/inc-music-release.app | Bin 7202 -> 7282 bytes src/test/samples/golden/inc-net-debug.app | Bin 7191 -> 7271 bytes src/test/samples/golden/inc-net-release.app | Bin 7136 -> 7216 bytes src/test/samples/golden/inc-print-debug.app | Bin 7125 -> 7205 bytes src/test/samples/golden/inc-print-release.app | Bin 7070 -> 7150 bytes .../samples/golden/inc-resource-debug.app | Bin 7151 -> 7231 bytes .../samples/golden/inc-resource-release.app | Bin 7087 -> 7167 bytes src/test/samples/golden/inc-sound-debug.app | Bin 7147 -> 7227 bytes src/test/samples/golden/inc-sound-release.app | Bin 7089 -> 7169 bytes src/test/samples/golden/inc-sql-debug.app | Bin 7194 -> 7274 bytes src/test/samples/golden/inc-sql-release.app | Bin 7139 -> 7219 bytes src/test/samples/golden/inc-xfer-debug.app | Bin 7150 -> 7230 bytes src/test/samples/golden/inc-xfer-release.app | Bin 7071 -> 7151 bytes src/test/samples/golden/notepad-debug.app | Bin 12240 -> 12330 bytes src/test/samples/golden/notepad-release.app | Bin 10909 -> 10999 bytes src/test/samples/golden/resedit-debug.app | Bin 13880 -> 13970 bytes src/test/samples/golden/resedit-release.app | Bin 12204 -> 12294 bytes src/test/samples/golden/widshow-debug.app | Bin 15195 -> 15275 bytes src/test/samples/golden/widshow-release.app | Bin 14185 -> 14265 bytes src/test/shell/appsTest.c | 61 +- src/widgets/kpunch/canvas/canvas.bhs | 3 + 170 files changed, 20422 insertions(+), 1271 deletions(-) create mode 100644 screenshots/86box/scrtest-back.png create mode 100644 screenshots/86box/scrtest-con1.png create mode 100644 screenshots/86box/scrtest-con2.png create mode 100644 screenshots/86box/scrtest-full1.png create mode 100644 screenshots/86box/scrtest-full2.png create mode 100644 screenshots/86box/scrtest-gfx.png create mode 100644 screenshots/dosbox/scrtest-back.png create mode 100644 screenshots/dosbox/scrtest-con1.png create mode 100644 screenshots/dosbox/scrtest-con2.png create mode 100644 screenshots/dosbox/scrtest-full1.png create mode 100644 screenshots/dosbox/scrtest-full2.png create mode 100644 screenshots/dosbox/scrtest-gfx.png create mode 100644 src/apps/kpunch/basdemo/shape.cls create mode 100644 src/apps/kpunch/dvxbasic/formrt/formGfx.c create mode 100644 src/apps/kpunch/dvxbasic/runtime/dates.c create mode 100644 src/apps/kpunch/dvxbasic/runtime/dates.h create mode 100644 src/apps/kpunch/dvxbasic/runtime/gfx.c create mode 100644 src/apps/kpunch/dvxbasic/runtime/gfx.h create mode 100644 src/apps/kpunch/dvxbasic/runtime/screen.c create mode 100644 src/apps/kpunch/dvxbasic/runtime/screen.h create mode 100644 src/test/basic/fixtures/classObjects.bas create mode 100644 src/test/basic/fixtures/classObjects.out create mode 100644 src/test/dos/fixtures/scrtest/scrtest.bas create mode 100644 src/test/dos/fixtures/scrtest/scrtest.dbp create mode 100644 src/test/dos/scripts/scrtest.scr diff --git a/Makefile b/Makefile index 24d41fbe..48b15eb9 100644 --- a/Makefile +++ b/Makefile @@ -218,9 +218,10 @@ deploy-sdk: tools apps done @# BASIC include files @cp src/include/basic/*.bas $(SDKDIR)/include/basic/ - @# BASIC sample: basdemo (project file + form + icon + help source) + @# BASIC sample: basdemo (project file + form + class module + icon + help source) @cp src/apps/kpunch/basdemo/basdemo.dbp $(SDKDIR)/samples/basic/basdemo/ @cp src/apps/kpunch/basdemo/basdemo.frm $(SDKDIR)/samples/basic/basdemo/ + @cp src/apps/kpunch/basdemo/shape.cls $(SDKDIR)/samples/basic/basdemo/ @cp src/apps/kpunch/basdemo/basdemo.dhs $(SDKDIR)/samples/basic/basdemo/ @cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/ @# BASIC sample: widshow (widget showcase) diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index b969b73e..58984ffc 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -67,6 +67,7 @@ img { max-width: 100%; }
  • Changes in This Release
  • Data Types
  • Form and Control Statements
  • +
  • Objects and Class Modules + +
  • SQL Functions
  • App Object
  • +
  • Clipboard Object
  • INI Functions
  • Predefined Constants
  • Include Libraries
  • @@ -101,6 +110,7 @@ img { max-width: 100%; }
  • Common Properties, Events, and Methods

    Index

    @@ -743,7 +946,8 @@ img { max-width: 100%; } --- Project Properties... Edit project metadata (name, author, version, startup form, icon, etc.) and the command line arguments used when the program is run from the IDE. --- - Add File... Ctrl+O Add a .bas or .frm file to the project. If no project is open, creates an implicit project from the file. A file that cannot be read, or is 64 KB or larger, is refused with a message. + Add File... Ctrl+O Add a .bas, .frm or .cls file to the project. If no project is open, creates an implicit project from the file. A file that cannot be read, or is 64 KB or larger, is refused with a message. + New Class Module Add a new, empty class module (Class1.cls, Class2.cls, ...) to the project and open it in the Code Editor. See Class Modules under Project System. Save File Ctrl+S Save the active file to disk. Save All Save all modified files in the project. --- @@ -752,7 +956,7 @@ img { max-width: 100%; } Remove File Remove the selected file from the project (prompts to save if modified). --- Exit Close the IDE (prompts to save unsaved changes). -

    A project (.dbp) or source file (.bas, .frm) named on the IDE's command line opens when the IDE starts, so double-clicking one in the File Manager, or starting it from the Program Manager, opens it in the IDE.

    +

    A project (.dbp) or source file (.bas, .frm, .cls) named on the IDE's command line opens when the IDE starts, so double-clicking one in the File Manager, or starting it from the Program Manager, opens it in the IDE.

    Files are saved exactly as edited (LF line endings). Each save writes a temporary file first and replaces the original only when the write succeeded. Opening a project that fails to load keeps the current project open and removes the failed entry from the Recent list.

    Edit Menu

    Back to Overview

    @@ -897,7 +1101,7 @@ img { max-width: 100%; } -------- -------- Default Text Identifiers, whitespace. Keywords IF, THEN, FOR, NEXT, SUB, FUNCTION, DIM, PRINT, SELECT, CASE, DO, LOOP, WHILE, WEND, END, EXIT, CALL, GOSUB, GOTO, RETURN, DECLARE, CONST, TYPE, AND, OR, NOT, XOR, MOD, etc. - Types INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN, TRUE, FALSE + Types INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN, DATE, VARIANT, OBJECT, COLLECTION, TRUE, FALSE, NOTHING, EMPTY, NULL Strings "Hello, World!" Comments ' This is a comment, REM This is a comment Numbers 42, 3.14, &HFF @@ -944,10 +1148,23 @@ img { max-width: 100%; }
  • Metadata -- Author, Company, Version, Copyright, Description (a single line), and Icon Path (for compiled binaries).
  • Arguments -- the command line the program gets (COMMAND$) when it is run from the IDE, for trying out a program that opens the file it is given. It is not built into the compiled program: there COMMAND$ returns whatever the program was started with.
  • Help File -- a compiled help file (.hlp) for the program. It is copied next to the compiled program, and F1 in the running program opens it, at the HelpTopic of the focused control or, failing that, of the form. Add help.bas to the project and call HelpView to open it from a Help menu.
  • -
  • File list -- relative paths of all .bas and .frm files in the project. Each entry tracks whether it is a form file.
  • +
  • File list -- relative paths of all the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section and forms (.frm) in the [Forms] section.
  • +

    The file sections all have the same shape: one FileN line per file, numbered from 0, and a Count line:

    +
    [Modules]
    +File0 = main.bas
    +Count = 1
    +
    +[Classes]
    +File0 = account.cls
    +File1 = ledger.cls
    +Count = 2
    +
    +[Forms]
    +File0 = form1.frm
    +Count = 1

    How Projects are Compiled

    -

    When the project is compiled, all source files are concatenated into a single source stream. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form.

    +

    When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module is likewise marked with its class name; a class module's header lines are not compiled.

    Compile-Time Validation

    Before the source is handed to the compiler, the IDE scans every .frm file and cross-checks the forms against the widget registry and the statements that reference them. Three classes of error are caught and reported without producing a binary:

      @@ -956,7 +1173,7 @@ img { max-width: 100%; }
    • Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.
    • Menu items -- only Name (read), Checked and Enabled can be used on a menu item in code; Caption, Visible and RadioCheck are design-time settings.
    -

    These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.

    +

    These checks use the same metadata the Properties panel and Toolbox read from each installed control, so property and method names always match the current widget set without any separate list to maintain.

    Project Operations

      Operation             Description
       ---------             -----------
    @@ -964,11 +1181,28 @@ img { max-width: 100%; }
       Open Project          Opens a .dbp file and loads all referenced files into memory. A project file that is 64 KB or larger, or cannot be read, is not loaded; opening it later reports "Cannot load <file>: unreadable or larger than 65535 bytes." Carriage returns are removed when files are loaded.
       Save Project          Writes the .dbp file to disk.
       Close Project         Closes the project, prompting to save unsaved changes.
    -  Add File              Adds a .bas or .frm file to the project. Opening a file without a project auto-creates an implicit project.
    +  Add File              Adds a .bas, .frm or .cls file to the project. Opening a file without a project auto-creates an implicit project.
    +  New Class Module      Adds a new class module to the project and opens it (see Class Modules below).
       Remove File           Removes a file from the project (prompts to save if modified). Removing the startup form clears the Startup Form setting.
       Project Properties    Opens a dialog for editing project metadata (name, author, company, version, copyright, description, icon, help file, startup form) and the Arguments for runs from the IDE.

    Project Explorer

    -

    The Project Explorer is a tree view window listing all project files. Double-click a file to open it: .bas files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.

    +

    The Project Explorer is a tree view window listing all project files, in three groups: Forms, Modules and Classes. Double-click a file to open it: .bas and .cls files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.

    +

    Class Modules

    +

    A class module (.cls file) defines a kind of object for your program: its member variables, properties and methods. The BASIC Language Reference describes how to write one (see Objects and Class Modules); this section covers working with them in the IDE.

    +
      +
    • Create -- choose File > New Class Module. The IDE adds a new class module named Class1 (Class2, and so on, when that name is taken) to the Classes group, saved in the project's directory as Class1.cls, and opens it in the Code Editor. The new file holds only its header and Option Explicit; it is written to disk when you save it.
    • +
    • Add an existing one -- choose File > Add File and pick a .cls file (the Class Modules file type). Class files from Visual Basic can be added as they are.
    • +
    • Open -- double-click it in the Project Explorer's Classes group. It opens in Code view; a class module has no designer.
    • +
    • Save -- File > Save File or Save All, as for any other file. The project file lists class modules in its [Classes] section.
    • +
    • Remove -- select it in the Project Explorer and choose File > Remove File. The file itself stays on disk.
    • +
    • Rename -- a class's name is the one in its Attribute VB_Name line, not the file name. To rename the class, edit that line (and the code that uses the old name) and save; the file keeps its name. A class module without an Attribute VB_Name line takes its name from the file name.
    • +
    +

    A new class module starts with these header lines:

    +
    VERSION DVX 1.00 CLASS
    +Attribute VB_Name = "Class1"
    +
    +Option Explicit
    +

    Leave the VERSION and Attribute lines at the top of the file; they are not compiled as code. Each class name must be different from the other class names and the form names in the project.

    Back to Overview

    @@ -1090,13 +1324,15 @@ img { max-width: 100%; }
      Column   Content
       ------   -------
       Name     Variable name (internal mangled names like static variable placeholders are filtered out).
    -  Type     Data type: Integer, Long, Single, Double, String, Boolean, Array, UDT. Array types show the element type (e.g., Integer()).
    +  Type     Data type: Integer, Long, Single, Double, String, Boolean, Date, Variant, Array, UDT, or the type of object. Array types show the element type (e.g., Integer()).
       Value    Current value. Strings are shown in quotes. Booleans as True/False. Arrays show bounds and element count (e.g., Integer(0 To 9) [10]). Uninitialized arrays show (uninitialized).
    +

    Variants show the value they currently hold, or Empty or Null. Object variables show Nothing, or which object they refer to: the class name of a class instance, a Collection with its number of items, or the form or control.

    The Locals window displays:

    • Local variables of the current procedure, including its STATIC variables and the module variables it names in a SHARED statement.
    • Module-level variables. While execution is paused inside a SUB or FUNCTION, only the variables declared with DIM SHARED (or REDIM SHARED) are listed, because the procedure cannot see the others. This also applies inside a DEF FN function, although a DEF FN can read every module variable.
    • Form-scoped variables for the current form (if the program is executing within a form context), with the same SHARED rule.
    • +
    • Member variables of the object, while execution is paused in a procedure of a class module: the variables of the very object the procedure was called for (Me), Private ones included.

    The Watch window and the Immediate window follow the same rules. The window is resizable.

    Call Stack Window

    @@ -1188,12 +1424,14 @@ img { max-width: 100%; }

    Output Window

    The Output window is a read-only TextArea at the bottom-left of the screen. It displays:

      -
    • PRINT output -- all PRINT statement output from the running program is appended here.
    • +
    • PRINT output -- PRINT statement output from the running program is appended here, until the program uses a console or graphics statement (CLS, LOCATE, COLOR, SCREEN, LINE, INKEY$ and the others): from then on PRINT and INPUT use the program's own screen window instead (see Console and Graphics in the language reference).
    • Runtime errors -- if the VM encounters a runtime error (division by zero, out-of-bounds, etc.), the error message and line number are displayed in the output with an Error on line N: prefix.
    • Compile errors -- if compilation fails, the error message and location are shown.

    The output buffer holds up to 32,768 characters; when it is full the oldest lines are dropped. The window always shows the newest output. Use Run > Clear Output to clear it.

    -

    INPUT statements prompt the user via a modal InputBox dialog; the prompt text is also echoed to the Output window.

    +

    INPUT statements prompt the user via a modal InputBox dialog; the prompt text is also echoed to the Output window. A program that has its own screen window types its INPUT there instead.

    +

    The Program's Screen Window

    +

    A program that uses the console or graphics statements gets a window of its own, the size of its screen mode (80x25 text, or 640x480 or 320x200 graphics), titled with the project's name. Alt+Enter in that window (or FULLSCREEN in the program) shows it on the whole screen; Alt+Tab brings the desktop back and the program continues. Keys typed in the window go to the program (INKEY$ and INPUT). The window stays open after the program's code has finished, so its output can be read; closing it ends the program.

    Back to Overview

    @@ -1239,7 +1477,7 @@ img { max-width: 100%; } Copyright Default copyright notice. (empty) Description Default project description (one line). (empty)

    Colors Tab

    -

    Customizes the syntax-highlighting colors used in the code editor. The left side is a list of seven color entries (Default Text, Keywords, Strings, Comments, Numbers, Operators, Types). Select an entry and adjust its red, green, and blue components using the three sliders on the right. The Preview swatch shows the current color. Changes are applied to the open editor when you click OK.

    +

    Customizes the syntax-highlighting colors used in the code editor. The left side is a list of seven color entries (Default Text, Keywords, Strings, Comments, Numbers, Operators, Types). Select an entry to see its color, and its red, green and blue amounts, on the right. Click Change... (or double-click the entry) to pick a new color in the color box: click a basic color or mix one with the Red, Green and Blue sliders or number boxes, then click OK. Changes are applied to the open editor when you click OK in the Preferences dialog.

    Back to Overview

    @@ -1310,6 +1548,21 @@ img { max-width: 100%; }
  • A SUB or FUNCTION may use at most 64 local slots (parameters, local variables and hidden temporaries). More is a compile error "Too many local variables in X (n, limit is 64)" instead of a runtime error 9; READ and INPUT # into many array elements no longer use a slot per target.
  • SHARED naming a variable that does not exist at module level creates it there; in a form's code it becomes a variable of that form, not a program-wide one.
  • +

    Objects, Classes and Variants

    +
      +
    • New: class modules (.cls files) define your own objects, with Public and Private member variables, Subs and Functions, Property Get / Let / Set, Class_Initialize and Class_Terminate, Me and a default member. Objects are made with NEW (DIM x AS NEW Account, SET x = NEW Account), compared with Is and tested with TypeOf ... Is. An object is freed, and its Class_Terminate run, as soon as nothing refers to it any more.
    • +
    • New: the Collection object (Add, Remove, Item, Count, with optional text keys) and FOR EACH ... NEXT over Collections and arrays.
    • +
    • New: the Variant type (DIM v AS VARIANT) with the special values Empty and Null, and the functions VARTYPE, TYPENAME, ISEMPTY, ISNULL, ISNUMERIC, ISOBJECT, ISARRAY and CVAR, with the vb... VarType constants and vbObjectError.
    • +
    • New: PUBLIC and PRIVATE at module level (PUBLIC x is DIM SHARED x, PRIVATE x is DIM x).
    • +
    • New error numbers: 94 Invalid use of Null, 424 Object required, 438 Object doesn't support this property or method, 450 Wrong number of arguments or invalid property assignment, 451 Object not a collection, 457 duplicate Collection key.
    • +
    • Changed: CHOOSE with an index outside its list returns Null instead of raising error 5.
    • +
    • Changed: a PARAMARRAY without an AS clause is an array of Variants (TYPENAME "Variant()").
    • +
    • A form or control name can be used directly wherever an object is expected: SET c = Text1, either side of Is, TypeOf, and an argument for an object parameter.
    • +
    • NEW, PROPERTY, PUBLIC, PRIVATE and TYPEOF are now reserved words: a variable, SUB or FUNCTION with one of these names must be renamed. VARIANT, OBJECT, COLLECTION, EMPTY, NULL and EACH are colored like keywords but are not reserved.
    • +
    • IDE: File > New Class Module, .cls files in Add File, a Classes group in the Project Explorer and a [Classes] section in the project file. The debugger shows Variants, objects and the member variables of the object whose code is running.
    • +
    +

    See: Objects and Class Modules

    +

    See: Variants

    Sound, Joysticks and Frames

    • New statements from QBasic: BEEP, SOUND frequency, duration (in clock ticks, 18.2 a second) and PLAY with the whole music macro language (notes, octaves, lengths, dots, pauses, tempo, MN/ML/MS, and MF/MB for foreground or background music), plus the PLAY(n) function (notes still waiting in the background). They play on whatever sound hardware the computer has, or silently when there is none.
    • @@ -1405,14 +1658,33 @@ img { max-width: 100%; } Single 4 bytes ! 32-bit float, approximately 7 digits precision Double 8 bytes # 64-bit float, approximately 15 digits precision String variable $ Variable-length, reference-counted, dynamic string - Boolean 2 bytes (none) True (-1) or False (0) + Boolean 2 bytes (none) True (-1) or False (0) + Date 8 bytes (none) A date and time of day from 1/1/100 to 12/31/9999 + Variant varies (none) Any value at all: a number, text, a date, an object, an array, or the special values Empty and Null (see Variants) +

      Object Types

      +

      An object variable does not hold a value of its own; it refers to an object that lives elsewhere, so two variables can refer to the same object. Store a reference with Set, use the object's properties and methods with a dot, and set the variable to Nothing when you are done with it.

      +
        Declared As     Holds
      +  -----------     -----
      +  Control         A control on a form (any type of control)
      +  Form            A form
      +  Object          Any object: a form, a control, an instance of a class module or a Collection
      +  ClassName       An instance of that class module (see Objects and Class Modules)
      +  Collection      A Collection object (see Collection Object)
      +
      Dim c As Control
      +Set c = Text1
      +c.Text = "Hello"
      +
      +Dim people As New Collection
      +people.Add "Ann"
      +

      An object variable that has not been Set holds Nothing. Control, Form and Object variables accept each other's values; a variable declared As a class name or As Collection accepts only an object of that kind, or Nothing: an object of another kind raises error 13 (Type mismatch), and a value that is not an object at all raises error 424 (Object required). The Source parameter of the DragDrop and DragOver events is a Control.

      +

      See also: Objects and Class Modules

      Internal Types

      These types are not directly declarable but are used internally by the runtime.

        Internal Type   Description
         -------------   -----------
         Array           Reference-counted multi-dimensional array (up to 8 dimensions)
         UDT             User-defined type instance (created with TYPE...END TYPE)
      -  Object          Opaque host object (form reference, control reference)
      +  Object          Reference to a form, a control, a class instance or a Collection; declarable as Control, Form, Object, a class name or Collection
         Ref             ByRef pointer to a variable slot (used for ByRef parameters)

      Type Suffixes

      Type suffixes can be appended to variable names to declare their type implicitly:

      @@ -1438,7 +1710,7 @@ name$ = "Hello" ' String

      Type Promotion

      When mixing types in expressions, values are automatically promoted to a common type: Integer -> Long -> Single -> Double. + joins two strings; when a string meets a number in an arithmetic operator (including +) the string is converted as VAL does ("1" + 2 is 3, "abc" + 2 is 2). Use & to join text and numbers, and VAL / STR$ to convert explicitly.

      Assignment Conversion

      -

      Storing a value into a variable declared as Integer, Long, Single or Boolean (by suffix, by AS clause, by DEFINT/DEFLNG/DEFSNG, or as a typed parameter, STATIC variable, FUNCTION result, TYPE field or array element) converts the value to that type. DEFtype statements also give parameters, STATIC variables and FUNCTION results their default type. Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has.

      +

      Storing a value into a variable declared as Integer, Long, Single, Boolean or Date (by suffix, by AS clause, by DEFINT/DEFLNG/DEFSNG, or as a typed parameter, STATIC variable, FUNCTION result, TYPE field or array element) converts the value to that type. DEFtype statements also give parameters, STATIC variables and FUNCTION results their default type. Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. Date takes a number as a date's number of days and reads a string as a date (error 13 when the text is not a date, error 6 outside 1/1/100 to 12/31/9999); INPUT into a Date variable reads the date the same way. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has.

      Dim n As Integer
       n = 3.7            ' n is 4
       n = 40000          ' Error 6: Overflow
      @@ -1449,7 +1721,93 @@ s = 1 / 3          ' s is Single: prints 0.3333333
       x = 1 / 3          ' x was never declared and keeps the Double: prints 0.333333333333333

      Boolean Values

      Boolean values use -1 for True and 0 for False. Any non-zero numeric value is treated as True in a conditional context. The keywords True and False are reserved and may be used anywhere a Boolean value is expected.

      +

      Date Values

      +

      A Date holds a calendar date and a time of day, as in Visual Basic: the whole part of its number counts days from 12/30/1899 and the fraction is the time (0.5 is noon, so 36526.5 is noon on 1/1/2000). Days before 12/30/1899 are negative numbers whose time still counts forward from midnight: -1.25 is 6:00 AM on 12/29/1899. CDBL gives a date's number and CDATE turns a number back into a date.

      +

      Write a date literal between # signs. The month comes before the day; a year can also come first, and month names (full or abbreviated) work too. A time can follow the date or stand alone:

      +
      Dim d As Date
      +d = #1/15/2000#                  ' January 15, 2000
      +d = #2000-01-15 15:30#           ' year first, 24-hour time
      +d = #Jan 15, 2000 3:30:00 PM#
      +d = #3:30 PM#                    ' a time alone is on 12/30/1899
      +

      Two-digit years 00 to 29 are 2000 to 2029 and 30 to 99 are 1930 to 1999. Date is not a reserved word, so a program may still use Date (or any of the date function names) for its own variables and procedures; the program's own name wins.

      +

      PRINT, STR$, CSTR and the & operator show a date as m/d/yyyy h:mm:ss AM (1/15/2000 3:30:00 PM). A date at midnight shows only the date and a time on 12/30/1899 only the time; a Date variable that was never assigned is 12:00:00 AM. Use FORMAT$ for any other layout.

      +

      Adding a number of days to a date, or subtracting one, gives a date (fractions are parts of a day, so d + 0.25 is six hours later); subtracting one date from another gives the number of days between them as a Double. Comparisons put dates in time order, and a string compared with a date is read as a date:

      +
      Dim due As Date
      +due = Date + 30                     ' thirty days from today
      +If due > #12/31/2025# Then Print "Next year"
      +Print #3/1/2000# - #2/1/2000#       ' 29 (2000 is a leap year)
      +If due = "1/15/2026" Then Print "Match"
      +

      A Date variable has no Empty or Null state (a Variant does): a date that is out of range is error 6 (Overflow), and text that is not a date is error 13 (Type mismatch).

      See also: Conversion Functions

      +

      See also: Variants

      +
    +
    +

    Variants

    +

    A Variant is a variable that can hold any kind of value: a number, text, a date, True or False, an object or a whole array. It takes on the type of whatever is stored in it, and can change type each time something new is stored. A Variant can also hold two special values that no other type has: Empty (nothing has been stored yet) and Null (the value is unknown or missing).

    +
    Dim v As Variant
    +Print TypeName(v)       ' Empty
    +v = 42
    +Print TypeName(v)       ' Integer
    +v = "forty-two"
    +Print TypeName(v)       ' String
    +v = #1/15/2000#
    +Print TypeName(v)       ' Date
    +v = Null
    +Print IsNull(v)         ' True
    +

    Declare a Variant with AS VARIANT. Arrays of Variants (DIM list(10) AS VARIANT), Variant parameters, and FUNCTIONs and properties AS VARIANT work the same way; each element of a Variant array starts Empty. A variable declared with DIM but no AS clause, or never declared at all, is not a Variant: it keeps the older rules described under Assignment Conversion.

    +

    Storing a Variant in a variable of another type converts the value to that type, as for any other assignment. A Variant that is given an array refers to that same array: a change made through either name is seen through the other, as when one array variable is assigned to another. Index it like the array itself, v(1).

    +

    Empty

    +

    Empty means that nothing has been stored in the Variant yet. A new Variant is Empty, and assigning Empty makes it Empty again. In arithmetic Empty counts as 0 and in text as "", so a counter kept in a Variant needs no starting value; PRINT shows nothing for it.

    +
    Dim total As Variant
    +total = total + 5       ' Empty counts as 0: total is 5
    +If IsEmpty(v) Then Print "Nothing stored yet"
    +

    Null

    +

    Null means "no valid value" -- for example a field that was never filled in. Null spreads through most expressions: anything calculated from a Null is Null too, so a missing value is not mistaken for a real one.

    +
      Expression                                    Result
    +  ----------                                    ------
    +  Null + 1, Null * x, -Null, Not Null           Null
    +  Null = 1, Null < x, Null = Null               Null (a comparison with Null is never True)
    +  Abs(Null), Int(Null), Fix(Null), Sgn(Null)    Null
    +  "Name: " & Null                               "Name: " (& treats Null as "")
    +  Null & Null                                   Null
    +  False And Null                                False
    +  True Or Null                                  True
    +  False Imp Null, Null Imp True                 True
    +  Any other And, Or, Xor, Eqv or Imp with Null  Null
    +

    An IF, ELSEIF, WHILE, DO or LOOP condition that is Null counts as False. Any other use that needs a real number -- storing Null in a numeric, Boolean or Date variable, CINT(Null) and the other conversion functions, a string function such as LEN or UCASE$, or an array subscript -- raises error 94 (Invalid use of Null).

    +

    Because Null = Null is itself Null, test for Null with ISNULL, never with =:

    +
    If IsNull(price) Then
    +    Print "No price yet"
    +Else
    +    Print "Price: "; price
    +End If
    +

    Empty and Null are not reserved words: a program that declares its own variable or constant named Empty or Null keeps using it.

    +

    Variant Functions

    +
      Function      Returns   Description
    +  --------      -------   -----------
    +  VARTYPE(v)    Integer   A number for the kind of value v holds (see VarType Constants): vbEmpty (0), vbNull (1), vbInteger (2), vbString (8), vbObject (9) and so on. For an array it is vbArray (8192) plus the element type, so an Integer array is 8194 and a Variant array 8204.
    +  TYPENAME(v)   String    The name of the kind of value: "Integer", "Long", "Single", "Double", "String", "Boolean", "Date", "Empty", "Null", the name of a TYPE, "Integer()" for an array, and for objects the class name, "Collection", "Nothing", or the type of a form or control.
    +  ISEMPTY(v)    Boolean   True when v is Empty.
    +  ISNULL(v)     Boolean   True when v is Null.
    +  ISNUMERIC(v)  Boolean   True when v is a number, True or False, Empty, or text that reads as a number ("12", "1.5"). False for Null, dates and other text.
    +  ISOBJECT(v)   Boolean   True when v is an object reference, including Nothing.
    +  ISARRAY(v)    Boolean   True when v is an array.
    +  ISDATE(v)     Boolean   True for a Date or text holding a date (see Date and Time Functions).
    +  CVAR(v)       Variant   v unchanged, as a Variant.
    +
    Sub Describe(ByVal v As Variant)
    +    Select Case VarType(v)
    +        Case vbEmpty
    +            Print "(empty)"
    +        Case vbNull
    +            Print "(unknown)"
    +        Case vbString
    +            Print "Text: "; v
    +        Case Else
    +            Print TypeName(v); ": "; v
    +    End Select
    +End Sub
    +

    See also: Data Types

    +

    See also: VarType Constants

    Operators

    @@ -1463,7 +1821,7 @@ x = 1 / 3 ' x was never declared and keeps the Double: prints 0.3333333 5 MOD Modulus 6 + - Addition, subtraction (+ also concatenates strings) 7 & String concatenation - 8 = <> < > <= >= Comparison (returns Boolean) + 8 = <> < > <= >= Is Comparison (returns Boolean) 9 NOT Logical/bitwise NOT 10 AND Logical/bitwise AND 11 XOR Logical/bitwise exclusive-or @@ -1489,10 +1847,16 @@ x = 1 / 3 ' x was never declared and keeps the Double: prints 0.3333333 < Less than > Greater than <= Less than or equal to - >= Greater than or equal to -

    String comparisons are controlled by OPTION COMPARE. With OPTION COMPARE BINARY (the default) strings are compared by byte value. With OPTION COMPARE TEXT comparisons are case-insensitive.

    + >= Greater than or equal to + Is Both sides refer to the same object (objects only) +

    String comparisons are controlled by OPTION COMPARE. With OPTION COMPARE BINARY (the default) strings are compared by byte value. With OPTION COMPARE TEXT comparisons are case-insensitive. A comparison with Null gives Null (see Variants).

    +

    Is compares object references, not values: a Is b is True when both refer to the very same object, and x Is Nothing tests for an object variable that holds no object. Either side may be a form or control name. Using Is on something that is not an object raises error 424 (Object required).

    +

    TypeOf object Is TypeName is True when the object is of that type. TypeName can be a class module's name, Collection, Form, Control (any control), Object (any object that is not Nothing), or a control type such as TextBox. It is False for Nothing. Use Not TypeOf ... Is ... for the opposite test.

    +
    If player Is Nothing Then Set player = New Player
    +If a Is b Then Print "Same object"
    +If TypeOf Source Is TextBox Then Print Source.Text

    Logical / Bitwise Operators

    -

    The logical operators work on Boolean values and also perform bitwise operations on integer values.

    +

    The logical operators work on Boolean values and also perform bitwise operations on integer values. With Null they give Null unless the other side decides the answer (False And Null is False, True Or Null is True); see Variants.

      Operator   Description
       --------   -----------
       NOT        Logical/bitwise complement
    @@ -1528,6 +1892,8 @@ REM This is a comment
     

    Input/Output (PRINT, INPUT, DATA/READ)

    Miscellaneous Statements (ON ERROR, SHELL, SLEEP, END)

    Sound, Joysticks and Frames (BEEP, SOUND, PLAY, STICK, STRIG, FRAMEWAIT)

    +

    Objects and Class Modules (NEW, PROPERTY, Class_Initialize, Me)

    +

    Collection Object (Add, Remove, Item, Count)

    Declaration Statements

    @@ -1540,6 +1906,7 @@ DIM variable(dim1, dim2, ...) AS type DIM variable() AS type DIM variable AS UdtName DIM variable AS STRING * n +DIM variable AS NEW ClassName DIM SHARED variable AS type DIM variable1 [AS type], variable2 [AS type], ...

    An array declared with bounds is a fixed array. DIM variable() with empty parentheses declares a dynamic array that has no storage until a REDIM gives it bounds; using it before then raises error 13. An array that is first created by REDIM, and an array parameter, are dynamic too (see ERASE for the difference).

    @@ -1556,6 +1923,7 @@ Dim record As PersonType Dim fixedStr As String * 20
    Note: A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables".

    Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.

    +

    DIM variable AS NEW ClassName (or AS NEW Collection) creates a new object at the DIM and stores it in the variable; without NEW an object variable starts as Nothing. See Objects and Class Modules.

    Variable Scope and SHARED

    DVX BASIC follows the QBasic scope rules:

      @@ -1563,7 +1931,7 @@ Dim fixedStr As String * 20
    • A SUB or FUNCTION sees a module variable only when it was declared with DIM SHARED at module level, or when the procedure names it in a SHARED statement. This applies to arrays as well.
    • Any other name used inside a SUB or FUNCTION is a local variable of that procedure, even when a module variable of the same name exists. With OPTION EXPLICIT, using such a name without a DIM is a compile error that says the module variable is not SHARED.
    • Indexing (name(...)) or dotting (name.member) a module variable the procedure cannot see is a compile error.
    • -
    • CONSTs declared at module level, TYPEs, SUBs, FUNCTIONs, forms and controls are visible everywhere; no SHARED is needed for them.
    • +
    • CONSTs declared at module level, TYPEs, SUBs, FUNCTIONs, class modules, forms and controls are visible everywhere; no SHARED is needed for them. PUBLIC at module level is the same as DIM SHARED (see PUBLIC / PRIVATE).
    • A DEF FN is module-level code: its expression sees every module variable.
    DIM SHARED total AS LONG       ' visible in every SUB and FUNCTION
    @@ -1581,6 +1949,11 @@ END SUB

    DIM SHARED and REDIM SHARED are only allowed at module level. Inside a procedure use the SHARED statement:

    SHARED variable [()] [AS type] [, variable [()] [AS type] ...]

    SHARED gives the procedure access to the named module variables (write name() for an array). The AS type, when given, must match the module variable's declaration. Naming a variable that does not exist at module level creates it there (in a form's code, as a variable of that form).

    +

    PUBLIC / PRIVATE

    +

    At module level, PUBLIC and PRIVATE can be used in place of DIM, as in Visual Basic. PUBLIC declares a variable that every SUB and FUNCTION can use (the same as DIM SHARED); PRIVATE declares one that only module-level code sees (the same as DIM).

    +
    Public highScore As Long        ' same as Dim Shared highScore As Long
    +Private tries As Integer        ' same as Dim tries As Integer
    +

    PUBLIC and PRIVATE may also be written in front of SUB and FUNCTION. Outside a class module they change nothing, because procedures are visible to the whole program anyway. In a class module they decide what code outside the class can use; see Objects and Class Modules. Inside a procedure, use DIM.

    REDIM

    Reallocates an array, optionally preserving existing data. The array may have been declared with DIM name() or with bounds, or not declared at all (REDIM then declares a dynamic array).

    REDIM array(newBounds) AS type
    @@ -1590,15 +1963,30 @@ REDIM SHARED array(newBounds) AS type
    ReDim items(newSize) As String
     ReDim Preserve scores(1 To newCount) As Integer

    CONST

    -

    Declares a named constant. The value must be a literal (integer, long, float, string, or boolean). Constants are not variables -- you cannot assign a new value to them or pass them by reference.

    -
    CONST name = value
    -CONST name AS type = value
    +

    Declares a named constant. The value is worked out once, when the program is compiled, and can never change -- you cannot assign a new value to a constant or pass it by reference. Several constants can be declared in one CONST statement, separated by commas.

    +
    CONST name = expression
    +CONST name AS type = expression
    +CONST name = expression, name = expression, ...
    Const MAX_SIZE = 100
     Const PI = 3.14159265
     Const APP_NAME = "DVX App"
     Const DEBUG_MODE = True
    -Const HEADER As String = "=== Report ==="
    -

    With an AS clause the literal is converted to that type: Integer and Long values are rounded and must fit the type, and a string literal for a numeric type (or a number for String) is a compile error. Without an AS clause an integer literal makes a Long constant and a literal with a fraction a Double constant.

    +Const HEADER As String = "=== Report ===" +Const LAUNCH = #7/20/1969 8:17 PM# +Const LAST_INDEX = MAX_SIZE - 1, HALF = MAX_SIZE \ 2 +Const TITLE = APP_NAME & " " & "1.0" +Const CRLF = Chr$(13) & Chr$(10) +Const TEAL = RGB(0, 128, 128) +Const BIG = MAX_SIZE > 50 +

    The value can be any expression built from:

    +
      +
    • literals (numbers, strings, True and False, #date# literals);
    • +
    • constants declared earlier, including ENUM members (Member or EnumName.Member);
    • +
    • the arithmetic operators + - * / \ MOD ^, the comparison operators, AND OR NOT XOR EQV IMP, and string joining with & or +;
    • +
    • these built-in functions: ABS, ASC, ATN, CBOOL, CDATE, CDBL, CHOOSE, CHR$, CINT, CLNG, COS, CSNG, CSTR, DATEADD, DATEDIFF, DATEPART, DATESERIAL, DATEVALUE, DAY, EXP, FIX, FORMAT$, GETBLUE, GETGREEN, GETRED, HEX$, HOUR, IIF, INSTR, INSTRREV, INT, ISDATE, LCASE$, LEFT$, LEN, LOG, LTRIM$, MID$, MINUTE, MONTH, MONTHNAME, OCT$, REPLACE$, RGB, RIGHT$, RTRIM$, SECOND, SGN, SIN, SPACE$, SQR, STR$, STRCOMP, STRING$, STRREVERSE$, TAN, TIMESERIAL, TIMEVALUE, TRIM$, UCASE$, VAL, WEEKDAY, WEEKDAYNAME and YEAR.
    • +
    +

    Anything else -- a variable, a FUNCTION of your own, or a built-in whose answer can change from run to run such as TIMER, NOW, RND or DIR$ -- is a compile error ("Constant expression required"). An expression that fails, such as a division by zero or a result too big for its type, is also reported when the program is compiled. Text comparisons in a constant follow the OPTION COMPARE setting in effect where the constant is declared. A string constant can be at most 255 characters long.

    +

    Without an AS clause the constant takes the type of its value: a whole number makes a Long constant, a number with a fraction a Double constant, and strings, Booleans and dates keep their type. With an AS clause the value is converted to that type: Integer and Long values are rounded and must fit the type, and a string for a numeric type (or a number for String) is a compile error.

    Constants declared at module level are visible to all procedures. A CONST declared inside a SUB or FUNCTION is local to that procedure; it may reuse the name of a module constant without changing it.

    TYPE...END TYPE

    Defines a user-defined type (record/structure).

    @@ -1617,6 +2005,32 @@ Dim p As PersonType p.firstName = "Scott" p.age = 30

    UDT fields can themselves be UDTs (nested types), and field access chains through array elements too: shapes(i).origin.x = 5. A new instance starts with numeric fields at 0, Boolean fields False, variable strings empty and STRING * n fields padded to n spaces. TYPE variables can be written to and read from RANDOM and BINARY files with PUT and GET (see GET / PUT).

    +

    ENUM...END ENUM

    +

    Defines a set of named whole-number constants, and a type name for variables that hold one of them.

    +
    ENUM EnumName
    +    member
    +    member = expression
    +    ...
    +END ENUM
    +
    Enum Direction
    +    North           ' 0
    +    East            ' 1
    +    South = 10
    +    West            ' 11
    +End Enum
    +
    +Enum Style
    +    Bold = 1
    +    Italic = Bold * 2
    +    Underline = Style.Italic * 2
    +    AllStyles = Bold Or Italic Or Underline
    +End Enum
    +
    +Dim heading As Direction
    +heading = West
    +If heading = Direction.West Then Print "Going west"
    +

    The first member is 0 and each member without a value is one more than the member before it. A value may be any constant expression (see CONST), including members declared earlier in the same ENUM or in another one. Members are Long constants and can be used on their own (West) or with the ENUM name in front (Direction.West). If two ENUMs have a member with the same name, that name must be written with its ENUM name.

    +

    The ENUM name can be used as a type wherever a type is allowed: DIM, parameters, FUNCTION results and TYPE fields. Such a variable is a Long, so it can hold any whole number, not only the members. ENUMs are declared at module level (not inside a SUB or FUNCTION) and are visible everywhere in the program; declare an ENUM before the code that uses its members.

    DECLARE

    Forward-declares a SUB or FUNCTION. This is rarely required because the compiler supports forward references within a module, but it is still accepted for compatibility:

    DECLARE SUB name ([BYVAL] [OPTIONAL] param AS type, ...)
    @@ -1642,6 +2056,7 @@ End Declare
    count = count + 1 Print count End Sub +

    In a class module, a STATIC variable is shared by every object of the class; use a member variable for a value each object keeps separately.

    OPTION

    Sets compiler options. Must appear before any executable code.

    OPTION BASE 0          ' Arrays start at index 0 (default)
    @@ -1671,13 +2086,32 @@ LET variable = expression
    SWAP array(index), variable
    Swap a, b
     Swap names(i), names(j)
    +

    LSET / RSET

    +

    Stores a string into a string variable without changing the variable's length: LSET places the value at the left and RSET at the right, filling the rest with spaces. A value longer than the variable is cut off at the right.

    +
    LSET variable = expression
    +RSET variable = expression
    +
    Dim field As String
    +field = Space$(8)
    +LSet field = "Name"          ' "Name    "
    +RSet field = "42"            ' "      42"
    +

    The target may be an array element or a TYPE field. It must hold a string; any other value raises error 13 (Type mismatch).

    SET

    -

    Assigns an object reference (form or control) to a variable. Required when creating a form or control with CreateForm / CreateControl so that the returned reference is stored in the variable:

    -
    SET variable = objectExpression
    -
    Dim frm As Long
    +

    Stores an object reference -- a form, a control, an instance of a class module, a Collection, or Nothing -- in a variable, array element, TYPE field or property. After SET both names refer to the same object; nothing is copied.

    +
    SET variable = objectExpression
    +SET variable = NEW ClassName
    +SET variable = Nothing
    +
    Dim frm As Form
     Set frm = CreateForm("MyForm", 320, 240)
    -frm.Caption = "Built in code"
    -

    For ordinary numeric or string assignment, SET is not used.

    +frm.Caption = "Built in code" + +Dim c As Control +Set c = Text1 ' a control on the form +c.Text = "Hello" + +Dim list As Collection +Set list = New Collection ' a new, empty Collection +Set list = Nothing ' let it go
    +

    The value must be an object or Nothing; anything else raises error 424 (Object required). SET is not used for ordinary numeric or string assignment.

    ERASE

    Frees the memory of a dynamic array (declared with DIM name(), created by REDIM, or an array parameter) and resets it to the undimensioned state; using it again before a REDIM raises error 13. A fixed array (declared with bounds) keeps its bounds and has every element reset to zero, an empty string, False or a fresh TYPE instance.

    ERASE arrayName
    @@ -1750,6 +2184,27 @@ For x = 10 To 0 Step -2 Next

    The variable name after NEXT is optional. Use EXIT FOR to break out early.

    With an Integer or Long counter a constant STEP must be a whole number (STEP 0.5 is a compile error); a STEP computed at run time is rounded to the counter's type. A Single or Double counter takes any step.

    +

    FOR EACH...NEXT

    +

    Runs the loop once for every item in a Collection or every element of an array, without counting.

    +
    FOR EACH item IN group
    +    statements
    +NEXT [item]
    +
    Dim names As New Collection
    +names.Add "Ann"
    +names.Add "Bob"
    +
    +Dim person As Variant
    +For Each person In names
    +    Print "Hello, "; person
    +Next person
    +
    +Dim scores(1 To 3) As Integer
    +Dim s As Variant
    +For Each s In scores
    +    total = total + s
    +Next
    +

    group can be a Collection, an array (of any number of dimensions; elements are visited in row order, the last subscript changing fastest), a Variant holding an array, or a ParamArray. On each pass item receives a copy of the next item: for a Collection of objects it refers to the object itself, but assigning a new value to item does not change the array or Collection. Declare item AS VARIANT, or as an object type for a Collection of objects. The number of items is checked on every pass, so items added or removed inside the loop are taken into account.

    +

    EXIT FOR leaves the loop early. Looping over something that is not an array or Collection raises error 13 (Type mismatch), or error 451 (Object not a collection) for an object that is not a Collection. EACH and IN are not reserved words.

    DO...LOOP

    General-purpose loop with pre-test, post-test, or infinite forms.

    DO [WHILE condition | UNTIL condition]
    @@ -1816,6 +2271,29 @@ End Sub
     
     Announce "hello"            ' loud defaults to 0
     Announce "hello", True      ' loud is -1
    +

    ParamArray

    +

    The last parameter of a SUB or FUNCTION can be marked PARAMARRAY to accept any number of extra arguments. It is written as an array, name(), and receives every remaining argument as an array numbered from 0. With no extra arguments the array is empty: UBOUND returns -1.

    +
    Sub PrintAll(ByVal title As String, ParamArray items())
    +    Dim i As Integer
    +    Print title;
    +    For i = 0 To UBound(items)
    +        Print " "; items(i);
    +    Next
    +    Print
    +End Sub
    +
    +PrintAll "Totals:", 1, "two", 3.5    ' three items: 0 To 2
    +PrintAll "Nothing:"                  ' UBound(items) is -1
    +
    +Function Sum(ParamArray n() As Double) As Double
    +    Dim i As Integer
    +    Dim total As Double
    +    For i = 0 To UBound(n)
    +        total = total + n(i)
    +    Next
    +    Sum = total
    +End Function
    +

    Without an AS clause (and no type suffix or DEFtype rule for the name) the ParamArray is an array of Variants (TYPENAME gives "Variant()"): each element holds the argument that was passed with its own type, so numbers, strings and objects can be mixed, and FOR EACH can walk it. With an AS clause every argument is converted to that type. Arguments are passed by value: changing an element does not change the caller's variable. A PARAMARRAY cannot be combined with OPTIONAL parameters, cannot be BYVAL or BYREF, cannot hold a TYPE, and takes at most 31 arguments. It is not available in DEF FN or DECLARE LIBRARY.

    FUNCTION...END FUNCTION

    Defines a function with a return value.

    FUNCTION name ([BYVAL] [OPTIONAL] param AS type, ...) AS returnType
    @@ -1851,7 +2329,9 @@ Print FnCelsius(212)  ' prints 100
    EXIT FOR
     EXIT DO
     EXIT SUB
    -EXIT FUNCTION
    +EXIT FUNCTION +EXIT PROPERTY +

    EXIT PROPERTY leaves a Property Get, Let or Set procedure of a class module (see Objects and Class Modules).

    CALL

    Explicitly calls a subroutine or function. The return value (if any) is discarded.

    CALL name
    @@ -1888,7 +2368,7 @@ ON expression GOSUB label1, label2, ...

    Input/Output Statements

    PRINT

    -

    Writes expressions to the output window (or to a file channel).

    +

    Writes expressions to the output window, or to a file channel. Once the program has used a console or graphics statement (see Console and Graphics), PRINT writes on the program's screen instead, at the cursor, in the current COLOR.

    PRINT [expression [{; | ,} expression] ...]
     PRINT #channel, expression [; expression] ...
     PRINT USING format$; expression [; expression] ...
    @@ -1945,7 +2425,7 @@ Print Using "& likes &"; name$; food$ ' "Scott likes Print Using "Total: ###.## units"; 7.5 ' "Total: 7.50 units" Print Using "## and ##"; 1; 2; 3 ' " 1 and 2 3 and "

    INPUT

    -

    Reads a line of text from the user or from a file channel.

    +

    Reads a line of text from the user or from a file channel. The user types it in a dialog box, or on the program's screen once it has one (see Console and Graphics).

    INPUT variable
     INPUT "prompt"; variable
     INPUT #channel, variable
    @@ -2016,7 +2496,13 @@ ErrorHandler: 68 Device unavailable (no printer to print on) 75 Path/file access error 76 Path not found - 91 Object variable not set (property, method, Show/Hide/Unload on a value that is not a form or control) + 91 Object variable not set (property, method, Show/Hide/Unload on a value that is not a form or control, or on Nothing) + 94 Invalid use of Null (Null where a real number or text is needed; see Variants) + 424 Object required (SET, Is or TypeOf with a value that is not an object) + 438 Object doesn't support this property or method (the object has no member of that name, or it is Private) + 450 Wrong number of arguments or invalid property assignment + 451 Object not a collection (FOR EACH over an object that is not a Collection) + 457 This key is already associated with an element of this collection (Collection.Add with a key in use) 1000 No form runtime (a form or control statement ran without the form runtime, e.g. under a headless runner)

    SHELL

    Executes an operating-system command.

    @@ -2099,6 +2585,125 @@ Do FRAMEWAIT 30 Loop Until quitting
    +
    +

    Console and Graphics

    +

    These statements work as they do in QBasic. The first one a program uses gives it a screen of its own: a window, titled with the program's name, the size of the screen mode. From then on PRINT and INPUT use that screen too (before it, PRINT writes to the output window and INPUT asks in a dialog box, as always). Keys typed in the window go to INKEY$ and INPUT. The window stays open when the program's code has finished, so its output can be read; closing it ends the program.

    +

    The names are not reserved: a program that has its own variable, SUB or constant called Color, Cls, Draw and so on keeps using it.

    +

    Screen Modes

    +
      Mode        Size and colours                       Text
    +  ----        ----------------                       ----
    +  SCREEN 0    Text only, 16 colours                  80 columns, 25 rows (WIDTH 80, 50 for 50 rows)
    +  SCREEN 12   640 x 480 graphics, 16 colours         80 columns, 30 rows (WIDTH 80, 60 for 60 rows)
    +  SCREEN 13   320 x 200 graphics, 256 colours        40 columns, 25 rows
    +

    A program starts in SCREEN 0. SCREEN mode (and WIDTH) clears the screen and restores the mode's colours: the 16 standard colours, and in SCREEN 13 the standard 256-colour palette. Other mode numbers, and graphics statements in SCREEN 0, are error 5 (Illegal function call). SCREEN's colour switch and page arguments are accepted and ignored (there is one page), and PCOPY is not supported.

    +
    SCREEN mode
    +WIDTH [columns] [, rows]
    +

    FULLSCREEN

    +

    Shows the program's screen on the whole screen, or back in its window. Alt+Enter in the window does the same. While the program has the whole screen, Alt+Tab brings the desktop back (the program keeps running in its window) and switching back to the program returns it to the whole screen. Other programs keep running either way. When the computer has no suitable video mode, the screen stays in its window.

    +
    FULLSCREEN [ON | OFF]
    +FULLSCREEN expression      ' True: whole screen, False: window
    +

    Text: CLS, LOCATE, COLOR, CSRLIN, POS, SCREEN( )

    +
    CLS [n]
    +LOCATE [row] [, [column] [, cursor]]
    +COLOR [foreground] [, background]
    +row = CSRLIN
    +column = POS(0)
    +code = SCREEN(row, column [, colorflag])
    +
      +
    • CLS clears the screen and puts the cursor in the top-left corner. In a graphics mode with a VIEW set, CLS (or CLS 1) clears only the view; CLS 0 and CLS 2 clear everything.
    • +
    • LOCATE moves the cursor (rows and columns count from 1). cursor 1 shows the text cursor in SCREEN 0, 0 hides it (the default). QBasic's cursor size arguments are accepted and ignored.
    • +
    • COLOR in SCREEN 0 takes a foreground 0 to 31 (16 to 31 blink in QBasic; here they show steady) and a background 0 to 15. In SCREEN 12 and 13 it takes only a foreground (0 to 15, or 0 to 255 in SCREEN 13), which is also the default colour of the drawing statements.
    • +
    • CSRLIN is the cursor's row and POS(0) its column. Before the program has a screen, POS(0) is the output window's column.
    • +
    • SCREEN(row, column) is the character code shown there; with a colorflag of 1, its colour (in SCREEN 0, foreground + 16 * background).
    • +
    • PRINT scrolls the screen up at the bottom. CHR$(13) and CHR$(10) start a new line and CHR$(9) moves to the next multiple of 8 columns.
    • +
    +

    INKEY$

    +

    Returns the next key typed in the program's window without waiting: "" when there is none, one character for a character key, and CHR$(0) + CHR$(scan code) for other keys (arrows, function keys). Keys wait for INKEY$ in a queue of 64.

    +
    Do
    +    k$ = INKEY$
    +Loop While k$ = ""
    +If k$ = Chr$(0) + Chr$(72) Then Print "Up arrow"
    +

    INPUT and LINE INPUT on the Screen

    +

    On the program's screen INPUT shows its prompt at the cursor and reads what is typed there: Backspace takes back a character, Esc clears the line and Enter ends it. LINE INPUT reads a whole line, commas included, without adding "? " to the prompt. Without the program's screen LINE INPUT asks in a dialog box, like INPUT.

    +
    LINE INPUT ["prompt";] variable$
    +

    Graphics Coordinates

    +

    Points are (x, y) in pixels from the top-left corner. STEP(x, y) is relative to the last point drawn (the centre of the screen at first). VIEW makes a smaller part of the screen the drawing area (coordinates start at its corner, and drawing stays inside it); WINDOW gives it coordinates of your own. Colour numbers are attributes: 0 to 15 in SCREEN 12, 0 to 255 in SCREEN 13; anything else is error 5.

    +
    VIEW [[SCREEN] (x1, y1)-(x2, y2) [, [fill] [, border]]]
    +WINDOW [[SCREEN] (x1, y1)-(x2, y2)]
    +
      +
    • VIEW with coordinates of its own sets the view; fill fills it and border draws a frame around it. VIEW SCREEN keeps coordinates counting from the screen's corner. VIEW alone is the whole screen again.
    • +
    • WINDOW (x1, y1)-(x2, y2) maps the view to those coordinates with y going up; WINDOW SCREEN keeps y going down. WINDOW alone goes back to pixels.
    • +
    • VIEW PRINT and PMAP are not supported.
    • +
    +

    PSET, PRESET, POINT

    +
    PSET [STEP](x, y) [, color]
    +PRESET [STEP](x, y) [, color]
    +c = POINT(x, y)
    +v = POINT(n)
    +

    PSET draws a point in the given colour or the COLOR foreground, PRESET in the given colour or colour 0. POINT(x, y) is the colour at a point, or -1 outside the view. POINT(0) and POINT(1) are the last point in pixels, POINT(2) and POINT(3) in WINDOW coordinates.

    +

    LINE

    +
    LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, [color] [, [B | BF] [, style]]]
    +

    Draws a line, a box (B) or a filled box (BF). Without a start point the line starts at the last point. style is a 16-bit pattern: each set bit (from the top) draws a pixel, each clear one skips it.

    +
    LINE (10, 10)-(100, 50), 4, BF
    +LINE -STEP(20, 0), 15
    +LINE (0, 100)-(319, 100), 7, , &HF0F0
    +

    CIRCLE

    +
    CIRCLE [STEP](x, y), radius [, [color] [, [start] [, [end] [, aspect]]]]
    +

    Draws a circle, an ellipse or an arc. start and end are angles in radians (0 to 2 * pi, counterclockwise from the right); a negative angle also draws the radius to that end, so CIRCLE can draw pie slices. aspect is the height divided by the width; the default makes circles round on a 4:3 monitor (0.83 in SCREEN 13, 1 in SCREEN 12), so in SCREEN 13 a circle in the window looks slightly flat and a round one needs aspect 1.

    +
    CIRCLE (160, 100), 50, 14
    +CIRCLE (160, 100), 50, 4, -0.0001, -1.5708    ' a quarter pie slice
    +CIRCLE (160, 100), 80, 2, , , 0.5              ' an ellipse
    +

    PAINT

    +
    PAINT [STEP](x, y) [, [color] [, border]]
    +

    Fills the area around the point with color, up to pixels of the border colour (by default the fill colour itself). Tile patterns (a string instead of a colour) are not supported.

    +

    DRAW

    +

    Draws with a small command language. Letters may be upper or lower case; spaces and semicolons are ignored.

    +
      Command     Meaning
    +  -------     -------
    +  U n         Up n (without n: 1).
    +  D n         Down n.
    +  L n         Left n.
    +  R n         Right n.
    +  E n         Diagonally up and right.
    +  F n         Diagonally down and right.
    +  G n         Diagonally down and left.
    +  H n         Diagonally up and left.
    +  M x,y       Move to x, y; with a sign (M+10,-5) move by that much.
    +  B           Before a move: move without drawing.
    +  N           Before a move: draw, then come back to where the move started.
    +  C n         Colour of the lines that follow.
    +  S n         Scale: moves are n/4 pixels a unit (1 to 255, default 4).
    +  A n         Turn the directions by n * 90 degrees (0 to 3).
    +  TA n        Turn the directions by n degrees (-360 to 360).
    +  P c,b       PAINT at the current point with colour c up to border colour b.
    +

    The pen starts at the last point and stays where it ends. The colour, scale and turn stay from one DRAW to the next. QBasic's X command (draw a string variable) and the "=" form of numbers are not supported; build the string with + instead, such as DRAW "R" + STR$(n). A mistake in the string is error 5.

    +
    DRAW "BM100,100 C14 R50 D50 L50 U50"      ' a square
    +DRAW "BM160,100 TA45 C2 R40"              ' a line at 45 degrees
    +

    PALETTE

    +
    PALETTE [attribute, color]
    +PALETTE USING array[(first)]
    +

    Changes the colour an attribute shows. In SCREEN 12 and 13, color is blue * 65536 + green * 256 + red with each part 0 to 63; in SCREEN 0 it is an EGA colour number 0 to 63. PALETTE alone restores the mode's colours. PALETTE USING sets every attribute (16, or 256 in SCREEN 13) from a one-dimensional LONG array starting at element first (or its first element); -1 leaves that attribute as it is. On the whole screen a palette change shows at once, like on a VGA card; in the window, whatever is already drawn in that attribute changes colour too.

    +
    SCREEN 13
    +PALETTE 1, 63 * 65536        ' attribute 1 becomes bright blue
    +FOR i = 0 TO 63
    +    PALETTE 2, i             ' a red that fades in
    +    FRAMEWAIT 30
    +NEXT i
    +

    Example

    +
    SCREEN 13
    +CLS
    +COLOR 14
    +PRINT "Press a key"
    +CIRCLE (160, 100), 60, 4
    +PAINT (160, 100), 1, 4
    +LINE (0, 0)-(319, 199), 15, B
    +DRAW "BM150,90 C15 R20 D20 L20 U20"
    +Do
    +Loop While INKEY$ = ""
    +FULLSCREEN
    +Do
    +Loop While INKEY$ = ""
    +

    File I/O

    OPEN

    @@ -2229,30 +2834,58 @@ WEND

    String Functions

    -
      Function                          Returns   Description
    -  --------                          -------   -----------
    -  ASC(s$)                           Integer   ASCII code of the first character of s$
    -  CHR$(n)                           String    Character with ASCII code n
    -  FORMAT$(value, fmt$)              String    Formats a numeric value using a format string
    -  HEX$(n)                           String    Hexadecimal representation of n (uppercase, no leading &H)
    -  INSTR(s$, find$)                  Long      Position of find$ in s$ (1-based), 0 if not found
    -  INSTR(start, s$, find$)           Long      Search starting at position start (1-based)
    -  LCASE$(s$)                        String    Converts s$ to lowercase
    -  LEFT$(s$, n)                      String    Leftmost n characters of s$
    -  LEN(s$)                           Long      Length of s$ in characters
    -  LTRIM$(s$)                        String    Removes leading spaces from s$
    -  MID$(s$, start)                   String    Substring from start (1-based) to end of string
    -  MID$(s$, start, length)           String    Substring of length characters starting at start
    -  OCT$(n)                           String    Octal representation of n (no leading &O)
    -  RIGHT$(s$, n)                     String    Rightmost n characters of s$
    -  RTRIM$(s$)                        String    Removes trailing spaces from s$
    -  SPACE$(n)                         String    String of n spaces (n up to 65535)
    -  STR$(n)                           String    Converts number n to string (leading space for non-negative)
    -  STRING$(n, char)                  String    String of n copies of char (char can be an ASCII code or single-character string; n up to 65535)
    -  TRIM$(s$)                         String    Removes leading and trailing spaces from s$
    -  UCASE$(s$)                        String    Converts s$ to uppercase
    -  VAL(s$)                           Double    Converts string s$ to a numeric value; stops at first non-numeric character
    +
      Function                                                     Returns        Description
    +  --------                                                     -------        -----------
    +  ASC(s$)                                                      Integer        ASCII code of the first character of s$
    +  CHR$(n)                                                      String         Character with ASCII code n
    +  FILTER(arr, match$ [, include [, compare]])                  String array   The elements of arr that contain match$ (or, with include False, that do not)
    +  FORMAT$(value, fmt$)                                         String         Formats a number or a date using a format string
    +  HEX$(n)                                                      String         Hexadecimal representation of n (uppercase, no leading &H)
    +  INSTR(s$, find$)                                             Long           Position of find$ in s$ (1-based), 0 if not found
    +  INSTR(start, s$, find$)                                      Long           Search starting at position start (1-based)
    +  INSTRREV(s$, find$ [, start [, compare]])                    Long           Position of the last find$ in s$ that ends at or before start (default: the end), 0 if not found
    +  JOIN$(arr [, delim$])                                        String         The elements of a one-dimensional array joined with delim$ between them (default: a space)
    +  LCASE$(s$)                                                   String         Converts s$ to lowercase
    +  LEFT$(s$, n)                                                 String         Leftmost n characters of s$
    +  LEN(s$)                                                      Long           Length of s$ in characters
    +  LTRIM$(s$)                                                   String         Removes leading spaces from s$
    +  MID$(s$, start)                                              String         Substring from start (1-based) to end of string
    +  MID$(s$, start, length)                                      String         Substring of length characters starting at start
    +  OCT$(n)                                                      String         Octal representation of n (no leading &O)
    +  REPLACE$(s$, find$, repl$ [, start [, count [, compare]]])   String         s$ from position start (default 1) on, with the first count (default all) occurrences of find$ replaced by repl$
    +  RIGHT$(s$, n)                                                String         Rightmost n characters of s$
    +  RTRIM$(s$)                                                   String         Removes trailing spaces from s$
    +  SPACE$(n)                                                    String         String of n spaces (n up to 65535)
    +  SPLIT(s$ [, delim$ [, limit [, compare]]])                   String array   The pieces of s$ between occurrences of delim$ (default: a space), at most limit of them (default: all)
    +  STRCOMP(a$, b$ [, compare])                                  Integer        -1 if a$ sorts before b$, 0 if they are equal, 1 if a$ sorts after b$
    +  STR$(n)                                                      String         Converts number n to string (leading space for non-negative)
    +  STRING$(n, char)                                             String         String of n copies of char (char can be an ASCII code or single-character string; n up to 65535)
    +  STRREVERSE$(s$)                                              String         s$ with its characters in reverse order
    +  TRIM$(s$)                                                    String         Removes leading and trailing spaces from s$
    +  UCASE$(s$)                                                   String         Converts s$ to uppercase
    +  VAL(s$)                                                      Double         Converts string s$ to a numeric value; stops at first non-numeric character

    A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$ or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call). INSTR follows OPTION COMPARE TEXT (a case-insensitive search), and an INSTR start position below 1 raises error 5.

    +

    REPLACE$, JOIN$ and STRREVERSE$ may also be written without the $. Items in square brackets may be left out.

    +

    Comparing Text

    +

    The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT and STRCOMP chooses how text is matched: vbBinaryCompare (0) matches exactly, vbTextCompare (1) ignores upper and lower case, and vbUseCompareOption (-1, the default) follows OPTION COMPARE. Any other value raises error 5.

    +
    Print StrComp("apple", "Apple")                  ' 1 ("a" sorts after "A")
    +Print StrComp("apple", "Apple", vbTextCompare)   ' 0
    +Print Replace$("Red red RED", "red", "blue", 1, -1, vbTextCompare)   ' blue blue blue
    +Print InStrRev("C:\DVX\APPS\EDIT.APP", "\")     ' 12
    +

    REPLACE$ Details

    +

    REPLACE$ returns the text from position start on, so REPLACE$("abcabc", "b", "x", 3) is "caxc" -- the characters before start are left out, as in Visual Basic. A count of -1 replaces every occurrence. An empty find$ returns the text unchanged. A start below 1 or a count below -1 raises error 5.

    +

    SPLIT, JOIN$ and FILTER

    +

    SPLIT returns a string array numbered from 0. Assign it to a dynamic array (one declared with empty parentheses) and use UBOUND to find the last piece. The last piece keeps the rest of the text when limit stops the split. Splitting an empty string, or a limit of 0, gives an array with no elements: its UBOUND is -1. An empty delim$ gives the whole string as the only piece.

    +
    Dim parts() As String
    +Dim i As Integer
    +parts = Split("red,green,,blue", ",")
    +For i = 0 To UBound(parts)
    +    Print i; "[" & parts(i) & "]"     ' the empty third piece is kept
    +Next i
    +Print Join$(parts, " / ")             ' red / green /  / blue
    +

    JOIN$ works on any one-dimensional array; numbers are converted as STR$ would, without the leading space. FILTER returns a new string array holding the elements that contain match$ (include True, the default) or that do not contain it (include False), or an array with no elements when nothing matches. Passing JOIN$ or FILTER something that is not a one-dimensional array raises error 13 (Type mismatch).

    +

    INSTRREV Details

    +

    INSTRREV searches backward: INSTRREV("abcabc", "bc") is 5 and INSTRREV("abcabc", "bc", 4) is 2. A start of -1 (the default) searches from the end. It returns 0 when s$ is empty or start is past the end of s$, and start when find$ is empty. A start of 0 or below -1 raises error 5.

    FORMAT$

    FORMAT$ formats a numeric value using a BASIC-style format string. The format characters are the same as the ones used by PRINT USING.

    s$ = FORMAT$(value, fmt$)
    @@ -2262,7 +2895,48 @@ Print Format$(42, "00000") ' "00042" Print Format$(1234.5, "#,##0.00") ' "1,234.50" Print Format$(5, "#,##0.00") ' " 5.00" Print Format$(0.5, "PERCENT") ' "50%" -

    The whole format string is one numeric field. The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation) and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.

    +

    The whole format string is one numeric field (for a number). The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation) and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.

    +

    FORMAT (without the $) is the same function.

    +

    FORMAT$ with Dates

    +

    When the format string holds a date or time code, or is one of the named date formats, FORMAT$ formats its value as a date. The value may be a Date, a number (taken as a date's number of days) or a string that holds a date (anything else raises error 13). A Date with an empty format string gives the usual m/d/yyyy h:mm:ss AM form. The codes may be written in upper or lower case.

    +
      Code          Result
    +  ----          ------
    +  d / dd        Day of the month (dd: two digits)
    +  ddd / dddd    Weekday name, abbreviated (Mon) or full (Monday)
    +  ddddd         Short date (m/d/yyyy)
    +  dddddd        Long date (dddd, mmmm d, yyyy)
    +  w             Day of the week (1 = Sunday)
    +  ww            Week of the year (the week holding January 1 is week 1)
    +  m / mm        Month number (mm: two digits); the minute when it follows an hour or comes before a second
    +  mmm / mmmm    Month name, abbreviated (Jan) or full (January)
    +  q             Quarter (1-4)
    +  y             Day of the year (1-366)
    +  yy / yyyy     Year as two or four digits
    +  h / hh        Hour (hh: two digits); 1-12 when the format has AM/PM, else 0-23
    +  n / nn        Minute
    +  s / ss        Second
    +  ttttt         Long time (h:mm:ss AM/PM)
    +  c             The usual date and time form
    +  AM/PM am/pm   AM or PM in the case written; A/P and a/p give one letter; AMPM gives AM or PM
    +  "text"        Text in double quotes is copied as it is
    +  \x            The character after a backslash is copied as it is
    +

    Any other character, such as / : - , or a space, is copied as it is. These names give whole formats:

    +
      Name           Format                 Example
    +  ----           ------                 -------
    +  General Date   (the usual form)       1/5/2000 3:04:05 PM
    +  Long Date      dddd, mmmm d, yyyy     Wednesday, January 5, 2000
    +  Medium Date    dd-mmm-yy              05-Jan-00
    +  Short Date     m/d/yyyy               1/5/2000
    +  Long Time      h:mm:ss AM/PM          3:04:05 PM
    +  Medium Time    hh:mm AM/PM            03:04 PM
    +  Short Time     hh:mm                  15:04
    +
    Dim d As Date
    +d = #1/5/2000 3:04:05 PM#
    +Print Format$(d, "yyyy-mm-dd hh:nn:ss")   ' 2000-01-05 15:04:05
    +Print Format$(d, "dddd, mmmm d")          ' Wednesday, January 5
    +Print Format$(d, "h:mm AM/PM")            ' 3:04 PM
    +Print Format$(d, "Long Date")             ' Wednesday, January 5, 2000
    +Print Format$(Now, "Short Time")          ' the time now, e.g. 09:41

    MID$ Assignment

    MID$ can also be used on the left side of an assignment to replace a portion of a string without changing its length:

    Mid$(s$, start [, length]) = replacement$
    @@ -2310,11 +2984,86 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
      Function   Returns   Description
       --------   -------   -----------
       CBOOL(n)   Boolean   Returns True (-1) if n is nonzero or a non-empty string; False (0) otherwise
    -  CDBL(n)    Double    Converts n to Double
    +  CDATE(v)   Date      Converts a number (days since 12/30/1899) or date text to a Date; error 13 for text that is not a date, error 6 outside 1/1/100 to 12/31/9999
    +  CDBL(n)    Double    Converts n to Double (a Date gives its number of days)
       CINT(n)    Integer   Converts n to Integer; halves round to the nearest even number (2.5 -> 2, 3.5 -> 4); error 6 (Overflow) outside -32768 to 32767
       CLNG(n)    Long      Converts n to Long with the same rounding; error 6 (Overflow) outside the Long range
       CSNG(n)    Single    Converts n to Single
    -  CSTR(n)    String    Converts n to its String representation
    + CSTR(n) String Converts n to its String representation (a Date gives m/d/yyyy h:mm:ss AM) + CVAR(v) Variant Returns v unchanged as a Variant +

    Passing Null to any of these except CVAR raises error 94 (Invalid use of Null). VARTYPE, TYPENAME and the IS functions (ISNULL, ISEMPTY, ISNUMERIC, ...) are described under Variants.

    +

    See also: Variants

    +
    +
    +

    Date and Time Functions

    +

    These functions work with Date values (see Data Types). Wherever a date is expected, a Date, a number of days since 12/30/1899 or a string holding a date may be given; text that is not a date raises error 13 (Type mismatch).

    +
      Function                                                 Returns   Description
    +  --------                                                 -------   -----------
    +  NOW                                                      Date      The current date and time
    +  DATE                                                     Date      Today's date (at midnight)
    +  TIME                                                     Date      The current time of day (on 12/30/1899)
    +  DATESERIAL(year, month, day)                             Date      The date with those parts
    +  TIMESERIAL(hour, minute, second)                         Date      The time with those parts
    +  DATEVALUE(d)                                             Date      The date part of d (its time dropped)
    +  TIMEVALUE(d)                                             Date      The time part of d (on 12/30/1899)
    +  YEAR(d)                                                  Integer   Year (100-9999)
    +  MONTH(d)                                                 Integer   Month (1-12)
    +  DAY(d)                                                   Integer   Day of the month (1-31)
    +  WEEKDAY(d [, firstDay])                                  Integer   Day of the week, 1 for firstDay (default vbSunday)
    +  HOUR(d)                                                  Integer   Hour (0-23)
    +  MINUTE(d)                                                Integer   Minute (0-59)
    +  SECOND(d)                                                Integer   Second (0-59)
    +  DATEADD(interval$, n, d)                                 Date      d moved by n intervals (n may be negative)
    +  DATEDIFF(interval$, d1, d2 [, firstDay [, firstWeek]])   Long      Intervals from d1 to d2 (negative when d2 is earlier)
    +  DATEPART(interval$, d [, firstDay [, firstWeek]])        Integer   One part of d
    +  ISDATE(v)                                                Boolean   True when v is a Date or a string holding a valid date
    +  MONTHNAME(month [, abbreviate])                          String    January ... December (Jan ... Dec when abbreviate is True)
    +  WEEKDAYNAME(n [, abbreviate [, firstDay]])               String    Name of day n of the week counted from firstDay (default vbSunday)
    +

    NOW, DATE and TIME need no parentheses. DATE$ and TIME$ still return the date and time as text.

    +

    Intervals

    +

    DATEADD, DATEDIFF and DATEPART take the interval as a string, in upper or lower case:

    +
      Interval   Meaning
    +  --------   -------
    +  "yyyy"     Year
    +  "q"        Quarter
    +  "m"        Month
    +  "y"        Day of the year
    +  "d"        Day
    +  "w"        Weekday
    +  "ww"       Week
    +  "h"        Hour
    +  "n"        Minute
    +  "s"        Second
    +

    Any other interval raises error 5 (Illegal function call).

    +

    DATESERIAL and TIMESERIAL

    +

    A year from 0 to 29 means 2000 to 2029 and one from 30 to 99 means 1930 to 1999. Months, days, hours, minutes and seconds outside their usual range carry into the next or previous unit, so DATESERIAL(2000, 13, 1) is 1/1/2001, DATESERIAL(2000, 3, 0) is the last day of February 2000 and TIMESERIAL(0, 90, 0) is 1:30 AM. Fractional arguments are rounded. A result before 1/1/100 or after 12/31/9999 raises error 5.

    +
    Dim lastDay As Date
    +lastDay = DateSerial(Year(Date), Month(Date) + 1, 0)    ' last day of this month
    +Print Day(lastDay)
    +

    DATEADD

    +

    Adding months, quarters or years keeps the day of the month where it can; when the new month is shorter the day becomes its last day, so DATEADD("m", 1, #1/31/2000#) is 2/29/2000. The time of day is kept. "y", "d" and "w" all add days and "ww" adds weeks. A fractional n drops its fraction. A result outside the Date range raises error 5.

    +
    Print DateAdd("m", 1, #1/31/2001#)      ' 2/28/2001
    +Print DateAdd("h", -2, #1/1/2000#)      ' 12/31/1999 10:00:00 PM
    +

    DATEDIFF

    +

    DATEDIFF counts how many interval boundaries lie between d1 and d2: DATEDIFF("yyyy", #12/31/1999#, #1/1/2000#) is 1 although the dates are a day apart, and DATEDIFF("d", ...) counts midnights, whatever the times of day. "w" counts whole weeks (7 days) and "ww" counts calendar weeks -- the number of firstDay days (Sundays by default) after d1 up to and including d2. A result too large for a Long raises error 6 (Overflow).

    +
    Print DateDiff("d", #1/1/2000#, #3/1/2000#)     ' 60
    +Print DateDiff("ww", #1/1/2000#, #1/2/2000#)    ' 1 (1/2/2000 is a Sunday)
    +Print DateDiff("w", #1/1/2000#, #1/2/2000#)     ' 0
    +

    DATEPART and Weeks of the Year

    +

    DATEPART returns the part of d the interval names: "w" is the day of the week (1 for firstDay) and "ww" the week of the year. firstDay is one of vbSunday (1, the default) to vbSaturday (7), or vbUseSystemDayOfWeek (0, meaning Sunday). firstWeek chooses which week is week 1:

    +
      Constant          Value   Week 1 is
    +  --------          -----   ---------
    +  vbUseSystem       0       Same as vbFirstJan1
    +  vbFirstJan1       1       The week holding January 1 (the default)
    +  vbFirstFourDays   2       The first week with at least four days in the new year
    +  vbFirstFullWeek   3       The first full week of the year
    +

    Days before week 1 belong to the last week of the previous year. A firstDay or firstWeek outside these values raises error 5.

    +
    Print DatePart("q", #5/15/2000#)                               ' 2
    +Print DatePart("ww", #1/1/2000#, vbSunday, vbFirstFourDays)    ' 52
    +

    ISDATE, MONTHNAME and WEEKDAYNAME

    +

    ISDATE is True for a Date and for a string that holds a valid date or time; numbers are not dates to ISDATE. MONTHNAME with a month outside 1 to 12, and WEEKDAYNAME with n outside 1 to 7, raise error 5.

    +
    If IsDate(Text1.Text) Then due = CDate(Text1.Text)
    +Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True)

    File I/O Functions

    @@ -2337,16 +3086,23 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background

    Miscellaneous Functions

    -
      Function          Returns   Description
    -  --------          -------   -----------
    -  DATE$             String    Current date as "MM-DD-YYYY"
    -  TIME$             String    Current time as "HH:MM:SS" (24-hour)
    -  TIMER             Double    Seconds since midnight
    -  ENVIRON$(name$)   String    Value of the environment variable; empty string if not set
    -  COMMAND$          String    The arguments the program was started with; empty string if none
    -  COMMAND$(n)       String    The nth argument (1 = first) without its quotes; empty string when there are fewer; COMMAND$(0) is the same as COMMAND$
    -  ERR               Long      Current runtime error number (0 if no error)
    -  SHELL(cmd$)       Integer   Exit code of the command (also usable as a statement, discarding the code)
    +
      Function               Returns   Description
    +  --------               -------   -----------
    +  DATE$                  String    Current date as "MM-DD-YYYY"
    +  TIME$                  String    Current time as "HH:MM:SS" (24-hour)
    +  TIMER                  Double    Seconds since midnight
    +  ENVIRON$(name$)        String    Value of the environment variable; empty string if not set
    +  COMMAND$               String    The arguments the program was started with; empty string if none
    +  COMMAND$(n)            String    The nth argument (1 = first) without its quotes; empty string when there are fewer; COMMAND$(0) is the same as COMMAND$
    +  ERR                    Long      Current runtime error number (0 if no error)
    +  SHELL(cmd$)            Integer   Exit code of the command (also usable as a statement, discarding the code)
    +  IIF(cond, a, b)        (any)     a when cond is True, b when it is False
    +  CHOOSE(n, a, b, ...)   (any)     The nth value of the list (1 = a), or Null when there is no nth value; up to 31 values
    +

    DATE$ and TIME$ return text; the Date and Time Functions return Date values.

    +

    IIF and CHOOSE

    +

    IIF and CHOOSE evaluate every argument before picking one, as in Visual Basic, so a function called in an argument runs even when its value is not chosen. A fractional n in CHOOSE drops its fraction (2.9 picks the second value). An n below 1 or past the end of the list gives Null, as in Visual Basic (test it with ISNULL; see Variants).

    +
    Print IIf(score >= 50, "Pass", "Fail")
    +Print Choose(Weekday(Date), "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")

    COMMAND$

    COMMAND$ returns the program's arguments exactly as they were given, as in Visual Basic (QBasic turned them into capitals; DVX keeps them as they are). Arguments are separated by blanks; an argument with blanks in it is written in double quotes. COMMAND$(n) picks out one argument with its quotes removed, so a program that opens the document it was started with only needs:

    IF COMMAND$(1) <> "" THEN
    @@ -2387,13 +3143,15 @@ Form2.List1.Clear
    List1.AddItem "New entry"
     List1.Clear

    Me Keyword

    -

    Me refers to the current form -- that is, the form whose event handler is running. Use it to access the form's own properties, controls, and methods from within event handlers, especially when the form's name may change or when the same code runs from multiple forms.

    +

    Me refers to the current form -- that is, the form whose event handler is running. Use it to access the form's own properties, controls, and methods from within event handlers, especially when the form's name may change or when the same code runs from multiple forms. In the code of a class module, Me is instead the object whose procedure is running (see Objects and Class Modules).

    Me.Caption = "Updated Title"
     Me.Text1.Text = ""
     Me.Hide

    Nothing

    -

    Nothing is a null object reference. Assign it to an object-typed variable to indicate "no object".

    -
    Set myCtrl = Nothing
    +

    Nothing is a null object reference. Assign it with SET to an object variable to indicate "no object", and test for it with Is. An object variable that has not been Set holds Nothing, and using a property or method of Nothing raises error 91 (Object variable not set).

    +
    Set myCtrl = Nothing
    +If myCtrl Is Nothing Then Print "No control chosen"
    +

    Setting the last variable that refers to a class instance or Collection to Nothing frees the object; see Objects and Class Modules.

    Control Arrays

    Multiple controls can share a name with unique indices. Access individual controls with parenthesized indices:

    Option1(0).Value = True
    @@ -2425,8 +3183,8 @@ End With

    RemoveControl

    REMOVECONTROL formRef, ctrlName$

    Removes a control from a form. A later reference to the removed control raises a runtime error (unknown control), as does a reference to a control of a form that has been unloaded.

    -
    Dim frm As Long
    -Dim lbl As Long
    +
    Dim frm As Form
    +Dim lbl As Control
     
     Set frm = CreateForm("MyDlg", 300, 200)
     frm.Caption = "Built in code"
    @@ -2434,7 +3192,7 @@ frm.Caption = "Built in code"
     Set lbl = CreateControl(frm, "Label", "StatusLabel")
     StatusLabel.Caption = "Hello, world!"
     
    -Dim btn As Long
    +Dim btn As Control
     Set btn = CreateControl(frm, "CommandButton", "BtnClose")
     BtnClose.Caption = "Close"
     SetEvent btn, "Click", "OnCloseClick"
    @@ -2504,6 +3262,181 @@ End Sub
    Form_Deactivate Form lost focus Timer1_Timer Timer control interval elapsed (Timer control's default event)
    +
    +

    Objects and Class Modules

    +

    A class module lets you define your own kind of object. The class describes what every object of that kind knows (its member variables and properties) and what it can do (its methods). Each object made from the class -- an instance -- has its own copy of the member variables, so you can have as many independent objects as you need, much as you can put several TextBoxes on a form.

    +

    A First Class

    +

    This class module, saved as Account.cls, describes a bank account:

    +
    VERSION DVX 1.00 CLASS
    +Attribute VB_Name = "Account"
    +
    +Option Explicit
    +
    +Public Owner As String
    +Private mBalance As Double
    +
    +Public Sub Deposit(ByVal amount As Double)
    +    mBalance = mBalance + amount
    +End Sub
    +
    +Public Property Get Balance() As Double
    +    Balance = mBalance
    +End Property
    +
    +Private Sub Class_Initialize()
    +    Print "Account created"
    +End Sub
    +
    +Private Sub Class_Terminate()
    +    Print "Account for "; Owner; " closed"
    +End Sub
    +

    Any other code in the project can then make and use Account objects:

    +
    Dim acct As Account
    +Set acct = New Account          ' prints "Account created"
    +acct.Owner = "Scott"
    +acct.Deposit 100
    +acct.Deposit 50
    +Print acct.Owner & ": " & acct.Balance   ' Scott: 150
    +Set acct = Nothing              ' prints "Account for Scott closed"
    +

    Owner is Public, so code outside the class can read and change it. mBalance is Private: only the class's own code can touch it, and the outside world reads it through the Balance property, which has no way to change it.

    +

    Class Module Files (.cls)

    +

    Each class module is kept in its own .cls file, listed in the project's Classes group (see the IDE Guide). The file can begin with a few header lines, as a Visual Basic class file does; the IDE writes them for a new class module:

    +
    VERSION DVX 1.00 CLASS
    +Attribute VB_Name = "Account"
    +
    +Option Explicit
    +

    The header is everything before the first line of code: blank lines, a VERSION line, a BEGIN ... END block and Attribute VB_... lines, in that order. All of it is optional, and a Visual Basic .cls file can be added as it is. The class's name comes from the Attribute VB_Name line; without one, the name is the file's name without .cls (ACCOUNT.CLS makes a class named ACCOUNT). The class name is the type name used in DIM ... AS, NEW, TypeOf and TYPENAME.

    +

    What a Class Contains

    +

    Outside its procedures, a class module may hold only declarations: member variables, CONSTs, OPTION and DEFtype statements, and Attribute lines. Statements that do something belong in a procedure, usually Class_Initialize. The members are:

    +
      Member                             Written as                                 Used from outside as
    +  ------                             ----------                                 --------------------
    +  Public member variable             Public name As type                        obj.name (read and assign)
    +  Private member variable            Private name As type, or Dim name As type  not visible outside
    +  Method                             Sub or Function (Public unless Private)    obj.Method args, x = obj.Func(args)
    +  Read property                      Property Get name(...) As type             x = obj.name
    +  Write property                     Property Let name(..., value)              obj.name = x
    +  Object property                    Property Set name(..., value)              Set obj.name = someObject
    +

    A Sub, Function or Property is Public unless it is marked Private; Private procedures can be called only by the class's own code. Inside the class's procedures the member variables are used by name, with no SHARED needed, and each call works with the variables of the object it was called for. The class's code can also use the program's DIM SHARED (or PUBLIC) module variables, procedures and constants.

    +

    Properties

    +

    A property looks like a variable from outside, but reading or assigning it runs a procedure, so the class can check or calculate the value:

    +
    Private mC As Double
    +
    +Public Property Get Celsius() As Double
    +    Celsius = mC
    +End Property
    +
    +Public Property Let Celsius(ByVal v As Double)
    +    mC = v
    +End Property
    +
    +Public Property Get Fahrenheit() As Double
    +    Fahrenheit = mC * 9 / 5 + 32
    +End Property
    +
    +Public Property Let Fahrenheit(ByVal v As Double)
    +    mC = (v - 32) * 5 / 9
    +End Property
    +
    Dim t As New Thermometer
    +t.Celsius = 100
    +Print t.Fahrenheit      ' 212
    +

    Property Get returns the value: assign it to the property name, as in a FUNCTION. Property Let receives the value being assigned as its last parameter; Property Set does the same for an object assigned with SET. A property can take arguments of its own before the value, such as Property Get Cell(r, c) and Property Let Cell(r, c, v), used as obj.Cell(1, 2) = 7. A property with only a Property Get is read-only: assigning it raises error 438. EXIT PROPERTY leaves a property procedure early.

    +

    Creating and Releasing Objects

    +

    An object variable starts as Nothing. NEW makes an object:

    +
    Dim a As Account               ' Nothing so far
    +Set a = New Account            ' now an Account
    +
    +Dim b As New Account           ' an Account is made right here, at the DIM
    +

    When an object is made, its Class_Initialize runs; when it is freed, its Class_Terminate runs. Both are Subs without parameters, usually Private, and both are optional.

    +

    An object stays alive as long as something refers to it: a variable, an array element, a TYPE field, a Collection, a Variant, or a member variable of another object. Each SET to another variable adds a reference; nothing is copied. When the last reference goes away -- SET to Nothing or to another object, a local variable at the end of its SUB or FUNCTION, removal from a Collection, ERASE -- the object is freed and its Class_Terminate runs right then, just after the statement that let go of it:

    +
    Dim a As New Account
    +Dim b As Account
    +Set b = a                ' two references to one account
    +Set a = Nothing          ' still alive: b refers to it
    +Set b = Nothing          ' last reference gone: Class_Terminate runs now
    +
    Warning: Objects that refer to each other, directly or around a longer loop, keep each other alive even after the program's own variables let them go. Such objects stay in memory until the program ends, and their Class_Terminate never runs. Break the loop yourself (for example Set a.Partner = Nothing) before letting go of the objects.
    +

    When the program ends (END, or the last form closes), every object still alive is freed without running Class_Terminate, as with END in Visual Basic. Work that must always happen, such as saving a file, belongs in a method the program calls itself, not only in Class_Terminate.

    +

    Me

    +

    Inside a class's procedures, Me is the object the procedure was called for. Use it to pass the object to other code, or to go through the object's own properties (Me.Balance runs Property Get Balance):

    +
    Public Sub Move(ByVal dx As Double)
    +    Me.X = Me.X + dx     ' uses Property Get X and Property Let X
    +End Sub
    +

    Using Objects

    +

    Members are found by name when the program runs (late binding): obj.Deposit 5 looks for a Public member named Deposit in whatever object obj holds at that moment. A misspelled or Private member is therefore not reported when the program is compiled but raises error 438 when the line runs, and the wrong number of arguments raises error 450. Using a member of Nothing raises error 91.

    +

    An object can be kept in a variable declared as its class name, As Object, or As Variant, in an array, in a TYPE field, in a Collection, or in another object's member variable. It can be passed to a SUB or FUNCTION and returned from a FUNCTION (assign the result with SET inside the function, Set MakeAccount = acct). TYPENAME gives an object's class name, and TypeOf obj Is ClassName tests it; Is tells whether two variables refer to the same object.

    +

    Arguments of a class's procedures follow the usual rules when the object variable is declared as the class name: a parameter without BYVAL gets the caller's variable, so the procedure can change it. When the object is reached through a variable declared As Object or As Variant, every argument is passed by value. An argument left out in the middle of a call (obj.Report a, , c) arrives as Empty; OPTIONAL parameters left out at the end get their type's usual empty value (0, "" and so on).

    +

    An object parameter is itself passed like any other parameter: without BYVAL, SET on the parameter changes the caller's variable (a SUB can make a new object for the caller); with BYVAL, the procedure gets its own reference to the same object, and SET on it does not affect the caller.

    +

    Default Member

    +

    A class can name one property or method as its default member, used when the object is written with parentheses directly after it. Put this line in the class, inside the procedure (as Visual Basic writes it) or on its own at the top of the class:

    +
    Attribute Item.VB_UserMemId = 0
    +

    With Item as the default member, obj(3) means obj.Item(3), and obj(3) = x calls Property Let Item. The object name on its own, without parentheses, still means the object itself. Collection's default member is Item, which is why c(1) and c("key") work.

    +

    Errors in a Class

    +

    A class reports a problem with ERROR, normally numbered from vbObjectError so that its numbers cannot be mistaken for the built-in ones. The error is handled by the ON ERROR handler of the code that called the class, as with any procedure:

    +
    ' in the class
    +Public Sub Withdraw(ByVal amount As Double)
    +    If amount > mBalance Then Error vbObjectError + 1
    +    mBalance = mBalance - amount
    +End Sub
    +
    +' in the program
    +On Error GoTo NoMoney
    +acct.Withdraw 500
    +Exit Sub
    +NoMoney:
    +    If Err = vbObjectError + 1 Then MsgBox "Not enough money"
    +    Resume Next
    +

    Rules and Limits

    +
      +
    • A member's name cannot be a reserved word. Names such as Name, Show, Hide, Load, Print, Open, Close, Line, Input, Get, Put, Type, Error and Timer are keywords in DVX BASIC and cannot be used; Text, Caption, Value, Count, Item and Add are fine.
    • +
    • SHARED and DIM SHARED cannot be used in a class module; a class's code reaches the program's module variables that are declared DIM SHARED or PUBLIC.
    • +
    • A STATIC variable in a class procedure is shared by all objects of the class, not kept per object. Use a member variable for a value each object keeps.
    • +
    • Class_Initialize and Class_Terminate take no parameters.
    • +
    • A class module cannot hold statements outside its procedures; the compiler reports "Only declarations are allowed outside procedures in a class module".
    • +
    • NEW, PROPERTY, PUBLIC, PRIVATE and TYPEOF are reserved words. VARIANT, OBJECT, COLLECTION, EMPTY, NULL, EACH and IN are not.
    • +
    +

    See also: Collection Object

    +

    See also: Variants

    +

    See also: SET and PUBLIC / PRIVATE

    +
    +
    +

    Collection Object

    +

    A Collection is a ready-made object that holds a list of items of any kind -- numbers, text, objects -- in order. Items can be looked up by position or by a text key, added anywhere in the list and removed, and FOR EACH walks through them. The list grows as needed; there is no size to declare.

    +
    Dim fruit As New Collection
    +fruit.Add "Apple", "a"
    +fruit.Add "Cherry", "c"
    +fruit.Add "Banana", "b", "c"    ' before the item with key "c"
    +
    +Print fruit.Count               ' 3
    +Print fruit(1)                  ' Apple
    +Print fruit("B")                ' Banana (keys ignore case)
    +
    +Dim f As Variant
    +For Each f In fruit
    +    Print f                     ' Apple, Banana, Cherry
    +Next
    +
    +fruit.Remove "a"
    +fruit.Remove 1                  ' removes Banana, now the first item
    +

    Members

    +
      Member                                  Description
    +  ------                                  -----------
    +  Add item [, key] [, before] [, after]   Adds item at the end of the list, or before or after the item given by position or key. key, when given, is text that names the item.
    +  Remove indexOrKey                       Removes the item at that position (1 = first) or with that key.
    +  Item(indexOrKey)                        The item at that position or with that key. Item is the default member: c(2) is c.Item(2) and c("key") is c.Item("key").
    +  Count                                   The number of items.
    +

    Positions start at 1 and change as items are added and removed. A key must be text, is compared without regard to upper and lower case, and must be unique in the Collection. Give either before or after, not both; leave out the arguments you do not use with commas (c.Add item, , 1 puts item first). Items are stored as they are: an object added to a Collection stays alive while the Collection holds it.

    +

    Make a Collection with DIM c AS NEW COLLECTION or SET c = NEW COLLECTION. Like any object, it is freed when the last reference to it goes.

    +

    Errors

    +
      Error   When
    +  -----   ----
    +  5       A key that is not in the Collection (Item, Remove, before or after), or Add given both before and after
    +  9       A position that is not in the Collection
    +  13      A key that is not text
    +  438     A member other than Add, Remove, Item and Count
    +  450     The wrong number of arguments
    +  457     Add with a key that is already in use
    +

    See also: Objects and Class Modules

    +

    See also: FOR EACH...NEXT

    +

    SQL Functions

    DVX BASIC includes SQLite database support through the sql.bas include library. Add sql.bas to your project to gain access to these functions. All functions use database handles and result-set handles (Long integers) returned by SQLOpen and SQLQuery. Numeric parameters and results are Long, so row counts and integer column values keep their full 32-bit range.

    @@ -2592,6 +3525,19 @@ SQLClose db
    dbPath$ = App.Data & "\myapp.db" Print "Running from: " & App.Path
    +
    +

    Clipboard Object

    +

    The Clipboard object shares text with other programs and with the copy and paste of text boxes, as in Visual Basic. The DVX clipboard holds text only, so the format arguments accept vbCFText (1): any other format is never present and GetText returns "" for it.

    +
      Method                            Returns   Description
    +  ------                            -------   -----------
    +  Clipboard.Clear                   (none)    Empty the clipboard.
    +  Clipboard.GetFormat(format)       Boolean   True when the clipboard holds data of that format (text for vbCFText).
    +  Clipboard.GetText([format])       String    The clipboard text, or "" when it is empty.
    +  Clipboard.SetText text [, format] (none)    Put text on the clipboard, replacing what was there. A format other than vbCFText is an error.
    +
    Clipboard.SetText Text1.Text
    +If Clipboard.GetFormat(vbCFText) Then Text2.Text = Clipboard.GetText()
    +

    A control or form of your own named Clipboard takes the name's place.

    +

    INI Functions

    DVX BASIC provides built-in functions for reading and writing standard INI configuration files (the same text format used by DVX system configuration).

    @@ -2647,6 +3593,68 @@ IniWrite App.Config & "\app.ini", "Display", "FontS vbSystem 4 System file vbDirectory 16 Entry is a directory vbArchive 32 File has been modified since last backup +

    Compare Constants

    +

    The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT and STRCOMP.

    +
      Constant             Value   Description
    +  --------             -----   -----------
    +  vbUseCompareOption   -1      Follow OPTION COMPARE (the default)
    +  vbBinaryCompare      0       Exact match
    +  vbTextCompare        1       Ignore upper and lower case
    +

    Day and Week Constants

    +

    The firstDay and firstWeek arguments of the date functions, and WEEKDAY results.

    +
      Constant               Value   Description
    +  --------               -----   -----------
    +  vbUseSystemDayOfWeek   0       Weeks start on Sunday
    +  vbSunday               1       Sunday
    +  vbMonday               2       Monday
    +  vbTuesday              3       Tuesday
    +  vbWednesday            4       Wednesday
    +  vbThursday             5       Thursday
    +  vbFriday               6       Friday
    +  vbSaturday             7       Saturday
    +  vbUseSystem            0       Week 1 holds January 1
    +  vbFirstJan1            1       Week 1 holds January 1
    +  vbFirstFourDays        2       Week 1 is the first with four days in the new year
    +  vbFirstFullWeek        3       Week 1 is the first full week
    +

    Drag and Drop Constants

    +

    The DragMode property, the Drag method and the State argument of DragOver.

    +
      Constant      Value   Description
    +  --------      -----   -----------
    +  vbManual      0       DragMode: the program starts drags with Drag
    +  vbAutomatic   1       DragMode: pressing the control starts a drag
    +  vbBeginDrag   1       Drag: start dragging (the default)
    +  vbEndDrag     2       Drag: drop where the mouse is (Drag 0 cancels)
    +  vbEnter       0       DragOver: the drag came onto the object
    +  vbLeave       1       DragOver: the drag went off the object
    +  vbOver        2       DragOver: the drag moved over the object
    +

    Clipboard Constants

    +
      Constant   Value   Description
    +  --------   -----   -----------
    +  vbCFText   1       Text, the format the DVX clipboard holds
    +

    VarType Constants

    +

    The values VARTYPE returns (see Variants). For an array, VARTYPE adds vbArray to the type of its elements.

    +
      Constant            Value   Kind of value
    +  --------            -----   -------------
    +  vbEmpty             0       Empty (nothing stored yet)
    +  vbNull              1       Null
    +  vbInteger           2       Integer
    +  vbLong              3       Long
    +  vbSingle            4       Single
    +  vbDouble            5       Double
    +  vbCurrency          6       Currency (Visual Basic only; no DVX value has this type)
    +  vbDate              7       Date
    +  vbString            8       String
    +  vbObject            9       An object or Nothing
    +  vbError             10      Error value (Visual Basic only; no DVX value has this type)
    +  vbBoolean           11      Boolean
    +  vbVariant           12      Variant (only in vbArray + vbVariant, an array of Variants)
    +  vbDataObject        13      Data object (Visual Basic only; no DVX value has this type)
    +  vbUserDefinedType   36      A TYPE variable
    +  vbArray             8192    Added to the element type for an array
    +

    Error Constants

    +
      Constant        Value         Description
    +  --------        -----         -----------
    +  vbObjectError   -2147221504   Base for your own error numbers, as in Visual Basic: a class module raises ERROR vbObjectError + n, and the handler recovers n with ERR - vbObjectError

    Boolean Constants

      Constant   Value   Description
       --------   -----   -----------
    @@ -2658,7 +3666,7 @@ IniWrite App.Config & "\app.ini", "Display", "FontS
     

    DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory. Each file is a small library of DECLARE LIBRARY wrappers around native DVX runtime functions. Add an include file to your project (the IDE's Add File command, or editing the .dbp with another tool) and the declared functions become available to your code.

      File           Provides
       ----           --------
    -  commdlg.bas    File Open/Save dialogs, input dialogs, choice dialogs, save-prompt dialogs
    +  commdlg.bas    File Open/Save dialogs, input dialogs, choice dialogs, save-prompt dialogs, the color dialog
       sql.bas        SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.)
       comm.bas       Serial-port I/O (raw and packet) and optional encrypted links
       resource.bas   Read and write DVX resource blocks (icons, text, binary data) in bundled files
    @@ -2677,6 +3685,14 @@ path = DlgFileOpen$("Open a file", "Text Files (*.txt)|All Files
     If path <> "" Then
         Print "Selected: " & path
     End If
    +

    Color Dialog

    +

    With commdlg.bas in the project, DlgColor(title, defaultColor) shows the color dialog: a palette of basic colors, Red, Green and Blue sliders with number boxes, and a preview. It starts at defaultColor and returns the color chosen, in the form RGB makes and ForeColor and BackColor use, or -1 when the user cancels.

    +
    Dim c As Long
    +c = DlgColor("Text Color", Label1.ForeColor)
    +
    +If c <> -1 Then
    +    Label1.ForeColor = c
    +End If
    Note: DVX BASIC does not have an $INCLUDE directive. Libraries are added to the project so that the compiler sees their DECLARE LIBRARY blocks.

    Encrypted Serial Links

    With comm.bas in the project, CommOpen opens a packet link and CommHandshake sets up encryption. Give both ends the same passphrase to authenticate the connection. Without a passphrase the link is still encrypted and tamper-checked, but it cannot tell who is on the other end. CommHandshake returns False if the passphrases differ, if only one side gives one, or if the link fails.

    @@ -2826,7 +3842,9 @@ End If
    ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct). TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order. ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip. - HelpTopic String R/W Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file. + HelpTopic String R/W Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file. + DragMode Integer R/W vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below. + DragIcon String R/W Picture file (BMP, PNG, JPEG or GIF) shown under the mouse while the control is dragged. Empty = a dotted outline the size of the control.

    Size properties (Width, Height and the Min/Max sizes) are limited to 0..32767.

    Common Events

    These events are wired on every control loaded from a .frm file. Controls created dynamically at runtime via code only receive Click, DblClick, Change, GotFocus, and LostFocus; the keyboard, mouse, and scroll events below require the control to be defined in the .frm file.

    @@ -2843,16 +3861,83 @@ End If MouseDown Button As Integer, X As Integer, Y As Integer Fires when a mouse button is pressed over the control. MouseUp Button As Integer, X As Integer, Y As Integer Fires when a mouse button is released over the control. MouseMove Button As Integer, X As Integer, Y As Integer Fires when the mouse moves over the control. - Scroll Delta As Integer Fires when the control is scrolled (mouse wheel or scrollbar). + Scroll Delta As Integer Fires when the control is scrolled (mouse wheel or scrollbar). + DragDrop Source As Control, X As Integer, Y As Integer Fires when a dragged control is dropped on this control. Source is the dragged control; X and Y are where, in this control. + DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over this control. State is vbEnter (0) when it comes onto the control, vbOver (2) as it moves across, vbLeave (1) when it goes off.

    Common Methods

      Method     Parameters   Description
       --------   ----------   -------------------------------------------
       SetFocus   (none)       Gives keyboard focus to this control.
    -  Refresh    (none)       Forces the control to repaint.
    + Refresh (none) Forces the control to repaint. + Drag [Action] Starts or ends dragging this control: vbBeginDrag (1, the default) starts, vbEndDrag (2) drops it where the mouse is, 0 cancels the drag without a drop. +

    Drag and Drop

    +

    Any control can be dragged onto another control or onto a form, as in Visual Basic. While a control is dragged, a dotted outline of it (or its DragIcon picture) follows the mouse, and the controls and forms it passes over get DragOver events. Releasing the mouse button drops it: the control or form under the mouse gets a DragDrop event, with the dragged control as Source. Pressing Esc cancels the drag.

    +

    With DragMode set to vbAutomatic, pressing the left mouse button on the control starts the drag, and the control gets no Click, MouseDown or MouseUp from the left button. With DragMode vbManual, start the drag yourself, usually in MouseDown. A drag started while the mouse button is up ends at the next click. During a drag no mouse events other than DragOver and DragDrop are raised.

    +

    Drop targets only react: nothing is moved or copied for you. Use Source in DragDrop to decide what to do, for example read its Name or Caption, or check what kind of control it is with TypeOf Source Is TextBox (see Operators).

    +

    Note that vbCancel is the MsgBox Cancel value (2), which as a Drag action means vbEndDrag; cancel a drag with Drag 0.

    +
    ' Label1 has DragMode = 1 (vbAutomatic) in the form designer.
    +Sub List1_DragOver(Source As Control, X As Integer, Y As Integer, State As Integer)
    +    If State = vbEnter Then Label2.Caption = "Drop to add " & Source.Caption
    +End Sub
    +
    +Sub List1_DragDrop(Source As Control, X As Integer, Y As Integer)
    +    List1.AddItem Source.Caption
    +End Sub
    +
    +' DragMode vbManual: the button starts its own drag, left button only.
    +Sub Command1_MouseDown(Button As Integer, X As Integer, Y As Integer)
    +    If Button = 1 Then Command1.Drag vbBeginDrag
    +End Sub

    Form

    Data Binding

    FRM File Format

    +
    +

    Drawing on Forms and PictureBoxes

    +

    Forms and PictureBoxes have Visual Basic's drawing methods. They draw with the same code as the QBasic graphics statements (see Console and Graphics in the language reference), in pixels, with colours as RGB values (RGB(255, 0, 0) or &HFF0000 is red). A PictureBox draws on its picture; a form gets a drawing area the size of its window the first time it draws, placed with its controls like a PictureBox (so draw on a form that has no controls, or use a PictureBox beside them).

    +

    Drawings stay until they are drawn over or cleared: AutoRedraw is accepted, but a drawing always behaves as with AutoRedraw = True. In a form's own code, write Me.Line, Me.Circle and so on: Line, Circle and PSet without an object are the QBasic graphics statements (they draw on the program's screen), and Print without an object is the PRINT statement.

    +

    Drawing Methods

    +
      Method                                                          Description
    +  ------                                                          -----------
    +  Cls                                                             Clears the drawing to BackColor; CurrentX and CurrentY go to 0.
    +  PSet [Step](x, y) [, color]                                     Draws a point (DrawWidth wide).
    +  Line [[Step](x1, y1)]-[Step](x2, y2) [, color] [, B | BF]         Draws a line, a box (B) or a filled box (BF). A box without F is filled with FillColor when FillStyle is 0.
    +  Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]]  Draws a circle, ellipse or arc, filled with FillColor when FillStyle is 0 (full circles).
    +  Point(x, y)                                                     The colour of a pixel, -1 outside the picture.
    +  Print items                                                     Writes text at CurrentX, CurrentY in ForeColor, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,.
    +  PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight]]   Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG or GIF), or a PictureBox given by its name as a string or by a Control variable.
    +  TextWidth(text)                                                 Width of the text in pixels.
    +  TextHeight(text)                                                Height of a line of text in pixels.
    +

    Without a colour, drawing uses ForeColor. Step makes a point relative to CurrentX, CurrentY, which every drawing method leaves at its last point. A colour or angle out of range stops the program with an error.

    +

    Drawing Properties

    +
      Property      Type      Default   Description
    +  --------      ----      -------   -----------
    +  ForeColor     Long      &H000000  Colour of drawing and printing.
    +  BackColor     Long      &HFFFFFF  Colour Cls clears to. Setting it clears the drawing to the new colour.
    +  CurrentX      Single    0         Where the next drawing or printing starts.
    +  CurrentY      Single    0
    +  DrawWidth     Integer   1         Pen width in pixels.
    +  FillStyle     Integer   1         0 fills boxes and circles with FillColor; 1 (the default) leaves them empty. Other VB fill patterns draw as 1.
    +  FillColor     Long      &H000000  Colour boxes and circles are filled with.
    +  AutoRedraw    Boolean   False     Accepted; drawings always persist.
    +  ScaleWidth    Long      (size)    Width of the drawing in pixels (read-only).
    +  ScaleHeight   Long      (size)    Height of the drawing in pixels (read-only).
    +

    Example

    +
    Sub Command1_Click ()
    +    Picture1.Resize 200, 150
    +    Picture1.BackColor = RGB(255, 255, 255)
    +    Picture1.DrawWidth = 2
    +    Picture1.Line (10, 10)-(190, 140), RGB(0, 0, 255), B
    +    Picture1.FillStyle = 0
    +    Picture1.FillColor = RGB(255, 255, 0)
    +    Picture1.Circle (100, 75), 40, RGB(255, 0, 0)
    +    Picture1.CurrentX = 70
    +    Picture1.CurrentY = 120
    +    Picture1.Print "Hello!"
    +End Sub
    +

    PictureBox

    +

    Form

    +

    Data Binding

    DVX BASIC provides VB3-style data binding through three properties that can be set on most controls:

    @@ -3122,7 +4207,10 @@ End Sub QueryUnload Cancel As Integer Fires before Unload. Set Cancel = 1 to abort the close. Resize (none) Fires when the window is resized by the user. Activate (none) Fires when the window gains focus. - Deactivate (none) Fires when the window loses focus. + Deactivate (none) Fires when the window loses focus. + DragDrop Source As Control, X As Integer, Y As Integer Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form. + DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave or vbOver. +

    See Drag and Drop under Common Properties, Events, and Methods.

    Form Methods

      Statement           Description
       ------------------  -------------------------------------------
    @@ -3131,6 +4219,8 @@ End Sub
    FormName.Show Make the form visible. FormName.Show 1 Show as modal dialog (blocks until closed). FormName.Hide Hide the form without unloading it. +

    Drawing

    +

    Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.

    Example

    Sub Form_Load ()
         Form1.Caption = "Hello World"
    @@ -3147,6 +4237,7 @@ Sub Form_Resize ()
         Print "Window resized"
     End Sub

    Common Properties, Events, and Methods

    +

    Drawing on Forms and PictureBoxes

    FRM File Format

    @@ -3313,7 +4404,9 @@ Picture1.DrawText 60, 130, "Hello!" If Picture1.Save("output.bmp") <> 0 Then MsgBox "Could not save output.bmp" End If +

    A PictureBox also has Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle): see Drawing on Forms and PictureBoxes.

    Common Properties, Events, and Methods

    +

    Drawing on Forms and PictureBoxes

    CheckBox

    diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index 28357fd2..12e2e1e0 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -367,6 +367,7 @@ img { max-width: 100%; }
  • Clipboard
  • Clipboard
  • Closing Windows
  • +
  • Color Dialog
  • ColorIdE
  • Colors
  • Colors
  • @@ -453,6 +454,7 @@ img { max-width: 100%; }
  • Double-click
  • Double-Click
  • DPMI
  • +
  • Drag Feedback
  • Drag Select
  • Dragging
  • drawArrowGlyph
  • @@ -504,6 +506,7 @@ img { max-width: 100%; }
  • dvxClipboardClear
  • dvxClipboardCopy
  • dvxClipboardGet
  • +
  • dvxColorDialog
  • dvxColorLabel
  • dvxColorName
  • dvxComp
  • @@ -520,6 +523,9 @@ img { max-width: 100%; }
  • dvxDialogEnd
  • dvxDialogRun
  • dvxDlg.h
  • +
  • dvxDragFeedbackHide
  • +
  • dvxDragImageShow
  • +
  • dvxDragOutlineShow
  • dvxDraw
  • dvxDraw.h
  • DvxDriverClassT
  • @@ -4393,6 +4399,17 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); --------- ----------- outLen Output: length of clipboard text

    Returns: Clipboard text, or NULL.

    +

    Drag Feedback

    +

    For programs that implement drag and drop: an outline or a picture that follows the mouse over every window (drawn under the cursor) until it is hidden. One is shown at a time; showing another replaces it. The program still tracks the mouse and decides where a drop lands; this only draws.

    +

    dvxDragOutlineShow

    +
    void dvxDragOutlineShow(AppContextT *ctx, int32_t w, int32_t h, int32_t offX, int32_t offY);
    +

    Show a w x h dotted outline (two-tone, so it shows on any background) whose top-left corner stays (offX, offY) from the mouse pointer -- typically the dragged object's size and its offset from where it was grabbed.

    +

    dvxDragImageShow

    +
    void dvxDragImageShow(AppContextT *ctx, const uint8_t *pixels, int32_t w, int32_t h, int32_t pitch, bool hasKey, uint32_t keyColor);
    +

    Show a picture centered on the mouse pointer. pixels are in the display's native format, as dvxLoadImage or dvxLoadImageAlpha return them, and must stay valid until the feedback is hidden or replaced. With hasKey, pixels of keyColor are transparent (pass dvxLoadImageAlpha's outputs).

    +

    dvxDragFeedbackHide

    +
    void dvxDragFeedbackHide(AppContextT *ctx);
    +

    Remove the outline or picture.

    Resource Loading

    dvxResLoadIcon

    uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resName, int32_t *outW, int32_t *outH, int32_t *outPitch);
    @@ -5084,6 +5101,22 @@ int32_t pick = dvxButtonBox(ctx, "Copy", "The disk is full." defaultIdx Initially highlighted item (-1 = none) outIdx Output: selected index

    Returns: true if a choice was made, false if cancelled.

    +

    Color Dialog

    +

    dvxColorDialog

    +
    bool dvxColorDialog(AppContextT *ctx, const char *title, uint32_t *rgbInOut);
    +

    Display a modal color picker: a palette of 48 basic colors, Red, Green and Blue sliders each paired with a number box (0-255), and a preview swatch. Clicking a basic color sets all three channels; the basic color matching the current color is framed. From the keyboard, Tab moves between the sliders and number boxes (the Red slider has the focus first), the arrow keys, Page Up/Down, Home and End move a slider, and Enter chooses OK. Works at every color depth; on 8-bit desktops the swatches show the nearest palette colors, but the chosen value is exact.

    +
      Parameter     Description
    +  ---------     -----------
    +  ctx           Application context
    +  title         Window title (NULL: "Color")
    +  rgbInOut      In: the starting color; out: the chosen color. Both 0x00RRGGBB, the layout of DVX BASIC's RGB()
    +

    Returns: true if OK was chosen, false if cancelled or closed (then *rgbInOut is unchanged).

    +

    Macros for the 0x00RRGGBB layout: DVX_RGB(r, g, b) builds a color from three 0-255 channels, and DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb) and DVX_RGB_BLUE(rgb) take one apart.

    +
    uint32_t rgb = DVX_RGB(r, g, b);
    +
    +if (dvxColorDialog(ctx, "Title Bar", &rgb)) {
    +    dvxSetColor(ctx, ColorActiveTitleBgE, DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb), DVX_RGB_BLUE(rgb));
    +}

    Custom Dialogs

    Any dialog built from widgets, with the behaviour of the boxes above: modal, Enter chooses OK (unless a button has the focus), Escape or closing the window cancels, a nested dialog (a Browse button, an error box) works, and the dialog ends cleanly if its program is ended meanwhile.

    typedef bool (*DvxDialogOkFnT)(WindowT *dlg, void *userCtx);
    diff --git a/screenshots/86box/apps-cpanel.png b/screenshots/86box/apps-cpanel.png
    index 7f0a5be4..4df6fcb0 100644
    --- a/screenshots/86box/apps-cpanel.png
    +++ b/screenshots/86box/apps-cpanel.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3a552f5d179370a426c8db508ed30fdf0b799e52b4c101858c53937668d051d8
    -size 16638
    +oid sha256:cf2fc814413dc000180596ba36f49716ec9571669a1959a2a5ce9bbfbad33f63
    +size 16430
    diff --git a/screenshots/86box/cpanel-cp-color.png b/screenshots/86box/cpanel-cp-color.png
    index 62e6e266..00d41f40 100644
    --- a/screenshots/86box/cpanel-cp-color.png
    +++ b/screenshots/86box/cpanel-cp-color.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7b0c2270d01ee64b7e9e9880bf2dc0034099b4182bf1b2ef9dd59c13aaf2f8a4
    -size 18832
    +oid sha256:6a695a45d25535a2cdfa489e932d221c87bc029e8e79991478ddcd1925d37b6e
    +size 18280
    diff --git a/screenshots/86box/cpanel-cp-desk.png b/screenshots/86box/cpanel-cp-desk.png
    index 1f9e0c47..f1dec2e1 100644
    --- a/screenshots/86box/cpanel-cp-desk.png
    +++ b/screenshots/86box/cpanel-cp-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:fcec234fae2160f7ff70179a0c897640485e9a8fe35dd8f01461bcfe6f42dbf9
    -size 14652
    +oid sha256:5b32dc4362913fb0a87d3cfa530bf6628ea90b21a5448d881e87ca0184fc6010
    +size 14443
    diff --git a/screenshots/86box/cpanel-cp-joy.png b/screenshots/86box/cpanel-cp-joy.png
    index 1d42f700..902d99a9 100644
    --- a/screenshots/86box/cpanel-cp-joy.png
    +++ b/screenshots/86box/cpanel-cp-joy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1a954517da07e005b67e634f0459fbd2cd95e9b524d9e1a0ad84346b56a8f3e0
    -size 16347
    +oid sha256:6c5abef727eff67db56905b030e31f83dea87ad20391a4af15427cffd4996256
    +size 16348
    diff --git a/screenshots/86box/cpanel-cp-mouse.png b/screenshots/86box/cpanel-cp-mouse.png
    index 7f0a5be4..4df6fcb0 100644
    --- a/screenshots/86box/cpanel-cp-mouse.png
    +++ b/screenshots/86box/cpanel-cp-mouse.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3a552f5d179370a426c8db508ed30fdf0b799e52b4c101858c53937668d051d8
    -size 16638
    +oid sha256:cf2fc814413dc000180596ba36f49716ec9571669a1959a2a5ce9bbfbad33f63
    +size 16430
    diff --git a/screenshots/86box/cpanel-cp-net.png b/screenshots/86box/cpanel-cp-net.png
    index 45a4c623..e93f245d 100644
    --- a/screenshots/86box/cpanel-cp-net.png
    +++ b/screenshots/86box/cpanel-cp-net.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b114767d1f704ec78e4edb3891a66edbab23c9001681af3f2b7d7fa781420250
    -size 16376
    +oid sha256:c1f37bac1331a4f7d3c58ee9bd45aed805fdd8ffa95a059487e84f5ec5fb1e63
    +size 16214
    diff --git a/screenshots/86box/cpanel-cp-print.png b/screenshots/86box/cpanel-cp-print.png
    index 32a48645..bd712763 100644
    --- a/screenshots/86box/cpanel-cp-print.png
    +++ b/screenshots/86box/cpanel-cp-print.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b7173e14107ae74f2e96111169ec6b0ab56bd72dd7fbc0ccaf4f16a6fd2c4b60
    -size 15793
    +oid sha256:0978c06a46d4d39b7d09acc02714e6f48689346f67d05f94363b0819616fa9ff
    +size 15645
    diff --git a/screenshots/86box/cpanel-cp-types.png b/screenshots/86box/cpanel-cp-types.png
    index 43e0942f..c6fa0024 100644
    --- a/screenshots/86box/cpanel-cp-types.png
    +++ b/screenshots/86box/cpanel-cp-types.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2e53c9cb6890c109b4bf3c5d5f58958c5bc68cd6ff9979c2b996ebebf9173f42
    -size 21461
    +oid sha256:3de4147235ff1e6471f76dba46886eebb711bb47971f919a5b3a46acf051e774
    +size 21268
    diff --git a/screenshots/86box/cpanel-cp-video.png b/screenshots/86box/cpanel-cp-video.png
    index 963d45cf..8d658949 100644
    --- a/screenshots/86box/cpanel-cp-video.png
    +++ b/screenshots/86box/cpanel-cp-video.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9857c9af97b7e509206e783385dd908da278be16e0a520bf057cc1e5731ce431
    -size 20089
    +oid sha256:69e8ed2e0257779ded5b0515b45691ef4ce92c5417e8a65b5b7db70ae6550ef8
    +size 19567
    diff --git a/screenshots/86box/f1-h-basdem.png b/screenshots/86box/f1-h-basdem.png
    index a4185f1a..83acc722 100644
    --- a/screenshots/86box/f1-h-basdem.png
    +++ b/screenshots/86box/f1-h-basdem.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:619e9b6fe89ab1113a27a41a0e0995c210f15ff3826cc4d17a2994930d3e96f4
    -size 20577
    +oid sha256:a853797b56c53f72336edbf96c838aed1d0806f7a3dcd77e6e3322a405dd5f75
    +size 20582
    diff --git a/screenshots/86box/f1-h-cpanel.png b/screenshots/86box/f1-h-cpanel.png
    index 7eef8a88..73217281 100644
    --- a/screenshots/86box/f1-h-cpanel.png
    +++ b/screenshots/86box/f1-h-cpanel.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1fb48fdfbd15a92b36aa9fa363388c59c8bc00b0c35b6cb9f6f1971344d13160
    -size 22306
    +oid sha256:6e7bbc951b075d68fc197dad925013d6cb1d87c841ced7d5df7c9814e2eb8bf6
    +size 22615
    diff --git a/screenshots/86box/hang-ended.png b/screenshots/86box/hang-ended.png
    index 1ac0e3f5..117141f4 100644
    --- a/screenshots/86box/hang-ended.png
    +++ b/screenshots/86box/hang-ended.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:41f9969dce1449f1a615e379f5e67d18df8eecfd0148f692319c587be99ed188
    -size 14972
    +oid sha256:219af82c920e15e77699f3066ba3f01e879a5bf1b74cd44df479e5843d0704a0
    +size 14950
    diff --git a/screenshots/86box/hang-taskmgr.png b/screenshots/86box/hang-taskmgr.png
    index a5d90bcb..fdfcc6b7 100644
    --- a/screenshots/86box/hang-taskmgr.png
    +++ b/screenshots/86box/hang-taskmgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:8ce98676506a89ccbf81b1e059f853ee852e72d0a451fe3380865e3fc5545c5a
    -size 15954
    +oid sha256:d6904e5ed0879155215fea1b7767e83fbfa4095517274d26ce0a1c612375a888
    +size 15948
    diff --git a/screenshots/86box/menus-m-altdn.png b/screenshots/86box/menus-m-altdn.png
    index b55ca120..b973564f 100644
    --- a/screenshots/86box/menus-m-altdn.png
    +++ b/screenshots/86box/menus-m-altdn.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:567bb467a163dc66097ef24db383c3f490e17a23c44f0390344debd84dc6e0a6
    -size 17021
    +oid sha256:c3198835d6ce56caccb99633baaf23e337fe01f79d49a2212598ea7a69ff021a
    +size 16818
    diff --git a/screenshots/86box/menus-m-altup.png b/screenshots/86box/menus-m-altup.png
    index 806814db..11f8b529 100644
    --- a/screenshots/86box/menus-m-altup.png
    +++ b/screenshots/86box/menus-m-altup.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6c5191e4aca1b4ff81a6455b89d468517daa339a0601885c6855b81a625a153e
    -size 16634
    +oid sha256:4944d691ae52c5420751cd71a8e7532273bd644ee186ab14fe899a5af8789467
    +size 16436
    diff --git a/screenshots/86box/menus-m-drop.png b/screenshots/86box/menus-m-drop.png
    index b55ca120..b973564f 100644
    --- a/screenshots/86box/menus-m-drop.png
    +++ b/screenshots/86box/menus-m-drop.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:567bb467a163dc66097ef24db383c3f490e17a23c44f0390344debd84dc6e0a6
    -size 17021
    +oid sha256:c3198835d6ce56caccb99633baaf23e337fe01f79d49a2212598ea7a69ff021a
    +size 16818
    diff --git a/screenshots/86box/scrtest-back.png b/screenshots/86box/scrtest-back.png
    new file mode 100644
    index 00000000..0654c6ce
    --- /dev/null
    +++ b/screenshots/86box/scrtest-back.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:a8632d38fe8ef985cd706b596123f212912abc77198f99ae9941a1a62eb049e4
    +size 17152
    diff --git a/screenshots/86box/scrtest-con1.png b/screenshots/86box/scrtest-con1.png
    new file mode 100644
    index 00000000..34c5cb85
    --- /dev/null
    +++ b/screenshots/86box/scrtest-con1.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:1f2f82fe7e6fe4ee98370bfb8cf9b387909bc13c1e2947286d914755091de51c
    +size 17373
    diff --git a/screenshots/86box/scrtest-con2.png b/screenshots/86box/scrtest-con2.png
    new file mode 100644
    index 00000000..d161a89d
    --- /dev/null
    +++ b/screenshots/86box/scrtest-con2.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:f1df92fc90ed00eb21504a81e1bd0e57cb15e3d5adec34e811f27b5d693679e1
    +size 17718
    diff --git a/screenshots/86box/scrtest-full1.png b/screenshots/86box/scrtest-full1.png
    new file mode 100644
    index 00000000..121a92af
    --- /dev/null
    +++ b/screenshots/86box/scrtest-full1.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:5ea2cfb968300ba18191fae5a02c746b5b55207980bd0c457ec5f6efda8f3265
    +size 2968
    diff --git a/screenshots/86box/scrtest-full2.png b/screenshots/86box/scrtest-full2.png
    new file mode 100644
    index 00000000..121a92af
    --- /dev/null
    +++ b/screenshots/86box/scrtest-full2.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:5ea2cfb968300ba18191fae5a02c746b5b55207980bd0c457ec5f6efda8f3265
    +size 2968
    diff --git a/screenshots/86box/scrtest-gfx.png b/screenshots/86box/scrtest-gfx.png
    new file mode 100644
    index 00000000..73c1e730
    --- /dev/null
    +++ b/screenshots/86box/scrtest-gfx.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:95e81698b0f456092a5f51612444c736c92a102b4dba2b9670ae074f44defc24
    +size 17034
    diff --git a/screenshots/86box/video-v-800.png b/screenshots/86box/video-v-800.png
    index 5c86d8c7..47a6a6e8 100644
    --- a/screenshots/86box/video-v-800.png
    +++ b/screenshots/86box/video-v-800.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:14c7915c618878af23bc1ea445df426f0430066a57fe48fe83f352059e3fbe73
    -size 26536
    +oid sha256:0a97c8b6db807df057d4b1acd9d48e9e04bdd5da64d537d5b76dc51a1942fe52
    +size 26516
    diff --git a/screenshots/86box/video-v-pick.png b/screenshots/86box/video-v-pick.png
    index ae6d0639..7d78961a 100644
    --- a/screenshots/86box/video-v-pick.png
    +++ b/screenshots/86box/video-v-pick.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:f5b8034a8ccdecb1e03f7eabccb2f4fc2d4b8f0b6833249b1baea1cebde15307
    -size 20426
    +oid sha256:802f7e81d1aa1b6bfc9661a9fef4824c44513f3e271d0be2825e02f16c25900c
    +size 20174
    diff --git a/screenshots/dosbox/apps-cpanel.png b/screenshots/dosbox/apps-cpanel.png
    index 7f0a5be4..4df6fcb0 100644
    --- a/screenshots/dosbox/apps-cpanel.png
    +++ b/screenshots/dosbox/apps-cpanel.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3a552f5d179370a426c8db508ed30fdf0b799e52b4c101858c53937668d051d8
    -size 16638
    +oid sha256:cf2fc814413dc000180596ba36f49716ec9571669a1959a2a5ce9bbfbad33f63
    +size 16430
    diff --git a/screenshots/dosbox/cpanel-cp-color.png b/screenshots/dosbox/cpanel-cp-color.png
    index 62e6e266..00d41f40 100644
    --- a/screenshots/dosbox/cpanel-cp-color.png
    +++ b/screenshots/dosbox/cpanel-cp-color.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7b0c2270d01ee64b7e9e9880bf2dc0034099b4182bf1b2ef9dd59c13aaf2f8a4
    -size 18832
    +oid sha256:6a695a45d25535a2cdfa489e932d221c87bc029e8e79991478ddcd1925d37b6e
    +size 18280
    diff --git a/screenshots/dosbox/cpanel-cp-desk.png b/screenshots/dosbox/cpanel-cp-desk.png
    index 100cde70..76360a2e 100644
    --- a/screenshots/dosbox/cpanel-cp-desk.png
    +++ b/screenshots/dosbox/cpanel-cp-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:21045ba1f8bd6994a2f3eb910549a04f43bf70dd33d77802957a9009e23dffb0
    -size 14671
    +oid sha256:cfe0af167d1535d62e34ddfd9e53deaaf6441e397d815d9e4246293b7e953be4
    +size 14435
    diff --git a/screenshots/dosbox/cpanel-cp-joy.png b/screenshots/dosbox/cpanel-cp-joy.png
    index 1d42f700..902d99a9 100644
    --- a/screenshots/dosbox/cpanel-cp-joy.png
    +++ b/screenshots/dosbox/cpanel-cp-joy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1a954517da07e005b67e634f0459fbd2cd95e9b524d9e1a0ad84346b56a8f3e0
    -size 16347
    +oid sha256:6c5abef727eff67db56905b030e31f83dea87ad20391a4af15427cffd4996256
    +size 16348
    diff --git a/screenshots/dosbox/cpanel-cp-mouse.png b/screenshots/dosbox/cpanel-cp-mouse.png
    index 7f0a5be4..4df6fcb0 100644
    --- a/screenshots/dosbox/cpanel-cp-mouse.png
    +++ b/screenshots/dosbox/cpanel-cp-mouse.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3a552f5d179370a426c8db508ed30fdf0b799e52b4c101858c53937668d051d8
    -size 16638
    +oid sha256:cf2fc814413dc000180596ba36f49716ec9571669a1959a2a5ce9bbfbad33f63
    +size 16430
    diff --git a/screenshots/dosbox/cpanel-cp-net.png b/screenshots/dosbox/cpanel-cp-net.png
    index 45a4c623..e93f245d 100644
    --- a/screenshots/dosbox/cpanel-cp-net.png
    +++ b/screenshots/dosbox/cpanel-cp-net.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b114767d1f704ec78e4edb3891a66edbab23c9001681af3f2b7d7fa781420250
    -size 16376
    +oid sha256:c1f37bac1331a4f7d3c58ee9bd45aed805fdd8ffa95a059487e84f5ec5fb1e63
    +size 16214
    diff --git a/screenshots/dosbox/cpanel-cp-print.png b/screenshots/dosbox/cpanel-cp-print.png
    index 32a48645..bd712763 100644
    --- a/screenshots/dosbox/cpanel-cp-print.png
    +++ b/screenshots/dosbox/cpanel-cp-print.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b7173e14107ae74f2e96111169ec6b0ab56bd72dd7fbc0ccaf4f16a6fd2c4b60
    -size 15793
    +oid sha256:0978c06a46d4d39b7d09acc02714e6f48689346f67d05f94363b0819616fa9ff
    +size 15645
    diff --git a/screenshots/dosbox/cpanel-cp-types.png b/screenshots/dosbox/cpanel-cp-types.png
    index 43e0942f..c6fa0024 100644
    --- a/screenshots/dosbox/cpanel-cp-types.png
    +++ b/screenshots/dosbox/cpanel-cp-types.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2e53c9cb6890c109b4bf3c5d5f58958c5bc68cd6ff9979c2b996ebebf9173f42
    -size 21461
    +oid sha256:3de4147235ff1e6471f76dba46886eebb711bb47971f919a5b3a46acf051e774
    +size 21268
    diff --git a/screenshots/dosbox/cpanel-cp-video.png b/screenshots/dosbox/cpanel-cp-video.png
    index d3ca0ab1..f3bb8a38 100644
    --- a/screenshots/dosbox/cpanel-cp-video.png
    +++ b/screenshots/dosbox/cpanel-cp-video.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:aea28c938cfbf06e4f667b67fa016c3268c4222f6ec6e4f3d63f865870b131e8
    -size 20347
    +oid sha256:ed7c9e4142fba46ed3ebfc49d5dd984823ccece4c7c8f6aab6bbc0298ab2d3ad
    +size 19744
    diff --git a/screenshots/dosbox/f1-h-basdem.png b/screenshots/dosbox/f1-h-basdem.png
    index a4185f1a..83acc722 100644
    --- a/screenshots/dosbox/f1-h-basdem.png
    +++ b/screenshots/dosbox/f1-h-basdem.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:619e9b6fe89ab1113a27a41a0e0995c210f15ff3826cc4d17a2994930d3e96f4
    -size 20577
    +oid sha256:a853797b56c53f72336edbf96c838aed1d0806f7a3dcd77e6e3322a405dd5f75
    +size 20582
    diff --git a/screenshots/dosbox/f1-h-cpanel.png b/screenshots/dosbox/f1-h-cpanel.png
    index 7eef8a88..73217281 100644
    --- a/screenshots/dosbox/f1-h-cpanel.png
    +++ b/screenshots/dosbox/f1-h-cpanel.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1fb48fdfbd15a92b36aa9fa363388c59c8bc00b0c35b6cb9f6f1971344d13160
    -size 22306
    +oid sha256:6e7bbc951b075d68fc197dad925013d6cb1d87c841ced7d5df7c9814e2eb8bf6
    +size 22615
    diff --git a/screenshots/dosbox/hang-ended.png b/screenshots/dosbox/hang-ended.png
    index 4be3af3a..9bd9e559 100644
    --- a/screenshots/dosbox/hang-ended.png
    +++ b/screenshots/dosbox/hang-ended.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5b3c9d0acb75c7d4dd0f8138b834708c2189570f540d2ed9e759eaa0d7e5feb0
    -size 14941
    +oid sha256:dc84216c8c0d465ed307386200def6ec543f5576e6da27486fabe3fd6f70c882
    +size 14919
    diff --git a/screenshots/dosbox/hang-taskmgr.png b/screenshots/dosbox/hang-taskmgr.png
    index 4ce368d1..55931b84 100644
    --- a/screenshots/dosbox/hang-taskmgr.png
    +++ b/screenshots/dosbox/hang-taskmgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a966e5f1b7fdb276e03ad7f50a5ccfe327aa3b32edd5d00f3bf5c52371b8fcc3
    -size 15948
    +oid sha256:180d4b700e6f94d84856f4419304fad93c29d1844df48c1c6a9fd32b64e478bd
    +size 15967
    diff --git a/screenshots/dosbox/menus-m-altdn.png b/screenshots/dosbox/menus-m-altdn.png
    index b55ca120..b973564f 100644
    --- a/screenshots/dosbox/menus-m-altdn.png
    +++ b/screenshots/dosbox/menus-m-altdn.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:567bb467a163dc66097ef24db383c3f490e17a23c44f0390344debd84dc6e0a6
    -size 17021
    +oid sha256:c3198835d6ce56caccb99633baaf23e337fe01f79d49a2212598ea7a69ff021a
    +size 16818
    diff --git a/screenshots/dosbox/menus-m-altup.png b/screenshots/dosbox/menus-m-altup.png
    index 806814db..11f8b529 100644
    --- a/screenshots/dosbox/menus-m-altup.png
    +++ b/screenshots/dosbox/menus-m-altup.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6c5191e4aca1b4ff81a6455b89d468517daa339a0601885c6855b81a625a153e
    -size 16634
    +oid sha256:4944d691ae52c5420751cd71a8e7532273bd644ee186ab14fe899a5af8789467
    +size 16436
    diff --git a/screenshots/dosbox/menus-m-drop.png b/screenshots/dosbox/menus-m-drop.png
    index b55ca120..b973564f 100644
    --- a/screenshots/dosbox/menus-m-drop.png
    +++ b/screenshots/dosbox/menus-m-drop.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:567bb467a163dc66097ef24db383c3f490e17a23c44f0390344debd84dc6e0a6
    -size 17021
    +oid sha256:c3198835d6ce56caccb99633baaf23e337fe01f79d49a2212598ea7a69ff021a
    +size 16818
    diff --git a/screenshots/dosbox/scrtest-back.png b/screenshots/dosbox/scrtest-back.png
    new file mode 100644
    index 00000000..0654c6ce
    --- /dev/null
    +++ b/screenshots/dosbox/scrtest-back.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:a8632d38fe8ef985cd706b596123f212912abc77198f99ae9941a1a62eb049e4
    +size 17152
    diff --git a/screenshots/dosbox/scrtest-con1.png b/screenshots/dosbox/scrtest-con1.png
    new file mode 100644
    index 00000000..34c5cb85
    --- /dev/null
    +++ b/screenshots/dosbox/scrtest-con1.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:1f2f82fe7e6fe4ee98370bfb8cf9b387909bc13c1e2947286d914755091de51c
    +size 17373
    diff --git a/screenshots/dosbox/scrtest-con2.png b/screenshots/dosbox/scrtest-con2.png
    new file mode 100644
    index 00000000..d161a89d
    --- /dev/null
    +++ b/screenshots/dosbox/scrtest-con2.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:f1df92fc90ed00eb21504a81e1bd0e57cb15e3d5adec34e811f27b5d693679e1
    +size 17718
    diff --git a/screenshots/dosbox/scrtest-full1.png b/screenshots/dosbox/scrtest-full1.png
    new file mode 100644
    index 00000000..121a92af
    --- /dev/null
    +++ b/screenshots/dosbox/scrtest-full1.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:5ea2cfb968300ba18191fae5a02c746b5b55207980bd0c457ec5f6efda8f3265
    +size 2968
    diff --git a/screenshots/dosbox/scrtest-full2.png b/screenshots/dosbox/scrtest-full2.png
    new file mode 100644
    index 00000000..121a92af
    --- /dev/null
    +++ b/screenshots/dosbox/scrtest-full2.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:5ea2cfb968300ba18191fae5a02c746b5b55207980bd0c457ec5f6efda8f3265
    +size 2968
    diff --git a/screenshots/dosbox/scrtest-gfx.png b/screenshots/dosbox/scrtest-gfx.png
    new file mode 100644
    index 00000000..73c1e730
    --- /dev/null
    +++ b/screenshots/dosbox/scrtest-gfx.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:95e81698b0f456092a5f51612444c736c92a102b4dba2b9670ae074f44defc24
    +size 17034
    diff --git a/screenshots/dosbox/video-v-800.png b/screenshots/dosbox/video-v-800.png
    index 70065b8a..bbc92f67 100644
    --- a/screenshots/dosbox/video-v-800.png
    +++ b/screenshots/dosbox/video-v-800.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:40a0ba8d0fc6c0a45af15255ded238308cc6ece90f2c4ba0fdd7ba010a01e617
    -size 27058
    +oid sha256:682c2a4eadf7c84bfaff80a564ea2cd6b89b32d8e2648f64b94c040dff2ea9a3
    +size 27054
    diff --git a/screenshots/dosbox/video-v-pick.png b/screenshots/dosbox/video-v-pick.png
    index cc839630..6b939fa9 100644
    --- a/screenshots/dosbox/video-v-pick.png
    +++ b/screenshots/dosbox/video-v-pick.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b078b20e99cb8bff8d6c3936d6d144d9eff0166933c04cafdc721e1c589a44a0
    -size 20671
    +oid sha256:1c4e224d31db97ff099dc9cc0d268388419ebe8f3d131f1f1366cd100acecca4
    +size 20301
    diff --git a/src/apps/kpunch/Makefile b/src/apps/kpunch/Makefile
    index 9794f683..da03f1b6 100644
    --- a/src/apps/kpunch/Makefile
    +++ b/src/apps/kpunch/Makefile
    @@ -120,7 +120,7 @@ $(BINDIR)/kpunch/resedit/resedit.app: resedit/resedit.dbp resedit/resedit.frm re
     
     basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app
     
    -$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
    +$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/shape.cls basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
     	$(BASCOMP) basdemo/basdemo.dbp -o $@ -release
     
     widshow: $(BINDIR)/kpunch/widshow/widshow.app
    diff --git a/src/apps/kpunch/basdemo/basdemo.dbp b/src/apps/kpunch/basdemo/basdemo.dbp
    index cb5714b6..9baa3477 100644
    --- a/src/apps/kpunch/basdemo/basdemo.dbp
    +++ b/src/apps/kpunch/basdemo/basdemo.dbp
    @@ -12,6 +12,10 @@ File0 = ../../../include/basic/commdlg.bas
     File1 = ../../../include/basic/help.bas
     Count = 2
     
    +[Classes]
    +File0 = shape.cls
    +Count = 1
    +
     [Forms]
     File0 = basdemo.frm
     Count = 1
    diff --git a/src/apps/kpunch/basdemo/basdemo.dhs b/src/apps/kpunch/basdemo/basdemo.dhs
    index f7876171..ec44f306 100644
    --- a/src/apps/kpunch/basdemo/basdemo.dhs
    +++ b/src/apps/kpunch/basdemo/basdemo.dhs
    @@ -73,6 +73,7 @@ The File I/O and INI examples create their files in the program's own data folde
       Demos   Graphics...            Open a window with a drawing. Draw paints shapes and text; Clear wipes it.
       Demos   Dynamic Form...        Open a window that the program builds while running, with a button that counts.
       Demos   Timer...               Open a window with a counter that ticks twice a second.
    +  Demos   Objects                Make shapes from the Shape class module, keep them in a Collection and list them with FOR EACH.
       Help    Contents               Open this help.
       Help    About...               Show the program name and copyright.
     .endtable
    diff --git a/src/apps/kpunch/basdemo/basdemo.frm b/src/apps/kpunch/basdemo/basdemo.frm
    index a6f0c408..57771ea8 100644
    --- a/src/apps/kpunch/basdemo/basdemo.frm
    +++ b/src/apps/kpunch/basdemo/basdemo.frm
    @@ -42,6 +42,9 @@ Begin Form BasicDemo
             Begin Menu mnuTimer
                 Caption = "&Timer..."
             End
    +        Begin Menu mnuObjects
    +            Caption = "&Objects"
    +        End
         End
     
         Begin Menu mnuHelp
    @@ -281,6 +284,7 @@ SUB mnuRunAll_Click
         btnFileIO_Click
         btnSystem_Click
         btnIni_Click
    +    mnuObjects_Click
         LblStatus.Caption = "All text demos complete."
     END SUB
     
    @@ -941,3 +945,33 @@ SUB TimerForm_Unload
         timerOpen = False
         tickCount = 0
     END SUB
    +
    +
    +SUB mnuObjects_Click
    +    ' Objects: the Shape class module, NEW, a Collection and FOR EACH
    +    Header "Objects"
    +
    +    DIM shapes AS NEW Collection
    +    DIM s AS Shape
    +    DIM item AS Shape
    +    DIM total AS DOUBLE
    +
    +    SET s = NEW Shape
    +    s.Size = 2
    +    shapes.Add s, "small"
    +
    +    SET s = NEW Shape
    +    s.Kind = "circle"
    +    s.Size = 1.5
    +    shapes.Add s, "round"
    +
    +    FOR EACH item IN shapes
    +        Say item.Describe
    +        total = total + item.Area
    +    NEXT
    +
    +    Say "Shapes:" + STR$(shapes.Count) + ", total area" + STR$(total)
    +    Say "The one called ROUND is a " + shapes("ROUND").Kind
    +    Say "TypeName(s) = " + TypeName(s) + ", s Is shapes(2): " + IIf(s Is shapes(2), "yes", "no")
    +    LblStatus.Caption = "Objects demo complete."
    +END SUB
    diff --git a/src/apps/kpunch/basdemo/shape.cls b/src/apps/kpunch/basdemo/shape.cls
    new file mode 100644
    index 00000000..5a07ae17
    --- /dev/null
    +++ b/src/apps/kpunch/basdemo/shape.cls
    @@ -0,0 +1,36 @@
    +VERSION DVX 1.00 CLASS
    +Attribute VB_Name = "Shape"
    +
    +' A shape for the Objects demo: its kind and size, and the area that
    +' follows from them.  Every new Shape starts as a square of size 1.
    +
    +Public Kind As String
    +Private mSize As Double
    +
    +Private Sub Class_Initialize()
    +    Kind = "square"
    +    mSize = 1
    +End Sub
    +
    +Public Property Get Size() As Double
    +    Size = mSize
    +End Property
    +
    +Public Property Let Size(newSize As Double)
    +    IF newSize < 0 THEN
    +        ERROR 5
    +    END IF
    +    mSize = newSize
    +End Property
    +
    +Public Function Area() As Double
    +    IF Kind = "circle" THEN
    +        Area = 3.14159 * mSize * mSize
    +    ELSE
    +        Area = mSize * mSize
    +    END IF
    +End Function
    +
    +Public Function Describe() As String
    +    Describe = Kind + " of size" + STR$(mSize) + ", area" + STR$(Area)
    +End Function
    diff --git a/src/apps/kpunch/cpanel/cpanel.c b/src/apps/kpunch/cpanel/cpanel.c
    index 1b023d5e..8936a442 100644
    --- a/src/apps/kpunch/cpanel/cpanel.c
    +++ b/src/apps/kpunch/cpanel/cpanel.c
    @@ -92,7 +92,6 @@
     #define CP_REVERT_PREFIX      "The old mode comes back in "
     #define CP_REVERT_SUFFIX      " seconds."
     #define CP_WPAPER_BTN_W       90
    -#define CP_RGB_MAX            255
     #define CP_LABEL_BUF          48
     #define CP_FILTER_BUF         128
     #define CP_ENTRY_NAME_MAX     64
    @@ -170,9 +169,6 @@ static WidgetT *sDblTestLbl    = NULL;
     
     // Colors tab widgets
     static WidgetT *sColorList     = NULL;
    -static WidgetT *sRedSldr       = NULL;
    -static WidgetT *sGreenSldr     = NULL;
    -static WidgetT *sBlueSldr      = NULL;
     static WidgetT *sRedLbl        = NULL;
     static WidgetT *sGreenLbl      = NULL;
     static WidgetT *sBlueLbl       = NULL;
    @@ -218,8 +214,8 @@ static void        onCancel(WidgetT *w);
     static void        onChooseWallpaper(WidgetT *w);
     static void        onClearWallpaper(WidgetT *w);
     static void        onClose(WindowT *win);
    +static void        onColorChange(WidgetT *w);
     static void        onColorSelect(WidgetT *w);
    -static void        onColorSlider(WidgetT *w);
     static void        onDblClickSlider(WidgetT *w);
     static void        onDblClickTest(WidgetT *w);
     static void        onMenu(WindowT *win, int32_t menuId);
    @@ -241,11 +237,9 @@ static void        scanThemes(void);
     static void        scanWallpapers(void);
     static void        setIntLabel(WidgetT *lbl, const char *prefix, int32_t val, const char *suffix);
     static void        setWallpaperFromPath(const char *path);
    -static void        updateColorSliders(void);
    +static void        updateColorSwatch(void);
     static void        updateDblClickLabel(void);
    -static void        updateRgbLabels(uint8_t r, uint8_t g, uint8_t b);
     static void        updateSpeedLabel(void);
    -static void        updateSwatch(void);
     static void        updateWheelStepLabel(void);
     int32_t            appMain(DxeAppContextT *ctx);
     
    @@ -320,7 +314,7 @@ void appShutdown(void) {
     
     
     static void buildColorsTab(WidgetT *page) {
    -    // Color list on the left, sliders on the right
    +    // Color list on the left, the selected color and Change... on the right
         WidgetT *hbox = wgtHBox(page);
         hbox->weight  = WGT_WEIGHT_FILL;
         hbox->spacing = wgtPixels(CP_SPACING);
    @@ -340,8 +334,9 @@ static void buildColorsTab(WidgetT *page) {
         }
     
         sColorList = wgtListBox(leftVbox);
    -    sColorList->weight   = WGT_WEIGHT_FILL;
    -    sColorList->onChange  = onColorSelect;
    +    sColorList->weight     = WGT_WEIGHT_FILL;
    +    sColorList->onChange   = onColorSelect;
    +    sColorList->onDblClick = onColorChange;
         wgtListBoxSetItems(sColorList, colorNames, ColorCountE);
         wgtListBoxSetSelected(sColorList, 0);
     
    @@ -372,39 +367,27 @@ static void buildColorsTab(WidgetT *page) {
         resetBtn->onClick = onResetColors;
         resetBtn->prefW   = wgtPixels(CP_THEME_BTN_W);
     
    -    // Right side: RGB sliders in a non-weighted inner box so they stay
    -    // at natural size.  The outer box absorbs extra vertical space.
    +    // Right side: the selected color and its values, in a non-weighted
    +    // inner box so they stay at natural size; the outer box absorbs extra
    +    // vertical space.  Change... picks a new color in the color dialog.
         WidgetT *rightVbox = wgtVBox(hbox);
         rightVbox->weight  = CP_HALF_WEIGHT;
     
    -    WidgetT *sliderBox = wgtVBox(rightVbox);
    -    sliderBox->spacing = wgtPixels(CP_SPACING_SMALL);
    +    WidgetT *colorBox = wgtVBox(rightVbox);
    +    colorBox->spacing = wgtPixels(CP_SPACING_SMALL);
     
    -    wgtLabel(sliderBox, "Red:");
    -    sRedSldr = wgtSlider(sliderBox, 0, CP_RGB_MAX);
    -    sRedSldr->onChange = onColorSlider;
    -    sRedLbl = wgtLabel(sliderBox, "0");
    -    wgtLabelSetAlign(sRedLbl, AlignEndE);
    +    wgtLabel(colorBox, "Color:");
    +    sColorSwatch = wgtCanvas(colorBox, CP_SWATCH_W, CP_SWATCH_H);
    +    sRedLbl      = wgtLabel(colorBox, "");
    +    sGreenLbl    = wgtLabel(colorBox, "");
    +    sBlueLbl     = wgtLabel(colorBox, "");
     
    -    wgtLabel(sliderBox, "Green:");
    -    sGreenSldr = wgtSlider(sliderBox, 0, CP_RGB_MAX);
    -    sGreenSldr->onChange = onColorSlider;
    -    sGreenLbl = wgtLabel(sliderBox, "0");
    -    wgtLabelSetAlign(sGreenLbl, AlignEndE);
    +    WidgetT *changeBtn = wgtButton(colorBox, "Change...");
    +    changeBtn->onClick = onColorChange;
     
    -    wgtLabel(sliderBox, "Blue:");
    -    sBlueSldr = wgtSlider(sliderBox, 0, CP_RGB_MAX);
    -    sBlueSldr->onChange = onColorSlider;
    -    sBlueLbl = wgtLabel(sliderBox, "0");
    -    wgtLabelSetAlign(sBlueLbl, AlignEndE);
    -
    -    wgtLabel(sliderBox, "Preview:");
    -    sColorSwatch = wgtCanvas(sliderBox, CP_SWATCH_W, CP_SWATCH_H);
    -
    -    // Absorb remaining vertical space below the sliders
         wgtSpacer(rightVbox)->weight = WGT_WEIGHT_FILL;
     
    -    updateColorSliders();
    +    updateColorSwatch();
     }
     
     
    @@ -634,7 +617,7 @@ static void onApplyTheme(WidgetT *w) {
         }
     
         dvxLoadTheme(sAc, sThemeEntries[sel].path);
    -    updateColorSliders();
    +    updateColorSwatch();
     }
     
     
    @@ -658,7 +641,7 @@ static void onBrowseTheme(WidgetT *w) {
     
         if (dvxFileDialog(sAc, "Load Theme", FD_OPEN, THEME_DIR, sThemeFilters, DVX_ARRAY_LEN(sThemeFilters), path, sizeof(path))) {
             dvxLoadTheme(sAc, path);
    -        updateColorSliders();
    +        updateColorSwatch();
         }
     }
     
    @@ -718,13 +701,9 @@ static void onClose(WindowT *win) {
     }
     
     
    -static void onColorSelect(WidgetT *w) {
    -    (void)w;
    -    updateColorSliders();
    -}
    -
    -
    -static void onColorSlider(WidgetT *w) {
    +// Change... (or a double-click on the list): pick a new color for the
    +// selected entry.  It is applied at once, like every other change here.
    +static void onColorChange(WidgetT *w) {
         (void)w;
     
         int32_t sel = wgtListBoxGetSelected(sColorList);
    @@ -733,12 +712,25 @@ static void onColorSlider(WidgetT *w) {
             return;
         }
     
    -    uint8_t r = (uint8_t)wgtSliderGetValue(sRedSldr);
    -    uint8_t g = (uint8_t)wgtSliderGetValue(sGreenSldr);
    -    uint8_t b = (uint8_t)wgtSliderGetValue(sBlueSldr);
    +    uint8_t r;
    +    uint8_t g;
    +    uint8_t b;
    +    dvxGetColor(sAc, (ColorIdE)sel, &r, &g, &b);
     
    -    updateRgbLabels(r, g, b);
    -    dvxSetColor(sAc, (ColorIdE)sel, r, g, b);
    +    uint32_t rgb = DVX_RGB(r, g, b);
    +
    +    if (!dvxColorDialog(sAc, dvxColorLabel((ColorIdE)sel), &rgb)) {
    +        return;
    +    }
    +
    +    dvxSetColor(sAc, (ColorIdE)sel, DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb), DVX_RGB_BLUE(rgb));
    +    updateColorSwatch();
    +}
    +
    +
    +static void onColorSelect(WidgetT *w) {
    +    (void)w;
    +    updateColorSwatch();
     }
     
     
    @@ -852,7 +844,7 @@ static void onOk(WidgetT *w) {
     static void onResetColors(WidgetT *w) {
         (void)w;
         dvxResetColorScheme(sAc);
    -    updateColorSliders();
    +    updateColorSwatch();
     }
     
     
    @@ -1176,7 +1168,8 @@ static void setWallpaperFromPath(const char *path) {
     }
     
     
    -static void updateColorSliders(void) {
    +// Shows the selected entry's color in the swatch and as numbers.
    +static void updateColorSwatch(void) {
         int32_t sel = wgtListBoxGetSelected(sColorList);
     
         if (sel < 0 || sel >= ColorCountE) {
    @@ -1188,10 +1181,10 @@ static void updateColorSliders(void) {
         uint8_t b;
         dvxGetColor(sAc, (ColorIdE)sel, &r, &g, &b);
     
    -    wgtSliderSetValue(sRedSldr, r);
    -    wgtSliderSetValue(sGreenSldr, g);
    -    wgtSliderSetValue(sBlueSldr, b);
    -    updateRgbLabels(r, g, b);
    +    setIntLabel(sRedLbl, "Red ", r, "");
    +    setIntLabel(sGreenLbl, "Green ", g, "");
    +    setIntLabel(sBlueLbl, "Blue ", b, "");
    +    wgtCanvasClear(sColorSwatch, packColor(&sAc->display, r, g, b));
     }
     
     
    @@ -1200,34 +1193,11 @@ static void updateDblClickLabel(void) {
     }
     
     
    -// Shows the RGB values next to the sliders and repaints the swatch.
    -static void updateRgbLabels(uint8_t r, uint8_t g, uint8_t b) {
    -    setIntLabel(sRedLbl, "", r, "");
    -    setIntLabel(sGreenLbl, "", g, "");
    -    setIntLabel(sBlueLbl, "", b, "");
    -    updateSwatch();
    -}
    -
    -
     static void updateSpeedLabel(void) {
         setIntLabel(sSpeedLbl, "", wgtSliderGetValue(sSpeedSldr), "");
     }
     
     
    -static void updateSwatch(void) {
    -    if (!sColorSwatch) {
    -        return;
    -    }
    -
    -    uint8_t r = (uint8_t)wgtSliderGetValue(sRedSldr);
    -    uint8_t g = (uint8_t)wgtSliderGetValue(sGreenSldr);
    -    uint8_t b = (uint8_t)wgtSliderGetValue(sBlueSldr);
    -
    -    uint32_t color = packColor(&sAc->display, r, g, b);
    -    wgtCanvasClear(sColorSwatch, color);
    -}
    -
    -
     static void updateWheelStepLabel(void) {
         setIntLabel(sWheelStepLbl, "", wgtSliderGetValue(sWheelStepSldr), "");
     }
    diff --git a/src/apps/kpunch/cpanel/cpanel.dhs b/src/apps/kpunch/cpanel/cpanel.dhs
    index 63b7d511..4fe2600c 100644
    --- a/src/apps/kpunch/cpanel/cpanel.dhs
    +++ b/src/apps/kpunch/cpanel/cpanel.dhs
    @@ -108,7 +108,9 @@ If the pointer is hard to control, set Acceleration to Off and adjust Cursor Spe
     
     .h2 Changing One Color
     
    -The System Colors list names every part of the screen that has a color, such as the desktop, title bars, menus and buttons. Click a name, then move the Red, Green and Blue sliders to mix a new color for it. Each slider goes from 0 (none of that color) to 255 (full). The Preview box and the screen itself change as you go.
    +The System Colors list names every part of the screen that has a color, such as the desktop, title bars, menus and buttons. Click a name to see its color beside the list, with its red, green and blue amounts (each from 0, none of that color, to 255, full).
    +
    +To change it, click Change... (or double-click the name). A color box named after the part opens: click one of the basic colors, or mix your own with the Red, Green and Blue sliders or by typing the numbers, and watch the preview. Click OK and the screen changes to the new color at once; Cancel leaves it as it was.
     
     .h2 Themes
     
    diff --git a/src/apps/kpunch/dvxbasic/Makefile b/src/apps/kpunch/dvxbasic/Makefile
    index 4fc3f315..fc5056d3 100644
    --- a/src/apps/kpunch/dvxbasic/Makefile
    +++ b/src/apps/kpunch/dvxbasic/Makefile
    @@ -59,7 +59,7 @@ SYSTEMDIR = ../../../../bin/system
     # Sources every command-line build (bascomp, basrun, test_suite) links:
     # the build pipeline, the full compiler and the runtime without forms.
     COMP_SRCS    = compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c
    -VM_SRCS      = runtime/vm.c runtime/values.c runtime/serialize.c
    +VM_SRCS      = runtime/vm.c runtime/values.c runtime/dates.c runtime/serialize.c runtime/gfx.c runtime/screen.c
     # Standalone programs that link the VM also need what libdvx provides it
     # at run time on the target (INI access through dvxPrefs).
     VM_LINK_SRCS = $(VM_SRCS) $(LIBDVX)/dvxPrefs.c
    @@ -68,7 +68,7 @@ RES_SRCS     = $(LIBDVX)/dvxResource.c $(TOOLSDIR)/dvxResWrite.c
     CLI_SRCS     = basBuild.c $(COMP_SRCS) $(VM_LINK_SRCS) $(RES_SRCS) $(SUPPORT_SRCS)
     
     # Runtime library objects (VM + values + form runtime + serialization)
    -RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o
    +RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/dates.o $(OBJDIR)/gfx.o $(OBJDIR)/screen.o $(OBJDIR)/formrt.o $(OBJDIR)/formGfx.o $(OBJDIR)/frmParser.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o
     RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
     RT_TARGET    = $(RT_TARGETDIR)/basrt.lib
     
    @@ -281,7 +281,7 @@ clean: host-basrt-clean
     # of the host build.
     HOSTRT_DIR      = $(HOST_OBJROOT)/basrt
     HOSTRT_TARGET   = $(HOST_OBJROOT)/basrt.a
    -HOSTRT_SRCS     = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/frmParser.c formrt/basNativeHost.c
    +HOSTRT_SRCS     = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/formGfx.c formrt/frmParser.c formrt/basNativeHost.c
     HOSTRT_OBJS     = $(patsubst %.c,$(HOSTRT_DIR)/%.o,$(notdir $(HOSTRT_SRCS)))
     HOSTRT_CFLAGS   = $(HOST_CFLAGS) -Wno-stringop-truncation $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks
     
    diff --git a/src/apps/kpunch/dvxbasic/basBuild.c b/src/apps/kpunch/dvxbasic/basBuild.c
    index ade2915a..bf57cae7 100644
    --- a/src/apps/kpunch/dvxbasic/basBuild.c
    +++ b/src/apps/kpunch/dvxbasic/basBuild.c
    @@ -77,7 +77,7 @@
     
     // Growth unit of the project source buffer.
     #define BAS_BUILD_SOURCE_CHUNK 8192
    -// Room for a BEGINFORM "name" / ENDFORM wrapper around a form's code.
    +// Room for a BEGINFORM "name" / BEGINCLASS "name" line.
     #define BAS_BUILD_WRAP_SLACK   (BAS_MAX_IDENT + 32)
     // Longest [Modules]/[Forms] key, "File" plus a decimal index.
     #define BAS_BUILD_KEY_LEN      32
    @@ -89,15 +89,26 @@ static char sIconError[BAS_BUILD_ERROR_LEN];
     // basBuildConcatProject passes, in source order.
     typedef enum {
         BAS_BUILD_PASS_MODULES,
    +    BAS_BUILD_PASS_CLASSES,
         BAS_BUILD_PASS_FORMS,
         BAS_BUILD_PASS_COUNT
     } BasBuildPassE;
     
    +// The directives around a form's and a class module's code.
    +static const char *const sWrapBegin[] = {
    +    [BAS_BUILD_WRAP_FORM]  = "BEGINFORM",
    +    [BAS_BUILD_WRAP_CLASS] = "BEGINCLASS"
    +};
    +static const char *const sWrapEnd[] = {
    +    [BAS_BUILD_WRAP_FORM]  = "ENDFORM\n",
    +    [BAS_BUILD_WRAP_CLASS] = "ENDCLASS\n"
    +};
    +
     // Function prototypes (alphabetical)
     static bool        appendSource(BasProjectSourceT *ps, const char *text, int32_t len);
     static int32_t     appendText(const char *path, const char *name, const char *value);
     const char        *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
    -bool               basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, const char *formName, int32_t *textLine, int32_t *textLines);
    +bool               basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, BasBuildWrapE wrap, const char *name, int32_t *textLine, int32_t *textLines);
     bool               basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out);
     void               basBuildDirOf(const char *path, char *out, int32_t outSize);
     void               basBuildFreeProject(BasProjectSourceT *out);
    @@ -229,19 +240,17 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
     }
     
     
    -bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, const char *formName, int32_t *textLine, int32_t *textLines) {
    -    bool isForm = (formName && formName[0]);
    +bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, BasBuildWrapE wrap, const char *name, int32_t *textLine, int32_t *textLines) {
    +    if (wrap != BAS_BUILD_WRAP_NONE) {
    +        char    line[BAS_BUILD_WRAP_SLACK];
    +        int32_t n = snprintf(line, sizeof(line), "%s \"%s\"\n", sWrapBegin[wrap], name);
     
    -    if (isForm) {
    -        char    wrap[BAS_BUILD_WRAP_SLACK];
    -        int32_t n = snprintf(wrap, sizeof(wrap), "BEGINFORM \"%s\"\n", formName);
    -
    -        if (n < 0 || n >= (int32_t)sizeof(wrap)) {
    -            snprintf(ps->error, sizeof(ps->error), "Form name too long: %s", formName);
    +        if (n < 0 || n >= (int32_t)sizeof(line)) {
    +            snprintf(ps->error, sizeof(ps->error), "Name too long: %s", name);
                 return false;
             }
     
    -        if (!appendSource(ps, wrap, n)) {
    +        if (!appendSource(ps, line, n)) {
                 return false;
             }
         }
    @@ -260,8 +269,8 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, co
             *textLines = ps->lineCount + 1 - startLine;
         }
     
    -    if (isForm) {
    -        return appendSource(ps, "ENDFORM\n", (int32_t)strlen("ENDFORM\n"));
    +    if (wrap != BAS_BUILD_WRAP_NONE) {
    +        return appendSource(ps, sWrapEnd[wrap], (int32_t)strlen(sWrapEnd[wrap]));
         }
     
         return true;
    @@ -271,15 +280,17 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, co
     bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out) {
         static const char *const sections[BAS_BUILD_PASS_COUNT] = {
             [BAS_BUILD_PASS_MODULES] = BAS_INI_SECTION_MODULES,
    +        [BAS_BUILD_PASS_CLASSES] = BAS_INI_SECTION_CLASSES,
             [BAS_BUILD_PASS_FORMS]   = BAS_INI_SECTION_FORMS
         };
     
         memset(out, 0, sizeof(*out));
         out->optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
     
    -    // .bas modules first, then .frm code sections.
    +    // .bas modules first, then .cls class modules, then .frm code sections.
         for (int32_t pass = 0; pass < BAS_BUILD_PASS_COUNT; pass++) {
    -        bool isForm = (pass == BAS_BUILD_PASS_FORMS);
    +        bool isForm  = (pass == BAS_BUILD_PASS_FORMS);
    +        bool isClass = (pass == BAS_BUILD_PASS_CLASSES);
     
             for (int32_t i = 0; ; i++) {
                 char key[BAS_BUILD_KEY_LEN];
    @@ -302,11 +313,27 @@ bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProje
                     return false;
                 }
     
    -            const char *code                    = src;
    -            char        formName[BAS_MAX_IDENT] = "";
    +            const char   *code                    = src;
    +            char          formName[BAS_MAX_IDENT] = "";
    +            BasBuildWrapE wrap                    = BAS_BUILD_WRAP_NONE;
     
                 entry.firstFileLine = 1;
     
    +            if (isClass) {
    +                // The class code follows the .cls header
    +                if (!basExtractClassName(src, srcLen, val, formName, BAS_MAX_IDENT)) {
    +                    snprintf(out->error, sizeof(out->error), "No class name for %s", entry.path);
    +                    free(src);
    +                    return false;
    +                }
    +
    +                int32_t codeOff = basClassCodePos(src, srcLen);
    +
    +                code                = src + codeOff;
    +                entry.firstFileLine = countLines(src, codeOff) + 1;
    +                wrap                = BAS_BUILD_WRAP_CLASS;
    +            }
    +
                 if (isForm) {
                     // A .frm without a Begin Form line would otherwise compile
                     // its code unwrapped, as if it were a module.
    @@ -321,9 +348,10 @@ bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProje
     
                     code                = src + codeOff;
                     entry.firstFileLine = countLines(src, codeOff) + 1;
    +                wrap                = BAS_BUILD_WRAP_FORM;
                 }
     
    -            if (!basBuildAppendFile(out, code, (int32_t)strlen(code), formName, &entry.startLine, &entry.lineCount)) {
    +            if (!basBuildAppendFile(out, code, (int32_t)strlen(code), wrap, formName, &entry.startLine, &entry.lineCount)) {
                     free(src);
                     return false;
                 }
    diff --git a/src/apps/kpunch/dvxbasic/basBuild.h b/src/apps/kpunch/dvxbasic/basBuild.h
    index c7096d4a..df19d95d 100644
    --- a/src/apps/kpunch/dvxbasic/basBuild.h
    +++ b/src/apps/kpunch/dvxbasic/basBuild.h
    @@ -99,8 +99,10 @@ typedef struct {
     } BasSourceMapT;
     
     // A project's BASIC source as the compiler consumes it: every [Modules]
    -// file in order, then the code section of every [Forms] file wrapped in
    -// BEGINFORM "name" / ENDFORM, each file ending on a newline.  frmPaths
    +// file in order, then the code of every [Classes] file wrapped in
    +// BEGINCLASS "name" / ENDCLASS, then the code section of every [Forms]
    +// file wrapped in BEGINFORM "name" / ENDFORM, each file ending on a
    +// newline.  frmPaths
     // and frmTexts list the .frm files (path and full text, as read) in the
     // same order for the resource step; map locates each file's lines.
     typedef struct {
    @@ -111,18 +113,25 @@ typedef struct {
         char         **frmPaths;        // stb_ds array of malloc'd paths
         char         **frmTexts;        // stb_ds array of malloc'd .frm texts
         BasSourceMapT *map;             // stb_ds array, one entry per file
    -    int32_t        fileCount;       // modules + forms
    +    int32_t        fileCount;       // modules + classes + forms
         bool           optionExplicit;  // [Settings] OptionExplicit
         char           error[BAS_BUILD_ERROR_LEN];
     } BasProjectSourceT;
     
    +// What basBuildAppendFile wraps a file's text in.
    +typedef enum {
    +    BAS_BUILD_WRAP_NONE,    // a module: the text as it is
    +    BAS_BUILD_WRAP_FORM,    // a form's code: BEGINFORM "name" / ENDFORM
    +    BAS_BUILD_WRAP_CLASS    // a class module's code: BEGINCLASS "name" / ENDCLASS
    +} BasBuildWrapE;
    +
     // Appends one file's text to *ps (a zeroed struct is a valid empty
    -// project) and makes sure it ends on a newline.  A non-empty formName
    -// wraps the text in BEGINFORM "name" / ENDFORM.  textLine and textLines
    -// (either may be NULL) receive the 1-based line the text starts on and
    -// the number of lines it occupies, wrapper lines excluded -- what an IDE
    -// source map needs.  Returns false with ps->error set on failure.
    -bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, const char *formName, int32_t *textLine, int32_t *textLines);
    +// project) and makes sure it ends on a newline, wrapped as wrap says in
    +// the directives naming name.  textLine and textLines (either may be
    +// NULL) receive the 1-based line the text starts on and the number of
    +// lines it occupies, wrapper lines excluded -- what an IDE source map
    +// needs.  Returns false with ps->error set on failure.
    +bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, BasBuildWrapE wrap, const char *name, int32_t *textLine, int32_t *textLines);
     
     // Reads the source files named by an open .dbp (paths relative to
     // projectDir) into *out.  Returns false with out->error set on failure;
    diff --git a/src/apps/kpunch/dvxbasic/basRes.h b/src/apps/kpunch/dvxbasic/basRes.h
    index d094fc15..1955cea8 100644
    --- a/src/apps/kpunch/dvxbasic/basRes.h
    +++ b/src/apps/kpunch/dvxbasic/basRes.h
    @@ -34,6 +34,7 @@
     #include 
     #include  // NULL
     #include 
    +#include   // memchr, strlen
     #include  // strncasecmp
     
     #include "formrt/frmParser.h" // FRM_KW_* .frm keyword spellings
    @@ -65,8 +66,29 @@ const char *dvxSkipWs(const char *s);
     #define BAS_INI_KEY_STARTUPFORM     "StartupForm"
     #define BAS_INI_KEY_OPTIONEXPLICIT  "OptionExplicit"
     #define BAS_INI_SECTION_MODULES     "Modules"
    +#define BAS_INI_SECTION_CLASSES     "Classes"
     #define BAS_INI_SECTION_FORMS       "Forms"
     
    +// ------------------------------------------------------------
    +// .cls class module files
    +// ------------------------------------------------------------
    +//
    +// A class module is BASIC code with an optional VB-style header in front:
    +//
    +//   VERSION DVX 1.00 CLASS
    +//   Attribute VB_Name = "Stack"
    +//
    +// Blank lines, a VERSION line, a BEGIN ... END block and Attribute VB_xxx
    +// lines before the first line of code make up the header.  The class
    +// takes its name from Attribute VB_Name, else from the file name.
    +
    +#define BAS_CLS_KW_VERSION   "VERSION"
    +#define BAS_CLS_KW_BEGIN     "BEGIN"
    +#define BAS_CLS_KW_ATTRIBUTE "Attribute"
    +#define BAS_CLS_ATTR_PREFIX  "VB_"
    +#define BAS_CLS_ATTR_NAME    "VB_Name"
    +#define BAS_CLS_EXT          ".cls"
    +
     // ------------------------------------------------------------
     // Resource names inside a compiled .app DXE
     // ------------------------------------------------------------
    @@ -114,12 +136,122 @@ const char *dvxSkipWs(const char *s);
     // .frm scanners (shared by bascomp, basstub, basBuild and the obfuscator)
     // ------------------------------------------------------------
     
    +static inline int32_t basClassCodePos(const char *text, int32_t len);
    +static inline bool    basClassHeaderLine(const char *s, int32_t len);
    +static inline bool    basExtractClassName(const char *text, int32_t len, const char *path, char *nameBuf, int32_t nameBufSize);
     static inline bool    basExtractFormName(const char *frmText, char *nameBuf, int32_t nameBufSize);
     static inline int32_t basFindFormEndPos(const char *text, int32_t len);
    +static inline bool    basLineIsWord(const char *s, int32_t len, const char *word);
     static inline int32_t basMatchBeginForm(const char *s, int32_t len);
     static inline int32_t basMatchKeywordWs(const char *s, int32_t len, const char *kw, int32_t kwLen);
     
     
    +// Offset of the first line of code in a .cls text (just past its header;
    +// 0 when it has none).
    +static inline int32_t basClassCodePos(const char *text, int32_t len) {
    +    int32_t pos   = 0;
    +    int32_t depth = 0;
    +
    +    while (pos < len) {
    +        int32_t lineStart = pos;
    +
    +        while (pos < len && text[pos] != '\n') {
    +            pos++;
    +        }
    +
    +        int32_t lineEnd = pos;
    +
    +        if (pos < len) {
    +            pos++;
    +        }
    +
    +        while (lineEnd > lineStart && (text[lineEnd - 1] == '\r' || text[lineEnd - 1] == ' ' || text[lineEnd - 1] == '\t')) {
    +            lineEnd--;
    +        }
    +
    +        int32_t l = lineStart;
    +
    +        while (l < lineEnd && (text[l] == ' ' || text[l] == '\t')) {
    +            l++;
    +        }
    +
    +        // Inside BEGIN ... END everything belongs to the header
    +        if (basLineIsWord(text + l, lineEnd - l, BAS_CLS_KW_BEGIN)) {
    +            depth++;
    +        } else if (depth > 0 && frmIsEndLine(text + l, lineEnd - l)) {
    +            depth--;
    +        } else if (depth == 0 && l < lineEnd && !basClassHeaderLine(text + l, lineEnd - l)) {
    +            return lineStart;
    +        }
    +    }
    +
    +    return len;
    +}
    +
    +
    +// A .cls header line outside BEGIN ... END: VERSION ... or Attribute
    +// VB_xxx (a member's Attribute Name.VB_xxx belongs to the code).
    +static inline bool basClassHeaderLine(const char *s, int32_t len) {
    +    if (basLineIsWord(s, len, BAS_CLS_KW_VERSION)) {
    +        return true;
    +    }
    +
    +    int32_t a = basMatchKeywordWs(s, len, BAS_CLS_KW_ATTRIBUTE, (int32_t)sizeof(BAS_CLS_KW_ATTRIBUTE) - 1);
    +
    +    return a >= 0 && len - a > (int32_t)sizeof(BAS_CLS_ATTR_PREFIX) - 1 && strncasecmp(s + a, BAS_CLS_ATTR_PREFIX, sizeof(BAS_CLS_ATTR_PREFIX) - 1) == 0;
    +}
    +
    +
    +// The class name of a .cls text: its Attribute VB_Name = "name" when the
    +// header has one, else the base name of path (without directory and
    +// extension).  Returns false when neither gives a name.
    +static inline bool basExtractClassName(const char *text, int32_t len, const char *path, char *nameBuf, int32_t nameBufSize) {
    +    int32_t codePos = basClassCodePos(text, len);
    +    int32_t attrLen = (int32_t)sizeof(BAS_CLS_ATTR_NAME) - 1;
    +
    +    nameBuf[0] = '\0';
    +
    +    for (int32_t pos = 0; pos < codePos; pos++) {
    +        if ((pos == 0 || text[pos - 1] == ' ' || text[pos - 1] == '\t') && codePos - pos > attrLen && strncasecmp(text + pos, BAS_CLS_ATTR_NAME, attrLen) == 0) {
    +            const char *q = memchr(text + pos, '"', codePos - pos);
    +
    +            if (q) {
    +                int32_t ni = 0;
    +
    +                for (q++; q < text + codePos && *q != '"' && *q != '\n' && ni < nameBufSize - 1; q++) {
    +                    nameBuf[ni++] = *q;
    +                }
    +
    +                nameBuf[ni] = '\0';
    +            }
    +
    +            break;
    +        }
    +    }
    +
    +    if (nameBuf[0] == '\0' && path) {
    +        const char *base = path;
    +
    +        for (const char *c = path; *c; c++) {
    +            if (*c == '/' || *c == '\\' || *c == ':') {
    +                base = c + 1;
    +            }
    +        }
    +
    +        int32_t ni = 0;
    +
    +        while (base[ni] && base[ni] != '.' && ni < nameBufSize - 1) {
    +            nameBuf[ni] = base[ni];
    +            ni++;
    +        }
    +
    +        nameBuf[ni] = '\0';
    +    }
    +
    +    return nameBuf[0] != '\0';
    +}
    +
    +
     // Extract the form name from a "Begin Form " line in .frm text.
     // Writes a NUL-terminated name into nameBuf (max nameBufSize bytes,
     // including the terminator) and returns true if a name was found.
    @@ -233,6 +365,15 @@ static inline int32_t basFindFormEndPos(const char *text, int32_t len) {
     }
     
     
    +// True when s[0..len) (left-trimmed) starts with word followed by the end
    +// of the line or a space or tab.
    +static inline bool basLineIsWord(const char *s, int32_t len, const char *word) {
    +    int32_t wordLen = (int32_t)strlen(word);
    +
    +    return len >= wordLen && strncasecmp(s, word, wordLen) == 0 && (len == wordLen || s[wordLen] == ' ' || s[wordLen] == '\t');
    +}
    +
    +
     // Match "BeginForm" at s[0..len) (s already left-trimmed).  The
     // single definition of the Begin Form syntax for basExtractFormName and
     // basFindFormEndPos.  Returns the offset of the form name, or -1.
    diff --git a/src/apps/kpunch/dvxbasic/compiler/basEvents.h b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
    index b1fd13cf..70962764 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/basEvents.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
    @@ -74,6 +74,8 @@ typedef enum {
     #define BAS_EVT_SCROLL      "Scroll"
     #define BAS_EVT_REPOSITION  "Reposition"
     #define BAS_EVT_VALIDATE    "Validate"
    +#define BAS_EVT_DRAGDROP    "DragDrop"
    +#define BAS_EVT_DRAGOVER    "DragOver"
     
     #define BAS_EVENT_LIST(X) \
         X(BAS_EVT_LOAD,        BAS_EVT_SCOPE_FORM, "") \
    @@ -96,7 +98,25 @@ typedef enum {
         X(BAS_EVT_MOUSEMOVE,   BAS_EVT_SCOPE_BOTH, "Button As Integer, X As Integer, Y As Integer") \
         X(BAS_EVT_SCROLL,      BAS_EVT_SCOPE_CTRL, "Delta As Integer") \
         X(BAS_EVT_REPOSITION,  BAS_EVT_SCOPE_CTRL, "") \
    -    X(BAS_EVT_VALIDATE,    BAS_EVT_SCOPE_CTRL, "Cancel As Integer")
    +    X(BAS_EVT_VALIDATE,    BAS_EVT_SCOPE_CTRL, "Cancel As Integer") \
    +    X(BAS_EVT_DRAGDROP,    BAS_EVT_SCOPE_BOTH, "Source As Control, X As Integer, Y As Integer") \
    +    X(BAS_EVT_DRAGOVER,    BAS_EVT_SCOPE_BOTH, "Source As Control, X As Integer, Y As Integer, State As Integer")
    +
    +// Drag and drop values, shared by the compiler's predefined vb*
    +// constants and the form runtime: the DragMode property, the Drag
    +// method's action and DragOver's State.
    +#define BAS_DRAGMODE_MANUAL    0   // vbManual: the program calls Drag
    +#define BAS_DRAGMODE_AUTOMATIC 1   // vbAutomatic: pressing the control starts a drag
    +#define BAS_DRAG_CANCEL        0   // Drag 0: stop without dropping
    +#define BAS_DRAG_BEGIN         1   // vbBeginDrag (the default)
    +#define BAS_DRAG_END           2   // vbEndDrag: drop where the mouse is
    +#define BAS_DRAG_ENTER         0   // vbEnter: the drag came onto the object
    +#define BAS_DRAG_LEAVE         1   // vbLeave: the drag went off it
    +#define BAS_DRAG_OVER          2   // vbOver: the drag moved over it
    +
    +// Clipboard formats (Clipboard.GetFormat / GetText / SetText): the DVX
    +// clipboard holds text.
    +#define BAS_CF_TEXT            1   // vbCFText
     
     // One row of BAS_EVENT_LIST, for consumers that want a runtime table
     // (e.g. `static const BasEventInfoT sEvents[] = { BAS_EVENT_LIST(BAS_EVENT_ROW) };`).
    diff --git a/src/apps/kpunch/dvxbasic/compiler/basKeywords.h b/src/apps/kpunch/dvxbasic/compiler/basKeywords.h
    index 524c6d29..c2a78903 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/basKeywords.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/basKeywords.h
    @@ -28,15 +28,18 @@
     // BAS_KEYWORD(text, token, cls) before including this header to expand
     // the list; text is the uppercase word, token its TOK_* value (ignore it
     // outside the compiler) and cls its BasKeywordClassE.  The entries are in
    -// strcmp order, so an expanded table can be searched with bsearch.
    +// strcmp order, so an expanded table can be searched with bsearch.  An
    +// entry whose token is TOK_IDENT is not reserved -- the lexer returns it
    +// as an identifier and a program may use the name for its own things --
    +// but colorizers still show it (built-in type names, Empty, Null, EACH).
     
     #ifndef DVXBASIC_BAS_KEYWORDS_H
     #define DVXBASIC_BAS_KEYWORDS_H
     
     typedef enum {
         BAS_KW_CLASS_OTHER   = 0,  // control-flow / statement / operator keyword
    -    BAS_KW_CLASS_TYPE    = 1,  // INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
    -    BAS_KW_CLASS_LITERAL = 2   // TRUE, FALSE, NOTHING
    +    BAS_KW_CLASS_TYPE    = 1,  // INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN, DATE, VARIANT, OBJECT, COLLECTION
    +    BAS_KW_CLASS_LITERAL = 2   // TRUE, FALSE, NOTHING, EMPTY, NULL, EMPTY, NULL
     } BasKeywordClassE;
     
     #endif // DVXBASIC_BAS_KEYWORDS_H
    @@ -57,12 +60,14 @@ BAS_KEYWORD("CASE",          TOK_CASE,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("CHDIR",         TOK_CHDIR,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("CHDRIVE",       TOK_CHDRIVE,       BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("CLOSE",         TOK_CLOSE,         BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("COLLECTION",    TOK_IDENT,         BAS_KW_CLASS_TYPE)
     BAS_KEYWORD("CONST",         TOK_CONST,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("CREATECONTROL", TOK_CREATECONTROL, BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("CREATEFORM",    TOK_CREATEFORM,    BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("CURDIR",        TOK_CURDIR,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("CURDIR$",       TOK_CURDIR,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("DATA",          TOK_DATA,          BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("DATE",          TOK_IDENT,         BAS_KW_CLASS_TYPE)
     BAS_KEYWORD("DECLARE",       TOK_DECLARE,       BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("DEF",           TOK_DEF,           BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("DEFDBL",        TOK_DEFDBL,        BAS_KW_CLASS_OTHER)
    @@ -76,9 +81,12 @@ BAS_KEYWORD("DIR$",          TOK_DIR,           BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("DO",            TOK_DO,            BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("DOEVENTS",      TOK_DOEVENTS,      BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("DOUBLE",        TOK_DOUBLE,        BAS_KW_CLASS_TYPE)
    +BAS_KEYWORD("EACH",          TOK_IDENT,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("ELSE",          TOK_ELSE,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("ELSEIF",        TOK_ELSEIF,        BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("EMPTY",         TOK_IDENT,         BAS_KW_CLASS_LITERAL)
     BAS_KEYWORD("END",           TOK_END,           BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("ENUM",          TOK_ENUM,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("EOF",           TOK_EOF_KW,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("EQV",           TOK_EQV,           BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("ERASE",         TOK_ERASE,         BAS_KW_CLASS_OTHER)
    @@ -111,6 +119,7 @@ BAS_KEYWORD("IS",            TOK_IS,            BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("KILL",          TOK_KILL,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("LBOUND",        TOK_LBOUND,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("LET",           TOK_LET,           BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("LIBRARY",       TOK_IDENT,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("LINE",          TOK_LINE,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("LOAD",          TOK_LOAD,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("LONG",          TOK_LONG,          BAS_KW_CLASS_TYPE)
    @@ -121,18 +130,25 @@ BAS_KEYWORD("MKDIR",         TOK_MKDIR,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("MOD",           TOK_MOD,           BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("MSGBOX",        TOK_MSGBOX,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("NAME",          TOK_NAME,          BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("NEW",           TOK_NEW,           BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("NEXT",          TOK_NEXT,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("NOT",           TOK_NOT,           BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("NOTHING",       TOK_NOTHING,       BAS_KW_CLASS_LITERAL)
    +BAS_KEYWORD("NULL",          TOK_IDENT,         BAS_KW_CLASS_LITERAL)
    +BAS_KEYWORD("OBJECT",        TOK_IDENT,         BAS_KW_CLASS_TYPE)
     BAS_KEYWORD("ON",            TOK_ON,            BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("OPEN",          TOK_OPEN,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("OPTION",        TOK_OPTION,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("OPTIONAL",      TOK_OPTIONAL,      BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("OR",            TOK_OR,            BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("OUTPUT",        TOK_OUTPUT,        BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("PARAMARRAY",    TOK_PARAMARRAY,    BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("PLAY",          TOK_PLAY,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("PRESERVE",      TOK_PRESERVE,      BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("PRINT",         TOK_PRINT,         BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("PRIVATE",       TOK_PRIVATE,       BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("PROPERTY",      TOK_PROPERTY,      BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("PUBLIC",        TOK_PUBLIC,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("PUT",           TOK_PUT,           BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("RANDOM",        TOK_RANDOM,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("RANDOMIZE",     TOK_RANDOMIZE,     BAS_KW_CLASS_OTHER)
    @@ -157,6 +173,7 @@ BAS_KEYWORD("SLEEP",         TOK_SLEEP,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("SOUND",         TOK_SOUND,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("STATIC",        TOK_STATIC,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("STEP",          TOK_STEP,          BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("STOP",          TOK_IDENT,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("STRING",        TOK_STRING_KW,     BAS_KW_CLASS_TYPE)
     BAS_KEYWORD("SUB",           TOK_SUB,           BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("SWAP",          TOK_SWAP,          BAS_KW_CLASS_OTHER)
    @@ -165,9 +182,11 @@ BAS_KEYWORD("TIMER",         TOK_TIMER,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("TO",            TOK_TO,            BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("TRUE",          TOK_TRUE_KW,       BAS_KW_CLASS_LITERAL)
     BAS_KEYWORD("TYPE",          TOK_TYPE,          BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("TYPEOF",        TOK_TYPEOF,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("UBOUND",        TOK_UBOUND,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("UNLOAD",        TOK_UNLOAD,        BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("UNTIL",         TOK_UNTIL,         BAS_KW_CLASS_OTHER)
    +BAS_KEYWORD("VARIANT",       TOK_IDENT,         BAS_KW_CLASS_TYPE)
     BAS_KEYWORD("WEND",          TOK_WEND,          BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("WHILE",         TOK_WHILE,         BAS_KW_CLASS_OTHER)
     BAS_KEYWORD("WITH",          TOK_WITH,          BAS_KW_CLASS_OTHER)
    diff --git a/src/apps/kpunch/dvxbasic/compiler/codegen.c b/src/apps/kpunch/dvxbasic/compiler/codegen.c
    index 87e625c9..252938c5 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/codegen.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/codegen.c
    @@ -30,6 +30,7 @@
     #include 
     #include 
     #include 
    +#include 
     
     
     // Function prototypes (alphabetical)
    @@ -43,6 +44,7 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
     void basCodeGenFree(BasCodeGenT *cg);
     void basCodeGenInit(BasCodeGenT *cg);
     void basCodeGenInsert(BasCodeGenT *cg, int32_t pos, const uint8_t *bytes, int32_t len);
    +void basCodeGenTruncate(BasCodeGenT *cg, int32_t codeLen, int32_t constCount);
     int32_t basCodePos(const BasCodeGenT *cg);
     void basEmit16(BasCodeGenT *cg, int16_t v);
     void basEmit32(BasCodeGenT *cg, int32_t v);
    @@ -51,10 +53,12 @@ static void basEmitBytes(BasCodeGenT *cg, const void *data, int32_t len);
     void basEmitDouble(BasCodeGenT *cg, double v);
     void basEmitU16(BasCodeGenT *cg, uint16_t v);
     void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
    +static bool buildClasses(BasModuleT *mod, const BasSymTabT *tab);
     static uint32_t constantHash(const char *text, int32_t len);
     static bool copyDebugInfo(BasModuleT *mod, const BasCodeGenT *cg);
     static bool isEmittableProc(const BasSymbolT *s);
     static bool needsGlobalInit(const BasSymbolT *s);
    +static int32_t procIndexAt(const BasModuleT *mod, int32_t codeAddr);
     
     
     uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
    @@ -252,7 +256,7 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
         }
     
         if (procCount == 0) {
    -        return mod;
    +        return buildClasses(mod, tab) ? mod : NULL;
         }
     
         mod->procs = (BasProcEntryT *)malloc(procCount * sizeof(BasProcEntryT));
    @@ -300,7 +304,7 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
         }
     
         mod->procCount = idx;
    -    return mod;
    +    return buildClasses(mod, tab) ? mod : NULL;
     }
     
     
    @@ -345,6 +349,24 @@ void basCodeGenInsert(BasCodeGenT *cg, int32_t pos, const uint8_t *bytes, int32_
     }
     
     
    +void basCodeGenTruncate(BasCodeGenT *cg, int32_t codeLen, int32_t constCount) {
    +    arrsetlen(cg->code, codeLen);
    +    cg->codeLen = codeLen;
    +
    +    // Newest first: an entry being dropped is always the head of its
    +    // bucket once every newer entry is gone.
    +    for (int32_t i = cg->constCount - 1; i >= constCount; i--) {
    +        cg->constBuckets[cg->constHashes[i] & (BAS_CONST_BUCKETS - 1)] = cg->constNext[i];
    +        basStringUnref(cg->constants[i]);
    +    }
    +
    +    arrsetlen(cg->constants, constCount);
    +    arrsetlen(cg->constHashes, constCount);
    +    arrsetlen(cg->constNext, constCount);
    +    cg->constCount = constCount;
    +}
    +
    +
     int32_t basCodePos(const BasCodeGenT *cg) {
         return cg->codeLen;
     }
    @@ -398,6 +420,74 @@ void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val) {
     }
     
     
    +// Copies the class modules into mod->classes: each class's size, set-up
    +// code and public members, procedures by their index in the (sorted)
    +// procedure table.  Frees mod and returns false when memory runs out.
    +static bool buildClasses(BasModuleT *mod, const BasSymTabT *tab) {
    +    int32_t count = (int32_t)arrlen(tab->classes);
    +
    +    if (count == 0) {
    +        return true;
    +    }
    +
    +    mod->classes = (BasClassInfoT *)calloc(count, sizeof(BasClassInfoT));
    +
    +    if (!mod->classes) {
    +        basModuleFree(mod);
    +        return false;
    +    }
    +
    +    mod->classCount = count;
    +
    +    for (int32_t c = 0; c < count; c++) {
    +        const BasClassDefT *def  = &tab->classes[c];
    +        BasClassInfoT      *info = &mod->classes[c];
    +        int32_t             n    = (int32_t)arrlen(def->members);
    +
    +        snprintf(info->name, sizeof(info->name), "%s", def->name);
    +        info->varCount      = def->varCount;
    +        info->initCodeAddr  = def->initCodeAddr;
    +        info->terminateProc = def->terminateSym ? procIndexAt(mod, def->terminateSym->codeAddr) : -1;
    +
    +        if (n == 0) {
    +            continue;
    +        }
    +
    +        info->members = (BasClassMemberT *)calloc(n, sizeof(BasClassMemberT));
    +
    +        if (!info->members) {
    +            basModuleFree(mod);
    +            return false;
    +        }
    +
    +        info->memberCount = n;
    +
    +        for (int32_t i = 0; i < n; i++) {
    +            const BasClassMemberDefT *md  = &def->members[i];
    +            const BasSymbolT         *sym = md->sym;
    +            BasClassMemberT          *m   = &info->members[i];
    +
    +            snprintf(m->name, sizeof(m->name), "%s", md->name);
    +            m->kind           = md->kind;
    +            m->dataType       = sym->dataType;
    +            m->requiredParams = (uint8_t)sym->requiredParams;
    +            m->index          = (md->kind == BAS_MEMBER_VAR) ? sym->index : procIndexAt(mod, sym->codeAddr);
    +            memcpy(m->paramTypes, sym->paramTypes, DVX_MIN(sizeof(m->paramTypes), sizeof(sym->paramTypes)));
    +
    +            if (strcasecmp(md->name, def->defaultMember) == 0) {
    +                m->flags |= BAS_MEMBER_DEFAULT;
    +            }
    +
    +            if (sym->hasParamArray) {
    +                m->flags |= BAS_MEMBER_PARAMARRAY;
    +            }
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
     // FNV-1a over the constant's bytes (case-sensitive: constants are exact).
     static uint32_t constantHash(const char *text, int32_t len) {
         uint32_t h = BAS_FNV1A_OFFSET;
    @@ -483,3 +573,25 @@ static bool isEmittableProc(const BasSymbolT *s) {
     static bool needsGlobalInit(const BasSymbolT *s) {
         return s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray;
     }
    +
    +
    +// Index into mod->procs (sorted by code address) of the procedure at
    +// codeAddr, or -1.
    +static int32_t procIndexAt(const BasModuleT *mod, int32_t codeAddr) {
    +    int32_t lo = 0;
    +    int32_t hi = mod->procCount - 1;
    +
    +    while (lo <= hi) {
    +        int32_t mid = lo + (hi - lo) / 2;
    +
    +        if (mod->procs[mid].codeAddr < codeAddr) {
    +            lo = mid + 1;
    +        } else if (mod->procs[mid].codeAddr > codeAddr) {
    +            hi = mid - 1;
    +        } else {
    +            return mid;
    +        }
    +    }
    +
    +    return -1;
    +}
    diff --git a/src/apps/kpunch/dvxbasic/compiler/codegen.h b/src/apps/kpunch/dvxbasic/compiler/codegen.h
    index 6c5ec57e..6f8ccc94 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/codegen.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/codegen.h
    @@ -104,6 +104,11 @@ void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
     // Insert len bytes at pos, moving the code after it up.
     void basCodeGenInsert(BasCodeGenT *cg, int32_t pos, const uint8_t *bytes, int32_t len);
     
    +// Drop the code emitted after codeLen and the constants added after
    +// constCount (both from an earlier basCodePos / constCount), e.g. the
    +// code of a constant expression once it has been evaluated.
    +void basCodeGenTruncate(BasCodeGenT *cg, int32_t codeLen, int32_t constCount);
    +
     // Add a string to the constant pool. Returns the pool index.
     uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
     
    diff --git a/src/apps/kpunch/dvxbasic/compiler/compact.c b/src/apps/kpunch/dvxbasic/compiler/compact.c
    index bb432435..7cd0cba3 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/compact.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/compact.c
    @@ -34,7 +34,8 @@
     // Address references:
     //   - BasProcEntryT::codeAddr (absolute)
     //   - BasFormVarInfoT::initCodeAddr (absolute, negative = no init)
    -//   - OP_CALL operand (absolute uint16)
    +//   - BasClassInfoT::initCodeAddr (absolute, negative = none)
    +//   - OP_CALL / OP_CALL_ME operand (absolute uint16)
     //   - OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE / OP_GOSUB operand (relative int16)
     //   - OP_FOR_INIT / OP_FOR_NEXT offset operand (relative int16)
     //   - OP_ON_ERROR handler operand (relative int16; 0 = disable, not remapped)
    @@ -156,7 +157,8 @@ int32_t basCompactBytecode(BasModuleT *mod) {
             memcpy(newCode + newPc, oldCode + oldPc, instSize);
     
             switch (op) {
    -            case OP_CALL: {
    +            case OP_CALL:
    +            case OP_CALL_ME: {
                     uint16_t oldAddr = basReadU16LE(oldCode + oldPc + BAS_ADDR_OPERAND_OFFSET);
     
                     ok = remapAbsU16(newCode, newPc, BAS_ADDR_OPERAND_OFFSET, oldAddr, remap, oldCodeLen);
    @@ -240,6 +242,21 @@ int32_t basCompactBytecode(BasModuleT *mod) {
             mod->formVarInfo[i].initCodeLen  = newEnd - newAddr;
         }
     
    +    // Rewrite class set-up code addresses (negative: none).
    +    for (int32_t i = 0; i < mod->classCount; i++) {
    +        int32_t oldAddr = mod->classes[i].initCodeAddr;
    +
    +        if (oldAddr > oldCodeLen) {
    +            free(newCode);
    +            free(remap);
    +            return 0;
    +        }
    +
    +        if (oldAddr >= 0) {
    +            mod->classes[i].initCodeAddr = remap[oldAddr];
    +        }
    +    }
    +
         // Rewrite entry point
         if (mod->entryPoint < 0 || mod->entryPoint > oldCodeLen) {
             free(newCode);
    diff --git a/src/apps/kpunch/dvxbasic/compiler/lexer.c b/src/apps/kpunch/dvxbasic/compiler/lexer.c
    index 40ae2d08..347eacd3 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/lexer.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/lexer.c
    @@ -30,6 +30,7 @@
     #include "lexer.h"
     #include "opcodes.h"
     #include "dvxTypes.h"
    +#include "../runtime/dates.h"
     
     #include 
     #include 
    @@ -62,6 +63,11 @@ static const KeywordEntryT sKeywords[] = {
     #define LEX_BIN_BITS 1
     #define LEX_HEX_BITS 4
     
    +// Date literals: the characters allowed between the '#'s besides letters
    +// and digits, and those that mark the text as a date or time.
    +#define LEX_DATE_LITERAL_CHARS " /-:,"
    +#define LEX_DATE_SEPARATORS    "/-:"
    +
     // First value of the hex letter digits A-F.
     #define LEX_HEX_LETTER_BASE 10
     
    @@ -120,6 +126,7 @@ static char peekNext(const BasLexerT *lex);
     static void setError(BasLexerT *lex, const char *msg);
     static void skipLineComment(BasLexerT *lex);
     static void skipWhitespace(BasLexerT *lex);
    +static bool tokenizeDateLiteral(BasLexerT *lex);
     static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex);
     static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex);
     static BasTokenTypeE tokenizeNumber(BasLexerT *lex);
    @@ -296,6 +303,12 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
             }
         }
     
    +    // Date literal #...#; any other '#' is the file-number sign
    +    if (c == '#' && tokenizeDateLiteral(lex)) {
    +        lex->token.type = TOK_DATE_LIT;
    +        return TOK_DATE_LIT;
    +    }
    +
         // Identifier or keyword
         if (basIsIdentChar(c) && !isdigit((unsigned char)c)) {
             lex->token.type = tokenizeIdentOrKeyword(lex);
    @@ -454,6 +467,8 @@ const char *basTokenName(BasTokenTypeE type) {
                 return "float";
             case TOK_STRING_LIT:
                 return "string";
    +        case TOK_DATE_LIT:
    +            return "date";
             case TOK_IDENT:
                 return "identifier";
             case TOK_DOT:
    @@ -695,6 +710,53 @@ static void skipWhitespace(BasLexerT *lex) {
     }
     
     
    +// A date literal at the current '#': the text up to the closing '#' on
    +// the same line must read as a date (see basDateParse) and hold a date
    +// or time separator or a word, so file numbers such as CLOSE #1, 2, #3
    +// never read as one.  On success the literal is consumed, the token text
    +// holds what was between the '#'s and dblVal the serial; otherwise
    +// nothing is consumed.
    +static bool tokenizeDateLiteral(BasLexerT *lex) {
    +    int32_t end          = lex->pos + 1;
    +    bool    hasSeparator = false;
    +
    +    while (end < lex->sourceLen && lex->source[end] != '#') {
    +        char c = lex->source[end];
    +
    +        if (c == '\0' || end - lex->pos > BAS_MAX_STRING_LEN || !(isalnum((unsigned char)c) || strchr(LEX_DATE_LITERAL_CHARS, c) != NULL)) {
    +            return false;
    +        }
    +
    +        hasSeparator = hasSeparator || isalpha((unsigned char)c) || strchr(LEX_DATE_SEPARATORS, c) != NULL;
    +        end++;
    +    }
    +
    +    int32_t len = end - lex->pos - 1;
    +    char    text[BAS_MAX_TOKEN_LEN];
    +    double  serial;
    +
    +    if (end >= lex->sourceLen || !hasSeparator) {
    +        return false;
    +    }
    +
    +    memcpy(text, lex->source + lex->pos + 1, len);
    +    text[len] = '\0';
    +
    +    if (!basDateParse(text, &serial)) {
    +        return false;
    +    }
    +
    +    while (lex->pos <= end) {
    +        advance(lex);
    +    }
    +
    +    memcpy(lex->token.text, text, len + 1);
    +    lex->token.textLen = len;
    +    lex->token.dblVal  = serial;
    +    return true;
    +}
    +
    +
     static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
         advance(lex);                       // skip &
         char base = basAsciiUpper(peek(lex));
    diff --git a/src/apps/kpunch/dvxbasic/compiler/lexer.h b/src/apps/kpunch/dvxbasic/compiler/lexer.h
    index 73b7b534..201274fc 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/lexer.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/lexer.h
    @@ -46,6 +46,7 @@ typedef enum {
         TOK_LONG_LIT,          // long literal (123&)
         TOK_FLOAT_LIT,         // float literal (3.14, 1.5E10)
         TOK_STRING_LIT,        // "string literal"
    +    TOK_DATE_LIT,          // #1/2/2000# date literal (dblVal holds the serial)
     
         // Identifiers and symbols
         TOK_IDENT,             // variable/function name
    @@ -102,6 +103,7 @@ typedef enum {
         TOK_ELSE,
         TOK_ELSEIF,
         TOK_END,
    +    TOK_ENUM,
         TOK_EOF_KW,            // EOF (keyword, not end-of-file)
         TOK_EQV,
         TOK_ERASE,
    @@ -134,6 +136,7 @@ typedef enum {
         TOK_MOD,
         TOK_INPUTBOX,
         TOK_MSGBOX,
    +    TOK_NEW,
         TOK_NEXT,
         TOK_NOT,
         TOK_NOTHING,
    @@ -143,9 +146,13 @@ typedef enum {
         TOK_OPTION,
         TOK_OR,
         TOK_OUTPUT,
    +    TOK_PARAMARRAY,
         TOK_PLAY,
         TOK_PRESERVE,
         TOK_PRINT,
    +    TOK_PRIVATE,
    +    TOK_PROPERTY,
    +    TOK_PUBLIC,
         TOK_PUT,
         TOK_RANDOMIZE,
         TOK_READ,
    @@ -177,6 +184,7 @@ typedef enum {
         TOK_TO,
         TOK_TRUE_KW,
         TOK_TYPE,
    +    TOK_TYPEOF,
         TOK_UBOUND,
         TOK_UNLOAD,
         TOK_UNTIL,
    diff --git a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c
    index 9cebb2db..91b4313f 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c
    @@ -110,6 +110,18 @@ bool basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
             }
         }
     
    +    // Class and class member names are found by their spelling at run
    +    // time (obj.Member), so a control that shares one keeps its name.
    +    for (int32_t c = 0; ok && c < mod->classCount; c++) {
    +        const BasClassInfoT *cls = &mod->classes[c];
    +
    +        ok = nameMapAdd(&reserved, cls->name) != NULL;
    +
    +        for (int32_t m = 0; ok && m < cls->memberCount; m++) {
    +            ok = nameMapAdd(&reserved, cls->members[m].name) != NULL;
    +        }
    +    }
    +
         for (int32_t i = 0; ok && i < names.count; i++) {
             if (!nameMapLookup(&reserved, names.entries[i].orig)) {
                 ok = nameMapAdd(&map, names.entries[i].orig) != NULL;
    diff --git a/src/apps/kpunch/dvxbasic/compiler/obfuscate.h b/src/apps/kpunch/dvxbasic/compiler/obfuscate.h
    index db93cc95..efcc2d29 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/obfuscate.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/obfuscate.h
    @@ -41,8 +41,9 @@ typedef struct {
     // Obfuscate form/control names in the module and all .frm texts.
     //
     // Reads original names from the Begin declarations in each .frm,
    -// generates C1..Cn (skipping any name that is also a control type or a
    -// property key, since bytecode cannot tell those apart), then rewrites:
    +// generates C1..Cn (skipping any name that is also a control type, a
    +// property key, a class or a class member, since bytecode cannot tell
    +// those apart), then rewrites:
     //   - The .frm text (Begin-line names and property values naming a
     //     control, stripping the trailing BASIC code section after the
     //     outer form closes)
    diff --git a/src/apps/kpunch/dvxbasic/compiler/opcodes.h b/src/apps/kpunch/dvxbasic/compiler/opcodes.h
    index f2950367..97a7387c 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/opcodes.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/opcodes.h
    @@ -42,7 +42,8 @@
     typedef enum {
         SCOPE_GLOBAL = 0,
         SCOPE_LOCAL  = 1,
    -    SCOPE_FORM   = 2        // per-form variable (persists while form is loaded)
    +    SCOPE_FORM   = 2,       // per-form variable (persists while form is loaded)
    +    SCOPE_MEMBER = 3        // class member variable of the running procedure's Me
     } BasScopeE;
     
     // ============================================================
    @@ -74,6 +75,11 @@ typedef enum {
     #define BAS_TYPE_OBJECT    8  // opaque host object (form, control, etc.)
     #define BAS_TYPE_REF       9  // ByRef pointer to a BasValueT slot
     #define BAS_TYPE_ELEM_REF  10 // ByRef array element: counted BasArrayT* + flat index
    +#define BAS_TYPE_DATE      11 // Double serial date (dates.h), held in dblVal
    +#define BAS_TYPE_EMPTY     12 // Variant that holds nothing yet (Empty)
    +#define BAS_TYPE_NULL      13 // Variant Null (no valid data)
    +#define BAS_TYPE_INSTANCE  14 // ref-counted class instance or Collection (BasObjectT)
    +#define BAS_TYPE_VARIANT   15 // declared type only (AS VARIANT, array element type): holds any value, never a value's tag
     
     // ============================================================
     // Stack operations
    @@ -82,6 +88,7 @@ typedef enum {
     #define OP_NOP             0x00
     #define OP_PUSH_INT16      0x01  // [int16] push 16-bit integer
     #define OP_PUSH_INT32      0x02  // [int32] push 32-bit integer
    +#define OP_PUSH_SPECIAL    0x03  // [uint8 BasSpecialValE] push Empty, Null or Nothing
     #define OP_PUSH_FLT64      0x04  // [float64] push 64-bit float
     #define OP_PUSH_STR        0x05  // [uint16 idx] push string from constant pool
     #define OP_PUSH_TRUE       0x06  // push boolean True (-1)
    @@ -105,6 +112,9 @@ typedef enum {
     #define OP_PUSH_LOCAL_ADDR 0x1A  // [uint16 idx] push address of local (for ByRef)
     #define OP_PUSH_GLOBAL_ADDR 0x1B // [uint16 idx] push address of global (for ByRef)
     #define OP_STORE_ARRAY_FIELD 0x1C // [uint8 dims, uint16 fieldIdx] value, indices, array on stack
    +#define OP_LOAD_MEMBER     0x14  // [uint16 idx] push member variable idx of the running procedure's Me
    +#define OP_STORE_MEMBER    0x15  // [uint16 idx] pop to member variable idx of Me
    +#define OP_PUSH_MEMBER_ADDR 0x1D // [uint16 idx] push address of member variable idx of Me (for ByRef)
     
     // ============================================================
     // Arithmetic (integer)
    @@ -155,6 +165,8 @@ typedef enum {
     #define OP_CMP_GT          0x43
     #define OP_CMP_LE          0x44
     #define OP_CMP_GE          0x45
    +#define OP_IS              0x46  // pop b, a (objects); push a Is b (the same object)
    +#define OP_OBJ_CHECK       0x47  // [uint16 class] TOS must be an object (BAS_OBJ_CHECK_ANY) or an instance of class (Nothing passes); else error
     
     // ============================================================
     // Logical / bitwise
    @@ -182,6 +194,7 @@ typedef enum {
     #define OP_FOR_NEXT        0x58  // [uint16 varIdx] [uint8 scope] [int16 loopTop]
     #define OP_FOR_POP         0x59  // pop top FOR stack entry (for EXIT FOR)
     #define OP_GOSUB           0x5A  // [int16 offset] push the next instruction's address on the GOSUB stack, jump
    +#define OP_CALL_ME         0x5B  // [uint16 addr] [uint8 argc] [uint8 baseSlot] OP_CALL whose callee keeps the caller's Me (a class calling its own procedure)
     
     // ============================================================
     // Type conversion
    @@ -193,6 +206,7 @@ typedef enum {
     #define OP_CONV_STR_INT    0x63  // string -> int (VAL)
     #define OP_CONV_STR_FLT    0x65  // any -> double (VAL, CDBL)
     #define OP_CONV_INT_LONG   0x66  // int16 -> int32
    +#define OP_CONV_DATE       0x67  // any -> DATE (CDATE; a string is parsed as a date)
     
     // ============================================================
     // I/O
    @@ -241,6 +255,7 @@ typedef enum {
     #define OP_FIND_CTRL       0x8C  // pop ctrlName, pop formRef, push controlRef
     #define OP_FIND_CTRL_IDX   0x8E  // pop index, pop ctrlName, pop formRef, push ctrlRef
     #define OP_LOAD_FORM_VAR   0x8F  // [uint16 idx] push currentFormVars[idx]
    +#define OP_ME_OBJ          0x8D  // push the running procedure's Me (class module code)
     
     // ============================================================
     // Array / misc
    @@ -260,6 +275,8 @@ typedef enum {
     #define OP_STORE_FORM_VAR  0x9B  // [uint16 idx] pop, store to currentFormVars[idx]
     #define OP_PUSH_FORM_ADDR  0x9C  // [uint16 idx] push ¤tFormVars[idx] (ByRef)
     #define OP_CREATE_CTRL_EX  0x9D  // pop parentRef, pop name, pop type, pop formRef, push ctrlRef
    +#define OP_NEW_OBJ         0x9E  // [uint16 class] push a new instance of class (BAS_NEW_COLLECTION: a Collection), running its initialisation
    +#define OP_PROP_ASSIGN     0x9F  // [uint8 argc] [uint8 flags] pop value, args, memberName, objRef; assign obj.member(args) = value (BAS_ASSIGN_SET: Set)
     
     // ============================================================
     // Math built-ins (single opcode each for common functions)
    @@ -429,6 +446,187 @@ typedef enum {
     #define OP_STRIG           0xD6  // pop n, push joystick button state (STRIG(n))
     #define OP_FRAME_WAIT      0xD7  // pop frames per second; wait for the next frame
     
    +// ============================================================
    +// Library functions without an opcode of their own
    +// ============================================================
    +
    +// [uint8 BasBuiltinFnE] [uint8 argc] pop argc arguments (pushed in source
    +// order), push the result.  Optional trailing arguments are omitted from
    +// argc and take their defaults in the VM.
    +#define OP_BUILTIN         0xD8
    +
    +// Most arguments one OP_BUILTIN call takes (CHOOSE: index + 31 choices).
    +#define BAS_BUILTIN_MAX_ARGS 32
    +
    +// OP_BUILTIN function numbers.  Part of the bytecode ABI -- append only.
    +typedef enum {
    +    BAS_FN_REPLACE      = 0,   // REPLACE$(s, find, repl [, start [, count [, compare]]])
    +    BAS_FN_SPLIT        = 1,   // SPLIT(s [, delim [, limit [, compare]]]) -> STRING array
    +    BAS_FN_JOIN         = 2,   // JOIN$(array [, delim])
    +    BAS_FN_INSTRREV     = 3,   // INSTRREV(s, find [, start [, compare]])
    +    BAS_FN_STRCOMP      = 4,   // STRCOMP(a, b [, compare])
    +    BAS_FN_STRREVERSE   = 5,   // STRREVERSE$(s)
    +    BAS_FN_FILTER       = 6,   // FILTER(array, match [, include [, compare]]) -> STRING array
    +    BAS_FN_LSET         = 7,   // LSET target = s: (current target, s) -> s left-justified in LEN(target)
    +    BAS_FN_RSET         = 8,   // RSET target = s: (current target, s) -> s right-justified in LEN(target)
    +    BAS_FN_IIF          = 9,   // IIF(cond, a, b)
    +    BAS_FN_CHOOSE       = 10,  // CHOOSE(index, a [, b ...])
    +    BAS_FN_NOW          = 11,  // NOW
    +    BAS_FN_DATE         = 12,  // DATE
    +    BAS_FN_TIME         = 13,  // TIME
    +    BAS_FN_DATESERIAL   = 14,  // DATESERIAL(y, m, d)
    +    BAS_FN_TIMESERIAL   = 15,  // TIMESERIAL(h, m, s)
    +    BAS_FN_DATEVALUE    = 16,  // DATEVALUE(d)
    +    BAS_FN_TIMEVALUE    = 17,  // TIMEVALUE(d)
    +    BAS_FN_YEAR         = 18,  // YEAR(d)
    +    BAS_FN_MONTH        = 19,  // MONTH(d)
    +    BAS_FN_DAY          = 20,  // DAY(d)
    +    BAS_FN_WEEKDAY      = 21,  // WEEKDAY(d [, firstDayOfWeek])
    +    BAS_FN_HOUR         = 22,  // HOUR(d)
    +    BAS_FN_MINUTE       = 23,  // MINUTE(d)
    +    BAS_FN_SECOND       = 24,  // SECOND(d)
    +    BAS_FN_DATEADD      = 25,  // DATEADD(interval, n, d)
    +    BAS_FN_DATEDIFF     = 26,  // DATEDIFF(interval, d1, d2 [, firstDayOfWeek [, firstWeekOfYear]])
    +    BAS_FN_DATEPART     = 27,  // DATEPART(interval, d [, firstDayOfWeek [, firstWeekOfYear]])
    +    BAS_FN_ISDATE       = 28,  // ISDATE(v)
    +    BAS_FN_MONTHNAME    = 29,  // MONTHNAME(m [, abbrev])
    +    BAS_FN_WEEKDAYNAME  = 30,  // WEEKDAYNAME(w [, abbrev [, firstDayOfWeek]])
    +    BAS_FN_PARAMARRAY   = 31,  // (elementType, v0, v1, ...) -> 0-based array of the values: a ParamArray argument (compiler-generated, no BASIC name)
    +    // The program screen (runtime/screen.c): statements return 0, which
    +    // the compiler pops.  Omitted arguments are pushed as 0 and flagged in
    +    // a BAS_GIVEN_* / BAS_GFX_* mask pushed last.
    +    BAS_FN_CLS          = 32,  // CLS [n]: (n, -1 when omitted)
    +    BAS_FN_LOCATE       = 33,  // LOCATE [row] [, [col] [, cursor]]: (row, col, cursor, given)
    +    BAS_FN_COLOR        = 34,  // COLOR [fore] [, back]: (fore, back, given)
    +    BAS_FN_SCREEN       = 35,  // SCREEN mode
    +    BAS_FN_WIDTH        = 36,  // WIDTH [cols] [, rows]: (cols, rows, given)
    +    BAS_FN_PSET         = 37,  // PSET / PRESET [STEP](x, y) [, color]: (x, y, color, flags)
    +    BAS_FN_LINE         = 38,  // LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, color] [, B|BF] [, style]: (x1, y1, x2, y2, color, style, flags)
    +    BAS_FN_CIRCLE       = 39,  // CIRCLE [STEP](x, y), r [, color [, start [, end [, aspect]]]]: (x, y, r, color, start, end, aspect, flags)
    +    BAS_FN_PAINT        = 40,  // PAINT [STEP](x, y) [, paint [, border]]: (x, y, paint, border, flags)
    +    BAS_FN_DRAW         = 41,  // DRAW macro$
    +    BAS_FN_PALETTE      = 42,  // PALETTE [attr, color]: (attr, color, given)
    +    BAS_FN_PALETTE_USING = 43, // PALETTE USING array[(first)]: (array, first, given)
    +    BAS_FN_VIEW         = 44,  // VIEW [[SCREEN] (x1, y1)-(x2, y2) [, [fill] [, border]]]: (x1, y1, x2, y2, fill, border, flags)
    +    BAS_FN_WINDOW       = 45,  // WINDOW [[SCREEN] (x1, y1)-(x2, y2)]: (x1, y1, x2, y2, flags)
    +    BAS_FN_FULLSCREEN   = 46,  // FULLSCREEN [ON | OFF | expr]: (on)
    +    BAS_FN_INKEY        = 47,  // INKEY$ -> "" / one character / CHR$(0) + scan code
    +    BAS_FN_CSRLIN       = 48,  // CSRLIN -> cursor row
    +    BAS_FN_POS          = 49,  // POS(n) -> cursor column
    +    BAS_FN_POINT        = 50,  // POINT(x, y) -> colour (-1 outside the view); POINT(n) -> the last point
    +    BAS_FN_SCREENFN     = 51,  // SCREEN(row, col [, colorflag]) -> character code or colour
    +    BAS_FN_LINE_INPUT   = 52,  // LINE INPUT [prompt$;] v$: (prompt) -> the line typed
    +    // Variants and objects
    +    BAS_FN_VARTYPE      = 53,  // VARTYPE(v) -> vbEmpty .. vbArray + element type
    +    BAS_FN_TYPENAME     = 54,  // TYPENAME(v) -> "Integer", "Nothing", class name ...
    +    BAS_FN_ISEMPTY      = 55,  // ISEMPTY(v)
    +    BAS_FN_ISNULL       = 56,  // ISNULL(v)
    +    BAS_FN_ISNUMERIC    = 57,  // ISNUMERIC(v)
    +    BAS_FN_ISOBJECT     = 58,  // ISOBJECT(v)
    +    BAS_FN_ISARRAY      = 59,  // ISARRAY(v)
    +    BAS_FN_CVAR         = 60,  // CVAR(v) -> v as it is
    +    BAS_FN_TYPEOF       = 61,  // TYPEOF obj IS name: (obj, name) -> the object is of that type
    +    BAS_FN_EACH_COUNT   = 62,  // FOR EACH (compiler-generated): (group) -> items in the array or Collection
    +    BAS_FN_EACH_ITEM    = 63   // FOR EACH (compiler-generated): (group, i) -> item i (0-based)
    +} BasBuiltinFnE;
    +
    +// The Variant and object functions are numbered BAS_FN_VARTYPE through
    +// BAS_FN_EACH_ITEM.
    +#define BAS_FN_OBJECT_FIRST BAS_FN_VARTYPE
    +#define BAS_FN_OBJECT_LAST  BAS_FN_EACH_ITEM
    +
    +// OP_PUSH_SPECIAL operand.  Part of the bytecode ABI.
    +typedef enum {
    +    BAS_SPECIAL_EMPTY   = 0,
    +    BAS_SPECIAL_NULL    = 1,
    +    BAS_SPECIAL_NOTHING = 2
    +} BasSpecialValE;
    +
    +// OP_NEW_OBJ / OP_OBJ_CHECK class operand values besides a class index.
    +#define BAS_NEW_COLLECTION  0xFFFF  // OP_NEW_OBJ / OP_OBJ_CHECK: a Collection
    +#define BAS_OBJ_CHECK_ANY   0xFFFE  // OP_OBJ_CHECK: any object
    +
    +// OP_PROP_ASSIGN flags.
    +#define BAS_ASSIGN_SET      0x01   // Set obj.member = value (Property Set)
    +
    +// VARTYPE results (VB's vbEmpty .. vbArray).
    +#define BAS_VT_EMPTY        0
    +#define BAS_VT_NULL         1
    +#define BAS_VT_INTEGER      2
    +#define BAS_VT_LONG         3
    +#define BAS_VT_SINGLE       4
    +#define BAS_VT_DOUBLE       5
    +#define BAS_VT_CURRENCY     6
    +#define BAS_VT_DATE         7
    +#define BAS_VT_STRING       8
    +#define BAS_VT_OBJECT       9
    +#define BAS_VT_ERROR        10
    +#define BAS_VT_BOOLEAN      11
    +#define BAS_VT_VARIANT      12
    +#define BAS_VT_DATAOBJECT   13
    +#define BAS_VT_USERTYPE     36
    +#define BAS_VT_ARRAY        8192
    +
    +// The program-screen functions are numbered BAS_FN_CLS through
    +// BAS_FN_LINE_INPUT (the VM hands them to runtime/screen.c).
    +#define BAS_FN_SCREEN_FIRST BAS_FN_CLS
    +#define BAS_FN_SCREEN_LAST  BAS_FN_LINE_INPUT
    +
    +// Given-argument masks of LOCATE, COLOR, WIDTH and PALETTE (bit n: the
    +// n-th argument was written).
    +#define BAS_GIVEN_1        0x01
    +#define BAS_GIVEN_2        0x02
    +#define BAS_GIVEN_3        0x04
    +
    +// Flags of the drawing argument blocks (PSET, LINE, CIRCLE, PAINT).  The
    +// same blocks are the arguments of the Form / PictureBox PSet, Line and
    +// Circle methods, so both compile and run through one path.
    +#define BAS_GFX_STEP       0x01  // the (first) point is relative to the last point
    +#define BAS_GFX_COLOR      0x02  // a colour was given
    +#define BAS_GFX_PRESET     0x04  // PSET: PRESET (the default colour is the background)
    +#define BAS_GFX_START      0x08  // LINE: the start point was given
    +#define BAS_GFX_STEP2      0x10  // LINE: the end point is relative to the start point
    +#define BAS_GFX_BOX        0x20  // LINE: ,B (a rectangle)
    +#define BAS_GFX_BOX_FILL   0x40  // LINE: ,BF (a filled rectangle)
    +#define BAS_GFX_STYLE      0x80  // LINE: a style mask was given
    +#define BAS_GFX_ARC_START  0x08  // CIRCLE: a start angle was given
    +#define BAS_GFX_ARC_END    0x10  // CIRCLE: an end angle was given
    +#define BAS_GFX_ASPECT     0x20  // CIRCLE: an aspect was given
    +#define BAS_GFX_BORDER     0x08  // PAINT: a border colour was given
    +
    +// Argument counts of the drawing blocks.
    +#define BAS_GFX_PSET_ARGS   4
    +#define BAS_GFX_LINE_ARGS   7
    +#define BAS_GFX_CIRCLE_ARGS 8
    +#define BAS_GFX_PAINT_ARGS  5
    +
    +// Argument counts of the other screen statements with optional parts.
    +#define BAS_LOCATE_ARGS     4
    +#define BAS_COLOR_ARGS      3
    +#define BAS_WIDTH_ARGS      3
    +#define BAS_PALETTE_ARGS    3
    +#define BAS_VIEW_ARGS       7
    +#define BAS_WINDOW_ARGS     5
    +
    +// VIEW / WINDOW flags.
    +#define BAS_VIEW_COORDS    0x01  // a rectangle was given (none: reset)
    +#define BAS_VIEW_SCREEN    0x02  // VIEW SCREEN / WINDOW SCREEN
    +#define BAS_VIEW_FILL      0x04  // VIEW: a fill colour was given
    +#define BAS_VIEW_BORDER    0x08  // VIEW: a border colour was given
    +
    +// Object Print (Form / PictureBox .Print): one method call per item with
    +// (value, mode).
    +#define BAS_OBJ_PRINT_TEXT    0  // the value's text
    +#define BAS_OBJ_PRINT_NEWLINE 1  // end of the line (value unused)
    +#define BAS_OBJ_PRINT_ZONE    2  // ',' : the next print zone (value unused)
    +#define BAS_OBJ_PRINT_TAB     3  // TAB(n): column n
    +#define BAS_OBJ_PRINT_SPC     4  // SPC(n): n spaces
    +#define BAS_OBJ_PRINT_ARGS    2
    +
    +// Compare argument of the string functions: OPTION COMPARE decides when
    +// it is omitted (vbUseCompareOption).
    +#define BAS_COMPARE_USE_OPTION (-1)
    +
     // ============================================================
     // Halt
     // ============================================================
    @@ -498,6 +696,8 @@ typedef enum {
     // Byte offsets inside the FOR opcodes' BAS_OPERAND_FOR operand.
     #define BAS_FOR_OFFSET_OPERAND  (1 + BAS_OPERAND_U16 + BAS_OPERAND_U8)  // opcode-relative position of the int16 jump offset
     
    +static inline bool basBuiltinArity(uint8_t fn, int32_t *minArgs, int32_t *maxArgs);
    +static inline uint8_t basConvOpForStore(uint8_t dataType);
     static inline int32_t basOpcodeOperandSize(uint8_t op);
     static inline int16_t basReadI16LE(const uint8_t *p);
     static inline int32_t basReadI32LE(const uint8_t *p);
    @@ -507,6 +707,181 @@ static inline void basWriteI32LE(uint8_t *p, int32_t v);
     static inline void basWriteU16LE(uint8_t *p, uint16_t v);
     
     
    +// Argument counts an OP_BUILTIN function accepts (the compiler checks
    +// calls against them, the VM checks the bytecode).  False for an unknown
    +// function number.
    +static inline bool basBuiltinArity(uint8_t fn, int32_t *minArgs, int32_t *maxArgs) {
    +    switch (fn) {
    +        case BAS_FN_NOW:        case BAS_FN_DATE:       case BAS_FN_TIME:
    +        case BAS_FN_INKEY:      case BAS_FN_CSRLIN:
    +            *minArgs = 0;
    +            *maxArgs = 0;
    +            return true;
    +
    +        case BAS_FN_STRREVERSE: case BAS_FN_DATEVALUE:  case BAS_FN_TIMEVALUE:
    +        case BAS_FN_YEAR:       case BAS_FN_MONTH:      case BAS_FN_DAY:
    +        case BAS_FN_HOUR:       case BAS_FN_MINUTE:     case BAS_FN_SECOND:
    +        case BAS_FN_ISDATE:     case BAS_FN_CLS:        case BAS_FN_SCREEN:
    +        case BAS_FN_DRAW:       case BAS_FN_FULLSCREEN: case BAS_FN_POS:
    +        case BAS_FN_LINE_INPUT: case BAS_FN_VARTYPE:    case BAS_FN_TYPENAME:
    +        case BAS_FN_ISEMPTY:    case BAS_FN_ISNULL:     case BAS_FN_ISNUMERIC:
    +        case BAS_FN_ISOBJECT:   case BAS_FN_ISARRAY:    case BAS_FN_CVAR:
    +        case BAS_FN_EACH_COUNT:
    +            *minArgs = 1;
    +            *maxArgs = 1;
    +            return true;
    +
    +        case BAS_FN_POINT:
    +            *minArgs = 1;
    +            *maxArgs = 2;
    +            return true;
    +
    +        case BAS_FN_SCREENFN:
    +            *minArgs = 2;
    +            *maxArgs = 3;
    +            return true;
    +
    +        case BAS_FN_COLOR:
    +            *minArgs = BAS_COLOR_ARGS;
    +            *maxArgs = BAS_COLOR_ARGS;
    +            return true;
    +
    +        case BAS_FN_WIDTH:
    +            *minArgs = BAS_WIDTH_ARGS;
    +            *maxArgs = BAS_WIDTH_ARGS;
    +            return true;
    +
    +        case BAS_FN_PALETTE:    case BAS_FN_PALETTE_USING:
    +            *minArgs = BAS_PALETTE_ARGS;
    +            *maxArgs = BAS_PALETTE_ARGS;
    +            return true;
    +
    +        case BAS_FN_LOCATE:
    +            *minArgs = BAS_LOCATE_ARGS;
    +            *maxArgs = BAS_LOCATE_ARGS;
    +            return true;
    +
    +        case BAS_FN_WINDOW:
    +            *minArgs = BAS_WINDOW_ARGS;
    +            *maxArgs = BAS_WINDOW_ARGS;
    +            return true;
    +
    +        case BAS_FN_VIEW:
    +            *minArgs = BAS_VIEW_ARGS;
    +            *maxArgs = BAS_VIEW_ARGS;
    +            return true;
    +
    +        case BAS_FN_PSET:
    +            *minArgs = BAS_GFX_PSET_ARGS;
    +            *maxArgs = BAS_GFX_PSET_ARGS;
    +            return true;
    +
    +        case BAS_FN_LINE:
    +            *minArgs = BAS_GFX_LINE_ARGS;
    +            *maxArgs = BAS_GFX_LINE_ARGS;
    +            return true;
    +
    +        case BAS_FN_CIRCLE:
    +            *minArgs = BAS_GFX_CIRCLE_ARGS;
    +            *maxArgs = BAS_GFX_CIRCLE_ARGS;
    +            return true;
    +
    +        case BAS_FN_PAINT:
    +            *minArgs = BAS_GFX_PAINT_ARGS;
    +            *maxArgs = BAS_GFX_PAINT_ARGS;
    +            return true;
    +
    +        case BAS_FN_JOIN:       case BAS_FN_WEEKDAY:    case BAS_FN_MONTHNAME:
    +            *minArgs = 1;
    +            *maxArgs = 2;
    +            return true;
    +
    +        case BAS_FN_WEEKDAYNAME:
    +            *minArgs = 1;
    +            *maxArgs = 3;
    +            return true;
    +
    +        case BAS_FN_SPLIT:
    +            *minArgs = 1;
    +            *maxArgs = 4;
    +            return true;
    +
    +        case BAS_FN_LSET:       case BAS_FN_RSET:       case BAS_FN_TYPEOF:
    +        case BAS_FN_EACH_ITEM:
    +            *minArgs = 2;
    +            *maxArgs = 2;
    +            return true;
    +
    +        case BAS_FN_STRCOMP:
    +            *minArgs = 2;
    +            *maxArgs = 3;
    +            return true;
    +
    +        case BAS_FN_INSTRREV:   case BAS_FN_FILTER:     case BAS_FN_DATEPART:
    +            *minArgs = 2;
    +            *maxArgs = 4;
    +            return true;
    +
    +        case BAS_FN_CHOOSE:
    +            *minArgs = 2;
    +            *maxArgs = BAS_BUILTIN_MAX_ARGS;
    +            return true;
    +
    +        case BAS_FN_PARAMARRAY:
    +            *minArgs = 1;
    +            *maxArgs = BAS_BUILTIN_MAX_ARGS;
    +            return true;
    +
    +        case BAS_FN_IIF:        case BAS_FN_DATESERIAL: case BAS_FN_TIMESERIAL:
    +        case BAS_FN_DATEADD:
    +            *minArgs = 3;
    +            *maxArgs = 3;
    +            return true;
    +
    +        case BAS_FN_DATEDIFF:
    +            *minArgs = 3;
    +            *maxArgs = 5;
    +            return true;
    +
    +        case BAS_FN_REPLACE:
    +            *minArgs = 3;
    +            *maxArgs = 6;
    +            return true;
    +
    +        default:
    +            return false;
    +    }
    +}
    +
    +
    +// The conversion a value goes through when it is stored where dataType
    +// is declared: a numeric, BOOLEAN or DATE type is converted to it, every
    +// other type (STRING, OBJECT, VARIANT, TYPE) stores the value as it is
    +// (OP_NOP).  One source for the compiler's typed stores and the VM's
    +// stores into class member variables.
    +static inline uint8_t basConvOpForStore(uint8_t dataType) {
    +    switch (dataType) {
    +        case BAS_TYPE_INTEGER:
    +            return OP_CONV_FLT_INT;
    +
    +        case BAS_TYPE_LONG:
    +            return OP_CONV_INT_LONG;
    +
    +        case BAS_TYPE_SINGLE:
    +            return OP_CONV_INT_FLT;
    +
    +        case BAS_TYPE_BOOLEAN:
    +            return OP_CONV_BOOL;
    +
    +        case BAS_TYPE_DATE:
    +            return OP_CONV_DATE;
    +
    +        default:
    +            return OP_NOP;
    +    }
    +}
    +
    +
     // Returns operand byte count for op, or BAS_OPERAND_UNKNOWN.
     static inline int32_t basOpcodeOperandSize(uint8_t op) {
         switch (op) {
    @@ -526,11 +901,13 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
             case OP_CMP_GT:          case OP_CMP_LE:       case OP_CMP_GE:
             case OP_AND:             case OP_OR:           case OP_NOT:
             case OP_XOR:             case OP_EQV:          case OP_IMP:
    +        case OP_IS:              case OP_ME_OBJ:
             case OP_GOSUB_RET:       case OP_RET:          case OP_RET_VAL:
             case OP_FOR_POP:
             case OP_CONV_INT_FLT:    case OP_CONV_FLT_INT:
             case OP_CONV_INT_STR:    case OP_CONV_STR_INT:
             case OP_CONV_STR_FLT:    case OP_CONV_INT_LONG:
    +        case OP_CONV_DATE:
             case OP_PRINT:           case OP_PRINT_NL:     case OP_PRINT_TAB:
             case OP_INPUT:
             case OP_FILE_CLOSE:      case OP_FILE_PRINT:   case OP_FILE_INPUT:
    @@ -584,6 +961,7 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
             case OP_LBOUND:          case OP_UBOUND:
             case OP_COMPARE_MODE:    case OP_PRINT_USING:
             case OP_ERASE:           case OP_LPRINT:
    +        case OP_PUSH_SPECIAL:
                 return BAS_OPERAND_U8;
     
             case OP_PUSH_INT16:      case OP_PUSH_STR:
    @@ -599,6 +977,11 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
             case OP_ON_ERROR:
             case OP_STR_FIXLEN:
             case OP_LINE:
    +        case OP_BUILTIN:
    +        case OP_LOAD_MEMBER:     case OP_STORE_MEMBER:
    +        case OP_PUSH_MEMBER_ADDR:
    +        case OP_OBJ_CHECK:       case OP_NEW_OBJ:
    +        case OP_PROP_ASSIGN:
                 return BAS_OPERAND_U16;
     
             case OP_STORE_ARRAY_FIELD:
    @@ -606,7 +989,7 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
                 return BAS_OPERAND_U8_U16;
     
             case OP_PUSH_INT32:
    -        case OP_CALL:
    +        case OP_CALL:            case OP_CALL_ME:
             case OP_STR_APPEND:
                 return BAS_OPERAND_I32;
     
    diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.c b/src/apps/kpunch/dvxbasic/compiler/parser.c
    index 741eda58..d901e381 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/parser.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/parser.c
    @@ -28,7 +28,9 @@
     // Embeddable: no DVX dependencies, pure C.
     
     #include "parser.h"
    +#include "basEvents.h"
     #include "opcodes.h"
    +#include "../runtime/dates.h"
     #include "thirdparty/stb_ds_wrap.h"
     
     #include 
    @@ -38,76 +40,136 @@
     // Built-in function table
     // ============================================================
     
    +// An OP_BUILTIN entry names its BasBuiltinFnE in func and takes its
    +// argument counts from basBuiltinArity (minArgs / maxArgs unused).
     typedef struct {
         const char *name;
         uint8_t     opcode;
         int32_t     minArgs;
         int32_t     maxArgs;
    +    uint8_t     func;
     } BuiltinFuncT;
     
     static const BuiltinFuncT builtinFuncs[] = {
         // String functions
    -    {"ASC",        OP_STR_ASC,     1, 1},
    -    {"CHR$",       OP_STR_CHR,     1, 1},
    -    {"COMMAND$",   OP_COMMAND,     0, 1},
    -    {"DATE$",      OP_DATE_STR,    0, 0},
    -    {"ENVIRON$",   OP_ENVIRON,     1, 1},
    -    {"FORMAT$",    OP_FORMAT,      2, 2},
    -    {"HEX$",       OP_STR_HEX,    1, 1},
    -    {"INSTR",      OP_STR_INSTR,   2, 3},
    -    {"LCASE$",     OP_STR_LCASE,   1, 1},
    -    {"LEFT$",      OP_STR_LEFT,    2, 2},
    -    {"LEN",        OP_STR_LEN,     1, 1},
    -    {"LTRIM$",     OP_STR_LTRIM,   1, 1},
    -    {"MID$",       OP_STR_MID2,    2, 3},
    -    {"OCT$",       OP_STR_OCT,     1, 1},
    -    {"RIGHT$",     OP_STR_RIGHT,   2, 2},
    -    {"RTRIM$",     OP_STR_RTRIM,   1, 1},
    -    {"SPACE$",     OP_STR_SPACE,   1, 1},
    -    {"STR$",       OP_STR_STRF,    1, 1},
    -    {"STRING$",    OP_STR_STRING,  2, 2},
    -    {"TRIM$",      OP_STR_TRIM,    1, 1},
    -    {"UCASE$",     OP_STR_UCASE,   1, 1},
    -    {"VAL",        OP_STR_VAL,     1, 1},
    +    {"ASC",         OP_STR_ASC,       1, 1, 0},
    +    {"CHR$",        OP_STR_CHR,       1, 1, 0},
    +    {"COMMAND$",    OP_COMMAND,       0, 1, 0},
    +    {"DATE$",       OP_DATE_STR,      0, 0, 0},
    +    {"ENVIRON$",    OP_ENVIRON,       1, 1, 0},
    +    {"FORMAT",      OP_FORMAT,        2, 2, 0},
    +    {"FORMAT$",     OP_FORMAT,        2, 2, 0},
    +    {"HEX$",        OP_STR_HEX,       1, 1, 0},
    +    {"INSTR",       OP_STR_INSTR,     2, 3, 0},
    +    {"LCASE$",      OP_STR_LCASE,     1, 1, 0},
    +    {"LEFT$",       OP_STR_LEFT,      2, 2, 0},
    +    {"LEN",         OP_STR_LEN,       1, 1, 0},
    +    {"LTRIM$",      OP_STR_LTRIM,     1, 1, 0},
    +    {"MID$",        OP_STR_MID2,      2, 3, 0},
    +    {"OCT$",        OP_STR_OCT,       1, 1, 0},
    +    {"RIGHT$",      OP_STR_RIGHT,     2, 2, 0},
    +    {"RTRIM$",      OP_STR_RTRIM,     1, 1, 0},
    +    {"SPACE$",      OP_STR_SPACE,     1, 1, 0},
    +    {"STR$",        OP_STR_STRF,      1, 1, 0},
    +    {"STRING$",     OP_STR_STRING,    2, 2, 0},
    +    {"TRIM$",       OP_STR_TRIM,      1, 1, 0},
    +    {"UCASE$",      OP_STR_UCASE,     1, 1, 0},
    +    {"VAL",         OP_STR_VAL,       1, 1, 0},
     
         // File I/O functions
    -    {"FREEFILE",   OP_FILE_FREEFILE, 0, 0},
    -    {"LOC",        OP_FILE_LOC,      1, 1},
    -    {"LOF",        OP_FILE_LOF,      1, 1},
    +    {"FREEFILE",    OP_FILE_FREEFILE, 0, 0, 0},
    +    {"LOC",         OP_FILE_LOC,      1, 1, 0},
    +    {"LOF",         OP_FILE_LOF,      1, 1, 0},
     
         // Conversion functions
    -    {"CBOOL",      OP_CONV_BOOL,   1, 1},
    -    {"CDBL",       OP_CONV_STR_FLT, 1, 1},   // any value -> DOUBLE
    -    {"CINT",       OP_CONV_FLT_INT, 1, 1},
    -    {"CLNG",       OP_CONV_INT_LONG, 1, 1},
    -    {"CSNG",       OP_CONV_INT_FLT, 1, 1},
    -    {"CSTR",       OP_CONV_INT_STR, 1, 1},
    +    {"CBOOL",       OP_CONV_BOOL,     1, 1, 0},
    +    {"CDBL",        OP_CONV_STR_FLT,  1, 1, 0},   // any value -> DOUBLE
    +    {"CINT",        OP_CONV_FLT_INT,  1, 1, 0},
    +    {"CLNG",        OP_CONV_INT_LONG, 1, 1, 0},
    +    {"CSNG",        OP_CONV_INT_FLT,  1, 1, 0},
    +    {"CSTR",        OP_CONV_INT_STR,  1, 1, 0},
     
         // Math functions
    -    {"ABS",        OP_MATH_ABS,    1, 1},
    -    {"ATN",        OP_MATH_ATN,    1, 1},
    -    {"COS",        OP_MATH_COS,    1, 1},
    -    {"EXP",        OP_MATH_EXP,    1, 1},
    -    {"FIX",        OP_MATH_FIX,    1, 1},
    -    {"GETBLUE",    OP_GET_BLUE,    1, 1},
    -    {"GETGREEN",   OP_GET_GREEN,   1, 1},
    -    {"GETRED",     OP_GET_RED,     1, 1},
    -    {"INT",        OP_MATH_INT,    1, 1},
    -    {"LOG",        OP_MATH_LOG,    1, 1},
    -    {"RGB",        OP_RGB,         3, 3},
    -    {"RND",        OP_MATH_RND,    0, 1},
    -    {"SGN",        OP_MATH_SGN,    1, 1},
    -    {"SIN",        OP_MATH_SIN,    1, 1},
    -    {"SQR",        OP_MATH_SQR,    1, 1},
    -    {"TAN",        OP_MATH_TAN,    1, 1},
    -    {"TIME$",      OP_TIME_STR,    0, 0},
    -    {"TIMER",      OP_MATH_TIMER,  0, 0},
    +    {"ABS",         OP_MATH_ABS,      1, 1, 0},
    +    {"ATN",         OP_MATH_ATN,      1, 1, 0},
    +    {"COS",         OP_MATH_COS,      1, 1, 0},
    +    {"EXP",         OP_MATH_EXP,      1, 1, 0},
    +    {"FIX",         OP_MATH_FIX,      1, 1, 0},
    +    {"GETBLUE",     OP_GET_BLUE,      1, 1, 0},
    +    {"GETGREEN",    OP_GET_GREEN,     1, 1, 0},
    +    {"GETRED",      OP_GET_RED,       1, 1, 0},
    +    {"INT",         OP_MATH_INT,      1, 1, 0},
    +    {"LOG",         OP_MATH_LOG,      1, 1, 0},
    +    {"RGB",         OP_RGB,           3, 3, 0},
    +    {"RND",         OP_MATH_RND,      0, 1, 0},
    +    {"SGN",         OP_MATH_SGN,      1, 1, 0},
    +    {"SIN",         OP_MATH_SIN,      1, 1, 0},
    +    {"SQR",         OP_MATH_SQR,      1, 1, 0},
    +    {"TAN",         OP_MATH_TAN,      1, 1, 0},
    +    {"TIME$",       OP_TIME_STR,      0, 0, 0},
    +    {"TIMER",       OP_MATH_TIMER,    0, 0, 0},
     
         // Joysticks
    -    {"STICK",      OP_STICK,       1, 1},
    -    {"STRIG",      OP_STRIG,       1, 1},
    +    {"STICK",       OP_STICK,         1, 1, 0},
    +    {"STRIG",       OP_STRIG,         1, 1, 0},
     
    -    {NULL, 0, 0, 0}
    +    // The program screen
    +    {"CSRLIN",      OP_BUILTIN,       0, 0, BAS_FN_CSRLIN},
    +    {"INKEY$",      OP_BUILTIN,       0, 0, BAS_FN_INKEY},
    +    {"POINT",       OP_BUILTIN,       0, 0, BAS_FN_POINT},
    +    {"POS",         OP_BUILTIN,       0, 0, BAS_FN_POS},
    +    {"SCREEN",      OP_BUILTIN,       0, 0, BAS_FN_SCREENFN},
    +
    +    // String library functions
    +    {"FILTER",      OP_BUILTIN,       0, 0, BAS_FN_FILTER},
    +    {"INSTRREV",    OP_BUILTIN,       0, 0, BAS_FN_INSTRREV},
    +    {"JOIN",        OP_BUILTIN,       0, 0, BAS_FN_JOIN},
    +    {"JOIN$",       OP_BUILTIN,       0, 0, BAS_FN_JOIN},
    +    {"REPLACE",     OP_BUILTIN,       0, 0, BAS_FN_REPLACE},
    +    {"REPLACE$",    OP_BUILTIN,       0, 0, BAS_FN_REPLACE},
    +    {"SPLIT",       OP_BUILTIN,       0, 0, BAS_FN_SPLIT},
    +    {"STRCOMP",     OP_BUILTIN,       0, 0, BAS_FN_STRCOMP},
    +    {"STRREVERSE",  OP_BUILTIN,       0, 0, BAS_FN_STRREVERSE},
    +    {"STRREVERSE$", OP_BUILTIN,       0, 0, BAS_FN_STRREVERSE},
    +
    +    // Choice functions
    +    {"CHOOSE",      OP_BUILTIN,       0, 0, BAS_FN_CHOOSE},
    +    {"IIF",         OP_BUILTIN,       0, 0, BAS_FN_IIF},
    +
    +    // Dates and times
    +    {"CDATE",       OP_CONV_DATE,     1, 1, 0},
    +    {"DATE",        OP_BUILTIN,       0, 0, BAS_FN_DATE},
    +    {"DATEADD",     OP_BUILTIN,       0, 0, BAS_FN_DATEADD},
    +    {"DATEDIFF",    OP_BUILTIN,       0, 0, BAS_FN_DATEDIFF},
    +    {"DATEPART",    OP_BUILTIN,       0, 0, BAS_FN_DATEPART},
    +    {"DATESERIAL",  OP_BUILTIN,       0, 0, BAS_FN_DATESERIAL},
    +    {"DATEVALUE",   OP_BUILTIN,       0, 0, BAS_FN_DATEVALUE},
    +    {"DAY",         OP_BUILTIN,       0, 0, BAS_FN_DAY},
    +    {"HOUR",        OP_BUILTIN,       0, 0, BAS_FN_HOUR},
    +    {"ISDATE",      OP_BUILTIN,       0, 0, BAS_FN_ISDATE},
    +    {"MINUTE",      OP_BUILTIN,       0, 0, BAS_FN_MINUTE},
    +    {"MONTH",       OP_BUILTIN,       0, 0, BAS_FN_MONTH},
    +    {"MONTHNAME",   OP_BUILTIN,       0, 0, BAS_FN_MONTHNAME},
    +    {"NOW",         OP_BUILTIN,       0, 0, BAS_FN_NOW},
    +    {"SECOND",      OP_BUILTIN,       0, 0, BAS_FN_SECOND},
    +    {"TIME",        OP_BUILTIN,       0, 0, BAS_FN_TIME},
    +    {"TIMESERIAL",  OP_BUILTIN,       0, 0, BAS_FN_TIMESERIAL},
    +    {"TIMEVALUE",   OP_BUILTIN,       0, 0, BAS_FN_TIMEVALUE},
    +    {"WEEKDAY",     OP_BUILTIN,       0, 0, BAS_FN_WEEKDAY},
    +    {"WEEKDAYNAME", OP_BUILTIN,       0, 0, BAS_FN_WEEKDAYNAME},
    +    {"YEAR",        OP_BUILTIN,       0, 0, BAS_FN_YEAR},
    +
    +    // Variants and objects
    +    {"CVAR",        OP_BUILTIN,       0, 0, BAS_FN_CVAR},
    +    {"ISARRAY",     OP_BUILTIN,       0, 0, BAS_FN_ISARRAY},
    +    {"ISEMPTY",     OP_BUILTIN,       0, 0, BAS_FN_ISEMPTY},
    +    {"ISNULL",      OP_BUILTIN,       0, 0, BAS_FN_ISNULL},
    +    {"ISNUMERIC",   OP_BUILTIN,       0, 0, BAS_FN_ISNUMERIC},
    +    {"ISOBJECT",    OP_BUILTIN,       0, 0, BAS_FN_ISOBJECT},
    +    {"TYPENAME",    OP_BUILTIN,       0, 0, BAS_FN_TYPENAME},
    +    {"VARTYPE",     OP_BUILTIN,       0, 0, BAS_FN_VARTYPE},
    +
    +    {NULL, 0, 0, 0, 0}
     };
     
     // ============================================================
    @@ -138,6 +200,66 @@ static const BuiltinFuncT builtinFuncs[] = {
     #define BAS_PLAY_ARGS          1
     #define BAS_FRAMEWAIT_ARGS     1
     
    +// LSET / RSET pass OP_BUILTIN the target's current value and the new one.
    +#define BAS_LSET_ARGS         2
    +
    +// The DATE, VARIANT and Collection types are named by identifiers, not
    +// reserved words.
    +#define BAS_DATE_TYPE_NAME       "DATE"
    +#define BAS_VARIANT_TYPE_NAME    "VARIANT"
    +#define BAS_COLLECTION_TYPE_NAME "COLLECTION"
    +
    +// Object type names (identifiers, not reserved words): a variable or
    +// parameter of these types holds a form or control reference.
    +static const char *const sObjectTypeNames[] = { "CONTROL", "FORM", "OBJECT" };
    +
    +// Console and graphics statements.  They are identifiers, not reserved
    +// words: a variable, procedure or constant of the same name (or a name
    +// followed by '=' or '.') is the program's own.
    +typedef struct {
    +    const char *name;
    +    uint8_t     func;     // BasBuiltinFnE
    +    int32_t     flags;    // extra BAS_GFX_* flags (PRESET)
    +} ScreenStmtT;
    +
    +static const ScreenStmtT sScreenStmts[] = {
    +    { "CIRCLE",     BAS_FN_CIRCLE,     0 },
    +    { "CLS",        BAS_FN_CLS,        0 },
    +    { "COLOR",      BAS_FN_COLOR,      0 },
    +    { "DRAW",       BAS_FN_DRAW,       0 },
    +    { "FULLSCREEN", BAS_FN_FULLSCREEN, 0 },
    +    { "LOCATE",     BAS_FN_LOCATE,     0 },
    +    { "PAINT",      BAS_FN_PAINT,      0 },
    +    { "PALETTE",    BAS_FN_PALETTE,    0 },
    +    { "PRESET",     BAS_FN_PSET,       BAS_GFX_PRESET },
    +    { "PSET",       BAS_FN_PSET,       0 },
    +    { "SCREEN",     BAS_FN_SCREEN,     0 },
    +    { "VIEW",       BAS_FN_VIEW,       0 },
    +    { "WIDTH",      BAS_FN_WIDTH,      0 },
    +    { "WINDOW",     BAS_FN_WINDOW,     0 }
    +};
    +
    +// Form / PictureBox drawing methods with QBasic statement syntax.
    +#define BAS_METHOD_CIRCLE     "Circle"
    +#define BAS_METHOD_LINE       "Line"
    +#define BAS_METHOD_PRINT      "Print"
    +#define BAS_METHOD_PSET       "PSet"
    +
    +// CLS with no argument, and FULLSCREEN with none / ON / OFF.
    +#define BAS_CLS_OMITTED       (-1)
    +#define BAS_FULLSCREEN_ON     1
    +#define BAS_FULLSCREEN_OFF    0
    +
    +// Arguments LOCATE, COLOR and WIDTH take before their given mask, and
    +// the extra ones QBasic accepts that DVX ignores (LOCATE's cursor start
    +// and stop lines, COLOR's border, SCREEN's colour switch and pages).
    +#define BAS_LOCATE_VALUES     3
    +#define BAS_PAIR_VALUES       2
    +#define BAS_LOCATE_IGNORED    2
    +#define BAS_COLOR_IGNORED     1
    +#define BAS_SCREEN_IGNORED    3
    +#define BAS_PAINT_IGNORED     1
    +
     // MsgBox flags when the argument is omitted (vbOKOnly).
     #define BAS_MSGBOX_OK_ONLY    0x0000
     
    @@ -153,6 +275,8 @@ typedef struct {
         uint8_t types[BAS_MAX_PARAMS];
         bool    byVal[BAS_MAX_PARAMS];
         bool    optional[BAS_MAX_PARAMS];
    +    bool    hasParamArray;            // the last parameter is a ParamArray
    +    bool    paramArrayTyped;          // ... with a declared element type (AS clause, suffix or DEFxxx)
     } ParamListT;
     
     // parseParamList behaviour flags
    @@ -160,8 +284,43 @@ typedef struct {
     #define PARAM_PRESCAN     (1u << 1)  // accept AS  for a TYPE not parsed yet
     #define PARAM_FORCE_BYVAL (1u << 2)  // DEF FN: parameters are always by value
     
    -// Name prefix of compiler temporaries; '$' cannot start a BASIC identifier.
    +// BASIC's TRUE as an integer.
    +#define BAS_TRUE_INT            (-1)
    +
    +// Most arguments a ParamArray takes: OP_BUILTIN's limit less the element
    +// type argument.
    +#define BAS_PARAMARRAY_MAX_ARGS (BAS_BUILTIN_MAX_ARGS - 1)
    +
    +// Name prefix of compiler temporaries and hidden variables; '$' cannot
    +// start a BASIC identifier.
     #define BAS_TEMP_PREFIX   "$tmp"
    +#define BAS_HIDDEN_PREFIX "$hid"
    +
    +// Words the parser reads by their text (identifiers, not reserved): the
    +// project builder's form and class markers, FOR EACH ... IN, and a class
    +// module's Attribute lines.
    +#define BAS_KW_ATTRIBUTE  "ATTRIBUTE"
    +#define BAS_KW_BEGINCLASS "BEGINCLASS"
    +#define BAS_KW_BEGINFORM  "BEGINFORM"
    +#define BAS_KW_EACH       "EACH"
    +#define BAS_KW_ENDCLASS   "ENDCLASS"
    +#define BAS_KW_ENDFORM    "ENDFORM"
    +#define BAS_KW_IN         "IN"
    +
    +// Empty and Null are identifiers too (a program's own symbol wins).
    +#define BAS_EMPTY_NAME    "EMPTY"
    +#define BAS_NULL_NAME     "NULL"
    +#define BAS_SPECIAL_NONE  (-1)
    +
    +// Class modules: the event procedures, and the Attribute that makes a
    +// member the default one (Attribute Name.VB_UserMemId = 0).
    +#define BAS_CLASS_INITIALIZE  "Class_Initialize"
    +#define BAS_CLASS_TERMINATE   "Class_Terminate"
    +#define BAS_ATTR_USER_MEM_ID  "VB_UserMemId"
    +#define BAS_DEFAULT_MEMBER_ID 0
    +
    +// VB's vbObjectError: add an error number to it for errors a class raises.
    +#define BAS_VB_OBJECT_ERROR   (-2147221504)
     
     // A store target: a variable, an array element or a TYPE field chain on
     // either.  Subscripts are evaluated once into temporaries when the target
    @@ -184,10 +343,21 @@ typedef struct {
         double  dblVal;
     } NumLitT;
     
    +// A folded constant (CONST value, ENUM member): the result of a constant
    +// expression, typed.  Integer results are LONG, as integer literals are.
    +typedef struct {
    +    double  dblVal;                     // every numeric type and DATE
    +    int32_t intVal;                     // INTEGER / LONG / BOOLEAN
    +    int32_t strLen;                     // STRING: length of str (it may hold NULs)
    +    uint8_t dataType;                   // BAS_TYPE_*
    +    char    str[BAS_MAX_CONST_STR];
    +} ConstValT;
    +
     // State saved around a FOR / DO / WHILE loop body.
     typedef struct {
         ExitListT list;
         int32_t   selectBase;
    +    bool      forIsEach;
     } LoopSaveT;
     
     // State saved around a SUB / FUNCTION body: EXIT lists, loop bookkeeping
    @@ -209,6 +379,8 @@ typedef struct {
         int32_t      bodyAddr;            // first byte of the body (prologue jumps back here)
         BasSymbolT **tempPool;            // module-level temporaries, restored on exit
         bool         isFunction;
    +    bool         forIsEach;
    +    uint8_t      returnType;          // FUNCTION: an OBJECT / VARIANT result starts as Nothing / Empty
     } ProcSaveT;
     
     // ============================================================
    @@ -216,6 +388,8 @@ typedef struct {
     // ============================================================
     
     static BasSymbolT *acquireTemp(BasParserT *p);
    +static void addClassMember(BasParserT *p, const char *name, BasSymbolT *sym, uint8_t kind, bool isPrivate);
    +static bool addEnumMember(BasParserT *p, const BasSymbolT *enumSym, const char *member, int32_t value);
     static void addPatchAddr(BasSymbolT *sym, int32_t addr);
     static void addPredefConst(BasParserT *p, const char *name, int32_t val);
     static void addPredefConsts(BasParserT *p);
    @@ -224,6 +398,7 @@ static void advance(BasParserT *p);
     static bool appendRhsQualifies(BasParserT *p, BasTokenTypeE op);
     static void applyParamList(BasSymbolT *sym, const ParamListT *pl);
     static bool atStatementEnd(BasParserT *p);
    +static bool bareObjectAhead(BasParserT *p);
     bool basParse(BasParserT *p);
     BasModuleT *basParserBuildModule(BasParserT *p);
     const char *basParserFormScope(const BasParserT *p);
    @@ -231,23 +406,36 @@ void basParserFree(BasParserT *p);
     void basParserInit(BasParserT *p, const char *source, int32_t sourceLen);
     void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v);
     static bool buildUdtLayout(BasParserT *p, int32_t typeId, uint8_t *buf, int32_t *len, int32_t depth);
    +static bool canIndexValue(const BasSymbolT *sym);
     static bool check(BasParserT *p, BasTokenTypeE type);
     static void checkIdentLength(BasParserT *p);
     static bool checkKeyword(BasParserT *p, const char *kw);
     static void checkUndefinedLabels(BasParserT *p, BasScopeE scope);
     static bool clampArgCount(BasParserT *p, int32_t argc);
     static bool clampParamCount(BasParserT *p, int32_t paramCount);
    +static BasSymbolT *classEventProc(BasParserT *p, const char *eventName);
    +static bool classLevelStatement(BasParserT *p);
    +static const BasSymbolT *classMethod(BasParserT *p, const char *memberName);
     static void closeSelectCase(BasParserT *p, int32_t **endJumps, int32_t skipAddr);
     static void collectDebugGlobals(BasParserT *p);
     static void collectDebugLocals(BasParserT *p, BasSymbolT *procSym, int32_t procIndex);
    +static bool constConvert(BasParserT *p, ConstValT *c, uint8_t asType, const char *what);
    +static bool constExprCheck(BasParserT *p, int32_t codeStart);
    +static bool constExprRun(BasParserT *p, int32_t codeStart, ConstValT *out);
    +static bool constFromValue(BasParserT *p, BasValueT v, ConstValT *out);
    +static void constStore(BasSymbolT *sym, const ConstValT *c);
     static bool debugAliasTargetShared(const BasParserT *p, const BasSymbolT *alias);
     static void defineLabel(BasParserT *p, const char *name);
     static uint8_t defTypeForToken(BasTokenTypeE tt);
     static void emitAddrOf(BasParserT *p, BasSymbolT *sym);
    +static void emitAssignValue(BasParserT *p);
    +static void emitBareObject(BasParserT *p);
    +static void emitBuiltinOp(BasParserT *p, uint8_t opcode, uint8_t func, int32_t argc);
     static void emitByRefArg(BasParserT *p);
     static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc);
     static void emitCtrlRef(BasParserT *p, BasSymbolT *sym, const char *name);
     static void emitElementRef(BasParserT *p, const VarRefT *ref);
    +static void emitEnumMember(BasParserT *p, const BasSymbolT *enumSym);
     static void emitExternArgConv(BasParserT *p, uint8_t dataType);
     static void emitFieldStoreConv(BasParserT *p, const BasFieldDefT *field);
     static bool emitFormByName(BasParserT *p);
    @@ -258,11 +446,21 @@ static void emitInputConv(BasParserT *p, uint8_t dataType);
     static int32_t emitJump(BasParserT *p, uint8_t opcode);
     static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName);
     static void emitLoad(BasParserT *p, BasSymbolT *sym);
    -static void emitMethodCallStatement(BasParserT *p);
    +static void emitMemberCallStatement(BasParserT *p, const char *memberName);
    +static void emitMethodCallStatement(BasParserT *p, const BasSymbolT *method);
    +static void emitNewObject(BasParserT *p, int32_t objClass);
    +static void emitObjCheck(BasParserT *p, int32_t objClass);
    +static void emitObjectPrintHead(BasParserT *p, BasSymbolT *obj);
    +static void emitObjectPrintTail(BasParserT *p, int32_t mode);
    +static void emitParamArray(BasParserT *p, const BasSymbolT *sym, bool hasArgs);
    +static void emitPropAssign(BasParserT *p, int32_t argc);
     static void emitPushCStr(BasParserT *p, const char *text);
    +static void emitPushDate(BasParserT *p, double serial);
     static void emitPushInt16(BasParserT *p, int16_t v);
     static void emitPushInt32(BasParserT *p, int32_t v);
    +static void emitPushSpecial(BasParserT *p, uint8_t kind);
     static void emitPushStr(BasParserT *p, const char *text, int32_t len);
    +static void emitScreenStmt(BasParserT *p, uint8_t func, int32_t argc);
     static void emitSelectPops(BasParserT *p, int32_t count);
     static void emitStore(BasParserT *p, BasSymbolT *sym);
     static void emitStoreConv(BasParserT *p, uint8_t dataType);
    @@ -270,6 +468,7 @@ static bool emitUdtLayout(BasParserT *p, int32_t typeId);
     static bool emitUdtSpec(BasParserT *p, int32_t typeId);
     static void emitVarRefLoad(BasParserT *p, const VarRefT *ref);
     static void emitVarRefStore(BasParserT *p, const VarRefT *ref);
    +static void emitZeroValue(BasParserT *p, uint8_t dataType);
     static BasSymbolT *ensureVariable(BasParserT *p, const char *name);
     static void error(BasParserT *p, const char *msg);
     static void errorAt(BasParserT *p, int32_t line, int32_t col, const char *msg);
    @@ -283,24 +482,38 @@ static void expectEndOfStatement(BasParserT *p);
     static bool exprEnter(BasParserT *p);
     static void exprLeave(BasParserT *p);
     static const BuiltinFuncT *findBuiltin(const char *name);
    +static BasSymbolT *findEnum(BasParserT *p, const char *name);
     static BasSymbolT *findOrAddProc(BasParserT *p, const char *name, BasSymKindE kind, uint8_t returnType);
    +static const ScreenStmtT *findScreenStmt(BasParserT *p);
     static BasSymbolT *findTypeDef(BasParserT *p, const char *name);
     static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId);
    +static BasSymbolT *hiddenVariable(BasParserT *p);
     static uint8_t implicitType(BasParserT *p, const char *name, bool *outTyped);
     static bool isArgDelimiter(BasTokenTypeE tt);
    +static bool isBuiltinTypeName(const char *name);
    +static bool isConstExprOp(uint8_t op, uint8_t fn);
     static bool isDefFnName(const char *name);
    +static bool isFormName(const BasParserT *p, const char *name);
     static bool isFractionalLiteralAhead(const BasParserT *p);
     static bool isNumericLiteralAhead(const BasParserT *p);
    +static bool isOwnResult(const BasParserT *p, const BasSymbolT *sym);
    +static bool isPropertyKind(uint8_t kind);
     static void lexError(BasParserT *p);
     static void loopBegin(BasParserT *p, bool isFor, LoopSaveT *save);
     static void loopEnd(BasParserT *p, bool isFor, LoopSaveT *save);
     static bool match(BasParserT *p, BasTokenTypeE type);
     static bool matchEnd(BasParserT *p, BasTokenTypeE kw);
    +static bool memberNameAhead(BasParserT *p);
    +static uint8_t meOpcode(const BasParserT *p);
    +static bool moduleLevelName(BasParserT *p, const char *name, char *out, int32_t outSize);
     static bool nameHasTypeSuffix(const char *name);
    +static bool ownResultKeywordAhead(BasParserT *p);
     static void parseAddExpr(BasParserT *p);
     static void parseAndExpr(BasParserT *p);
     static int32_t parseArgList(BasParserT *p);
     static void parseAssignOrCall(BasParserT *p);
    +static void parseAttribute(BasParserT *p);
    +static void parseBeginClass(BasParserT *p);
     static void parseBeginForm(BasParserT *p);
     static void parseBoundFunc(BasParserT *p, uint8_t opcode, const char *what);
     static int32_t parseCallArgs(BasParserT *p, BasSymbolT *sym, bool parens);
    @@ -310,6 +523,8 @@ static void parseClose(BasParserT *p);
     static void parseCompareExpr(BasParserT *p);
     static void parseConcatExpr(BasParserT *p);
     static void parseConst(BasParserT *p);
    +static void parseConstDeclarator(BasParserT *p);
    +static bool parseConstExpr(BasParserT *p, ConstValT *out);
     static void parseData(BasParserT *p);
     static bool parseDataItem(BasParserT *p);
     static void parseDeclare(BasParserT *p);
    @@ -318,10 +533,12 @@ static void parseDef(BasParserT *p);
     static void parseDefType(BasParserT *p, uint8_t dataType);
     static void parseDim(BasParserT *p);
     static void parseDimBounds(BasParserT *p, int32_t *outDims);
    -static void parseDimDeclarator(BasParserT *p, bool isShared);
    +static void parseDimDeclarator(BasParserT *p, bool isShared, bool isPrivate);
     static void parseDo(BasParserT *p);
     static void parseEnd(BasParserT *p);
    +static void parseEndClass(BasParserT *p);
     static void parseEndForm(BasParserT *p);
    +static void parseEnum(BasParserT *p);
     static void parseEqvExpr(BasParserT *p);
     static void parseErase(BasParserT *p);
     static void parseExit(BasParserT *p);
    @@ -331,19 +548,31 @@ static int32_t parseFieldName(BasParserT *p, int32_t typeId, BasSymbolT **outTyp
     static void parseFieldStoreChain(BasParserT *p, int32_t typeId);
     static void parseFileCopy(BasParserT *p);
     static int32_t parseFixedStringLen(BasParserT *p);
    +static void parseFlaggedArgs(BasParserT *p, const int32_t *slots, int32_t count, int32_t *flags);
     static void parseFor(BasParserT *p);
    +static void parseForEach(BasParserT *p);
     static void parseGet(BasParserT *p);
    +static void parseGfxCircleArgs(BasParserT *p);
    +static void parseGfxLineArgs(BasParserT *p);
    +static void parseGfxPoint(BasParserT *p, int32_t *flags, int32_t stepFlag);
    +static void parseGfxPsetArgs(BasParserT *p, int32_t flags);
    +static void parseGfxRect(BasParserT *p);
     static void parseGosub(BasParserT *p);
     static void parseGoto(BasParserT *p);
     static void parseIdivExpr(BasParserT *p);
     static void parseIf(BasParserT *p);
    +static void parseIgnoredArgs(BasParserT *p, int32_t max);
     static void parseImpExpr(BasParserT *p);
     static int32_t parseIndexList(BasParserT *p);
     static void parseInput(BasParserT *p);
     static void parseInputBoxArgs(BasParserT *p);
     static void parseKill(BasParserT *p);
    +static void parseLine(BasParserT *p);
     static void parseLineInput(BasParserT *p);
     static void parseLprint(BasParserT *p);
    +static void parseMemberAssignOrCall(BasParserT *p, const char *memberName);
    +static void parseMeStatement(BasParserT *p);
    +static int32_t parseMethodArgs(BasParserT *p, const BasSymbolT *method, bool parens);
     static bool parseMinusForLiteral(BasParserT *p);
     static void parseMkDir(BasParserT *p);
     static void parseModule(BasParserT *p);
    @@ -355,10 +584,13 @@ static void parseNotExpr(BasParserT *p);
     static bool parseNumLit(BasParserT *p, bool isNeg, NumLitT *out);
     static void parseObjectMemberExpr(BasParserT *p);
     static void parseObjectMemberStatement(BasParserT *p);
    +static void parseObjectOperand(BasParserT *p, void (*parseOperand)(BasParserT *p));
    +static void parseObjectPrint(BasParserT *p);
     static void parseOn(BasParserT *p);
     static void parseOnError(BasParserT *p);
     static void parseOpen(BasParserT *p);
     static void parseOption(BasParserT *p);
    +static int32_t parseOptionalArgs(BasParserT *p, int32_t count);
     static void parseOrExpr(BasParserT *p);
     static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags);
     static void parsePowExpr(BasParserT *p);
    @@ -368,7 +600,8 @@ static void parsePrimary(BasParserT *p);
     static void parsePrint(BasParserT *p);
     static void parsePrintItems(BasParserT *p);
     static bool parseProcDeclarator(BasParserT *p, const char *kindError, BasSymKindE *outKind, char *name, ParamListT *pl, uint8_t *outReturnType);
    -static void parseProcDef(BasParserT *p, bool isFunction);
    +static void parseProcDef(BasParserT *p, uint8_t kind, bool isPrivate);
    +static void parsePropertyDef(BasParserT *p, bool isPrivate);
     static void parsePut(BasParserT *p);
     static void parseRead(BasParserT *p);
     static void parseRedim(BasParserT *p);
    @@ -376,8 +609,10 @@ static void parseRemoveControl(BasParserT *p);
     static void parseRestore(BasParserT *p);
     static void parseResume(BasParserT *p);
     static void parseRmDir(BasParserT *p);
    +static void parseScreenStatement(BasParserT *p, const ScreenStmtT *stmt);
     static void parseSeek(BasParserT *p);
     static void parseSelectCase(BasParserT *p);
    +static void parseSet(BasParserT *p);
     static void parseSetAttr(BasParserT *p);
     static void parseSetEvent(BasParserT *p);
     static void parseShared(BasParserT *p);
    @@ -391,6 +626,7 @@ static void parseSwap(BasParserT *p);
     static void parseType(BasParserT *p);
     static void parseUnaryExpr(BasParserT *p);
     static bool parseVarRef(BasParserT *p, VarRefT *ref, bool allowFields);
    +static void parseVisibility(BasParserT *p);
     static void parseWhile(BasParserT *p);
     static void parseWrite(BasParserT *p);
     static void parseXorExpr(BasParserT *p);
    @@ -398,17 +634,21 @@ static void patchCallAddrs(BasParserT *p, BasSymbolT *sym);
     static void patchJump(BasParserT *p, int32_t addr);
     static void patchLabelRefs(BasParserT *p, BasSymbolT *sym);
     static BasTokenTypeE peekAfterParens(const BasParserT *p);
    +static void prescanClasses(BasParserT *p);
     static void prescanSignatures(BasParserT *p);
     static void procBegin(BasParserT *p, ProcSaveT *save, bool isFunction);
     static void procEnd(BasParserT *p, ProcSaveT *save);
    +static bool procSymbolName(BasParserT *p, int32_t classIdx, const char *name, uint8_t kind, char *out, int32_t outSize);
     static bool rejectHiddenModuleVar(BasParserT *p, const char *name);
     static int16_t relJumpOffset(BasParserT *p, int32_t target, int32_t operandAddr);
     static bool requireMemberName(BasParserT *p);
    +static const BuiltinFuncT *resolveBuiltin(BasParserT *p, const char *name);
     static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName);
     static uint8_t resolveParamType(BasParserT *p, bool prescan, int32_t *outUdtTypeId);
     static uint8_t resolveTypeName(BasParserT *p);
     static void shiftBackpatchAddrs(BasParserT *p, int32_t from, int32_t delta);
     static void skipNewlines(BasParserT *p);
    +static int32_t specialValueName(const char *text);
     static BasScopeE storageScope(const BasSymbolT *sym);
     static uint8_t suffixToType(const char *name);
     static void tempPoolReset(BasParserT *p);
    @@ -443,6 +683,86 @@ static BasSymbolT *acquireTemp(BasParserT *p) {
     }
     
     
    +// Records a public member of the class being compiled (name as programs
    +// write it after the dot).  Private members are not recorded: only the
    +// class's own code reaches them.  Two members may share a name only when
    +// both are Property procedures (Get, Let and Set of one property).
    +static void addClassMember(BasParserT *p, const char *name, BasSymbolT *sym, uint8_t kind, bool isPrivate) {
    +    BasClassDefT *cls = &p->sym.classes[p->sym.classIdx];
    +
    +    sym->ownerClass = p->sym.classIdx;
    +    sym->isPrivate  = isPrivate;
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(cls->members); i++) {
    +        const BasClassMemberDefT *m = &cls->members[i];
    +
    +        if (strcasecmp(m->name, name) == 0 && (m->kind == kind || !isPropertyKind(m->kind) || !isPropertyKind(kind))) {
    +            char buf[BAS_PARSE_ERR_SCRATCH];
    +            snprintf(buf, sizeof(buf), "Member '%s' is already defined in class '%s'", name, cls->name);
    +            error(p, buf);
    +            return;
    +        }
    +    }
    +
    +    if (isPrivate) {
    +        return;
    +    }
    +
    +    BasClassMemberDefT def;
    +
    +    memset(&def, 0, sizeof(def));
    +    snprintf(def.name, sizeof(def.name), "%s", name);
    +    def.sym  = sym;
    +    def.kind = kind;
    +    arrput(cls->members, def);
    +}
    +
    +
    +// Registers ENUM member = value: always as EnumName.Member, and as the
    +// bare name unless another ENUM already has a member of that name (the
    +// bare name is then ambiguous and must be qualified, as in VB).
    +static bool addEnumMember(BasParserT *p, const BasSymbolT *enumSym, const char *member, int32_t value) {
    +    char buf[BAS_PARSE_ERR_SCRATCH];
    +    char qualified[BAS_MAX_IDENT];
    +    int  n = snprintf(qualified, sizeof(qualified), "%s.%s", enumSym->name, member);
    +
    +    if (n < 0 || n >= (int)sizeof(qualified)) {
    +        snprintf(buf, sizeof(buf), "ENUM member name '%s.%s' is too long", enumSym->name, member);
    +        error(p, buf);
    +        return false;
    +    }
    +
    +    BasSymbolT *qualSym = basSymTabAddGlobal(&p->sym, qualified, SYM_CONST, BAS_TYPE_LONG);
    +
    +    if (qualSym == NULL) {
    +        snprintf(buf, sizeof(buf), "Duplicate member '%s' in ENUM '%s'", member, enumSym->name);
    +        error(p, buf);
    +        return false;
    +    }
    +
    +    qualSym->constInt = value;
    +
    +    BasSymbolT *bare = basSymTabFindGlobal(&p->sym, member);
    +
    +    if (bare != NULL && bare->isEnumMember) {
    +        bare->isAmbiguous = true;
    +        return true;
    +    }
    +
    +    bare = (bare == NULL) ? basSymTabAddGlobal(&p->sym, member, SYM_CONST, BAS_TYPE_LONG) : NULL;
    +
    +    if (bare == NULL) {
    +        snprintf(buf, sizeof(buf), "ENUM member '%s' is already defined", member);
    +        error(p, buf);
    +        return false;
    +    }
    +
    +    bare->constInt     = value;
    +    bare->isEnumMember = true;
    +    return true;
    +}
    +
    +
     // Record a code position that must be patched once sym (a forward-
     // referenced SUB/FUNCTION/label) is defined.
     static void addPatchAddr(BasSymbolT *sym, int32_t addr) {
    @@ -490,6 +810,57 @@ static void addPredefConsts(BasParserT *p) {
         addPredefConst(p, "vbSystem",       4);
         addPredefConst(p, "vbDirectory",    16);
         addPredefConst(p, "vbArchive",      32);
    +
    +    // Compare arguments of the string functions
    +    addPredefConst(p, "vbUseCompareOption", BAS_COMPARE_USE_OPTION);
    +    addPredefConst(p, "vbBinaryCompare",    BAS_COMPARE_MODE_BINARY);
    +    addPredefConst(p, "vbTextCompare",      BAS_COMPARE_MODE_TEXT);
    +
    +    // Days of the week and first weeks of the year (date functions)
    +    addPredefConst(p, "vbUseSystemDayOfWeek", BAS_DATE_USE_SYSTEM_DAY);
    +    addPredefConst(p, "vbSunday",             BAS_DATE_SUNDAY);
    +    addPredefConst(p, "vbMonday",             BAS_DATE_SUNDAY + 1);
    +    addPredefConst(p, "vbTuesday",            BAS_DATE_SUNDAY + 2);
    +    addPredefConst(p, "vbWednesday",          BAS_DATE_SUNDAY + 3);
    +    addPredefConst(p, "vbThursday",           BAS_DATE_SUNDAY + 4);
    +    addPredefConst(p, "vbFriday",             BAS_DATE_SUNDAY + 5);
    +    addPredefConst(p, "vbSaturday",           BAS_DATE_SATURDAY);
    +    addPredefConst(p, "vbUseSystem",          BAS_DATE_USE_SYSTEM_WEEK);
    +    addPredefConst(p, "vbFirstJan1",          BAS_DATE_FIRST_JAN1);
    +    addPredefConst(p, "vbFirstFourDays",      BAS_DATE_FIRST_FOUR_DAYS);
    +    addPredefConst(p, "vbFirstFullWeek",      BAS_DATE_FIRST_FULL_WEEK);
    +
    +    // Drag and drop: DragMode, the Drag method's action, DragOver's State.
    +    // (Drag 0 cancels; vbCancel is the MsgBox value.)
    +    addPredefConst(p, "vbManual",    BAS_DRAGMODE_MANUAL);
    +    addPredefConst(p, "vbAutomatic", BAS_DRAGMODE_AUTOMATIC);
    +    addPredefConst(p, "vbBeginDrag", BAS_DRAG_BEGIN);
    +    addPredefConst(p, "vbEndDrag",   BAS_DRAG_END);
    +    addPredefConst(p, "vbEnter",     BAS_DRAG_ENTER);
    +    addPredefConst(p, "vbLeave",     BAS_DRAG_LEAVE);
    +    addPredefConst(p, "vbOver",      BAS_DRAG_OVER);
    +
    +    // Clipboard formats
    +    addPredefConst(p, "vbCFText",    BAS_CF_TEXT);
    +
    +    // VARTYPE results, and the base of the error numbers a class raises
    +    addPredefConst(p, "vbEmpty",           BAS_VT_EMPTY);
    +    addPredefConst(p, "vbNull",            BAS_VT_NULL);
    +    addPredefConst(p, "vbInteger",         BAS_VT_INTEGER);
    +    addPredefConst(p, "vbLong",            BAS_VT_LONG);
    +    addPredefConst(p, "vbSingle",          BAS_VT_SINGLE);
    +    addPredefConst(p, "vbDouble",          BAS_VT_DOUBLE);
    +    addPredefConst(p, "vbCurrency",        BAS_VT_CURRENCY);
    +    addPredefConst(p, "vbDate",            BAS_VT_DATE);
    +    addPredefConst(p, "vbString",          BAS_VT_STRING);
    +    addPredefConst(p, "vbObject",          BAS_VT_OBJECT);
    +    addPredefConst(p, "vbError",           BAS_VT_ERROR);
    +    addPredefConst(p, "vbBoolean",         BAS_VT_BOOLEAN);
    +    addPredefConst(p, "vbVariant",         BAS_VT_VARIANT);
    +    addPredefConst(p, "vbDataObject",      BAS_VT_DATAOBJECT);
    +    addPredefConst(p, "vbUserDefinedType", BAS_VT_USERTYPE);
    +    addPredefConst(p, "vbArray",           BAS_VT_ARRAY);
    +    addPredefConst(p, "vbObjectError",     BAS_VB_OBJECT_ERROR);
     }
     
     
    @@ -628,9 +999,11 @@ static bool appendRhsQualifies(BasParserT *p, BasTokenTypeE op) {
     // Copy a parsed parameter list onto a SUB/FUNCTION symbol and mark the
     // signature as known (call-site arity checks trust hasSignature).
     static void applyParamList(BasSymbolT *sym, const ParamListT *pl) {
    -    sym->paramCount     = pl->count;
    -    sym->requiredParams = pl->required;
    -    sym->hasSignature   = true;
    +    sym->paramCount      = pl->count;
    +    sym->requiredParams  = pl->required;
    +    sym->hasSignature    = true;
    +    sym->hasParamArray   = pl->hasParamArray;
    +    sym->paramArrayTyped = pl->paramArrayTyped;
     
         for (int32_t i = 0; i < pl->count; i++) {
             sym->paramTypes[i]    = pl->types[i];
    @@ -646,6 +1019,22 @@ static bool atStatementEnd(BasParserT *p) {
     }
     
     
    +// A bare name at the current token that names an object the program has
    +// not declared -- a form or a control, as a bare name does in VB where an
    +// object is expected -- rather than a variable, constant, procedure or
    +// built-in.  The name must stand alone (no '(' or '.' after it).
    +static bool bareObjectAhead(BasParserT *p) {
    +    if (!check(p, TOK_IDENT) || basSymTabFind(&p->sym, p->lex.token.text) != NULL || findBuiltin(p->lex.token.text) != NULL || specialValueName(p->lex.token.text) != BAS_SPECIAL_NONE) {
    +        return false;
    +    }
    +
    +    BasLexerT peek = p->lex;
    +
    +    basLexerNext(&peek);
    +    return peek.token.type != TOK_LPAREN && peek.token.type != TOK_DOT;
    +}
    +
    +
     bool basParse(BasParserT *p) {
         parseModule(p);
     
    @@ -710,12 +1099,21 @@ void basParserFree(BasParserT *p) {
         arrfree(p->exitFuncList.patchAddr);
         arrfree(p->tempPool);
         arrfree(p->typeDefs);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(p->formNames); i++) {
    +        free(p->formNames[i]);
    +    }
    +
    +    arrfree(p->formNames);
    +    basVmDestroy(p->constVm);
         p->exitForList.patchAddr  = NULL;
         p->exitDoList.patchAddr   = NULL;
         p->exitSubList.patchAddr  = NULL;
         p->exitFuncList.patchAddr = NULL;
         p->tempPool               = NULL;
         p->typeDefs               = NULL;
    +    p->formNames              = NULL;
    +    p->constVm                = NULL;
     }
     
     
    @@ -728,6 +1126,8 @@ void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) {
     
         p->formInitJmpAddr   = -1;
         p->formInitCodeStart = -1;
    +    p->lastObjClass      = BAS_CLASS_NONE;
    +    p->memberClass       = BAS_CLASS_NONE;
     
         addPredefConsts(p);
     
    @@ -797,6 +1197,14 @@ static bool buildUdtLayout(BasParserT *p, int32_t typeId, uint8_t *buf, int32_t
     }
     
     
    +// A scalar variable that can hold an object or an array, so name(args)
    +// indexes what it holds (an array element, or an object's default
    +// member): one declared As VARIANT or as an object.
    +static bool canIndexValue(const BasSymbolT *sym) {
    +    return sym->kind == SYM_VARIABLE && !sym->isArray && (sym->dataType == BAS_TYPE_OBJECT || sym->dataType == BAS_TYPE_VARIANT);
    +}
    +
    +
     static bool check(BasParserT *p, BasTokenTypeE type) {
         return p->lex.token.type == type;
     }
    @@ -867,6 +1275,73 @@ static bool clampParamCount(BasParserT *p, int32_t paramCount) {
     }
     
     
    +// Class_Initialize / Class_Terminate of the class being compiled: NULL
    +// when it has none; a SUB without parameters, else an error.
    +static BasSymbolT *classEventProc(BasParserT *p, const char *eventName) {
    +    char qualified[BAS_MAX_IDENT];
    +
    +    if (!basSymTabQualify(&p->sym, p->sym.classIdx, eventName, qualified, (int32_t)sizeof(qualified))) {
    +        return NULL;
    +    }
    +
    +    BasSymbolT *sym = basSymTabFindGlobal(&p->sym, qualified);
    +
    +    if (sym == NULL || !sym->isDefined) {
    +        return NULL;
    +    }
    +
    +    if (sym->kind != SYM_SUB || sym->paramCount != 0) {
    +        char buf[BAS_PARSE_ERR_SCRATCH];
    +        snprintf(buf, sizeof(buf), "%s must be a Sub without parameters", eventName);
    +        error(p, buf);
    +        return NULL;
    +    }
    +
    +    return sym;
    +}
    +
    +
    +// True for a statement a class module may hold outside its procedures:
    +// declarations, procedures, OPTION, DEFxxx, ENDCLASS and Attribute lines.
    +static bool classLevelStatement(BasParserT *p) {
    +    switch (p->lex.token.type) {
    +        case TOK_DIM:      case TOK_PUBLIC:   case TOK_PRIVATE:
    +        case TOK_CONST:    case TOK_SUB:      case TOK_FUNCTION:
    +        case TOK_PROPERTY: case TOK_TYPE:     case TOK_ENUM:
    +        case TOK_DECLARE:  case TOK_OPTION:   case TOK_DEFINT:
    +        case TOK_DEFLNG:   case TOK_DEFSNG:   case TOK_DEFDBL:
    +        case TOK_DEFSTR:   case TOK_REM:
    +            return true;
    +
    +        case TOK_IDENT:
    +            return checkKeyword(p, BAS_KW_ENDCLASS) || checkKeyword(p, BAS_KW_ATTRIBUTE);
    +
    +        default:
    +            return false;
    +    }
    +}
    +
    +
    +// The public SUB / FUNCTION / Property Get procedure memberName of the
    +// class the member access at hand starts from (p->memberClass, which is
    +// consumed), so the call can pass ByRef arguments by address.  NULL when
    +// the class is unknown (or not a class module) or has no such procedure.
    +static const BasSymbolT *classMethod(BasParserT *p, const char *memberName) {
    +    int32_t classIdx = p->memberClass;
    +    char    qualified[BAS_MAX_IDENT];
    +
    +    p->memberClass = BAS_CLASS_NONE;
    +
    +    if (classIdx < 0 || !basSymTabQualify(&p->sym, classIdx, memberName, qualified, (int32_t)sizeof(qualified))) {
    +        return NULL;
    +    }
    +
    +    BasSymbolT *sym = basSymTabFindGlobal(&p->sym, qualified);
    +
    +    return (sym != NULL && (sym->kind == SYM_SUB || sym->kind == SYM_FUNCTION) && !sym->isPrivate) ? sym : NULL;
    +}
    +
    +
     // Emit the shared END SELECT epilogue: land every pending end-of-case jump on
     // the OP_POP that discards the SELECT test value, land the test-value error
     // skip (skipAddr) just past it, free the jump list, and drop the
    @@ -896,6 +1371,11 @@ static void collectDebugGlobals(BasParserT *p) {
                 basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_GLOBAL, s->dataType, s->index, -1, NULL, s->isShared);
             } else if (s->scope == SCOPE_FORM) {
                 basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_FORM, s->dataType, s->index, -1, s->formName, s->isShared);
    +        } else if (s->scope == SCOPE_MEMBER && s->ownerClass >= 0) {
    +            // A member variable: its bare name, with the class as the scope name
    +            const BasClassDefT *cls = &p->sym.classes[s->ownerClass];
    +
    +            basCodeGenAddDebugVar(&p->cg, s->name + strlen(cls->name) + strlen(BAS_CLASS_SEP), SCOPE_MEMBER, s->dataType, s->index, -1, cls->name, false);
             }
         }
     
    @@ -951,6 +1431,179 @@ static void collectDebugLocals(BasParserT *p, BasSymbolT *procSym, int32_t procI
     }
     
     
    +// Converts a constant to asType the way assignment would: numbers round
    +// to INTEGER/LONG (overflow is an error), become SINGLE/DOUBLE/DATE or
    +// TRUE/FALSE; a string never mixes with a number.  what names the
    +// statement in the error messages.
    +static bool constConvert(BasParserT *p, ConstValT *c, uint8_t asType, const char *what) {
    +    char buf[BAS_PARSE_ERR_SCRATCH];
    +    bool fromString = (c->dataType == BAS_TYPE_STRING);
    +    bool toString   = (asType == BAS_TYPE_STRING);
    +
    +    if (fromString != toString || asType == BAS_TYPE_UDT || asType == BAS_TYPE_OBJECT) {
    +        snprintf(buf, sizeof(buf), "Type mismatch in %s", what);
    +        error(p, buf);
    +        return false;
    +    }
    +
    +    if (asType == BAS_TYPE_INTEGER || asType == BAS_TYPE_LONG) {
    +        int64_t rounded = 0;
    +        int64_t minVal  = (asType == BAS_TYPE_INTEGER) ? INT16_MIN : INT32_MIN;
    +        int64_t maxVal  = (asType == BAS_TYPE_INTEGER) ? INT16_MAX : INT32_MAX;
    +
    +        if (!basRoundHalfEven(c->dblVal, &rounded) || rounded < minVal || rounded > maxVal) {
    +            snprintf(buf, sizeof(buf), "Overflow in %s", what);
    +            error(p, buf);
    +            return false;
    +        }
    +
    +        c->intVal = (int32_t)rounded;
    +        c->dblVal = c->intVal;
    +    } else if (asType == BAS_TYPE_SINGLE) {
    +        c->dblVal = (float)c->dblVal;
    +    } else if (asType == BAS_TYPE_BOOLEAN) {
    +        c->intVal = (c->dblVal != 0.0) ? BAS_TRUE_INT : 0;
    +        c->dblVal = c->intVal;
    +    }
    +
    +    c->dataType = asType;
    +    return true;
    +}
    +
    +
    +// Rejects the code of a constant expression when it does anything but
    +// compute from literals (see isConstExprOp).
    +static bool constExprCheck(BasParserT *p, int32_t codeStart) {
    +    const uint8_t *code = p->cg.code;
    +
    +    for (int32_t pc = codeStart; pc < p->cg.codeLen;) {
    +        uint8_t op      = code[pc];
    +        int32_t operand = basOpcodeOperandSize(op);
    +        uint8_t fn      = (op == OP_BUILTIN) ? code[pc + 1] : 0;
    +
    +        if (operand == BAS_OPERAND_UNKNOWN || !isConstExprOp(op, fn)) {
    +            error(p, "Constant expression required");
    +            return false;
    +        }
    +
    +        pc += 1 + operand;
    +    }
    +
    +    return true;
    +}
    +
    +
    +// Runs the checked code of a constant expression (from codeStart to the
    +// end of the code) on the parser's VM and takes its value.  The code is
    +// left in place; the caller removes it.
    +static bool constExprRun(BasParserT *p, int32_t codeStart, ConstValT *out) {
    +    if (p->constVm == NULL) {
    +        p->constVm = basVmCreate();
    +
    +        if (p->constVm == NULL) {
    +            error(p, "Out of memory evaluating a constant expression");
    +            return false;
    +        }
    +    }
    +
    +    basEmit8(&p->cg, OP_HALT);
    +
    +    BasModuleT mod;
    +    memset(&mod, 0, sizeof(mod));
    +    mod.code       = &p->cg.code[codeStart];
    +    mod.codeLen    = p->cg.codeLen - codeStart;
    +    mod.constants  = p->cg.constants;
    +    mod.constCount = p->cg.constCount;
    +
    +    BasVmT *vm = p->constVm;
    +
    +    basVmLoadModule(vm, &mod);
    +    vm->compareTextMode = p->compareText;
    +
    +    BasVmResultE rc = basVmRun(vm);
    +
    +    vm->module = NULL;
    +
    +    if (rc != BAS_VM_HALTED || vm->sp != 1) {
    +        // The VM's message reads "Runtime error N at PC n: text"; the PC
    +        // means nothing here, so only the text is reported.
    +        char        buf[BAS_PARSE_ERR_SCRATCH];
    +        const char *msg  = basVmGetError(vm);
    +        const char *text = strstr(msg, BAS_VM_ERROR_TEXT_SEP);
    +
    +        snprintf(buf, sizeof(buf), "Error in constant expression: %s", text != NULL ? text + strlen(BAS_VM_ERROR_TEXT_SEP) : msg);
    +        error(p, buf);
    +
    +        // A failed run can leave values behind: the next one gets a new VM.
    +        basVmDestroy(vm);
    +        p->constVm = NULL;
    +        return false;
    +    }
    +
    +    vm->sp = 0;
    +
    +    BasValueT v  = vm->stack[0];
    +    bool      ok = constFromValue(p, v, out);
    +
    +    basValRelease(&v);
    +    return ok;
    +}
    +
    +
    +// The ConstValT of a constant expression's value.
    +static bool constFromValue(BasParserT *p, BasValueT v, ConstValT *out) {
    +    memset(out, 0, sizeof(*out));
    +
    +    switch (v.type) {
    +        case BAS_TYPE_INTEGER:
    +        case BAS_TYPE_LONG:
    +        case BAS_TYPE_BOOLEAN:
    +            out->dataType = (v.type == BAS_TYPE_BOOLEAN) ? BAS_TYPE_BOOLEAN : BAS_TYPE_LONG;
    +            out->dblVal   = basValToNumber(v);
    +            out->intVal   = (int32_t)out->dblVal;
    +            return true;
    +
    +        case BAS_TYPE_SINGLE:
    +        case BAS_TYPE_DOUBLE:
    +        case BAS_TYPE_DATE:
    +            out->dataType = v.type;
    +            out->dblVal   = basValToNumber(v);
    +            return true;
    +
    +        case BAS_TYPE_STRING:
    +            if (v.strVal->len >= BAS_MAX_CONST_STR) {
    +                char buf[BAS_PARSE_ERR_SCRATCH];
    +                snprintf(buf, sizeof(buf), "Constant string longer than %d characters", (int)(BAS_MAX_CONST_STR - 1));
    +                error(p, buf);
    +                return false;
    +            }
    +
    +            out->dataType = BAS_TYPE_STRING;
    +            out->strLen   = v.strVal->len;
    +            memcpy(out->str, v.strVal->data, (size_t)out->strLen);
    +            out->str[out->strLen] = '\0';
    +            return true;
    +
    +        default:
    +            error(p, "Constant expression required");
    +            return false;
    +    }
    +}
    +
    +
    +// Gives a CONST symbol the value c (its dataType is already c's).
    +static void constStore(BasSymbolT *sym, const ConstValT *c) {
    +    if (c->dataType == BAS_TYPE_STRING) {
    +        memcpy(sym->constStr, c->str, (size_t)c->strLen + 1);
    +        sym->constStrLen = c->strLen;
    +    } else if (c->dataType == BAS_TYPE_SINGLE || c->dataType == BAS_TYPE_DOUBLE || c->dataType == BAS_TYPE_DATE) {
    +        sym->constDbl = c->dblVal;
    +    } else {
    +        sym->constInt = c->intVal;
    +    }
    +}
    +
    +
     // True when the module slot a procedure alias addresses belongs to a DIM
     // SHARED variable (of the form being compiled, for a form slot).
     static bool debugAliasTargetShared(const BasParserT *p, const BasSymbolT *alias) {
    @@ -1047,6 +1700,8 @@ static void emitAddrOf(BasParserT *p, BasSymbolT *sym) {
             basEmit8(&p->cg, OP_PUSH_LOCAL_ADDR);
         } else if (scope == SCOPE_FORM) {
             basEmit8(&p->cg, OP_PUSH_FORM_ADDR);
    +    } else if (scope == SCOPE_MEMBER) {
    +        basEmit8(&p->cg, OP_PUSH_MEMBER_ADDR);
         } else {
             basEmit8(&p->cg, OP_PUSH_GLOBAL_ADDR);
         }
    @@ -1055,6 +1710,47 @@ static void emitAddrOf(BasParserT *p, BasSymbolT *sym) {
     }
     
     
    +// The value of an assignment: an expression, or with SET an object (a
    +// bare form or control name included).
    +static void emitAssignValue(BasParserT *p) {
    +    if (p->setAssign) {
    +        parseObjectOperand(p, parseExpression);
    +        emitObjCheck(p, BAS_CLASS_NONE);
    +    } else {
    +        parseExpression(p);
    +    }
    +}
    +
    +
    +// Push the form or control a bare name names (see bareObjectAhead) and
    +// consume the name.
    +static void emitBareObject(BasParserT *p) {
    +    char name[BAS_MAX_TOKEN_LEN];
    +
    +    snprintf(name, sizeof(name), "%s", p->lex.token.text);
    +    advance(p);
    +
    +    if (isFormName(p, name)) {
    +        emitFormRef(p, NULL, name);
    +    } else {
    +        emitCtrlRef(p, NULL, name);
    +    }
    +}
    +
    +
    +// Emit a built-in function call whose argc arguments are on the stack:
    +// the opcode, plus the function number and argument count of an
    +// OP_BUILTIN.
    +static void emitBuiltinOp(BasParserT *p, uint8_t opcode, uint8_t func, int32_t argc) {
    +    basEmit8(&p->cg, opcode);
    +
    +    if (opcode == OP_BUILTIN) {
    +        basEmit8(&p->cg, func);
    +        basEmit8(&p->cg, (uint8_t)argc);
    +    }
    +}
    +
    +
     // Emit a BYREF argument: the address of a variable or array element when
     // the argument is exactly that, otherwise the value (effectively BYVAL).
     static void emitByRefArg(BasParserT *p) {
    @@ -1115,6 +1811,12 @@ static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc) {
             return;
         }
     
    +    // No argument left for a ParamArray: it receives an empty array.
    +    if (sym->hasParamArray && argc == sym->paramCount - 1) {
    +        emitParamArray(p, sym, false);
    +        argc++;
    +    }
    +
         // requiredParams counts up to the last non-OPTIONAL parameter, so it is
         // the full list when nothing is OPTIONAL.
         int32_t minArgs = sym->requiredParams;
    @@ -1123,7 +1825,9 @@ static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc) {
             char        buf[BAS_PARSE_ERR_SCRATCH];
             const char *what = (sym->kind == SYM_FUNCTION) ? "Function" : "Sub";
     
    -        if (minArgs == sym->paramCount) {
    +        if (sym->hasParamArray) {
    +            snprintf(buf, sizeof(buf), "%s '%s' expects at least %d arguments, got %d", what, sym->name, (int)minArgs, (int)argc);
    +        } else if (minArgs == sym->paramCount) {
                 snprintf(buf, sizeof(buf), "%s '%s' expects %d arguments, got %d", what, sym->name, (int)sym->paramCount, (int)argc);
             } else {
                 snprintf(buf, sizeof(buf), "%s '%s' expects %d to %d arguments, got %d", what, sym->name, (int)minArgs, (int)sym->paramCount, (int)argc);
    @@ -1135,11 +1839,7 @@ static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc) {
     
         // Push default zero-values for omitted optional parameters
         for (int32_t i = argc; i < sym->paramCount; i++) {
    -        if (sym->paramTypes[i] == BAS_TYPE_STRING) {
    -            emitPushStr(p, "", 0);
    -        } else {
    -            emitPushInt16(p, 0);
    -        }
    +        emitZeroValue(p, sym->paramTypes[i]);
     
             if (sym->isExtern) {
                 emitExternArgConv(p, sym->paramTypes[i]);
    @@ -1162,11 +1862,12 @@ static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc) {
             return;
         }
     
    -    // Internal BASIC procedure: emit OP_CALL
    -    // baseSlot: functions reserve slot 0 for the return value
    +    // Internal BASIC procedure: emit OP_CALL (a class calling one of its
    +    // own procedures hands its Me on: OP_CALL_ME).  baseSlot: functions
    +    // reserve slot 0 for the return value
         uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
     
    -    basEmit8(&p->cg, OP_CALL);
    +    basEmit8(&p->cg, (sym->ownerClass != BAS_CLASS_NONE) ? OP_CALL_ME : OP_CALL);
         int32_t addrPos = basCodePos(&p->cg);
         basEmitU16(&p->cg, (uint16_t)sym->codeAddr);
         basEmit8(&p->cg, (uint8_t)argc);
    @@ -1180,13 +1881,21 @@ static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc) {
     
     
     // Push the control a statement or expression names: the value of an
    -// object variable, else the control of that name on the current form.
    +// object variable (or of the FUNCTION being compiled, its result), else
    +// the control of that name on the current form.
     static void emitCtrlRef(BasParserT *p, BasSymbolT *sym, const char *name) {
         if (sym != NULL && sym->kind == SYM_VARIABLE) {
             emitLoad(p, sym);
             return;
         }
     
    +    // Inside a FUNCTION its own name holds the result (Make.Id = n)
    +    if (isOwnResult(p, sym)) {
    +        basEmit8(&p->cg, OP_LOAD_LOCAL);
    +        basEmitU16(&p->cg, 0);
    +        return;
    +    }
    +
         emitPushInt16(p, 0); // NULL form ref = current form
         emitPushCStr(p, name);
         basEmit8(&p->cg, OP_FIND_CTRL);
    @@ -1204,12 +1913,40 @@ static void emitElementRef(BasParserT *p, const VarRefT *ref) {
     }
     
     
    +// Loads EnumName.Member (the current token is the '.').
    +static void emitEnumMember(BasParserT *p, const BasSymbolT *enumSym) {
    +    advance(p); // consume '.'
    +
    +    if (!basTokenIsWord(&p->lex.token)) {
    +        errorExpected(p, "ENUM member name");
    +        return;
    +    }
    +
    +    char qualified[BAS_MAX_IDENT + BAS_MAX_TOKEN_LEN];
    +    snprintf(qualified, sizeof(qualified), "%s.%s", enumSym->name, p->lex.token.text);
    +
    +    BasSymbolT *member = basSymTabFindGlobal(&p->sym, qualified);
    +
    +    if (member == NULL) {
    +        char buf[BAS_PARSE_ERR_SCRATCH];
    +        snprintf(buf, sizeof(buf), "'%s' is not a member of ENUM '%s'", p->lex.token.text, enumSym->name);
    +        error(p, buf);
    +        return;
    +    }
    +
    +    advance(p);
    +    emitLoad(p, member);
    +}
    +
    +
     // DECLARE LIBRARY arguments reach the native side typed exactly as the
     // declaration says: the marshaller sizes and encodes each argument by its
     // runtime type, so the value is converted to the declared type first.
    +// A typed ParamArray converts its arguments the same way.
     static void emitExternArgConv(BasParserT *p, uint8_t dataType) {
         switch (dataType) {
             case BAS_TYPE_DOUBLE:
    +        case BAS_TYPE_DATE:
                 basEmit8(&p->cg, OP_CONV_STR_FLT);
                 break;
     
    @@ -1317,7 +2054,8 @@ static void emitGotoWithSelectPops(BasParserT *p, const char *labelName) {
     // INTEGER conversion range-checks to 16 bits itself; every other numeric
     // type goes through the double conversion and is narrowed by emitStore.
     static void emitInputConv(BasParserT *p, uint8_t dataType) {
    -    if (dataType == BAS_TYPE_STRING) {
    +    // A DATE target parses the text itself (emitStore converts it).
    +    if (dataType == BAS_TYPE_STRING || dataType == BAS_TYPE_DATE) {
             return;
         }
         if (dataType == BAS_TYPE_INTEGER) {
    @@ -1382,10 +2120,17 @@ static void emitLoad(BasParserT *p, BasSymbolT *sym) {
         }
     
         if (sym->kind == SYM_CONST) {
    +        if (sym->isAmbiguous) {
    +            char buf[BAS_PARSE_ERR_SCRATCH];
    +            snprintf(buf, sizeof(buf), "Ambiguous name '%s': more than one ENUM has this member, so write EnumName.%s", sym->name, sym->name);
    +            error(p, buf);
    +            return;
    +        }
    +
             // Emit the constant value directly, typed as declared
             switch (sym->dataType) {
                 case BAS_TYPE_STRING:
    -                emitPushCStr(p, sym->constStr);
    +                emitPushStr(p, sym->constStr, sym->constStrLen);
                     break;
     
                 case BAS_TYPE_INTEGER:
    @@ -1406,6 +2151,10 @@ static void emitLoad(BasParserT *p, BasSymbolT *sym) {
                     basEmit8(&p->cg, OP_CONV_INT_FLT);
                     break;
     
    +            case BAS_TYPE_DATE:
    +                emitPushDate(p, sym->constDbl);
    +                break;
    +
                 default:
                     basEmit8(&p->cg, OP_PUSH_FLT64);
                     basEmitDouble(&p->cg, sym->constDbl);
    @@ -1420,6 +2169,8 @@ static void emitLoad(BasParserT *p, BasSymbolT *sym) {
             basEmit8(&p->cg, OP_LOAD_LOCAL);
         } else if (scope == SCOPE_FORM) {
             basEmit8(&p->cg, OP_LOAD_FORM_VAR);
    +    } else if (scope == SCOPE_MEMBER) {
    +        basEmit8(&p->cg, OP_LOAD_MEMBER);
         } else {
             basEmit8(&p->cg, OP_LOAD_GLOBAL);
         }
    @@ -1428,20 +2179,51 @@ static void emitLoad(BasParserT *p, BasSymbolT *sym) {
     }
     
     
    -// Statement-form method call "obj.Method arg, arg": the object and
    -// method name are already on the stack.  Arguments are comma-separated
    -// (VB style, no parentheses); the return value is discarded.
    -static void emitMethodCallStatement(BasParserT *p) {
    +// A method call statement on the object reference and method name
    +// already pushed.  The drawing methods Line, Circle and PSet take QBasic
    +// statement syntax (their argument blocks are the LINE / CIRCLE / PSET
    +// statements'), Print takes PRINT's items; every other method (and every
    +// method of an object declared As a class) comma-separated arguments.
    +static void emitMemberCallStatement(BasParserT *p, const char *memberName) {
         int32_t argc = 0;
     
    -    while (!atStatementEnd(p) && !p->hasError) {
    -        if (argc > 0) {
    -            expect(p, TOK_COMMA);
    -        }
    -        parseExpression(p);
    -        argc++;
    +    if (p->memberClass >= 0) {
    +        emitMethodCallStatement(p, classMethod(p, memberName));
    +        return;
         }
     
    +    p->memberClass = BAS_CLASS_NONE;
    +
    +    if (strcasecmp(memberName, BAS_METHOD_PRINT) == 0) {
    +        parseObjectPrint(p);
    +        return;
    +    }
    +
    +    if (strcasecmp(memberName, BAS_METHOD_LINE) == 0) {
    +        parseGfxLineArgs(p);
    +        argc = BAS_GFX_LINE_ARGS;
    +    } else if (strcasecmp(memberName, BAS_METHOD_CIRCLE) == 0) {
    +        parseGfxCircleArgs(p);
    +        argc = BAS_GFX_CIRCLE_ARGS;
    +    } else if (strcasecmp(memberName, BAS_METHOD_PSET) == 0) {
    +        parseGfxPsetArgs(p, 0);
    +        argc = BAS_GFX_PSET_ARGS;
    +    } else {
    +        emitMethodCallStatement(p, classMethod(p, memberName));
    +        return;
    +    }
    +
    +    basEmit8(&p->cg, OP_CALL_METHOD);
    +    basEmit8(&p->cg, (uint8_t)argc);
    +    basEmit8(&p->cg, OP_POP);
    +}
    +
    +
    +// "obj.Method arg, arg" (object and method name pushed); method is the
    +// class procedure when the compiler knows it (ByRef arguments), else NULL.
    +static void emitMethodCallStatement(BasParserT *p, const BasSymbolT *method) {
    +    int32_t argc = parseMethodArgs(p, method, false);
    +
         if (clampArgCount(p, argc)) {
             return;
         }
    @@ -1452,12 +2234,100 @@ static void emitMethodCallStatement(BasParserT *p) {
     }
     
     
    +// OP_NEW_OBJ for objClass (a class index or BAS_CLASS_COLLECTION).
    +static void emitNewObject(BasParserT *p, int32_t objClass) {
    +    basEmit8(&p->cg, OP_NEW_OBJ);
    +    basEmitU16(&p->cg, (objClass == BAS_CLASS_COLLECTION) ? BAS_NEW_COLLECTION : (uint16_t)objClass);
    +}
    +
    +
    +// OP_OBJ_CHECK for a store where objClass (a class index,
    +// BAS_CLASS_COLLECTION or BAS_CLASS_NONE: any object) is declared.
    +static void emitObjCheck(BasParserT *p, int32_t objClass) {
    +    uint16_t operand = BAS_OBJ_CHECK_ANY;
    +
    +    if (objClass == BAS_CLASS_COLLECTION) {
    +        operand = BAS_NEW_COLLECTION;
    +    } else if (objClass != BAS_CLASS_NONE) {
    +        operand = (uint16_t)objClass;
    +    }
    +
    +    basEmit8(&p->cg, OP_OBJ_CHECK);
    +    basEmitU16(&p->cg, operand);
    +}
    +
    +
    +// The ParamArray argument of a call to sym: with hasArgs the remaining
    +// arguments (the current token starts the first), each converted to the
    +// element type when the ParamArray declares one, packed into a 0-based
    +// array; without, an empty array (UBOUND -1).
    +// One object Print item: the object (from its temporary) and the method
    +// name, then (after the caller pushes the value) the mode and the call.
    +static void emitObjectPrintHead(BasParserT *p, BasSymbolT *obj) {
    +    emitLoad(p, obj);
    +    emitPushCStr(p, BAS_METHOD_PRINT);
    +}
    +
    +
    +static void emitObjectPrintTail(BasParserT *p, int32_t mode) {
    +    emitPushInt16(p, (int16_t)mode);
    +    basEmit8(&p->cg, OP_CALL_METHOD);
    +    basEmit8(&p->cg, BAS_OBJ_PRINT_ARGS);
    +    basEmit8(&p->cg, OP_POP);
    +}
    +
    +
    +static void emitParamArray(BasParserT *p, const BasSymbolT *sym, bool hasArgs) {
    +    uint8_t elementType = sym->paramTypes[sym->paramCount - 1];
    +    int32_t count       = 0;
    +
    +    emitPushInt16(p, elementType);
    +
    +    while (hasArgs && !p->hasError) {
    +        if (count >= BAS_PARAMARRAY_MAX_ARGS) {
    +            char buf[BAS_PARSE_ERR_SCRATCH];
    +            snprintf(buf, sizeof(buf), "Too many ParamArray arguments (at most %d)", (int)BAS_PARAMARRAY_MAX_ARGS);
    +            error(p, buf);
    +            return;
    +        }
    +
    +        parseExpression(p);
    +
    +        if (sym->paramArrayTyped) {
    +            emitExternArgConv(p, elementType);
    +        }
    +
    +        count++;
    +        hasArgs = match(p, TOK_COMMA);
    +    }
    +
    +    emitBuiltinOp(p, OP_BUILTIN, BAS_FN_PARAMARRAY, count + 1);
    +}
    +
    +
    +// An assignment to obj.member(args) (the object, member name and argc
    +// arguments pushed, then the value): OP_PROP_ASSIGN, a Set with SET.
    +static void emitPropAssign(BasParserT *p, int32_t argc) {
    +    basEmit8(&p->cg, OP_PROP_ASSIGN);
    +    basEmit8(&p->cg, (uint8_t)argc);
    +    basEmit8(&p->cg, p->setAssign ? BAS_ASSIGN_SET : 0);
    +}
    +
    +
     // Push a NUL-terminated string constant.
     static void emitPushCStr(BasParserT *p, const char *text) {
         emitPushStr(p, text, (int32_t)strlen(text));
     }
     
     
    +// Push a DATE: its serial, tagged as a date.
    +static void emitPushDate(BasParserT *p, double serial) {
    +    basEmit8(&p->cg, OP_PUSH_FLT64);
    +    basEmitDouble(&p->cg, serial);
    +    basEmit8(&p->cg, OP_CONV_DATE);
    +}
    +
    +
     static void emitPushInt16(BasParserT *p, int16_t v) {
         basEmit8(&p->cg, OP_PUSH_INT16);
         basEmit16(&p->cg, v);
    @@ -1470,6 +2340,13 @@ static void emitPushInt32(BasParserT *p, int32_t v) {
     }
     
     
    +// Push Empty, Null or Nothing (BasSpecialValE).
    +static void emitPushSpecial(BasParserT *p, uint8_t kind) {
    +    basEmit8(&p->cg, OP_PUSH_SPECIAL);
    +    basEmit8(&p->cg, kind);
    +}
    +
    +
     // Push a string constant of len bytes.
     static void emitPushStr(BasParserT *p, const char *text, int32_t len) {
         uint16_t idx = basAddConstant(&p->cg, text, len);
    @@ -1482,6 +2359,14 @@ static void emitPushStr(BasParserT *p, const char *text, int32_t len) {
     // Emit OP_POP `count` times.  Used to discard the live SELECT CASE test
     // values from the eval stack when EXIT/GOTO jumps out of one or more
     // enclosing SELECT constructs (see selectDepth).
    +// A program-screen statement: the built-in with argc arguments, result
    +// dropped.
    +static void emitScreenStmt(BasParserT *p, uint8_t func, int32_t argc) {
    +    emitBuiltinOp(p, OP_BUILTIN, func, argc);
    +    basEmit8(&p->cg, OP_POP);
    +}
    +
    +
     static void emitSelectPops(BasParserT *p, int32_t count) {
         for (int32_t i = 0; i < count; i++) {
             basEmit8(&p->cg, OP_POP);
    @@ -1522,12 +2407,20 @@ static void emitStore(BasParserT *p, BasSymbolT *sym) {
             emitStoreConv(p, sym->dataType);
         }
     
    +    // A variable declared As a class takes only an instance of it (or
    +    // Nothing)
    +    if (sym->objClass != BAS_CLASS_NONE && !sym->isArray) {
    +        emitObjCheck(p, sym->objClass);
    +    }
    +
         BasScopeE scope = storageScope(sym);
     
         if (scope == SCOPE_LOCAL) {
             basEmit8(&p->cg, OP_STORE_LOCAL);
         } else if (scope == SCOPE_FORM) {
             basEmit8(&p->cg, OP_STORE_FORM_VAR);
    +    } else if (scope == SCOPE_MEMBER) {
    +        basEmit8(&p->cg, OP_STORE_MEMBER);
         } else {
             basEmit8(&p->cg, OP_STORE_GLOBAL);
         }
    @@ -1537,25 +2430,14 @@ static void emitStore(BasParserT *p, BasSymbolT *sym) {
     
     
     // Emit the conversion that coerces the value on top of the stack to a
    -// declared numeric type before it is stored.  Shared by scalar, array
    -// element and TYPE field stores.  STRING, DOUBLE, BOOLEAN, OBJECT and
    +// declared numeric, BOOLEAN or DATE type before it is stored.  Shared by
    +// scalar, array element and TYPE field stores.  STRING, DOUBLE, OBJECT and
     // user types store the value as it is.
     static void emitStoreConv(BasParserT *p, uint8_t dataType) {
    -    switch (dataType) {
    -        case BAS_TYPE_INTEGER:
    -            basEmit8(&p->cg, OP_CONV_FLT_INT);
    -            break;
    -        case BAS_TYPE_LONG:
    -            basEmit8(&p->cg, OP_CONV_INT_LONG);
    -            break;
    -        case BAS_TYPE_SINGLE:
    -            basEmit8(&p->cg, OP_CONV_INT_FLT);
    -            break;
    -        case BAS_TYPE_BOOLEAN:
    -            basEmit8(&p->cg, OP_CONV_BOOL);
    -            break;
    -        default:
    -            break;
    +    uint8_t op = basConvOpForStore(dataType);
    +
    +    if (op != OP_NOP) {
    +        basEmit8(&p->cg, op);
         }
     }
     
    @@ -1652,6 +2534,39 @@ static void emitVarRefStore(BasParserT *p, const VarRefT *ref) {
     }
     
     
    +// Push the value a variable of dataType starts as (the value an omitted
    +// Optional argument takes): "" for STRING, 12:00:00 AM for DATE, Empty for
    +// VARIANT, Nothing for an object, 0 otherwise (a typed store converts it).
    +static void emitZeroValue(BasParserT *p, uint8_t dataType) {
    +    switch (dataType) {
    +        case BAS_TYPE_STRING:
    +            emitPushStr(p, "", 0);
    +            break;
    +
    +        case BAS_TYPE_DATE:
    +            emitPushDate(p, 0.0);
    +            break;
    +
    +        case BAS_TYPE_VARIANT:
    +            emitPushSpecial(p, BAS_SPECIAL_EMPTY);
    +            break;
    +
    +        case BAS_TYPE_OBJECT:
    +            emitPushSpecial(p, BAS_SPECIAL_NOTHING);
    +            break;
    +
    +        case BAS_TYPE_DOUBLE:
    +            basEmit8(&p->cg, OP_PUSH_FLT64);
    +            basEmitDouble(&p->cg, 0.0);
    +            break;
    +
    +        default:
    +            emitPushInt16(p, 0);
    +            break;
    +    }
    +}
    +
    +
     static BasSymbolT *ensureVariable(BasParserT *p, const char *name) {
         BasSymbolT *sym = basSymTabFind(&p->sym, name);
         if (sym != NULL) {
    @@ -1826,6 +2741,14 @@ static const BuiltinFuncT *findBuiltin(const char *name) {
     }
     
     
    +// The ENUM named name, or NULL.
    +static BasSymbolT *findEnum(BasParserT *p, const char *name) {
    +    BasSymbolT *sym = basSymTabFindGlobal(&p->sym, name);
    +
    +    return (sym != NULL && sym->kind == SYM_ENUM) ? sym : NULL;
    +}
    +
    +
     // The SUB/FUNCTION symbol a DECLARE refers to: the one the prescan or an
     // earlier DECLARE registered, else a new undefined entry.  NULL (error
     // set) when the name is already something else.
    @@ -1847,6 +2770,32 @@ static BasSymbolT *findOrAddProc(BasParserT *p, const char *name, BasSymKindE ki
     }
     
     
    +// The console / graphics statement at the current identifier, or NULL
    +// when it is none or the program gives the name its own meaning (a
    +// declared name, an assignment, a member access).
    +static const ScreenStmtT *findScreenStmt(BasParserT *p) {
    +    const char *name = p->lex.token.text;
    +
    +    for (int32_t i = 0; i < (int32_t)(sizeof(sScreenStmts) / sizeof(sScreenStmts[0])); i++) {
    +        if (strcasecmp(name, sScreenStmts[i].name) != 0) {
    +            continue;
    +        }
    +
    +        BasLexerT peek = p->lex;
    +
    +        basLexerNext(&peek);
    +
    +        if (peek.token.type == TOK_EQ || peek.token.type == TOK_DOT || basSymTabFind(&p->sym, name) != NULL) {
    +            return NULL;
    +        }
    +
    +        return &sScreenStmts[i];
    +    }
    +
    +    return NULL;
    +}
    +
    +
     static BasSymbolT *findTypeDef(BasParserT *p, const char *name) {
         for (int32_t i = 0; i < (int32_t)arrlen(p->typeDefs); i++) {
             if (strcasecmp(p->typeDefs[i]->name, name) == 0) {
    @@ -1862,6 +2811,26 @@ static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId) {
     }
     
     
    +// A compiler variable of the current scope that lives as long as the
    +// scope (unlike a temporary): FOR EACH keeps its group and position in
    +// two.  The '$' prefix keeps it out of reach of BASIC source.
    +static BasSymbolT *hiddenVariable(BasParserT *p) {
    +    char name[BAS_MAX_IDENT];
    +
    +    snprintf(name, sizeof(name), "%s%d", BAS_HIDDEN_PREFIX, (int)p->tempCount++);
    +
    +    BasSymbolT *sym = basSymTabAdd(&p->sym, name, SYM_VARIABLE, BAS_TYPE_VARIANT);
    +
    +    if (sym == NULL) {
    +        error(p, "Symbol table full");
    +        return NULL;
    +    }
    +
    +    sym->index = basSymTabAllocSlot(&p->sym);
    +    return sym;
    +}
    +
    +
     // Type a name takes without an AS clause: the suffix type when the name
     // has one, else the DEFxxx type for its first letter, else the QB default
     // (Single).  *outTyped (may be NULL) reports whether a suffix or DEFxxx
    @@ -1901,12 +2870,97 @@ static bool isArgDelimiter(BasTokenTypeE tt) {
     }
     
     
    +// DATE, VARIANT, Collection, Control, Form and Object: types named by
    +// identifiers rather than reserved words.
    +static bool isBuiltinTypeName(const char *name) {
    +    if (strcasecmp(name, BAS_DATE_TYPE_NAME) == 0 || strcasecmp(name, BAS_VARIANT_TYPE_NAME) == 0 || strcasecmp(name, BAS_COLLECTION_TYPE_NAME) == 0) {
    +        return true;
    +    }
    +
    +    for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(sObjectTypeNames); i++) {
    +        if (strcasecmp(name, sObjectTypeNames[i]) == 0) {
    +            return true;
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
    +// True for an instruction a constant expression may contain: literals,
    +// operators, conversions and the built-in functions whose result depends
    +// on nothing but their arguments (not the clock, files, the screen or
    +// variables).  fn is the function number of an OP_BUILTIN.
    +static bool isConstExprOp(uint8_t op, uint8_t fn) {
    +    switch (op) {
    +        case OP_PUSH_INT16:    case OP_PUSH_INT32:    case OP_PUSH_FLT64:
    +        case OP_PUSH_STR:      case OP_PUSH_TRUE:     case OP_PUSH_FALSE:
    +        case OP_ADD_INT:       case OP_SUB_INT:       case OP_MUL_INT:
    +        case OP_IDIV_INT:      case OP_MOD_INT:       case OP_NEG_INT:
    +        case OP_DIV_FLT:       case OP_POW:
    +        case OP_CMP_EQ:        case OP_CMP_NE:        case OP_CMP_LT:
    +        case OP_CMP_GT:        case OP_CMP_LE:        case OP_CMP_GE:
    +        case OP_AND:           case OP_OR:            case OP_NOT:
    +        case OP_XOR:           case OP_EQV:           case OP_IMP:
    +        case OP_CONV_INT_FLT:  case OP_CONV_FLT_INT:  case OP_CONV_INT_STR:
    +        case OP_CONV_STR_INT:  case OP_CONV_STR_FLT:  case OP_CONV_INT_LONG:
    +        case OP_CONV_DATE:     case OP_CONV_BOOL:
    +        case OP_STR_CONCAT:    case OP_STR_LEFT:      case OP_STR_RIGHT:
    +        case OP_STR_MID:       case OP_STR_MID2:      case OP_STR_LEN:
    +        case OP_STR_INSTR:     case OP_STR_INSTR3:    case OP_STR_UCASE:
    +        case OP_STR_LCASE:     case OP_STR_TRIM:      case OP_STR_LTRIM:
    +        case OP_STR_RTRIM:     case OP_STR_CHR:       case OP_STR_ASC:
    +        case OP_STR_SPACE:     case OP_STR_VAL:       case OP_STR_STRF:
    +        case OP_STR_HEX:       case OP_STR_OCT:       case OP_STR_STRING:
    +        case OP_FORMAT:
    +        case OP_MATH_ABS:      case OP_MATH_INT:      case OP_MATH_FIX:
    +        case OP_MATH_SGN:      case OP_MATH_SQR:      case OP_MATH_SIN:
    +        case OP_MATH_COS:      case OP_MATH_TAN:      case OP_MATH_ATN:
    +        case OP_MATH_LOG:      case OP_MATH_EXP:
    +        case OP_RGB:           case OP_GET_RED:       case OP_GET_GREEN:
    +        case OP_GET_BLUE:
    +            return true;
    +
    +        case OP_BUILTIN:
    +            switch (fn) {
    +                case BAS_FN_REPLACE:     case BAS_FN_INSTRREV:    case BAS_FN_STRCOMP:
    +                case BAS_FN_STRREVERSE:  case BAS_FN_IIF:         case BAS_FN_CHOOSE:
    +                case BAS_FN_DATESERIAL:  case BAS_FN_TIMESERIAL:  case BAS_FN_DATEVALUE:
    +                case BAS_FN_TIMEVALUE:   case BAS_FN_YEAR:        case BAS_FN_MONTH:
    +                case BAS_FN_DAY:         case BAS_FN_WEEKDAY:     case BAS_FN_HOUR:
    +                case BAS_FN_MINUTE:      case BAS_FN_SECOND:      case BAS_FN_DATEADD:
    +                case BAS_FN_DATEDIFF:    case BAS_FN_DATEPART:    case BAS_FN_ISDATE:
    +                case BAS_FN_MONTHNAME:   case BAS_FN_WEEKDAYNAME:
    +                    return true;
    +
    +                default:
    +                    return false;
    +            }
    +
    +        default:
    +            return false;
    +    }
    +}
    +
    +
     // DEF FN function names start with FN.
     static bool isDefFnName(const char *name) {
         return basAsciiUpper(name[0]) == 'F' && basAsciiUpper(name[1]) == 'N';
     }
     
     
    +// True when name is a form of the project (a BEGINFORM name).
    +static bool isFormName(const BasParserT *p, const char *name) {
    +    for (int32_t i = 0; i < (int32_t)arrlen(p->formNames); i++) {
    +        if (strcasecmp(p->formNames[i], name) == 0) {
    +            return true;
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
     // True when the next tokens are a (signed) numeric literal with a
     // fractional part standing alone as the rest of the statement.
     static bool isFractionalLiteralAhead(const BasParserT *p) {
    @@ -1940,6 +2994,19 @@ static bool isNumericLiteralAhead(const BasParserT *p) {
     }
     
     
    +// True when sym is the FUNCTION (or Property Get) being compiled: inside
    +// it the name stands for its result.
    +static bool isOwnResult(const BasParserT *p, const BasSymbolT *sym) {
    +    return sym != NULL && sym->kind == SYM_FUNCTION && p->currentProcIsFunction && sym->codeAddr == p->currentProcAddr;
    +}
    +
    +
    +// Property Get / Let / Set.
    +static bool isPropertyKind(uint8_t kind) {
    +    return kind == BAS_MEMBER_GET || kind == BAS_MEMBER_LET || kind == BAS_MEMBER_SET;
    +}
    +
    +
     // Report the lexer's error at the token that raised it.
     static void lexError(BasParserT *p) {
         errorAt(p, p->lex.errLine, p->lex.errCol, p->lex.error);
    @@ -1955,12 +3022,14 @@ static void loopBegin(BasParserT *p, bool isFor, LoopSaveT *save) {
     
         save->list       = *list;
         save->selectBase = *base;
    +    save->forIsEach  = p->forIsEach;
     
         exitListInit(list);
         *base = p->selectDepth;
     
         if (isFor) {
             p->forDepth++;
    +        p->forIsEach = false;
         } else {
             p->doDepth++;
         }
    @@ -1976,8 +3045,9 @@ static void loopEnd(BasParserT *p, bool isFor, LoopSaveT *save) {
     
         exitListPatch(list, p);
     
    -    *list = save->list;
    -    *base = save->selectBase;
    +    *list        = save->list;
    +    *base        = save->selectBase;
    +    p->forIsEach = save->forIsEach;
     
         if (isFor) {
             p->forDepth--;
    @@ -2023,6 +3093,39 @@ static bool matchEnd(BasParserT *p, BasTokenTypeE kw) {
     }
     
     
    +// True at the name of something being declared: an identifier, or in a
    +// class module (outside procedures) any word, so a member may be called
    +// Name, Open, Show ... as in VB (programs reach it as obj.Name / Me.Name).
    +static bool memberNameAhead(BasParserT *p) {
    +    return check(p, TOK_IDENT) || (p->sym.inClassScope && !p->sym.inLocalScope && basTokenIsWord(&p->lex.token));
    +}
    +
    +
    +// Me: the class instance in a class module, else the form.
    +static uint8_t meOpcode(const BasParserT *p) {
    +    return p->sym.inClassScope ? OP_ME_OBJ : OP_ME_REF;
    +}
    +
    +
    +// The name a module-level declaration registers: "Class.name" in a class
    +// module, else name.  False (error set) when that does not fit.
    +static bool moduleLevelName(BasParserT *p, const char *name, char *out, int32_t outSize) {
    +    if (!p->sym.inClassScope || p->sym.inLocalScope) {
    +        snprintf(out, outSize, "%s", name);
    +        return true;
    +    }
    +
    +    if (!basSymTabQualify(&p->sym, p->sym.classIdx, name, out, outSize)) {
    +        char buf[BAS_PARSE_ERR_SCRATCH];
    +        snprintf(buf, sizeof(buf), "Name '%s' is too long for a class member", name);
    +        error(p, buf);
    +        return false;
    +    }
    +
    +    return true;
    +}
    +
    +
     // Report whether a name carries an explicit BASIC type-suffix character.
     // Lets callers distinguish an explicitly typed name like "a!" from a bare
     // "a", which suffixToType cannot do alone because it returns
    @@ -2037,6 +3140,20 @@ static bool nameHasTypeSuffix(const char *name) {
     }
     
     
    +// True at "Name =" where Name is a keyword naming the FUNCTION / Property
    +// Get being compiled (its result is set by its name).
    +static bool ownResultKeywordAhead(BasParserT *p) {
    +    if (!p->currentProcIsFunction || check(p, TOK_IDENT) || !basTokenIsWord(&p->lex.token) || strcasecmp(p->lex.token.text, p->currentProc) != 0) {
    +        return false;
    +    }
    +
    +    BasLexerT peek = p->lex;
    +
    +    basLexerNext(&peek);
    +    return peek.token.type == TOK_EQ;
    +}
    +
    +
     static void parseAddExpr(BasParserT *p) {
         parseMulExpr(p);
         while (!p->hasError) {
    @@ -2136,6 +3253,24 @@ static void parseAssignOrCall(BasParserT *p) {
             return;
         }
     
    +    // LSET / RSET target = value: the value left / right justified in the
    +    // target string's current length (a procedure of that name wins)
    +    if ((strcasecmp(name, "LSET") == 0 || strcasecmp(name, "RSET") == 0) && check(p, TOK_IDENT) && basSymTabFind(&p->sym, name) == NULL) {
    +        uint8_t func = (basAsciiUpper(name[0]) == 'L') ? BAS_FN_LSET : BAS_FN_RSET;
    +        VarRefT ref;
    +
    +        if (!parseVarRef(p, &ref, true)) {
    +            return;
    +        }
    +
    +        expect(p, TOK_EQ);
    +        emitVarRefLoad(p, &ref);
    +        parseExpression(p);
    +        emitBuiltinOp(p, OP_BUILTIN, func, BAS_LSET_ARGS);
    +        emitVarRefStore(p, &ref);
    +        return;
    +    }
    +
         BasSymbolT *sym = basSymTabFind(&p->sym, name);
     
         if (sym == NULL && (check(p, TOK_DOT) || check(p, TOK_LPAREN)) && rejectHiddenModuleVar(p, name)) {
    @@ -2166,7 +3301,7 @@ static void parseAssignOrCall(BasParserT *p) {
             memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
             advance(p); // consume member name
     
    -        bool isVarRef = (sym != NULL && sym->kind == SYM_VARIABLE);
    +        bool isVarRef = (sym != NULL && (sym->kind == SYM_VARIABLE || isOwnResult(p, sym)));
     
             // FormName.Ctrl.Member... (or formVar.Ctrl...): the control lives
             // on the named form.
    @@ -2194,7 +3329,7 @@ static void parseAssignOrCall(BasParserT *p) {
                 return;
             }
     
    -        bool isAssign = match(p, TOK_EQ);
    +        bool isAssign = check(p, TOK_EQ) || (check(p, TOK_LPAREN) && peekAfterParens(p) == TOK_EQ);
     
             // Compile-time validation (IDE only).  Skipped when `name` is a
             // variable reference: a dynamic control we can't statically type.
    @@ -2204,15 +3339,11 @@ static void parseAssignOrCall(BasParserT *p) {
     
             emitCtrlRef(p, sym, name);
             emitPushCStr(p, memberName);
    +        p->memberClass = isVarRef ? sym->objClass : BAS_CLASS_NONE;
     
    -        if (isAssign) {
    -            // Property assignment: CtrlName.Property = expr
    -            parseExpression(p);
    -            basEmit8(&p->cg, OP_STORE_PROP);
    -        } else {
    -            // Method call: CtrlName.Method [args]
    -            emitMethodCallStatement(p);
    -        }
    +        // CtrlName.Property = expr, obj.Property(args) = expr, or a
    +        // method call: CtrlName.Method [args]
    +        parseMemberAssignOrCall(p, memberName);
             return;
         }
     
    @@ -2247,7 +3378,7 @@ static void parseAssignOrCall(BasParserT *p) {
                 return;
             }
     
    -        if (sym->kind != SYM_VARIABLE || !sym->isArray) {
    +        if (sym->kind != SYM_VARIABLE || (!sym->isArray && !canIndexValue(sym))) {
                 char buf[BAS_PARSE_ERR_SCRATCH];
                 snprintf(buf, sizeof(buf), "'%s' is not an array", name);
                 error(p, buf);
    @@ -2258,6 +3389,15 @@ static void parseAssignOrCall(BasParserT *p) {
             expect(p, TOK_LPAREN);
             int32_t dims = parseIndexList(p);
     
    +        // A member of the object at that element: arr(i).Name = x,
    +        // c(1).Method args
    +        if (sym->dataType != BAS_TYPE_UDT && check(p, TOK_DOT)) {
    +            basEmit8(&p->cg, OP_LOAD_ARRAY);
    +            basEmit8(&p->cg, (uint8_t)dims);
    +            parseObjectMemberStatement(p);
    +            return;
    +        }
    +
             // Array-of-UDT field store: arr(i).field = expr, or a nested
             // arr(i).field.sub = expr (the element is loaded and the chain
             // stored through it -- TYPE elements are shared references).
    @@ -2290,11 +3430,16 @@ static void parseAssignOrCall(BasParserT *p) {
             }
     
             expect(p, TOK_EQ);
    -        parseExpression(p);
    +        emitAssignValue(p);
     
    -        if (sym->isTyped) {
    +        if (sym->isTyped && sym->isArray) {
                 emitStoreConv(p, sym->dataType);
             }
    +
    +        if (sym->objClass != BAS_CLASS_NONE && sym->isArray) {
    +            emitObjCheck(p, sym->objClass);
    +        }
    +
             basEmit8(&p->cg, OP_STORE_ARRAY);
             basEmit8(&p->cg, (uint8_t)dims);
             return;
    @@ -2309,6 +3454,13 @@ static void parseAssignOrCall(BasParserT *p) {
             if (sym == NULL) {
                 return;
             }
    +
    +        // SET makes the variable an object variable from here on, whatever
    +        // it was declared as (DIM f AS LONG: SET f = CreateForm(...) keeps
    +        // the form) -- unless it is declared As a class or VARIANT
    +        if (p->setAssign && sym->kind == SYM_VARIABLE && sym->objClass == BAS_CLASS_NONE && sym->dataType != BAS_TYPE_VARIANT) {
    +            sym->dataType = BAS_TYPE_OBJECT;
    +        }
             // Assigning to the enclosing FUNCTION's own name sets its return
             // value.  Any other procedure name is not a store target (a
             // STORE_LOCAL 0 here would clobber the caller's first slot).
    @@ -2319,23 +3471,27 @@ static void parseAssignOrCall(BasParserT *p) {
                     error(p, buf);
                     return;
                 }
    -            parseExpression(p);
    +            emitAssignValue(p);
     
                 if (sym->isTyped) {
                     emitStoreConv(p, sym->dataType);
                 }
     
    +            if (sym->objClass != BAS_CLASS_NONE) {
    +                emitObjCheck(p, sym->objClass);
    +            }
    +
                 // Store to the implicit return-value local slot (index 0 in function scope)
                 basEmit8(&p->cg, OP_STORE_LOCAL);
                 basEmitU16(&p->cg, 0);
                 return;
             }
     
    -        if (tryEmitAppend(p, sym)) {
    +        if (!p->setAssign && tryEmitAppend(p, sym)) {
                 return;
             }
     
    -        parseExpression(p);
    +        emitAssignValue(p);
             emitStore(p, sym);
             return;
         }
    @@ -2365,6 +3521,78 @@ static void parseAssignOrCall(BasParserT *p) {
     }
     
     
    +// Attribute Name.VB_UserMemId = 0 in a class module makes Name the
    +// default member (obj(args) reaches it).  Every other Attribute line
    +// (VB_Name, VB_Description ...) is accepted and ignored.
    +static void parseAttribute(BasParserT *p) {
    +    char member[BAS_MAX_TOKEN_LEN];
    +    bool isDefault = false;
    +
    +    advance(p); // consume Attribute
    +    snprintf(member, sizeof(member), "%s", p->lex.token.text);
    +
    +    if (basTokenIsWord(&p->lex.token)) {
    +        advance(p);
    +
    +        if (match(p, TOK_DOT) && basTokenIsWord(&p->lex.token) && strcasecmp(p->lex.token.text, BAS_ATTR_USER_MEM_ID) == 0) {
    +            advance(p);
    +            isDefault = match(p, TOK_EQ) && check(p, TOK_INT_LIT) && p->lex.token.intVal == BAS_DEFAULT_MEMBER_ID;
    +        }
    +    }
    +
    +    if (isDefault) {
    +        snprintf(p->sym.classes[p->sym.classIdx].defaultMember, BAS_MAX_IDENT, "%s", member);
    +    }
    +
    +    while (!p->hasError && !atStatementEnd(p)) {
    +        advance(p);
    +    }
    +}
    +
    +
    +// BEGINCLASS "Name" ... ENDCLASS wrap a class module's code (the project
    +// builder adds them).  Module-level declarations inside make member
    +// variables; the code they emit, then a call of Class_Initialize, is the
    +// instance set-up a New runs, compiled behind a jump like a form's init
    +// block.
    +static void parseBeginClass(BasParserT *p) {
    +    advance(p); // consume BEGINCLASS
    +
    +    if (!check(p, TOK_STRING_LIT)) {
    +        errorExpected(p, "class name string");
    +        return;
    +    }
    +
    +    char name[BAS_MAX_TOKEN_LEN];
    +
    +    snprintf(name, sizeof(name), "%s", p->lex.token.text);
    +    advance(p);
    +
    +    if (p->sym.inClassScope || p->sym.inFormScope || p->sym.inLocalScope) {
    +        error(p, "BEGINCLASS inside a form, class or procedure is not allowed");
    +        return;
    +    }
    +
    +    // The prescan registered every class
    +    int32_t       classIdx = -1;
    +    BasClassDefT *cls      = basSymTabFindClass(&p->sym, name, &classIdx);
    +
    +    if (cls == NULL || cls->defined) {
    +        char buf[BAS_PARSE_ERR_SCRATCH];
    +        snprintf(buf, sizeof(buf), "'%s' cannot be used as a class name (it is already defined)", name);
    +        error(p, buf);
    +        return;
    +    }
    +
    +    cls->defined = true;
    +    basSymTabEnterClassScope(&p->sym, classIdx);
    +    tempPoolReset(p);
    +
    +    p->formInitJmpAddr   = emitJump(p, OP_JMP);
    +    p->formInitCodeStart = basCodePos(&p->cg);
    +}
    +
    +
     // BEGINFORM "FormName" enters form scope: DIM at module level
     // creates per-form variables. ENDFORM exits form scope.
     static void parseBeginForm(BasParserT *p) {
    @@ -2455,11 +3683,21 @@ static int32_t parseCallArgs(BasParserT *p, BasSymbolT *sym, bool parens) {
         }
     
         while (more && !p->hasError) {
    +        // A ParamArray takes every remaining argument as one array.
    +        if (sym->hasParamArray && argc == sym->paramCount - 1) {
    +            emitParamArray(p, sym, true);
    +            argc++;
    +            break;
    +        }
    +
             bool declared = argc < sym->paramCount;
     
             // Native functions take every argument by value, converted to the
    -        // declared parameter type.
    -        if (sym->isExtern) {
    +        // declared parameter type.  An object parameter may be given a
    +        // bare form or control name.
    +        if (declared && sym->paramTypes[argc] == BAS_TYPE_OBJECT && bareObjectAhead(p)) {
    +            emitBareObject(p);
    +        } else if (sym->isExtern) {
                 parseExpression(p);
     
                 if (declared) {
    @@ -2540,6 +3778,11 @@ static void parseCompareExpr(BasParserT *p) {
                 advance(p);
                 parseConcatExpr(p);
                 basEmit8(&p->cg, OP_CMP_GE);
    +        } else if (check(p, TOK_IS)) {
    +            // a Is b: the same object (b may be a bare form or control name)
    +            advance(p);
    +            parseObjectOperand(p, parseConcatExpr);
    +            basEmit8(&p->cg, OP_IS);
             } else {
                 break;
             }
    @@ -2558,10 +3801,22 @@ static void parseConcatExpr(BasParserT *p) {
     
     
     static void parseConst(BasParserT *p) {
    -    // CONST name [AS type] = literal.  A CONST inside a SUB/FUNCTION is
    -    // local to it (it may shadow a module CONST of the same name).
    +    // CONST name [AS type] = expr [, name [AS type] = expr ...].  Each
    +    // value is a constant expression folded at compile time.  A CONST
    +    // inside a SUB/FUNCTION is local to it (it may shadow a module CONST
    +    // of the same name).
         advance(p); // consume CONST
     
    +    do {
    +        parseConstDeclarator(p);
    +    } while (!p->hasError && match(p, TOK_COMMA));
    +}
    +
    +
    +// One "name [AS type] = expr" of a CONST statement.  Without an AS clause
    +// the constant takes the expression's type (integers are LONG); with one,
    +// the value is converted to that type as assignment would convert it.
    +static void parseConstDeclarator(BasParserT *p) {
         if (!check(p, TOK_IDENT)) {
             errorExpected(p, "constant name");
             return;
    @@ -2577,85 +3832,51 @@ static void parseConst(BasParserT *p) {
     
         expect(p, TOK_EQ);
     
    -    // The value: a string, TRUE/FALSE or a numeric literal (optionally
    -    // negative).  Untyped integer literals are LONG constants.
    -    bool        isNeg  = parseMinusForLiteral(p);
    -    uint8_t     dt     = BAS_TYPE_LONG;
    -    int32_t     intVal = 0;
    -    double      dblVal = 0.0;
    -    const char *strVal = NULL;
    -    char        strBuf[BAS_MAX_CONST_STR];
    -    NumLitT     lit;
    +    ConstValT val;
     
    -    if (p->hasError) {
    +    if (p->hasError || !parseConstExpr(p, &val)) {
             return;
         }
     
    -    if (parseNumLit(p, isNeg, &lit)) {
    -        dt     = lit.isFloat ? BAS_TYPE_DOUBLE : BAS_TYPE_LONG;
    -        intVal = lit.intVal;
    -        dblVal = lit.dblVal;
    -    } else if (check(p, TOK_STRING_LIT) && !isNeg) {
    -        snprintf(strBuf, sizeof(strBuf), "%s", p->lex.token.text);
    -        strVal = strBuf;
    -        dt     = BAS_TYPE_STRING;
    -        advance(p);
    -    } else if ((check(p, TOK_TRUE_KW) || check(p, TOK_FALSE_KW)) && !isNeg) {
    -        dt     = BAS_TYPE_BOOLEAN;
    -        intVal = check(p, TOK_TRUE_KW) ? -1 : 0;
    -        dblVal = intVal;
    -        advance(p);
    -    } else {
    -        error(p, "Constant value must be a literal");
    +    if (hasAs && !constConvert(p, &val, asType, "CONST")) {
             return;
         }
     
    -    // An AS clause converts the literal to that type the way assignment
    -    // would: numbers round to INTEGER/LONG (overflow is an error), become
    -    // SINGLE/DOUBLE, or TRUE/FALSE; a string never mixes with a number.
    -    if (hasAs && !p->hasError) {
    -        bool fromString = (dt == BAS_TYPE_STRING);
    -        bool toString   = (asType == BAS_TYPE_STRING);
    +    char symName[BAS_MAX_IDENT];
     
    -        if (fromString != toString || asType == BAS_TYPE_UDT) {
    -            error(p, "Type mismatch in CONST");
    -            return;
    -        }
    -
    -        if (asType == BAS_TYPE_INTEGER || asType == BAS_TYPE_LONG) {
    -            int64_t rounded = 0;
    -            int64_t minVal  = (asType == BAS_TYPE_INTEGER) ? INT16_MIN : INT32_MIN;
    -            int64_t maxVal  = (asType == BAS_TYPE_INTEGER) ? INT16_MAX : INT32_MAX;
    -
    -            if (!basRoundHalfEven(dblVal, &rounded) || rounded < minVal || rounded > maxVal) {
    -                error(p, "Overflow in CONST");
    -                return;
    -            }
    -
    -            intVal = (int32_t)rounded;
    -        } else if (asType == BAS_TYPE_SINGLE) {
    -            dblVal = (float)dblVal;
    -        } else if (asType == BAS_TYPE_BOOLEAN) {
    -            intVal = (dblVal != 0.0) ? -1 : 0;
    -        }
    -
    -        dt = asType;
    +    if (!moduleLevelName(p, name, symName, (int32_t)sizeof(symName))) {
    +        return;
         }
     
    -    BasSymbolT *sym = basSymTabAdd(&p->sym, name, SYM_CONST, dt);
    +    BasSymbolT *sym = basSymTabAdd(&p->sym, symName, SYM_CONST, val.dataType);
     
         if (sym == NULL) {
             error(p, "Duplicate constant or symbol table full");
             return;
         }
     
    -    if (dt == BAS_TYPE_STRING) {
    -        snprintf(sym->constStr, sizeof(sym->constStr), "%s", strVal);
    -    } else if (dt == BAS_TYPE_SINGLE || dt == BAS_TYPE_DOUBLE) {
    -        sym->constDbl = dblVal;
    -    } else {
    -        sym->constInt = intVal;
    -    }
    +    constStore(sym, &val);
    +}
    +
    +
    +// A constant expression (CONST and ENUM values): compiled like any
    +// expression with only constants nameable, checked to hold nothing but
    +// literals, operators and pure built-ins, run on the parser's VM, and
    +// removed from the code again.  False with an error set when the
    +// expression is not constant or its evaluation fails (division by zero,
    +// overflow ...).
    +static bool parseConstExpr(BasParserT *p, ConstValT *out) {
    +    int32_t codeStart  = basCodePos(&p->cg);
    +    int32_t constStart = p->cg.constCount;
    +
    +    p->inConstExpr = true;
    +    parseExpression(p);
    +    p->inConstExpr = false;
    +
    +    bool ok = !p->hasError && constExprCheck(p, codeStart) && constExprRun(p, codeStart, out);
    +
    +    basCodeGenTruncate(&p->cg, codeStart, constStart);
    +    return ok;
     }
     
     
    @@ -2787,7 +4008,7 @@ static void parseDeclare(BasParserT *p) {
         // partial overwrite.  A call-site stub has no signature yet and simply
         // takes this one.
         if (sym->hasSignature) {
    -        bool matches = (sym->paramCount == pl.count);
    +        bool matches = (sym->paramCount == pl.count && sym->hasParamArray == pl.hasParamArray && sym->paramArrayTyped == pl.paramArrayTyped);
     
             for (int32_t i = 0; matches && i < pl.count; i++) {
                 matches = sym->paramTypes[i] == pl.types[i] && sym->paramByVal[i] == pl.byVal[i] && sym->paramOptional[i] == pl.optional[i];
    @@ -2854,6 +4075,11 @@ static void parseDeclareLibrary(BasParserT *p) {
                 return;
             }
     
    +        if (pl.hasParamArray) {
    +            error(p, "ParamArray is not allowed in DECLARE LIBRARY");
    +            return;
    +        }
    +
             // Strip the type suffix ($%&!#) from the extern name so dlsym finds
             // the C function; the suffix still gave the return type above.
             char    externName[BAS_MAX_TOKEN_LEN];
    @@ -2941,6 +4167,11 @@ static void parseDef(BasParserT *p) {
             return;
         }
     
    +    if (pl.hasParamArray) {
    +        error(p, "ParamArray is not allowed in DEF FN");
    +        return;
    +    }
    +
         // Names other than the parameters are module variables (QB), so the
         // body is parsed with implicit declarations routed to module scope.
         expect(p, TOK_EQ);
    @@ -3044,8 +4275,13 @@ static void parseDim(BasParserT *p) {
             return;
         }
     
    +    if (isShared && p->sym.inClassScope) {
    +        error(p, "DIM SHARED is not allowed in a class module");
    +        return;
    +    }
    +
         do {
    -        parseDimDeclarator(p, isShared);
    +        parseDimDeclarator(p, isShared, true);
         } while (match(p, TOK_COMMA));
     }
     
    @@ -3090,8 +4326,8 @@ static void parseDimBounds(BasParserT *p, int32_t *outDims) {
     
     
     // One "name[(bounds)] [AS type]" of a DIM statement.
    -static void parseDimDeclarator(BasParserT *p, bool isShared) {
    -    if (!check(p, TOK_IDENT)) {
    +static void parseDimDeclarator(BasParserT *p, bool isShared, bool isPrivate) {
    +    if (!memberNameAhead(p)) {
             errorExpected(p, "variable name");
             return;
         }
    @@ -3118,25 +4354,44 @@ static void parseDimDeclarator(BasParserT *p, bool isShared) {
             expect(p, TOK_RPAREN);
         }
     
    -    // Optional AS type.  Without one the suffix or DEFxxx type applies,
    -    // else the QB default (Single) -- either way the variable is typed.
    +    // Optional AS [NEW] type.  Without one the suffix or DEFxxx type
    +    // applies, else the QB default (Single) -- either way the variable is
    +    // typed (a VARIANT holds any value as it is).
         uint8_t dt        = implicitType(p, name, NULL);
         int32_t udtTypeId = -1;
         int32_t fixedLen  = 0;
    +    int32_t objClass  = BAS_CLASS_NONE;
    +    bool    asNew     = false;
         if (match(p, TOK_AS)) {
    -        dt = resolveTypeName(p);
    +        asNew = match(p, TOK_NEW);
    +        dt    = resolveTypeName(p);
             if (dt == BAS_TYPE_UDT) {
                 udtTypeId = p->lastUdtTypeId;
             }
             if (dt == BAS_TYPE_STRING) {
                 fixedLen = parseFixedStringLen(p);
             }
    +        objClass = p->lastObjClass;
         }
     
         if (p->hasError) {
             return;
         }
     
    +    if (asNew && (objClass == BAS_CLASS_NONE || isArray)) {
    +        error(p, isArray ? "New cannot be used with an array" : "New needs a class name (or Collection)");
    +        return;
    +    }
    +
    +    // A class module's member variables are "Class.name"
    +    char    qualified[BAS_MAX_IDENT];
    +    bool    isMember  = p->sym.inClassScope && !p->sym.inLocalScope;
    +
    +    if (isMember && !basSymTabQualify(&p->sym, p->sym.classIdx, name, qualified, (int32_t)sizeof(qualified))) {
    +        error(p, "Member variable name too long");
    +        return;
    +    }
    +
         // Duplicate detection only rejects a name already declared in the SAME
         // scope this DIM creates (local > form > global): a local DIM may
         // shadow a module-level variable (standard QB semantics).  Only a
    @@ -3157,7 +4412,7 @@ static void parseDimDeclarator(BasParserT *p, bool isShared) {
             return;
         }
     
    -    BasSymbolT *sym = basSymTabAdd(&p->sym, name, SYM_VARIABLE, dt);
    +    BasSymbolT *sym = basSymTabAdd(&p->sym, isMember ? qualified : name, SYM_VARIABLE, dt);
         if (sym == NULL) {
             error(p, "Symbol table full or duplicate name");
             return;
    @@ -3168,13 +4423,24 @@ static void parseDimDeclarator(BasParserT *p, bool isShared) {
         sym->isShared  = isShared;
         sym->udtTypeId = udtTypeId;
         sym->fixedLen  = fixedLen;
    -    sym->isTyped   = true;
    +    sym->isTyped   = (dt != BAS_TYPE_VARIANT);
    +    sym->objClass  = objClass;
    +
    +    if (isMember) {
    +        addClassMember(p, name, sym, BAS_MEMBER_VAR, isPrivate);
    +    }
     
         if (isDynamic) {
             // Nothing to allocate until REDIM.
             return;
         }
     
    +    if (asNew) {
    +        emitNewObject(p, objClass);
    +        emitStore(p, sym);
    +        return;
    +    }
    +
         if (isArray) {
             if (dt == BAS_TYPE_UDT && udtTypeId >= 0 && !emitUdtSpec(p, udtTypeId)) {
                 return;
    @@ -3198,6 +4464,12 @@ static void parseDimDeclarator(BasParserT *p, bool isShared) {
             // stay harmless, but STRING concat checks actual slot type.
             emitPushStr(p, "", 0);
             emitStore(p, sym);
    +    } else if (dt == BAS_TYPE_DATE || dt == BAS_TYPE_VARIANT || dt == BAS_TYPE_OBJECT || isMember) {
    +        // DATE slots start as 12:00:00 AM on day 0, VARIANTs Empty and
    +        // object variables Nothing, not the number 0; a new object's member
    +        // variables start Empty until this sets their declared zero.
    +        emitZeroValue(p, dt);
    +        emitStore(p, sym);
         }
     }
     
    @@ -3285,6 +4557,39 @@ static void parseEnd(BasParserT *p) {
     }
     
     
    +// ENDCLASS: the instance set-up calls Class_Initialize and returns.
    +static void parseEndClass(BasParserT *p) {
    +    advance(p); // consume ENDCLASS
    +
    +    if (!p->sym.inClassScope || p->sym.inLocalScope) {
    +        error(p, "ENDCLASS without BEGINCLASS");
    +        return;
    +    }
    +
    +    BasClassDefT *cls  = &p->sym.classes[p->sym.classIdx];
    +    BasSymbolT   *init = classEventProc(p, BAS_CLASS_INITIALIZE);
    +
    +    cls->terminateSym = classEventProc(p, BAS_CLASS_TERMINATE);
    +
    +    if (p->hasError) {
    +        return;
    +    }
    +
    +    if (init != NULL) {
    +        emitCallWithArgs(p, init, 0);
    +    }
    +
    +    basEmit8(&p->cg, OP_RET);
    +    patchJump(p, p->formInitJmpAddr);
    +
    +    cls->initCodeAddr    = p->formInitCodeStart;
    +    cls->varCount        = basSymTabLeaveClassScope(&p->sym);
    +    p->formInitJmpAddr   = -1;
    +    p->formInitCodeStart = -1;
    +    tempPoolReset(p);
    +}
    +
    +
     static void parseEndForm(BasParserT *p) {
         advance(p); // consume ENDFORM
     
    @@ -3334,6 +4639,106 @@ static void parseEndForm(BasParserT *p) {
     }
     
     
    +static void parseEnum(BasParserT *p) {
    +    // ENUM name
    +    //     member [= constant expression]
    +    //     ...
    +    // END ENUM
    +    // Members are LONG constants numbered from 0, each one more than the
    +    // member before it unless given a value.  They are used as Member or
    +    // EnumName.Member; the ENUM name is a type (stored as LONG).
    +    advance(p); // consume ENUM
    +
    +    if (p->sym.inLocalScope) {
    +        error(p, "ENUM cannot be defined inside SUB or FUNCTION");
    +        return;
    +    }
    +
    +    if (!check(p, TOK_IDENT)) {
    +        errorExpected(p, "ENUM name");
    +        return;
    +    }
    +
    +    // The prescan registered the name (signatures may use it as a type);
    +    // a second ENUM of that name, or any other symbol with it, is a
    +    // duplicate.
    +    BasSymbolT *enumSym = basSymTabFindGlobal(&p->sym, p->lex.token.text);
    +
    +    if (enumSym == NULL) {
    +        enumSym = basSymTabAddGlobal(&p->sym, p->lex.token.text, SYM_ENUM, BAS_TYPE_LONG);
    +    } else if (enumSym->kind != SYM_ENUM || enumSym->isDefined) {
    +        enumSym = NULL;
    +    }
    +
    +    if (enumSym == NULL) {
    +        char buf[BAS_PARSE_ERR_SCRATCH];
    +        snprintf(buf, sizeof(buf), "'%s' is already defined", p->lex.token.text);
    +        error(p, buf);
    +        return;
    +    }
    +
    +    enumSym->isDefined = true;
    +    advance(p);
    +    expectEndOfStatement(p);
    +    skipNewlines(p);
    +
    +    int64_t nextVal     = 0;
    +    int32_t memberCount = 0;
    +    bool    ended       = false;
    +
    +    while (!p->hasError && !check(p, TOK_EOF)) {
    +        if (matchEnd(p, TOK_ENUM)) {
    +            ended = true;
    +            break;
    +        }
    +
    +        if (!check(p, TOK_IDENT)) {
    +            errorExpected(p, "ENUM member name or END ENUM");
    +            return;
    +        }
    +
    +        char member[BAS_MAX_TOKEN_LEN];
    +        strncpy(member, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
    +        member[BAS_MAX_TOKEN_LEN - 1] = '\0';
    +        advance(p);
    +
    +        if (match(p, TOK_EQ)) {
    +            ConstValT val;
    +
    +            if (!parseConstExpr(p, &val) || !constConvert(p, &val, BAS_TYPE_LONG, "ENUM")) {
    +                return;
    +            }
    +
    +            nextVal = val.intVal;
    +        } else if (nextVal > INT32_MAX) {
    +            error(p, "Overflow in ENUM");
    +            return;
    +        }
    +
    +        if (!addEnumMember(p, enumSym, member, (int32_t)nextVal)) {
    +            return;
    +        }
    +
    +        nextVal++;
    +        memberCount++;
    +        expectEndOfStatement(p);
    +        skipNewlines(p);
    +    }
    +
    +    if (p->hasError) {
    +        return;
    +    }
    +
    +    if (!ended) {
    +        error(p, "Expected END ENUM");
    +    } else if (memberCount == 0) {
    +        char buf[BAS_PARSE_ERR_SCRATCH];
    +        snprintf(buf, sizeof(buf), "ENUM '%s' has no members", enumSym->name);
    +        error(p, buf);
    +    }
    +}
    +
    +
     static void parseEqvExpr(BasParserT *p) {
         parseOrExpr(p);
         while (!p->hasError && check(p, TOK_EQV)) {
    @@ -3384,9 +4789,14 @@ static void parseExit(BasParserT *p) {
                 return;
             }
             // Discard the test values of any SELECT CASE bodies this EXIT
    -        // jumps out of, then drop the VM for-frame and jump.
    +        // jumps out of, then drop the VM for-frame (FOR EACH has none) and
    +        // jump.
             emitSelectPops(p, p->selectDepth - p->forSelectBase);
    -        basEmit8(&p->cg, OP_FOR_POP);
    +
    +        if (!p->forIsEach) {
    +            basEmit8(&p->cg, OP_FOR_POP);
    +        }
    +
             int32_t addr = emitJump(p, OP_JMP);
             exitListAdd(&p->exitForList, addr);
         } else if (check(p, TOK_DO)) {
    @@ -3398,9 +4808,18 @@ static void parseExit(BasParserT *p) {
             emitSelectPops(p, p->selectDepth - p->doSelectBase);
             int32_t addr = emitJump(p, OP_JMP);
             exitListAdd(&p->exitDoList, addr);
    +    } else if (check(p, TOK_PROPERTY)) {
    +        advance(p);
    +        if (!p->currentProcIsProperty) {
    +            error(p, "EXIT PROPERTY outside PROPERTY");
    +            return;
    +        }
    +        emitSelectPops(p, p->selectDepth);
    +        int32_t addr = emitJump(p, OP_JMP);
    +        exitListAdd(p->currentProcIsFunction ? &p->exitFuncList : &p->exitSubList, addr);
         } else if (check(p, TOK_SUB)) {
             advance(p);
    -        if (!p->sym.inLocalScope || p->currentProcIsFunction) {
    +        if (!p->sym.inLocalScope || p->currentProcIsFunction || p->currentProcIsProperty) {
                 error(p, "EXIT SUB outside SUB");
                 return;
             }
    @@ -3410,7 +4829,7 @@ static void parseExit(BasParserT *p) {
             exitListAdd(&p->exitSubList, addr);
         } else if (check(p, TOK_FUNCTION)) {
             advance(p);
    -        if (!p->sym.inLocalScope || !p->currentProcIsFunction) {
    +        if (!p->sym.inLocalScope || !p->currentProcIsFunction || p->currentProcIsProperty) {
                 error(p, "EXIT FUNCTION outside FUNCTION");
                 return;
             }
    @@ -3418,7 +4837,7 @@ static void parseExit(BasParserT *p) {
             int32_t addr = emitJump(p, OP_JMP);
             exitListAdd(&p->exitFuncList, addr);
         } else {
    -        error(p, "Expected FOR, DO, SUB, or FUNCTION after EXIT");
    +        error(p, "Expected FOR, DO, SUB, FUNCTION or PROPERTY after EXIT");
         }
     }
     
    @@ -3433,8 +4852,11 @@ static void parseExpression(BasParserT *p) {
     
     
     // Field-access chain after a TYPE value has been pushed: .a.b.c loads
    -// each field in turn (stops when a field is not itself a TYPE).
    +// each field in turn (stops when a field is not itself a TYPE); members of
    +// an object held in a field follow (.obj.Name).
     static void parseFieldLoadChain(BasParserT *p, int32_t typeId) {
    +    uint8_t lastType = BAS_TYPE_UDT;
    +
         while (check(p, TOK_DOT) && typeId >= 0) {
             advance(p); // consume DOT
     
    @@ -3448,7 +4870,12 @@ static void parseFieldLoadChain(BasParserT *p, int32_t typeId) {
             basEmit8(&p->cg, OP_LOAD_FIELD);
             basEmitU16(&p->cg, (uint16_t)fieldIdx);
     
    -        typeId = (typeSym->fields[fieldIdx].dataType == BAS_TYPE_UDT) ? typeSym->fields[fieldIdx].udtTypeId : -1;
    +        lastType = typeSym->fields[fieldIdx].dataType;
    +        typeId   = (lastType == BAS_TYPE_UDT) ? typeSym->fields[fieldIdx].udtTypeId : -1;
    +    }
    +
    +    if (check(p, TOK_DOT) && (lastType == BAS_TYPE_OBJECT || lastType == BAS_TYPE_VARIANT)) {
    +        parseObjectMemberExpr(p);
         }
     }
     
    @@ -3498,7 +4925,9 @@ static void parseFieldStoreChain(BasParserT *p, int32_t typeId) {
                 return;
             }
     
    -        if (typeSym->fields[fieldIdx].dataType == BAS_TYPE_UDT && check(p, TOK_DOT)) {
    +        uint8_t fieldType = typeSym->fields[fieldIdx].dataType;
    +
    +        if (fieldType == BAS_TYPE_UDT && check(p, TOK_DOT)) {
                 // Intermediate level: load this field and continue
                 basEmit8(&p->cg, OP_LOAD_FIELD);
                 basEmitU16(&p->cg, (uint16_t)fieldIdx);
    @@ -3506,8 +4935,16 @@ static void parseFieldStoreChain(BasParserT *p, int32_t typeId) {
                 continue;
             }
     
    +        // A member of the object held in the field: t.obj.Name = x
    +        if ((fieldType == BAS_TYPE_OBJECT || fieldType == BAS_TYPE_VARIANT) && check(p, TOK_DOT)) {
    +            basEmit8(&p->cg, OP_LOAD_FIELD);
    +            basEmitU16(&p->cg, (uint16_t)fieldIdx);
    +            parseObjectMemberStatement(p);
    +            return;
    +        }
    +
             expect(p, TOK_EQ);
    -        parseExpression(p);
    +        emitAssignValue(p);
             emitFieldStoreConv(p, &typeSym->fields[fieldIdx]);
             basEmit8(&p->cg, OP_STORE_FIELD);
             basEmitU16(&p->cg, (uint16_t)fieldIdx);
    @@ -3550,12 +4987,44 @@ static int32_t parseFixedStringLen(BasParserT *p) {
     }
     
     
    +// Optional arguments, each after a comma and each allowed to be empty:
    +// a written one is parsed and adds its slots[i] flag, a missing one
    +// pushes 0.
    +static void parseFlaggedArgs(BasParserT *p, const int32_t *slots, int32_t count, int32_t *flags) {
    +    bool more = true;
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        more = more && match(p, TOK_COMMA);
    +
    +        if (more && !check(p, TOK_COMMA) && !atStatementEnd(p)) {
    +            parseExpression(p);
    +            *flags |= slots[i];
    +        } else {
    +            emitPushInt16(p, 0);
    +        }
    +    }
    +}
    +
    +
     static void parseFor(BasParserT *p) {
         // FOR var = start TO limit [STEP step]
         //   ...
         // NEXT [var]
         advance(p); // consume FOR
     
    +    // FOR EACH item IN group (a variable named Each is still a counter:
    +    // FOR each = ...)
    +    if (checkKeyword(p, BAS_KW_EACH)) {
    +        BasLexerT peek = p->lex;
    +
    +        basLexerNext(&peek);
    +
    +        if (peek.token.type == TOK_IDENT) {
    +            parseForEach(p);
    +            return;
    +        }
    +    }
    +
         // Loop variable
         if (!check(p, TOK_IDENT)) {
             errorExpected(p, "loop variable");
    @@ -3667,6 +5136,114 @@ static void parseFor(BasParserT *p) {
     }
     
     
    +// FOR EACH item IN group ... NEXT [item] (FOR EACH read up to EACH):
    +// item takes each element of an array (a Variant holding one included)
    +// or each item of a Collection in turn.  Two hidden variables hold the
    +// group and the position; the count is read again on every pass, so
    +// items removed inside the loop end it early instead of failing.
    +static void parseForEach(BasParserT *p) {
    +    advance(p); // consume EACH
    +
    +    if (!check(p, TOK_IDENT)) {
    +        errorExpected(p, "loop variable");
    +        return;
    +    }
    +
    +    char varName[BAS_MAX_TOKEN_LEN];
    +
    +    snprintf(varName, sizeof(varName), "%s", p->lex.token.text);
    +    advance(p);
    +
    +    BasSymbolT *item = ensureVariable(p, varName);
    +
    +    if (item == NULL) {
    +        return;
    +    }
    +
    +    if (item->kind != SYM_VARIABLE || item->isArray) {
    +        error(p, "FOR EACH needs a variable (not an array) before IN");
    +        return;
    +    }
    +
    +    if (!checkKeyword(p, BAS_KW_IN)) {
    +        errorExpected(p, "IN");
    +        return;
    +    }
    +
    +    advance(p); // consume IN
    +
    +    BasSymbolT *group = hiddenVariable(p);
    +    BasSymbolT *index = hiddenVariable(p);
    +
    +    if (group == NULL || index == NULL) {
    +        return;
    +    }
    +
    +    parseExpression(p);
    +    emitStore(p, group);
    +    emitPushInt16(p, 0);
    +    emitStore(p, index);
    +
    +    LoopSaveT save;
    +    loopBegin(p, true, &save);
    +    p->forIsEach = true;
    +
    +    int32_t loopTop = basCodePos(&p->cg);
    +
    +    emitLoad(p, index);
    +    emitLoad(p, group);
    +    emitBuiltinOp(p, OP_BUILTIN, BAS_FN_EACH_COUNT, 1);
    +    basEmit8(&p->cg, OP_CMP_LT);
    +
    +    int32_t doneJump = emitJump(p, OP_JMP_FALSE);
    +
    +    emitLoad(p, group);
    +    emitLoad(p, index);
    +    emitBuiltinOp(p, OP_BUILTIN, BAS_FN_EACH_ITEM, 2);
    +    emitStore(p, item);
    +
    +    expectEndOfStatement(p);
    +    skipNewlines(p);
    +
    +    while (!p->hasError && !check(p, TOK_NEXT) && !check(p, TOK_EOF)) {
    +        parseStatement(p);
    +        skipNewlines(p);
    +    }
    +
    +    if (p->hasError) {
    +        loopEnd(p, true, &save);
    +        return;
    +    }
    +
    +    expect(p, TOK_NEXT);
    +
    +    if (check(p, TOK_IDENT)) {
    +        if (strcasecmp(p->lex.token.text, varName) != 0) {
    +            char buf[BAS_PARSE_ERR_SCRATCH];
    +            snprintf(buf, sizeof(buf), "NEXT %s does not match FOR EACH %s", p->lex.token.text, varName);
    +            error(p, buf);
    +            loopEnd(p, true, &save);
    +            return;
    +        }
    +
    +        advance(p);
    +    }
    +
    +    emitLoad(p, index);
    +    emitPushInt16(p, 1);
    +    basEmit8(&p->cg, OP_ADD_INT);
    +    emitStore(p, index);
    +    basEmit8(&p->cg, OP_JMP);
    +    basEmit16(&p->cg, relJumpOffset(p, loopTop, basCodePos(&p->cg)));
    +    patchJump(p, doneJump);
    +
    +    // EXIT FOR lands here too; then the loop lets go of the group
    +    loopEnd(p, true, &save);
    +    emitPushSpecial(p, BAS_SPECIAL_EMPTY);
    +    emitStore(p, group);
    +}
    +
    +
     static void parseGet(BasParserT *p) {
         // GET #channel, [recno], var
         advance(p); // consume GET
    @@ -3738,6 +5315,105 @@ static void parseGet(BasParserT *p) {
     }
     
     
    +// CIRCLE [STEP](x, y), r [, color [, start [, end [, aspect]]]] ->
    +// (x, y, r, color, start, end, aspect, flags).
    +static void parseGfxCircleArgs(BasParserT *p) {
    +    static const int32_t slots[] = { BAS_GFX_COLOR, BAS_GFX_ARC_START, BAS_GFX_ARC_END, BAS_GFX_ASPECT };
    +    int32_t              flags   = 0;
    +
    +    parseGfxPoint(p, &flags, BAS_GFX_STEP);
    +    expect(p, TOK_COMMA);
    +    parseExpression(p);
    +    parseFlaggedArgs(p, slots, (int32_t)(sizeof(slots) / sizeof(slots[0])), &flags);
    +    emitPushInt16(p, (int16_t)flags);
    +}
    +
    +
    +// LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, [color] [, [B | BF] [, style]]]
    +// -> (x1, y1, x2, y2, color, style, flags).
    +static void parseGfxLineArgs(BasParserT *p) {
    +    int32_t flags = 0;
    +
    +    if (check(p, TOK_LPAREN) || check(p, TOK_STEP)) {
    +        parseGfxPoint(p, &flags, BAS_GFX_STEP);
    +        flags |= BAS_GFX_START;
    +    } else {
    +        emitPushInt16(p, 0);
    +        emitPushInt16(p, 0);
    +    }
    +
    +    expect(p, TOK_MINUS);
    +    parseGfxPoint(p, &flags, BAS_GFX_STEP2);
    +
    +    bool colorComma = match(p, TOK_COMMA);
    +
    +    if (colorComma && !check(p, TOK_COMMA) && !atStatementEnd(p)) {
    +        parseExpression(p);
    +        flags |= BAS_GFX_COLOR;
    +    } else {
    +        emitPushInt16(p, 0);
    +    }
    +
    +    if (colorComma && match(p, TOK_COMMA)) {
    +        if (checkKeyword(p, "BF")) {
    +            flags |= BAS_GFX_BOX_FILL;
    +            advance(p);
    +        } else if (checkKeyword(p, "B")) {
    +            flags |= BAS_GFX_BOX;
    +            advance(p);
    +        } else if (!check(p, TOK_COMMA) && !atStatementEnd(p)) {
    +            errorExpected(p, "B or BF");
    +            return;
    +        }
    +
    +        if (match(p, TOK_COMMA)) {
    +            parseExpression(p);
    +            flags |= BAS_GFX_STYLE;
    +        } else {
    +            emitPushInt16(p, 0);
    +        }
    +    } else {
    +        emitPushInt16(p, 0);
    +    }
    +
    +    emitPushInt16(p, (int16_t)flags);
    +}
    +
    +
    +// [STEP](x, y): pushes x and y; STEP adds stepFlag.
    +static void parseGfxPoint(BasParserT *p, int32_t *flags, int32_t stepFlag) {
    +    if (match(p, TOK_STEP)) {
    +        *flags |= stepFlag;
    +    }
    +
    +    expect(p, TOK_LPAREN);
    +    parseExpression(p);
    +    expect(p, TOK_COMMA);
    +    parseExpression(p);
    +    expect(p, TOK_RPAREN);
    +}
    +
    +
    +// PSET / PRESET [STEP](x, y) [, color] -> (x, y, color, flags).
    +static void parseGfxPsetArgs(BasParserT *p, int32_t flags) {
    +    static const int32_t slots[] = { BAS_GFX_COLOR };
    +
    +    parseGfxPoint(p, &flags, BAS_GFX_STEP);
    +    parseFlaggedArgs(p, slots, (int32_t)(sizeof(slots) / sizeof(slots[0])), &flags);
    +    emitPushInt16(p, (int16_t)flags);
    +}
    +
    +
    +// (x1, y1)-(x2, y2) of VIEW and WINDOW.
    +static void parseGfxRect(BasParserT *p) {
    +    int32_t noStep = 0;
    +
    +    parseGfxPoint(p, &noStep, 0);
    +    expect(p, TOK_MINUS);
    +    parseGfxPoint(p, &noStep, 0);
    +}
    +
    +
     static void parseGosub(BasParserT *p) {
         // GOSUB label -- record the return address on this frame's GOSUB stack, jump
         advance(p); // consume GOSUB
    @@ -3906,6 +5582,18 @@ static void parseIf(BasParserT *p) {
     }
     
     
    +// Up to max further arguments QBasic accepts and DVX ignores: evaluated
    +// (for their side effects) and dropped.
    +static void parseIgnoredArgs(BasParserT *p, int32_t max) {
    +    for (int32_t i = 0; i < max && match(p, TOK_COMMA); i++) {
    +        if (!check(p, TOK_COMMA) && !atStatementEnd(p)) {
    +            parseExpression(p);
    +            basEmit8(&p->cg, OP_POP);
    +        }
    +    }
    +}
    +
    +
     static void parseImpExpr(BasParserT *p) {
         parseEqvExpr(p);
         while (!p->hasError && check(p, TOK_IMP)) {
    @@ -4023,20 +5711,47 @@ static void parseKill(BasParserT *p) {
     }
     
     
    -static void parseLineInput(BasParserT *p) {
    -    // LINE INPUT #channel, target
    +// LINE: LINE INPUT, or the graphics LINE statement.
    +static void parseLine(BasParserT *p) {
         advance(p); // consume LINE
     
    -    if (!check(p, TOK_INPUT)) {
    -        error(p, "Expected INPUT after LINE");
    +    if (match(p, TOK_INPUT)) {
    +        parseLineInput(p);
             return;
         }
     
    -    advance(p); // consume INPUT
    +    parseGfxLineArgs(p);
    +    emitScreenStmt(p, BAS_FN_LINE, BAS_GFX_LINE_ARGS);
    +}
    +
    +
    +static void parseLineInput(BasParserT *p) {
    +    // LINE INPUT #channel, target
    +    // LINE INPUT [;] ["prompt" {; | ,}] target
    +    VarRefT ref;
     
    -    // Must have # for file I/O
         if (!match(p, TOK_HASH)) {
    -        error(p, "Expected # for file channel in LINE INPUT");
    +        match(p, TOK_SEMICOLON);
    +
    +        if (check(p, TOK_STRING_LIT)) {
    +            emitPushStr(p, p->lex.token.text, p->lex.token.textLen);
    +            advance(p);
    +
    +            if (!match(p, TOK_SEMICOLON)) {
    +                expect(p, TOK_COMMA);
    +            }
    +        } else {
    +            emitPushStr(p, "", 0);
    +        }
    +
    +        emitBuiltinOp(p, OP_BUILTIN, BAS_FN_LINE_INPUT, 1);
    +
    +        if (!parseVarRef(p, &ref, true)) {
    +            return;
    +        }
    +
    +        emitInputConv(p, varRefType(&ref));
    +        emitVarRefStore(p, &ref);
             return;
         }
     
    @@ -4046,8 +5761,6 @@ static void parseLineInput(BasParserT *p) {
         // Comma separator
         expect(p, TOK_COMMA);
     
    -    VarRefT ref;
    -
         if (!parseVarRef(p, &ref, true)) {
             return;
         }
    @@ -4068,6 +5781,119 @@ static void parseLprint(BasParserT *p) {
     }
     
     
    +// The rest of a statement on obj.member (the object and member name are
    +// pushed): "= value" (a property; with SET an object), "(args) = value"
    +// (a property with arguments), or a method call with its arguments.
    +static void parseMemberAssignOrCall(BasParserT *p, const char *memberName) {
    +    if (check(p, TOK_LPAREN) && peekAfterParens(p) == TOK_EQ) {
    +        int32_t argc = parseMethodArgs(p, NULL, true);
    +
    +        if (clampArgCount(p, argc)) {
    +            return;
    +        }
    +
    +        expect(p, TOK_EQ);
    +        emitAssignValue(p);
    +        emitPropAssign(p, argc);
    +        return;
    +    }
    +
    +    if (match(p, TOK_EQ)) {
    +        emitAssignValue(p);
    +
    +        if (p->setAssign) {
    +            emitPropAssign(p, 0);
    +        } else {
    +            basEmit8(&p->cg, OP_STORE_PROP);
    +        }
    +
    +        return;
    +    }
    +
    +    emitMemberCallStatement(p, memberName);
    +}
    +
    +
    +// Me.Show [modal] / Me.Hide on a form, else a member chain on Me:
    +// Me.Prop = expr, Me.Method args, Me.Ctrl[(idx)].Member ...  In a class
    +// module Me is the instance, and every member goes through it.
    +static void parseMeStatement(BasParserT *p) {
    +    advance(p); // consume Me
    +
    +    if (!check(p, TOK_DOT)) {
    +        errorExpected(p, "'.' after Me");
    +        return;
    +    }
    +
    +    BasLexerT memberLex = p->lex;
    +    basLexerNext(&memberLex);
    +
    +    bool onForm = !p->sym.inClassScope && basTokenIsWord(&memberLex.token);
    +    bool isShow = onForm && strcasecmp(memberLex.token.text, "Show") == 0;
    +    bool isHide = onForm && strcasecmp(memberLex.token.text, "Hide") == 0;
    +
    +    basEmit8(&p->cg, meOpcode(p));
    +
    +    if (isShow) {
    +        advance(p); // DOT
    +        advance(p); // Show
    +        uint8_t modal = parseShowModalFlag(p);
    +        basEmit8(&p->cg, OP_SHOW_FORM);
    +        basEmit8(&p->cg, modal);
    +    } else if (isHide) {
    +        advance(p); // DOT
    +        advance(p); // Hide
    +        basEmit8(&p->cg, OP_HIDE_FORM);
    +    } else {
    +        parseObjectMemberStatement(p);
    +    }
    +}
    +
    +
    +// The arguments of a method call on an object: up to the end of the
    +// statement (parens false) or in parentheses.  An empty argument (a, , c)
    +// is passed as Empty (an omitted argument, as Collection.Add takes it);
    +// with method (a class procedure the compiler knows) a variable given for
    +// a ByRef parameter is passed by address.  Returns the argument count.
    +static int32_t parseMethodArgs(BasParserT *p, const BasSymbolT *method, bool parens) {
    +    int32_t argc = 0;
    +    bool    more;
    +
    +    if (parens) {
    +        expect(p, TOK_LPAREN);
    +        more = !check(p, TOK_RPAREN);
    +    } else {
    +        more = !atStatementEnd(p);
    +    }
    +
    +    while (more && !p->hasError) {
    +        if (check(p, TOK_COMMA) || (parens && check(p, TOK_RPAREN))) {
    +            emitPushSpecial(p, BAS_SPECIAL_EMPTY);
    +        } else if (method != NULL && argc < method->paramCount && !method->paramByVal[argc]) {
    +            emitByRefArg(p);
    +        } else {
    +            parseExpression(p);
    +        }
    +
    +        argc++;
    +        more = match(p, TOK_COMMA);
    +
    +        // A trailing comma leaves one more (empty) argument
    +        if (more && (parens ? check(p, TOK_RPAREN) : atStatementEnd(p))) {
    +            emitPushSpecial(p, BAS_SPECIAL_EMPTY);
    +            argc++;
    +            more = false;
    +        }
    +    }
    +
    +    if (parens) {
    +        expect(p, TOK_RPAREN);
    +    }
    +
    +    return argc;
    +}
    +
    +
     // Consume an optional leading minus of a literal, letting the lexer accept
     // the magnitude of the most negative INTEGER / LONG after it.
     static bool parseMinusForLiteral(BasParserT *p) {
    @@ -4112,6 +5938,10 @@ static void parseModule(BasParserT *p) {
             error(p, "BEGINFORM without ENDFORM");
         }
     
    +    if (!p->hasError && p->sym.inClassScope) {
    +        error(p, "BEGINCLASS without ENDCLASS");
    +    }
    +
         checkUndefinedLabels(p, SCOPE_GLOBAL);
     
         // Check for unresolved forward references (skip externs from DECLARE LIBRARY)
    @@ -4251,12 +6081,18 @@ static void parseObjectMemberExpr(BasParserT *p) {
         advance(p); // consume DOT
     
         if (!requireMemberName(p)) {
    +        p->memberClass = BAS_CLASS_NONE;
             return;
         }
     
    -    emitPushCStr(p, p->lex.token.text);
    +    char memberName[BAS_MAX_TOKEN_LEN];
    +
    +    snprintf(memberName, sizeof(memberName), "%s", p->lex.token.text);
    +    emitPushCStr(p, memberName);
         advance(p);
     
    +    const BasSymbolT *method = classMethod(p, memberName);
    +
         if (check(p, TOK_DOT)) {
             basEmit8(&p->cg, OP_FIND_CTRL);
             parseObjectMemberExpr(p);
    @@ -4268,7 +6104,7 @@ static void parseObjectMemberExpr(BasParserT *p) {
             return;
         }
     
    -    int32_t argc = parseArgList(p);
    +    int32_t argc = parseMethodArgs(p, method, true);
     
         if (check(p, TOK_DOT)) {
             if (argc != 1) {
    @@ -4295,11 +6131,17 @@ static void parseObjectMemberExpr(BasParserT *p) {
     static void parseObjectMemberStatement(BasParserT *p) {
         advance(p); // consume DOT
     
    +    // The chain's later members are on objects of unknown class
    +    p->memberClass = BAS_CLASS_NONE;
    +
         if (!requireMemberName(p)) {
             return;
         }
     
    -    emitPushCStr(p, p->lex.token.text);
    +    char memberName[BAS_MAX_TOKEN_LEN];
    +
    +    snprintf(memberName, sizeof(memberName), "%s", p->lex.token.text);
    +    emitPushCStr(p, memberName);
         advance(p);
     
         if (check(p, TOK_DOT)) {
    @@ -4317,13 +6159,75 @@ static void parseObjectMemberStatement(BasParserT *p) {
             return;
         }
     
    -    if (match(p, TOK_EQ)) {
    -        parseExpression(p);
    -        basEmit8(&p->cg, OP_STORE_PROP);
    +    parseMemberAssignOrCall(p, memberName);
    +}
    +
    +
    +// An operand where an object is expected (SET's value, IS and TYPEOF
    +// operands, an argument for an object parameter): a bare form or control
    +// name (see bareObjectAhead), else what parseOperand reads.
    +static void parseObjectOperand(BasParserT *p, void (*parseOperand)(BasParserT *p)) {
    +    if (bareObjectAhead(p)) {
    +        emitBareObject(p);
             return;
         }
     
    -    emitMethodCallStatement(p);
    +    parseOperand(p);
    +}
    +
    +
    +// Object Print items (PRINT's, without USING) on the object and method
    +// name already pushed: one Print call per item, with the object kept in
    +// a temporary.
    +static void parseObjectPrint(BasParserT *p) {
    +    BasSymbolT *obj     = acquireTemp(p);
    +    bool        newline = true;
    +
    +    basEmit8(&p->cg, OP_POP); // the method name: each item pushes its own
    +
    +    if (obj == NULL) {
    +        return;
    +    }
    +
    +    emitStore(p, obj);
    +
    +    while (!p->hasError && !atStatementEnd(p)) {
    +        if (match(p, TOK_SEMICOLON)) {
    +            newline = false;
    +            continue;
    +        }
    +
    +        if (match(p, TOK_COMMA)) {
    +            emitObjectPrintHead(p, obj);
    +            emitPushInt16(p, 0);
    +            emitObjectPrintTail(p, BAS_OBJ_PRINT_ZONE);
    +            newline = false;
    +            continue;
    +        }
    +
    +        int32_t mode = BAS_OBJ_PRINT_TEXT;
    +
    +        if (checkKeyword(p, "SPC") || checkKeyword(p, "TAB")) {
    +            mode = checkKeyword(p, "SPC") ? BAS_OBJ_PRINT_SPC : BAS_OBJ_PRINT_TAB;
    +            advance(p);
    +            emitObjectPrintHead(p, obj);
    +            expect(p, TOK_LPAREN);
    +            parseExpression(p);
    +            expect(p, TOK_RPAREN);
    +        } else {
    +            emitObjectPrintHead(p, obj);
    +            parseExpression(p);
    +        }
    +
    +        emitObjectPrintTail(p, mode);
    +        newline = true;
    +    }
    +
    +    if (newline) {
    +        emitObjectPrintHead(p, obj);
    +        emitPushInt16(p, 0);
    +        emitObjectPrintTail(p, BAS_OBJ_PRINT_NEWLINE);
    +    }
     }
     
     
    @@ -4538,10 +6442,12 @@ static void parseOption(BasParserT *p) {
                 advance(p);
                 basEmit8(&p->cg, OP_COMPARE_MODE);
                 basEmit8(&p->cg, BAS_COMPARE_MODE_BINARY);
    +            p->compareText = false;
             } else if (checkKeyword(p, "TEXT")) {
                 advance(p);
                 basEmit8(&p->cg, OP_COMPARE_MODE);
                 basEmit8(&p->cg, BAS_COMPARE_MODE_TEXT);
    +            p->compareText = true;
             } else {
                 error(p, "Expected BINARY or TEXT after OPTION COMPARE");
             }
    @@ -4559,6 +6465,34 @@ static void parseOption(BasParserT *p) {
     }
     
     
    +// count optional positional arguments (LOCATE row, col, cursor ...):
    +// each may be left empty; a missing one pushes 0.  Returns the
    +// BAS_GIVEN_* mask of those written.
    +static int32_t parseOptionalArgs(BasParserT *p, int32_t count) {
    +    int32_t given = 0;
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        if (i > 0 && !match(p, TOK_COMMA)) {
    +            for (int32_t j = i; j < count; j++) {
    +                emitPushInt16(p, 0);
    +            }
    +
    +            return given;
    +        }
    +
    +        if (check(p, TOK_COMMA) || atStatementEnd(p)) {
    +            emitPushInt16(p, 0);
    +            continue;
    +        }
    +
    +        parseExpression(p);
    +        given |= 1 << i;
    +    }
    +
    +    return given;
    +}
    +
    +
     static void parseOrExpr(BasParserT *p) {
         parseXorExpr(p);
         while (!p->hasError && check(p, TOK_OR)) {
    @@ -4588,7 +6522,28 @@ static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags) {
                 expect(p, TOK_COMMA);
             }
     
    -        bool optional = match(p, TOK_OPTIONAL);
    +        if (pl->hasParamArray) {
    +            error(p, "ParamArray must be the last parameter");
    +            return false;
    +        }
    +
    +        // [OPTIONAL] [BYVAL | BYREF] [PARAMARRAY]; BYREF is the default
    +        // and may be written out for clarity.
    +        bool optional   = match(p, TOK_OPTIONAL);
    +        bool byRef      = match(p, TOK_BYREF);
    +        bool byValKw    = !byRef && match(p, TOK_BYVAL);
    +        bool byVal      = byValKw || (flags & PARAM_FORCE_BYVAL) != 0;
    +        bool paramArray = match(p, TOK_PARAMARRAY);
    +
    +        if (paramArray && (optional || seenOptional)) {
    +            error(p, "ParamArray cannot be used with Optional parameters");
    +            return false;
    +        }
    +
    +        if (paramArray && (byRef || byValKw)) {
    +            error(p, "ParamArray cannot be ByVal or ByRef");
    +            return false;
    +        }
     
             if (optional) {
                 seenOptional = true;
    @@ -4597,10 +6552,6 @@ static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags) {
                 return false;
             }
     
    -        // BYREF is the default and may be written out for clarity.
    -        bool byRef = match(p, TOK_BYREF);
    -        bool byVal = (!byRef && match(p, TOK_BYVAL)) || (flags & PARAM_FORCE_BYVAL) != 0;
    -
             if (!check(p, TOK_IDENT)) {
                 errorExpected(p, "parameter name");
                 return false;
    @@ -4624,6 +6575,13 @@ static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags) {
                 isArrayParam = true;
             }
     
    +        if (paramArray && !isArrayParam) {
    +            char buf[BAS_PARSE_ERR_SCRATCH];
    +            snprintf(buf, sizeof(buf), "ParamArray parameter must be an array: write %s()", paramName);
    +            error(p, buf);
    +            return false;
    +        }
    +
             bool    pTyped;
             uint8_t pdt        = implicitType(p, paramName, &pTyped);
             int32_t pUdtTypeId = -1;
    @@ -4637,6 +6595,19 @@ static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags) {
                 return false;
             }
     
    +        // An untyped ParamArray is an array of Variants: each argument
    +        // keeps its own type
    +        if (paramArray && !pTyped) {
    +            pdt = BAS_TYPE_VARIANT;
    +        }
    +
    +        // The prescan reads a TYPE (or ENUM) defined later as a TYPE; the
    +        // main pass checks the real type.
    +        if (paramArray && pdt == BAS_TYPE_UDT && (flags & PARAM_PRESCAN) == 0) {
    +            error(p, "ParamArray elements cannot be a TYPE");
    +            return false;
    +        }
    +
             if (flags & PARAM_REGISTER) {
                 BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
     
    @@ -4649,16 +6620,22 @@ static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags) {
     
                 paramSym->index     = basSymTabAllocSlot(&p->sym);
                 paramSym->udtTypeId = pUdtTypeId;
    -            paramSym->isTyped   = pTyped;
    +            paramSym->isTyped   = pTyped && pdt != BAS_TYPE_VARIANT;
    +            paramSym->objClass  = (pdt == BAS_TYPE_OBJECT) ? p->lastObjClass : BAS_CLASS_NONE;
                 paramSym->isArray   = isArrayParam;
                 paramSym->isDynamic = isArrayParam;
             }
     
    +        // A ParamArray receives a new array, so it is passed by value; it
    +        // is not counted as required (no argument gives an empty array).
             pl->types[pl->count]    = pdt;
    -        pl->byVal[pl->count]    = byVal;
    +        pl->byVal[pl->count]    = byVal || paramArray;
             pl->optional[pl->count] = optional;
     
    -        if (!optional) {
    +        if (paramArray) {
    +            pl->hasParamArray   = true;
    +            pl->paramArrayTyped = pTyped;
    +        } else if (!optional) {
                 pl->required = pl->count + 1;
             }
     
    @@ -4771,6 +6748,13 @@ static void parsePrimary(BasParserT *p) {
             return;
         }
     
    +    // Date literal
    +    if (tt == TOK_DATE_LIT) {
    +        emitPushDate(p, p->lex.token.dblVal);
    +        advance(p);
    +        return;
    +    }
    +
         // Boolean literals
         if (tt == TOK_TRUE_KW) {
             basEmit8(&p->cg, OP_PUSH_TRUE);
    @@ -4783,11 +6767,11 @@ static void parsePrimary(BasParserT *p) {
             return;
         }
     
    -    // Me -- reference to current form, optionally Me.Member,
    -    // Me.Ctrl.Member or Me.Ctrl(idx).Member
    +    // Me -- reference to current form (the instance in a class module),
    +    // optionally Me.Member, Me.Ctrl.Member or Me.Ctrl(idx).Member
         if (tt == TOK_ME) {
             advance(p);
    -        basEmit8(&p->cg, OP_ME_REF);
    +        basEmit8(&p->cg, meOpcode(p));
             if (check(p, TOK_DOT)) {
                 parseObjectMemberExpr(p);
             }
    @@ -4797,7 +6781,45 @@ static void parsePrimary(BasParserT *p) {
         // Nothing -- null object reference
         if (tt == TOK_NOTHING) {
             advance(p);
    -        emitPushInt16(p, 0);
    +        emitPushSpecial(p, BAS_SPECIAL_NOTHING);
    +        return;
    +    }
    +
    +    // New ClassName / New Collection -- a new object
    +    if (tt == TOK_NEW) {
    +        advance(p);
    +
    +        int32_t classIdx = BAS_CLASS_NONE;
    +
    +        if (check(p, TOK_IDENT) && basSymTabFindClass(&p->sym, p->lex.token.text, &classIdx) == NULL && strcasecmp(p->lex.token.text, BAS_COLLECTION_TYPE_NAME) == 0) {
    +            classIdx = BAS_CLASS_COLLECTION;
    +        }
    +
    +        if (classIdx == BAS_CLASS_NONE) {
    +            errorExpected(p, "class name (or Collection) after New");
    +            return;
    +        }
    +
    +        advance(p);
    +        emitNewObject(p, classIdx);
    +        return;
    +    }
    +
    +    // TypeOf object Is TypeName -- the object is of that class or control
    +    // type (Object: any object; Control: any control)
    +    if (tt == TOK_TYPEOF) {
    +        advance(p);
    +        parseObjectOperand(p, parsePrimary);
    +        expect(p, TOK_IS);
    +
    +        if (!p->hasError && !basTokenIsWord(&p->lex.token)) {
    +            errorExpected(p, "type name after Is");
    +            return;
    +        }
    +
    +        emitPushCStr(p, p->lex.token.text);
    +        advance(p);
    +        emitBuiltinOp(p, OP_BUILTIN, BAS_FN_TYPEOF, 2);
             return;
         }
     
    @@ -5031,11 +7053,18 @@ static void parsePrimary(BasParserT *p) {
             }
     
             // Check for built-in function
    -        const BuiltinFuncT *builtin = findBuiltin(name);
    +        const BuiltinFuncT *builtin = resolveBuiltin(p, name);
             if (builtin != NULL) {
    +            int32_t minArgs = builtin->minArgs;
    +            int32_t maxArgs = builtin->maxArgs;
    +
    +            if (builtin->opcode == OP_BUILTIN) {
    +                basBuiltinArity(builtin->func, &minArgs, &maxArgs);
    +            }
    +
                 // Zero-arg builtins can be used without parens
    -            if (builtin->minArgs == 0 && builtin->maxArgs == 0 && !check(p, TOK_LPAREN)) {
    -                basEmit8(&p->cg, builtin->opcode);
    +            if (maxArgs == 0 && !check(p, TOK_LPAREN)) {
    +                emitBuiltinOp(p, builtin->opcode, builtin->func, 0);
                     return;
                 }
     
    @@ -5046,9 +7075,9 @@ static void parsePrimary(BasParserT *p) {
                     return;
                 }
     
    -            if (argc < builtin->minArgs || argc > builtin->maxArgs) {
    +            if (argc < minArgs || argc > maxArgs) {
                     char buf[BAS_PARSE_ERR_SCRATCH];
    -                snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, (int)builtin->minArgs, (int)builtin->maxArgs, (int)argc);
    +                snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, (int)minArgs, (int)maxArgs, (int)argc);
                     error(p, buf);
                     return;
                 }
    @@ -5065,7 +7094,7 @@ static void parsePrimary(BasParserT *p) {
                     emitPushInt16(p, BAS_COMMAND_ALL_ARGS);
                     basEmit8(&p->cg, OP_COMMAND);
                 } else {
    -                basEmit8(&p->cg, builtin->opcode);
    +                emitBuiltinOp(p, builtin->opcode, builtin->func, argc);
                 }
                 return;
             }
    @@ -5073,6 +7102,28 @@ static void parsePrimary(BasParserT *p) {
             // Check symbol table for user-defined function or variable
             BasSymbolT *sym = basSymTabFind(&p->sym, name);
     
    +        // Empty and Null (a symbol of the program's own of that name wins)
    +        int32_t special = specialValueName(name);
    +
    +        if (sym == NULL && special != BAS_SPECIAL_NONE && !check(p, TOK_LPAREN) && !check(p, TOK_DOT)) {
    +            emitPushSpecial(p, (uint8_t)special);
    +            return;
    +        }
    +
    +        if (sym != NULL && sym->kind == SYM_ENUM && check(p, TOK_DOT)) {
    +            emitEnumMember(p, sym);
    +            return;
    +        }
    +
    +        // A constant expression names nothing but constants (a variable
    +        // or FUNCTION reference would also create a symbol or stub).
    +        if (p->inConstExpr && (sym == NULL || sym->kind != SYM_CONST)) {
    +            char buf[BAS_PARSE_ERR_SCRATCH];
    +            snprintf(buf, sizeof(buf), "Constant expression required: '%s' is not a constant", name);
    +            error(p, buf);
    +            return;
    +        }
    +
             if (sym == NULL && (check(p, TOK_DOT) || check(p, TOK_LPAREN)) && rejectHiddenModuleVar(p, name)) {
                 return;
             }
    @@ -5082,6 +7133,12 @@ static void parsePrimary(BasParserT *p) {
             if (check(p, TOK_LPAREN)) {
                 if (sym != NULL && sym->kind == SYM_FUNCTION) {
                     emitFunctionCall(p, sym);
    +
    +                // A member of the object the FUNCTION returns: Make(1).Name
    +                if (check(p, TOK_DOT) && (sym->dataType == BAS_TYPE_OBJECT || sym->dataType == BAS_TYPE_VARIANT)) {
    +                    p->memberClass = sym->objClass;
    +                    parseObjectMemberExpr(p);
    +                }
                     return;
                 }
     
    @@ -5092,7 +7149,10 @@ static void parsePrimary(BasParserT *p) {
                     return;
                 }
     
    -            if (sym != NULL && sym->isArray) {
    +            // An array element, or what an object / Variant holds at
    +            // (args): an element of the array in it, or the default
    +            // member of the object in it
    +            if (sym != NULL && (sym->isArray || canIndexValue(sym))) {
                     emitLoad(p, sym);
                     expect(p, TOK_LPAREN);
                     int32_t dims = parseIndexList(p);
    @@ -5102,6 +7162,9 @@ static void parsePrimary(BasParserT *p) {
                     // Array-of-UDT field access: arr(i).field[.sub...]
                     if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0 && check(p, TOK_DOT)) {
                         parseFieldLoadChain(p, sym->udtTypeId);
    +                } else if (check(p, TOK_DOT)) {
    +                    // A member of the object there: arr(i).Name, c(1).Name
    +                    parseObjectMemberExpr(p);
                     }
                     return;
                 }
    @@ -5166,7 +7229,7 @@ static void parsePrimary(BasParserT *p) {
                 memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
                 advance(p);
     
    -            bool isVarRef = (sym != NULL && sym->kind == SYM_VARIABLE);
    +            bool isVarRef = (sym != NULL && (sym->kind == SYM_VARIABLE || isOwnResult(p, sym)));
     
                 // Three-part access: FormName.Ctrl.Member or formVar.Ctrl.Member.
                 // The first part names (or holds) a form; the control is
    @@ -5187,6 +7250,7 @@ static void parsePrimary(BasParserT *p) {
     
                 p->lex = dotLex;
                 emitCtrlRef(p, sym, name);
    +            p->memberClass = isVarRef ? sym->objClass : BAS_CLASS_NONE;
                 parseObjectMemberExpr(p);
                 return;
             }
    @@ -5425,17 +7489,24 @@ static bool parseProcDeclarator(BasParserT *p, const char *kindError, BasSymKind
     }
     
     
    -// SUB name(params) ... END SUB, or FUNCTION name(params) [AS type] ...
    -// END FUNCTION.  The body is compiled in place behind a jump that module
    -// code takes over it; labels and CONSTs in the body belong to it.
    -static void parseProcDef(BasParserT *p, bool isFunction) {
    -    BasSymKindE   kind   = isFunction ? SYM_FUNCTION : SYM_SUB;
    -    BasTokenTypeE endKw  = isFunction ? TOK_FUNCTION : TOK_SUB;
    -    const char   *kwName = isFunction ? "FUNCTION" : "SUB";
    +// SUB name(params) ... END SUB, FUNCTION name(params) [AS type] ... END
    +// FUNCTION, or (the PROPERTY keyword read) GET / LET / SET name(params)
    +// [AS type] ... END PROPERTY.  kind is the BasMemberKindE the keyword
    +// makes; isPrivate a Private in front.  The body is compiled in place
    +// behind a jump that module code takes over it; labels and CONSTs in the
    +// body belong to it.  In a class module the procedure is "Class.name"
    +// (Property Let / Set "Class.name.Let" / ".Set") and, unless Private, a
    +// public member.
    +static void parseProcDef(BasParserT *p, uint8_t kind, bool isPrivate) {
    +    bool          isProperty = (kind == BAS_MEMBER_GET || kind == BAS_MEMBER_LET || kind == BAS_MEMBER_SET);
    +    bool          isFunction = (kind == BAS_MEMBER_FUNCTION || kind == BAS_MEMBER_GET);
    +    BasSymKindE   symKind    = isFunction ? SYM_FUNCTION : SYM_SUB;
    +    BasTokenTypeE endKw      = isProperty ? TOK_PROPERTY : (isFunction ? TOK_FUNCTION : TOK_SUB);
    +    const char   *kwName     = isProperty ? "PROPERTY" : (isFunction ? "FUNCTION" : "SUB");
     
    -    advance(p); // consume SUB / FUNCTION
    +    advance(p); // consume SUB / FUNCTION / GET / LET / SET
     
    -    if (!check(p, TOK_IDENT)) {
    +    if (!memberNameAhead(p)) {
             errorExpected(p, isFunction ? "function name" : "subroutine name");
             return;
         }
    @@ -5452,6 +7523,17 @@ static void parseProcDef(BasParserT *p, bool isFunction) {
             return;
         }
     
    +    if (isProperty && !p->sym.inClassScope) {
    +        error(p, "PROPERTY procedures belong in a class module");
    +        return;
    +    }
    +
    +    char symName[BAS_MAX_IDENT];
    +
    +    if (!procSymbolName(p, p->sym.inClassScope ? p->sym.classIdx : BAS_CLASS_NONE, name, kind, symName, (int32_t)sizeof(symName))) {
    +        return;
    +    }
    +
         // Save current proc name for STATIC variable mangling
         strncpy(p->currentProc, name, BAS_MAX_TOKEN_LEN - 1);
         p->currentProc[BAS_MAX_TOKEN_LEN - 1] = '\0';
    @@ -5464,6 +7546,8 @@ static void parseProcDef(BasParserT *p, bool isFunction) {
     
         ProcSaveT save;
         procBegin(p, &save, isFunction);
    +    p->currentProcIsProperty = isProperty;
    +    p->currentProcAddr       = procAddr;
     
         // A FUNCTION's slot 0 holds its return value
         if (isFunction) {
    @@ -5477,31 +7561,41 @@ static void parseProcDef(BasParserT *p, bool isFunction) {
             return;
         }
     
    +    if ((kind == BAS_MEMBER_LET || kind == BAS_MEMBER_SET) && pl.count == 0) {
    +        error(p, "Property Let and Property Set take the value as their last parameter");
    +        procEnd(p, &save);
    +        return;
    +    }
    +
         // A FUNCTION typed by AS, suffix or DEFxxx converts the value
         // assigned to its name to that type (isTyped, as for variables).
         bool    returnTyped = false;
         uint8_t returnType  = isFunction ? implicitType(p, name, &returnTyped) : BAS_TYPE_INTEGER;
    +    int32_t returnClass = BAS_CLASS_NONE;
     
         if (isFunction && match(p, TOK_AS)) {
             returnType  = resolveTypeName(p);
    -        returnTyped = true;
    +        returnTyped = (returnType != BAS_TYPE_VARIANT);
    +        returnClass = p->lastObjClass;
         }
     
    +    save.returnType = returnType;
    +
         // Register the procedure (global scope).  The prescan / a DECLARE / a
         // call-site stub may already have added it; a symbol that is already
         // defined is a duplicate body.
    -    BasSymbolT *procSym = basSymTabFindGlobal(&p->sym, name);
    +    BasSymbolT *procSym = basSymTabFindGlobal(&p->sym, symName);
     
    -    if (procSym != NULL && procSym->kind == kind && procSym->isDefined) {
    +    if (procSym != NULL && procSym->kind == symKind && procSym->isDefined) {
             char buf[BAS_PARSE_ERR_SCRATCH];
    -        snprintf(buf, sizeof(buf), "%s '%s' is already defined", isFunction ? "Function" : "Sub", procSym->name);
    +        snprintf(buf, sizeof(buf), "%s '%s' is already defined", isProperty ? "Property" : (isFunction ? "Function" : "Sub"), name);
             error(p, buf);
             procEnd(p, &save);
             return;
         }
     
    -    if (procSym == NULL || procSym->kind != kind) {
    -        procSym = basSymTabAddGlobal(&p->sym, name, kind, returnType);
    +    if (procSym == NULL || procSym->kind != symKind) {
    +        procSym = basSymTabAddGlobal(&p->sym, symName, symKind, returnType);
         }
     
         if (procSym == NULL) {
    @@ -5512,10 +7606,13 @@ static void parseProcDef(BasParserT *p, bool isFunction) {
             return;
         }
     
    -    procSym->codeAddr  = procAddr;
    -    procSym->isDefined = true;
    -    procSym->dataType  = returnType;
    -    procSym->isTyped   = returnTyped;
    +    procSym->codeAddr   = procAddr;
    +    procSym->isDefined  = true;
    +    procSym->dataType   = returnType;
    +    procSym->isTyped    = returnTyped;
    +    procSym->objClass   = returnClass;
    +    procSym->isPrivate  = isPrivate;
    +    procSym->isProperty = isProperty;
         applyParamList(procSym, &pl);
     
         // Record the owning form so fireCtrlEvent can bind the procedure's
    @@ -5528,6 +7625,11 @@ static void parseProcDef(BasParserT *p, bool isFunction) {
             procSym->formName[BAS_MAX_IDENT - 1] = '\0';
         }
     
    +    if (p->sym.inClassScope) {
    +        procSym->ownerClass = p->sym.classIdx;
    +        addClassMember(p, name, procSym, kind, isPrivate);
    +    }
    +
         // Backpatch any forward-reference calls to this procedure
         patchCallAddrs(p, procSym);
     
    @@ -5572,6 +7674,23 @@ static void parseProcDef(BasParserT *p, bool isFunction) {
     }
     
     
    +// PROPERTY GET / LET / SET name(params) ... END PROPERTY (isPrivate: a
    +// Private in front).
    +static void parsePropertyDef(BasParserT *p, bool isPrivate) {
    +    advance(p); // consume PROPERTY
    +
    +    if (check(p, TOK_GET)) {
    +        parseProcDef(p, BAS_MEMBER_GET, isPrivate);
    +    } else if (check(p, TOK_LET)) {
    +        parseProcDef(p, BAS_MEMBER_LET, isPrivate);
    +    } else if (check(p, TOK_SET)) {
    +        parseProcDef(p, BAS_MEMBER_SET, isPrivate);
    +    } else {
    +        errorExpected(p, "GET, LET or SET after PROPERTY");
    +    }
    +}
    +
    +
     static void parsePut(BasParserT *p) {
         // PUT #channel, [recno], var
         advance(p); // consume PUT
    @@ -5788,6 +7907,176 @@ static void parseRmDir(BasParserT *p) {
     }
     
     
    +// The console and graphics statements (the identifier is the current
    +// token): their arguments in the layout opcodes.h gives each built-in.
    +static void parseScreenStatement(BasParserT *p, const ScreenStmtT *stmt) {
    +    int32_t flags = stmt->flags;
    +    int32_t given = 0;
    +
    +    advance(p); // consume the statement name
    +
    +    switch (stmt->func) {
    +        case BAS_FN_CLS:
    +            if (atStatementEnd(p)) {
    +                emitPushInt16(p, BAS_CLS_OMITTED);
    +            } else {
    +                parseExpression(p);
    +            }
    +
    +            emitScreenStmt(p, BAS_FN_CLS, 1);
    +            return;
    +
    +        case BAS_FN_LOCATE:
    +            given = parseOptionalArgs(p, BAS_LOCATE_VALUES);
    +            parseIgnoredArgs(p, BAS_LOCATE_IGNORED);
    +            emitPushInt16(p, (int16_t)given);
    +            emitScreenStmt(p, BAS_FN_LOCATE, BAS_LOCATE_ARGS);
    +            return;
    +
    +        case BAS_FN_COLOR:
    +            given = parseOptionalArgs(p, BAS_PAIR_VALUES);
    +            parseIgnoredArgs(p, BAS_COLOR_IGNORED);
    +            emitPushInt16(p, (int16_t)given);
    +            emitScreenStmt(p, BAS_FN_COLOR, BAS_COLOR_ARGS);
    +            return;
    +
    +        case BAS_FN_WIDTH:
    +            if (check(p, TOK_HASH) || check(p, TOK_STRING_LIT)) {
    +                error(p, "WIDTH for files and devices is not supported");
    +                return;
    +            }
    +
    +            given = parseOptionalArgs(p, BAS_PAIR_VALUES);
    +            emitPushInt16(p, (int16_t)given);
    +            emitScreenStmt(p, BAS_FN_WIDTH, BAS_WIDTH_ARGS);
    +            return;
    +
    +        case BAS_FN_SCREEN:
    +            parseExpression(p);
    +            parseIgnoredArgs(p, BAS_SCREEN_IGNORED);
    +            emitScreenStmt(p, BAS_FN_SCREEN, 1);
    +            return;
    +
    +        case BAS_FN_PSET:
    +            parseGfxPsetArgs(p, flags);
    +            emitScreenStmt(p, BAS_FN_PSET, BAS_GFX_PSET_ARGS);
    +            return;
    +
    +        case BAS_FN_CIRCLE:
    +            parseGfxCircleArgs(p);
    +            emitScreenStmt(p, BAS_FN_CIRCLE, BAS_GFX_CIRCLE_ARGS);
    +            return;
    +
    +        case BAS_FN_PAINT: {
    +            static const int32_t slots[] = { BAS_GFX_COLOR, BAS_GFX_BORDER };
    +
    +            parseGfxPoint(p, &flags, BAS_GFX_STEP);
    +            parseFlaggedArgs(p, slots, (int32_t)(sizeof(slots) / sizeof(slots[0])), &flags);
    +            parseIgnoredArgs(p, BAS_PAINT_IGNORED);
    +            emitPushInt16(p, (int16_t)flags);
    +            emitScreenStmt(p, BAS_FN_PAINT, BAS_GFX_PAINT_ARGS);
    +            return;
    +        }
    +
    +        case BAS_FN_DRAW:
    +            parseExpression(p);
    +            emitScreenStmt(p, BAS_FN_DRAW, 1);
    +            return;
    +
    +        case BAS_FN_PALETTE:
    +            if (checkKeyword(p, "USING")) {
    +                advance(p);
    +
    +                BasSymbolT *arr = check(p, TOK_IDENT) ? basSymTabFind(&p->sym, p->lex.token.text) : NULL;
    +
    +                if (arr == NULL || arr->kind != SYM_VARIABLE || !arr->isArray) {
    +                    errorExpected(p, "array name");
    +                    return;
    +                }
    +
    +                advance(p);
    +                emitLoad(p, arr);
    +
    +                if (match(p, TOK_LPAREN)) {
    +                    parseExpression(p);
    +                    expect(p, TOK_RPAREN);
    +                    given = BAS_GIVEN_2;
    +                } else {
    +                    emitPushInt16(p, 0);
    +                }
    +
    +                emitPushInt16(p, (int16_t)given);
    +                emitScreenStmt(p, BAS_FN_PALETTE_USING, BAS_PALETTE_ARGS);
    +                return;
    +            }
    +
    +            if (atStatementEnd(p)) {
    +                emitPushInt16(p, 0);
    +                emitPushInt16(p, 0);
    +            } else {
    +                parseExpression(p);
    +                expect(p, TOK_COMMA);
    +                parseExpression(p);
    +                given = BAS_GIVEN_1;
    +            }
    +
    +            emitPushInt16(p, (int16_t)given);
    +            emitScreenStmt(p, BAS_FN_PALETTE, BAS_PALETTE_ARGS);
    +            return;
    +
    +        case BAS_FN_VIEW:
    +        case BAS_FN_WINDOW: {
    +            static const int32_t slots[] = { BAS_VIEW_FILL, BAS_VIEW_BORDER };
    +            bool                 isView  = stmt->func == BAS_FN_VIEW;
    +
    +            if (isView && check(p, TOK_PRINT)) {
    +                error(p, "VIEW PRINT is not supported");
    +                return;
    +            }
    +
    +            if (checkKeyword(p, "SCREEN")) {
    +                advance(p);
    +                flags |= BAS_VIEW_SCREEN;
    +            }
    +
    +            if (check(p, TOK_LPAREN)) {
    +                parseGfxRect(p);
    +                flags |= BAS_VIEW_COORDS;
    +            } else {
    +                for (int32_t i = 0; i < BAS_PAIR_VALUES * BAS_PAIR_VALUES; i++) {
    +                    emitPushInt16(p, 0);
    +                }
    +            }
    +
    +            if (isView) {
    +                parseFlaggedArgs(p, slots, (int32_t)(sizeof(slots) / sizeof(slots[0])), &flags);
    +            }
    +
    +            emitPushInt16(p, (int16_t)flags);
    +            emitScreenStmt(p, stmt->func, isView ? BAS_VIEW_ARGS : BAS_WINDOW_ARGS);
    +            return;
    +        }
    +
    +        case BAS_FN_FULLSCREEN:
    +            if (atStatementEnd(p) || match(p, TOK_ON)) {
    +                emitPushInt16(p, BAS_FULLSCREEN_ON);
    +            } else if (checkKeyword(p, "OFF")) {
    +                advance(p);
    +                emitPushInt16(p, BAS_FULLSCREEN_OFF);
    +            } else {
    +                parseExpression(p);
    +            }
    +
    +            emitScreenStmt(p, BAS_FN_FULLSCREEN, 1);
    +            return;
    +
    +        default:
    +            error(p, "Unknown screen statement");
    +            return;
    +    }
    +}
    +
    +
     static void parseSeek(BasParserT *p) {
         // SEEK #channel, position
         advance(p); // consume SEEK
    @@ -6025,6 +8314,26 @@ static void parseSelectCase(BasParserT *p) {
     }
     
     
    +// SET target = object: a variable (declared As a class it takes only
    +// that class), an array element or obj.member (Property Set).  The value
    +// may be a bare form or control name.
    +static void parseSet(BasParserT *p) {
    +    advance(p); // consume SET
    +
    +    p->setAssign = true;
    +
    +    if (check(p, TOK_ME)) {
    +        parseMeStatement(p);
    +    } else if (check(p, TOK_IDENT) || ownResultKeywordAhead(p)) {
    +        parseAssignOrCall(p);
    +    } else {
    +        errorExpected(p, "variable name");
    +    }
    +
    +    p->setAssign = false;
    +}
    +
    +
     static void parseSetAttr(BasParserT *p) {
         // SETATTR filename, attributes
         advance(p);
    @@ -6060,6 +8369,11 @@ static void parseShared(BasParserT *p) {
             return;
         }
     
    +    if (p->sym.inClassScope) {
    +        error(p, "SHARED is not allowed in a class module (its procedures see the member variables)");
    +        return;
    +    }
    +
         do {
             if (!check(p, TOK_IDENT)) {
                 errorExpected(p, "variable name");
    @@ -6246,6 +8560,13 @@ static void parseStatement(BasParserT *p) {
             return;
         }
     
    +    // A class module holds declarations and procedures only: code
    +    // outside procedures would have no instance to run for.
    +    if (p->sym.inClassScope && !p->sym.inLocalScope && !classLevelStatement(p)) {
    +        error(p, "Only declarations are allowed outside procedures in a class module");
    +        return;
    +    }
    +
         p->blockDepth++;
     
         // Compiler temporaries only live for one statement.
    @@ -6257,8 +8578,11 @@ static void parseStatement(BasParserT *p) {
     
         // A label "Name:" consumes its own colon and may be followed by a
         // statement on the same line, so it skips the end-of-statement check.
    +    // A class's Property Get / Function named by a keyword (Name, Open
    +    // ...) sets its result with that name inside its own body: the word
    +    // is taken as the identifier it names there.
         bool          endOfStatement = true;
    -    BasTokenTypeE tt             = p->lex.token.type;
    +    BasTokenTypeE tt             = ownResultKeywordAhead(p) ? TOK_IDENT : p->lex.token.type;
     
         switch (tt) {
             case TOK_PRINT:
    @@ -6330,11 +8654,20 @@ static void parseStatement(BasParserT *p) {
                 break;
     
             case TOK_SUB:
    -            parseProcDef(p, false);
    +            parseProcDef(p, BAS_MEMBER_SUB, false);
                 break;
     
             case TOK_FUNCTION:
    -            parseProcDef(p, true);
    +            parseProcDef(p, BAS_MEMBER_FUNCTION, false);
    +            break;
    +
    +        case TOK_PROPERTY:
    +            parsePropertyDef(p, false);
    +            break;
    +
    +        case TOK_PUBLIC:
    +        case TOK_PRIVATE:
    +            parseVisibility(p);
                 break;
     
             case TOK_EXIT:
    @@ -6364,6 +8697,10 @@ static void parseStatement(BasParserT *p) {
                 parseType(p);
                 break;
     
    +        case TOK_ENUM:
    +            parseEnum(p);
    +            break;
    +
             case TOK_REDIM:
                 parseRedim(p);
                 break;
    @@ -6421,7 +8758,7 @@ static void parseStatement(BasParserT *p) {
                 break;
     
             case TOK_LINE:
    -            parseLineInput(p);
    +            parseLine(p);
                 break;
     
             case TOK_GOTO:
    @@ -6567,25 +8904,7 @@ static void parseStatement(BasParserT *p) {
                 break;
     
             case TOK_SET:
    -            // SET var = expr (object assignment)
    -            advance(p); // consume SET
    -            if (!check(p, TOK_IDENT)) {
    -                errorExpected(p, "variable name");
    -                break;
    -            }
    -            {
    -                char varName[BAS_MAX_TOKEN_LEN];
    -                strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
    -                varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
    -                advance(p);
    -                expect(p, TOK_EQ);
    -                parseExpression(p);
    -                BasSymbolT *varSym = ensureVariable(p, varName);
    -                if (varSym) {
    -                    varSym->dataType = BAS_TYPE_OBJECT;
    -                    emitStore(p, varSym);
    -                }
    -            }
    +            parseSet(p);
                 break;
     
             case TOK_LOAD:
    @@ -6636,38 +8955,9 @@ static void parseStatement(BasParserT *p) {
                 basEmit8(&p->cg, OP_INI_WRITE);
                 break;
     
    -        case TOK_ME: {
    -            // Me.Show [modal] / Me.Hide, else a member chain on the form:
    -            // Me.Prop = expr, Me.Method args, Me.Ctrl[(idx)].Member ...
    -            advance(p); // consume Me
    -            if (!check(p, TOK_DOT)) {
    -                errorExpected(p, "'.' after Me");
    -                break;
    -            }
    -
    -            BasLexerT memberLex = p->lex;
    -            basLexerNext(&memberLex);
    -
    -            bool isShow = basTokenIsWord(&memberLex.token) && strcasecmp(memberLex.token.text, "Show") == 0;
    -            bool isHide = basTokenIsWord(&memberLex.token) && strcasecmp(memberLex.token.text, "Hide") == 0;
    -
    -            basEmit8(&p->cg, OP_ME_REF);
    -
    -            if (isShow) {
    -                advance(p); // DOT
    -                advance(p); // Show
    -                uint8_t modal = parseShowModalFlag(p);
    -                basEmit8(&p->cg, OP_SHOW_FORM);
    -                basEmit8(&p->cg, modal);
    -            } else if (isHide) {
    -                advance(p); // DOT
    -                advance(p); // Hide
    -                basEmit8(&p->cg, OP_HIDE_FORM);
    -            } else {
    -                parseObjectMemberStatement(p);
    -            }
    +        case TOK_ME:
    +            parseMeStatement(p);
                 break;
    -        }
     
             case TOK_LET:
                 advance(p); // consume LET, then fall through to assignment
    @@ -6679,17 +8969,41 @@ static void parseStatement(BasParserT *p) {
                 break;
     
             case TOK_IDENT: {
    -            // Check for form scope directives (injected by IDE)
    -            if (checkKeyword(p, "BEGINFORM")) {
    +            // Form and class scope directives (injected by the project
    +            // builder), and a class module's Attribute lines
    +            if (checkKeyword(p, BAS_KW_BEGINFORM)) {
                     parseBeginForm(p);
                     break;
                 }
     
    -            if (checkKeyword(p, "ENDFORM")) {
    +            if (checkKeyword(p, BAS_KW_ENDFORM)) {
                     parseEndForm(p);
                     break;
                 }
     
    +            if (checkKeyword(p, BAS_KW_BEGINCLASS)) {
    +                parseBeginClass(p);
    +                break;
    +            }
    +
    +            if (checkKeyword(p, BAS_KW_ENDCLASS)) {
    +                parseEndClass(p);
    +                break;
    +            }
    +
    +            if (p->sym.inClassScope && checkKeyword(p, BAS_KW_ATTRIBUTE)) {
    +                parseAttribute(p);
    +                break;
    +            }
    +
    +            // CLS, LOCATE, PSET, ... (before the label test: CLS: PRINT)
    +            const ScreenStmtT *screenStmt = findScreenStmt(p);
    +
    +            if (screenStmt != NULL) {
    +                parseScreenStatement(p, screenStmt);
    +                break;
    +            }
    +
                 // Check for label: identifier followed by colon
                 BasLexerT savedLex = p->lex;
                 char labelName[BAS_MAX_TOKEN_LEN];
    @@ -6776,10 +9090,14 @@ static void parseStatic(BasParserT *p) {
             return;
         }
     
    -    // Create a mangled global name: "procName$varName".  It must fit a
    -    // symbol name like any other; the symbol table never truncates.
    -    char mangledName[BAS_MAX_IDENT * 2 + 1];
    -    int32_t mangledLen = snprintf(mangledName, sizeof(mangledName), "%s$%s", p->currentProc, varName);
    +    // Create a mangled global name: "procName$varName" ("Class.procName$
    +    // varName" in a class module: one variable shared by every instance).
    +    // It must fit a symbol name like any other; the symbol table never
    +    // truncates.
    +    char        mangledName[BAS_MAX_IDENT * 2 + 1];
    +    const char *className  = p->sym.inClassScope ? p->sym.classes[p->sym.classIdx].name : "";
    +    const char *classSep   = p->sym.inClassScope ? BAS_CLASS_SEP : "";
    +    int32_t     mangledLen = snprintf(mangledName, sizeof(mangledName), "%s%s%s$%s", className, classSep, p->currentProc, varName);
     
         if (mangledLen >= BAS_MAX_IDENT) {
             char buf[BAS_PARSE_ERR_SCRATCH];
    @@ -7055,6 +9373,62 @@ static bool parseVarRef(BasParserT *p, VarRefT *ref, bool allowFields) {
     }
     
     
    +// PUBLIC / PRIVATE in front of a declaration outside procedures.  In a
    +// class module they make a member public (reachable through an object)
    +// or private; elsewhere a Public variable is DIM SHARED and a Private one
    +// a plain DIM.  Procedures, constants, types and ENUMs are visible to the
    +// whole program either way.
    +static void parseVisibility(BasParserT *p) {
    +    bool isPrivate = check(p, TOK_PRIVATE);
    +
    +    advance(p); // consume PUBLIC / PRIVATE
    +
    +    if (p->sym.inLocalScope) {
    +        error(p, "PUBLIC and PRIVATE are not allowed inside SUB or FUNCTION");
    +        return;
    +    }
    +
    +    switch (p->lex.token.type) {
    +        case TOK_SUB:
    +            parseProcDef(p, BAS_MEMBER_SUB, isPrivate);
    +            return;
    +
    +        case TOK_FUNCTION:
    +            parseProcDef(p, BAS_MEMBER_FUNCTION, isPrivate);
    +            return;
    +
    +        case TOK_PROPERTY:
    +            parsePropertyDef(p, isPrivate);
    +            return;
    +
    +        case TOK_CONST:
    +            parseConst(p);
    +            return;
    +
    +        case TOK_TYPE:
    +            parseType(p);
    +            return;
    +
    +        case TOK_ENUM:
    +            parseEnum(p);
    +            return;
    +
    +        case TOK_DECLARE:
    +            parseDeclare(p);
    +            return;
    +
    +        default:
    +            break;
    +    }
    +
    +    bool isShared = !isPrivate && !p->sym.inClassScope;
    +
    +    do {
    +        parseDimDeclarator(p, isShared, isPrivate);
    +    } while (match(p, TOK_COMMA));
    +}
    +
    +
     static void parseWhile(BasParserT *p) {
         // WHILE cond
         //   ...
    @@ -7235,6 +9609,40 @@ static BasTokenTypeE peekAfterParens(const BasParserT *p) {
     }
     
     
    +// Registers every class module (each BEGINCLASS "name") before anything
    +// else is read, so code ahead of a class can use its name as a type.
    +static void prescanClasses(BasParserT *p) {
    +    BasLexerT savedLex = p->lex;
    +
    +    while (!check(p, TOK_EOF)) {
    +        if (p->hasError) {
    +            p->hasError  = false;
    +            p->errorLine = 0;
    +            p->error[0]  = '\0';
    +        }
    +
    +        if (checkKeyword(p, BAS_KW_BEGINFORM) || checkKeyword(p, BAS_KW_BEGINCLASS)) {
    +            bool isClass = checkKeyword(p, BAS_KW_BEGINCLASS);
    +
    +            advance(p);
    +
    +            if (check(p, TOK_STRING_LIT) && isClass) {
    +                basSymTabAddClass(&p->sym, p->lex.token.text);
    +            } else if (check(p, TOK_STRING_LIT)) {
    +                arrput(p->formNames, strdup(p->lex.token.text));
    +            }
    +
    +            continue;
    +        }
    +
    +        advance(p);
    +    }
    +
    +    p->hasError = false;
    +    p->lex      = savedLex;
    +}
    +
    +
     // Walk the token stream from current position, find every
     // top-level SUB/FUNCTION declaration, extract the signature
     // (name, params, return type), and register it in the symbol
    @@ -7251,8 +9659,11 @@ static BasTokenTypeE peekAfterParens(const BasParserT *p) {
     static void prescanSignatures(BasParserT *p) {
         BasLexerT savedLex = p->lex;
         uint8_t   savedDefType[BAS_DEFTYPE_LETTERS];
    +    int32_t   classIdx  = BAS_CLASS_NONE;
    +    bool      isPrivate = false;
     
         memcpy(savedDefType, p->defType, sizeof(savedDefType));
    +    prescanClasses(p);
     
         while (!check(p, TOK_EOF)) {
             // Clear a latched lexer error so the walk continues; advance()
    @@ -7281,17 +9692,71 @@ static void prescanSignatures(BasParserT *p) {
                 continue;
             }
     
    -        bool isFn  = check(p, TOK_FUNCTION);
    -        bool isSub = check(p, TOK_SUB);
    +        // ENUM names are types a signature may use (AS EnumName); the
    +        // main pass defines the ENUM on this symbol.
    +        if (check(p, TOK_ENUM)) {
    +            advance(p);
     
    -        if (!isFn && !isSub) {
    +            if (check(p, TOK_IDENT) && basSymTabFindGlobal(&p->sym, p->lex.token.text) == NULL) {
    +                BasSymbolT *enumSym = basSymTabAddGlobal(&p->sym, p->lex.token.text, SYM_ENUM, BAS_TYPE_LONG);
    +
    +                if (enumSym != NULL) {
    +                    enumSym->isDefined = false;
    +                }
    +            }
    +
    +            continue;
    +        }
    +
    +        // Class modules: their procedures are "Class.name"
    +        if (checkKeyword(p, BAS_KW_BEGINCLASS)) {
    +            advance(p);
    +            classIdx = BAS_CLASS_NONE;
    +
    +            if (check(p, TOK_STRING_LIT)) {
    +                basSymTabFindClass(&p->sym, p->lex.token.text, &classIdx);
    +            }
    +
    +            continue;
    +        }
    +
    +        if (checkKeyword(p, BAS_KW_ENDCLASS)) {
    +            advance(p);
    +            classIdx = BAS_CLASS_NONE;
    +            continue;
    +        }
    +
    +        if (check(p, TOK_PUBLIC) || check(p, TOK_PRIVATE)) {
    +            isPrivate = check(p, TOK_PRIVATE);
                 advance(p);
                 continue;
             }
     
    -        advance(p); // consume SUB / FUNCTION
    +        uint8_t kind = BAS_MEMBER_VAR;
     
    -        if (!check(p, TOK_IDENT)) {
    +        if (check(p, TOK_FUNCTION)) {
    +            kind = BAS_MEMBER_FUNCTION;
    +        } else if (check(p, TOK_SUB)) {
    +            kind = BAS_MEMBER_SUB;
    +        } else if (check(p, TOK_PROPERTY)) {
    +            advance(p);
    +            kind = check(p, TOK_GET) ? BAS_MEMBER_GET : (check(p, TOK_LET) ? BAS_MEMBER_LET : (check(p, TOK_SET) ? BAS_MEMBER_SET : BAS_MEMBER_VAR));
    +        }
    +
    +        bool procPrivate = isPrivate;
    +
    +        isPrivate = false;
    +
    +        if (kind == BAS_MEMBER_VAR) {
    +            advance(p);
    +            continue;
    +        }
    +
    +        bool isFn = (kind == BAS_MEMBER_FUNCTION || kind == BAS_MEMBER_GET);
    +
    +        advance(p); // consume SUB / FUNCTION / GET / LET / SET
    +
    +        if (!check(p, TOK_IDENT) && !(classIdx != BAS_CLASS_NONE && basTokenIsWord(&p->lex.token))) {
                 continue;
             }
     
    @@ -7320,14 +9785,23 @@ static void prescanSignatures(BasParserT *p) {
     
             // Register / update the symbol.  If a call site already
             // created a forward-ref stub, update it in place.
    -        BasSymbolT *sym = basSymTabFindGlobal(&p->sym, name);
    +        char symName[BAS_MAX_IDENT];
    +
    +        if (!procSymbolName(p, classIdx, name, kind, symName, (int32_t)sizeof(symName))) {
    +            continue;
    +        }
    +
    +        BasSymbolT *sym = basSymTabFindGlobal(&p->sym, symName);
     
             if (sym == NULL) {
    -            sym = basSymTabAddGlobal(&p->sym, name, isFn ? SYM_FUNCTION : SYM_SUB, returnType);
    +            sym = basSymTabAddGlobal(&p->sym, symName, isFn ? SYM_FUNCTION : SYM_SUB, returnType);
             }
     
             if (sym != NULL) {
    -            sym->dataType = returnType;
    +            sym->dataType   = returnType;
    +            sym->ownerClass = classIdx;
    +            sym->isPrivate  = procPrivate;
    +            sym->isProperty = isPropertyKind(kind);
                 applyParamList(sym, &pl);
     
                 // basSymTabAdd defaults isDefined=true; clear it so
    @@ -7361,6 +9835,8 @@ static void procBegin(BasParserT *p, ProcSaveT *save, bool isFunction) {
         save->doDepth              = p->doDepth;
         save->lastLabelSelectDepth = p->lastLabelSelectDepth;
         save->isFunction           = isFunction;
    +    save->forIsEach            = p->forIsEach;
    +    save->returnType           = BAS_TYPE_INTEGER;
     
         exitListInit(&p->exitForList);
         exitListInit(&p->exitDoList);
    @@ -7373,6 +9849,7 @@ static void procBegin(BasParserT *p, ProcSaveT *save, bool isFunction) {
         p->doDepth                = 0;
         p->lastLabelSelectDepth   = 0;
         p->currentProcIsFunction  = isFunction;
    +    p->forIsEach              = false;
     
         // The procedure's temporaries are locals; the module's pool is
         // restored by procEnd.
    @@ -7405,10 +9882,17 @@ static void procEnd(BasParserT *p, ProcSaveT *save) {
             basEmit8(&p->cg, OP_RET);
         }
     
    -    // Prologue: "" into every implicit STRING local, then back to the
    -    // body.  With nothing to initialise the entry jump falls through.
    +    // Prologue: an OBJECT / VARIANT FUNCTION result starts as Nothing /
    +    // Empty and every implicit STRING local as "", then back to the body.
    +    // With nothing to initialise the entry jump falls through.
         int32_t prologueStart = basCodePos(&p->cg);
     
    +    if (save->isFunction && (save->returnType == BAS_TYPE_OBJECT || save->returnType == BAS_TYPE_VARIANT)) {
    +        emitZeroValue(p, save->returnType);
    +        basEmit8(&p->cg, OP_STORE_LOCAL);
    +        basEmitU16(&p->cg, 0);
    +    }
    +
         for (int32_t i = 0; i < p->sym.count; i++) {
             BasSymbolT *sym = p->sym.symbols[i];
     
    @@ -7445,11 +9929,38 @@ static void procEnd(BasParserT *p, ProcSaveT *save) {
         p->forDepth               = save->forDepth;
         p->doDepth                = save->doDepth;
         p->lastLabelSelectDepth   = save->lastLabelSelectDepth;
    +    p->forIsEach              = save->forIsEach;
         p->currentProcIsFunction  = false;
    +    p->currentProcIsProperty  = false;
         p->currentProc[0]         = '\0';
     }
     
     
    +// The symbol name of procedure name of member kind in class classIdx
    +// (BAS_CLASS_NONE: not in a class): "Class.name", and for Property Let /
    +// Set "Class.name.Let" / ".Set"; else name.  False when it is too long
    +// (an error is set).
    +static bool procSymbolName(BasParserT *p, int32_t classIdx, const char *name, uint8_t kind, char *out, int32_t outSize) {
    +    const char *suffix = (kind == BAS_MEMBER_LET) ? BAS_PROP_LET_SUFFIX : ((kind == BAS_MEMBER_SET) ? BAS_PROP_SET_SUFFIX : "");
    +    char        member[BAS_MAX_IDENT];
    +    int         n      = snprintf(member, sizeof(member), "%s%s", name, suffix);
    +
    +    if (classIdx == BAS_CLASS_NONE) {
    +        snprintf(out, outSize, "%s", member);
    +        return true;
    +    }
    +
    +    if (n < 0 || n >= (int)sizeof(member) || !basSymTabQualify(&p->sym, classIdx, member, out, outSize)) {
    +        char buf[BAS_PARSE_ERR_SCRATCH];
    +        snprintf(buf, sizeof(buf), "Name '%s' is too long for a class member", name);
    +        error(p, buf);
    +        return false;
    +    }
    +
    +    return true;
    +}
    +
    +
     // Inside a SUB/FUNCTION a module variable that is not SHARED is not
     // visible.  Used as an array or object (name( or name.) it would silently
     // compile as a control reference or a procedure call, so say why instead.
    @@ -7492,6 +10003,21 @@ static bool requireMemberName(BasParserT *p) {
     }
     
     
    +// The built-in function name calls, or NULL when name is none, is a
    +// variable, procedure or constant the program declares, or names a control
    +// (a '.' follows): the program's own meaning wins, so a new built-in never
    +// breaks a program that already uses the name.
    +static const BuiltinFuncT *resolveBuiltin(BasParserT *p, const char *name) {
    +    const BuiltinFuncT *builtin = findBuiltin(name);
    +
    +    if (builtin == NULL || check(p, TOK_DOT) || basSymTabFind(&p->sym, name) != NULL) {
    +        return NULL;
    +    }
    +
    +    return builtin;
    +}
    +
    +
     static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) {
         for (int32_t i = 0; i < typeSym->fieldCount; i++) {
             if (strcasecmp(typeSym->fields[i].name, fieldName) == 0) {
    @@ -7509,7 +10035,8 @@ static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) {
     static uint8_t resolveParamType(BasParserT *p, bool prescan, int32_t *outUdtTypeId) {
         *outUdtTypeId = -1;
     
    -    if (prescan && check(p, TOK_IDENT) && findTypeDef(p, p->lex.token.text) == NULL) {
    +    if (prescan && check(p, TOK_IDENT) && findTypeDef(p, p->lex.token.text) == NULL && findEnum(p, p->lex.token.text) == NULL && basSymTabFindClass(&p->sym, p->lex.token.text, NULL) == NULL && !isBuiltinTypeName(p->lex.token.text)) {
    +        p->lastObjClass = BAS_CLASS_NONE;
             advance(p);
             return BAS_TYPE_UDT;
         }
    @@ -7525,6 +10052,8 @@ static uint8_t resolveParamType(BasParserT *p, bool prescan, int32_t *outUdtType
     
     
     static uint8_t resolveTypeName(BasParserT *p) {
    +    p->lastObjClass = BAS_CLASS_NONE;
    +
         // Expect a type keyword after AS
         if (check(p, TOK_INTEGER)) {
             advance(p);
    @@ -7550,16 +10079,48 @@ static uint8_t resolveTypeName(BasParserT *p) {
             advance(p);
             return BAS_TYPE_BOOLEAN;
         }
    -    // Check for user-defined TYPE name
    +    // Check for user-defined TYPE name, then a class, DATE, VARIANT and
    +    // Collection (not reserved words, so a TYPE or class may take the
    +    // name)
         if (check(p, TOK_IDENT)) {
             BasSymbolT *typeSym = findTypeDef(p, p->lex.token.text);
    +        int32_t     classIdx;
    +
             if (typeSym != NULL) {
                 p->lastUdtTypeId = typeSym->index;
                 advance(p);
                 return BAS_TYPE_UDT;
             }
    +        if (findEnum(p, p->lex.token.text) != NULL) {
    +            advance(p);
    +            return BAS_TYPE_LONG;
    +        }
    +        if (basSymTabFindClass(&p->sym, p->lex.token.text, &classIdx) != NULL) {
    +            p->lastObjClass = classIdx;
    +            advance(p);
    +            return BAS_TYPE_OBJECT;
    +        }
    +        if (strcasecmp(p->lex.token.text, BAS_DATE_TYPE_NAME) == 0) {
    +            advance(p);
    +            return BAS_TYPE_DATE;
    +        }
    +        if (strcasecmp(p->lex.token.text, BAS_VARIANT_TYPE_NAME) == 0) {
    +            advance(p);
    +            return BAS_TYPE_VARIANT;
    +        }
    +        if (strcasecmp(p->lex.token.text, BAS_COLLECTION_TYPE_NAME) == 0) {
    +            p->lastObjClass = BAS_CLASS_COLLECTION;
    +            advance(p);
    +            return BAS_TYPE_OBJECT;
    +        }
    +        for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(sObjectTypeNames); i++) {
    +            if (strcasecmp(p->lex.token.text, sObjectTypeNames[i]) == 0) {
    +                advance(p);
    +                return BAS_TYPE_OBJECT;
    +            }
    +        }
         }
    -    error(p, "Expected type name (Integer, Long, Single, Double, String, Boolean, or TYPE name)");
    +    error(p, "Expected type name (Integer, Long, Single, Double, String, Boolean, Date, Variant, Control, Form, Object, Collection, TYPE, ENUM or class name)");
         return BAS_TYPE_INTEGER;
     }
     
    @@ -7611,6 +10172,21 @@ static void skipNewlines(BasParserT *p) {
     }
     
     
    +// Empty or Null named by the identifier text (not reserved: a program's
    +// own symbol of that name wins), else BAS_SPECIAL_NONE.
    +static int32_t specialValueName(const char *text) {
    +    if (strcasecmp(text, BAS_EMPTY_NAME) == 0) {
    +        return BAS_SPECIAL_EMPTY;
    +    }
    +
    +    if (strcasecmp(text, BAS_NULL_NAME) == 0) {
    +        return BAS_SPECIAL_NULL;
    +    }
    +
    +    return BAS_SPECIAL_NONE;
    +}
    +
    +
     // Scope whose slot table holds a variable: a procedure-local alias (a
     // STATIC variable, a SHARED statement) lives in a module slot.
     static BasScopeE storageScope(const BasSymbolT *sym) {
    diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.h b/src/apps/kpunch/dvxbasic/compiler/parser.h
    index 89401b5f..1896bbd4 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/parser.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/parser.h
    @@ -97,6 +97,7 @@ typedef struct {
         int32_t      errorLine;
         int32_t      prevLine;      // line of the previous token (for error reporting)
         int32_t      lastUdtTypeId;  // index of last resolved UDT type from resolveTypeName
    +    int32_t      lastObjClass;   // class of the last object type resolveTypeName read (BAS_CLASS_NONE for Object / Control / Form)
         int32_t      tempCount;      // compiler temporaries created so far (names them uniquely)
         BasSymbolT **tempPool;       // stb_ds: temporaries of the current scope, reused from the start of every statement
         int32_t      tempPoolUsed;   // temporaries of tempPool taken by the current statement
    @@ -105,6 +106,12 @@ typedef struct {
         int32_t      optionBase;         // default array lower bound (0 or 1)
         bool         optionExplicit;     // true = variables must be declared with DIM
         bool         currentProcIsFunction; // true while parsing a FUNCTION body (EXIT FUNCTION / return-value assignment)
    +    bool         currentProcIsProperty; // true while parsing a Property Get / Let / Set body (EXIT PROPERTY)
    +    int32_t      currentProcAddr;       // code address of the SUB / FUNCTION being parsed
    +    bool         forIsEach;      // the innermost FOR loop is a FOR EACH (no VM FOR frame for EXIT FOR to drop)
    +    bool         setAssign;      // parsing a SET statement: the target takes an object
    +    int32_t      memberClass;    // class of the object the member access being parsed starts from (BAS_CLASS_NONE: unknown)
    +    char       **formNames;      // stb_ds: every BEGINFORM name (the prescan collects them), malloc'd
         uint8_t      defType[BAS_DEFTYPE_LETTERS]; // default type per letter (A-Z), set by DEFINT etc.
         char         currentProc[BAS_MAX_TOKEN_LEN]; // name of current SUB/FUNCTION
         // Per-form init block tracking
    @@ -136,6 +143,11 @@ typedef struct {
         int32_t      blockDepth;
         // Optional compile-time CtrlName.Member validator (IDE-only).
         const BasCtrlValidatorT *validator;
    +    // Constant expressions (CONST, ENUM values) are compiled like any
    +    // expression and run on this private VM, created on first use.
    +    BasVmT      *constVm;
    +    bool         inConstExpr;    // parsing a constant expression: only constants may be named
    +    bool         compareText;    // OPTION COMPARE TEXT in effect (string comparisons in constant expressions)
     } BasParserT;
     
     // ============================================================
    diff --git a/src/apps/kpunch/dvxbasic/compiler/symtab.c b/src/apps/kpunch/dvxbasic/compiler/symtab.c
    index 33aa4fef..7a28cfa2 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/symtab.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/symtab.c
    @@ -25,6 +25,7 @@
     #include "symtab.h"
     #include "thirdparty/stb_ds_wrap.h"
     
    +#include 
     #include 
     #include 
     
    @@ -32,23 +33,29 @@
     // Function prototypes (alphabetical)
     static void basSymbolFree(BasSymbolT *sym);
     BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
    +int32_t basSymTabAddClass(BasSymTabT *tab, const char *name);
     BasSymbolT *basSymTabAddGlobal(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
     BasSymbolT *basSymTabAddModuleVar(BasSymTabT *tab, const char *name, uint8_t dataType);
     int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab);
     int32_t basSymTabAllocSlot(BasSymTabT *tab);
     BasScopeE basSymTabCurrentScope(const BasSymTabT *tab, BasSymKindE kind);
    +void basSymTabEnterClassScope(BasSymTabT *tab, int32_t classIdx);
     void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);
     void basSymTabEnterLocal(BasSymTabT *tab);
     BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
    +BasClassDefT *basSymTabFindClass(BasSymTabT *tab, const char *name, int32_t *outIdx);
     BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
     BasSymbolT *basSymTabFindLabel(BasSymTabT *tab, const char *name);
     BasSymbolT *basSymTabFindModuleVar(BasSymTabT *tab, const char *name);
     void basSymTabFree(BasSymTabT *tab);
     void basSymTabInit(BasSymTabT *tab);
    +int32_t basSymTabLeaveClassScope(BasSymTabT *tab);
     int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
     void basSymTabLeaveLocal(BasSymTabT *tab);
    +bool basSymTabQualify(const BasSymTabT *tab, int32_t classIdx, const char *name, char *out, int32_t outSize);
     static BasSymbolT **bucketFor(BasSymTabT *tab, uint32_t hash);
     static BasSymbolT *findInScope(BasSymTabT *tab, const char *name, BasScopeE scope, bool isLabel);
    +static BasSymbolT *findModuleSymbol(BasSymTabT *tab, const char *name, bool includeHidden);
     static BasSymbolT *findSymbol(BasSymTabT *tab, const char *name, bool includeHidden);
     static uint32_t nameHashCI(const char *name);
     static bool namesEqual(const char *a, const char *b);
    @@ -71,6 +78,24 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
     }
     
     
    +int32_t basSymTabAddClass(BasSymTabT *tab, const char *name) {
    +    BasSymbolT *sym = symTabAddScoped(tab, name, SYM_CLASS, BAS_TYPE_OBJECT, SCOPE_GLOBAL);
    +
    +    if (sym == NULL) {
    +        return -1;
    +    }
    +
    +    BasClassDefT def;
    +
    +    memset(&def, 0, sizeof(def));
    +    snprintf(def.name, sizeof(def.name), "%s", name);
    +    def.initCodeAddr = -1;
    +    sym->index       = (int32_t)arrlen(tab->classes);
    +    arrput(tab->classes, def);
    +    return sym->index;
    +}
    +
    +
     BasSymbolT *basSymTabAddGlobal(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
         return symTabAddScoped(tab, name, kind, dataType, SCOPE_GLOBAL);
     }
    @@ -98,6 +123,10 @@ int32_t basSymTabAllocSlot(BasSymTabT *tab) {
             return tab->nextLocalIdx++;
         }
     
    +    if (tab->inClassScope) {
    +        return tab->nextMemberIdx++;
    +    }
    +
         if (tab->inFormScope) {
             return tab->nextFormVarIdx++;
         }
    @@ -106,13 +135,18 @@ int32_t basSymTabAllocSlot(BasSymTabT *tab) {
     }
     
     
    -// Local > form > global.  Only variables get SCOPE_FORM; SUBs, FUNCTIONs,
    -// CONSTs and labels declared in a form scope remain global.
    +// Local > member > form > global.  Only variables get SCOPE_MEMBER or
    +// SCOPE_FORM; SUBs, FUNCTIONs, CONSTs and labels declared in a class or
    +// form scope remain global.
     BasScopeE basSymTabCurrentScope(const BasSymTabT *tab, BasSymKindE kind) {
         if (tab->inLocalScope) {
             return SCOPE_LOCAL;
         }
     
    +    if (tab->inClassScope && kind == SYM_VARIABLE) {
    +        return SCOPE_MEMBER;
    +    }
    +
         if (tab->inFormScope && kind == SYM_VARIABLE) {
             return SCOPE_FORM;
         }
    @@ -121,6 +155,13 @@ BasScopeE basSymTabCurrentScope(const BasSymTabT *tab, BasSymKindE kind) {
     }
     
     
    +void basSymTabEnterClassScope(BasSymTabT *tab, int32_t classIdx) {
    +    tab->inClassScope  = true;
    +    tab->classIdx      = classIdx;
    +    tab->nextMemberIdx = 0;
    +}
    +
    +
     void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName) {
         tab->inFormScope = true;
         strncpy(tab->formScopeName, formName, BAS_MAX_IDENT - 1);
    @@ -142,6 +183,21 @@ BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) {
     }
     
     
    +BasClassDefT *basSymTabFindClass(BasSymTabT *tab, const char *name, int32_t *outIdx) {
    +    BasSymbolT *sym = findInScope(tab, name, SCOPE_GLOBAL, false);
    +
    +    if (sym == NULL || sym->kind != SYM_CLASS) {
    +        return NULL;
    +    }
    +
    +    if (outIdx != NULL) {
    +        *outIdx = sym->index;
    +    }
    +
    +    return &tab->classes[sym->index];
    +}
    +
    +
     BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name) {
         return findInScope(tab, name, SCOPE_GLOBAL, false);
     }
    @@ -170,6 +226,13 @@ void basSymTabFree(BasSymTabT *tab) {
         tab->symbols = NULL;
         tab->count   = 0;
         memset(tab->buckets, 0, sizeof(tab->buckets));
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(tab->classes); i++) {
    +        arrfree(tab->classes[i].members);
    +    }
    +
    +    arrfree(tab->classes);
    +    tab->classes = NULL;
     }
     
     
    @@ -178,6 +241,16 @@ void basSymTabInit(BasSymTabT *tab) {
     }
     
     
    +int32_t basSymTabLeaveClassScope(BasSymTabT *tab) {
    +    int32_t varCount = tab->nextMemberIdx;
    +
    +    tab->inClassScope  = false;
    +    tab->classIdx      = 0;
    +    tab->nextMemberIdx = 0;
    +    return varCount;
    +}
    +
    +
     int32_t basSymTabLeaveFormScope(BasSymTabT *tab) {
         int32_t varCount = tab->nextFormVarIdx;
     
    @@ -220,6 +293,13 @@ void basSymTabLeaveLocal(BasSymTabT *tab) {
     }
     
     
    +bool basSymTabQualify(const BasSymTabT *tab, int32_t classIdx, const char *name, char *out, int32_t outSize) {
    +    int n = snprintf(out, outSize, "%s" BAS_CLASS_SEP "%s", tab->classes[classIdx].name, name);
    +
    +    return n > 0 && n < outSize;
    +}
    +
    +
     static BasSymbolT **bucketFor(BasSymTabT *tab, uint32_t hash) {
         return &tab->buckets[hash & (BAS_SYMTAB_BUCKETS - 1)];
     }
    @@ -240,29 +320,19 @@ static BasSymbolT *findInScope(BasSymTabT *tab, const char *name, BasScopeE scop
     }
     
     
    -// Non-label lookup: local scope first, then form and global.  Chains run
    -// newest first, so the most recent declaration wins.  A module variable
    -// that is not SHARED is skipped inside a procedure unless includeHidden.
    -static BasSymbolT *findSymbol(BasSymTabT *tab, const char *name, bool includeHidden) {
    -    uint32_t    h    = nameHashCI(name);
    -    BasSymbolT *head = *bucketFor(tab, h);
    +// Module-level (non-local, non-label) lookup of exactly name.  A module
    +// variable that is not SHARED is skipped inside a procedure unless
    +// includeHidden; a class's member variables are always visible to it.
    +static BasSymbolT *findModuleSymbol(BasSymTabT *tab, const char *name, bool includeHidden) {
    +    uint32_t h    = nameHashCI(name);
    +    bool     hide = tab->hideUnshared && !includeHidden;
     
    -    if (tab->inLocalScope) {
    -        for (BasSymbolT *s = head; s != NULL; s = s->hashNext) {
    -            if (s->nameHash == h && s->scope == SCOPE_LOCAL && s->kind != SYM_LABEL && namesEqual(s->name, name)) {
    -                return s;
    -            }
    -        }
    -    }
    -
    -    bool hide = tab->hideUnshared && !includeHidden;
    -
    -    for (BasSymbolT *s = head; s != NULL; s = s->hashNext) {
    +    for (BasSymbolT *s = *bucketFor(tab, h); s != NULL; s = s->hashNext) {
             if (s->nameHash != h || s->formScopeEnded || s->kind == SYM_LABEL || s->scope == SCOPE_LOCAL || !namesEqual(s->name, name)) {
                 continue;
             }
     
    -        if (hide && s->kind == SYM_VARIABLE && !s->isShared) {
    +        if (hide && s->kind == SYM_VARIABLE && !s->isShared && s->scope != SCOPE_MEMBER) {
                 continue;
             }
     
    @@ -273,6 +343,34 @@ static BasSymbolT *findSymbol(BasSymTabT *tab, const char *name, bool includeHid
     }
     
     
    +// Non-label lookup: local scope first, then (in a class module) the
    +// class's own "Class.name", then form and global.  Chains run newest
    +// first, so the most recent declaration wins.
    +static BasSymbolT *findSymbol(BasSymTabT *tab, const char *name, bool includeHidden) {
    +    if (tab->inLocalScope) {
    +        uint32_t h = nameHashCI(name);
    +
    +        for (BasSymbolT *s = *bucketFor(tab, h); s != NULL; s = s->hashNext) {
    +            if (s->nameHash == h && s->scope == SCOPE_LOCAL && s->kind != SYM_LABEL && namesEqual(s->name, name)) {
    +                return s;
    +            }
    +        }
    +    }
    +
    +    char qualified[BAS_MAX_IDENT];
    +
    +    if (tab->inClassScope && basSymTabQualify(tab, tab->classIdx, name, qualified, (int32_t)sizeof(qualified))) {
    +        BasSymbolT *member = findModuleSymbol(tab, qualified, includeHidden);
    +
    +        if (member != NULL) {
    +            return member;
    +        }
    +    }
    +
    +    return findModuleSymbol(tab, name, includeHidden);
    +}
    +
    +
     // ============================================================
     // Case-insensitive FNV-1a hash used to accelerate symbol-table lookups.
     // Caller computes hash of search-name once; each entry stores its own
    @@ -331,11 +429,13 @@ static BasSymbolT *symTabAddScoped(BasSymTabT *tab, const char *name, BasSymKind
         }
     
         strcpy(sym->name, name);
    -    sym->nameHash  = nameHashCI(name);
    -    sym->kind      = kind;
    -    sym->scope     = scope;
    -    sym->dataType  = dataType;
    -    sym->isDefined = true;
    +    sym->nameHash   = nameHashCI(name);
    +    sym->kind       = kind;
    +    sym->scope      = scope;
    +    sym->dataType   = dataType;
    +    sym->isDefined  = true;
    +    sym->ownerClass = BAS_CLASS_NONE;
    +    sym->objClass   = BAS_CLASS_NONE;
     
         // Record owning form for both SCOPE_FORM vars AND SUBs/FUNCTIONs
         // declared inside BEGINFORM...ENDFORM.  SUBs stay at SCOPE_GLOBAL
    diff --git a/src/apps/kpunch/dvxbasic/compiler/symtab.h b/src/apps/kpunch/dvxbasic/compiler/symtab.h
    index 4c7b9041..44ace02c 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/symtab.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/symtab.h
    @@ -48,9 +48,23 @@ typedef enum {
         SYM_SUB,
         SYM_FUNCTION,
         SYM_LABEL,
    -    SYM_TYPE_DEF    // user-defined TYPE
    +    SYM_TYPE_DEF,   // user-defined TYPE
    +    SYM_ENUM,       // ENUM name (a LONG type); members are SYM_CONSTs
    +    SYM_CLASS       // class module name (index = class number)
     } BasSymKindE;
     
    +// objClass of an object variable declared As Collection (a class module
    +// has its index there; BAS_CLASS_NONE: no class, e.g. As Object).
    +#define BAS_CLASS_NONE         (-1)
    +#define BAS_CLASS_COLLECTION   (-2)
    +
    +// Qualified names of class module symbols: "Class.Member", and the
    +// Property Let / Set procedures of a property "Class.Member.Let" /
    +// "Class.Member.Set" (Property Get takes the plain qualified name).
    +#define BAS_CLASS_SEP          "."
    +#define BAS_PROP_LET_SUFFIX    ".Let"
    +#define BAS_PROP_SET_SUFFIX    ".Set"
    +
     // BasScopeE moved to opcodes.h (shared with runtime).
     
     // ============================================================
    @@ -93,6 +107,8 @@ typedef struct BasSymbolS {
         int32_t      udtTypeId;    // for variables of BAS_TYPE_UDT: index of TYPE_DEF symbol
         int32_t      fixedLen;     // for STRING * n: fixed length (0 = variable-length)
         int32_t      refLine;      // LABEL/SUB/FUNCTION: source line of the first reference made before the definition
    +    int32_t      ownerClass;   // class module a procedure or member variable belongs to (BAS_CLASS_NONE: none)
    +    int32_t      objClass;     // object variable / FUNCTION / Property Get declared As a class: its index, BAS_CLASS_COLLECTION, else BAS_CLASS_NONE
     
         // For SUB/FUNCTION: parameter info
         int32_t      paramCount;
    @@ -107,6 +123,7 @@ typedef struct BasSymbolS {
             int32_t  constInt;
             double   constDbl;
         };
    +    int32_t      constStrLen;  // STRING CONST: length of constStr (it may hold NULs)
     
         // For TYPE_DEF: field definitions
         BasFieldDefT *fields;     // stb_ds dynamic array
    @@ -127,6 +144,12 @@ typedef struct BasSymbolS {
         bool         isTyped;      // variable declared with an explicit numeric type (suffix, AS clause or DEFxxx); stores convert to it
         bool         isAlias;      // procedure-local name for a module slot (STATIC backing variable or SHARED statement): lives in aliasScope slot 'index'
         uint8_t      aliasScope;   // BasScopeE of the slot an alias addresses (SCOPE_GLOBAL or SCOPE_FORM)
    +    bool         hasParamArray;   // SUB/FUNCTION: the last parameter is a ParamArray (paramTypes[paramCount - 1] is its element type)
    +    bool         paramArrayTyped; // the ParamArray has a declared type: each argument is converted to it (else it keeps its own type)
    +    bool         isEnumMember;    // CONST that is the unqualified name of an ENUM member
    +    bool         isAmbiguous;     // ENUM member name shared by two ENUMs: usable only qualified (EnumName.Member)
    +    bool         isPrivate;       // class module member declared Private (not reachable through an object)
    +    bool         isProperty;      // Property Get / Let / Set procedure
     
         char         name[BAS_MAX_IDENT];
         char         formName[BAS_MAX_IDENT]; // form name for SCOPE_FORM vars
    @@ -136,6 +159,29 @@ typedef struct BasSymbolS {
         char         constStr[BAS_MAX_CONST_STR];
     } BasSymbolT;
     
    +// ============================================================
    +// Class modules
    +// ============================================================
    +
    +// A public member of a class module, recorded as it is declared.
    +typedef struct {
    +    char        name[BAS_MAX_IDENT];  // as the program uses it (obj.name)
    +    BasSymbolT *sym;                  // the procedure or member variable
    +    uint8_t     kind;                 // BasMemberKindE
    +} BasClassMemberDefT;
    +
    +// A class module (BEGINCLASS "name" ... ENDCLASS).  The prescan registers
    +// every class so code before the class can use its name as a type.
    +typedef struct {
    +    char                name[BAS_MAX_IDENT];
    +    char                defaultMember[BAS_MAX_IDENT]; // Attribute Name.VB_UserMemId = 0 ("" = none)
    +    BasClassMemberDefT *members;      // stb_ds array, public members in declaration order
    +    int32_t             varCount;     // member variables per instance (set at ENDCLASS)
    +    int32_t             initCodeAddr; // instance set-up code (member variables, then Class_Initialize)
    +    BasSymbolT         *terminateSym; // Class_Terminate, NULL = none
    +    bool                defined;      // its BEGINCLASS was compiled (a second one is a duplicate)
    +} BasClassDefT;
    +
     // ============================================================
     // Symbol table
     // ============================================================
    @@ -158,6 +204,10 @@ typedef struct {
         char        formScopeName[BAS_MAX_IDENT]; // current form name
         int32_t     nextFormVarIdx;   // next form-level variable slot
         int32_t     formScopeSymStart; // symbol count at BEGINFORM (for marking ended)
    +    bool        inClassScope;     // true inside BEGINCLASS...ENDCLASS
    +    int32_t     classIdx;         // the class being compiled (valid while inClassScope)
    +    int32_t     nextMemberIdx;    // next member variable slot of that class
    +    BasClassDefT *classes;        // stb_ds array, indexed by class number
     } BasSymTabT;
     
     // ============================================================
    @@ -226,6 +276,27 @@ int32_t basSymTabAllocSlot(BasSymTabT *tab);
     // SCOPE_GLOBAL but the table is currently inside a SUB/FUNCTION or form.
     int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab);
     
    +// Register class module name (the prescan does, for every BEGINCLASS).
    +// Returns its class number, or -1 (the name is taken, too long, or out
    +// of memory).
    +int32_t basSymTabAddClass(BasSymTabT *tab, const char *name);
    +
    +// Enter class scope (called at BEGINCLASS): module-level declarations
    +// create member variables (SCOPE_MEMBER), and names are looked up as
    +// "Class.name" first.
    +void basSymTabEnterClassScope(BasSymTabT *tab, int32_t classIdx);
    +
    +// The class module named name, or NULL.
    +BasClassDefT *basSymTabFindClass(BasSymTabT *tab, const char *name, int32_t *outIdx);
    +
    +// Leave class scope (called at ENDCLASS).  Returns the number of member
    +// variables allocated.
    +int32_t basSymTabLeaveClassScope(BasSymTabT *tab);
    +
    +// Write "Class.name" for class classIdx into out (outSize bytes); false
    +// when it does not fit.
    +bool basSymTabQualify(const BasSymTabT *tab, int32_t classIdx, const char *name, char *out, int32_t outSize);
    +
     // Enter form scope (called at BEGINFORM). Form-level DIMs create SCOPE_FORM variables.
     void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);
     
    diff --git a/src/apps/kpunch/dvxbasic/ctrlover.dhs b/src/apps/kpunch/dvxbasic/ctrlover.dhs
    index 10ef8cbe..d4904e21 100644
    --- a/src/apps/kpunch/dvxbasic/ctrlover.dhs
    +++ b/src/apps/kpunch/dvxbasic/ctrlover.dhs
    @@ -30,6 +30,12 @@
     .index Properties
     .index Events
     .index Methods
    +.index DragMode
    +.index DragIcon
    +.index Drag
    +.index DragDrop
    +.index DragOver
    +.index Drag and Drop
     
     .h1 Common Properties, Events, and Methods
     
    @@ -57,6 +63,8 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth
       TabIndex     Integer   R     Accepted for VB compatibility but ignored. DVX has no tab order.
       ToolTipText  String    R/W   Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
       HelpTopic    String    R/W   Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file.
    +  DragMode     Integer   R/W   vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below.
    +  DragIcon     String    R/W   Picture file (BMP, PNG, JPEG or GIF) shown under the mouse while the control is dragged. Empty = a dotted outline the size of the control.
     .endtable
     
     Size properties (Width, Height and the Min/Max sizes) are limited to 0..32767.
    @@ -80,6 +88,8 @@ These events are wired on every control loaded from a .frm file. Controls create
       MouseUp     Button As Integer, X As Integer, Y As Integer Fires when a mouse button is released over the control.
       MouseMove   Button As Integer, X As Integer, Y As Integer Fires when the mouse moves over the control.
       Scroll      Delta As Integer                              Fires when the control is scrolled (mouse wheel or scrollbar).
    +  DragDrop    Source As Control, X As Integer, Y As Integer Fires when a dragged control is dropped on this control. Source is the dragged control; X and Y are where, in this control.
    +  DragOver    Source As Control, X As Integer, Y As Integer, State As Integer   Fires while a dragged control moves over this control. State is vbEnter (0) when it comes onto the control, vbOver (2) as it moves across, vbLeave (1) when it goes off.
     .endtable
     
     .h2 Common Methods
    @@ -89,13 +99,124 @@ These events are wired on every control loaded from a .frm file. Controls create
       --------   ----------   -------------------------------------------
       SetFocus   (none)       Gives keyboard focus to this control.
       Refresh    (none)       Forces the control to repaint.
    +  Drag       [Action]     Starts or ends dragging this control: vbBeginDrag (1, the default) starts, vbEndDrag (2) drops it where the mouse is, 0 cancels the drag without a drop.
     .endtable
     
    +.h2 Drag and Drop
    +
    +Any control can be dragged onto another control or onto a form, as in Visual Basic. While a control is dragged, a dotted outline of it (or its DragIcon picture) follows the mouse, and the controls and forms it passes over get DragOver events. Releasing the mouse button drops it: the control or form under the mouse gets a DragDrop event, with the dragged control as Source. Pressing Esc cancels the drag.
    +
    +With DragMode set to vbAutomatic, pressing the left mouse button on the control starts the drag, and the control gets no Click, MouseDown or MouseUp from the left button. With DragMode vbManual, start the drag yourself, usually in MouseDown. A drag started while the mouse button is up ends at the next click. During a drag no mouse events other than DragOver and DragDrop are raised.
    +
    +Drop targets only react: nothing is moved or copied for you. Use Source in DragDrop to decide what to do, for example read its Name or Caption, or check what kind of control it is with TypeOf Source Is TextBox (see Operators).
    +
    +Note that vbCancel is the MsgBox Cancel value (2), which as a Drag action means vbEndDrag; cancel a drag with Drag 0.
    +
    +.code
    +' Label1 has DragMode = 1 (vbAutomatic) in the form designer.
    +Sub List1_DragOver(Source As Control, X As Integer, Y As Integer, State As Integer)
    +    If State = vbEnter Then Label2.Caption = "Drop to add " & Source.Caption
    +End Sub
    +
    +Sub List1_DragDrop(Source As Control, X As Integer, Y As Integer)
    +    List1.AddItem Source.Caption
    +End Sub
    +
    +' DragMode vbManual: the button starts its own drag, left button only.
    +Sub Command1_MouseDown(Button As Integer, X As Integer, Y As Integer)
    +    If Button = 1 Then Command1.Drag vbBeginDrag
    +End Sub
    +.endcode
    +
     .link ctrl.form Form
     .link ctrl.databinding Data Binding
     .link ctrl.frm FRM File Format
     
     
    +.topic ctrl.drawing
    +.title Drawing on Forms and PictureBoxes
    +.toc 1 Drawing on Forms and PictureBoxes
    +.index Drawing Methods
    +.index Cls
    +.index PSet method
    +.index Line method
    +.index Circle method
    +.index Point method
    +.index Print method
    +.index PaintPicture
    +.index TextWidth
    +.index TextHeight
    +.index CurrentX
    +.index CurrentY
    +.index DrawWidth
    +.index FillColor
    +.index FillStyle
    +.index AutoRedraw
    +.index ScaleWidth
    +.index ScaleHeight
    +
    +.h1 Drawing on Forms and PictureBoxes
    +
    +Forms and PictureBoxes have Visual Basic's drawing methods. They draw with the same code as the QBasic graphics statements (see Console and Graphics in the language reference), in pixels, with colours as RGB values (RGB(255, 0, 0) or &HFF0000 is red). A PictureBox draws on its picture; a form gets a drawing area the size of its window the first time it draws, placed with its controls like a PictureBox (so draw on a form that has no controls, or use a PictureBox beside them).
    +
    +Drawings stay until they are drawn over or cleared: AutoRedraw is accepted, but a drawing always behaves as with AutoRedraw = True. In a form's own code, write Me.Line, Me.Circle and so on: Line, Circle and PSet without an object are the QBasic graphics statements (they draw on the program's screen), and Print without an object is the PRINT statement.
    +
    +.h2 Drawing Methods
    +
    +.table
    +  Method                                                          Description
    +  ------                                                          -----------
    +  Cls                                                             Clears the drawing to BackColor; CurrentX and CurrentY go to 0.
    +  PSet [Step](x, y) [, color]                                     Draws a point (DrawWidth wide).
    +  Line [[Step](x1, y1)]-[Step](x2, y2) [, color] [, B | BF]         Draws a line, a box (B) or a filled box (BF). A box without F is filled with FillColor when FillStyle is 0.
    +  Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]]  Draws a circle, ellipse or arc, filled with FillColor when FillStyle is 0 (full circles).
    +  Point(x, y)                                                     The colour of a pixel, -1 outside the picture.
    +  Print items                                                     Writes text at CurrentX, CurrentY in ForeColor, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,.
    +  PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight]]   Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG or GIF), or a PictureBox given by its name as a string or by a Control variable.
    +  TextWidth(text)                                                 Width of the text in pixels.
    +  TextHeight(text)                                                Height of a line of text in pixels.
    +.endtable
    +
    +Without a colour, drawing uses ForeColor. Step makes a point relative to CurrentX, CurrentY, which every drawing method leaves at its last point. A colour or angle out of range stops the program with an error.
    +
    +.h2 Drawing Properties
    +
    +.table
    +  Property      Type      Default   Description
    +  --------      ----      -------   -----------
    +  ForeColor     Long      &H000000  Colour of drawing and printing.
    +  BackColor     Long      &HFFFFFF  Colour Cls clears to. Setting it clears the drawing to the new colour.
    +  CurrentX      Single    0         Where the next drawing or printing starts.
    +  CurrentY      Single    0
    +  DrawWidth     Integer   1         Pen width in pixels.
    +  FillStyle     Integer   1         0 fills boxes and circles with FillColor; 1 (the default) leaves them empty. Other VB fill patterns draw as 1.
    +  FillColor     Long      &H000000  Colour boxes and circles are filled with.
    +  AutoRedraw    Boolean   False     Accepted; drawings always persist.
    +  ScaleWidth    Long      (size)    Width of the drawing in pixels (read-only).
    +  ScaleHeight   Long      (size)    Height of the drawing in pixels (read-only).
    +.endtable
    +
    +.h2 Example
    +
    +.code
    +Sub Command1_Click ()
    +    Picture1.Resize 200, 150
    +    Picture1.BackColor = RGB(255, 255, 255)
    +    Picture1.DrawWidth = 2
    +    Picture1.Line (10, 10)-(190, 140), RGB(0, 0, 255), B
    +    Picture1.FillStyle = 0
    +    Picture1.FillColor = RGB(255, 255, 0)
    +    Picture1.Circle (100, 75), 40, RGB(255, 0, 0)
    +    Picture1.CurrentX = 70
    +    Picture1.CurrentY = 120
    +    Picture1.Print "Hello!"
    +End Sub
    +.endcode
    +
    +.link ctrl.picturebox PictureBox
    +.link ctrl.form Form
    +
    +
     .topic ctrl.databinding
     .title Data Binding
     .toc 1 Data Binding
    diff --git a/src/apps/kpunch/dvxbasic/form.dhs b/src/apps/kpunch/dvxbasic/form.dhs
    index e673fabd..52f383ce 100644
    --- a/src/apps/kpunch/dvxbasic/form.dhs
    +++ b/src/apps/kpunch/dvxbasic/form.dhs
    @@ -70,8 +70,12 @@ The Form is the top-level container representing a DVX window. It is declared in
       Resize        (none)                  Fires when the window is resized by the user.
       Activate      (none)                  Fires when the window gains focus.
       Deactivate    (none)                  Fires when the window loses focus.
    +  DragDrop      Source As Control, X As Integer, Y As Integer                     Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form.
    +  DragOver      Source As Control, X As Integer, Y As Integer, State As Integer   Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave or vbOver.
     .endtable
     
    +See Drag and Drop under Common Properties, Events, and Methods.
    +
     .h2 Form Methods
     
     .table
    @@ -84,6 +88,10 @@ The Form is the top-level container representing a DVX window. It is declared in
       FormName.Hide       Hide the form without unloading it.
     .endtable
     
    +.h2 Drawing
    +
    +Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.
    +
     .h2 Example
     
     .code
    @@ -104,4 +112,5 @@ End Sub
     .endcode
     
     .link ctrl.common.props Common Properties, Events, and Methods
    +.link ctrl.drawing Drawing on Forms and PictureBoxes
     .link ctrl.frm FRM File Format
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formGfx.c b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
    new file mode 100644
    index 00000000..78c7c672
    --- /dev/null
    +++ b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
    @@ -0,0 +1,1021 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// formGfx.c -- DVX BASIC form runtime: the program screen's window and the
    +// Form / PictureBox drawing methods
    +//
    +// Program screen: the VM's screen (runtime/screen.c) is shown in a window
    +// of its own at the mode's size (a whole multiple when the desktop has
    +// room).  Each present converts the dirty rectangle of the 8-bit
    +// framebuffer through a colour table (screen colour -> desktop pixel; the
    +// nearest desktop colour on a 256-colour desktop) into the window's
    +// content buffer.  FULLSCREEN / Alt+Enter give the window the whole screen
    +// through dvxExclusiveBegin; then the frame goes out with
    +// dvxExclusivePresent8 and the palette with dvxExclusiveSetPalette.  When
    +// another window takes the focus DVX suspends it (the window shows the
    +// frame again) and resumes it when the focus comes back.  Keys typed in the
    +// window go to INKEY$ / INPUT; closing it ends the program.
    +//
    +// Drawing methods: a PictureBox draws on its canvas, a form on a canvas
    +// added to it on the first drawing call.  Both run the same drawing code
    +// as the program screen (runtime/gfx.c) over a canvas surface with RGB
    +// colours (VB's ForeColor, BackColor, FillColor ...).  The canvas keeps
    +// its pixels, so drawings always persist (AutoRedraw is accepted and
    +// always behaves as True).
    +
    +#include "formrt.h"
    +#include "../compiler/opcodes.h"
    +#include "../runtime/basErrors.h"
    +#include "../runtime/gfx.h"
    +#include "../runtime/screen.h"
    +#include "canvas/canvas.h"
    +#include "dvxApp.h"
    +#include "dvxVideo.h"
    +#include "dvxWm.h"
    +#include "thirdparty/stb_ds_wrap.h"
    +
    +#include 
    +#include 
    +#include 
    +
    +#define SCREEN_SCALE_MAX        3       // largest whole-number window scale
    +#define SCREEN_FULL_BPP         8       // full screen: the screen's own palette in the DAC
    +#define SCREEN_SCAN_ALT_KP_ENTER 0xA6   // BIOS scan code of Alt with the keypad Enter
    +#define SCREEN_KEY_BYTE         0xFF
    +#define SCREEN_KEY_CR           13
    +#define SCREEN_TITLE_DEFAULT    "BASIC Program"
    +
    +#define DRAW_CANVAS_WGT         "canvas"
    +#define DRAW_PROP_IMAGE_W       "ImageWidth"
    +#define DRAW_PROP_IMAGE_H       "ImageHeight"
    +#define DRAW_CANVAS_BORDER      2       // the canvas widget's sunken frame (each side)
    +#define DRAW_DEFAULT_FORE       0x000000
    +#define DRAW_DEFAULT_BACK       0xFFFFFF
    +#define DRAW_RED_SHIFT          16
    +#define DRAW_GREEN_SHIFT        8
    +#define DRAW_BYTE_MASK          0xFF
    +#define DRAW_ASPECT             1.0     // forms and PictureBoxes have square pixels
    +#define DRAW_PAINT_MIN_ARGS     3       // PaintPicture picture, x, y
    +#define DRAW_PAINT_SIZE_ARGS    5       // ... , width, height
    +#define DRAW_PAINT_SRC_ARGS     9       // ... , srcX, srcY, srcWidth, srcHeight
    +
    +// A canvas integer property getter (ImageWidth / ImageHeight)
    +typedef int32_t (*DrawIntGetterT)(const WidgetT *w);
    +
    +// The program screen's window
    +struct BasScreenWinT {
    +    WindowT *win;
    +    int32_t  scale;
    +    int32_t  lutBits;                                        // desktop bits per pixel the table was made for
    +    uint32_t lut[BAS_SCREEN_PAL_ENTRIES];                    // screen colour -> desktop pixel
    +    uint8_t  deskPal[BAS_SCREEN_PAL_ENTRIES * BAS_SCREEN_PAL_CHANNELS];  // 256-colour desktop palette the table was made for
    +};
    +
    +// One Form's or PictureBox's drawing state
    +struct BasDrawT {
    +    BasGfxT      gfx;
    +    WidgetT     *canvas;       // the PictureBox, or the form's drawing area (NULL until a form draws)
    +    AppContextT *ctx;
    +    bool         autoRedraw;   // accepted; drawings always persist
    +    bool         cacheValid;   // colour -> pixel cache
    +    uint32_t     cacheColor;
    +    uint32_t     cachePixel;
    +};
    +
    +// Drawing members (the IDE accepts them on forms and PictureBoxes)
    +typedef enum {
    +    DRAW_M_CIRCLE,
    +    DRAW_M_CLS,
    +    DRAW_M_LINE,
    +    DRAW_M_PAINTPICTURE,
    +    DRAW_M_POINT,
    +    DRAW_M_PRINT,
    +    DRAW_M_PSET,
    +    DRAW_M_TEXTHEIGHT,
    +    DRAW_M_TEXTWIDTH,
    +    DRAW_M_COUNT
    +} DrawMethodE;
    +
    +typedef enum {
    +    DRAW_P_AUTOREDRAW,
    +    DRAW_P_BACKCOLOR,
    +    DRAW_P_CURRENTX,
    +    DRAW_P_CURRENTY,
    +    DRAW_P_DRAWWIDTH,
    +    DRAW_P_FILLCOLOR,
    +    DRAW_P_FILLSTYLE,
    +    DRAW_P_FORECOLOR,
    +    DRAW_P_SCALEHEIGHT,
    +    DRAW_P_SCALEWIDTH,
    +    DRAW_P_COUNT
    +} DrawPropE;
    +
    +static const char *const sDrawMethods[DRAW_M_COUNT] = {
    +    [DRAW_M_CIRCLE]       = "Circle",
    +    [DRAW_M_CLS]          = "Cls",
    +    [DRAW_M_LINE]         = "Line",
    +    [DRAW_M_PAINTPICTURE] = "PaintPicture",
    +    [DRAW_M_POINT]        = "Point",
    +    [DRAW_M_PRINT]        = "Print",
    +    [DRAW_M_PSET]         = "PSet",
    +    [DRAW_M_TEXTHEIGHT]   = "TextHeight",
    +    [DRAW_M_TEXTWIDTH]    = "TextWidth"
    +};
    +
    +static const char *const sDrawProps[DRAW_P_COUNT] = {
    +    [DRAW_P_AUTOREDRAW]  = "AutoRedraw",
    +    [DRAW_P_BACKCOLOR]   = BAS_PROP_BACKCOLOR,
    +    [DRAW_P_CURRENTX]    = "CurrentX",
    +    [DRAW_P_CURRENTY]    = "CurrentY",
    +    [DRAW_P_DRAWWIDTH]   = "DrawWidth",
    +    [DRAW_P_FILLCOLOR]   = "FillColor",
    +    [DRAW_P_FILLSTYLE]   = "FillStyle",
    +    [DRAW_P_FORECOLOR]   = BAS_PROP_FORECOLOR,
    +    [DRAW_P_SCALEHEIGHT] = "ScaleHeight",
    +    [DRAW_P_SCALEWIDTH]  = "ScaleWidth"
    +};
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static bool      canvasSize(WidgetT *canvas, int32_t *w, int32_t *h);
    +static int32_t   drawFind(const char *const *names, int32_t count, const char *name);
    +static BasDrawT *drawFor(BasFormRtT *rt, BasControlT *ctrl, bool needCanvas);
    +static bool      drawIsSurface(const BasControlT *ctrl);
    +static void      drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc);
    +static void      drawPrint(BasDrawT *d, BasValueT value, int32_t mode);
    +static void      drawSync(BasDrawT *d);
    +static uint32_t  readPixel(const uint8_t *src, int32_t bytesPerPixel);
    +static void      screenExpandRow(const BasScreenWinT *sw, uint8_t *dst, const uint8_t *src, int32_t count, int32_t bytesPerPixel);
    +static void      screenFullChanged(WindowT *win, ExclusiveStateE state, void *userData);
    +static void      screenFullScreen(void *ctx, BasScreenT *scr, bool on);
    +static bool      screenLutSync(BasFormRtT *rt, const BasScreenT *scr, bool force);
    +static void      screenModeChanged(void *ctx, BasScreenT *scr);
    +static void      screenOnClose(WindowT *win);
    +static void      screenOnKey(WindowT *win, int32_t key, int32_t mod);
    +static void      screenOnPaint(WindowT *win, RectT *dirty);
    +static void      screenPresent(void *ctx, BasScreenT *scr);
    +static void      screenPresentRect(BasFormRtT *rt, const BasScreenT *scr, int32_t x, int32_t y, int32_t w, int32_t h);
    +static uint32_t  surfGet(const void *ctx, int32_t x, int32_t y);
    +static void      surfSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel);
    +static uint32_t  surfToColor(const void *ctx, uint32_t pixel);
    +static uint32_t  surfToPixel(void *ctx, uint32_t color);
    +
    +
    +void basFormRtDrawFree(BasControlT *ctrl) {
    +    free(ctrl->draw);
    +    ctrl->draw = NULL;
    +}
    +
    +
    +bool basFormRtDrawGetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, BasValueT *out) {
    +    int32_t prop = drawIsSurface(ctrl) ? drawFind(sDrawProps, DRAW_P_COUNT, name) : -1;
    +
    +    if (prop < 0) {
    +        return false;
    +    }
    +
    +    BasDrawT *d = drawFor(rt, ctrl, false);
    +
    +    if (!d) {
    +        *out = basValInteger(0);
    +        return true;
    +    }
    +
    +    switch ((DrawPropE)prop) {
    +        case DRAW_P_AUTOREDRAW:
    +            *out = basValBool(d->autoRedraw);
    +            break;
    +
    +        case DRAW_P_BACKCOLOR:
    +            *out = basValLong((int32_t)d->gfx.back);
    +            break;
    +
    +        case DRAW_P_CURRENTX:
    +            *out = basValDouble(d->gfx.lastX);
    +            break;
    +
    +        case DRAW_P_CURRENTY:
    +            *out = basValDouble(d->gfx.lastY);
    +            break;
    +
    +        case DRAW_P_DRAWWIDTH:
    +            *out = basValInteger((int16_t)d->gfx.drawWidth);
    +            break;
    +
    +        case DRAW_P_FILLCOLOR:
    +            *out = basValLong((int32_t)d->gfx.fillColor);
    +            break;
    +
    +        case DRAW_P_FILLSTYLE:
    +            *out = basValInteger((int16_t)d->gfx.fillStyle);
    +            break;
    +
    +        case DRAW_P_FORECOLOR:
    +            *out = basValLong((int32_t)d->gfx.fore);
    +            break;
    +
    +        case DRAW_P_SCALEHEIGHT:
    +            drawSync(d);
    +            *out = basValLong(d->gfx.surf.height);
    +            break;
    +
    +        case DRAW_P_SCALEWIDTH:
    +            drawSync(d);
    +            *out = basValLong(d->gfx.surf.width);
    +            break;
    +
    +        default:
    +            return false;
    +    }
    +
    +    return true;
    +}
    +
    +
    +bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, BasValueT *args, int32_t argc, BasValueT *result) {
    +    int32_t method = drawIsSurface(ctrl) ? drawFind(sDrawMethods, DRAW_M_COUNT, name) : -1;
    +
    +    if (method < 0) {
    +        return false;
    +    }
    +
    +    *result = basValInteger(0);
    +
    +    // TextWidth / TextHeight need no drawing area
    +    if (method == DRAW_M_TEXTWIDTH || method == DRAW_M_TEXTHEIGHT) {
    +        BasStringT *s = argc >= 1 ? basValFormatString(args[0]) : basStringNew("", 0);
    +
    +        *result = basValLong(method == DRAW_M_TEXTWIDTH ? s->len * rt->ctx->font.charWidth : rt->ctx->font.charHeight);
    +        basStringUnref(s);
    +        return true;
    +    }
    +
    +    BasDrawT *d = drawFor(rt, ctrl, true);
    +
    +    if (!d || !d->canvas) {
    +        basFormRtRuntimeError(rt, "No drawing area", "Method: %s\nThe form has no room for a drawing area.", name);
    +        return true;
    +    }
    +
    +    drawSync(d);
    +
    +    int32_t err = 0;
    +
    +    switch ((DrawMethodE)method) {
    +        case DRAW_M_CLS:
    +            err          = basGfxClear(&d->gfx, basValLong((int32_t)d->gfx.back));
    +            d->gfx.lastX = 0.0;
    +            d->gfx.lastY = 0.0;
    +            break;
    +
    +        case DRAW_M_PSET:
    +            err = argc == BAS_GFX_PSET_ARGS ? basGfxPset(&d->gfx, args) : BAS_ERR_ILLEGAL_FUNC_CALL;
    +            break;
    +
    +        case DRAW_M_LINE:
    +            err = argc == BAS_GFX_LINE_ARGS ? basGfxLine(&d->gfx, args) : BAS_ERR_ILLEGAL_FUNC_CALL;
    +            break;
    +
    +        case DRAW_M_CIRCLE:
    +            err = argc == BAS_GFX_CIRCLE_ARGS ? basGfxCircle(&d->gfx, args) : BAS_ERR_ILLEGAL_FUNC_CALL;
    +            break;
    +
    +        case DRAW_M_POINT:
    +            err = argc == 2 ? basGfxPoint(&d->gfx, args[0], args[1], result) : BAS_ERR_ILLEGAL_FUNC_CALL;
    +            break;
    +
    +        case DRAW_M_PRINT:
    +            if (argc == BAS_OBJ_PRINT_ARGS) {
    +                drawPrint(d, args[0], basValToInt32(args[1]));
    +            } else {
    +                err = BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            break;
    +
    +        case DRAW_M_PAINTPICTURE:
    +            drawPaintPicture(rt, ctrl, d, args, argc);
    +            break;
    +
    +        default:
    +            break;
    +    }
    +
    +    if (err != 0) {
    +        basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nAn argument is out of range (a colour, a radius or an angle).", name);
    +    }
    +
    +    return true;
    +}
    +
    +
    +bool basFormRtDrawSetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, BasValueT value) {
    +    int32_t prop = drawIsSurface(ctrl) ? drawFind(sDrawProps, DRAW_P_COUNT, name) : -1;
    +
    +    if (prop < 0 || prop == DRAW_P_SCALEHEIGHT || prop == DRAW_P_SCALEWIDTH) {
    +        return false;
    +    }
    +
    +    // Setting BackColor clears the drawing to it (VB), so it needs the
    +    // drawing area; the others are only remembered.
    +    BasDrawT *d = drawFor(rt, ctrl, prop == DRAW_P_BACKCOLOR);
    +
    +    if (!d) {
    +        return true;
    +    }
    +
    +    switch ((DrawPropE)prop) {
    +        case DRAW_P_AUTOREDRAW:
    +            d->autoRedraw = basValIsTruthy(value);
    +            break;
    +
    +        case DRAW_P_BACKCOLOR:
    +            d->gfx.back = (uint32_t)basValToInt32(value);
    +
    +            if (d->canvas) {
    +                drawSync(d);
    +                basGfxClear(&d->gfx, value);
    +            }
    +
    +            break;
    +
    +        case DRAW_P_CURRENTX:
    +            d->gfx.lastX = basValToNumber(value);
    +            break;
    +
    +        case DRAW_P_CURRENTY:
    +            d->gfx.lastY = basValToNumber(value);
    +            break;
    +
    +        case DRAW_P_DRAWWIDTH: {
    +            int32_t w = basValToInt32(value);
    +
    +            d->gfx.drawWidth = w < 1 ? 1 : w;
    +            break;
    +        }
    +
    +        case DRAW_P_FILLCOLOR:
    +            d->gfx.fillColor = (uint32_t)basValToInt32(value);
    +            break;
    +
    +        case DRAW_P_FILLSTYLE:
    +            d->gfx.fillStyle = basValToInt32(value);
    +            break;
    +
    +        case DRAW_P_FORECOLOR:
    +            d->gfx.fore = (uint32_t)basValToInt32(value);
    +            break;
    +
    +        default:
    +            return false;
    +    }
    +
    +    return true;
    +}
    +
    +
    +bool basFormRtIsDrawMember(const char *name, bool isProp) {
    +    return isProp ? drawFind(sDrawProps, DRAW_P_COUNT, name) >= 0 : drawFind(sDrawMethods, DRAW_M_COUNT, name) >= 0;
    +}
    +
    +
    +bool basFormRtKeepsRunning(const BasFormRtT *rt) {
    +    return !rt->terminated && (arrlen(rt->forms) > 0 || basFormRtScreenWindow(rt) != NULL);
    +}
    +
    +
    +void basFormRtScreenBind(BasFormRtT *rt) {
    +    BasScreenHostT host;
    +
    +    host.modeChanged = screenModeChanged;
    +    host.present     = screenPresent;
    +    host.fullScreen  = screenFullScreen;
    +    host.ctx         = rt;
    +    basVmSetScreenHost(rt->vm, &host);
    +
    +    // Screen text uses the DVX font when it is the 8x16 one
    +    if (rt->ctx->font.charWidth == BAS_GFX_GLYPH_W && rt->ctx->font.charHeight == BAS_GFX_GLYPH_TALL) {
    +        basGfxSetFont(rt->ctx->font.glyphData);
    +    }
    +}
    +
    +
    +void basFormRtScreenClose(BasFormRtT *rt) {
    +    BasScreenWinT *sw = rt->screenWin;
    +
    +    if (!sw) {
    +        return;
    +    }
    +
    +    if (sw->win) {
    +        dvxDestroyWindow(rt->ctx, sw->win);
    +    }
    +
    +    free(sw);
    +    rt->screenWin = NULL;
    +}
    +
    +
    +WindowT *basFormRtScreenWindow(const BasFormRtT *rt) {
    +    return rt->screenWin ? rt->screenWin->win : NULL;
    +}
    +
    +
    +// The drawing buffer size of a canvas (its ImageWidth / ImageHeight).
    +static bool canvasSize(WidgetT *canvas, int32_t *w, int32_t *h) {
    +    const WgtIfaceT *iface = wgtGetIface(DRAW_CANVAS_WGT);
    +
    +    *w = 0;
    +    *h = 0;
    +
    +    for (int32_t i = 0; iface && i < iface->propCount; i++) {
    +        const WgtPropDescT *p   = &iface->props[i];
    +        DrawIntGetterT      get = (DrawIntGetterT)p->getFn;
    +
    +        if (strcmp(p->name, DRAW_PROP_IMAGE_W) == 0) {
    +            *w = get(canvas);
    +        } else if (strcmp(p->name, DRAW_PROP_IMAGE_H) == 0) {
    +            *h = get(canvas);
    +        }
    +    }
    +
    +    return *w > 0 && *h > 0;
    +}
    +
    +
    +static int32_t drawFind(const char *const *names, int32_t count, const char *name) {
    +    for (int32_t i = 0; name && i < count; i++) {
    +        if (strcasecmp(names[i], name) == 0) {
    +            return i;
    +        }
    +    }
    +
    +    return -1;
    +}
    +
    +
    +// The control's drawing state, made on first use; with needCanvas a form
    +// also gets its drawing area (a canvas the size of its content, added to
    +// its controls).
    +static BasDrawT *drawFor(BasFormRtT *rt, BasControlT *ctrl, bool needCanvas) {
    +    BasDrawT *d = ctrl->draw;
    +
    +    if (!d) {
    +        BasGfxSurfaceT surf;
    +
    +        d = (BasDrawT *)calloc(1, sizeof(BasDrawT));
    +
    +        if (!d) {
    +            return NULL;
    +        }
    +
    +        memset(&surf, 0, sizeof(surf));
    +        surf.ctx     = d;
    +        surf.span    = surfSpan;
    +        surf.get     = surfGet;
    +        surf.toPixel = surfToPixel;
    +        surf.toColor = surfToColor;
    +        d->ctx       = rt->ctx;
    +        basGfxInit(&d->gfx, &surf, DRAW_DEFAULT_FORE, DRAW_DEFAULT_BACK, DRAW_ASPECT);
    +        bool isForm = ctrl->form && ctrl == &ctrl->form->formCtrl;
    +
    +        d->gfx.lastX = 0.0;
    +        d->gfx.lastY = 0.0;
    +        d->canvas    = isForm ? NULL : ctrl->widget;
    +        ctrl->draw   = d;
    +    }
    +
    +    if (needCanvas && !d->canvas && ctrl->form && ctrl->form->contentBox) {
    +        BasFormT *form = ctrl->form;
    +        int32_t   w    = form->contentBox->w > 0 ? form->contentBox->w : (form->window ? form->window->contentW : form->frmWidth);
    +        int32_t   h    = form->contentBox->h > 0 ? form->contentBox->h : (form->window ? form->window->contentH : form->frmHeight);
    +
    +        d->canvas = wgtCanvas(form->contentBox, w - 2 * DRAW_CANVAS_BORDER, h - 2 * DRAW_CANVAS_BORDER);
    +
    +        if (d->canvas) {
    +            wgtInvalidate(d->canvas);
    +        }
    +    }
    +
    +    return d;
    +}
    +
    +
    +// Forms and PictureBoxes take the drawing members.
    +static bool drawIsSurface(const BasControlT *ctrl) {
    +    if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
    +        return true;
    +    }
    +
    +    return ctrl->iface && ctrl->iface->basName && strcasecmp(ctrl->iface->basName, BAS_PICTUREBOX_TYPE) == 0;
    +}
    +
    +
    +// PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth,
    +// srcHeight]]: the source rectangle is stretched to the destination
    +// (nearest pixel).
    +static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc) {
    +    uint8_t  *pixels = NULL;
    +    WidgetT  *src    = NULL;
    +    int32_t   srcW   = 0;
    +    int32_t   srcH   = 0;
    +    int32_t   pitch  = 0;
    +    int32_t   bpp    = rt->ctx->display.format.bytesPerPixel;
    +
    +    if (argc < DRAW_PAINT_MIN_ARGS) {
    +        basFormRtRuntimeError(rt, "PaintPicture needs a picture, x and y", "Usage: PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight]]");
    +        return;
    +    }
    +
    +    // The picture: a PictureBox (a Control variable, or its name), else an
    +    // image file.
    +    BasStringT  *name = args[0].type == BAS_TYPE_OBJECT ? NULL : basValFormatString(args[0]);
    +    void        *ref  = name ? basFormRtFindCtrl(rt, ctrl->form, name->data) : args[0].objVal;
    +    BasControlT *pic  = basFormRtResolveCtrl(rt, ref);
    +
    +    src = pic && drawIsSurface(pic) && pic != &pic->form->formCtrl ? pic->widget : NULL;
    +
    +    if (!src && name) {
    +        pixels = dvxLoadImage(rt->ctx, name->data, &srcW, &srcH, &pitch);
    +    }
    +
    +    if (name) {
    +        basStringUnref(name);
    +    }
    +
    +    if (!pixels && (!src || !canvasSize(src, &srcW, &srcH))) {
    +        basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it) or a BMP, PNG, JPEG or GIF image file.");
    +        return;
    +    }
    +
    +    int32_t dx = basValToInt32(args[1]);
    +    int32_t dy = basValToInt32(args[2]);
    +    int32_t sx = argc >= DRAW_PAINT_SRC_ARGS ? basValToInt32(args[5]) : 0;
    +    int32_t sy = argc >= DRAW_PAINT_SRC_ARGS ? basValToInt32(args[6]) : 0;
    +    int32_t sw = argc >= DRAW_PAINT_SRC_ARGS ? basValToInt32(args[7]) : srcW - sx;
    +    int32_t sh = argc >= DRAW_PAINT_SRC_ARGS ? basValToInt32(args[8]) : srcH - sy;
    +    int32_t dw = argc >= DRAW_PAINT_SIZE_ARGS ? basValToInt32(args[3]) : sw;
    +    int32_t dh = argc >= DRAW_PAINT_SIZE_ARGS ? basValToInt32(args[4]) : sh;
    +
    +    for (int32_t y = 0; y < dh && sw > 0 && sh > 0; y++) {
    +        int32_t py = dy + y;
    +        int32_t ry = sy + (int32_t)((int64_t)y * sh / dh);
    +
    +        if (py < 0 || py >= d->gfx.surf.height || ry < 0 || ry >= srcH) {
    +            continue;
    +        }
    +
    +        for (int32_t x = 0; x < dw; x++) {
    +            int32_t px = dx + x;
    +            int32_t rx = sx + (int32_t)((int64_t)x * sw / dw);
    +
    +            if (px < 0 || px >= d->gfx.surf.width || rx < 0 || rx >= srcW) {
    +                continue;
    +            }
    +
    +            uint32_t pixel = pixels ? readPixel(pixels + ry * pitch + rx * bpp, bpp) : wgtCanvasGetPixel(src, rx, ry);
    +
    +            surfSpan(d, px, px, py, pixel);
    +        }
    +    }
    +
    +    dvxFreeImage(pixels);
    +}
    +
    +
    +// One object Print item at CurrentX / CurrentY in ForeColor (the DVX
    +// font, transparent background).
    +static void drawPrint(BasDrawT *d, BasValueT value, int32_t mode) {
    +    int32_t charW  = d->ctx->font.charWidth;
    +    int32_t charH  = d->ctx->font.charHeight;
    +    int32_t column = (int32_t)(d->gfx.lastX / charW);
    +
    +    switch (mode) {
    +        case BAS_OBJ_PRINT_NEWLINE:
    +            d->gfx.lastX  = 0.0;
    +            d->gfx.lastY += charH;
    +            return;
    +
    +        case BAS_OBJ_PRINT_ZONE:
    +            d->gfx.lastX = (double)(column / BAS_PRINT_ZONE_WIDTH + 1) * BAS_PRINT_ZONE_WIDTH * charW;
    +            return;
    +
    +        case BAS_OBJ_PRINT_TAB: {
    +            int32_t target = basValToInt32(value) - 1;
    +
    +            if (target < column) {
    +                d->gfx.lastY += charH;
    +            }
    +
    +            d->gfx.lastX = (double)(target < 0 ? 0 : target) * charW;
    +            return;
    +        }
    +
    +        case BAS_OBJ_PRINT_SPC:
    +            d->gfx.lastX += (double)basValToInt32(value) * charW;
    +            return;
    +
    +        default:
    +            break;
    +    }
    +
    +    BasStringT *s = basValPrintString(value);
    +
    +    wgtCanvasSetPenColor(d->canvas, basGfxPixel(&d->gfx, d->gfx.fore));
    +    wgtCanvasDrawText(d->canvas, (int32_t)d->gfx.lastX, (int32_t)d->gfx.lastY, s->data);
    +    d->gfx.lastX += (double)s->len * charW;
    +    basStringUnref(s);
    +}
    +
    +
    +// Follow a PictureBox's current size (Resize and Load change it) and the
    +// desktop's pixel format.
    +static void drawSync(BasDrawT *d) {
    +    int32_t w = 0;
    +    int32_t h = 0;
    +
    +    // The desktop's pixel format may have changed since the last call
    +    d->cacheValid = false;
    +
    +    if (!d->canvas || !canvasSize(d->canvas, &w, &h) || (w == d->gfx.surf.width && h == d->gfx.surf.height)) {
    +        return;
    +    }
    +
    +    d->gfx.surf.width  = w;
    +    d->gfx.surf.height = h;
    +    d->gfx.viewX0      = 0;
    +    d->gfx.viewY0      = 0;
    +    d->gfx.viewX1      = w - 1;
    +    d->gfx.viewY1      = h - 1;
    +}
    +
    +
    +// A pixel of an image in the display format.
    +static uint32_t readPixel(const uint8_t *src, int32_t bytesPerPixel) {
    +    switch (bytesPerPixel) {
    +        case 1:
    +            return *src;
    +
    +        case 2:
    +            return *(const uint16_t *)src;
    +
    +        default:
    +            return *(const uint32_t *)src;
    +    }
    +}
    +
    +
    +// One framebuffer row to desktop pixels, each repeated scale times.
    +static void screenExpandRow(const BasScreenWinT *sw, uint8_t *dst, const uint8_t *src, int32_t count, int32_t bytesPerPixel) {
    +    switch (bytesPerPixel) {
    +        case 1:
    +            for (int32_t i = 0; i < count; i++) {
    +                memset(dst, (uint8_t)sw->lut[src[i]], (size_t)sw->scale);
    +                dst += sw->scale;
    +            }
    +
    +            break;
    +
    +        case 2: {
    +            uint16_t *d = (uint16_t *)dst;
    +
    +            for (int32_t i = 0; i < count; i++) {
    +                uint16_t v = (uint16_t)sw->lut[src[i]];
    +
    +                for (int32_t s = 0; s < sw->scale; s++) {
    +                    *d++ = v;
    +                }
    +            }
    +
    +            break;
    +        }
    +
    +        default: {
    +            uint32_t *d = (uint32_t *)dst;
    +
    +            for (int32_t i = 0; i < count; i++) {
    +                uint32_t v = sw->lut[src[i]];
    +
    +                for (int32_t s = 0; s < sw->scale; s++) {
    +                    *d++ = v;
    +                }
    +            }
    +
    +            break;
    +        }
    +    }
    +}
    +
    +
    +// Full screen came, was suspended (another window has the focus: the
    +// window shows the screen again) or ended.
    +static void screenFullChanged(WindowT *win, ExclusiveStateE state, void *userData) {
    +    (void)win;
    +    BasFormRtT *rt  = (BasFormRtT *)userData;
    +    BasScreenT *scr = rt->vm->screen;
    +
    +    if (!scr) {
    +        return;
    +    }
    +
    +    if (state == ExclusiveEndedE) {
    +        scr->fullScreen = false;
    +    }
    +
    +    dvxLog("basrt: program screen full screen %s", state == ExclusiveActiveE ? "on" : state == ExclusiveSuspendedE ? "suspended" : "ended");
    +    basScreenInvalidate(scr);
    +    screenPresent(rt, scr);
    +}
    +
    +
    +// FULLSCREEN ON / OFF and Alt+Enter: the whole screen at the mode's size
    +// (or the nearest mode DVX has), or back to the window.
    +static void screenFullScreen(void *ctx, BasScreenT *scr, bool on) {
    +    BasFormRtT    *rt = (BasFormRtT *)ctx;
    +    BasScreenWinT *sw = rt->screenWin;
    +
    +    if (!sw || !sw->win || on == scr->fullScreen) {
    +        return;
    +    }
    +
    +    if (on) {
    +        scr->fullScreen = dvxExclusiveBegin(rt->ctx, sw->win, scr->width, scr->height, SCREEN_FULL_BPP, screenFullChanged, rt) == DVX_EXCL_OK;
    +        dvxLog("basrt: program screen full screen %s", scr->fullScreen ? "on" : "unavailable");
    +    } else {
    +        dvxExclusiveEnd(rt->ctx, sw->win);
    +        scr->fullScreen = false;
    +        dvxLog("basrt: program screen full screen off");
    +    }
    +
    +    basScreenInvalidate(scr);
    +    screenPresent(rt, scr);
    +}
    +
    +
    +// Rebuild the colour table when forced (a palette change) or when the
    +// desktop's pixel format or 256-colour palette changed.  True when it was.
    +static bool screenLutSync(BasFormRtT *rt, const BasScreenT *scr, bool force) {
    +    BasScreenWinT  *sw = rt->screenWin;
    +    const DisplayT *d  = dvxGetDisplay(rt->ctx);
    +
    +    if (!force && d->format.bitsPerPixel == sw->lutBits && (!d->palette || memcmp(sw->deskPal, d->palette, sizeof(sw->deskPal)) == 0)) {
    +        return false;
    +    }
    +
    +    sw->lutBits = d->format.bitsPerPixel;
    +
    +    if (d->palette) {
    +        memcpy(sw->deskPal, d->palette, sizeof(sw->deskPal));
    +    }
    +
    +    for (int32_t i = 0; i < BAS_SCREEN_PAL_ENTRIES; i++) {
    +        const uint8_t *c = &scr->palette[i * BAS_SCREEN_PAL_CHANNELS];
    +
    +        sw->lut[i] = packColor(d, c[0], c[1], c[2]);
    +    }
    +
    +    return true;
    +}
    +
    +
    +// The screen was made or changed mode: a window whose content area is the
    +// framebuffer at the largest whole scale that fits the desktop.
    +static void screenModeChanged(void *ctx, BasScreenT *scr) {
    +    BasFormRtT    *rt      = (BasFormRtT *)ctx;
    +    BasScreenWinT *sw      = rt->screenWin;
    +    bool           wasFull = scr->fullScreen;
    +
    +    if (!sw) {
    +        sw = (BasScreenWinT *)calloc(1, sizeof(BasScreenWinT));
    +
    +        if (!sw) {
    +            return;
    +        }
    +
    +        rt->screenWin = sw;
    +    }
    +
    +    // A new size needs a new full-screen mode
    +    if (wasFull) {
    +        screenFullScreen(rt, scr, false);
    +    }
    +
    +    int32_t frameW = 2 * CHROME_TOTAL_SIDE;
    +    int32_t frameH = CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM;
    +    int32_t scaleW = (rt->ctx->display.width - frameW) / scr->width;
    +    int32_t scaleH = (rt->ctx->display.height - frameH) / scr->height;
    +    int32_t scale  = scaleW < scaleH ? scaleW : scaleH;
    +
    +    sw->scale = scale < 1 ? 1 : (scale > SCREEN_SCALE_MAX ? SCREEN_SCALE_MAX : scale);
    +
    +    int32_t wantW = scr->width * sw->scale;
    +    int32_t wantH = scr->height * sw->scale;
    +
    +    if (!sw->win) {
    +        sw->win = dvxCreateWindowCentered(rt->ctx, rt->title[0] ? rt->title : SCREEN_TITLE_DEFAULT, wantW + frameW, wantH + frameH, false);
    +
    +        if (!sw->win) {
    +            return;
    +        }
    +
    +        sw->win->userData  = rt;
    +        sw->win->gameInput = true;
    +        sw->win->onKey     = screenOnKey;
    +        sw->win->onClose   = screenOnClose;
    +        sw->win->onPaint   = screenOnPaint;
    +    }
    +
    +    if (sw->win->contentW != wantW || sw->win->contentH != wantH) {
    +        dvxResizeWindow(rt->ctx, sw->win, sw->win->w + wantW - sw->win->contentW, sw->win->h + wantH - sw->win->contentH);
    +    }
    +
    +    screenLutSync(rt, scr, true);
    +    basScreenInvalidate(scr);
    +    screenPresent(rt, scr);
    +
    +    if (wasFull) {
    +        screenFullScreen(rt, scr, true);
    +    }
    +}
    +
    +
    +// Closing the program's screen ends the program.
    +static void screenOnClose(WindowT *win) {
    +    BasFormRtT *rt = (BasFormRtT *)win->userData;
    +
    +    basVmRequestStop(rt->vm);
    +    rt->terminated = true;
    +    dvxDestroyWindow(rt->ctx, win);
    +    rt->screenWin->win = NULL;
    +}
    +
    +
    +// Keys for INKEY$ and INPUT: characters as they are, other keys as
    +// BAS_SCREEN_KEY_EXT | scan code.  Alt+Enter toggles the full screen.
    +static void screenOnKey(WindowT *win, int32_t key, int32_t mod) {
    +    BasFormRtT *rt  = (BasFormRtT *)win->userData;
    +    BasScreenT *scr = rt->vm->screen;
    +
    +    if (!scr) {
    +        return;
    +    }
    +
    +    if ((mod & KEY_MOD_ALT) && (key == KEY_ENTER || key == (KEY_SCAN_ENTER | KEY_EXT_FLAG) || key == (SCREEN_SCAN_ALT_KP_ENTER | KEY_EXT_FLAG))) {
    +        screenFullScreen(rt, scr, !scr->fullScreen);
    +        return;
    +    }
    +
    +    if (key == (KEY_SCAN_KP_ENTER | KEY_EXT_FLAG)) {
    +        basScreenPushKey(scr, SCREEN_KEY_CR);
    +    } else if (key & KEY_EXT_FLAG) {
    +        basScreenPushKey(scr, BAS_SCREEN_KEY_EXT | (key & SCREEN_KEY_BYTE));
    +    } else {
    +        basScreenPushKey(scr, key & SCREEN_KEY_BYTE);
    +    }
    +}
    +
    +
    +// DVX asks for the whole content buffer (first show, video mode change).
    +static void screenOnPaint(WindowT *win, RectT *dirty) {
    +    (void)dirty;
    +    BasFormRtT *rt  = (BasFormRtT *)win->userData;
    +    BasScreenT *scr = rt->vm->screen;
    +
    +    if (scr) {
    +        screenLutSync(rt, scr, false);
    +        screenPresentRect(rt, scr, 0, 0, scr->width, scr->height);
    +    }
    +}
    +
    +
    +// Show what changed: the whole frame in full screen, else the dirty
    +// rectangle (all of it after a palette or desktop change) in the window.
    +static void screenPresent(void *ctx, BasScreenT *scr) {
    +    BasFormRtT    *rt = (BasFormRtT *)ctx;
    +    BasScreenWinT *sw = rt->screenWin;
    +
    +    if (!sw || !sw->win) {
    +        basScreenClean(scr);
    +        return;
    +    }
    +
    +    bool whole = screenLutSync(rt, scr, scr->paletteChanged);
    +
    +    if (scr->fullScreen) {
    +        if (scr->paletteChanged) {
    +            dvxExclusiveSetPalette(rt->ctx, scr->palette, 0, BAS_SCREEN_PAL_ENTRIES);
    +        }
    +
    +        if (dvxExclusivePresent8(rt->ctx, scr->fb, scr->width, scr->height, scr->width)) {
    +            basScreenClean(scr);
    +            return;
    +        }
    +    }
    +
    +    int32_t x0 = whole || !scr->dirty ? 0 : scr->dirtyX0;
    +    int32_t y0 = whole || !scr->dirty ? 0 : scr->dirtyY0;
    +    int32_t x1 = whole || !scr->dirty ? scr->width - 1 : scr->dirtyX1;
    +    int32_t y1 = whole || !scr->dirty ? scr->height - 1 : scr->dirtyY1;
    +
    +    screenPresentRect(rt, scr, x0, y0, x1 - x0 + 1, y1 - y0 + 1);
    +    dvxInvalidateRect(rt->ctx, sw->win, x0 * sw->scale, y0 * sw->scale, (x1 - x0 + 1) * sw->scale, (y1 - y0 + 1) * sw->scale);
    +    basScreenClean(scr);
    +}
    +
    +
    +// Framebuffer rectangle -> content buffer, scaled, clipped to the window.
    +static void screenPresentRect(BasFormRtT *rt, const BasScreenT *scr, int32_t x, int32_t y, int32_t w, int32_t h) {
    +    BasScreenWinT *sw  = rt->screenWin;
    +    WindowT       *win = sw->win;
    +
    +    if (!win || !win->contentBuf) {
    +        return;
    +    }
    +
    +    int32_t bpp   = dvxGetDisplay(rt->ctx)->format.bytesPerPixel;
    +    int32_t right = x + w < win->contentW / sw->scale ? x + w : win->contentW / sw->scale;
    +    int32_t lines = y + h < win->contentH / sw->scale ? y + h : win->contentH / sw->scale;
    +    int32_t count = right - x;
    +
    +    if (count <= 0) {
    +        return;
    +    }
    +
    +    size_t bytes = (size_t)(count * sw->scale * bpp);
    +
    +    for (int32_t row = y; row < lines; row++) {
    +        uint8_t *dst = win->contentBuf + row * sw->scale * win->contentPitch + x * sw->scale * bpp;
    +
    +        screenExpandRow(sw, dst, &scr->fb[row * scr->width + x], count, bpp);
    +
    +        for (int32_t s = 1; s < sw->scale; s++) {
    +            memcpy(dst + s * win->contentPitch, dst, bytes);
    +        }
    +    }
    +}
    +
    +
    +// Canvas surface: pixels in the display format, colours 0x00RRGGBB.
    +static uint32_t surfGet(const void *ctx, int32_t x, int32_t y) {
    +    const BasDrawT *d = (const BasDrawT *)ctx;
    +
    +    return wgtCanvasGetPixel(d->canvas, x, y);
    +}
    +
    +
    +static void surfSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel) {
    +    BasDrawT *d = (BasDrawT *)ctx;
    +
    +    if (x0 == x1) {
    +        wgtCanvasSetPixel(d->canvas, x0, y, pixel);
    +        return;
    +    }
    +
    +    wgtCanvasSetPenColor(d->canvas, pixel);
    +    wgtCanvasFillRect(d->canvas, x0, y, x1 - x0 + 1, 1);
    +}
    +
    +
    +static uint32_t surfToColor(const void *ctx, uint32_t pixel) {
    +    const BasDrawT *d = (const BasDrawT *)ctx;
    +    uint8_t         r = 0;
    +    uint8_t         g = 0;
    +    uint8_t         b = 0;
    +
    +    unpackColor(&d->ctx->display, pixel, &r, &g, &b);
    +    return ((uint32_t)r << DRAW_RED_SHIFT) | ((uint32_t)g << DRAW_GREEN_SHIFT) | b;
    +}
    +
    +
    +static uint32_t surfToPixel(void *ctx, uint32_t color) {
    +    BasDrawT *d = (BasDrawT *)ctx;
    +
    +    if (!d->cacheValid || d->cacheColor != color) {
    +        d->cacheColor = color;
    +        d->cachePixel = packColor(&d->ctx->display, (color >> DRAW_RED_SHIFT) & DRAW_BYTE_MASK, (color >> DRAW_GREEN_SHIFT) & DRAW_BYTE_MASK, color & DRAW_BYTE_MASK);
    +        d->cacheValid = true;
    +    }
    +
    +    return d->cachePixel;
    +}
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c
    index 495756c0..1781cd76 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c
    @@ -37,7 +37,9 @@
     #include "../../../../libs/kpunch/music/music.h"
     #include "../../../../libs/kpunch/print/print.h"
     #include "dvxDlg.h"
    +#include "dvxPlat.h"
     #include "dvxRes.h"
    +#include "dvxWgtP.h"
     #include "dvxWm.h"
     #include "shellApp.h"
     // Widget headers below publish inline accessor macros over
    @@ -95,6 +97,28 @@ DVX_ABI_STAMP(basrt);
     // clamped to [0, BAS_CTRL_MAX_DIM] before they become size hints.
     #define BAS_CTRL_MAX_DIM     32767
     
    +// DlgColor: the RGB() bits of a color, and the result when cancelled.
    +#define BAS_RGB_MASK             0xFFFFFF
    +#define BAS_DLG_COLOR_CANCELLED  (-1)
    +
    +// The Clipboard object's name and methods.
    +#define BAS_CLIPBOARD_NAME       "Clipboard"
    +#define BAS_FORM_TYPE_NAME       "Form"      // TypeName / TypeOf of a form
    +#define BAS_CLIP_CLEAR           "Clear"
    +#define BAS_CLIP_GETFORMAT       "GetFormat"
    +#define BAS_CLIP_GETTEXT         "GetText"
    +#define BAS_CLIP_SETTEXT         "SetText"
    +#define BAS_CLIP_SETTEXT_FORMAT  1     // SetText's optional format argument
    +
    +// The button number widget mouse callbacks give the left button.
    +#define BAS_MOUSE_BUTTON_LEFT    1
    +
    +// DragDrop's arguments (Source, X, Y); DragOver adds State.  dragFire
    +// takes BAS_DRAG_STATE_NONE for DragDrop.
    +#define BAS_DRAG_DROP_ARGS       3
    +#define BAS_DRAG_OVER_ARGS       4
    +#define BAS_DRAG_STATE_NONE      (-1)
    +
     // ResGetText result cap (static return buffer).
     #define BAS_RES_TEXT_LEN     1024
     
    @@ -117,6 +141,32 @@ static bool sLoadingFrm = false;
     // on an empty field.
     static bool sLastInputBoxCancelled = false;
     
    +// The Clipboard object: basFormRtFindCtrl hands out this address for the
    +// name Clipboard when no control or form has it; only its address is used.
    +static const uint8_t sClipboardObj = 0;
    +
    +// Drag and drop in progress.  There is one mouse, so one drag for every
    +// program: the runtime, app and control it comes from (rt NULL: no drag),
    +// the object last sent DragOver, the DragIcon picture, and the mouse
    +// buttons at the last drag event.  forwarding is set while the drop's
    +// button release is passed on to its window's own handler, whose BASIC
    +// mouse events are dropped.
    +typedef struct {
    +    BasFormRtT  *rt;
    +    AppContextT *ctx;
    +    int32_t      appId;
    +    BasControlT *source;
    +    BasControlT *over;
    +    uint8_t     *icon;
    +    int32_t      prevButtons;
    +    bool         forwarding;
    +} BasDragT;
    +
    +static BasDragT sDrag;
    +
    +// dragAppEnded is registered with the shell (once, on the first drag).
    +static bool sDragCleanupHooked = false;
    +
     
     // ============================================================
     // Module-scope declarations
    @@ -381,6 +431,8 @@ typedef enum {
         CTRL_PROP_DATASOURCE,
         CTRL_PROP_DATAFIELD,
         CTRL_PROP_LISTCOUNT,
    +    CTRL_PROP_DRAGMODE,
    +    CTRL_PROP_DRAGICON,
         CTRL_PROP_COUNT
     } BasCtrlPropE;
     
    @@ -408,6 +460,8 @@ static const BasPropDescT sCtrlProps[CTRL_PROP_COUNT] = {
         [CTRL_PROP_DATASOURCE]  = { BAS_PROP_DATASOURCE,  WGT_IFACE_STRING, true, true  },
         [CTRL_PROP_DATAFIELD]   = { BAS_PROP_DATAFIELD,   WGT_IFACE_STRING, true, true  },
         [CTRL_PROP_LISTCOUNT]   = { BAS_PROP_LISTCOUNT,   WGT_IFACE_INT,    true, false },
    +    [CTRL_PROP_DRAGMODE]    = { BAS_PROP_DRAGMODE,    WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_DRAGICON]    = { BAS_PROP_DRAGICON,    WGT_IFACE_STRING, true, true  },
     };
     
     // Menu item property ids (MenuName.Prop).  The .frm-only menu keys
    @@ -444,6 +498,7 @@ static const char *const sCommonMethodNames[BAS_CM_COUNT] = {
         [BAS_CM_ADDSEPARATOR]     = "AddSeparator",
         [BAS_CM_CLEAR]            = "Clear",
         [BAS_CM_BUTTONCOUNT]      = "ButtonCount",
    +    [BAS_CM_DRAG]             = "Drag",
     };
     
     
    @@ -487,6 +542,7 @@ const BasPropDescT *basFormRtMenuProps(int32_t *count);
     int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title);
     void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen);
     void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName);
    +BasControlT *basFormRtResolveCtrl(BasFormRtT *rt, void *ref);
     void basFormRtRunSimple(BasFormRtT *rt);
     void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...);
     void basFormRtSerialShutdown(void);
    @@ -497,9 +553,11 @@ static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form);
     #ifdef DVX_HOST_TEST
     void basFormRtTestReset(void);
     #endif
    +const char *basFormRtTypeName(void *ctx, void *objRef);
     void basFormRtUnloadForm(void *ctx, void *formRef);
     static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc);
     static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *proc, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount);
    +static BasValueT clipboardMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc);
     void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt);
     void CommClose(int32_t handle);
     void CommDetach(int32_t handle);
    @@ -535,16 +593,30 @@ static bool deviceSoundBusy(void *ctx);
     static int32_t deviceStick(void *ctx, int32_t n);
     static int32_t deviceStrig(void *ctx, int32_t n);
     int32_t DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx);
    +int32_t DlgColor(const char *title, int32_t defaultColor);
     const char *DlgFileOpen(const char *title, const char *filter);
     const char *DlgFileSave(const char *title, const char *filter);
     const char *DlgInputBox(const char *title, const char *prompt, const char *defaultText);
     int32_t DlgInputCancelled(void);
     int32_t DlgIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal);
     int32_t DlgPromptSave(const char *title);
    +static void dragAppEnded(int32_t appId);
    +static void dragBegin(BasFormRtT *rt, BasControlT *ctrl);
    +static bool dragBlocksMouse(const BasControlT *ctrl, bool automaticBlocks);
    +static void dragEnd(bool drop, WindowT *releaseWin, int32_t x, int32_t y, int32_t buttons);
    +static void dragFire(BasFormRtT *rt, BasControlT *obj, BasControlT *source, const char *eventName, int32_t state);
    +static void dragForget(BasFormRtT *rt, const BasFormT *form, const BasControlT *ctrl);
    +static bool dragGoing(const BasControlT *obj, const BasFormT *form, const BasControlT *ctrl);
    +static void dragHookForms(BasFormRtT *rt, bool hook);
    +static void dragOnKey(WindowT *win, int32_t key, int32_t mod);
    +static void dragOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
    +static void dragOverUpdate(void);
    +static bool dragShowIcon(AppContextT *ctx, const char *path);
    +static BasControlT *dragTargetAt(BasFormRtT *rt);
     static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w);
     static BasControlT *findCtrlInForm(BasFormT *form, const char *name);
     static WidgetT *findCtrlWidgetByName(const char *name);
    -static BasFormT *findFormByWindow(const WindowT *win);
    +static BasFormT *findFormByWindow(const BasFormRtT *rt, const WindowT *win);
     static BasControlT *findMenuProxyInForm(BasFormT *form, const char *name);
     static BasControlT *findNamedObjectInForm(BasFormT *form, const char *name);
     static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name);
    @@ -771,7 +843,8 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha
                     break;
                 }
     
    -            case BAS_TYPE_DOUBLE: {
    +            case BAS_TYPE_DOUBLE:
    +            case BAS_TYPE_DATE: {
                     union { double d; uint32_t u[2]; } conv;
                     conv.d = args[i].dblVal;
                     nativeArgs[nativeCount++] = conv.u[0];
    @@ -793,7 +866,7 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha
             basFormRtRuntimeError(sFormRt, "Out of memory", "Out of memory marshalling arguments for external function '%s'.", funcName ? funcName : "?");
         } else {
             double   dblResult = 0.0;
    -        bool     fpReturn  = (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_SINGLE);
    +        bool     fpReturn  = (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_SINGLE || retType == BAS_TYPE_DATE);
             uint32_t rawResult = basNativeCall(funcPtr, nativeArgs, nativeCount, fpReturn, &dblResult);
     
             // The i386 ABI leaves the upper bits of a sub-32-bit return (bool,
    @@ -801,6 +874,8 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha
             // of trusting all of EAX.
             if (retType == BAS_TYPE_DOUBLE) {
                 result = basValDouble(dblResult);
    +        } else if (retType == BAS_TYPE_DATE) {
    +            result = basValDate(dblResult);
             } else if (retType == BAS_TYPE_SINGLE) {
                 result = basValSingle((float)dblResult);
             } else if (retType == BAS_TYPE_STRING) {
    @@ -854,17 +929,24 @@ void basFormRtBindVm(BasFormRtT *rt) {
         ui.createCtrlEx = basFormRtCreateCtrlEx;
         ui.setEvent     = basFormRtSetEvent;
         ui.removeCtrl   = basFormRtRemoveCtrl;
    +    ui.typeName     = basFormRtTypeName;
         ui.ctx          = rt;
     
         basVmSetUiCallbacks(rt->vm, &ui);
         printerBind(rt);
         gameBind(rt);
         deviceBind(rt);
    +    basFormRtScreenBind(rt);
     }
     
     
     BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) {
         BasFormRtT  *rt   = (BasFormRtT *)ctx;
    +
    +    if (ctrlRef == &sClipboardObj) {
    +        return clipboardMethod(rt, methodName, args, argc);
    +    }
    +
         BasControlT *ctrl = resolveCtrlRef(rt, ctrlRef);
     
         if (!ctrl) {
    @@ -885,6 +967,13 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName,
             return zeroValue();
         }
     
    +    // Drawing methods of forms and PictureBoxes
    +    BasValueT drawResult;
    +
    +    if (basFormRtDrawMethod(rt, ctrl, methodName, args, argc, &drawResult)) {
    +        return drawResult;
    +    }
    +
         // Interface descriptor methods
         const WgtIfaceT *iface = ctrl->iface;
     
    @@ -1340,6 +1429,12 @@ void basFormRtDestroy(BasFormRtT *rt) {
             sFormRt = NULL;
         }
     
    +    if (sDrag.rt == rt) {
    +        dragEnd(false, NULL, 0, 0, 0);
    +    }
    +
    +    basFormRtScreenClose(rt);
    +
         // Close serial/secLink and drop the idle pollers before tearing down the
         // forms whose terminal widgets they reference.  Also invoked from the
         // stub's _appShutdown hook so a force-killed app gets the same cleanup.
    @@ -1388,7 +1483,7 @@ void basFormRtEventLoop(BasFormRtT *rt) {
             return;
         }
     
    -    while (rt->ctx->running && arrlen(rt->forms) > 0 && !rt->terminated) {
    +    while (rt->ctx->running && basFormRtKeepsRunning(rt)) {
             if (!dvxUpdate(rt->ctx)) {
                 break;
             }
    @@ -1439,7 +1534,8 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
             }
         }
     
    -    return NULL;
    +    // VB's Clipboard object, unless a control or form has the name.
    +    return (ctrlName && strcasecmp(ctrlName, BAS_CLIPBOARD_NAME) == 0) ? (void *)&sClipboardObj : NULL;
     }
     
     
    @@ -1515,6 +1611,13 @@ const BasPropDescT *basFormRtFormProps(int32_t *count) {
     
     BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
         BasFormRtT  *rt   = (BasFormRtT *)ctx;
    +
    +    // Clipboard.GetText and friends without parentheses read like a
    +    // property: they are the methods called with no arguments.
    +    if (ctrlRef == &sClipboardObj) {
    +        return clipboardMethod(rt, propName, NULL, 0);
    +    }
    +
         BasControlT *ctrl = resolveCtrlRef(rt, ctrlRef);
     
         if (!ctrl) {
    @@ -1567,6 +1670,13 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
             return zeroValue();
         }
     
    +    // Drawing properties of forms and PictureBoxes
    +    BasValueT drawValue;
    +
    +    if (basFormRtDrawGetProp(rt, ctrl, propName, &drawValue)) {
    +        return drawValue;
    +    }
    +
         // Form-level properties use the window and BasFormT, not the root widget
         if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
             return getFormProp(rt, ctrl->form, propName);
    @@ -2173,6 +2283,11 @@ void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName) {
     }
     
     
    +BasControlT *basFormRtResolveCtrl(BasFormRtT *rt, void *ref) {
    +    return resolveCtrlRef(rt, ref);
    +}
    +
    +
     void basFormRtRunSimple(BasFormRtT *rt) {
         if (!rt || !rt->vm) {
             return;
    @@ -2392,6 +2507,15 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch
     
     void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value) {
         BasFormRtT  *rt   = (BasFormRtT *)ctx;
    +
    +    if (ctrlRef == &sClipboardObj) {
    +        basFormRtRuntimeError(rt,
    +            "Clipboard has no properties",
    +            "Property: %s\nUse the Clipboard methods Clear, GetFormat, GetText and SetText.",
    +            propName ? propName : "?");
    +        return;
    +    }
    +
         BasControlT *ctrl = resolveCtrlRef(rt, ctrlRef);
     
         if (!ctrl) {
    @@ -2451,6 +2575,11 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
             return;
         }
     
    +    // Drawing properties of forms and PictureBoxes
    +    if (basFormRtDrawSetProp(rt, ctrl, propName, value)) {
    +        return;
    +    }
    +
         // Form-level property assignment uses the window and BasFormT
         if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
             setFormProp(rt, ctrl->form, propName, value);
    @@ -2633,10 +2762,35 @@ void basFormRtTestReset(void) {
         sFormRt                = NULL;
         sLoadingFrm            = false;
         sLastInputBoxCancelled = false;
    +    memset(&sDrag, 0, sizeof(sDrag));
    +    sDragCleanupHooked = false;
     }
     #endif // DVX_HOST_TEST
     
     
    +// TypeName / TypeOf of a host object: "Form" for a form, the control's
    +// VB type name ("TextBox" ...) for a control, NULL when unknown.
    +const char *basFormRtTypeName(void *ctx, void *objRef) {
    +    BasFormRtT *rt = (BasFormRtT *)ctx;
    +
    +    if (objRef == (void *)&sClipboardObj) {
    +        return BAS_CLIPBOARD_NAME;
    +    }
    +
    +    BasControlT *obj = resolveCtrlRef(rt, objRef);
    +
    +    if (!obj) {
    +        return NULL;
    +    }
    +
    +    if (obj == &obj->form->formCtrl) {
    +        return BAS_FORM_TYPE_NAME;
    +    }
    +
    +    return obj->typeName[0] ? obj->typeName : NULL;
    +}
    +
    +
     void basFormRtUnloadForm(void *ctx, void *formRef) {
         BasFormRtT *rt   = (BasFormRtT *)ctx;
         BasFormT   *form = resolveFormRef(rt, formRef);
    @@ -2706,6 +2860,27 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char
             return zeroValue();
         }
     
    +    if (methodId == BAS_CM_DRAG) {
    +        // Drag [action]: begin (the default), cancel, or drop where the
    +        // mouse is.  Cancel and end only act on this control's own drag.
    +        int32_t action = (argc >= 1) ? (int32_t)basValToNumber(args[0]) : BAS_DRAG_BEGIN;
    +
    +        if (action == BAS_DRAG_BEGIN) {
    +            dragBegin(rt, ctrl);
    +        } else if (action == BAS_DRAG_CANCEL || action == BAS_DRAG_END) {
    +            if (sDrag.source == ctrl) {
    +                dragEnd(action == BAS_DRAG_END, NULL, 0, 0, 0);
    +            }
    +        } else {
    +            basFormRtRuntimeError(rt,
    +                "Invalid Drag action",
    +                "Control: %s\nAction: %d\nUse 0 (cancel), vbBeginDrag or vbEndDrag.",
    +                ctrlDisplayName(ctrl), (int)action);
    +        }
    +
    +        return zeroValue();
    +    }
    +
         // ========================================================
         // Popup / context menu methods
         // ========================================================
    @@ -3103,6 +3278,51 @@ static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *p
     }
     
     
    +// The Clipboard object's methods (VB semantics; the DVX clipboard holds
    +// text, vbCFText): Clear, GetFormat(format), GetText([format]) and
    +// SetText text [, format].  Other formats are never present, read as "",
    +// and cannot be set.
    +static BasValueT clipboardMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc) {
    +    int32_t len = 0;
    +
    +    if (!name) {
    +        name = "?";
    +    }
    +
    +    if (strcasecmp(name, BAS_CLIP_CLEAR) == 0) {
    +        dvxClipboardClear();
    +        return zeroValue();
    +    }
    +
    +    if (strcasecmp(name, BAS_CLIP_GETFORMAT) == 0) {
    +        bool isText = argc >= 1 && (int32_t)basValToNumber(args[0]) == BAS_CF_TEXT;
    +
    +        return basValBool(isText && dvxClipboardGet(&len) != NULL);
    +    }
    +
    +    if (strcasecmp(name, BAS_CLIP_GETTEXT) == 0) {
    +        bool        isText = argc < 1 || (int32_t)basValToNumber(args[0]) == BAS_CF_TEXT;
    +        const char *text   = isText ? dvxClipboardGet(&len) : NULL;
    +
    +        return basValStringFromC(text ? text : "");
    +    }
    +
    +    if (strcasecmp(name, BAS_CLIP_SETTEXT) == 0 && argc >= 1 && (argc <= BAS_CLIP_SETTEXT_FORMAT || (int32_t)basValToNumber(args[BAS_CLIP_SETTEXT_FORMAT]) == BAS_CF_TEXT)) {
    +        BasStringT *s = basValFormatString(args[0]);
    +
    +        dvxClipboardCopy(s->data, s->len);
    +        basStringUnref(s);
    +        return zeroValue();
    +    }
    +
    +    basFormRtRuntimeError(rt,
    +        "Clipboard method not supported",
    +        "Method: %s\nThe Clipboard has Clear, GetFormat, GetText and SetText, for text (vbCFText).",
    +        name);
    +    return zeroValue();
    +}
    +
    +
     void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt) {
         CommSlotT *slot = commGetSlot(handle);
     
    @@ -3595,6 +3815,7 @@ static void destroyFormNow(BasFormRtT *rt, BasFormT *form) {
             dvxDestroyWindow(rt->ctx, form->window);
         }
     
    +    basFormRtDrawFree(&form->formCtrl);
         free(form);
     }
     
    @@ -3611,6 +3832,7 @@ static void destroyFormNow(BasFormRtT *rt, BasFormT *form) {
     // survivors at free time.
     static void detachFormForUnload(BasFormRtT *rt, BasFormT *form) {
         rt->refCacheKey = NULL;
    +    dragForget(rt, form, NULL);
     
         for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
             if (rt->forms[i] == form) {
    @@ -3770,6 +3992,19 @@ int32_t DlgChoice(const char *title, const char *prompt, const char *items, int3
     }
     
     
    +// The color dialog: the chosen color as RGB() makes it, or -1 when
    +// cancelled.  defaultColor is the starting color.
    +int32_t DlgColor(const char *title, int32_t defaultColor) {
    +    uint32_t rgb = (uint32_t)defaultColor & BAS_RGB_MASK;
    +
    +    if (sFormRt && dvxColorDialog(sFormRt->ctx, title, &rgb)) {
    +        return (int32_t)rgb;
    +    }
    +
    +    return BAS_DLG_COLOR_CANCELLED;
    +}
    +
    +
     const char *DlgFileOpen(const char *title, const char *filter) {
         if (!sFormRt) {
             return "";
    @@ -3862,6 +4097,293 @@ int32_t DlgPromptSave(const char *title) {
     }
     
     
    +// Shell app-cleanup hook: a program that ends while its drag runs (it
    +// was killed) has lost its windows already; drop the drag without
    +// touching them or its runtime.
    +static void dragAppEnded(int32_t appId) {
    +    if (sDrag.rt && sDrag.appId == appId) {
    +        dvxDragFeedbackHide(sDrag.ctx);
    +        dvxFreeImage(sDrag.icon);
    +        memset(&sDrag, 0, sizeof(sDrag));
    +    }
    +}
    +
    +
    +// Start dragging ctrl (a DragMode automatic press, or its Drag method):
    +// the program's form windows hand their mouse and keys to the drag, and
    +// the DragIcon picture, else an outline of the control, follows the
    +// mouse.  A drag already running (in any program) is cancelled first.
    +static void dragBegin(BasFormRtT *rt, BasControlT *ctrl) {
    +    AppContextT *ctx = rt->ctx;
    +    WindowT     *win = ctrl->form ? ctrl->form->window : NULL;
    +    WidgetT     *w   = ctrl->widget;
    +
    +    if (!win || !w) {
    +        return;
    +    }
    +
    +    dragEnd(false, NULL, 0, 0, 0);
    +
    +    // The shell is looked up at run time (the host tests have none).
    +    if (!sDragCleanupHooked) {
    +        void (*reg)(ShellAppCleanupFnT fn) = dlsym(NULL, DVX_SYM("shellRegisterAppCleanup"));
    +
    +        sDragCleanupHooked = true;
    +
    +        if (reg) {
    +            reg(dragAppEnded);
    +        }
    +    }
    +
    +    sDrag.rt          = rt;
    +    sDrag.ctx         = ctx;
    +    sDrag.appId       = ctx->currentAppId;
    +    sDrag.source      = ctrl;
    +    sDrag.prevButtons = ctx->mouseButtons;
    +    dragHookForms(rt, true);
    +
    +    if (!ctrl->dragIcon || !dragShowIcon(ctx, ctrl->dragIcon)) {
    +        dvxDragOutlineShow(ctx, w->w, w->h, win->x + win->contentX + w->x - ctx->mouseX, win->y + win->contentY + w->y - ctx->mouseY);
    +    }
    +}
    +
    +
    +// True when a control's BASIC mouse event must not fire: a drag is
    +// running or its release is being passed on, or (automaticBlocks) the
    +// control's DragMode is automatic, where the left button only drags.
    +static bool dragBlocksMouse(const BasControlT *ctrl, bool automaticBlocks) {
    +    return sDrag.rt || sDrag.forwarding || (automaticBlocks && ctrl->dragMode == BAS_DRAGMODE_AUTOMATIC);
    +}
    +
    +
    +// End the drag in progress (none: nothing happens).  The forms get their
    +// own handlers back and the outline or picture goes.  releaseWin, when
    +// given, is the window whose button release ended the drag: the release
    +// is passed on to its own handler, so the pressed control lets go, with
    +// the BASIC mouse events it would raise dropped.  With drop, DragDrop
    +// then fires on the control or form under the mouse.
    +static void dragEnd(bool drop, WindowT *releaseWin, int32_t x, int32_t y, int32_t buttons) {
    +    BasFormRtT *rt = sDrag.rt;
    +
    +    if (!rt) {
    +        return;
    +    }
    +
    +    BasControlT *source  = sDrag.source;
    +    BasFormT    *relForm = releaseWin ? findFormByWindow(rt, releaseWin) : NULL;
    +    void       (*release)(WindowT *win, int32_t x, int32_t y, int32_t buttons) = relForm ? relForm->dragPrevOnMouse : NULL;
    +
    +    dragHookForms(rt, false);
    +    dvxDragFeedbackHide(rt->ctx);
    +    dvxFreeImage(sDrag.icon);
    +    memset(&sDrag, 0, sizeof(sDrag));
    +
    +    if (release) {
    +        sDrag.forwarding = true;
    +        release(releaseWin, x, y, buttons);
    +        sDrag.forwarding = false;
    +    }
    +
    +    BasControlT *target = drop ? dragTargetAt(rt) : NULL;
    +
    +    if (target) {
    +        dragFire(rt, target, source, BAS_EVT_DRAGDROP, BAS_DRAG_STATE_NONE);
    +    }
    +}
    +
    +
    +// Raise DragDrop (state BAS_DRAG_STATE_NONE) or DragOver on obj -- a
    +// control, or a form's formCtrl -- with Source, the mouse position in
    +// obj's own coordinates and, for DragOver, State.
    +static void dragFire(BasFormRtT *rt, BasControlT *obj, BasControlT *source, const char *eventName, int32_t state) {
    +    BasFormT  *form = obj->form;
    +    WindowT   *win  = form->window;
    +    WidgetT   *w    = obj->widget;
    +    BasValueT  args[BAS_DRAG_OVER_ARGS];
    +    int32_t    argc = BAS_DRAG_DROP_ARGS;
    +
    +    args[0] = basValObject(source);
    +    args[1] = basValLong(rt->ctx->mouseX - win->x - win->contentX - w->x - w->contentOffX);
    +    args[2] = basValLong(rt->ctx->mouseY - win->y - win->contentY - w->y - w->contentOffY);
    +
    +    if (state != BAS_DRAG_STATE_NONE) {
    +        args[argc++] = basValLong(state);
    +    }
    +
    +    if (obj == &form->formCtrl) {
    +        basFormRtFireEventArgs(rt, form, form->name, eventName, args, argc);
    +    } else {
    +        fireCtrlEvent(rt, obj, eventName, args, argc);
    +    }
    +}
    +
    +
    +// A form (ctrl NULL) or one control is going away: it stops being the
    +// DragOver object, and a drag from it is cancelled.
    +static void dragForget(BasFormRtT *rt, const BasFormT *form, const BasControlT *ctrl) {
    +    if (sDrag.rt != rt) {
    +        return;
    +    }
    +
    +    if (dragGoing(sDrag.over, form, ctrl)) {
    +        sDrag.over = NULL;
    +    }
    +
    +    if (dragGoing(sDrag.source, form, ctrl)) {
    +        dragEnd(false, NULL, 0, 0, 0);
    +    }
    +}
    +
    +
    +// dragForget helper: true when obj is ctrl, or (ctrl NULL) is on form.
    +static bool dragGoing(const BasControlT *obj, const BasFormT *form, const BasControlT *ctrl) {
    +    return obj && (ctrl ? obj == ctrl : obj->form == form);
    +}
    +
    +
    +// Hand every form window's mouse and keys to the drag (hook), or give
    +// them back.  A form loaded during the drag was never hooked and keeps
    +// its own handlers.
    +static void dragHookForms(BasFormRtT *rt, bool hook) {
    +    for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
    +        BasFormT *form = rt->forms[i];
    +        WindowT  *win  = form->window;
    +
    +        if (!win) {
    +            continue;
    +        }
    +
    +        if (hook) {
    +            form->dragPrevOnMouse = win->onMouse;
    +            form->dragPrevOnKey   = win->onKey;
    +            win->onMouse          = dragOnMouse;
    +            win->onKey            = dragOnKey;
    +        } else if (win->onMouse == dragOnMouse) {
    +            win->onMouse = form->dragPrevOnMouse;
    +            win->onKey   = form->dragPrevOnKey;
    +        }
    +    }
    +}
    +
    +
    +// Keys during a drag: Escape cancels it, the rest go to the window as
    +// usual.
    +static void dragOnKey(WindowT *win, int32_t key, int32_t mod) {
    +    if (key == KEY_ESCAPE) {
    +        dragEnd(false, NULL, 0, 0, 0);
    +        return;
    +    }
    +
    +    BasFormT *form = findFormByWindow(sDrag.rt, win);
    +
    +    if (form && form->dragPrevOnKey) {
    +        form->dragPrevOnKey(win, key, mod);
    +    }
    +}
    +
    +
    +// The mouse during a drag.  The left button's release drops; a move
    +// updates DragOver.  A drag started from code with the button up is
    +// dropped by the next click: its press only arms the release.
    +static void dragOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
    +    bool wasDown = (sDrag.prevButtons & MOUSE_LEFT) != 0;
    +    bool isDown  = (buttons & MOUSE_LEFT) != 0;
    +
    +    sDrag.prevButtons = buttons;
    +
    +    if (wasDown && !isDown) {
    +        dragEnd(true, win, x, y, buttons);
    +    } else if (wasDown == isDown) {
    +        dragOverUpdate();
    +    }
    +}
    +
    +
    +// The mouse moved during the drag: DragOver vbLeave on the object it
    +// left, then vbEnter (new object) or vbOver on the one under it.  A
    +// handler may end the drag or remove objects, so the target is looked up
    +// again after the leave.
    +static void dragOverUpdate(void) {
    +    BasFormRtT  *rt     = sDrag.rt;
    +    BasControlT *source = sDrag.source;
    +    BasControlT *prev   = sDrag.over;
    +
    +    if (prev && prev != dragTargetAt(rt)) {
    +        sDrag.over = NULL;
    +        dragFire(rt, prev, source, BAS_EVT_DRAGOVER, BAS_DRAG_LEAVE);
    +    }
    +
    +    BasControlT *target = (sDrag.source == source) ? dragTargetAt(rt) : NULL;
    +
    +    if (!target) {
    +        return;
    +    }
    +
    +    int32_t state = (target == sDrag.over) ? BAS_DRAG_OVER : BAS_DRAG_ENTER;
    +
    +    sDrag.over = target;
    +    dragFire(rt, target, source, BAS_EVT_DRAGOVER, state);
    +}
    +
    +
    +// Show the DragIcon picture at path as the drag feedback; false when it
    +// cannot be read (the outline is shown instead).
    +static bool dragShowIcon(AppContextT *ctx, const char *path) {
    +    int32_t  len    = 0;
    +    char    *data   = platformReadFile(path, &len);
    +    int32_t  w      = 0;
    +    int32_t  h      = 0;
    +    int32_t  pitch  = 0;
    +    bool     hasKey = false;
    +    uint32_t key    = 0;
    +
    +    if (!data) {
    +        return false;
    +    }
    +
    +    sDrag.icon = dvxLoadImageAlpha(ctx, (const uint8_t *)data, len, &w, &h, &pitch, &hasKey, &key);
    +    free(data);
    +
    +    if (!sDrag.icon) {
    +        return false;
    +    }
    +
    +    dvxDragImageShow(ctx, sDrag.icon, w, h, pitch, hasKey, key);
    +    return true;
    +}
    +
    +
    +// The object a drop at the mouse lands on: the innermost control of the
    +// dragging runtime's form under the mouse, else that form (its formCtrl);
    +// NULL off the program's forms.
    +static BasControlT *dragTargetAt(BasFormRtT *rt) {
    +    AppContextT *ctx  = rt->ctx;
    +    int32_t      part = 0;
    +    int32_t      idx  = wmHitTest(&ctx->stack, ctx->mouseX, ctx->mouseY, &part);
    +
    +    if (idx < 0 || part != HIT_CONTENT) {
    +        return NULL;
    +    }
    +
    +    WindowT  *win  = ctx->stack.windows[idx];
    +    BasFormT *form = findFormByWindow(rt, win);
    +
    +    if (!form) {
    +        return NULL;
    +    }
    +
    +    for (WidgetT *w = widgetCallbackTarget(form->root, ctx->mouseX - win->x - win->contentX, ctx->mouseY - win->y - win->contentY); w; w = w->parent) {
    +        BasControlT *ctrl = findCtrlByWidget(form, w);
    +
    +        if (ctrl) {
    +            return ctrl;
    +        }
    +    }
    +
    +    return &form->formCtrl;
    +}
    +
    +
     // Control whose live widget is exactly w, or NULL.
     static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w) {
         for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
    @@ -3909,16 +4431,12 @@ static WidgetT *findCtrlWidgetByName(const char *name) {
     }
     
     
    -// Loaded form owning this window, or NULL.  Shared by every window
    -// callback (onForm*).
    -static BasFormT *findFormByWindow(const WindowT *win) {
    -    if (!sFormRt) {
    -        return NULL;
    -    }
    -
    -    for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
    -        if (sFormRt->forms[i]->window == win) {
    -            return sFormRt->forms[i];
    +// rt's loaded form owning this window, or NULL (also when rt is NULL).
    +// Shared by every window callback (onForm*) and the drag.
    +static BasFormT *findFormByWindow(const BasFormRtT *rt, const WindowT *win) {
    +    for (int32_t i = 0; rt && i < (int32_t)arrlen(rt->forms); i++) {
    +        if (rt->forms[i]->window == win) {
    +            return rt->forms[i];
             }
         }
     
    @@ -4298,6 +4816,8 @@ static void freeControl(BasControlT *ctrl) {
         }
     
         free(ctrl->tooltip);
    +    free(ctrl->dragIcon);
    +    basFormRtDrawFree(ctrl);
         free(ctrl);
     }
     
    @@ -4741,6 +5261,12 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha
                 return basValLong(m ? ((int32_t (*)(const WidgetT *))m->fn)(w) : 0);
             }
     
    +        case CTRL_PROP_DRAGMODE:
    +            return basValLong(ctrl->dragMode);
    +
    +        case CTRL_PROP_DRAGICON:
    +            return basValStringFromC(ctrl->dragIcon ? ctrl->dragIcon : "");
    +
             case CTRL_PROP_COUNT:
                 break;
         }
    @@ -4749,10 +5275,16 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha
     }
     
     
    -// FormName.Prop reads: window geometry/state plus the BasFormT flags.
    +// FormName.Prop reads: window geometry/state plus the BasFormT flags; a
    +// control of the form read by its name (Me.Text1) is the control.
     static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName) {
         WindowT            *win = frm->window;
         const BasPropDescT *pd  = basFormRtFindFormProp(propName);
    +    BasControlT        *sub = pd ? NULL : findCtrlInForm(frm, propName);
    +
    +    if (sub) {
    +        return basValObject(sub);
    +    }
     
         if (!pd || !pd->readable) {
             char valid[BAS_PROP_LIST_LEN];
    @@ -5001,7 +5533,7 @@ static void onFormActivate(WindowT *win) {
         // bridge (icon refresh, focus bookkeeping) never runs for forms.
         widgetOnFocus(win);
     
    -    BasFormT *form = findFormByWindow(win);
    +    BasFormT *form = findFormByWindow(sFormRt, win);
     
         if (form) {
             basFormRtFireEvent(sFormRt, form, form->name, BAS_EVT_ACTIVATE);
    @@ -5014,7 +5546,7 @@ static void onFormActivate(WindowT *win) {
     // QueryUnload/Unload/teardown path (including the `unloading`
     // re-entrancy guard and deferred frees) as the UNLOAD statement.
     static void onFormClose(WindowT *win) {
    -    BasFormT *form = findFormByWindow(win);
    +    BasFormT *form = findFormByWindow(sFormRt, win);
     
         if (!form) {
             return;
    @@ -5037,7 +5569,7 @@ static void onFormDeactivate(WindowT *win) {
         // data-binding write-back), which this override otherwise suppresses.
         widgetOnBlur(win);
     
    -    BasFormT *form = findFormByWindow(win);
    +    BasFormT *form = findFormByWindow(sFormRt, win);
     
         if (form) {
             basFormRtFireEvent(sFormRt, form, form->name, BAS_EVT_DEACTIVATE);
    @@ -5047,7 +5579,7 @@ static void onFormDeactivate(WindowT *win) {
     
     // Dispatch menu clicks as ControlName_Click events.
     static void onFormMenu(WindowT *win, int32_t menuId) {
    -    BasFormT *form = findFormByWindow(win);
    +    BasFormT *form = findFormByWindow(sFormRt, win);
     
         if (!form) {
             return;
    @@ -5066,7 +5598,7 @@ static void onFormResize(WindowT *win, int32_t newW, int32_t newH) {
         // Let the widget system re-evaluate scrollbars for the new size
         widgetOnResize(win, newW, newH);
     
    -    BasFormT *form = findFormByWindow(win);
    +    BasFormT *form = findFormByWindow(sFormRt, win);
     
         if (form) {
             basFormRtFireEvent(sFormRt, form, form->name, BAS_EVT_RESIZE);
    @@ -5145,7 +5677,7 @@ static void onWidgetClick(WidgetT *w) {
         BasFormRtT  *rt;
         BasControlT *ctrl = ctrlFromWidget(w, &rt);
     
    -    if (ctrl) {
    +    if (ctrl && !dragBlocksMouse(ctrl, true)) {
             fireCtrlEvent(rt, ctrl, BAS_EVT_CLICK, NULL, 0);
         }
     }
    @@ -5155,7 +5687,7 @@ static void onWidgetDblClick(WidgetT *w) {
         BasFormRtT  *rt;
         BasControlT *ctrl = ctrlFromWidget(w, &rt);
     
    -    if (ctrl) {
    +    if (ctrl && !dragBlocksMouse(ctrl, true)) {
             fireCtrlEvent(rt, ctrl, BAS_EVT_DBLCLICK, NULL, 0);
         }
     }
    @@ -5213,7 +5745,14 @@ static void onWidgetMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y)
         BasFormRtT  *rt;
         BasControlT *ctrl = ctrlFromWidget(w, &rt);
     
    -    if (ctrl) {
    +    // DragMode automatic: the left button starts a drag instead of
    +    // raising MouseDown.
    +    if (ctrl && button == BAS_MOUSE_BUTTON_LEFT && ctrl->dragMode == BAS_DRAGMODE_AUTOMATIC && !sDrag.rt) {
    +        dragBegin(rt, ctrl);
    +        return;
    +    }
    +
    +    if (ctrl && !dragBlocksMouse(ctrl, false)) {
             BasValueT args[3];
             args[0] = basValLong(button);
             args[1] = basValLong(x);
    @@ -5227,7 +5766,7 @@ static void onWidgetMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y)
         BasFormRtT  *rt;
         BasControlT *ctrl = ctrlFromWidget(w, &rt);
     
    -    if (ctrl) {
    +    if (ctrl && !dragBlocksMouse(ctrl, false)) {
             BasValueT args[3];
             args[0] = basValLong(button);
             args[1] = basValLong(x);
    @@ -5241,7 +5780,7 @@ static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y) {
         BasFormRtT  *rt;
         BasControlT *ctrl = ctrlFromWidget(w, &rt);
     
    -    if (ctrl) {
    +    if (ctrl && !dragBlocksMouse(ctrl, button == BAS_MOUSE_BUTTON_LEFT)) {
             BasValueT args[3];
             args[0] = basValLong(button);
             args[1] = basValLong(x);
    @@ -5492,6 +6031,8 @@ static void removeCtrlTree(BasFormRtT *rt, BasFormT *form, BasControlT *ctrl) {
                 serDetached = true;
             }
     
    +        dragForget(rt, form, c);
    +
             c->widget = NULL;
             arrdel(form->controls, i);
             rt->refCacheKey = NULL;
    @@ -6453,6 +6994,18 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
                 setControlString(ctrl->dataField, sizeof(ctrl->dataField), value);
                 break;
     
    +        case CTRL_PROP_DRAGMODE:
    +            ctrl->dragMode = ((int32_t)basValToNumber(value) == BAS_DRAGMODE_AUTOMATIC) ? BAS_DRAGMODE_AUTOMATIC : BAS_DRAGMODE_MANUAL;
    +            break;
    +
    +        case CTRL_PROP_DRAGICON: {
    +            BasStringT *s = basValFormatString(value);
    +            free(ctrl->dragIcon);
    +            ctrl->dragIcon = (s->len > 0) ? strdup(s->data) : NULL;
    +            basStringUnref(s);
    +            break;
    +        }
    +
             case CTRL_PROP_NAME:
             case CTRL_PROP_LISTCOUNT:
             case CTRL_PROP_COUNT:
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h
    index 0e79f6b0..bbabb83a 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formrt.h
    +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h
    @@ -45,6 +45,8 @@
     
     typedef struct BasFormT BasFormT;
     typedef struct BasControlT BasControlT;
    +typedef struct BasDrawT BasDrawT;              // a Form's / PictureBox's drawing state (formGfx.c)
    +typedef struct BasScreenWinT BasScreenWinT;    // the program screen's window (formGfx.c)
     
     // ============================================================
     // Limits
    @@ -60,6 +62,7 @@ typedef struct BasControlT BasControlT;
     // Layout of a form or container that names none ("VBox" / "HBox" /
     // "WrapBox" are the box widgets' BASIC names).
     #define BAS_LAYOUT_VBOX          "VBox"
    +#define BAS_PICTUREBOX_TYPE      "PictureBox"     // takes the drawing members, as forms do
     
     // ============================================================
     // Property names
    @@ -82,6 +85,8 @@ typedef struct BasControlT BasControlT;
     #define BAS_PROP_DATAFIELD       "DataField"
     #define BAS_PROP_DATASOURCE      "DataSource"
     #define BAS_PROP_DETAILFIELD     "DetailField"
    +#define BAS_PROP_DRAGICON        "DragIcon"
    +#define BAS_PROP_DRAGMODE        "DragMode"
     #define BAS_PROP_ENABLED         "Enabled"
     #define BAS_PROP_FORECOLOR       "ForeColor"
     #define BAS_PROP_HEIGHT          "Height"
    @@ -168,6 +173,7 @@ typedef enum {
         BAS_CM_ADDSEPARATOR,
         BAS_CM_CLEAR,
         BAS_CM_BUTTONCOUNT,
    +    BAS_CM_DRAG,
         BAS_CM_COUNT
     } BasCommonMethodE;
     
    @@ -216,6 +222,9 @@ typedef struct BasControlT {
         char              dataField[BAS_MAX_IDENT];  // column name for binding
         char              helpTopic[BAS_MAX_IDENT];  // help topic ID for F1
         char             *tooltip;                       // heap-owned tooltip text (NULL = none); wgtSetTooltip references this buffer, so it must outlive the widget
    +    char             *dragIcon;                      // heap-owned DragIcon picture path (NULL = the drag shows an outline)
    +    BasDrawT         *draw;                          // drawing state (forms and PictureBoxes that drew), NULL before
    +    int32_t           dragMode;                      // DragMode: BAS_DRAGMODE_MANUAL or BAS_DRAGMODE_AUTOMATIC
         int32_t           menuId;                        // WM menu item ID (>0 for menu items, 0 for controls)
         BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES];
         int32_t           eventOverrideCount;
    @@ -276,6 +285,10 @@ typedef struct BasFormT {
         // (fireCtrlEvent drops events for it).  Distinct from `unloading`,
         // which is also true while QueryUnload/Unload still fire.
         bool              detached;
    +    // The window's own mouse and key handlers, kept while a drag and
    +    // drop has taken them over (see dragHookForms).
    +    void            (*dragPrevOnMouse)(WindowT *win, int32_t x, int32_t y, int32_t buttons);
    +    void            (*dragPrevOnKey)(WindowT *win, int32_t key, int32_t mod);
         // Synthetic control entry for the form itself, so that
         // FormName.Property works through the same getProp/setProp path.
         BasControlT       formCtrl;
    @@ -357,6 +370,10 @@ typedef struct {
         GameFramePacerT   framePacer;
         int32_t           frameHz;
         uint64_t          frameDueUs;
    +    // The program screen's window (made by the first console or graphics
    +    // statement) and its caption: hosts set title to the program's name.
    +    BasScreenWinT    *screenWin;
    +    char              title[BAS_MAX_IDENT];
     } BasFormRtT;
     
     // ============================================================
    @@ -381,6 +398,20 @@ void basFormRtRunSimple(BasFormRtT *rt);
     // Destroy the form runtime and all forms/controls.
     void basFormRtDestroy(BasFormRtT *rt);
     
    +// True while the program is alive for its host's event loop: a form is
    +// loaded or the program's screen window is open, and it was not stopped
    +// (runtime error, screen window closed).
    +bool basFormRtKeepsRunning(const BasFormRtT *rt);
    +
    +// The program screen's window, or NULL (none yet, or closed).
    +WindowT *basFormRtScreenWindow(const BasFormRtT *rt);
    +
    +// True when name is a Form / PictureBox drawing method (Cls, PSet, Line,
    +// Circle, Point, Print, PaintPicture, TextWidth, TextHeight) or, with
    +// isProp, a drawing property (CurrentX, CurrentY, DrawWidth, FillColor,
    +// FillStyle, ForeColor, BackColor, AutoRedraw, ScaleWidth, ScaleHeight).
    +bool basFormRtIsDrawMember(const char *name, bool isProp);
    +
     // Close all serial/secLink resources and unregister the idle pollers.
     // Called by basFormRtDestroy and by the stub's _appShutdown force-kill hook.
     void basFormRtSerialShutdown(void);
    @@ -418,6 +449,7 @@ void     *basFormRtLoadForm(void *ctx, const char *formName);
     int32_t   basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title);
     void      basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value);
     void      basFormRtShowForm(void *ctx, void *formRef, bool modal);
    +const char *basFormRtTypeName(void *ctx, void *objRef);
     void      basFormRtUnloadForm(void *ctx, void *formRef);
     
     // ---- Extern call callbacks (shared by IDE and stub) ----
    @@ -493,4 +525,23 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName
     
     BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen);
     
    +// ============================================================
    +// Between formrt.c and formGfx.c
    +// ============================================================
    +
    +// Drawing members of forms and PictureBoxes: true when handled.
    +bool basFormRtDrawGetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, BasValueT *out);
    +bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, BasValueT *args, int32_t argc, BasValueT *result);
    +bool basFormRtDrawSetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, BasValueT value);
    +
    +// Free a control's drawing state.
    +void basFormRtDrawFree(BasControlT *ctrl);
    +
    +// The live control an object reference names, or NULL.
    +BasControlT *basFormRtResolveCtrl(BasFormRtT *rt, void *ref);
    +
    +// Program screen: become the VM's screen host / close the screen window.
    +void basFormRtScreenBind(BasFormRtT *rt);
    +void basFormRtScreenClose(BasFormRtT *rt);
    +
     #endif // DVXBASIC_FORMRT_H
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
    index f61a78b7..ac3245d4 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
    @@ -74,7 +74,8 @@ static const DsgnIntPropT sIntProps[] = {
         { BAS_PROP_MINHEIGHT, BAS_PROP_HEIGHT, offsetof(DsgnControlT, height),    DSGN_SAVE_ALWAYS      },
         { BAS_PROP_MAXWIDTH,  NULL,            offsetof(DsgnControlT, maxWidth),  DSGN_SAVE_IF_POSITIVE },
         { BAS_PROP_MAXHEIGHT, NULL,            offsetof(DsgnControlT, maxHeight), DSGN_SAVE_IF_POSITIVE },
    -    { BAS_PROP_WEIGHT,    NULL,            offsetof(DsgnControlT, weight),    DSGN_SAVE_IF_POSITIVE }
    +    { BAS_PROP_WEIGHT,    NULL,            offsetof(DsgnControlT, weight),    DSGN_SAVE_IF_POSITIVE },
    +    { BAS_PROP_DRAGMODE,  NULL,            offsetof(DsgnControlT, dragMode),  DSGN_SAVE_IF_NONZERO  }
     };
     
     #define DSGN_INT_PROP_COUNT DVX_ARRAY_LEN(sIntProps)
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
    index 7e845861..7b0cc472 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
    +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
    @@ -130,6 +130,7 @@ typedef struct {
         int32_t    maxWidth;    // 0 = no cap (stretch to fill)
         int32_t    maxHeight;   // 0 = no cap (stretch to fill)
         int32_t    weight;      // layout weight (0 = fixed size, >0 = share extra space)
    +    int32_t    dragMode;    // DragMode (0 manual, 1 automatic)
         char       helpTopic[DSGN_MAX_NAME]; // help topic ID for F1
         bool       visible;     // design-time visibility (default true)
         bool       enabled;     // design-time enabled (default true)
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c
    index 0d683345..e4457aee 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideMain.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c
    @@ -44,7 +44,6 @@
     #include "dropdown/dropdown.h"
     #include "canvas/canvas.h"
     #include "listBox/listBox.h"
    -#include "slider/slider.h"
     #include "tabControl/tabCtrl.h"
     #include "button/button.h"
     #include "splitter/splitter.h"
    @@ -102,8 +101,8 @@
     #define IDE_VALUE_BUF      256    // formatted variable value / watch text
     #define IDE_FIND_TEXT_BUF  256    // find / replace text
     #define IDE_LINE_NUM_BUF   16     // decimal line number text
    -#define IDE_TYPE_NAME_BUF  16     // "Integer()" style type column text
    -#define IDE_RGB_LABEL_BUF  8      // "255"
    +#define IDE_TYPE_NAME_BUF  (BAS_MAX_IDENT + 16) // "Integer()" / "Variant/ClassName" type column text
    +#define IDE_RGB_LABEL_BUF  16     // "Green 255"
     #define IDE_TAB_WIDTH_BUF  8      // tab width text input
     #define IDE_OUTPUT_LINE_RESERVE 256 // headroom kept in sOutputBuf per error line
     #define IDE_QUOTE_OVERHEAD 3      // two quotes plus NUL around a string value
    @@ -116,11 +115,12 @@
     #define IDE_GENERAL_SECTION "(General)"
     #define IDE_GLOBAL_SECTION  "(Global)"
     #define IDE_MODULE_FRAME    "(module)"
    +#define IDE_COLLECTION_TYPE "Collection"  // debugger type name of a Collection
    +#define IDE_OBJECT_TYPE     "Object"      // debugger type name of any other object
     #define IDE_BREAK_TAG       " [break]"
     #define IDE_DEFAULT_FORM    "Form1"
     #define IDE_HELP_FILE       "dvxbasic.hlp"
     #define IDE_PREFS_FILE      "dvxbasic.ini"
    -#define IDE_FRM_EXT         ".frm"
     #define IDE_DBP_EXT         ".dbp"
     #define IDE_FORM_TYPE_NAME  FRM_KW_FORM // designer/validator type name of a form
     #define IDE_MENU_TYPE_NAME  FRM_KW_MENU // validator type name of a menu item
    @@ -129,12 +129,17 @@
     #define IDE_IMM_RHS_VAR     "IDE_IMM_RHS" // Immediate assignment scratch variable
     #define IDE_RENAME_TEMP_FMT "IDE_MNU_REN%d"  // placeholder during menu renames
     #define IDE_PASTE_FORM      "IdePaste"       // scratch form wrapping a pasted control
    +#define IDE_NEW_CLASS_FMT   "Class%d"        // name of a new class module
    +// Text of a new class module (%s = its name)
    +#define IDE_NEW_CLASS_TEXT  BAS_CLS_KW_VERSION " DVX 1.00 CLASS\n" BAS_CLS_KW_ATTRIBUTE " " BAS_CLS_ATTR_NAME " = \"%s\"\n\nOption Explicit\n"
     
     // Keyword tags for procedure scanning (case-insensitive prefix match)
     #define KW_SUB          "SUB "
     #define KW_FUNCTION     "FUNCTION "
     #define KW_END_SUB      "END SUB"
     #define KW_END_FUNCTION "END FUNCTION"
    +#define KW_PUBLIC       "PUBLIC "
    +#define KW_PRIVATE      "PRIVATE "
     #define KW_REM          "REM"
     #define KW_LET          "LET "
     #define KW_PRINT        "PRINT"
    @@ -240,6 +245,7 @@
     #define CMD_PRJ_OPEN     131
     #define CMD_PRJ_SAVE     132
     #define CMD_PRJ_CLOSE    133
    +#define CMD_PRJ_NEW_CLASS 134
     #define CMD_PRJ_REMOVE   136
     #define CMD_PRJ_PROPS    138
     #define CMD_WIN_PROJECT  137
    @@ -403,9 +409,6 @@ typedef struct {
         WidgetT *defDescription;
         // Colors tab
         WidgetT *colorList;
    -    WidgetT *sliderR;
    -    WidgetT *sliderG;
    -    WidgetT *sliderB;
         WidgetT *lblR;
         WidgetT *lblG;
         WidgetT *lblB;
    @@ -425,6 +428,7 @@ static void anchorCodeLine(IdeLineAnchorT *a, int32_t codeLine, int32_t cur, int
     static void anchorEditedLine(IdeLineAnchorT *a, const IdeLineDestT *map, char **procs, int32_t genBase);
     static int32_t anchorLine(const IdeLineAnchorT *a, const int32_t *starts);
     static bool anyFileModified(void);
    +static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char *text, int32_t len, BasSourceMapT *entry);
     static int32_t appendDroppingLines(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen);
     static void applySyntaxColors(void);
     static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx);
    @@ -432,6 +436,7 @@ static BasVmT *buildSnippetVm(const char *src, BasModuleT **outMod, char *errBuf
     static void buildVmBreakpoints(void);
     static void buildWindow(void);
     static uint8_t classifyWord(const char *word, int32_t wordLen);
    +static bool classNameUsed(const char *name, const char *fileName);
     static void cleanupFormWin(void);
     static void clearAllBreakpoints(void);
     static void clearOutput(void);
    @@ -477,6 +482,7 @@ static const BasDebugVarT *findDebugVar(const char *name);
     static bool findDialogBuild(bool showReplace);
     static bool findInEditorSections(int32_t fromIdx, const char *needle, bool caseSensitive, bool forward, const char *objFilter);
     static bool findInProject(const char *needle, bool caseSensitive, bool forward, FindScopeE scope);
    +static int32_t findMemberVarIdx(const BasObjectT *obj, const char *name);
     static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen);
     static int32_t findUdtFieldIdx(int32_t typeId, const char *fieldName);
     static void flushOutput(void);
    @@ -512,9 +518,9 @@ static void immReportSnippetResult(BasVmT *vm, BasVmResultE result);
     static bool immTryAssign(const char *expr);
     static void initSyntaxMap(void);
     static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize);
    +static const char *instanceTypeName(const BasObjectT *obj);
     static void invalidateCompiled(void);
     static bool isCtrlArrayInDesigner(const char *ctrlName);
    -static bool isFormPath(const char *path);
     static bool isInStringOrComment(const char *src, int32_t i);
     static bool isMangledDebugName(const char *name);
     static bool isRemAt(const char *line, int32_t pos, int32_t lineLen);
    @@ -539,11 +545,14 @@ static int32_t localToConcatLine(int32_t editorLine);
     static void logFlushLine(void);
     static bool lookupWatchVar(const char *expr, BasValueT *outVal);
     static void makeExecutable(void);
    +static BasValueT *memberVarSlot(const BasDebugVarT *dv);
     static void navigateToBreakpoint(int32_t bpIdx);
     static int32_t navigateToCodeLine(int32_t fileIdx, int32_t codeLine, bool setDbgLine);
     static void navigateToEventSub(void);
     static void navigateToNamedEventSub(const char *ctrlName, const char *eventName);
    +static void newClassModule(void);
     static void newProject(void);
    +static const char *objectClassName(const BasObjectT *obj);
     static void onBreakpointHit(void *ctx, int32_t line);
     static void onBreakpointListDblClick(WidgetT *w);
     static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
    @@ -551,8 +560,8 @@ static void onBreakpointWinClose(WindowT *win);
     static void onCallStackClose(WindowT *win);
     static void onClose(WindowT *win);
     static void onCodeWinClose(WindowT *win);
    +static void onColorChange(WidgetT *w);
     static void onColorListChange(WidgetT *w);
    -static void onColorSliderChange(WidgetT *w);
     static void onContentFocus(WindowT *win);
     static void onEditorChange(WidgetT *w);
     static void onEvtDropdownChange(WidgetT *w);
    @@ -576,7 +585,7 @@ static void onOutWinClose(WindowT *win);
     static void onPrefsCancel(WidgetT *w);
     static void onPrefsClose(WindowT *win);
     static void onPrefsOk(WidgetT *w);
    -static void onPrjFileDblClick(int32_t fileIdx, bool isForm);
    +static void onPrjFileDblClick(int32_t fileIdx);
     static void onProjectWinClose(WindowT *win);
     static void onReplace(WidgetT *w);
     static void onReplaceAll(WidgetT *w);
    @@ -608,11 +617,9 @@ static const char *parseIndexList(const char *p, int32_t *indices, int32_t *outC
     static void parseProcs(const char *source);
     static DsgnControlT *pasteParseControl(const char *clip, int32_t clipLen);
     static bool prefsBuild(WindowT *win);
    -static WidgetT *prefsColorSlider(WidgetT *parent, const char *caption, WidgetT **outValueLabel);
    -static void prefsSetRgbLabels(uint8_t r, uint8_t g, uint8_t b);
    +static void prefsSetChannelLabel(WidgetT *label, const char *channel, uint8_t value);
     static WidgetT *prefsTextRow(WidgetT *parent, const char *caption, int32_t maxLen, const char *value);
    -static void prefsUpdateColorSliders(void);
    -static void prefsUpdateSwatch(void);
    +static void prefsUpdateColorSwatch(void);
     static void printCallback(void *ctx, const char *text, bool newline);
     static bool procBodyEmpty(const char *procText);
     static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive);
    @@ -791,6 +798,26 @@ static const HelpMapEntryT sHelpMap[] = {
         {"Stick",       "lang.sound"},
         {"Strig",       "lang.sound"},
     
    +    // Console and graphics
    +    {"Circle",      "lang.screen"},
    +    {"Cls",         "lang.screen"},
    +    {"Color",       "lang.screen"},
    +    {"CsrLin",      "lang.screen"},
    +    {"Draw",        "lang.screen"},
    +    {"FullScreen",  "lang.screen"},
    +    {"InKey$",      "lang.screen"},
    +    {"Locate",      "lang.screen"},
    +    {"Paint",       "lang.screen"},
    +    {"Palette",     "lang.screen"},
    +    {"Point",       "lang.screen"},
    +    {"Pos",         "lang.screen"},
    +    {"PReset",      "lang.screen"},
    +    {"PSet",        "lang.screen"},
    +    {"Screen",      "lang.screen"},
    +    {"View",        "lang.screen"},
    +    {"Width",       "lang.screen"},
    +    {"Window",      "lang.screen"},
    +
         // File I/O
         {"Close",       "lang.fileio"},
         {"Eof",         "lang.func.fileio"},
    @@ -900,9 +927,37 @@ static const HelpMapEntryT sHelpMap[] = {
         {"vbOKCancel",  "lang.constants"},
         {"vbYesNo",     "lang.constants"},
     
    +    // Variants
    +    {"CVar",        "lang.variants"},
    +    {"Empty",       "lang.variants"},
    +    {"IsArray",     "lang.variants"},
    +    {"IsEmpty",     "lang.variants"},
    +    {"IsNull",      "lang.variants"},
    +    {"IsNumeric",   "lang.variants"},
    +    {"IsObject",    "lang.variants"},
    +    {"Null",        "lang.variants"},
    +    {"TypeName",    "lang.variants"},
    +    {"VarType",     "lang.variants"},
    +    {"Variant",     "lang.variants"},
    +
    +    // Objects and class modules
    +    {"Class_Initialize", "lang.classes"},
    +    {"Class_Terminate",  "lang.classes"},
    +    {"Is",          "lang.classes"},
    +    {"New",         "lang.classes"},
    +    {"Nothing",     "lang.classes"},
    +    {"Private",     "lang.classes"},
    +    {"Property",    "lang.classes"},
    +    {"Public",      "lang.classes"},
    +    {"Set",         "lang.classes"},
    +    {"TypeOf",      "lang.classes"},
    +
    +    // Collections
    +    {"Collection",  "lang.collection"},
    +    {"Each",        "lang.collection"},
    +
         // Form/control statements
         {"Me",          "lang.forms"},
    -    {"Set",         "lang.forms"},
     };
     
     #define HELP_MAP_COUNT DVX_ARRAY_LEN(sHelpMap)
    @@ -1101,7 +1156,7 @@ static bool activateFile(int32_t fileIdx, IdeViewModeE view) {
     
         // Resolve ViewAutoE
         if (view == ViewAutoE) {
    -        view = target->isForm ? ViewDesignE : ViewCodeE;
    +        view = target->kind == PrjKindFormE ? ViewDesignE : ViewCodeE;
         }
     
         // If already active, just ensure the right view is showing
    @@ -1125,7 +1180,7 @@ static bool activateFile(int32_t fileIdx, IdeViewModeE view) {
         }
     
         // PHASE 2: Load the incoming file
    -    if (target->isForm) {
    +    if (target->kind == PrjKindFormE) {
             // A form already in the designer keeps its live (possibly unsaved) state
             if ((fileIdx != sDesignerFileIdx || !sDesigner.form) && !loadDesignerFile(fileIdx)) {
                 return activateFailed();
    @@ -1183,7 +1238,7 @@ static bool activateFile(int32_t fileIdx, IdeViewModeE view) {
     
     // activeIsForm -- true when the active project file is a .frm
     static bool activeIsForm(void) {
    -    return sProject.activeFileIdx >= 0 && sProject.activeFileIdx < prjFileCount(&sProject) && sProject.files[sProject.activeFileIdx].isForm;
    +    return sProject.activeFileIdx >= 0 && sProject.activeFileIdx < prjFileCount(&sProject) && sProject.files[sProject.activeFileIdx].kind == PrjKindFormE;
     }
     
     
    @@ -1194,7 +1249,6 @@ static bool activeIsForm(void) {
     // or when it exists but cannot be loaded.
     static bool addFileToProject(const char *path) {
         const char *fileName = platformPathBaseName(path);
    -    bool        isForm   = isFormPath(path);
         char        dir[DVX_MAX_PATH];
     
         snprintf(dir, sizeof(dir), "%s", path);
    @@ -1220,7 +1274,7 @@ static bool addFileToProject(const char *path) {
     
         bool wasDirty = sProject.dirty;
     
    -    prjAddFile(&sProject, fileName, isForm);
    +    prjAddFile(&sProject, fileName, prjKindForPath(path));
     
         // A file that exists but cannot be loaded is not kept: its empty
         // stand-in would overwrite the real file on the next save.
    @@ -1331,6 +1385,39 @@ static bool anyFileModified(void) {
     }
     
     
    +// appendCompileFile -- append project file fileIdx's compile text (text,
    +// len bytes: its canonical layout, a form's code section only) to ps the
    +// way basBuildConcatProject does: a form's code wrapped in BEGINFORM, a
    +// class module's code after its header wrapped in BEGINCLASS, a module
    +// as it is.  Fills entry's line fields; false with ps->error set on
    +// failure.
    +static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char *text, int32_t len, BasSourceMapT *entry) {
    +    PrjFileT     *file    = &sProject.files[fileIdx];
    +    const char   *name    = NULL;
    +    BasBuildWrapE wrap    = BAS_BUILD_WRAP_NONE;
    +    int32_t       codePos = 0;
    +    char          className[BAS_MAX_IDENT];
    +
    +    entry->firstFileLine = 1;
    +
    +    if (file->kind == PrjKindFormE) {
    +        name = file->formName;
    +        wrap = BAS_BUILD_WRAP_FORM;
    +    } else if (file->kind == PrjKindClassE) {
    +        if (!prjClassName(&sProject, fileIdx, text, className, (int32_t)sizeof(className))) {
    +            snprintf(ps->error, sizeof(ps->error), "No class name for %s", file->path);
    +            return false;
    +        }
    +
    +        codePos = prjClassCodePos(text, len, &entry->firstFileLine);
    +        name    = className;
    +        wrap    = BAS_BUILD_WRAP_CLASS;
    +    }
    +
    +    return basBuildAppendFile(ps, text + codePos, len - codePos, wrap, name, &entry->startLine, &entry->lineCount);
    +}
    +
    +
     // appendDroppingLines -- append text to a NUL-terminated buffer of cap
     // bytes holding len bytes, dropping whole leading lines while it would
     // not fit so the newest text always survives (an oversize chunk keeps
    @@ -1568,13 +1655,10 @@ static void buildVmBreakpoints(void) {
         sVmBreakpoints = NULL;
     
         for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) {
    -        int32_t fileIdx  = sBreakpoints[i].fileIdx;
    -        int32_t codeLine = sBreakpoints[i].codeLine;
    +        int32_t concatLine = prjMapFileLine(&sProject, sBreakpoints[i].fileIdx, sBreakpoints[i].codeLine);
     
    -        int32_t startLine = prjMapStartLine(&sProject, fileIdx);
    -
    -        if (startLine > 0) {
    -            arrput(sVmBreakpoints, startLine + codeLine - 1);
    +        if (concatLine > 0) {
    +            arrput(sVmBreakpoints, concatLine);
             }
         }
     }
    @@ -1603,6 +1687,7 @@ static void buildWindow(void) {
         wmAddMenuItem(fileMenu, "Project Propert&ies...", CMD_PRJ_PROPS);
         wmAddMenuSeparator(fileMenu);
         wmAddMenuItem(fileMenu, "&Add File...\tCtrl+O", CMD_OPEN);
    +    wmAddMenuItem(fileMenu, "&New Class Module", CMD_PRJ_NEW_CLASS);
         wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE);
         wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL);
         wmAddMenuSeparator(fileMenu);
    @@ -1807,6 +1892,31 @@ static uint8_t classifyWord(const char *word, int32_t wordLen) {
     }
     
     
    +// classNameUsed -- true when a project file is named fileName or a form
    +// or class module of the project is called name
    +static bool classNameUsed(const char *name, const char *fileName) {
    +    bool used = false;
    +
    +    for (int32_t i = 0; i < prjFileCount(&sProject) && !used; i++) {
    +        PrjFileT *file = &sProject.files[i];
    +
    +        used = strcasecmp(file->path, fileName) == 0 || (file->kind == PrjKindFormE && strcasecmp(file->formName, name) == 0);
    +
    +        if (!used && file->kind == PrjKindClassE) {
    +            bool        missing = false;
    +            char       *owned   = NULL;
    +            const char *text    = projectFileText(i, &owned, &missing);
    +            char        other[BAS_MAX_IDENT];
    +
    +            used = prjClassName(&sProject, i, text, other, (int32_t)sizeof(other)) && strcasecmp(other, name) == 0;
    +            free(owned);
    +        }
    +    }
    +
    +    return used;
    +}
    +
    +
     // cleanupFormWin -- release designer-related state without destroying
     // the form window itself (the caller handles that).
     static void cleanupFormWin(void) {
    @@ -2023,12 +2133,12 @@ static bool compileProject(void) {
             arrfree(sProject.sourceMap);
             sProject.sourceMap = NULL;
     
    -        // Two passes: .bas modules first (so CONST declarations are
    -        // available), then .frm code sections.
    -        for (int32_t pass = 0; pass < 2; pass++) {
    +        // One pass per file kind in compile order: .bas modules first (so
    +        // CONST declarations are available), then .cls class modules,
    +        // then .frm code sections (see basBuildConcatProject).
    +        for (int32_t kind = 0; kind < PrjKindCountE; kind++) {
                 for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
    -                // Pass 0: modules only.  Pass 1: forms only.
    -                if ((pass == 0) == sProject.files[i].isForm) {
    +                if (sProject.files[i].kind != kind) {
                         continue;
                     }
     
    @@ -2054,16 +2164,15 @@ static bool compileProject(void) {
                         return false;
                     }
     
    -                // startLine is recorded AFTER the injected BEGINFORM so the
    -                // source map lines match what the editor shows.
    -                const char    *formName = sProject.files[i].isForm ? sProject.files[i].formName : NULL;
    -                BasSourceMapT  mapEntry;
    +                // startLine is recorded AFTER the injected BEGINFORM /
    +                // BEGINCLASS so the source map lines match what the editor
    +                // shows.
    +                BasSourceMapT mapEntry;
     
                     memset(&mapEntry, 0, sizeof(mapEntry));
                     snprintf(mapEntry.path, sizeof(mapEntry.path), "%s", sProject.files[i].path);
    -                mapEntry.firstFileLine = 1;
     
    -                bool ok = basBuildAppendFile(&project, fileSrc, fileLen, formName, &mapEntry.startLine, &mapEntry.lineCount);
    +                bool ok = appendCompileFile(&project, i, fileSrc, fileLen, &mapEntry);
     
                     free(fileSrc);
     
    @@ -2486,10 +2595,15 @@ static void debugUpdateWindows(void) {
     
     
     // debugVarVisible -- whether a debug variable is in scope where the VM is
    -// paused: locals of the current proc, module variables, and the variables of the
    +// paused: locals of the current proc, module variables, the variables of the
     // form whose code is running (form variables of different forms share
    -// slot numbers, so the form must match).
    +// slot numbers, so the form must match) and the member variables of the
    +// object a class procedure runs for.
     static bool debugVarVisible(const BasDebugVarT *dv, int32_t curProcIdx) {
    +    if (dv->scope == SCOPE_MEMBER) {
    +        return memberVarSlot(dv) != NULL;
    +    }
    +
         // Locals and a procedure's SHARED / STATIC names belong to that
         // procedure; inside a procedure a module variable shows only when it
         // is DIM SHARED (QBasic scope).
    @@ -2875,13 +2989,13 @@ static void ensureProject(const char *filePath) {
     
         prjNew(&sProject, baseName, dir, sPrefs);
     
    -    bool isForm = isFormPath(filePath);
    +    PrjFileKindE kind = prjKindForPath(filePath);
     
    -    prjAddFile(&sProject, fileName, isForm);
    +    prjAddFile(&sProject, fileName, kind);
     
         // If it's a .bas, check for a matching .frm and add it too.  Only the
         // file-name part's extension is replaced (a directory may contain dots).
    -    if (!isForm) {
    +    if (kind == PrjKindModuleE) {
             char    frmPath[DVX_MAX_PATH];
             int32_t baseOff = (int32_t)(platformPathBaseName(filePath) - filePath);
     
    @@ -2890,11 +3004,11 @@ static void ensureProject(const char *filePath) {
             char   *frmDot  = strrchr(frmPath + baseOff, '.');
             int32_t stemLen = frmDot ? (int32_t)(frmDot - frmPath) : (int32_t)strlen(frmPath);
     
    -        if (stemLen + (int32_t)sizeof(IDE_FRM_EXT) <= (int32_t)sizeof(frmPath)) {
    -            snprintf(frmPath + stemLen, sizeof(frmPath) - stemLen, "%s", IDE_FRM_EXT);
    +        if (stemLen + (int32_t)sizeof(PRJ_FRM_EXT) <= (int32_t)sizeof(frmPath)) {
    +            snprintf(frmPath + stemLen, sizeof(frmPath) - stemLen, "%s", PRJ_FRM_EXT);
     
                 if (prjPathExists(frmPath)) {
    -                prjAddFile(&sProject, platformPathBaseName(frmPath), true);
    +                prjAddFile(&sProject, platformPathBaseName(frmPath), PrjKindFormE);
                 }
             }
         }
    @@ -3064,7 +3178,7 @@ static bool fileCodeContains(int32_t fileIdx, const char *needle, int32_t needle
         scan.validator = NULL;
         scan.code      = NULL;
     
    -    if (text && sProject.files[fileIdx].isForm) {
    +    if (text && sProject.files[fileIdx].kind == PrjKindFormE) {
             frmScan(text, &scan);
             text = scan.code;
         }
    @@ -3287,6 +3401,23 @@ static bool findInProject(const char *needle, bool caseSensitive, bool forward,
     }
     
     
    +// findMemberVarIdx -- member variable slot of class instance obj called
    +// name, from the debug variable table (-1 when there is none)
    +static int32_t findMemberVarIdx(const BasObjectT *obj, const char *name) {
    +    const char *cls = objectClassName(obj);
    +
    +    for (int32_t i = 0; cls && i < sDbgModule->debugVarCount; i++) {
    +        const BasDebugVarT *dv = &sDbgModule->debugVars[i];
    +
    +        if (dv->scope == SCOPE_MEMBER && strcasecmp(dv->formName, cls) == 0 && strcasecmp(dv->name, name) == 0 && dv->index < obj->fieldCount) {
    +            return dv->index;
    +        }
    +    }
    +
    +    return -1;
    +}
    +
    +
     // Case-insensitive strstr replacement (strcasestr is a GNU extension)
     static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen) {
         for (; *haystack; haystack++) {
    @@ -3357,6 +3488,14 @@ static void formatValue(const BasValueT *v, char *buf, int32_t bufSize) {
                 snprintf(buf, bufSize, "%s", v->boolVal ? "True" : "False");
                 break;
     
    +        case BAS_TYPE_DATE: {
    +            BasStringT *text = basValFormatString(*v);
    +
    +            snprintf(buf, bufSize, "#%s#", text->data);
    +            basStringUnref(text);
    +            break;
    +        }
    +
             case BAS_TYPE_STRING:
                 if (v->strVal) {
                     snprintf(buf, bufSize, "\"%.*s\"", (int)(bufSize - IDE_QUOTE_OVERHEAD), v->strVal->data);
    @@ -3365,6 +3504,26 @@ static void formatValue(const BasValueT *v, char *buf, int32_t bufSize) {
                 }
                 break;
     
    +        case BAS_TYPE_EMPTY:
    +            snprintf(buf, bufSize, "Empty");
    +            break;
    +
    +        case BAS_TYPE_NULL:
    +            snprintf(buf, bufSize, "Null");
    +            break;
    +
    +        case BAS_TYPE_OBJECT:
    +            snprintf(buf, bufSize, "%s", v->objVal ? "<" IDE_OBJECT_TYPE ">" : "Nothing");
    +            break;
    +
    +        case BAS_TYPE_INSTANCE:
    +            if (v->instVal->classIdx == BAS_OBJ_COLLECTION) {
    +                snprintf(buf, bufSize, IDE_COLLECTION_TYPE " (%d items)", (int)v->instVal->itemCount);
    +            } else {
    +                snprintf(buf, bufSize, "<%s>", instanceTypeName(v->instVal));
    +            }
    +            break;
    +
             case BAS_TYPE_ARRAY: {
                 BasArrayT *arr = v->arrVal;
     
    @@ -3503,6 +3662,8 @@ static BasValueT *getDebugVarSlot(const BasDebugVarT *dv) {
             if (dv->index >= 0 && dv->index < sVm->currentFormVarCount) {
                 return &sVm->currentFormVars[dv->index];
             }
    +    } else if (dv->scope == SCOPE_MEMBER) {
    +        return memberVarSlot(dv);
         }
     
         return NULL;
    @@ -3691,6 +3852,12 @@ static void handleProjectCmd(int32_t cmd) {
                 }
                 break;
     
    +        case CMD_PRJ_NEW_CLASS:
    +            if (isIdle && hasProject()) {
    +                newClassModule();
    +            }
    +            break;
    +
             case CMD_PRJ_REMOVE: {
                 int32_t rmIdx = prjGetSelectedFileIdx();
     
    @@ -3853,7 +4020,7 @@ static void handleViewCmd(int32_t cmd) {
     
                 // The Shift+F7 accelerator bypasses the menu state: only a form
                 // opens in the designer.
    -            if (selFileIdx >= 0 && selFileIdx < prjFileCount(&sProject) && sProject.files[selFileIdx].isForm) {
    +            if (selFileIdx >= 0 && selFileIdx < prjFileCount(&sProject) && sProject.files[selFileIdx].kind == PrjKindFormE) {
                     activateFile(selFileIdx, ViewDesignE);
                 } else if (activeIsForm()) {
                     switchToDesign();
    @@ -3888,7 +4055,7 @@ static void handleViewCmd(int32_t cmd) {
                 // Activate selected form if not already active
                 int32_t selMenuIdx = prjGetSelectedFileIdx();
     
    -            if (selMenuIdx >= 0 && selMenuIdx != sProject.activeFileIdx && selMenuIdx < prjFileCount(&sProject) && sProject.files[selMenuIdx].isForm) {
    +            if (selMenuIdx >= 0 && selMenuIdx != sProject.activeFileIdx && selMenuIdx < prjFileCount(&sProject) && sProject.files[selMenuIdx].kind == PrjKindFormE) {
                     activateFile(selMenuIdx, ViewDesignE);
                 }
     
    @@ -4394,7 +4561,7 @@ static bool immTryAssign(const char *expr) {
     
         // Only scalars can be assigned; the RHS is evaluated as an expression
         BasValueT newVal;
    -    bool      scalar = slot->type == BAS_TYPE_INTEGER || slot->type == BAS_TYPE_LONG || slot->type == BAS_TYPE_SINGLE || slot->type == BAS_TYPE_DOUBLE || slot->type == BAS_TYPE_STRING || slot->type == BAS_TYPE_BOOLEAN;
    +    bool      scalar = slot->type == BAS_TYPE_INTEGER || slot->type == BAS_TYPE_LONG || slot->type == BAS_TYPE_SINGLE || slot->type == BAS_TYPE_DOUBLE || slot->type == BAS_TYPE_STRING || slot->type == BAS_TYPE_BOOLEAN || slot->type == BAS_TYPE_DATE;
     
         if (!scalar) {
             immPrintCallback(NULL, "Cannot assign to this variable type", true);
    @@ -4455,11 +4622,6 @@ static void initSyntaxMap(void) {
                 shput(sSyntaxMap, kw, TEXT_SYNTAX_KEYWORD);
             }
         }
    -
    -    // LIBRARY is a contextual keyword in DECLARE LIBRARY and not a
    -    // real token, but the colorizer still treats it as a keyword.
    -    shput(sSyntaxMap, "LIBRARY", TEXT_SYNTAX_KEYWORD);
    -    shput(sSyntaxMap, "STOP",    TEXT_SYNTAX_KEYWORD);
     }
     
     
    @@ -4476,6 +4638,19 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS
     }
     
     
    +// instanceTypeName -- what a class instance or Collection is, for the
    +// debugger: its class name, or "Collection"
    +static const char *instanceTypeName(const BasObjectT *obj) {
    +    const char *cls = objectClassName(obj);
    +
    +    if (cls) {
    +        return cls;
    +    }
    +
    +    return obj->classIdx == BAS_OBJ_COLLECTION ? IDE_COLLECTION_TYPE : IDE_OBJECT_TYPE;
    +}
    +
    +
     // invalidateCompiled -- the compiled module and source map describe the
     // project's previous file list; drop them (a running program keeps the
     // module it executes).
    @@ -4508,13 +4683,6 @@ static bool isCtrlArrayInDesigner(const char *ctrlName) {
     }
     
     
    -// isFormPath -- true when path names a .frm file (by its file name's
    -// extension; a directory may contain dots)
    -static bool isFormPath(const char *path) {
    -    return dvxHasExt(platformPathBaseName(path), IDE_FRM_EXT);
    -}
    -
    -
     // Check if position i is inside a string literal or comment (' or REM).
     // Scans from the start of the line containing i.
     static bool isInStringOrComment(const char *src, int32_t i) {
    @@ -4738,7 +4906,7 @@ static char *loadCompileSource(int32_t fileIdx, IdeValidatorCtxT *validator) {
         scan.validator = validator;
         scan.code      = NULL;
     
    -    if (sProject.files[fileIdx].isForm) {
    +    if (sProject.files[fileIdx].kind == PrjKindFormE) {
             frmScan(text, &scan);
     
             // The designer's form: its code is exactly what the editor numbers
    @@ -4800,6 +4968,7 @@ static void loadFile(void) {
         FileFilterT filters[] = {
             { "BASIC Files (*.bas)" },
             { "Form Files (*.frm)" },
    +        { "Class Modules (*.cls)" },
             { "All Files (*.*)" }
         };
     
    @@ -4846,7 +5015,7 @@ static void loadFormCodeIntoEditor(void) {
     // project file order.
     static void loadFrmFiles(BasFormRtT *rt) {
         for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
    -        if (!sProject.files[i].isForm) {
    +        if (sProject.files[i].kind != PrjKindFormE) {
                 continue;
             }
     
    @@ -4888,24 +5057,23 @@ static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fal
     }
     
     
    -// Convert an editor-local line number to a full-source line number.
    -// The editor shows one procedure at a time (sCurProcIdx), so we need
    -// to add the procedure's starting line offset within the file's code.
    -// For multi-file projects, we also add the file's offset in the
    -// concatenated source. The source map's startLine already accounts for
    -// the injected BEGINFORM directive, so no extra adjustment is needed.
    +// Convert an editor-local line number to a full-source line number, or
    +// IDE_NO_LINE when that line was not compiled (e.g. a class module's
    +// header).  The editor shows one procedure at a time (sCurProcIdx), so
    +// the procedure's starting line within the file's code is added, then
    +// the source map places the file's line in the concatenated source (its
    +// startLine already accounts for the injected BEGINFORM / BEGINCLASS).
     static int32_t localToConcatLine(int32_t editorLine) {
    -    // Add the current procedure's start offset within the file's code.
    -    // sCurProcIdx == -1 means (General) section which starts at line 1
    -    // of the file's code (not the concatenated source).
         int32_t fileLine = editorLineToCodeLine(editorLine);
     
    -    // For projects, add the file's offset in the concatenated source.
    -    // startLine is recorded AFTER the injected BEGINFORM line, so it
    -    // already points at the first real code line (matching prjMapLine).
    -    int32_t startLine = prjMapStartLine(&sProject, sEditorFileIdx);
    +    // Editor text bound to no project file is compiled on its own
    +    if (sEditorFileIdx < 0) {
    +        return fileLine;
    +    }
     
    -    return (startLine > 0) ? startLine + fileLine - 1 : fileLine;
    +    int32_t concatLine = prjMapFileLine(&sProject, sEditorFileIdx, fileLine);
    +
    +    return concatLine > 0 ? concatLine : IDE_NO_LINE;
     }
     
     
    @@ -4977,7 +5145,7 @@ static void makeExecutable(void) {
         char **frmSources = NULL; // stb_ds: owned form text
     
         for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
    -        if (!sProject.files[i].isForm) {
    +        if (sProject.files[i].kind != PrjKindFormE) {
                 continue;
             }
     
    @@ -5028,6 +5196,31 @@ static void makeExecutable(void) {
     }
     
     
    +// memberVarSlot -- member variable dv (SCOPE_MEMBER) of the object the
    +// innermost procedure runs for, or NULL when that procedure is not a
    +// procedure of dv's class
    +static BasValueT *memberVarSlot(const BasDebugVarT *dv) {
    +    if (sVm->callDepth <= 0) {
    +        return NULL;
    +    }
    +
    +    const BasValueT *me = &sVm->callStack[sVm->callDepth - 1].me;
    +
    +    if (me->type != BAS_TYPE_INSTANCE) {
    +        return NULL;
    +    }
    +
    +    BasObjectT *obj = me->instVal;
    +    const char *cls = objectClassName(obj);
    +
    +    if (!cls || strcasecmp(cls, dv->formName) != 0 || dv->index < 0 || dv->index >= obj->fieldCount) {
    +        return NULL;
    +    }
    +
    +    return &obj->fields[dv->index];
    +}
    +
    +
     static void navigateToBreakpoint(int32_t bpIdx) {
         if (bpIdx < 0 || bpIdx >= (int32_t)arrlen(sBreakpoints)) {
             return;
    @@ -5172,6 +5365,47 @@ static void navigateToNamedEventSub(const char *ctrlName, const char *eventName)
     }
     
     
    +// newClassModule -- add class module ClassN (the first N whose name no
    +// form or class of the project uses and whose file does not exist yet),
    +// holding the standard header, to the project and open it in the editor.
    +static void newClassModule(void) {
    +    char name[BAS_MAX_IDENT];
    +    char fileName[DVX_MAX_PATH];
    +    char path[DVX_MAX_PATH * 2];
    +
    +    for (int32_t n = 1; ; n++) {
    +        snprintf(name, sizeof(name), IDE_NEW_CLASS_FMT, (int)n);
    +        snprintf(fileName, sizeof(fileName), "%s%s", name, BAS_CLS_EXT);
    +        snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", sProject.projectDir, fileName);
    +
    +        if (!classNameUsed(name, fileName) && !prjPathExists(path)) {
    +            break;
    +        }
    +    }
    +
    +    char text[sizeof(IDE_NEW_CLASS_TEXT) + BAS_MAX_IDENT];
    +
    +    snprintf(text, sizeof(text), IDE_NEW_CLASS_TEXT, name);
    +
    +    bool    wasDirty = sProject.dirty;
    +    int32_t idx      = prjAddFile(&sProject, fileName, PrjKindClassE);
    +
    +    sProject.files[idx].buffer   = strdup(text);
    +    sProject.files[idx].modified = true;
    +
    +    if (!sProject.files[idx].buffer || !activateFile(idx, ViewCodeE)) {
    +        prjRemoveFile(&sProject, idx);
    +        sProject.dirty = wasDirty;
    +        return;
    +    }
    +
    +    invalidateCompiled();
    +    prjRebuildTree(&sProject);
    +    updateDirtyIndicators();
    +    setStatus("Class module added.");
    +}
    +
    +
     static void newProject(void) {
         if (!promptAndSave()) {
             return;
    @@ -5224,6 +5458,15 @@ static void newProject(void) {
     }
     
     
    +// objectClassName -- the class module name of class instance obj in the
    +// running program, or NULL (a Collection)
    +static const char *objectClassName(const BasObjectT *obj) {
    +    const BasModuleT *mod = sVm->module;
    +
    +    return (mod && obj->classIdx >= 0 && obj->classIdx < mod->classCount) ? mod->classes[obj->classIdx].name : NULL;
    +}
    +
    +
     static void onBreakpointHit(void *ctx, int32_t line) {
         (void)ctx;
     
    @@ -5459,13 +5702,9 @@ static void onCodeWinClose(WindowT *win) {
     }
     
     
    -static void onColorListChange(WidgetT *w) {
    -    (void)w;
    -    prefsUpdateColorSliders();
    -}
    -
    -
    -static void onColorSliderChange(WidgetT *w) {
    +// Preferences Colors tab: Change... (or a double-click on the list) picks
    +// a new color for the selected entry in the color dialog.
    +static void onColorChange(WidgetT *w) {
         (void)w;
     
         int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList);
    @@ -5474,14 +5713,15 @@ static void onColorSliderChange(WidgetT *w) {
             return;
         }
     
    -    uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR);
    -    uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG);
    -    uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB);
    +    if (dvxColorDialog(sAc, sSyntaxColorNames[idx], &sPrefsDlg.syntaxColors[idx])) {
    +        prefsUpdateColorSwatch();
    +    }
    +}
     
    -    prefsSetRgbLabels(r, g, b);
     
    -    sPrefsDlg.syntaxColors[idx] = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
    -    prefsUpdateSwatch();
    +static void onColorListChange(WidgetT *w) {
    +    (void)w;
    +    prefsUpdateColorSwatch();
     }
     
     
    @@ -6187,8 +6427,7 @@ static void onPrefsOk(WidgetT *w) {
     }
     
     
    -static void onPrjFileDblClick(int32_t fileIdx, bool isForm) {
    -    (void)isForm;
    +static void onPrjFileDblClick(int32_t fileIdx) {
         activateFile(fileIdx, ViewAutoE);
     }
     
    @@ -6563,7 +6802,7 @@ static void openProjectPath(const char *path) {
     }
     
     
    -// openSinglePath -- open a .bas/.frm: added to the current project, or
    +// openSinglePath -- open a .bas/.cls/.frm: added to the current project, or
     // used to create an implicit project when none is open.
     static void openSinglePath(const char *path) {
         if (hasProject()) {
    @@ -6796,41 +7035,37 @@ static bool prefsBuild(WindowT *win) {
         WidgetT *colorsPage = wgtTabPage(tabs, "Colors");
         WidgetT *colorsHBox = colorsPage ? wgtHBox(colorsPage) : NULL;
     
    -    // Left: color list.  Right: RGB sliders + value labels + swatch preview
    +    // Left: color list.  Right: swatch, values and Change...
         sPrefsDlg.colorList = colorsHBox ? wgtListBox(colorsHBox) : NULL;
     
    -    WidgetT *sliderBox = sPrefsDlg.colorList ? wgtVBox(colorsHBox) : NULL;
    +    WidgetT *colorBox = sPrefsDlg.colorList ? wgtVBox(colorsHBox) : NULL;
    +    WidgetT *caption  = colorBox ? wgtLabel(colorBox, "Color:") : NULL;
     
    -    if (!sliderBox) {
    +    sPrefsDlg.colorSwatch = caption ? wgtCanvas(colorBox, IDE_SWATCH_W, IDE_SWATCH_H) : NULL;
    +    sPrefsDlg.lblR        = sPrefsDlg.colorSwatch ? wgtLabel(colorBox, "") : NULL;
    +    sPrefsDlg.lblG        = sPrefsDlg.lblR ? wgtLabel(colorBox, "") : NULL;
    +    sPrefsDlg.lblB        = sPrefsDlg.lblG ? wgtLabel(colorBox, "") : NULL;
    +
    +    WidgetT *changeBtn = sPrefsDlg.lblB ? wgtButton(colorBox, "Change...") : NULL;
    +
    +    if (!changeBtn) {
             return false;
         }
     
    -    colorsPage->spacing           = wgtPixels(IDE_WIDGET_SPACING);
    -    colorsHBox->spacing           = wgtPixels(IDE_WIDGET_SPACING_LG);
    -    colorsHBox->weight            = WGT_WEIGHT_FILL;
    -    sPrefsDlg.colorList->weight   = WGT_WEIGHT_FILL;
    -    sPrefsDlg.colorList->onChange = onColorListChange;
    -    sliderBox->spacing            = wgtPixels(IDE_WIDGET_SPACING_SM);
    -    sliderBox->weight             = WGT_WEIGHT_FILL;
    +    colorsPage->spacing             = wgtPixels(IDE_WIDGET_SPACING);
    +    colorsHBox->spacing             = wgtPixels(IDE_WIDGET_SPACING_LG);
    +    colorsHBox->weight              = WGT_WEIGHT_FILL;
    +    sPrefsDlg.colorList->weight     = WGT_WEIGHT_FILL;
    +    sPrefsDlg.colorList->onChange   = onColorListChange;
    +    sPrefsDlg.colorList->onDblClick = onColorChange;
    +    colorBox->spacing               = wgtPixels(IDE_WIDGET_SPACING_SM);
    +    colorBox->weight                = WGT_WEIGHT_FILL;
    +    changeBtn->onClick              = onColorChange;
         wgtListBoxSetItems(sPrefsDlg.colorList, sSyntaxColorNames, TEXT_SYNTAX_MAX);
     
    -    sPrefsDlg.sliderR = prefsColorSlider(sliderBox, "Red:", &sPrefsDlg.lblR);
    -    sPrefsDlg.sliderG = prefsColorSlider(sliderBox, "Green:", &sPrefsDlg.lblG);
    -    sPrefsDlg.sliderB = prefsColorSlider(sliderBox, "Blue:", &sPrefsDlg.lblB);
    -
    -    if (!sPrefsDlg.sliderR || !sPrefsDlg.sliderG || !sPrefsDlg.sliderB || !wgtLabel(sliderBox, "Preview:")) {
    -        return false;
    -    }
    -
    -    sPrefsDlg.colorSwatch = wgtCanvas(sliderBox, IDE_SWATCH_W, IDE_SWATCH_H);
    -
    -    if (!sPrefsDlg.colorSwatch) {
    -        return false;
    -    }
    -
    -    // Start on the Keywords entry (the most commonly customized) and load sliders
    +    // Start on the Keywords entry (the most commonly customized)
         wgtListBoxSetSelected(sPrefsDlg.colorList, TEXT_SYNTAX_KEYWORD);
    -    prefsUpdateColorSliders();
    +    prefsUpdateColorSwatch();
         wgtTabControlSetActive(tabs, 0);
     
         // ---- OK / Cancel ----
    @@ -6852,36 +7087,12 @@ static bool prefsBuild(WindowT *win) {
     }
     
     
    -// prefsColorSlider -- "Caption:" label, 0..255 slider and value label for
    -// the Colors tab.  Returns the slider, NULL if a widget could not be made.
    -static WidgetT *prefsColorSlider(WidgetT *parent, const char *caption, WidgetT **outValueLabel) {
    -    WidgetT *slider = wgtLabel(parent, caption) ? wgtSlider(parent, 0, BAS_RGB_COMPONENT_MAX) : NULL;
    -    WidgetT *value  = slider ? wgtLabel(parent, "0") : NULL;
    +// prefsSetChannelLabel -- "Channel value" under the Colors tab swatch
    +static void prefsSetChannelLabel(WidgetT *label, const char *channel, uint8_t value) {
    +    char buf[IDE_RGB_LABEL_BUF];
     
    -    *outValueLabel = value;
    -
    -    if (!value) {
    -        return NULL;
    -    }
    -
    -    slider->onChange = onColorSliderChange;
    -    wgtLabelSetAlign(value, AlignEndE);
    -    return slider;
    -}
    -
    -
    -// prefsSetRgbLabels -- show the slider values next to the sliders
    -static void prefsSetRgbLabels(uint8_t r, uint8_t g, uint8_t b) {
    -    char rBuf[IDE_RGB_LABEL_BUF];
    -    char gBuf[IDE_RGB_LABEL_BUF];
    -    char bBuf[IDE_RGB_LABEL_BUF];
    -
    -    snprintf(rBuf, sizeof(rBuf), "%d", (int)r);
    -    snprintf(gBuf, sizeof(gBuf), "%d", (int)g);
    -    snprintf(bBuf, sizeof(bBuf), "%d", (int)b);
    -    wgtSetText(sPrefsDlg.lblR, rBuf);
    -    wgtSetText(sPrefsDlg.lblG, gBuf);
    -    wgtSetText(sPrefsDlg.lblB, bBuf);
    +    snprintf(buf, sizeof(buf), "%s %d", channel, (int)value);
    +    wgtSetText(label, buf);
     }
     
     
    @@ -6904,37 +7115,23 @@ static WidgetT *prefsTextRow(WidgetT *parent, const char *caption, int32_t maxLe
     }
     
     
    -static void prefsUpdateColorSliders(void) {
    +// Shows the selected syntax color in the swatch and as numbers.
    +static void prefsUpdateColorSwatch(void) {
         int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList);
     
         if (idx < 0 || idx >= TEXT_SYNTAX_MAX) {
             return;
         }
     
    -    uint32_t c = sPrefsDlg.syntaxColors[idx];
    -    uint8_t  r = (c >> 16) & 0xFF;
    -    uint8_t  g = (c >> 8) & 0xFF;
    -    uint8_t  b = c & 0xFF;
    +    uint32_t rgb = sPrefsDlg.syntaxColors[idx];
    +    uint8_t  r   = DVX_RGB_RED(rgb);
    +    uint8_t  g   = DVX_RGB_GREEN(rgb);
    +    uint8_t  b   = DVX_RGB_BLUE(rgb);
     
    -    wgtSliderSetValue(sPrefsDlg.sliderR, r);
    -    wgtSliderSetValue(sPrefsDlg.sliderG, g);
    -    wgtSliderSetValue(sPrefsDlg.sliderB, b);
    -    prefsSetRgbLabels(r, g, b);
    -    prefsUpdateSwatch();
    -}
    -
    -
    -static void prefsUpdateSwatch(void) {
    -    if (!sPrefsDlg.colorSwatch) {
    -        return;
    -    }
    -
    -    uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR);
    -    uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG);
    -    uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB);
    -
    -    uint32_t color = packColor(&sAc->display, r, g, b);
    -    wgtCanvasClear(sPrefsDlg.colorSwatch, color);
    +    prefsSetChannelLabel(sPrefsDlg.lblR, "Red", r);
    +    prefsSetChannelLabel(sPrefsDlg.lblG, "Green", g);
    +    prefsSetChannelLabel(sPrefsDlg.lblB, "Blue", b);
    +    wgtCanvasClear(sPrefsDlg.colorSwatch, packColor(&sAc->display, r, g, b));
     }
     
     
    @@ -7009,8 +7206,15 @@ static bool procBufContains(const char *hay, const char *needle, int32_t needleL
     
     
     // procDeclAt -- if p (already whitespace-skipped) starts a SUB or
    -// FUNCTION declaration, returns the text after the keyword; else NULL.
    +// FUNCTION declaration (Public or Private in front allowed), returns the
    +// text after the keyword; else NULL.
     static const char *procDeclAt(const char *p, bool *outIsSub) {
    +    if (kwMatch(p, KW_PUBLIC)) {
    +        p = dvxSkipWs(p + KW_LEN(KW_PUBLIC));
    +    } else if (kwMatch(p, KW_PRIVATE)) {
    +        p = dvxSkipWs(p + KW_LEN(KW_PRIVATE));
    +    }
    +
         if (kwMatch(p, KW_SUB)) {
             *outIsSub = true;
             return p + KW_LEN(KW_SUB);
    @@ -7641,11 +7845,13 @@ static BasValueT *resolveVarPath(const char *path, const char **endPtr) {
             slot = &slot->arrVal->elements[flatIdx];
         }
     
    -    // Parse optional UDT field: .fieldName
    +    // Parse optional UDT field or class member variable: .fieldName
         if (*p == '.') {
             p++; // skip '.'
     
    -        if (slot->type != BAS_TYPE_UDT || !slot->udtVal) {
    +        bool isInstance = slot->type == BAS_TYPE_INSTANCE;
    +
    +        if (!isInstance && (slot->type != BAS_TYPE_UDT || !slot->udtVal)) {
                 return NULL;
             }
     
    @@ -7665,13 +7871,23 @@ static BasValueT *resolveVarPath(const char *path, const char **endPtr) {
             memcpy(fieldName, fieldStart, fieldLen);
             fieldName[fieldLen] = '\0';
     
    -        int32_t fieldIdx = findUdtFieldIdx(slot->udtVal->typeId, fieldName);
    +        if (isInstance) {
    +            int32_t memberIdx = findMemberVarIdx(slot->instVal, fieldName);
     
    -        if (fieldIdx < 0 || fieldIdx >= slot->udtVal->fieldCount) {
    -            return NULL;
    +            if (memberIdx < 0) {
    +                return NULL;
    +            }
    +
    +            slot = &slot->instVal->fields[memberIdx];
    +        } else {
    +            int32_t fieldIdx = findUdtFieldIdx(slot->udtVal->typeId, fieldName);
    +
    +            if (fieldIdx < 0 || fieldIdx >= slot->udtVal->fieldCount) {
    +                return NULL;
    +            }
    +
    +            slot = &slot->udtVal->fields[fieldIdx];
             }
    -
    -        slot = &slot->udtVal->fields[fieldIdx];
         }
     
         if (endPtr) {
    @@ -7808,6 +8024,9 @@ static void runModule(BasModuleT *mod) {
         // events coming out of the user's program.
         formRt->suppressErrorDialog = true;
     
    +    // The program's screen window (console / graphics) carries its name
    +    snprintf(formRt->title, sizeof(formRt->title), "%s", sProject.name);
    +
         sVm            = vm;
         sDbgFormRt     = formRt;
         sDbgModule     = mod;
    @@ -7898,7 +8117,7 @@ static void runModule(BasModuleT *mod) {
         // Breakpoints inside event handlers pause inside the VM's own
         // spin (basVmCallSub -> doEventsCallback), so nothing here needs
         // to watch for DbgPausedE.
    -    if (result == BAS_VM_HALTED && (int32_t)arrlen(formRt->forms) > 0) {
    +    if (result == BAS_VM_HALTED && basFormRtKeepsRunning(formRt)) {
             setStatus("Running (event loop)...");
     
             // vm->errorMsg outliving an event dispatch means an unhandled
    @@ -7906,7 +8125,7 @@ static void runModule(BasModuleT *mod) {
             // when it takes over); without this test the program kept
             // running after the error and the report below never fired
             // until the user pressed Stop.
    -        while (sWin && sAc->running && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended && vm->errorMsg[0] == '\0') {
    +        while (sWin && sAc->running && basFormRtKeepsRunning(formRt) && !sStopRequested && !vm->ended && vm->errorMsg[0] == '\0') {
                 idePump();
             }
         }
    @@ -8311,7 +8530,7 @@ static void saveFile(void) {
         syncFormDirty();
     
         for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
    -        if (i != sProject.activeFileIdx && sProject.files[i].isForm && sProject.files[i].modified) {
    +        if (i != sProject.activeFileIdx && sProject.files[i].kind == PrjKindFormE && sProject.files[i].modified) {
                 writeProjectFile(i);
             }
         }
    @@ -8606,7 +8825,7 @@ static void showCodeView(int32_t fileIdx) {
         }
     
         if (reopened || sEditorFileIdx != fileIdx) {
    -        if (sProject.files[fileIdx].isForm) {
    +        if (sProject.files[fileIdx].kind == PrjKindFormE) {
                 loadFormCodeIntoEditor();
             } else {
                 // Stash first: it may replace this very file's buffer
    @@ -9259,7 +9478,7 @@ static void stashEditor(void) {
     
         PrjFileT *file = &sProject.files[sEditorFileIdx];
     
    -    if (file->isForm) {
    +    if (file->kind == PrjKindFormE) {
             stashFormCode();
             return;
         }
    @@ -9451,11 +9670,23 @@ static void toggleBreakpointLine(int32_t editorLine) {
             }
         }
     
    -    // Validate that this line is breakable (not blank, comment, or SUB/FUNCTION decl/end)
    +    // Validate that this line is breakable (not blank, comment, SUB/FUNCTION
    +    // decl/end or a class module's header)
         if (sEditor) {
             const char *text = wgtGetText(sEditor);
     
             if (text) {
    +            // A class module's header opens its (General) section
    +            if (sProject.files[fileIdx].kind == PrjKindClassE && sCurProcIdx == IDE_SECTION_GENERAL) {
    +                int32_t firstCodeLine = 1;
    +
    +                prjClassCodePos(text, (int32_t)strlen(text), &firstCodeLine);
    +
    +                if (editorLine < firstCodeLine) {
    +                    return;
    +                }
    +            }
    +
                 // Find the start of editorLine (1-based)
                 const char *p = text;
     
    @@ -9535,12 +9766,28 @@ static const char *typeNameStr(uint8_t dt) {
             case BAS_TYPE_BOOLEAN:
                 return "Boolean";
     
    +        case BAS_TYPE_DATE:
    +            return "Date";
    +
             case BAS_TYPE_ARRAY:
                 return "Array";
     
             case BAS_TYPE_UDT:
                 return "UDT";
     
    +        case BAS_TYPE_OBJECT:
    +        case BAS_TYPE_INSTANCE:
    +            return IDE_OBJECT_TYPE;
    +
    +        case BAS_TYPE_EMPTY:
    +            return "Empty";
    +
    +        case BAS_TYPE_NULL:
    +            return "Null";
    +
    +        case BAS_TYPE_VARIANT:
    +            return "Variant";
    +
             default:
                 return "?";
         }
    @@ -9760,9 +10007,16 @@ static void updateLocalsWindow(void) {
                     memset(&val, 0, sizeof(val));
                 }
     
    -            // Type column -- arrays show "Array(type)" with element type
    +            // Type column -- arrays show "type()" with the element type, an
    +            // object its class, a Variant "Variant/" and what it holds
    +            const char *valType = (val.type == BAS_TYPE_INSTANCE) ? instanceTypeName(val.instVal) : typeNameStr(val.type);
    +
                 if (dv->dataType == BAS_TYPE_ARRAY && val.arrVal) {
                     snprintf(typeBuf, sizeof(typeBuf), "%s()", typeNameStr(val.arrVal->elementType));
    +            } else if (dv->dataType == BAS_TYPE_VARIANT) {
    +                snprintf(typeBuf, sizeof(typeBuf), "%s/%s", typeNameStr(BAS_TYPE_VARIANT), valType);
    +            } else if (val.type == BAS_TYPE_INSTANCE) {
    +                snprintf(typeBuf, sizeof(typeBuf), "%s", valType);
                 } else {
                     snprintf(typeBuf, sizeof(typeBuf), "%s", typeNameStr(dv->dataType));
                 }
    @@ -9819,6 +10073,7 @@ static void updateProjectMenuState(void) {
         wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW,    isIdle);
         wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_OPEN,   isIdle);
         wmMenuItemSetEnabled(sWin->menuBar, CMD_OPEN,       isIdle);
    +    wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW_CLASS, project && isIdle);
         wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS,  project);
         wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, isIdle && prjGetSelectedFileIdx() >= 0);
     
    @@ -9840,7 +10095,7 @@ static void updateProjectMenuState(void) {
         // View menu -- consider both active file and project tree selection
         int32_t selIdx     = prjGetSelectedFileIdx();
         bool    selIsFile  = (project && selIdx >= 0 && selIdx < prjFileCount(&sProject));
    -    bool    selIsForm  = (selIsFile && sProject.files[selIdx].isForm);
    +    bool    selIsForm  = (selIsFile && sProject.files[selIdx].kind == PrjKindFormE);
         bool    canCode    = hasFile || selIsFile;
         bool    canDesign  = hasForm || selIsForm;
     
    @@ -9967,6 +10222,11 @@ static bool validatorIsMethodValid(void *ctx, const char *wgtType, const char *m
             return true;
         }
     
    +    // Drawing methods of forms and PictureBoxes
    +    if ((strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0 || strcasecmp(wgtType, BAS_PICTUREBOX_TYPE) == 0) && basFormRtIsDrawMember(methodName, false)) {
    +        return true;
    +    }
    +
         // Form-level methods
         if (strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0) {
             return strcasecmp(methodName, "Show") == 0 || strcasecmp(methodName, "Hide") == 0;
    @@ -10010,6 +10270,11 @@ static bool validatorIsPropValid(void *ctx, const char *wgtType, const char *pro
             return true;
         }
     
    +    // Drawing properties of forms and PictureBoxes
    +    if ((strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0 || strcasecmp(wgtType, BAS_PICTUREBOX_TYPE) == 0) && basFormRtIsDrawMember(propName, true)) {
    +        return true;
    +    }
    +
         // Form-level properties (the runtime's own table)
         if (strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0) {
             return basFormRtFindFormProp(propName) != NULL;
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.c b/src/apps/kpunch/dvxbasic/ide/ideProject.c
    index 4280d3e0..4ba4ce67 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideProject.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.c
    @@ -31,6 +31,9 @@
     //   File0 = MAIN.BAS
     //   File1 = UTILS.BAS
     //
    +//   [Classes]
    +//   File0 = STACK.CLS
    +//
     //   [Forms]
     //   File0 = FORM1.FRM
     //
    @@ -75,7 +78,7 @@
     #define PRJ_MODIFIED_SUFFIX " *"  // tree label suffix for an unsaved file
     #define PRJ_PLACEHOLDER_BPP 4 // bytes per pixel of the 1x1 icon placeholder
     #define PRJ_INI_KEY_LEN   16    // "FileNNN" INI key buffer size
    -#define PRJ_FILE_KEY_FMT  "File%d"  // INI key for an indexed module/form path
    +#define PRJ_FILE_KEY_FMT  "File%d"  // INI key for an indexed module/class/form path
     
     #define PPD_WIDTH    380
     #define PPD_HEIGHT   410
    @@ -96,6 +99,23 @@ static PrjFileClickFnT  sOnClick     = NULL;
     static PrjSelChangeFnT  sOnSelChange = NULL;
     static char            **sLabels  = NULL; // stb_ds array of strdup'd strings
     
    +// Per file kind: the .dbp section listing its files and its Project
    +// window group.
    +static const char *const sKindSections[PrjKindCountE] = {
    +    [PrjKindModuleE] = BAS_INI_SECTION_MODULES,
    +    [PrjKindClassE]  = BAS_INI_SECTION_CLASSES,
    +    [PrjKindFormE]   = BAS_INI_SECTION_FORMS
    +};
    +
    +static const char *const sKindGroups[PrjKindCountE] = {
    +    [PrjKindModuleE] = "Modules",
    +    [PrjKindClassE]  = "Classes",
    +    [PrjKindFormE]   = "Forms"
    +};
    +
    +// Project window group order
    +static const PrjFileKindE sGroupOrder[PrjKindCountE] = { PrjKindFormE, PrjKindModuleE, PrjKindClassE };
    +
     static struct {
         bool          done;
         bool          accepted;
    @@ -134,10 +154,10 @@ static void     ppdOnOk(WidgetT *w);
     static void     ppdReadInput(WidgetT *input, char *dst, int32_t dstSize);
     static void     prjDeriveDir(PrjStateT *prj, const char *dbpPath);
     static int32_t  prjFileIndexByPath(const PrjStateT *prj, const char *path);
    -static void     prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, const char *section, bool isForm);
    +static void     prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, PrjFileKindE kind);
     static void     prjLoadString(PrefsHandleT *h, const char *section, const char *key, char *dst, int32_t dstSize);
     static const char *prjMakeRelative(const PrjStateT *prj, const char *path);
    -static void     prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm);
    +static void     prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, PrjFileKindE kind);
     static void     prjSaveOptional(PrefsHandleT *h, const char *key, const char *value);
     static bool     validateIcon(const char *fullPath, bool showErrors);
     
    @@ -155,7 +175,7 @@ static void onTreeItemDblClick(WidgetT *w) {
         int32_t fileIdx = (int32_t)(intptr_t)w->userData;
     
         if (fileIdx >= 0 && fileIdx < prjFileCount(sPrj)) {
    -        sOnClick(fileIdx, sPrj->files[fileIdx].isForm);
    +        sOnClick(fileIdx);
         }
     }
     
    @@ -340,17 +360,39 @@ static void ppdReadInput(WidgetT *input, char *dst, int32_t dstSize) {
     }
     
     
    -int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) {
    +int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind) {
         PrjFileT entry;
         memset(&entry, 0, sizeof(entry));
         snprintf(entry.path, sizeof(entry.path), "%s", relativePath);
    -    entry.isForm = isForm;
    +    entry.kind = (uint8_t)kind;
         arrput(prj->files, entry);
         prj->dirty = true;
         return prjFileCount(prj) - 1;
     }
     
     
    +int32_t prjClassCodePos(const char *text, int32_t len, int32_t *outFirstLine) {
    +    int32_t codePos = basClassCodePos(text, len);
    +    int32_t line    = 1;
    +
    +    for (int32_t i = 0; i < codePos; i++) {
    +        if (text[i] == '\n') {
    +            line++;
    +        }
    +    }
    +
    +    *outFirstLine = line;
    +    return codePos;
    +}
    +
    +
    +bool prjClassName(const PrjStateT *prj, int32_t fileIdx, const char *text, char *out, int32_t outSize) {
    +    const char *src = text ? text : "";
    +
    +    return basExtractClassName(src, (int32_t)strlen(src), prj->files[fileIdx].path, out, outSize);
    +}
    +
    +
     void prjClose(PrjStateT *prj) {
         for (int32_t i = 0; i < prjFileCount(prj); i++) {
             free(prj->files[i].buffer);
    @@ -471,6 +513,21 @@ void prjInit(PrjStateT *prj) {
     }
     
     
    +PrjFileKindE prjKindForPath(const char *path) {
    +    const char *base = platformPathBaseName(path);
    +
    +    if (dvxHasExt(base, PRJ_FRM_EXT)) {
    +        return PrjKindFormE;
    +    }
    +
    +    if (dvxHasExt(base, BAS_CLS_EXT)) {
    +        return PrjKindClassE;
    +    }
    +
    +    return PrjKindModuleE;
    +}
    +
    +
     bool prjLoad(PrjStateT *prj, const char *dbpPath) {
         // prefsLoad treats a missing file as an empty INI, so probe first or a
         // deleted .dbp would "open" as an empty project.
    @@ -503,9 +560,10 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
         prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, prj->helpFile, sizeof(prj->helpFile));
         prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COMMANDARGS, prj->commandArgs, sizeof(prj->commandArgs));
     
    -    // [Modules] and [Forms] sections -- File0, File1, ... (no cap)
    -    prjLoadFileSection(prj, h, BAS_INI_SECTION_MODULES, false);
    -    prjLoadFileSection(prj, h, BAS_INI_SECTION_FORMS, true);
    +    // [Modules], [Classes] and [Forms] sections -- File0, File1, ... (no cap)
    +    for (int32_t kind = 0; kind < PrjKindCountE; kind++) {
    +        prjLoadFileSection(prj, h, (PrjFileKindE)kind);
    +    }
     
         // [Settings] section
         prjLoadString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm, sizeof(prj->startupForm));
    @@ -537,7 +595,7 @@ void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
             prj->files[i].buffer = buf;
     
             // Extract form name from .frm files
    -        if (prj->files[i].isForm) {
    +        if (prj->files[i].kind == PrjKindFormE) {
                 basExtractFormName(buf, prj->files[i].formName, sizeof(prj->files[i].formName));
             }
     
    @@ -549,20 +607,21 @@ void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
     }
     
     
    -// Load File0, File1, ... entries from an INI section, registering each as a
    -// project file.  The loop ends at the first missing key (no fixed cap).
    -static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, const char *section, bool isForm) {
    +// Load File0, File1, ... entries from the INI section of kind, registering
    +// each as a project file.  The loop ends at the first missing key (no
    +// fixed cap).
    +static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, PrjFileKindE kind) {
         for (int32_t i = 0; ; i++) {
             char key[PRJ_INI_KEY_LEN];
             snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)i);
     
    -        const char *val = prefsGetString(h, section, key, NULL);
    +        const char *val = prefsGetString(h, sKindSections[kind], key, NULL);
     
             if (!val) {
                 break;
             }
     
    -        prjAddFile(prj, val, isForm);
    +        prjAddFile(prj, val, kind);
         }
     }
     
    @@ -592,6 +651,25 @@ static const char *prjMakeRelative(const PrjStateT *prj, const char *path) {
     }
     
     
    +int32_t prjMapFileLine(const PrjStateT *prj, int32_t fileIdx, int32_t fileLine) {
    +    if (fileIdx < 0 || fileIdx >= prjFileCount(prj)) {
    +        return 0;
    +    }
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(prj->sourceMap); i++) {
    +        const BasSourceMapT *m = &prj->sourceMap[i];
    +
    +        if (strcmp(m->path, prj->files[fileIdx].path) == 0) {
    +            int32_t offset = fileLine - m->firstFileLine;
    +
    +            return (offset >= 0 && offset < m->lineCount) ? m->startLine + offset : 0;
    +        }
    +    }
    +
    +    return 0;
    +}
    +
    +
     bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) {
         const char *path      = NULL;
         int32_t     localLine = 0;
    @@ -612,21 +690,6 @@ bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, i
     }
     
     
    -int32_t prjMapStartLine(const PrjStateT *prj, int32_t fileIdx) {
    -    if (fileIdx < 0 || fileIdx >= prjFileCount(prj)) {
    -        return 0;
    -    }
    -
    -    for (int32_t i = 0; i < (int32_t)arrlen(prj->sourceMap); i++) {
    -        if (strcmp(prj->sourceMap[i].path, prj->files[fileIdx].path) == 0) {
    -            return prj->sourceMap[i].startLine;
    -        }
    -    }
    -
    -    return 0;
    -}
    -
    -
     void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs) {
         prjInit(prj);
         snprintf(prj->name, sizeof(prj->name), "%s", name);
    @@ -711,7 +774,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
             int32_t selectedIdx = 0;
     
             for (int32_t i = 0; i < prjFileCount(prj); i++) {
    -            if (!prj->files[i].isForm) {
    +            if (prj->files[i].kind != PrjKindFormE) {
                     continue;
                 }
     
    @@ -936,25 +999,23 @@ void prjRebuildTree(PrjStateT *prj) {
         projNode->userData = (void *)(intptr_t)-1;
         wgtTreeItemSetExpanded(projNode, true);
     
    -    // Forms and Modules groups
    -    char *formsLabel = strdup("Forms");
    -    arrput(sLabels, formsLabel);
    -    WidgetT *formsNode = wgtTreeItem(projNode, formsLabel);
    -    formsNode->userData = (void *)(intptr_t)-1;
    -    wgtTreeItemSetExpanded(formsNode, true);
    +    // One group per file kind (the group names are static text)
    +    WidgetT *groupNodes[PrjKindCountE];
     
    -    char *modsLabel = strdup("Modules");
    -    arrput(sLabels, modsLabel);
    -    WidgetT *modsNode = wgtTreeItem(projNode, modsLabel);
    -    modsNode->userData = (void *)(intptr_t)-1;
    -    wgtTreeItemSetExpanded(modsNode, true);
    +    for (int32_t g = 0; g < PrjKindCountE; g++) {
    +        PrjFileKindE kind = sGroupOrder[g];
    +
    +        groupNodes[kind]           = wgtTreeItem(projNode, sKindGroups[kind]);
    +        groupNodes[kind]->userData = (void *)(intptr_t)-1;
    +        wgtTreeItemSetExpanded(groupNodes[kind], true);
    +    }
     
         for (int32_t i = 0; i < prjFileCount(prj); i++) {
             char buf[DVX_MAX_PATH + sizeof(PRJ_MODIFIED_SUFFIX)];
             snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? PRJ_MODIFIED_SUFFIX : "");
             char *label = strdup(buf);
             arrput(sLabels, label);
    -        WidgetT *item = wgtTreeItem(prj->files[i].isForm ? formsNode : modsNode, label);
    +        WidgetT *item = wgtTreeItem(groupNodes[prj->files[i].kind], label);
             item->userData   = (void *)(intptr_t)i;
             item->onDblClick = onTreeItemDblClick;
         }
    @@ -969,7 +1030,7 @@ void prjRemoveFile(PrjStateT *prj, int32_t idx) {
         }
     
         // A removed startup form is no longer a valid startup form.
    -    if (prj->files[idx].isForm && prj->files[idx].formName[0] && strcasecmp(prj->files[idx].formName, prj->startupForm) == 0) {
    +    if (prj->files[idx].kind == PrjKindFormE && prj->files[idx].formName[0] && strcasecmp(prj->files[idx].formName, prj->startupForm) == 0) {
             prj->startupForm[0] = '\0';
         }
     
    @@ -1017,9 +1078,10 @@ bool prjSave(const PrjStateT *prj) {
         prjSaveOptional(h, BAS_INI_KEY_HELPFILE, prj->helpFile);
         prjSaveOptional(h, BAS_INI_KEY_COMMANDARGS, prj->commandArgs);
     
    -    // [Modules] and [Forms] sections
    -    prjSaveFileSection(h, prj, BAS_INI_SECTION_MODULES, false);
    -    prjSaveFileSection(h, prj, BAS_INI_SECTION_FORMS, true);
    +    // [Modules], [Classes] and [Forms] sections
    +    for (int32_t kind = 0; kind < PrjKindCountE; kind++) {
    +        prjSaveFileSection(h, prj, (PrjFileKindE)kind);
    +    }
     
         // [Settings] section
         prefsSetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm);
    @@ -1031,16 +1093,16 @@ bool prjSave(const PrjStateT *prj) {
     }
     
     
    -// Write the project files whose isForm flag matches into an INI section as
    -// File0, File1, ...  (readers stop at the first missing key).
    -static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm) {
    +// Write the project files of kind into its INI section as File0, File1,
    +// ...  (readers stop at the first missing key).
    +static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, PrjFileKindE kind) {
         int32_t idx = 0;
     
         for (int32_t i = 0; i < prjFileCount(prj); i++) {
    -        if (prj->files[i].isForm == isForm) {
    +        if (prj->files[i].kind == kind) {
                 char key[PRJ_INI_KEY_LEN];
                 snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)idx++);
    -            prefsSetString(h, section, key, prj->files[i].path);
    +            prefsSetString(h, sKindSections[kind], key, prj->files[i].path);
             }
         }
     }
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.h b/src/apps/kpunch/dvxbasic/ide/ideProject.h
    index ca11ede1..a266ebcc 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideProject.h
    +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.h
    @@ -59,6 +59,23 @@
     #define IDE_PREF_SECTION_EDITOR     "editor"
     #define IDE_PREF_KEY_OPTION_EXPLICIT "optionExplicit"
     
    +// Form file extension; a class module's is BAS_CLS_EXT (basRes.h).  A
    +// file's kind follows its extension (see prjKindForPath).
    +#define PRJ_FRM_EXT        ".frm"
    +
    +// ============================================================
    +// Project file kinds
    +// ============================================================
    +
    +// The order is the compile order: modules, then class modules, then
    +// forms (see basBuildConcatProject).
    +typedef enum {
    +    PrjKindModuleE,     // .bas: [Modules]
    +    PrjKindClassE,      // .cls: [Classes]
    +    PrjKindFormE,       // .frm: [Forms]
    +    PrjKindCountE
    +} PrjFileKindE;
    +
     // ============================================================
     // Project file entry
     // ============================================================
    @@ -68,7 +85,7 @@ typedef struct {
         char    *buffer;             // in-memory edit buffer (malloc'd, NULL = not loaded)
         char     path[DVX_MAX_PATH]; // relative path (8.3 DOS name)
         char     formName[BAS_MAX_IDENT]; // form object name (from "Begin Form ")
    -    bool     isForm;             // true = .frm, false = .bas
    +    uint8_t  kind;               // PrjFileKindE
         bool     modified;           // true = buffer has unsaved changes
     } PrjFileT;
     
    @@ -79,7 +96,7 @@ typedef struct {
     typedef struct {
         // Fields are ordered by alignment to minimize struct padding.
         PrjFileT       *files;                      // stb_ds dynamic array (prjFileCount)
    -    BasSourceMapT  *sourceMap;                  // stb_ds array: path = files[].path, firstFileLine = 1
    +    BasSourceMapT  *sourceMap;                  // stb_ds array: path = files[].path, firstFileLine = first code line
         int32_t         activeFileIdx; // index of file open in editor (-1 = none)
         char            name[PRJ_MAX_NAME];
         char            projectPath[DVX_MAX_PATH]; // full path to .dbp file
    @@ -102,11 +119,24 @@ typedef struct {
     // Project management
     // ============================================================
     
    -int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm);
    +int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind);
    +
    +// Offset of the first line of code in the text of a class module (len
    +// bytes), just past its header; *outFirstLine receives that line's
    +// 1-based number.
    +int32_t prjClassCodePos(const char *text, int32_t len, int32_t *outFirstLine);
    +
    +// The class name of project file fileIdx (a class module) given its
    +// text: its Attribute VB_Name, else its file name.  False when neither
    +// gives one.
    +bool    prjClassName(const PrjStateT *prj, int32_t fileIdx, const char *text, char *out, int32_t outSize);
     void    prjClose(PrjStateT *prj);
     int32_t prjFileCount(const PrjStateT *prj);
     void    prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize);
     void    prjInit(PrjStateT *prj);
    +
    +// The kind of source file path names, by its file name's extension.
    +PrjFileKindE prjKindForPath(const char *path);
     bool    prjLoad(PrjStateT *prj, const char *dbpPath);
     void    prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx);
     void    prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs);
    @@ -136,15 +166,16 @@ void    prjSingleLine(char *dst, int32_t dstSize, const char *src);
     // outLocalLine to the originating file and line within that file.
     bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine);
     
    -// The concatenated line holding line 1 of file fileIdx's code, or 0 when
    -// the file is not in the map (not compiled since it was added).
    -int32_t prjMapStartLine(const PrjStateT *prj, int32_t fileIdx);
    +// The concatenated line holding line fileLine of file fileIdx, or 0 when
    +// the file is not in the map (not compiled since it was added) or the
    +// line is not compiled code (e.g. a class module's header).
    +int32_t prjMapFileLine(const PrjStateT *prj, int32_t fileIdx, int32_t fileLine);
     
     // ============================================================
     // Project window UI
     // ============================================================
     
    -typedef void (*PrjFileClickFnT)(int32_t fileIdx, bool isForm);
    +typedef void (*PrjFileClickFnT)(int32_t fileIdx);
     typedef void (*PrjSelChangeFnT)(void);
     
     WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick, PrjSelChangeFnT onSelChange);
    diff --git a/src/apps/kpunch/dvxbasic/ideguide.dhs b/src/apps/kpunch/dvxbasic/ideguide.dhs
    index 1b38047e..764ce1dc 100644
    --- a/src/apps/kpunch/dvxbasic/ideguide.dhs
    +++ b/src/apps/kpunch/dvxbasic/ideguide.dhs
    @@ -88,6 +88,7 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
     .index Close Project
     .index Project Properties
     .index Add File
    +.index New Class Module
     .index Save File
     .index Save All
     .index Remove File
    @@ -105,7 +106,8 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
       ---
       Project Properties...             Edit project metadata (name, author, version, startup form, icon, etc.) and the command line arguments used when the program is run from the IDE.
       ---
    -  Add File...            Ctrl+O     Add a .bas or .frm file to the project. If no project is open, creates an implicit project from the file. A file that cannot be read, or is 64 KB or larger, is refused with a message.
    +  Add File...            Ctrl+O     Add a .bas, .frm or .cls file to the project. If no project is open, creates an implicit project from the file. A file that cannot be read, or is 64 KB or larger, is refused with a message.
    +  New Class Module                  Add a new, empty class module (Class1.cls, Class2.cls, ...) to the project and open it in the Code Editor. See Class Modules under Project System.
       Save File              Ctrl+S     Save the active file to disk.
       Save All                          Save all modified files in the project.
       ---
    @@ -116,7 +118,7 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
       Exit                              Close the IDE (prompts to save unsaved changes).
     .endtable
     
    -A project (.dbp) or source file (.bas, .frm) named on the IDE's command line opens when the IDE starts, so double-clicking one in the File Manager, or starting it from the Program Manager, opens it in the IDE.
    +A project (.dbp) or source file (.bas, .frm, .cls) named on the IDE's command line opens when the IDE starts, so double-clicking one in the File Manager, or starting it from the Program Manager, opens it in the IDE.
     
     Files are saved exactly as edited (LF line endings). Each save writes a temporary file first and replaces the original only when the write succeeded. Opening a project that fails to load keeps the current project open and removes the failed entry from the Recent list.
     
    @@ -380,7 +382,7 @@ The editor applies real-time syntax coloring as you type. Seven categories are h
       --------          --------
       Default Text      Identifiers, whitespace.
       Keywords          IF, THEN, FOR, NEXT, SUB, FUNCTION, DIM, PRINT, SELECT, CASE, DO, LOOP, WHILE, WEND, END, EXIT, CALL, GOSUB, GOTO, RETURN, DECLARE, CONST, TYPE, AND, OR, NOT, XOR, MOD, etc.
    -  Types             INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN, TRUE, FALSE
    +  Types             INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN, DATE, VARIANT, OBJECT, COLLECTION, TRUE, FALSE, NOTHING, EMPTY, NULL
       Strings           "Hello, World!"
       Comments          ' This is a comment, REM This is a comment
       Numbers           42, 3.14, &HFF
    @@ -447,6 +449,9 @@ Forms have the following design-time properties: Name, Caption, Width, Height, L
     .index .dbp Files
     .index Project Explorer
     .index Source Map
    +.index Class Modules in the IDE
    +.index .cls Files
    +.index Classes Section
     
     .h1 Project System
     
    @@ -460,12 +465,29 @@ A DVX BASIC project is stored as a .dbp file (DVX BASIC Project). The project fi
     .item Metadata -- Author, Company, Version, Copyright, Description (a single line), and Icon Path (for compiled binaries).
     .item Arguments -- the command line the program gets (COMMAND$) when it is run from the IDE, for trying out a program that opens the file it is given. It is not built into the compiled program: there COMMAND$ returns whatever the program was started with.
     .item Help File -- a compiled help file (.hlp) for the program. It is copied next to the compiled program, and F1 in the running program opens it, at the HelpTopic of the focused control or, failing that, of the form. Add help.bas to the project and call HelpView to open it from a Help menu.
    -.item File list -- relative paths of all .bas and .frm files in the project. Each entry tracks whether it is a form file.
    +.item File list -- relative paths of all the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section and forms (.frm) in the [Forms] section.
     .endlist
     
    +The file sections all have the same shape: one FileN line per file, numbered from 0, and a Count line:
    +
    +.code
    +[Modules]
    +File0 = main.bas
    +Count = 1
    +
    +[Classes]
    +File0 = account.cls
    +File1 = ledger.cls
    +Count = 2
    +
    +[Forms]
    +File0 = form1.frm
    +Count = 1
    +.endcode
    +
     .h2 How Projects are Compiled
     
    -When the project is compiled, all source files are concatenated into a single source stream. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form.
    +When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module is likewise marked with its class name; a class module's header lines are not compiled.
     
     .h2 Compile-Time Validation
     
    @@ -478,7 +500,7 @@ Before the source is handed to the compiler, the IDE scans every .frm file and c
     .item Menu items -- only Name (read), Checked and Enabled can be used on a menu item in code; Caption, Visible and RadioCheck are design-time settings.
     .endlist
     
    -These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.
    +These checks use the same metadata the Properties panel and Toolbox read from each installed control, so property and method names always match the current widget set without any separate list to maintain.
     
     .h2 Project Operations
     
    @@ -489,14 +511,39 @@ These checks use the same metadata the Properties panel and Toolbox read from ea
       Open Project          Opens a .dbp file and loads all referenced files into memory. A project file that is 64 KB or larger, or cannot be read, is not loaded; opening it later reports "Cannot load : unreadable or larger than 65535 bytes." Carriage returns are removed when files are loaded.
       Save Project          Writes the .dbp file to disk.
       Close Project         Closes the project, prompting to save unsaved changes.
    -  Add File              Adds a .bas or .frm file to the project. Opening a file without a project auto-creates an implicit project.
    +  Add File              Adds a .bas, .frm or .cls file to the project. Opening a file without a project auto-creates an implicit project.
    +  New Class Module      Adds a new class module to the project and opens it (see Class Modules below).
       Remove File           Removes a file from the project (prompts to save if modified). Removing the startup form clears the Startup Form setting.
       Project Properties    Opens a dialog for editing project metadata (name, author, company, version, copyright, description, icon, help file, startup form) and the Arguments for runs from the IDE.
     .endtable
     
     .h2 Project Explorer
     
    -The Project Explorer is a tree view window listing all project files. Double-click a file to open it: .bas files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.
    +The Project Explorer is a tree view window listing all project files, in three groups: Forms, Modules and Classes. Double-click a file to open it: .bas and .cls files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.
    +
    +.h2 Class Modules
    +
    +A class module (.cls file) defines a kind of object for your program: its member variables, properties and methods. The BASIC Language Reference describes how to write one (see Objects and Class Modules); this section covers working with them in the IDE.
    +
    +.list
    +.item Create -- choose File > New Class Module. The IDE adds a new class module named Class1 (Class2, and so on, when that name is taken) to the Classes group, saved in the project's directory as Class1.cls, and opens it in the Code Editor. The new file holds only its header and Option Explicit; it is written to disk when you save it.
    +.item Add an existing one -- choose File > Add File and pick a .cls file (the Class Modules file type). Class files from Visual Basic can be added as they are.
    +.item Open -- double-click it in the Project Explorer's Classes group. It opens in Code view; a class module has no designer.
    +.item Save -- File > Save File or Save All, as for any other file. The project file lists class modules in its [Classes] section.
    +.item Remove -- select it in the Project Explorer and choose File > Remove File. The file itself stays on disk.
    +.item Rename -- a class's name is the one in its Attribute VB_Name line, not the file name. To rename the class, edit that line (and the code that uses the old name) and save; the file keeps its name. A class module without an Attribute VB_Name line takes its name from the file name.
    +.endlist
    +
    +A new class module starts with these header lines:
    +
    +.code
    +VERSION DVX 1.00 CLASS
    +Attribute VB_Name = "Class1"
    +
    +Option Explicit
    +.endcode
    +
    +Leave the VERSION and Attribute lines at the top of the file; they are not compiled as code. Each class name must be different from the other class names and the form names in the project.
     
     .link ide.overview Back to Overview
     
    @@ -689,16 +736,19 @@ Shows variables for the current execution scope. Displayed as a three-column Lis
       Column   Content
       ------   -------
       Name     Variable name (internal mangled names like static variable placeholders are filtered out).
    -  Type     Data type: Integer, Long, Single, Double, String, Boolean, Array, UDT. Array types show the element type (e.g., Integer()).
    +  Type     Data type: Integer, Long, Single, Double, String, Boolean, Date, Variant, Array, UDT, or the type of object. Array types show the element type (e.g., Integer()).
       Value    Current value. Strings are shown in quotes. Booleans as True/False. Arrays show bounds and element count (e.g., Integer(0 To 9) [10]). Uninitialized arrays show (uninitialized).
     .endtable
     
    +Variants show the value they currently hold, or Empty or Null. Object variables show Nothing, or which object they refer to: the class name of a class instance, a Collection with its number of items, or the form or control.
    +
     The Locals window displays:
     
     .list
     .item Local variables of the current procedure, including its STATIC variables and the module variables it names in a SHARED statement.
     .item Module-level variables. While execution is paused inside a SUB or FUNCTION, only the variables declared with DIM SHARED (or REDIM SHARED) are listed, because the procedure cannot see the others. This also applies inside a DEF FN function, although a DEF FN can read every module variable.
     .item Form-scoped variables for the current form (if the program is executing within a form context), with the same SHARED rule.
    +.item Member variables of the object, while execution is paused in a procedure of a class module: the variables of the very object the procedure was called for (Me), Private ones included.
     .endlist
     
     The Watch window and the Immediate window follow the same rules. The window is resizable.
    @@ -860,6 +910,7 @@ If the assignment target cannot be resolved (unknown variable, out-of-bounds ind
     .toc 1 Output Window
     .index Output Window
     .index PRINT Output
    +.index Program Screen Window
     .index Runtime Errors
     .index Compile Errors
     
    @@ -868,14 +919,18 @@ If the assignment target cannot be resolved (unknown variable, out-of-bounds ind
     The Output window is a read-only TextArea at the bottom-left of the screen. It displays:
     
     .list
    -.item PRINT output -- all PRINT statement output from the running program is appended here.
    +.item PRINT output -- PRINT statement output from the running program is appended here, until the program uses a console or graphics statement (CLS, LOCATE, COLOR, SCREEN, LINE, INKEY$ and the others): from then on PRINT and INPUT use the program's own screen window instead (see Console and Graphics in the language reference).
     .item Runtime errors -- if the VM encounters a runtime error (division by zero, out-of-bounds, etc.), the error message and line number are displayed in the output with an Error on line N: prefix.
     .item Compile errors -- if compilation fails, the error message and location are shown.
     .endlist
     
     The output buffer holds up to 32,768 characters; when it is full the oldest lines are dropped. The window always shows the newest output. Use Run > Clear Output to clear it.
     
    -INPUT statements prompt the user via a modal InputBox dialog; the prompt text is also echoed to the Output window.
    +INPUT statements prompt the user via a modal InputBox dialog; the prompt text is also echoed to the Output window.  A program that has its own screen window types its INPUT there instead.
    +
    +.h2 The Program's Screen Window
    +
    +A program that uses the console or graphics statements gets a window of its own, the size of its screen mode (80x25 text, or 640x480 or 320x200 graphics), titled with the project's name. Alt+Enter in that window (or FULLSCREEN in the program) shows it on the whole screen; Alt+Tab brings the desktop back and the program continues. Keys typed in the window go to the program (INKEY$ and INPUT). The window stays open after the program's code has finished, so its output can be read; closing it ends the program.
     
     .link ide.overview Back to Overview
     
    @@ -961,7 +1016,7 @@ These fields set the default values for new project metadata:
     
     .h2 Colors Tab
     
    -Customizes the syntax-highlighting colors used in the code editor. The left side is a list of seven color entries (Default Text, Keywords, Strings, Comments, Numbers, Operators, Types). Select an entry and adjust its red, green, and blue components using the three sliders on the right. The Preview swatch shows the current color. Changes are applied to the open editor when you click OK.
    +Customizes the syntax-highlighting colors used in the code editor. The left side is a list of seven color entries (Default Text, Keywords, Strings, Comments, Numbers, Operators, Types). Select an entry to see its color, and its red, green and blue amounts, on the right. Click Change... (or double-click the entry) to pick a new color in the color box: click a basic color or mix one with the Red, Green and Blue sliders or number boxes, then click OK. Changes are applied to the open editor when you click OK in the Preferences dialog.
     
     .link ide.overview Back to Overview
     
    @@ -1058,6 +1113,24 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item SHARED naming a variable that does not exist at module level creates it there; in a form's code it becomes a variable of that form, not a program-wide one.
     .endlist
     
    +.h2 Objects, Classes and Variants
    +
    +.list
    +.item New: class modules (.cls files) define your own objects, with Public and Private member variables, Subs and Functions, Property Get / Let / Set, Class_Initialize and Class_Terminate, Me and a default member. Objects are made with NEW (DIM x AS NEW Account, SET x = NEW Account), compared with Is and tested with TypeOf ... Is. An object is freed, and its Class_Terminate run, as soon as nothing refers to it any more.
    +.item New: the Collection object (Add, Remove, Item, Count, with optional text keys) and FOR EACH ... NEXT over Collections and arrays.
    +.item New: the Variant type (DIM v AS VARIANT) with the special values Empty and Null, and the functions VARTYPE, TYPENAME, ISEMPTY, ISNULL, ISNUMERIC, ISOBJECT, ISARRAY and CVAR, with the vb... VarType constants and vbObjectError.
    +.item New: PUBLIC and PRIVATE at module level (PUBLIC x is DIM SHARED x, PRIVATE x is DIM x).
    +.item New error numbers: 94 Invalid use of Null, 424 Object required, 438 Object doesn't support this property or method, 450 Wrong number of arguments or invalid property assignment, 451 Object not a collection, 457 duplicate Collection key.
    +.item Changed: CHOOSE with an index outside its list returns Null instead of raising error 5.
    +.item Changed: a PARAMARRAY without an AS clause is an array of Variants (TYPENAME "Variant()").
    +.item A form or control name can be used directly wherever an object is expected: SET c = Text1, either side of Is, TypeOf, and an argument for an object parameter.
    +.item NEW, PROPERTY, PUBLIC, PRIVATE and TYPEOF are now reserved words: a variable, SUB or FUNCTION with one of these names must be renamed. VARIANT, OBJECT, COLLECTION, EMPTY, NULL and EACH are colored like keywords but are not reserved.
    +.item IDE: File > New Class Module, .cls files in Add File, a Classes group in the Project Explorer and a [Classes] section in the project file. The debugger shows Variants, objects and the member variables of the object whose code is running.
    +.endlist
    +
    +.link lang.classes See: Objects and Class Modules
    +.link lang.variants See: Variants
    +
     .h2 Sound, Joysticks and Frames
     
     .list
    diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs
    index 623f2f01..6027ff10 100644
    --- a/src/apps/kpunch/dvxbasic/langref.dhs
    +++ b/src/apps/kpunch/dvxbasic/langref.dhs
    @@ -31,6 +31,13 @@
     .index Double
     .index String
     .index Boolean
    +.index Date
    +.index Control Type
    +.index Form Type
    +.index Object Type
    +.index Date Literals
    +.index Variant Type
    +.index Class Types
     
     .h1 Data Types
     
    @@ -47,8 +54,37 @@ DVX BASIC supports the following data types. Each type has a corresponding type
       Double     8 bytes    #        64-bit float, approximately 15 digits precision
       String     variable   $        Variable-length, reference-counted, dynamic string
       Boolean    2 bytes    (none)   True (-1) or False (0)
    +  Date       8 bytes    (none)   A date and time of day from 1/1/100 to 12/31/9999
    +  Variant    varies     (none)   Any value at all: a number, text, a date, an object, an array, or the special values Empty and Null (see Variants)
     .endtable
     
    +.h2 Object Types
    +
    +An object variable does not hold a value of its own; it refers to an object that lives elsewhere, so two variables can refer to the same object. Store a reference with Set, use the object's properties and methods with a dot, and set the variable to Nothing when you are done with it.
    +
    +.table
    +  Declared As     Holds
    +  -----------     -----
    +  Control         A control on a form (any type of control)
    +  Form            A form
    +  Object          Any object: a form, a control, an instance of a class module or a Collection
    +  ClassName       An instance of that class module (see Objects and Class Modules)
    +  Collection      A Collection object (see Collection Object)
    +.endtable
    +
    +.code
    +Dim c As Control
    +Set c = Text1
    +c.Text = "Hello"
    +
    +Dim people As New Collection
    +people.Add "Ann"
    +.endcode
    +
    +An object variable that has not been Set holds Nothing. Control, Form and Object variables accept each other's values; a variable declared As a class name or As Collection accepts only an object of that kind, or Nothing: an object of another kind raises error 13 (Type mismatch), and a value that is not an object at all raises error 424 (Object required). The Source parameter of the DragDrop and DragOver events is a Control.
    +
    +.link lang.classes See also: Objects and Class Modules
    +
     .h2 Internal Types
     
     These types are not directly declarable but are used internally by the runtime.
    @@ -58,7 +94,7 @@ These types are not directly declarable but are used internally by the runtime.
       -------------   -----------
       Array           Reference-counted multi-dimensional array (up to 8 dimensions)
       UDT             User-defined type instance (created with TYPE...END TYPE)
    -  Object          Opaque host object (form reference, control reference)
    +  Object          Reference to a form, a control, a class instance or a Collection; declarable as Control, Form, Object, a class name or Collection
       Ref             ByRef pointer to a variable slot (used for ByRef parameters)
     .endtable
     
    @@ -102,7 +138,7 @@ When mixing types in expressions, values are automatically promoted to a common
     
     .h2 Assignment Conversion
     
    -Storing a value into a variable declared as Integer, Long, Single or Boolean (by suffix, by AS clause, by DEFINT/DEFLNG/DEFSNG, or as a typed parameter, STATIC variable, FUNCTION result, TYPE field or array element) converts the value to that type. DEFtype statements also give parameters, STATIC variables and FUNCTION results their default type. Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has.
    +Storing a value into a variable declared as Integer, Long, Single, Boolean or Date (by suffix, by AS clause, by DEFINT/DEFLNG/DEFSNG, or as a typed parameter, STATIC variable, FUNCTION result, TYPE field or array element) converts the value to that type. DEFtype statements also give parameters, STATIC variables and FUNCTION results their default type. Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. Date takes a number as a date's number of days and reads a string as a date (error 13 when the text is not a date, error 6 outside 1/1/100 to 12/31/9999); INPUT into a Date variable reads the date the same way. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has.
     
     .code
     Dim n As Integer
    @@ -119,7 +155,152 @@ x = 1 / 3          ' x was never declared and keeps the Double: prints 0.3333333
     
     Boolean values use -1 for True and 0 for False. Any non-zero numeric value is treated as True in a conditional context. The keywords True and False are reserved and may be used anywhere a Boolean value is expected.
     
    +.h2 Date Values
    +
    +A Date holds a calendar date and a time of day, as in Visual Basic: the whole part of its number counts days from 12/30/1899 and the fraction is the time (0.5 is noon, so 36526.5 is noon on 1/1/2000). Days before 12/30/1899 are negative numbers whose time still counts forward from midnight: -1.25 is 6:00 AM on 12/29/1899. CDBL gives a date's number and CDATE turns a number back into a date.
    +
    +Write a date literal between # signs. The month comes before the day; a year can also come first, and month names (full or abbreviated) work too. A time can follow the date or stand alone:
    +
    +.code
    +Dim d As Date
    +d = #1/15/2000#                  ' January 15, 2000
    +d = #2000-01-15 15:30#           ' year first, 24-hour time
    +d = #Jan 15, 2000 3:30:00 PM#
    +d = #3:30 PM#                    ' a time alone is on 12/30/1899
    +.endcode
    +
    +Two-digit years 00 to 29 are 2000 to 2029 and 30 to 99 are 1930 to 1999. Date is not a reserved word, so a program may still use Date (or any of the date function names) for its own variables and procedures; the program's own name wins.
    +
    +PRINT, STR$, CSTR and the & operator show a date as m/d/yyyy h:mm:ss AM (1/15/2000 3:30:00 PM). A date at midnight shows only the date and a time on 12/30/1899 only the time; a Date variable that was never assigned is 12:00:00 AM. Use FORMAT$ for any other layout.
    +
    +Adding a number of days to a date, or subtracting one, gives a date (fractions are parts of a day, so d + 0.25 is six hours later); subtracting one date from another gives the number of days between them as a Double. Comparisons put dates in time order, and a string compared with a date is read as a date:
    +
    +.code
    +Dim due As Date
    +due = Date + 30                     ' thirty days from today
    +If due > #12/31/2025# Then Print "Next year"
    +Print #3/1/2000# - #2/1/2000#       ' 29 (2000 is a leap year)
    +If due = "1/15/2026" Then Print "Match"
    +.endcode
    +
    +A Date variable has no Empty or Null state (a Variant does): a date that is out of range is error 6 (Overflow), and text that is not a date is error 13 (Type mismatch).
    +
     .link lang.func.conversion See also: Conversion Functions
    +.link lang.variants See also: Variants
    +
    +.topic lang.variants
    +.title Variants
    +.toc 1 Variants
    +.index Variant
    +.index VARIANT
    +.index Empty
    +.index Null
    +.index VARTYPE
    +.index TYPENAME
    +.index ISEMPTY
    +.index ISNULL
    +.index ISNUMERIC
    +.index ISOBJECT
    +.index ISARRAY
    +.index CVAR
    +.index Invalid use of Null
    +
    +.h1 Variants
    +
    +A Variant is a variable that can hold any kind of value: a number, text, a date, True or False, an object or a whole array. It takes on the type of whatever is stored in it, and can change type each time something new is stored. A Variant can also hold two special values that no other type has: Empty (nothing has been stored yet) and Null (the value is unknown or missing).
    +
    +.code
    +Dim v As Variant
    +Print TypeName(v)       ' Empty
    +v = 42
    +Print TypeName(v)       ' Integer
    +v = "forty-two"
    +Print TypeName(v)       ' String
    +v = #1/15/2000#
    +Print TypeName(v)       ' Date
    +v = Null
    +Print IsNull(v)         ' True
    +.endcode
    +
    +Declare a Variant with AS VARIANT. Arrays of Variants (DIM list(10) AS VARIANT), Variant parameters, and FUNCTIONs and properties AS VARIANT work the same way; each element of a Variant array starts Empty. A variable declared with DIM but no AS clause, or never declared at all, is not a Variant: it keeps the older rules described under Assignment Conversion.
    +
    +Storing a Variant in a variable of another type converts the value to that type, as for any other assignment. A Variant that is given an array refers to that same array: a change made through either name is seen through the other, as when one array variable is assigned to another. Index it like the array itself, v(1).
    +
    +.h2 Empty
    +
    +Empty means that nothing has been stored in the Variant yet. A new Variant is Empty, and assigning Empty makes it Empty again. In arithmetic Empty counts as 0 and in text as "", so a counter kept in a Variant needs no starting value; PRINT shows nothing for it.
    +
    +.code
    +Dim total As Variant
    +total = total + 5       ' Empty counts as 0: total is 5
    +If IsEmpty(v) Then Print "Nothing stored yet"
    +.endcode
    +
    +.h2 Null
    +
    +Null means "no valid value" -- for example a field that was never filled in. Null spreads through most expressions: anything calculated from a Null is Null too, so a missing value is not mistaken for a real one.
    +
    +.table
    +  Expression                                    Result
    +  ----------                                    ------
    +  Null + 1, Null * x, -Null, Not Null           Null
    +  Null = 1, Null < x, Null = Null               Null (a comparison with Null is never True)
    +  Abs(Null), Int(Null), Fix(Null), Sgn(Null)    Null
    +  "Name: " & Null                               "Name: " (& treats Null as "")
    +  Null & Null                                   Null
    +  False And Null                                False
    +  True Or Null                                  True
    +  False Imp Null, Null Imp True                 True
    +  Any other And, Or, Xor, Eqv or Imp with Null  Null
    +.endtable
    +
    +An IF, ELSEIF, WHILE, DO or LOOP condition that is Null counts as False. Any other use that needs a real number -- storing Null in a numeric, Boolean or Date variable, CINT(Null) and the other conversion functions, a string function such as LEN or UCASE$, or an array subscript -- raises error 94 (Invalid use of Null).
    +
    +Because Null = Null is itself Null, test for Null with ISNULL, never with =:
    +
    +.code
    +If IsNull(price) Then
    +    Print "No price yet"
    +Else
    +    Print "Price: "; price
    +End If
    +.endcode
    +
    +Empty and Null are not reserved words: a program that declares its own variable or constant named Empty or Null keeps using it.
    +
    +.h2 Variant Functions
    +
    +.table
    +  Function      Returns   Description
    +  --------      -------   -----------
    +  VARTYPE(v)    Integer   A number for the kind of value v holds (see VarType Constants): vbEmpty (0), vbNull (1), vbInteger (2), vbString (8), vbObject (9) and so on. For an array it is vbArray (8192) plus the element type, so an Integer array is 8194 and a Variant array 8204.
    +  TYPENAME(v)   String    The name of the kind of value: "Integer", "Long", "Single", "Double", "String", "Boolean", "Date", "Empty", "Null", the name of a TYPE, "Integer()" for an array, and for objects the class name, "Collection", "Nothing", or the type of a form or control.
    +  ISEMPTY(v)    Boolean   True when v is Empty.
    +  ISNULL(v)     Boolean   True when v is Null.
    +  ISNUMERIC(v)  Boolean   True when v is a number, True or False, Empty, or text that reads as a number ("12", "1.5"). False for Null, dates and other text.
    +  ISOBJECT(v)   Boolean   True when v is an object reference, including Nothing.
    +  ISARRAY(v)    Boolean   True when v is an array.
    +  ISDATE(v)     Boolean   True for a Date or text holding a date (see Date and Time Functions).
    +  CVAR(v)       Variant   v unchanged, as a Variant.
    +.endtable
    +
    +.code
    +Sub Describe(ByVal v As Variant)
    +    Select Case VarType(v)
    +        Case vbEmpty
    +            Print "(empty)"
    +        Case vbNull
    +            Print "(unknown)"
    +        Case vbString
    +            Print "Text: "; v
    +        Case Else
    +            Print TypeName(v); ": "; v
    +    End Select
    +End Sub
    +.endcode
    +
    +.link lang.datatypes See also: Data Types
    +.link lang.constants See also: VarType Constants
     
     .topic lang.operators
     .title Operators
    @@ -133,6 +314,8 @@ Boolean values use -1 for True and 0 for False. Any non-zero numeric value is tr
     .index EQV
     .index IMP
     .index MOD
    +.index IS
    +.index TYPEOF
     
     .h1 Operators
     
    @@ -148,7 +331,7 @@ Operators listed from highest precedence (evaluated first) to lowest precedence
       5              MOD                       Modulus
       6              +  -                      Addition, subtraction (+ also concatenates strings)
       7              &                         String concatenation
    -  8              =  <>  <  >  <=  >=       Comparison (returns Boolean)
    +  8              =  <>  <  >  <=  >=  Is   Comparison (returns Boolean)
       9              NOT                       Logical/bitwise NOT
       10             AND                       Logical/bitwise AND
       11             XOR                       Logical/bitwise exclusive-or
    @@ -184,13 +367,24 @@ Comparison operators return True (-1) or False (0).
       >          Greater than
       <=         Less than or equal to
       >=         Greater than or equal to
    +  Is         Both sides refer to the same object (objects only)
     .endtable
     
    -String comparisons are controlled by OPTION COMPARE. With OPTION COMPARE BINARY (the default) strings are compared by byte value. With OPTION COMPARE TEXT comparisons are case-insensitive.
    +String comparisons are controlled by OPTION COMPARE. With OPTION COMPARE BINARY (the default) strings are compared by byte value. With OPTION COMPARE TEXT comparisons are case-insensitive. A comparison with Null gives Null (see Variants).
    +
    +Is compares object references, not values: a Is b is True when both refer to the very same object, and x Is Nothing tests for an object variable that holds no object. Either side may be a form or control name. Using Is on something that is not an object raises error 424 (Object required).
    +
    +TypeOf object Is TypeName is True when the object is of that type. TypeName can be a class module's name, Collection, Form, Control (any control), Object (any object that is not Nothing), or a control type such as TextBox. It is False for Nothing. Use Not TypeOf ... Is ... for the opposite test.
    +
    +.code
    +If player Is Nothing Then Set player = New Player
    +If a Is b Then Print "Same object"
    +If TypeOf Source Is TextBox Then Print Source.Text
    +.endcode
     
     .h2 Logical / Bitwise Operators
     
    -The logical operators work on Boolean values and also perform bitwise operations on integer values.
    +The logical operators work on Boolean values and also perform bitwise operations on integer values. With Null they give Null unless the other side decides the answer (False And Null is False, True Or Null is True); see Variants.
     
     .table
       Operator   Description
    @@ -245,6 +439,8 @@ REM This is a comment
     .link lang.io Input/Output (PRINT, INPUT, DATA/READ)
     .link lang.misc Miscellaneous Statements (ON ERROR, SHELL, SLEEP, END)
     .link lang.sound Sound, Joysticks and Frames (BEEP, SOUND, PLAY, STICK, STRIG, FRAMEWAIT)
    +.link lang.classes Objects and Class Modules (NEW, PROPERTY, Class_Initialize, Me)
    +.link lang.collection Collection Object (Add, Remove, Item, Count)
     
     .topic lang.declarations
     .title Declaration Statements
    @@ -254,6 +450,8 @@ REM This is a comment
     .index CONST
     .index TYPE
     .index END TYPE
    +.index ENUM
    +.index END ENUM
     .index DECLARE
     .index DECLARE LIBRARY
     .index SHARED
    @@ -269,7 +467,14 @@ REM This is a comment
     .index DEFSTR
     .index LET
     .index SWAP
    +.index LSET
    +.index RSET
     .index ERASE
    +.index PUBLIC
    +.index PRIVATE
    +.index SET
    +.index NEW
    +.index Nothing
     
     .h1 Declaration Statements
     
    @@ -285,6 +490,7 @@ DIM variable(dim1, dim2, ...) AS type
     DIM variable() AS type
     DIM variable AS UdtName
     DIM variable AS STRING * n
    +DIM variable AS NEW ClassName
     DIM SHARED variable AS type
     DIM variable1 [AS type], variable2 [AS type], ...
     .endcode
    @@ -316,6 +522,8 @@ A module-level variable is visible inside a SUB or FUNCTION only when it is decl
     
     Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.
     
    +DIM variable AS NEW ClassName (or AS NEW Collection) creates a new object at the DIM and stores it in the variable; without NEW an object variable starts as Nothing. See Objects and Class Modules.
    +
     .h2 Variable Scope and SHARED
     
     DVX BASIC follows the QBasic scope rules:
    @@ -325,7 +533,7 @@ DVX BASIC follows the QBasic scope rules:
     .item A SUB or FUNCTION sees a module variable only when it was declared with DIM SHARED at module level, or when the procedure names it in a SHARED statement. This applies to arrays as well.
     .item Any other name used inside a SUB or FUNCTION is a local variable of that procedure, even when a module variable of the same name exists. With OPTION EXPLICIT, using such a name without a DIM is a compile error that says the module variable is not SHARED.
     .item Indexing (name(...)) or dotting (name.member) a module variable the procedure cannot see is a compile error.
    -.item CONSTs declared at module level, TYPEs, SUBs, FUNCTIONs, forms and controls are visible everywhere; no SHARED is needed for them.
    +.item CONSTs declared at module level, TYPEs, SUBs, FUNCTIONs, class modules, forms and controls are visible everywhere; no SHARED is needed for them. PUBLIC at module level is the same as DIM SHARED (see PUBLIC / PRIVATE).
     .item A DEF FN is module-level code: its expression sees every module variable.
     .endlist
     
    @@ -352,6 +560,17 @@ SHARED variable [()] [AS type] [, variable [()] [AS type] ...]
     
     SHARED gives the procedure access to the named module variables (write name() for an array). The AS type, when given, must match the module variable's declaration. Naming a variable that does not exist at module level creates it there (in a form's code, as a variable of that form).
     
    +.h2 PUBLIC / PRIVATE
    +
    +At module level, PUBLIC and PRIVATE can be used in place of DIM, as in Visual Basic. PUBLIC declares a variable that every SUB and FUNCTION can use (the same as DIM SHARED); PRIVATE declares one that only module-level code sees (the same as DIM).
    +
    +.code
    +Public highScore As Long        ' same as Dim Shared highScore As Long
    +Private tries As Integer        ' same as Dim tries As Integer
    +.endcode
    +
    +PUBLIC and PRIVATE may also be written in front of SUB and FUNCTION. Outside a class module they change nothing, because procedures are visible to the whole program anyway. In a class module they decide what code outside the class can use; see Objects and Class Modules. Inside a procedure, use DIM.
    +
     .h2 REDIM
     
     Reallocates an array, optionally preserving existing data. The array may have been declared with DIM name() or with bounds, or not declared at all (REDIM then declares a dynamic array).
    @@ -371,11 +590,12 @@ ReDim Preserve scores(1 To newCount) As Integer
     
     .h2 CONST
     
    -Declares a named constant. The value must be a literal (integer, long, float, string, or boolean). Constants are not variables -- you cannot assign a new value to them or pass them by reference.
    +Declares a named constant. The value is worked out once, when the program is compiled, and can never change -- you cannot assign a new value to a constant or pass it by reference. Several constants can be declared in one CONST statement, separated by commas.
     
     .code
    -CONST name = value
    -CONST name AS type = value
    +CONST name = expression
    +CONST name AS type = expression
    +CONST name = expression, name = expression, ...
     .endcode
     
     .code
    @@ -384,9 +604,26 @@ Const PI = 3.14159265
     Const APP_NAME = "DVX App"
     Const DEBUG_MODE = True
     Const HEADER As String = "=== Report ==="
    +Const LAUNCH = #7/20/1969 8:17 PM#
    +Const LAST_INDEX = MAX_SIZE - 1, HALF = MAX_SIZE \ 2
    +Const TITLE = APP_NAME & " " & "1.0"
    +Const CRLF = Chr$(13) & Chr$(10)
    +Const TEAL = RGB(0, 128, 128)
    +Const BIG = MAX_SIZE > 50
     .endcode
     
    -With an AS clause the literal is converted to that type: Integer and Long values are rounded and must fit the type, and a string literal for a numeric type (or a number for String) is a compile error. Without an AS clause an integer literal makes a Long constant and a literal with a fraction a Double constant.
    +The value can be any expression built from:
    +
    +.list
    +.item literals (numbers, strings, True and False, #date# literals);
    +.item constants declared earlier, including ENUM members (Member or EnumName.Member);
    +.item the arithmetic operators + - * / \ MOD ^, the comparison operators, AND OR NOT XOR EQV IMP, and string joining with & or +;
    +.item these built-in functions: ABS, ASC, ATN, CBOOL, CDATE, CDBL, CHOOSE, CHR$, CINT, CLNG, COS, CSNG, CSTR, DATEADD, DATEDIFF, DATEPART, DATESERIAL, DATEVALUE, DAY, EXP, FIX, FORMAT$, GETBLUE, GETGREEN, GETRED, HEX$, HOUR, IIF, INSTR, INSTRREV, INT, ISDATE, LCASE$, LEFT$, LEN, LOG, LTRIM$, MID$, MINUTE, MONTH, MONTHNAME, OCT$, REPLACE$, RGB, RIGHT$, RTRIM$, SECOND, SGN, SIN, SPACE$, SQR, STR$, STRCOMP, STRING$, STRREVERSE$, TAN, TIMESERIAL, TIMEVALUE, TRIM$, UCASE$, VAL, WEEKDAY, WEEKDAYNAME and YEAR.
    +.endlist
    +
    +Anything else -- a variable, a FUNCTION of your own, or a built-in whose answer can change from run to run such as TIMER, NOW, RND or DIR$ -- is a compile error ("Constant expression required"). An expression that fails, such as a division by zero or a result too big for its type, is also reported when the program is compiled. Text comparisons in a constant follow the OPTION COMPARE setting in effect where the constant is declared. A string constant can be at most 255 characters long.
    +
    +Without an AS clause the constant takes the type of its value: a whole number makes a Long constant, a number with a fraction a Double constant, and strings, Booleans and dates keep their type. With an AS clause the value is converted to that type: Integer and Long values are rounded and must fit the type, and a string for a numeric type (or a number for String) is a compile error.
     
     Constants declared at module level are visible to all procedures. A CONST declared inside a SUB or FUNCTION is local to that procedure; it may reuse the name of a module constant without changing it.
     
    @@ -416,6 +653,42 @@ p.age = 30
     
     UDT fields can themselves be UDTs (nested types), and field access chains through array elements too: shapes(i).origin.x = 5. A new instance starts with numeric fields at 0, Boolean fields False, variable strings empty and STRING * n fields padded to n spaces. TYPE variables can be written to and read from RANDOM and BINARY files with PUT and GET (see GET / PUT).
     
    +.h2 ENUM...END ENUM
    +
    +Defines a set of named whole-number constants, and a type name for variables that hold one of them.
    +
    +.code
    +ENUM EnumName
    +    member
    +    member = expression
    +    ...
    +END ENUM
    +.endcode
    +
    +.code
    +Enum Direction
    +    North           ' 0
    +    East            ' 1
    +    South = 10
    +    West            ' 11
    +End Enum
    +
    +Enum Style
    +    Bold = 1
    +    Italic = Bold * 2
    +    Underline = Style.Italic * 2
    +    AllStyles = Bold Or Italic Or Underline
    +End Enum
    +
    +Dim heading As Direction
    +heading = West
    +If heading = Direction.West Then Print "Going west"
    +.endcode
    +
    +The first member is 0 and each member without a value is one more than the member before it. A value may be any constant expression (see CONST), including members declared earlier in the same ENUM or in another one. Members are Long constants and can be used on their own (West) or with the ENUM name in front (Direction.West). If two ENUMs have a member with the same name, that name must be written with its ENUM name.
    +
    +The ENUM name can be used as a type wherever a type is allowed: DIM, parameters, FUNCTION results and TYPE fields. Such a variable is a Long, so it can hold any whole number, not only the members. ENUMs are declared at module level (not inside a SUB or FUNCTION) and are visible everywhere in the program; declare an ENUM before the code that uses its members.
    +
     .h2 DECLARE
     
     Forward-declares a SUB or FUNCTION. This is rarely required because the compiler supports forward references within a module, but it is still accepted for compatibility:
    @@ -465,6 +738,8 @@ Sub Counter()
     End Sub
     .endcode
     
    +In a class module, a STATIC variable is shared by every object of the class; use a member variable for a value each object keeps separately.
    +
     .h2 OPTION
     
     Sets compiler options. Must appear before any executable code.
    @@ -521,21 +796,49 @@ Swap a, b
     Swap names(i), names(j)
     .endcode
     
    +.h2 LSET / RSET
    +
    +Stores a string into a string variable without changing the variable's length: LSET places the value at the left and RSET at the right, filling the rest with spaces. A value longer than the variable is cut off at the right.
    +
    +.code
    +LSET variable = expression
    +RSET variable = expression
    +.endcode
    +
    +.code
    +Dim field As String
    +field = Space$(8)
    +LSet field = "Name"          ' "Name    "
    +RSet field = "42"            ' "      42"
    +.endcode
    +
    +The target may be an array element or a TYPE field. It must hold a string; any other value raises error 13 (Type mismatch).
    +
     .h2 SET
     
    -Assigns an object reference (form or control) to a variable. Required when creating a form or control with CreateForm / CreateControl so that the returned reference is stored in the variable:
    +Stores an object reference -- a form, a control, an instance of a class module, a Collection, or Nothing -- in a variable, array element, TYPE field or property. After SET both names refer to the same object; nothing is copied.
     
     .code
     SET variable = objectExpression
    +SET variable = NEW ClassName
    +SET variable = Nothing
     .endcode
     
     .code
    -Dim frm As Long
    +Dim frm As Form
     Set frm = CreateForm("MyForm", 320, 240)
     frm.Caption = "Built in code"
    +
    +Dim c As Control
    +Set c = Text1                  ' a control on the form
    +c.Text = "Hello"
    +
    +Dim list As Collection
    +Set list = New Collection      ' a new, empty Collection
    +Set list = Nothing             ' let it go
     .endcode
     
    -For ordinary numeric or string assignment, SET is not used.
    +The value must be an object or Nothing; anything else raises error 424 (Object required). SET is not used for ordinary numeric or string assignment.
     
     .h2 ERASE
     
    @@ -644,6 +947,8 @@ CASE items can be combined with commas. The IS keyword allows comparison operato
     .index UNTIL
     .index EXIT FOR
     .index EXIT DO
    +.index FOR EACH
    +.index EACH
     
     .h1 Loop Statements
     
    @@ -671,6 +976,37 @@ The variable name after NEXT is optional. Use EXIT FOR to break out early.
     
     With an Integer or Long counter a constant STEP must be a whole number (STEP 0.5 is a compile error); a STEP computed at run time is rounded to the counter's type. A Single or Double counter takes any step.
     
    +.h2 FOR EACH...NEXT
    +
    +Runs the loop once for every item in a Collection or every element of an array, without counting.
    +
    +.code
    +FOR EACH item IN group
    +    statements
    +NEXT [item]
    +.endcode
    +
    +.code
    +Dim names As New Collection
    +names.Add "Ann"
    +names.Add "Bob"
    +
    +Dim person As Variant
    +For Each person In names
    +    Print "Hello, "; person
    +Next person
    +
    +Dim scores(1 To 3) As Integer
    +Dim s As Variant
    +For Each s In scores
    +    total = total + s
    +Next
    +.endcode
    +
    +group can be a Collection, an array (of any number of dimensions; elements are visited in row order, the last subscript changing fastest), a Variant holding an array, or a ParamArray. On each pass item receives a copy of the next item: for a Collection of objects it refers to the object itself, but assigning a new value to item does not change the array or Collection. Declare item AS VARIANT, or as an object type for a Collection of objects. The number of items is checked on every pass, so items added or removed inside the loop are taken into account.
    +
    +EXIT FOR leaves the loop early. Looping over something that is not an array or Collection raises error 13 (Type mismatch), or error 451 (Object not a collection) for an object that is not a Collection. EACH and IN are not reserved words.
    +
     .h2 DO...LOOP
     
     General-purpose loop with pre-test, post-test, or infinite forms.
    @@ -725,8 +1061,10 @@ Wend
     .index END FUNCTION
     .index DEF FN
     .index BYVAL
    +.index PARAMARRAY
     .index EXIT SUB
     .index EXIT FUNCTION
    +.index OPTIONAL
     
     .h1 Procedures
     
    @@ -784,6 +1122,35 @@ Announce "hello"            ' loud defaults to 0
     Announce "hello", True      ' loud is -1
     .endcode
     
    +.h3 ParamArray
    +
    +The last parameter of a SUB or FUNCTION can be marked PARAMARRAY to accept any number of extra arguments. It is written as an array, name(), and receives every remaining argument as an array numbered from 0. With no extra arguments the array is empty: UBOUND returns -1.
    +
    +.code
    +Sub PrintAll(ByVal title As String, ParamArray items())
    +    Dim i As Integer
    +    Print title;
    +    For i = 0 To UBound(items)
    +        Print " "; items(i);
    +    Next
    +    Print
    +End Sub
    +
    +PrintAll "Totals:", 1, "two", 3.5    ' three items: 0 To 2
    +PrintAll "Nothing:"                  ' UBound(items) is -1
    +
    +Function Sum(ParamArray n() As Double) As Double
    +    Dim i As Integer
    +    Dim total As Double
    +    For i = 0 To UBound(n)
    +        total = total + n(i)
    +    Next
    +    Sum = total
    +End Function
    +.endcode
    +
    +Without an AS clause (and no type suffix or DEFtype rule for the name) the ParamArray is an array of Variants (TYPENAME gives "Variant()"): each element holds the argument that was passed with its own type, so numbers, strings and objects can be mixed, and FOR EACH can walk it. With an AS clause every argument is converted to that type. Arguments are passed by value: changing an element does not change the caller's variable. A PARAMARRAY cannot be combined with OPTIONAL parameters, cannot be BYVAL or BYREF, cannot hold a TYPE, and takes at most 31 arguments. It is not available in DEF FN or DECLARE LIBRARY.
    +
     .h2 FUNCTION...END FUNCTION
     
     Defines a function with a return value.
    @@ -857,8 +1224,11 @@ EXIT FOR
     EXIT DO
     EXIT SUB
     EXIT FUNCTION
    +EXIT PROPERTY
     .endcode
     
    +EXIT PROPERTY leaves a Property Get, Let or Set procedure of a class module (see Objects and Class Modules).
    +
     .h2 CALL
     
     Explicitly calls a subroutine or function. The return value (if any) is discarded.
    @@ -933,7 +1303,7 @@ If the expression evaluates to 1, control goes to the first label; 2, the second
     
     .h2 PRINT
     
    -Writes expressions to the output window (or to a file channel).
    +Writes expressions to the output window, or to a file channel. Once the program has used a console or graphics statement (see Console and Graphics), PRINT writes on the program's screen instead, at the cursor, in the current COLOR.
     
     .code
     PRINT [expression [{; | ,} expression] ...]
    @@ -1021,7 +1391,7 @@ Print Using "## and ##"; 1; 2; 3          ' " 1 and  2 3 and "
     
     .h2 INPUT
     
    -Reads a line of text from the user or from a file channel.
    +Reads a line of text from the user or from a file channel. The user types it in a dialog box, or on the program's screen once it has one (see Console and Graphics).
     
     .code
     INPUT variable
    @@ -1136,7 +1506,13 @@ ErrorHandler:
       68       Device unavailable (no printer to print on)
       75       Path/file access error
       76       Path not found
    -  91       Object variable not set (property, method, Show/Hide/Unload on a value that is not a form or control)
    +  91       Object variable not set (property, method, Show/Hide/Unload on a value that is not a form or control, or on Nothing)
    +  94       Invalid use of Null (Null where a real number or text is needed; see Variants)
    +  424      Object required (SET, Is or TypeOf with a value that is not an object)
    +  438      Object doesn't support this property or method (the object has no member of that name, or it is Private)
    +  450      Wrong number of arguments or invalid property assignment
    +  451      Object not a collection (FOR EACH over an object that is not a Collection)
    +  457      This key is already associated with an element of this collection (Collection.Add with a key in use)
       1000     No form runtime (a form or control statement ran without the form runtime, e.g. under a headless runner)
     .endtable
     
    @@ -1301,6 +1677,235 @@ Do
     Loop Until quitting
     .endcode
     
    +.topic lang.screen
    +.title Console and Graphics
    +.toc 2 Console and Graphics
    +.index CLS
    +.index LOCATE
    +.index COLOR
    +.index SCREEN
    +.index SCREEN function
    +.index WIDTH
    +.index CSRLIN
    +.index POS
    +.index INKEY$
    +.index LINE INPUT
    +.index PSET
    +.index PRESET
    +.index LINE (graphics)
    +.index CIRCLE
    +.index PAINT
    +.index DRAW
    +.index POINT
    +.index PALETTE
    +.index PALETTE USING
    +.index VIEW
    +.index WINDOW
    +.index FULLSCREEN
    +.index Full screen
    +.index Console
    +
    +.h1 Console and Graphics
    +
    +These statements work as they do in QBasic. The first one a program uses gives it a screen of its own: a window, titled with the program's name, the size of the screen mode. From then on PRINT and INPUT use that screen too (before it, PRINT writes to the output window and INPUT asks in a dialog box, as always). Keys typed in the window go to INKEY$ and INPUT. The window stays open when the program's code has finished, so its output can be read; closing it ends the program.
    +
    +The names are not reserved: a program that has its own variable, SUB or constant called Color, Cls, Draw and so on keeps using it.
    +
    +.h2 Screen Modes
    +
    +.table
    +  Mode        Size and colours                       Text
    +  ----        ----------------                       ----
    +  SCREEN 0    Text only, 16 colours                  80 columns, 25 rows (WIDTH 80, 50 for 50 rows)
    +  SCREEN 12   640 x 480 graphics, 16 colours         80 columns, 30 rows (WIDTH 80, 60 for 60 rows)
    +  SCREEN 13   320 x 200 graphics, 256 colours        40 columns, 25 rows
    +.endtable
    +
    +A program starts in SCREEN 0. SCREEN mode (and WIDTH) clears the screen and restores the mode's colours: the 16 standard colours, and in SCREEN 13 the standard 256-colour palette. Other mode numbers, and graphics statements in SCREEN 0, are error 5 (Illegal function call). SCREEN's colour switch and page arguments are accepted and ignored (there is one page), and PCOPY is not supported.
    +
    +.code
    +SCREEN mode
    +WIDTH [columns] [, rows]
    +.endcode
    +
    +.h2 FULLSCREEN
    +
    +Shows the program's screen on the whole screen, or back in its window. Alt+Enter in the window does the same. While the program has the whole screen, Alt+Tab brings the desktop back (the program keeps running in its window) and switching back to the program returns it to the whole screen. Other programs keep running either way. When the computer has no suitable video mode, the screen stays in its window.
    +
    +.code
    +FULLSCREEN [ON | OFF]
    +FULLSCREEN expression      ' True: whole screen, False: window
    +.endcode
    +
    +.h2 Text: CLS, LOCATE, COLOR, CSRLIN, POS, SCREEN( )
    +
    +.code
    +CLS [n]
    +LOCATE [row] [, [column] [, cursor]]
    +COLOR [foreground] [, background]
    +row = CSRLIN
    +column = POS(0)
    +code = SCREEN(row, column [, colorflag])
    +.endcode
    +
    +.list
    +.item CLS clears the screen and puts the cursor in the top-left corner. In a graphics mode with a VIEW set, CLS (or CLS 1) clears only the view; CLS 0 and CLS 2 clear everything.
    +.item LOCATE moves the cursor (rows and columns count from 1). cursor 1 shows the text cursor in SCREEN 0, 0 hides it (the default). QBasic's cursor size arguments are accepted and ignored.
    +.item COLOR in SCREEN 0 takes a foreground 0 to 31 (16 to 31 blink in QBasic; here they show steady) and a background 0 to 15. In SCREEN 12 and 13 it takes only a foreground (0 to 15, or 0 to 255 in SCREEN 13), which is also the default colour of the drawing statements.
    +.item CSRLIN is the cursor's row and POS(0) its column. Before the program has a screen, POS(0) is the output window's column.
    +.item SCREEN(row, column) is the character code shown there; with a colorflag of 1, its colour (in SCREEN 0, foreground + 16 * background).
    +.item PRINT scrolls the screen up at the bottom. CHR$(13) and CHR$(10) start a new line and CHR$(9) moves to the next multiple of 8 columns.
    +.endlist
    +
    +.h2 INKEY$
    +
    +Returns the next key typed in the program's window without waiting: "" when there is none, one character for a character key, and CHR$(0) + CHR$(scan code) for other keys (arrows, function keys). Keys wait for INKEY$ in a queue of 64.
    +
    +.code
    +Do
    +    k$ = INKEY$
    +Loop While k$ = ""
    +If k$ = Chr$(0) + Chr$(72) Then Print "Up arrow"
    +.endcode
    +
    +.h2 INPUT and LINE INPUT on the Screen
    +
    +On the program's screen INPUT shows its prompt at the cursor and reads what is typed there: Backspace takes back a character, Esc clears the line and Enter ends it. LINE INPUT reads a whole line, commas included, without adding "? " to the prompt. Without the program's screen LINE INPUT asks in a dialog box, like INPUT.
    +
    +.code
    +LINE INPUT ["prompt";] variable$
    +.endcode
    +
    +.h2 Graphics Coordinates
    +
    +Points are (x, y) in pixels from the top-left corner. STEP(x, y) is relative to the last point drawn (the centre of the screen at first). VIEW makes a smaller part of the screen the drawing area (coordinates start at its corner, and drawing stays inside it); WINDOW gives it coordinates of your own. Colour numbers are attributes: 0 to 15 in SCREEN 12, 0 to 255 in SCREEN 13; anything else is error 5.
    +
    +.code
    +VIEW [[SCREEN] (x1, y1)-(x2, y2) [, [fill] [, border]]]
    +WINDOW [[SCREEN] (x1, y1)-(x2, y2)]
    +.endcode
    +
    +.list
    +.item VIEW with coordinates of its own sets the view; fill fills it and border draws a frame around it. VIEW SCREEN keeps coordinates counting from the screen's corner. VIEW alone is the whole screen again.
    +.item WINDOW (x1, y1)-(x2, y2) maps the view to those coordinates with y going up; WINDOW SCREEN keeps y going down. WINDOW alone goes back to pixels.
    +.item VIEW PRINT and PMAP are not supported.
    +.endlist
    +
    +.h2 PSET, PRESET, POINT
    +
    +.code
    +PSET [STEP](x, y) [, color]
    +PRESET [STEP](x, y) [, color]
    +c = POINT(x, y)
    +v = POINT(n)
    +.endcode
    +
    +PSET draws a point in the given colour or the COLOR foreground, PRESET in the given colour or colour 0. POINT(x, y) is the colour at a point, or -1 outside the view. POINT(0) and POINT(1) are the last point in pixels, POINT(2) and POINT(3) in WINDOW coordinates.
    +
    +.h2 LINE
    +
    +.code
    +LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, [color] [, [B | BF] [, style]]]
    +.endcode
    +
    +Draws a line, a box (B) or a filled box (BF). Without a start point the line starts at the last point. style is a 16-bit pattern: each set bit (from the top) draws a pixel, each clear one skips it.
    +
    +.code
    +LINE (10, 10)-(100, 50), 4, BF
    +LINE -STEP(20, 0), 15
    +LINE (0, 100)-(319, 100), 7, , &HF0F0
    +.endcode
    +
    +.h2 CIRCLE
    +
    +.code
    +CIRCLE [STEP](x, y), radius [, [color] [, [start] [, [end] [, aspect]]]]
    +.endcode
    +
    +Draws a circle, an ellipse or an arc. start and end are angles in radians (0 to 2 * pi, counterclockwise from the right); a negative angle also draws the radius to that end, so CIRCLE can draw pie slices. aspect is the height divided by the width; the default makes circles round on a 4:3 monitor (0.83 in SCREEN 13, 1 in SCREEN 12), so in SCREEN 13 a circle in the window looks slightly flat and a round one needs aspect 1.
    +
    +.code
    +CIRCLE (160, 100), 50, 14
    +CIRCLE (160, 100), 50, 4, -0.0001, -1.5708    ' a quarter pie slice
    +CIRCLE (160, 100), 80, 2, , , 0.5              ' an ellipse
    +.endcode
    +
    +.h2 PAINT
    +
    +.code
    +PAINT [STEP](x, y) [, [color] [, border]]
    +.endcode
    +
    +Fills the area around the point with color, up to pixels of the border colour (by default the fill colour itself). Tile patterns (a string instead of a colour) are not supported.
    +
    +.h2 DRAW
    +
    +Draws with a small command language. Letters may be upper or lower case; spaces and semicolons are ignored.
    +
    +.table
    +  Command     Meaning
    +  -------     -------
    +  U n         Up n (without n: 1).
    +  D n         Down n.
    +  L n         Left n.
    +  R n         Right n.
    +  E n         Diagonally up and right.
    +  F n         Diagonally down and right.
    +  G n         Diagonally down and left.
    +  H n         Diagonally up and left.
    +  M x,y       Move to x, y; with a sign (M+10,-5) move by that much.
    +  B           Before a move: move without drawing.
    +  N           Before a move: draw, then come back to where the move started.
    +  C n         Colour of the lines that follow.
    +  S n         Scale: moves are n/4 pixels a unit (1 to 255, default 4).
    +  A n         Turn the directions by n * 90 degrees (0 to 3).
    +  TA n        Turn the directions by n degrees (-360 to 360).
    +  P c,b       PAINT at the current point with colour c up to border colour b.
    +.endtable
    +
    +The pen starts at the last point and stays where it ends. The colour, scale and turn stay from one DRAW to the next. QBasic's X command (draw a string variable) and the "=" form of numbers are not supported; build the string with + instead, such as DRAW "R" + STR$(n). A mistake in the string is error 5.
    +
    +.code
    +DRAW "BM100,100 C14 R50 D50 L50 U50"      ' a square
    +DRAW "BM160,100 TA45 C2 R40"              ' a line at 45 degrees
    +.endcode
    +
    +.h2 PALETTE
    +
    +.code
    +PALETTE [attribute, color]
    +PALETTE USING array[(first)]
    +.endcode
    +
    +Changes the colour an attribute shows. In SCREEN 12 and 13, color is blue * 65536 + green * 256 + red with each part 0 to 63; in SCREEN 0 it is an EGA colour number 0 to 63. PALETTE alone restores the mode's colours. PALETTE USING sets every attribute (16, or 256 in SCREEN 13) from a one-dimensional LONG array starting at element first (or its first element); -1 leaves that attribute as it is. On the whole screen a palette change shows at once, like on a VGA card; in the window, whatever is already drawn in that attribute changes colour too.
    +
    +.code
    +SCREEN 13
    +PALETTE 1, 63 * 65536        ' attribute 1 becomes bright blue
    +FOR i = 0 TO 63
    +    PALETTE 2, i             ' a red that fades in
    +    FRAMEWAIT 30
    +NEXT i
    +.endcode
    +
    +.h2 Example
    +
    +.code
    +SCREEN 13
    +CLS
    +COLOR 14
    +PRINT "Press a key"
    +CIRCLE (160, 100), 60, 4
    +PAINT (160, 100), 1, 4
    +LINE (0, 0)-(319, 199), 15, B
    +DRAW "BM150,90 C15 R20 D20 L20 U20"
    +Do
    +Loop While INKEY$ = ""
    +FULLSCREEN
    +Do
    +Loop While INKEY$ = ""
    +.endcode
    +
     .topic lang.fileio
     .title File I/O
     .toc 1 File I/O
    @@ -1608,37 +2213,88 @@ bytes = FILELEN(filename$)
     .index TRIM$
     .index UCASE$
     .index VAL
    +.index FILTER
    +.index INSTRREV
    +.index JOIN$
    +.index REPLACE$
    +.index SPLIT
    +.index STRCOMP
    +.index STRREVERSE$
     
     .h1 String Functions
     
     .table
    -  Function                          Returns   Description
    -  --------                          -------   -----------
    -  ASC(s$)                           Integer   ASCII code of the first character of s$
    -  CHR$(n)                           String    Character with ASCII code n
    -  FORMAT$(value, fmt$)              String    Formats a numeric value using a format string
    -  HEX$(n)                           String    Hexadecimal representation of n (uppercase, no leading &H)
    -  INSTR(s$, find$)                  Long      Position of find$ in s$ (1-based), 0 if not found
    -  INSTR(start, s$, find$)           Long      Search starting at position start (1-based)
    -  LCASE$(s$)                        String    Converts s$ to lowercase
    -  LEFT$(s$, n)                      String    Leftmost n characters of s$
    -  LEN(s$)                           Long      Length of s$ in characters
    -  LTRIM$(s$)                        String    Removes leading spaces from s$
    -  MID$(s$, start)                   String    Substring from start (1-based) to end of string
    -  MID$(s$, start, length)           String    Substring of length characters starting at start
    -  OCT$(n)                           String    Octal representation of n (no leading &O)
    -  RIGHT$(s$, n)                     String    Rightmost n characters of s$
    -  RTRIM$(s$)                        String    Removes trailing spaces from s$
    -  SPACE$(n)                         String    String of n spaces (n up to 65535)
    -  STR$(n)                           String    Converts number n to string (leading space for non-negative)
    -  STRING$(n, char)                  String    String of n copies of char (char can be an ASCII code or single-character string; n up to 65535)
    -  TRIM$(s$)                         String    Removes leading and trailing spaces from s$
    -  UCASE$(s$)                        String    Converts s$ to uppercase
    -  VAL(s$)                           Double    Converts string s$ to a numeric value; stops at first non-numeric character
    +  Function                                                     Returns        Description
    +  --------                                                     -------        -----------
    +  ASC(s$)                                                      Integer        ASCII code of the first character of s$
    +  CHR$(n)                                                      String         Character with ASCII code n
    +  FILTER(arr, match$ [, include [, compare]])                  String array   The elements of arr that contain match$ (or, with include False, that do not)
    +  FORMAT$(value, fmt$)                                         String         Formats a number or a date using a format string
    +  HEX$(n)                                                      String         Hexadecimal representation of n (uppercase, no leading &H)
    +  INSTR(s$, find$)                                             Long           Position of find$ in s$ (1-based), 0 if not found
    +  INSTR(start, s$, find$)                                      Long           Search starting at position start (1-based)
    +  INSTRREV(s$, find$ [, start [, compare]])                    Long           Position of the last find$ in s$ that ends at or before start (default: the end), 0 if not found
    +  JOIN$(arr [, delim$])                                        String         The elements of a one-dimensional array joined with delim$ between them (default: a space)
    +  LCASE$(s$)                                                   String         Converts s$ to lowercase
    +  LEFT$(s$, n)                                                 String         Leftmost n characters of s$
    +  LEN(s$)                                                      Long           Length of s$ in characters
    +  LTRIM$(s$)                                                   String         Removes leading spaces from s$
    +  MID$(s$, start)                                              String         Substring from start (1-based) to end of string
    +  MID$(s$, start, length)                                      String         Substring of length characters starting at start
    +  OCT$(n)                                                      String         Octal representation of n (no leading &O)
    +  REPLACE$(s$, find$, repl$ [, start [, count [, compare]]])   String         s$ from position start (default 1) on, with the first count (default all) occurrences of find$ replaced by repl$
    +  RIGHT$(s$, n)                                                String         Rightmost n characters of s$
    +  RTRIM$(s$)                                                   String         Removes trailing spaces from s$
    +  SPACE$(n)                                                    String         String of n spaces (n up to 65535)
    +  SPLIT(s$ [, delim$ [, limit [, compare]]])                   String array   The pieces of s$ between occurrences of delim$ (default: a space), at most limit of them (default: all)
    +  STRCOMP(a$, b$ [, compare])                                  Integer        -1 if a$ sorts before b$, 0 if they are equal, 1 if a$ sorts after b$
    +  STR$(n)                                                      String         Converts number n to string (leading space for non-negative)
    +  STRING$(n, char)                                             String         String of n copies of char (char can be an ASCII code or single-character string; n up to 65535)
    +  STRREVERSE$(s$)                                              String         s$ with its characters in reverse order
    +  TRIM$(s$)                                                    String         Removes leading and trailing spaces from s$
    +  UCASE$(s$)                                                   String         Converts s$ to uppercase
    +  VAL(s$)                                                      Double         Converts string s$ to a numeric value; stops at first non-numeric character
     .endtable
     
     A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$ or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call). INSTR follows OPTION COMPARE TEXT (a case-insensitive search), and an INSTR start position below 1 raises error 5.
     
    +REPLACE$, JOIN$ and STRREVERSE$ may also be written without the $. Items in square brackets may be left out.
    +
    +.h2 Comparing Text
    +
    +The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT and STRCOMP chooses how text is matched: vbBinaryCompare (0) matches exactly, vbTextCompare (1) ignores upper and lower case, and vbUseCompareOption (-1, the default) follows OPTION COMPARE. Any other value raises error 5.
    +
    +.code
    +Print StrComp("apple", "Apple")                  ' 1 ("a" sorts after "A")
    +Print StrComp("apple", "Apple", vbTextCompare)   ' 0
    +Print Replace$("Red red RED", "red", "blue", 1, -1, vbTextCompare)   ' blue blue blue
    +Print InStrRev("C:\DVX\APPS\EDIT.APP", "\")     ' 12
    +.endcode
    +
    +.h2 REPLACE$ Details
    +
    +REPLACE$ returns the text from position start on, so REPLACE$("abcabc", "b", "x", 3) is "caxc" -- the characters before start are left out, as in Visual Basic. A count of -1 replaces every occurrence. An empty find$ returns the text unchanged. A start below 1 or a count below -1 raises error 5.
    +
    +.h2 SPLIT, JOIN$ and FILTER
    +
    +SPLIT returns a string array numbered from 0. Assign it to a dynamic array (one declared with empty parentheses) and use UBOUND to find the last piece. The last piece keeps the rest of the text when limit stops the split. Splitting an empty string, or a limit of 0, gives an array with no elements: its UBOUND is -1. An empty delim$ gives the whole string as the only piece.
    +
    +.code
    +Dim parts() As String
    +Dim i As Integer
    +parts = Split("red,green,,blue", ",")
    +For i = 0 To UBound(parts)
    +    Print i; "[" & parts(i) & "]"     ' the empty third piece is kept
    +Next i
    +Print Join$(parts, " / ")             ' red / green /  / blue
    +.endcode
    +
    +JOIN$ works on any one-dimensional array; numbers are converted as STR$ would, without the leading space. FILTER returns a new string array holding the elements that contain match$ (include True, the default) or that do not contain it (include False), or an array with no elements when nothing matches. Passing JOIN$ or FILTER something that is not a one-dimensional array raises error 13 (Type mismatch).
    +
    +.h2 INSTRREV Details
    +
    +INSTRREV searches backward: INSTRREV("abcabc", "bc") is 5 and INSTRREV("abcabc", "bc", 4) is 2. A start of -1 (the default) searches from the end. It returns 0 when s$ is empty or start is past the end of s$, and start when find$ is empty. A start of 0 or below -1 raises error 5.
    +
     .h2 FORMAT$
     
     FORMAT$ formats a numeric value using a BASIC-style format string. The format characters are the same as the ones used by PRINT USING.
    @@ -1656,7 +2312,61 @@ Print Format$(5, "#,##0.00")          ' "    5.00"
     Print Format$(0.5, "PERCENT")         ' "50%"
     .endcode
     
    -The whole format string is one numeric field. The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation) and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.
    +The whole format string is one numeric field (for a number). The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation) and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.
    +
    +FORMAT (without the $) is the same function.
    +
    +.h2 FORMAT$ with Dates
    +
    +When the format string holds a date or time code, or is one of the named date formats, FORMAT$ formats its value as a date. The value may be a Date, a number (taken as a date's number of days) or a string that holds a date (anything else raises error 13). A Date with an empty format string gives the usual m/d/yyyy h:mm:ss AM form. The codes may be written in upper or lower case.
    +
    +.table
    +  Code          Result
    +  ----          ------
    +  d / dd        Day of the month (dd: two digits)
    +  ddd / dddd    Weekday name, abbreviated (Mon) or full (Monday)
    +  ddddd         Short date (m/d/yyyy)
    +  dddddd        Long date (dddd, mmmm d, yyyy)
    +  w             Day of the week (1 = Sunday)
    +  ww            Week of the year (the week holding January 1 is week 1)
    +  m / mm        Month number (mm: two digits); the minute when it follows an hour or comes before a second
    +  mmm / mmmm    Month name, abbreviated (Jan) or full (January)
    +  q             Quarter (1-4)
    +  y             Day of the year (1-366)
    +  yy / yyyy     Year as two or four digits
    +  h / hh        Hour (hh: two digits); 1-12 when the format has AM/PM, else 0-23
    +  n / nn        Minute
    +  s / ss        Second
    +  ttttt         Long time (h:mm:ss AM/PM)
    +  c             The usual date and time form
    +  AM/PM am/pm   AM or PM in the case written; A/P and a/p give one letter; AMPM gives AM or PM
    +  "text"        Text in double quotes is copied as it is
    +  \x            The character after a backslash is copied as it is
    +.endtable
    +
    +Any other character, such as / : - , or a space, is copied as it is. These names give whole formats:
    +
    +.table
    +  Name           Format                 Example
    +  ----           ------                 -------
    +  General Date   (the usual form)       1/5/2000 3:04:05 PM
    +  Long Date      dddd, mmmm d, yyyy     Wednesday, January 5, 2000
    +  Medium Date    dd-mmm-yy              05-Jan-00
    +  Short Date     m/d/yyyy               1/5/2000
    +  Long Time      h:mm:ss AM/PM          3:04:05 PM
    +  Medium Time    hh:mm AM/PM            03:04 PM
    +  Short Time     hh:mm                  15:04
    +.endtable
    +
    +.code
    +Dim d As Date
    +d = #1/5/2000 3:04:05 PM#
    +Print Format$(d, "yyyy-mm-dd hh:nn:ss")   ' 2000-01-05 15:04:05
    +Print Format$(d, "dddd, mmmm d")          ' Wednesday, January 5
    +Print Format$(d, "h:mm AM/PM")            ' 3:04 PM
    +Print Format$(d, "Long Date")             ' Wednesday, January 5, 2000
    +Print Format$(Now, "Short Time")          ' the time now, e.g. 09:41
    +.endcode
     
     .h2 MID$ Assignment
     
    @@ -1744,11 +2454,13 @@ Me.BackColor = RGB(0, 0, 128)  ' dark blue background
     .title Conversion Functions
     .toc 2 Conversion Functions
     .index CBOOL
    +.index CDATE
     .index CDBL
     .index CINT
     .index CLNG
     .index CSNG
     .index CSTR
    +.index CVAR
     
     .h1 Conversion Functions
     
    @@ -1756,13 +2468,153 @@ Me.BackColor = RGB(0, 0, 128)  ' dark blue background
       Function   Returns   Description
       --------   -------   -----------
       CBOOL(n)   Boolean   Returns True (-1) if n is nonzero or a non-empty string; False (0) otherwise
    -  CDBL(n)    Double    Converts n to Double
    +  CDATE(v)   Date      Converts a number (days since 12/30/1899) or date text to a Date; error 13 for text that is not a date, error 6 outside 1/1/100 to 12/31/9999
    +  CDBL(n)    Double    Converts n to Double (a Date gives its number of days)
       CINT(n)    Integer   Converts n to Integer; halves round to the nearest even number (2.5 -> 2, 3.5 -> 4); error 6 (Overflow) outside -32768 to 32767
       CLNG(n)    Long      Converts n to Long with the same rounding; error 6 (Overflow) outside the Long range
       CSNG(n)    Single    Converts n to Single
    -  CSTR(n)    String    Converts n to its String representation
    +  CSTR(n)    String    Converts n to its String representation (a Date gives m/d/yyyy h:mm:ss AM)
    +  CVAR(v)    Variant   Returns v unchanged as a Variant
     .endtable
     
    +Passing Null to any of these except CVAR raises error 94 (Invalid use of Null). VARTYPE, TYPENAME and the IS functions (ISNULL, ISEMPTY, ISNUMERIC, ...) are described under Variants.
    +
    +.link lang.variants See also: Variants
    +
    +.topic lang.func.date
    +.title Date and Time Functions
    +.toc 2 Date and Time Functions
    +.index NOW
    +.index DATE
    +.index TIME
    +.index DATESERIAL
    +.index TIMESERIAL
    +.index DATEVALUE
    +.index TIMEVALUE
    +.index YEAR
    +.index MONTH
    +.index DAY
    +.index WEEKDAY
    +.index HOUR
    +.index MINUTE
    +.index SECOND
    +.index DATEADD
    +.index DATEDIFF
    +.index DATEPART
    +.index ISDATE
    +.index MONTHNAME
    +.index WEEKDAYNAME
    +
    +.h1 Date and Time Functions
    +
    +These functions work with Date values (see Data Types). Wherever a date is expected, a Date, a number of days since 12/30/1899 or a string holding a date may be given; text that is not a date raises error 13 (Type mismatch).
    +
    +.table
    +  Function                                                 Returns   Description
    +  --------                                                 -------   -----------
    +  NOW                                                      Date      The current date and time
    +  DATE                                                     Date      Today's date (at midnight)
    +  TIME                                                     Date      The current time of day (on 12/30/1899)
    +  DATESERIAL(year, month, day)                             Date      The date with those parts
    +  TIMESERIAL(hour, minute, second)                         Date      The time with those parts
    +  DATEVALUE(d)                                             Date      The date part of d (its time dropped)
    +  TIMEVALUE(d)                                             Date      The time part of d (on 12/30/1899)
    +  YEAR(d)                                                  Integer   Year (100-9999)
    +  MONTH(d)                                                 Integer   Month (1-12)
    +  DAY(d)                                                   Integer   Day of the month (1-31)
    +  WEEKDAY(d [, firstDay])                                  Integer   Day of the week, 1 for firstDay (default vbSunday)
    +  HOUR(d)                                                  Integer   Hour (0-23)
    +  MINUTE(d)                                                Integer   Minute (0-59)
    +  SECOND(d)                                                Integer   Second (0-59)
    +  DATEADD(interval$, n, d)                                 Date      d moved by n intervals (n may be negative)
    +  DATEDIFF(interval$, d1, d2 [, firstDay [, firstWeek]])   Long      Intervals from d1 to d2 (negative when d2 is earlier)
    +  DATEPART(interval$, d [, firstDay [, firstWeek]])        Integer   One part of d
    +  ISDATE(v)                                                Boolean   True when v is a Date or a string holding a valid date
    +  MONTHNAME(month [, abbreviate])                          String    January ... December (Jan ... Dec when abbreviate is True)
    +  WEEKDAYNAME(n [, abbreviate [, firstDay]])               String    Name of day n of the week counted from firstDay (default vbSunday)
    +.endtable
    +
    +NOW, DATE and TIME need no parentheses. DATE$ and TIME$ still return the date and time as text.
    +
    +.h2 Intervals
    +
    +DATEADD, DATEDIFF and DATEPART take the interval as a string, in upper or lower case:
    +
    +.table
    +  Interval   Meaning
    +  --------   -------
    +  "yyyy"     Year
    +  "q"        Quarter
    +  "m"        Month
    +  "y"        Day of the year
    +  "d"        Day
    +  "w"        Weekday
    +  "ww"       Week
    +  "h"        Hour
    +  "n"        Minute
    +  "s"        Second
    +.endtable
    +
    +Any other interval raises error 5 (Illegal function call).
    +
    +.h2 DATESERIAL and TIMESERIAL
    +
    +A year from 0 to 29 means 2000 to 2029 and one from 30 to 99 means 1930 to 1999. Months, days, hours, minutes and seconds outside their usual range carry into the next or previous unit, so DATESERIAL(2000, 13, 1) is 1/1/2001, DATESERIAL(2000, 3, 0) is the last day of February 2000 and TIMESERIAL(0, 90, 0) is 1:30 AM. Fractional arguments are rounded. A result before 1/1/100 or after 12/31/9999 raises error 5.
    +
    +.code
    +Dim lastDay As Date
    +lastDay = DateSerial(Year(Date), Month(Date) + 1, 0)    ' last day of this month
    +Print Day(lastDay)
    +.endcode
    +
    +.h2 DATEADD
    +
    +Adding months, quarters or years keeps the day of the month where it can; when the new month is shorter the day becomes its last day, so DATEADD("m", 1, #1/31/2000#) is 2/29/2000. The time of day is kept. "y", "d" and "w" all add days and "ww" adds weeks. A fractional n drops its fraction. A result outside the Date range raises error 5.
    +
    +.code
    +Print DateAdd("m", 1, #1/31/2001#)      ' 2/28/2001
    +Print DateAdd("h", -2, #1/1/2000#)      ' 12/31/1999 10:00:00 PM
    +.endcode
    +
    +.h2 DATEDIFF
    +
    +DATEDIFF counts how many interval boundaries lie between d1 and d2: DATEDIFF("yyyy", #12/31/1999#, #1/1/2000#) is 1 although the dates are a day apart, and DATEDIFF("d", ...) counts midnights, whatever the times of day. "w" counts whole weeks (7 days) and "ww" counts calendar weeks -- the number of firstDay days (Sundays by default) after d1 up to and including d2. A result too large for a Long raises error 6 (Overflow).
    +
    +.code
    +Print DateDiff("d", #1/1/2000#, #3/1/2000#)     ' 60
    +Print DateDiff("ww", #1/1/2000#, #1/2/2000#)    ' 1 (1/2/2000 is a Sunday)
    +Print DateDiff("w", #1/1/2000#, #1/2/2000#)     ' 0
    +.endcode
    +
    +.h2 DATEPART and Weeks of the Year
    +
    +DATEPART returns the part of d the interval names: "w" is the day of the week (1 for firstDay) and "ww" the week of the year. firstDay is one of vbSunday (1, the default) to vbSaturday (7), or vbUseSystemDayOfWeek (0, meaning Sunday). firstWeek chooses which week is week 1:
    +
    +.table
    +  Constant          Value   Week 1 is
    +  --------          -----   ---------
    +  vbUseSystem       0       Same as vbFirstJan1
    +  vbFirstJan1       1       The week holding January 1 (the default)
    +  vbFirstFourDays   2       The first week with at least four days in the new year
    +  vbFirstFullWeek   3       The first full week of the year
    +.endtable
    +
    +Days before week 1 belong to the last week of the previous year. A firstDay or firstWeek outside these values raises error 5.
    +
    +.code
    +Print DatePart("q", #5/15/2000#)                               ' 2
    +Print DatePart("ww", #1/1/2000#, vbSunday, vbFirstFourDays)    ' 52
    +.endcode
    +
    +.h2 ISDATE, MONTHNAME and WEEKDAYNAME
    +
    +ISDATE is True for a Date and for a string that holds a valid date or time; numbers are not dates to ISDATE. MONTHNAME with a month outside 1 to 12, and WEEKDAYNAME with n outside 1 to 7, raise error 5.
    +
    +.code
    +If IsDate(Text1.Text) Then due = CDate(Text1.Text)
    +Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True)
    +.endcode
    +
     .topic lang.func.fileio
     .title File I/O Functions
     .toc 2 File I/O Functions
    @@ -1804,22 +2656,37 @@ The dim argument of LBOUND and UBOUND must be a constant from 1 to 8.
     .index ENVIRON$
     .index COMMAND$
     .index Command Line
    +.index IIF
    +.index CHOOSE
     
     .h1 Miscellaneous Functions
     
     .table
    -  Function          Returns   Description
    -  --------          -------   -----------
    -  DATE$             String    Current date as "MM-DD-YYYY"
    -  TIME$             String    Current time as "HH:MM:SS" (24-hour)
    -  TIMER             Double    Seconds since midnight
    -  ENVIRON$(name$)   String    Value of the environment variable; empty string if not set
    -  COMMAND$          String    The arguments the program was started with; empty string if none
    -  COMMAND$(n)       String    The nth argument (1 = first) without its quotes; empty string when there are fewer; COMMAND$(0) is the same as COMMAND$
    -  ERR               Long      Current runtime error number (0 if no error)
    -  SHELL(cmd$)       Integer   Exit code of the command (also usable as a statement, discarding the code)
    +  Function               Returns   Description
    +  --------               -------   -----------
    +  DATE$                  String    Current date as "MM-DD-YYYY"
    +  TIME$                  String    Current time as "HH:MM:SS" (24-hour)
    +  TIMER                  Double    Seconds since midnight
    +  ENVIRON$(name$)        String    Value of the environment variable; empty string if not set
    +  COMMAND$               String    The arguments the program was started with; empty string if none
    +  COMMAND$(n)            String    The nth argument (1 = first) without its quotes; empty string when there are fewer; COMMAND$(0) is the same as COMMAND$
    +  ERR                    Long      Current runtime error number (0 if no error)
    +  SHELL(cmd$)            Integer   Exit code of the command (also usable as a statement, discarding the code)
    +  IIF(cond, a, b)        (any)     a when cond is True, b when it is False
    +  CHOOSE(n, a, b, ...)   (any)     The nth value of the list (1 = a), or Null when there is no nth value; up to 31 values
     .endtable
     
    +DATE$ and TIME$ return text; the Date and Time Functions return Date values.
    +
    +.h2 IIF and CHOOSE
    +
    +IIF and CHOOSE evaluate every argument before picking one, as in Visual Basic, so a function called in an argument runs even when its value is not chosen. A fractional n in CHOOSE drops its fraction (2.9 picks the second value). An n below 1 or past the end of the list gives Null, as in Visual Basic (test it with ISNULL; see Variants).
    +
    +.code
    +Print IIf(score >= 50, "Pass", "Fail")
    +Print Choose(Weekday(Date), "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
    +.endcode
    +
     .h2 COMMAND$
     
     COMMAND$ returns the program's arguments exactly as they were given, as in Visual Basic (QBasic turned them into capitals; DVX keeps them as they are). Arguments are separated by blanks; an argument with blanks in it is written in double quotes. COMMAND$(n) picks out one argument with its quotes removed, so a program that opens the document it was started with only needs:
    @@ -1924,7 +2791,7 @@ List1.Clear
     
     .h2 Me Keyword
     
    -Me refers to the current form -- that is, the form whose event handler is running. Use it to access the form's own properties, controls, and methods from within event handlers, especially when the form's name may change or when the same code runs from multiple forms.
    +Me refers to the current form -- that is, the form whose event handler is running. Use it to access the form's own properties, controls, and methods from within event handlers, especially when the form's name may change or when the same code runs from multiple forms. In the code of a class module, Me is instead the object whose procedure is running (see Objects and Class Modules).
     
     .code
     Me.Caption = "Updated Title"
    @@ -1934,12 +2801,15 @@ Me.Hide
     
     .h2 Nothing
     
    -Nothing is a null object reference. Assign it to an object-typed variable to indicate "no object".
    +Nothing is a null object reference. Assign it with SET to an object variable to indicate "no object", and test for it with Is. An object variable that has not been Set holds Nothing, and using a property or method of Nothing raises error 91 (Object variable not set).
     
     .code
     Set myCtrl = Nothing
    +If myCtrl Is Nothing Then Print "No control chosen"
     .endcode
     
    +Setting the last variable that refers to a class instance or Collection to Nothing frees the object; see Objects and Class Modules.
    +
     .h2 Control Arrays
     
     Multiple controls can share a name with unique indices. Access individual controls with parenthesized indices:
    @@ -2007,8 +2877,8 @@ REMOVECONTROL formRef, ctrlName$
     Removes a control from a form. A later reference to the removed control raises a runtime error (unknown control), as does a reference to a control of a form that has been unloaded.
     
     .code
    -Dim frm As Long
    -Dim lbl As Long
    +Dim frm As Form
    +Dim lbl As Control
     
     Set frm = CreateForm("MyDlg", 300, 200)
     frm.Caption = "Built in code"
    @@ -2016,7 +2886,7 @@ frm.Caption = "Built in code"
     Set lbl = CreateControl(frm, "Label", "StatusLabel")
     StatusLabel.Caption = "Hello, world!"
     
    -Dim btn As Long
    +Dim btn As Control
     Set btn = CreateControl(frm, "CommandButton", "BtnClose")
     BtnClose.Caption = "Close"
     SetEvent btn, "Click", "OnCloseClick"
    @@ -2121,6 +2991,298 @@ End Sub
       Timer1_Timer         Timer control interval elapsed (Timer control's default event)
     .endtable
     
    +.topic lang.classes
    +.title Objects and Class Modules
    +.toc 1 Objects and Class Modules
    +.index Class Modules
    +.index Class Module
    +.index CLS Files
    +.index .cls Files
    +.index Objects
    +.index PROPERTY
    +.index PROPERTY GET
    +.index PROPERTY LET
    +.index PROPERTY SET
    +.index EXIT PROPERTY
    +.index Class_Initialize
    +.index Class_Terminate
    +.index Attribute
    +.index VB_Name
    +.index VB_UserMemId
    +.index Default Member
    +.index Reference Counting
    +.index Late Binding
    +.index Me in Classes
    +
    +.h1 Objects and Class Modules
    +
    +A class module lets you define your own kind of object. The class describes what every object of that kind knows (its member variables and properties) and what it can do (its methods). Each object made from the class -- an instance -- has its own copy of the member variables, so you can have as many independent objects as you need, much as you can put several TextBoxes on a form.
    +
    +.h2 A First Class
    +
    +This class module, saved as Account.cls, describes a bank account:
    +
    +.code
    +VERSION DVX 1.00 CLASS
    +Attribute VB_Name = "Account"
    +
    +Option Explicit
    +
    +Public Owner As String
    +Private mBalance As Double
    +
    +Public Sub Deposit(ByVal amount As Double)
    +    mBalance = mBalance + amount
    +End Sub
    +
    +Public Property Get Balance() As Double
    +    Balance = mBalance
    +End Property
    +
    +Private Sub Class_Initialize()
    +    Print "Account created"
    +End Sub
    +
    +Private Sub Class_Terminate()
    +    Print "Account for "; Owner; " closed"
    +End Sub
    +.endcode
    +
    +Any other code in the project can then make and use Account objects:
    +
    +.code
    +Dim acct As Account
    +Set acct = New Account          ' prints "Account created"
    +acct.Owner = "Scott"
    +acct.Deposit 100
    +acct.Deposit 50
    +Print acct.Owner & ": " & acct.Balance   ' Scott: 150
    +Set acct = Nothing              ' prints "Account for Scott closed"
    +.endcode
    +
    +Owner is Public, so code outside the class can read and change it. mBalance is Private: only the class's own code can touch it, and the outside world reads it through the Balance property, which has no way to change it.
    +
    +.h2 Class Module Files (.cls)
    +
    +Each class module is kept in its own .cls file, listed in the project's Classes group (see the IDE Guide). The file can begin with a few header lines, as a Visual Basic class file does; the IDE writes them for a new class module:
    +
    +.code
    +VERSION DVX 1.00 CLASS
    +Attribute VB_Name = "Account"
    +
    +Option Explicit
    +.endcode
    +
    +The header is everything before the first line of code: blank lines, a VERSION line, a BEGIN ... END block and Attribute VB_... lines, in that order. All of it is optional, and a Visual Basic .cls file can be added as it is. The class's name comes from the Attribute VB_Name line; without one, the name is the file's name without .cls (ACCOUNT.CLS makes a class named ACCOUNT). The class name is the type name used in DIM ... AS, NEW, TypeOf and TYPENAME.
    +
    +.h2 What a Class Contains
    +
    +Outside its procedures, a class module may hold only declarations: member variables, CONSTs, OPTION and DEFtype statements, and Attribute lines. Statements that do something belong in a procedure, usually Class_Initialize. The members are:
    +
    +.table
    +  Member                             Written as                                 Used from outside as
    +  ------                             ----------                                 --------------------
    +  Public member variable             Public name As type                        obj.name (read and assign)
    +  Private member variable            Private name As type, or Dim name As type  not visible outside
    +  Method                             Sub or Function (Public unless Private)    obj.Method args, x = obj.Func(args)
    +  Read property                      Property Get name(...) As type             x = obj.name
    +  Write property                     Property Let name(..., value)              obj.name = x
    +  Object property                    Property Set name(..., value)              Set obj.name = someObject
    +.endtable
    +
    +A Sub, Function or Property is Public unless it is marked Private; Private procedures can be called only by the class's own code. Inside the class's procedures the member variables are used by name, with no SHARED needed, and each call works with the variables of the object it was called for. The class's code can also use the program's DIM SHARED (or PUBLIC) module variables, procedures and constants.
    +
    +.h2 Properties
    +
    +A property looks like a variable from outside, but reading or assigning it runs a procedure, so the class can check or calculate the value:
    +
    +.code
    +Private mC As Double
    +
    +Public Property Get Celsius() As Double
    +    Celsius = mC
    +End Property
    +
    +Public Property Let Celsius(ByVal v As Double)
    +    mC = v
    +End Property
    +
    +Public Property Get Fahrenheit() As Double
    +    Fahrenheit = mC * 9 / 5 + 32
    +End Property
    +
    +Public Property Let Fahrenheit(ByVal v As Double)
    +    mC = (v - 32) * 5 / 9
    +End Property
    +.endcode
    +
    +.code
    +Dim t As New Thermometer
    +t.Celsius = 100
    +Print t.Fahrenheit      ' 212
    +.endcode
    +
    +Property Get returns the value: assign it to the property name, as in a FUNCTION. Property Let receives the value being assigned as its last parameter; Property Set does the same for an object assigned with SET. A property can take arguments of its own before the value, such as Property Get Cell(r, c) and Property Let Cell(r, c, v), used as obj.Cell(1, 2) = 7. A property with only a Property Get is read-only: assigning it raises error 438. EXIT PROPERTY leaves a property procedure early.
    +
    +.h2 Creating and Releasing Objects
    +
    +An object variable starts as Nothing. NEW makes an object:
    +
    +.code
    +Dim a As Account               ' Nothing so far
    +Set a = New Account            ' now an Account
    +
    +Dim b As New Account           ' an Account is made right here, at the DIM
    +.endcode
    +
    +When an object is made, its Class_Initialize runs; when it is freed, its Class_Terminate runs. Both are Subs without parameters, usually Private, and both are optional.
    +
    +An object stays alive as long as something refers to it: a variable, an array element, a TYPE field, a Collection, a Variant, or a member variable of another object. Each SET to another variable adds a reference; nothing is copied. When the last reference goes away -- SET to Nothing or to another object, a local variable at the end of its SUB or FUNCTION, removal from a Collection, ERASE -- the object is freed and its Class_Terminate runs right then, just after the statement that let go of it:
    +
    +.code
    +Dim a As New Account
    +Dim b As Account
    +Set b = a                ' two references to one account
    +Set a = Nothing          ' still alive: b refers to it
    +Set b = Nothing          ' last reference gone: Class_Terminate runs now
    +.endcode
    +
    +.note warning
    +Objects that refer to each other, directly or around a longer loop, keep each other alive even after the program's own variables let them go. Such objects stay in memory until the program ends, and their Class_Terminate never runs. Break the loop yourself (for example Set a.Partner = Nothing) before letting go of the objects.
    +.endnote
    +
    +When the program ends (END, or the last form closes), every object still alive is freed without running Class_Terminate, as with END in Visual Basic. Work that must always happen, such as saving a file, belongs in a method the program calls itself, not only in Class_Terminate.
    +
    +.h2 Me
    +
    +Inside a class's procedures, Me is the object the procedure was called for. Use it to pass the object to other code, or to go through the object's own properties (Me.Balance runs Property Get Balance):
    +
    +.code
    +Public Sub Move(ByVal dx As Double)
    +    Me.X = Me.X + dx     ' uses Property Get X and Property Let X
    +End Sub
    +.endcode
    +
    +.h2 Using Objects
    +
    +Members are found by name when the program runs (late binding): obj.Deposit 5 looks for a Public member named Deposit in whatever object obj holds at that moment. A misspelled or Private member is therefore not reported when the program is compiled but raises error 438 when the line runs, and the wrong number of arguments raises error 450. Using a member of Nothing raises error 91.
    +
    +An object can be kept in a variable declared as its class name, As Object, or As Variant, in an array, in a TYPE field, in a Collection, or in another object's member variable. It can be passed to a SUB or FUNCTION and returned from a FUNCTION (assign the result with SET inside the function, Set MakeAccount = acct). TYPENAME gives an object's class name, and TypeOf obj Is ClassName tests it; Is tells whether two variables refer to the same object.
    +
    +Arguments of a class's procedures follow the usual rules when the object variable is declared as the class name: a parameter without BYVAL gets the caller's variable, so the procedure can change it. When the object is reached through a variable declared As Object or As Variant, every argument is passed by value. An argument left out in the middle of a call (obj.Report a, , c) arrives as Empty; OPTIONAL parameters left out at the end get their type's usual empty value (0, "" and so on).
    +
    +An object parameter is itself passed like any other parameter: without BYVAL, SET on the parameter changes the caller's variable (a SUB can make a new object for the caller); with BYVAL, the procedure gets its own reference to the same object, and SET on it does not affect the caller.
    +
    +.h2 Default Member
    +
    +A class can name one property or method as its default member, used when the object is written with parentheses directly after it. Put this line in the class, inside the procedure (as Visual Basic writes it) or on its own at the top of the class:
    +
    +.code
    +Attribute Item.VB_UserMemId = 0
    +.endcode
    +
    +With Item as the default member, obj(3) means obj.Item(3), and obj(3) = x calls Property Let Item. The object name on its own, without parentheses, still means the object itself. Collection's default member is Item, which is why c(1) and c("key") work.
    +
    +.h2 Errors in a Class
    +
    +A class reports a problem with ERROR, normally numbered from vbObjectError so that its numbers cannot be mistaken for the built-in ones. The error is handled by the ON ERROR handler of the code that called the class, as with any procedure:
    +
    +.code
    +' in the class
    +Public Sub Withdraw(ByVal amount As Double)
    +    If amount > mBalance Then Error vbObjectError + 1
    +    mBalance = mBalance - amount
    +End Sub
    +
    +' in the program
    +On Error GoTo NoMoney
    +acct.Withdraw 500
    +Exit Sub
    +NoMoney:
    +    If Err = vbObjectError + 1 Then MsgBox "Not enough money"
    +    Resume Next
    +.endcode
    +
    +.h2 Rules and Limits
    +
    +.list
    +.item A member's name cannot be a reserved word. Names such as Name, Show, Hide, Load, Print, Open, Close, Line, Input, Get, Put, Type, Error and Timer are keywords in DVX BASIC and cannot be used; Text, Caption, Value, Count, Item and Add are fine.
    +.item SHARED and DIM SHARED cannot be used in a class module; a class's code reaches the program's module variables that are declared DIM SHARED or PUBLIC.
    +.item A STATIC variable in a class procedure is shared by all objects of the class, not kept per object. Use a member variable for a value each object keeps.
    +.item Class_Initialize and Class_Terminate take no parameters.
    +.item A class module cannot hold statements outside its procedures; the compiler reports "Only declarations are allowed outside procedures in a class module".
    +.item NEW, PROPERTY, PUBLIC, PRIVATE and TYPEOF are reserved words. VARIANT, OBJECT, COLLECTION, EMPTY, NULL, EACH and IN are not.
    +.endlist
    +
    +.link lang.collection See also: Collection Object
    +.link lang.variants See also: Variants
    +.link lang.declarations See also: SET and PUBLIC / PRIVATE
    +
    +.topic lang.collection
    +.title Collection Object
    +.toc 2 Collection Object
    +.index Collection
    +.index COLLECTION
    +.index Add
    +.index Remove
    +.index Item
    +.index Count
    +.index Collection Keys
    +
    +.h1 Collection Object
    +
    +A Collection is a ready-made object that holds a list of items of any kind -- numbers, text, objects -- in order. Items can be looked up by position or by a text key, added anywhere in the list and removed, and FOR EACH walks through them. The list grows as needed; there is no size to declare.
    +
    +.code
    +Dim fruit As New Collection
    +fruit.Add "Apple", "a"
    +fruit.Add "Cherry", "c"
    +fruit.Add "Banana", "b", "c"    ' before the item with key "c"
    +
    +Print fruit.Count               ' 3
    +Print fruit(1)                  ' Apple
    +Print fruit("B")                ' Banana (keys ignore case)
    +
    +Dim f As Variant
    +For Each f In fruit
    +    Print f                     ' Apple, Banana, Cherry
    +Next
    +
    +fruit.Remove "a"
    +fruit.Remove 1                  ' removes Banana, now the first item
    +.endcode
    +
    +.h2 Members
    +
    +.table
    +  Member                                  Description
    +  ------                                  -----------
    +  Add item [, key] [, before] [, after]   Adds item at the end of the list, or before or after the item given by position or key. key, when given, is text that names the item.
    +  Remove indexOrKey                       Removes the item at that position (1 = first) or with that key.
    +  Item(indexOrKey)                        The item at that position or with that key. Item is the default member: c(2) is c.Item(2) and c("key") is c.Item("key").
    +  Count                                   The number of items.
    +.endtable
    +
    +Positions start at 1 and change as items are added and removed. A key must be text, is compared without regard to upper and lower case, and must be unique in the Collection. Give either before or after, not both; leave out the arguments you do not use with commas (c.Add item, , 1 puts item first). Items are stored as they are: an object added to a Collection stays alive while the Collection holds it.
    +
    +Make a Collection with DIM c AS NEW COLLECTION or SET c = NEW COLLECTION. Like any object, it is freed when the last reference to it goes.
    +
    +.h2 Errors
    +
    +.table
    +  Error   When
    +  -----   ----
    +  5       A key that is not in the Collection (Item, Remove, before or after), or Add given both before and after
    +  9       A position that is not in the Collection
    +  13      A key that is not text
    +  438     A member other than Add, Remove, Item and Count
    +  450     The wrong number of arguments
    +  457     Add with a key that is already in use
    +.endtable
    +
    +.link lang.classes See also: Objects and Class Modules
    +.link lang.loops See also: FOR EACH...NEXT
    +
     .topic lang.sql
     .title SQL Functions
     .toc 1 SQL Functions
    @@ -2335,6 +3497,36 @@ dbPath$ = App.Data & "\myapp.db"
     Print "Running from: " & App.Path
     .endcode
     
    +.topic lang.clipboard
    +.title Clipboard Object
    +.toc 1 Clipboard Object
    +.index Clipboard
    +.index Clipboard.Clear
    +.index Clipboard.GetFormat
    +.index Clipboard.GetText
    +.index Clipboard.SetText
    +.index vbCFText
    +
    +.h1 Clipboard Object
    +
    +The Clipboard object shares text with other programs and with the copy and paste of text boxes, as in Visual Basic. The DVX clipboard holds text only, so the format arguments accept vbCFText (1): any other format is never present and GetText returns "" for it.
    +
    +.table
    +  Method                            Returns   Description
    +  ------                            -------   -----------
    +  Clipboard.Clear                   (none)    Empty the clipboard.
    +  Clipboard.GetFormat(format)       Boolean   True when the clipboard holds data of that format (text for vbCFText).
    +  Clipboard.GetText([format])       String    The clipboard text, or "" when it is empty.
    +  Clipboard.SetText text [, format] (none)    Put text on the clipboard, replacing what was there. A format other than vbCFText is an error.
    +.endtable
    +
    +.code
    +Clipboard.SetText Text1.Text
    +If Clipboard.GetFormat(vbCFText) Then Text2.Text = Clipboard.GetText()
    +.endcode
    +
    +A control or form of your own named Clipboard takes the name's place.
    +
     .topic lang.ini
     .title INI Functions
     .toc 1 INI Functions
    @@ -2395,6 +3587,46 @@ IniWrite App.Config & "\app.ini", "Display", "FontSize", Str$(fontSize)
     .index vbSystem
     .index vbDirectory
     .index vbArchive
    +.index vbBinaryCompare
    +.index vbTextCompare
    +.index vbUseCompareOption
    +.index vbSunday
    +.index vbMonday
    +.index vbTuesday
    +.index vbWednesday
    +.index vbThursday
    +.index vbFriday
    +.index vbSaturday
    +.index vbUseSystemDayOfWeek
    +.index vbUseSystem
    +.index vbFirstJan1
    +.index vbFirstFourDays
    +.index vbFirstFullWeek
    +.index vbManual
    +.index vbAutomatic
    +.index vbBeginDrag
    +.index vbEndDrag
    +.index vbEnter
    +.index vbLeave
    +.index vbOver
    +.index vbCFText
    +.index vbEmpty
    +.index vbNull
    +.index vbInteger
    +.index vbLong
    +.index vbSingle
    +.index vbDouble
    +.index vbCurrency
    +.index vbDate
    +.index vbString
    +.index vbObject
    +.index vbError
    +.index vbBoolean
    +.index vbVariant
    +.index vbDataObject
    +.index vbUserDefinedType
    +.index vbArray
    +.index vbObjectError
     .index True
     .index False
     .index Predefined Constants
    @@ -2463,6 +3695,96 @@ Bit values used with GETATTR and SETATTR.
       vbArchive     32      File has been modified since last backup
     .endtable
     
    +.h2 Compare Constants
    +
    +The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT and STRCOMP.
    +
    +.table
    +  Constant             Value   Description
    +  --------             -----   -----------
    +  vbUseCompareOption   -1      Follow OPTION COMPARE (the default)
    +  vbBinaryCompare      0       Exact match
    +  vbTextCompare        1       Ignore upper and lower case
    +.endtable
    +
    +.h2 Day and Week Constants
    +
    +The firstDay and firstWeek arguments of the date functions, and WEEKDAY results.
    +
    +.table
    +  Constant               Value   Description
    +  --------               -----   -----------
    +  vbUseSystemDayOfWeek   0       Weeks start on Sunday
    +  vbSunday               1       Sunday
    +  vbMonday               2       Monday
    +  vbTuesday              3       Tuesday
    +  vbWednesday            4       Wednesday
    +  vbThursday             5       Thursday
    +  vbFriday               6       Friday
    +  vbSaturday             7       Saturday
    +  vbUseSystem            0       Week 1 holds January 1
    +  vbFirstJan1            1       Week 1 holds January 1
    +  vbFirstFourDays        2       Week 1 is the first with four days in the new year
    +  vbFirstFullWeek        3       Week 1 is the first full week
    +.endtable
    +
    +.h2 Drag and Drop Constants
    +
    +The DragMode property, the Drag method and the State argument of DragOver.
    +
    +.table
    +  Constant      Value   Description
    +  --------      -----   -----------
    +  vbManual      0       DragMode: the program starts drags with Drag
    +  vbAutomatic   1       DragMode: pressing the control starts a drag
    +  vbBeginDrag   1       Drag: start dragging (the default)
    +  vbEndDrag     2       Drag: drop where the mouse is (Drag 0 cancels)
    +  vbEnter       0       DragOver: the drag came onto the object
    +  vbLeave       1       DragOver: the drag went off the object
    +  vbOver        2       DragOver: the drag moved over the object
    +.endtable
    +
    +.h2 Clipboard Constants
    +
    +.table
    +  Constant   Value   Description
    +  --------   -----   -----------
    +  vbCFText   1       Text, the format the DVX clipboard holds
    +.endtable
    +
    +.h2 VarType Constants
    +
    +The values VARTYPE returns (see Variants). For an array, VARTYPE adds vbArray to the type of its elements.
    +
    +.table
    +  Constant            Value   Kind of value
    +  --------            -----   -------------
    +  vbEmpty             0       Empty (nothing stored yet)
    +  vbNull              1       Null
    +  vbInteger           2       Integer
    +  vbLong              3       Long
    +  vbSingle            4       Single
    +  vbDouble            5       Double
    +  vbCurrency          6       Currency (Visual Basic only; no DVX value has this type)
    +  vbDate              7       Date
    +  vbString            8       String
    +  vbObject            9       An object or Nothing
    +  vbError             10      Error value (Visual Basic only; no DVX value has this type)
    +  vbBoolean           11      Boolean
    +  vbVariant           12      Variant (only in vbArray + vbVariant, an array of Variants)
    +  vbDataObject        13      Data object (Visual Basic only; no DVX value has this type)
    +  vbUserDefinedType   36      A TYPE variable
    +  vbArray             8192    Added to the element type for an array
    +.endtable
    +
    +.h2 Error Constants
    +
    +.table
    +  Constant        Value         Description
    +  --------        -----         -----------
    +  vbObjectError   -2147221504   Base for your own error numbers, as in Visual Basic: a class module raises ERROR vbObjectError + n, and the handler recovers n with ERR - vbObjectError
    +.endtable
    +
     .h2 Boolean Constants
     
     .table
    @@ -2477,6 +3799,8 @@ Bit values used with GETATTR and SETATTR.
     .toc 1 Include Libraries
     .index Include Libraries
     .index commdlg.bas
    +.index DlgColor
    +.index Color Dialog
     .index sql.bas
     .index comm.bas
     .index resource.bas
    @@ -2545,7 +3869,7 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
     .table
       File           Provides
       ----           --------
    -  commdlg.bas    File Open/Save dialogs, input dialogs, choice dialogs, save-prompt dialogs
    +  commdlg.bas    File Open/Save dialogs, input dialogs, choice dialogs, save-prompt dialogs, the color dialog
       sql.bas        SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.)
       comm.bas       Serial-port I/O (raw and packet) and optional encrypted links
       resource.bas   Read and write DVX resource blocks (icons, text, binary data) in bundled files
    @@ -2570,6 +3894,19 @@ If path <> "" Then
     End If
     .endcode
     
    +.h2 Color Dialog
    +
    +With commdlg.bas in the project, DlgColor(title, defaultColor) shows the color dialog: a palette of basic colors, Red, Green and Blue sliders with number boxes, and a preview. It starts at defaultColor and returns the color chosen, in the form RGB makes and ForeColor and BackColor use, or -1 when the user cancels.
    +
    +.code
    +Dim c As Long
    +c = DlgColor("Text Color", Label1.ForeColor)
    +
    +If c <> -1 Then
    +    Label1.ForeColor = c
    +End If
    +.endcode
    +
     .note info
     DVX BASIC does not have an $INCLUDE directive. Libraries are added to the project so that the compiler sees their DECLARE LIBRARY blocks.
     .endnote
    diff --git a/src/apps/kpunch/dvxbasic/runtime/basErrors.h b/src/apps/kpunch/dvxbasic/runtime/basErrors.h
    index 7c41aab0..2f1ad60e 100644
    --- a/src/apps/kpunch/dvxbasic/runtime/basErrors.h
    +++ b/src/apps/kpunch/dvxbasic/runtime/basErrors.h
    @@ -61,7 +61,13 @@
     #define BAS_ERR_PATH_FILE_ACCESS   75
     #define BAS_ERR_PATH_NOT_FOUND     76
     #define BAS_ERR_OBJECT_NOT_SET     91
    +#define BAS_ERR_INVALID_NULL       94
    +#define BAS_ERR_OBJECT_REQUIRED    424
    +#define BAS_ERR_NO_MEMBER          438
    +#define BAS_ERR_WRONG_ARGS         450
    +#define BAS_ERR_NOT_COLLECTION     451
     #define BAS_ERR_EXTERNAL_NOT_FOUND 453
    +#define BAS_ERR_DUPLICATE_KEY      457
     #define BAS_ERR_NO_FORM_RUNTIME    1000  // form/control statement with no form runtime attached (headless run)
     
     #endif
    diff --git a/src/apps/kpunch/dvxbasic/runtime/dates.c b/src/apps/kpunch/dvxbasic/runtime/dates.c
    new file mode 100644
    index 00000000..7304eca0
    --- /dev/null
    +++ b/src/apps/kpunch/dvxbasic/runtime/dates.c
    @@ -0,0 +1,1244 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dates.c -- DVX BASIC date arithmetic, parsing and formatting
    +//
    +// Every calculation works in whole seconds on the linear time line (see
    +// dates.h), so date and time fields never drift through floating-point
    +// error: a serial is converted to seconds once, rounded to the nearest
    +// second, and converted back once.
    +
    +#include "dates.h"
    +
    +#include 
    +#include 
    +#include 
    +#include 
    +#include 
    +#include 
    +
    +// Calendar arithmetic (proleptic Gregorian).
    +#define DATE_DAYS_PER_YEAR       365
    +#define DATE_LEAP_EVERY          4       // a leap year every 4 years...
    +#define DATE_CENTURY_YEARS       100     // ...except centuries...
    +#define DATE_CYCLE_YEARS         400     // ...except every 400th year
    +#define DATE_CYCLE_DAYS          146097  // days in one 400-year cycle
    +#define DATE_FEBRUARY            2
    +#define DATE_MONTHS_PER_QUARTER  3
    +
    +// Day 0 of the serial scale.
    +#define DATE_EPOCH_YEAR          1899
    +#define DATE_EPOCH_MONTH         12
    +#define DATE_EPOCH_DAY           30
    +
    +// The DATE range.
    +#define DATE_MIN_YEAR            100
    +#define DATE_MAX_YEAR            9999
    +
    +// Two-digit years: 0-29 are 2000-2029, 30-99 are 1930-1999.
    +#define DATE_TWO_DIGIT_MAX       99
    +#define DATE_TWO_DIGIT_PIVOT     30
    +#define DATE_CENTURY_20XX        2000
    +#define DATE_CENTURY_19XX        1900
    +#define DATE_TWO_DIGIT_LEN       2       // digits a windowed year is written with
    +
    +// Time of day.
    +#define DATE_SECS_PER_MINUTE     60
    +#define DATE_SECS_PER_HOUR       3600
    +#define DATE_SECS_PER_DAY        86400
    +#define DATE_MINUTES_PER_HOUR    60
    +#define DATE_HOURS_PER_DAY       24
    +#define DATE_HOURS_HALF_DAY      12
    +#define DATE_ROUND_HALF          0.5     // rounds a serial to the nearest second
    +
    +// Parsing.
    +#define DATE_MAX_FIELDS          3       // month, day, year
    +#define DATE_MAX_NUM_DIGITS      4       // a year is at most four digits
    +#define DATE_MAX_WORD_LEN        16      // longest month name plus slack
    +#define DATE_NAME_MIN_PREFIX     3       // "Sep", "Sept" and "September" all name September
    +#define DATE_MAX_DAY_OF_MONTH    31
    +#define DATE_FIELD_NONE          (-1)
    +#define DATE_DECIMAL_BASE        10
    +
    +// Week of year: a week with fewer days in the new year belongs to the old.
    +#define DATE_FIRST_WEEK_MIN_DAYS 4
    +
    +// Format$ tokens.
    +#define DATE_FMT_SHORT_DATE      "m/d/yyyy"
    +#define DATE_FMT_LONG_DATE       "dddd, mmmm d, yyyy"
    +#define DATE_FMT_MEDIUM_DATE     "dd-mmm-yy"
    +#define DATE_FMT_LONG_TIME       "h:mm:ss AM/PM"
    +#define DATE_FMT_MEDIUM_TIME     "hh:mm AM/PM"
    +#define DATE_FMT_SHORT_TIME      "hh:mm"
    +#define DATE_FMT_GENERAL         "c"
    +#define DATE_FMT_AMPM_LONG       "AM/PM"
    +#define DATE_FMT_AMPM_SHORT      "A/P"
    +#define DATE_FMT_AMPM_LOCALE     "AMPM"
    +#define DATE_FMT_TOKEN_CHARS     "cdwmqyhnst"
    +#define DATE_FMT_TRIGGER_CHARS   "cdwmqyhns"   // letters that make Format$ treat its pattern as a date (as does ttttt)
    +#define DATE_FMT_LONG_DATE_RUN   6       // dddddd
    +#define DATE_FMT_SHORT_DATE_RUN  5       // ddddd
    +#define DATE_FMT_FULL_NAME_RUN   4       // dddd / mmmm
    +#define DATE_FMT_ABBREV_RUN      3       // ddd / mmm
    +#define DATE_FMT_TWO_DIGIT_RUN   2       // dd / mm / yy / hh / nn / ss / ww
    +#define DATE_FMT_LONG_TIME_RUN   5       // ttttt
    +#define DATE_FMT_YEAR_FULL_RUN   4       // yyyy
    +#define DATE_FMT_NUM_LEN         16
    +
    +// Month and weekday names.
    +#define DATE_ABBREV_LEN          3
    +
    +typedef struct {
    +    const char *name;
    +    const char *pattern;   // NULL: the general form
    +} NamedFormatT;
    +
    +typedef struct {
    +    char   *buf;
    +    int32_t size;
    +    int32_t len;
    +} FmtOutT;
    +
    +// Fields collected by basDateParse.
    +typedef struct {
    +    int32_t nums[DATE_MAX_FIELDS];
    +    int32_t digits[DATE_MAX_FIELDS];
    +    int32_t numCount;
    +    int32_t monthName;     // 1-12 when the text names the month, else 0
    +    bool    hasTime;
    +    int32_t hour;
    +    int32_t minute;
    +    int32_t second;
    +    int32_t ampm;          // 0 none, else DATE_AM / DATE_PM
    +} DateFieldsT;
    +
    +#define DATE_AM                  1
    +#define DATE_PM                  2
    +
    +static const int32_t sDaysInMonth[BAS_DATE_MONTHS_PER_YEAR] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    +
    +static const char *sMonthNames[BAS_DATE_MONTHS_PER_YEAR] = {
    +    "January", "February", "March", "April", "May", "June",
    +    "July", "August", "September", "October", "November", "December"
    +};
    +
    +static const char *sWeekdayNames[BAS_DATE_DAYS_PER_WEEK] = {
    +    "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
    +};
    +
    +static const NamedFormatT sNamedFormats[] = {
    +    { "General Date", NULL                 },
    +    { "Long Date",    DATE_FMT_LONG_DATE   },
    +    { "Medium Date",  DATE_FMT_MEDIUM_DATE },
    +    { "Short Date",   DATE_FMT_SHORT_DATE  },
    +    { "Long Time",    DATE_FMT_LONG_TIME   },
    +    { "Medium Time",  DATE_FMT_MEDIUM_TIME },
    +    { "Short Time",   DATE_FMT_SHORT_TIME  }
    +};
    +
    +#define DATE_NAMED_FORMAT_COUNT  ((int32_t)(sizeof(sNamedFormats) / sizeof(sNamedFormats[0])))
    +
    +// Function prototypes (alphabetical)
    +static bool assembleDate(const DateFieldsT *f, int64_t *outDay);
    +bool basDateAdd(BasDateIntervalE interval, int32_t count, double serial, double *out);
    +int64_t basDateDiff(BasDateIntervalE interval, double from, double to, int32_t firstDayOfWeek);
    +int32_t basDateFormat(double serial, const char *fmt, char *buf, int32_t bufSize);
    +bool basDateFromLinear(double linear, double *outSerial);
    +bool basDateIsPattern(const char *fmt);
    +bool basDateIsValid(double serial);
    +const char *basDateMonthName(int32_t month, bool abbrev);
    +bool basDateParse(const char *text, double *outSerial);
    +bool basDateParseInterval(const char *text, BasDateIntervalE *out);
    +int32_t basDatePart(BasDateIntervalE interval, double serial, int32_t firstDayOfWeek, int32_t firstWeekOfYear);
    +bool basDateSerial(int32_t year, int32_t month, int32_t day, double *outSerial);
    +void basDateSplit(double serial, BasDatePartsT *out);
    +double basDateToLinear(double serial);
    +int32_t basDateWeekday(double serial, int32_t firstDayOfWeek);
    +const char *basDateWeekdayName(int32_t weekday, bool abbrev);
    +bool basTimeSerial(int32_t hour, int32_t minute, int32_t second, double *outSerial);
    +static int32_t capRun(int32_t run);
    +static int64_t dayNumber(int64_t year, int32_t month, int32_t day);
    +static int64_t daysBeforeYear(int64_t year);
    +static int32_t daysInMonth(int64_t year, int32_t month);
    +static void dayToCivil(int64_t day, int32_t *outYear, int32_t *outMonth, int32_t *outDay);
    +static int64_t floorDiv(int64_t a, int64_t b);
    +static int64_t floorMod(int64_t a, int64_t b);
    +static void fmtGeneral(FmtOutT *o, const BasDatePartsT *p);
    +static void fmtNum(FmtOutT *o, int32_t value, int32_t minDigits);
    +static void fmtPattern(FmtOutT *o, const BasDatePartsT *p, const char *fmt);
    +static void fmtPutc(FmtOutT *o, char c);
    +static void fmtPuts(FmtOutT *o, const char *s);
    +static bool fromSeconds(int64_t secs, double *outSerial);
    +static bool isLeapYear(int64_t year);
    +static const char *lookupNamedFormat(const char *fmt, bool *outFound);
    +static int32_t matchAmPm(const char *s, bool *outUpper, bool *outShort);
    +static int32_t matchMonthName(const char *word);
    +static bool nextTokenIsSecond(const char *s);
    +static int64_t ordinalOfEpoch(void);
    +static int32_t parseAmPmWord(const char *s);
    +static int32_t runLength(const char *s, char lower);
    +static int64_t toSeconds(double serial);
    +static int32_t weekdayOfDay(int64_t day, int32_t firstDayOfWeek);
    +static int32_t weekOfYear(int64_t day, int32_t year, int32_t firstDayOfWeek, int32_t firstWeekOfYear);
    +static int64_t weekStart(int64_t day, int32_t firstDayOfWeek);
    +static int32_t windowYear(int32_t year, int32_t digits);
    +
    +
    +// Builds the day number from the parsed numeric fields and month name.
    +// False when the fields do not form a valid calendar date.
    +static bool assembleDate(const DateFieldsT *f, int64_t *outDay) {
    +    int32_t year       = DATE_FIELD_NONE;
    +    int32_t month      = DATE_FIELD_NONE;
    +    int32_t day        = DATE_FIELD_NONE;
    +    int32_t yearDigits = 0;
    +
    +    if (f->monthName > 0) {
    +        month = f->monthName;
    +
    +        if (f->numCount == 1) {
    +            // "Jan 5" or "Jan 2000"
    +            if (f->digits[0] > DATE_TWO_DIGIT_LEN || f->nums[0] > DATE_MAX_DAY_OF_MONTH) {
    +                year       = f->nums[0];
    +                yearDigits = f->digits[0];
    +                day        = 1;
    +            } else {
    +                day = f->nums[0];
    +            }
    +        } else if (f->numCount == 2) {
    +            // "Jan 5, 2000", "5 Jan 2000" or "2000 Jan 5"
    +            bool yearFirst = f->digits[0] > DATE_TWO_DIGIT_LEN;
    +
    +            day        = yearFirst ? f->nums[1] : f->nums[0];
    +            year       = yearFirst ? f->nums[0] : f->nums[1];
    +            yearDigits = yearFirst ? f->digits[0] : f->digits[1];
    +        } else {
    +            return false;
    +        }
    +    } else if (f->numCount == 3) {
    +        if (f->digits[0] > DATE_TWO_DIGIT_LEN) {
    +            // y-m-d
    +            year       = f->nums[0];
    +            yearDigits = f->digits[0];
    +            month      = f->nums[1];
    +            day        = f->nums[2];
    +        } else {
    +            month      = f->nums[0];
    +            day        = f->nums[1];
    +            year       = f->nums[2];
    +            yearDigits = f->digits[2];
    +        }
    +    } else if (f->numCount == 2) {
    +        if (f->digits[1] > DATE_TWO_DIGIT_LEN || f->nums[1] > DATE_MAX_DAY_OF_MONTH) {
    +            // "1/2000": the first of the month
    +            month      = f->nums[0];
    +            year       = f->nums[1];
    +            yearDigits = f->digits[1];
    +            day        = 1;
    +        } else {
    +            month = f->nums[0];
    +            day   = f->nums[1];
    +        }
    +    } else {
    +        return false;
    +    }
    +
    +    // VB reads a month over 12 as the day when that makes a valid date.
    +    if (f->monthName == 0 && month > BAS_DATE_MONTHS_PER_YEAR && day <= BAS_DATE_MONTHS_PER_YEAR) {
    +        int32_t swap = month;
    +
    +        month = day;
    +        day   = swap;
    +    }
    +
    +    if (year == DATE_FIELD_NONE) {
    +        time_t     now   = time(NULL);
    +        struct tm *local = localtime(&now);
    +
    +        year = local ? local->tm_year + DATE_CENTURY_19XX : DATE_EPOCH_YEAR;
    +    } else {
    +        year = windowYear(year, yearDigits);
    +    }
    +
    +    if (year < DATE_MIN_YEAR || year > DATE_MAX_YEAR || month < 1 || month > BAS_DATE_MONTHS_PER_YEAR || day < 1 || day > daysInMonth(year, month)) {
    +        return false;
    +    }
    +
    +    *outDay = dayNumber(year, month, day);
    +    return true;
    +}
    +
    +
    +bool basDateAdd(BasDateIntervalE interval, int32_t count, double serial, double *out) {
    +    int64_t secs = toSeconds(serial);
    +
    +    switch (interval) {
    +        case BAS_DATE_IV_YEAR:
    +        case BAS_DATE_IV_QUARTER:
    +        case BAS_DATE_IV_MONTH: {
    +            int64_t months = count;
    +
    +            if (interval == BAS_DATE_IV_YEAR) {
    +                months *= BAS_DATE_MONTHS_PER_YEAR;
    +            } else if (interval == BAS_DATE_IV_QUARTER) {
    +                months *= DATE_MONTHS_PER_QUARTER;
    +            }
    +
    +            BasDatePartsT p;
    +
    +            basDateSplit(serial, &p);
    +
    +            // The day is clamped to the new month's length (Jan 31 + 1
    +            // month is the last day of February).
    +            int64_t total = (int64_t)p.year * BAS_DATE_MONTHS_PER_YEAR + (p.month - 1) + months;
    +            int64_t year  = floorDiv(total, BAS_DATE_MONTHS_PER_YEAR);
    +            int32_t month = (int32_t)(total - year * BAS_DATE_MONTHS_PER_YEAR) + 1;
    +
    +            if (year < DATE_MIN_YEAR || year > DATE_MAX_YEAR) {
    +                return false;
    +            }
    +
    +            int32_t day     = p.day;
    +            int32_t lastDay = daysInMonth(year, month);
    +
    +            if (day > lastDay) {
    +                day = lastDay;
    +            }
    +
    +            secs = dayNumber(year, month, day) * DATE_SECS_PER_DAY + p.secOfDay;
    +            break;
    +        }
    +
    +        case BAS_DATE_IV_DAY_OF_YEAR:
    +        case BAS_DATE_IV_DAY:
    +        case BAS_DATE_IV_WEEKDAY:
    +            secs += (int64_t)count * DATE_SECS_PER_DAY;
    +            break;
    +
    +        case BAS_DATE_IV_WEEK:
    +            secs += (int64_t)count * BAS_DATE_DAYS_PER_WEEK * DATE_SECS_PER_DAY;
    +            break;
    +
    +        case BAS_DATE_IV_HOUR:
    +            secs += (int64_t)count * DATE_SECS_PER_HOUR;
    +            break;
    +
    +        case BAS_DATE_IV_MINUTE:
    +            secs += (int64_t)count * DATE_SECS_PER_MINUTE;
    +            break;
    +
    +        case BAS_DATE_IV_SECOND:
    +            secs += count;
    +            break;
    +    }
    +
    +    return fromSeconds(secs, out);
    +}
    +
    +
    +int64_t basDateDiff(BasDateIntervalE interval, double from, double to, int32_t firstDayOfWeek) {
    +    BasDatePartsT a;
    +    BasDatePartsT b;
    +
    +    basDateSplit(from, &a);
    +    basDateSplit(to, &b);
    +
    +    int64_t secsA = a.dayNumber * DATE_SECS_PER_DAY + a.secOfDay;
    +    int64_t secsB = b.dayNumber * DATE_SECS_PER_DAY + b.secOfDay;
    +
    +    switch (interval) {
    +        case BAS_DATE_IV_YEAR:
    +            return (int64_t)b.year - a.year;
    +
    +        case BAS_DATE_IV_QUARTER:
    +            return ((int64_t)b.year * BAS_DATE_MONTHS_PER_YEAR + b.month - 1) / DATE_MONTHS_PER_QUARTER - ((int64_t)a.year * BAS_DATE_MONTHS_PER_YEAR + a.month - 1) / DATE_MONTHS_PER_QUARTER;
    +
    +        case BAS_DATE_IV_MONTH:
    +            return ((int64_t)b.year * BAS_DATE_MONTHS_PER_YEAR + b.month) - ((int64_t)a.year * BAS_DATE_MONTHS_PER_YEAR + a.month);
    +
    +        case BAS_DATE_IV_DAY_OF_YEAR:
    +        case BAS_DATE_IV_DAY:
    +            return b.dayNumber - a.dayNumber;
    +
    +        case BAS_DATE_IV_WEEKDAY:
    +            // Whole weeks: the times the first date's weekday comes round.
    +            return (b.dayNumber - a.dayNumber) / BAS_DATE_DAYS_PER_WEEK;
    +
    +        case BAS_DATE_IV_WEEK:
    +            // Calendar weeks: first days of the week crossed.
    +            return (weekStart(b.dayNumber, firstDayOfWeek) - weekStart(a.dayNumber, firstDayOfWeek)) / BAS_DATE_DAYS_PER_WEEK;
    +
    +        case BAS_DATE_IV_HOUR:
    +            return floorDiv(secsB, DATE_SECS_PER_HOUR) - floorDiv(secsA, DATE_SECS_PER_HOUR);
    +
    +        case BAS_DATE_IV_MINUTE:
    +            return floorDiv(secsB, DATE_SECS_PER_MINUTE) - floorDiv(secsA, DATE_SECS_PER_MINUTE);
    +
    +        case BAS_DATE_IV_SECOND:
    +            return secsB - secsA;
    +    }
    +
    +    return 0;
    +}
    +
    +
    +int32_t basDateFormat(double serial, const char *fmt, char *buf, int32_t bufSize) {
    +    FmtOutT       o;
    +    BasDatePartsT p;
    +
    +    o.buf  = buf;
    +    o.size = bufSize;
    +    o.len  = 0;
    +
    +    if (bufSize > 0) {
    +        buf[0] = '\0';
    +    }
    +
    +    basDateSplit(serial, &p);
    +
    +    bool        named   = false;
    +    const char *pattern = (fmt == NULL) ? NULL : lookupNamedFormat(fmt, &named);
    +
    +    if (fmt == NULL || fmt[0] == '\0' || (named && pattern == NULL)) {
    +        fmtGeneral(&o, &p);
    +    } else {
    +        fmtPattern(&o, &p, named ? pattern : fmt);
    +    }
    +
    +    return o.len;
    +}
    +
    +
    +bool basDateFromLinear(double linear, double *outSerial) {
    +    if (!(linear >= (double)dayNumber(DATE_MIN_YEAR, 1, 1) && linear < (double)(dayNumber(DATE_MAX_YEAR, BAS_DATE_MONTHS_PER_YEAR, DATE_MAX_DAY_OF_MONTH) + 1))) {
    +        return false;
    +    }
    +
    +    double day  = floor(linear);
    +    double frac = linear - day;
    +
    +    // Before the epoch the time still counts forward from midnight, so the
    +    // fraction moves away from zero with the day: -0.75 is -1.25.
    +    *outSerial = (day < 0.0 && frac > 0.0) ? day - frac : linear;
    +    return true;
    +}
    +
    +
    +bool basDateIsPattern(const char *fmt) {
    +    bool named = false;
    +
    +    lookupNamedFormat(fmt, &named);
    +
    +    if (named) {
    +        return true;
    +    }
    +
    +    for (const char *s = fmt; *s; s++) {
    +        bool upper;
    +        bool isShort;
    +
    +        if (*s == '"') {
    +            const char *end = strchr(s + 1, '"');
    +
    +            if (end == NULL) {
    +                return false;
    +            }
    +
    +            s = end;
    +        } else if (*s == '\\') {
    +            if (s[1] == '\0') {
    +                return false;
    +            }
    +
    +            s++;
    +        } else if (strchr(DATE_FMT_TRIGGER_CHARS, tolower((unsigned char)*s)) != NULL || matchAmPm(s, &upper, &isShort) > 0 || runLength(s, 't') >= DATE_FMT_LONG_TIME_RUN) {
    +            return true;
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
    +bool basDateIsValid(double serial) {
    +    double unused;
    +
    +    return !isnan(serial) && basDateFromLinear(basDateToLinear(serial), &unused);
    +}
    +
    +
    +const char *basDateMonthName(int32_t month, bool abbrev) {
    +    static char abbrevBuf[BAS_DATE_MONTHS_PER_YEAR][DATE_ABBREV_LEN + 1];
    +
    +    if (month < 1 || month > BAS_DATE_MONTHS_PER_YEAR) {
    +        return NULL;
    +    }
    +
    +    if (!abbrev) {
    +        return sMonthNames[month - 1];
    +    }
    +
    +    snprintf(abbrevBuf[month - 1], sizeof(abbrevBuf[0]), "%.*s", DATE_ABBREV_LEN, sMonthNames[month - 1]);
    +    return abbrevBuf[month - 1];
    +}
    +
    +
    +bool basDateParse(const char *text, double *outSerial) {
    +    DateFieldsT f;
    +    const char *s = text;
    +
    +    memset(&f, 0, sizeof(f));
    +
    +    while (*s) {
    +        unsigned char c = (unsigned char)*s;
    +
    +        if (c == ' ' || c == '\t' || c == '/' || c == '-' || c == ',') {
    +            s++;
    +            continue;
    +        }
    +
    +        if (isdigit(c)) {
    +            int32_t value  = 0;
    +            int32_t digits = 0;
    +
    +            while (isdigit((unsigned char)*s)) {
    +                if (++digits > DATE_MAX_NUM_DIGITS) {
    +                    return false;
    +                }
    +
    +                value = value * DATE_DECIMAL_BASE + (*s - '0');
    +                s++;
    +            }
    +
    +            const char *after = s;
    +
    +            while (*after == ' ') {
    +                after++;
    +            }
    +
    +            if (*s == ':' || parseAmPmWord(after) > 0) {
    +                // A time: h:mm[:ss] [AM|PM] or h AM|PM
    +                if (f.hasTime) {
    +                    return false;
    +                }
    +
    +                f.hasTime = true;
    +                f.hour    = value;
    +
    +                int32_t *fields[]   = { &f.minute, &f.second };
    +                int32_t  fieldCount = (int32_t)(sizeof(fields) / sizeof(fields[0]));
    +
    +                for (int32_t i = 0; i < fieldCount && *s == ':'; i++) {
    +                    s++;
    +
    +                    if (!isdigit((unsigned char)*s)) {
    +                        return false;
    +                    }
    +
    +                    int32_t fieldDigits = 0;
    +
    +                    while (isdigit((unsigned char)*s)) {
    +                        if (++fieldDigits > DATE_TWO_DIGIT_LEN) {
    +                            return false;
    +                        }
    +
    +                        *fields[i] = *fields[i] * DATE_DECIMAL_BASE + (*s - '0');
    +                        s++;
    +                    }
    +                }
    +
    +                while (*s == ' ') {
    +                    s++;
    +                }
    +
    +                int32_t ampmLen = parseAmPmWord(s);
    +
    +                if (ampmLen > 0) {
    +                    f.ampm = (tolower((unsigned char)*s) == 'a') ? DATE_AM : DATE_PM;
    +                    s     += ampmLen;
    +                }
    +
    +                continue;
    +            }
    +
    +            if (f.numCount >= DATE_MAX_FIELDS) {
    +                return false;
    +            }
    +
    +            f.nums[f.numCount]   = value;
    +            f.digits[f.numCount] = digits;
    +            f.numCount++;
    +            continue;
    +        }
    +
    +        if (isalpha(c)) {
    +            char    word[DATE_MAX_WORD_LEN];
    +            int32_t len = 0;
    +
    +            while (isalpha((unsigned char)*s)) {
    +                if (len >= DATE_MAX_WORD_LEN - 1) {
    +                    return false;
    +                }
    +
    +                word[len++] = *s++;
    +            }
    +
    +            word[len] = '\0';
    +
    +            int32_t month = matchMonthName(word);
    +
    +            if (month == 0 || f.monthName != 0) {
    +                return false;
    +            }
    +
    +            f.monthName = month;
    +            continue;
    +        }
    +
    +        return false;
    +    }
    +
    +    int64_t day = 0;
    +
    +    if (f.numCount > 0 || f.monthName > 0) {
    +        if (!assembleDate(&f, &day)) {
    +            return false;
    +        }
    +    } else if (!f.hasTime) {
    +        return false;
    +    }
    +
    +    int32_t hour = f.hour;
    +
    +    if (f.ampm != 0) {
    +        if (hour < 1 || hour > DATE_HOURS_HALF_DAY) {
    +            return false;
    +        }
    +
    +        hour %= DATE_HOURS_HALF_DAY;
    +
    +        if (f.ampm == DATE_PM) {
    +            hour += DATE_HOURS_HALF_DAY;
    +        }
    +    }
    +
    +    if (hour >= DATE_HOURS_PER_DAY || f.minute >= DATE_MINUTES_PER_HOUR || f.second >= DATE_SECS_PER_MINUTE) {
    +        return false;
    +    }
    +
    +    return fromSeconds(day * DATE_SECS_PER_DAY + (int64_t)hour * DATE_SECS_PER_HOUR + (int64_t)f.minute * DATE_SECS_PER_MINUTE + f.second, outSerial);
    +}
    +
    +
    +bool basDateParseInterval(const char *text, BasDateIntervalE *out) {
    +    static const struct {
    +        const char      *name;
    +        BasDateIntervalE interval;
    +    } intervals[] = {
    +        { "yyyy", BAS_DATE_IV_YEAR        },
    +        { "q",    BAS_DATE_IV_QUARTER     },
    +        { "m",    BAS_DATE_IV_MONTH       },
    +        { "y",    BAS_DATE_IV_DAY_OF_YEAR },
    +        { "d",    BAS_DATE_IV_DAY         },
    +        { "w",    BAS_DATE_IV_WEEKDAY     },
    +        { "ww",   BAS_DATE_IV_WEEK        },
    +        { "h",    BAS_DATE_IV_HOUR        },
    +        { "n",    BAS_DATE_IV_MINUTE      },
    +        { "s",    BAS_DATE_IV_SECOND      }
    +    };
    +
    +    for (size_t i = 0; i < sizeof(intervals) / sizeof(intervals[0]); i++) {
    +        if (strcasecmp(text, intervals[i].name) == 0) {
    +            *out = intervals[i].interval;
    +            return true;
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
    +int32_t basDatePart(BasDateIntervalE interval, double serial, int32_t firstDayOfWeek, int32_t firstWeekOfYear) {
    +    BasDatePartsT p;
    +
    +    basDateSplit(serial, &p);
    +
    +    switch (interval) {
    +        case BAS_DATE_IV_YEAR:
    +            return p.year;
    +
    +        case BAS_DATE_IV_QUARTER:
    +            return (p.month - 1) / DATE_MONTHS_PER_QUARTER + 1;
    +
    +        case BAS_DATE_IV_MONTH:
    +            return p.month;
    +
    +        case BAS_DATE_IV_DAY_OF_YEAR:
    +            return p.dayOfYear;
    +
    +        case BAS_DATE_IV_DAY:
    +            return p.day;
    +
    +        case BAS_DATE_IV_WEEKDAY:
    +            return weekdayOfDay(p.dayNumber, firstDayOfWeek);
    +
    +        case BAS_DATE_IV_WEEK:
    +            return weekOfYear(p.dayNumber, p.year, firstDayOfWeek, firstWeekOfYear);
    +
    +        case BAS_DATE_IV_HOUR:
    +            return p.hour;
    +
    +        case BAS_DATE_IV_MINUTE:
    +            return p.minute;
    +
    +        case BAS_DATE_IV_SECOND:
    +            return p.second;
    +    }
    +
    +    return 0;
    +}
    +
    +
    +bool basDateSerial(int32_t year, int32_t month, int32_t day, double *outSerial) {
    +    int64_t total = (int64_t)windowYear(year, DATE_TWO_DIGIT_LEN) * BAS_DATE_MONTHS_PER_YEAR + (int64_t)month - 1;
    +    int64_t y     = floorDiv(total, BAS_DATE_MONTHS_PER_YEAR);
    +    int32_t m     = (int32_t)(total - y * BAS_DATE_MONTHS_PER_YEAR) + 1;
    +
    +    return fromSeconds((dayNumber(y, m, 1) + (int64_t)day - 1) * DATE_SECS_PER_DAY, outSerial);
    +}
    +
    +
    +void basDateSplit(double serial, BasDatePartsT *out) {
    +    int64_t secs = toSeconds(serial);
    +    int64_t day  = floorDiv(secs, DATE_SECS_PER_DAY);
    +    int32_t sod  = (int32_t)(secs - day * DATE_SECS_PER_DAY);
    +
    +    dayToCivil(day, &out->year, &out->month, &out->day);
    +
    +    out->dayNumber = day;
    +    out->secOfDay  = sod;
    +    out->hour      = sod / DATE_SECS_PER_HOUR;
    +    out->minute    = (sod % DATE_SECS_PER_HOUR) / DATE_SECS_PER_MINUTE;
    +    out->second    = sod % DATE_SECS_PER_MINUTE;
    +    out->weekday   = weekdayOfDay(day, BAS_DATE_SUNDAY);
    +    out->dayOfYear = (int32_t)(day - dayNumber(out->year, 1, 1)) + 1;
    +}
    +
    +
    +double basDateToLinear(double serial) {
    +    double day = trunc(serial);
    +
    +    return day + fabs(serial - day);
    +}
    +
    +
    +int32_t basDateWeekday(double serial, int32_t firstDayOfWeek) {
    +    return weekdayOfDay(floorDiv(toSeconds(serial), DATE_SECS_PER_DAY), firstDayOfWeek);
    +}
    +
    +
    +const char *basDateWeekdayName(int32_t weekday, bool abbrev) {
    +    static char abbrevBuf[BAS_DATE_DAYS_PER_WEEK][DATE_ABBREV_LEN + 1];
    +
    +    if (weekday < 1 || weekday > BAS_DATE_DAYS_PER_WEEK) {
    +        return NULL;
    +    }
    +
    +    if (!abbrev) {
    +        return sWeekdayNames[weekday - 1];
    +    }
    +
    +    snprintf(abbrevBuf[weekday - 1], sizeof(abbrevBuf[0]), "%.*s", DATE_ABBREV_LEN, sWeekdayNames[weekday - 1]);
    +    return abbrevBuf[weekday - 1];
    +}
    +
    +
    +bool basTimeSerial(int32_t hour, int32_t minute, int32_t second, double *outSerial) {
    +    return fromSeconds((int64_t)hour * DATE_SECS_PER_HOUR + (int64_t)minute * DATE_SECS_PER_MINUTE + second, outSerial);
    +}
    +
    +
    +// h / n / s tokens are one or two digits wide; a longer run repeats.
    +static int32_t capRun(int32_t run) {
    +    return (run > DATE_FMT_TWO_DIGIT_RUN) ? DATE_FMT_TWO_DIGIT_RUN : run;
    +}
    +
    +
    +// Serial day number of a calendar date (month 1-12, day may run past
    +// the month's end).
    +static int64_t dayNumber(int64_t year, int32_t month, int32_t day) {
    +    int64_t days = daysBeforeYear(year);
    +
    +    for (int32_t m = 1; m < month; m++) {
    +        days += daysInMonth(year, m);
    +    }
    +
    +    return days + day - 1 - ordinalOfEpoch();
    +}
    +
    +
    +// Days from 1/1/0001 to 1/1 of year.
    +static int64_t daysBeforeYear(int64_t year) {
    +    int64_t y = year - 1;
    +
    +    return y * DATE_DAYS_PER_YEAR + floorDiv(y, DATE_LEAP_EVERY) - floorDiv(y, DATE_CENTURY_YEARS) + floorDiv(y, DATE_CYCLE_YEARS);
    +}
    +
    +
    +static int32_t daysInMonth(int64_t year, int32_t month) {
    +    if (month == DATE_FEBRUARY && isLeapYear(year)) {
    +        return sDaysInMonth[month - 1] + 1;
    +    }
    +
    +    return sDaysInMonth[month - 1];
    +}
    +
    +
    +static void dayToCivil(int64_t day, int32_t *outYear, int32_t *outMonth, int32_t *outDay) {
    +    int64_t ordinal = day + ordinalOfEpoch();
    +    int64_t year    = floorDiv(ordinal * DATE_CYCLE_YEARS, DATE_CYCLE_DAYS) + 1;
    +
    +    // The estimate is within a year either way.
    +    while (daysBeforeYear(year) > ordinal) {
    +        year--;
    +    }
    +
    +    while (daysBeforeYear(year + 1) <= ordinal) {
    +        year++;
    +    }
    +
    +    int64_t rest  = ordinal - daysBeforeYear(year);
    +    int32_t month = 1;
    +
    +    while (month < BAS_DATE_MONTHS_PER_YEAR && rest >= daysInMonth(year, month)) {
    +        rest -= daysInMonth(year, month);
    +        month++;
    +    }
    +
    +    *outYear  = (int32_t)year;
    +    *outMonth = month;
    +    *outDay   = (int32_t)rest + 1;
    +}
    +
    +
    +static int64_t floorDiv(int64_t a, int64_t b) {
    +    int64_t q = a / b;
    +
    +    if ((a % b != 0) && ((a < 0) != (b < 0))) {
    +        q--;
    +    }
    +
    +    return q;
    +}
    +
    +
    +static int64_t floorMod(int64_t a, int64_t b) {
    +    return a - floorDiv(a, b) * b;
    +}
    +
    +
    +// The general form: time alone on day 0, date alone at midnight, else
    +// both.
    +static void fmtGeneral(FmtOutT *o, const BasDatePartsT *p) {
    +    if (p->dayNumber == 0) {
    +        fmtPattern(o, p, DATE_FMT_LONG_TIME);
    +    } else if (p->secOfDay == 0) {
    +        fmtPattern(o, p, DATE_FMT_SHORT_DATE);
    +    } else {
    +        fmtPattern(o, p, DATE_FMT_SHORT_DATE " " DATE_FMT_LONG_TIME);
    +    }
    +}
    +
    +
    +static void fmtNum(FmtOutT *o, int32_t value, int32_t minDigits) {
    +    char num[DATE_FMT_NUM_LEN];
    +
    +    snprintf(num, sizeof(num), "%0*d", (int)minDigits, (int)value);
    +    fmtPuts(o, num);
    +}
    +
    +
    +// Expand a Format$ date pattern.  Tokens are case-insensitive; text in
    +// double quotes and characters after a backslash are copied as they are,
    +// as is every character that is not a token.
    +static void fmtPattern(FmtOutT *o, const BasDatePartsT *p, const char *fmt) {
    +    bool upper;
    +    bool isShort;
    +    bool twelveHour = false;
    +    bool afterHour  = false;
    +
    +    for (const char *s = fmt; *s; s++) {
    +        if (matchAmPm(s, &upper, &isShort) > 0) {
    +            twelveHour = true;
    +        }
    +    }
    +
    +    const char *s = fmt;
    +
    +    while (*s) {
    +        int32_t ampmLen = matchAmPm(s, &upper, &isShort);
    +
    +        if (ampmLen > 0) {
    +            bool        pm   = p->hour >= DATE_HOURS_HALF_DAY;
    +            const char *text = pm ? (upper ? "PM" : "pm") : (upper ? "AM" : "am");
    +
    +            if (isShort) {
    +                fmtPutc(o, text[0]);
    +            } else {
    +                fmtPuts(o, text);
    +            }
    +
    +            s += ampmLen;
    +            continue;
    +        }
    +
    +        if (*s == '"') {
    +            s++;
    +
    +            while (*s && *s != '"') {
    +                fmtPutc(o, *s++);
    +            }
    +
    +            if (*s == '"') {
    +                s++;
    +            }
    +
    +            continue;
    +        }
    +
    +        if (*s == '\\') {
    +            if (s[1] != '\0') {
    +                fmtPutc(o, s[1]);
    +                s += 2;
    +            } else {
    +                s++;
    +            }
    +
    +            continue;
    +        }
    +
    +        char    lower = (char)tolower((unsigned char)*s);
    +        int32_t run   = runLength(s, lower);
    +        bool    token = true;
    +
    +        switch (lower) {
    +            case 'c':
    +                fmtGeneral(o, p);
    +                run = 1;
    +                break;
    +
    +            case 'd':
    +                if (run >= DATE_FMT_LONG_DATE_RUN) {
    +                    fmtPattern(o, p, DATE_FMT_LONG_DATE);
    +                } else if (run == DATE_FMT_SHORT_DATE_RUN) {
    +                    fmtPattern(o, p, DATE_FMT_SHORT_DATE);
    +                } else if (run == DATE_FMT_FULL_NAME_RUN) {
    +                    fmtPuts(o, basDateWeekdayName(p->weekday, false));
    +                } else if (run == DATE_FMT_ABBREV_RUN) {
    +                    fmtPuts(o, basDateWeekdayName(p->weekday, true));
    +                } else {
    +                    fmtNum(o, p->day, run);
    +                }
    +                break;
    +
    +            case 'w':
    +                if (run >= DATE_FMT_TWO_DIGIT_RUN) {
    +                    fmtNum(o, weekOfYear(p->dayNumber, p->year, BAS_DATE_SUNDAY, BAS_DATE_FIRST_JAN1), 1);
    +                    run = DATE_FMT_TWO_DIGIT_RUN;
    +                } else {
    +                    fmtNum(o, p->weekday, 1);
    +                }
    +                break;
    +
    +            case 'm':
    +                // m / mm right after an hour or right before a second is
    +                // the minute.
    +                if (run <= DATE_FMT_TWO_DIGIT_RUN && (afterHour || nextTokenIsSecond(s + run))) {
    +                    fmtNum(o, p->minute, run);
    +                } else if (run >= DATE_FMT_FULL_NAME_RUN) {
    +                    fmtPuts(o, basDateMonthName(p->month, false));
    +                } else if (run == DATE_FMT_ABBREV_RUN) {
    +                    fmtPuts(o, basDateMonthName(p->month, true));
    +                } else {
    +                    fmtNum(o, p->month, run);
    +                }
    +                break;
    +
    +            case 'q':
    +                fmtNum(o, (p->month - 1) / DATE_MONTHS_PER_QUARTER + 1, 1);
    +                run = 1;
    +                break;
    +
    +            case 'y':
    +                if (run >= DATE_FMT_YEAR_FULL_RUN) {
    +                    fmtNum(o, p->year, 1);
    +                } else if (run >= DATE_FMT_TWO_DIGIT_RUN) {
    +                    fmtNum(o, p->year % DATE_CENTURY_YEARS, DATE_FMT_TWO_DIGIT_RUN);
    +                } else {
    +                    fmtNum(o, p->dayOfYear, 1);
    +                }
    +                break;
    +
    +            case 'h': {
    +                int32_t hour = p->hour;
    +
    +                if (twelveHour) {
    +                    hour %= DATE_HOURS_HALF_DAY;
    +
    +                    if (hour == 0) {
    +                        hour = DATE_HOURS_HALF_DAY;
    +                    }
    +                }
    +
    +                run = capRun(run);
    +                fmtNum(o, hour, run);
    +                break;
    +            }
    +
    +            case 'n':
    +                run = capRun(run);
    +                fmtNum(o, p->minute, run);
    +                break;
    +
    +            case 's':
    +                run = capRun(run);
    +                fmtNum(o, p->second, run);
    +                break;
    +
    +            case 't':
    +                if (run >= DATE_FMT_LONG_TIME_RUN) {
    +                    fmtPattern(o, p, DATE_FMT_LONG_TIME);
    +                } else {
    +                    token = false;
    +                }
    +                break;
    +
    +            default:
    +                token = false;
    +                break;
    +        }
    +
    +        if (!token) {
    +            fmtPutc(o, *s++);
    +            continue;
    +        }
    +
    +        afterHour = (lower == 'h');
    +        s        += run;
    +    }
    +}
    +
    +
    +static void fmtPutc(FmtOutT *o, char c) {
    +    if (o->len + 1 < o->size) {
    +        o->buf[o->len++] = c;
    +        o->buf[o->len]   = '\0';
    +    }
    +}
    +
    +
    +static void fmtPuts(FmtOutT *o, const char *s) {
    +    while (*s) {
    +        fmtPutc(o, *s++);
    +    }
    +}
    +
    +
    +// Serial of an instant given in seconds on the linear time line.
    +static bool fromSeconds(int64_t secs, double *outSerial) {
    +    return basDateFromLinear((double)secs / DATE_SECS_PER_DAY, outSerial);
    +}
    +
    +
    +static bool isLeapYear(int64_t year) {
    +    return (year % DATE_LEAP_EVERY == 0 && year % DATE_CENTURY_YEARS != 0) || year % DATE_CYCLE_YEARS == 0;
    +}
    +
    +
    +// The pattern of a named format; *outFound tells a match from no match
    +// (General Date matches with a NULL pattern).
    +static const char *lookupNamedFormat(const char *fmt, bool *outFound) {
    +    for (int32_t i = 0; i < DATE_NAMED_FORMAT_COUNT; i++) {
    +        if (strcasecmp(fmt, sNamedFormats[i].name) == 0) {
    +            *outFound = true;
    +            return sNamedFormats[i].pattern;
    +        }
    +    }
    +
    +    *outFound = false;
    +    return NULL;
    +}
    +
    +
    +// Length of a Format$ AM/PM marker at s ("AM/PM", "AMPM" or "A/P"), 0
    +// when there is none.  *outUpper follows the case of its first letter;
    +// *outShort is set for the one-letter form.
    +static int32_t matchAmPm(const char *s, bool *outUpper, bool *outShort) {
    +    static const struct {
    +        const char *text;
    +        bool        isShort;
    +    } markers[] = {
    +        { DATE_FMT_AMPM_LONG,   false },
    +        { DATE_FMT_AMPM_LOCALE, false },
    +        { DATE_FMT_AMPM_SHORT,  true  }
    +    };
    +
    +    *outUpper = isupper((unsigned char)*s) != 0;
    +
    +    for (size_t i = 0; i < sizeof(markers) / sizeof(markers[0]); i++) {
    +        size_t len = strlen(markers[i].text);
    +
    +        if (strncasecmp(s, markers[i].text, len) == 0) {
    +            *outShort = markers[i].isShort;
    +            return (int32_t)len;
    +        }
    +    }
    +
    +    return 0;
    +}
    +
    +
    +// Month (1-12) whose name word abbreviates (at least three letters), or 0.
    +static int32_t matchMonthName(const char *word) {
    +    size_t len = strlen(word);
    +
    +    if (len < DATE_NAME_MIN_PREFIX) {
    +        return 0;
    +    }
    +
    +    for (int32_t i = 0; i < BAS_DATE_MONTHS_PER_YEAR; i++) {
    +        if (len <= strlen(sMonthNames[i]) && strncasecmp(word, sMonthNames[i], len) == 0) {
    +            return i + 1;
    +        }
    +    }
    +
    +    return 0;
    +}
    +
    +
    +// True when the next date token after s (skipping separators) is a
    +// second, making a preceding m / mm the minute.
    +static bool nextTokenIsSecond(const char *s) {
    +    while (*s && strchr(DATE_FMT_TOKEN_CHARS, tolower((unsigned char)*s)) == NULL) {
    +        s++;
    +    }
    +
    +    return tolower((unsigned char)*s) == 's';
    +}
    +
    +
    +// Days from 1/1/0001 to day 0 of the serial scale.
    +static int64_t ordinalOfEpoch(void) {
    +    int64_t days = daysBeforeYear(DATE_EPOCH_YEAR);
    +
    +    for (int32_t m = 1; m < DATE_EPOCH_MONTH; m++) {
    +        days += daysInMonth(DATE_EPOCH_YEAR, m);
    +    }
    +
    +    return days + DATE_EPOCH_DAY - 1;
    +}
    +
    +
    +// Length of an AM / PM / A / P word at s (a whole word, any case), 0
    +// when there is none.
    +static int32_t parseAmPmWord(const char *s) {
    +    static const char *words[] = { "AM", "PM", "A", "P" };
    +
    +    for (size_t i = 0; i < sizeof(words) / sizeof(words[0]); i++) {
    +        size_t len = strlen(words[i]);
    +
    +        if (strncasecmp(s, words[i], len) == 0 && !isalpha((unsigned char)s[len])) {
    +            return (int32_t)len;
    +        }
    +    }
    +
    +    return 0;
    +}
    +
    +
    +// Number of consecutive characters at s equal to lower, ignoring case.
    +static int32_t runLength(const char *s, char lower) {
    +    int32_t run = 0;
    +
    +    while (tolower((unsigned char)s[run]) == lower) {
    +        run++;
    +    }
    +
    +    return run;
    +}
    +
    +
    +// The serial as whole seconds on the linear time line.  A value outside
    +// the DATE range (a corrupt record read by GET, say) counts as day 0 so
    +// the calendar arithmetic always works on sane numbers.
    +static int64_t toSeconds(double serial) {
    +    if (!basDateIsValid(serial)) {
    +        return 0;
    +    }
    +
    +    return (int64_t)floor(basDateToLinear(serial) * DATE_SECS_PER_DAY + DATE_ROUND_HALF);
    +}
    +
    +
    +// Weekday of a day number counted from firstDayOfWeek (0 = Sunday).
    +static int32_t weekdayOfDay(int64_t day, int32_t firstDayOfWeek) {
    +    int32_t first = (firstDayOfWeek == BAS_DATE_USE_SYSTEM_DAY) ? BAS_DATE_SUNDAY : firstDayOfWeek;
    +
    +    // Day 0 (12/30/1899) was a Saturday.
    +    int32_t weekday = (int32_t)floorMod(day + BAS_DATE_SATURDAY - 1, BAS_DATE_DAYS_PER_WEEK) + 1;
    +
    +    return (weekday - first + BAS_DATE_DAYS_PER_WEEK) % BAS_DATE_DAYS_PER_WEEK + 1;
    +}
    +
    +
    +// Week of the year holding day.  A day before the year's first week
    +// belongs to the last week of the previous year.
    +static int32_t weekOfYear(int64_t day, int32_t year, int32_t firstDayOfWeek, int32_t firstWeekOfYear) {
    +    int64_t jan1        = dayNumber(year, 1, 1);
    +    int64_t start       = weekStart(jan1, firstDayOfWeek);
    +    int32_t daysInFirst = BAS_DATE_DAYS_PER_WEEK - (int32_t)(jan1 - start);
    +
    +    if ((firstWeekOfYear == BAS_DATE_FIRST_FOUR_DAYS && daysInFirst < DATE_FIRST_WEEK_MIN_DAYS) || (firstWeekOfYear == BAS_DATE_FIRST_FULL_WEEK && daysInFirst < BAS_DATE_DAYS_PER_WEEK)) {
    +        start += BAS_DATE_DAYS_PER_WEEK;
    +    }
    +
    +    if (day < start) {
    +        return weekOfYear(day, year - 1, firstDayOfWeek, firstWeekOfYear);
    +    }
    +
    +    return (int32_t)((day - start) / BAS_DATE_DAYS_PER_WEEK) + 1;
    +}
    +
    +
    +// First day of the week holding day.
    +static int64_t weekStart(int64_t day, int32_t firstDayOfWeek) {
    +    return day - (weekdayOfDay(day, firstDayOfWeek) - 1);
    +}
    +
    +
    +// Apply the two-digit year window to a year written with digits digits.
    +static int32_t windowYear(int32_t year, int32_t digits) {
    +    if (digits > DATE_TWO_DIGIT_LEN || year < 0 || year > DATE_TWO_DIGIT_MAX) {
    +        return year;
    +    }
    +
    +    return year + ((year < DATE_TWO_DIGIT_PIVOT) ? DATE_CENTURY_20XX : DATE_CENTURY_19XX);
    +}
    diff --git a/src/apps/kpunch/dvxbasic/runtime/dates.h b/src/apps/kpunch/dvxbasic/runtime/dates.h
    new file mode 100644
    index 00000000..c95fe8f2
    --- /dev/null
    +++ b/src/apps/kpunch/dvxbasic/runtime/dates.h
    @@ -0,0 +1,148 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dates.h -- DVX BASIC date arithmetic, parsing and formatting
    +//
    +// A DATE is a Double serial in the VB format: the whole part counts days
    +// from 12/30/1899 (day 0) and the fraction is the time of day.  Days
    +// before the epoch are negative, and their time fraction still counts
    +// forward from midnight: -1.25 is 12/29/1899 6:00 AM.  "Linear" values
    +// below are the same instants on a continuous number line (-1.25 is
    +// linear -0.75), which is what arithmetic on instants needs.
    +//
    +// The proleptic Gregorian calendar covers the whole range, 1/1/100
    +// through 12/31/9999.  Month and day names are English.
    +//
    +// Embeddable: no DVX dependencies, pure C.
    +
    +#ifndef DVXBASIC_DATES_H
    +#define DVXBASIC_DATES_H
    +
    +#include 
    +#include 
    +
    +// Days of the week (Weekday results and firstDayOfWeek arguments).
    +#define BAS_DATE_USE_SYSTEM_DAY  0   // vbUseSystemDayOfWeek: Sunday here
    +#define BAS_DATE_SUNDAY          1
    +#define BAS_DATE_SATURDAY        7
    +#define BAS_DATE_DAYS_PER_WEEK   7
    +
    +// firstWeekOfYear arguments of DatePart / DateDiff.
    +#define BAS_DATE_USE_SYSTEM_WEEK 0   // vbUseSystem: same as vbFirstJan1
    +#define BAS_DATE_FIRST_JAN1      1   // the week holding January 1
    +#define BAS_DATE_FIRST_FOUR_DAYS 2   // the first week with four days in the new year
    +#define BAS_DATE_FIRST_FULL_WEEK 3   // the first full week
    +
    +#define BAS_DATE_MONTHS_PER_YEAR 12
    +
    +// Longest text basDateFormat can need for one named or general form.
    +#define BAS_DATE_TEXT_LEN        64
    +
    +// DateAdd / DateDiff / DatePart interval.
    +typedef enum {
    +    BAS_DATE_IV_YEAR,        // "yyyy"
    +    BAS_DATE_IV_QUARTER,     // "q"
    +    BAS_DATE_IV_MONTH,       // "m"
    +    BAS_DATE_IV_DAY_OF_YEAR, // "y"
    +    BAS_DATE_IV_DAY,         // "d"
    +    BAS_DATE_IV_WEEKDAY,     // "w"
    +    BAS_DATE_IV_WEEK,        // "ww"
    +    BAS_DATE_IV_HOUR,        // "h"
    +    BAS_DATE_IV_MINUTE,      // "n"
    +    BAS_DATE_IV_SECOND       // "s"
    +} BasDateIntervalE;
    +
    +// A serial broken into calendar fields (time rounded to the second).
    +typedef struct {
    +    int32_t year;
    +    int32_t month;       // 1-12
    +    int32_t day;         // 1-31
    +    int32_t hour;        // 0-23
    +    int32_t minute;
    +    int32_t second;
    +    int32_t weekday;     // 1 = Sunday ... 7 = Saturday
    +    int32_t dayOfYear;   // 1-366
    +    int64_t dayNumber;   // days from 12/30/1899
    +    int32_t secOfDay;    // seconds since midnight
    +} BasDatePartsT;
    +
    +// DateAdd: count intervals added to serial.  False when the result falls
    +// outside the DATE range.
    +bool basDateAdd(BasDateIntervalE interval, int32_t count, double serial, double *out);
    +
    +// DateDiff: interval boundaries crossed from one serial to another.
    +int64_t basDateDiff(BasDateIntervalE interval, double from, double to, int32_t firstDayOfWeek);
    +
    +// Format serial with a Format$ pattern, a named format ("Long Date") or,
    +// with fmt NULL or empty, the general form ("1/2/2000 3:04:05 PM"; the
    +// date alone at midnight, the time alone on day 0).  Returns the length
    +// written; the output is truncated to bufSize - 1 characters.
    +int32_t basDateFormat(double serial, const char *fmt, char *buf, int32_t bufSize);
    +
    +// Serial of a linear instant.  False when it falls outside the DATE range.
    +bool basDateFromLinear(double linear, double *outSerial);
    +
    +// True when fmt is a named date format or holds a date/time token (d, w,
    +// m, q, y, h, n, s, c, ttttt, AM/PM) outside quotes -- Format$ then formats its
    +// value as a date instead of a number.
    +bool basDateIsPattern(const char *fmt);
    +
    +// True when serial is a number inside the DATE range.
    +bool basDateIsValid(double serial);
    +
    +// English month name (month 1-12), NULL when out of range.
    +const char *basDateMonthName(int32_t month, bool abbrev);
    +
    +// Parse date and/or time text: m/d/y, m-d-y, y-m-d, month names ("Jan 5,
    +// 2000", "5 January 2000"), m/d alone (current year), h:mm[:ss] with an
    +// optional AM/PM, "3 PM".  Two-digit years map to 1930-2029.  False when
    +// the text is not a valid date.
    +bool basDateParse(const char *text, double *outSerial);
    +
    +// Parse a DateAdd / DateDiff / DatePart interval string.
    +bool basDateParseInterval(const char *text, BasDateIntervalE *out);
    +
    +// DatePart: one field of serial.
    +int32_t basDatePart(BasDateIntervalE interval, double serial, int32_t firstDayOfWeek, int32_t firstWeekOfYear);
    +
    +// DateSerial: VB rules (years 0-29 are 2000-2029 and 30-99 are 1930-1999,
    +// months and days outside their range roll into the neighbouring ones).
    +// False when the result falls outside the DATE range.
    +bool basDateSerial(int32_t year, int32_t month, int32_t day, double *outSerial);
    +
    +// Break serial into calendar fields.
    +void basDateSplit(double serial, BasDatePartsT *out);
    +
    +// Linear instant of a serial.
    +double basDateToLinear(double serial);
    +
    +// Weekday of serial counted from firstDayOfWeek (1 = that day).
    +int32_t basDateWeekday(double serial, int32_t firstDayOfWeek);
    +
    +// English weekday name (1 = Sunday), NULL when out of range.
    +const char *basDateWeekdayName(int32_t weekday, bool abbrev);
    +
    +// TimeSerial: hours, minutes and seconds (any of them may overflow or be
    +// negative) as a serial.  False when the result falls outside the range.
    +bool basTimeSerial(int32_t hour, int32_t minute, int32_t second, double *outSerial);
    +
    +#endif // DVXBASIC_DATES_H
    diff --git a/src/apps/kpunch/dvxbasic/runtime/gfx.c b/src/apps/kpunch/dvxbasic/runtime/gfx.c
    new file mode 100644
    index 00000000..dbcf5176
    --- /dev/null
    +++ b/src/apps/kpunch/dvxbasic/runtime/gfx.c
    @@ -0,0 +1,1172 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// gfx.c -- DVX BASIC drawing primitives (see gfx.h)
    +
    +#include "gfx.h"
    +#include "basErrors.h"
    +#include "../compiler/opcodes.h"
    +#include "stb_ds.h"
    +
    +#include 
    +#include 
    +#include 
    +
    +#define GFX_PI             3.14159265358979323846
    +#define GFX_TWO_PI         (2.0 * GFX_PI)
    +#define GFX_ARC_SLACK      1e-9          // angles this far past 2*pi still count as 2*pi
    +#define GFX_DEG_PER_RAD    (180.0 / GFX_PI)
    +#define GFX_TURN_MAX       360           // DRAW TA range
    +#define GFX_QUARTER_TURN   90            // DRAW A step
    +#define GFX_ANGLE_MAX      3             // DRAW A range 0..3
    +#define GFX_SCALE_MAX      255           // DRAW S range 1..255
    +#define GFX_STYLE_SOLID    0xFFFF        // LINE style: every pixel
    +#define GFX_STYLE_TOP_BIT  0x8000
    +#define GFX_STYLE_BITS     16
    +#define GFX_COORD_LIMIT    1.0e9         // pixel coordinates are clamped to this
    +#define GFX_RADIUS_MAX     32767.0       // largest CIRCLE radius in pixels (QBasic's integer range)
    +#define GFX_ROUND_HALF     0.5
    +
    +typedef struct {
    +    int32_t x;
    +    int32_t y;
    +} GfxSeedT;
    +
    +// Unit steps of DRAW's U D L R E F G H (screen y grows downwards).
    +typedef struct {
    +    char    letter;
    +    int32_t dx;
    +    int32_t dy;
    +} GfxDrawDirT;
    +
    +static const GfxDrawDirT sDrawDirs[] = {
    +    { 'U',  0, -1 },
    +    { 'D',  0,  1 },
    +    { 'L', -1,  0 },
    +    { 'R',  1,  0 },
    +    { 'E',  1, -1 },
    +    { 'F',  1,  1 },
    +    { 'G', -1,  1 },
    +    { 'H', -1, -1 }
    +};
    +
    +static const uint8_t *sTallGlyphs = NULL;
    +static uint8_t        sShortGlyphs[BAS_GFX_GLYPHS * BAS_GFX_GLYPH_SHORT];
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static bool     arcContains(double angle, double start, double end);
    +static void     boxOutline(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel, uint16_t *style);
    +static bool     clipLine(const BasGfxT *g, double *x0, double *y0, double *x1, double *y1);
    +static int32_t  drawNumber(const char *s, int32_t len, int32_t *pos, bool allowSign, int32_t *out);
    +static void     ellipse(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, bool arc, double start, double end);
    +static void     ellipseFill(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel);
    +static void     ellipsePoint(BasGfxT *g, int32_t cx, int32_t cy, int32_t dx, int32_t dy, int32_t rx, int32_t ry, uint32_t pixel, bool arc, double start, double end);
    +static bool     fillable(const BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint32_t border);
    +static void     fillRect(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel);
    +static void     floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint32_t border);
    +static void     hspan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pixel);
    +static bool     inView(const BasGfxT *g, int32_t x, int32_t y);
    +static void     linePixels(BasGfxT *g, double x0, double y0, double x1, double y1, uint32_t pixel, uint16_t *style);
    +static double   mapX(const BasGfxT *g, double x);
    +static double   mapY(const BasGfxT *g, double y);
    +static int32_t  originX(const BasGfxT *g);
    +static int32_t  originY(const BasGfxT *g);
    +static void     plot(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel);
    +static void     plotPixel(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel);
    +static void     pointArg(const BasGfxT *g, BasValueT x, BasValueT y, bool step, double *outX, double *outY);
    +static int32_t  roundPixel(double v);
    +static double   scaleLen(const BasGfxT *g, double len);
    +static double   unmapX(const BasGfxT *g, double px);
    +static double   unmapY(const BasGfxT *g, double py);
    +
    +
    +// True when angle (0..2*pi) lies on the arc from start to end, counter-
    +// clockwise; an arc whose end is below its start wraps through 0.
    +static bool arcContains(double angle, double start, double end) {
    +    if (start <= end) {
    +        return angle >= start && angle <= end;
    +    }
    +
    +    return angle >= start || angle <= end;
    +}
    +
    +
    +int32_t basGfxCircle(BasGfxT *g, const BasValueT *args) {
    +    int32_t  flags = basValToInt32(args[BAS_GFX_CIRCLE_ARGS - 1]);
    +    double   x     = 0.0;
    +    double   y     = 0.0;
    +    uint32_t color = g->fore;
    +    double   start = 0.0;
    +    double   end   = GFX_TWO_PI;
    +    double   asp   = (flags & BAS_GFX_ASPECT) ? basValToNumber(args[6]) : g->aspect;
    +    double   r     = scaleLen(g, fabs(basValToNumber(args[2])));
    +
    +    pointArg(g, args[0], args[1], (flags & BAS_GFX_STEP) != 0, &x, &y);
    +
    +    if ((flags & BAS_GFX_COLOR) && basGfxColorArg(g, args[3], &color) != 0) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if (flags & BAS_GFX_ARC_START) {
    +        start = basValToNumber(args[4]);
    +    }
    +
    +    if (flags & BAS_GFX_ARC_END) {
    +        end = basValToNumber(args[5]);
    +    }
    +
    +    if (fabs(start) > GFX_TWO_PI + GFX_ARC_SLACK || fabs(end) > GFX_TWO_PI + GFX_ARC_SLACK || asp < 0.0 || isnan(r) || isnan(asp)) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    double rx = asp < 1.0 ? r : r / asp;
    +    double ry = asp < 1.0 ? r * asp : r;
    +
    +    if (rx > GFX_RADIUS_MAX || ry > GFX_RADIUS_MAX) {
    +        return BAS_ERR_OVERFLOW;
    +    }
    +
    +    int32_t  cx    = roundPixel(mapX(g, x));
    +    int32_t  cy    = roundPixel(mapY(g, y));
    +    int32_t  irx   = roundPixel(rx);
    +    int32_t  iry   = roundPixel(ry);
    +    bool     arc   = (flags & (BAS_GFX_ARC_START | BAS_GFX_ARC_END)) != 0;
    +    uint32_t pixel = basGfxPixel(g, color);
    +
    +    g->lastX = x;
    +    g->lastY = y;
    +
    +    if (!arc && g->fillStyle == BAS_GFX_FILL_SOLID) {
    +        ellipseFill(g, cx, cy, irx, iry, basGfxPixel(g, g->fillColor));
    +    }
    +
    +    ellipse(g, cx, cy, irx, iry, pixel, arc, fabs(start), fabs(end));
    +
    +    // A negative angle also draws the radius to that end of the arc.
    +    double ends[] = { start, end };
    +
    +    for (int32_t i = 0; i < (int32_t)(sizeof(ends) / sizeof(ends[0])); i++) {
    +        if (ends[i] < 0.0) {
    +            double a = -ends[i];
    +
    +            linePixels(g, cx, cy, cx + irx * cos(a), cy - iry * sin(a), pixel, NULL);
    +        }
    +    }
    +
    +    return 0;
    +}
    +
    +
    +int32_t basGfxClear(BasGfxT *g, BasValueT color) {
    +    uint32_t c = 0;
    +
    +    if (basGfxColorArg(g, color, &c) != 0) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    fillRect(g, g->viewX0, g->viewY0, g->viewX1, g->viewY1, basGfxPixel(g, c));
    +    return 0;
    +}
    +
    +
    +int32_t basGfxColorArg(const BasGfxT *g, BasValueT v, uint32_t *out) {
    +    double n = basValToNumber(v);
    +
    +    if (g->surf.colorMax != 0 && (isnan(n) || n < 0.0 || n > (double)g->surf.colorMax)) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    *out = (uint32_t)basValToInt32(v);
    +    return 0;
    +}
    +
    +
    +// DRAW: the pen starts at the last point and works in view pixels.
    +int32_t basGfxDraw(BasGfxT *g, const char *macro, int32_t len) {
    +    double  px  = mapX(g, g->lastX);
    +    double  py  = mapY(g, g->lastY);
    +    int32_t pos = 0;
    +
    +    while (pos < len) {
    +        char c = (char)toupper((unsigned char)macro[pos++]);
    +
    +        if (c == ' ' || c == ';') {
    +            continue;
    +        }
    +
    +        bool noDraw = false;
    +        bool noMove = false;
    +
    +        // B (move without drawing) and N (draw, then come back) prefixes
    +        while ((c == 'B' || c == 'N') && pos <= len) {
    +            noDraw = noDraw || c == 'B';
    +            noMove = noMove || c == 'N';
    +
    +            while (pos < len && macro[pos] == ' ') {
    +                pos++;
    +            }
    +
    +            if (pos >= len) {
    +                return BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            c = (char)toupper((unsigned char)macro[pos++]);
    +        }
    +
    +        double  nx    = px;
    +        double  ny    = py;
    +        bool    moved = false;
    +        int32_t n     = 0;
    +
    +        for (int32_t d = 0; d < (int32_t)(sizeof(sDrawDirs) / sizeof(sDrawDirs[0])); d++) {
    +            if (sDrawDirs[d].letter != c) {
    +                continue;
    +            }
    +
    +            int32_t count = 1;
    +            int32_t next  = pos;
    +
    +            while (next < len && macro[next] == ' ') {
    +                next++;
    +            }
    +
    +            // The count is optional (1)
    +            if (next < len && isdigit((unsigned char)macro[next]) && drawNumber(macro, len, &pos, false, &count) != 0) {
    +                return BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            double t    = g->drawTurn / GFX_DEG_PER_RAD;
    +            double unit = (double)count * g->drawScale / BAS_GFX_DRAW_SCALE_ONE;
    +            double vx   = sDrawDirs[d].dx * unit;
    +            double vy   = sDrawDirs[d].dy * unit;
    +
    +            nx    = px + vx * cos(t) + vy * sin(t);
    +            ny    = py - vx * sin(t) + vy * cos(t);
    +            moved = true;
    +        }
    +
    +        if (!moved) {
    +            switch (c) {
    +                case 'M': {
    +                    while (pos < len && macro[pos] == ' ') {
    +                        pos++;
    +                    }
    +
    +                    bool    relative = pos < len && (macro[pos] == '+' || macro[pos] == '-');
    +                    int32_t mx       = 0;
    +                    int32_t my       = 0;
    +
    +                    if (drawNumber(macro, len, &pos, true, &mx) != 0) {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    while (pos < len && macro[pos] == ' ') {
    +                        pos++;
    +                    }
    +
    +                    if (pos >= len || macro[pos] != ',') {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    pos++;
    +
    +                    if (drawNumber(macro, len, &pos, true, &my) != 0) {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    double unit = (double)g->drawScale / BAS_GFX_DRAW_SCALE_ONE;
    +
    +                    nx    = relative ? px + mx * unit : originX(g) + mx;
    +                    ny    = relative ? py + my * unit : originY(g) + my;
    +                    moved = true;
    +                    break;
    +                }
    +
    +                case 'C': {
    +                    uint32_t color = 0;
    +
    +                    if (drawNumber(macro, len, &pos, false, &n) != 0 || basGfxColorArg(g, basValLong(n), &color) != 0) {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    g->drawColor    = color;
    +                    g->drawColorSet = true;
    +                    break;
    +                }
    +
    +                case 'S':
    +                    if (drawNumber(macro, len, &pos, false, &n) != 0 || n < 1 || n > GFX_SCALE_MAX) {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    g->drawScale = n;
    +                    break;
    +
    +                case 'A':
    +                    if (drawNumber(macro, len, &pos, false, &n) != 0 || n > GFX_ANGLE_MAX) {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    g->drawTurn = n * GFX_QUARTER_TURN;
    +                    break;
    +
    +                case 'T':
    +                    if (pos >= len || toupper((unsigned char)macro[pos]) != 'A') {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    pos++;
    +
    +                    if (drawNumber(macro, len, &pos, true, &n) != 0 || n < -GFX_TURN_MAX || n > GFX_TURN_MAX) {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    g->drawTurn = n;
    +                    break;
    +
    +                case 'P': {
    +                    int32_t  paint  = 0;
    +                    int32_t  border = 0;
    +                    uint32_t pc     = 0;
    +                    uint32_t bc     = 0;
    +
    +                    if (drawNumber(macro, len, &pos, false, &paint) != 0 || pos >= len || macro[pos] != ',') {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    pos++;
    +
    +                    if (drawNumber(macro, len, &pos, false, &border) != 0 || basGfxColorArg(g, basValLong(paint), &pc) != 0 || basGfxColorArg(g, basValLong(border), &bc) != 0) {
    +                        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                    }
    +
    +                    int32_t ix = roundPixel(px);
    +                    int32_t iy = roundPixel(py);
    +
    +                    if (inView(g, ix, iy)) {
    +                        floodFill(g, ix, iy, basGfxPixel(g, pc), basGfxPixel(g, bc));
    +                    }
    +
    +                    break;
    +                }
    +
    +                default:
    +                    // X (substrings) and "=" arguments need VARPTR$, which
    +                    // DVX BASIC does not have.
    +                    return BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +        }
    +
    +        if (!moved) {
    +            continue;
    +        }
    +
    +        if (!noDraw) {
    +            linePixels(g, px, py, nx, ny, basGfxPixel(g, g->drawColorSet ? g->drawColor : g->fore), NULL);
    +        }
    +
    +        if (!noMove) {
    +            px = nx;
    +            py = ny;
    +        }
    +    }
    +
    +    g->lastX = unmapX(g, px);
    +    g->lastY = unmapY(g, py);
    +    return 0;
    +}
    +
    +
    +void basGfxGlyph(BasGfxT *g, int32_t x, int32_t y, uint8_t ch, int32_t rows, uint32_t forePixel, uint32_t backPixel, bool opaque) {
    +    const uint8_t *glyph = NULL;
    +
    +    if (sTallGlyphs) {
    +        glyph = rows == BAS_GFX_GLYPH_TALL ? &sTallGlyphs[ch * BAS_GFX_GLYPH_TALL] : &sShortGlyphs[ch * BAS_GFX_GLYPH_SHORT];
    +    }
    +
    +    for (int32_t r = 0; r < rows; r++) {
    +        int32_t py = y + r;
    +
    +        if (py < 0 || py >= g->surf.height) {
    +            continue;
    +        }
    +
    +        uint8_t bits = glyph ? glyph[r] : 0;
    +
    +        for (int32_t c = 0; c < BAS_GFX_GLYPH_W; c++) {
    +            int32_t px = x + c;
    +            bool    on = (bits & (0x80 >> c)) != 0;
    +
    +            if (px < 0 || px >= g->surf.width || (!on && !opaque)) {
    +                continue;
    +            }
    +
    +            g->surf.span(g->surf.ctx, px, px, py, on ? forePixel : backPixel);
    +        }
    +    }
    +}
    +
    +
    +void basGfxHome(BasGfxT *g) {
    +    g->lastX = unmapX(g, (g->viewX0 + g->viewX1) / 2);
    +    g->lastY = unmapY(g, (g->viewY0 + g->viewY1) / 2);
    +}
    +
    +
    +void basGfxInit(BasGfxT *g, const BasGfxSurfaceT *surf, uint32_t fore, uint32_t back, double aspect) {
    +    memset(g, 0, sizeof(*g));
    +    g->surf      = *surf;
    +    g->viewX1    = surf->width - 1;
    +    g->viewY1    = surf->height - 1;
    +    g->fore      = fore;
    +    g->back      = back;
    +    g->fillStyle = BAS_GFX_FILL_TRANSPARENT;
    +    g->drawWidth = 1;
    +    g->aspect    = aspect;
    +    g->drawScale = BAS_GFX_DRAW_SCALE_ONE;
    +    basGfxHome(g);
    +}
    +
    +
    +int32_t basGfxLine(BasGfxT *g, const BasValueT *args) {
    +    int32_t  flags = basValToInt32(args[BAS_GFX_LINE_ARGS - 1]);
    +    double   x1    = g->lastX;
    +    double   y1    = g->lastY;
    +    double   x2    = 0.0;
    +    double   y2    = 0.0;
    +    uint32_t color = g->fore;
    +    uint16_t style = GFX_STYLE_SOLID;
    +
    +    if (flags & BAS_GFX_START) {
    +        pointArg(g, args[0], args[1], (flags & BAS_GFX_STEP) != 0, &x1, &y1);
    +    }
    +
    +    x2 = basValToNumber(args[2]);
    +    y2 = basValToNumber(args[3]);
    +
    +    if (flags & BAS_GFX_STEP2) {
    +        x2 += x1;
    +        y2 += y1;
    +    }
    +
    +    if ((flags & BAS_GFX_COLOR) && basGfxColorArg(g, args[4], &color) != 0) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if (flags & BAS_GFX_STYLE) {
    +        style = (uint16_t)basValToInt32(args[5]);
    +    }
    +
    +    g->lastX = x2;
    +    g->lastY = y2;
    +
    +    uint32_t pixel = basGfxPixel(g, color);
    +
    +    if (flags & (BAS_GFX_BOX | BAS_GFX_BOX_FILL)) {
    +        int32_t px1 = roundPixel(mapX(g, x1));
    +        int32_t py1 = roundPixel(mapY(g, y1));
    +        int32_t px2 = roundPixel(mapX(g, x2));
    +        int32_t py2 = roundPixel(mapY(g, y2));
    +
    +        if (flags & BAS_GFX_BOX_FILL) {
    +            fillRect(g, px1, py1, px2, py2, pixel);
    +            return 0;
    +        }
    +
    +        if (g->fillStyle == BAS_GFX_FILL_SOLID) {
    +            fillRect(g, px1, py1, px2, py2, basGfxPixel(g, g->fillColor));
    +        }
    +
    +        boxOutline(g, px1, py1, px2, py2, pixel, (flags & BAS_GFX_STYLE) ? &style : NULL);
    +        return 0;
    +    }
    +
    +    linePixels(g, mapX(g, x1), mapY(g, y1), mapX(g, x2), mapY(g, y2), pixel, (flags & BAS_GFX_STYLE) ? &style : NULL);
    +    return 0;
    +}
    +
    +
    +int32_t basGfxPaint(BasGfxT *g, const BasValueT *args) {
    +    int32_t  flags  = basValToInt32(args[BAS_GFX_PAINT_ARGS - 1]);
    +    double   x      = 0.0;
    +    double   y      = 0.0;
    +    uint32_t paint  = g->fore;
    +    uint32_t border = 0;
    +
    +    pointArg(g, args[0], args[1], (flags & BAS_GFX_STEP) != 0, &x, &y);
    +
    +    // A tile pattern (a string) is not supported.
    +    if ((flags & BAS_GFX_COLOR) && (args[2].type == BAS_TYPE_STRING || basGfxColorArg(g, args[2], &paint) != 0)) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    border = paint;
    +
    +    if ((flags & BAS_GFX_BORDER) && basGfxColorArg(g, args[3], &border) != 0) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    g->lastX = x;
    +    g->lastY = y;
    +
    +    int32_t px = roundPixel(mapX(g, x));
    +    int32_t py = roundPixel(mapY(g, y));
    +
    +    if (inView(g, px, py)) {
    +        floodFill(g, px, py, basGfxPixel(g, paint), basGfxPixel(g, border));
    +    }
    +
    +    return 0;
    +}
    +
    +
    +uint32_t basGfxPixel(const BasGfxT *g, uint32_t color) {
    +    return g->surf.toPixel ? g->surf.toPixel(g->surf.ctx, color) : color;
    +}
    +
    +
    +int32_t basGfxPoint(BasGfxT *g, BasValueT x, BasValueT y, BasValueT *out) {
    +    int32_t px = roundPixel(mapX(g, basValToNumber(x)));
    +    int32_t py = roundPixel(mapY(g, basValToNumber(y)));
    +
    +    if (!inView(g, px, py)) {
    +        *out = basValLong(-1);
    +        return 0;
    +    }
    +
    +    uint32_t pixel = g->surf.get(g->surf.ctx, px, py);
    +
    +    *out = basValLong((int32_t)(g->surf.toColor ? g->surf.toColor(g->surf.ctx, pixel) : pixel));
    +    return 0;
    +}
    +
    +
    +int32_t basGfxPointLast(const BasGfxT *g, BasValueT n, BasValueT *out) {
    +    switch (basValToInt32(n)) {
    +        case 0:
    +            *out = basValDouble(roundPixel(mapX(g, g->lastX)) - originX(g));
    +            return 0;
    +
    +        case 1:
    +            *out = basValDouble(roundPixel(mapY(g, g->lastY)) - originY(g));
    +            return 0;
    +
    +        case 2:
    +            *out = basValDouble(g->lastX);
    +            return 0;
    +
    +        case 3:
    +            *out = basValDouble(g->lastY);
    +            return 0;
    +
    +        default:
    +            return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +}
    +
    +
    +int32_t basGfxPset(BasGfxT *g, const BasValueT *args) {
    +    int32_t  flags = basValToInt32(args[BAS_GFX_PSET_ARGS - 1]);
    +    double   x     = 0.0;
    +    double   y     = 0.0;
    +    uint32_t color = (flags & BAS_GFX_PRESET) ? g->back : g->fore;
    +
    +    pointArg(g, args[0], args[1], (flags & BAS_GFX_STEP) != 0, &x, &y);
    +
    +    if ((flags & BAS_GFX_COLOR) && basGfxColorArg(g, args[2], &color) != 0) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    g->lastX = x;
    +    g->lastY = y;
    +    plot(g, roundPixel(mapX(g, x)), roundPixel(mapY(g, y)), basGfxPixel(g, color));
    +    return 0;
    +}
    +
    +
    +// The short glyphs merge each pair of rows of the tall ones, so a line of
    +// the tall glyph is never lost.
    +void basGfxSetFont(const uint8_t *tallGlyphs) {
    +    int32_t merge = BAS_GFX_GLYPH_TALL / BAS_GFX_GLYPH_SHORT;
    +
    +    sTallGlyphs = tallGlyphs;
    +
    +    if (!tallGlyphs) {
    +        return;
    +    }
    +
    +    for (int32_t ch = 0; ch < BAS_GFX_GLYPHS; ch++) {
    +        for (int32_t r = 0; r < BAS_GFX_GLYPH_SHORT; r++) {
    +            uint8_t bits = 0;
    +
    +            for (int32_t m = 0; m < merge; m++) {
    +                bits |= tallGlyphs[ch * BAS_GFX_GLYPH_TALL + r * merge + m];
    +            }
    +
    +            sShortGlyphs[ch * BAS_GFX_GLYPH_SHORT + r] = bits;
    +        }
    +    }
    +}
    +
    +
    +int32_t basGfxSetView(BasGfxT *g, const BasValueT *args) {
    +    int32_t flags = basValToInt32(args[BAS_VIEW_ARGS - 1]);
    +
    +    if (!(flags & BAS_VIEW_COORDS)) {
    +        g->viewX0       = 0;
    +        g->viewY0       = 0;
    +        g->viewX1       = g->surf.width - 1;
    +        g->viewY1       = g->surf.height - 1;
    +        g->viewAbsolute = false;
    +        basGfxHome(g);
    +        return 0;
    +    }
    +
    +    int32_t  x0     = roundPixel(basValToNumber(args[0]));
    +    int32_t  y0     = roundPixel(basValToNumber(args[1]));
    +    int32_t  x1     = roundPixel(basValToNumber(args[2]));
    +    int32_t  y1     = roundPixel(basValToNumber(args[3]));
    +    uint32_t fill   = 0;
    +    uint32_t border = 0;
    +
    +    if (x0 > x1) {
    +        int32_t t = x0;
    +
    +        x0 = x1;
    +        x1 = t;
    +    }
    +
    +    if (y0 > y1) {
    +        int32_t t = y0;
    +
    +        y0 = y1;
    +        y1 = t;
    +    }
    +
    +    if (x0 < 0 || y0 < 0 || x1 >= g->surf.width || y1 >= g->surf.height) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if ((flags & BAS_VIEW_FILL) && basGfxColorArg(g, args[4], &fill) != 0) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if ((flags & BAS_VIEW_BORDER) && basGfxColorArg(g, args[5], &border) != 0) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    // The border goes just outside the view, over the whole surface.
    +    if (flags & BAS_VIEW_BORDER) {
    +        g->viewX0 = 0;
    +        g->viewY0 = 0;
    +        g->viewX1 = g->surf.width - 1;
    +        g->viewY1 = g->surf.height - 1;
    +        boxOutline(g, x0 - 1, y0 - 1, x1 + 1, y1 + 1, basGfxPixel(g, border), NULL);
    +    }
    +
    +    g->viewX0       = x0;
    +    g->viewY0       = y0;
    +    g->viewX1       = x1;
    +    g->viewY1       = y1;
    +    g->viewAbsolute = (flags & BAS_VIEW_SCREEN) != 0;
    +
    +    if (flags & BAS_VIEW_FILL) {
    +        fillRect(g, x0, y0, x1, y1, basGfxPixel(g, fill));
    +    }
    +
    +    basGfxHome(g);
    +    return 0;
    +}
    +
    +
    +int32_t basGfxSetWindow(BasGfxT *g, const BasValueT *args) {
    +    int32_t flags = basValToInt32(args[BAS_WINDOW_ARGS - 1]);
    +
    +    if (!(flags & BAS_VIEW_COORDS)) {
    +        g->world = false;
    +        basGfxHome(g);
    +        return 0;
    +    }
    +
    +    double x1 = basValToNumber(args[0]);
    +    double y1 = basValToNumber(args[1]);
    +    double x2 = basValToNumber(args[2]);
    +    double y2 = basValToNumber(args[3]);
    +
    +    if (x1 == x2 || y1 == y2 || isnan(x1) || isnan(y1) || isnan(x2) || isnan(y2)) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    g->worldX1   = x1 < x2 ? x1 : x2;
    +    g->worldX2   = x1 < x2 ? x2 : x1;
    +    g->worldY1   = y1 < y2 ? y1 : y2;
    +    g->worldY2   = y1 < y2 ? y2 : y1;
    +    g->worldDown = (flags & BAS_VIEW_SCREEN) != 0;
    +    g->world     = true;
    +    basGfxHome(g);
    +    return 0;
    +}
    +
    +
    +// A rectangle outline (one pixel wide, or the pen), each corner once so a
    +// style pattern runs on around it.
    +static void boxOutline(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel, uint16_t *style) {
    +    linePixels(g, x0, y0, x1, y0, pixel, style);
    +
    +    if (y1 == y0) {
    +        return;
    +    }
    +
    +    linePixels(g, x1, y0 + (y1 > y0 ? 1 : -1), x1, y1, pixel, style);
    +
    +    if (x1 != x0) {
    +        linePixels(g, x1 + (x0 > x1 ? 1 : -1), y1, x0, y1, pixel, style);
    +    }
    +
    +    if (y1 - y0 > 1 || y0 - y1 > 1) {
    +        linePixels(g, x0, y1 + (y0 > y1 ? 1 : -1), x0, y0 + (y1 > y0 ? 1 : -1), pixel, style);
    +    }
    +}
    +
    +
    +// Clip a line (pixel coordinates) to the view grown by the pen: Liang-
    +// Barsky.  False when nothing of it is visible.
    +static bool clipLine(const BasGfxT *g, double *x0, double *y0, double *x1, double *y1) {
    +    double       pad  = g->drawWidth / 2 + 1;
    +    double       minX = g->viewX0 - pad;
    +    double       minY = g->viewY0 - pad;
    +    double       maxX = g->viewX1 + pad;
    +    double       maxY = g->viewY1 + pad;
    +    double       dx   = *x1 - *x0;
    +    double       dy   = *y1 - *y0;
    +    const double p[]  = { -dx, dx, -dy, dy };
    +    const double q[]  = { *x0 - minX, maxX - *x0, *y0 - minY, maxY - *y0 };
    +    double       t0   = 0.0;
    +    double       t1   = 1.0;
    +
    +    for (int32_t i = 0; i < (int32_t)(sizeof(p) / sizeof(p[0])); i++) {
    +        if (p[i] == 0.0) {
    +            if (q[i] < 0.0) {
    +                return false;
    +            }
    +
    +            continue;
    +        }
    +
    +        double t = q[i] / p[i];
    +
    +        if (p[i] < 0.0) {
    +            t0 = t > t0 ? t : t0;
    +        } else {
    +            t1 = t < t1 ? t : t1;
    +        }
    +    }
    +
    +    if (t0 > t1) {
    +        return false;
    +    }
    +
    +    double sx = *x0;
    +    double sy = *y0;
    +
    +    *x0 = sx + t0 * dx;
    +    *y0 = sy + t0 * dy;
    +    *x1 = sx + t1 * dx;
    +    *y1 = sy + t1 * dy;
    +    return true;
    +}
    +
    +
    +// A decimal number in a DRAW string (spaces before it skipped; a sign only
    +// where allowSign).  Error without digits.
    +static int32_t drawNumber(const char *s, int32_t len, int32_t *pos, bool allowSign, int32_t *out) {
    +    int32_t sign   = 1;
    +    int32_t value  = 0;
    +    bool    digits = false;
    +
    +    while (*pos < len && s[*pos] == ' ') {
    +        (*pos)++;
    +    }
    +
    +    if (allowSign && *pos < len && (s[*pos] == '+' || s[*pos] == '-')) {
    +        sign = s[*pos] == '-' ? -1 : 1;
    +        (*pos)++;
    +    }
    +
    +    while (*pos < len && isdigit((unsigned char)s[*pos])) {
    +        if (value > (INT32_MAX - 9) / 10) {
    +            return BAS_ERR_OVERFLOW;
    +        }
    +
    +        value  = value * 10 + (s[*pos] - '0');
    +        digits = true;
    +        (*pos)++;
    +    }
    +
    +    if (!digits) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    *out = sign * value;
    +    return 0;
    +}
    +
    +
    +// Ellipse outline by the midpoint algorithm (rx, ry in pixels), each point
    +// once per quadrant; an arc keeps only the points between start and end.
    +static void ellipse(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, bool arc, double start, double end) {
    +    if (rx == 0 || ry == 0) {
    +        linePixels(g, cx - rx, cy - ry, cx + rx, cy + ry, pixel, NULL);
    +        return;
    +    }
    +
    +    double  rx2 = (double)rx * rx;
    +    double  ry2 = (double)ry * ry;
    +    int32_t x   = 0;
    +    int32_t y   = ry;
    +    double  dx  = 0.0;
    +    double  dy  = 2.0 * rx2 * y;
    +    double  d1  = ry2 - rx2 * ry + 0.25 * rx2;
    +
    +    while (dx < dy) {
    +        ellipsePoint(g, cx, cy, x, y, rx, ry, pixel, arc, start, end);
    +        x++;
    +        dx += 2.0 * ry2;
    +
    +        if (d1 < 0.0) {
    +            d1 += dx + ry2;
    +        } else {
    +            y--;
    +            dy -= 2.0 * rx2;
    +            d1 += dx - dy + ry2;
    +        }
    +    }
    +
    +    double d2 = ry2 * (x + GFX_ROUND_HALF) * (x + GFX_ROUND_HALF) + rx2 * (y - 1.0) * (y - 1.0) - rx2 * ry2;
    +
    +    while (y >= 0) {
    +        ellipsePoint(g, cx, cy, x, y, rx, ry, pixel, arc, start, end);
    +        y--;
    +        dy -= 2.0 * rx2;
    +
    +        if (d2 > 0.0) {
    +            d2 += rx2 - dy;
    +        } else {
    +            x++;
    +            dx += 2.0 * ry2;
    +            d2 += dx - dy + rx2;
    +        }
    +    }
    +}
    +
    +
    +// A filled ellipse: one span per row.
    +static void ellipseFill(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel) {
    +    for (int32_t dy = -ry; dy <= ry; dy++) {
    +        double  f    = ry > 0 ? 1.0 - ((double)dy * dy) / ((double)ry * ry) : 1.0;
    +        int32_t half = roundPixel(rx * sqrt(f > 0.0 ? f : 0.0));
    +
    +        hspan(g, cy + dy, cx - half, cx + half, pixel);
    +    }
    +}
    +
    +
    +// The four mirror points of an ellipse point (dx, dy >= 0).
    +static void ellipsePoint(BasGfxT *g, int32_t cx, int32_t cy, int32_t dx, int32_t dy, int32_t rx, int32_t ry, uint32_t pixel, bool arc, double start, double end) {
    +    static const int32_t signs[][2] = { { 1, 1 }, { -1, 1 }, { -1, -1 }, { 1, -1 } };
    +
    +    for (int32_t i = 0; i < (int32_t)(sizeof(signs) / sizeof(signs[0])); i++) {
    +        int32_t sx = dx * signs[i][0];
    +        int32_t sy = dy * signs[i][1];
    +
    +        // Skip the duplicates on the axes
    +        if ((dx == 0 && signs[i][0] < 0) || (dy == 0 && signs[i][1] < 0)) {
    +            continue;
    +        }
    +
    +        if (arc) {
    +            double a = atan2((double)-sy / ry, (double)sx / rx);
    +
    +            if (a < 0.0) {
    +                a += GFX_TWO_PI;
    +            }
    +
    +            if (!arcContains(a, start, end)) {
    +                continue;
    +            }
    +        }
    +
    +        plot(g, cx + sx, cy + sy, pixel);
    +    }
    +}
    +
    +
    +static bool fillable(const BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint32_t border) {
    +    uint32_t pixel = g->surf.get(g->surf.ctx, x, y);
    +
    +    return pixel != border && pixel != paint;
    +}
    +
    +
    +// A filled rectangle between two corners (any order), clipped to the view.
    +static void fillRect(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel) {
    +    int32_t top    = y0 < y1 ? y0 : y1;
    +    int32_t bottom = y0 < y1 ? y1 : y0;
    +
    +    top    = top < g->viewY0 ? g->viewY0 : top;
    +    bottom = bottom > g->viewY1 ? g->viewY1 : bottom;
    +
    +    for (int32_t y = top; y <= bottom; y++) {
    +        hspan(g, y, x0, x1, pixel);
    +    }
    +}
    +
    +
    +// Scan-line flood fill inside the view: everything reachable from (x, y)
    +// that is neither the border colour nor already the paint colour.
    +static void floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint32_t border) {
    +    GfxSeedT *seeds = NULL;
    +
    +    arrput(seeds, ((GfxSeedT){ x, y }));
    +
    +    while (arrlen(seeds) > 0) {
    +        GfxSeedT s = arrpop(seeds);
    +
    +        if (!fillable(g, s.x, s.y, paint, border)) {
    +            continue;
    +        }
    +
    +        int32_t left  = s.x;
    +        int32_t right = s.x;
    +
    +        while (left > g->viewX0 && fillable(g, left - 1, s.y, paint, border)) {
    +            left--;
    +        }
    +
    +        while (right < g->viewX1 && fillable(g, right + 1, s.y, paint, border)) {
    +            right++;
    +        }
    +
    +        g->surf.span(g->surf.ctx, left, right, s.y, paint);
    +
    +        for (int32_t ny = s.y - 1; ny <= s.y + 1; ny += 2) {
    +            if (ny < g->viewY0 || ny > g->viewY1) {
    +                continue;
    +            }
    +
    +            bool inRun = false;
    +
    +            for (int32_t nx = left; nx <= right; nx++) {
    +                bool can = fillable(g, nx, ny, paint, border);
    +
    +                if (can && !inRun) {
    +                    arrput(seeds, ((GfxSeedT){ nx, ny }));
    +                }
    +
    +                inRun = can;
    +            }
    +        }
    +    }
    +
    +    arrfree(seeds);
    +}
    +
    +
    +// A horizontal span clipped to the view.
    +static void hspan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pixel) {
    +    int32_t left  = x0 < x1 ? x0 : x1;
    +    int32_t right = x0 < x1 ? x1 : x0;
    +
    +    if (y < g->viewY0 || y > g->viewY1) {
    +        return;
    +    }
    +
    +    left  = left < g->viewX0 ? g->viewX0 : left;
    +    right = right > g->viewX1 ? g->viewX1 : right;
    +
    +    if (left <= right) {
    +        g->surf.span(g->surf.ctx, left, right, y, pixel);
    +    }
    +}
    +
    +
    +static bool inView(const BasGfxT *g, int32_t x, int32_t y) {
    +    return x >= g->viewX0 && x <= g->viewX1 && y >= g->viewY0 && y <= g->viewY1;
    +}
    +
    +
    +// A line between two pixel positions with the pen; a style mask (LINE's
    +// style) plots only where its rotating top bit is set.
    +static void linePixels(BasGfxT *g, double x0, double y0, double x1, double y1, uint32_t pixel, uint16_t *style) {
    +    if (!clipLine(g, &x0, &y0, &x1, &y1)) {
    +        return;
    +    }
    +
    +    int32_t ax  = roundPixel(x0);
    +    int32_t ay  = roundPixel(y0);
    +    int32_t bx  = roundPixel(x1);
    +    int32_t by  = roundPixel(y1);
    +    int32_t dx  = bx > ax ? bx - ax : ax - bx;
    +    int32_t dy  = by > ay ? ay - by : by - ay;
    +    int32_t sx  = ax < bx ? 1 : -1;
    +    int32_t sy  = ay < by ? 1 : -1;
    +    int32_t err = dx + dy;
    +
    +    for (;;) {
    +        bool on = true;
    +
    +        if (style) {
    +            on     = (*style & GFX_STYLE_TOP_BIT) != 0;
    +            *style = (uint16_t)((*style << 1) | (*style >> (GFX_STYLE_BITS - 1)));
    +        }
    +
    +        if (on) {
    +            plot(g, ax, ay, pixel);
    +        }
    +
    +        if (ax == bx && ay == by) {
    +            break;
    +        }
    +
    +        int32_t e2 = 2 * err;
    +
    +        if (e2 >= dy) {
    +            err += dy;
    +            ax  += sx;
    +        }
    +
    +        if (e2 <= dx) {
    +            err += dx;
    +            ay  += sy;
    +        }
    +    }
    +}
    +
    +
    +// Logical x -> pixel x (WINDOW scaling, else the view's corner as origin).
    +static double mapX(const BasGfxT *g, double x) {
    +    if (g->world) {
    +        return g->viewX0 + (x - g->worldX1) * (g->viewX1 - g->viewX0) / (g->worldX2 - g->worldX1);
    +    }
    +
    +    return originX(g) + x;
    +}
    +
    +
    +static double mapY(const BasGfxT *g, double y) {
    +    if (!g->world) {
    +        return originY(g) + y;
    +    }
    +
    +    double f = (y - g->worldY1) * (g->viewY1 - g->viewY0) / (g->worldY2 - g->worldY1);
    +
    +    return g->worldDown ? g->viewY0 + f : g->viewY1 - f;
    +}
    +
    +
    +static int32_t originX(const BasGfxT *g) {
    +    return g->viewAbsolute ? 0 : g->viewX0;
    +}
    +
    +
    +static int32_t originY(const BasGfxT *g) {
    +    return g->viewAbsolute ? 0 : g->viewY0;
    +}
    +
    +
    +// One point with the pen: a pixel, or a disc DrawWidth across.
    +static void plot(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel) {
    +    int32_t r = g->drawWidth / 2;
    +
    +    if (r <= 0) {
    +        plotPixel(g, x, y, pixel);
    +        return;
    +    }
    +
    +    for (int32_t dy = -r; dy <= r; dy++) {
    +        int32_t half = roundPixel(sqrt((double)(r * r - dy * dy)));
    +
    +        hspan(g, y + dy, x - half, x + half, pixel);
    +    }
    +}
    +
    +
    +static void plotPixel(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel) {
    +    if (inView(g, x, y)) {
    +        g->surf.span(g->surf.ctx, x, x, y, pixel);
    +    }
    +}
    +
    +
    +// A point argument in logical units; STEP makes it relative to the last point.
    +static void pointArg(const BasGfxT *g, BasValueT x, BasValueT y, bool step, double *outX, double *outY) {
    +    *outX = basValToNumber(x) + (step ? g->lastX : 0.0);
    +    *outY = basValToNumber(y) + (step ? g->lastY : 0.0);
    +}
    +
    +
    +static int32_t roundPixel(double v) {
    +    if (isnan(v)) {
    +        return 0;
    +    }
    +
    +    if (v > GFX_COORD_LIMIT) {
    +        v = GFX_COORD_LIMIT;
    +    } else if (v < -GFX_COORD_LIMIT) {
    +        v = -GFX_COORD_LIMIT;
    +    }
    +
    +    return (int32_t)floor(v + GFX_ROUND_HALF);
    +}
    +
    +
    +// A length in logical x units -> pixels.
    +static double scaleLen(const BasGfxT *g, double len) {
    +    if (!g->world) {
    +        return len;
    +    }
    +
    +    return len * (g->viewX1 - g->viewX0) / (g->worldX2 - g->worldX1);
    +}
    +
    +
    +static double unmapX(const BasGfxT *g, double px) {
    +    if (g->world && g->viewX1 != g->viewX0) {
    +        return g->worldX1 + (px - g->viewX0) * (g->worldX2 - g->worldX1) / (g->viewX1 - g->viewX0);
    +    }
    +
    +    return px - originX(g);
    +}
    +
    +
    +static double unmapY(const BasGfxT *g, double py) {
    +    if (!g->world || g->viewY1 == g->viewY0) {
    +        return py - originY(g);
    +    }
    +
    +    double f = g->worldDown ? py - g->viewY0 : g->viewY1 - py;
    +
    +    return g->worldY1 + f * (g->worldY2 - g->worldY1) / (g->viewY1 - g->viewY0);
    +}
    diff --git a/src/apps/kpunch/dvxbasic/runtime/gfx.h b/src/apps/kpunch/dvxbasic/runtime/gfx.h
    new file mode 100644
    index 00000000..f8c73faa
    --- /dev/null
    +++ b/src/apps/kpunch/dvxbasic/runtime/gfx.h
    @@ -0,0 +1,153 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// gfx.h -- DVX BASIC drawing primitives
    +//
    +// One implementation of the QBasic / VB drawing statements (PSET, LINE,
    +// CIRCLE, PAINT, DRAW, POINT, VIEW, WINDOW) for every drawing surface: the
    +// program screen's 8-bit framebuffer (screen.c) and the Form / PictureBox
    +// canvases (the form runtime).  A surface is a span writer and a pixel
    +// reader in its own pixel format plus colour conversion; the drawing state
    +// (last point, colours, view, window, DRAW pen) lives in BasGfxT.
    +//
    +// The statement functions take the argument blocks the compiler builds
    +// (opcodes.h: BAS_GFX_* flags and counts) and return 0 or a BASIC error
    +// number.
    +
    +#ifndef DVXBASIC_GFX_H
    +#define DVXBASIC_GFX_H
    +
    +#include "values.h"
    +
    +#include 
    +#include 
    +
    +// Text glyphs: 8 pixels wide, 16 rows (the DVX font) or 8 rows (every
    +// pair of rows of the tall glyph merged).
    +#define BAS_GFX_GLYPH_W      8
    +#define BAS_GFX_GLYPH_TALL   16
    +#define BAS_GFX_GLYPH_SHORT  8
    +#define BAS_GFX_GLYPHS       256
    +
    +// FillStyle values (VB): solid, or transparent (the default).
    +#define BAS_GFX_FILL_SOLID       0
    +#define BAS_GFX_FILL_TRANSPARENT 1
    +
    +// DRAW's S (scale) is in quarter pixels; S4 is one pixel a unit.
    +#define BAS_GFX_DRAW_SCALE_ONE   4
    +
    +// A drawing surface.  span and get take pixel coordinates the caller has
    +// already clipped to the surface.
    +typedef struct {
    +    void     *ctx;
    +    int32_t   width;
    +    int32_t   height;
    +    uint32_t  colorMax;                                                    // largest colour number it takes (0: any value, e.g. RGB)
    +    void     (*span)(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel);
    +    uint32_t (*get)(const void *ctx, int32_t x, int32_t y);
    +    uint32_t (*toPixel)(void *ctx, uint32_t color);                        // NULL: colours are pixels
    +    uint32_t (*toColor)(const void *ctx, uint32_t pixel);                  // NULL: pixels are colours
    +} BasGfxSurfaceT;
    +
    +// Drawing state of one surface.
    +typedef struct {
    +    BasGfxSurfaceT surf;
    +    int32_t        viewX0;         // VIEW rectangle (inclusive pixels); the whole surface without VIEW
    +    int32_t        viewY0;
    +    int32_t        viewX1;
    +    int32_t        viewY1;
    +    bool           viewAbsolute;   // VIEW SCREEN: coordinates are not moved to the view's corner
    +    bool           world;          // WINDOW is on
    +    bool           worldDown;      // WINDOW SCREEN: y grows downwards
    +    double         worldX1;
    +    double         worldY1;
    +    double         worldX2;
    +    double         worldY2;
    +    double         lastX;          // last point referenced (logical coordinates; VB CurrentX / CurrentY)
    +    double         lastY;
    +    uint32_t       fore;           // default drawing colour
    +    uint32_t       back;           // background colour (PRESET, Cls)
    +    uint32_t       fillColor;      // VB FillColor / FillStyle for boxes and circles
    +    int32_t        fillStyle;
    +    int32_t        drawWidth;      // pen size in pixels (VB DrawWidth)
    +    double         aspect;         // CIRCLE aspect when none is given
    +    int32_t        drawScale;      // DRAW S (quarter pixels a unit)
    +    int32_t        drawTurn;       // DRAW A / TA (degrees)
    +    bool           drawColorSet;   // DRAW C was used (else DRAW draws in fore)
    +    uint32_t       drawColor;
    +} BasGfxT;
    +
    +// CIRCLE block (BAS_GFX_CIRCLE_ARGS values).
    +int32_t basGfxCircle(BasGfxT *g, const BasValueT *args);
    +
    +// Fill the view with color (CLS, Cls).
    +int32_t basGfxClear(BasGfxT *g, BasValueT color);
    +
    +// A colour value checked against the surface's colour range.
    +int32_t basGfxColorArg(const BasGfxT *g, BasValueT v, uint32_t *out);
    +
    +// DRAW macro language.
    +int32_t basGfxDraw(BasGfxT *g, const char *macro, int32_t len);
    +
    +// One text glyph with its top-left corner at pixel (x, y), rows tall
    +// (BAS_GFX_GLYPH_TALL or _SHORT), clipped to the surface (not the view).
    +// opaque paints the background pixels too.  Without a font (basGfxSetFont)
    +// only the background is drawn.
    +void basGfxGlyph(BasGfxT *g, int32_t x, int32_t y, uint8_t ch, int32_t rows, uint32_t forePixel, uint32_t backPixel, bool opaque);
    +
    +// The last point goes to the centre of the view (QBasic after SCREEN,
    +// VIEW, WINDOW and CLS).
    +void basGfxHome(BasGfxT *g);
    +
    +// Start a surface's drawing state: whole-surface view, no WINDOW, last
    +// point in the centre, pen 1 pixel, transparent fill.
    +void basGfxInit(BasGfxT *g, const BasGfxSurfaceT *surf, uint32_t fore, uint32_t back, double aspect);
    +
    +// LINE block (BAS_GFX_LINE_ARGS values).
    +int32_t basGfxLine(BasGfxT *g, const BasValueT *args);
    +
    +// PAINT block (BAS_GFX_PAINT_ARGS values).
    +int32_t basGfxPaint(BasGfxT *g, const BasValueT *args);
    +
    +// The pixel colour of a surface colour value (the surface's toPixel).
    +uint32_t basGfxPixel(const BasGfxT *g, uint32_t color);
    +
    +// POINT(x, y): the colour at a point, -1 outside the view.
    +int32_t basGfxPoint(BasGfxT *g, BasValueT x, BasValueT y, BasValueT *out);
    +
    +// POINT(n): 0/1 the last point in view pixels, 2/3 in logical units.
    +int32_t basGfxPointLast(const BasGfxT *g, BasValueT n, BasValueT *out);
    +
    +// PSET / PRESET block (BAS_GFX_PSET_ARGS values).
    +int32_t basGfxPset(BasGfxT *g, const BasValueT *args);
    +
    +// The 8x16 glyph table (256 x 16 bytes, MSB = left pixel) text uses;
    +// NULL draws backgrounds only.  Shared by every surface.
    +void basGfxSetFont(const uint8_t *tallGlyphs);
    +
    +// VIEW block (BAS_VIEW_ARGS values).
    +int32_t basGfxSetView(BasGfxT *g, const BasValueT *args);
    +
    +// WINDOW block (BAS_WINDOW_ARGS values).
    +int32_t basGfxSetWindow(BasGfxT *g, const BasValueT *args);
    +
    +#endif // DVXBASIC_GFX_H
    diff --git a/src/apps/kpunch/dvxbasic/runtime/screen.c b/src/apps/kpunch/dvxbasic/runtime/screen.c
    new file mode 100644
    index 00000000..7cc05bb3
    --- /dev/null
    +++ b/src/apps/kpunch/dvxbasic/runtime/screen.c
    @@ -0,0 +1,996 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// screen.c -- DVX BASIC program screen (see screen.h)
    +
    +#include "screen.h"
    +#include "basErrors.h"
    +#include "../compiler/opcodes.h"
    +
    +#include 
    +#include 
    +
    +// Mode geometry
    +#define SCR_WIDE_W            640     // SCREEN 0 and 12 width
    +#define SCR_WIDE_COLS         80
    +#define SCR_TEXT_ROWS         25      // SCREEN 0 (8x16 glyphs); WIDTH 80, 50 uses 8x8
    +#define SCR_TEXT_ROWS_SHORT   50
    +#define SCR_VGA16_H           480
    +#define SCR_VGA16_ROWS        30      // SCREEN 12 (8x16 glyphs); WIDTH 80, 60 uses 8x8
    +#define SCR_VGA16_ROWS_SHORT  60
    +#define SCR_VGA256_W          320
    +#define SCR_VGA256_H          200
    +#define SCR_VGA256_COLS       40
    +#define SCR_VGA256_ROWS       25
    +
    +// Colours
    +#define SCR_EGA_COLORS        16      // SCREEN 0 and 12 colours
    +#define SCR_TEXT_FORE_MAX     31      // COLOR foreground in SCREEN 0 (16..31 blink in QBasic; shown steady)
    +#define SCR_NIBBLE            0x0F
    +#define SCR_BACK_SHIFT        4
    +#define SCR_TEXT_FORE         7       // default COLOR in SCREEN 0
    +#define SCR_GFX_FORE          15      // default COLOR in SCREEN 12 and 13
    +#define SCR_DAC_MAX           63      // 6-bit VGA DAC component
    +#define SCR_DAC_HIGH_SHIFT    2       // 6-bit -> 8-bit: v << 2 | v >> 4
    +#define SCR_DAC_LOW_SHIFT     4
    +#define SCR_VGA_GREEN_SHIFT   8       // PALETTE colour: blue * 65536 + green * 256 + red
    +#define SCR_VGA_BLUE_SHIFT    16
    +#define SCR_BYTE_MASK         0xFF
    +#define SCR_EGA_COLOR_MAX     63      // PALETTE colour in SCREEN 0: an EGA colour number rgbRGB
    +#define SCR_EGA_PRIMARY       42      // 6-bit level of the EGA primary bits
    +#define SCR_EGA_SECONDARY     21      // 6-bit level of the EGA secondary bits
    +#define SCR_EGA_BLUE          0x01
    +#define SCR_EGA_GREEN         0x02
    +#define SCR_EGA_RED           0x04
    +#define SCR_EGA_BLUE_LOW      0x08
    +#define SCR_EGA_GREEN_LOW     0x10
    +#define SCR_EGA_RED_LOW       0x20
    +#define SCR_PALETTE_KEEP      (-1)    // PALETTE USING: leave this attribute alone
    +
    +// The VGA default palette after the 16 EGA colours: 16 greys, then three
    +// intensities x three saturations x 24 hues, then black.
    +#define SCR_GREY_FIRST        16
    +#define SCR_RAMP_FIRST        32
    +#define SCR_RAMP_GROUPS       3
    +#define SCR_RAMP_STEPS        5
    +#define SCR_HUES              24
    +
    +// A screen is 4:3, so CIRCLE's default aspect is 4/3 * height / width.
    +#define SCR_ASPECT_W          4.0
    +#define SCR_ASPECT_H          3.0
    +
    +// Characters PRINT and INPUT treat specially
    +#define SCR_CHAR_BELL         7
    +#define SCR_CHAR_BACKSPACE    8
    +#define SCR_CHAR_TAB          9
    +#define SCR_CHAR_LF           10
    +#define SCR_CHAR_CR           13
    +#define SCR_CHAR_ESC          27
    +#define SCR_CHAR_SPACE        ' '
    +#define SCR_TAB_STOP          8
    +#define SCR_CURSOR_ROWS       2       // the text cursor: the cell's bottom rows
    +
    +// CLS arguments
    +#define SCR_CLS_OMITTED       (-1)
    +#define SCR_CLS_ALL           0
    +#define SCR_CLS_GRAPHICS      1
    +#define SCR_CLS_TEXT          2
    +
    +// 6-bit EGA colours 0..15
    +static const uint8_t sEgaPalette[SCR_EGA_COLORS][BAS_SCREEN_PAL_CHANNELS] = {
    +    {  0,  0,  0 }, {  0,  0, 42 }, {  0, 42,  0 }, {  0, 42, 42 },
    +    { 42,  0,  0 }, { 42,  0, 42 }, { 42, 21,  0 }, { 42, 42, 42 },
    +    { 21, 21, 21 }, { 21, 21, 63 }, { 21, 63, 21 }, { 21, 63, 63 },
    +    { 63, 21, 21 }, { 63, 21, 63 }, { 63, 63, 21 }, { 63, 63, 63 }
    +};
    +
    +// 6-bit grey ramp, VGA entries 16..31
    +static const uint8_t sGreyRamp[SCR_EGA_COLORS] = {
    +    0, 5, 8, 11, 14, 17, 20, 24, 28, 32, 36, 40, 45, 50, 56, 63
    +};
    +
    +// 6-bit levels of the hue ramps: [intensity][saturation][step]
    +static const uint8_t sRampLevels[SCR_RAMP_GROUPS][SCR_RAMP_GROUPS][SCR_RAMP_STEPS] = {
    +    { {  0, 16, 31, 47, 63 }, { 31, 39, 47, 55, 63 }, { 45, 49, 54, 58, 63 } },
    +    { {  0,  7, 14, 21, 28 }, { 14, 17, 21, 24, 28 }, { 20, 22, 24, 26, 28 } },
    +    { {  0,  4,  8, 12, 16 }, {  8, 10, 12, 14, 16 }, { 11, 12, 13, 15, 16 } }
    +};
    +
    +// Step index of R, G and B round the hue ring: blue, magenta, red, yellow,
    +// green, cyan and back towards blue.
    +static const uint8_t sHueRing[SCR_HUES][BAS_SCREEN_PAL_CHANNELS] = {
    +    { 0, 0, 4 }, { 1, 0, 4 }, { 2, 0, 4 }, { 3, 0, 4 }, { 4, 0, 4 }, { 4, 0, 3 }, { 4, 0, 2 }, { 4, 0, 1 },
    +    { 4, 0, 0 }, { 4, 1, 0 }, { 4, 2, 0 }, { 4, 3, 0 }, { 4, 4, 0 }, { 3, 4, 0 }, { 2, 4, 0 }, { 1, 4, 0 },
    +    { 0, 4, 0 }, { 0, 4, 1 }, { 0, 4, 2 }, { 0, 4, 3 }, { 0, 4, 4 }, { 0, 3, 4 }, { 0, 2, 4 }, { 0, 1, 4 }
    +};
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static uint8_t     cellAttr(const BasScreenT *scr);
    +static void        cellRender(BasScreenT *scr, int32_t r, int32_t c);
    +static void        clearAll(BasScreenT *scr);
    +static int32_t     cursorCol(const BasScreenT *scr);
    +static void        cursorHide(BasScreenT *scr);
    +static void        cursorShow(BasScreenT *scr);
    +static int32_t     execCls(BasScreenT *scr, BasValueT n);
    +static int32_t     execColor(BasScreenT *scr, const BasValueT *args);
    +static int32_t     execLineInput(BasVmT *vm, BasValueT prompt, BasValueT *result);
    +static int32_t     execLocate(BasScreenT *scr, const BasValueT *args);
    +static int32_t     execPalette(BasScreenT *scr, const BasValueT *args);
    +static int32_t     execPaletteUsing(BasScreenT *scr, const BasValueT *args);
    +static int32_t     execScreenFn(const BasScreenT *scr, const BasValueT *args, int32_t argc, BasValueT *result);
    +static int32_t     execSetMode(BasVmT *vm, BasScreenT *scr, int32_t mode, int32_t rows);
    +static int32_t     execWidth(BasVmT *vm, BasScreenT *scr, const BasValueT *args);
    +static uint8_t     expandDac(uint8_t v);
    +static void        markDirty(BasScreenT *scr, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
    +static void        newLine(BasScreenT *scr);
    +static int32_t     paletteColors(const BasScreenT *scr);
    +static void        paletteDefault(BasScreenT *scr);
    +static int32_t     paletteSet(BasScreenT *scr, int32_t attr, int32_t color);
    +static int32_t     popKey(BasScreenT *scr);
    +static void        putChar(BasScreenT *scr, uint8_t ch);
    +static int32_t     rowsDefault(int32_t mode);
    +static BasScreenT *screenEnsure(BasVmT *vm);
    +static void        scrollUp(BasScreenT *scr);
    +static int32_t     setMode(BasScreenT *scr, int32_t mode, int32_t rows);
    +static uint32_t    surfGet(const void *ctx, int32_t x, int32_t y);
    +static void        surfSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel);
    +static uint8_t     textBackPixel(const BasScreenT *scr);
    +
    +
    +void basScreenClean(BasScreenT *scr) {
    +    scr->dirty          = false;
    +    scr->paletteChanged = false;
    +}
    +
    +
    +int32_t basScreenColumn(const BasScreenT *scr) {
    +    return scr->col;
    +}
    +
    +
    +void basScreenDestroy(BasScreenT *scr) {
    +    if (!scr) {
    +        return;
    +    }
    +
    +    free(scr->fb);
    +    free(scr->chars);
    +    free(scr->attrs);
    +    free(scr);
    +}
    +
    +
    +int32_t basScreenExec(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, BasValueT *result) {
    +    *result = basValInteger(0);
    +
    +    // LINE INPUT and POS work without a screen (the host's input and
    +    // output); everything else makes one.
    +    if (fn == BAS_FN_LINE_INPUT) {
    +        return execLineInput(vm, args[0], result);
    +    }
    +
    +    if (fn == BAS_FN_POS && !vm->screen) {
    +        *result = basValInteger((int16_t)(vm->printColumn + 1));
    +        return 0;
    +    }
    +
    +    BasScreenT *scr = screenEnsure(vm);
    +
    +    if (!scr) {
    +        return BAS_ERR_OUT_OF_MEMORY;
    +    }
    +
    +    bool graphics = scr->mode != BAS_SCREEN_MODE_TEXT;
    +
    +    switch (fn) {
    +        case BAS_FN_CLS:
    +            return execCls(scr, args[0]);
    +
    +        case BAS_FN_LOCATE:
    +            return execLocate(scr, args);
    +
    +        case BAS_FN_COLOR:
    +            return execColor(scr, args);
    +
    +        case BAS_FN_SCREEN: {
    +            int32_t mode = basValToInt32(args[0]);
    +
    +            return execSetMode(vm, scr, mode, rowsDefault(mode));
    +        }
    +
    +        case BAS_FN_WIDTH:
    +            return execWidth(vm, scr, args);
    +
    +        case BAS_FN_PALETTE:
    +            return execPalette(scr, args);
    +
    +        case BAS_FN_PALETTE_USING:
    +            return execPaletteUsing(scr, args);
    +
    +        case BAS_FN_FULLSCREEN: {
    +            bool on = basValIsTruthy(args[0]);
    +
    +            if (vm->screenHost.fullScreen) {
    +                vm->screenHost.fullScreen(vm->screenHost.ctx, scr, on);
    +            } else {
    +                scr->fullScreen = on;
    +            }
    +
    +            return 0;
    +        }
    +
    +        case BAS_FN_INKEY: {
    +            char    key[2];
    +            int32_t len  = 0;
    +            int32_t code = popKey(scr);
    +
    +            if (code >= BAS_SCREEN_KEY_EXT) {
    +                key[len++] = 0;
    +                key[len++] = (char)(code & SCR_BYTE_MASK);
    +            } else if (code >= 0) {
    +                key[len++] = (char)code;
    +            }
    +
    +            BasStringT *text = basStringNew(key, len);
    +
    +            *result = basValString(text);
    +            basStringUnref(text);
    +            return 0;
    +        }
    +
    +        case BAS_FN_CSRLIN:
    +            *result = basValInteger((int16_t)(scr->row + 1));
    +            return 0;
    +
    +        case BAS_FN_POS:
    +            *result = basValInteger((int16_t)(cursorCol(scr) + 1));
    +            return 0;
    +
    +        case BAS_FN_SCREENFN:
    +            return execScreenFn(scr, args, argc, result);
    +
    +        default:
    +            break;
    +    }
    +
    +    // Graphics statements and POINT: not in the text mode (QBasic's
    +    // Illegal function call).
    +    if (!graphics) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    switch (fn) {
    +        case BAS_FN_PSET:
    +            return basGfxPset(&scr->gfx, args);
    +
    +        case BAS_FN_LINE:
    +            return basGfxLine(&scr->gfx, args);
    +
    +        case BAS_FN_CIRCLE:
    +            return basGfxCircle(&scr->gfx, args);
    +
    +        case BAS_FN_PAINT:
    +            return basGfxPaint(&scr->gfx, args);
    +
    +        case BAS_FN_DRAW: {
    +            if (args[0].type != BAS_TYPE_STRING) {
    +                return BAS_ERR_TYPE_MISMATCH;
    +            }
    +
    +            return basGfxDraw(&scr->gfx, args[0].strVal->data, args[0].strVal->len);
    +        }
    +
    +        case BAS_FN_VIEW:
    +            return basGfxSetView(&scr->gfx, args);
    +
    +        case BAS_FN_WINDOW:
    +            return basGfxSetWindow(&scr->gfx, args);
    +
    +        case BAS_FN_POINT:
    +            return argc == 1 ? basGfxPointLast(&scr->gfx, args[0], result) : basGfxPoint(&scr->gfx, args[0], args[1], result);
    +
    +        default:
    +            return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +}
    +
    +
    +bool basScreenInput(BasVmT *vm, const char *prompt, char *buf, int32_t bufSize) {
    +    BasScreenT *scr = vm->screen;
    +    int32_t     len = 0;
    +    bool        ok  = true;
    +
    +    basScreenPrint(scr, prompt, false);
    +    scr->inputActive = true;
    +    cursorShow(scr);
    +
    +    for (;;) {
    +        if (scr->keyCount == 0) {
    +            // Without an event pump no key can come: the line ends here.
    +            if (!vm->doEventsFn) {
    +                break;
    +            }
    +
    +            if (!basVmDoEvents(vm)) {
    +                ok = false;
    +                break;
    +            }
    +
    +            continue;
    +        }
    +
    +        int32_t key = popKey(scr);
    +
    +        if (key == SCR_CHAR_CR) {
    +            break;
    +        }
    +
    +        cursorHide(scr);
    +
    +        // Backspace takes one character back, Esc the whole line.
    +        int32_t erase = key == SCR_CHAR_BACKSPACE ? 1 : (key == SCR_CHAR_ESC ? len : 0);
    +
    +        for (int32_t i = 0; i < erase && len > 0; i++) {
    +            len--;
    +
    +            if (scr->col > 0) {
    +                scr->col--;
    +            } else if (scr->row > 0) {
    +                scr->row--;
    +                scr->col = scr->cols - 1;
    +            }
    +
    +            scr->chars[scr->row * scr->cols + scr->col] = SCR_CHAR_SPACE;
    +            cellRender(scr, scr->row, scr->col);
    +        }
    +
    +        if (key >= SCR_CHAR_SPACE && key < BAS_SCREEN_KEY_EXT && len < bufSize - 1) {
    +            buf[len++] = (char)key;
    +            putChar(scr, (uint8_t)key);
    +        }
    +
    +        cursorShow(scr);
    +    }
    +
    +    buf[len] = '\0';
    +    cursorHide(scr);
    +    scr->inputActive = false;
    +    newLine(scr);
    +    cursorShow(scr);
    +    return ok;
    +}
    +
    +
    +void basScreenInvalidate(BasScreenT *scr) {
    +    markDirty(scr, 0, 0, scr->width - 1, scr->height - 1);
    +    scr->paletteChanged = true;
    +}
    +
    +
    +void basScreenPresent(BasVmT *vm) {
    +    BasScreenT *scr = vm->screen;
    +
    +    if (scr && (scr->dirty || scr->paletteChanged) && vm->screenHost.present) {
    +        vm->screenHost.present(vm->screenHost.ctx, scr);
    +    }
    +}
    +
    +
    +void basScreenPrint(BasScreenT *scr, const char *text, bool newline) {
    +    cursorHide(scr);
    +
    +    for (const uint8_t *c = (const uint8_t *)text; *c; c++) {
    +        switch (*c) {
    +            case SCR_CHAR_CR:
    +            case SCR_CHAR_LF:
    +                newLine(scr);
    +                break;
    +
    +            case SCR_CHAR_TAB:
    +                do {
    +                    putChar(scr, SCR_CHAR_SPACE);
    +                } while (scr->col % SCR_TAB_STOP != 0 && scr->col < scr->cols);
    +
    +                break;
    +
    +            case SCR_CHAR_BELL:
    +                break;
    +
    +            default:
    +                putChar(scr, *c);
    +                break;
    +        }
    +    }
    +
    +    if (newline) {
    +        newLine(scr);
    +    }
    +
    +    cursorShow(scr);
    +}
    +
    +
    +void basScreenPushKey(BasScreenT *scr, int32_t code) {
    +    if (scr->keyCount >= BAS_SCREEN_KEY_QUEUE) {
    +        return;
    +    }
    +
    +    scr->keys[(scr->keyHead + scr->keyCount) % BAS_SCREEN_KEY_QUEUE] = code;
    +    scr->keyCount++;
    +}
    +
    +
    +// The attribute new text gets: SCREEN 0 fore | back << 4, else fore.
    +static uint8_t cellAttr(const BasScreenT *scr) {
    +    if (scr->mode == BAS_SCREEN_MODE_TEXT) {
    +        return (uint8_t)((scr->fore & SCR_NIBBLE) | ((scr->back & SCR_NIBBLE) << SCR_BACK_SHIFT));
    +    }
    +
    +    return scr->fore;
    +}
    +
    +
    +// Draw one text cell (and the cursor when it is there).
    +static void cellRender(BasScreenT *scr, int32_t r, int32_t c) {
    +    int32_t i    = r * scr->cols + c;
    +    uint8_t attr = scr->attrs[i];
    +    uint8_t fore = attr;
    +    uint8_t back = 0;
    +    int32_t x    = c * BAS_GFX_GLYPH_W;
    +    int32_t y    = r * scr->cellH;
    +
    +    if (scr->mode == BAS_SCREEN_MODE_TEXT) {
    +        fore = attr & SCR_NIBBLE;
    +        back = attr >> SCR_BACK_SHIFT;
    +    }
    +
    +    basGfxGlyph(&scr->gfx, x, y, scr->chars[i], scr->cellH, fore, back, true);
    +
    +    if (scr->cursorDrawn && r == scr->row && c == cursorCol(scr)) {
    +        for (int32_t k = scr->cellH - SCR_CURSOR_ROWS; k < scr->cellH; k++) {
    +            surfSpan(scr, x, x + BAS_GFX_GLYPH_W - 1, y + k, fore);
    +        }
    +    }
    +}
    +
    +
    +// Blank screen, text home.
    +static void clearAll(BasScreenT *scr) {
    +    memset(scr->fb, textBackPixel(scr), (size_t)scr->width * (size_t)scr->height);
    +    memset(scr->chars, SCR_CHAR_SPACE, (size_t)(scr->rows * scr->cols));
    +    memset(scr->attrs, cellAttr(scr), (size_t)(scr->rows * scr->cols));
    +    scr->row = 0;
    +    scr->col = 0;
    +    markDirty(scr, 0, 0, scr->width - 1, scr->height - 1);
    +}
    +
    +
    +// The cell the cursor is in (a pending wrap stays in the last column).
    +static int32_t cursorCol(const BasScreenT *scr) {
    +    return scr->col < scr->cols ? scr->col : scr->cols - 1;
    +}
    +
    +
    +static void cursorHide(BasScreenT *scr) {
    +    if (!scr->cursorDrawn) {
    +        return;
    +    }
    +
    +    scr->cursorDrawn = false;
    +    cellRender(scr, scr->row, cursorCol(scr));
    +}
    +
    +
    +// The cursor shows in SCREEN 0 after LOCATE ,,1, and in every mode while
    +// INPUT waits.
    +static void cursorShow(BasScreenT *scr) {
    +    scr->cursorDrawn = scr->inputActive || (scr->cursorVisible && scr->mode == BAS_SCREEN_MODE_TEXT);
    +
    +    if (scr->cursorDrawn) {
    +        cellRender(scr, scr->row, cursorCol(scr));
    +    }
    +}
    +
    +
    +// CLS: everything (and the text cursor home), or with n = 1 (or omitted
    +// under a VIEW) the graphics view only.
    +static int32_t execCls(BasScreenT *scr, BasValueT n) {
    +    int32_t what     = basValToInt32(n);
    +    bool    viewSet  = scr->gfx.viewX0 > 0 || scr->gfx.viewY0 > 0 || scr->gfx.viewX1 < scr->width - 1 || scr->gfx.viewY1 < scr->height - 1;
    +    bool    graphics = scr->mode != BAS_SCREEN_MODE_TEXT;
    +
    +    if (what < SCR_CLS_OMITTED || what > SCR_CLS_TEXT) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if (graphics && (what == SCR_CLS_GRAPHICS || (what == SCR_CLS_OMITTED && viewSet))) {
    +        basGfxClear(&scr->gfx, basValInteger(0));
    +        basGfxHome(&scr->gfx);
    +        return 0;
    +    }
    +
    +    cursorHide(scr);
    +    clearAll(scr);
    +    basGfxHome(&scr->gfx);
    +    cursorShow(scr);
    +    return 0;
    +}
    +
    +
    +// COLOR fore, back: SCREEN 0 takes both (fore 0..31, back 0..15); the
    +// graphics modes only a foreground in their colour range.
    +static int32_t execColor(BasScreenT *scr, const BasValueT *args) {
    +    int32_t given = basValToInt32(args[2]);
    +    int32_t fore  = basValToInt32(args[0]);
    +    int32_t back  = basValToInt32(args[1]);
    +    bool    text  = scr->mode == BAS_SCREEN_MODE_TEXT;
    +    int32_t max   = text ? SCR_TEXT_FORE_MAX : (int32_t)scr->gfx.surf.colorMax;
    +
    +    if ((given & BAS_GIVEN_1) && (fore < 0 || fore > max)) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if ((given & BAS_GIVEN_2) && (!text || back < 0 || back > SCR_NIBBLE)) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if (given & BAS_GIVEN_1) {
    +        scr->fore     = (uint8_t)fore;
    +        scr->gfx.fore = (uint32_t)fore;
    +    }
    +
    +    if (given & BAS_GIVEN_2) {
    +        scr->back = (uint8_t)back;
    +    }
    +
    +    return 0;
    +}
    +
    +
    +// LINE INPUT: the whole line as typed, on the screen when the program has
    +// one, else through the host's input (no "? " added).
    +static int32_t execLineInput(BasVmT *vm, BasValueT prompt, BasValueT *result) {
    +    char        buf[BAS_INPUT_LINE_LEN];
    +    BasValueT   text = basValToString(prompt);
    +    const char *p    = text.strVal->data;
    +    bool        ok   = false;
    +
    +    buf[0] = '\0';
    +
    +    if (vm->screen) {
    +        ok = basScreenInput(vm, p, buf, sizeof(buf));
    +    } else if (vm->inputFn) {
    +        ok = vm->inputFn(vm->inputCtx, p, buf, sizeof(buf));
    +    }
    +
    +    basValRelease(&text);
    +    *result = basValStringFromC(ok ? buf : "");
    +    return 0;
    +}
    +
    +
    +static int32_t execLocate(BasScreenT *scr, const BasValueT *args) {
    +    int32_t given  = basValToInt32(args[3]);
    +    int32_t row    = basValToInt32(args[0]);
    +    int32_t col    = basValToInt32(args[1]);
    +    int32_t cursor = basValToInt32(args[2]);
    +
    +    if (((given & BAS_GIVEN_1) && (row < 1 || row > scr->rows)) || ((given & BAS_GIVEN_2) && (col < 1 || col > scr->cols)) || ((given & BAS_GIVEN_3) && (cursor < 0 || cursor > 1))) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    cursorHide(scr);
    +
    +    if (given & BAS_GIVEN_1) {
    +        scr->row = row - 1;
    +    }
    +
    +    if (given & BAS_GIVEN_2) {
    +        scr->col = col - 1;
    +    }
    +
    +    if (given & BAS_GIVEN_3) {
    +        scr->cursorVisible = cursor != 0;
    +    }
    +
    +    cursorShow(scr);
    +    return 0;
    +}
    +
    +
    +// PALETTE attr, color / PALETTE (every attribute back to the default).
    +static int32_t execPalette(BasScreenT *scr, const BasValueT *args) {
    +    if (!(basValToInt32(args[2]) & BAS_GIVEN_1)) {
    +        paletteDefault(scr);
    +        return 0;
    +    }
    +
    +    return paletteSet(scr, basValToInt32(args[0]), basValToInt32(args[1]));
    +}
    +
    +
    +// PALETTE USING array(first): one colour per attribute from the array
    +// (-1 keeps that attribute); the array must reach the last attribute.
    +static int32_t execPaletteUsing(BasScreenT *scr, const BasValueT *args) {
    +    if (args[0].type != BAS_TYPE_ARRAY || !args[0].arrVal) {
    +        return BAS_ERR_TYPE_MISMATCH;
    +    }
    +
    +    const BasArrayT *arr   = args[0].arrVal;
    +    int32_t          first = (basValToInt32(args[2]) & BAS_GIVEN_2) ? basValToInt32(args[1]) : arr->lbound[0];
    +    int32_t          start = first - arr->lbound[0];
    +    int32_t          count = paletteColors(scr);
    +
    +    if (arr->dims != 1 || start < 0 || start + count > arr->totalElements) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    for (int32_t attr = 0; attr < count; attr++) {
    +        int32_t color = basValToInt32(arr->elements[start + attr]);
    +
    +        if (color == SCR_PALETTE_KEEP) {
    +            continue;
    +        }
    +
    +        int32_t err = paletteSet(scr, attr, color);
    +
    +        if (err != 0) {
    +            return err;
    +        }
    +    }
    +
    +    return 0;
    +}
    +
    +
    +// SCREEN(row, col [, colorflag]): the character there, or its colour.
    +static int32_t execScreenFn(const BasScreenT *scr, const BasValueT *args, int32_t argc, BasValueT *result) {
    +    int32_t row = basValToInt32(args[0]);
    +    int32_t col = basValToInt32(args[1]);
    +
    +    if (row < 1 || row > scr->rows || col < 1 || col > scr->cols) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    int32_t i = (row - 1) * scr->cols + (col - 1);
    +
    +    *result = basValInteger(argc > 2 && basValIsTruthy(args[2]) ? scr->attrs[i] : scr->chars[i]);
    +    return 0;
    +}
    +
    +
    +// SCREEN mode / WIDTH: a new mode (cleared), shown by the host.
    +static int32_t execSetMode(BasVmT *vm, BasScreenT *scr, int32_t mode, int32_t rows) {
    +    int32_t err = setMode(scr, mode, rows);
    +
    +    if (err == 0 && vm->screenHost.modeChanged) {
    +        vm->screenHost.modeChanged(vm->screenHost.ctx, scr);
    +    }
    +
    +    return err;
    +}
    +
    +
    +// WIDTH cols, rows: 80 columns in SCREEN 0 (25 or 50 rows) and SCREEN 12
    +// (30 or 60 rows); SCREEN 13 has only 40x25.
    +static int32_t execWidth(BasVmT *vm, BasScreenT *scr, const BasValueT *args) {
    +    int32_t given = basValToInt32(args[2]);
    +    int32_t cols  = (given & BAS_GIVEN_1) ? basValToInt32(args[0]) : scr->cols;
    +    int32_t rows  = (given & BAS_GIVEN_2) ? basValToInt32(args[1]) : scr->rows;
    +    bool    ok    = false;
    +
    +    switch (scr->mode) {
    +        case BAS_SCREEN_MODE_TEXT:
    +            ok = cols == SCR_WIDE_COLS && (rows == SCR_TEXT_ROWS || rows == SCR_TEXT_ROWS_SHORT);
    +            break;
    +
    +        case BAS_SCREEN_MODE_VGA16:
    +            ok = cols == SCR_WIDE_COLS && (rows == SCR_VGA16_ROWS || rows == SCR_VGA16_ROWS_SHORT);
    +            break;
    +
    +        default:
    +            ok = cols == SCR_VGA256_COLS && rows == SCR_VGA256_ROWS;
    +            break;
    +    }
    +
    +    if (!ok) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    return rows == scr->rows ? 0 : execSetMode(vm, scr, scr->mode, rows);
    +}
    +
    +
    +// 6-bit DAC level -> 8 bits.
    +static uint8_t expandDac(uint8_t v) {
    +    return (uint8_t)((v << SCR_DAC_HIGH_SHIFT) | (v >> SCR_DAC_LOW_SHIFT));
    +}
    +
    +
    +static void markDirty(BasScreenT *scr, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
    +    if (!scr->dirty) {
    +        scr->dirty   = true;
    +        scr->dirtyX0 = x0;
    +        scr->dirtyY0 = y0;
    +        scr->dirtyX1 = x1;
    +        scr->dirtyY1 = y1;
    +        return;
    +    }
    +
    +    scr->dirtyX0 = x0 < scr->dirtyX0 ? x0 : scr->dirtyX0;
    +    scr->dirtyY0 = y0 < scr->dirtyY0 ? y0 : scr->dirtyY0;
    +    scr->dirtyX1 = x1 > scr->dirtyX1 ? x1 : scr->dirtyX1;
    +    scr->dirtyY1 = y1 > scr->dirtyY1 ? y1 : scr->dirtyY1;
    +}
    +
    +
    +// Next line, scrolling at the bottom.
    +static void newLine(BasScreenT *scr) {
    +    scr->col = 0;
    +
    +    if (scr->row < scr->rows - 1) {
    +        scr->row++;
    +    } else {
    +        scrollUp(scr);
    +    }
    +}
    +
    +
    +// Attributes PALETTE can change: 256 in SCREEN 13, else 16.
    +static int32_t paletteColors(const BasScreenT *scr) {
    +    return scr->mode == BAS_SCREEN_MODE_VGA256 ? BAS_SCREEN_PAL_ENTRIES : SCR_EGA_COLORS;
    +}
    +
    +
    +// The mode's default palette: the 16 EGA colours, and in SCREEN 13 the
    +// rest of the VGA default palette.
    +static void paletteDefault(BasScreenT *scr) {
    +    uint8_t *pal = scr->palette;
    +
    +    memset(pal, 0, sizeof(scr->palette));
    +
    +    for (int32_t i = 0; i < SCR_EGA_COLORS; i++) {
    +        for (int32_t ch = 0; ch < BAS_SCREEN_PAL_CHANNELS; ch++) {
    +            pal[i * BAS_SCREEN_PAL_CHANNELS + ch] = expandDac(sEgaPalette[i][ch]);
    +        }
    +    }
    +
    +    if (scr->mode == BAS_SCREEN_MODE_VGA256) {
    +        for (int32_t i = 0; i < SCR_EGA_COLORS; i++) {
    +            memset(&pal[(SCR_GREY_FIRST + i) * BAS_SCREEN_PAL_CHANNELS], expandDac(sGreyRamp[i]), BAS_SCREEN_PAL_CHANNELS);
    +        }
    +
    +        int32_t entry = SCR_RAMP_FIRST;
    +
    +        for (int32_t level = 0; level < SCR_RAMP_GROUPS; level++) {
    +            for (int32_t sat = 0; sat < SCR_RAMP_GROUPS; sat++) {
    +                for (int32_t hue = 0; hue < SCR_HUES; hue++) {
    +                    for (int32_t ch = 0; ch < BAS_SCREEN_PAL_CHANNELS; ch++) {
    +                        pal[entry * BAS_SCREEN_PAL_CHANNELS + ch] = expandDac(sRampLevels[level][sat][sHueRing[hue][ch]]);
    +                    }
    +
    +                    entry++;
    +                }
    +            }
    +        }
    +    }
    +
    +    scr->paletteChanged = true;
    +}
    +
    +
    +// One attribute's colour: an EGA colour number (0..63) in SCREEN 0, else
    +// blue * 65536 + green * 256 + red with 6-bit levels.
    +static int32_t paletteSet(BasScreenT *scr, int32_t attr, int32_t color) {
    +    uint8_t rgb[BAS_SCREEN_PAL_CHANNELS];
    +
    +    if (attr < 0 || attr >= paletteColors(scr) || color < 0) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if (scr->mode == BAS_SCREEN_MODE_TEXT) {
    +        if (color > SCR_EGA_COLOR_MAX) {
    +            return BAS_ERR_ILLEGAL_FUNC_CALL;
    +        }
    +
    +        rgb[0] = (uint8_t)(((color & SCR_EGA_RED) ? SCR_EGA_PRIMARY : 0) + ((color & SCR_EGA_RED_LOW) ? SCR_EGA_SECONDARY : 0));
    +        rgb[1] = (uint8_t)(((color & SCR_EGA_GREEN) ? SCR_EGA_PRIMARY : 0) + ((color & SCR_EGA_GREEN_LOW) ? SCR_EGA_SECONDARY : 0));
    +        rgb[2] = (uint8_t)(((color & SCR_EGA_BLUE) ? SCR_EGA_PRIMARY : 0) + ((color & SCR_EGA_BLUE_LOW) ? SCR_EGA_SECONDARY : 0));
    +    } else {
    +        rgb[0] = (uint8_t)(color & SCR_BYTE_MASK);
    +        rgb[1] = (uint8_t)((color >> SCR_VGA_GREEN_SHIFT) & SCR_BYTE_MASK);
    +        rgb[2] = (uint8_t)((color >> SCR_VGA_BLUE_SHIFT) & SCR_BYTE_MASK);
    +
    +        if (rgb[0] > SCR_DAC_MAX || rgb[1] > SCR_DAC_MAX || (color >> SCR_VGA_BLUE_SHIFT) > SCR_DAC_MAX) {
    +            return BAS_ERR_ILLEGAL_FUNC_CALL;
    +        }
    +    }
    +
    +    for (int32_t ch = 0; ch < BAS_SCREEN_PAL_CHANNELS; ch++) {
    +        scr->palette[attr * BAS_SCREEN_PAL_CHANNELS + ch] = expandDac(rgb[ch]);
    +    }
    +
    +    scr->paletteChanged = true;
    +    return 0;
    +}
    +
    +
    +// The next key, or -1.
    +static int32_t popKey(BasScreenT *scr) {
    +    if (scr->keyCount == 0) {
    +        return -1;
    +    }
    +
    +    int32_t code = scr->keys[scr->keyHead];
    +
    +    scr->keyHead = (scr->keyHead + 1) % BAS_SCREEN_KEY_QUEUE;
    +    scr->keyCount--;
    +    return code;
    +}
    +
    +
    +static void putChar(BasScreenT *scr, uint8_t ch) {
    +    if (scr->col >= scr->cols) {
    +        newLine(scr);
    +    }
    +
    +    int32_t i = scr->row * scr->cols + scr->col;
    +
    +    scr->chars[i] = ch;
    +    scr->attrs[i] = cellAttr(scr);
    +    cellRender(scr, scr->row, scr->col);
    +    scr->col++;
    +}
    +
    +
    +static int32_t rowsDefault(int32_t mode) {
    +    return mode == BAS_SCREEN_MODE_VGA16 ? SCR_VGA16_ROWS : SCR_TEXT_ROWS;
    +}
    +
    +
    +// The program's screen, made in SCREEN 0 by its first console or graphics
    +// statement.
    +static BasScreenT *screenEnsure(BasVmT *vm) {
    +    if (vm->screen) {
    +        return vm->screen;
    +    }
    +
    +    BasScreenT *scr = (BasScreenT *)calloc(1, sizeof(BasScreenT));
    +
    +    if (!scr || setMode(scr, BAS_SCREEN_MODE_TEXT, SCR_TEXT_ROWS) != 0) {
    +        basScreenDestroy(scr);
    +        return NULL;
    +    }
    +
    +    vm->screen = scr;
    +
    +    if (vm->screenHost.modeChanged) {
    +        vm->screenHost.modeChanged(vm->screenHost.ctx, scr);
    +    }
    +
    +    return scr;
    +}
    +
    +
    +// Text moves up a line; the new bottom line is blank.
    +static void scrollUp(BasScreenT *scr) {
    +    size_t  line  = (size_t)scr->cols;
    +    size_t  band  = (size_t)scr->width * (size_t)scr->cellH;
    +    int32_t last  = (scr->rows - 1) * scr->cols;
    +
    +    memmove(scr->chars, scr->chars + line, (size_t)last);
    +    memmove(scr->attrs, scr->attrs + line, (size_t)last);
    +    memset(scr->chars + last, SCR_CHAR_SPACE, line);
    +    memset(scr->attrs + last, cellAttr(scr), line);
    +    memmove(scr->fb, scr->fb + band, (size_t)scr->width * (size_t)scr->height - band);
    +    memset(scr->fb + (size_t)scr->width * (size_t)scr->height - band, textBackPixel(scr), band);
    +    markDirty(scr, 0, 0, scr->width - 1, scr->height - 1);
    +}
    +
    +
    +// Make the screen mode with rows text rows: cleared, default colours,
    +// palette, view and cursor.
    +static int32_t setMode(BasScreenT *scr, int32_t mode, int32_t rows) {
    +    int32_t width  = SCR_WIDE_W;
    +    int32_t height = 0;
    +    int32_t cols   = SCR_WIDE_COLS;
    +
    +    switch (mode) {
    +        case BAS_SCREEN_MODE_TEXT:
    +            height = SCR_TEXT_ROWS * BAS_GFX_GLYPH_TALL;
    +            break;
    +
    +        case BAS_SCREEN_MODE_VGA16:
    +            height = SCR_VGA16_H;
    +            break;
    +
    +        case BAS_SCREEN_MODE_VGA256:
    +            width  = SCR_VGA256_W;
    +            height = SCR_VGA256_H;
    +            cols   = SCR_VGA256_COLS;
    +            break;
    +
    +        default:
    +            return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    uint8_t *fb    = (uint8_t *)realloc(scr->fb, (size_t)width * (size_t)height);
    +    uint8_t *chars = fb ? (uint8_t *)realloc(scr->chars, (size_t)(rows * cols)) : NULL;
    +    uint8_t *attrs = chars ? (uint8_t *)realloc(scr->attrs, (size_t)(rows * cols)) : NULL;
    +
    +    scr->fb    = fb ? fb : scr->fb;
    +    scr->chars = chars ? chars : scr->chars;
    +
    +    if (!attrs) {
    +        return BAS_ERR_OUT_OF_MEMORY;
    +    }
    +
    +    BasGfxSurfaceT surf;
    +
    +    memset(&surf, 0, sizeof(surf));
    +    surf.ctx      = scr;
    +    surf.width    = width;
    +    surf.height   = height;
    +    surf.colorMax = mode == BAS_SCREEN_MODE_VGA256 ? BAS_SCREEN_PAL_ENTRIES - 1 : SCR_EGA_COLORS - 1;
    +    surf.span     = surfSpan;
    +    surf.get      = surfGet;
    +
    +    scr->attrs         = attrs;
    +    scr->mode          = mode;
    +    scr->width         = width;
    +    scr->height        = height;
    +    scr->cols          = cols;
    +    scr->rows          = rows;
    +    scr->cellH         = height / rows;
    +    scr->fore          = mode == BAS_SCREEN_MODE_TEXT ? SCR_TEXT_FORE : SCR_GFX_FORE;
    +    scr->back          = 0;
    +    scr->cursorVisible = false;
    +    scr->cursorDrawn   = false;
    +    basGfxInit(&scr->gfx, &surf, scr->fore, 0, SCR_ASPECT_W / SCR_ASPECT_H * height / width);
    +    paletteDefault(scr);
    +    clearAll(scr);
    +    return 0;
    +}
    +
    +
    +static uint32_t surfGet(const void *ctx, int32_t x, int32_t y) {
    +    const BasScreenT *scr = (const BasScreenT *)ctx;
    +
    +    return scr->fb[y * scr->width + x];
    +}
    +
    +
    +static void surfSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel) {
    +    BasScreenT *scr = (BasScreenT *)ctx;
    +
    +    memset(scr->fb + y * scr->width + x0, (uint8_t)pixel, (size_t)(x1 - x0 + 1));
    +    markDirty(scr, x0, y, x1, y);
    +}
    +
    +
    +// Blank text: the COLOR background in SCREEN 0, colour 0 in graphics modes.
    +static uint8_t textBackPixel(const BasScreenT *scr) {
    +    return scr->mode == BAS_SCREEN_MODE_TEXT ? (uint8_t)(scr->back & SCR_NIBBLE) : 0;
    +}
    diff --git a/src/apps/kpunch/dvxbasic/runtime/screen.h b/src/apps/kpunch/dvxbasic/runtime/screen.h
    new file mode 100644
    index 00000000..a78a8f4a
    --- /dev/null
    +++ b/src/apps/kpunch/dvxbasic/runtime/screen.h
    @@ -0,0 +1,122 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// screen.h -- DVX BASIC program screen (QBasic console and graphics)
    +//
    +// A program gets its screen with its first console or graphics statement
    +// (CLS, LOCATE, COLOR, SCREEN, WIDTH, PSET, LINE, INKEY$, ...).  From then
    +// on PRINT and INPUT use it too.  The screen is an 8-bit framebuffer with
    +// its own palette in one of QBasic's modes:
    +//
    +//   SCREEN 0   text, 80x25 (WIDTH 80, 50: 80x50), 16 colours, 640x400 pixels
    +//   SCREEN 12  640x480, 16 colours, 80x30 text (WIDTH 80, 60: 80x60)
    +//   SCREEN 13  320x200, 256 colours (the VGA default palette), 40x25 text
    +//
    +// The VM keeps the screen (BasVmT.screen); the host shows it and feeds it
    +// keys through BasScreenHostT (vm.h) and basScreenPushKey.  Everything
    +// here works without a host too (the test harnesses read the framebuffer
    +// and the text cells directly).
    +
    +#ifndef DVXBASIC_SCREEN_H
    +#define DVXBASIC_SCREEN_H
    +
    +#include "gfx.h"
    +#include "vm.h"
    +
    +#include 
    +#include 
    +
    +#define BAS_SCREEN_MODE_TEXT     0
    +#define BAS_SCREEN_MODE_VGA16    12
    +#define BAS_SCREEN_MODE_VGA256   13
    +
    +#define BAS_SCREEN_KEY_EXT       0x100   // a key without a character: BAS_SCREEN_KEY_EXT | scan code (INKEY$: CHR$(0) + CHR$(scan))
    +#define BAS_SCREEN_KEY_QUEUE     64      // keys waiting for INKEY$ / INPUT
    +#define BAS_SCREEN_PAL_ENTRIES   256
    +#define BAS_SCREEN_PAL_CHANNELS  3       // R, G, B (8 bits each)
    +
    +typedef struct BasScreenT {
    +    int32_t  mode;                 // BAS_SCREEN_MODE_*
    +    int32_t  width;                // framebuffer size in pixels
    +    int32_t  height;
    +    uint8_t *fb;                   // width * height colour numbers
    +    uint8_t  palette[BAS_SCREEN_PAL_ENTRIES * BAS_SCREEN_PAL_CHANNELS];
    +    int32_t  cols;                 // text grid
    +    int32_t  rows;
    +    int32_t  cellH;                // glyph rows (BAS_GFX_GLYPH_TALL / _SHORT)
    +    uint8_t *chars;                // rows * cols character codes
    +    uint8_t *attrs;                // text mode: fore | back << 4; graphics modes: fore
    +    int32_t  row;                  // text cursor, 0-based
    +    int32_t  col;                  // == cols after the last column: the next character wraps first
    +    bool     cursorVisible;        // LOCATE ,,1
    +    bool     cursorDrawn;          // the cursor is on the screen now (visible, or INPUT waiting)
    +    bool     inputActive;          // INPUT is reading (the cursor shows in every mode)
    +    uint8_t  fore;                 // COLOR
    +    uint8_t  back;
    +    BasGfxT  gfx;                  // graphics state (graphics modes)
    +    bool     dirty;                // pixels changed since the host showed the screen
    +    int32_t  dirtyX0;
    +    int32_t  dirtyY0;
    +    int32_t  dirtyX1;
    +    int32_t  dirtyY1;
    +    bool     paletteChanged;       // the palette changed since the host showed the screen
    +    bool     fullScreen;           // FULLSCREEN / Alt+Enter state (the host keeps it)
    +    int32_t  keys[BAS_SCREEN_KEY_QUEUE];
    +    int32_t  keyHead;
    +    int32_t  keyCount;
    +} BasScreenT;
    +
    +// The host has shown everything: forget the dirty rectangle and palette.
    +void basScreenClean(BasScreenT *scr);
    +
    +// The column PRINT is at (0-based; cols while a wrap is pending).
    +int32_t basScreenColumn(const BasScreenT *scr);
    +
    +// Free a screen (NULL is fine).
    +void basScreenDestroy(BasScreenT *scr);
    +
    +// Run a program-screen built-in (BAS_FN_SCREEN_FIRST .. _LAST).  Returns 0
    +// or a BASIC error number; *result is the function's value (0 for the
    +// statements).
    +int32_t basScreenExec(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, BasValueT *result);
    +
    +// INPUT / LINE INPUT on the screen: prompt, then the line typed (Enter
    +// ends it; Backspace and Esc edit), keeping the host's events running.
    +// False when the program is stopping.
    +bool basScreenInput(BasVmT *vm, const char *prompt, char *buf, int32_t bufSize);
    +
    +// Everything must be shown again (the host's window or mode changed).
    +void basScreenInvalidate(BasScreenT *scr);
    +
    +// Let the host show what changed (the VM calls this whenever it gives the
    +// host a turn).
    +void basScreenPresent(BasVmT *vm);
    +
    +// PRINT text at the cursor (CHR$(13) / CHR$(10) start a new line, CHR$(9)
    +// goes to the next multiple of 8), then a new line when newline.
    +void basScreenPrint(BasScreenT *scr, const char *text, bool newline);
    +
    +// A key for INKEY$ / INPUT: a character, or BAS_SCREEN_KEY_EXT | scan.
    +// Dropped when the queue is full.
    +void basScreenPushKey(BasScreenT *scr, int32_t code);
    +
    +#endif // DVXBASIC_SCREEN_H
    diff --git a/src/apps/kpunch/dvxbasic/runtime/serialize.c b/src/apps/kpunch/dvxbasic/runtime/serialize.c
    index 7f9c5290..e96188f9 100644
    --- a/src/apps/kpunch/dvxbasic/runtime/serialize.c
    +++ b/src/apps/kpunch/dvxbasic/runtime/serialize.c
    @@ -60,6 +60,20 @@
     //   globalInitCount  int32, then that many entries:
     //                    index        int32
     //                    dataType     uint8
    +//
    +//   classCount       int32, then that many class modules:
    +//                    name          uint16 len + bytes
    +//                    varCount      int32
    +//                    initCodeAddr  int32
    +//                    terminateProc int32 (-1 = none)
    +//                    memberCount   int32, then that many members:
    +//                      name           uint16 len + bytes
    +//                      index          int32
    +//                      kind           uint8
    +//                      flags          uint8
    +//                      dataType       uint8
    +//                      requiredParams uint8
    +//                      paramCount     uint8, then that many parameter types (uint8)
     
     #include "serialize.h"
     #include "../compiler/opcodes.h"
    @@ -75,7 +89,7 @@
     // so both sides pull from the same definition.
     #define BAS_MOD_MAGIC        "DBXM"
     #define BAS_MOD_MAGIC_LEN    4
    -#define BAS_MOD_VERSION      3  // 2: per-frame GOSUB stack (OP_GOSUB), OP_STR_APPEND; 3: BEEP / SOUND / PLAY / STICK / STRIG / FRAMEWAIT opcodes (runtime and stub rebuilt together)
    +#define BAS_MOD_VERSION      7  // 2: per-frame GOSUB stack (OP_GOSUB), OP_STR_APPEND; 3: BEEP / SOUND / PLAY / STICK / STRIG / FRAMEWAIT opcodes (runtime and stub rebuilt together); 4: DATE type, OP_CONV_DATE, OP_BUILTIN; 5: BAS_FN_PARAMARRAY; 6: program screen built-ins (BAS_FN_CLS .. BAS_FN_LINE_INPUT); 7: Variants, objects and class modules (class table, OP_PUSH_SPECIAL .. OP_PROP_ASSIGN, BAS_FN_VARTYPE .. BAS_FN_EACH_ITEM)
     
     // Fixed header sizes on the wire.  BAS_MOD_HEADER_SIZE is magic(4) +
     // version(2) + 6 int32 header fields(24) = 30.  BAS_DBG_HEADER_SIZE is
    @@ -90,6 +104,8 @@
     #define BAS_PROC_MIN_ENTRY        18
     #define BAS_FORMVAR_MIN_ENTRY     14
     #define BAS_GLOBALINIT_MIN_ENTRY  5
    +#define BAS_CLASS_MIN_ENTRY       18
    +#define BAS_MEMBER_MIN_ENTRY      11
     #define BAS_DEBUGVAR_MIN_ENTRY    15
     #define BAS_BUF_INIT_CAP          4096
     
    @@ -126,6 +142,7 @@ static void bufWriteI32(BufT *b, int32_t v);
     static void bufWriteStr(BufT *b, const char *s, int32_t len);
     static void bufWriteU16(BufT *b, uint16_t v);
     static void bufWriteU8(BufT *b, uint8_t v);
    +static bool classRangesValid(const BasModuleT *mod, const BasClassInfoT *cls);
     static bool countFits(ReaderT *r, int32_t count, int32_t minBytesPerEntry);
     static bool moduleRangesValid(const BasModuleT *mod);
     static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry);
    @@ -428,6 +445,64 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
             }
         }
     
    +    // Class modules
    +    int32_t classCount = readCount(&r, BAS_CLASS_MIN_ENTRY);
    +
    +    if (classCount > 0) {
    +        mod->classes = (BasClassInfoT *)calloc(classCount, sizeof(BasClassInfoT));
    +
    +        if (!mod->classes) {
    +            basModuleFree(mod);
    +            return NULL;
    +        }
    +
    +        mod->classCount = classCount;
    +
    +        for (int32_t c = 0; c < classCount && !r.err; c++) {
    +            BasClassInfoT *cls = &mod->classes[c];
    +
    +            rStrInto(&r, cls->name, BAS_MAX_IDENT);
    +            cls->varCount      = rI32(&r);
    +            cls->initCodeAddr  = rI32(&r);
    +            cls->terminateProc = rI32(&r);
    +
    +            int32_t memberCount = readCount(&r, BAS_MEMBER_MIN_ENTRY);
    +
    +            if (memberCount > 0) {
    +                cls->members = (BasClassMemberT *)calloc(memberCount, sizeof(BasClassMemberT));
    +
    +                if (!cls->members) {
    +                    basModuleFree(mod);
    +                    return NULL;
    +                }
    +
    +                cls->memberCount = memberCount;
    +            }
    +
    +            for (int32_t i = 0; i < memberCount; i++) {
    +                BasClassMemberT *m = &cls->members[i];
    +
    +                rStrInto(&r, m->name, BAS_MAX_IDENT);
    +                m->index          = rI32(&r);
    +                m->kind           = rU8(&r);
    +                m->flags          = rU8(&r);
    +                m->dataType       = rU8(&r);
    +                m->requiredParams = rU8(&r);
    +
    +                int32_t paramCount = rU8(&r);
    +
    +                if (paramCount > BAS_VM_MAX_CALL_ARGS) {
    +                    r.err = true;
    +                    break;
    +                }
    +
    +                for (int32_t k = 0; k < paramCount; k++) {
    +                    m->paramTypes[k] = rU8(&r);
    +                }
    +            }
    +        }
    +    }
    +
         // A latched read error means the stream was truncated or corrupt; the
         // reader returned benign defaults for everything past the fault, so the
         // module would load with zeroed proc/code addresses.  Reject it, and
    @@ -485,6 +560,12 @@ void basModuleFree(BasModuleT *mod) {
         free(mod->globalInits);
         free(mod->debugVars);
     
    +    for (int32_t i = 0; i < mod->classCount; i++) {
    +        free(mod->classes[i].members);
    +    }
    +
    +    free(mod->classes);
    +
         if (mod->debugUdtDefs) {
             for (int32_t i = 0; i < mod->debugUdtDefCount; i++) {
                 free(mod->debugUdtDefs[i].fields);
    @@ -597,6 +678,35 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
             bufWriteU8(&b, mod->globalInits[i].dataType);
         }
     
    +    // Class modules (runtime-essential: New and member access)
    +    bufWriteI32(&b, mod->classCount);
    +
    +    for (int32_t c = 0; c < mod->classCount; c++) {
    +        const BasClassInfoT *cls = &mod->classes[c];
    +
    +        bufWriteStr(&b, cls->name, (int32_t)strlen(cls->name));
    +        bufWriteI32(&b, cls->varCount);
    +        bufWriteI32(&b, cls->initCodeAddr);
    +        bufWriteI32(&b, cls->terminateProc);
    +        bufWriteI32(&b, cls->memberCount);
    +
    +        for (int32_t i = 0; i < cls->memberCount; i++) {
    +            const BasClassMemberT *m = &cls->members[i];
    +
    +            bufWriteStr(&b, m->name, (int32_t)strlen(m->name));
    +            bufWriteI32(&b, m->index);
    +            bufWriteU8(&b, m->kind);
    +            bufWriteU8(&b, m->flags);
    +            bufWriteU8(&b, m->dataType);
    +            bufWriteU8(&b, m->requiredParams);
    +
    +            int32_t paramCount = (m->kind == BAS_MEMBER_VAR || m->index < 0 || m->index >= mod->procCount) ? 0 : DVX_MIN(mod->procs[m->index].paramCount, BAS_VM_MAX_CALL_ARGS);
    +
    +            bufWriteU8(&b, (uint8_t)paramCount);
    +            bufWrite(&b, m->paramTypes, paramCount);
    +        }
    +    }
    +
         if (b.err) {
             free(b.buf);
             return NULL;
    @@ -705,6 +815,27 @@ static void bufWriteU8(BufT *b, uint8_t v) {
     }
     
     
    +// A class module read from a file: its set-up code inside the module
    +// (-1: none), its procedures in the procedure table, member variables
    +// among its own and member kinds known.
    +static bool classRangesValid(const BasModuleT *mod, const BasClassInfoT *cls) {
    +    if (cls->varCount < 0 || cls->initCodeAddr < -1 || cls->initCodeAddr >= mod->codeLen || cls->terminateProc < -1 || cls->terminateProc >= mod->procCount) {
    +        return false;
    +    }
    +
    +    for (int32_t i = 0; i < cls->memberCount; i++) {
    +        const BasClassMemberT *m     = &cls->members[i];
    +        int32_t                limit = (m->kind == BAS_MEMBER_VAR) ? cls->varCount : mod->procCount;
    +
    +        if (m->kind > BAS_MEMBER_SET || m->index < 0 || m->index >= limit) {
    +            return false;
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
     // True when count is non-negative and count entries of at least
     // minBytesPerEntry bytes each still fit in the remaining input.  Uses
     // division, not multiplication, to avoid overflow.
    @@ -715,7 +846,7 @@ static bool countFits(ReaderT *r, int32_t count, int32_t minBytesPerEntry) {
     
     // Semantic checks on a module read from an untrusted file: every code
     // address and slot index must point inside the module, and every
    -// procedure's local count must fit a call frame.  entryPoint may equal
    +// procedure's local and parameter counts must fit a call frame.  entryPoint may equal
     // codeLen (an empty program halts at once); a negative initCodeAddr means
     // the form has no init code.
     static bool moduleRangesValid(const BasModuleT *mod) {
    @@ -724,7 +855,9 @@ static bool moduleRangesValid(const BasModuleT *mod) {
         }
     
         for (int32_t i = 0; i < mod->procCount; i++) {
    -        if (mod->procs[i].codeAddr < 0 || mod->procs[i].codeAddr >= mod->codeLen || mod->procs[i].localCount < 0 || mod->procs[i].localCount > BAS_VM_MAX_LOCALS) {
    +        const BasProcEntryT *p = &mod->procs[i];
    +
    +        if (p->codeAddr < 0 || p->codeAddr >= mod->codeLen || p->localCount < 0 || p->localCount > BAS_VM_MAX_LOCALS || p->paramCount < 0 || p->paramCount > BAS_VM_MAX_CALL_ARGS) {
                 return false;
             }
         }
    @@ -747,6 +880,12 @@ static bool moduleRangesValid(const BasModuleT *mod) {
             }
         }
     
    +    for (int32_t c = 0; c < mod->classCount; c++) {
    +        if (!classRangesValid(mod, &mod->classes[c])) {
    +            return false;
    +        }
    +    }
    +
         return true;
     }
     
    diff --git a/src/apps/kpunch/dvxbasic/runtime/values.c b/src/apps/kpunch/dvxbasic/runtime/values.c
    index 3a170054..0458cde9 100644
    --- a/src/apps/kpunch/dvxbasic/runtime/values.c
    +++ b/src/apps/kpunch/dvxbasic/runtime/values.c
    @@ -27,6 +27,8 @@
     // decrements, zero frees. No garbage collector needed.
     
     #include "values.h"
    +#include "basErrors.h"
    +#include "dates.h"
     #include "../compiler/opcodes.h"
     #include "dvxTypes.h"
     
    @@ -37,12 +39,13 @@
     #include 
     
     #define BAS_STRING_IMMORTAL_REFCOUNT 999999  // documents intent only: the empty string is never counted or freed (see basStringRef/Unref)
    -#define BAS_NUM_FORMAT_BUF_LEN       64      // scratch for the widest %g rendering of a double
    +#define BAS_NUM_FORMAT_BUF_LEN       64      // scratch for the widest %g rendering of a double or general date
     #define BAS_NUM_PARSE_BUF_LEN        64      // VAL: sign + mantissa digits + "e" + exponent
     #define BAS_NUM_MANTISSA_DIGITS      40      // VAL: significant digits kept (well past a double's 17)
     #define BAS_NUM_EXP_CLAMP            100000  // VAL: explicit exponents saturate here (far past a double's range)
     #define BAS_BOOL_TRUE_TEXT           "True"
     #define BAS_BOOL_FALSE_TEXT          "False"
    +#define BAS_NULL_TEXT                "Null"  // PRINT / STR of a Null
     #define BAS_SINGLE_SIG_DIGITS        7       // significant digits shown for SINGLE (VB3)
     #define BAS_DOUBLE_SIG_DIGITS        15      // significant digits shown for DOUBLE (VB3)
     #define BAS_RADIX_HEX                16
    @@ -62,13 +65,21 @@ BasStringT *basEmptyString = &sEmptyStringStorage.hdr;
     
     bool basStringAllocFailed = false;
     
    +bool basValNullUsed = false;
    +
     
     // Function prototypes (alphabetical)
     void basArrayFree(BasArrayT *arr);
     int32_t basArrayIndex(BasArrayT *arr, int32_t *indices, int32_t ndims);
     BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType);
    +BasArrayT *basArrayNewEmpty(uint8_t elementType);
     BasArrayT *basArrayRef(BasArrayT *arr);
     void basArrayUnref(BasArrayT *arr);
    +void basObjectClear(BasObjectT *obj);
    +static void basObjectFree(BasObjectT *obj);
    +BasObjectT *basObjectNew(int32_t classIdx, int32_t fieldCount, BasObjectT **liveList);
    +BasObjectT *basObjectRef(BasObjectT *obj);
    +void basObjectUnref(BasObjectT *obj);
     double basParseNumber(const char *s);
     BasStringT *basStringAlloc(int32_t cap);
     bool basStringAppend(BasStringT **dst, const BasStringT *b);
    @@ -89,11 +100,16 @@ int32_t basValCompare(BasValueT a, BasValueT b);
     int32_t basValCompareCI(BasValueT a, BasValueT b);
     static int32_t basValCompareImpl(BasValueT a, BasValueT b, bool caseInsensitive);
     BasValueT basValCopy(BasValueT v);
    +BasValueT basValDate(double serial);
     BasValueT basValDouble(double v);
    +BasValueT basValEmpty(void);
     BasStringT *basValFormatString(BasValueT v);
    +BasValueT basValInstance(BasObjectT *obj);
     BasValueT basValInteger(int16_t v);
    +bool basValIsObject(BasValueT v);
     bool basValIsTruthy(BasValueT v);
     BasValueT basValLong(int32_t v);
    +BasValueT basValNull(void);
     BasValueT basValObject(void *obj);
     void basValRelease(BasValueT *v);
     bool basValRoundToInt32(BasValueT v, int32_t lo, int32_t hi, int32_t *out);
    @@ -101,6 +117,7 @@ BasValueT basValSingle(float v);
     BasValueT basValString(BasStringT *s);
     BasValueT basValStringFromC(const char *text);
     BasValueT basValToBool(BasValueT v);
    +int32_t basValToDateSerial(BasValueT v, double *out);
     BasValueT basValToDouble(BasValueT v);
     int32_t basValToInt32(BasValueT v);
     BasValueT basValToInteger(BasValueT v);
    @@ -206,9 +223,13 @@ BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t
         // tag set; STRING elements additionally need a real (empty) string --
         // calloc leaves strVal NULL and the VM's string ops dereference it
         // unguarded, so an uninitialized string element would crash.
    +    // A VARIANT element starts Empty (VARIANT is a declared type, never
    +    // a value's tag).
    +    uint8_t tag = (elementType == BAS_TYPE_VARIANT) ? BAS_TYPE_EMPTY : elementType;
    +
         if (elementType != BAS_TYPE_INTEGER) {
             for (int32_t i = 0; i < arr->totalElements; i++) {
    -            arr->elements[i].type = elementType;
    +            arr->elements[i].type = tag;
     
                 if (elementType == BAS_TYPE_STRING) {
                     arr->elements[i].strVal = basStringRef(basEmptyString);
    @@ -220,6 +241,21 @@ BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t
     }
     
     
    +BasArrayT *basArrayNewEmpty(uint8_t elementType) {
    +    BasArrayT *arr = (BasArrayT *)calloc(1, sizeof(BasArrayT));
    +
    +    if (!arr) {
    +        return NULL;
    +    }
    +
    +    arr->refCount    = 1;
    +    arr->elementType = elementType;
    +    arr->dims        = 1;
    +    arr->ubound[0]   = -1;
    +    return arr;
    +}
    +
    +
     BasArrayT *basArrayRef(BasArrayT *arr) {
         if (arr) {
             arr->refCount++;
    @@ -242,6 +278,123 @@ void basArrayUnref(BasArrayT *arr) {
     }
     
     
    +void basObjectClear(BasObjectT *obj) {
    +    // Detach the storage first: releasing a member can run into this
    +    // object again (a cycle), which must then find nothing to release.
    +    BasValueT    *fields     = obj->fields;
    +    int32_t       fieldCount = obj->fieldCount;
    +    BasCollItemT *items      = obj->items;
    +    int32_t       itemCount  = obj->itemCount;
    +
    +    obj->fields     = NULL;
    +    obj->fieldCount = 0;
    +    obj->items      = NULL;
    +    obj->itemCount  = 0;
    +    obj->itemCap    = 0;
    +
    +    for (int32_t i = 0; i < fieldCount; i++) {
    +        basValRelease(&fields[i]);
    +    }
    +
    +    for (int32_t i = 0; i < itemCount; i++) {
    +        basStringUnref(items[i].key);
    +        basValRelease(&items[i].value);
    +    }
    +
    +    free(fields);
    +    free(items);
    +}
    +
    +
    +// Releases what the object holds, leaves its owner's live list and frees it.
    +static void basObjectFree(BasObjectT *obj) {
    +    basObjectClear(obj);
    +
    +    if (obj->liveList) {
    +        if (obj->livePrev) {
    +            obj->livePrev->liveNext = obj->liveNext;
    +        } else {
    +            *obj->liveList = obj->liveNext;
    +        }
    +
    +        if (obj->liveNext) {
    +            obj->liveNext->livePrev = obj->livePrev;
    +        }
    +    }
    +
    +    free(obj);
    +}
    +
    +
    +BasObjectT *basObjectNew(int32_t classIdx, int32_t fieldCount, BasObjectT **liveList) {
    +    if (fieldCount < 0) {
    +        return NULL;
    +    }
    +
    +    BasObjectT *obj = (BasObjectT *)calloc(1, sizeof(BasObjectT));
    +
    +    if (!obj) {
    +        return NULL;
    +    }
    +
    +    if (fieldCount > 0) {
    +        obj->fields = (BasValueT *)calloc(fieldCount, sizeof(BasValueT));
    +
    +        if (!obj->fields) {
    +            free(obj);
    +            return NULL;
    +        }
    +
    +        for (int32_t i = 0; i < fieldCount; i++) {
    +            obj->fields[i].type = BAS_TYPE_EMPTY;
    +        }
    +    }
    +
    +    obj->refCount   = 1;
    +    obj->classIdx   = classIdx;
    +    obj->fieldCount = fieldCount;
    +    obj->liveList   = liveList;
    +
    +    if (liveList) {
    +        obj->liveNext = *liveList;
    +
    +        if (*liveList) {
    +            (*liveList)->livePrev = obj;
    +        }
    +
    +        *liveList = obj;
    +    }
    +
    +    return obj;
    +}
    +
    +
    +BasObjectT *basObjectRef(BasObjectT *obj) {
    +    if (obj) {
    +        obj->refCount++;
    +    }
    +
    +    return obj;
    +}
    +
    +
    +void basObjectUnref(BasObjectT *obj) {
    +    if (!obj || --obj->refCount > 0) {
    +        return;
    +    }
    +
    +    // Class_Terminate runs first; the finalizer keeps the object alive
    +    // for it and frees it afterwards (it clears the hook, so the next
    +    // time the count reaches zero the object is freed).
    +    if (obj->finalize) {
    +        obj->finalize(obj->finalizeCtx, obj);
    +        return;
    +    }
    +
    +    basObjectFree(obj);
    +}
    +
    +
     // ============================================================
     // Number parsing (VAL)
     // ============================================================
    @@ -667,6 +820,15 @@ int32_t basValCompareCI(BasValueT a, BasValueT b) {
     
     
     static int32_t basValCompareImpl(BasValueT a, BasValueT b, bool caseInsensitive) {
    +    // Empty compares as "" with a string (as 0 with anything else)
    +    if (a.type == BAS_TYPE_EMPTY && b.type == BAS_TYPE_STRING) {
    +        a = basValString(NULL);
    +    }
    +
    +    if (b.type == BAS_TYPE_EMPTY && a.type == BAS_TYPE_STRING) {
    +        b = basValString(NULL);
    +    }
    +
         // String comparison
         if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
             BasStringT *sa = a.strVal ? a.strVal : basEmptyString;
    @@ -674,10 +836,25 @@ static int32_t basValCompareImpl(BasValueT a, BasValueT b, bool caseInsensitive)
             return caseInsensitive ? basStringCompareCI(sa, sb) : basStringCompare(sa, sb);
         }
     
    -    // Numeric comparison
    +    // Numeric comparison.  Against a DATE both sides compare as instants
    +    // (a string holding a date as that date), so days before 12/30/1899
    +    // order by time of day too.
         double na = basValToNumber(a);
         double nb = basValToNumber(b);
     
    +    if (a.type == BAS_TYPE_DATE || b.type == BAS_TYPE_DATE) {
    +        if (a.type == BAS_TYPE_STRING) {
    +            basValToDateSerial(a, &na);
    +        }
    +
    +        if (b.type == BAS_TYPE_STRING) {
    +            basValToDateSerial(b, &nb);
    +        }
    +
    +        na = basDateToLinear(na);
    +        nb = basDateToLinear(nb);
    +    }
    +
         if (na < nb) {
             return -1;
         }
    @@ -699,12 +876,22 @@ BasValueT basValCopy(BasValueT v) {
             basUdtRef(v.udtVal);
         } else if (v.type == BAS_TYPE_ELEM_REF) {
             basArrayRef(v.elemRef.arr);
    +    } else if (v.type == BAS_TYPE_INSTANCE) {
    +        basObjectRef(v.instVal);
         }
     
         return v;
     }
     
     
    +BasValueT basValDate(double serial) {
    +    BasValueT val;
    +    val.type   = BAS_TYPE_DATE;
    +    val.dblVal = serial;
    +    return val;
    +}
    +
    +
     BasValueT basValDouble(double v) {
         BasValueT val;
         val.type   = BAS_TYPE_DOUBLE;
    @@ -713,6 +900,15 @@ BasValueT basValDouble(double v) {
     }
     
     
    +BasValueT basValEmpty(void) {
    +    BasValueT val;
    +
    +    memset(&val, 0, sizeof(val));
    +    val.type = BAS_TYPE_EMPTY;
    +    return val;
    +}
    +
    +
     BasStringT *basValFormatString(BasValueT v) {
         char buf[BAS_NUM_FORMAT_BUF_LEN];
     
    @@ -729,6 +925,9 @@ BasStringT *basValFormatString(BasValueT v) {
             case BAS_TYPE_DOUBLE:
                 return basStringNew(buf, formatDouble(v.dblVal, BAS_DOUBLE_SIG_DIGITS, buf, sizeof(buf)));
     
    +        case BAS_TYPE_DATE:
    +            return basStringNew(buf, basDateFormat(v.dblVal, NULL, buf, sizeof(buf)));
    +
             case BAS_TYPE_BOOLEAN:
                 if (v.boolVal) {
                     return basStringNew(BAS_BOOL_TRUE_TEXT, (int32_t)sizeof(BAS_BOOL_TRUE_TEXT) - 1);
    @@ -739,12 +938,23 @@ BasStringT *basValFormatString(BasValueT v) {
             case BAS_TYPE_STRING:
                 return basStringRef(v.strVal ? v.strVal : basEmptyString);
     
    +        case BAS_TYPE_NULL:
    +            return basStringNew(BAS_NULL_TEXT, (int32_t)sizeof(BAS_NULL_TEXT) - 1);
    +
             default:
                 return basStringRef(basEmptyString);
         }
     }
     
     
    +BasValueT basValInstance(BasObjectT *obj) {
    +    BasValueT val;
    +    val.type    = BAS_TYPE_INSTANCE;
    +    val.instVal = obj;
    +    return val;
    +}
    +
    +
     BasValueT basValInteger(int16_t v) {
         BasValueT val;
         val.type   = BAS_TYPE_INTEGER;
    @@ -753,6 +963,11 @@ BasValueT basValInteger(int16_t v) {
     }
     
     
    +bool basValIsObject(BasValueT v) {
    +    return v.type == BAS_TYPE_OBJECT || v.type == BAS_TYPE_INSTANCE;
    +}
    +
    +
     bool basValIsTruthy(BasValueT v) {
         switch (v.type) {
             case BAS_TYPE_INTEGER:
    @@ -765,6 +980,7 @@ bool basValIsTruthy(BasValueT v) {
                 return v.sngVal != 0.0f;
     
             case BAS_TYPE_DOUBLE:
    +        case BAS_TYPE_DATE:
                 return v.dblVal != 0.0;
     
             case BAS_TYPE_BOOLEAN:
    @@ -787,6 +1003,15 @@ BasValueT basValLong(int32_t v) {
     }
     
     
    +BasValueT basValNull(void) {
    +    BasValueT val;
    +
    +    memset(&val, 0, sizeof(val));
    +    val.type = BAS_TYPE_NULL;
    +    return val;
    +}
    +
    +
     BasValueT basValObject(void *obj) {
         BasValueT val;
         val.type   = BAS_TYPE_OBJECT;
    @@ -795,6 +1020,31 @@ BasValueT basValObject(void *obj) {
     }
     
     
    +// PRINT / PRINT # text for one value: its string form, followed by the
    +// space PRINT puts after every non-string value.
    +BasStringT *basValPrintString(BasValueT val) {
    +    BasStringT *text = basValFormatString(val);
    +
    +    // Strings, dates, Empty and Null print as they are; numbers take a
    +    // trailing space
    +    if (val.type == BAS_TYPE_STRING || val.type == BAS_TYPE_DATE || val.type == BAS_TYPE_EMPTY || val.type == BAS_TYPE_NULL) {
    +        return text;
    +    }
    +
    +    BasStringT *out = basStringAlloc(text->len + 2);
    +
    +    if (out->cap >= text->len + 2) {
    +        memcpy(out->data, text->data, text->len);
    +        out->data[text->len]     = ' ';
    +        out->data[text->len + 1] = '\0';
    +        out->len                 = text->len + 1;
    +    }
    +
    +    basStringUnref(text);
    +    return out;
    +}
    +
    +
     void basValRelease(BasValueT *v) {
         if (v->type == BAS_TYPE_STRING) {
             basStringUnref(v->strVal);
    @@ -808,6 +1058,13 @@ void basValRelease(BasValueT *v) {
         } else if (v->type == BAS_TYPE_ELEM_REF) {
             basArrayUnref(v->elemRef.arr);
             v->elemRef.arr = NULL;
    +    } else if (v->type == BAS_TYPE_INSTANCE) {
    +        BasObjectT *obj = v->instVal;
    +
    +        // Clear the slot before the object can run code (Class_Terminate)
    +        // or free what points back here.
    +        v->instVal = NULL;
    +        basObjectUnref(obj);
         }
     }
     
    @@ -864,6 +1121,42 @@ BasValueT basValToBool(BasValueT v) {
     }
     
     
    +int32_t basValToDateSerial(BasValueT v, double *out) {
    +    double serial;
    +
    +    switch (v.type) {
    +        case BAS_TYPE_STRING:
    +            if (!basDateParse(v.strVal ? v.strVal->data : "", &serial)) {
    +                return BAS_ERR_TYPE_MISMATCH;
    +            }
    +            break;
    +
    +        case BAS_TYPE_INTEGER:
    +        case BAS_TYPE_LONG:
    +        case BAS_TYPE_SINGLE:
    +        case BAS_TYPE_DOUBLE:
    +        case BAS_TYPE_BOOLEAN:
    +        case BAS_TYPE_DATE:
    +        case BAS_TYPE_EMPTY:
    +            serial = basValToNumber(v);
    +            break;
    +
    +        case BAS_TYPE_NULL:
    +            return BAS_ERR_INVALID_NULL;
    +
    +        default:
    +            return BAS_ERR_TYPE_MISMATCH;
    +    }
    +
    +    if (!basDateIsValid(serial)) {
    +        return BAS_ERR_OVERFLOW;
    +    }
    +
    +    *out = serial;
    +    return 0;
    +}
    +
    +
     BasValueT basValToDouble(BasValueT v) {
         return basValDouble(basValToNumber(v));
     }
    @@ -923,6 +1216,7 @@ double basValToNumber(BasValueT v) {
                 return (double)v.sngVal;
     
             case BAS_TYPE_DOUBLE:
    +        case BAS_TYPE_DATE:
                 return v.dblVal;
     
             case BAS_TYPE_BOOLEAN:
    @@ -935,6 +1229,10 @@ double basValToNumber(BasValueT v) {
     
                 return 0.0;
     
    +        case BAS_TYPE_NULL:
    +            basValNullUsed = true;
    +            return 0.0;
    +
             default:
                 return 0.0;
         }
    @@ -958,6 +1256,13 @@ BasValueT basValToString(BasValueT v) {
             return basValCopy(v);
         }
     
    +    // Null has no text to convert (PRINT shows "Null" through
    +    // basValFormatString; CSTR(Null) is an error).
    +    if (v.type == BAS_TYPE_NULL) {
    +        basValNullUsed = true;
    +        return basValString(NULL);
    +    }
    +
         BasValueT result;
         result.type   = BAS_TYPE_STRING;
         result.strVal = basValFormatString(v);
    diff --git a/src/apps/kpunch/dvxbasic/runtime/values.h b/src/apps/kpunch/dvxbasic/runtime/values.h
    index 17e898a5..f75a60c3 100644
    --- a/src/apps/kpunch/dvxbasic/runtime/values.h
    +++ b/src/apps/kpunch/dvxbasic/runtime/values.h
    @@ -114,6 +114,11 @@ typedef struct {
     // Allocate a new array. refCount starts at 1.
     BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType);
     
    +// Allocate a one-dimensional array with no elements (LBOUND 0, UBOUND -1),
    +// what SPLIT and FILTER return when there is nothing to hold.  refCount
    +// starts at 1; NULL when out of memory.
    +BasArrayT *basArrayNewEmpty(uint8_t elementType);
    +
     // Free all elements and release the array.
     void basArrayFree(BasArrayT *arr);
     
    @@ -155,6 +160,35 @@ void basUdtUnref(BasUdtT *udt);
     // value semantics on assignment and BYVAL parameter passing.
     BasUdtT *basUdtClone(const BasUdtT *udt);
     
    +// ============================================================
    +// Reference-counted object (class instance or Collection)
    +// ============================================================
    +//
    +// One structure for every object the VM makes itself: an instance of a
    +// class module (member variables in fields) or a Collection (items).
    +// Forms and controls are host objects (BAS_TYPE_OBJECT) and never use it.
    +
    +typedef struct BasObjectS BasObjectT;
    +
    +// Called once when the last reference to an object whose class has a
    +// Class_Terminate goes away.  The callee takes over that (now zero) last
    +// reference: it must bring refCount back to 1 before keeping the object.
    +typedef void (*BasObjFinalizeFnT)(void *ctx, BasObjectT *obj);
    +
    +// classIdx of a Collection (user classes index the module's class table).
    +#define BAS_OBJ_COLLECTION (-1)
    +
    +// Increment reference count.
    +BasObjectT *basObjectRef(BasObjectT *obj);
    +
    +// Decrement reference count.  At zero the object is handed to its
    +// finalizer when it has one, else freed (members and items released).
    +void basObjectUnref(BasObjectT *obj);
    +
    +// Release every member variable and item (the object stays allocated,
    +// empty).  Used to break reference cycles when a program ends.
    +void basObjectClear(BasObjectT *obj);
    +
     // ============================================================
     // Tagged value
     // ============================================================
    @@ -165,12 +199,13 @@ struct BasValueTag {
             int16_t     intVal;    // BAS_TYPE_INTEGER
             int32_t     longVal;   // BAS_TYPE_LONG
             float       sngVal;    // BAS_TYPE_SINGLE
    -        double      dblVal;    // BAS_TYPE_DOUBLE
    +        double      dblVal;    // BAS_TYPE_DOUBLE, BAS_TYPE_DATE (serial, see dates.h)
             BasStringT *strVal;    // BAS_TYPE_STRING (ref-counted)
             int16_t     boolVal;   // BAS_TYPE_BOOLEAN (True=-1, False=0)
             BasArrayT  *arrVal;    // BAS_TYPE_ARRAY (ref-counted)
             BasUdtT    *udtVal;    // BAS_TYPE_UDT (ref-counted)
    -        void       *objVal;    // BAS_TYPE_OBJECT (opaque host pointer)
    +        void       *objVal;    // BAS_TYPE_OBJECT (opaque host pointer, NULL = Nothing)
    +        BasObjectT *instVal;   // BAS_TYPE_INSTANCE (ref-counted class instance or Collection)
             BasValueT  *refVal;    // BAS_TYPE_REF (ByRef pointer to variable slot)
             struct {
                 BasArrayT *arr;    // BAS_TYPE_ELEM_REF: array holding the element (a counted reference)
    @@ -179,17 +214,59 @@ struct BasValueTag {
         };
     };
     
    +// One Collection item: an optional key (NULL = none) and the value.
    +typedef struct {
    +    BasStringT *key;
    +    BasValueT   value;
    +} BasCollItemT;
    +
    +struct BasObjectS {
    +    int32_t            refCount;
    +    int32_t            classIdx;     // BAS_OBJ_COLLECTION, else index into the module's class table
    +    int32_t            fieldCount;
    +    int32_t            itemCount;
    +    int32_t            itemCap;
    +    BasValueT         *fields;       // class instance: member variables
    +    BasCollItemT      *items;        // Collection: items in order
    +    BasObjFinalizeFnT  finalize;     // Class_Terminate hook (NULL = free at once)
    +    void              *finalizeCtx;
    +    BasObjectT       **liveList;     // head of the owner's list of live objects (NULL = none)
    +    BasObjectT        *livePrev;
    +    BasObjectT        *liveNext;
    +    BasObjectT        *termNext;     // next object waiting for Class_Terminate (owner's queue)
    +};
    +
    +// Allocate an object with fieldCount member variables (each EMPTY until
    +// the class's initialisation code runs).  refCount starts at 1; it joins
    +// *liveList when that is non-NULL.  NULL when out of memory.
    +BasObjectT *basObjectNew(int32_t classIdx, int32_t fieldCount, BasObjectT **liveList);
    +
     // Create values.  Out-of-line so they can be resolved via the DXE
     // dynamic-symbol table when basrt.lib is loaded.
     BasValueT basValBool(bool v);
    +BasValueT basValDate(double serial);
     BasValueT basValDouble(double v);
    +BasValueT basValEmpty(void);
     BasValueT basValInteger(int16_t v);
     BasValueT basValLong(int32_t v);
    +BasValueT basValNull(void);
     BasValueT basValObject(void *obj);
     BasValueT basValSingle(float v);
     BasValueT basValString(BasStringT *s);
     BasValueT basValStringFromC(const char *text);
     
    +// Wraps an object the caller holds a reference to (the reference moves
    +// into the value; no extra count).
    +BasValueT basValInstance(BasObjectT *obj);
    +
    +// True for an object value: a host object (Nothing included) or a class
    +// instance / Collection.
    +bool basValIsObject(BasValueT v);
    +
    +// Set when a Null was converted to a number or a string (Invalid use of
    +// Null).  The VM checks and clears it after every instruction.
    +extern bool basValNullUsed;
    +
     // Copy a value (increments string/array/udt refcount if applicable).
     BasValueT basValCopy(BasValueT v);
     
    @@ -205,6 +282,12 @@ BasValueT basValToLong(BasValueT v);
     BasValueT basValToSingle(BasValueT v);
     BasValueT basValToString(BasValueT v);
     
    +// DATE serial of a value: a DATE as it is, a number (or BOOLEAN) as a
    +// serial, a string parsed as date text.  Returns 0 and sets *out, else
    +// the BASIC error number: Type mismatch for anything else (a string that
    +// is not a date included), Overflow outside the DATE range.
    +int32_t basValToDateSerial(BasValueT v, double *out);
    +
     // Get the numeric value as a double (for mixed-type arithmetic).
     double basValToNumber(BasValueT v);
     
    @@ -226,11 +309,15 @@ double basParseNumber(const char *s);
     // Get the string representation. Returns a new ref-counted string.
     BasStringT *basValFormatString(BasValueT v);
     
    +// PRINT's text for a value: its string form, numbers followed by a space.
    +BasStringT *basValPrintString(BasValueT val);
    +
     // Check if a value is truthy (non-zero number, non-empty string).
     bool basValIsTruthy(BasValueT v);
     
     // Compare two values. Returns -1, 0, or 1.
     // Numeric types are compared numerically. Strings lexicographically.
    +// A DATE compared with a string compares with the date the string holds.
     int32_t basValCompare(BasValueT a, BasValueT b);
     
     // Compare two values case-insensitively (for OPTION COMPARE TEXT).
    diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c
    index 3e877f5d..2b993173 100644
    --- a/src/apps/kpunch/dvxbasic/runtime/vm.c
    +++ b/src/apps/kpunch/dvxbasic/runtime/vm.c
    @@ -31,8 +31,10 @@
     #include "vm.h"
     #include "../compiler/opcodes.h"
     #include "basErrors.h"
    +#include "dates.h"
     #include "dvxPlat.h"
     #include "dvxPrefs.h"
    +#include "screen.h"
     
     #include 
     #include 
    @@ -68,7 +70,6 @@
     
     // Scratch buffer capacities used by individual opcode handlers.
     #define BAS_INPUT_PROMPT_LEN     512   // OP_INPUT: user prompt + "? "
    -#define BAS_INPUT_LINE_LEN       1024  // OP_INPUT: longest line the host input callback may return
     #define BAS_FILE_LINE_CHUNK      1024  // INPUT #/LINE INPUT #: fgets chunk while growing a line
     #define BAS_RADIX_STR_LEN        16    // HEX$/OCT$ output ("FFFFFFFF", "37777777777")
     #define BAS_TIME_STR_LEN         16    // TIME$ "HH:MM:SS"
    @@ -77,7 +78,6 @@
     #define BAS_FORMAT_PERCENT       "PERCENT"   // FORMAT$ named format
     #define BAS_FORMAT_OVERFLOW_MARK '%'         // PRINT USING: value wider than its field
     #define BAS_USING_ESCAPE         '_'         // PRINT USING: next format character is literal
    -#define BAS_PRINT_ZONE_WIDTH     14          // PRINT comma: width of one print zone
     #define BAS_PRINTER_NEWLINE      "\r\n"      // line end sent to a printer
     #define BAS_PRINTER_NEWLINE_LEN  2
     #define BAS_PRINTER_NAME_PRN     "PRN"       // the printer in the settings
    @@ -108,6 +108,17 @@
     #define BAS_SECS_PER_HOUR        3600.0
     #define BAS_SECS_PER_MINUTE      60.0
     #define BAS_USEC_PER_SEC         1000000.0
    +#define BAS_SECS_PER_DAY         86400.0
    +
    +// struct tm counts years from 1900.
    +#define BAS_TM_YEAR_BASE         1900
    +
    +// OP_BUILTIN: fields of DATESERIAL / TIMESERIAL, the "all" value of
    +// counts and start positions (REPLACE$ count, SPLIT limit, INSTRREV
    +// start) and the delimiter SPLIT and JOIN$ use when none is given.
    +#define BAS_SERIAL_PARTS         3
    +#define BAS_COUNT_ALL            (-1)
    +#define BAS_DEFAULT_DELIMITER    " "
     
     // SLEEP: pause between event pumps while waiting (microseconds).
     #define BAS_SLEEP_POLL_USEC      10000
    @@ -132,6 +143,44 @@
     #define BAS_RGB_CHANNEL_MASK     0xFF
     #define BAS_CHAR_MASK            0xFF
     
    +// Accesses of an object member: a read, or an assignment made with Let
    +// (obj.p = v) or Set (Set obj.p = v).
    +#define BAS_MEMBER_OP_GET        0
    +#define BAS_MEMBER_OP_LET        1
    +#define BAS_MEMBER_OP_SET        2
    +
    +// findMember: rank of a member that cannot serve the access.
    +#define BAS_MEMBER_NO_RANK       2
    +
    +// Collection members, the most arguments Add takes (item, key, before,
    +// after), and how its item array grows.
    +#define BAS_COLL_ADD             "Add"
    +#define BAS_COLL_COUNT           "Count"
    +#define BAS_COLL_ITEM            "Item"
    +#define BAS_COLL_REMOVE          "Remove"
    +#define BAS_COLL_ADD_ARGS        4
    +#define BAS_COLL_MIN_CAP         4
    +#define BAS_COLL_GROWTH          2
    +
    +// TYPENAME / TYPEOF names that are not class or control names, and the
    +// suffix of an array's type name ("Integer()").
    +#define BAS_TYPENAME_COLLECTION  "Collection"
    +#define BAS_TYPENAME_CONTROL     "Control"
    +#define BAS_TYPENAME_FORM        "Form"
    +#define BAS_TYPENAME_NOTHING     "Nothing"
    +#define BAS_TYPENAME_OBJECT      "Object"
    +#define BAS_TYPENAME_UDT         "UserType"
    +#define BAS_ARRAY_SUFFIX         "()"
    +#define BAS_ARRAY_SUFFIX_LEN     3
    +
    +// ISNUMERIC: the characters that start an exponent, and the length of
    +// the &H / &O prefix.
    +#define BAS_EXPONENT_CHARS       "eEdD"
    +#define BAS_RADIX_PREFIX_LEN     2
    +
    +// BASIC's True as a bit pattern (every bit set).
    +#define BAS_TRUE_BITS            (-1)
    +
     // Divergence between the PRINT USING and FORMAT$ numeric formatters,
     // captured as options so a single helper serves both call sites.
     typedef struct BasNumFormatT {
    @@ -164,15 +213,28 @@ typedef struct BasPutSinkT {
     } BasPutSinkT;
     
     static BasVmResultE   allocArray(BasVmT *vm, int32_t dims, uint8_t elementType, bool preserve, BasArrayT **outArr);
    +static int32_t        argCompare(BasVmT *vm, BasValueT v, bool *outText);
    +static BasStringT    *argString(BasValueT *arg);
     static void           arrayClear(BasArrayT *arr);
    +static BasValueT      arrValue(BasArrayT *arr);
     static BasVmResultE   basVmStepExec(BasVmT *vm);
     static BasVmResultE   boundOp(BasVmT *vm, uint8_t op);
    +static int32_t        builtinDate(uint8_t fn, BasValueT *args, BasValueT *out);
    +static int32_t        builtinDefaults(uint8_t fn, BasValueT *args, int32_t argc);
    +static int32_t        builtinObject(BasVmT *vm, uint8_t fn, BasValueT *args, BasValueT *out);
    +static int32_t        builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, BasValueT *out);
    +static BasVmResultE   callMember(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, bool wantResult);
     static int32_t        clampComponent(int32_t c);
     static void           clearErrorState(BasVmT *vm);
     static bool           closeChannel(BasVmT *vm, int32_t channel);
    +static BasVmResultE   collectionAccess(BasVmT *vm, BasObjectT *coll, const char *name, BasValueT *args, int32_t argc, int32_t op);
    +static int32_t        collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc);
    +static int32_t        collectionFind(const BasObjectT *coll, BasValueT index, int32_t *outPos);
    +static int32_t        collectionKeyPos(const BasObjectT *coll, const BasStringT *key);
     static BasStringT    *commandArg(const char *args, int32_t n);
     static BasCallFrameT *currentFrame(BasVmT *vm);
     static void           defaultPrint(void *ctx, const char *text, bool newline);
    +static void           derefArg(BasValueT *arg);
     static BasValueT     *derefSlot(BasValueT *slot);
     static void           dirClose(BasVmT *vm);
     static const char    *dirNext(BasVmT *vm);
    @@ -180,10 +242,16 @@ static void           discardFrame(BasVmT *vm);
     static bool           dispatchError(BasVmT *vm, BasVmResultE result, int32_t floorDepth, int32_t faultPc);
     static BasVmResultE   divByZero(BasVmT *vm);
     static int64_t        divMod32(uint8_t op, int32_t ia, int32_t ib);
    +static void           dropArgs(BasValueT *args, int32_t count);
     static void           dropValues(BasVmT *vm, int32_t count);
    +static int32_t        eachCount(BasValueT group, int32_t *outCount);
    +static void           emptyOperand(BasValueT *v, BasValueT other);
     static const char    *errorText(int32_t errNum);
     static BasVmResultE   execArith(BasVmT *vm, uint8_t op);
    +static BasVmResultE   execBuiltin(BasVmT *vm);
    +static BasVmResultE   execCall(BasVmT *vm, bool keepMe);
     static BasVmResultE   execCompare(BasVmT *vm, uint8_t op);
    +static BasVmResultE   execConvert(BasVmT *vm, uint8_t op);
     static BasVmResultE   execDevice(BasVmT *vm, uint8_t op);
     static BasVmResultE   execFileOp(BasVmT *vm, uint8_t op);
     static BasVmResultE   execFsOp(BasVmT *vm, uint8_t op);
    @@ -194,10 +262,13 @@ static BasVmResultE   execPrint(BasVmT *vm);
     static BasVmResultE   execStrAppend(BasVmT *vm);
     static BasVmResultE   execStringOp(BasVmT *vm, uint8_t op);
     static BasStringT    *fillString(char ch, int32_t count);
    +static int32_t        filterScan(const BasArrayT *arr, const BasStringT *match, bool keep, bool ignoreCase, BasArrayT *out);
    +static const BasClassMemberT *findMember(const BasClassInfoT *cls, const char *name, int32_t op);
     static BasValueT      forCounterValue(uint8_t varType, bool stepFractional, double v);
     static void           formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize);
     static int32_t        forStackFloor(BasVmT *vm);
     static void           forStackTrim(BasVmT *vm, int32_t newDepth);
    +static int32_t        indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, int32_t *outIdx);
     static BasValueT      iniRead(const char *path, const char *sec, const char *key, BasStringT *defStr);
     static BasVmResultE   iniWrite(BasVmT *vm, const char *path, const char *sec, const char *key, const char *val);
     static BasValueT      integralValue(double v);
    @@ -209,14 +280,30 @@ static BasValueT      ioReadScalar(FILE *fp, uint8_t type);
     static void           ioWritePrefixedString(BasPutSinkT *sink, const BasStringT *s);
     static void           ioWriteScalar(BasPutSinkT *sink, uint8_t type, BasValueT val);
     static bool           isIntegerType(uint8_t type);
    +static bool           isOneDimArray(BasValueT v);
    +static int32_t        joinScan(const BasArrayT *arr, const BasStringT *delim, char *out);
     static bool           layoutNext(const BasStringT *layout, int32_t *pos, BasLayoutEntryT *out);
     static void           localNow(struct tm *out, int32_t *outUsec);
    +static BasVmResultE   logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b);
     static void           lprintWrite(BasVmT *vm, const char *data, int32_t len);
     static BasVmResultE   mathRangeError(BasVmT *vm, double result);
    +static int32_t        memberRank(uint8_t kind, int32_t op);
    +static BasValueT     *memberSlot(BasVmT *vm, uint16_t idx);
    +static BasVmResultE   memberVarAccess(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, BasValueT *value, int32_t op);
    +static BasVmResultE   newObject(BasVmT *vm, uint16_t cls);
    +static BasArrayT     *newVector(int32_t count, uint8_t elementType);
     static int32_t        nextStatementPc(BasVmT *vm, int32_t start, bool includeStart);
     static BasVmResultE   noFormRuntime(BasVmT *vm);
    +static bool           nullOperands(BasVmT *vm, BasValueT *a, BasValueT *b, BasVmResultE *rc);
    +static BasVmResultE   objectAccess(BasVmT *vm, BasObjectT *obj, const char *name, BasValueT *args, int32_t argc, BasValueT *value, int32_t op);
    +static int32_t        objectCheck(BasValueT v, uint16_t cls);
    +static const BasClassInfoT *objectClass(const BasVmT *vm, const BasObjectT *obj);
    +static BasVmResultE   objectDefault(BasVmT *vm, int32_t argc, bool isStore);
    +static BasVmResultE   objectMemberOp(BasVmT *vm, BasValueT objVal, BasValueT nameVal, BasValueT *args, int32_t argc, BasValueT *value, int32_t op);
    +static const char    *objectTypeName(const BasVmT *vm, BasValueT v);
     static BasVmResultE   openPrinterChannel(BasVmT *vm, int32_t channel, int32_t port, uint8_t mode, int32_t recLen);
     static bool           operandFits(BasVmT *vm, int32_t size);
    +static void           padArg(BasValueT *args, int32_t *argc, int32_t idx, BasValueT value);
     static void           parseNumFormat(const char *fmt, int32_t fmtLen, BasNumFormatT *f, bool *outSci);
     static bool           pop(BasVmT *vm, BasValueT *val);
     static bool           popArgs(BasVmT *vm, int32_t count, ...);
    @@ -228,13 +315,14 @@ static bool           popStringArg(BasVmT *vm, BasValueT *out);
     static void           primeModuleFrame(BasVmT *vm);
     static int32_t       *printColumnOf(BasVmT *vm);
     static bool           printerDevice(const char *name, int32_t *outPort);
    -static BasStringT    *printString(BasValueT val);
     static void           printText(BasVmT *vm, const char *text, bool newline);
     static int32_t        procLocalCount(const BasVmT *vm, int32_t codeAddr, int32_t minCount);
     static bool           push(BasVmT *vm, BasValueT val);
     static BasCallFrameT *pushFrame(BasVmT *vm, int32_t returnPc, int32_t codeAddr, int32_t minLocals);
     static void           putBounded(char *out, int32_t *idx, int32_t limit, char c);
     static BasVmResultE   putValue(BasVmT *vm, BasPutSinkT *sink, int32_t mode, BasValueT val, const BasStringT *layout);
    +static void           queueTerminate(void *ctx, BasObjectT *obj);
    +static BasVmResultE   raiseError(BasVmT *vm, int32_t errNum);
     static BasStringT    *readField(FILE *fp);
     static int16_t        readInt16(BasVmT *vm);
     static BasStringT    *readLine(FILE *fp);
    @@ -244,6 +332,7 @@ static uint8_t        readUint8(BasVmT *vm);
     static BasVmResultE   recordBegin(BasVmT *vm, int32_t channel, int32_t recno, long *start);
     static BasVmResultE   recordEnd(BasVmT *vm, int32_t channel, long start, bool writing);
     static void           releaseVmState(BasVmT *vm);
    +static int32_t        replaceScan(const BasStringT *s, int32_t start, const BasStringT *find, const BasStringT *repl, int32_t count, bool ignoreCase, char *out);
     static BasVmResultE   roundedOperand(BasVmT *vm, BasValueT v, int32_t *out);
     static bool           runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning);
     static void           runtimeError(BasVmT *vm, int32_t errNum, const char *msg);
    @@ -254,10 +343,17 @@ static bool           sendSpool(BasVmT *vm, BasFileChannelT *ch);
     static void           sinkFill(BasPutSinkT *sink, char c, int32_t count);
     static void           sinkWrite(BasPutSinkT *sink, const void *data, int32_t len);
     static bool           sleepSeconds(BasVmT *vm, int32_t secs);
    +static int32_t        splitScan(const BasStringT *s, const BasStringT *delim, int32_t limit, bool ignoreCase, BasArrayT *out);
    +static BasVmResultE   startTerminate(BasVmT *vm);
    +static BasVmResultE   storeConvert(BasVmT *vm, uint8_t dataType, BasValueT *val);
     static bool           storeElement(BasVmT *vm, BasValueT *target, BasValueT val);
     static void           storeValue(BasValueT *target, BasValueT val);
     static int32_t        stringFind(const BasStringT *haystack, int32_t start, const BasStringT *needle, bool ignoreCase);
    +static bool           stringMatchAt(const BasStringT *haystack, int32_t i, const BasStringT *needle, bool ignoreCase);
     static BasValueT      strValue(BasStringT *s);
    +static void           sweepObjects(BasVmT *vm);
    +static bool           textIsNumeric(const BasStringT *s);
    +static const char    *typeNameOf(const BasVmT *vm, uint8_t type, int32_t typeId);
     static BasValueT      udtDetach(BasValueT val);
     static BasUdtT       *udtFromLayout(BasVmT *vm, const BasStringT *layout, int32_t *pos, int32_t typeId, int32_t fieldCount, int32_t depth);
     static BasVmResultE   udtGet(BasVmT *vm, FILE *fp, BasUdtT *udt, const BasStringT *layout, int32_t *pos, int32_t depth);
    @@ -266,6 +362,7 @@ static BasVmResultE   uiTarget(BasVmT *vm, bool hasCallback, BasValueT target);
     static int32_t        usingFieldLen(const char *fmt, int32_t fmtLen, int32_t pos);
     static BasStringT    *usingFormat(BasVmT *vm, const BasStringT *fmt, BasValueT val, bool last);
     static bool           validArrayDims(BasVmT *vm, int32_t dims);
    +static int32_t        varTypeOf(uint8_t type);
     static bool           waitWhile(BasVmT *vm, bool (*busy)(void *ctx));
     static double         wallSeconds(void);
     static BasValueT      zeroValue(uint8_t type);
    @@ -390,6 +487,38 @@ static BasVmResultE allocArray(BasVmT *vm, int32_t dims, uint8_t elementType, bo
     }
     
     
    +// Resolves an optional compare argument of the string functions
    +// (vbBinaryCompare, vbTextCompare or vbUseCompareOption).  Returns 0 and
    +// sets *outText, else the BASIC error number.
    +static int32_t argCompare(BasVmT *vm, BasValueT v, bool *outText) {
    +    int32_t mode = basValToInt32(v);
    +
    +    if (mode == BAS_COMPARE_USE_OPTION) {
    +        *outText = vm->compareTextMode;
    +    } else if (mode == BAS_COMPARE_MODE_BINARY || mode == BAS_COMPARE_MODE_TEXT) {
    +        *outText = (mode == BAS_COMPARE_MODE_TEXT);
    +    } else {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    return 0;
    +}
    +
    +
    +// The string an OP_BUILTIN argument holds, converting the argument in
    +// place (the caller releases every argument afterwards).
    +static BasStringT *argString(BasValueT *arg) {
    +    if (arg->type != BAS_TYPE_STRING || !arg->strVal) {
    +        BasValueT sv = basValToString(*arg);
    +
    +        basValRelease(arg);
    +        *arg = sv;
    +    }
    +
    +    return arg->strVal;
    +}
    +
    +
     // ERASE on a fixed array: every element goes back to its declared zero
     // value (TYPE elements keep their instance and have each field reset).
     static void arrayClear(BasArrayT *arr) {
    @@ -415,10 +544,19 @@ static void arrayClear(BasArrayT *arr) {
     }
     
     
    +// Wraps an array the caller already owns a reference to (no extra ref).
    +static BasValueT arrValue(BasArrayT *arr) {
    +    BasValueT v;
    +    v.type   = BAS_TYPE_ARRAY;
    +    v.arrVal = arr;
    +    return v;
    +}
    +
    +
     bool basVmCallSub(BasVmT *vm, int32_t codeAddr) {
         // Thin wrapper: no args, no out-args.  basVmCallSubWithArgsOut handles
         // NULL args / zero counts and contains the full validation + frame setup.
    -    return basVmCallSubWithArgsOut(vm, codeAddr, NULL, 0, NULL, 0);
    +    return basVmCallSubWithArgsOut(vm, codeAddr, NULL, 0, NULL, BAS_MEMBER_OP_GET);
     }
     
     
    @@ -502,9 +640,23 @@ void basVmDestroy(BasVmT *vm) {
             return;
         }
     
    +    // No Class_Terminate once the program is being torn down
    +    for (BasObjectT *o = vm->liveObjects; o; o = o->liveNext) {
    +        o->finalize = NULL;
    +    }
    +
         // Release eval stack, globals, call-frame locals and FOR-stack values
         releaseVmState(vm);
     
    +    while (vm->terminateQueue) {
    +        BasObjectT *obj = vm->terminateQueue;
    +
    +        vm->terminateQueue = obj->termNext;
    +        basObjectUnref(obj);
    +    }
    +
    +    sweepObjects(vm);
    +
         for (int32_t i = 0; i < BAS_VM_MAX_FILES; i++) {
             closeChannel(vm, i);
         }
    @@ -514,12 +666,24 @@ void basVmDestroy(BasVmT *vm) {
         }
     
         dirClose(vm);
    +    basScreenDestroy(vm->screen);
         free(vm->commandArgs);
         free(vm->externCache);
         free(vm);
     }
     
     
    +bool basVmDoEvents(BasVmT *vm) {
    +    basScreenPresent(vm);
    +
    +    if (vm->doEventsFn && !vm->doEventsFn(vm->doEventsCtx)) {
    +        return false;
    +    }
    +
    +    return !vm->stopRequested;
    +}
    +
    +
     const char *basVmGetError(const BasVmT *vm) {
         return vm ? vm->errorMsg : "";
     }
    @@ -587,6 +751,12 @@ void basVmLoadModule(BasVmT *vm, BasModuleT *module) {
     }
     
     
    +void basVmRequestStop(BasVmT *vm) {
    +    vm->stopRequested = true;
    +    vm->running       = false;
    +}
    +
    +
     BasVmResultE basVmRun(BasVmT *vm) {
         // basVmLoadModule refused the module (too many globals): report why
         // instead of returning a clean HALTED for a program that never ran.
    @@ -594,12 +764,18 @@ BasVmResultE basVmRun(BasVmT *vm) {
             return BAS_VM_ERROR;
         }
     
    +    // The host asked the program to stop (its screen was closed)
    +    if (vm->stopRequested) {
    +        return BAS_VM_HALTED;
    +    }
    +
         vm->running   = true;
         vm->stepCount = 0;
     
         while (vm->running) {
    -        // Check step limit
    +        // Check step limit: the host gets a turn, with the screen shown
             if (vm->stepLimit > 0 && vm->stepCount >= vm->stepLimit) {
    +            basScreenPresent(vm);
                 return BAS_VM_STEP_LIMIT;
             }
     
    @@ -622,7 +798,9 @@ BasVmResultE basVmRun(BasVmT *vm) {
                     continue;
                 }
     
    +            // END, the end of the program or an error: show what it drew
                 vm->running = false;
    +            basScreenPresent(vm);
                 return result;
             }
         }
    @@ -638,6 +816,7 @@ BasVmResultE basVmRun(BasVmT *vm) {
             return BAS_VM_ERROR;
         }
     
    +    basScreenPresent(vm);
         return BAS_VM_HALTED;
     }
     
    @@ -710,6 +889,11 @@ void basVmSetPrinterCallbacks(BasVmT *vm, const BasPrinterCallbacksT *printer) {
     }
     
     
    +void basVmSetScreenHost(BasVmT *vm, const BasScreenHostT *host) {
    +    vm->screenHost = *host;
    +}
    +
    +
     void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql) {
         vm->sql = *sql;
     }
    @@ -726,6 +910,8 @@ void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui) {
     
     
     BasVmResultE basVmStep(BasVmT *vm) {
    +    basValNullUsed = false;
    +
         BasVmResultE result = basVmStepExec(vm);
     
         // A string allocation that failed during the step substituted an
    @@ -740,6 +926,21 @@ BasVmResultE basVmStep(BasVmT *vm) {
             }
         }
     
    +    // A Null reached a conversion: a number or text was needed
    +    if (basValNullUsed) {
    +        basValNullUsed = false;
    +
    +        if (result == BAS_VM_OK) {
    +            result = raiseError(vm, BAS_ERR_INVALID_NULL);
    +        }
    +    }
    +
    +    // Class_Terminate of an object the instruction let go of runs now, as
    +    // a call made between this instruction and the next
    +    if (result == BAS_VM_OK && vm->terminateQueue) {
    +        result = startTerminate(vm);
    +    }
    +
         return result;
     }
     
    @@ -823,6 +1024,21 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                 break;
             }
     
    +        case OP_PUSH_SPECIAL: {
    +            uint8_t   kind = readUint8(vm);
    +            BasValueT val  = (kind == BAS_SPECIAL_NULL) ? basValNull() : basValEmpty();
    +
    +            if (kind == BAS_SPECIAL_NOTHING) {
    +                val = basValObject(NULL);
    +            }
    +
    +            if (!push(vm, val)) {
    +                return BAS_VM_STACK_OVERFLOW;
    +            }
    +
    +            break;
    +        }
    +
             case OP_PUSH_TRUE:
                 if (!push(vm, basValBool(true))) {
                     return BAS_VM_STACK_OVERFLOW;
    @@ -1003,6 +1219,59 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                 break;
             }
     
    +        case OP_LOAD_MEMBER: {
    +            BasValueT *slot = memberSlot(vm, readUint16(vm));
    +
    +            if (!slot) {
    +                return BAS_VM_ERROR;
    +            }
    +
    +            if (!push(vm, basValCopy(*slot))) {
    +                return BAS_VM_STACK_OVERFLOW;
    +            }
    +
    +            break;
    +        }
    +
    +        case OP_STORE_MEMBER: {
    +            uint16_t  idx = readUint16(vm);
    +            BasValueT val;
    +
    +            if (!pop(vm, &val)) {
    +                return BAS_VM_STACK_UNDERFLOW;
    +            }
    +
    +            BasValueT *slot = memberSlot(vm, idx);
    +
    +            if (!slot) {
    +                basValRelease(&val);
    +                return BAS_VM_ERROR;
    +            }
    +
    +            storeValue(slot, val);
    +            break;
    +        }
    +
    +        case OP_PUSH_MEMBER_ADDR: {
    +            // The caller's frame holds Me for the whole call, so the slot
    +            // outlives the callee that writes through the reference.
    +            BasValueT *slot = memberSlot(vm, readUint16(vm));
    +
    +            if (!slot) {
    +                return BAS_VM_ERROR;
    +            }
    +
    +            BasValueT ref;
    +            ref.type   = BAS_TYPE_REF;
    +            ref.refVal = slot;
    +
    +            if (!push(vm, ref)) {
    +                return BAS_VM_STACK_OVERFLOW;
    +            }
    +
    +            break;
    +        }
    +
             // ============================================================
             // Arithmetic
             // ============================================================
    @@ -1023,6 +1292,15 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
     
                 BasValueT *top = &vm->stack[vm->sp - 1];
     
    +            // -Null is Null; -Empty is 0
    +            if (top->type == BAS_TYPE_NULL) {
    +                break;
    +            }
    +
    +            if (top->type == BAS_TYPE_EMPTY) {
    +                *top = basValInteger(0);
    +            }
    +
                 // Negating the most negative INTEGER / LONG does not fit the
                 // type: promote exactly as 0 - x does in execArith.  (The
                 // literal -2147483648 is folded by the compiler and never
    @@ -1079,6 +1357,47 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
             case OP_CMP_GE:
                 return execCompare(vm, op);
     
    +        case OP_IS: {
    +            BasValueT a;
    +            BasValueT b;
    +
    +            if (!popPair(vm, &a, &b)) {
    +                return BAS_VM_STACK_UNDERFLOW;
    +            }
    +
    +            bool objects = basValIsObject(a) && basValIsObject(b);
    +            bool same    = objects && a.type == b.type && ((a.type == BAS_TYPE_OBJECT) ? a.objVal == b.objVal : a.instVal == b.instVal);
    +
    +            basValRelease(&a);
    +            basValRelease(&b);
    +
    +            if (!objects) {
    +                return raiseError(vm, BAS_ERR_OBJECT_REQUIRED);
    +            }
    +
    +            if (!push(vm, basValBool(same))) {
    +                return BAS_VM_STACK_OVERFLOW;
    +            }
    +
    +            break;
    +        }
    +
    +        case OP_OBJ_CHECK: {
    +            uint16_t cls = readUint16(vm);
    +
    +            if (vm->sp < 1) {
    +                return BAS_VM_STACK_UNDERFLOW;
    +            }
    +
    +            int32_t err = objectCheck(vm->stack[vm->sp - 1], cls);
    +
    +            if (err != 0) {
    +                return raiseError(vm, err);
    +            }
    +
    +            break;
    +        }
    +
             // ============================================================
             // Logical / bitwise
             // ============================================================
    @@ -1133,42 +1452,9 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                 break;
             }
     
    -        case OP_CALL: {
    -            uint16_t addr     = readUint16(vm);
    -            uint8_t  argc     = readUint8(vm);
    -            uint8_t  baseSlot = readUint8(vm);
    -
    -            // Guard the locals window: baseSlot and argc come straight from
    -            // bytecode, and a corrupt module could otherwise make the arg
    -            // copy below write past locals[].  Drain the pushed args first
    -            // so the eval stack stays balanced.
    -            if ((int32_t)baseSlot + (int32_t)argc > BAS_VM_MAX_LOCALS) {
    -                dropValues(vm, argc);
    -                runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "CALL argument slots out of range");
    -                return BAS_VM_ERROR;
    -            }
    -
    -            if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
    -                return BAS_VM_CALL_OVERFLOW;
    -            }
    -
    -            BasCallFrameT *frame = pushFrame(vm, vm->pc, addr, baseSlot + argc);
    -
    -            // Pop arguments into locals starting at baseSlot (in reverse
    -            // order).  A BYVAL TYPE argument must be the callee's own copy.
    -            for (int32_t i = baseSlot + argc - 1; i >= baseSlot; i--) {
    -                if (!pop(vm, &frame->locals[i])) {
    -                    return BAS_VM_STACK_UNDERFLOW;
    -                }
    -
    -                frame->locals[i] = udtDetach(frame->locals[i]);
    -            }
    -
    -            // The callee starts with no handler; if it raises an error
    -            // the dispatcher will unwind to the nearest frame with one.
    -            vm->pc = addr;
    -            break;
    -        }
    +        case OP_CALL:
    +        case OP_CALL_ME:
    +            return execCall(vm, op == OP_CALL_ME);
     
             case OP_RET: {
                 // The module frame (depth 1) is never popped: a RET reached at
    @@ -1179,7 +1465,16 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                     return BAS_VM_HALTED;
                 }
     
    +            // A SUB or Property Let/Set reached through an object where a
    +            // value is expected leaves Empty.
    +            bool pushResult = vm->callStack[vm->callDepth - 1].pushResult;
    +
                 popCallFrame(vm);
    +
    +            if (pushResult && !push(vm, basValEmpty())) {
    +                return BAS_VM_STACK_OVERFLOW;
    +            }
    +
                 break;
             }
     
    @@ -1393,78 +1688,15 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
             // Type conversion
             // ============================================================
     
    -        case OP_CONV_INT_FLT: {
    -            if (vm->sp < 1) {
    -                return BAS_VM_STACK_UNDERFLOW;
    -            }
    -
    -            BasValueT *top = &vm->stack[vm->sp - 1];
    -            double n = basValToNumber(*top);
    -            basValRelease(top);
    -            *top = basValSingle((float)n);
    -            break;
    -        }
    -
    +        case OP_CONV_INT_FLT:
             case OP_CONV_FLT_INT:
    -        case OP_CONV_STR_INT: {
    -            if (vm->sp < 1) {
    -                return BAS_VM_STACK_UNDERFLOW;
    -            }
    -
    -            BasValueT *top = &vm->stack[vm->sp - 1];
    -            int32_t    n;
    -
    -            if (!basValRoundToInt32(*top, INT16_MIN, INT16_MAX, &n)) {
    -                runtimeError(vm, BAS_ERR_OVERFLOW, "Overflow");
    -                return BAS_VM_ERROR;
    -            }
    -
    -            basValRelease(top);
    -            *top = basValInteger((int16_t)n);
    -            break;
    -        }
    -
    -        case OP_CONV_INT_STR: {
    -            if (vm->sp < 1) {
    -                return BAS_VM_STACK_UNDERFLOW;
    -            }
    -
    -            BasValueT *top = &vm->stack[vm->sp - 1];
    -            BasValueT conv = basValToString(*top);
    -            basValRelease(top);
    -            *top = conv;
    -            break;
    -        }
    -
    -        case OP_CONV_STR_FLT: {
    -            if (vm->sp < 1) {
    -                return BAS_VM_STACK_UNDERFLOW;
    -            }
    -
    -            BasValueT *top = &vm->stack[vm->sp - 1];
    -            BasValueT conv = basValToDouble(*top);
    -            basValRelease(top);
    -            *top = conv;
    -            break;
    -        }
    -
    -        case OP_CONV_INT_LONG: {
    -            if (vm->sp < 1) {
    -                return BAS_VM_STACK_UNDERFLOW;
    -            }
    -
    -            BasValueT *top = &vm->stack[vm->sp - 1];
    -            int32_t    n;
    -
    -            if (!basValRoundToInt32(*top, INT32_MIN, INT32_MAX, &n)) {
    -                runtimeError(vm, BAS_ERR_OVERFLOW, "Overflow");
    -                return BAS_VM_ERROR;
    -            }
    -
    -            basValRelease(top);
    -            *top = basValLong(n);
    -            break;
    -        }
    +        case OP_CONV_STR_INT:
    +        case OP_CONV_INT_STR:
    +        case OP_CONV_STR_FLT:
    +        case OP_CONV_INT_LONG:
    +        case OP_CONV_DATE:
    +        case OP_CONV_BOOL:
    +            return execConvert(vm, op);
     
             // ============================================================
             // I/O
    @@ -1616,7 +1848,11 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                 char buf[BAS_INPUT_LINE_LEN];
                 buf[0] = '\0';
     
    -            if (vm->inputFn) {
    +            if (vm->screen) {
    +                if (!basScreenInput(vm, promptBuf, buf, sizeof(buf))) {
    +                    buf[0] = '\0';
    +                }
    +            } else if (vm->inputFn) {
                     if (!vm->inputFn(vm->inputCtx, promptBuf, buf, sizeof(buf))) {
                         buf[0] = '\0';
                     }
    @@ -1738,7 +1974,8 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                 BasStringT *digits = basValFormatString(num);
                 BasStringT *result;
     
    -            if (digits->data[0] == '-') {
    +            // A date is its date text, with no sign position
    +            if (digits->data[0] == '-' || num.type == BAS_TYPE_DATE) {
                     result = basStringRef(digits);
                 } else {
                     BasStringT *space = basStringNew(" ", 1);
    @@ -1782,26 +2019,6 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                 break;
             }
     
    -        case OP_CONV_BOOL: {
    -            if (vm->sp < 1) {
    -                return BAS_VM_STACK_UNDERFLOW;
    -            }
    -
    -            BasValueT *top = &vm->stack[vm->sp - 1];
    -            bool truthy;
    -
    -            if (top->type == BAS_TYPE_STRING) {
    -                truthy = (top->strVal && top->strVal->len > 0);
    -            } else {
    -                truthy = (basValToNumber(*top) != 0.0);
    -            }
    -
    -            basValRelease(top);
    -            top->type    = BAS_TYPE_BOOLEAN;
    -            top->boolVal = truthy ? -1 : 0;
    -            break;
    -        }
    -
             case OP_STR_STRING: {
                 // STRING$(n, char)
                 BasValueT charVal;
    @@ -1978,16 +2195,17 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
             case OP_FRAME_WAIT:
                 return execDevice(vm, op);
     
    +        case OP_BUILTIN:
    +            return execBuiltin(vm);
    +
             // ============================================================
             // DoEvents
             // ============================================================
     
             case OP_DO_EVENTS:
    -            if (vm->doEventsFn) {
    -                if (!vm->doEventsFn(vm->doEventsCtx)) {
    -                    vm->running = false;
    -                    return BAS_VM_HALTED;
    -                }
    +            if (!basVmDoEvents(vm)) {
    +                vm->running = false;
    +                return BAS_VM_HALTED;
                 }
                 break;
     
    @@ -2126,6 +2344,12 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
     
             case OP_LOAD_ARRAY: {
                 uint8_t      dims = readUint8(vm);
    +
    +            // obj(args): the default member of a class instance or Collection
    +            if (vm->sp > dims && vm->stack[vm->sp - 1 - dims].type == BAS_TYPE_INSTANCE) {
    +                return objectDefault(vm, dims, false);
    +            }
    +
                 BasValueT    arrRef;
                 int32_t      flatIdx;
                 BasVmResultE rc   = popArrayElement(vm, dims, &arrRef, &flatIdx);
    @@ -2181,6 +2405,11 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                 uint16_t  fieldIdx = (op == OP_STORE_ARRAY_FIELD) ? readUint16(vm) : 0;
                 BasValueT storeVal;
     
    +            // obj(args) = value: the default member's Property Let
    +            if (op == OP_STORE_ARRAY && vm->sp > dims + 1 && vm->stack[vm->sp - 2 - dims].type == BAS_TYPE_INSTANCE) {
    +                return objectDefault(vm, dims, true);
    +            }
    +
                 if (!pop(vm, &storeVal)) {
                     return BAS_VM_STACK_UNDERFLOW;
                 }
    @@ -2407,13 +2636,25 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                 const char *fmt    = fmtStr.strVal->data;
                 int32_t     fmtLen = fmtStr.strVal->len;
                 double      n      = basValToNumber(val);
    -            basValRelease(&val);
     
                 char buf[BAS_FORMAT_BUF_SIZE];
                 buf[0] = '\0';
     
                 if (strcasecmp(fmt, BAS_FORMAT_PERCENT) == 0) {
                     snprintf(buf, sizeof(buf), "%.0f%%", n * 100.0);
    +            } else if (basDateIsPattern(fmt) || (val.type == BAS_TYPE_DATE && fmtLen == 0)) {
    +                // A date pattern formats the value as a date (a number as
    +                // a serial, a string as the date it holds).
    +                double  serial = 0.0;
    +                int32_t err    = basValToDateSerial(val, &serial);
    +
    +                if (err != 0) {
    +                    basValRelease(&val);
    +                    basValRelease(&fmtStr);
    +                    return raiseError(vm, err);
    +                }
    +
    +                basDateFormat(serial, fmt, buf, sizeof(buf));
                 } else {
                     BasNumFormatT f;
                     bool          sciNotation;
    @@ -2422,6 +2663,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                     formatNumber(n, &f, buf, sizeof(buf));
                 }
     
    +            basValRelease(&val);
                 basValRelease(&fmtStr);
     
                 if (!push(vm, basValStringFromC(buf))) {
    @@ -2496,6 +2738,10 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                     return BAS_VM_STACK_UNDERFLOW;
                 }
     
    +            if (ctrlRefVal.type == BAS_TYPE_INSTANCE) {
    +                return objectMemberOp(vm, ctrlRefVal, propNameVal, NULL, 0, NULL, BAS_MEMBER_OP_GET);
    +            }
    +
                 BasVmResultE rc = uiTarget(vm, vm->ui.getProp != NULL, ctrlRefVal);
     
                 if (rc != BAS_VM_OK) {
    @@ -2527,6 +2773,10 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                     return BAS_VM_STACK_UNDERFLOW;
                 }
     
    +            if (ctrlRefVal.type == BAS_TYPE_INSTANCE) {
    +                return objectMemberOp(vm, ctrlRefVal, propNameVal, NULL, 0, &value, BAS_MEMBER_OP_LET);
    +            }
    +
                 BasVmResultE rc = uiTarget(vm, vm->ui.setProp != NULL, ctrlRefVal);
     
                 if (rc == BAS_VM_OK) {
    @@ -2590,10 +2840,20 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                     return BAS_VM_STACK_UNDERFLOW;
                 }
     
    +            if (ctrlRefVal.type == BAS_TYPE_INSTANCE) {
    +                return objectMemberOp(vm, ctrlRefVal, methodNameVal, args, argCount, NULL, BAS_MEMBER_OP_GET);
    +            }
    +
                 BasValueT    result = basValInteger(0);
                 BasVmResultE rc     = uiTarget(vm, vm->ui.callMethod != NULL, ctrlRefVal);
     
                 if (rc == BAS_VM_OK) {
    +                // A host method takes values: ByRef arguments meant for a
    +                // class member arrive as references
    +                for (int32_t i = 0; i < argCount; i++) {
    +                    derefArg(&args[i]);
    +                }
    +
                     BasValueT sv = basValToString(methodNameVal);
                     result = vm->ui.callMethod(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data, args, argCount);
                     basValRelease(&sv);
    @@ -2781,6 +3041,66 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
     
                 break;
     
    +        case OP_ME_OBJ: {
    +            BasValueT me = currentFrame(vm)->me;
    +
    +            if (me.type != BAS_TYPE_INSTANCE || !me.instVal) {
    +                return raiseError(vm, BAS_ERR_OBJECT_NOT_SET);
    +            }
    +
    +            if (!push(vm, basValCopy(me))) {
    +                return BAS_VM_STACK_OVERFLOW;
    +            }
    +
    +            break;
    +        }
    +
    +        case OP_NEW_OBJ:
    +            return newObject(vm, readUint16(vm));
    +
    +        case OP_PROP_ASSIGN: {
    +            uint8_t   argc  = readUint8(vm);
    +            uint8_t   flags = readUint8(vm);
    +            BasValueT args[BAS_VM_MAX_CALL_ARGS];
    +            BasValueT value;
    +            BasValueT nameVal;
    +            BasValueT objVal;
    +
    +            if (argc > BAS_VM_MAX_CALL_ARGS) {
    +                dropValues(vm, argc + BAS_CALL_METHOD_FIXED_OPERANDS + 1);
    +                return raiseError(vm, BAS_ERR_ILLEGAL_FUNC_CALL);
    +            }
    +
    +            if (vm->sp < argc + BAS_CALL_METHOD_FIXED_OPERANDS + 1) {
    +                return BAS_VM_STACK_UNDERFLOW;
    +            }
    +
    +            pop(vm, &value);
    +            vm->sp -= argc;
    +            memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT));
    +            pop(vm, &nameVal);
    +            pop(vm, &objVal);
    +
    +            if (objVal.type == BAS_TYPE_INSTANCE) {
    +                return objectMemberOp(vm, objVal, nameVal, args, argc, &value, (flags & BAS_ASSIGN_SET) ? BAS_MEMBER_OP_SET : BAS_MEMBER_OP_LET);
    +            }
    +
    +            // A form or control property takes no arguments
    +            BasVmResultE rc = (argc > 0) ? raiseError(vm, BAS_ERR_NO_MEMBER) : uiTarget(vm, vm->ui.setProp != NULL, objVal);
    +
    +            if (rc == BAS_VM_OK) {
    +                BasValueT sv = basValToString(nameVal);
    +                vm->ui.setProp(vm->ui.ctx, objVal.objVal, sv.strVal->data, value);
    +                basValRelease(&sv);
    +            }
    +
    +            dropArgs(args, argc);
    +            basValRelease(&value);
    +            basValRelease(&nameVal);
    +            basValRelease(&objVal);
    +            return rc;
    +        }
    +
             case OP_CREATE_CTRL: {
                 BasValueT nameVal;
                 BasValueT typeVal;
    @@ -2863,6 +3183,11 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                     return BAS_VM_STACK_UNDERFLOW;
                 }
     
    +            // a.b.c on an object: member b of a
    +            if (formVal.type == BAS_TYPE_INSTANCE) {
    +                return objectMemberOp(vm, formVal, nameVal, NULL, 0, NULL, BAS_MEMBER_OP_GET);
    +            }
    +
                 void *ctrlRef = NULL;
     
                 if (vm->ui.findCtrl) {
    @@ -2897,6 +3222,11 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
                     return BAS_VM_STACK_UNDERFLOW;
                 }
     
    +            // a.b(i).c on an object: member b of a with argument i
    +            if (formVal.type == BAS_TYPE_INSTANCE) {
    +                return objectMemberOp(vm, formVal, nameVal, &idxVal, 1, NULL, BAS_MEMBER_OP_GET);
    +            }
    +
                 void *ctrlRef = NULL;
     
                 if (vm->ui.findCtrlIdx) {
    @@ -3347,6 +3677,669 @@ static BasVmResultE boundOp(BasVmT *vm, uint8_t op) {
     }
     
     
    +// The date functions of OP_BUILTIN.  args holds every argument (defaults
    +// filled in).  Returns 0 and sets *out, else the BASIC error number.
    +static int32_t builtinDate(uint8_t fn, BasValueT *args, BasValueT *out) {
    +    double           serial = 0.0;
    +    int32_t          err    = 0;
    +    BasDateIntervalE interval;
    +
    +    switch (fn) {
    +        case BAS_FN_NOW:
    +        case BAS_FN_DATE:
    +        case BAS_FN_TIME: {
    +            struct tm t;
    +            double    datePart = 0.0;
    +            double    timePart = 0.0;
    +
    +            localNow(&t, NULL);
    +
    +            if (fn != BAS_FN_TIME && !basDateSerial(t.tm_year + BAS_TM_YEAR_BASE, t.tm_mon + 1, t.tm_mday, &datePart)) {
    +                return BAS_ERR_OVERFLOW;
    +            }
    +
    +            if (fn != BAS_FN_DATE && !basTimeSerial(t.tm_hour, t.tm_min, t.tm_sec, &timePart)) {
    +                return BAS_ERR_OVERFLOW;
    +            }
    +
    +            *out = basValDate(datePart + timePart);
    +            return 0;
    +        }
    +
    +        case BAS_FN_DATESERIAL:
    +        case BAS_FN_TIMESERIAL: {
    +            int32_t parts[BAS_SERIAL_PARTS];
    +
    +            for (int32_t i = 0; i < BAS_SERIAL_PARTS; i++) {
    +                if (!basValRoundToInt32(args[i], INT32_MIN, INT32_MAX, &parts[i])) {
    +                    return BAS_ERR_OVERFLOW;
    +                }
    +            }
    +
    +            bool ok = (fn == BAS_FN_DATESERIAL) ? basDateSerial(parts[0], parts[1], parts[2], &serial) : basTimeSerial(parts[0], parts[1], parts[2], &serial);
    +
    +            if (!ok) {
    +                return BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            *out = basValDate(serial);
    +            return 0;
    +        }
    +
    +        case BAS_FN_ISDATE:
    +            *out = basValBool(args[0].type == BAS_TYPE_DATE || (args[0].type == BAS_TYPE_STRING && basValToDateSerial(args[0], &serial) == 0));
    +            return 0;
    +
    +        case BAS_FN_MONTHNAME:
    +        case BAS_FN_WEEKDAYNAME: {
    +            int32_t     n     = basValToInt32(args[0]);
    +            bool        abbr  = basValIsTruthy(args[1]);
    +            const char *name;
    +
    +            if (fn == BAS_FN_MONTHNAME) {
    +                name = basDateMonthName(n, abbr);
    +            } else {
    +                int32_t first = basValToInt32(args[2]);
    +
    +                if (first < BAS_DATE_USE_SYSTEM_DAY || first > BAS_DATE_SATURDAY || n < 1 || n > BAS_DATE_DAYS_PER_WEEK) {
    +                    return BAS_ERR_ILLEGAL_FUNC_CALL;
    +                }
    +
    +                // n counts from the first day of the week
    +                if (first == BAS_DATE_USE_SYSTEM_DAY) {
    +                    first = BAS_DATE_SUNDAY;
    +                }
    +
    +                name = basDateWeekdayName((n - 1 + first - 1) % BAS_DATE_DAYS_PER_WEEK + 1, abbr);
    +            }
    +
    +            if (name == NULL) {
    +                return BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            *out = basValStringFromC(name);
    +            return 0;
    +        }
    +
    +        default:
    +            break;
    +    }
    +
    +    // Every other function reads a date: the third argument of DATEADD
    +    // and DATEDIFF (the second date), the second of DATEPART and the first
    +    // otherwise.  The interval of the three is the first.
    +    int32_t dateArg = 0;
    +
    +    if (fn == BAS_FN_DATEADD || fn == BAS_FN_DATEDIFF) {
    +        dateArg = 2;
    +    } else if (fn == BAS_FN_DATEPART) {
    +        dateArg = 1;
    +    }
    +
    +    err = basValToDateSerial(args[dateArg], &serial);
    +
    +    if (err != 0) {
    +        return err;
    +    }
    +
    +    if ((fn == BAS_FN_DATEADD || fn == BAS_FN_DATEDIFF || fn == BAS_FN_DATEPART) && !basDateParseInterval(argString(&args[0])->data, &interval)) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    BasDatePartsT parts;
    +
    +    basDateSplit(serial, &parts);
    +
    +    switch (fn) {
    +        case BAS_FN_DATEVALUE:
    +            *out = basValDate((double)parts.dayNumber);
    +            return 0;
    +
    +        case BAS_FN_TIMEVALUE:
    +            *out = basValDate((double)parts.secOfDay / BAS_SECS_PER_DAY);
    +            return 0;
    +
    +        case BAS_FN_YEAR:
    +            *out = basValInteger((int16_t)parts.year);
    +            return 0;
    +
    +        case BAS_FN_MONTH:
    +            *out = basValInteger((int16_t)parts.month);
    +            return 0;
    +
    +        case BAS_FN_DAY:
    +            *out = basValInteger((int16_t)parts.day);
    +            return 0;
    +
    +        case BAS_FN_HOUR:
    +            *out = basValInteger((int16_t)parts.hour);
    +            return 0;
    +
    +        case BAS_FN_MINUTE:
    +            *out = basValInteger((int16_t)parts.minute);
    +            return 0;
    +
    +        case BAS_FN_SECOND:
    +            *out = basValInteger((int16_t)parts.second);
    +            return 0;
    +
    +        case BAS_FN_DATEADD: {
    +            double  n = trunc(basValToNumber(args[1]));
    +            double  result;
    +
    +            if (!(n >= INT32_MIN && n <= INT32_MAX)) {
    +                return BAS_ERR_OVERFLOW;
    +            }
    +
    +            if (!basDateAdd(interval, (int32_t)n, serial, &result)) {
    +                return BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            *out = basValDate(result);
    +            return 0;
    +        }
    +
    +        default:
    +            break;
    +    }
    +
    +    // WEEKDAY, DATEDIFF and DATEPART take a first day of the week (and
    +    // the last two a first week of the year) after their dates.
    +    int32_t firstDayArg = (fn == BAS_FN_WEEKDAY) ? 1 : dateArg + 1;
    +    int32_t firstDay    = basValToInt32(args[firstDayArg]);
    +    int32_t firstWeek   = (fn == BAS_FN_WEEKDAY) ? BAS_DATE_FIRST_JAN1 : basValToInt32(args[firstDayArg + 1]);
    +
    +    if (firstDay < BAS_DATE_USE_SYSTEM_DAY || firstDay > BAS_DATE_SATURDAY || firstWeek < BAS_DATE_USE_SYSTEM_WEEK || firstWeek > BAS_DATE_FIRST_FULL_WEEK) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if (fn == BAS_FN_WEEKDAY) {
    +        *out = basValInteger((int16_t)basDateWeekday(serial, firstDay));
    +        return 0;
    +    }
    +
    +    if (fn == BAS_FN_DATEPART) {
    +        *out = basValInteger((int16_t)basDatePart(interval, serial, firstDay, firstWeek));
    +        return 0;
    +    }
    +
    +    // DATEDIFF: the first date is args[1], the second (serial) args[2]
    +    double from;
    +
    +    err = basValToDateSerial(args[1], &from);
    +
    +    if (err != 0) {
    +        return err;
    +    }
    +
    +    int64_t diff = basDateDiff(interval, from, serial, firstDay);
    +
    +    if (diff < INT32_MIN || diff > INT32_MAX) {
    +        return BAS_ERR_OVERFLOW;
    +    }
    +
    +    *out = basValLong((int32_t)diff);
    +    return 0;
    +}
    +
    +
    +// Pads the optional trailing arguments of an OP_BUILTIN call with their
    +// defaults.  Returns the new argument count.
    +static int32_t builtinDefaults(uint8_t fn, BasValueT *args, int32_t argc) {
    +    switch (fn) {
    +        case BAS_FN_REPLACE:
    +            padArg(args, &argc, 3, basValLong(1));
    +            padArg(args, &argc, 4, basValLong(BAS_COUNT_ALL));
    +            padArg(args, &argc, 5, basValLong(BAS_COMPARE_USE_OPTION));
    +            break;
    +
    +        case BAS_FN_SPLIT:
    +            padArg(args, &argc, 1, basValStringFromC(BAS_DEFAULT_DELIMITER));
    +            padArg(args, &argc, 2, basValLong(BAS_COUNT_ALL));
    +            padArg(args, &argc, 3, basValLong(BAS_COMPARE_USE_OPTION));
    +            break;
    +
    +        case BAS_FN_JOIN:
    +            padArg(args, &argc, 1, basValStringFromC(BAS_DEFAULT_DELIMITER));
    +            break;
    +
    +        case BAS_FN_INSTRREV:
    +            padArg(args, &argc, 2, basValLong(BAS_COUNT_ALL));
    +            padArg(args, &argc, 3, basValLong(BAS_COMPARE_USE_OPTION));
    +            break;
    +
    +        case BAS_FN_STRCOMP:
    +            padArg(args, &argc, 2, basValLong(BAS_COMPARE_USE_OPTION));
    +            break;
    +
    +        case BAS_FN_FILTER:
    +            padArg(args, &argc, 2, basValBool(true));
    +            padArg(args, &argc, 3, basValLong(BAS_COMPARE_USE_OPTION));
    +            break;
    +
    +        case BAS_FN_WEEKDAY:
    +            padArg(args, &argc, 1, basValLong(BAS_DATE_SUNDAY));
    +            break;
    +
    +        case BAS_FN_DATEDIFF:
    +            padArg(args, &argc, 3, basValLong(BAS_DATE_SUNDAY));
    +            padArg(args, &argc, 4, basValLong(BAS_DATE_FIRST_JAN1));
    +            break;
    +
    +        case BAS_FN_DATEPART:
    +            padArg(args, &argc, 2, basValLong(BAS_DATE_SUNDAY));
    +            padArg(args, &argc, 3, basValLong(BAS_DATE_FIRST_JAN1));
    +            break;
    +
    +        case BAS_FN_MONTHNAME:
    +            padArg(args, &argc, 1, basValBool(false));
    +            break;
    +
    +        case BAS_FN_WEEKDAYNAME:
    +            padArg(args, &argc, 1, basValBool(false));
    +            padArg(args, &argc, 2, basValLong(BAS_DATE_SUNDAY));
    +            break;
    +
    +        default:
    +            break;
    +    }
    +
    +    return argc;
    +}
    +
    +
    +// The object and Variant functions of OP_BUILTIN (BAS_FN_OBJECT_FIRST ..
    +// BAS_FN_OBJECT_LAST).  Returns 0 and sets *out, else the BASIC error
    +// number.
    +static int32_t builtinObject(BasVmT *vm, uint8_t fn, BasValueT *args, BasValueT *out) {
    +    BasValueT v = args[0];
    +
    +    switch (fn) {
    +        case BAS_FN_VARTYPE: {
    +            int32_t vt = varTypeOf(v.type);
    +
    +            if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
    +                vt = BAS_VT_ARRAY + varTypeOf(v.arrVal->elementType);
    +            }
    +
    +            *out = basValInteger((int16_t)vt);
    +            return 0;
    +        }
    +
    +        case BAS_FN_TYPENAME: {
    +            char        buf[BAS_MAX_IDENT + BAS_ARRAY_SUFFIX_LEN];
    +            const char *name = basValIsObject(v) ? objectTypeName(vm, v) : typeNameOf(vm, v.type, v.type == BAS_TYPE_UDT && v.udtVal ? v.udtVal->typeId : -1);
    +
    +            if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
    +                snprintf(buf, sizeof(buf), "%s%s", typeNameOf(vm, v.arrVal->elementType, -1), BAS_ARRAY_SUFFIX);
    +                name = buf;
    +            }
    +
    +            *out = basValStringFromC(name);
    +            return 0;
    +        }
    +
    +        case BAS_FN_ISEMPTY:
    +            *out = basValBool(v.type == BAS_TYPE_EMPTY);
    +            return 0;
    +
    +        case BAS_FN_ISNULL:
    +            *out = basValBool(v.type == BAS_TYPE_NULL);
    +            return 0;
    +
    +        case BAS_FN_ISNUMERIC: {
    +            bool numeric = isIntegerType(v.type) || v.type == BAS_TYPE_SINGLE || v.type == BAS_TYPE_DOUBLE || v.type == BAS_TYPE_BOOLEAN || v.type == BAS_TYPE_EMPTY;
    +
    +            if (v.type == BAS_TYPE_STRING && v.strVal) {
    +                numeric = textIsNumeric(v.strVal);
    +            }
    +
    +            *out = basValBool(numeric);
    +            return 0;
    +        }
    +
    +        case BAS_FN_ISOBJECT:
    +            *out = basValBool(basValIsObject(v));
    +            return 0;
    +
    +        case BAS_FN_ISARRAY:
    +            *out = basValBool(v.type == BAS_TYPE_ARRAY);
    +            return 0;
    +
    +        case BAS_FN_CVAR:
    +            *out = basValCopy(v);
    +            return 0;
    +
    +        case BAS_FN_TYPEOF: {
    +            if (!basValIsObject(v)) {
    +                return BAS_ERR_OBJECT_REQUIRED;
    +            }
    +
    +            const char *want   = argString(&args[1])->data;
    +            const char *actual = objectTypeName(vm, v);
    +            bool        isHost = (v.type == BAS_TYPE_OBJECT && v.objVal);
    +            bool        match  = strcasecmp(actual, want) == 0;
    +
    +            // Object is every object, Control every control on a form
    +            if (strcasecmp(want, BAS_TYPENAME_OBJECT) == 0) {
    +                match = !(v.type == BAS_TYPE_OBJECT && !v.objVal);
    +            } else if (strcasecmp(want, BAS_TYPENAME_CONTROL) == 0) {
    +                match = isHost && strcasecmp(actual, BAS_TYPENAME_FORM) != 0;
    +            }
    +
    +            *out = basValBool(match);
    +            return 0;
    +        }
    +
    +        case BAS_FN_EACH_COUNT:
    +        case BAS_FN_EACH_ITEM: {
    +            int32_t count = 0;
    +            int32_t err   = eachCount(v, &count);
    +
    +            if (err != 0 || fn == BAS_FN_EACH_COUNT) {
    +                *out = basValLong(count);
    +                return err;
    +            }
    +
    +            int32_t i = basValToInt32(args[1]);
    +
    +            if (i < 0 || i >= count) {
    +                return BAS_ERR_SUBSCRIPT_RANGE;
    +            }
    +
    +            *out = basValCopy((v.type == BAS_TYPE_ARRAY) ? v.arrVal->elements[i] : v.instVal->items[i].value);
    +            return 0;
    +        }
    +
    +        default:
    +            return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +}
    +
    +
    +// The string, array and choice functions of OP_BUILTIN.  args holds every
    +// argument (defaults filled in).  Returns 0 and sets *out, else the BASIC
    +// error number.
    +static int32_t builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, BasValueT *out) {
    +    bool    text = false;
    +    int32_t err;
    +
    +    switch (fn) {
    +        case BAS_FN_REPLACE: {
    +            BasStringT *s     = argString(&args[0]);
    +            BasStringT *find  = argString(&args[1]);
    +            BasStringT *repl  = argString(&args[2]);
    +            int32_t     start = basValToInt32(args[3]);
    +            int32_t     count = basValToInt32(args[4]);
    +
    +            err = argCompare(vm, args[5], &text);
    +
    +            if (err != 0 || start < 1 || count < BAS_COUNT_ALL) {
    +                return err != 0 ? err : BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            // VB returns the text from start on, with the replacements made
    +            int32_t     len    = replaceScan(s, start - 1, find, repl, count, text, NULL);
    +            BasStringT *result = basStringAlloc(len + 1);
    +
    +            if (result->cap >= len + 1) {
    +                replaceScan(s, start - 1, find, repl, count, text, result->data);
    +                result->len       = len;
    +                result->data[len] = '\0';
    +            }
    +
    +            *out = strValue(result);
    +            return 0;
    +        }
    +
    +        case BAS_FN_SPLIT:
    +        case BAS_FN_FILTER: {
    +            int32_t limit = BAS_COUNT_ALL;
    +            bool    keep  = true;
    +
    +            // SPLIT(s, delim, limit, compare) / FILTER(array, match, include, compare)
    +            if (fn == BAS_FN_SPLIT) {
    +                argString(&args[0]);
    +                limit = basValToInt32(args[2]);
    +            } else {
    +                if (!isOneDimArray(args[0])) {
    +                    return BAS_ERR_TYPE_MISMATCH;
    +                }
    +
    +                keep = basValIsTruthy(args[2]);
    +            }
    +
    +            argString(&args[1]);
    +            err = argCompare(vm, args[3], &text);
    +
    +            if (err != 0 || limit < BAS_COUNT_ALL) {
    +                return err != 0 ? err : BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            // Count the pieces, then fill an array of exactly that many
    +            int32_t    n   = (fn == BAS_FN_SPLIT) ? splitScan(args[0].strVal, args[1].strVal, limit, text, NULL) : filterScan(args[0].arrVal, args[1].strVal, keep, text, NULL);
    +            BasArrayT *arr = newVector(n, BAS_TYPE_STRING);
    +
    +            if (!arr) {
    +                return BAS_ERR_OUT_OF_MEMORY;
    +            }
    +
    +            if (fn == BAS_FN_SPLIT) {
    +                splitScan(args[0].strVal, args[1].strVal, limit, text, arr);
    +            } else {
    +                filterScan(args[0].arrVal, args[1].strVal, keep, text, arr);
    +            }
    +
    +            *out = arrValue(arr);
    +            return 0;
    +        }
    +
    +        case BAS_FN_JOIN: {
    +            if (!isOneDimArray(args[0])) {
    +                return BAS_ERR_TYPE_MISMATCH;
    +            }
    +
    +            BasStringT *delim  = argString(&args[1]);
    +            int32_t     len    = joinScan(args[0].arrVal, delim, NULL);
    +            BasStringT *result = basStringAlloc(len + 1);
    +
    +            if (result->cap >= len + 1) {
    +                joinScan(args[0].arrVal, delim, result->data);
    +                result->len       = len;
    +                result->data[len] = '\0';
    +            }
    +
    +            *out = strValue(result);
    +            return 0;
    +        }
    +
    +        case BAS_FN_INSTRREV: {
    +            BasStringT *s     = argString(&args[0]);
    +            BasStringT *find  = argString(&args[1]);
    +            int32_t     start = basValToInt32(args[2]);
    +
    +            err = argCompare(vm, args[3], &text);
    +
    +            if (err != 0 || start == 0 || start < BAS_COUNT_ALL) {
    +                return err != 0 ? err : BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            if (start == BAS_COUNT_ALL) {
    +                start = s->len;
    +            }
    +
    +            int32_t pos = 0;
    +
    +            if (s->len > 0 && start <= s->len) {
    +                pos = start;
    +
    +                if (find->len > 0) {
    +                    pos = 0;
    +
    +                    for (int32_t i = start - find->len; i >= 0 && pos == 0; i--) {
    +                        if (stringMatchAt(s, i, find, text)) {
    +                            pos = i + 1;
    +                        }
    +                    }
    +                }
    +            }
    +
    +            *out = basValLong(pos);
    +            return 0;
    +        }
    +
    +        case BAS_FN_STRCOMP: {
    +            BasStringT *a = argString(&args[0]);
    +            BasStringT *b = argString(&args[1]);
    +
    +            err = argCompare(vm, args[2], &text);
    +
    +            if (err != 0) {
    +                return err;
    +            }
    +
    +            int32_t cmp = text ? basStringCompareCI(a, b) : basStringCompare(a, b);
    +
    +            *out = basValInteger((int16_t)((cmp > 0) - (cmp < 0)));
    +            return 0;
    +        }
    +
    +        case BAS_FN_STRREVERSE: {
    +            BasStringT *s      = argString(&args[0]);
    +            BasStringT *result = basStringAlloc(s->len + 1);
    +
    +            if (result->cap >= s->len + 1) {
    +                for (int32_t i = 0; i < s->len; i++) {
    +                    result->data[i] = s->data[s->len - 1 - i];
    +                }
    +
    +                result->len          = s->len;
    +                result->data[s->len] = '\0';
    +            }
    +
    +            *out = strValue(result);
    +            return 0;
    +        }
    +
    +        case BAS_FN_LSET:
    +        case BAS_FN_RSET: {
    +            // The target keeps its length; the value is cut or padded
    +            if (args[0].type != BAS_TYPE_STRING) {
    +                return BAS_ERR_TYPE_MISMATCH;
    +            }
    +
    +            int32_t     width  = argString(&args[0])->len;
    +            BasStringT *value  = argString(&args[1]);
    +            int32_t     copy   = DVX_MIN(value->len, width);
    +            int32_t     offset = (fn == BAS_FN_RSET) ? width - copy : 0;
    +            BasStringT *result = fillString(' ', width);
    +
    +            if (result->len == width) {
    +                memcpy(result->data + offset, value->data, copy);
    +            }
    +
    +            *out = strValue(result);
    +            return 0;
    +        }
    +
    +        case BAS_FN_IIF:
    +            *out = basValCopy(basValIsTruthy(args[0]) ? args[1] : args[2]);
    +            return 0;
    +
    +        case BAS_FN_CHOOSE: {
    +            // An index outside the choices gives Null (VB)
    +            double index = trunc(basValToNumber(args[0]));
    +
    +            *out = (index >= 1 && index < argc) ? basValCopy(args[(int32_t)index]) : basValNull();
    +            return 0;
    +        }
    +
    +        case BAS_FN_PARAMARRAY: {
    +            // (elementType, values...): the values move into a 0-based
    +            // array; each keeps the type the compiler gave it.
    +            int32_t elementType = basValToInt32(args[0]);
    +
    +            if (elementType > BAS_TYPE_BOOLEAN && elementType != BAS_TYPE_OBJECT && elementType != BAS_TYPE_DATE && elementType != BAS_TYPE_VARIANT) {
    +                return BAS_ERR_ILLEGAL_FUNC_CALL;
    +            }
    +
    +            BasArrayT *arr = newVector(argc - 1, (uint8_t)elementType);
    +
    +            if (!arr) {
    +                return BAS_ERR_OUT_OF_MEMORY;
    +            }
    +
    +            for (int32_t i = 1; i < argc; i++) {
    +                basValRelease(&arr->elements[i - 1]);
    +                arr->elements[i - 1] = args[i];
    +                args[i]              = basValInteger(0);
    +            }
    +
    +            *out = arrValue(arr);
    +            return 0;
    +        }
    +
    +        default:
    +            return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +}
    +
    +
    +// Runs procedure member m of obj: the arguments (taken over) become its
    +// parameters, omitted Optional ones their type's zero, the rest of a
    +// ParamArray's packed into an array.  The procedure runs as a call made from here
    +// with obj as its Me; wantResult makes a SUB (or Property Let/Set) leave
    +// Empty where the caller expects a value.  args has room for
    +// BAS_VM_MAX_CALL_ARGS + 1 values.
    +static BasVmResultE callMember(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, bool wantResult) {
    +    const BasProcEntryT *proc       = &vm->module->procs[m->index];
    +    int32_t              paramCount = proc->paramCount;
    +    int32_t              baseSlot   = proc->isFunction ? 1 : 0;
    +    bool                 packed     = (m->flags & BAS_MEMBER_PARAMARRAY) != 0;
    +
    +    if (argc < m->requiredParams || (argc > paramCount && !packed)) {
    +        dropArgs(args, argc);
    +        return raiseError(vm, BAS_ERR_WRONG_ARGS);
    +    }
    +
    +    if (packed) {
    +        int32_t    first = paramCount - 1;
    +        int32_t    extra = (argc > first) ? argc - first : 0;
    +        BasArrayT *arr   = newVector(extra, BAS_TYPE_VARIANT);
    +
    +        if (!arr) {
    +            dropArgs(args, argc);
    +            return raiseError(vm, BAS_ERR_OUT_OF_MEMORY);
    +        }
    +
    +        for (int32_t i = 0; i < extra; i++) {
    +            derefArg(&args[first + i]);
    +            basValRelease(&arr->elements[i]);
    +            arr->elements[i] = args[first + i];
    +        }
    +
    +        args[first] = arrValue(arr);
    +        argc        = paramCount;
    +    }
    +
    +    if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
    +        dropArgs(args, argc);
    +        return BAS_VM_CALL_OVERFLOW;
    +    }
    +
    +    BasCallFrameT *frame = pushFrame(vm, vm->pc, proc->codeAddr, baseSlot + paramCount);
    +
    +    for (int32_t i = 0; i < paramCount; i++) {
    +        frame->locals[baseSlot + i] = (i < argc) ? udtDetach(args[i]) : zeroValue(m->paramTypes[i]);
    +    }
    +
    +    frame->me         = basValInstance(basObjectRef(obj));
    +    frame->pushResult = wantResult && !proc->isFunction;
    +    vm->pc            = proc->codeAddr;
    +    return BAS_VM_OK;
    +}
    +
    +
     // Clamps an RGB channel value into [0, BAS_RGB_COMPONENT_MAX].
     static int32_t clampComponent(int32_t c) {
         if (c < 0) {
    @@ -3391,6 +4384,154 @@ static bool closeChannel(BasVmT *vm, int32_t channel) {
     }
     
     
    +// Collection members: Add item [, key] [, before] [, after], Remove
    +// index-or-key, Item index-or-key (the default member, name NULL) and
    +// Count.  Arguments given as Empty count as omitted.  Takes over args.
    +static BasVmResultE collectionAccess(BasVmT *vm, BasObjectT *coll, const char *name, BasValueT *args, int32_t argc, int32_t op) {
    +    BasValueT result = basValEmpty();
    +    int32_t   err    = 0;
    +    int32_t   pos    = 0;
    +
    +    for (int32_t i = 0; i < argc; i++) {
    +        derefArg(&args[i]);
    +    }
    +
    +    if (!name || strcasecmp(name, BAS_COLL_ITEM) == 0) {
    +        err = (op != BAS_MEMBER_OP_GET) ? BAS_ERR_NO_MEMBER : ((argc != 1) ? BAS_ERR_WRONG_ARGS : collectionFind(coll, args[0], &pos));
    +
    +        if (err == 0) {
    +            result = basValCopy(coll->items[pos].value);
    +        }
    +    } else if (strcasecmp(name, BAS_COLL_COUNT) == 0) {
    +        err    = (argc != 0) ? BAS_ERR_WRONG_ARGS : 0;
    +        result = basValLong(coll->itemCount);
    +    } else if (strcasecmp(name, BAS_COLL_ADD) == 0) {
    +        err = (argc < 1 || argc > BAS_COLL_ADD_ARGS) ? BAS_ERR_WRONG_ARGS : collectionAdd(coll, args, argc);
    +    } else if (strcasecmp(name, BAS_COLL_REMOVE) == 0) {
    +        err = (argc != 1) ? BAS_ERR_WRONG_ARGS : collectionFind(coll, args[0], &pos);
    +
    +        if (err == 0) {
    +            BasCollItemT gone = coll->items[pos];
    +
    +            coll->itemCount--;
    +            memmove(&coll->items[pos], &coll->items[pos + 1], (size_t)(coll->itemCount - pos) * sizeof(BasCollItemT));
    +            basStringUnref(gone.key);
    +            basValRelease(&gone.value);
    +        }
    +    } else {
    +        err = BAS_ERR_NO_MEMBER;
    +    }
    +
    +    dropArgs(args, argc);
    +
    +    if (err != 0) {
    +        basValRelease(&result);
    +        return raiseError(vm, err);
    +    }
    +
    +    // Add and Remove are statements, but the caller may still want a value
    +    if (op == BAS_MEMBER_OP_GET && !push(vm, result)) {
    +        return BAS_VM_STACK_OVERFLOW;
    +    }
    +
    +    return BAS_VM_OK;
    +}
    +
    +
    +// Collection.Add item, key, before, after (args[1..] may be missing or
    +// Empty).  Returns 0 or the BASIC error number.  The item is copied.
    +static int32_t collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc) {
    +    bool    hasKey    = argc > 1 && args[1].type != BAS_TYPE_EMPTY;
    +    bool    hasBefore = argc > 2 && args[2].type != BAS_TYPE_EMPTY;
    +    bool    hasAfter  = argc > 3 && args[3].type != BAS_TYPE_EMPTY;
    +    int32_t pos       = coll->itemCount;
    +    int32_t err       = 0;
    +
    +    if (hasKey && args[1].type != BAS_TYPE_STRING) {
    +        return BAS_ERR_TYPE_MISMATCH;
    +    }
    +
    +    if (hasBefore && hasAfter) {
    +        return BAS_ERR_ILLEGAL_FUNC_CALL;
    +    }
    +
    +    if (hasBefore) {
    +        err = collectionFind(coll, args[2], &pos);
    +    } else if (hasAfter) {
    +        err = collectionFind(coll, args[3], &pos);
    +        pos++;
    +    }
    +
    +    if (err != 0) {
    +        return err;
    +    }
    +
    +    if (hasKey && collectionKeyPos(coll, args[1].strVal) >= 0) {
    +        return BAS_ERR_DUPLICATE_KEY;
    +    }
    +
    +    if (coll->itemCount == coll->itemCap) {
    +        int32_t       cap   = coll->itemCap ? coll->itemCap * BAS_COLL_GROWTH : BAS_COLL_MIN_CAP;
    +        BasCollItemT *items = (BasCollItemT *)realloc(coll->items, (size_t)cap * sizeof(BasCollItemT));
    +
    +        if (!items) {
    +            return BAS_ERR_OUT_OF_MEMORY;
    +        }
    +
    +        coll->items   = items;
    +        coll->itemCap = cap;
    +    }
    +
    +    memmove(&coll->items[pos + 1], &coll->items[pos], (size_t)(coll->itemCount - pos) * sizeof(BasCollItemT));
    +    coll->items[pos].key   = hasKey ? basStringRef(args[1].strVal) : NULL;
    +    coll->items[pos].value = basValCopy(args[0]);
    +    coll->itemCount++;
    +    return 0;
    +}
    +
    +
    +// The 0-based position of the item index (1-based) or key (a string,
    +// matched without regard to case) names.  Returns 0 and sets *outPos,
    +// else the BASIC error number.
    +static int32_t collectionFind(const BasObjectT *coll, BasValueT index, int32_t *outPos) {
    +    if (index.type == BAS_TYPE_STRING) {
    +        int32_t pos = collectionKeyPos(coll, index.strVal);
    +
    +        if (pos < 0) {
    +            return BAS_ERR_ILLEGAL_FUNC_CALL;
    +        }
    +
    +        *outPos = pos;
    +        return 0;
    +    }
    +
    +    if (!isIntegerType(index.type) && index.type != BAS_TYPE_SINGLE && index.type != BAS_TYPE_DOUBLE) {
    +        return BAS_ERR_TYPE_MISMATCH;
    +    }
    +
    +    int32_t n = basValToInt32(index);
    +
    +    if (n < 1 || n > coll->itemCount) {
    +        return BAS_ERR_SUBSCRIPT_RANGE;
    +    }
    +
    +    *outPos = n - 1;
    +    return 0;
    +}
    +
    +
    +// The position of the item with key (case ignored), or -1.
    +static int32_t collectionKeyPos(const BasObjectT *coll, const BasStringT *key) {
    +    for (int32_t i = 0; i < coll->itemCount; i++) {
    +        if (coll->items[i].key && basStringCompareCI(coll->items[i].key, key) == 0) {
    +            return i;
    +        }
    +    }
    +
    +    return -1;
    +}
    +
    +
     // COMMAND$(n): the whole launch argument string for n = 0, else its nth
     // argument without quotes ("" when there are fewer).  An argument is
     // never longer than the whole string, so that sizes the result.
    @@ -3442,6 +4583,21 @@ static void defaultPrint(void *ctx, const char *text, bool newline) {
     }
     
     
    +// Turns a ByRef argument into the value it refers to (a host method, a
    +// variable member or a Collection takes values).
    +static void derefArg(BasValueT *arg) {
    +    if (arg->type != BAS_TYPE_REF && arg->type != BAS_TYPE_ELEM_REF) {
    +        return;
    +    }
    +
    +    BasValueT *slot = derefSlot(arg);
    +    BasValueT  v    = slot ? basValCopy(*slot) : basValEmpty();
    +
    +    basValRelease(arg);
    +    *arg = v;
    +}
    +
    +
     // Resolves a variable slot through any ByRef indirection it holds: a plain
     // slot is returned as-is, a BAS_TYPE_REF yields the referenced slot, and a
     // BAS_TYPE_ELEM_REF yields the array element it names (NULL if the index
    @@ -3522,6 +4678,7 @@ static void discardFrame(BasVmT *vm) {
             basValRelease(&frame->locals[i]);
         }
     
    +    basValRelease(&frame->me);
         forStackTrim(vm, frame->savedForDepth);
         vm->gosubDepth = frame->savedGosubDepth;
         vm->stmtPc     = frame->savedStmtPc;
    @@ -3624,6 +4781,14 @@ static int64_t divMod32(uint8_t op, int32_t ia, int32_t ib) {
     }
     
     
    +// Releases count values taken off the stack.
    +static void dropArgs(BasValueT *args, int32_t count) {
    +    for (int32_t i = 0; i < count; i++) {
    +        basValRelease(&args[i]);
    +    }
    +}
    +
    +
     // Pops and releases up to count values (drains operands on an error path).
     static void dropValues(BasVmT *vm, int32_t count) {
         for (int32_t i = 0; i < count && vm->sp > 0; i++) {
    @@ -3632,6 +4797,32 @@ static void dropValues(BasVmT *vm, int32_t count) {
     }
     
     
    +// Items FOR EACH walks in group: an array's elements or a Collection's
    +// items.  Returns 0 and sets *outCount, else the BASIC error number.
    +static int32_t eachCount(BasValueT group, int32_t *outCount) {
    +    if (group.type == BAS_TYPE_ARRAY && group.arrVal) {
    +        *outCount = group.arrVal->totalElements;
    +        return 0;
    +    }
    +
    +    if (group.type == BAS_TYPE_INSTANCE && group.instVal && group.instVal->classIdx == BAS_OBJ_COLLECTION) {
    +        *outCount = group.instVal->itemCount;
    +        return 0;
    +    }
    +
    +    return basValIsObject(group) ? BAS_ERR_NOT_COLLECTION : BAS_ERR_TYPE_MISMATCH;
    +}
    +
    +
    +// An Empty operand of a binary operator takes part as "" beside a
    +// string and as the Integer 0 otherwise.
    +static void emptyOperand(BasValueT *v, BasValueT other) {
    +    if (v->type == BAS_TYPE_EMPTY) {
    +        *v = (other.type == BAS_TYPE_STRING) ? basValString(NULL) : basValInteger(0);
    +    }
    +}
    +
    +
     // Standard message for a runtime error number (ERROR n raises it with the
     // same text the VM uses when it raises that error itself).
     static const char *errorText(int32_t errNum) {
    @@ -3711,6 +4902,24 @@ static const char *errorText(int32_t errNum) {
             case BAS_ERR_OBJECT_NOT_SET:
                 return "Object variable not set";
     
    +        case BAS_ERR_INVALID_NULL:
    +            return "Invalid use of Null";
    +
    +        case BAS_ERR_OBJECT_REQUIRED:
    +            return "Object required";
    +
    +        case BAS_ERR_NO_MEMBER:
    +            return "Object doesn't support this property or method";
    +
    +        case BAS_ERR_WRONG_ARGS:
    +            return "Wrong number of arguments or invalid property assignment";
    +
    +        case BAS_ERR_NOT_COLLECTION:
    +            return "Object not a collection";
    +
    +        case BAS_ERR_DUPLICATE_KEY:
    +            return "This key is already associated with an element of this collection";
    +
             default:
                 return "User-defined error";
         }
    @@ -3725,6 +4934,16 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
             return BAS_VM_STACK_UNDERFLOW;
         }
     
    +    BasVmResultE nullRc;
    +
    +    if (nullOperands(vm, &a, &b, &nullRc)) {
    +        return nullRc;
    +    }
    +
    +    // Empty takes part as "" beside a string, else as 0
    +    emptyOperand(&a, b);
    +    emptyOperand(&b, a);
    +
         // VB behavior: + on two strings concatenates
         if (op == OP_ADD_INT && a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
             BasStringT *sa  = a.strVal ? a.strVal : basEmptyString;
    @@ -3791,10 +5010,14 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
         // tell us whether this is an integer or floating-point op.
         uint8_t aType    = a.type;
         uint8_t bType    = b.type;
    -    bool    hadFloat = (aType == BAS_TYPE_SINGLE || aType == BAS_TYPE_DOUBLE || bType == BAS_TYPE_SINGLE || bType == BAS_TYPE_DOUBLE);
    +    bool    aDate    = (aType == BAS_TYPE_DATE);
    +    bool    bDate    = (bType == BAS_TYPE_DATE);
    +    bool    hadFloat = (aType == BAS_TYPE_SINGLE || aType == BAS_TYPE_DOUBLE || bType == BAS_TYPE_SINGLE || bType == BAS_TYPE_DOUBLE || aDate || bDate);
     
    -    double na = basValToNumber(a);
    -    double nb = basValToNumber(b);
    +    // A DATE takes part as its instant on the linear time line, so adding
    +    // days to a date before 12/30/1899 keeps its time of day.
    +    double na = aDate ? basDateToLinear(a.dblVal) : basValToNumber(a);
    +    double nb = bDate ? basDateToLinear(b.dblVal) : basValToNumber(b);
         basValRelease(&a);
         basValRelease(&b);
     
    @@ -3868,6 +5091,22 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
         // either operand was a float -- otherwise 1.5 + 2.25 truncates to 3.
         bool intOp = (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT);
     
    +    // DATE + n, n + DATE and DATE - n are dates; DATE - DATE is the number
    +    // of days between them.
    +    if ((op == OP_ADD_INT && (aDate || bDate)) || (op == OP_SUB_INT && aDate && !bDate)) {
    +        double serial;
    +
    +        if (!basDateFromLinear(result, &serial)) {
    +            return raiseError(vm, BAS_ERR_OVERFLOW);
    +        }
    +
    +        if (!push(vm, basValDate(serial))) {
    +            return BAS_VM_STACK_OVERFLOW;
    +        }
    +
    +        return BAS_VM_OK;
    +    }
    +
         if (!push(vm, (intOp && !hadFloat) ? integralValue(result) : basValDouble(result))) {
             return BAS_VM_STACK_OVERFLOW;
         }
    @@ -3876,6 +5115,105 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
     }
     
     
    +// OP_BUILTIN: [uint8 fn] [uint8 argc].  Pops the arguments (pushed in
    +// source order), fills in omitted optional ones and pushes the result.
    +static BasVmResultE execBuiltin(BasVmT *vm) {
    +    uint8_t   fn      = readUint8(vm);
    +    int32_t   argc    = readUint8(vm);
    +    int32_t   minArgs = 0;
    +    int32_t   maxArgs = 0;
    +    BasValueT args[BAS_BUILTIN_MAX_ARGS];
    +
    +    if (!basBuiltinArity(fn, &minArgs, &maxArgs) || argc < minArgs || argc > maxArgs) {
    +        dropValues(vm, argc);
    +        runtimeError(vm, BAS_ERR_BAD_OPCODE, "Bad opcode");
    +        return BAS_VM_BAD_OPCODE;
    +    }
    +
    +    if (vm->sp < argc) {
    +        return BAS_VM_STACK_UNDERFLOW;
    +    }
    +
    +    // Take ownership of the arguments straight off the stack
    +    vm->sp -= argc;
    +    memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT));
    +    argc = builtinDefaults(fn, args, argc);
    +
    +    // The date functions are numbered NOW through WEEKDAYNAME, the program
    +    // screen's BAS_FN_SCREEN_FIRST through _LAST.
    +    BasValueT result = basValInteger(0);
    +    int32_t   err    = 0;
    +
    +    if (fn >= BAS_FN_NOW && fn <= BAS_FN_WEEKDAYNAME) {
    +        err = builtinDate(fn, args, &result);
    +    } else if (fn >= BAS_FN_OBJECT_FIRST && fn <= BAS_FN_OBJECT_LAST) {
    +        err = builtinObject(vm, fn, args, &result);
    +    } else if (fn >= BAS_FN_SCREEN_FIRST && fn <= BAS_FN_SCREEN_LAST) {
    +        err = basScreenExec(vm, fn, args, argc, &result);
    +    } else {
    +        err = builtinString(vm, fn, args, argc, &result);
    +    }
    +
    +    for (int32_t i = 0; i < argc; i++) {
    +        basValRelease(&args[i]);
    +    }
    +
    +    if (err != 0) {
    +        return raiseError(vm, err);
    +    }
    +
    +    if (!push(vm, result)) {
    +        return BAS_VM_STACK_OVERFLOW;
    +    }
    +
    +    return BAS_VM_OK;
    +}
    +
    +
    +// OP_CALL / OP_CALL_ME: [uint16 addr] [uint8 argc] [uint8 baseSlot].  The
    +// arguments move from the stack into the callee's locals; keepMe hands
    +// the caller's Me on (a class calling one of its own procedures).
    +static BasVmResultE execCall(BasVmT *vm, bool keepMe) {
    +    uint16_t addr     = readUint16(vm);
    +    uint8_t  argc     = readUint8(vm);
    +    uint8_t  baseSlot = readUint8(vm);
    +
    +    // Guard the locals window: baseSlot and argc come straight from
    +    // bytecode, and a corrupt module could otherwise make the arg copy
    +    // below write past locals[].  Drain the pushed args first so the eval
    +    // stack stays balanced.
    +    if ((int32_t)baseSlot + (int32_t)argc > BAS_VM_MAX_LOCALS) {
    +        dropValues(vm, argc);
    +        runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "CALL argument slots out of range");
    +        return BAS_VM_ERROR;
    +    }
    +
    +    if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
    +        return BAS_VM_CALL_OVERFLOW;
    +    }
    +
    +    BasValueT      me    = keepMe ? basValCopy(currentFrame(vm)->me) : basValEmpty();
    +    BasCallFrameT *frame = pushFrame(vm, vm->pc, addr, baseSlot + argc);
    +
    +    frame->me = me;
    +
    +    // Pop arguments into locals starting at baseSlot (in reverse order).  A
    +    // BYVAL TYPE argument must be the callee's own copy.
    +    for (int32_t i = baseSlot + argc - 1; i >= baseSlot; i--) {
    +        if (!pop(vm, &frame->locals[i])) {
    +            return BAS_VM_STACK_UNDERFLOW;
    +        }
    +
    +        frame->locals[i] = udtDetach(frame->locals[i]);
    +    }
    +
    +    // The callee starts with no handler; if it raises an error the
    +    // dispatcher will unwind to the nearest frame with one.
    +    vm->pc = addr;
    +    return BAS_VM_OK;
    +}
    +
    +
     static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
         BasValueT a;
         BasValueT b;
    @@ -3884,6 +5222,12 @@ static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
             return BAS_VM_STACK_UNDERFLOW;
         }
     
    +    BasVmResultE nullRc;
    +
    +    if (nullOperands(vm, &a, &b, &nullRc)) {
    +        return nullRc;
    +    }
    +
         int32_t cmp;
     
         // Same-type integer fast path: loop conditions like i < n hit this on
    @@ -3939,6 +5283,74 @@ static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
     }
     
     
    +// The conversion opcodes (CINT, CLNG, CSNG, CDBL, CSTR, CDATE, CBOOL and
    +// the conversions stores into typed variables make), on the value on top
    +// of the stack.
    +static BasVmResultE execConvert(BasVmT *vm, uint8_t op) {
    +    if (vm->sp < 1) {
    +        return BAS_VM_STACK_UNDERFLOW;
    +    }
    +
    +    BasValueT *top  = &vm->stack[vm->sp - 1];
    +    BasValueT  conv = *top;
    +    int32_t    n    = 0;
    +
    +    switch (op) {
    +        case OP_CONV_INT_FLT:
    +            conv = basValSingle((float)basValToNumber(*top));
    +            break;
    +
    +        case OP_CONV_FLT_INT:
    +        case OP_CONV_STR_INT:
    +            if (!basValRoundToInt32(*top, INT16_MIN, INT16_MAX, &n)) {
    +                return raiseError(vm, BAS_ERR_OVERFLOW);
    +            }
    +
    +            conv = basValInteger((int16_t)n);
    +            break;
    +
    +        case OP_CONV_INT_STR:
    +            conv = basValToString(*top);
    +            break;
    +
    +        case OP_CONV_STR_FLT:
    +            conv = basValToDouble(*top);
    +            break;
    +
    +        case OP_CONV_INT_LONG:
    +            if (!basValRoundToInt32(*top, INT32_MIN, INT32_MAX, &n)) {
    +                return raiseError(vm, BAS_ERR_OVERFLOW);
    +            }
    +
    +            conv = basValLong(n);
    +            break;
    +
    +        case OP_CONV_DATE: {
    +            double  serial = 0.0;
    +            int32_t err    = basValToDateSerial(*top, &serial);
    +
    +            if (err != 0) {
    +                return raiseError(vm, err);
    +            }
    +
    +            conv = basValDate(serial);
    +            break;
    +        }
    +
    +        case OP_CONV_BOOL:
    +            conv = basValBool((top->type == BAS_TYPE_STRING) ? (top->strVal && top->strVal->len > 0) : (basValToNumber(*top) != 0.0));
    +            break;
    +
    +        default:
    +            return BAS_VM_OK;
    +    }
    +
    +    basValRelease(top);
    +    *top = conv;
    +    return BAS_VM_OK;
    +}
    +
    +
     // BEEP, SOUND, PLAY, PLAY(n), STICK, STRIG and FRAMEWAIT through the
     // host's device callbacks (BasDeviceCallbacksT), with QBasic's argument
     // ranges.  The statements then wait, keeping the host's events running,
    @@ -4199,7 +5611,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
                     return chResult;
                 }
     
    -            BasStringT *s = printString(val);
    +            BasStringT *s = basValPrintString(val);
                 basValRelease(&val);
                 fwrite(s->data, 1, s->len, (FILE *)vm->files[channel].handle);
                 basStringUnref(s);
    @@ -4286,22 +5698,23 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
     
                 FILE *fp = (FILE *)vm->files[channel].handle;
     
    -            if (val.type == BAS_TYPE_STRING) {
    -                // Strings: enclosed in quotes
    -                fputc('"', fp);
    -
    -                if (val.strVal) {
    -                    fwrite(val.strVal->data, 1, val.strVal->len, fp);
    -                }
    +            // Numbers: no leading space (unlike PRINT).  Strings and
    +            // dates: enclosed in quotes, so INPUT # reads a date back whole.
    +            BasStringT *s      = basValFormatString(val);
    +            bool        quoted = (val.type == BAS_TYPE_STRING || val.type == BAS_TYPE_DATE);
     
    +            if (quoted) {
                     fputc('"', fp);
    -            } else {
    -                // Numbers: no leading space (unlike PRINT)
    -                BasStringT *s = basValFormatString(val);
    -                fputs(s->data, fp);
    -                basStringUnref(s);
                 }
     
    +            fwrite(s->data, 1, s->len, fp);
    +
    +            if (quoted) {
    +                fputc('"', fp);
    +            }
    +
    +            basStringUnref(s);
    +
                 basValRelease(&val);
                 break;
             }
    @@ -5016,6 +6429,12 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op) {
     
             BasValueT   *top = &vm->stack[vm->sp - 1];
             int32_t      n;
    +
    +        // NOT Null is Null
    +        if (top->type == BAS_TYPE_NULL) {
    +            return BAS_VM_OK;
    +        }
    +
             BasVmResultE rc  = roundedOperand(vm, *top, &n);
     
             if (rc != BAS_VM_OK) {
    @@ -5038,6 +6457,10 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op) {
             return BAS_VM_STACK_UNDERFLOW;
         }
     
    +    if (a.type == BAS_TYPE_NULL || b.type == BAS_TYPE_NULL) {
    +        return logicalNull(vm, op, a, b);
    +    }
    +
         int32_t      na;
         int32_t      nb;
         BasVmResultE rc = roundedOperand(vm, a, &na);
    @@ -5178,6 +6601,12 @@ static BasVmResultE execMath(BasVmT *vm, uint8_t op) {
         }
     
         BasValueT *top = &vm->stack[vm->sp - 1];
    +
    +    // ABS, INT, FIX and SGN of Null are Null
    +    if (top->type == BAS_TYPE_NULL && (op == OP_MATH_ABS || op == OP_MATH_INT || op == OP_MATH_FIX || op == OP_MATH_SGN)) {
    +        return BAS_VM_OK;
    +    }
    +
         double     n   = basValToNumber(*top);
         double     result;
     
    @@ -5263,7 +6692,7 @@ static BasVmResultE execPrint(BasVmT *vm) {
             return BAS_VM_STACK_UNDERFLOW;
         }
     
    -    BasStringT *s = printString(val);
    +    BasStringT *s = basValPrintString(val);
         basValRelease(&val);
         printText(vm, s->data, false);
         basStringUnref(s);
    @@ -5338,6 +6767,18 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
                 BasValueT sb;
                 BasValueT sa;
     
    +            // Null & x is x; Null & Null stays Null
    +            if (vm->sp >= 2 && vm->stack[vm->sp - 1].type == BAS_TYPE_NULL && vm->stack[vm->sp - 2].type == BAS_TYPE_NULL) {
    +                vm->sp--;
    +                return BAS_VM_OK;
    +            }
    +
    +            for (int32_t i = 1; i <= 2 && i <= vm->sp; i++) {
    +                if (vm->stack[vm->sp - i].type == BAS_TYPE_NULL) {
    +                    vm->stack[vm->sp - i] = basValString(NULL);
    +                }
    +            }
    +
                 if (!popStringArg(vm, &sb)) {
                     return BAS_VM_STACK_UNDERFLOW;
                 }
    @@ -5733,6 +7174,55 @@ static BasStringT *fillString(char ch, int32_t count) {
     }
     
     
    +// FILTER: the elements of arr (as strings) that contain match when keep
    +// is set, or that do not when it is clear.  Returns how many there are;
    +// with out non-NULL they are also stored into out's elements.
    +static int32_t filterScan(const BasArrayT *arr, const BasStringT *match, bool keep, bool ignoreCase, BasArrayT *out) {
    +    int32_t n = 0;
    +
    +    for (int32_t i = 0; i < arr->totalElements; i++) {
    +        BasValueT sv = basValToString(arr->elements[i]);
    +
    +        if ((stringFind(sv.strVal, 0, match, ignoreCase) >= 0) == keep) {
    +            if (out) {
    +                basValRelease(&out->elements[n]);
    +                out->elements[n] = basValCopy(sv);
    +            }
    +
    +            n++;
    +        }
    +
    +        basValRelease(&sv);
    +    }
    +
    +    return n;
    +}
    +
    +
    +// The public member of cls that a read (BAS_MEMBER_OP_GET), Let or Set
    +// named name reaches (name NULL: the default member).  A read takes a
    +// variable, Property Get, Function or Sub; a Let a variable or Property
    +// Let, else Property Set; a Set a variable or Property Set, else Property
    +// Let.  NULL when there is none.
    +static const BasClassMemberT *findMember(const BasClassInfoT *cls, const char *name, int32_t op) {
    +    const BasClassMemberT *best     = NULL;
    +    int32_t                bestRank = BAS_MEMBER_NO_RANK;
    +
    +    for (int32_t i = 0; i < cls->memberCount; i++) {
    +        const BasClassMemberT *m     = &cls->members[i];
    +        bool                   match = name ? strcasecmp(m->name, name) == 0 : (m->flags & BAS_MEMBER_DEFAULT) != 0;
    +        int32_t                rank  = match ? memberRank(m->kind, op) : BAS_MEMBER_NO_RANK;
    +
    +        if (rank < bestRank) {
    +            best     = m;
    +            bestRank = rank;
    +        }
    +    }
    +
    +    return best;
    +}
    +
    +
     // Shared numeric formatter for OP_PRINT_USING (numeric fields) and OP_FORMAT.
     // Renders n per the flags in f, right-aligned in f->width: the sign (or a
     // trailing sign), the floating dollar, the thousands-separated integer part
    @@ -5914,6 +7404,25 @@ static void forStackTrim(BasVmT *vm, int32_t newDepth) {
     }
     
     
    +// Flat index of args[0..argc) in arr.  Returns 0 and sets *outIdx, else
    +// the BASIC error number.
    +static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, int32_t *outIdx) {
    +    int32_t indices[BAS_ARRAY_MAX_DIMS];
    +
    +    if (argc > BAS_ARRAY_MAX_DIMS) {
    +        return BAS_ERR_SUBSCRIPT_RANGE;
    +    }
    +
    +    for (int32_t i = 0; i < argc; i++) {
    +        derefArg(&args[i]);
    +        indices[i] = basValToInt32(args[i]);
    +    }
    +
    +    *outIdx = basArrayIndex((BasArrayT *)arr, indices, argc);
    +    return (*outIdx < 0) ? BAS_ERR_SUBSCRIPT_RANGE : 0;
    +}
    +
    +
     // IniRead: the value of key in [sec] of path through dvxPrefs, or a
     // copy of defStr when the file, section or key is missing.
     static BasValueT iniRead(const char *path, const char *sec, const char *key, BasStringT *defStr) {
    @@ -6027,10 +7536,11 @@ static BasValueT ioReadScalar(FILE *fp, uint8_t type) {
                 return basValSingle(val);
             }
     
    -        case BAS_TYPE_DOUBLE: {
    +        case BAS_TYPE_DOUBLE:
    +        case BAS_TYPE_DATE: {
                 double val = 0.0;
                 readRaw(fp, &val, sizeof(val));
    -            return basValDouble(val);
    +            return (type == BAS_TYPE_DATE) ? basValDate(val) : basValDouble(val);
             }
     
             case BAS_TYPE_BOOLEAN: {
    @@ -6079,7 +7589,8 @@ static void ioWriteScalar(BasPutSinkT *sink, uint8_t type, BasValueT val) {
                 break;
             }
     
    -        case BAS_TYPE_DOUBLE: {
    +        case BAS_TYPE_DOUBLE:
    +        case BAS_TYPE_DATE: {
                 double v = basValToNumber(val);
                 sinkWrite(sink, &v, sizeof(v));
                 break;
    @@ -6106,6 +7617,41 @@ static bool isIntegerType(uint8_t type) {
     }
     
     
    +// A one-dimensional array (what JOIN$ and FILTER take).
    +static bool isOneDimArray(BasValueT v) {
    +    return v.type == BAS_TYPE_ARRAY && v.arrVal && v.arrVal->dims == 1;
    +}
    +
    +
    +// JOIN$: the elements of arr as strings with delim between them.  Returns
    +// the length; with out non-NULL the text is also written there (not
    +// terminated).
    +static int32_t joinScan(const BasArrayT *arr, const BasStringT *delim, char *out) {
    +    int32_t len = 0;
    +
    +    for (int32_t i = 0; i < arr->totalElements; i++) {
    +        BasValueT sv = basValToString(arr->elements[i]);
    +
    +        if (i > 0) {
    +            if (out) {
    +                memcpy(out + len, delim->data, delim->len);
    +            }
    +
    +            len += delim->len;
    +        }
    +
    +        if (out) {
    +            memcpy(out + len, sv.strVal->data, sv.strVal->len);
    +        }
    +
    +        len += sv.strVal->len;
    +        basValRelease(&sv);
    +    }
    +
    +    return len;
    +}
    +
    +
     // Reads the next TYPE layout entry (see BAS_LAYOUT_* in opcodes.h).
     // Returns false when the layout is exhausted or truncated.
     static bool layoutNext(const BasStringT *layout, int32_t *pos, BasLayoutEntryT *out) {
    @@ -6162,6 +7708,37 @@ static void localNow(struct tm *out, int32_t *outUsec) {
     }
     
     
    +// AND / OR / XOR / EQV / IMP with a Null operand, as VB: False AND Null is
    +// False, True OR Null is True, False IMP Null and Null IMP True are True;
    +// everything else is Null.  Takes over both operands.
    +static BasVmResultE logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b) {
    +    bool      aNull  = (a.type == BAS_TYPE_NULL);
    +    bool      bNull  = (b.type == BAS_TYPE_NULL);
    +    int32_t   other  = 0;
    +    BasValueT result = basValNull();
    +
    +    if (!(aNull && bNull) && !basValRoundToInt32(aNull ? b : a, INT32_MIN, INT32_MAX, &other)) {
    +        basValRelease(&a);
    +        basValRelease(&b);
    +        return raiseError(vm, BAS_ERR_OVERFLOW);
    +    }
    +
    +    bool known = !(aNull && bNull);
    +
    +    if (known && op == OP_AND && other == 0) {
    +        result = intResult(0);
    +    } else if (known && op == OP_OR && other == BAS_TRUE_BITS) {
    +        result = intResult(BAS_TRUE_BITS);
    +    } else if (known && op == OP_IMP && ((bNull && other == 0) || (aNull && other == BAS_TRUE_BITS))) {
    +        result = intResult(BAS_TRUE_BITS);
    +    }
    +
    +    basValRelease(&a);
    +    basValRelease(&b);
    +    return push(vm, result) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
    +}
    +
    +
     // Send LPRINT output; after a failure the rest of the statement is dropped.
     static void lprintWrite(BasVmT *vm, const char *data, int32_t len) {
         if (!vm->lprintFailed && len > 0) {
    @@ -6183,6 +7760,181 @@ static BasVmResultE mathRangeError(BasVmT *vm, double result) {
     }
     
     
    +// How well a member of kind serves an access op: 0 as intended, 1 as a
    +// fallback (Let through Property Set and back), BAS_MEMBER_NO_RANK not.
    +static int32_t memberRank(uint8_t kind, int32_t op) {
    +    switch (kind) {
    +        case BAS_MEMBER_VAR:
    +            return 0;
    +
    +        case BAS_MEMBER_GET:
    +        case BAS_MEMBER_FUNCTION:
    +        case BAS_MEMBER_SUB:
    +            return (op == BAS_MEMBER_OP_GET) ? 0 : BAS_MEMBER_NO_RANK;
    +
    +        case BAS_MEMBER_LET:
    +            return (op == BAS_MEMBER_OP_LET) ? 0 : ((op == BAS_MEMBER_OP_SET) ? 1 : BAS_MEMBER_NO_RANK);
    +
    +        case BAS_MEMBER_SET:
    +            return (op == BAS_MEMBER_OP_SET) ? 0 : ((op == BAS_MEMBER_OP_LET) ? 1 : BAS_MEMBER_NO_RANK);
    +
    +        default:
    +            return BAS_MEMBER_NO_RANK;
    +    }
    +}
    +
    +
    +// Member variable idx of the running procedure's Me.  Raises the error
    +// and returns NULL outside a class instance.
    +static BasValueT *memberSlot(BasVmT *vm, uint16_t idx) {
    +    BasValueT me = currentFrame(vm)->me;
    +
    +    if (me.type == BAS_TYPE_INSTANCE && me.instVal && idx < (uint16_t)me.instVal->fieldCount) {
    +        return &me.instVal->fields[idx];
    +    }
    +
    +    runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Member variable used outside a class instance");
    +    return NULL;
    +}
    +
    +
    +// A public variable member read or assigned through an object: obj.v,
    +// obj.v(i) (an array element, or the default member of an object held
    +// there) and the same with a value (Let / Set; argc does not count it).
    +// Takes over args and value.
    +static BasVmResultE memberVarAccess(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, BasValueT *value, int32_t op) {
    +    if (m->index < 0 || m->index >= obj->fieldCount) {
    +        dropArgs(args, argc);
    +
    +        if (value) {
    +            basValRelease(value);
    +        }
    +
    +        return raiseError(vm, BAS_ERR_NO_MEMBER);
    +    }
    +
    +    BasValueT *field = &obj->fields[m->index];
    +
    +    // obj.v(args): an element of the array v holds, or v's default member
    +    if (argc > 0) {
    +        if (field->type == BAS_TYPE_INSTANCE && field->instVal) {
    +            BasObjectT  *inner = basObjectRef(field->instVal);
    +            BasVmResultE rc    = objectAccess(vm, inner, NULL, args, argc, value, op);
    +
    +            basObjectUnref(inner);
    +            return rc;
    +        }
    +
    +        int32_t flat = 0;
    +        int32_t err  = (field->type == BAS_TYPE_ARRAY && field->arrVal) ? indexArray(field->arrVal, args, argc, &flat) : BAS_ERR_TYPE_MISMATCH;
    +
    +        dropArgs(args, argc);
    +
    +        if (err != 0) {
    +            if (value) {
    +                basValRelease(value);
    +            }
    +
    +            return raiseError(vm, err);
    +        }
    +
    +        if (value) {
    +            return storeElement(vm, &field->arrVal->elements[flat], *value) ? BAS_VM_OK : BAS_VM_TYPE_MISMATCH;
    +        }
    +
    +        return push(vm, basValCopy(field->arrVal->elements[flat])) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
    +    }
    +
    +    if (!value) {
    +        return push(vm, basValCopy(*field)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
    +    }
    +
    +    derefArg(value);
    +
    +    BasVmResultE rc = storeConvert(vm, m->dataType, value);
    +
    +    if (rc != BAS_VM_OK) {
    +        return rc;
    +    }
    +
    +    // The conversion ran no BASIC code, so the object still holds field
    +    storeValue(field, *value);
    +    return BAS_VM_OK;
    +}
    +
    +
    +// OP_NEW_OBJ: pushes a new Collection, or a new instance of class cls
    +// and runs its initialisation (member variables, then Class_Initialize)
    +// as a call made from here.
    +static BasVmResultE newObject(BasVmT *vm, uint16_t cls) {
    +    if (cls == BAS_NEW_COLLECTION) {
    +        BasObjectT *coll = basObjectNew(BAS_OBJ_COLLECTION, 0, &vm->liveObjects);
    +
    +        if (!coll) {
    +            return raiseError(vm, BAS_ERR_OUT_OF_MEMORY);
    +        }
    +
    +        return push(vm, basValInstance(coll)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
    +    }
    +
    +    if (cls >= vm->module->classCount) {
    +        runtimeError(vm, BAS_ERR_BAD_OPCODE, "Bad class number");
    +        return BAS_VM_BAD_OPCODE;
    +    }
    +
    +    if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
    +        return BAS_VM_CALL_OVERFLOW;
    +    }
    +
    +    const BasClassInfoT *info = &vm->module->classes[cls];
    +    BasObjectT          *obj  = basObjectNew(cls, info->varCount, &vm->liveObjects);
    +
    +    if (!obj) {
    +        return raiseError(vm, BAS_ERR_OUT_OF_MEMORY);
    +    }
    +
    +    // One reference for the stack, one for the initialisation's Me
    +    basObjectRef(obj);
    +
    +    if (!push(vm, basValInstance(obj))) {
    +        basObjectUnref(obj);
    +        return BAS_VM_STACK_OVERFLOW;
    +    }
    +
    +    // Class_Terminate only for an object that exists
    +    if (info->terminateProc >= 0) {
    +        obj->finalize    = queueTerminate;
    +        obj->finalizeCtx = vm;
    +    }
    +
    +    // A class with no set-up code (none compiled) is ready as it is
    +    if (info->initCodeAddr < 0) {
    +        basObjectUnref(obj);
    +        return BAS_VM_OK;
    +    }
    +
    +    BasCallFrameT *frame = pushFrame(vm, vm->pc, info->initCodeAddr, 0);
    +
    +    frame->me = basValInstance(obj);
    +    vm->pc    = info->initCodeAddr;
    +    return BAS_VM_OK;
    +}
    +
    +
    +// A one-dimensional 0-based array of count zero elements of elementType
    +// (LBOUND 0, UBOUND -1 when count is 0).  NULL when out of memory.
    +static BasArrayT *newVector(int32_t count, uint8_t elementType) {
    +    if (count == 0) {
    +        return basArrayNewEmpty(elementType);
    +    }
    +
    +    int32_t lb = 0;
    +    int32_t ub = count - 1;
    +
    +    return basArrayNew(1, &lb, &ub, elementType);
    +}
    +
    +
     // Returns the bytecode address the error dispatcher records for RESUME
     // NEXT: where execution continues with the eval stack at the failing
     // statement's boundary.  Walks forward from start -- the failing
    @@ -6253,6 +8005,156 @@ static BasVmResultE noFormRuntime(BasVmT *vm) {
     }
     
     
    +// The Null and object rules of an arithmetic or comparison operator: when
    +// either operand is Null both are dropped and Null is pushed; a class
    +// instance or Collection has no value to compute with (Type mismatch --
    +// compare objects with IS).  Returns true when the operator is done
    +// (*rc set).
    +static bool nullOperands(BasVmT *vm, BasValueT *a, BasValueT *b, BasVmResultE *rc) {
    +    bool isObj = (a->type == BAS_TYPE_INSTANCE || b->type == BAS_TYPE_INSTANCE);
    +
    +    if (!isObj && a->type != BAS_TYPE_NULL && b->type != BAS_TYPE_NULL) {
    +        return false;
    +    }
    +
    +    basValRelease(a);
    +    basValRelease(b);
    +
    +    if (isObj) {
    +        *rc = raiseError(vm, BAS_ERR_TYPE_MISMATCH);
    +    } else {
    +        *rc = push(vm, basValNull()) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
    +    }
    +
    +    return true;
    +}
    +
    +
    +// obj.name(args) on a class instance or Collection (name NULL: the
    +// default member): a read (BAS_MEMBER_OP_GET), or with value a Let or
    +// Set.  Takes over args and value.  A read pushes its value, at once or
    +// when the member procedure returns.
    +static BasVmResultE objectAccess(BasVmT *vm, BasObjectT *obj, const char *name, BasValueT *args, int32_t argc, BasValueT *value, int32_t op) {
    +    BasValueT callArgs[BAS_VM_MAX_CALL_ARGS + 1];
    +    int32_t   total = argc;
    +
    +    if (argc > 0) {
    +        memcpy(callArgs, args, (size_t)argc * sizeof(BasValueT));
    +    }
    +
    +    // Property Let / Set take the value as their last argument
    +    if (value) {
    +        callArgs[total++] = *value;
    +    }
    +
    +    if (obj->classIdx == BAS_OBJ_COLLECTION) {
    +        return collectionAccess(vm, obj, name, callArgs, total, op);
    +    }
    +
    +    const BasClassInfoT   *cls = objectClass(vm, obj);
    +    const BasClassMemberT *m   = cls ? findMember(cls, name, op) : NULL;
    +
    +    if (!m) {
    +        dropArgs(callArgs, total);
    +        return raiseError(vm, BAS_ERR_NO_MEMBER);
    +    }
    +
    +    if (m->kind == BAS_MEMBER_VAR) {
    +        return memberVarAccess(vm, obj, m, callArgs, argc, value ? &callArgs[argc] : NULL, op);
    +    }
    +
    +    return callMember(vm, obj, m, callArgs, total, op == BAS_MEMBER_OP_GET);
    +}
    +
    +
    +// OP_OBJ_CHECK: 0 when v may be stored where class cls (an index,
    +// BAS_NEW_COLLECTION or BAS_OBJ_CHECK_ANY) is declared -- an object of
    +// that class, or Nothing -- else the BASIC error number.
    +static int32_t objectCheck(BasValueT v, uint16_t cls) {
    +    if (!basValIsObject(v)) {
    +        return BAS_ERR_OBJECT_REQUIRED;
    +    }
    +
    +    if (cls == BAS_OBJ_CHECK_ANY || (v.type == BAS_TYPE_OBJECT && !v.objVal)) {
    +        return 0;
    +    }
    +
    +    int32_t want = (cls == BAS_NEW_COLLECTION) ? BAS_OBJ_COLLECTION : (int32_t)cls;
    +
    +    return (v.type == BAS_TYPE_INSTANCE && v.instVal && v.instVal->classIdx == want) ? 0 : BAS_ERR_TYPE_MISMATCH;
    +}
    +
    +
    +// The class module an instance belongs to (NULL for a Collection).
    +static const BasClassInfoT *objectClass(const BasVmT *vm, const BasObjectT *obj) {
    +    if (obj->classIdx < 0 || obj->classIdx >= vm->module->classCount) {
    +        return NULL;
    +    }
    +
    +    return &vm->module->classes[obj->classIdx];
    +}
    +
    +
    +// obj(args) (OP_LOAD_ARRAY) or obj(args) = value (OP_STORE_ARRAY) on a
    +// class instance or Collection: its default member.  The operands are on
    +// the stack (the value on top for a store).
    +static BasVmResultE objectDefault(BasVmT *vm, int32_t argc, bool isStore) {
    +    BasValueT args[BAS_VM_MAX_CALL_ARGS];
    +    BasValueT value;
    +    BasValueT objVal;
    +
    +    if (argc > BAS_VM_MAX_CALL_ARGS) {
    +        dropValues(vm, argc + 1 + (isStore ? 1 : 0));
    +        return raiseError(vm, BAS_ERR_WRONG_ARGS);
    +    }
    +
    +    if (isStore) {
    +        pop(vm, &value);
    +    }
    +
    +    vm->sp -= argc;
    +    memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT));
    +    pop(vm, &objVal);
    +
    +    BasVmResultE rc = objectAccess(vm, objVal.instVal, NULL, args, argc, isStore ? &value : NULL, isStore ? BAS_MEMBER_OP_LET : BAS_MEMBER_OP_GET);
    +
    +    basValRelease(&objVal);
    +    return rc;
    +}
    +
    +
    +// objectAccess for the member-access opcodes: takes over the object and
    +// member-name values as well.
    +static BasVmResultE objectMemberOp(BasVmT *vm, BasValueT objVal, BasValueT nameVal, BasValueT *args, int32_t argc, BasValueT *value, int32_t op) {
    +    BasValueT    sv = basValToString(nameVal);
    +    BasVmResultE rc = objectAccess(vm, objVal.instVal, sv.strVal->data, args, argc, value, op);
    +
    +    basValRelease(&sv);
    +    basValRelease(&nameVal);
    +    basValRelease(&objVal);
    +    return rc;
    +}
    +
    +
    +// TypeName of an object: "Nothing", the class name, "Collection", or what
    +// the host calls a form or control ("Object" when it does not say).
    +static const char *objectTypeName(const BasVmT *vm, BasValueT v) {
    +    if (v.type == BAS_TYPE_INSTANCE && v.instVal) {
    +        const BasClassInfoT *cls = objectClass(vm, v.instVal);
    +
    +        return cls ? cls->name : BAS_TYPENAME_COLLECTION;
    +    }
    +
    +    if (v.type != BAS_TYPE_OBJECT || !v.objVal) {
    +        return BAS_TYPENAME_NOTHING;
    +    }
    +
    +    const char *name = vm->ui.typeName ? vm->ui.typeName(vm->ui.ctx, v.objVal) : NULL;
    +
    +    return name ? name : BAS_TYPENAME_OBJECT;
    +}
    +
    +
     // OPEN "LPTn:" / "PRN" FOR OUTPUT or APPEND: the channel writes to an
     // anonymous spool file that goes to the printer at CLOSE.
     static BasVmResultE openPrinterChannel(BasVmT *vm, int32_t channel, int32_t port, uint8_t mode, int32_t recLen) {
    @@ -6302,6 +8204,17 @@ static inline bool operandFits(BasVmT *vm, int32_t size) {
     }
     
     
    +// Appends value as argument idx when the call left it out (the arguments
    +// before it are all present); otherwise value is released.
    +static void padArg(BasValueT *args, int32_t *argc, int32_t idx, BasValueT value) {
    +    if (*argc == idx) {
    +        args[(*argc)++] = value;
    +    } else {
    +        basValRelease(&value);
    +    }
    +}
    +
    +
     // Scans a PRINT USING / FORMAT$ numeric format string into f.  Both
     // statements share the placeholder grammar: '#' and '0' (and '*' before
     // the decimal point) are digit positions, '0' also fixes the minimum
    @@ -6456,11 +8369,13 @@ static void popCallFrame(BasVmT *vm) {
             vm->outArgFrame = -1;
         }
     
    -    // Release locals
    +    // Release locals and Me
         for (int32_t i = 0; i < frame->localCount; i++) {
             basValRelease(&frame->locals[i]);
         }
     
    +    basValRelease(&frame->me);
    +
         // Discard any FOR frames the callee left open (EXIT SUB/FUNCTION
         // from inside a FOR body jumps straight to this epilogue) and
         // restore the caller's statement boundary for error dispatch.
    @@ -6551,6 +8466,8 @@ static void primeModuleFrame(BasVmT *vm) {
         vm->callStack[0].savedGosubDepth = 0;
         vm->callStack[0].savedStmtPc     = -1;
         vm->callStack[0].savedStmtSp     = 0;
    +    vm->callStack[0].pushResult      = false;
    +    memset(&vm->callStack[0].me, 0, sizeof(vm->callStack[0].me));
     
         // Hosts that run module-level expressions (the IDE's watch evaluator)
         // widen localCount themselves, so leave the slots zeroed.
    @@ -6561,7 +8478,16 @@ static void primeModuleFrame(BasVmT *vm) {
     // The output column of what PRINT is writing to: the screen, or the
     // printer during an LPRINT.
     static int32_t *printColumnOf(BasVmT *vm) {
    -    return vm->lprinting ? &vm->lprintColumn : &vm->printColumn;
    +    if (vm->lprinting) {
    +        return &vm->lprintColumn;
    +    }
    +
    +    // The program screen keeps its own cursor (LOCATE moves it)
    +    if (vm->screen) {
    +        vm->printColumn = basScreenColumn(vm->screen);
    +    }
    +
    +    return &vm->printColumn;
     }
     
     
    @@ -6601,34 +8527,18 @@ static bool printerDevice(const char *name, int32_t *outPort) {
     }
     
     
    -// PRINT / PRINT # text for one value: its string form, followed by the
    -// space PRINT puts after every non-string value.
    -static BasStringT *printString(BasValueT val) {
    -    BasStringT *text = basValFormatString(val);
    -
    -    if (val.type == BAS_TYPE_STRING) {
    -        return text;
    -    }
    -
    -    BasStringT *out = basStringAlloc(text->len + 2);
    -
    -    if (out->cap >= text->len + 2) {
    -        memcpy(out->data, text->data, text->len);
    -        out->data[text->len]     = ' ';
    -        out->data[text->len + 1] = '\0';
    -        out->len                 = text->len + 1;
    -    }
    -
    -    basStringUnref(text);
    -    return out;
    -}
    -
    -
     // Every PRINT byte goes through here so the output column is known for
     // comma zones and TAB().
     static void printText(BasVmT *vm, const char *text, bool newline) {
         int32_t *column = printColumnOf(vm);
     
    +    // Once the program has its screen, PRINT writes there
    +    if (!vm->lprinting && vm->screen) {
    +        basScreenPrint(vm->screen, text, newline);
    +        *column = basScreenColumn(vm->screen);
    +        return;
    +    }
    +
         if (vm->lprinting) {
             lprintWrite(vm, text, (int32_t)strlen(text));
     
    @@ -6706,6 +8616,8 @@ static BasCallFrameT *pushFrame(BasVmT *vm, int32_t returnPc, int32_t codeAddr,
         frame->savedGosubDepth = vm->gosubDepth;
         frame->savedStmtPc     = vm->stmtPc;
         frame->savedStmtSp     = vm->stmtSp;
    +    frame->pushResult      = false;
    +    memset(&frame->me, 0, sizeof(frame->me));
         memset(frame->locals, 0, sizeof(BasValueT) * frame->localCount);
         return frame;
     }
    @@ -6762,6 +8674,26 @@ static BasVmResultE putValue(BasVmT *vm, BasPutSinkT *sink, int32_t mode, BasVal
     }
     
     
    +// Finalizer of instances whose class has a Class_Terminate: the object
    +// (now unreferenced) waits in the VM's queue, alive again, until
    +// basVmStep starts its Class_Terminate after the current instruction.
    +static void queueTerminate(void *ctx, BasObjectT *obj) {
    +    BasVmT *vm = (BasVmT *)ctx;
    +
    +    obj->refCount      = 1;
    +    obj->finalize      = NULL;
    +    obj->termNext      = vm->terminateQueue;
    +    vm->terminateQueue = obj;
    +}
    +
    +
    +// Raises BASIC error errNum with its standard message.
    +static BasVmResultE raiseError(BasVmT *vm, int32_t errNum) {
    +    runtimeError(vm, errNum, errorText(errNum));
    +    return BAS_VM_ERROR;
    +}
    +
    +
     // INPUT #: reads one field the way WRITE # wrote it -- leading blanks are
     // skipped, a quoted field runs to the closing quote, an unquoted field runs
     // to the next comma or end of line, and the field's delimiter (comma or
    @@ -7021,11 +8953,13 @@ static void releaseVmState(BasVmT *vm) {
             basValRelease(&vm->globals[i]);
         }
     
    -    // Release call frame locals
    +    // Release call frame locals and Me
         for (int32_t d = 0; d < vm->callDepth; d++) {
             for (int32_t i = 0; i < vm->callStack[d].localCount; i++) {
                 basValRelease(&vm->callStack[d].locals[i]);
             }
    +
    +        basValRelease(&vm->callStack[d].me);
         }
     
         // Release FOR stack
    @@ -7036,6 +8970,43 @@ static void releaseVmState(BasVmT *vm) {
     }
     
     
    +// REPLACE$: s from byte start on with up to count (BAS_COUNT_ALL: every)
    +// occurrences of find replaced by repl.  Returns the result length; with
    +// out non-NULL the result is also written there (not terminated).
    +static int32_t replaceScan(const BasStringT *s, int32_t start, const BasStringT *find, const BasStringT *repl, int32_t count, bool ignoreCase, char *out) {
    +    int32_t len  = 0;
    +    int32_t done = 0;
    +    int32_t pos  = start;
    +
    +    while (pos < s->len && find->len > 0 && (count == BAS_COUNT_ALL || done < count)) {
    +        int32_t hit = stringFind(s, pos, find, ignoreCase);
    +
    +        if (hit < 0) {
    +            break;
    +        }
    +
    +        if (out) {
    +            memcpy(out + len, s->data + pos, hit - pos);
    +            memcpy(out + len + hit - pos, repl->data, repl->len);
    +        }
    +
    +        len += hit - pos + repl->len;
    +        pos  = hit + find->len;
    +        done++;
    +    }
    +
    +    if (pos < s->len) {
    +        if (out) {
    +            memcpy(out + len, s->data + pos, s->len - pos);
    +        }
    +
    +        len += s->len - pos;
    +    }
    +
    +    return len;
    +}
    +
    +
     // Rounds a numeric operand to LONG (banker's rounding) for the integer
     // operators; raises Overflow when it does not fit.
     static BasVmResultE roundedOperand(BasVmT *vm, BasValueT v, int32_t *out) {
    @@ -7122,7 +9093,7 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
             if (++stepsSinceYield >= BAS_VM_DEFAULT_STEP_SLICE) {
                 stepsSinceYield = 0;
     
    -            if (vm->doEventsFn && !vm->doEventsFn(vm->doEventsCtx)) {
    +            if (!basVmDoEvents(vm)) {
                     vm->running = false;
                     break;
                 }
    @@ -7142,6 +9113,9 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
         vm->pc      = savedPc;
         vm->running = savedRunning;
     
    +    // Show what the handler drew before the host carries on
    +    basScreenPresent(vm);
    +
         // If we paused at a breakpoint during this sub, notify the host
         // so it can pause the event loop before any new event fires.
         if (hadBreakpoint && vm->breakpointFn) {
    @@ -7158,9 +9132,9 @@ static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg) {
         vm->lprinting   = false;
     
         if (vm->currentLine > 0) {
    -        snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d on line %d: %s", (int)errNum, (int)vm->currentLine, msg);
    +        snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d on line %d" BAS_VM_ERROR_TEXT_SEP "%s", (int)errNum, (int)vm->currentLine, msg);
         } else {
    -        snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d: %s", (int)errNum, (int)vm->currentOpPc, msg);
    +        snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d" BAS_VM_ERROR_TEXT_SEP "%s", (int)errNum, (int)vm->currentOpPc, msg);
         }
     }
     
    @@ -7173,6 +9147,10 @@ static BasValueT *scopedVarSlot(BasVmT *vm, uint16_t varIdx, uint8_t scopeTag) {
         BasValueT  *slot = NULL;
         const char *what = "Invalid global variable index";
     
    +    if (scopeTag == SCOPE_MEMBER) {
    +        return memberSlot(vm, varIdx);
    +    }
    +
         if (scopeTag == SCOPE_LOCAL) {
             BasCallFrameT *frame = currentFrame(vm);
     
    @@ -7296,7 +9274,7 @@ static bool sleepSeconds(BasVmT *vm, int32_t secs) {
         double deadline = wallSeconds() + secs;
     
         do {
    -        if (!vm->doEventsFn(vm->doEventsCtx)) {
    +        if (!basVmDoEvents(vm)) {
                 return false;
             }
     
    @@ -7309,6 +9287,89 @@ static bool sleepSeconds(BasVmT *vm, int32_t secs) {
     }
     
     
    +// SPLIT: the pieces of s between occurrences of delim, at most limit of
    +// them (BAS_COUNT_ALL: no limit; the last piece keeps the rest).  Returns
    +// how many there are; with out non-NULL they are also stored into out's
    +// elements.  An empty s has no pieces; an empty delim gives s whole.
    +static int32_t splitScan(const BasStringT *s, const BasStringT *delim, int32_t limit, bool ignoreCase, BasArrayT *out) {
    +    int32_t n   = 0;
    +    int32_t pos = 0;
    +
    +    if (s->len == 0 || limit == 0) {
    +        return 0;
    +    }
    +
    +    while (true) {
    +        int32_t hit = (delim->len > 0 && (limit == BAS_COUNT_ALL || n < limit - 1)) ? stringFind(s, pos, delim, ignoreCase) : -1;
    +        int32_t end = (hit < 0) ? s->len : hit;
    +
    +        if (out) {
    +            basValRelease(&out->elements[n]);
    +            out->elements[n] = strValue(basStringSub(s, pos, end - pos));
    +        }
    +
    +        n++;
    +
    +        if (hit < 0) {
    +            return n;
    +        }
    +
    +        pos = hit + delim->len;
    +    }
    +}
    +
    +
    +// Starts the Class_Terminate of the next queued instance as a call made
    +// between two instructions.  The call's Me holds the queue's reference;
    +// when Class_Terminate returns the object is freed (unless it stored a
    +// reference to itself somewhere).
    +static BasVmResultE startTerminate(BasVmT *vm) {
    +    BasObjectT *obj = vm->terminateQueue;
    +
    +    vm->terminateQueue = obj->termNext;
    +    obj->termNext      = NULL;
    +
    +    const BasClassInfoT *cls = objectClass(vm, obj);
    +
    +    if (!cls || cls->terminateProc < 0 || vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
    +        basObjectUnref(obj);
    +        return BAS_VM_OK;
    +    }
    +
    +    int32_t        addr  = vm->module->procs[cls->terminateProc].codeAddr;
    +    BasCallFrameT *frame = pushFrame(vm, vm->pc, addr, 0);
    +
    +    frame->me = basValInstance(obj);
    +    vm->pc    = addr;
    +    return BAS_VM_OK;
    +}
    +
    +
    +// Converts *val (in place) to a declared type the way a store into a
    +// variable of that type does (the conversion opcode basConvOpForStore names).
    +static BasVmResultE storeConvert(BasVmT *vm, uint8_t dataType, BasValueT *val) {
    +    uint8_t op = basConvOpForStore(dataType);
    +
    +    if (op == OP_NOP) {
    +        return BAS_VM_OK;
    +    }
    +
    +    if (!push(vm, *val)) {
    +        return BAS_VM_STACK_OVERFLOW;
    +    }
    +
    +    BasVmResultE rc = execConvert(vm, op);
    +
    +    pop(vm, val);
    +
    +    if (rc != BAS_VM_OK) {
    +        basValRelease(val);
    +    }
    +
    +    return rc;
    +}
    +
    +
     // Stores into an array element or TYPE field.  An array reference is
     // refused there: the compiler never emits one (elements and fields are
     // scalars or TYPE instances) and a corrupt module storing an array
    @@ -7344,17 +9405,7 @@ static int32_t stringFind(const BasStringT *haystack, int32_t start, const BasSt
         }
     
         for (int32_t i = start; i + needle->len <= haystack->len; i++) {
    -        bool match = true;
    -
    -        if (!ignoreCase) {
    -            match = (memcmp(haystack->data + i, needle->data, needle->len) == 0);
    -        } else {
    -            for (int32_t k = 0; k < needle->len && match; k++) {
    -                match = (toupper((unsigned char)haystack->data[i + k]) == toupper((unsigned char)needle->data[k]));
    -            }
    -        }
    -
    -        if (match) {
    +        if (stringMatchAt(haystack, i, needle, ignoreCase)) {
                 return i;
             }
         }
    @@ -7363,6 +9414,23 @@ static int32_t stringFind(const BasStringT *haystack, int32_t start, const BasSt
     }
     
     
    +// True when needle occurs in haystack at byte i (the caller keeps i +
    +// needle->len within haystack).
    +static bool stringMatchAt(const BasStringT *haystack, int32_t i, const BasStringT *needle, bool ignoreCase) {
    +    if (!ignoreCase) {
    +        return memcmp(haystack->data + i, needle->data, needle->len) == 0;
    +    }
    +
    +    for (int32_t k = 0; k < needle->len; k++) {
    +        if (toupper((unsigned char)haystack->data[i + k]) != toupper((unsigned char)needle->data[k])) {
    +            return false;
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
     // Wraps a string the caller already owns a reference to (no extra ref).
     static BasValueT strValue(BasStringT *s) {
         BasValueT v;
    @@ -7372,6 +9440,148 @@ static BasValueT strValue(BasStringT *s) {
     }
     
     
    +// Releases every object the VM made that is still alive when it is
    +// destroyed.  Each object is held while all of them let go of what they
    +// hold (breaking reference cycles), then the holds are dropped: objects
    +// nothing else refers to are freed, the rest (still referenced by the
    +// host) leave the VM's list and live on.  No Class_Terminate runs.
    +static void sweepObjects(BasVmT *vm) {
    +    for (BasObjectT *o = vm->liveObjects; o; o = o->liveNext) {
    +        o->finalize = NULL;
    +        o->refCount++;
    +    }
    +
    +    for (BasObjectT *o = vm->liveObjects; o; o = o->liveNext) {
    +        basObjectClear(o);
    +    }
    +
    +    BasObjectT *o = vm->liveObjects;
    +
    +    while (o) {
    +        BasObjectT *next = o->liveNext;
    +
    +        basObjectUnref(o);
    +        o = next;
    +    }
    +
    +    for (o = vm->liveObjects; o; o = vm->liveObjects) {
    +        vm->liveObjects = o->liveNext;
    +        o->liveList     = NULL;
    +        o->livePrev     = NULL;
    +        o->liveNext     = NULL;
    +    }
    +}
    +
    +
    +// ISNUMERIC of a string: optional blanks, a sign, digits with an
    +// optional point and exponent (or an &H / &O number), optional blanks.
    +static bool textIsNumeric(const BasStringT *s) {
    +    const char *p      = s->data;
    +    const char *end    = s->data + s->len;
    +    int32_t     digits = 0;
    +
    +    while (p < end && *p == ' ') {
    +        p++;
    +    }
    +
    +    if (p < end && (*p == '+' || *p == '-')) {
    +        p++;
    +    }
    +
    +    if (end - p >= BAS_RADIX_PREFIX_LEN && p[0] == '&' && (toupper((unsigned char)p[1]) == 'H' || toupper((unsigned char)p[1]) == 'O')) {
    +        bool hex = toupper((unsigned char)p[1]) == 'H';
    +
    +        for (p += BAS_RADIX_PREFIX_LEN; p < end && (hex ? isxdigit((unsigned char)*p) : (*p >= '0' && *p <= '7')); p++) {
    +            digits++;
    +        }
    +    } else {
    +        for (; p < end && isdigit((unsigned char)*p); p++) {
    +            digits++;
    +        }
    +
    +        if (p < end && *p == '.') {
    +            for (p++; p < end && isdigit((unsigned char)*p); p++) {
    +                digits++;
    +            }
    +        }
    +
    +        if (digits > 0 && p < end && strchr(BAS_EXPONENT_CHARS, *p)) {
    +            int32_t expDigits = 0;
    +
    +            p++;
    +
    +            if (p < end && (*p == '+' || *p == '-')) {
    +                p++;
    +            }
    +
    +            for (; p < end && isdigit((unsigned char)*p); p++) {
    +                expDigits++;
    +            }
    +
    +            digits = (expDigits > 0) ? digits : 0;
    +        }
    +    }
    +
    +    while (p < end && *p == ' ') {
    +        p++;
    +    }
    +
    +    return digits > 0 && p == end;
    +}
    +
    +
    +// TypeName of a value of a non-object type (typeId names a TYPE).
    +static const char *typeNameOf(const BasVmT *vm, uint8_t type, int32_t typeId) {
    +    switch (type) {
    +        case BAS_TYPE_EMPTY:
    +            return "Empty";
    +
    +        case BAS_TYPE_NULL:
    +            return "Null";
    +
    +        case BAS_TYPE_INTEGER:
    +            return "Integer";
    +
    +        case BAS_TYPE_LONG:
    +            return "Long";
    +
    +        case BAS_TYPE_SINGLE:
    +            return "Single";
    +
    +        case BAS_TYPE_DOUBLE:
    +            return "Double";
    +
    +        case BAS_TYPE_DATE:
    +            return "Date";
    +
    +        case BAS_TYPE_STRING:
    +            return "String";
    +
    +        case BAS_TYPE_BOOLEAN:
    +            return "Boolean";
    +
    +        case BAS_TYPE_VARIANT:
    +            return "Variant";
    +
    +        case BAS_TYPE_OBJECT:
    +        case BAS_TYPE_INSTANCE:
    +            return BAS_TYPENAME_OBJECT;
    +
    +        case BAS_TYPE_UDT:
    +            for (int32_t i = 0; i < vm->module->debugUdtDefCount; i++) {
    +                if (vm->module->debugUdtDefs[i].typeId == typeId) {
    +                    return vm->module->debugUdtDefs[i].name;
    +                }
    +            }
    +
    +            return BAS_TYPENAME_UDT;
    +
    +        default:
    +            return "Unknown";
    +    }
    +}
    +
    +
     // Gives val its own TYPE instance when the one it carries is shared.  The
     // shared reference is released; a clone failure leaves the value shared.
     static BasValueT udtDetach(BasValueT val) {
    @@ -7765,6 +9975,48 @@ static bool validArrayDims(BasVmT *vm, int32_t dims) {
     }
     
     
    +// VARTYPE of a type tag (VB's vbInteger ... vbVariant).
    +static int32_t varTypeOf(uint8_t type) {
    +    switch (type) {
    +        case BAS_TYPE_EMPTY:
    +            return BAS_VT_EMPTY;
    +
    +        case BAS_TYPE_NULL:
    +            return BAS_VT_NULL;
    +
    +        case BAS_TYPE_INTEGER:
    +            return BAS_VT_INTEGER;
    +
    +        case BAS_TYPE_LONG:
    +            return BAS_VT_LONG;
    +
    +        case BAS_TYPE_SINGLE:
    +            return BAS_VT_SINGLE;
    +
    +        case BAS_TYPE_DOUBLE:
    +            return BAS_VT_DOUBLE;
    +
    +        case BAS_TYPE_DATE:
    +            return BAS_VT_DATE;
    +
    +        case BAS_TYPE_STRING:
    +            return BAS_VT_STRING;
    +
    +        case BAS_TYPE_BOOLEAN:
    +            return BAS_VT_BOOLEAN;
    +
    +        case BAS_TYPE_VARIANT:
    +            return BAS_VT_VARIANT;
    +
    +        case BAS_TYPE_UDT:
    +            return BAS_VT_USERTYPE;
    +
    +        default:
    +            return BAS_VT_OBJECT;
    +    }
    +}
    +
    +
     // Keep the host's events running until busy says the wait is over (or
     // just check again after a pause without an event pump).  False when the
     // host asks the program to stop.
    @@ -7772,7 +10024,7 @@ static bool waitWhile(BasVmT *vm, bool (*busy)(void *ctx)) {
         while (busy(vm->device.ctx)) {
             if (!vm->doEventsFn) {
                 usleep(BAS_WAIT_POLL_USEC);
    -        } else if (!vm->doEventsFn(vm->doEventsCtx)) {
    +        } else if (!basVmDoEvents(vm)) {
                 return false;
             }
         }
    @@ -7809,6 +10061,15 @@ static BasValueT zeroValue(uint8_t type) {
             case BAS_TYPE_DOUBLE:
                 return basValDouble(0.0);
     
    +        case BAS_TYPE_DATE:
    +            return basValDate(0.0);
    +
    +        case BAS_TYPE_VARIANT:
    +            return basValEmpty();
    +
    +        case BAS_TYPE_OBJECT:
    +            return basValObject(NULL);
    +
             default:
                 return basValInteger(0);
         }
    diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h
    index ab5d7700..d7070156 100644
    --- a/src/apps/kpunch/dvxbasic/runtime/vm.h
    +++ b/src/apps/kpunch/dvxbasic/runtime/vm.h
    @@ -63,7 +63,10 @@
     
     #define BAS_RGB_COMPONENT_MAX     255    // RGB(r,g,b): max per-channel value
     #define BAS_PRINT_SPC_MAX         255    // PRINT SPC()/TAB(): max column count
    +#define BAS_PRINT_ZONE_WIDTH      14     // PRINT comma: width of one print zone (columns)
    +#define BAS_INPUT_LINE_LEN        1024   // INPUT / LINE INPUT: longest line read
     #define BAS_VM_ERROR_MSG_LEN      256    // runtime error description buffer
    +#define BAS_VM_ERROR_TEXT_SEP     ": "   // ends the "Runtime error N ..." prefix of a runtime error message, before its text
     
     // ============================================================
     // Result codes
    @@ -173,6 +176,11 @@ typedef void (*BasUiSetEventFnT)(void *ctx, void *ctrlRef, const char *eventName
     // Remove a control from a form by name.
     typedef void (*BasUiRemoveCtrlFnT)(void *ctx, void *formRef, const char *ctrlName);
     
    +// Type name of a host object for TypeName / TypeOf: "Form" for a form,
    +// the control's type ("TextBox", "CommandButton" ...) for a control, NULL
    +// when unknown.
    +typedef const char *(*BasUiTypeNameFnT)(void *ctx, void *objRef);
    +
     // Collected UI callbacks
     typedef struct {
         BasUiGetPropFnT      getProp;
    @@ -191,6 +199,7 @@ typedef struct {
         BasUiCreateCtrlExFnT createCtrlEx;
         BasUiSetEventFnT     setEvent;
         BasUiRemoveCtrlFnT   removeCtrl;
    +    BasUiTypeNameFnT     typeName;
         void                *ctx;        // passed as first arg to all callbacks
     } BasUiCallbacksT;
     
    @@ -274,6 +283,28 @@ typedef struct {
         void     *ctx;
     } BasDeviceCallbacksT;
     
    +// ============================================================
    +// Program screen callbacks (host-provided)
    +// ============================================================
    +//
    +// The program screen (screen.h) lives in the VM from the program's first
    +// console or graphics statement.  The host shows it: modeChanged when it
    +// is made or changes mode (size, palette and text layout), present when
    +// its pixels or palette changed (the VM calls it whenever it gives the
    +// host a turn; the host shows the dirty rectangle, then basScreenClean),
    +// fullScreen for FULLSCREEN ON / OFF (the host sets scr->fullScreen to
    +// what it managed).  Keys reach INKEY$ and INPUT through basScreenPushKey.
    +// Without callbacks the screen still works, unseen.
    +
    +struct BasScreenT;
    +
    +typedef struct {
    +    void  (*modeChanged)(void *ctx, struct BasScreenT *scr);
    +    void  (*present)(void *ctx, struct BasScreenT *scr);
    +    void  (*fullScreen)(void *ctx, struct BasScreenT *scr, bool on);
    +    void   *ctx;
    +} BasScreenHostT;
    +
     // ============================================================
     // External library callbacks (host-provided)
     // ============================================================
    @@ -329,6 +360,8 @@ typedef struct {
         int32_t   savedStmtSp;            // caller's vm->stmtSp at call time (restored on return)
         int32_t   savedGosubDepth;        // vm->gosubDepth when this frame was pushed: its own GOSUB returns sit above it (restored on return)
         bool      inHandler;              // this frame's ON ERROR handler trapped an error and has not RESUMEd yet
    +    bool      pushResult;             // a SUB or Property Let/Set reached through an object: push Empty when it returns (the caller expects a value)
    +    BasValueT me;                     // class module code: the object the procedure runs for (a counted BAS_TYPE_INSTANCE), else zero
         BasValueT locals[BAS_VM_MAX_LOCALS];
     } BasCallFrameT;
     
    @@ -413,6 +446,44 @@ typedef struct {
         int32_t initCodeLen;    // length of init bytecode
     } BasFormVarInfoT;
     
    +// ============================================================
    +// Class modules
    +// ============================================================
    +
    +// What a public member of a class is.  Part of the module format.
    +typedef enum {
    +    BAS_MEMBER_VAR      = 0,  // Public variable
    +    BAS_MEMBER_SUB      = 1,
    +    BAS_MEMBER_FUNCTION = 2,
    +    BAS_MEMBER_GET      = 3,  // Property Get
    +    BAS_MEMBER_LET      = 4,  // Property Let
    +    BAS_MEMBER_SET      = 5   // Property Set
    +} BasMemberKindE;
    +
    +// BasClassMemberT flags.
    +#define BAS_MEMBER_DEFAULT     0x01  // the default member (Attribute Name.VB_UserMemId = 0)
    +#define BAS_MEMBER_PARAMARRAY  0x02  // the last parameter is a ParamArray
    +
    +// A public member, found by name when a program uses obj.Name.
    +typedef struct {
    +    char    name[BAS_MAX_IDENT];
    +    int32_t index;           // BAS_MEMBER_VAR: member variable slot; else index into procs[]
    +    uint8_t kind;            // BasMemberKindE
    +    uint8_t flags;           // BAS_MEMBER_*
    +    uint8_t dataType;        // BAS_MEMBER_VAR: declared type (stores convert to it)
    +    uint8_t requiredParams;  // procedures: arguments that must be given (the rest are Optional)
    +    uint8_t paramTypes[BAS_VM_MAX_CALL_ARGS]; // procedures: declared parameter types (an omitted Optional argument takes the type's zero)
    +} BasClassMemberT;
    +
    +typedef struct {
    +    char             name[BAS_MAX_IDENT];
    +    int32_t          varCount;       // member variables per instance
    +    int32_t          initCodeAddr;   // code that sets up a new instance (member variables, then Class_Initialize), ends with OP_RET
    +    int32_t          terminateProc;  // index into procs[] of Class_Terminate, -1 = none
    +    BasClassMemberT *members;        // public members (malloc'd)
    +    int32_t          memberCount;
    +} BasClassInfoT;
    +
     // ============================================================
     // Compiled module (output of the compiler)
     // ============================================================
    @@ -442,6 +513,8 @@ typedef struct {
         int32_t           formVarInfoCount;
         BasGlobalInitT   *globalInits;      // runtime global type init (survives stripping)
         int32_t           globalInitCount;
    +    BasClassInfoT    *classes;          // class modules (runtime-essential)
    +    int32_t           classCount;
         BasDebugVarT     *debugVars;       // variable names for debugger
         int32_t           debugVarCount;
         BasDebugUdtDefT  *debugUdtDefs;   // UDT type definitions for debugger
    @@ -555,6 +628,13 @@ typedef struct {
         BasSqlCallbacksT     sql;
         BasPrinterCallbacksT printer;
         BasDeviceCallbacksT  device;
    +    BasScreenHostT       screenHost;
    +
    +    // The program screen (NULL until the first console or graphics
    +    // statement), and the host's request to stop the program (its screen
    +    // window was closed): basVmRun and basVmDoEvents then halt.
    +    struct BasScreenT   *screen;
    +    bool                 stopRequested;
     
         // External library callbacks (set by host)
         BasExternCallbacksT ext;
    @@ -564,6 +644,13 @@ typedef struct {
         BasExternCacheEntryT *externCache;
         int32_t               externCacheCount;
     
    +    // Objects: every live class instance / Collection this VM made (so a
    +    // program's reference cycles can be broken when it ends), and the
    +    // instances whose Class_Terminate is waiting to run (linked through
    +    // termNext).
    +    BasObjectT *liveObjects;
    +    BasObjectT *terminateQueue;
    +
         // Current form reference (set during event dispatch)
         void      *currentForm;
         BasValueT *currentFormVars;     // points to current form's variable storage
    @@ -610,6 +697,15 @@ BasVmResultE basVmStep(BasVmT *vm);
     // False when the copy cannot be allocated (COMMAND$ is then empty).
     bool basVmSetCommand(BasVmT *vm, const char *args);
     
    +// Give the host a turn: show what changed on the program screen, then
    +// the DoEvents callback.  False when the program must stop (the host said
    +// so, or basVmRequestStop).
    +bool basVmDoEvents(BasVmT *vm);
    +
    +// Ask the running program to stop at its next turn (basVmRun then
    +// returns BAS_VM_HALTED).
    +void basVmRequestStop(BasVmT *vm);
    +
     // Set I/O callbacks.
     void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);
     void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
    @@ -622,6 +718,9 @@ void basVmSetPrinterCallbacks(BasVmT *vm, const BasPrinterCallbacksT *printer);
     // STRIG, FRAMEWAIT).
     void basVmSetDeviceCallbacks(BasVmT *vm, const BasDeviceCallbacksT *device);
     
    +// Set the program screen callbacks.
    +void basVmSetScreenHost(BasVmT *vm, const BasScreenHostT *host);
    +
     // Set UI callbacks (for form/control system).
     void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql);
     void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);
    diff --git a/src/apps/kpunch/dvxbasic/stub/basstub.c b/src/apps/kpunch/dvxbasic/stub/basstub.c
    index 60538201..3c2a7373 100644
    --- a/src/apps/kpunch/dvxbasic/stub/basstub.c
    +++ b/src/apps/kpunch/dvxbasic/stub/basstub.c
    @@ -231,6 +231,13 @@ int32_t appMain(DxeAppContextT *ctx) {
             return 1;
         }
     
    +    // The program's screen window (console / graphics) carries its name
    +    ShellAppT *self = shellGetApp(ctx->appId);
    +
    +    if (self) {
    +        snprintf(rt->title, sizeof(rt->title), "%s", self->name);
    +    }
    +
         // Register .frm source text for lazy loading
         for (int32_t i = 0; i < BAS_MAX_FORM_RESOURCES; i++) {
             char resName[BAS_RES_FORM_NAME_LEN];
    diff --git a/src/apps/kpunch/dvxbasic/test_compiler.c b/src/apps/kpunch/dvxbasic/test_compiler.c
    index 5314f21c..980e51ff 100644
    --- a/src/apps/kpunch/dvxbasic/test_compiler.c
    +++ b/src/apps/kpunch/dvxbasic/test_compiler.c
    @@ -27,8 +27,13 @@
     #include "compiler/parser.h"
     #include "runtime/vm.h"
     #include "runtime/values.h"
    +#include "runtime/screen.h"
     #include "testCapture.h"
     
    +// The DVX 8x16 font, so screen text draws real glyphs
    +#define DVX_FONT_IMPL
    +#include "dvxFont.h"
    +
     #include 
     #include 
     #include 
    @@ -52,13 +57,30 @@ static int32_t gFailCount = 0;
     #define TEST_TOKEN_NAME_SWEEP  512           // covers the whole token enum
     #define TEST_SWAP_LINES        600           // element SWAPs sharing the temporaries
     #define TEST_EXTERN_REC_LEN    128           // one recorded extern call
    +#define TEST_ROW_TEXT_LEN      96            // one screen text row read back
    +#define TEST_KEYS_MAX          16            // keys a screen test types
    +
    +// Keys a screen test's DoEvents stub types into the program screen (once,
    +// on its first call).
    +typedef struct {
    +    BasVmT *vm;
    +    int32_t keys[TEST_KEYS_MAX];
    +    int32_t count;
    +    int32_t calls;
    +} TestKeysT;
     
     // Function prototypes
    +static void checkTrue(const char *what, bool ok);
     static void compileOnly(const char *name, const char *source);
     static void expectCompileError(const char *name, const char *source, const char *needle);
     static void expectValidatorParse(const char *name, const char *source, const char *needle);
    +static uint8_t fbAt(const BasScreenT *scr, int32_t x, int32_t y);
    +static double globalNum(const BasVmT *vm, const char *name);
    +static void rowText(const BasScreenT *scr, int32_t row, char *out);
     static void runExternProgram(const char *name, const char *source, const char *expected);
     static void runProgram(const char *name, const char *source, const char *expected);
    +static BasVmT *screenRun(const char *name, const char *source, CaptureT *cap, TestKeysT *keys);
    +static void screenRunFree(BasVmT *vm);
     static BasValueT stubCallExtern(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType);
     static bool stubIsMethodValid(void *ctx, const char *wgtType, const char *methodName);
     static bool stubIsPropValid(void *ctx, const char *wgtType, const char *propName);
    @@ -67,8 +89,13 @@ static int32_t stubMsgBox(void *ctx, const char *msg, int32_t flags, const char
     static void *stubResolveExtern(void *ctx, const char *libName, const char *funcName);
     static void testCompilerFixes(void);
     static void testCompilerLimits(void);
    +static void testConsoleGraphics(void);
    +static void testConstEnumParamArray(void);
    +static bool testKeysDoEvents(void *ctx);
     static void testKeywordApi(void);
     static void testLexerLimits(void);
    +static void testObjectsAndVariants(void);
    +static void testStringsAndDates(void);
     static void testTokenNames(void);
     
     // Headless stand-in for the form runtime: MsgBox answers OK so programs
    @@ -78,6 +105,15 @@ static const BasUiCallbacksT sStubUi = { .msgBox = stubMsgBox };
     
     
     
    +// One named check of a screen test.
    +static void checkTrue(const char *what, bool ok) {
    +    if (!ok) {
    +        printf("FAIL: %s\n", what);
    +        gFailCount++;
    +    }
    +}
    +
    +
     // Compile source expecting success without running it (constructs whose
     // execution needs a form runtime or host I/O).
     static void compileOnly(const char *name, const char *source) {
    @@ -177,6 +213,41 @@ static void expectValidatorParse(const char *name, const char *source, const cha
     // Compile and run source with DECLARE LIBRARY resolution stubbed out;
     // every extern call records "Name(types)" (see stubCallExtern) and the
     // record must equal expected.
    +static uint8_t fbAt(const BasScreenT *scr, int32_t x, int32_t y) {
    +    return scr->fb[y * scr->width + x];
    +}
    +
    +
    +// A module-level variable's value by name (the module's debug names).
    +static double globalNum(const BasVmT *vm, const char *name) {
    +    for (int32_t i = 0; i < vm->module->debugVarCount; i++) {
    +        const BasDebugVarT *v = &vm->module->debugVars[i];
    +
    +        if (v->procIndex < 0 && strcasecmp(v->name, name) == 0) {
    +            return basValToNumber(vm->globals[v->index]);
    +        }
    +    }
    +
    +    printf("FAIL: no variable %s\n", name);
    +    gFailCount++;
    +    return 0.0;
    +}
    +
    +
    +// A screen text row, trailing blanks removed.
    +static void rowText(const BasScreenT *scr, int32_t row, char *out) {
    +    int32_t len = scr->cols;
    +
    +    memcpy(out, &scr->chars[row * scr->cols], (size_t)len);
    +
    +    while (len > 0 && out[len - 1] == ' ') {
    +        len--;
    +    }
    +
    +    out[len] = '\0';
    +}
    +
    +
     static void runExternProgram(const char *name, const char *source, const char *expected) {
         printf("=== %s ===\n", name);
     
    @@ -281,6 +352,63 @@ static void runProgram(const char *name, const char *source, const char *expecte
     }
     
     
    +// Compile and run a program that uses the program screen; the VM is
    +// returned for the checks (screenRunFree ends it).  keys (optional) are
    +// typed at the first DoEvents.
    +static BasVmT *screenRun(const char *name, const char *source, CaptureT *cap, TestKeysT *keys) {
    +    printf("=== %s ===\n", name);
    +
    +    BasParserT parser;
    +    basParserInit(&parser, source, (int32_t)strlen(source));
    +
    +    if (!basParse(&parser)) {
    +        printf("FAIL: compile error: %s\n\n", parser.error);
    +        gFailCount++;
    +        basParserFree(&parser);
    +        return NULL;
    +    }
    +
    +    BasModuleT *mod = basParserBuildModule(&parser);
    +    basParserFree(&parser);
    +
    +    if (!mod) {
    +        printf("FAIL: module build failed\n\n");
    +        gFailCount++;
    +        return NULL;
    +    }
    +
    +    BasVmT *vm = basVmCreate();
    +    basVmLoadModule(vm, mod);
    +    captureReset(cap);
    +    basVmSetPrintCallback(vm, capturePrint, cap);
    +    basVmSetUiCallbacks(vm, &sStubUi);
    +
    +    if (keys) {
    +        keys->vm    = vm;
    +        keys->calls = 0;
    +        basVmSetDoEventsCallback(vm, testKeysDoEvents, keys);
    +    }
    +
    +    BasVmResultE result = basVmRun(vm);
    +
    +    if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
    +        printf("FAIL: [VM error %d: %s]\n", result, basVmGetError(vm));
    +        gFailCount++;
    +    }
    +
    +    return vm;
    +}
    +
    +
    +static void screenRunFree(BasVmT *vm) {
    +    BasModuleT *mod = vm->module;
    +
    +    basVmDestroy(vm);
    +    basModuleFree(mod);
    +    printf("\n");
    +}
    +
    +
     // Extern call stub: appends "Func(t,t,...)" to the capture (ctx), one
     // type letter per argument, and returns 0.
     static BasValueT stubCallExtern(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType) {
    @@ -684,7 +812,9 @@ static void testCompilerFixes(void) {
             "DATA -2147483648, -32768%\n"
             "READ a&, b%\n"
             "PRINT a&; b%\n",
    -        "-2147483648 -2147483648 -32768 -2147483648 \n-2147483648 -32768 \n"
    +        // A CONST value is an expression: -&H80000000 negates the LONG
    +        // -2147483648 as it would anywhere else.
    +        "-2147483648 2147483648 -32768 -2147483648 \n-2147483648 -32768 \n"
         );
     
         // S2-078
    @@ -1004,6 +1134,797 @@ static void testCompilerLimits(void) {
     
     
     // The keyword enumeration API used by the IDE's syntax highlighter.
    +// Phase 3: constant expressions, ENUM and ParamArray.
    +// Phase 4: the program screen (console text, SCREEN 12 / 13 graphics,
    +// INKEY$ / INPUT from typed keys, PALETTE, VIEW / WINDOW, FULLSCREEN).
    +static void testConsoleGraphics(void) {
    +    CaptureT  cap;
    +    TestKeysT keys;
    +    char      row[TEST_ROW_TEXT_LEN];
    +    BasVmT   *vm = NULL;
    +
    +    basGfxSetFont(font8x16);
    +
    +    // PRINT goes to the host until the program uses the screen
    +    vm = screenRun("Console: PRINT moves to the screen after CLS",
    +        "PRINT \"before\"\n"
    +        "CLS\n"
    +        "PRINT \"HI\"; 12\n"
    +        "PRINT \"A\"; TAB(10); \"B\"; SPC(2); \"C\"\n"
    +        "PRINT \"X\", \"Y\"\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        checkTrue("host got only the first PRINT", strcmp(cap.buf, "before\n") == 0);
    +        checkTrue("screen made in SCREEN 0 80x25", scr && scr->mode == BAS_SCREEN_MODE_TEXT && scr->cols == 80 && scr->rows == 25 && scr->width == 640 && scr->height == 400);
    +        rowText(scr, 0, row);
    +        checkTrue("row 1 text", strcmp(row, "HI12") == 0);
    +        rowText(scr, 1, row);
    +        checkTrue("TAB and SPC", strcmp(row, "A        B  C") == 0);
    +        rowText(scr, 2, row);
    +        checkTrue("comma zones", strcmp(row, "X             Y") == 0);
    +        checkTrue("cursor on row 4", scr->row == 3 && scr->col == 0);
    +
    +        // 'H' at (0,0): some pixels in the foreground (7), background 0
    +        int32_t on = 0;
    +
    +        for (int32_t y = 0; y < BAS_GFX_GLYPH_TALL; y++) {
    +            for (int32_t x = 0; x < BAS_GFX_GLYPH_W; x++) {
    +                on += fbAt(scr, x, y) == 7;
    +            }
    +        }
    +
    +        checkTrue("glyph drawn in colour 7", on > 0 && fbAt(scr, 0, 0) == 0);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("Console: LOCATE, COLOR, CSRLIN, POS, SCREEN()",
    +        "LOCATE 5, 10\n"
    +        "COLOR 14, 1\n"
    +        "PRINT \"Z\";\n"
    +        "r = CSRLIN\n"
    +        "c = POS(0)\n"
    +        "ch = SCREEN(5, 10)\n"
    +        "at = SCREEN(5, 10, 1)\n"
    +        "LOCATE , , 1\n"
    +        "LOCATE 25, 80\n"
    +        "PRINT \"E\";\n"
    +        "c2 = POS(0)\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        checkTrue("character at row 5 col 10", scr->chars[4 * 80 + 9] == 'Z');
    +        checkTrue("attribute yellow on blue", scr->attrs[4 * 80 + 9] == (14 | 1 << 4));
    +        checkTrue("CSRLIN", globalNum(vm, "r") == 5);
    +        checkTrue("POS after Z", globalNum(vm, "c") == 11);
    +        checkTrue("SCREEN() char", globalNum(vm, "ch") == 'Z');
    +        checkTrue("SCREEN() colour", globalNum(vm, "at") == (14 | 1 << 4));
    +        checkTrue("cursor visible", scr->cursorVisible && scr->cursorDrawn);
    +        checkTrue("last cell, no scroll yet", scr->chars[24 * 80 + 79] == 'E' && scr->row == 24 && globalNum(vm, "c2") == 80);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("Console: scrolling and CLS colours",
    +        "COLOR 7, 4\n"
    +        "CLS\n"
    +        "FOR i = 1 TO 26\n"
    +        "    PRINT i\n"
    +        "NEXT i\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        rowText(scr, 0, row);
    +        checkTrue("scrolled two lines", strcmp(row, "3") == 0);
    +        rowText(scr, 23, row);
    +        checkTrue("last number", strcmp(row, "26") == 0);
    +        checkTrue("background 4 kept", fbAt(scr, 600, 5) == 4 && fbAt(scr, 600, 399) == 4);
    +        screenRunFree(vm);
    +    }
    +
    +    // INKEY$: a character, an extended key, then nothing
    +    memset(&keys, 0, sizeof(keys));
    +    keys.keys[keys.count++] = 'a';
    +    keys.keys[keys.count++] = BAS_SCREEN_KEY_EXT | 0x48;
    +    vm = screenRun("Console: INKEY$",
    +        "CLS\n"
    +        "DoEvents\n"
    +        "a$ = INKEY$\n"
    +        "b$ = INKEY$\n"
    +        "c$ = INKEY$\n"
    +        "n1 = ASC(a$)\n"
    +        "n2 = LEN(b$)\n"
    +        "n3 = ASC(MID$(b$, 2))\n"
    +        "n4 = ASC(b$)\n"
    +        "n5 = LEN(c$)\n",
    +        &cap, &keys);
    +
    +    if (vm) {
    +        checkTrue("INKEY$ character", globalNum(vm, "n1") == 'a');
    +        checkTrue("INKEY$ extended = CHR$(0) + scan", globalNum(vm, "n2") == 2 && globalNum(vm, "n3") == 0x48 && globalNum(vm, "n4") == 0);
    +        checkTrue("INKEY$ empty", globalNum(vm, "n5") == 0);
    +        screenRunFree(vm);
    +    }
    +
    +    // INPUT and LINE INPUT at the console, with Backspace
    +    memset(&keys, 0, sizeof(keys));
    +    keys.keys[keys.count++] = '4';
    +    keys.keys[keys.count++] = '3';
    +    keys.keys[keys.count++] = 8;
    +    keys.keys[keys.count++] = '2';
    +    keys.keys[keys.count++] = 13;
    +    keys.keys[keys.count++] = 'h';
    +    keys.keys[keys.count++] = ',';
    +    keys.keys[keys.count++] = 'i';
    +    keys.keys[keys.count++] = 13;
    +    vm = screenRun("Console: INPUT and LINE INPUT",
    +        "CLS\n"
    +        "INPUT \"N\"; n\n"
    +        "LINE INPUT \"Say: \"; t$\n"
    +        "PRINT n * 2; t$\n",
    +        &cap, &keys);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        checkTrue("INPUT value", globalNum(vm, "n") == 42);
    +        rowText(scr, 0, row);
    +        checkTrue("INPUT echo", strcmp(row, "N? 42") == 0);
    +        rowText(scr, 1, row);
    +        checkTrue("LINE INPUT echo", strcmp(row, "Say: h,i") == 0);
    +        rowText(scr, 2, row);
    +        checkTrue("result line", strcmp(row, "84 h,i") == 0);
    +        checkTrue("cursor hidden again", !scr->cursorDrawn && !scr->inputActive);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("SCREEN 13: PSET, PRESET, LINE, B, BF, STEP, POINT",
    +        "SCREEN 13\n"
    +        "PSET (10, 10), 4\n"
    +        "PSET STEP(1, 1)\n"
    +        "PRESET (12, 12)\n"
    +        "LINE (0, 20)-(9, 20), 2\n"
    +        "LINE -STEP(0, 5), 3\n"
    +        "LINE (30, 30)-(40, 40), 5, B\n"
    +        "LINE (50, 30)-(60, 40), 6, BF\n"
    +        "LINE (70, 30)-(85, 30), 7, , &HFF00\n"
    +        "p = POINT(10, 10)\n"
    +        "q = POINT(500, 10)\n"
    +        "lx = POINT(0)\n"
    +        "ly = POINT(1)\n"
    +        "COLOR 9\n"
    +        "PSET (100, 100)\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        checkTrue("SCREEN 13 is 320x200", scr->mode == BAS_SCREEN_MODE_VGA256 && scr->width == 320 && scr->height == 200 && scr->cols == 40);
    +        checkTrue("PSET colour", fbAt(scr, 10, 10) == 4);
    +        checkTrue("PSET STEP in foreground 15", fbAt(scr, 11, 11) == 15);
    +        checkTrue("PRESET background", fbAt(scr, 12, 12) == 0);
    +        checkTrue("LINE horizontal", fbAt(scr, 0, 20) == 2 && fbAt(scr, 8, 20) == 2 && fbAt(scr, 10, 20) == 0);
    +        checkTrue("LINE -STEP from the last point", fbAt(scr, 9, 25) == 3 && fbAt(scr, 9, 23) == 3);
    +        checkTrue("LINE B outline", fbAt(scr, 30, 30) == 5 && fbAt(scr, 40, 40) == 5 && fbAt(scr, 30, 35) == 5 && fbAt(scr, 35, 35) == 0);
    +        checkTrue("LINE BF filled", fbAt(scr, 55, 35) == 6 && fbAt(scr, 60, 40) == 6);
    +        checkTrue("LINE style", fbAt(scr, 70, 30) == 7 && fbAt(scr, 77, 30) == 7 && fbAt(scr, 78, 30) == 0 && fbAt(scr, 85, 30) == 0);
    +        checkTrue("POINT", globalNum(vm, "p") == 4 && globalNum(vm, "q") == -1);
    +        checkTrue("POINT(0/1) = last point", globalNum(vm, "lx") == 85 && globalNum(vm, "ly") == 30);
    +        checkTrue("COLOR sets the foreground", fbAt(scr, 100, 100) == 9);
    +        checkTrue("default VGA palette", scr->palette[32 * 3 + 2] == 255 && scr->palette[32 * 3] == 0 && scr->palette[40 * 3] == 255 && scr->palette[40 * 3 + 1] == 0 && scr->palette[31 * 3] == 255 && scr->palette[248 * 3 + 1] == 0);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("SCREEN 13: CIRCLE, arcs, aspect, PAINT",
    +        "SCREEN 13\n"
    +        "CIRCLE (160, 100), 40, 5, , , 1\n"
    +        "PAINT (160, 100), 6, 5\n"
    +        "CIRCLE (60, 60), 20, 3\n"
    +        "CIRCLE (60, 150), 20, 4, 0, 3.14159\n"
    +        "CIRCLE (260, 150), 20, 2, -0.0001, -1.5708\n"
    +        "cx = POINT(2)\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        checkTrue("circle outline", fbAt(scr, 200, 100) == 5 && fbAt(scr, 120, 100) == 5 && fbAt(scr, 160, 60) == 5 && fbAt(scr, 160, 140) == 5);
    +        checkTrue("PAINT inside", fbAt(scr, 160, 100) == 6 && fbAt(scr, 190, 100) == 6);
    +        checkTrue("PAINT stops at the border", fbAt(scr, 205, 100) == 0 && fbAt(scr, 0, 0) == 0);
    +        checkTrue("default aspect: round on 4:3", fbAt(scr, 80, 60) == 3 && fbAt(scr, 60, 60 - 17) == 3 && fbAt(scr, 60, 40) == 0);
    +        checkTrue("upper half arc only", fbAt(scr, 60, 150 - 17) == 4 && fbAt(scr, 60, 150 + 17) == 0);
    +        checkTrue("radius lines of negative angles", fbAt(scr, 270, 150) == 2 && fbAt(scr, 260, 145) == 2);
    +        checkTrue("CIRCLE sets the last point", globalNum(vm, "cx") == 260);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("SCREEN 13: DRAW",
    +        "SCREEN 13\n"
    +        "DRAW \"C2 BM100,100 R10 D10 L10 U10\"\n"
    +        "DRAW \"BM200,50 C3 E5 F5 NG3 H2\"\n"
    +        "DRAW \"BM50,150 S8 C4 R5\"\n"
    +        "DRAW \"BM20,180 TA90 C5 R10\"\n"
    +        "DRAW \"BM10,10 A2 C6 R4 P7,6\"\n"
    +        "DRAW \"BM150,150 S4 TA0 C1 M+10,+0\"\n"
    +        "px = POINT(0)\n"
    +        "py = POINT(1)\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        checkTrue("DRAW square", fbAt(scr, 105, 100) == 2 && fbAt(scr, 110, 105) == 2 && fbAt(scr, 105, 110) == 2 && fbAt(scr, 100, 105) == 2 && fbAt(scr, 105, 105) == 0);
    +        checkTrue("DRAW diagonals", fbAt(scr, 205, 45) == 3 && fbAt(scr, 210, 50) == 3);
    +        checkTrue("DRAW N returns", fbAt(scr, 207, 53) == 3 && fbAt(scr, 208, 48) == 3);
    +        checkTrue("DRAW S8 doubles", fbAt(scr, 60, 150) == 4 && fbAt(scr, 61, 150) == 0);
    +        checkTrue("DRAW TA90 turns R upwards", fbAt(scr, 20, 170) == 5 && fbAt(scr, 30, 180) == 0);
    +        checkTrue("DRAW A2 turns R left", fbAt(scr, 6, 10) == 6);
    +        checkTrue("DRAW relative M and the pen position", fbAt(scr, 160, 150) == 1 && globalNum(vm, "px") == 160 && globalNum(vm, "py") == 150);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("PALETTE, PALETTE USING, SCREEN 12",
    +        "SCREEN 12\n"
    +        "PALETTE 1, &H3F0000\n"
    +        "b1 = 1\n"
    +        "DIM pal(15) AS LONG\n"
    +        "FOR i = 0 TO 15\n"
    +        "    pal(i) = -1\n"
    +        "NEXT i\n"
    +        "pal(2) = 63\n"
    +        "PALETTE USING pal\n"
    +        "CIRCLE (320, 240), 100, 2\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        checkTrue("SCREEN 12 is 640x480, 80x30", scr->mode == BAS_SCREEN_MODE_VGA16 && scr->width == 640 && scr->height == 480 && scr->rows == 30);
    +        checkTrue("PALETTE blue", scr->palette[3] == 0 && scr->palette[5] == 255 && scr->paletteChanged);
    +        checkTrue("PALETTE USING red, -1 keeps", scr->palette[6] == 255 && scr->palette[7] == 0 && scr->palette[5] == 255);
    +        checkTrue("SCREEN 12 circle is round (aspect 1)", fbAt(scr, 420, 240) == 2 && fbAt(scr, 320, 140) == 2);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("PALETTE reset, SCREEN 0 EGA numbers",
    +        "PALETTE 1, 63\n"
    +        "w = 1\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        checkTrue("EGA 63 is white", vm->screen->palette[3] == 255 && vm->screen->palette[4] == 255 && vm->screen->palette[5] == 255);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("VIEW and WINDOW",
    +        "SCREEN 13\n"
    +        "VIEW (10, 10)-(50, 50), 1, 2\n"
    +        "PSET (0, 0), 7\n"
    +        "PSET (100, 100), 7\n"
    +        "vx = POINT(0, 0)\n"
    +        "VIEW SCREEN (100, 100)-(150, 150)\n"
    +        "PSET (100, 100), 8\n"
    +        "PSET (10, 10), 8\n"
    +        "VIEW\n"
    +        "WINDOW (0, 0)-(100, 100)\n"
    +        "PSET (0, 0), 9\n"
    +        "PSET (100, 100), 9\n"
    +        "WINDOW SCREEN (0, 0)-(100, 100)\n"
    +        "PSET (0, 0), 10\n"
    +        "WINDOW\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        checkTrue("VIEW fill and border", fbAt(scr, 30, 30) == 1 && fbAt(scr, 9, 9) == 2 && fbAt(scr, 51, 30) == 2);
    +        checkTrue("VIEW origin and clipping", fbAt(scr, 10, 10) == 7 && fbAt(scr, 110, 110) == 0);
    +        checkTrue("VIEW POINT is view-relative", globalNum(vm, "vx") == 7);
    +        checkTrue("VIEW SCREEN absolute", fbAt(scr, 100, 100) == 8 && fbAt(scr, 10, 10) == 7);
    +        checkTrue("WINDOW y up", fbAt(scr, 0, 199) == 9 && fbAt(scr, 319, 0) == 9);
    +        checkTrue("WINDOW SCREEN y down", fbAt(scr, 0, 0) == 10);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("CLS 1 clears only the view; WIDTH; FULLSCREEN without a host",
    +        "SCREEN 13\n"
    +        "LINE (0, 0)-(319, 199), 4, BF\n"
    +        "VIEW (10, 10)-(20, 20)\n"
    +        "CLS\n"
    +        "VIEW\n"
    +        "SCREEN 0\n"
    +        "WIDTH 80, 50\n"
    +        "FULLSCREEN\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        BasScreenT *scr = vm->screen;
    +
    +        checkTrue("WIDTH 80, 50", scr->mode == BAS_SCREEN_MODE_TEXT && scr->rows == 50 && scr->cellH == BAS_GFX_GLYPH_SHORT);
    +        checkTrue("FULLSCREEN flag", scr->fullScreen);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("CLS in a view",
    +        "SCREEN 13\n"
    +        "LINE (0, 0)-(319, 199), 4, BF\n"
    +        "VIEW (10, 10)-(20, 20)\n"
    +        "CLS\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        checkTrue("CLS cleared the view only", fbAt(vm->screen, 15, 15) == 0 && fbAt(vm->screen, 5, 5) == 4);
    +        screenRunFree(vm);
    +    }
    +
    +    vm = screenRun("Errors: graphics in SCREEN 0, bad colours, bad WIDTH",
    +        "ON ERROR GOTO h\n"
    +        "PSET (1, 1)\n"
    +        "SCREEN 13\n"
    +        "COLOR 300\n"
    +        "COLOR 1, 2\n"
    +        "WIDTH 80\n"
    +        "SCREEN 7\n"
    +        "DRAW \"Q\"\n"
    +        "PALETTE 1, &H400000\n"
    +        "LOCATE 26\n"
    +        "END\n"
    +        "h:\n"
    +        "n = n + 1\n"
    +        "IF ERR <> 5 THEN bad = bad + 1\n"
    +        "RESUME NEXT\n",
    +        &cap, NULL);
    +
    +    if (vm) {
    +        checkTrue("eight Illegal function calls", globalNum(vm, "n") == 8 && globalNum(vm, "bad") == 0);
    +        screenRunFree(vm);
    +    }
    +
    +    // The program's own names win; LINE syntax errors
    +    runProgram("A variable named COLOR and a SUB named Cls",
    +        "color = 5\n"
    +        "PRINT color\n"
    +        "Cls 3\n"
    +        "SUB Cls (n)\n"
    +        "    PRINT \"mine\"; n\n"
    +        "END SUB\n",
    +        "5 \nmine3 \n"
    +    );
    +
    +    expectCompileError("LINE needs B or BF",
    +        "SCREEN 13\nLINE (0, 0)-(1, 1), 2, X\n",
    +        "B or BF"
    +    );
    +
    +    expectCompileError("VIEW PRINT is not supported",
    +        "VIEW PRINT 1 TO 5\n",
    +        "VIEW PRINT"
    +    );
    +
    +    expectCompileError("PALETTE USING needs an array",
    +        "x = 1\nPALETTE USING x\n",
    +        "array name"
    +    );
    +
    +    compileOnly("Drawing methods compile on objects",
    +        "Picture1.Line (0, 0)-(10, 10), RGB(255, 0, 0), BF\n"
    +        "Picture1.Circle STEP(5, 5), 10\n"
    +        "Me.PSet (1, 2)\n"
    +        "Picture1.Print \"a\"; 1, TAB(5); SPC(2); \"b\";\n"
    +        "Picture1.Cls\n"
    +        "x = Picture1.Point(1, 1)\n"
    +    );
    +}
    +
    +
    +static void testConstEnumParamArray(void) {
    +    // ---- CONST with constant expressions ----
    +
    +    runProgram("CONST arithmetic and earlier constants",
    +        "CONST A = 2 + 3 * 4\n"
    +        "CONST B = (A - 4) \\ 3\n"
    +        "CONST C = A MOD 5, D = -A\n"
    +        "CONST E = 2 ^ 10\n"
    +        "CONST F = 7 / 2\n"
    +        "PRINT A; B; C; D; E; F\n",
    +        "14 3 4 -14 1024 3.5 \n"
    +    );
    +
    +    runProgram("CONST comparison and logic",
    +        "CONST T = 3 > 2\n"
    +        "CONST U = NOT T\n"
    +        "CONST M = &HF0 AND &H3C OR 1\n"
    +        "CONST X = 5 XOR 3\n"
    +        "CONST Q = (1 = 1) AND (2 <> 3)\n"
    +        "PRINT T; U; M; X; Q\n",
    +        // NOT and AND give numbers here as they do at run time
    +        "True 0 49 6 -1 \n"
    +    );
    +
    +    runProgram("CONST string expressions and pure built-ins",
    +        "CONST P = \"DVX\"\n"
    +        "CONST S = P & \" \" + \"BASIC\"\n"
    +        "CONST L = LEN(S)\n"
    +        "CONST CR = CHR$(13)\n"
    +        "CONST H = HEX$(255) & LCASE$(\"AB\") & UCASE$(LEFT$(\"xyz\", 2))\n"
    +        "CONST K = ASC(\"A\") + ABS(-2) + INT(2.7) + FIX(-2.7) + SGN(-9)\n"
    +        "CONST R = RGB(1, 2, 3)\n"
    +        "CONST G = GetGreen(R)\n"
    +        "CONST M2 = MID$(S, 5), T2 = TRIM$(\"  a  \") & STRING$(3, \"-\")\n"
    +        "CONST I = IIf(L > 5, \"long\", \"short\")\n"
    +        "PRINT S; L; ASC(CR); H; K\n"
    +        "PRINT R; G; M2; T2; I\n",
    +        "DVX BASIC9 13 FFabXY66 \n66051 2 BASICa---long\n"
    +    );
    +
    +    runProgram("CONST holding NUL characters",
    +        "CONST Z = \"a\" & CHR$(0) & \"b\"\n"
    +        "PRINT LEN(Z); ASC(MID$(Z, 2, 1)); RIGHT$(Z, 1)\n",
    +        "3 0 b\n"
    +    );
    +
    +    runProgram("CONST AS type converts the expression",
    +        "CONST N AS INTEGER = 10 / 4\n"
    +        "CONST D AS DOUBLE = 1 + 2\n"
    +        "CONST B AS BOOLEAN = 3 - 3\n"
    +        "CONST S AS STRING = \"a\" & \"b\"\n"
    +        "CONST DT AS DATE = DateSerial(2026, 9, 29)\n"
    +        "PRINT N; D; B; S; Year(DT)\n",
    +        "2 3 False ab2026 \n"
    +    );
    +
    +    runProgram("CONST in a SUB and in SELECT CASE",
    +        "CONST LO = 1, HI = LO + 9\n"
    +        "SUB Disp(v)\n"
    +        "  CONST MSG = \"v=\" & \"\"\n"
    +        "  SELECT CASE v\n"
    +        "    CASE LO TO HI: PRINT MSG; v\n"
    +        "    CASE ELSE: PRINT \"out\"\n"
    +        "  END SELECT\n"
    +        "END SUB\n"
    +        "Disp 5\n"
    +        "Disp HI + 1\n",
    +        "v=5 \nout\n"
    +    );
    +
    +    runProgram("OPTION COMPARE TEXT applies to constant comparisons",
    +        "OPTION COMPARE TEXT\n"
    +        "CONST SAME = \"abc\" = \"ABC\"\n"
    +        "PRINT SAME\n",
    +        "True \n"
    +    );
    +
    +    expectCompileError("CONST of a FUNCTION call rejected",
    +        "FUNCTION F()\nF = 1\nEND FUNCTION\n"
    +        "CONST A = F()\n",
    +        "Constant expression required: 'F' is not a constant"
    +    );
    +
    +    expectCompileError("CONST of an undeclared name rejected",
    +        "CONST A = Nope + 1\n",
    +        "Constant expression required: 'Nope' is not a constant"
    +    );
    +
    +    expectCompileError("CONST of a clock built-in rejected",
    +        "CONST A = TIMER\n",
    +        "Constant expression required"
    +    );
    +
    +    expectCompileError("CONST of RND rejected",
    +        "CONST A = RND(1)\n",
    +        "Constant expression required"
    +    );
    +
    +    expectCompileError("CONST of NOW rejected",
    +        "CONST A = Now\n",
    +        "Constant expression required"
    +    );
    +
    +    expectCompileError("CONST referring to itself rejected",
    +        "CONST A = A + 1\n",
    +        "'A' is not a constant"
    +    );
    +
    +    expectCompileError("CONST division by zero reported",
    +        "CONST A = 1 \\ 0\n",
    +        "Error in constant expression: Division by zero"
    +    );
    +
    +    expectCompileError("CONST INTEGER overflow reported",
    +        "CONST A AS INTEGER = 200 * 200\n",
    +        "Overflow in CONST"
    +    );
    +
    +    expectCompileError("CONST string too long rejected",
    +        "CONST A = STRING$(300, \"x\")\n",
    +        "Constant string longer than"
    +    );
    +
    +    expectCompileError("CONST string into a number rejected",
    +        "CONST A AS LONG = \"1\" & \"2\"\n",
    +        "Type mismatch in CONST"
    +    );
    +
    +    expectCompileError("CONST list duplicate rejected",
    +        "CONST A = 1, A = 2\n",
    +        "Duplicate constant"
    +    );
    +
    +    // ---- ENUM ----
    +
    +    runProgram("ENUM auto-numbering, explicit values and continuation",
    +        "ENUM Color\n"
    +        "  Red\n"
    +        "  Green\n"
    +        "  Blue = 10\n"
    +        "  Cyan\n"
    +        "  Dark = -5\n"
    +        "  Darker\n"
    +        "END ENUM\n"
    +        "PRINT Red; Green; Blue; Cyan; Dark; Darker\n"
    +        "PRINT Color.Red; Color.Cyan\n",
    +        "0 1 10 11 -5 -4 \n0 11 \n"
    +    );
    +
    +    runProgram("ENUM values from constant expressions",
    +        "CONST START0 = 100\n"
    +        "ENUM Flags\n"
    +        "  fNone = 0\n"
    +        "  fA = 1\n"
    +        "  fB = fA * 2\n"
    +        "  fC = Flags.fB * 2\n"
    +        "  fAll = fA OR fB OR fC\n"
    +        "  fBase = START0 + 1\n"
    +        "END ENUM\n"
    +        "PRINT fB; fC; fAll; fBase\n",
    +        "2 4 7 101 \n"
    +    );
    +
    +    runProgram("ENUM as a type: DIM, parameters, returns, TYPE fields",
    +        "ENUM Shape\n"
    +        "  Circle = 1\n"
    +        "  Square\n"
    +        "END ENUM\n"
    +        "TYPE Item\n"
    +        "  kind AS Shape\n"
    +        "  size AS INTEGER\n"
    +        "END TYPE\n"
    +        "FUNCTION Other(s AS Shape) AS Shape\n"
    +        "  IF s = Circle THEN Other = Square ELSE Other = Circle\n"
    +        "END FUNCTION\n"
    +        "DIM s AS Shape\n"
    +        "DIM it AS Item\n"
    +        "s = 2.6\n"
    +        "it.kind = Other(s)\n"
    +        "PRINT s; it.kind; Other(Shape.Circle)\n",
    +        "3 1 2 \n"
    +    );
    +
    +    runProgram("ENUM used before its SUB definition and in SELECT CASE",
    +        "ENUM Heading\n"
    +        "  North\n"
    +        "  South\n"
    +        "END ENUM\n"
    +        "Go South\n"
    +        "SUB Go(d AS Heading)\n"
    +        "  SELECT CASE d\n"
    +        "    CASE North: PRINT \"N\"\n"
    +        "    CASE Heading.South: PRINT \"S\"\n"
    +        "  END SELECT\n"
    +        "END SUB\n",
    +        "S\n"
    +    );
    +
    +    runProgram("ENUM member shared by two ENUMs is usable qualified",
    +        "ENUM A\n"
    +        "  Same = 1\n"
    +        "  OnlyA\n"
    +        "END ENUM\n"
    +        "ENUM B\n"
    +        "  Same = 5\n"
    +        "END ENUM\n"
    +        "PRINT A.Same; B.Same; OnlyA\n",
    +        "1 5 2 \n"
    +    );
    +
    +    compileOnly("DECLARE with an ENUM parameter matches its definition",
    +        "ENUM E\n"
    +        "  One = 1\n"
    +        "END ENUM\n"
    +        "DECLARE SUB S(v AS E)\n"
    +        "SUB S(v AS E)\n"
    +        "END SUB\n"
    +    );
    +
    +    expectCompileError("Ambiguous ENUM member rejected unqualified",
    +        "ENUM A\n  Same\nEND ENUM\n"
    +        "ENUM B\n  Same\nEND ENUM\n"
    +        "PRINT Same\n",
    +        "Ambiguous name 'Same'"
    +    );
    +
    +    expectCompileError("Duplicate ENUM member rejected",
    +        "ENUM E\n  X\n  Y\n  X\nEND ENUM\n",
    +        "Duplicate member 'X' in ENUM 'E'"
    +    );
    +
    +    expectCompileError("Duplicate ENUM rejected",
    +        "ENUM E\n  X\nEND ENUM\n"
    +        "ENUM E\n  Y\nEND ENUM\n",
    +        "'E' is already defined"
    +    );
    +
    +    expectCompileError("ENUM member clashing with a CONST rejected",
    +        "CONST X = 1\n"
    +        "ENUM E\n  X\nEND ENUM\n",
    +        "ENUM member 'X' is already defined"
    +    );
    +
    +    expectCompileError("Unknown qualified ENUM member rejected",
    +        "ENUM E\n  X\nEND ENUM\n"
    +        "PRINT E.Y\n",
    +        "'Y' is not a member of ENUM 'E'"
    +    );
    +
    +    expectCompileError("ENUM inside a SUB rejected",
    +        "SUB S\n"
    +        "ENUM E\n  X\nEND ENUM\n"
    +        "END SUB\n",
    +        "ENUM cannot be defined inside SUB or FUNCTION"
    +    );
    +
    +    expectCompileError("ENUM without END ENUM rejected",
    +        "ENUM E\n  X\n",
    +        "Expected END ENUM"
    +    );
    +
    +    expectCompileError("Empty ENUM rejected",
    +        "ENUM E\nEND ENUM\n",
    +        "ENUM 'E' has no members"
    +    );
    +
    +    expectCompileError("ENUM value that is not constant rejected",
    +        "DIM v\n"
    +        "ENUM E\n  X = v\nEND ENUM\n",
    +        "Constant expression required: 'v' is not a constant"
    +    );
    +
    +    expectCompileError("ENUM string value rejected",
    +        "ENUM E\n  X = \"a\"\nEND ENUM\n",
    +        "Type mismatch in ENUM"
    +    );
    +
    +    expectCompileError("ENUM auto value overflow rejected",
    +        "ENUM E\n  X = 2147483647\n  Y\nEND ENUM\n",
    +        "Overflow in ENUM"
    +    );
    +
    +    expectCompileError("ENUM member assignment rejected",
    +        "ENUM E\n  X\nEND ENUM\n"
    +        "X = 3\n",
    +        "Cannot assign to a constant"
    +    );
    +
    +    // ---- ParamArray ----
    +
    +    runProgram("ParamArray receives the remaining arguments",
    +        "SUB ShowAll(title AS STRING, ParamArray items())\n"
    +        "  DIM i\n"
    +        "  PRINT title; LBOUND(items); UBOUND(items)\n"
    +        "  FOR i = LBOUND(items) TO UBOUND(items)\n"
    +        "    PRINT items(i);\n"
    +        "  NEXT\n"
    +        "  PRINT\n"
    +        "END SUB\n"
    +        "ShowAll \"three:\", 1, \"two\", 3.5\n"
    +        "ShowAll \"none:\"\n"
    +        "CALL ShowAll(\"one:\", \"x\")\n",
    +        "three:0 2 \n1 two3.5 \n"
    +        "none:0 -1 \n\n"
    +        "one:0 0 \nx\n"
    +    );
    +
    +    runProgram("Typed ParamArray converts each argument",
    +        "FUNCTION Sum(ParamArray n() AS DOUBLE) AS DOUBLE\n"
    +        "  DIM i, t AS DOUBLE\n"
    +        "  FOR i = 0 TO UBOUND(n)\n"
    +        "    t = t + n(i)\n"
    +        "  NEXT\n"
    +        "  Sum = t\n"
    +        "END FUNCTION\n"
    +        "FUNCTION Cat$(ParamArray s() AS STRING)\n"
    +        "  Cat$ = Join$(s, \"-\")\n"
    +        "END FUNCTION\n"
    +        "PRINT Sum(1, 2.5, \"3\"); Sum()\n"
    +        "PRINT Cat$(1, \"b\", 2.5)\n",
    +        "6.5 0 \n1-b-2.5\n"
    +    );
    +
    +    runProgram("ParamArray called before its definition",
    +        "PRINT Count(1, 2, 3); Count()\n"
    +        "FUNCTION Count(ParamArray a()) AS LONG\n"
    +        "  Count = UBOUND(a) + 1\n"
    +        "END FUNCTION\n",
    +        "3 0 \n"
    +    );
    +
    +    expectCompileError("ParamArray not last rejected",
    +        "SUB S(ParamArray a(), b)\nEND SUB\n",
    +        "ParamArray must be the last parameter"
    +    );
    +
    +    expectCompileError("ParamArray with Optional rejected",
    +        "SUB S(OPTIONAL a, ParamArray b())\nEND SUB\n",
    +        "ParamArray cannot be used with Optional parameters"
    +    );
    +
    +    expectCompileError("ParamArray without () rejected",
    +        "SUB S(ParamArray b)\nEND SUB\n",
    +        "ParamArray parameter must be an array"
    +    );
    +
    +    expectCompileError("ParamArray ByVal rejected",
    +        "SUB S(BYVAL ParamArray b())\nEND SUB\n",
    +        "ParamArray cannot be ByVal or ByRef"
    +    );
    +
    +    expectCompileError("ParamArray of a TYPE rejected",
    +        "TYPE T\n  x AS INTEGER\nEND TYPE\n"
    +        "SUB S(ParamArray b() AS T)\nEND SUB\n",
    +        "ParamArray elements cannot be a TYPE"
    +    );
    +
    +    expectCompileError("ParamArray in DEF FN rejected",
    +        "DEF FNa(ParamArray b()) = 1\n",
    +        "ParamArray is not allowed in DEF FN"
    +    );
    +
    +    expectCompileError("ParamArray in DECLARE LIBRARY rejected",
    +        "DECLARE LIBRARY \"x\"\n"
    +        "DECLARE SUB F(ParamArray b())\n"
    +        "END DECLARE\n",
    +        "ParamArray is not allowed in DECLARE LIBRARY"
    +    );
    +
    +    expectCompileError("Too few arguments before a ParamArray rejected",
    +        "SUB S(a, b, ParamArray c())\nEND SUB\n"
    +        "S 1\n",
    +        "Sub 'S' expects at least 2 arguments, got 1"
    +    );
    +
    +    expectCompileError("Too many ParamArray arguments rejected",
    +        "SUB S(ParamArray c())\nEND SUB\n"
    +        "S 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32\n",
    +        "Too many ParamArray arguments (at most 31)"
    +    );
    +
    +    expectCompileError("DECLARE disagreeing on ParamArray rejected",
    +        "DECLARE SUB S(a())\n"
    +        "SUB S(ParamArray a())\nEND SUB\n",
    +        "DECLARE for 'S' does not match its definition"
    +    );
    +}
    +
    +
    +// DoEvents stub of the screen tests: types the test's keys once.
    +static bool testKeysDoEvents(void *ctx) {
    +    TestKeysT *k = (TestKeysT *)ctx;
    +
    +    if (k->calls++ == 0 && k->vm->screen) {
    +        for (int32_t i = 0; i < k->count; i++) {
    +            basScreenPushKey(k->vm->screen, k->keys[i]);
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
     static void testKeywordApi(void) {
         printf("=== lexer keyword API ===\n");
     
    @@ -1062,6 +1983,1207 @@ static void testLexerLimits(void) {
     }
     
     
    +// Phase 5: Variants, class modules, Collection and FOR EACH.
    +static void testObjectsAndVariants(void) {
    +    runProgram("Variant: Empty, VarType and TypeName",
    +        "DIM v AS VARIANT\n"
    +        "PRINT IsEmpty(v); VarType(v); TypeName(v); \"[\"; v; \"]\"\n"
    +        "PRINT v + 1; \"x\" & v & \"y\"; v = 0; v = \"\"\n"
    +        "v = 5\n"
    +        "PRINT VarType(v); TypeName(v)\n"
    +        "v = 2.5\n"
    +        "PRINT VarType(v); TypeName(v)\n"
    +        "v = \"12.5\"\n"
    +        "PRINT VarType(v); TypeName(v)\n"
    +        "v = #1/2/2000#\n"
    +        "PRINT VarType(v); TypeName(v); IsDate(v)\n"
    +        "v = TRUE\n"
    +        "PRINT VarType(v); TypeName(v)\n"
    +        "PRINT vbEmpty; vbNull; vbInteger; vbString; vbObject; vbVariant; vbArray\n",
    +        "True 0 Empty[]\n"
    +        "1 xyTrue True \n"
    +        "2 Integer\n"
    +        "5 Double\n"
    +        "8 String\n"
    +        "7 DateTrue \n"
    +        "11 Boolean\n"
    +        "0 1 2 8 9 12 8192 \n"
    +    );
    +
    +    runProgram("Variant: IsNumeric, IsObject, IsArray, CVar",
    +        "PRINT IsNumeric(\"12.5\"); IsNumeric(\"abc\"); IsNumeric(\" -1e3 \"); IsNumeric(\"&HFF\"); IsNumeric(\"\"); IsNumeric(7); IsNumeric(#1/1/2000#)\n"
    +        "DIM o AS OBJECT\n"
    +        "PRINT IsObject(o); IsObject(Nothing); IsObject(5); o Is Nothing\n"
    +        "DIM a(2) AS INTEGER\n"
    +        "DIM v AS VARIANT\n"
    +        "v = a\n"
    +        "PRINT IsArray(v); IsArray(a); IsArray(5); VarType(v); TypeName(v); UBOUND(v)\n"
    +        "DIM w(1) AS VARIANT\n"
    +        "PRINT VarType(w); TypeName(w); IsEmpty(w(0))\n"
    +        "PRINT CVar(3) + 1; TypeName(CVar(\"s\")); TypeName(Nothing); VarType(Nothing)\n",
    +        "True False True True False True False \n"
    +        "True True False True \n"
    +        "True True False 8194 Integer()2 \n"
    +        "8204 Variant()True \n"
    +        "4 StringNothing9 \n"
    +    );
    +
    +    runProgram("Variant: Null propagates",
    +        "DIM v AS VARIANT\n"
    +        "v = Null\n"
    +        "PRINT IsNull(v); VarType(v); TypeName(v); v\n"
    +        "PRINT IsNull(v + 1); IsNull(v * 2); IsNull(-v); IsNull(v = 1); IsNull(v & Null)\n"
    +        "PRINT \"a\" & v & \"b\"\n"
    +        "PRINT IsNull(v AND TRUE); v AND FALSE; v OR TRUE; IsNull(v OR FALSE); IsNull(NOT v); IsNull(v XOR 1)\n"
    +        "IF v THEN PRINT \"yes\" ELSE PRINT \"no\"\n"
    +        "PRINT IsNull(ABS(v)); IsNull(INT(v)); IsNull(FIX(v)); IsNull(SGN(v))\n"
    +        "PRINT IsNull(Choose(9, 1, 2))\n",
    +        "True 1 NullNull\n"
    +        "True True True True True \n"
    +        "ab\n"
    +        "True 0 -1 True True True \n"
    +        "no\n"
    +        "True True True True \n"
    +        "True \n"
    +    );
    +
    +    runProgram("Variant: Invalid use of Null",
    +        "ON ERROR GOTO h\n"
    +        "DIM i AS INTEGER\n"
    +        "i = Null\n"
    +        "PRINT LEN(CSTR(Null))\n"
    +        "PRINT Null + 1 > 0\n"
    +        "PRINT MID$(Null, 1)\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n",
    +        "err94 \n"
    +        "err94 \n"
    +        "Null\n"
    +        "err94 \n"
    +    );
    +
    +    runProgram("Class: variables, properties and methods",
    +        "DIM p AS Person\n"
    +        "PRINT p Is Nothing; TypeName(p)\n"
    +        "SET p = NEW Person\n"
    +        "p.First = \"Ada\"\n"
    +        "p.Age = \"36\"\n"
    +        "PRINT p.First; p.Age; VarType(p.Age); p.Describe; \" \"; TypeName(p); VarType(p)\n"
    +        "p.Tag(2) = \"two\"\n"
    +        "PRINT p.Tag(2); \"|\"; p.Tag(1); \"|\"\n"
    +        "SET p.Friend = NEW Person\n"
    +        "p.Friend.First = \"Bob\"\n"
    +        "PRINT p.Friend.First; p.Friend.Friend Is Nothing\n"
    +        "PRINT p.Total(1, 2, 3); p.Total()\n"
    +        "SET p = Nothing\n"
    +        "PRINT \"end\"\n"
    +        "BEGINCLASS \"Person\"\n"
    +        "Public First As String\n"
    +        "Private mAge As Integer\n"
    +        "Private mFriend As Person\n"
    +        "Private tags(3) As String\n"
    +        "Private Sub Class_Initialize()\n"
    +        "  PRINT \"[new]\";\n"
    +        "End Sub\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"[bye \"; First; \"]\"\n"
    +        "End Sub\n"
    +        "Property Get Age() As Integer\n"
    +        "  Age = mAge\n"
    +        "End Property\n"
    +        "Property Let Age(v As Integer)\n"
    +        "  mAge = v\n"
    +        "End Property\n"
    +        "Property Get Friend() As Person\n"
    +        "  SET Friend = mFriend\n"
    +        "End Property\n"
    +        "Property Set Friend(f As Person)\n"
    +        "  SET mFriend = f\n"
    +        "End Property\n"
    +        "Property Get Tag(i As Integer) As String\n"
    +        "  Tag = tags(i)\n"
    +        "End Property\n"
    +        "Property Let Tag(i As Integer, s As String)\n"
    +        "  tags(i) = s\n"
    +        "End Property\n"
    +        "Public Function Describe() As String\n"
    +        "  Describe = First & \" is \" & Me.Age\n"
    +        "End Function\n"
    +        "Public Sub AddTo(n As Integer)\n"
    +        "  n = n + mAge\n"
    +        "End Sub\n"
    +        "Public Function Total(ParamArray v()) As Integer\n"
    +        "  DIM t AS INTEGER\n"
    +        "  FOR EACH e IN v\n"
    +        "    t = t + e\n"
    +        "  NEXT\n"
    +        "  Total = t + UBOUND(v) + 1\n"
    +        "End Function\n"
    +        "Private Sub Secret()\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "True Nothing\n"
    +        "[new]Ada36 2 Ada is 36 Person9 \n"
    +        "two||\n"
    +        "[new]BobTrue \n"
    +        "9 0 \n"
    +        "[bye Ada]\n"
    +        "[bye Bob]\n"
    +        "end\n"
    +    );
    +
    +    runProgram("Class: ByRef arguments of a known class's method",
    +        "DIM p AS NEW Person\n"
    +        "DIM x AS INTEGER\n"
    +        "p.Age = 36\n"
    +        "x = 1\n"
    +        "p.AddTo x\n"
    +        "PRINT x\n"
    +        "p.AddTo (x)\n"
    +        "PRINT x\n"
    +        "DIM o AS OBJECT\n"
    +        "SET o = p\n"
    +        "o.AddTo x\n"
    +        "PRINT x\n"
    +        "SET o = Nothing\n"
    +        "SET p = Nothing\n"
    +        "BEGINCLASS \"Person\"\n"
    +        "Public First As String\n"
    +        "Private mAge As Integer\n"
    +        "Private mFriend As Person\n"
    +        "Private tags(3) As String\n"
    +        "Private Sub Class_Initialize()\n"
    +        "  PRINT \"[new]\";\n"
    +        "End Sub\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"[bye \"; First; \"]\"\n"
    +        "End Sub\n"
    +        "Property Get Age() As Integer\n"
    +        "  Age = mAge\n"
    +        "End Property\n"
    +        "Property Let Age(v As Integer)\n"
    +        "  mAge = v\n"
    +        "End Property\n"
    +        "Property Get Friend() As Person\n"
    +        "  SET Friend = mFriend\n"
    +        "End Property\n"
    +        "Property Set Friend(f As Person)\n"
    +        "  SET mFriend = f\n"
    +        "End Property\n"
    +        "Property Get Tag(i As Integer) As String\n"
    +        "  Tag = tags(i)\n"
    +        "End Property\n"
    +        "Property Let Tag(i As Integer, s As String)\n"
    +        "  tags(i) = s\n"
    +        "End Property\n"
    +        "Public Function Describe() As String\n"
    +        "  Describe = First & \" is \" & Me.Age\n"
    +        "End Function\n"
    +        "Public Sub AddTo(n As Integer)\n"
    +        "  n = n + mAge\n"
    +        "End Sub\n"
    +        "Public Function Total(ParamArray v()) As Integer\n"
    +        "  DIM t AS INTEGER\n"
    +        "  FOR EACH e IN v\n"
    +        "    t = t + e\n"
    +        "  NEXT\n"
    +        "  Total = t + UBOUND(v) + 1\n"
    +        "End Function\n"
    +        "Private Sub Secret()\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "[new]37 \n"
    +        "37 \n"
    +        "37 \n"
    +        "[bye ]\n"
    +    );
    +
    +    runProgram("Class: references, Is and Class_Terminate",
    +        "DIM p AS Person\n"
    +        "DIM q AS Person\n"
    +        "SET p = NEW Person\n"
    +        "p.First = \"Ada\"\n"
    +        "SET p.Friend = NEW Person\n"
    +        "p.Friend.First = \"Bob\"\n"
    +        "SET q = p\n"
    +        "PRINT q Is p; q Is p.Friend; p Is Nothing\n"
    +        "SET p = Nothing\n"
    +        "PRINT \"p cleared\"\n"
    +        "SET q = Nothing\n"
    +        "PRINT \"q cleared\"\n"
    +        "Scoped\n"
    +        "PRINT \"after Scoped\"\n"
    +        "SUB Scoped\n"
    +        "  DIM t AS NEW Person\n"
    +        "  t.First = \"Tmp\"\n"
    +        "END SUB\n"
    +        "BEGINCLASS \"Person\"\n"
    +        "Public First As String\n"
    +        "Private mAge As Integer\n"
    +        "Private mFriend As Person\n"
    +        "Private tags(3) As String\n"
    +        "Private Sub Class_Initialize()\n"
    +        "  PRINT \"[new]\";\n"
    +        "End Sub\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"[bye \"; First; \"]\"\n"
    +        "End Sub\n"
    +        "Property Get Age() As Integer\n"
    +        "  Age = mAge\n"
    +        "End Property\n"
    +        "Property Let Age(v As Integer)\n"
    +        "  mAge = v\n"
    +        "End Property\n"
    +        "Property Get Friend() As Person\n"
    +        "  SET Friend = mFriend\n"
    +        "End Property\n"
    +        "Property Set Friend(f As Person)\n"
    +        "  SET mFriend = f\n"
    +        "End Property\n"
    +        "Property Get Tag(i As Integer) As String\n"
    +        "  Tag = tags(i)\n"
    +        "End Property\n"
    +        "Property Let Tag(i As Integer, s As String)\n"
    +        "  tags(i) = s\n"
    +        "End Property\n"
    +        "Public Function Describe() As String\n"
    +        "  Describe = First & \" is \" & Me.Age\n"
    +        "End Function\n"
    +        "Public Sub AddTo(n As Integer)\n"
    +        "  n = n + mAge\n"
    +        "End Sub\n"
    +        "Public Function Total(ParamArray v()) As Integer\n"
    +        "  DIM t AS INTEGER\n"
    +        "  FOR EACH e IN v\n"
    +        "    t = t + e\n"
    +        "  NEXT\n"
    +        "  Total = t + UBOUND(v) + 1\n"
    +        "End Function\n"
    +        "Private Sub Secret()\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "[new][new]True False False \n"
    +        "p cleared\n"
    +        "[bye Ada]\n"
    +        "[bye Bob]\n"
    +        "q cleared\n"
    +        "[new][bye Tmp]\n"
    +        "after Scoped\n"
    +    );
    +
    +    runProgram("Class: member errors",
    +        "ON ERROR GOTO h\n"
    +        "DIM r AS Person\n"
    +        "SET r = NEW Person\n"
    +        "r.Missing = 1\n"
    +        "r.Secret\n"
    +        "PRINT r.Age(1)\n"
    +        "SET r = NEW Collection\n"
    +        "SET r = 5\n"
    +        "DIM o AS OBJECT\n"
    +        "SET o = r\n"
    +        "o.Total\n"
    +        "PRINT \"end\"\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n"
    +        "BEGINCLASS \"Person\"\n"
    +        "Public First As String\n"
    +        "Private mAge As Integer\n"
    +        "Private mFriend As Person\n"
    +        "Private tags(3) As String\n"
    +        "Private Sub Class_Initialize()\n"
    +        "  PRINT \"[new]\";\n"
    +        "End Sub\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"[bye \"; First; \"]\"\n"
    +        "End Sub\n"
    +        "Property Get Age() As Integer\n"
    +        "  Age = mAge\n"
    +        "End Property\n"
    +        "Property Let Age(v As Integer)\n"
    +        "  mAge = v\n"
    +        "End Property\n"
    +        "Property Get Friend() As Person\n"
    +        "  SET Friend = mFriend\n"
    +        "End Property\n"
    +        "Property Set Friend(f As Person)\n"
    +        "  SET mFriend = f\n"
    +        "End Property\n"
    +        "Property Get Tag(i As Integer) As String\n"
    +        "  Tag = tags(i)\n"
    +        "End Property\n"
    +        "Property Let Tag(i As Integer, s As String)\n"
    +        "  tags(i) = s\n"
    +        "End Property\n"
    +        "Public Function Describe() As String\n"
    +        "  Describe = First & \" is \" & Me.Age\n"
    +        "End Function\n"
    +        "Public Sub AddTo(n As Integer)\n"
    +        "  n = n + mAge\n"
    +        "End Sub\n"
    +        "Public Function Total(ParamArray v()) As Integer\n"
    +        "  DIM t AS INTEGER\n"
    +        "  FOR EACH e IN v\n"
    +        "    t = t + e\n"
    +        "  NEXT\n"
    +        "  Total = t + UBOUND(v) + 1\n"
    +        "End Function\n"
    +        "Private Sub Secret()\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "[new]err438 \n"
    +        "err438 \n"
    +        "err450 \n"
    +        "err13 \n"
    +        "err424 \n"
    +        "end\n"
    +    );
    +
    +    runProgram("Class: a reference cycle is left alone",
    +        "DIM a AS Node, b AS Node\n"
    +        "SET a = NEW Node\n"
    +        "SET b = NEW Node\n"
    +        "a.Id = 1\n"
    +        "b.Id = 2\n"
    +        "SET a.NextNode = b\n"
    +        "SET b.NextNode = a\n"
    +        "SET a = Nothing\n"
    +        "SET b = Nothing\n"
    +        "PRINT \"cycle dropped\"\n"
    +        "BEGINCLASS \"Node\"\n"
    +        "Public Id As Integer\n"
    +        "Public NextNode As Node\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"[term\"; Id; \"]\"\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "cycle dropped\n"
    +    );
    +
    +    runProgram("Collection: Add, Item, Remove, Count and keys",
    +        "DIM c AS NEW Collection\n"
    +        "FOR i = 1 TO 3\n"
    +        "  DIM n AS Node\n"
    +        "  SET n = NEW Node\n"
    +        "  n.Id = i * 10\n"
    +        "  c.Add n, \"k\" & i\n"
    +        "NEXT\n"
    +        "SET n = Nothing\n"
    +        "c.Add \"first\", , 1\n"
    +        "c.Add \"last\", , , c.Count\n"
    +        "c.Add \"mid\", \"m\", \"k2\"\n"
    +        "FOR EACH it IN c\n"
    +        "  IF IsObject(it) THEN PRINT it.Id; ELSE PRINT it; \" \";\n"
    +        "NEXT\n"
    +        "PRINT\n"
    +        "PRINT c(\"K2\").Id; c(2).Id; c.Item(\"m\"); TypeName(c); VarType(c)\n"
    +        "c.Remove 1\n"
    +        "c.Remove \"k3\"\n"
    +        "PRINT c.Count\n"
    +        "ON ERROR GOTO h\n"
    +        "c.Add 1, \"m\"\n"
    +        "PRINT c(99)\n"
    +        "PRINT c(\"nope\")\n"
    +        "c.Add 1, 5\n"
    +        "c.Add 1, , 1, 2\n"
    +        "c(1) = 5\n"
    +        "PRINT \"end\"\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n"
    +        "BEGINCLASS \"Node\"\n"
    +        "Public Id As Integer\n"
    +        "Public NextNode As Node\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"[term\"; Id; \"]\"\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "first 10 mid 20 30 last \n"
    +        "20 10 midCollection9 \n"
    +        "[term30 ]\n"
    +        "4 \n"
    +        "err457 \n"
    +        "err9 \n"
    +        "err5 \n"
    +        "err13 \n"
    +        "err5 \n"
    +        "err438 \n"
    +        "end\n"
    +    );
    +
    +    runProgram("For Each over arrays, Variants, ParamArrays and Collections",
    +        "DIM a(3) AS INTEGER\n"
    +        "FOR i = 0 TO 3\n"
    +        "  a(i) = i * i\n"
    +        "NEXT\n"
    +        "FOR EACH x IN a\n"
    +        "  PRINT x;\n"
    +        "NEXT\n"
    +        "PRINT\n"
    +        "DIM tot AS INTEGER\n"
    +        "FOR EACH k IN Array3()\n"
    +        "  tot = tot + k\n"
    +        "  IF tot > 3 THEN EXIT FOR\n"
    +        "NEXT\n"
    +        "PRINT tot\n"
    +        "FOR EACH ch IN Split(\"a,b,c\", \",\")\n"
    +        "  FOR EACH d IN Split(\"1,2\", \",\")\n"
    +        "    PRINT ch; d; \" \";\n"
    +        "  NEXT d\n"
    +        "NEXT ch\n"
    +        "PRINT\n"
    +        "DIM e AS NEW Collection\n"
    +        "FOR EACH z IN e\n"
    +        "  PRINT \"never\"\n"
    +        "NEXT\n"
    +        "e.Add 1\n"
    +        "e.Add 2\n"
    +        "e.Add 3\n"
    +        "FOR EACH z IN e\n"
    +        "  PRINT z;\n"
    +        "  e.Remove 1\n"
    +        "NEXT\n"
    +        "PRINT e.Count\n"
    +        "ShowArgs 1, \"two\", 3.5\n"
    +        "ON ERROR GOTO h\n"
    +        "FOR EACH z IN 5\n"
    +        "NEXT\n"
    +        "PRINT \"end\"\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n"
    +        "FUNCTION Array3() AS VARIANT\n"
    +        "  DIM t(2) AS INTEGER\n"
    +        "  t(0) = 1\n"
    +        "  t(1) = 2\n"
    +        "  t(2) = 3\n"
    +        "  Array3 = t\n"
    +        "END FUNCTION\n"
    +        "SUB ShowArgs(ParamArray args())\n"
    +        "  PRINT VarType(args); TypeName(args);\n"
    +        "  FOR EACH v IN args\n"
    +        "    PRINT \" \"; TypeName(v);\n"
    +        "  NEXT\n"
    +        "  PRINT\n"
    +        "END SUB\n",
    +        "0 1 4 9 \n"
    +        "6 \n"
    +        "a1 a2 b1 b2 c1 c2 \n"
    +        "1 3 1 \n"
    +        "8204 Variant() Integer String Double\n"
    +        "err13 \n"
    +        "end\n"
    +    );
    +
    +    runProgram("Objects in arrays and TYPE fields",
    +        "DIM arr(2) AS Node\n"
    +        "SET arr(1) = NEW Node\n"
    +        "arr(1).Id = 77\n"
    +        "PRINT arr(1).Id; arr(0) Is Nothing; TypeName(arr)\n"
    +        "TYPE Holder\n"
    +        "  obj AS Node\n"
    +        "  v AS VARIANT\n"
    +        "END TYPE\n"
    +        "DIM hd AS Holder\n"
    +        "PRINT hd.obj Is Nothing; IsEmpty(hd.v)\n"
    +        "SET hd.obj = arr(1)\n"
    +        "hd.v = \"var\"\n"
    +        "hd.obj.Id = 78\n"
    +        "PRINT hd.obj.Id; arr(1).Id; hd.v\n"
    +        "SET arr(1) = Nothing\n"
    +        "SET hd.obj = Nothing\n"
    +        "PRINT \"gone\"\n"
    +        "BEGINCLASS \"Node\"\n"
    +        "Public Id As Integer\n"
    +        "Public NextNode As Node\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"[term\"; Id; \"]\"\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "77 True Object()\n"
    +        "True True \n"
    +        "78 78 var\n"
    +        "[term78 ]\n"
    +        "gone\n"
    +    );
    +
    +    runProgram("Class: default member, Me and its own procedures",
    +        "DIM l AS NEW List\n"
    +        "l.Add \"a\"\n"
    +        "l.Add \"b\"\n"
    +        "PRINT l(1); l.Item(2); l.Count; TypeOf l Is List; TypeOf l Is Object; TypeOf l Is Collection\n"
    +        "l(1) = \"z\"\n"
    +        "PRINT l(1); l.Self Is l; l.Self.Self.Count\n"
    +        "PRINT l.Joined(\", \")\n"
    +        "BEGINCLASS \"List\"\n"
    +        "Private items As New Collection\n"
    +        "Private sep As String\n"
    +        "Property Get Item(i As Integer) As Variant\n"
    +        "Attribute Item.VB_UserMemId = 0\n"
    +        "  Item = items(i)\n"
    +        "End Property\n"
    +        "Property Let Item(i As Integer, v As Variant)\n"
    +        "  items.Add v, , i\n"
    +        "  items.Remove i + 1\n"
    +        "End Property\n"
    +        "Public Sub Add(v As Variant)\n"
    +        "  items.Add v\n"
    +        "End Sub\n"
    +        "Public Function Count() As Long\n"
    +        "  Count = items.Count\n"
    +        "End Function\n"
    +        "Public Function Self() As List\n"
    +        "  SET Self = Me\n"
    +        "End Function\n"
    +        "Public Function Joined(s As String) As String\n"
    +        "  sep = s\n"
    +        "  Joined = JoinFrom(1)\n"
    +        "End Function\n"
    +        "Private Function JoinFrom(i As Integer) As String\n"
    +        "  DIM r AS STRING\n"
    +        "  IF i > Count THEN EXIT FUNCTION\n"
    +        "  r = Item(i)\n"
    +        "  IF i < Count THEN r = r & sep & JoinFrom(i + 1)\n"
    +        "  JoinFrom = r\n"
    +        "End Function\n"
    +        "ENDCLASS\n",
    +        "ab2 True True False \n"
    +        "zTrue 2 \n"
    +        "z, b\n"
    +    );
    +
    +    runProgram("Class: member variables start typed; Class_Initialize runs after them",
    +        "DIM t AS NEW Typed\n"
    +        "t.Report\n"
    +        "DIM o AS OBJECT\n"
    +        "SET o = t\n"
    +        "o.Opt\n"
    +        "o.Opt 5\n"
    +        "o.Opt , \"x\"\n"
    +        "BEGINCLASS \"Typed\"\n"
    +        "Private i As Integer\n"
    +        "Private d As Double\n"
    +        "Private s As String\n"
    +        "Private v As Variant\n"
    +        "Private b As Boolean\n"
    +        "Private obj As Object\n"
    +        "Private Sub Class_Initialize()\n"
    +        "  PRINT \"init\"; i; d; \"[\"; s; \"]\"; IsEmpty(v); b; obj Is Nothing\n"
    +        "  i = 7\n"
    +        "End Sub\n"
    +        "Public Sub Report()\n"
    +        "  PRINT \"i =\"; i\n"
    +        "End Sub\n"
    +        "Public Sub Opt(Optional a As Integer, Optional t As String)\n"
    +        "  PRINT \"opt\"; a; \"[\"; t; \"]\"\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "init0 0 []True False True \n"
    +        "i =7 \n"
    +        "opt0 []\n"
    +        "opt5 []\n"
    +        "opt[x]\n"
    +    );
    +
    +    runProgram("PUBLIC and PRIVATE at module level",
    +        "PUBLIC shared1 AS INTEGER\n"
    +        "PRIVATE own1 AS INTEGER\n"
    +        "shared1 = 5\n"
    +        "own1 = 6\n"
    +        "Report\n"
    +        "PRIVATE SUB Report\n"
    +        "  PRINT shared1\n"
    +        "END SUB\n",
    +        "5 \n"
    +    );
    +
    +    runProgram("Class: Terminate of a result released inside an expression",
    +        "PRINT Make(3).Id + 1\n"
    +        "PRINT \"after\"\n"
    +        "FUNCTION Make(n AS INTEGER) AS Node\n"
    +        "  SET Make = NEW Node\n"
    +        "  Make.Id = n\n"
    +        "END FUNCTION\n"
    +        "BEGINCLASS \"Node\"\n"
    +        "Public Id As Integer\n"
    +        "Public NextNode As Node\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"[term\"; Id; \"]\"\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "[term3 ]\n"
    +        "4 \n"
    +        "after\n"
    +    );
    +
    +    expectCompileError("PROPERTY outside a class rejected",
    +        "PROPERTY GET X()\n"
    +        "END PROPERTY\n",
    +        "PROPERTY procedures belong in a class module"
    +    );
    +
    +    expectCompileError("Code outside procedures in a class rejected",
    +        "BEGINCLASS \"C\"\n"
    +        "PRINT 1\n"
    +        "ENDCLASS\n",
    +        "Only declarations are allowed outside procedures in a class module"
    +    );
    +
    +    expectCompileError("Duplicate class member rejected",
    +        "BEGINCLASS \"C\"\n"
    +        "PUBLIC x AS INTEGER\n"
    +        "PUBLIC SUB x\n"
    +        "END SUB\n"
    +        "ENDCLASS\n",
    +        "'x' already declared"
    +    );
    +
    +    expectCompileError("NEW of a non-class rejected",
    +        "x = NEW Foo\n",
    +        "class name (or Collection) after New"
    +    );
    +
    +    expectCompileError("AS NEW needs a class",
    +        "DIM x AS NEW INTEGER\n",
    +        "New needs a class name"
    +    );
    +
    +    expectCompileError("AS NEW array rejected",
    +        "DIM x(3) AS NEW Collection\n",
    +        "New cannot be used with an array"
    +    );
    +
    +    expectCompileError("EXIT PROPERTY outside PROPERTY rejected",
    +        "SUB S\n"
    +        "EXIT PROPERTY\n"
    +        "END SUB\n",
    +        "EXIT PROPERTY outside PROPERTY"
    +    );
    +
    +    expectCompileError("BEGINCLASS without ENDCLASS rejected",
    +        "BEGINCLASS \"C\"\n"
    +        "SUB S\n"
    +        "END SUB\n",
    +        "BEGINCLASS without ENDCLASS"
    +    );
    +
    +    expectCompileError("Class_Initialize with parameters rejected",
    +        "BEGINCLASS \"C\"\n"
    +        "SUB Class_Initialize(a)\n"
    +        "END SUB\n"
    +        "ENDCLASS\n",
    +        "Class_Initialize must be a Sub without parameters"
    +    );
    +
    +    expectCompileError("SHARED in a class rejected",
    +        "BEGINCLASS \"C\"\n"
    +        "SUB S\n"
    +        "SHARED q\n"
    +        "END SUB\n"
    +        "ENDCLASS\n",
    +        "SHARED is not allowed in a class module"
    +    );
    +
    +    expectCompileError("DIM SHARED in a class rejected",
    +        "BEGINCLASS \"C\"\n"
    +        "DIM SHARED q\n"
    +        "ENDCLASS\n",
    +        "DIM SHARED is not allowed in a class module"
    +    );
    +
    +    expectCompileError("Property Let without a value parameter rejected",
    +        "BEGINCLASS \"C\"\n"
    +        "PROPERTY LET P()\n"
    +        "END PROPERTY\n"
    +        "ENDCLASS\n",
    +        "Property Let and Property Set take the value"
    +    );
    +
    +    expectCompileError("Class defined twice rejected",
    +        "BEGINCLASS \"C\"\n"
    +        "ENDCLASS\n"
    +        "BEGINCLASS \"C\"\n"
    +        "ENDCLASS\n",
    +        "cannot be used as a class name"
    +    );
    +    runProgram("Class: members named by reserved words",
    +        "DIM f AS NEW Doc\n"
    +        "f.Name = \"x\"\n"
    +        "f.Open\n"
    +        "PRINT f.Name; f.IsOpen; f.Close\n"
    +        "BEGINCLASS \"Doc\"\n"
    +        "Private mName As String\n"
    +        "Public IsOpen As Boolean\n"
    +        "Property Get Name() As String\n"
    +        "  Name = \"<\" & mName & \">\"\n"
    +        "End Property\n"
    +        "Property Let Name(s As String)\n"
    +        "  mName = s\n"
    +        "End Property\n"
    +        "Public Sub Open()\n"
    +        "  IsOpen = True\n"
    +        "End Sub\n"
    +        "Public Function Close() As Boolean\n"
    +        "  Close = IsOpen\n"
    +        "  IsOpen = False\n"
    +        "End Function\n"
    +        "ENDCLASS\n",
    +        "True True \n"
    +    );
    +
    +    runProgram("Class: STATIC variables are per class (shared by its instances)",
    +        "DIM a AS NEW A1\n"
    +        "DIM b AS NEW B1\n"
    +        "a.Tick\n"
    +        "a.Tick\n"
    +        "b.Tick\n"
    +        "Tick\n"
    +        "BEGINCLASS \"A1\"\n"
    +        "Public Sub Tick()\n"
    +        "  STATIC n AS INTEGER\n"
    +        "  n = n + 1\n"
    +        "  PRINT \"A\"; n\n"
    +        "End Sub\n"
    +        "ENDCLASS\n"
    +        "BEGINCLASS \"B1\"\n"
    +        "Public Sub Tick()\n"
    +        "  STATIC n AS INTEGER\n"
    +        "  n = n + 10\n"
    +        "  PRINT \"B\"; n\n"
    +        "End Sub\n"
    +        "ENDCLASS\n"
    +        "SUB Tick\n"
    +        "  STATIC n AS INTEGER\n"
    +        "  n = n + 100\n"
    +        "  PRINT \"M\"; n\n"
    +        "END SUB\n",
    +        "A1 \nA2 \nB10 \nM100 \n"
    +    );
    +
    +}
    +
    +
    +// Phase 1 library functions: the string functions (REPLACE$, SPLIT, JOIN$,
    +// INSTRREV, STRCOMP, STRREVERSE$, FILTER, LSET / RSET, IIF, CHOOSE) and
    +// the DATE type with its functions, literals, arithmetic and Format$.
    +static void testStringsAndDates(void) {
    +    runProgram("Replace$",
    +        "PRINT Replace$(\"aXbXc\", \"X\", \"-\")\n"
    +        "PRINT Replace(\"aXbXc\", \"X\", \"-\", 3)\n"
    +        "PRINT Replace(\"aXbXc\", \"X\", \"-\", 1, 1)\n"
    +        "PRINT Replace(\"aXbXc\", \"x\", \"-\", 1, -1, vbTextCompare)\n"
    +        "PRINT Replace(\"aXbXc\", \"x\", \"-\")\n"
    +        "PRINT \"[\" & Replace(\"abc\", \"\", \"-\") & \"]\"\n"
    +        "PRINT \"[\" & Replace(\"abc\", \"b\", \"\") & \"]\"\n"
    +        "PRINT \"[\" & Replace(\"abc\", \"b\", \"x\", 9) & \"]\"\n"
    +        "PRINT Replace(\"aaa\", \"a\", \"aa\")\n"
    +        "PRINT Replace(\"aXbXc\", \"X\", \"-\", 1, 0)\n",
    +        "a-b-c\nb-c\na-bXc\na-b-c\naXbXc\n[abc]\n[ac]\n[]\naaaaaa\naXbXc\n"
    +    );
    +
    +    runProgram("Split with delimiters, limits and empty input",
    +        "DIM p() AS STRING\n"
    +        "DIM i AS INTEGER\n"
    +        "p = Split(\"a,b,,c\", \",\")\n"
    +        "PRINT UBOUND(p); LBOUND(p)\n"
    +        "FOR i = 0 TO UBOUND(p): PRINT \"[\" & p(i) & \"]\";: NEXT: PRINT\n"
    +        "p = Split(\"a b c\")\n"
    +        "PRINT UBOUND(p)\n"
    +        "p = Split(\"a,b,c\", \",\", 2)\n"
    +        "FOR i = 0 TO UBOUND(p): PRINT \"[\" & p(i) & \"]\";: NEXT: PRINT\n"
    +        "p = Split(\"\", \",\")\n"
    +        "PRINT UBOUND(p); LBOUND(p)\n"
    +        "p = Split(\"abc\", \"\")\n"
    +        "PRINT UBOUND(p); p(0)\n"
    +        "p = Split(\"aXbxc\", \"x\", -1, vbTextCompare)\n"
    +        "FOR i = 0 TO UBOUND(p): PRINT \"[\" & p(i) & \"]\";: NEXT: PRINT\n"
    +        "p = Split(\"a,b\", \",\", 0)\n"
    +        "PRINT UBOUND(p)\n"
    +        "p = Split(\"a,b,\", \",\")\n"
    +        "PRINT UBOUND(p)\n"
    +        "p = Split(\"a::b\", \"::\")\n"
    +        "PRINT p(0); p(1)\n",
    +        "3 0 \n[a][b][][c]\n2 \n[a][b,c]\n-1 0 \n0 abc\n[a][b][c]\n-1 \n2 \nab\n"
    +    );
    +
    +    runProgram("Join$ and Filter",
    +        "DIM p() AS STRING\n"
    +        "DIM n(2) AS INTEGER\n"
    +        "DIM f() AS STRING\n"
    +        "p = Split(\"apple,banana,cherry\", \",\")\n"
    +        "PRINT Join$(p, \"-\")\n"
    +        "PRINT Join(p)\n"
    +        "PRINT Join(Split(\"x y z\"), \"\")\n"
    +        "n(0) = 1: n(1) = 22: n(2) = 333\n"
    +        "PRINT Join(n, \",\")\n"
    +        "PRINT \"[\" & Join(Split(\"\", \",\"), \",\") & \"]\"\n"
    +        "f = Filter(p, \"an\")\n"
    +        "PRINT UBOUND(f); f(0)\n"
    +        "f = Filter(p, \"an\", False)\n"
    +        "PRINT Join(f, \"+\")\n"
    +        "f = Filter(p, \"AN\", True, vbTextCompare)\n"
    +        "PRINT Join(f, \"+\")\n"
    +        "f = Filter(p, \"zz\")\n"
    +        "PRINT UBOUND(f)\n",
    +        "apple-banana-cherry\napple banana cherry\nxyz\n1,22,333\n[]\n0 banana\napple+cherry\nbanana\n-1 \n"
    +    );
    +
    +    runProgram("InStrRev, StrComp and StrReverse$",
    +        "PRINT InStrRev(\"abcabc\", \"bc\")\n"
    +        "PRINT InStrRev(\"abcabc\", \"bc\", 4)\n"
    +        "PRINT InStrRev(\"abc\", \"x\")\n"
    +        "PRINT InStrRev(\"abc\", \"\")\n"
    +        "PRINT InStrRev(\"ABC\", \"b\", -1, vbTextCompare)\n"
    +        "PRINT InStrRev(\"abc\", \"c\", 9)\n"
    +        "PRINT InStrRev(\"\", \"a\")\n"
    +        "PRINT StrComp(\"a\", \"b\"); StrComp(\"b\", \"a\"); StrComp(\"a\", \"a\")\n"
    +        "PRINT StrComp(\"A\", \"a\"); StrComp(\"A\", \"a\", vbTextCompare); StrComp(\"A\", \"a\", vbBinaryCompare)\n"
    +        "PRINT StrReverse$(\"abc\"); \"[\" & StrReverse(\"\") & \"]\"\n",
    +        "5 \n2 \n0 \n3 \n2 \n0 \n0 \n-1 1 0 \n-1 0 -1 \ncba[]\n"
    +    );
    +
    +    runProgram("Drag and clipboard constants; Control, Form and Object types",
    +        "PRINT vbManual; vbAutomatic; vbBeginDrag; vbEndDrag\n"
    +        "PRINT vbEnter; vbLeave; vbOver; vbCFText\n"
    +        "DIM c AS Control\n"
    +        "SET c = Nothing\n"
    +        "T Nothing, Nothing, c\n"
    +        "SUB T(s AS Control, f AS Form, o AS Object)\n"
    +        "    PRINT \"objects\"\n"
    +        "END SUB\n",
    +        "0 1 1 2 \n0 1 2 1 \nobjects\n"
    +    );
    +
    +    runProgram("StrComp and Replace follow OPTION COMPARE TEXT",
    +        "OPTION COMPARE TEXT\n"
    +        "PRINT StrComp(\"A\", \"a\"); StrComp(\"A\", \"a\", vbBinaryCompare)\n"
    +        "PRINT Replace(\"aXbxc\", \"x\", \"-\")\n"
    +        "PRINT InStrRev(\"aXbx\", \"X\")\n",
    +        "0 -1 \na-b-c\n4 \n"
    +    );
    +
    +    runProgram("LSET and RSET",
    +        "DIM s AS STRING\n"
    +        "DIM f AS STRING * 4\n"
    +        "DIM a(1) AS STRING\n"
    +        "s = \"12345\"\n"
    +        "LSET s = \"ab\"\n"
    +        "PRINT \"[\" & s & \"]\"\n"
    +        "RSET s = \"xy\"\n"
    +        "PRINT \"[\" & s & \"]\"\n"
    +        "LSET s = \"toolongvalue\"\n"
    +        "PRINT \"[\" & s & \"]\"\n"
    +        "RSET s = \"toolongvalue\"\n"
    +        "PRINT \"[\" & s & \"]\"\n"
    +        "RSET f = \"7\"\n"
    +        "PRINT \"[\" & f & \"]\"\n"
    +        "a(1) = \"....\"\n"
    +        "LSET a(1) = \"x\"\n"
    +        "PRINT \"[\" & a(1) & \"]\"\n",
    +        "[ab   ]\n[   xy]\n[toolo]\n[toolo]\n[   7]\n[x   ]\n"
    +    );
    +
    +    runProgram("IIf and Choose",
    +        "PRINT IIf(1 > 0, \"yes\", \"no\")\n"
    +        "PRINT IIf(0, 1, 2)\n"
    +        "PRINT Choose(2, \"a\", \"b\", \"c\")\n"
    +        "PRINT Choose(3.7, \"a\", \"b\", \"c\")\n"
    +        "PRINT Choose(4, \"a\", \"b\", \"c\")\n"
    +        "PRINT IsNull(Choose(0, \"a\"))\n",
    +        "yes\n2 \nb\nc\nNull\nTrue \n"
    +    );
    +
    +    runProgram("Library errors",
    +        "DIM s AS INTEGER\n"
    +        "ON ERROR GOTO h\n"
    +        "PRINT Join(5)\n"
    +        "PRINT Replace(\"a\", \"a\", \"b\", 0)\n"
    +        "PRINT Split(\"a\", \",\", -2)\n"
    +        "PRINT InStrRev(\"a\", \"a\", 0)\n"
    +        "PRINT StrComp(\"a\", \"b\", 7)\n"
    +        "LSET s = \"x\"\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n",
    +        "err13 \nerr5 \nerr5 \nerr5 \nerr5 \nerr13 \n"
    +    );
    +
    +    runProgram("New built-in names stay free for the program",
    +        "DIM day AS INTEGER\n"
    +        "day = 5\n"
    +        "PRINT day\n"
    +        "FUNCTION Replace(a AS STRING) AS STRING\n"
    +        "    Replace = \"mine \" & a\n"
    +        "END FUNCTION\n"
    +        "PRINT Replace(\"x\")\n"
    +        "PRINT Month(#3/4/2005#)\n",
    +        "5 \nmine x\n3 \n"
    +    );
    +
    +    runProgram("DATE variables, literals and printing",
    +        "DIM d AS DATE\n"
    +        "PRINT d\n"
    +        "d = #1/15/2000#\n"
    +        "PRINT d\n"
    +        "PRINT Year(d); Month(d); Day(d); Weekday(d)\n"
    +        "d = #1/15/2000 3:04:05 PM#\n"
    +        "PRINT d\n"
    +        "PRINT Hour(d); Minute(d); Second(d)\n"
    +        "PRINT CDbl(#1/1/2000#)\n"
    +        "PRINT #12:00:00 PM#\n"
    +        "PRINT CDate(36526.5)\n"
    +        "PRINT \"[\" & STR$(d) & \"][\" & CSTR(d) & \"]\"\n"
    +        "PRINT \"Date: \" & #2000-02-29#\n"
    +        "PRINT #March 5, 2001#; \"|\"; #5 Mar 2001 16:30#\n"
    +        "PRINT #1/1/100#; \" \"; #12/31/9999 11:59:59 PM#\n",
    +        "12:00:00 AM\n1/15/2000\n2000 1 15 7 \n1/15/2000 3:04:05 PM\n15 4 5 \n36526 \n12:00:00 PM\n1/1/2000 12:00:00 PM\n"
    +        "[1/15/2000 3:04:05 PM][1/15/2000 3:04:05 PM]\nDate: 2/29/2000\n3/5/2001|3/5/2001 4:30:00 PM\n1/1/100 12/31/9999 11:59:59 PM\n"
    +    );
    +
    +    runProgram("DateSerial and TimeSerial rules",
    +        "PRINT DateSerial(2000, 2, 29)\n"
    +        "PRINT DateSerial(2001, 2, 29)\n"
    +        "PRINT DateSerial(2000, 13, 1)\n"
    +        "PRINT DateSerial(2000, 0, 1)\n"
    +        "PRINT DateSerial(2000, 3, 0)\n"
    +        "PRINT DateSerial(2000, -11, 1)\n"
    +        "PRINT DateSerial(99, 1, 1); \" \"; DateSerial(29, 1, 1); \" \"; DateSerial(30, 1, 1)\n"
    +        "PRINT DateSerial(1900, 2, 29)\n"
    +        "PRINT DateSerial(2000, 1, 366)\n"
    +        "PRINT TimeSerial(25, 0, 0)\n"
    +        "PRINT TimeSerial(-1, 0, 0)\n"
    +        "PRINT TimeSerial(1, 90, 0)\n"
    +        "PRINT CDbl(TimeSerial(-1, 0, 0))\n"
    +        "PRINT TimeSerial(0, 0, -1)\n"
    +        "ON ERROR GOTO h\n"
    +        "PRINT DateSerial(10000, 1, 1)\n"
    +        "PRINT DateSerial(100, 1, 0)\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n",
    +        "2/29/2000\n3/1/2001\n1/1/2001\n12/1/1999\n2/29/2000\n1/1/1999\n1/1/1999 1/1/2029 1/1/1930\n3/1/1900\n12/31/2000\n"
    +        "12/31/1899 1:00:00 AM\n12/29/1899 11:00:00 PM\n2:30:00 AM\n-1.95833333333333 \n12/29/1899 11:59:59 PM\nerr5 \nerr5 \n"
    +    );
    +
    +    runProgram("DateValue, TimeValue and dates before 1900",
    +        "DIM d AS DATE\n"
    +        "PRINT DateValue(\"March 5, 2001 4:00 PM\")\n"
    +        "PRINT TimeValue(\"March 5, 2001 4:00 PM\")\n"
    +        "d = CDate(-1.25)\n"
    +        "PRINT d\n"
    +        "PRINT DateValue(d); \" \"; TimeValue(d)\n"
    +        "PRINT d + 1\n"
    +        "PRINT d - 1\n"
    +        "PRINT CDbl(d - 1)\n"
    +        "PRINT CDate(-1.25) > CDate(-1)\n"
    +        "PRINT Weekday(CDate(-1)); Year(d); Month(d); Day(d); Hour(d)\n"
    +        "PRINT #12/29/1899 6:00 AM# = d\n",
    +        "3/5/2001\n4:00:00 PM\n12/29/1899 6:00:00 AM\n12/29/1899 6:00:00 AM\n6:00:00 AM\n12/28/1899 6:00:00 AM\n-2.25 \nTrue \n6 1899 12 29 6 \nTrue \n"
    +    );
    +
    +    runProgram("DateAdd with month-end clamping and overflow",
    +        "PRINT DateAdd(\"m\", 1, #1/31/2000#)\n"
    +        "PRINT DateAdd(\"m\", 1, #1/31/2001#)\n"
    +        "PRINT DateAdd(\"yyyy\", 1, #2/29/2000#)\n"
    +        "PRINT DateAdd(\"q\", 1, #11/30/2000#)\n"
    +        "PRINT DateAdd(\"m\", -13, #1/31/2000#)\n"
    +        "PRINT DateAdd(\"d\", -1, #3/1/2000#)\n"
    +        "PRINT DateAdd(\"y\", 1, #12/31/2000#); \" \"; DateAdd(\"w\", 1, #12/31/2000#)\n"
    +        "PRINT DateAdd(\"ww\", 2, #1/1/2000#)\n"
    +        "PRINT DateAdd(\"h\", 25, #1/1/2000#)\n"
    +        "PRINT DateAdd(\"n\", -1, #1/1/2000#)\n"
    +        "PRINT DateAdd(\"s\", 86400, #1/1/2000#)\n"
    +        "PRINT DateAdd(\"M\", 2.9, #1/15/2000#)\n"
    +        "PRINT DateAdd(\"m\", 1, #1/31/2000 10:30#)\n"
    +        "ON ERROR GOTO h\n"
    +        "PRINT DateAdd(\"x\", 1, #1/1/2000#)\n"
    +        "PRINT DateAdd(\"yyyy\", 8000, #1/1/2000#)\n"
    +        "PRINT DateAdd(\"d\", -1, #1/1/100#)\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n",
    +        "2/29/2000\n2/28/2001\n2/28/2001\n2/28/2001\n12/31/1998\n2/29/2000\n1/1/2001 1/1/2001\n1/15/2000\n1/2/2000 1:00:00 AM\n"
    +        "12/31/1999 11:59:00 PM\n1/2/2000\n3/15/2000\n2/29/2000 10:30:00 AM\nerr5 \nerr5 \nerr5 \n"
    +    );
    +
    +    runProgram("DateDiff intervals and week rules",
    +        "PRINT DateDiff(\"d\", #1/1/2000#, #3/1/2000#)\n"
    +        "PRINT DateDiff(\"d\", #3/1/2000#, #1/1/2000#)\n"
    +        "PRINT DateDiff(\"d\", #1/1/2000 11:00 PM#, #1/2/2000 1:00 AM#)\n"
    +        "PRINT DateDiff(\"yyyy\", #12/31/1999#, #1/1/2000#)\n"
    +        "PRINT DateDiff(\"m\", #1/31/2000#, #2/1/2000#)\n"
    +        "PRINT DateDiff(\"q\", #3/31/2000#, #4/1/2000#)\n"
    +        "PRINT DateDiff(\"w\", #1/3/2000#, #1/16/2000#); DateDiff(\"w\", #1/3/2000#, #1/17/2000#)\n"
    +        "PRINT DateDiff(\"ww\", #1/1/2000#, #1/2/2000#); DateDiff(\"ww\", #1/2/2000#, #1/8/2000#)\n"
    +        "PRINT DateDiff(\"ww\", #1/1/2000#, #1/2/2000#, vbMonday); DateDiff(\"ww\", #1/1/2000#, #12/31/2000#)\n"
    +        "PRINT DateDiff(\"h\", #1/1/2000 10:59 AM#, #1/1/2000 11:00 AM#)\n"
    +        "PRINT DateDiff(\"n\", #1/1/2000 10:59:59 AM#, #1/1/2000 11:00 AM#)\n"
    +        "PRINT DateDiff(\"s\", #1/1/2000#, #1/2/2000#)\n"
    +        "PRINT DateDiff(\"d\", CDate(-1.25), CDate(0.5))\n"
    +        "ON ERROR GOTO h\n"
    +        "PRINT DateDiff(\"s\", #1/1/100#, #12/31/9999#)\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n",
    +        "60 \n-60 \n1 \n1 \n1 \n1 \n1 2 \n1 0 \n0 53 \n1 \n1 \n86400 \n1 \nerr6 \n"
    +    );
    +
    +    runProgram("DatePart, Weekday and week-of-year rules",
    +        "PRINT DatePart(\"yyyy\", #5/15/2000#); DatePart(\"q\", #5/15/2000#); DatePart(\"m\", #5/15/2000#)\n"
    +        "PRINT DatePart(\"y\", #3/1/2000#); DatePart(\"y\", #3/1/2001#); DatePart(\"d\", #3/1/2001#)\n"
    +        "PRINT DatePart(\"w\", #1/1/2000#); DatePart(\"w\", #1/1/2000#, vbMonday)\n"
    +        "PRINT DatePart(\"ww\", #1/1/2000#); DatePart(\"ww\", #1/1/2000#, vbSunday, vbFirstFourDays); DatePart(\"ww\", #1/1/2000#, vbSunday, vbFirstFullWeek)\n"
    +        "PRINT DatePart(\"ww\", #12/31/2000#); DatePart(\"ww\", #1/2/2000#, vbSunday, vbFirstFullWeek)\n"
    +        "PRINT DatePart(\"h\", #1/1/2000 1:02:03 PM#); DatePart(\"n\", #1/1/2000 1:02:03 PM#); DatePart(\"s\", #1/1/2000 1:02:03 PM#)\n"
    +        "PRINT Weekday(#1/1/2000#, vbMonday); Weekday(#1/2/2000#); Weekday(#1/2/2000#, vbUseSystemDayOfWeek)\n"
    +        "ON ERROR GOTO h\n"
    +        "PRINT Weekday(#1/1/2000#, 9)\n"
    +        "PRINT DatePart(\"ww\", #1/1/2000#, vbSunday, 4)\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n",
    +        "2000 2 5 \n61 60 1 \n7 6 \n1 52 52 \n54 1 \n13 2 3 \n6 1 1 \nerr5 \nerr5 \n"
    +    );
    +
    +    runProgram("IsDate, CDate, MonthName and WeekdayName",
    +        "DIM d AS DATE\n"
    +        "PRINT IsDate(\"1/1/2000\"); IsDate(\"hello\"); IsDate(#1/1/2000#); IsDate(5); IsDate(\"2/30/2000\"); IsDate(\"13:00\"); IsDate(\"25:00\")\n"
    +        "PRINT MonthName(1); \" \"; MonthName(12, True)\n"
    +        "PRINT WeekdayName(1); \" \"; WeekdayName(1, True); \" \"; WeekdayName(1, False, vbMonday); \" \"; WeekdayName(7, False, vbMonday)\n"
    +        "d = \"2/3/2004\"\n"
    +        "PRINT d\n"
    +        "d = \"13/1/2000\"\n"
    +        "PRINT d\n"
    +        "ON ERROR GOTO h\n"
    +        "PRINT MonthName(13)\n"
    +        "d = \"garbage\"\n"
    +        "d = 1E10\n"
    +        "PRINT CDate(\"2/30/2000\")\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n",
    +        "True False True False False True False \nJanuary Dec\nSunday Sun Monday Sunday\n2/3/2004\n1/13/2000\nerr5 \nerr13 \nerr6 \nerr13 \n"
    +    );
    +
    +    runProgram("Format$ with date patterns",
    +        "DIM d AS DATE\n"
    +        "d = #1/5/2000 3:04:05 PM#\n"
    +        "PRINT Format$(d, \"yyyy-mm-dd hh:nn:ss\")\n"
    +        "PRINT Format$(d, \"m/d/yy\")\n"
    +        "PRINT Format$(d, \"dddd, mmmm d, yyyy\")\n"
    +        "PRINT Format$(d, \"ddd mmm\")\n"
    +        "PRINT Format$(d, \"h:mm AM/PM\"); \"|\"; Format$(d, \"hh:mm am/pm\"); \"|\"; Format$(d, \"h A/P\")\n"
    +        "PRINT Format$(d, \"Long Date\"); \"|\"; Format$(d, \"Short Date\"); \"|\"; Format$(d, \"Medium Date\")\n"
    +        "PRINT Format$(d, \"Long Time\"); \"|\"; Format$(d, \"Medium Time\"); \"|\"; Format$(d, \"Short Time\")\n"
    +        "PRINT Format$(d, \"q\"); \"|\"; Format$(d, \"\\d\\a\\y d\"); \"|\"; Format$(d, CHR$(34) & \"Year\" & CHR$(34) & \" yyyy\")\n"
    +        "PRINT Format$(d, \"c\"); \"|\"; Format$(d, \"\"); \"|\"; Format$(d, \"General Date\")\n"
    +        "PRINT Format$(d, \"ddddd\"); \"|\"; Format$(d, \"ttttt\"); \"|\"; Format$(d, \"y w ww\")\n"
    +        "PRINT Format(36526, \"mm/dd/yyyy\"); \"|\"; Format$(\"1/2/2000\", \"yyyy\")\n"
    +        "PRINT Format$(1234.5, \"#,##0.00\"); \"|\"; Format$(0.5, \"Percent\")\n"
    +        "PRINT Format$(#1/1/2000 12:05 AM#, \"h:nn AM/PM\"); \"|\"; Format$(#1/1/2000 12:05 PM#, \"hh:nn\")\n"
    +        "ON ERROR GOTO h\n"
    +        "PRINT Format$(\"xyz\", \"yyyy\")\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n",
    +        "2000-01-05 15:04:05\n1/5/00\nWednesday, January 5, 2000\nWed Jan\n3:04 PM|03:04 pm|3 P\n"
    +        "Wednesday, January 5, 2000|1/5/2000|05-Jan-00\n3:04:05 PM|03:04 PM|15:04\n1|day 5|Year 2000\n"
    +        "1/5/2000 3:04:05 PM|1/5/2000 3:04:05 PM|1/5/2000 3:04:05 PM\n1/5/2000|3:04:05 PM|5 4 2\n01/01/2000|2000\n1,234.50|50%\n12:05 AM|12:05\nerr13 \n"
    +    );
    +
    +    runProgram("Date arithmetic and comparison",
    +        "DIM a AS DATE\n"
    +        "DIM b AS DATE\n"
    +        "a = #1/1/2000#\n"
    +        "b = #1/2/2000 12:00 PM#\n"
    +        "PRINT b > a; a = #1/1/2000#; a < \"1/2/2000\"; a = \"1/1/2000\"\n"
    +        "PRINT b - a\n"
    +        "PRINT a + 1.5\n"
    +        "PRINT 2 + a\n"
    +        "PRINT a - 366\n"
    +        "a = a + 1\n"
    +        "PRINT a\n"
    +        "ON ERROR GOTO h\n"
    +        "a = #12/31/9999# + 1\n"
    +        "END\n"
    +        "h:\n"
    +        "PRINT \"err\"; ERR\n"
    +        "RESUME NEXT\n",
    +        "True True True True \n1.5 \n1/2/2000 12:00:00 PM\n1/3/2000\n12/31/1998\n1/2/2000\nerr6 \n"
    +    );
    +
    +    runProgram("DATE constants, arrays, TYPE fields and functions",
    +        "CONST Y2K = #1/1/2000#\n"
    +        "CONST NOON AS DATE = 0.5\n"
    +        "TYPE Rec\n"
    +        "    stamp AS DATE\n"
    +        "END TYPE\n"
    +        "DIM a(2) AS DATE\n"
    +        "DIM r AS Rec\n"
    +        "FUNCTION NextDay(x AS DATE) AS DATE\n"
    +        "    NextDay = x + 1\n"
    +        "END FUNCTION\n"
    +        "PRINT Y2K; \"|\"; NOON; \"|\"; Year(Y2K)\n"
    +        "PRINT a(0)\n"
    +        "a(1) = \"1/1/2000\"\n"
    +        "PRINT a(1)\n"
    +        "r.stamp = #2/2/2002#\n"
    +        "PRINT r.stamp\n"
    +        "PRINT NextDay(#2/28/2000#)\n"
    +        "PRINT NextDay(CDate(\"12/31/1999\"))\n",
    +        "1/1/2000|12:00:00 PM|2000 \n12:00:00 AM\n1/1/2000\n2/2/2002\n2/29/2000\n1/1/2000\n"
    +    );
    +
    +    runProgram("Now, Date and Time",
    +        "DIM d AS DATE\n"
    +        "d = Now\n"
    +        "PRINT Year(d) >= 2024; Int(CDbl(Date)) = CDbl(Date); CDbl(Time) < 1; DateValue(Now) = Date; Now() >= Date()\n",
    +        "True True True True True \n"
    +    );
    +
    +    runProgram("WRITE # and INPUT # round-trip a date",
    +        "DIM d AS DATE\n"
    +        "OPEN \"datert.txt\" FOR OUTPUT AS #1\n"
    +        "WRITE #1, #1/2/2000 3:04 PM#, 5\n"
    +        "CLOSE #1\n"
    +        "OPEN \"datert.txt\" FOR INPUT AS #1\n"
    +        "INPUT #1, d\n"
    +        "CLOSE #1\n"
    +        "KILL \"datert.txt\"\n"
    +        "PRINT d\n",
    +        "1/2/2000 3:04:00 PM\n"
    +    );
    +
    +    compileOnly("File numbers are not date literals",
    +        "PRINT #1, 5: PRINT #2, 6\n"
    +        "WRITE #1, a, b: CLOSE #1\n"
    +    );
    +
    +    expectCompileError("Built-in argument count is checked",
    +        "x = Replace(\"a\")\n",
    +        "Built-in 'REPLACE' expects 3-6 arguments, got 1"
    +    );
    +
    +    expectCompileError("Zero-argument built-in rejects arguments",
    +        "x = Now(1)\n",
    +        "Built-in 'NOW' expects 0-0 arguments, got 1"
    +    );
    +}
    +
    +
     // Every token type must map to a printable name (unknowns report "?").
     static void testTokenNames(void) {
         printf("=== token names ===\n");
    @@ -4315,7 +6437,7 @@ int main(void) {
     
         expectCompileError("Method arguments require commas",
             "ctl.Method 1 2\n",
    -        "Expected ','"
    +        "Expected end of statement"
         );
     
         expectCompileError("Show modal flag must be a constant",
    @@ -4523,10 +6645,10 @@ int main(void) {
             "constant name"
         );
     
    -    expectCompileError("CONST of a non-literal rejected",
    +    expectCompileError("CONST of a variable rejected",
             "DIM b AS INTEGER\n"
             "CONST A = b\n",
    -        "Constant value must be a literal"
    +        "Constant expression required: 'b' is not a constant"
         );
     
         expectCompileError("Duplicate CONST rejected",
    @@ -4735,7 +6857,7 @@ int main(void) {
     
         expectCompileError("EXIT of a non-loop rejected",
             "EXIT PRINT\n",
    -        "Expected FOR, DO, SUB, or FUNCTION after EXIT"
    +        "Expected FOR, DO, SUB, FUNCTION or PROPERTY after EXIT"
         );
     
         compileOnly("GET and PUT of a whole UDT record",
    @@ -4882,15 +7004,14 @@ int main(void) {
             "INPUT #1, v$, w$\n"
         );
     
    -    expectCompileError("LINE without INPUT rejected",
    +    expectCompileError("LINE without INPUT or a point rejected",
             "LINE PRINT\n",
    -        "Expected INPUT after LINE"
    +        "Expected '-'"
         );
     
    -    expectCompileError("LINE INPUT without channel rejected",
    +    compileOnly("LINE INPUT without a channel reads the console",
             "DIM v$\n"
    -        "LINE INPUT v$\n",
    -        "Expected # for file channel in LINE INPUT"
    +        "LINE INPUT v$\n"
         );
     
         expectCompileError("LINE INPUT into a non-variable rejected",
    @@ -5521,6 +7642,10 @@ int main(void) {
     
         testCompilerFixes();
         testCompilerLimits();
    +    testConsoleGraphics();
    +    testConstEnumParamArray();
    +    testObjectsAndVariants();
    +    testStringsAndDates();
     
         printf("All tests complete.  Failures: %d\n", (int)gFailCount);
         return gFailCount > 0 ? 1 : 0;
    diff --git a/src/apps/kpunch/dvxbasic/test_suite.c b/src/apps/kpunch/dvxbasic/test_suite.c
    index 35d08cc8..40d0dc37 100644
    --- a/src/apps/kpunch/dvxbasic/test_suite.c
    +++ b/src/apps/kpunch/dvxbasic/test_suite.c
    @@ -71,6 +71,9 @@
     #define UI_STUB_REF_LEN    64
     #define UI_STUB_REF_COUNT  16
     
    +// Failure detail text of a check.
    +#define TEST_DETAIL_LEN    512
    +
     static CaptureT *sUiCap = NULL;
     static char      sUiRefs[UI_STUB_REF_COUNT][UI_STUB_REF_LEN];
     static int32_t   sUiRefNext = 0;
    @@ -251,6 +254,7 @@ static int32_t runSubAndCapture(const char *source, const char *subName, char *o
     static void testBasBuild(void);
     static void testBreakpointNotTrapped(const char *name);
     static void testBytecode(const char *name, const char *source, const uint8_t *expected, int32_t expectedLen);
    +static void testClassRoundTrip(const char *name);
     static void testCommandArgs(const char *name);
     static void testCompileError(const char *name, const char *source, const char *needle);
     static void testDebuggerApi(void);
    @@ -972,10 +976,10 @@ static void testBasBuild(void) {
         int32_t           lines = 0;
     
         memset(&ps, 0, sizeof(ps));
    -    expectTrue("build-append-module", basBuildAppendFile(&ps, "PRINT 1", 7, NULL, &line, &lines), ps.error);
    +    expectTrue("build-append-module", basBuildAppendFile(&ps, "PRINT 1", 7, BAS_BUILD_WRAP_NONE, NULL, &line, &lines), ps.error);
         expectTrue("build-append-newline", ps.sourceLen == 8 && ps.source[7] == '\n', "missing forced newline");
         expectTrue("build-append-lines", line == 1 && lines == 1, "line map wrong");
    -    expectTrue("build-append-form", basBuildAppendFile(&ps, "SUB F_Load\nEND SUB\n", 19, "F", &line, &lines), ps.error);
    +    expectTrue("build-append-form", basBuildAppendFile(&ps, "SUB F_Load\nEND SUB\n", 19, BAS_BUILD_WRAP_FORM, "F", &line, &lines), ps.error);
         expectTrue("build-append-form-map", line == 3 && lines == 2, "form line map wrong");
         expectTrue("build-append-wrapped", strstr(ps.source, "BEGINFORM \"F\"\n") != NULL && strstr(ps.source, "ENDFORM\n") != NULL, "wrapper missing");
         basBuildFreeProject(&ps);
    @@ -1197,6 +1201,103 @@ static void testBytecode(const char *name, const char *source, const uint8_t *ex
     }
     
     
    +// A module with a class keeps its class table through strip, compaction
    +// (set-up code address, OP_CALL_ME) and serialization, and runs the same.
    +static void testClassRoundTrip(const char *name) {
    +    const char *source =
    +        "DIM a AS NEW Acc\n"
    +        "a.Add 5\n"
    +        "a.Add 7\n"
    +        "PRINT a(0); a.Total; a.Count\n"
    +        "SET a = Nothing\n"
    +        "PRINT \"end\"\n"
    +        "BEGINCLASS \"Acc\"\n"
    +        "Private vals As New Collection\n"
    +        "Private Sub Class_Initialize()\n"
    +        "  PRINT \"init\"\n"
    +        "End Sub\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"term\"; Count\n"
    +        "End Sub\n"
    +        "Public Sub Add(v As Integer)\n"
    +        "  vals.Add v\n"
    +        "End Sub\n"
    +        "Public Function Count() As Long\n"
    +        "  Count = vals.Count\n"
    +        "End Function\n"
    +        "Public Function Total(Optional start As Integer) As Long\n"
    +        "  DIM t AS LONG\n"
    +        "  t = start\n"
    +        "  FOR EACH v IN vals\n"
    +        "    t = t + v\n"
    +        "  NEXT\n"
    +        "  Total = t\n"
    +        "End Function\n"
    +        "Property Get Item(i As Integer) As Integer\n"
    +        "Attribute Item.VB_UserMemId = 0\n"
    +        "  Item = vals(i + 1)\n"
    +        "End Property\n"
    +        "ENDCLASS\n";
    +    char        detail[TEST_DETAIL_LEN];
    +    BasModuleT *mod = compileSource(name, source);
    +    bool        ok  = mod != NULL;
    +
    +    detail[0] = '\0';
    +
    +    if (ok) {
    +        const BasClassInfoT *cls     = (mod->classCount == 1) ? &mod->classes[0] : NULL;
    +        bool                 dflt    = false;
    +
    +        for (int32_t i = 0; cls && i < cls->memberCount; i++) {
    +            dflt = dflt || (strcmp(cls->members[i].name, "Item") == 0 && (cls->members[i].flags & BAS_MEMBER_DEFAULT) != 0);
    +        }
    +
    +        ok = cls && strcmp(cls->name, "Acc") == 0 && cls->memberCount == 4 && cls->terminateProc >= 0 && dflt;
    +
    +        if (!ok) {
    +            snprintf(detail, sizeof(detail), "class table wrong (classes %d)", (int)mod->classCount);
    +        }
    +    }
    +
    +    if (ok) {
    +        basStripModule(mod);
    +        basCompactBytecode(mod);
    +
    +        int32_t     len  = 0;
    +        uint8_t    *data = basModuleSerialize(mod, &len);
    +        BasModuleT *back = data ? basModuleDeserialize(data, len) : NULL;
    +
    +        free(data);
    +
    +        if (!back) {
    +            snprintf(detail, sizeof(detail), "serialize round trip failed");
    +            ok = false;
    +        } else {
    +            const char *expected = "init\n5 12 2 \nterm2 \nend\n";
    +            BasVmT     *vm       = basVmCreate();
    +            CaptureT    cap;
    +
    +            captureReset(&cap);
    +            basVmLoadModule(vm, back);
    +            basVmSetPrintCallback(vm, capturePrint, &cap);
    +
    +            BasVmResultE rc = basVmRun(vm);
    +
    +            if (rc != BAS_VM_HALTED || strcmp(cap.buf, expected) != 0) {
    +                snprintf(detail, sizeof(detail), "rc=%d out=[%s] expected=[%s]", (int)rc, cap.buf, expected);
    +                ok = false;
    +            }
    +
    +            basVmDestroy(vm);
    +            basModuleFree(back);
    +        }
    +    }
    +
    +    basModuleFree(mod);
    +    expectTrue(name, ok, detail);
    +}
    +
    +
     // COMMAND$: the whole argument string, each argument by number (quotes
     // removed, "" past the end or with no arguments), a replaced command
     // line, and a negative index is error 5.
    @@ -4529,6 +4630,21 @@ int main(void) {
             "handler",
             "hi\n");
     
    +    // An object a handler made goes when the handler returns: its
    +    // Class_Terminate still runs before the call hands back to the host.
    +    TEST_SUB_EQ("sub-call-class-terminate",
    +        "SUB handler\n"
    +        "  DIM o AS NEW Tracker\n"
    +        "  PRINT \"in handler\"\n"
    +        "END SUB\n"
    +        "BEGINCLASS \"Tracker\"\n"
    +        "Private Sub Class_Terminate()\n"
    +        "  PRINT \"terminated\"\n"
    +        "End Sub\n"
    +        "ENDCLASS\n",
    +        "handler",
    +        "in handler\nterminated\n");
    +
         TEST_SUB_EQ("sub-call-on-error-div",
             "SUB handler\n"
             "  ON ERROR GOTO h\n"
    @@ -6986,6 +7102,7 @@ int main(void) {
         // Debugger pause vs ON ERROR, and the release payload path.
         testBreakpointNotTrapped("breakpoint-not-trapped");
         testReleaseRoundTrip("release-round-trip");
    +    testClassRoundTrip("class-round-trip");
     
         // Hand-assembled p-code, lexer token stream and raw codegen output.
         testHandAssembled();
    diff --git a/src/include/basic/commdlg.bas b/src/include/basic/commdlg.bas
    index 0b5b0460..9c621326 100644
    --- a/src/include/basic/commdlg.bas
    +++ b/src/include/basic/commdlg.bas
    @@ -22,7 +22,8 @@
     
     ' commdlg.bas -- Common Dialog Library for DVX BASIC
     '
    -' Provides file dialogs, input dialogs, and other common UI dialogs.
    +' Provides file dialogs, input dialogs, the color dialog, and other
    +' common UI dialogs.
     ' Wrappers handle the AppContextT parameter internally so BASIC
     ' programs just pass the visible arguments.
     '
    @@ -57,6 +58,12 @@ DECLARE LIBRARY "basrt"
         ' Show a modal integer input box with spinner. Returns the entered value.
         DECLARE FUNCTION DlgIntInput(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL defaultVal AS LONG, BYVAL minVal AS LONG, BYVAL maxVal AS LONG) AS LONG
     
    +    ' Show the color dialog: basic colors, Red/Green/Blue sliders and a
    +    ' preview, starting at defaultColor.  Returns the chosen color as
    +    ' RGB() makes it (ForeColor/BackColor use the same form), or -1 if
    +    ' cancelled.
    +    DECLARE FUNCTION DlgColor(BYVAL title AS STRING, BYVAL defaultColor AS LONG) AS LONG
    +
         ' Show a "Save changes?" prompt with Yes/No/Cancel buttons.
         DECLARE FUNCTION DlgPromptSave(BYVAL title AS STRING) AS INTEGER
     END DECLARE
    diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs
    index b9208557..d99edb86 100644
    --- a/src/libs/kpunch/libdvx/apiref.dhs
    +++ b/src/libs/kpunch/libdvx/apiref.dhs
    @@ -2325,6 +2325,10 @@ Install an icon that is already in display pixel format (for example from dvxLoa
     .index dvxClipboardClear
     .index dvxClipboardCopy
     .index dvxClipboardGet
    +.index dvxDragFeedbackHide
    +.index dvxDragImageShow
    +.index dvxDragOutlineShow
    +.index Drag Feedback
     .index dvxResLoadIcon
     .index dvxResLoadText
     .index dvxResLoadData
    @@ -3579,6 +3583,34 @@ Retrieve the current clipboard contents. Returns a pointer to the internal buffe
     
     Returns: Clipboard text, or NULL.
     
    +.h2 Drag Feedback
    +
    +For programs that implement drag and drop: an outline or a picture that follows the mouse over every window (drawn under the cursor) until it is hidden. One is shown at a time; showing another replaces it. The program still tracks the mouse and decides where a drop lands; this only draws.
    +
    +.h3 dvxDragOutlineShow
    +
    +.code
    +void dvxDragOutlineShow(AppContextT *ctx, int32_t w, int32_t h, int32_t offX, int32_t offY);
    +.endcode
    +
    +Show a w x h dotted outline (two-tone, so it shows on any background) whose top-left corner stays (offX, offY) from the mouse pointer -- typically the dragged object's size and its offset from where it was grabbed.
    +
    +.h3 dvxDragImageShow
    +
    +.code
    +void dvxDragImageShow(AppContextT *ctx, const uint8_t *pixels, int32_t w, int32_t h, int32_t pitch, bool hasKey, uint32_t keyColor);
    +.endcode
    +
    +Show a picture centered on the mouse pointer. pixels are in the display's native format, as dvxLoadImage or dvxLoadImageAlpha return them, and must stay valid until the feedback is hidden or replaced. With hasKey, pixels of keyColor are transparent (pass dvxLoadImageAlpha's outputs).
    +
    +.h3 dvxDragFeedbackHide
    +
    +.code
    +void dvxDragFeedbackHide(AppContextT *ctx);
    +.endcode
    +
    +Remove the outline or picture.
    +
     .h2 Resource Loading
     
     .h3 dvxResLoadIcon
    @@ -4626,6 +4658,8 @@ WGT_MAX_CREATE_ARGS is 3.
     .index dvxInputBox
     .index dvxIntInputBox
     .index dvxChoiceDialog
    +.index dvxColorDialog
    +.index Color Dialog
     .index dvxDialogCreate
     .index dvxDialogAddOkCancel
     .index dvxDialogAddField
    @@ -4878,6 +4912,36 @@ Display a modal dialog with a listbox of choices. The user picks one item and cl
     
     Returns: true if a choice was made, false if cancelled.
     
    +.h2 Color Dialog
    +
    +.h3 dvxColorDialog
    +
    +.code
    +bool dvxColorDialog(AppContextT *ctx, const char *title, uint32_t *rgbInOut);
    +.endcode
    +
    +Display a modal color picker: a palette of 48 basic colors, Red, Green and Blue sliders each paired with a number box (0-255), and a preview swatch. Clicking a basic color sets all three channels; the basic color matching the current color is framed. From the keyboard, Tab moves between the sliders and number boxes (the Red slider has the focus first), the arrow keys, Page Up/Down, Home and End move a slider, and Enter chooses OK. Works at every color depth; on 8-bit desktops the swatches show the nearest palette colors, but the chosen value is exact.
    +
    +.table
    +  Parameter     Description
    +  ---------     -----------
    +  ctx           Application context
    +  title         Window title (NULL: "Color")
    +  rgbInOut      In: the starting color; out: the chosen color. Both 0x00RRGGBB, the layout of DVX BASIC's RGB()
    +.endtable
    +
    +Returns: true if OK was chosen, false if cancelled or closed (then *rgbInOut is unchanged).
    +
    +Macros for the 0x00RRGGBB layout: DVX_RGB(r, g, b) builds a color from three 0-255 channels, and DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb) and DVX_RGB_BLUE(rgb) take one apart.
    +
    +.code
    +uint32_t rgb = DVX_RGB(r, g, b);
    +
    +if (dvxColorDialog(ctx, "Title Bar", &rgb)) {
    +    dvxSetColor(ctx, ColorActiveTitleBgE, DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb), DVX_RGB_BLUE(rgb));
    +}
    +.endcode
    +
     .h2 Custom Dialogs
     
     Any dialog built from widgets, with the behaviour of the boxes above: modal, Enter chooses OK (unless a button has the focus), Escape or closing the window cancels, a nested dialog (a Browse button, an error box) works, and the dialog ends cleanly if its program is ended meanwhile.
    diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c
    index 138a77be..f9ade146 100644
    --- a/src/libs/kpunch/libdvx/dvxApp.c
    +++ b/src/libs/kpunch/libdvx/dvxApp.c
    @@ -195,6 +195,22 @@ typedef struct {
         uint8_t  value[RGB_CHANNELS][CHANNEL_LUT_SIZE];
     } ChannelLutT;
     
    +// Drag feedback (dvxDragOutlineShow / dvxDragImageShow): a w x h outline
    +// or picture whose top-left corner stays (offX, offY) from the mouse
    +// pointer, drawn over every window and under the cursor.  pixels NULL
    +// means the outline.
    +typedef struct {
    +    bool           active;
    +    int32_t        w;
    +    int32_t        h;
    +    int32_t        offX;
    +    int32_t        offY;
    +    const uint8_t *pixels;     // caller-owned, native format; NULL: dotted outline
    +    int32_t        pitch;
    +    bool           hasKey;     // pixels of keyColor are transparent
    +    uint32_t       keyColor;
    +} DragFeedbackT;
    +
     
     // ============================================================
     // Prototypes
    @@ -231,12 +247,15 @@ static void deferDestroyWindow(AppContextT *ctx, WindowT *win);
     static void destroyWindowPrep(AppContextT *ctx, WindowT *win);
     static void destroyWindowSync(AppContextT *ctx, WindowT *win);
     static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y);
    +static void dirtyDragFeedback(AppContextT *ctx, int32_t x, int32_t y);
     static void dirtyIconStrip(AppContextT *ctx);
     static bool dispatchAccelKey(AppContextT *ctx, char key);
     static void dispatchEvents(AppContextT *ctx);
     static void dispatchKey(AppContextT *ctx, int32_t scancode, int32_t ascii, int32_t shiftFlags);
     static void dispatchRawKey(AppContextT *ctx, const RawKeyEventT *evt);
    +static void dragFeedbackSet(AppContextT *ctx, const DragFeedbackT *fb);
     static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y);
    +static void drawDragFeedback(AppContextT *ctx, const RectT *clipTo);
     static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, bool hasCheck, const RectT *clipTo);
     static bool drawPopupRow(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, int32_t px, int32_t pw, int32_t itemY, bool separator, bool enabled, bool hovered, uint32_t *fg, uint32_t *bg);
     static void drawSysMenu(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const RectT *clipTo);
    @@ -368,6 +387,10 @@ static int32_t         sEntropyLen  = 0;
     static DvxSoundHookT sSoundHook      = NULL;
     static int32_t       sOpenSoundWinId = 0;
     
    +// The drag feedback shown (see DragFeedbackT); one mouse, so one for the
    +// whole desktop.
    +static DragFeedbackT sDragFeedback;
    +
     // 4x4 Bayer dithering offsets for ordered dithering when converting
     // 24-bit wallpaper images to 15/16-bit pixel formats.
     static const int32_t sBayerMatrix[BAYER_SIZE][BAYER_SIZE] = {
    @@ -1235,6 +1258,11 @@ static void compositeAndFlush(AppContextT *ctx) {
                 }
             }
     
    +        // 5b. Draw the drag outline or picture
    +        if (sDragFeedback.active) {
    +            drawDragFeedback(ctx, dr);
    +        }
    +
             // 6. Draw cursor
             drawCursorAt(ctx, ctx->mouseX, ctx->mouseY);
     
    @@ -1555,6 +1583,14 @@ static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y) {
     }
     
     
    +// Dirties the drag feedback's area for the mouse at (x, y), if shown.
    +static void dirtyDragFeedback(AppContextT *ctx, int32_t x, int32_t y) {
    +    if (sDragFeedback.active) {
    +        dirtyListAdd(&ctx->dirty, x + sDragFeedback.offX, y + sDragFeedback.offY, sDragFeedback.w, sDragFeedback.h);
    +    }
    +}
    +
    +
     // Dirties the minimized-icon strip at the bottom of the screen.  Called
     // whenever a minimized window appears, disappears or changes visibility,
     // since the remaining icons repack.
    @@ -1677,6 +1713,8 @@ static void dispatchEvents(AppContextT *ctx) {
         if (mx != ctx->prevMouseX || my != ctx->prevMouseY) {
             dirtyCursorArea(ctx, ctx->prevMouseX, ctx->prevMouseY);
             dirtyCursorArea(ctx, mx, my);
    +        dirtyDragFeedback(ctx, ctx->prevMouseX, ctx->prevMouseY);
    +        dirtyDragFeedback(ctx, mx, my);
     
             // Update cursor shape based on what the mouse is hovering over
             updateCursorShape(ctx);
    @@ -2190,6 +2228,15 @@ static void dispatchRawKey(AppContextT *ctx, const RawKeyEventT *evt) {
     }
     
     
    +// Replaces the drag feedback, repainting where the old one was and where
    +// the new one goes.
    +static void dragFeedbackSet(AppContextT *ctx, const DragFeedbackT *fb) {
    +    dirtyDragFeedback(ctx, ctx->mouseX, ctx->mouseY);
    +    sDragFeedback = *fb;
    +    dirtyDragFeedback(ctx, ctx->mouseX, ctx->mouseY);
    +}
    +
    +
     // ============================================================
     // drawCursorAt
     // ============================================================
    @@ -2207,6 +2254,29 @@ static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y) {
     }
     
     
    +// Draws the drag feedback at the mouse into the dirty rect clipTo (the
    +// display clip is already set to it): the picture, or a two-tone dotted
    +// outline that shows on any background.
    +static void drawDragFeedback(AppContextT *ctx, const RectT *clipTo) {
    +    DisplayT *d = &ctx->display;
    +    RectT     r = { ctx->mouseX + sDragFeedback.offX, ctx->mouseY + sDragFeedback.offY, sDragFeedback.w, sDragFeedback.h };
    +    RectT     isect;
    +
    +    if (!rectIntersect(clipTo, &r, &isect)) {
    +        return;
    +    }
    +
    +    if (!sDragFeedback.pixels) {
    +        drawFocusRect(d, &ctx->blitOps, r.x, r.y, r.w, r.h, ctx->colors.contentFg);
    +        drawFocusRect(d, &ctx->blitOps, r.x + 1, r.y + 1, r.w - 2, r.h - 2, ctx->colors.contentBg);
    +    } else if (sDragFeedback.hasKey) {
    +        rectCopyTransparent(d, &ctx->blitOps, r.x, r.y, sDragFeedback.pixels, sDragFeedback.pitch, 0, 0, r.w, r.h, sDragFeedback.keyColor);
    +    } else {
    +        rectCopy(d, &ctx->blitOps, r.x, r.y, sDragFeedback.pixels, sDragFeedback.pitch, 0, 0, r.w, r.h);
    +    }
    +}
    +
    +
     // ============================================================
     // drawPopupLevel  -- draw one popup menu (bevel + items)
     // ============================================================
    @@ -2444,6 +2514,7 @@ void dvxAppTestReset(void) {
         sLastHotkeyId = DVX_HOTKEY_NONE;
         arrfree(sAppCalls);
         sAppCalls = NULL;
    +    memset(&sDragFeedback, 0, sizeof(sDragFeedback));
     }
     #endif
     
    @@ -2833,6 +2904,55 @@ void dvxDestroyWindowNow(AppContextT *ctx, WindowT *win) {
     }
     
     
    +void dvxDragFeedbackHide(AppContextT *ctx) {
    +    DragFeedbackT none;
    +
    +    if (!ctx) {
    +        return;
    +    }
    +
    +    memset(&none, 0, sizeof(none));
    +    dragFeedbackSet(ctx, &none);
    +}
    +
    +
    +void dvxDragImageShow(AppContextT *ctx, const uint8_t *pixels, int32_t w, int32_t h, int32_t pitch, bool hasKey, uint32_t keyColor) {
    +    DragFeedbackT fb;
    +
    +    if (!ctx || !pixels || w <= 0 || h <= 0) {
    +        return;
    +    }
    +
    +    fb.active   = true;
    +    fb.w        = w;
    +    fb.h        = h;
    +    fb.offX     = -w / 2;
    +    fb.offY     = -h / 2;
    +    fb.pixels   = pixels;
    +    fb.pitch    = pitch;
    +    fb.hasKey   = hasKey;
    +    fb.keyColor = keyColor;
    +    dragFeedbackSet(ctx, &fb);
    +}
    +
    +
    +void dvxDragOutlineShow(AppContextT *ctx, int32_t w, int32_t h, int32_t offX, int32_t offY) {
    +    DragFeedbackT fb;
    +
    +    if (!ctx || w <= 0 || h <= 0) {
    +        return;
    +    }
    +
    +    memset(&fb, 0, sizeof(fb));
    +    fb.active = true;
    +    fb.w      = w;
    +    fb.h      = h;
    +    fb.offX   = offX;
    +    fb.offY   = offY;
    +    dragFeedbackSet(ctx, &fb);
    +}
    +
    +
     // ============================================================
     // dvxExclusiveBegin / dvxExclusiveEnd and the full-screen display
     // ============================================================
    diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h
    index 61e4769f..1e7e1f4e 100644
    --- a/src/libs/kpunch/libdvx/dvxApp.h
    +++ b/src/libs/kpunch/libdvx/dvxApp.h
    @@ -301,7 +301,7 @@ void dvxExclusiveEnd(AppContextT *ctx, WindowT *win);
     void dvxExclusiveFlush(AppContextT *ctx, const RectT *r);
     
     // Show an 8-bit picture of palette indexes on the whole screen: scaled by
    -// the largest whole factor that fits, centred on black, through the
    +// the largest whole factor that fits, centered on black, through the
     // palette (dvxExclusiveSetPalette) in any pixel format.  False when not
     // active.
     bool dvxExclusivePresent8(AppContextT *ctx, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch);
    @@ -617,6 +617,28 @@ void dvxClipboardCopy(const char *text, int32_t len);
     // NULL if the clipboard is empty.
     const char *dvxClipboardGet(int32_t *outLen);
     
    +// ============================================================
    +// Drag feedback
    +// ============================================================
    +//
    +// For programs that implement drag and drop: an outline or a picture
    +// that follows the mouse over every window (under the cursor) until it
    +// is hidden.  One is shown at a time; showing another replaces it.
    +
    +// Hide the outline or picture.
    +void dvxDragFeedbackHide(AppContextT *ctx);
    +
    +// Show a picture centered on the mouse pointer.  pixels are in the
    +// display's native format (as dvxLoadImage / dvxLoadImageAlpha return)
    +// and must stay valid until the feedback is hidden or replaced; with
    +// hasKey, pixels of keyColor are transparent.
    +void dvxDragImageShow(AppContextT *ctx, const uint8_t *pixels, int32_t w, int32_t h, int32_t pitch, bool hasKey, uint32_t keyColor);
    +
    +// Show a w x h dotted outline whose top-left corner stays (offX, offY)
    +// from the mouse pointer -- typically the dragged object's size and its
    +// offset from where it was grabbed.
    +void dvxDragOutlineShow(AppContextT *ctx, int32_t w, int32_t h, int32_t offX, int32_t offY);
    +
     // ============================================================
     // Resource loading helpers
     // ============================================================
    diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c
    index a55ed572..732d7741 100644
    --- a/src/libs/kpunch/libdvx/dvxDialog.c
    +++ b/src/libs/kpunch/libdvx/dvxDialog.c
    @@ -49,9 +49,11 @@
     #include "dvxWgtP.h"
     #include "../../../widgets/kpunch/box/box.h"
     #include "../../../widgets/kpunch/button/button.h"
    +#include "../../../widgets/kpunch/canvas/canvas.h"
     #include "../../../widgets/kpunch/dropdown/dropdown.h"
     #include "../../../widgets/kpunch/label/label.h"
     #include "../../../widgets/kpunch/listBox/listBox.h"
    +#include "../../../widgets/kpunch/slider/slider.h"
     #include "../../../widgets/kpunch/spacer/spacer.h"
     #include "../../../widgets/kpunch/spinner/spinner.h"
     #include "../../../widgets/kpunch/textInput/textInpt.h"
    @@ -129,6 +131,28 @@
     #define CB_LIST_HEIGHT   120
     #define CB_PADDING       8
     
    +// Color dialog: a CD_PALETTE_COLS x CD_PALETTE_ROWS grid of basic
    +// colors (each cell CD_SWATCH_W x CD_SWATCH_H, the color inset by
    +// CD_SWATCH_INSET so the selection frame fits around it), one slider and
    +// number box per channel, and a preview swatch.  Colors travel as
    +// 0x00RRGGBB.
    +#define CD_PALETTE_COLS    8
    +#define CD_PALETTE_ROWS    6
    +#define CD_PALETTE_COUNT   (CD_PALETTE_COLS * CD_PALETTE_ROWS)
    +#define CD_SWATCH_W        24
    +#define CD_SWATCH_H        18
    +#define CD_SWATCH_INSET    3
    +#define CD_PALETTE_W       (CD_PALETTE_COLS * CD_SWATCH_W)
    +#define CD_PALETTE_H       (CD_PALETTE_ROWS * CD_SWATCH_H)
    +#define CD_PREVIEW_H       32
    +#define CD_LABEL_W         48
    +#define CD_ROW_GAP         4
    +#define CD_CHANNEL_COUNT   3     // red, green, blue
    +#define CD_CHANNEL_MAX     255
    +#define CD_CHANNEL_BITS    8
    +#define CD_CHANNEL_MASK    0xFF
    +#define CD_SPINNER_STEP    1
    +
     // Nominal client height a dialog is created with before dvxFitWindow
     // shrink-wraps it to its widget tree.
     #define DLG_NOMINAL_H    100
    @@ -221,6 +245,17 @@ typedef struct {
         WidgetT    *listBox;
     } ChoiceBoxStateT;
     
    +typedef struct {
    +    DialogBaseT  base;                          // must be first (window userData)
    +    AppContextT *ctx;
    +    uint32_t    *rgbOut;
    +    uint8_t      rgb[CD_CHANNEL_COUNT];         // current color: red, green, blue
    +    WidgetT     *palette;
    +    WidgetT     *preview;
    +    WidgetT     *sliders[CD_CHANNEL_COUNT];
    +    WidgetT     *spinners[CD_CHANNEL_COUNT];
    +} ColorBoxStateT;
    +
     // dvxDialogCreate's state: heap-allocated (the caller builds the dialog
     // between create and run) and freed by dvxDialogRun.
     typedef struct {
    @@ -256,18 +291,39 @@ typedef struct {
     } FileDialogStateT;
     
     
    +// The color dialog's basic colors (0x00RRGGBB), row by row.
    +static const uint32_t sBasicColors[CD_PALETTE_COUNT] = {
    +    0xFF8080, 0xFFFF80, 0x80FF80, 0x00FF80, 0x80FFFF, 0x0080FF, 0xFF80C0, 0xFF80FF,
    +    0xFF0000, 0xFFFF00, 0x80FF00, 0x00FF40, 0x00FFFF, 0x0080C0, 0x8080C0, 0xFF00FF,
    +    0x804040, 0xFF8040, 0x00FF00, 0x008080, 0x004080, 0x8080FF, 0x800040, 0xFF0080,
    +    0x800000, 0xFF8000, 0x008000, 0x008040, 0x0000FF, 0x0000A0, 0x800080, 0x8000FF,
    +    0x400000, 0x804000, 0x004000, 0x004040, 0x000080, 0x000040, 0x400040, 0x400080,
    +    0x000000, 0x808000, 0x808040, 0x808080, 0x408080, 0xC0C0C0, 0x404040, 0xFFFFFF
    +};
    +
    +// The color dialog's channel labels, in cdPacked order.
    +static const char *const sChannelLabels[CD_CHANNEL_COUNT] = { "&Red:", "&Green:", "&Blue:" };
    +
    +
     // ============================================================
     // Prototypes
     // ============================================================
     
     static void        cbOnDblClick(WidgetT *w);
     static void        cbOnOk(WidgetT *w);
    +static void        cdOnChannel(WidgetT *w);
    +static void        cdOnOk(WidgetT *w);
    +static void        cdOnPalette(WidgetT *w, int32_t cx, int32_t cy, bool drag);
    +static uint32_t    cdPacked(const ColorBoxStateT *cd);
    +static void        cdRefresh(ColorBoxStateT *cd);
    +static void        cdSetColor(ColorBoxStateT *cd, uint32_t rgb);
     static void        customOnOk(WidgetT *w);
     static void        dialogAddOkCancel(WidgetT *root, DialogBaseT *base, const char *okLabel, void (*okFn)(WidgetT *), WidgetAlignE align);
     static void        dialogAddPrompt(AppContextT *ctx, WidgetT *root, const char *prompt, int32_t maxW);
     static void        dialogAddPromptLine(const char *line, int32_t lineLen, int32_t lineIndex, void *userData);
     static void        dialogCenter(AppContextT *ctx, WindowT *win);
     static WindowT    *dialogCreate(AppContextT *ctx, const char *title, int32_t contentW, int32_t contentH, DialogBaseT *base);
    +static void        dialogDestroy(AppContextT *ctx, WindowT *win);
     static void        dialogOnCancelClick(WidgetT *w);
     static void        dialogOnClose(WindowT *win);
     static void        dialogOnKey(WindowT *win, int32_t key, int32_t mod);
    @@ -332,6 +388,109 @@ static void cbOnOk(WidgetT *w) {
     }
     
     
    +// A channel's slider or number box moved: store the value, mirror it
    +// into the other control of the pair (programmatic sets fire no Change)
    +// and repaint.  The control the user is editing is left alone, so typing
    +// in a number box keeps its caret.
    +static void cdOnChannel(WidgetT *w) {
    +    ColorBoxStateT *cd = (ColorBoxStateT *)w->window->userData;
    +
    +    for (int32_t i = 0; i < CD_CHANNEL_COUNT; i++) {
    +        if (w == cd->sliders[i]) {
    +            cd->rgb[i] = (uint8_t)wgtSliderGetValue(w);
    +            wgtSpinnerSetValue(cd->spinners[i], cd->rgb[i]);
    +        } else if (w == cd->spinners[i]) {
    +            cd->rgb[i] = (uint8_t)wgtSpinnerGetValue(w);
    +            wgtSliderSetValue(cd->sliders[i], cd->rgb[i]);
    +        }
    +    }
    +
    +    cdRefresh(cd);
    +}
    +
    +
    +static void cdOnOk(WidgetT *w) {
    +    ColorBoxStateT *cd = (ColorBoxStateT *)w->window->userData;
    +
    +    *cd->rgbOut       = cdPacked(cd);
    +    cd->base.accepted = true;
    +    cd->base.done     = true;
    +}
    +
    +
    +// A click (or drag) on the palette picks the basic color under it.
    +static void cdOnPalette(WidgetT *w, int32_t cx, int32_t cy, bool drag) {
    +    ColorBoxStateT *cd  = (ColorBoxStateT *)w->window->userData;
    +    int32_t         col = cx / CD_SWATCH_W;
    +    int32_t         row = cy / CD_SWATCH_H;
    +
    +    (void)drag;
    +
    +    if (cx >= 0 && cy >= 0 && col < CD_PALETTE_COLS && row < CD_PALETTE_ROWS) {
    +        cdSetColor(cd, sBasicColors[row * CD_PALETTE_COLS + col]);
    +    }
    +}
    +
    +
    +// The current color as 0x00RRGGBB.
    +static uint32_t cdPacked(const ColorBoxStateT *cd) {
    +    uint32_t rgb = 0;
    +
    +    for (int32_t i = 0; i < CD_CHANNEL_COUNT; i++) {
    +        rgb = (rgb << CD_CHANNEL_BITS) | cd->rgb[i];
    +    }
    +
    +    return rgb;
    +}
    +
    +
    +// Repaint the preview in the current color and the palette, framing the
    +// basic color that matches it (if any).
    +static void cdRefresh(ColorBoxStateT *cd) {
    +    const DisplayT     *d       = &cd->ctx->display;
    +    const ColorSchemeT *colors  = &cd->ctx->colors;
    +    uint32_t            current = cdPacked(cd);
    +
    +    wgtCanvasClear(cd->preview, packColor(d, cd->rgb[0], cd->rgb[1], cd->rgb[2]));
    +    wgtCanvasClear(cd->palette, colors->windowFace);
    +
    +    for (int32_t i = 0; i < CD_PALETTE_COUNT; i++) {
    +        uint32_t rgb = sBasicColors[i];
    +        int32_t  x   = (i % CD_PALETTE_COLS) * CD_SWATCH_W;
    +        int32_t  y   = (i / CD_PALETTE_COLS) * CD_SWATCH_H;
    +        uint8_t  r   = (uint8_t)((rgb >> (CD_CHANNEL_BITS * 2)) & CD_CHANNEL_MASK);
    +        uint8_t  g   = (uint8_t)((rgb >> CD_CHANNEL_BITS) & CD_CHANNEL_MASK);
    +        uint8_t  b   = (uint8_t)(rgb & CD_CHANNEL_MASK);
    +
    +        wgtCanvasSetPenColor(cd->palette, packColor(d, r, g, b));
    +        wgtCanvasFillRect(cd->palette, x + CD_SWATCH_INSET, y + CD_SWATCH_INSET, CD_SWATCH_W - CD_SWATCH_INSET * 2, CD_SWATCH_H - CD_SWATCH_INSET * 2);
    +        wgtCanvasSetPenColor(cd->palette, colors->windowShadow);
    +        wgtCanvasDrawRect(cd->palette, x + CD_SWATCH_INSET, y + CD_SWATCH_INSET, CD_SWATCH_W - CD_SWATCH_INSET * 2, CD_SWATCH_H - CD_SWATCH_INSET * 2);
    +
    +        if (rgb == current) {
    +            wgtCanvasSetPenColor(cd->palette, colors->contentFg);
    +            wgtCanvasDrawRect(cd->palette, x, y, CD_SWATCH_W, CD_SWATCH_H);
    +            wgtCanvasDrawRect(cd->palette, x + 1, y + 1, CD_SWATCH_W - 2, CD_SWATCH_H - 2);
    +        }
    +    }
    +}
    +
    +
    +// Make rgb (0x00RRGGBB) the current color: every slider, number box
    +// and the swatches follow.
    +static void cdSetColor(ColorBoxStateT *cd, uint32_t rgb) {
    +    for (int32_t i = 0; i < CD_CHANNEL_COUNT; i++) {
    +        int32_t shift = CD_CHANNEL_BITS * (CD_CHANNEL_COUNT - 1 - i);
    +
    +        cd->rgb[i] = (uint8_t)((rgb >> shift) & CD_CHANNEL_MASK);
    +        wgtSliderSetValue(cd->sliders[i], cd->rgb[i]);
    +        wgtSpinnerSetValue(cd->spinners[i], cd->rgb[i]);
    +    }
    +
    +    cdRefresh(cd);
    +}
    +
    +
     // OK of a custom dialog: accepted once the caller's onOk (if any) agrees.
     static void customOnOk(WidgetT *w) {
         dvxDialogEnd(w->window, true);
    @@ -431,6 +590,17 @@ static WindowT *dialogCreate(AppContextT *ctx, const char *title, int32_t conten
     }
     
     
    +// Destroy a dialog window, first clearing its hooks into the caller's
    +// stack state: the destroy may be deferred past the caller's return.
    +static void dialogDestroy(AppContextT *ctx, WindowT *win) {
    +    win->userData = NULL;
    +    win->onKey    = NULL;
    +    win->onClose  = NULL;
    +    win->onPaint  = NULL;
    +    dvxDestroyWindow(ctx, win);
    +}
    +
    +
     // Cancel button of any dialog: end the dialog unaccepted.
     static void dialogOnCancelClick(WidgetT *w) {
         DialogBaseT *base = (DialogBaseT *)w->window->userData;
    @@ -512,11 +682,7 @@ static void dialogRunModal(AppContextT *ctx, WindowT *win, DialogBaseT *base) {
         restoreModal(ctx, prevModal, prevModalId);
     
         if (dialogWindowLive(ctx, win, winId, false)) {
    -        win->userData = NULL;
    -        win->onKey    = NULL;
    -        win->onClose  = NULL;
    -        win->onPaint  = NULL;
    -        dvxDestroyWindow(ctx, win);
    +        dialogDestroy(ctx, win);
         }
     }
     
    @@ -678,6 +844,70 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
     }
     
     
    +bool dvxColorDialog(AppContextT *ctx, const char *title, uint32_t *rgbInOut) {
    +    if (!ctx || !rgbInOut) {
    +        return false;
    +    }
    +
    +    ColorBoxStateT cd;
    +    memset(&cd, 0, sizeof(cd));
    +    cd.ctx    = ctx;
    +    cd.rgbOut = rgbInOut;
    +
    +    WindowT *win = dialogCreate(ctx, title ? title : "Color", CD_PALETTE_W, DLG_NOMINAL_H, &cd.base);
    +
    +    if (!win) {
    +        return false;
    +    }
    +
    +    WidgetT *root = win->widgetRoot;
    +
    +    cd.palette = wgtCanvas(root, CD_PALETTE_W, CD_PALETTE_H);
    +
    +    for (int32_t i = 0; i < CD_CHANNEL_COUNT; i++) {
    +        WidgetT *row = wgtHBox(root);
    +        WidgetT *lbl = row ? wgtLabel(row, sChannelLabels[i]) : NULL;
    +
    +        if (!lbl) {
    +            break;
    +        }
    +
    +        row->spacing   = wgtPixels(CD_ROW_GAP);
    +        lbl->minW      = wgtPixels(CD_LABEL_W);
    +        cd.sliders[i]  = wgtSlider(row, 0, CD_CHANNEL_MAX);
    +        cd.spinners[i] = wgtSpinner(row, 0, CD_CHANNEL_MAX, CD_SPINNER_STEP);
    +
    +        if (!cd.sliders[i] || !cd.spinners[i]) {
    +            break;
    +        }
    +
    +        cd.sliders[i]->weight    = WGT_WEIGHT_FILL;
    +        cd.sliders[i]->onChange  = cdOnChannel;
    +        cd.spinners[i]->onChange = cdOnChannel;
    +    }
    +
    +    cd.preview = wgtCanvas(root, CD_PALETTE_W, CD_PREVIEW_H);
    +
    +    // Every control must exist: the callbacks and cdRefresh use them all.
    +    bool complete = cd.palette && cd.preview && cd.sliders[CD_CHANNEL_COUNT - 1] && cd.spinners[CD_CHANNEL_COUNT - 1];
    +
    +    if (!complete) {
    +        dialogDestroy(ctx, win);
    +        return false;
    +    }
    +
    +    wgtCanvasSetMouseCallback(cd.palette, cdOnPalette);
    +    cdSetColor(&cd, *rgbInOut);
    +    wgtSetFocused(cd.sliders[0]);
    +    dialogAddOkCancel(root, &cd.base, "&OK", cdOnOk, AlignCenterE);
    +    dvxFitWindow(ctx, win);
    +    dialogCenter(ctx, win);
    +    dialogRunModal(ctx, win, &cd.base);
    +
    +    return cd.base.accepted;
    +}
    +
    +
     WidgetT *dvxDialogAddField(WindowT *dlg, const char *label, int32_t labelW, int32_t maxLen, const char *value) {
         WidgetT *row = (dlg && dlg->widgetRoot) ? wgtHBox(dlg->widgetRoot) : NULL;
     
    diff --git a/src/libs/kpunch/libdvx/dvxDlg.h b/src/libs/kpunch/libdvx/dvxDlg.h
    index e3a2899b..4a7bafa7 100644
    --- a/src/libs/kpunch/libdvx/dvxDlg.h
    +++ b/src/libs/kpunch/libdvx/dvxDlg.h
    @@ -165,6 +165,28 @@ int32_t dvxPromptSave(AppContextT *ctx, const char *title);
     
     bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, const char **items, int32_t itemCount, int32_t defaultIdx, int32_t *outIdx);
     
    +// ============================================================
    +// Color dialog
    +// ============================================================
    +//
    +// Display a modal color picker: a palette of basic colors, Red / Green /
    +// Blue sliders each paired with a number box (0-255), and a preview
    +// swatch.  *rgbInOut holds the starting color and receives the chosen
    +// one, both as 0x00RRGGBB (the layout of DVX BASIC's RGB()).  Returns
    +// true when the user chose OK, false when cancelled or closed (then
    +// *rgbInOut is unchanged).  title may be NULL ("Color").
    +
    +bool dvxColorDialog(AppContextT *ctx, const char *title, uint32_t *rgbInOut);
    +
    +// A 0x00RRGGBB color from its channels (0-255), and each channel of one.
    +#define DVX_RGB_SHIFT_RED    16
    +#define DVX_RGB_SHIFT_GREEN  8
    +#define DVX_RGB_CHANNEL_MASK 0xFFu
    +#define DVX_RGB(r, g, b)     (((uint32_t)(r) << DVX_RGB_SHIFT_RED) | ((uint32_t)(g) << DVX_RGB_SHIFT_GREEN) | (uint32_t)(b))
    +#define DVX_RGB_RED(rgb)     ((uint8_t)(((rgb) >> DVX_RGB_SHIFT_RED) & DVX_RGB_CHANNEL_MASK))
    +#define DVX_RGB_GREEN(rgb)   ((uint8_t)(((rgb) >> DVX_RGB_SHIFT_GREEN) & DVX_RGB_CHANNEL_MASK))
    +#define DVX_RGB_BLUE(rgb)    ((uint8_t)((rgb) & DVX_RGB_CHANNEL_MASK))
    +
     // ============================================================
     // Custom dialogs
     // ============================================================
    diff --git a/src/test/audio/Makefile b/src/test/audio/Makefile
    index 4170f694..f2848e0a 100644
    --- a/src/test/audio/Makefile
    +++ b/src/test/audio/Makefile
    @@ -84,7 +84,7 @@ $(AUDIO_TEST): audioTest.c $(AUDIO_OBJS) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-li
     	@mkdir -p $(BINDIR)
     	$(HOSTCC) $(CFLAGS) -o $@ $< $(AUDIO_OBJS) $(DVXTEST_LINK)
     
    --include $(AUDIO_OBJS:.o=.d) $(AUDIO_TEST).d
    +-include $(AUDIO_OBJS:.o=.d) $(AUDIO_TEST).d $(FAKE_DRVS:.drv=.d)
     
     run: $(AUDIO_TEST) $(FAKE_DRVS)
     	cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./audioTest
    diff --git a/src/test/basic/Makefile b/src/test/basic/Makefile
    index 6a16bfcc..c7b36d25 100644
    --- a/src/test/basic/Makefile
    +++ b/src/test/basic/Makefile
    @@ -72,7 +72,7 @@ FUZZ_STEPS  = 200000
     ASAN_ENV    = ASAN_OPTIONS=allocator_may_return_null=1:detect_leaks=1
     
     COMPILER_SRCS = $(DVXBASIC)/compiler/lexer.c $(DVXBASIC)/compiler/parser.c $(DVXBASIC)/compiler/codegen.c $(DVXBASIC)/compiler/symtab.c
    -RUNTIME_SRCS  = $(DVXBASIC)/runtime/vm.c $(DVXBASIC)/runtime/values.c $(DVXBASIC)/runtime/serialize.c $(LIBDVX)/dvxPrefs.c
    +RUNTIME_SRCS  = $(DVXBASIC)/runtime/vm.c $(DVXBASIC)/runtime/values.c $(DVXBASIC)/runtime/dates.c $(DVXBASIC)/runtime/serialize.c $(DVXBASIC)/runtime/gfx.c $(DVXBASIC)/runtime/screen.c $(LIBDVX)/dvxPrefs.c
     SUPPORT_SRCS  = $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c
     
     FUZZ_COMPILE_SRCS = fuzzCompile.c fuzzCommon.c $(COMPILER_SRCS) $(RUNTIME_SRCS) $(SUPPORT_SRCS)
    diff --git a/src/test/basic/fixtures/classObjects.bas b/src/test/basic/fixtures/classObjects.bas
    new file mode 100644
    index 00000000..16a984d6
    --- /dev/null
    +++ b/src/test/basic/fixtures/classObjects.bas
    @@ -0,0 +1,64 @@
    +' Class modules, Collection and FOR EACH: a plain run and the stripped,
    +' compacted run must agree (class set-up code, OP_CALL_ME, Terminate).
    +DIM inv AS NEW Inventory
    +inv.Stock "bolt", 10
    +inv.Stock "nut", 25
    +inv.Stock "gear", 3
    +PRINT inv.Count; inv.Total
    +DIM it AS Item
    +FOR EACH it IN inv.Items
    +    PRINT it.Name; "="; it.Qty; " ";
    +NEXT
    +PRINT
    +inv.Items.Remove "nut"
    +PRINT inv.Count; inv.Total
    +SET it = inv.Find("gear")
    +it.Qty = it.Qty + 1
    +PRINT inv.Find("GEAR").Qty
    +SET it = Nothing
    +SET inv = Nothing
    +PRINT "done"
    +
    +BEGINCLASS "Item"
    +Public Qty As Integer
    +Private mName As String
    +Property Get Name() As String
    +    Name = mName
    +End Property
    +Property Let Name(s As String)
    +    mName = UCASE$(s)
    +End Property
    +Private Sub Class_Terminate()
    +    PRINT "[drop "; mName; "]";
    +End Sub
    +ENDCLASS
    +
    +BEGINCLASS "Inventory"
    +Private list As New Collection
    +Public Sub Stock(n As String, q As Integer)
    +    DIM i AS Item
    +    SET i = NEW Item
    +    i.Name = n
    +    i.Qty = q
    +    list.Add i, n
    +End Sub
    +Public Function Count() As Long
    +    Count = list.Count
    +End Function
    +Public Function Total() As Long
    +    DIM t AS LONG
    +    FOR EACH x IN list
    +        t = t + x.Qty
    +    NEXT
    +    Total = t
    +End Function
    +Public Function Items() As Collection
    +    SET Items = list
    +End Function
    +Public Function Find(n As String) As Item
    +    SET Find = list(n)
    +End Function
    +Private Sub Class_Terminate()
    +    PRINT "[inventory closed]"
    +End Sub
    +ENDCLASS
    diff --git a/src/test/basic/fixtures/classObjects.out b/src/test/basic/fixtures/classObjects.out
    new file mode 100644
    index 00000000..e3a17add
    --- /dev/null
    +++ b/src/test/basic/fixtures/classObjects.out
    @@ -0,0 +1,6 @@
    +3 38 
    +BOLT=10  NUT=25  GEAR=3  
    +[drop NUT]2 13 
    +4 
    +[inventory closed]
    +[drop BOLT][drop GEAR]done
    diff --git a/src/test/dialogs/dialogsTest.c b/src/test/dialogs/dialogsTest.c
    index f32d6518..74e5cce0 100644
    --- a/src/test/dialogs/dialogsTest.c
    +++ b/src/test/dialogs/dialogsTest.c
    @@ -35,7 +35,10 @@
     #include "dvxWgtP.h"
     #include "box/box.h"
     #include "button/button.h"
    +#include "canvas/canvas.h"
     #include "listBox/listBox.h"
    +#include "slider/slider.h"
    +#include "spinner/spinner.h"
     #include "textInput/textInpt.h"
     
     #include 
    @@ -90,6 +93,30 @@
     #define FIXTURE_IDX_ATXT    2
     #define FIXTURE_SUB_ENTRIES 3   // "..", "[deep]", d.txt
     
    +// Color dialog fixture: the root's children are the palette, one row
    +// (label, slider, number box) per channel, the preview; a basic color's
    +// cell and where it sits relative to the palette's center.
    +#define COLOR_ROW_RED       1
    +#define COLOR_ROW_GREEN     2
    +#define COLOR_ROW_BLUE      3
    +#define COLOR_IDX_PREVIEW   4
    +#define COLOR_IDX_SLIDER    1
    +#define COLOR_IDX_SPINNER   2
    +#define COLOR_START         0x804020
    +#define COLOR_UNCHANGED     0x123456
    +#define COLOR_SWATCH_DX     (-60)     // cell (1, 1) center (36, 27) minus the palette center (96, 54)
    +#define COLOR_SWATCH_DY     (-27)
    +#define COLOR_SWATCH_RGB    0xFFFF00  // basic color at (1, 1)
    +#define COLOR_GREEN_TEXT    "200"
    +#define COLOR_KEYS_RGB      0xFFC820  // red End -> 255, green typed 200, blue from COLOR_START
    +#define COLOR_CHANNEL_MAX   255
    +#define COLOR_SHIFT_R       16
    +#define COLOR_SHIFT_G       8
    +#define COLOR_MASK          0xFF
    +#define BPP_8               8
    +#define BPP_15              15
    +#define BPP_16              16
    +
     #define SPIN_OVERRANGE_TEXT "999"
     #define SPIN_UNDERRANGE_TEXT "-55"
     #define BACKSPACE_CLEAR_COUNT 4
    @@ -149,6 +176,23 @@ typedef struct {
         WidgetT *input;
     } OtherWindowT;
     
    +typedef enum {
    +    ColorOpOkE,        // OK as opened
    +    ColorOpCancelE,
    +    ColorOpEscE,
    +    ColorOpSwatchE,    // click a basic color, then OK
    +    ColorOpKeysE       // End on the focused Red slider, type into the Green box, Enter
    +} ColorOpE;
    +
    +typedef struct {
    +    ColorOpE op;
    +    uint32_t start;          // the color the dialog was opened with
    +    int32_t  calls;
    +    bool     previewOk;      // the preview showed start
    +    bool     redFocused;     // the Red slider had the focus
    +    int32_t  greenAfter;     // Green slider after the typed number (keys op)
    +} ColorScriptT;
    +
     // caseCustom's onOk state: refuses the first `refuse` OKs, then copies
     // the input's text.
     typedef struct {
    @@ -194,6 +238,7 @@ typedef struct {
     static void     caseAbandon(void);
     static void     caseButtonBox(void);
     static void     caseChoice(void);
    +static void     caseColorDialog(void);
     static void     caseCustom(void);
     static void     caseErrorInfoBox(void);
     static void     caseFileDialog(void);
    @@ -212,6 +257,7 @@ static WidgetT *childAt(WidgetT *parent, int32_t idx);
     static void     clickCloseGadget(WindowT *win);
     static void     clickListRow(WidgetT *lb, int32_t row, bool dbl);
     static void     clickSpinnerArrow(WidgetT *sp, bool up);
    +static void     colorScript(AppContextT *ctx, WindowT *modal, void *userCtx);
     static void     customEnd(WidgetT *w);
     static bool     customOk(WindowT *dlg, void *userCtx);
     static void     fileScript(AppContextT *ctx, WindowT *modal, void *userCtx);
    @@ -409,6 +455,76 @@ static void caseChoice(void) {
     // Custom dialogs: onOk may refuse (the dialog stays up and is answered
     // again), sees the live widgets when it accepts, and is never called on
     // Escape, Cancel or close; NULL onOk accepts at once.
    +static void caseColorDialog(void) {
    +    tCase("color dialog: palette, sliders, number boxes, OK/Cancel at 8/15/16/32 bpp");
    +
    +    const int32_t bpps[] = { BPP_8, BPP_15, BPP_16, SCREEN_BPP };
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(bpps); i++) {
    +        tInit(SCREEN_W, SCREEN_H, bpps[i]);
    +
    +        ColorScriptT cs;
    +        uint32_t     rgb = COLOR_START;
    +
    +        memset(&cs, 0, sizeof(cs));
    +        cs.op    = ColorOpOkE;
    +        cs.start = rgb;
    +        tDialogScript(colorScript, &cs);
    +        TEST_TRUE(dvxColorDialog(tCtx(), NULL, &rgb));
    +        TEST_EQ(rgb, COLOR_START);
    +        TEST_TRUE(cs.previewOk);
    +        TEST_TRUE(cs.redFocused);
    +
    +        tShutdown();
    +    }
    +
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +
    +    ColorScriptT cs;
    +    uint32_t     rgb = COLOR_START;
    +
    +    // A basic color picked with the mouse.
    +    memset(&cs, 0, sizeof(cs));
    +    cs.op    = ColorOpSwatchE;
    +    cs.start = rgb;
    +    tDialogScript(colorScript, &cs);
    +    TEST_TRUE(dvxColorDialog(tCtx(), "Pick", &rgb));
    +    TEST_EQ(rgb, COLOR_SWATCH_RGB);
    +
    +    // Keyboard: End on the Red slider, a typed Green value, Enter.
    +    rgb = COLOR_START;
    +    memset(&cs, 0, sizeof(cs));
    +    cs.op    = ColorOpKeysE;
    +    cs.start = rgb;
    +    tDialogScript(colorScript, &cs);
    +    TEST_TRUE(dvxColorDialog(tCtx(), "Pick", &rgb));
    +    TEST_EQ(rgb, COLOR_KEYS_RGB);
    +    TEST_EQ(cs.greenAfter, (COLOR_KEYS_RGB >> COLOR_SHIFT_G) & COLOR_MASK);
    +
    +    // Cancel and Escape leave the color alone.
    +    rgb = COLOR_UNCHANGED;
    +    memset(&cs, 0, sizeof(cs));
    +    cs.op    = ColorOpCancelE;
    +    cs.start = rgb;
    +    tDialogScript(colorScript, &cs);
    +    TEST_TRUE(!dvxColorDialog(tCtx(), "Pick", &rgb));
    +    memset(&cs, 0, sizeof(cs));
    +    cs.op    = ColorOpEscE;
    +    cs.start = rgb;
    +    tDialogScript(colorScript, &cs);
    +    TEST_TRUE(!dvxColorDialog(tCtx(), "Pick", &rgb));
    +    TEST_EQ(rgb, COLOR_UNCHANGED);
    +
    +    TEST_TRUE(!dvxColorDialog(tCtx(), "Pick", NULL));
    +    TEST_TRUE(!dvxColorDialog(NULL, "Pick", &rgb));
    +
    +    tDialogScript(NULL, NULL);
    +    tPump(1);
    +    TEST_PTR_EQ(tFindWindow("Pick"), NULL);
    +    tShutdown();
    +}
    +
    +
     static void caseCustom(void) {
         tCase("custom dialog");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    @@ -1312,6 +1428,78 @@ static void clickSpinnerArrow(WidgetT *sp, bool up) {
     }
     
     
    +// Color dialog script: checks the opening state, then acts per op.
    +static void colorScript(AppContextT *ctx, WindowT *modal, void *userCtx) {
    +    ColorScriptT *cs = (ColorScriptT *)userCtx;
    +
    +    if (!scriptEnter(modal)) {
    +        return;
    +    }
    +
    +    cs->calls++;
    +
    +    if (cs->calls > 1) {
    +        scriptLeave();
    +        return;
    +    }
    +
    +    WidgetT *root     = modal->widgetRoot;
    +    WidgetT *palette  = childAt(root, 0);
    +    WidgetT *redRow   = childAt(root, COLOR_ROW_RED);
    +    WidgetT *greenRow = childAt(root, COLOR_ROW_GREEN);
    +    WidgetT *preview  = childAt(root, COLOR_IDX_PREVIEW);
    +    uint8_t  r        = (uint8_t)((cs->start >> COLOR_SHIFT_R) & COLOR_MASK);
    +    uint8_t  g        = (uint8_t)((cs->start >> COLOR_SHIFT_G) & COLOR_MASK);
    +    uint8_t  b        = (uint8_t)(cs->start & COLOR_MASK);
    +    int32_t  sx       = 0;
    +    int32_t  sy       = 0;
    +
    +    cs->previewOk  = wgtCanvasGetPixel(preview, 0, 0) == packColor(&ctx->display, r, g, b);
    +    cs->redFocused = wgtGetFocused() == childAt(redRow, COLOR_IDX_SLIDER);
    +
    +    switch (cs->op) {
    +        case ColorOpOkE:
    +            TEST_TRUE(tDialogClickButton(modal, "OK"));
    +            break;
    +
    +        case ColorOpCancelE:
    +            TEST_TRUE(tDialogClickButton(modal, "Cancel"));
    +            break;
    +
    +        case ColorOpEscE:
    +            tKeyEsc();
    +            break;
    +
    +        case ColorOpSwatchE:
    +            tWidgetCenter(palette, &sx, &sy);
    +            tAdvanceClock(SINGLE_CLICK_GAP_MS);
    +            tClick(sx + COLOR_SWATCH_DX, sy + COLOR_SWATCH_DY);
    +            TEST_TRUE(tDialogClickButton(modal, "OK"));
    +            break;
    +
    +        case ColorOpKeysE: {
    +            WidgetT *greenBox = childAt(greenRow, COLOR_IDX_SPINNER);
    +
    +            tKey(KEY_END & ~KEY_EXT_FLAG, 0, 0);
    +            TEST_EQ(wgtSpinnerGetValue(childAt(redRow, COLOR_IDX_SPINNER)), COLOR_CHANNEL_MAX);
    +            wgtSetFocused(greenBox);
    +            tKey(KEY_END & ~KEY_EXT_FLAG, 0, 0);
    +
    +            for (int32_t i = 0; i < BACKSPACE_CLEAR_COUNT; i++) {
    +                tKeyBackspace();
    +            }
    +
    +            tType(COLOR_GREEN_TEXT);
    +            cs->greenAfter = wgtSliderGetValue(childAt(greenRow, COLOR_IDX_SLIDER));
    +            tKeyEnter();
    +            break;
    +        }
    +    }
    +
    +    scriptLeave();
    +}
    +
    +
     // A button of the caller's own: ends the dialog, accepted when its
     // userData is set.
     static void customEnd(WidgetT *w) {
    @@ -1802,6 +1990,7 @@ int main(void) {
         caseIntInputBox();
         caseButtonBox();
         caseChoice();
    +    caseColorDialog();
         caseCustom();
         casePromptSave();
         caseFileDialog();
    diff --git a/src/test/dos/fixtures/scrtest/scrtest.bas b/src/test/dos/fixtures/scrtest/scrtest.bas
    new file mode 100644
    index 00000000..e09d5bad
    --- /dev/null
    +++ b/src/test/dos/fixtures/scrtest/scrtest.bas
    @@ -0,0 +1,61 @@
    +' scrtest.bas -- DOS harness fixture for scripts/scrtest.scr
    +'
    +' The program screen: console text (CLS, COLOR, LOCATE, PRINT with TAB
    +' and ;, INKEY$), then SCREEN 13 drawing (LINE, CIRCLE, PAINT, DRAW,
    +' PALETTE) and FULLSCREEN ON / OFF.  Each step waits for a key the script
    +' types.
    +
    +DIM i AS INTEGER
    +DIM k AS STRING
    +
    +CLS
    +COLOR 14, 1
    +PRINT " DVX BASIC console "
    +COLOR 7, 0
    +LOCATE 3, 5
    +PRINT "LOCATE 3,5"; TAB(30); "TAB 30"
    +COLOR 10
    +FOR i = 1 TO 5
    +    PRINT i;
    +NEXT i
    +PRINT
    +LOCATE 10, 1
    +COLOR 15, 4
    +PRINT " Press a key "
    +COLOR 7, 0
    +k = WaitKey()
    +LOCATE 12, 1
    +PRINT "Key code"; ASC(k); "row"; CSRLIN
    +k = WaitKey()
    +
    +SCREEN 13
    +LINE (0, 0)-(319, 199), 1, BF
    +LINE (10, 10)-(309, 189), 15, B
    +CIRCLE (80, 100), 50, 14, , , 1
    +PAINT (80, 100), 4, 14
    +DRAW "BM200,60 C10 R60 D60 L60 U60 BM230,90 P2,10"
    +PALETTE 2, 32 * 256 + 63
    +FOR i = 0 TO 15
    +    LINE (20 + i * 18, 170)-STEP(16, 12), 32 + i * 4, BF
    +NEXT i
    +LOCATE 2, 3
    +COLOR 15
    +PRINT "SCREEN 13"
    +k = WaitKey()
    +FULLSCREEN ON
    +k = WaitKey()
    +FULLSCREEN OFF
    +LOCATE 24, 3
    +PRINT "Done";
    +
    +
    +' The next key typed in the program's window.
    +FUNCTION WaitKey () AS STRING
    +    DIM key AS STRING
    +
    +    DO
    +        key = INKEY$
    +    LOOP WHILE key = ""
    +
    +    WaitKey = key
    +END FUNCTION
    diff --git a/src/test/dos/fixtures/scrtest/scrtest.dbp b/src/test/dos/fixtures/scrtest/scrtest.dbp
    new file mode 100644
    index 00000000..6f9d29c8
    --- /dev/null
    +++ b/src/test/dos/fixtures/scrtest/scrtest.dbp
    @@ -0,0 +1,11 @@
    +[Project]
    +Name = Screen Test
    +Author = Scott Duensing
    +Publisher = Kangaroo Punch Studios
    +Version = 1.00
    +Copyright = Copyright 2026 Scott Duensing
    +Description = DOS harness fixture: the program screen (console text, SCREEN 13 graphics, full screen)
    +
    +[Modules]
    +File0 = scrtest.bas
    +Count = 1
    diff --git a/src/test/dos/scripts/scrtest.scr b/src/test/dos/scripts/scrtest.scr
    new file mode 100644
    index 00000000..0ab4f72f
    --- /dev/null
    +++ b/src/test/dos/scripts/scrtest.scr
    @@ -0,0 +1,44 @@
    +# scrtest.scr -- the BASIC program screen (fixtures/scrtest).
    +#
    +# TEST\SCRTEST.APP gets its screen window with CLS: console text in
    +# colours, LOCATE, TAB and a number list (con1); a key typed there comes
    +# back from INKEY$ (con2).  The next key starts SCREEN 13: LINE, CIRCLE,
    +# PAINT, DRAW (with P) and PALETTE in the 320x200 window, shown at 2x on
    +# the 800x600 desktop (gfx).  Alt+Enter puts it on the whole screen
    +# (full1, the 320x200 mode) and back; FULLSCREEN ON in the program does
    +# the same (full2) and FULLSCREEN OFF returns (back).  Alt+F4 closes the
    +# window, which ends the program.
    +# VIDEO: 800 600 16
    +# LOG: basrt: program screen full screen on
    +# LOG: basrt: program screen full screen off
    +TIMEOUT 120
    +WAITWIN "Program Manager"
    +MOVE 799 599
    +LAUNCH TEST/SCRTEST.APP
    +WAITWIN "Screen Test"
    +WAIT 30
    +SHOT con1
    +KEY a
    +WAIT 30
    +SHOT con2
    +KEY b
    +WAIT 30
    +SHOT gfx
    +KEY Alt+Enter
    +WAITLOG "basrt: program screen full screen on"
    +WAIT 20
    +SHOT full1
    +KEY Alt+Enter
    +WAITLOG "basrt: program screen full screen off"
    +WAIT 20
    +KEY c
    +WAITLOG "basrt: program screen full screen on"
    +WAIT 20
    +SHOT full2
    +KEY d
    +WAITLOG "basrt: program screen full screen off"
    +WAIT 30
    +SHOT back
    +KEY Alt+F4
    +WAITNOWIN "Screen Test"
    +QUIT
    diff --git a/src/test/dvxtest/dvxtest.c b/src/test/dvxtest/dvxtest.c
    index a2f372d5..62c82760 100644
    --- a/src/test/dvxtest/dvxtest.c
    +++ b/src/test/dvxtest/dvxtest.c
    @@ -25,6 +25,7 @@
     #include "dvxtest.h"
     #include "dvxWgtP.h"
     #include "dvxVideo.h"
    +#include "slider/slider.h"
     #include "stb_ds_wrap.h"
     
     #include 
    @@ -662,6 +663,28 @@ void tClick(int32_t x, int32_t y) {
     }
     
     
    +bool tColorDialogPick(WindowT *modal, uint32_t rgb) {
    +    // dvxColorDialog's root holds the palette, then one row (label,
    +    // slider, number box) per channel, red first.
    +    WidgetT *row     = (modal && modal->widgetRoot && modal->widgetRoot->firstChild) ? modal->widgetRoot->firstChild->nextSibling : NULL;
    +    uint8_t  vals[]  = { DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb), DVX_RGB_BLUE(rgb) };
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(vals); i++) {
    +        WidgetT *slider = (row && row->firstChild) ? row->firstChild->nextSibling : NULL;
    +
    +        if (!slider || !slider->onChange) {
    +            return false;
    +        }
    +
    +        wgtSliderSetValue(slider, vals[i]);
    +        slider->onChange(slider);
    +        row = row->nextSibling;
    +    }
    +
    +    return tDialogClickButton(modal, "OK");
    +}
    +
    +
     AppContextT *tCtx(void) {
         return sCtx;
     }
    diff --git a/src/test/dvxtest/dvxtest.h b/src/test/dvxtest/dvxtest.h
    index caa49aa3..c8e2243d 100644
    --- a/src/test/dvxtest/dvxtest.h
    +++ b/src/test/dvxtest/dvxtest.h
    @@ -220,6 +220,11 @@ void tDialogScript(TDialogScriptFnT fn, void *userCtx);
     // dispatch).  Returns false when no such button exists.
     bool tDialogClickButton(WindowT *modal, const char *caption);
     
    +// In a dvxColorDialog modal: set the color to rgb (0x00RRGGBB) with the
    +// channel sliders and choose OK.  False when the dialog does not look
    +// like the color dialog.
    +bool tColorDialogPick(WindowT *modal, uint32_t rgb);
    +
     // ============================================================
     // Golden images
     // ============================================================
    diff --git a/src/test/formrt/testFormRt.c b/src/test/formrt/testFormRt.c
    index 8124d468..2e06bdd5 100644
    --- a/src/test/formrt/testFormRt.c
    +++ b/src/test/formrt/testFormRt.c
    @@ -34,6 +34,7 @@
     #include "../../apps/kpunch/dvxbasic/compiler/parser.h"
     #include "../../apps/kpunch/dvxbasic/basBuild.h"
     #include "../../apps/kpunch/dvxbasic/basRes.h"
    +#include "../../apps/kpunch/dvxbasic/runtime/screen.h"
     #include "thirdparty/stb_ds_wrap.h"
     #include "timer/timer.h"
     
    @@ -41,6 +42,8 @@
     #include "shellApp.h"
     #include "dvxDlg.h"
     #include "platform/dvxPlatHost.h"
    +#include "dvxWgtP.h"
    +#include "dvxVideo.h"
     #include "dvxWm.h"
     #include "../../libs/kpunch/serial/seclink/secLink.h"
     
    @@ -85,6 +88,12 @@
     #define FRT_PROBE_NUM_BUF   16
     #define FRT_APPS_DIR        FRT_INCLUDE_DIR "/../../apps/kpunch"
     #define FRT_APP_PATH_MAX    512
    +#define FRT_CLASS_WRAP_LEN  (BAS_MAX_IDENT + 64)  // the BEGINCLASS / ENDCLASS lines around a class module, separators and NUL
    +
    +// Clipboard / color dialog / drag and drop cases.
    +#define FRT_COLOR_START     0x123456
    +#define FRT_DRAG_BAD_ACTION 7
    +#define FRT_FORM_EDGE       30     // above the form's content bottom (empty form area, clear of the resize border)
     
     // Widget-independent line prefix of every round-trip form.
     #define RT_FORM_HEADER \
    @@ -114,6 +123,7 @@
     
     // BASIC-facing entry points live in formrt.c without a header (they
     // are reached through DECLARE LIBRARY on DOS).
    +static void caseObjectRefs(void);
     void        CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt);
     void        CommClose(int32_t handle);
     void        CommDetach(int32_t handle);
    @@ -127,6 +137,7 @@ int32_t     CommRecvEncrypted(int32_t handle, int32_t channel);
     int32_t     CommSend(int32_t handle, const char *data, int32_t channel, int32_t encrypt);
     int32_t     CommSetPlain(int32_t handle, int32_t channel, int32_t plain);
     int32_t     DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx);
    +int32_t     DlgColor(const char *title, int32_t defaultColor);
     const char *DlgFileOpen(const char *title, const char *filter);
     const char *DlgFileSave(const char *title, const char *filter);
     const char *DlgInputBox(const char *title, const char *prompt, const char *defaultText);
    @@ -224,7 +235,7 @@ typedef struct {
     } ParseRecT;
     
     
    -static bool        appBuild(FrtAppT *app, const char *frmPath, const char *stubs, const char *tests);
    +static bool        appBuild(FrtAppT *app, const char *frmPath, const char *clsPath, const char *stubs, const char *tests);
     static BasValueT   callIntM(FrtAppT *app, BasControlT *ctrl, const char *method, const int32_t *vals, int32_t count);
     static void        caseAppBasDemo(void);
     static void        caseAppCommandLine(void);
    @@ -232,11 +243,14 @@ static void        caseAppIconEd(void);
     static void        caseAppNotepad(void);
     static void        caseAppNotepadCloseRequest(void);
     static void        caseBuildProject(void);
    +static void        caseClipboard(void);
     static void        caseCommonMethods(void);
     static void        caseControlArrays(void);
     static void        caseCreateFormDynamic(void);
     static void        caseDataBinding(void);
     static void        caseDialogWrappers(void);
    +static void        caseDragDrop(void);
    +static void        caseDrawMethods(void);
     static void        caseEventLoopExits(void);
     static void        caseEvents(void);
     static void        caseExternCalls(void);
    @@ -255,6 +269,7 @@ static void        caseMsgBox(void);
     static void        casePendingUnloadDrain(void);
     static void        casePictureLoadSave(void);
     static void        casePopupMenu(void);
    +static void        caseProgramScreen(void);
     static void        casePropEdges(void);
     static void        casePropTables(void);
     static void        caseQueryUnloadCancel(void);
    @@ -276,6 +291,7 @@ static void        caseValidateCancel(void);
     static void        caseVisibleKeepsModal(void);
     static WidgetT    *childAt(WidgetT *w, int32_t idx);
     static void        dialogClick(AppContextT *ctx, WindowT *modal, void *userCtx);
    +static void        dragCallMethod(FrtAppT *app, BasControlT *ctrl, int32_t action);
     static bool        expectBuild(FrtAppT *app, const char *bas, const char *const *frms, int32_t frmCount);
     static void        fileDialogType(AppContextT *ctx, WindowT *modal, void *userCtx);
     static int32_t     frtProbeRetIntStr(WidgetT *w, const char *text);
    @@ -336,24 +352,37 @@ static const WgtMethodDescT sProbeMethods[] = {
     // help.bas and print.bas are the module source, as in the apps' projects
     // (HelpView resolves to the stub library; the printing functions are only
     // declared, the tests never call them).
    -static bool appBuild(FrtAppT *app, const char *frmPath, const char *stubs, const char *tests) {
    +static bool appBuild(FrtAppT *app, const char *frmPath, const char *clsPath, const char *stubs, const char *tests) {
         int32_t len      = 0;
         int32_t helpLen  = 0;
         int32_t printLen = 0;
    +    int32_t clsLen   = 0;
         char   *frm      = platformReadFile(frmPath, &len);
         char   *helpOnly = platformReadFile(FRT_INCLUDE_DIR "/help.bas", &helpLen);
         char   *printBas = platformReadFile(FRT_INCLUDE_DIR "/print.bas", &printLen);
    -    char   *helpBas  = (helpOnly && printBas) ? (char *)malloc((size_t)helpLen + (size_t)printLen + 2) : NULL;
    +    char   *cls      = clsPath ? platformReadFile(clsPath, &clsLen) : NULL;
    +    size_t  helpCap  = (size_t)helpLen + (size_t)printLen + (size_t)clsLen + FRT_CLASS_WRAP_LEN;
    +    char   *helpBas  = (helpOnly && printBas) ? (char *)malloc(helpCap) : NULL;
     
         TEST_TRUE(frm != NULL);
         TEST_TRUE(helpBas != NULL);
    +    TEST_TRUE(!clsPath || cls != NULL);
     
    +    // The modules, then the class module wrapped as the project builder
    +    // wraps it
         if (helpBas) {
    -        snprintf(helpBas, (size_t)helpLen + (size_t)printLen + 2, "%s\n%s", helpOnly, printBas);
    +        char className[BAS_MAX_IDENT] = "";
    +
    +        if (cls) {
    +            basExtractClassName(cls, clsLen, clsPath, className, (int32_t)sizeof(className));
    +        }
    +
    +        snprintf(helpBas, helpCap, cls ? "%s\n%s\nBEGINCLASS \"%s\"\n%s\nENDCLASS\n" : "%s\n%s", helpOnly, printBas, className, cls ? cls + basClassCodePos(cls, clsLen) : "");
         }
     
         free(helpOnly);
         free(printBas);
    +    free(cls);
     
         if (!frm || !helpBas) {
             free(frm);
    @@ -400,7 +429,7 @@ static BasValueT callIntM(FrtAppT *app, BasControlT *ctrl, const char *method, c
     // opened twice keeps one form with its 4 controls, and the dynamic form
     // gets its caption.
     static void caseAppBasDemo(void) {
    -    tCase("app basdemo: DATA twice, Graphics twice, DynForm caption");
    +    tCase("app basdemo: DATA twice, Graphics twice, DynForm caption, Objects");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
     
         static const char stubs[] =
    @@ -430,10 +459,14 @@ static void caseAppBasDemo(void) {
             "    mnuGraphics_Click\n"
             "    mnuDynamic_Click\n"
             "    PRINT \"B2 ok\"\n"
    +        "END SUB\n"
    +        "SUB BasicDemo_B3\n"
    +        "    mnuObjects_Click\n"
    +        "    PRINT \"B3 ok\"\n"
             "END SUB\n";
         FrtAppT app;
     
    -    if (!appBuild(&app, FRT_APPS_DIR "/basdemo/basdemo.frm", stubs, tests)) {
    +    if (!appBuild(&app, FRT_APPS_DIR "/basdemo/basdemo.frm", FRT_APPS_DIR "/basdemo/shape.cls", stubs, tests)) {
             tShutdown();
             return;
         }
    @@ -441,7 +474,8 @@ static void caseAppBasDemo(void) {
         frtStart(&app, "BasicDemo");
         TEST_TRUE(frtFire(&app, "BasicDemo", "B1"));
         TEST_TRUE(frtFire(&app, "BasicDemo", "B2"));
    -    TEST_STR_EQ(frtOut(&app), "B1 ok\nB2 ok\n");
    +    TEST_TRUE(frtFire(&app, "BasicDemo", "B3"));
    +    TEST_STR_EQ(frtOut(&app), "B1 ok\nB2 ok\nB3 ok\n");
         TEST_EQ(app.dialogsDismissed, 0);
     
         BasFormT *main = frtForm(&app, "BasicDemo");
    @@ -457,6 +491,13 @@ static void caseAppBasDemo(void) {
         }
     
         TEST_EQ(firsts, 2);
    +
    +    // The Objects demo: two Shapes from the class module through a
    +    // Collection and FOR EACH
    +    TEST_TRUE(out && strstr(out, "square of size 2, area 4") != NULL);
    +    TEST_TRUE(out && strstr(out, "Shapes: 2, total area") != NULL);
    +    TEST_TRUE(out && strstr(out, "The one called ROUND is a circle") != NULL);
    +    TEST_TRUE(out && strstr(out, "TypeName(s) = Shape, s Is shapes(2): yes") != NULL);
         TEST_TRUE(gfx != NULL);
         TEST_EQ(gfx ? (int32_t)arrlen(gfx->controls) : -1, 4);
         TEST_TRUE(dyn && dyn->window);
    @@ -517,7 +558,7 @@ static void caseAppCommandLine(void) {
                 arg = textPath;
             }
     
    -        if (!appBuild(&app, frmPath, stubs, runs[i].test)) {
    +        if (!appBuild(&app, frmPath, NULL, stubs, runs[i].test)) {
                 tShutdown();
                 return;
             }
    @@ -617,7 +658,7 @@ static void caseAppIconEd(void) {
         snprintf(bigPath, sizeof(bigPath), "%s/iconedNotIcon.png", tTempDir());
         snprintf(tests, sizeof(tests), testsFmt, pngPath, txtPath, pngPath, pngPath, bigPath, bigPath, pngPath);
     
    -    if (!appBuild(&app, FRT_APPS_DIR "/iconed/iconed.frm", stubs, tests)) {
    +    if (!appBuild(&app, FRT_APPS_DIR "/iconed/iconed.frm", NULL, stubs, tests)) {
             tShutdown();
             return;
         }
    @@ -706,7 +747,7 @@ static void caseAppNotepad(void) {
         TEST_TRUE(writeText(inPath, "\n\nabc\n"));
         snprintf(tests, sizeof(tests), testsFmt, outPath, inPath);
     
    -    if (!appBuild(&app, FRT_APPS_DIR "/notepad/notepad.frm", stubs, tests)) {
    +    if (!appBuild(&app, FRT_APPS_DIR "/notepad/notepad.frm", NULL, stubs, tests)) {
             tShutdown();
             return;
         }
    @@ -788,7 +829,7 @@ static void caseAppNotepadCloseRequest(void) {
             remove(outPath);
             snprintf(tests, sizeof(tests), testsFmt, outPath);
     
    -        if (!appBuild(&app, FRT_APPS_DIR "/notepad/notepad.frm", stubs, tests)) {
    +        if (!appBuild(&app, FRT_APPS_DIR "/notepad/notepad.frm", NULL, stubs, tests)) {
                 tShutdown();
                 return;
             }
    @@ -933,6 +974,64 @@ static void caseBuildProject(void) {
     }
     
     
    +static void caseClipboard(void) {
    +    tCase("Clipboard object: Clear, GetFormat, GetText, SetText (text only)");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Clip\"\n"
    +        "End\n"
    +        "\n"
    +        "Sub Form1_Test\n"
    +        "    Clipboard.Clear\n"
    +        "    Print \"fmt0=\"; Clipboard.GetFormat(vbCFText)\n"
    +        "    Clipboard.SetText \"hello\"\n"
    +        "    Print \"fmt1=\"; Clipboard.GetFormat(vbCFText); \" other=\"; Clipboard.GetFormat(2)\n"
    +        "    Print \"a=\"; Clipboard.GetText(); \" b=\"; Clipboard.GetText; \" c=[\"; Clipboard.GetText(2); \"]\"\n"
    +        "    Clipboard.SetText \"world\", vbCFText\n"
    +        "    Print \"d=\"; Clipboard.GetText(vbCFText)\n"
    +        "End Sub\n"
    +        "Sub Form1_BadFormat\n"
    +        "    Clipboard.SetText \"x\", 2\n"
    +        "End Sub\n"
    +        "Sub Form1_BadMethod\n"
    +        "    Clipboard.Paste\n"
    +        "End Sub\n"
    +        "Sub Form1_BadProp\n"
    +        "    Clipboard.Text = \"x\"\n"
    +        "End Sub\n";
    +    FrtAppT app;
    +
    +    if (!expectBuild(&app, "", &frm, 1)) {
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +    frtOutClear(&app);
    +    TEST_TRUE(frtFire(&app, "Form1", "Test"));
    +    TEST_STR_EQ(frtOut(&app), "fmt0=False \nfmt1=True  other=False \na=hello b=hello c=[]\nd=world\n");
    +
    +    int32_t len = 0;
    +
    +    TEST_STR_EQ(dvxClipboardGet(&len), "world");
    +
    +    const char *bad[] = { "BadFormat", "BadMethod", "BadProp" };
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(bad); i++) {
    +        frtFire(&app, "Form1", bad[i]);
    +        TEST_TRUE(app.rt->terminated);
    +        frtClearError(&app);
    +    }
    +
    +    TEST_STR_EQ(dvxClipboardGet(&len), "world");
    +    dvxClipboardClear();
    +    frtDestroy(&app);
    +    tShutdown();
    +}
    +
    +
     static void caseCommonMethods(void) {
         tCase("common methods");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    @@ -1268,6 +1367,7 @@ static void caseDialogWrappers(void) {
         TEST_EQ(DlgPromptSave("t"), DVX_SAVE_NO);
         TEST_STR_EQ(DlgFileOpen("t", "*.txt"), "");
         TEST_STR_EQ(DlgFileSave("t", ""), "");
    +    TEST_EQ(DlgColor("t", FRT_COLOR_START), -1);
     
         const char *frm = "Begin Form Form1\n    Caption = \"Dlg\"\nEnd\n";
         FrtAppT     app;
    @@ -1287,12 +1387,15 @@ static void caseDialogWrappers(void) {
         TEST_EQ(DlgChoice("Pick", "p", "A|B|C", 1), 1);
         TEST_EQ(DlgChoice("Pick", "p", "", 0), -1);
         TEST_EQ(DlgChoice("Pick", "p", NULL, 0), -1);
    +    TEST_EQ(DlgColor("Color", FRT_COLOR_START), FRT_COLOR_START);
    +    TEST_STR_EQ(dlg.title, "Color");
     
         TEST_STR_EQ(DlgInputBox("T", "P", "dflt"), "dflt");
         TEST_EQ(DlgInputCancelled(), 0);
         TEST_EQ(DlgIntInput("T", "P", 5, 0, 9), 5);
     
         snprintf(dlg.button, sizeof(dlg.button), "Cancel");
    +    TEST_EQ(DlgColor("Color", FRT_COLOR_START), -1);
         TEST_STR_EQ(DlgInputBox("T", "P", "dflt"), "");
         TEST_EQ(DlgInputCancelled(), -1);
         TEST_EQ(DlgPromptSave("T"), DVX_SAVE_CANCEL);
    @@ -1345,6 +1448,289 @@ static void caseDialogWrappers(void) {
     
         tDialogScript(NULL, NULL);
         frtDestroy(&app);
    +
    +    // The shipped include declares every wrapper.
    +    int32_t len     = 0;
    +    char   *commdlg = platformReadFile(FRT_INCLUDE_DIR "/commdlg.bas", &len);
    +
    +    TEST_TRUE(commdlg != NULL);
    +
    +    if (commdlg && expectBuild(&app, commdlg, &frm, 1)) {
    +        frtDestroy(&app);
    +    }
    +
    +    free(commdlg);
    +    tShutdown();
    +}
    +
    +
    +static void caseDragDrop(void) {
    +    tCase("drag and drop: DragMode, Drag, DragIcon, DragOver, DragDrop, Esc");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Drag\"\n"
    +        "    Width = 400\n"
    +        "    Height = 300\n"
    +        "    Begin Label Src\n"
    +        "        Caption = \"Source\"\n"
    +        "        DragMode = 1\n"
    +        "    End\n"
    +        "    Begin TextBox Dst\n"
    +        "    End\n"
    +        "    Begin CommandButton Cmd1\n"
    +        "        Caption = \"Go\"\n"
    +        "    End\n"
    +        "End\n"
    +        "\n"
    +        "Sub Src_Click\n"
    +        "    Print \"SrcClick\"\n"
    +        "End Sub\n"
    +        "Sub Src_MouseDown(Button As Integer, X As Integer, Y As Integer)\n"
    +        "    Print \"SrcDown\"\n"
    +        "End Sub\n"
    +        "Sub Dst_DragOver(Source As Control, X As Integer, Y As Integer, State As Integer)\n"
    +        "    Print \"Over \"; Source.Name; \" \"; State\n"
    +        "End Sub\n"
    +        "Sub Dst_DragDrop(Source As Control, X As Integer, Y As Integer)\n"
    +        "    Print \"Drop \"; Source.Name; \" \"; X; Y\n"
    +        "End Sub\n"
    +        "Sub Form1_DragDrop(Source As Control, X As Integer, Y As Integer)\n"
    +        "    Print \"FormDrop \"; Source.Name\n"
    +        "End Sub\n"
    +        "Sub Form1_DragOver(Source As Control, X As Integer, Y As Integer, State As Integer)\n"
    +        "    Print \"FormOver \"; State\n"
    +        "End Sub\n"
    +        "Sub Cmd1_MouseDown(Button As Integer, X As Integer, Y As Integer)\n"
    +        "    Cmd1.Drag vbBeginDrag\n"
    +        "End Sub\n"
    +        "Sub Cmd1_Click\n"
    +        "    Print \"CmdClick\"\n"
    +        "End Sub\n"
    +        "Sub Form1_Props\n"
    +        "    Print \"mode=\"; Src.DragMode; Cmd1.DragMode; \"[\"; Src.DragIcon; \"]\"\n"
    +        "    Cmd1.DragMode = vbAutomatic\n"
    +        "    Cmd1.DragMode = 5\n"
    +        "    Print \"mode2=\"; Cmd1.DragMode\n"
    +        "End Sub\n"
    +        "Sub Form1_BadAction\n"
    +        "    Src.Drag 7\n"
    +        "End Sub\n";
    +    FrtAppT app;
    +
    +    if (!expectBuild(&app, "", &frm, 1)) {
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +
    +    BasFormT    *form = frtForm(&app, "Form1");
    +    BasControlT *src  = frtCtrl(form, "Src");
    +    BasControlT *dst  = frtCtrl(form, "Dst");
    +    BasControlT *cmd  = frtCtrl(form, "Cmd1");
    +    WindowT     *win  = form ? form->window : NULL;
    +    int32_t      sx   = 0;
    +    int32_t      sy   = 0;
    +    int32_t      dx   = 0;
    +    int32_t      dy   = 0;
    +    int32_t      cx   = 0;
    +    int32_t      cy   = 0;
    +    char         expect[64];
    +
    +    TEST_TRUE(src && dst && cmd && win);
    +
    +    if (!(src && dst && cmd && win)) {
    +        frtDestroy(&app);
    +        tShutdown();
    +        return;
    +    }
    +
    +    int32_t fx = win->x + win->w / 2;
    +    int32_t fy = win->y + win->contentY + win->contentH - FRT_FORM_EDGE;
    +
    +    tWidgetCenter(src->widget, &sx, &sy);
    +    tWidgetCenter(dst->widget, &dx, &dy);
    +    tWidgetCenter(cmd->widget, &cx, &cy);
    +    snprintf(expect, sizeof(expect), "Drop Src %d %d \n", (int)(dst->widget->w / 2), (int)(dst->widget->h / 2));
    +
    +    // Properties, from the .frm and from code.
    +    frtOutClear(&app);
    +    TEST_TRUE(frtFire(&app, "Form1", "Props"));
    +    TEST_STR_EQ(frtOut(&app), "mode=1 0 []\nmode2=0 \n");
    +
    +    // DragMode automatic: the press drags (no Click or MouseDown), the
    +    // outline moves onto Dst (vbEnter), the release drops there.
    +    frtOutClear(&app);
    +    tDrag(sx, sy, dx, dy);
    +    outContains(&app, "Over Src 0 \n");
    +    outContains(&app, expect);
    +    TEST_TRUE(strstr(frtOut(&app), "SrcClick") == NULL);
    +    TEST_TRUE(strstr(frtOut(&app), "SrcDown") == NULL);
    +    TEST_TRUE(win->onMouse == widgetOnMouse);
    +    TEST_TRUE(win->onKey == widgetOnKey);
    +
    +    // Over, then Leave onto the form, dropped on the form.
    +    frtOutClear(&app);
    +    tMouseMove(sx, sy);
    +    tMouseDown(sx, sy, MOUSE_LEFT);
    +    tMouseMove(dx, dy);
    +    tMouseMove(dx + 1, dy);
    +    tMouseMove(fx, fy);
    +    tMouseUp(fx, fy, MOUSE_LEFT);
    +    TEST_STR_EQ(frtOut(&app), "Over Src 0 \nOver Src 2 \nOver Src 1 \nFormOver 0 \nFormDrop Src\n");
    +
    +    // Escape cancels: no drop on the release.
    +    frtOutClear(&app);
    +    tMouseMove(sx, sy);
    +    tMouseDown(sx, sy, MOUSE_LEFT);
    +    tMouseMove(dx, dy);
    +    tKeyEsc();
    +    tMouseUp(dx, dy, MOUSE_LEFT);
    +    TEST_STR_EQ(frtOut(&app), "Over Src 0 \n");
    +    TEST_TRUE(win->onMouse == widgetOnMouse);
    +
    +    // Manual: Cmd1's MouseDown calls Drag; its release drops, no Click.
    +    frtOutClear(&app);
    +    tAdvanceClock(TIMER_INTERVAL_MS);
    +    tDrag(cx, cy, dx, dy);
    +    outContains(&app, "Drop Cmd1 ");
    +    TEST_TRUE(strstr(frtOut(&app), "CmdClick") == NULL);
    +
    +    // Back in DragMode manual, Src clicks as usual.
    +    frtOutClear(&app);
    +    tAdvanceClock(TIMER_INTERVAL_MS);
    +    src->dragMode = BAS_DRAGMODE_MANUAL;
    +    tWidgetCenter(src->widget, &sx, &sy);
    +    tClick(sx, sy);
    +    TEST_STR_EQ(frtOut(&app), "SrcClick\nSrcDown\n");
    +
    +    // From code with the button up: vbEndDrag drops where the mouse is,
    +    // 0 cancels, and the next click drops a drag begun from code.
    +    frtOutClear(&app);
    +    tMouseMove(dx, dy);
    +    dragCallMethod(&app, src, BAS_DRAG_BEGIN);
    +    dragCallMethod(&app, src, BAS_DRAG_END);
    +    TEST_STR_EQ(frtOut(&app), expect);
    +    frtOutClear(&app);
    +    dragCallMethod(&app, src, BAS_DRAG_BEGIN);
    +    dragCallMethod(&app, src, BAS_DRAG_CANCEL);
    +    tClick(dx, dy);
    +    TEST_TRUE(strstr(frtOut(&app), "Drop") == NULL);
    +    frtOutClear(&app);
    +    dragCallMethod(&app, src, BAS_DRAG_BEGIN);
    +    tMouseMove(dx + 1, dy);
    +    tAdvanceClock(TIMER_INTERVAL_MS);
    +    tClick(dx, dy);
    +    outContains(&app, expect);
    +
    +    // DragIcon: a readable picture is the feedback; a missing one falls
    +    // back to the outline.  Both drops still land.
    +    setStrProp(&app, src, BAS_PROP_DRAGICON, FRT_FIXTURE_DIR "/icon.bmp");
    +
    +    BasValueT icon = basFormRtGetProp(app.rt, src, BAS_PROP_DRAGICON);
    +
    +    TEST_TRUE(icon.type == BAS_TYPE_STRING && strstr(icon.strVal->data, "icon.bmp") != NULL);
    +    basValRelease(&icon);
    +    frtOutClear(&app);
    +    dragCallMethod(&app, src, BAS_DRAG_BEGIN);
    +    dragCallMethod(&app, src, BAS_DRAG_END);
    +    TEST_STR_EQ(frtOut(&app), expect);
    +    setStrProp(&app, src, BAS_PROP_DRAGICON, FRT_FIXTURE_DIR "/noSuchIcon.bmp");
    +    frtOutClear(&app);
    +    dragCallMethod(&app, src, BAS_DRAG_BEGIN);
    +    dragCallMethod(&app, src, BAS_DRAG_END);
    +    TEST_STR_EQ(frtOut(&app), expect);
    +
    +    // Removing the dragged control cancels its drag.
    +    frtOutClear(&app);
    +    dragCallMethod(&app, src, BAS_DRAG_BEGIN);
    +    basFormRtRemoveCtrl(app.rt, form, "Src");
    +    TEST_TRUE(win->onMouse == widgetOnMouse);
    +    tAdvanceClock(TIMER_INTERVAL_MS);
    +    tClick(dx, dy);
    +    TEST_TRUE(strstr(frtOut(&app), "Drop") == NULL);
    +
    +    // An unknown Drag action is a runtime error.
    +    TEST_TRUE(!app.rt->terminated);
    +    frtFire(&app, "Form1", "BadAction");
    +    TEST_TRUE(app.rt->terminated);
    +    frtClearError(&app);
    +
    +    frtDestroy(&app);
    +    tShutdown();
    +}
    +
    +
    +// Form / PictureBox drawing methods (Phase 4): the QBasic drawing code
    +// on canvases, with RGB colours.
    +static void caseDrawMethods(void) {
    +    tCase("Form and PictureBox drawing methods");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Draw\"\n"
    +        "    Begin PictureBox Pic\n"
    +        "    End\n"
    +        "    Begin PictureBox Pic2\n"
    +        "    End\n"
    +        "End\n"
    +        "Sub Form1_T ()\n"
    +        "    Pic.Resize 40, 30\n"
    +        "    Pic.BackColor = RGB(0, 0, 255)\n"
    +        "    Pic.Line (0, 0)-(9, 0), RGB(255, 0, 0)\n"
    +        "    Pic.Line (10, 10)-(20, 20), RGB(0, 255, 0), BF\n"
    +        "    Pic.Circle (30, 15), 5, RGB(255, 255, 0)\n"
    +        "    Pic.PSet (1, 5), RGB(255, 0, 0)\n"
    +        "    Print Pic.Point(5, 0); Pic.Point(15, 15); Pic.Point(39, 29); Pic.Point(35, 15); Pic.Point(99, 99)\n"
    +        "    Print Pic.CurrentX; Pic.CurrentY; Pic.ScaleWidth; Pic.ScaleHeight; Pic.TextWidth(\"abc\"); Pic.TextHeight(\"a\")\n"
    +        "    Pic.DrawWidth = 3\n"
    +        "    Pic.PSet (25, 25), 0\n"
    +        "    Pic.DrawWidth = 1\n"
    +        "    Pic.FillStyle = 0\n"
    +        "    Pic.FillColor = RGB(255, 0, 255)\n"
    +        "    Pic.Circle (5, 20), 4, RGB(255, 255, 255)\n"
    +        "    Pic.Line (30, 0)-(39, 5), RGB(255, 255, 255), B\n"
    +        "    Print Pic.Point(26, 25); Pic.Point(5, 20); Pic.Point(34, 2); Pic.Point(30, 0)\n"
    +        "    Pic.CurrentX = 0\n"
    +        "    Pic.CurrentY = 0\n"
    +        "    Pic.ForeColor = RGB(255, 255, 255)\n"
    +        "    Pic.Print \"Hi\";\n"
    +        "    Print Pic.CurrentX; Pic.CurrentY;\n"
    +        "    Pic.Print\n"
    +        "    Print Pic.CurrentX; Pic.CurrentY\n"
    +        "    Pic2.Resize 40, 30\n"
    +        "    Pic2.PaintPicture \"Pic\", 0, 0\n"
    +        "    Pic2.PaintPicture \"Pic\", 0, 0, 20, 15, 10, 10, 20, 20\n"
    +        "    Print Pic2.Point(30, 15); Pic2.Point(5, 5)\n"
    +        "    Pic.Cls\n"
    +        "    Print Pic.Point(15, 15); Pic.CurrentX; Pic.AutoRedraw\n"
    +        "    Me.Line (0, 0)-(5, 5), RGB(255, 0, 0)\n"
    +        "    Me.Circle (20, 20), 5\n"
    +        "End Sub\n";
    +    FrtAppT app;
    +
    +    if (!expectBuild(&app, "", &frm, 1)) {
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +    TEST_TRUE(frtFire(&app, "Form1", "T"));
    +    TEST_STR_EQ(frtOut(&app),
    +        "16711680 65280 255 16776960 -1 \n"
    +        "1 5 40 30 24 16 \n"
    +        "0 16711935 16711935 16777215 \n"
    +        "16 0 0 16 \n"
    +        "255 65280 \n"
    +        "255 0 False \n");
    +
    +    BasFormT *form = frtForm(&app, "Form1");
    +
    +    TEST_TRUE(form != NULL && form->formCtrl.draw != NULL);
    +    frtDestroy(&app);
         tShutdown();
     }
     
    @@ -2923,6 +3309,61 @@ static void caseMsgBox(void) {
     }
     
     
    +// Controls and forms are object values: a bare control or form name where
    +// an object is expected (SET, IS, TYPEOF, an object parameter) is that
    +// control or form; TypeName / TypeOf know their types; they go into a
    +// Collection and through FOR EACH.
    +static void caseObjectRefs(void) {
    +    tCase("control and form references: SET c = Text1, TypeName, TypeOf, Collection");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Objects\"\n"
    +        "    Begin TextBox Text1\n"
    +        "    End\n"
    +        "    Begin CommandButton Command1\n"
    +        "        Caption = \"Go\"\n"
    +        "    End\n"
    +        "End\n"
    +        "\n"
    +        "Sub Form1_Load\n"
    +        "    Dim c As Control\n"
    +        "    Dim f As Form\n"
    +        "    Dim col As New Collection\n"
    +        "    Set c = Text1\n"
    +        "    c.Text = \"hi\"\n"
    +        "    Print Text1.Text; \" \"; TypeName(c); \" \"; TypeName(Me); \" \"; TypeOf c Is TextBox; TypeOf c Is Control; TypeOf Me Is Form\n"
    +        "    Print c Is Text1; c Is Command1; c Is Nothing\n"
    +        "    Set f = Form1\n"
    +        "    Print f.Caption; \" \"; TypeName(f); TypeOf f Is Control\n"
    +        "    col.Add c\n"
    +        "    col.Add Me.Command1\n"
    +        "    For Each x In col\n"
    +        "        Print TypeName(x); \" \";\n"
    +        "    Next\n"
    +        "    Print\n"
    +        "    ShowIt Command1\n"
    +        "End Sub\n"
    +        "Sub ShowIt(o As Control)\n"
    +        "    Print \"caption \"; o.Caption\n"
    +        "End Sub\n";
    +    const char *frms[1] = { frm };
    +    FrtAppT     app;
    +
    +    if (!expectBuild(&app, "", frms, 1)) {
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, "Form1");
    +    TEST_STR_EQ(frtOut(&app), "hi TextBox Form True True True \nTrue False False \nObjects FormFalse \nTextBox CommandButton \ncaption Go\n");
    +
    +    frtDestroy(&app);
    +    tShutdown();
    +}
    +
    +
     static void casePendingUnloadDrain(void) {
         tCase("deferred unloads of several forms drain after the handler");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    @@ -3120,6 +3561,110 @@ static void casePopupMenu(void) {
     }
     
     
    +// The program screen's window (Phase 4): made by the first console
    +// statement at the mode's size, keys to INKEY$, Alt+Enter and FULLSCREEN
    +// through the exclusive API, closing it ends the program.
    +static void caseProgramScreen(void) {
    +    tCase("Program screen window, keys, full screen");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +
    +    const char *bas =
    +        "SCREEN 13\n"
    +        "LINE (0, 0)-(319, 199), 4, BF\n"
    +        "PSET (5, 5), 15\n"
    +        "PRINT \"X\"\n";
    +    FrtAppT app;
    +
    +    if (!expectBuild(&app, bas, NULL, 0)) {
    +        tShutdown();
    +        return;
    +    }
    +
    +    snprintf(app.rt->title, sizeof(app.rt->title), "%s", "Screen Test");
    +    frtStart(&app, NULL);
    +    tPump(1);
    +
    +    BasScreenWinT *sw  = app.rt->screenWin;
    +    BasScreenT    *scr = app.vm->screen;
    +
    +    TEST_TRUE(sw != NULL && scr != NULL);
    +
    +    if (!sw || !scr) {
    +        frtDestroy(&app);
    +        tShutdown();
    +        return;
    +    }
    +
    +    WindowT        *win   = basFormRtScreenWindow(app.rt);
    +    const DisplayT *d     = &tCtx()->display;
    +    int32_t         bpp   = d->format.bytesPerPixel;
    +    int32_t         scale = win ? win->contentW / scr->width : 1;
    +    uint32_t        red   = packColor(d, scr->palette[4 * 3], scr->palette[4 * 3 + 1], scr->palette[4 * 3 + 2]);
    +    uint32_t        white = packColor(d, scr->palette[15 * 3], scr->palette[15 * 3 + 1], scr->palette[15 * 3 + 2]);
    +
    +    TEST_TRUE(win != NULL);
    +
    +    if (!win) {
    +        frtDestroy(&app);
    +        tShutdown();
    +        return;
    +    }
    +
    +    TEST_TRUE(strcmp(win->title, "Screen Test") == 0);
    +    TEST_EQ(win->contentW, 320 * scale);
    +    TEST_EQ(win->contentH, 200 * scale);
    +    TEST_TRUE(frtOut(&app)[0] == '\0');
    +    TEST_TRUE(basFormRtKeepsRunning(app.rt));
    +
    +    // The frame reached the window through the colour table
    +    TEST_EQ(*(uint32_t *)(win->contentBuf + 100 * win->contentPitch + 100 * bpp), red);
    +    TEST_EQ(*(uint32_t *)(win->contentBuf + 5 * scale * win->contentPitch + 5 * scale * bpp), white);
    +
    +    // Keys for INKEY$: characters, and extended keys as 0x100 | scan
    +    win->onKey(win, 'q', 0);
    +    win->onKey(win, KEY_UP, 0);
    +    TEST_EQ(scr->keyCount, 2);
    +    TEST_EQ(scr->keys[scr->keyHead], 'q');
    +    TEST_EQ(scr->keys[(scr->keyHead + 1) % BAS_SCREEN_KEY_QUEUE], BAS_SCREEN_KEY_EXT | KEY_SCAN_UP);
    +
    +    // Alt+Enter: the whole screen, and back
    +    win->onKey(win, KEY_ENTER, KEY_MOD_ALT);
    +    TEST_TRUE(scr->fullScreen);
    +    TEST_TRUE(tCtx()->exclusive.owner == win);
    +    win->onKey(win, KEY_ENTER, KEY_MOD_ALT);
    +    TEST_TRUE(!scr->fullScreen);
    +    TEST_TRUE(tCtx()->exclusive.owner == NULL);
    +
    +    // Closing the window stops the program
    +    win->onClose(win);
    +    tPump(1);
    +    TEST_TRUE(app.vm->stopRequested);
    +    TEST_TRUE(!basFormRtKeepsRunning(app.rt));
    +    TEST_TRUE(basFormRtScreenWindow(app.rt) == NULL);
    +    frtDestroy(&app);
    +
    +    // FULLSCREEN from the program, then a mode change while full
    +    const char *basFull =
    +        "SCREEN 13\n"
    +        "FULLSCREEN ON\n"
    +        "SCREEN 12\n"
    +        "CIRCLE (320, 240), 50, 2\n";
    +
    +    if (!expectBuild(&app, basFull, NULL, 0)) {
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +    tPump(1);
    +    TEST_TRUE(app.vm->screen && app.vm->screen->fullScreen && app.vm->screen->mode == BAS_SCREEN_MODE_VGA16);
    +    TEST_TRUE(tCtx()->exclusive.owner != NULL && tCtx()->exclusive.owner == basFormRtScreenWindow(app.rt));
    +    frtDestroy(&app);
    +    TEST_TRUE(tCtx()->exclusive.owner == NULL);
    +    tShutdown();
    +}
    +
    +
     static void casePropEdges(void) {
         tCase("form/control/menu property edges and error routing");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    @@ -5014,6 +5559,15 @@ static void dialogClick(AppContextT *ctx, WindowT *modal, void *userCtx) {
     }
     
     
    +// Call ctrl.Drag action through the runtime's method dispatch.
    +static void dragCallMethod(FrtAppT *app, BasControlT *ctrl, int32_t action) {
    +    int32_t   vals[1] = { action };
    +    BasValueT r       = callIntM(app, ctrl, "Drag", vals, 1);
    +
    +    basValRelease(&r);
    +}
    +
    +
     static bool expectBuild(FrtAppT *app, const char *bas, const char *const *frms, int32_t frmCount) {
         bool ok = frtBuild(app, bas, frms, frmCount);
     
    @@ -5451,6 +6005,7 @@ int main(void) {
         caseFormVars();
         caseControlArrays();
         caseMsgBox();
    +    caseObjectRefs();
         caseTimers();
         caseSerialUnresolved();
         caseRuntimeError();
    @@ -5460,9 +6015,13 @@ int main(void) {
         caseCreateFormDynamic();
         caseDataBinding();
         caseDialogWrappers();
    +    caseClipboard();
    +    caseDragDrop();
    +    caseDrawMethods();
         caseEventLoopExits();
         caseExternCalls();
         caseExternMarshal();
    +    caseProgramScreen();
         caseRefValidation();
         caseFrmLoadEdges();
         caseFrmParserUnit();
    diff --git a/src/test/ide/Makefile b/src/test/ide/Makefile
    index 5c2c7409..79597967 100644
    --- a/src/test/ide/Makefile
    +++ b/src/test/ide/Makefile
    @@ -60,8 +60,8 @@ STUBTARGET := $(BINDIR)/testStub
     
     IDE_SRCS   := ideDesigner.c ideProperties.c ideProject.c ideMenuEditor.c ideToolbox.c
     COMP_SRCS  := lexer.c parser.c codegen.c symtab.c strip.c obfuscate.c compact.c
    -RT_SRCS    := vm.c values.c serialize.c
    -FRM_SRCS   := formrt.c frmParser.c basNativeHost.c
    +RT_SRCS    := vm.c values.c dates.c serialize.c gfx.c screen.c
    +FRM_SRCS   := formrt.c formGfx.c frmParser.c basNativeHost.c
     
     OBJS       := $(BINDIR)/testIde.o $(BINDIR)/ideStubs.o
     OBJS       += $(patsubst %.c,$(BINDIR)/ide/%.o,$(IDE_SRCS))
    diff --git a/src/test/ide/testIde.c b/src/test/ide/testIde.c
    index 207d6c92..dc507918 100644
    --- a/src/test/ide/testIde.c
    +++ b/src/test/ide/testIde.c
    @@ -794,7 +794,7 @@ static void caseRemoveFile(void) {
         TEST_STR_EQ(plan.lastTitle, IDE_MAIN_TITLE);
     
         TEST_EQ(prjFileCount(&sProject), 1);
    -    TEST_TRUE(sProject.files[0].isForm);
    +    TEST_EQ(sProject.files[0].kind, PrjKindFormE);
         TEST_TRUE(sProject.files[0].modified);
         TEST_TRUE(fileHas(sBasPath, EDIT_MARK_BAS));
         TEST_TRUE(!fileHas(sFrmPath, EDIT_MARK_FRM));
    @@ -1548,7 +1548,7 @@ static void snapshotBascomp(void) {
     
     
     static int32_t sourceMapStart(int32_t fileIdx) {
    -    int32_t startLine = prjMapStartLine(&sProject, fileIdx);
    +    int32_t startLine = prjMapFileLine(&sProject, fileIdx, 1);
     
         return (startLine > 0) ? startLine : -1;
     }
    diff --git a/src/test/ide/testIdeDebug.c b/src/test/ide/testIdeDebug.c
    index 2dfe0376..8203170e 100644
    --- a/src/test/ide/testIdeDebug.c
    +++ b/src/test/ide/testIdeDebug.c
    @@ -300,7 +300,7 @@ static void caseDbgErrors(void) {
         TEST_TRUE(tWriteFile(badPath, badFrm, (int32_t)strlen(badFrm)));
         ideStart(true);
         openViaRecent();
    -    prjAddFile(&sProject, "badtype.frm", true);
    +    prjAddFile(&sProject, "badtype.frm", PrjKindFormE);
         prjLoadAllFiles(&sProject, sAc);
         runScript(NULL, CMD_RUN);
         tPump(SETTLE_FRAMES);
    diff --git a/src/test/ide/testIdeDesign.c b/src/test/ide/testIdeDesign.c
    index e9ad05db..563cffdf 100644
    --- a/src/test/ide/testIdeDesign.c
    +++ b/src/test/ide/testIdeDesign.c
    @@ -57,10 +57,17 @@
     #define DP_FORM_POS        25     // form Left/Top test value
     #define DP_FORM_W          320    // form Width test value
     #define DP_FORM_H          240    // form Height test value
    -#define DP_TEST_R          16     // color slider test components
    -#define DP_TEST_G          32
    -#define DP_TEST_B          48
    -#define DP_TEST_COLOR      0x102030
    +#define DP_TEST_COLOR      0x102030  // picked in the color dialog
    +#define DP_CLS_STACK       "stack.cls"
    +#define DP_CLS_QUEUE       "queue.cls"
    +#define DP_CLS_NAME        "Stack"
    +#define DP_CLS_HEADER      "VERSION DVX 1.00 CLASS\nAttribute VB_Name = \"" DP_CLS_NAME "\"\n\n"
    +#define DP_CLS_CODE        "Private items As Integer\nPublic Sub Push()\n    items = items + 1\nEnd Sub\nPrivate Function Size() As Integer\n    Size = items\nEnd Function\n"
    +#define DP_CLS_TEXT        DP_CLS_HEADER DP_CLS_CODE
    +#define DP_CLS_FIRST_LINE  4      // first code line of DP_CLS_TEXT (after three header lines)
    +#define DP_CLS_CODE_LINES  7      // lines of DP_CLS_CODE
    +#define DP_CLS_START       20     // concatenated line of the class code in the test source map
    +#define DP_TEST_COLOR_TEXT "Blue 48"
     #define DC_MIN_CTRL_SIZE   8      // ideDesigner.c MIN_CTRL_SIZE (private to that file)
     #define DM_DESC_INPUT      6      // Project Properties: text input index of Description
     #define DF_SAVE_BUF        65536  // .frm save buffer for the data-level cases
    @@ -100,6 +107,7 @@ static void caseHelpQuery(void);
     static void caseMenuEd(void);
     static void caseMenuEdLimits(void);
     static void casePrefsColors(void);
    +static void casePrjClasses(void);
     static void casePrjDialogs(void);
     static void casePrjFile(void);
     static void caseProcSplit(void);
    @@ -157,7 +165,7 @@ static void caseDsgnBadAndMissingFiles(void) {
     
         // Unparseable form: dsgnLoadFrm fails, switchToDesign creates a
         // default blank form so the designer still opens.
    -    prjAddFile(&sProject, DC_FORM2_FRM, true);
    +    prjAddFile(&sProject, DC_FORM2_FRM, PrjKindFormE);
         prjRebuildTree(&sProject);
         activateFile(prjFileCount(&sProject) - 1, ViewDesignE);
         TEST_TRUE(sDesigner.form != NULL);
    @@ -165,7 +173,7 @@ static void caseDsgnBadAndMissingFiles(void) {
     
         // Form file missing on disk: blank form named after the file, and
         // the entry is marked modified so Save writes it out.
    -    prjAddFile(&sProject, DC_BLANK_FRM, true);
    +    prjAddFile(&sProject, DC_BLANK_FRM, PrjKindFormE);
         prjRebuildTree(&sProject);
         activateFile(prjFileCount(&sProject) - 1, ViewDesignE);
         TEST_TRUE(sDesigner.form != NULL);
    @@ -173,7 +181,7 @@ static void caseDsgnBadAndMissingFiles(void) {
         TEST_TRUE(sProject.files[prjFileCount(&sProject) - 1].modified);
     
         // Module file missing on disk: empty editor, marked modified.
    -    prjAddFile(&sProject, DC_BLANK_BAS, false);
    +    prjAddFile(&sProject, DC_BLANK_BAS, PrjKindModuleE);
         prjRebuildTree(&sProject);
         activateFile(prjFileCount(&sProject) - 1, ViewCodeE);
         TEST_EQ(sEditorFileIdx, prjFileCount(&sProject) - 1);
    @@ -1349,7 +1357,7 @@ static void caseGapMisc(void) {
         openViaRecent();
     
         // A listed file that exists nowhere: compile refuses.
    -    prjAddFile(&sProject, "ghost.bas", false);
    +    prjAddFile(&sProject, "ghost.bas", PrjKindModuleE);
         runScript(NULL, CMD_RUN);
         TEST_TRUE(strstr(sOutputBuf, "Cannot read ghost.bas") != NULL);
         prjRemoveFile(&sProject, prjFileCount(&sProject) - 1);
    @@ -1374,7 +1382,7 @@ static void caseGapMisc(void) {
             free(big);
         }
     
    -    prjAddFile(&sProject, "big.bas", false);
    +    prjAddFile(&sProject, "big.bas", PrjKindModuleE);
         runScript(NULL, CMD_RUN);
         TEST_TRUE(strstr(sOutputBuf, "exceeds") != NULL);
         prjRemoveFile(&sProject, prjFileCount(&sProject) - 1);
    @@ -1903,8 +1911,9 @@ static void caseMenuEdLimits(void) {
     }
     
     
    -// Preferences Colors tab: pick entries, move sliders, verify the value
    -// labels, swatch and persisted color, and see it applied to the editor.
    +// Preferences Colors tab: pick an entry, choose a color in the color
    +// dialog, verify the value label and persisted color, and see it applied
    +// to the editor.
     static void casePrefsColors(void) {
         tCase("preferences colors tab and editor apply");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    @@ -1932,6 +1941,99 @@ static void casePrefsColors(void) {
     }
     
     
    +// Class modules in the project model: the kind follows the extension,
    +// [Classes] survives a save and load in compile order, the class name
    +// and first code line come from the .cls header, and source-map lines
    +// of a class start after its header.
    +static void casePrjClasses(void) {
    +    tCase("project file: class modules ([Classes], name, header lines, source map)");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +
    +    PrjStateT prj;
    +    PrjStateT back;
    +    char      path[DVX_MAX_PATH];
    +    char      name[BAS_MAX_IDENT];
    +    int32_t   firstLine = 0;
    +
    +    TEST_EQ(prjKindForPath("dir.x" DVX_PATH_SEP "a.frm"), PrjKindFormE);
    +    TEST_EQ(prjKindForPath("dir.frm" DVX_PATH_SEP "Stack.CLS"), PrjKindClassE);
    +    TEST_EQ(prjKindForPath("dir.cls" DVX_PATH_SEP "main.bas"), PrjKindModuleE);
    +
    +    // Saved in any order, loaded back as modules, classes, forms
    +    snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "cls.dbp", tTempDir());
    +    prjInit(&prj);
    +    snprintf(prj.projectPath, sizeof(prj.projectPath), "%s", path);
    +    snprintf(prj.name, sizeof(prj.name), "%s", "Cls");
    +    prjAddFile(&prj, "f.frm", PrjKindFormE);
    +    prjAddFile(&prj, DP_CLS_STACK, PrjKindClassE);
    +    prjAddFile(&prj, "a.bas", PrjKindModuleE);
    +    prjAddFile(&prj, DP_CLS_QUEUE, PrjKindClassE);
    +    TEST_TRUE(prjSave(&prj));
    +    prjClose(&prj);
    +
    +    prjInit(&back);
    +    TEST_TRUE(prjLoad(&back, path));
    +    TEST_EQ(prjFileCount(&back), 4);
    +
    +    if (prjFileCount(&back) == 4) {
    +        TEST_STR_EQ(back.files[0].path, "a.bas");
    +        TEST_EQ(back.files[0].kind, PrjKindModuleE);
    +        TEST_STR_EQ(back.files[1].path, DP_CLS_STACK);
    +        TEST_EQ(back.files[1].kind, PrjKindClassE);
    +        TEST_STR_EQ(back.files[2].path, DP_CLS_QUEUE);
    +        TEST_EQ(back.files[2].kind, PrjKindClassE);
    +        TEST_STR_EQ(back.files[3].path, "f.frm");
    +        TEST_EQ(back.files[3].kind, PrjKindFormE);
    +
    +        // Attribute VB_Name names the class; without a header the file does
    +        TEST_TRUE(prjClassName(&back, 1, DP_CLS_TEXT, name, (int32_t)sizeof(name)));
    +        TEST_STR_EQ(name, DP_CLS_NAME);
    +        TEST_TRUE(prjClassName(&back, 2, DP_CLS_CODE, name, (int32_t)sizeof(name)));
    +        TEST_STR_EQ(name, "queue");
    +        TEST_TRUE(prjClassName(&back, 2, NULL, name, (int32_t)sizeof(name)));
    +        TEST_STR_EQ(name, "queue");
    +    }
    +
    +    // The code starts after the header's lines
    +    TEST_EQ(prjClassCodePos(DP_CLS_TEXT, (int32_t)strlen(DP_CLS_TEXT), &firstLine), (int32_t)strlen(DP_CLS_HEADER));
    +    TEST_EQ(firstLine, DP_CLS_FIRST_LINE);
    +    TEST_EQ(prjClassCodePos(DP_CLS_CODE, (int32_t)strlen(DP_CLS_CODE), &firstLine), 0);
    +    TEST_EQ(firstLine, 1);
    +
    +    // Header lines map to nothing; code lines map past the header
    +    BasSourceMapT map       = { DP_CLS_STACK, DP_CLS_START, DP_CLS_CODE_LINES, DP_CLS_FIRST_LINE };
    +    int32_t       fileIdx   = -1;
    +    int32_t       localLine = 0;
    +
    +    arrput(back.sourceMap, map);
    +    TEST_EQ(prjMapFileLine(&back, 1, 1), 0);
    +    TEST_EQ(prjMapFileLine(&back, 1, DP_CLS_FIRST_LINE - 1), 0);
    +    TEST_EQ(prjMapFileLine(&back, 1, DP_CLS_FIRST_LINE), DP_CLS_START);
    +    TEST_EQ(prjMapFileLine(&back, 1, DP_CLS_FIRST_LINE + DP_CLS_CODE_LINES), 0);
    +    TEST_EQ(prjMapFileLine(&back, 2, 1), 0);
    +    TEST_TRUE(prjMapLine(&back, DP_CLS_START + 1, &fileIdx, &localLine));
    +    TEST_EQ(fileIdx, 1);
    +    TEST_EQ(localLine, DP_CLS_FIRST_LINE + 1);
    +    prjClose(&back);
    +
    +    // Public / Private procedures get their own editor sections
    +    char  *general = NULL;
    +    char **procs   = NULL;
    +
    +    TEST_TRUE(splitProcs(DP_CLS_TEXT, &general, &procs, NULL));
    +    TEST_EQ((int32_t)arrlen(procs), 2);
    +    TEST_TRUE(general != NULL && strstr(general, "Attribute VB_Name") != NULL && strstr(general, "Sub ") == NULL);
    +    free(general);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(procs); i++) {
    +        free(procs[i]);
    +    }
    +
    +    arrfree(procs);
    +    tShutdown();
    +}
    +
    +
     // Project Properties dialog: every field, the icon browse flows
     // (invalid file, wrong size, copy-into-project, overwrite), the help
     // browse, OK, Cancel, and OK with a broken icon path.
    @@ -2163,9 +2265,9 @@ static void casePrjFile(void) {
         BasSourceMapT maps[]    = { { "a.bas", 1, 10, 1 }, { "f.frm", 11, 5, 1 }, { "b.bas", 16, 7, 1 } };
     
         prjInit(&prj);
    -    prjAddFile(&prj, "a.bas", false);
    -    prjAddFile(&prj, "f.frm", true);
    -    prjAddFile(&prj, "b.bas", false);
    +    prjAddFile(&prj, "a.bas", PrjKindModuleE);
    +    prjAddFile(&prj, "f.frm", PrjKindFormE);
    +    prjAddFile(&prj, "b.bas", PrjKindModuleE);
         snprintf(prj.files[1].formName, sizeof(prj.files[1].formName), "%s", "Form1");
         snprintf(prj.startupForm, sizeof(prj.startupForm), "%s", "Form1");
     
    @@ -2173,7 +2275,7 @@ static void casePrjFile(void) {
             arrput(prj.sourceMap, maps[i]);
         }
     
    -    TEST_EQ(prjMapStartLine(&prj, 2), 16);
    +    TEST_EQ(prjMapFileLine(&prj, 2, 1), 16);
         prjRemoveFile(&prj, 1);
         TEST_EQ(prjFileCount(&prj), 2);
         TEST_EQ((int32_t)arrlen(prj.sourceMap), 2);
    @@ -2181,7 +2283,7 @@ static void casePrjFile(void) {
         TEST_TRUE(prjMapLine(&prj, 17, &fileIdx, &localLine));
         TEST_EQ(fileIdx, 1);
         TEST_EQ(localLine, 2);
    -    TEST_EQ(prjMapStartLine(&prj, 1), 16);
    +    TEST_EQ(prjMapFileLine(&prj, 1, 1), 16);
         TEST_TRUE(!prjMapLine(&prj, 12, &fileIdx, &localLine));
         prjClose(&prj);
     
    @@ -3637,18 +3739,29 @@ static int32_t dpLayoutIndex(const char *layoutName) {
     }
     
     
    -// Preferences custom script: drive the Colors tab, then OK.
    +// Preferences custom script: on the Colors tab, Change... picks a new
    +// Strings color in the color dialog (answered by this script too), then
    +// OK.
     static void dpPrefsColors(WindowT *modal) {
    -    TEST_TRUE(sPrefsDlg.colorList != NULL && sPrefsDlg.sliderR != NULL);
    +    if (modal != sPrefsDlg.win) {
    +        TEST_TRUE(tColorDialogPick(modal, DP_TEST_COLOR));
    +        return;
    +    }
    +
    +    TEST_TRUE(sPrefsDlg.colorList != NULL && sPrefsDlg.lblB != NULL);
         wgtListBoxSetSelected(sPrefsDlg.colorList, TEXT_SYNTAX_STRING);
         onColorListChange(sPrefsDlg.colorList);
    -    wgtSliderSetValue(sPrefsDlg.sliderR, DP_TEST_R);
    -    onColorSliderChange(sPrefsDlg.sliderR);
    -    wgtSliderSetValue(sPrefsDlg.sliderG, DP_TEST_G);
    -    onColorSliderChange(sPrefsDlg.sliderG);
    -    wgtSliderSetValue(sPrefsDlg.sliderB, DP_TEST_B);
    -    onColorSliderChange(sPrefsDlg.sliderB);
    -    TEST_STR_EQ(wgtGetText(sPrefsDlg.lblR), "16");
    +    // The button's handler runs the color dialog right here (a click
    +    // would only be queued).
    +    WidgetT *change = tFindByText(modal->widgetRoot, "Change...");
    +
    +    TEST_TRUE(change != NULL && change->onClick != NULL);
    +
    +    if (change && change->onClick) {
    +        change->onClick(change);
    +    }
    +
    +    TEST_STR_EQ(wgtGetText(sPrefsDlg.lblB), DP_TEST_COLOR_TEXT);
         TEST_TRUE(tDialogClickButton(modal, BTN_OK));
     }
     
    @@ -3731,6 +3844,7 @@ void runDesignCases(void) {
         caseMenuEdLimits();
         casePrjDialogs();
         casePrjFile();
    +    casePrjClasses();
         caseRecentAndRemove();
         caseProcSplit();
         caseFindScopes();
    diff --git a/src/test/ide/testIdeRegress.c b/src/test/ide/testIdeRegress.c
    index 116eddae..e756808f 100644
    --- a/src/test/ide/testIdeRegress.c
    +++ b/src/test/ide/testIdeRegress.c
    @@ -77,6 +77,23 @@
     #define RG_LAYOUT_BP_LINE    3      // editor line of RG_LAYOUT_BP_TEXT in Sub B
     #define RG_LAYOUT_COMMENT    "' typed above Sub B"
     #define RG_LAYOUT_DECL_B     "Sub B()"
    +#define RG_CLASS_NAME        "Class1"
    +#define RG_CLASS_FILE        "Class1.cls"
    +#define RG_CLASS2_NAME       "Class2"
    +#define RG_CLASS_IDX         2      // project index of the first new class module
    +#define RG_CLASS_HEADER      "VERSION DVX 1.00 CLASS\nAttribute VB_Name = \"" RG_CLASS_NAME "\"\n\n"
    +#define RG_CLASS_SUB         "Public Sub Bump()"
    +#define RG_CLASS_BODY        "Public n As Integer\n" RG_CLASS_SUB "\n    n = n + 1\nEnd Sub\n"
    +#define RG_CLASS_FIRST_LINE  4      // first code line of RG_CLASS_HEADER RG_CLASS_BODY
    +#define RG_MAP_ENTRIES       3      // source map: module, class, form
    +#define RG_DBG_CLASS         "Counter"
    +#define RG_DBG_MEMBER        "n"
    +#define RG_DBG_OBJ_VAR       "c"
    +#define RG_DBG_BUMP          "Counter.Bump"
    +#define RG_DBG_ITEMS         3      // pretend Collection item count
    +#define RG_DBG_UBOUND        2      // Variant array bounds 0 To RG_DBG_UBOUND
    +#define RG_DBG_MEMBER_VALUE  7
    +#define RG_VALUE_BUF         64
     
     // Module variables seen from a SUB: g is hidden (not SHARED), s is DIM
     // SHARED (and named again by SHARED), h comes in through a SHARED
    @@ -92,6 +109,20 @@ static const char *RG_SCOPE_BAS =
         "    l = h + s + st\n"
         "END SUB\n";
     
    +// A class module (with a public and a private member variable) and a
    +// module-level object of that class, compiled as one source the way the
    +// project concatenation wraps a .cls.
    +static const char *RG_CLASS_BAS =
    +    "BEGINCLASS \"" RG_DBG_CLASS "\"\n"
    +    "Public n As Integer\n"
    +    "Private tag As String\n"
    +    "Public Sub Bump()\n"
    +    "    n = n + 1\n"
    +    "END SUB\n"
    +    "ENDCLASS\n"
    +    "DIM c AS " RG_DBG_CLASS "\n"
    +    "DIM v AS VARIANT\n";
    +
     static const char *RG_MENU_FRM =
         "VERSION DVX 1.00\n"
         "Begin Form DbgForm\n"
    @@ -137,6 +168,8 @@ static void caseRgActivateFailRebinds(void);
     static void caseRgAddOversize(void);
     static void caseRgBreakpointShift(void);
     static void caseRgBusyErrorBox(void);
    +static void caseRgClassDebugger(void);
    +static void caseRgClassModule(void);
     static void caseRgCloseFreesModule(void);
     static void caseRgCodeWinReopen(void);
     static void caseRgCrlfLoad(void);
    @@ -386,6 +419,247 @@ static void caseRgBusyErrorBox(void) {
     
     // Closing a project releases its compiled module: Run Without Recompile
     // can no longer start the old project's bytecode.
    +// Debugger display of Phase 5 values: Empty / Null / Nothing / objects /
    +// Collections / Variant arrays format sensibly, a class procedure's
    +// member variables are visible (and only for an instance of their class),
    +// and c.member resolves to the object's member slot.
    +static void caseRgClassDebugger(void) {
    +    tCase("regress: debugger shows class members and Variant/object values");
    +
    +    BasParserT *parser = (BasParserT *)calloc(1, sizeof(BasParserT));
    +    BasModuleT *mod    = NULL;
    +    char        buf[RG_VALUE_BUF];
    +    BasValueT   v;
    +
    +    TEST_TRUE(parser != NULL);
    +
    +    if (!parser) {
    +        return;
    +    }
    +
    +    basParserInit(parser, RG_CLASS_BAS, (int32_t)strlen(RG_CLASS_BAS));
    +    TEST_TRUE(basParse(parser));
    +    mod = basParserBuildModule(parser);
    +    basParserFree(parser);
    +    free(parser);
    +    TEST_TRUE(mod != NULL && mod->classCount == 1);
    +
    +    const BasProcEntryT *proc = mod ? basModuleFindProc(mod, RG_DBG_BUMP) : NULL;
    +
    +    TEST_TRUE(proc != NULL);
    +    sVm = basVmCreate();
    +    TEST_TRUE(sVm != NULL);
    +
    +    if (!proc || !sVm || mod->classCount != 1) {
    +        basVmDestroy(sVm);
    +        sVm = NULL;
    +        basModuleFree(mod);
    +        return;
    +    }
    +
    +    basVmLoadModule(sVm, mod);
    +    sDbgModule = mod;
    +
    +    // Plain values
    +    v = basValEmpty();
    +    formatValue(&v, buf, sizeof(buf));
    +    TEST_STR_EQ(buf, "Empty");
    +    v = basValNull();
    +    formatValue(&v, buf, sizeof(buf));
    +    TEST_STR_EQ(buf, "Null");
    +    v = basValObject(NULL);
    +    formatValue(&v, buf, sizeof(buf));
    +    TEST_STR_EQ(buf, "Nothing");
    +    TEST_STR_EQ(typeNameStr(BAS_TYPE_VARIANT), "Variant");
    +
    +    // A Collection shows its item count
    +    BasObjectT *coll = basObjectNew(BAS_OBJ_COLLECTION, 0, NULL);
    +
    +    TEST_TRUE(coll != NULL);
    +
    +    if (coll) {
    +        v = basValInstance(coll);
    +        coll->itemCount = RG_DBG_ITEMS;
    +        formatValue(&v, buf, sizeof(buf));
    +        TEST_STR_EQ(buf, "Collection (3 items)");
    +        TEST_STR_EQ(instanceTypeName(coll), IDE_COLLECTION_TYPE);
    +        coll->itemCount = 0;
    +        basValRelease(&v);
    +    }
    +
    +    // A Variant array reads like any other array
    +    int32_t    lb  = 0;
    +    int32_t    ub  = RG_DBG_UBOUND;
    +    BasArrayT *arr = basArrayNew(1, &lb, &ub, BAS_TYPE_VARIANT);
    +
    +    TEST_TRUE(arr != NULL);
    +
    +    if (arr) {
    +        v.type   = BAS_TYPE_ARRAY;
    +        v.arrVal = arr;
    +        formatValue(&v, buf, sizeof(buf));
    +        TEST_STR_EQ(buf, "Variant(0 To 2) [3]");
    +        basValRelease(&v);
    +    }
    +
    +    // An instance of the class, held by the module-level c and running Bump
    +    const BasClassInfoT *cls = &mod->classes[0];
    +    BasObjectT          *obj = basObjectNew(0, cls->varCount, NULL);
    +    const BasDebugVarT  *cv  = findDebugVar(RG_DBG_OBJ_VAR);
    +
    +    TEST_TRUE(obj != NULL && cv != NULL);
    +
    +    if (obj && cv) {
    +        BasValueT *cSlot = getDebugVarSlot(cv);
    +
    +        TEST_TRUE(cSlot != NULL);
    +
    +        if (cSlot) {
    +            *cSlot = basValCopy(basValInstance(obj));
    +        }
    +
    +        v = basValInstance(obj);
    +        formatValue(&v, buf, sizeof(buf));
    +        TEST_STR_EQ(buf, "<" RG_DBG_CLASS ">");
    +
    +        // Module level: the members are out of scope, but c.n reaches
    +        // the member through the object
    +        int32_t   memberIdx = findMemberVarIdx(obj, RG_DBG_MEMBER);
    +        BasValueT watched;
    +
    +        sVm->callDepth = 1;
    +        TEST_EQ(rgCountVisibleVar(mod, RG_DBG_MEMBER), 0);
    +        TEST_EQ(rgCountVisibleVar(mod, "tag"), 0);
    +        TEST_TRUE(memberIdx >= 0);
    +
    +        if (memberIdx >= 0) {
    +            obj->fields[memberIdx] = basValInteger(RG_DBG_MEMBER_VALUE);
    +        }
    +
    +        memset(&watched, 0, sizeof(watched));
    +        TEST_TRUE(lookupWatchVar(RG_DBG_OBJ_VAR "." RG_DBG_MEMBER, &watched));
    +        TEST_EQ(watched.type, BAS_TYPE_INTEGER);
    +        TEST_EQ(watched.intVal, RG_DBG_MEMBER_VALUE);
    +        TEST_TRUE(!lookupWatchVar(RG_DBG_OBJ_VAR ".nosuch", &watched));
    +
    +        // Inside Bump for obj: both members show, reading obj's slots
    +        sVm->callDepth = 2;
    +        sVm->pc        = proc->codeAddr;
    +        sVm->callStack[1].me = basValInstance(obj);
    +        TEST_EQ(rgCountVisibleVar(mod, RG_DBG_MEMBER), 1);
    +        TEST_EQ(rgCountVisibleVar(mod, "tag"), 1);
    +
    +        const BasDebugVarT *nv = findDebugVar(RG_DBG_MEMBER);
    +
    +        TEST_TRUE(nv != NULL && nv->scope == SCOPE_MEMBER);
    +
    +        if (nv) {
    +            TEST_PTR_EQ(getDebugVarSlot(nv), &obj->fields[nv->index]);
    +        }
    +
    +        // A frame running for no object shows no members
    +        sVm->callStack[1].me.type = BAS_TYPE_INTEGER;
    +        TEST_EQ(rgCountVisibleVar(mod, RG_DBG_MEMBER), 0);
    +        memset(&sVm->callStack[1].me, 0, sizeof(sVm->callStack[1].me));
    +        sVm->callDepth = 0;
    +        basObjectUnref(obj);
    +    }
    +
    +    basVmDestroy(sVm);
    +    sVm        = NULL;
    +    sDbgModule = NULL;
    +    basModuleFree(mod);
    +}
    +
    +
    +// File / New Class Module adds ClassN with the standard header, the
    +// compile wraps its code (after the header) between modules and forms,
    +// breakpoints refuse header lines, and the project saves [Classes].
    +static void caseRgClassModule(void) {
    +    tCase("regress: New Class Module, compile order, header lines, save");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +    fixturePrepare();
    +
    +    char clsPath[DVX_MAX_PATH * 2];
    +
    +    snprintf(clsPath, sizeof(clsPath), "%s" DVX_PATH_SEP RG_CLASS_FILE, sProjectDir);
    +    remove(clsPath);
    +    ideStart(true);
    +    openViaRecent();
    +
    +    ideMenu(CMD_PRJ_NEW_CLASS);
    +    TEST_EQ(prjFileCount(&sProject), RG_CLASS_IDX + 1);
    +
    +    if (prjFileCount(&sProject) != RG_CLASS_IDX + 1) {
    +        ideExit();
    +        tShutdown();
    +        return;
    +    }
    +
    +    PrjFileT *file = &sProject.files[RG_CLASS_IDX];
    +    char      name[BAS_MAX_IDENT];
    +
    +    TEST_EQ(file->kind, PrjKindClassE);
    +    TEST_STR_EQ(file->path, RG_CLASS_FILE);
    +    TEST_TRUE(file->modified);
    +    TEST_EQ(sEditorFileIdx, RG_CLASS_IDX);
    +    TEST_TRUE(strstr(getFullSource(), "Attribute VB_Name = \"" RG_CLASS_NAME "\"") != NULL);
    +    TEST_TRUE(prjClassName(&sProject, RG_CLASS_IDX, getFullSource(), name, (int32_t)sizeof(name)));
    +    TEST_STR_EQ(name, RG_CLASS_NAME);
    +
    +    // Header lines take no breakpoint
    +    loadModuleIntoEditor(RG_CLASS_IDX, RG_CLASS_HEADER RG_CLASS_BODY);
    +    showProc(IDE_SECTION_GENERAL);
    +
    +    int32_t bpCount = (int32_t)arrlen(sBreakpoints);
    +
    +    toggleBreakpointLine(1);
    +    TEST_EQ((int32_t)arrlen(sBreakpoints), bpCount);
    +    toggleBreakpointLine(RG_CLASS_FIRST_LINE);
    +    TEST_EQ((int32_t)arrlen(sBreakpoints), bpCount + 1);
    +
    +    // Modules, then the class (code after its header), then the form
    +    TEST_TRUE(compileProject());
    +    TEST_TRUE(sCachedModule != NULL && sCachedModule->classCount == 1);
    +    TEST_EQ((int32_t)arrlen(sProject.sourceMap), RG_MAP_ENTRIES);
    +
    +    if (arrlen(sProject.sourceMap) == RG_MAP_ENTRIES) {
    +        const BasSourceMapT *m = &sProject.sourceMap[1];
    +
    +        TEST_STR_EQ(sProject.sourceMap[0].path, FIXTURE_BAS);
    +        TEST_STR_EQ(m->path, RG_CLASS_FILE);
    +        TEST_STR_EQ(sProject.sourceMap[2].path, FIXTURE_FRM);
    +        TEST_EQ(m->firstFileLine, RG_CLASS_FIRST_LINE);
    +        TEST_EQ(prjMapFileLine(&sProject, RG_CLASS_IDX, 1), 0);
    +        TEST_EQ(prjMapFileLine(&sProject, RG_CLASS_IDX, RG_CLASS_FIRST_LINE), m->startLine);
    +
    +        buildVmBreakpoints();
    +        TEST_EQ((int32_t)arrlen(sVmBreakpoints), bpCount + 1);
    +        TEST_TRUE(arrlen(sVmBreakpoints) > 0 && sVmBreakpoints[arrlen(sVmBreakpoints) - 1] == m->startLine);
    +    }
    +
    +    // A second new class does not reuse the name
    +    ideMenu(CMD_PRJ_NEW_CLASS);
    +    TEST_EQ(prjFileCount(&sProject), RG_CLASS_IDX + 2);
    +
    +    if (prjFileCount(&sProject) == RG_CLASS_IDX + 2) {
    +        TEST_TRUE(prjClassName(&sProject, RG_CLASS_IDX + 1, sProject.files[RG_CLASS_IDX + 1].buffer, name, (int32_t)sizeof(name)));
    +        TEST_STR_EQ(name, RG_CLASS2_NAME);
    +    }
    +
    +    // Save All writes the class files and the [Classes] section (the text
    +    // above went straight into the editor, bypassing edit tracking)
    +    sProject.files[RG_CLASS_IDX].modified = true;
    +    ideMenu(CMD_SAVE_ALL);
    +    TEST_TRUE(fileHas(clsPath, RG_CLASS_SUB));
    +    TEST_TRUE(fileHas(sDbpPath, "[" BAS_INI_SECTION_CLASSES "]"));
    +    TEST_TRUE(fileHas(sDbpPath, RG_CLASS_FILE));
    +
    +    ideExit();
    +    tShutdown();
    +}
    +
    +
     static void caseRgCloseFreesModule(void) {
         tCase("regress: closing the project frees the cached module");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    @@ -1598,6 +1872,8 @@ void runRegressCases(void) {
         caseRgAddOversize();
         caseRgBreakpointShift();
         caseRgBusyErrorBox();
    +    caseRgClassDebugger();
    +    caseRgClassModule();
         caseRgCloseFreesModule();
         caseRgCodeWinReopen();
         caseRgCrlfLoad();
    diff --git a/src/test/lint/analyzerBaseline.txt b/src/test/lint/analyzerBaseline.txt
    index 574564ef..a218d11e 100644
    --- a/src/test/lint/analyzerBaseline.txt
    +++ b/src/test/lint/analyzerBaseline.txt
    @@ -8,18 +8,22 @@
     
     # FP: value is stored via arrput in sFileValues (freed on page close); stb_ds OOM hook never returns, so arrput cannot fail and drop it.
     1 src/apps/kpunch/cpanel/cpSounds.c|addFile|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    -# FP: TooltipText strdup is stored in ctrl->tooltip (heap control, freed by freeControl); analyzer loses the store.
    -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|setCommonProp|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    +# FP: members calloc is stored in mod->classes[c].members (mod owned by caller, basModuleFree frees every classes[i].members); analyzer loses the store into the symbolically-indexed calloc'd classes array.
    +1 src/apps/kpunch/dvxbasic/compiler/codegen.c|buildClasses|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    +# FP: TooltipText and DragIcon strdups are stored in ctrl->tooltip / ctrl->dragIcon (heap control, both freed by freeControl); analyzer loses the store.
    +2 src/apps/kpunch/dvxbasic/formrt/formrt.c|setCommonProp|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: parents[] holds count entries (one arrput per control); the path needs normalizeOrder's loop to run 0 times while normalizeAppend's runs, same count.
     1 src/apps/kpunch/dvxbasic/ide/ideDesigner.c|normalizeAppend|dereference of NULL  [CWE-476] [-Wanalyzer-null-dereference]
     # FP: strdup result is stored via arrput in the static sLabelBufs array, freed on the next rebuild and in the dialog teardown.
     1 src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c|rebuildList|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: both strdups are stored via arrput in the static sCellData array, released by freeCellData.
     1 src/apps/kpunch/dvxbasic/ide/ideProperties.c|addPropRow|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    +# FP: stb_ds arrlen macro NULL-tests seeds after arrput already wrote through it; arrput cannot leave it NULL since the stb_ds OOM hook never returns.
    +1 src/apps/kpunch/dvxbasic/runtime/gfx.c|floodFill|check of  for NULL after already dereferencing it [-Wanalyzer-deref-before-check]
     # FP: path needs realloc to both succeed and return NULL; on real failure the old buffer stays in b->buf and the caller frees it.
     1 src/apps/kpunch/dvxbasic/runtime/serialize.c|bufGrow|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    -# FP: values filled by variadic popArgs (writes through va_arg pointers the analyzer does not follow); was the basVmStep key before the basVmStepExec split.
    -8 src/apps/kpunch/dvxbasic/runtime/vm.c|basVmStepExec|use of uninitialized value  [CWE-457] [-Wanalyzer-use-of-uninitialized-value]
    +# FP: values filled by variadic popArgs (writes through va_arg pointers the analyzer does not follow; FOR_INIT step/limit, RGB, STRING$ added more sites); was the basVmStep key before the basVmStepExec split.
    +13 src/apps/kpunch/dvxbasic/runtime/vm.c|basVmStepExec|use of uninitialized value  [CWE-457] [-Wanalyzer-use-of-uninitialized-value]
     # FP: a/b are written by popArgs/popStringArg (varargs out-pointers the analyzer cannot follow) before any use; failure returns first.
     2 src/apps/kpunch/dvxbasic/runtime/vm.c|execDevice|use of uninitialized value  [CWE-457] [-Wanalyzer-use-of-uninitialized-value]
     # FP: recnoVal is filled by variadic popArgs (writes through va_arg pointers the analyzer does not follow); popArgs returns false before any use when the stack underflows.
    @@ -38,6 +42,8 @@
     1 src/apps/kpunch/progman/pmModel.c|pmHistoryAdd|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: name copy is stored in fd->entries[] and freed by the listing free loop.
     1 src/libs/kpunch/libdvx/dvxDialog.c|fdAddEntry|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    +# FP: listItems malloc'd in fdLoadDir is freed by the free(fd->listItems) just above this NULL store; analyzer loses the pointer after the per-entry free loop.
    +1 src/libs/kpunch/libdvx/dvxDialog.c|fdFreeEntries|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: fdAddEntry stores the grown realloc block in fd->entries before returning; the listing is freed by the dialog free loop.
     1 src/libs/kpunch/libdvx/dvxDialog.c|fdLoadDir|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: copy replaces e->value (entry in h->entries, freed by prefsClose / freeEntry); new-key strings go into h->entries via arrins.
    @@ -47,7 +53,7 @@
     # FP: same arrput store as ideProperties.c|addPropRow, reported at the stb_ds macro expansion.
     1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|addPropRow|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: li->count > idx guarantees widgetAdoptOwnedStrings copies count > 0 entries; the path needs its loop to run 0 times.  Reached directly and via listPopupRemoveItem.
    -2 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|listItemsRemove|dereference of NULL  [CWE-476] [-Wanalyzer-null-dereference]
    +1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|listItemsRemove|dereference of NULL  [CWE-476] [-Wanalyzer-null-dereference]
     # FP: strdup("") is stored via arrput in remapped, which becomes lv->ownedCells (freed with the widget).
     1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|listViewRemapOwnedCells|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: row stored into d->rows via arrput; freeCache releases it (the real partial-load leak was fixed: freeCache walks arrlen).
    diff --git a/src/test/lint/cppcheckBaseline.txt b/src/test/lint/cppcheckBaseline.txt
    index aa9f8a83..d2aea87e 100644
    --- a/src/test/lint/cppcheckBaseline.txt
    +++ b/src/test/lint/cppcheckBaseline.txt
    @@ -93,6 +93,8 @@
     1 src/apps/kpunch/dvxbasic/ide/ideToolbox.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'onToolClick' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s).
     # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
     1 src/apps/kpunch/dvxbasic/ide/ideToolbox.c|unsignedLessThanZero|Checking if unsigned expression '(sTbxTools)?((struct stbds_array_header*)(sTbxTools)-1)->capacity:0' is less than zero.
    +# FP: floodFill starts the span's left and right edges at the seed column on purpose; the two scans that follow then widen each one independently.
    +1 src/apps/kpunch/dvxbasic/runtime/gfx.c|duplicateAssignExpression|Same expression used in consecutive assignments of 'left' and 'right'.
     # FP: nul is the storage for the empty string's terminator (struct hack); it is read through the BasStringT data, never by name.
     1 src/apps/kpunch/dvxbasic/runtime/values.c|unusedStructMember|struct member 'Anonymous1::nul' is never used.
     # FP: style suggestion only (variable scope), no defect.
    diff --git a/src/test/music/Makefile b/src/test/music/Makefile
    index 5c81840a..c17aa6bc 100644
    --- a/src/test/music/Makefile
    +++ b/src/test/music/Makefile
    @@ -100,7 +100,7 @@ $(MUSIC_TEST): musicTest.c $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVX_MAKEFILES)
     	@mkdir -p $(BINDIR)
     	$(HOSTCC) $(CFLAGS) -o $@ $< $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVXTEST_LINK)
     
    --include $(LIB_OBJS:.o=.d) $(MUSIC_TEST).d
    +-include $(LIB_OBJS:.o=.d) $(MUSIC_TEST).d $(FAKE_DRVS:.drv=.d)
     
     run: $(MUSIC_TEST) $(FAKE_DRVS) $(BANKFILE)
     	cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 LSAN_OPTIONS=suppressions=$(abspath lsan.supp) ./musicTest
    diff --git a/src/test/samples/golden/basdemo-debug.app b/src/test/samples/golden/basdemo-debug.app
    index 91750c8c67f602bcfef591c80ad20780a5c1b163..2c0f6202322a495be2502a59b4bdd8dac2ac017f 100644
    GIT binary patch
    delta 11332
    zcmZu%349dA(y#8>&cziV1PKAwaE40(K}C^pB%JDyg@i-IC0R%?7ug`7zPAYw1S1b{
    z^m*c~&jVB*9(W)qDqeW|yan%|XzTL^Wxs#*%xuE@ee=ugbp5-ky1KfmyQe3YJt|&$
    zMifq*oj>drB8ub6#nZ+uaKGD*DB#hP@Z_scn)}T0$~!EgO{qj{aI-vLC#P;M%n|jQ
    zmxZzzwwqrCxcb@j^@r#0-SAL9R&eXO&7X}F>vo(V28yxkz|@qVvU*R7-F(h5m$k7B
    zGL3sWta(cj?Q6Ug6w7TerI6k9FxQtI22|uU$Syr52WB)&SGI9aUC-5r+csUzLaX-#
    zLWRx_*}J2?A-~>kGJ4B?g`DktEvJNviZQ*+S0c}}lD
    zpV)%Akem4@cd~8M2G-NmgZn?DDAYX0z-avfuk}7q$&Nv1Rs@Qhd${%JUoUhEW-wpb
    za152Y8$V0(?B*j!jr;aB)}V?Vw*KMsQ1ZOqw52oS8}hFWl`Un7bi4V*qt2E<_Tc+m
    z!S#E&D}yBrn$eWMF3%1%Z}w^$D9>fy)
    zDf<*N^R=&@1>6z~@WqUh1y1SHeB}3dd|uPT?9Za+??7NsHbj0NwrAe?3!JsdLt@*3
    zEup3{>}$L1D~K02Z!vrsMa@^CFxwl|=F41y1Hx{;*s!pwotzQSHh-_Lrfeq=PgSAI
    z_w9dB$??>M+c9;8&7)8knbWj|HHMmVJ0>y^YZf=LVLv>*D>n69<{rH>d
    z=^-1{@NlU4AOQ68&}2mYCiikSrj$>qg?oyc&v$DPD~pTQ?=5c1KZv26!aB11tl!r>
    z)~()oGMF3Vb(8nye}T@>hWsyP*kvn?hHPhCYDSM_SEM=qtMAkGzZ)+s+O)ISsZCuo
    zs62@%8xp&4|Am|Vv|wR4T2NhESy)#XSzI2ii_{j>R#LHZV%jN6CZoU3+_W`fhjSop
    zym-;cX)`+G2(%c<(u-DvE-bGpj?%+UO`GohW*`S{g4%(BpRtL4_B05
    z9HC#F?QJrJqvTt+{IIYO6}Z(MU;U4RuD=SK&@}O4^>7B*8Kw;H+&sD0??1Kf{ggo?TtK
    ztRhlS8;MW{XkO;*Z#$>wG?c(Y3(%{=NOVF)bu>aZqpPb>@s=~O-Ny5OLH!`8Y=;KQ
    zaBoBHqqsl79Rk-J+}Gp28TTEypTNBb_W|5r;68-=C~iVm6mHIw=Wu_3yJPB(A?+V)
    z8{=yC8s38_2(PpSIS{uFlAcU@3hAk&%ShLd{Uzx-Qf;UUYL}V$--vb*q?ePO$*Y52
    zL8`4`SV=+3XtHq!^1daJr-A@rB8xSCLS3tzN^Jk-m)V
    zGmP{)%0NHbQC?vd>Gc$-THF1HKTb@|gU%ejq1bLmx``X{Xy_=?8_Dit6l^jI+EWm<
    z+k>hD1?R=CK+D^MzD-xsyb6Ea~S+KTrAv(l3&JiS)~)Um^V}CI5T_I{F$v;jGuWx8WNk>DU_#
    zS)|`&XweM;(r=SWr`0L?9rJ7?{VrFjj&z0UM9Hf}iheHvbfzQ*-Y1noq4(i${Q;>i
    zUhzXxnO^ZD9yOQY0NL5b8wXiS%;@cE1|W^b1fgw~{(|fr!}4W|*jJ=ZV6LF*MnR)#
    zmi~t9eunScgh5#>(V!0-pgT;?Ce?!)X6YZu-fwt+WDfXS|CjVn6w^PG>PZV#FIu>t
    zGe!ou?iX$fe0haQq<Q61s2Kj`XW(nn}-xFfoIlo`_niF#Tl^`Devc>ZE2
    z)%RK!)i2+rk1=+Uj<>cfNHOkGgC=30Oj-(UiF8REh5=#NlY~0a?RE|YuS3xEHaP;*
    z*1=I}!ftCi)kg4`t(m1e2)ofB=^`{5sjEARP##iOcM_q)NL$@mgoYsfbcP5Ow1NM1
    z7ZK`>l+&3aw6r~*T}7x1jM68FP#r(JiO^AgW{FTeyQaGcZRmt&HWYNmvxf*h(H+m8
    z7=2Qn%loj)bA;W=*wS05zQ(<2B;+jJU)b{vK0v6LI~KV@^&`OnV1BrrfkO4CSJ_*7
    zkbnVd0D68L$6%pyJ&qyF!P6b7fyOh#gbklUc7#xaD0GxVbEMFtgg#a1(}X@ST<+l>jCQHH<_KoRA(sU%B9>%w(a4Q@Tnp
    zP7`{%P$MWXvFo^*M==BWD$i*>2B+{C6bkz;>
    z!g-!hqr5ylPpDJ9;12rw=L>b3hn?tS=L>bZhvoR#0-;8GSZ~Da8D7Tf3xqn;!~3|r
    zG16h7#&}>Zn9uT=OL&M}2b2mmmc%By!*3uWR33?e)b|!2StQgr3S563U+wa4_2I=r
    zjcp2BB;Z8Xk4f
    zPQ=t)65|q)zFw$#q|T$We07%#HJ?PB{_ROL33WcH1@xfLu%UIxU%)QhEbPvf4R<+0
    zg*_MklesY5o6PdBuMzrMM#3Fqf?<@4QRqhcI)VNmY2Ck^a91%K6<^2ojb;!qtXqUy
    zNIbqbxf)t4=t25sp1@rlnON9W2?fE-NMkt8lRVcb`-6PZ@
    zu0-gvO)j{V%@;95GaiyQNi&&k6PSA#!JnXTSE&;S%$3I@khKhQzmWn1qyony05P{f
    zPl5+hNd|cUQZbWH5KB%#27y@0@d(6H4f3E+i#;c97y92q@8F({ciF)hJ;9_6VDtu40s)M456s78;Bas=kWrorzJJ4ONfC^^>fDeT3-2(!5Kg
    zC!4OO!5*QXG9(PnFywAS!jhhb+~YF`XfxEX{L==)a16lw3kA;@8g3tMB=!nbPHqc+
    z1TiJd*l|2D<&LCJS;y4{T8;8^Y`VWHFX8lfQP}UA)%RtgE~J>dH0~GrRiR%K`VD`s
    zc~j`OgnnD-cZ7aVJg%|1Lp)%;!;qs?jKaCt?TN>D0r^loW|ohS_=(i`n4bvhPdJ-Y
    z1xz*@-bxgB{Zm}vbx&2ryt!~T^lgvpPq}$FK0gzx+ADsJ_P=mj`ckMGPA@Nrzi!>V
    zrQE%5g*{o?u<1LYYQ60LL8z#w>PMmfYr2R9{wG9867f?1vvWbe%-An%Ek}Z&*%s?J0I6#6fA%p&gfF%xl67w0)Z1o1wB34tW{*=W^*%NV;H=C|LE
    zYfB~?NQ%is-;VWq8%|7>oVs%5O}spx!OVS8*7rMlR4
    zie1LA1Z@EB$dz_4DFinWU-GGmVx#=rTDT_{cWYQg{R
    z;VHUzi-~$z^6)Z^J+&TM8u@}n>U8(yg{GYch8MlHP+yy*dOT$g7t|aoUc=*J={!3(_)+)NP)bT5o3Wa}0X9nL>i939G61`d%pQIYuxf)fL__
    z&5~*Zs|bDEK?*k#ilq7nsg13nVyQNf+T0qN1G{30dGbm&Y`(M`jQ#~uIiCKoy!vd?
    zB~q749g*xT4uwUoci^?fQeEYFtxT$`y?p?7wXQAYQvK7bULx%r(iKuyN_7q4#V+)I
    z44&0htsQhN+fgg+n6aZys_Q&EE|Thc@AzY?lc#Q=m%J91OLZd!Mwn~IShXclu|lew
    z%!XOroWNE}bxX@BhKk*qsJW!I6}Pb!F=;28q-c=pcF&5{oOK@j5
    z*mve;sqXchd8OBc)2a!fuQKLgUcQ<&tejeyP5Pf)yqG8JYb3S@2sqbq_D69YyUuOt
    z2G;VKzEP_CJpDJdcE?tB$8FNyVFu+6skV9UxJ#=0>0@so{w37|^od8@E!Bf`z$5OF
    zYC9eBhHaEmW`Le#4x1
    zSY!owfANu2Z*uH={r*Ji15&*OwzqM=Lkp4l;r;w7h@>_fsDoysKv)$Tj(MjXC;vmW)IOU)d%jj!JUb|Y3-&D*-hWe
    zn0J<>WvDmYF-!j>?asz?Kg*5yeuR^-UnEXd^zTytA=O7DE)}9o{4nWQoP+a-2>=|o
    zeT>!hs7x{kj(c3YceexGa`C@f~C7zI?Hl4p4_8X
    zcV@%OYL4khZ*z*2Z45d9#oswEpPXA9w9I+IiI&c>bZ<-dv3T$?Bbhu#nHN{QzVv1M
    zKI0)iPe9lDxd{7+5m?&Hukr;o|xeLk>r_d-wXIr|7Fj+f7@0CQgr_R%O-Jzj}-sOXJ%)B
    zc~XM;XIe!#S&dDz?4iUXJUvlmPMLiV;?(=>M2#mj!@_=-WoBB(X6f0Mmr!#obNY@g
    z1?mOYpKomxCZQHs_AjItTDrtizmUii;sr7Z#glQV2~}j&uZYKpg^WV(EVgu+!G9z5
    zdm^LCP0E1v53pWn=?b6aNFrq_E#6*%VOwrm
    zA=2thHbD3ogqQiL5l3n)xAY262E*fv;5c+8hXJ=IM1#KA5XdGee$GSe5%q$C=9|PS_!XP6BW^NU0(IMin4;GD4!w+>#i!j(miN);3U)EPvw??%mEai2
    zvbPxJS6Uk_<7~&WFUJ9%YymoRudYj;tG`}#Lr;6&3GR?ZQy`Adfd*|mZr>=8?~$H6^R
    z>Ydh2@%W^;nWP`HEIA1u-vB@E6=CfYmfmIEM1(oit9&YE`q)9>J+49THXTgoFA}Pw
    zSS3^R)5o(vW4JpBd>m`gdp%KnecB1LFB5@m{LsF~4dC
    z*!$YiOJG=Dcdj3KURnxGtXMpux}v()`E_K!KD-1+mo5v}Mv|zcytbqwl0w1iMZqY(
    z3@6dxAS%L6_Ndo)d_QVZLl6N!#iGy(N>ZK1iYx-MK{^k>n`vPiJHDiSP#qDX15@Z4GF
    zObnJsgW;eeL$s%vD{CTS&k0vX21Ij%V}hNaAsU<#4MvUP0mE}g2Q69|iAt)f&h*U;
    zmPRV8gC*6KH5HM%$WSMkKR(8vFIz5r|6>J1Csc%^(FIeg%ImzZ;&iUZga9B5`!JV}
    zyG?{Y4h(Y>wQp57bb6$Y2;WT2b-XNlg7OI4opKsP3m6VCI*2QsDyx=xVYpyIMS01E&atUIaZF1zEZcAx
    z?dNaX5IP>l-3d@lxUMV#;kzfq7V(eiM2b^0EhEOL
    zMU~OT2|oT01VQC46UyxjT>;u0&UiF8k8N9`z7#Yz{0SO?3RBoXSAHRAtcVFZ3>rig
    zZhUt|nHgNnrE3kr4Y+hme>(t6_9hlaLE^$>g`#UVLrg2GD6fiyP!TYuraMcgXH4Om
    zB&rP8)t0X?;%ucW?l#I+x^#)DXJ;}UpsHn+Zi~qbIj>Id%AXL?t}Dpxx-`4f9VwS~
    zd&ll^Y1d9}xHS!u@OAk(yICh=f_;S{V2`*w(UR)g$Y9eDD`wE5((p=yuxeMV1~i*X
    z)Y;i|c9&RZlzd0AGNvtBURDj*EJ^%-8VJY5_>Y;o@+vnln2}-Z*7?U=co#s??HNN%
    z|4+=}TnsENF$WqiI#j`3afgICk||Pkk=;60ajf{isFkM
    zkri&hG0G4YoU-cj5|@vSXVAD12aP678L(&({`-kPw(y1jz=3Tg7a4#RF_1(H>#7Q?
    zmm823F{o(a#M-cnvFkm?2^EoWtzl&EGl-KH{xMfi{9-+em4!>$EH{}LbaUB_7N#Xd
    z-MD1c9?&wV3|JJ6xDm#o>gotXk!7YarZFEZueLeLEV$QKQvdnW}waG~QH><9Gt1{o2=qn~1OgK2C#%*t&*(3ESKO#5MDCIbN~uZy_D&t@_N
    zxAD#aa_(H_vbmcNvqodq<_rHO5m!{n)3Ub~SU4N@Dt4^4&$XQYgxYuaR+6*{;hMVg
    z>MCq4a{77Lj#oljiA$U(=bR+gI*0LlwUa&fB(c?*j^FLhrTBf#*^1xaosZ_8lr$9a
    qdh`^h34|MD2sfx)qJSU)67E2gA>kG#FpywKLJ}?&#}J~hAq&`|
    zfa`tVUKul0645!9e81-q^asM-Ik?wN%BzW$}B`~B;^SMRFo>gpVRS={)H
    z$jO|QHSjJXYQWJOR~x6mxh0v%=h5SF<>Od_drkN5AGN>_EE6Zo^Kr_*Ehk-MY+D)Z
    zz_4Ba7Qm`kQZ}8Kf3WuHo~+>R3ERFNA|}+e6Me;~31F(p@>d=3+x1g_cUh|oAX9yy
    z#fDvq=uq{wpjghXHMe$+X)idzkW(pJcV6S0Sub7L>H`%QRGny6vzLXc4)}t3PMz$Q
    zTAOu!j$KpBdJBGn+;rzVIWcJ4wOJcHb$0z7=!^}mUhMV$D*UgX@LNN}rdQdRuRz-1
    z=4LyuzCRjj+CZTC-4+`La9i!4{l7X@)^JH@otLb3(s$1J#2T&Uz`fZ{7hm_t+y#)E
    z@hdyo%+qr>`=4jmUtv&mzR>IZ1Y&!rx)c<-{Xw4Cw_S3_%96&%g8p-DEJm0
    z*!3R*s(K|Yul`NkEbzire*nZRw*c(=I>VAu8~$-Wm+agt7tBAn4CRH87VHP$Y1-_v
    z{qWH;s+qqbSTn>ha4&iFrAGODQ&D#PB9HS4a3&irKNK4^)9c5f@puKSj&?C2+Gf}H
    zMH_rOZlf773f8xGH65RbvUJ||7fu}H=kD(K*@O{Qx^(8IgEMQgzCso@0Ytnu9jcGG
    z&HGQq6IC}Q5S)_J@uQa{QSKm<1ZV%%S{b$?suse}#v
    z2F4K`g_?DcdJ!iFXhD9cd|XM{@|=pC@Y2Haig4MuvgOpp2_;^l4uikHvnz3fD0jLf
    zjS<_N{G{P&F9S7_i&w1)UR78+vz*pByOTQhY>$G!5^C>iqB=92zjA42xIA1|7%D2f
    zI!t?=(@AYw8Ts;pa8c3taK-4-QaUS~3z`ki+X>wmau
    z&lp376MkoxEM8d@9#<9)(?{rdgwr~C_64n=8$8B@oN)QrqLT73Ek*N0H0*RXCvTba
    zZ?t~^dNk=Vq{osDkuD_r#twQt=?$belddKGN77qJ
    zZza8r^i8B4(l?XdPWl$ox02pLdMD{!q;DhrC(?J2zLWG_r0*tu59#|T{>R(kj=rC7
    z1nCDj%AwmN=?59HNI%5LlG}WwA0d@URet@bxmJ^YoSRe1&dsGg6&gw>wi{=bnJNfC8*Hl{O+!Uy3TC6C
    zbv*^oL{QfHI0c8JJoWb!3?}0G0|f^p;d+9C*I^l;|4G607Py|IU?pEQ1^w_*pQ2zb
    zUr$qT7l-IykZ+0Wznl8gg+u)_*)5F?zmN);uqKd@x%yYcf4*t>Et-PgNp*D-Z?eyN
    z8%U+oTdY)*;4E~5$6OjQ4k1)`k3%vCk9tx)Ohn>@P52}27pf-(Qv`C9D0Gt0&4i95
    z3*B6B_EF|7giaNrArj(FTMCKEJhFsS+DiC}Z|HdHOxJ{rg`Ds-;VYb(R+IRW%1FgP
    zX)RPQqo9pIvK8reLiOep?QzRew->4pCof8O5UMX{bx$DT4v~&R^`jIk#?VQq3rVEI
    z#S|ZWL|^K0
    zR}{His1fAbwUdwJjJu=o2%$zc!aeRWIL4nIDby7t5~#<$QPwMj8pY*ye-s!cR0abN
    zM1c&UMx%ncQ2Rx(F{qt}$MjgCGXJUsHsAxyjd5p~iXnvQ4R?I;RUY-pp7*p(c2hLg!V=4521+EQdtR%N1%81Hz(#
    zUNz;3h??wSgFI}eP*V(~2&FLCLuUz<&FD~%o-Nc=QqyRdN6!(;=5{rmE{%3LSEwA<
    zG!>-bO{;jGP%~J&=o_IrPs4nna!HJiRr-}e<&m06S47n<5NZ~Q271a9SSZwNQgdi`
    zlp()q2f%UIY}=xYop8H_M`kwLFDs71b!i67lt`rAkYt`lkjPaF>SxsU?5
    zy&1P|U)Kp0;zrad+vI|kY(7`6`Q0kb#I;^vo?!%kf%C#W=d<0Vd^N`Eg(gXul3zhE$@dlx96nZ0j62g4TJV+aE
    zCXwDG)FP9oKRA^=T1RTQGfbE%anwfc!9xT|i6ilCgRYGwWfmm&HRx@u5A9qTc--D3
    zQsRwgiRfNv$B-~E&5$=65*=S)$n8;PAF&#izlEU)QZ0HLmRk)A!aT^3JA_(HZV%Cd
    zVXCowm~s=x#l(~qD=nscO_I8cO^?p@VJ`hUg#8g7tPw2vgj(X&{yjq9EA)LrKM+)JVSUGvfYR&Zl;lbQasC##NFaq^Jv(^SB!+G_=+N~6Fl;l
    z!bm|V!vfs!vyE>eC_mg4dehu2=-bnv_i^`bIX)xQGVgXj`hV8#={cdU;?d;Q>Q$i+85ff=zrSWG0ynQaclK%%In35_B?y{@e4lL4
    zZ-@@U6MVbTvUh|k@zB2u{VoS)348rsRNebLa(Nq;F+skac#D^Mj8r!fUe
    zV|3gjB2qn0dtD-;ub1ix+UGS@Mx(o4swZhb$GM6v8zOt-6?SY+EA00$ZODOYw;TK$
    zq}tOY_#2yA{uEojS=z^q<$si_&a-^0=_-OAwjqsAyP9q~KaDqYUurL@eS`sZL~oDM
    z&yd;=`W8b!OX@jN&x794n4XP7QoZ6i{GikiN%dErB1Z<)s}zvf7l@F|
    zKx6~z5N$N$`e9Znc^}R^;U1CdH8KO)P9pu7RIj^~e$s>wgLk)7hp|2MH=J*ny$p*i
    zAHOp1k?KvZeb4VYsh^hWEwH_f^Bv;%lMr^Cu-GM;)GtW&9u&O~MSW=rlr6y<<`QgC#gP(b4e_E>AGpf~cP4tNX_!9bFddW;
    zZ-3|&861kPbMAqmGg~MyK^xQXh%ReB;Vs1@pC$
    z`Noy`7BWZWMOd4#(035}h9$6_`JD__wdPp-fLU1DaO$5@pOjil=ktM`yPRS+ym_V@
    z2fCRpq0>h5KcKnZX+NmX%>PL9^8b_6KTG|K)Mq4T8{U|Ki~E_#G+s~6GXAIu3~vs=
    zuX8TKv0(&O{~FVM+}Sy(V-i_tLu|RPmj-o95;6LF&}~prXvg2<>y=+COUGF{-qLJd1KSmIXqoe$ve?_8Ru$ncTuMmMfA0Kx{c)kE%$g
    zSo|6lk0>zSCl!T~Y7{x^2e*v0bTt^Ac-Oi=Jp|1dK&@i7{y-^yN=cxUL`q5GaN-dW
    zH9E}#YAk7Op^@$2m$G#Yx_wOk1j!NTSiXiI^-rS!IvU`$&#xmL{$DC|9*fKlo
    zSXx;2PljQzwMCeLb%|vkC4HHthg#|si3}lLBQvmuS*S4Hm_Cgf9d2P@VHAz9^hksM
    zi`2i}fpvw&0}FgVf-l3;qoeJS|F}bEjD_LC3_pP()6!Xn0YmF&w4>O@TY7?lFtmQ*
    zsp57nxQP}I3P_!S)FeY-gq+0)nPTbeDE~Q^AN!kAxfGcHSMX0`BEm!JHwa8;?rIFB
    zT+8llyw9_?p!5X=%*Tb9CXD}aN96B>rab}7OLL>qo5zu#Qw!+lAle
    z6H3I0!zvD2yg(D)8vG)#kG-#o`nrLAg?-h`&2qW=M(3KL^PD3?dpiAwy@Tgh-ML|d
    znma#C7$Z-T^Y+9}BE>l|F(8Kur}?B_a^8{{9#gW`G$0!o^-K1
    zCY@to8E-kslY7Z*pL6Ntp7M-xuAJOQ4v%*>P3|p&iB28B0ZGoMxLlX)SW|k)@0&Y)
    zr(7V@TR4+IRkm3&dw=)WNnJ`%9P%6SE_Romtae$U3L#Srj;`)yN_I&<}AzZ
    zCD*1oPIg~e)!KO}yPNd4bB+NV(auSp+DA@m=M0^Cp`6#wSpsTSduQ9!F5Paxt5)L?
    zN11Dj$E*z}(3ntp;iAm&@{%3xo%g2pl25mHe#K>K2dBrh?s9zxXY#Z@c)7wyv_@8Z
    z-5^ZtbR)j4@i85zpmO}+1BiDhswk`|3db;3;iJ+}ML`VW#Q`3-{KJ3dn_(Q4l!lAL
    zi-BOO^gw*hTVB2tkgFaaRlr~93gO;w=pxYO_0OZ(96Tpt?W__@iWpr1+GpMaT={&^
    z7=E$#5NHrM67VR%Ju|o%zJ%kp%n;mxOE>zDuRRn`xQt!F9-W6qgUKc`mqn`!
    zO2C2n0aOd;%bYe5d{_e&;vaKhy@gxX6NZ@npR#OaSQ^WcX-P$Fxtk;g;wfCb%5GZC
    z{7n2GO|7vkMm3@cN=u5%Io{yG(FUiFD#B~r8f28AI9gOtg3KE}c8WnaWbWW#+M*@+
    z?*iV?x{6C#u{-EoV1>)Uqs{|d4wQSu>$+*oI)l%mqWmJL!SW3h
    zBR)n38HhvgLWm=moEtC?npn(Z%ng?ZVInmUJH}9Bj{1V4t_Et_AtQn~`)+=iu4|h&
    z&8piz`?wPQoydazVz~1pekVF#;&-9bVqt#~c1Gd1$|=F`W6qrm`=@@58F<_PNaM)N
    y8k0NTIg-$^H7B9>WTJCy5RP%vayZNq|iR)
    zcHhBFTMl{ywQ8Srai+hga-+Xn%WeJ&e17$jRTHSI>#5x8?>@ld>Lc$#85_E-2~*Zi
    z07RP)s_psBxTxCVnQjd~uU_yBwnlxU-tm;n
    zC2((<8s;4yUa=H>i~hwPg$%-<7y+5feS9_-op|DkHQp
    zY0%FUfp}ZAtNL)|2XKaZDnD50Z(gq@Nwj)={`-*E991GiB@8
    zmL#G)a2!BCkIr#gv9cjFr@eD+ZC7n@RZFNV*g2sH?#P8$1NTDgi3HIV!IkS)RRlx9&X$JOmJLDr
    zQtj=VJqT4xD?-h|*4C)wiMdQH`njNft;-z3S%Fv3W0G^V8ID_lowWb*;fU
    zoxvdWgXDGUNa~WI^DqJpt$_@(3&Cs;h=c-?p50hcn`9a2=3!iFO?#OTkZArEzp4(s@49TQhWq<0kStQ4%
    z11O27av%kk!C6~oxr1zX(AOFGbF^Wuph3Ks37cm4AOMyO)mVho5j2>VgTN;*BAropd@Al`9K5OnXh9Y&o6~%-C!KWmxf95oLJdvtkkjFeJkZ
    zi+JFqzJxMljL4Ry^hcJLnc?|WVf=W
    zu(dGJ=Sq`P9Y}r*GR=V$#30ih$i*mxODXKydYJNWe1rIc2T98@QD_zy}G`y&By?{KY62o$?4*MVz_IXa&;ifLP
    z`R3;eC%;%jiTPc?5t`(hA+O}A@KCWEQ@;Z!aRCc8KoW9UYG7eo)-rIaOA;Wth^D!-
    zs~9KiXmU(Ry)i{Drs=(GS;9h*f_*OQQWE9yQd~yzJ9K$W%5oA>`UOaf^k#Q8$rbc-
    z7aNa`{H_kp0n5)d*}%b3r+qV$5gm^4N;NJ2k^zn6!)&o2aWZP>ScLjeq5QdHns|vQ
    z?A0-_CC|0M?K!TphD>$j;xXY?
    zsMRl}wHWZuAbG%>f^2i@Y_XTb^U;Jg)9(2Aoc*jWuMSSH6O;r?uGN;DHYM5VmSo5c
    z5E~@0X(1*XhL0~W@2l9l>~hIq>NPq|&Fa#nI3K(er1?cNtbg@Ryxd45Gi|(Hwa{W@*&)5!PW2rRo=8J8!&%ztXPJzW`wW;9R
    zV0`eeWS|A0hy2Nu0){ee==TUbF!V+(RQ+MZ;IIq2$<;mpDU&YrKO89D@UcLJoj@%e
    z0*xD;NkSGj;ic?{HMZRBAQ$2NRA#cW#m&lAu0vs|RU?Ou+Q#X`Da)w^jCFyy<-o-T
    z7vxs;)W}=HH#asU-@%UtxEK;PrF)yp6#Ivk!1VnLzkYgrnxD2%+!W@^MF6f_qPB
    z_wF+8J?Xml6#dN&<bM@K2P#C`nx_RV)kp22b|e&NWMVwAZ8pwXIFfY
    zwL5ITr4z2Hhg>lD6TrXIU?V62{Ck)BC6b3@LUeo+JbGDsbk2D63icp#AeFE3k!FTZ
    zzD7Qa&#qP(9zOXxZP%wih_U!<((I2%NUs8a3RcfiGuCP6)$|5F6aIkyM}4aHkjJY}
    zjepW7MYDKp`6dYdj4uA7ULQNu_g2DTNeg}35o*h0pn6By<8s6AnzzHZ*56;XN9HRc
    za1UI?E_qV>w-dKw*x&tc@#6h25;qFbEIzOLCdw~AjyS$`{)SyP>GVDPf@R2iB#%=!
    zlfV~Lt~^23BFNlihk5pQeEH}ZPu}MZbCcowZ;~GvK9s(ebduzU?A9I@h8G>@;6w9$
    z^%1!6Vv`Y)r|63q`NuJG7|rA#8}d(>+}mH%e+qioi4Vy@p8;xQVHwQ5RsKV*DO{sR
    z|D*m|ILv`X3op^5&s4N9Ta}OBt!JOBx5rNkGuIbnU51oeFXJKqmrrEo@5#+x^I>`+
    zku^})#Q(XZxG_j1Jx!a5Kj_Bj;IjiP;1m6V!kdSwoiEO23i6Y^W;-d|pSWGGel7u00Y6;a*+PpQpBwi
    zr^eH49^wFeU%@4$E-TKLuf`s&KJ`
    z1egI=`n^P`ZwJhaqMyKl3A&&7znqHnCo2iZB7Y^JTC^nk?0SV%Hme}aS;sYI)st*$U&qqD_
    zi(6(3wsDZyh||(w;RAztxB0P$U8He2B3sD*kYGg0T=wVKWZ{!}pvP&54_-JS&5=Wd
    z94d~bcpOTOyK~VnOl(qHikI8TRMEO>c6)1kr#ewQvXCf=Lf5Wq=nN)NV@qdaYcQF7
    z?brB1xKT`^aXug#ESg>Hq1mNVRKJp6t0N@^V(PxnOH$HtrFP}Y)xpNDke33W_fUCL
    zlcdmETqAmEUP~J$o$c$|nxHcnYV2%T8T3*)1oojob3;ckG+okc;JZ?xsH?rJp;fEP
    zp+(I>Uwd27*9bwuCSUE9S1qXUwS;^PK1q6#PK(xe1ZOU2SQ{J_8tt3m>kkPbUropt
    z(t<~ojGpfE(E4D=qPF&I*GykiaBaJjw;~K`j=-~JT$d{e7C&qEDmU^H($J$`21LZm4llt1nm6HPANpaFM4$??t
    zhk8b%h!k$gSI4PsSjR{t#@V^X;wl^}t|4f_G@Ihghoysd1GB`ihe!#tNu7!EKHO3*fXL
    zML&=GP;LyDW?SGiaB<*&JW(byTL}hwj4AImOphZSV|mIv2$m2Y?=a#mhBIO@K5_41
    zkau5G^;uP(`nYO{pJ>kBK1-AbhWG|T}`MNr|!gWOU(pPs_v?}R7_NVshI{5OgCAT%q>?>%uQFD
    z=8jb#9H@DcKb5cvp@4M9n`*~RcB#t@bSh=55@PQ|*Lr3FJQYPD39P_5AVTGaMgQPD1z_lmF7s-Wrr%)NJaL+7`1cjlZk
    zXU_SbGjr$8$Iocz_G{karJkI}iKrK4JpN5o4Ru$4BA4BtkAE%~Vy*v7yL~7M@}L!?
    z@^~(nV|IHpwW8gdeW^?~gJ(c&c{Q>9(#n&a$HuaPCrWpJSfG`54b`%=*`*Nb@Wiy7
    zh%tlZ-&mq;br{ojBB6DkAnIwmA3YvtcB-b?q4;%|m?WF^!RcFEOM<#Jw(UgIh?Yz7
    z9o;;r<%G*uDZBJhNu8eUUbCZ<_11lkaTW5f`U0P6c6wTEb!PAYCSya}R@<|mg8#wN
    zuX}sjPq8uQ(KR8>Y_l?$4Ms-_2DH7E&^m>AoiE2+lPyt&I(;R76*W|MU6t=f%@NDt
    z-ZGit8XZ_sg>j3ov6Jz(o+sGg{J9<1jSiGZ~J%x9VV#1HETBx2J#a>L*;eADiXGf9g3;xDt
    zwbetMxqD=~n5G3}rC6ztyehlJGVPR1=ocLH-j_-VL#u?%;2YA2E=c$;=p8F#I+Wc{
    z70dGIw1MkEYac#3Si?L|Ophp*_eEz6DP(f~=wL4XS9EkUjFrJLU&^D=g@KD-^!BbJ
    zQ=w7!2|O@^=Rj(CHMuhQCK^>XT)`9QEVUBA40b7z#7_T}!@R8tFNO#3GghH!A(l}N|jM&u*R1B86^8FsTe3$YAS*yU|wv3)y@>9
    z4$ll`fd}6%nqUJXUvQYE>EZ&Ur92z6c0q0oPj}#73PxmU?UMGBOFBH~5ruXTfmeG^
    zFkrEdUydef>uoCS@mzqF)akiUVb)Do8g7%#vB~N4ts%kL)&L;T{)KXX%_GN$$Y*0)
    zC*<@a`Uq;aVbn{g3{cgo>c*0d4R?B*y#Cs?jZOZBl7>4eLslF4;&sSp%Y81|9rLk{l0Y{VewhKw@7>u;P_zp>FzwO}s*W3SxVf7kMV
    zfd5A*p91BZ!3m%~iJArpH|iaze$;iS&8QEeK7)D)^$6-Ks6D8^MO_N8+EDkQ{ywH_
    z>wxFuNAdJlc_@V_7M{kVI~vtc#oU%6Q-{w87}OGD2t3=!fRKozog$v1AJAe#-Tu^%
    zyJ7(O_5d>VS(Xnq+6tud+$-V7(s4^A&GXd!G<*t;Aus#TJ?M_h`r%k0fY_TS%lE=
    zeTpC&zC(cD@GZwKVfegp{5%EB4c{=l@(f?AdR|q}cLp$Ja{``P0JGV?WhmbgtvTJw
    zomkRz%v=%auNY}zs(@u*MJAm=U@WIc!2w1*%pD!(a@LX$N+r8Gf|CEqa&S7`yv
    z4v)wWjH4tM8U3y#31}bB03Ed=WQZFGFot?rl&FV$6DTn%+?z;RCJae*!6I(h*)xd}
    zMM%gHlgU*)WX|HNBV&skt&x~yD@-bH#_GlJqu{7qXkl1Vv@ef1YBJMsD>drx;a9T`I6nVfs
    zG%&YMZ06B1m|X0bjKNOYc@)G$EQm{Nh~bh-t@+kpsU1t|8_NZZMnTAko4Kn+Q4Zr5
    zU?*;|DY*`!3l%{S0+;8|%9u9I>6MdMMAMy~$vja(Q$v$@EmMS#3j5^MV%7<`yX>J$
    z#I2;w3FloUi6t~Eq-H5;z4WXz%`y_p>4?LO0B3~)=Ox|b{JV|8k#U(bV}Onh!b)jo
    zjvI74xzSsyk@j(~k*X7%mk(#&oK?lkL27{g4sxUU9W?DlP|YkvEX5+pZ!!;xRchc*
    zMx|;s^0iR~I<#CHwKlMt5~DfREGjS+_q0AeOO3;e>cew6Qy>}Lht#bXK4i)AFhpb!hN{R}Eh4Vy&G~OI@VB*c-7F*0DR7^#$X3l}YP4
    zlD7X5meeatu2|W3r;~jf?7_+)4SSKOk-NuC&a_@Yp%=(TM+~N}Q4y(;AB>q4=qqMu
    zDOQcrAy(xYq__
    zc{*&l&+bGZeLvo$W<0r}W}D4jjCV_s1^s_H&La6)*JSC9DEIPk(I&^S=Lt=Vh4}(?TObld#SaPc(@OtgT&5ALIgye%7$7?
    z=eJm=EW9B#u#5fU?d%br6&_;uw<)`AmN}fm^KHTz!s0||l}c-)rS&@w&fitv!9+L9
    zBC&^FcNBh)TzRT=%@&VZe;ePPMR$hG0Yw98-O%_+VgB1g=-U(NfjHSYmu
    zUVDYVFMgn4G{oc1!jy$ssevcRIw{49Cqu2L=$=^Ytj-ZiJk296Ii89i+8CTvuVM=!
    zZ98q&)au1P3B>a8a_gc%8lf-h_Jl|`b%rNjnBOso!wM95`P&u;Ka^y#jBR{71
    z9MFD3e{q_J=zXX8Q@ZFh4^y|({24vxH0^iX&*?btP?*0!m);430!M6QjylL701@Uq
    ztIT=RGN;Qi$5QsbICed(7!1{-o5XV@+UWzQ_X`ro?cVbwULf%zdJLw$ef^SWw}-t%
    z=N+S8cE}hXLH?B@TZ$scCmiWtlXxYhL_rP2|CMGx&zr!T0{`9q_~8;=YSkNj`ISGq?$7Z=>mH_-XtjdeZ;{=&Gwc(n<4%i!~Y;hPD%^>
    z(c0htV=4@yZyK|QSMuAW-LGkN+GRU4_}k+hY@q$FJjU-w
    zj?B9x{zM(@2l89|ne@f5uvvBfhXT4Q)O(L#*v;0UzmRwzgW!?Uah}8n?9G0DJ#j!j
    zus(A>WJTCtKO%8~J`2fyY{|k(mi^e0{e)#*O7^dig=zSfi}FnxC>KsHf{8mtQ0|_*
    zUbQYtEjz=eRb;2go3fKs?^9WkeNeUjCcn-u2=J&&q|X38`e{7G|MGFk`W=42Wqll8
    zFQg|!(k=ea5hdy)`geMO_@gRIq0J7kf=@WE*6*5xoa$kCc^hM$sgso1OJ3aV0=Tn`1r)%u=cm*
    zW$W8=pzCT@?-2!;rtR$GJ!#5&$8`9pX`#f#thzSdPQNHk
    zxWGdCG5#)Xr-0}c3}-aJ>?gP6j!cApHa1$@r4A}F^2fR9G9`b7yf1%jtWNWCqG(>;
    zRCy-*+C(32Mt))MSbTrB*U84)ZGcH3}aW&{|%B>Cd}
    zG#&feJM)L@>yqW?^W7Pb@}3ha7?D$T>xTM`)vFncCzEAC=@|V;vb?P{ZR-7lLvm>J
    zqvEX_7Hp_txu^e=Gk2?>InNH3hfCf1>A~`BX_{V~A}^Ju>kTO~Zb4=WXB5kDtT~9=
    zaM4E8^~Qo)T~56vQO;VJAh#|YB-bw-AondylRFpomvOfY(sQQDw--**Zj_>|Oq(VT
    zl#kSNrpvp_3biTnXxVgmq-=ovqAXi}ivNQ0$#Puz0C`vWL@ghpnOcGTb@?25qP)LM
    zSTqqzW-OX1Z(1}!KD20(+=BmC7G*&XE6vN1iRMh1VD^{G&1_kQ|2^hJ*^d9S<`k{q
    z+Z8jlsd84u2xwVZQLkmo*DJEMLirV*3uKlzTf0Lp$I~w##pqfQFRonbO)0a$;Qqe>4sg8k
    
    diff --git a/src/test/samples/golden/helpedit-debug.app b/src/test/samples/golden/helpedit-debug.app
    index 4aa90392d22e86ec4f209256e84e11d2e2629929..5973dd510c5876fc6a861aaba5d93032819bebc0 100644
    GIT binary patch
    delta 3705
    zcma)8dr(x@8UN1iURYqkML`J1|-n5GK>
    zB4i~%g6#zV(Q?z@bv3cb~moKa>gH!P-Y$3WBMw#$yt
    zUNUMMuxYB^Z^=pI-R>41lDNKWXvg(SmZe%nMYp@1hfWY&rSVsEMuB#$$1QmWWEf?)
    zMn<|$(l!;h96?CTD~tjZgrd-ZWIimF+WqV4BYKF}c|N%!f0x6NZq^vTK^%kT~TNu`QCtDjYj39tQEI;U`@
    zGB(sD?Qi9CkwZINFil<
    zhFZwLT#*4iq-Y_7Il0D->mwtpLdU3{6~@1DgH7r7UMU(_wYFM+I(kO%DDpNzXg<}N
    z`~HFEmBs<7msDXqhl>^At{S_{4Gjc0c1jh3I!-47(bfz`V0_1s6NXYvy?^}$g)~pe
    z<6e|`t3n3|Q8V(wnnyO_As{6?0!vM9#G`Du3DqL9vOBVGzmu5`a};BRHRA#@
    zFw5aGoccZHqxabBmIkmq6~_T=*&yKr4=?0=m%k|rK
    zXphx3R5rpfy&*1bYB`>Pmk{>C80b=OZS%%bZ=*L*SL3hS>4mHM(YT3|aB88t(dYI1
    zmw1~>8XDjRBA&PE=i+9w&-4Vx#0Ayp`%R>iNCGJ?0vhnv`2F?k;4$=O!ZZc8tgC7C
    zuG`!I6Y$h;L5kMbIkKWGs50B8Kkdj*{}3-fsg3DAT))2A?_C`5dSM(QH|wVzRg)jW
    z73x4WW~F)?7y0WOy>JjyeF_C<^wRjAwO3J|kC5@`z=w1MrSBu1L()*I3h4!;gGh&w
    zK0q2kI*)V_X&C7y5@0F{5}lKekY*sAeMpf
    zfoK4pWEUY2yFok)N*wf|PLEtL5PLxMg7{g4uSfXLQR_DJSwQTiRZX82#6D0Q&}J9=
    zLD_#
    zO`ieM5Wz3W6HX1)9+|BkaRGR?bnul7g68ULvl?>R{sx*KF8FJXKQ22dVsH-v>8rT1Cs#?Xh@CTYVFQuKqOZ^}j*m!^*xt86B(p&|6cY&_
    z=Yq9BGZT|olvGY($a;L4S_I-QTJ8)cB2+4KTFn;Yc}Qb)$UX&z&N7(O26D-VMnooA
    zY!g}71@q7+0ot{SY;r}nN)ClWh0bN>(VHSw&9b3RtC-4Ud&S+17lO!RA~=oVxlyA*
    z_XngC)p(?T@ya~NL^;yx0g
    zW6Inj<}qBx+=;kl`w8(Q;%yTTV3a|+As!qPrjkb^?rIhD%}_m}gz?4F=R#R&)L+Du
    zX`>Cbgd19FHq`8=oAE@vh9iYp%H)ysh=&<>NUY^yPEe>3mS$$*5tR|n3Pu-hyGN{M
    ze6k#dwWDblfT#|q{pcuj5{R0yx#VNXYZ?CvI5yn-gSu=m>w-6gyua~g!@_Jd3*!s7
    z&mT#6GnvxV)>B4vnGF`Hx%g?J9d2gLQ>J6y<3VgCPFN+Yh%B0kqs#yo&BgYy!Og_r
    zb_Cfjv6Ikidzd^QG5GmaQ$Rd1RT2bwAf6n{FGPr+hKJ50+9GvpXIeadYdxZq
    z@gK`_KVx#lZiYZOOY+clGoBg5PvBU@B1G&ZCAwi_@pI%6dl?@uL7r#ItTFr-BK$t4
    z%pSu($oLAW|8k^!hZtVjcRpQImRDq1=0Mq)Pa5%GWy)MSakxB8Ds*Rq1sC8_?v+R2
    zb;g|%{tYH?Jo-9##9R6UIjdvsQ0m{fsNP>6=--z!FRK5Oymo~>q4Rep|
    VkMaQqX^&57#R|2ffU0CZ{{ziTGJF64
    
    delta 3572
    zcmZuz4Nz3q6+U<2!ScTdDk{P%NFx{l6~_{PP+3IVIG5cn;t%4Yt3WUk7Jrh)SvRmz
    zvTV?~F;%HfG@Yi6t)nG$OxqYLj!ffET03dRB%RUdG|$%bnrUK;n$UCZdrL^CZ|3Z~
    z_ndRTbIy0qy>Gv$@2Pi|*Eot_0e~TV9>F(9VJO{60MX`r3BKvM8E3pFo8#kA=#RQ`
    z6k1Qu9qT^lWLCCst7|-|xqcay?dNQrw`&K&Z{!ic(aL=vm$1s-43^IpRH9SJVQufX
    za(($XhHJ0^*97}hIu2<7gTZHUM(cQ34S7h}8*Yuh8=qb9ec<<)JK(Kbq6rujRyv+136U1Gz(T=6>{t;<1YPArYYEr?qq#g
    zOw{Huzo^z~Et4LIK5ZpCFH&Okw7-9)K!{r9%=HT*KDH!fKk7rZ){x5k)pD^scEYG9
    zP}iB6tFI@U!#b#yiyy~kk1ZwjtXzEtzE_9(h{kGt%2(o4Y^nd&y`iBd;EIige_#b%
    z{~M~>&!t!EmvB&L(xvy~q{e6fuJTkYwKzUny|f)(uUKhFp;_pw|~9g
    z1fkjvSExieP?T!DMS<6-lKI+HP?)
    zBS$WMoPl&}DYm6}*V?#rhLyq{`BKq{UAk#S=fH}P;~MTlCn}Mz&OzO8m`~q{1qcqc
    zRSr6CVwMng+^pga-&Y80L?AAG;u2$tzQ9<340PU8&(
    zLLq|c>OH=SEv=iKZBFl|Mqit^wW4)1WD8IHEbSfiFBFI3JD5*oCoE$7M14YO`dQSf
    zl^eG0a6Qr3vcd;D#mR&TdE>C**COmIBcRK@^;s#Es#pi`hXkLnjgQ6>`d)0r?{vLuCA^29b_)(6b
    zOh6Ai$~qJ;N)t)|<$08!qZ~&$iEdqGQpcB|Y6njZ&<_Y&E(MCj6zKud<_m{sl{R;54;DPA5{*bK!_h83xX;=r(C
    z6flcI=^0j~ftC&|9_HD#(O@?QV?ex_mEt!h8K%~ljf?nzQNN32`qo+kX;l3=IG_JiYP_;A0h9AGn+hShochdLqU*f%}#5ci?&ljmw-i
    zkiREja*=-kz6RtOkZ*&Q1@)HzV$tTInEe#Qf{sw;91Zf1;L5;elV^eXjm11ic~C2t
    zIg>!1i=gs&kmsq~;VdoyU!}6RsH#PnOVEv5OWa^Sm(?U2?3faMx#gchvje4f1-hev
    zu>T6$B&wD?r2jpNgv~-!qcY%@SAkDf0X|SkP&<8Su%VjlBXBiio^E*!_$1|bosgF~
    z(?EVqnNCq1cq8Jb1MgC9x4`uYvgV8d`A^{6LEgrs^Hh(2O6m6?QU6xiP?vtUnZ&8A
    zvH!1$0#ceSb4RT-TV^u0%x94Vz5rgO61e+q0$&233-W%1!3Ut_K)sf$b`5tbny7h5
    zd|+N>l=0WN_X!|}U_5|oJh#*sPi7n)W0;I((#oj$kgPa0hz1$YVn3&DOo&*L*!~kB
    zli7f3iWG+U*cWUUY-Q5MVw7_#L)FvEoW&qV(eRm6L`q|}DDbN-#`aHVbbu}c!(d~W
    zEgIC24-Ux;TCq)z$6YYadK%!ufSf>~=rYNqREX$_%s6RT%GIC^eF8F@sqK|^#*08s
    zW-^D_EZ~gAR>?Kc^O!b8^^cYZ_JgPF+V{Xt@#0~R)h+WG->ld!U`AJ#Ig>$7W3CW&
    z{G*I72U$cW3`~_kViqxVB;B%t@kE8SB%=9&sqxDn8dbO@
    zk2qB^x@SAw(#?35YK9fVWfy|1G0R>#tQ-q+6%`YyB_AnY&G>cTxZ!KR!%N-&@Zu+8
    z))~ZjOk%u|f;ZAq>e>dXsDwqsVrMRXQD}yf8FRD(ta}v5CeoNLlZt7@lcdq6ft~uI
    zWn^-XVscNh-O@+k_5Nq?1?Up|iyK~m`ews5Urgv)oE{zq6*9!~TW8wq+T_)m-
    z3YkZg;_pV$gufl)P*I_!KUrKTny%f&OXsTirl`R3?o?4wJUz3a0ARrUUr}CN<18oy
    Jh^IX8`7bv|3c&yX
    
    diff --git a/src/test/samples/golden/helpedit-release.app b/src/test/samples/golden/helpedit-release.app
    index bcb95f31aaee76aceebebc0bb9a6c9f0a59a237b..6e10df530e68f4fdbbabab15b31818d1b8d6ef2a 100644
    GIT binary patch
    delta 2712
    zcmZuze{56N6+Z9TFL7)KoF)NA9LGsr0ys2K2%FFtq>UkHqI$85laPiqfd>vXP3DBI
    zW1A94Tw=K%pu4Tz82zEP(zJ>{ScV#@qK%ZTY5xGVtF&rp8fatNi^eofq8$sR-goYM
    zX>hkIo%infdC&LVd!PO1j!T`*f_0$A0b0-0Yso{c05#cxB?upPH13$rZ|4W!nUT
    zvJuzVv`aT1{oD#oCyHtF)ivad^9K&fk&r&87$8V!|>PAIHpQJ$tU4?YYPcvBbAH!>60?-x24W{)RWK
    z#LNssX;9HnTQ)MWU5}Y(Y)?9gRQ5hE{^Ie}>uK%uMHD9F&cS%=klQm@^`-S&f5v
    z%#Lz`HRA*x9H?@_rjR**JD=~$USvMw=0BidQ*y?w4bxrwdc?EE>qaiXc9^nTnQJ}q
    zSERR_=VV+(-25RX9rmu8CoP2!gl-;_AzW&TZv?7!&n%QlOu=H8idu1h{~erso+^1A
    z>5H11;WIk3=eT0bTw_O`Ka5MjK`s=Qk-Aw5-`p_57JRLXzJL8&BAwIW2E#G)It)NL
    z*NRhr+8W}NtAAGt^u*15mKSH`Pt=z>m{GrUOxy^D4$ZM1Evkg4~5LPd$CTLpugi
    zN20?~<52(5u#wu4IzsKD*1btni-A|{b&o5P;+p$m<$Y1-*-|l&Dl+rH)6c}7>>q3&
    zqKjhCQ?;rMci=^oy?#&W7NhUzp%!DvNcHz7`;Qs)iMZfdwj8IHdWI54GP%tRTK`#O72{=9yEv2XSeFA?NB}=hD0&*VSHz8LcF+_DjehPUB@*3nV$TZ{{$`SmP3EL2ye|pXJO%&!j3{6R31F5
    zk{j>dvkruLY0M>`X=&6-={ao)X-nyZ;^HW2@KMnTbne{#T9>Bvxmx%a|$rI-V$pXq#&$%G1#fK
    z{0@qGpw;oWj(xHtFLxxKsP0->!1w}ZUx4;~8MOGUdWFlzbHDHhY64apT8R3z2V{2&
    zsZL%8>uDa*83Y6A7E
    zBoM8hf<^Q)Q&@mjW
    z9ce%jpJWa^G$~H1-LSyqItucgV6Zm*4}z|fyj0&QMG{|vFF|x!K};jT7d);PTGt~J
    zAW;tx+!8`8kG@4kZTW}?Jz$&ks)9lrkYX?6#uY)H&*JJKxpbjz=3plNz#VfP`
    zZPUFssajIsL{|t<`8e>`jZ{o1sBIQAwJEoYT9Stz8AuMKCY$Q~+T`x~V-DrJ;=Q$v
    z%1Ln>&mW7`;YQ`W=)m)rVgk<(#8kLZ^)3~2;Rm&ctBCrAzai{@qaJ^l`0I$#67PuK
    LT}x#D+|z#nbSC(-
    
    delta 2622
    zcmZuyYfKbZ6h5=du*(1fuK3`wyC8xUD!wq4+A7K_Hqi{bbwvRYaAZL<pVF1`*au
    z){UB*wmwp$HEDm?G%BU>kCv#BzO<=Ht4-U)sA-3kA2v1CS4+QpXQ*s-k~8<-^Zvej
    zX7|kMn-$*D3eU_VL^OdhB@AGC$
    z3-@jIrE#^YUxu>lLUPYg_4&xBxjezqvVAv-q_W7Ak|I##tn&Hl^@3Kt!IjMvBpT$kFS$&0`;s
    zOWB(?*MwrOo|B``!de;b<23s9Dfih)TXDzWz2V^|QUx15*P>D(e+4)vJsoAPlFBE2345Bkx!pwWExzCv1H%c
    z8<)+p^Kys(5;S5;8&>t4UlsOT$1U_g3BGy;^bW&(`nHWIG~8Y`;JJk^DdM?Rt_J3b
    z3{|W%Zd~S4BSc?d1b~5_djkH3y{9LzQ*qt-Gh>NvBF#2Loq=$IYU*l(OIq4CdE32O
    zLu0UAYg^K`iCnB!nInG%|3Y?9>6U`b6~9Q@$LivX$DM;#M6Tbm)wi*+bybkIu@mv>
    zxsx#PYmxTIsMMue-R6c;EvU6M);2f3p-~_EBYwO?#0LXfbMsQIy`;639!SiYFx!6s
    zx$((S4RSBng2CDbZBffs%0<(yklm2aAXgwFMVnUJ+}u)6vr!sDOf|vwwpZ$FgIfKj
    zR{9CbxyTmFK1#^4JhZaQ3Hcdk@eK)2;9lOcesi<7q)pT4DhkhMsfnwd4&;W9Xu+!m
    z7dN*AHEMu)84L&5?!-N7{)GK|B%gugn_=mId;m#@ha0jEqCuJskyzzX2taHtMDpo~2>e*PVvOs>eH=@YB`VxO@;H(d
    zdciHHlAC2YCpeAML~@IbahhbFPBKW&q)tVW=x5+4@hWH%dta3m*wi}y
    z7bFm-7?4D>4i?GSX>(HDB_bP3%PcQZCdDoNRYE1+v1fIp<0WOL9$_%5Ty^$yV@(Y@=r4
    zf4#6OmosmQ04?W|`@l)JoQInb4eYX`Vx_1k=wnA+IV+9C+h04w?_$f7(54c6-M}0R
    z%!n@*@w`z0lb`tyt7SA+mgb&cgkkJPz@5hhK|vlVzW4B1nHRXw>xd$98VkFf6Q0@=
    z=;(K6b>t&SvY-?^6iG3ZHFJdG0&?@Ogo8EjaJ45b0p0~v+6|(I7Lg{(t#rq|;
    z1MgaP3hxc<2HxFlY(b%=e+*k(FkQZj!K`Fk3i30`^N76WFI1_&!h38Q5&m4o^Dh^L
    B(Eb1b
    
    diff --git a/src/test/samples/golden/iconed-debug.app b/src/test/samples/golden/iconed-debug.app
    index e6272010290e4619624f5685004f40fdadb944b1..cf023e8117e99c675cf3188196a42f140765a403 100644
    GIT binary patch
    delta 5606
    zcmai2dw5mVm0x@3p1gsCM<6e9^C%z$2`?>BB!L8iPW?9b_9jFGn&5?mMv{imTGZZ<
    z5Fry|z%y;Fk&4sNaqQR*rc9Y{+LC^>Qrqg(I@OA{q}Io!(r%rqRZug(wa&RQPXCzm
    zWuJTYZ$0*U?X~xp7Y?bDhgGP0bIs!8L^Ouy3j7)jh4I`ZqJTf&fM0&f(!Jjj|9d!z
    z=tvsTK@67Xry}iWs6=%g-51Vd*u4nzw)Zy7SZ+YR!lwTu)0-~>s&fzM8HKfhO>ynX
    z_KtMU?zx!R{}mSM83}|#?yxS+HUl+XreE|nzXLhrzNR;Z>*@w-4x0WSu)6yA@30sf
    z+IthGygC7h*M2ZI*7rD1^Ds3~!emnyUj#ybG6a~`j8KIJepo){_9U&;WgobwlV<68
    zAG*&cRXGiCZ;M+LD2r{s0dk*tpPkI?A7DNGh3x;Px^R58z*zmW{_15=$$^10w*>0r
    zg&rOL6~Qf-!F10~Vgc`q1^8h`%>v8H;+NhX8{5)>KPG(B>*D`_kIjBo#RLn^D>;Fftr^$ndUEvh61-LZF*sfClVifClS-WcO?otkDRJ^
    zJJJp=Xh|k2fW!|lKEU8OZEtGayS}aC=1^xSx~pYxXS8E|$IVpl7N;+BbPD`+Z%99=
    zhTZe&Yt<8ONyf_Qm!L%?Z~DS5;m@_S*YBm1ZhJ=loK4sR-+=@i>OX
    z$Ve#dh&Hyiw(X$LVKq-pXUCo$jeDazZf>V(*!6obQrsPx#mO2hO9Sp5nF|U|qxlIp
    z-2HUhP5WA->pP-RngY%H+$S>|W?zRd@X&U66^ia%*V?u>O83LnZV;Yvt0xU^dmHox
    zP&o+;G-Esp>i00tVT8fefN?Lz{TTm*@t+tY80Rrw#<+lS8H3=8gTedcCm822veSka
    zPJSqJ8I!%|{cNJY^S?{+YlCEK$!;WjE!k$W17sf}^M`DE+AYWr<~&XIS7e_d``=`L
    zP4-!`&yjuJU7a6{o#S-rAap22#YdDHQeZZG_tRd
    zDbP4v?5i}WSR8*lXDa)eOciOAwg7PfH0hC%5dkg;kVLwxt4n}C#smp)_&u4|1#lSf
    zSS|{X44}*7_>%xB04hC>KMRlwz+cx2kj5e&#~T8qGY84KZwin>I)+sQ`5!?t*?155
    zivW|@Ru6bffXM)Sj<*GvLaMTo-G4`>S)@EO|0>gLQeC3|U71dWeyqss?+Fl_27Nwq
    zNs#Hx;;FbSzzpW_fcFK+K|o;xEBQcxnGmV06yQSvW|6M+fiVGcNgqFcTsTN^((*`;
    zj*imK7?dbM@}YIq)2an10L$2j$Rr6+h-F4SARxeOh>Un5jsQjA@KPPIY0BUzK$WB`
    zo1ttb%8MGaQ5`|@Y7IcWN+$IwvXkyEo)yR=JK4QdSQN`vDQ^v>QpCrU`0LiG5gSw{Bsz-KASw|sRfeXLEfMw^
    zV9%lC17;yHbrw@Am|Q00nF;a~vU3EP$LGGjkTO3PePLxkqijUkI$;TZ
    zJR>5m#Jna~VeCA!s6bN&(3a12+=N1)iL!Y<1Rrp8A
    zglgTROp9=|DKttnXxpxw5>DKSRY9k8D+Q5rd@|+pYA}u4X;mnjYzMpPl!2Z`LhV$8
    zsT9f}yH7cvpr^RZ?H9$MIk0gqyLcdCZ&l{BRNQUK-tKmml*PWR%z(08Y7p%cGAMM^
    z?E&SrU(|p}C)=ao;Z81yUNtD&1ToCfh+@Vn<~zRmZ$u{=1VlNW%X$uSGay?~(dJAP|XCvR@b3
    zdCWIi
    ze(0L6H%
    z$z;E+!e7RP!+uAZFxkh@&MCYy30r1Zg?|P~`(3UjdaY2#@eLUx{D1v`h}|IfJLr;z=5!suV9@Eg3hp7f1<$~XS$n1axYvT-tT
    zr;%AWH&bEzJ*P~A81NfqpO;Y1BYRFcOD2XAI$w~Yzz+6e|DrO*xa9cujV3Jky}#m1
    zo&^#6sxqBq5P$ocvT@nr2=|4=LVr}w(*Lv2>mOg}qB7{*RyoV4$yrWKPx8G<2e{9@
    z!P$jVQVee@gPs2um8Zz}>^)^x%0|DWV)nAK?<+gTv6I8@Dh|KoqAFBCmb7yd?P?wZdU>sRVU9h2w7kxHKscScemXJ*zCr{^EMf+NYGu5VO
    zuk)p9XBGD*Z;douK8C0n@eDjAm%5>xj+O<}Rs&JPpIR+2(6!
    zbqqnjMuLvxBEV-7LG#&nPEQ8uv^S7}q*5*pGr3pLAv|Lu~Cwx2R
    zNX+~$Q7PisP319dy2Pr539GJ@4`hFlKJQd{jlW`o0n>4n(9YVA0=z^UT$PsT#L0mT
    zu$(8TAcKHht(|rJZC9{qO~_``lNp5TDsCMsbqv@`5qpg`nPSEo?aZN`gAuz&Ye7IvRIlz_vU7pL$mqNF|nZ`Rx7Zsp@QCh)D=>k%G%
    zqc$UAN|Uxbw7p5g2b?0MjoG;FMfr&GX1MI_)O^Nytv5t=mwRD;aSXN8EVYzGSpl4B
    z(py@z9M5+51ov#KHY>>P(Kyf0cG`4a66FOb981Bm*gi3V)m)V)f=&$up%NeKr8#zL
    z(<_$l)AkG6eo-u)nK1R1kD2-<*j5(X|Nm;Wx9PzY;^uz)M0+B(JCU;a3wgRu^Ah&
    zCrl1Ec8!Attq24+L5SFUv^geAeqGypwH?y-K6Y9zU0#BYvDb+?QuGb&ZRi^OXg8z0
    zAa=h-4CRtn0xZ~=jYHtL<|~_nE1P8N0Y6*+I3d52$bNG|@*?(Mw3#ZlJffXi@im>4
    zj@WN=3=BN`PGXP!8wW$B)9N5zm8<1)bwZ;;osH;@KJLMfY3Ex1sx{0Ua!&CFTIqG@
    zp%Hu1eYO0?^i-;D-LnV5b&J$1ZpFGq+4=eSpD6}E)irA)8{7xd@*NKN_uV7w78UGRg>8!CnV;&q&7qpL
    KME-wj>3;!C`r?=X
    
    delta 5462
    zcmai2d3;pYbw2k>PZ}+Ngv6o+X@taL77K$nz>5SDi??28JYqK<;Q@h#QIK#QZzF_c
    zqX<9Yz@E@VRkde(xX{8T@jAerujX`~U3;wipC(%cPcGT>!BuL>;qhvkx^W4Z
    zx@rSE`vN9$+aEmEjt0nd^o?rY?-2EOJcyZmC0`8=b{@CJ3}9-&ZONuCQ3*oVSO|2yIjVh@rFFj;xajUonWyQz`%=nyoqEB2
    zC#Bk11@~6ErKuJ1)$1U4`$g+ywy)<&>wld|+#o0xztdm*EMnW=(S(_Gz4OQ#ygk<4
    zGMT!*Wx?*+z2SzhAyJ!iOhS1oyK8s#*Y2|HT}P~^@NO>_Jaxf~>alX`#S9JamvzX^Fi5`cjnG&&MUSZ`^yfqtjPeIUJb+&l|kS&^8H-_>15Y#8ejlfHBh
    zrO%6>IX5`CnT$lE;aBj$Bz^&C=d1a3iIaF)=Z7oNhsg@B08HYru;g^dzBp=2cJ&?W
    z)*jo8@Rw(7O6)^P;WWwuD+jv5lLWTVmT5ZQDYnZZz{s
    z=LGmG-Tj&Es@W~gTA=p0^;z@sUjZtTcinqm`0h=zRXmf1+mL_@^;MLHU
    z=01~MrZ1$puV+s$ei3^}IN|rLTkqQ57+bV07NfVZ@OAE(5i3jbpc_15LMYa}uyJd1
    zj5dP26oh^5?h!q!J_r3RD4zx8+d+wAd
    z@neh?2v!HievF?64u5^*)7f#my!TwpCA!&vFUK#J5?2#1AYMouC4PkX+hk7V@=@YG
    z;+KhEAwEX@D)A4Aj}!M3|2y%Ih))o|M*L&qlfd!Q&Q
    z{ltk9w39$7jPh#RD=`
    zq_GhMcwYcby05QKfDZ&nA>G>AD!{3@AgMN0p3G?h91D0X9}18Lpw;8}l>q4gsyvQg
    z3lIR{ubU7c!-{wuX9UQ!93<<0BtRDFI93tlV?na5@g8tifDzVK5BMJeMgs6Tej~sr
    zQdL#feU@nsDbLJLWSUE=RrG%<)6vk66)pQ`0>ty6&qqENB;T@lD!ve4jOFlv-wH4m
    z0fh}#$?pUh2a&2O0e&yQc+yoqa87^%(gzP76pk+iC?q{FFhCnNLCJYRilBAC(|SRG
    zVz3PMiOfX-CSaKX5BN%e5{UG9B7YE|6dYcvBdnD{S4WkkC{9)ExFbu7RGNEB+4$5#
    z;&gXWNohP&rGL>Ipa|zEbAvGFsvM$duO=Sty`>Q6DQ!y(gz{)_DseuFn4HnH+sU&a
    zK5aS>;R0nsqN7mtpymKmRcJJEk+A23eGH}TFf++g$6M+wOP(O)u}ShM>^(uo`ACE-
    zl$kDyC%O5hQx;sNOuaI2Y?|Wfif1UEDFWjmfH>P)Z&Lxj+=9_uMLj9ru2ZxeyCp(g
    z={{6ikTq*asL*|~bXM6Mj~}-$cne-^t2xA14sjK^pOsE6yV~PQYPA}{bp&_(9iNdw;keWjGK-q+>+lS`G?(wpcc-_$4w`7$vQ^boM^i*WLUWK>fEa3Gj{BjzhZZ|5kNjMr5nj@MxZ&FT~P2r*WL1*Gx
    z3L^V>wUo`OC&RYT>QFB6-PRqa9P|tl>K4@#pima^J<6$|*KMM>Ner3<8zD84W30Mvn~9
    zirkL!X<3gBoBu23yN1oVe0Emnu=$LN+3|14T*^rHwX#LpmV$chQKnbI{Fv7i9O6Av
    z4{#VBmlJuYuT>VU&m;=f@Ef#)T@j+`EN}wu~M*O5QyNREY?9L+o8|7S1+Ui9m?2^j<
    zTZL@sREpcRMiM`x!VlwSz|SfZCVoy85{2ibU<-U#g-<||zh`UB9>mZ4od4h_=LHr1
    z4a$-a4KvS{qBGH}!=O`1K^Xfl%3LPK9`)<_`&J-LTIwwK{YiQ8A1HIbaJ;C(&%#)K
    zNy+uWGo;TqMkvRWEnU!L|PZhr@!JA6_Gv!<{Jb2Lgb14JtO)uJSDN}|ki*MgMNeh1I
    zulTNKL4@B^rbW~bD1KkIF5>$^a-CCg<;?zH3!T1np%0Zo-%TiI4%ItXQvFGL3(~2!
    zQ=PH-gjbUOkCeflKC229d9Lwi%FL53{JG*U6#rK7IZ2nT`>Sl#wJB9a*qOz|Un=Kn
    z3k98PD2VvlnhEfEWzG>_9Fn@0TBVS_8bao}hoXfD!a2#Ly&k5tGtYJz
    zPp8)AapF{M?UZ73v{!6tZr0R-c)Hy>_Ar4SXf4B7j<+eq0c|@9&J&z+P-eZD@6X#b
    zddUi~y)(E1)b9^+mWDI7a<}V~Mc^Zb*5IV^%7*9
    zTB#NtV>Km%JXWJvc)YedWlyG3H`&+~Xy=Ci8@obnE+;Nd+A%@P1%yj{J4&se$v$wS
    zh-1G@ifhv$ra>!=_wSh
    z$-wsE>Lk2COV9J+MM-$E7Iq)LISJpQ?cx;vP?VS?yiDIA_az^XB=I`!^#qT7l{S51
    z%4*GPG_Te00auXnXfCdBci6+mo8bbtPTSMUYpx;UJKcw;Rm7uGNGX()ila#yNxha+
    zme&vSjbqx(CEloUETP9V=)x2#Or@Bn
    z#L8h`!eI5bvCw)Xqk>K&Hc5yXEzEO`SRBnDr6c@|
    z4S<1L&n7q6-`Pm0Ou9aZud3_ij`a5$<>@RzEA(*>eoi|}{TtJFEr%RR_62?OCN#_l
    zKkv?-x#sZRnZb<1|918FvQ(wJc|oO`@9xL%O>fx~g|ST3A_}F&{e-XRLi{>Q;n4t|qckw9g*_XP(It
    
    diff --git a/src/test/samples/golden/iconed-release.app b/src/test/samples/golden/iconed-release.app
    index a5c92938d5ba4d1dfdb8bef9497f4bce96b2414b..3d8bde0d531353f83beda4327d8e33d3b653dadd 100644
    GIT binary patch
    delta 2887
    zcmZ`*dr(x@8UOBOFYE#$AP{9)c7X*YNWlkcvCxjHIM}|hPDIp
    zFfbbm%;_W3)Y?qjOed{>SmM-aCzUC!OeSOcNT-=Po#z*l${LK0R`Y^}#z3`NbJE)6rmQGE~!dVqqY6
    z>`rpzYwR?(VDWjyyjqZ^PkTo6h{O$k1V3H;Rc-Y7{nMUtJ@PukwHWUr7$-V$0!NNc
    z5Eu>jS68R5@-Z7n9U!dM{YDuSk$5<;E{Ilnr+>b6RgA^$Ro(Z+mAH-SmXF0B<7%{4
    zly^)xE$-p=qwst3KU`#TWSVgz4z9n+?=vc;7|}l;jqU~{HwML=wzL=yQ;+(bC<|$@
    zU2r?DO8Wt}o@(7#xhLDVG|>kYH*D(C=fFj~9yzyx<Om>QR+pm)}}76h%alc%2&iYnz7;g|CYfy
    zU1_WvLi{eiVz>?RNSoEn_q#k2OJgruiBdVgm%at==+t6s4+doiET?kZMhSa(tV_8B
    z2mb>17UP_>%kUdt1#={NtH$%@fd@i2hNKBYUFH`7w6?_`?1C=P8ac;^KBG8ld7%f7fQ_sGmX^A)4!Y$5n8kDr4|M{8EbcA813;J$4xC}D7&-2#C8NqzUoMC7#Ogr9lIJ#x
    zp=r0+#Y6zl2KUD<^%%wBF0VcU2~cRW!JJYEOg2XUX;J3IR8
    zdc&u@1Kz;N?*4&5xGsE(T0~yrPEAcfz2a!%xH2zpBpy)S5cx@aGgg2iJx@G)+V@m<
    zUrRq-5`9Vbq9(k7Zv*z`L(sK>&cTzlf&M_ayCc*+6rc~q#iXo9Ftya)9}I*-^?`w!
    zzCKz3;*B_QJ!yyXTal8SRn?BzuY#_E{tiltfrbMep-^uZJ%wl{%s}|*u8#gd*Qq|*
    zfLH%CC_!{3=f$fq*w!Edt|hnT9tH;+YDZPx
    zK>xu|Z-0Q!qtsC--WIj%W{!UdeF-4fAwUpx0otE{u7iBAY6X20bRP6Q(9b{%pc|mu
    zpk>fK5TO(e#P8%+pzEMC+k9!tE6JQsblFpg9v8Q=tCaNleEUIF+dwa98KTFLr)7f6
    z60?r1j7ONG_%nJkUx~I+Dt_vCOxpx5TikW*lbl`b$*q%Iju^|`ALXv*?vk8CD9%li
    z%N0&%r3~a09nQi{95RRYYYQ^DCmIdh<&%wg81Y$%w^_
    zNZO-fEC#3mDk97Zb^Ld{^7Vvjqo+O{7;K
    zmmm^OQRq*ypEhFFH)m6*S>MkHoQ;#T?XhfVvIG4`!u)*9lrzb9k7Jm~Yw0!Xeg=Rz
    zO^Lzmpaj_!RJGrom9>*PwF>II#yP4Mn58PP7d>wKFeki=r|ZSHN%pDHTu2rFqDeYGI~M&de!sr6^krZZ6vNf9=F;o3|&_%O+UG`~^9LG8vVq
    j?z7$}%EikzyNkc!>$eeA;G@i|)_=rXyPGKb-wFK}KO8mX
    
    delta 2742
    zcmaJ@dr(y86+idFgM125KcV$)ZCNMPRB;v6#Wq!W8Rni
    zz0cxB_2=rQ5{Ha=Cl|bv93GwHNip3^D0piz&g#{2p5hS|(;tX>C!2J?`EJ4$89+FD
    zzc1`sgEA@tT8CP?DCV?w<(kMzGxPpj%f^-mG{M^^v2OU_m-CWNtbN&eu4>f
    z^S3Zfyt5);UWd||pv#=c;ZS@5baPgQmxMOPLW}%Nkc)fmLQ6
    z`Q}OgvA&^$A$nfCx~$k;h=qR&urEIZ?J>Ge^mvSrG29m%=zGqfIq}zJ`FS!w)N2e3
    z>@h}m4h_-gig09Z2%G^nJ~`?H_C6yN3icSg22YY3O%Fj%L0*S^0Fe;GMsQ$Yu$wlZ
    zG?th;LnFh_bO%F5_wgb6J&c=xmL`6dS*k9kh#Q$TC2!#yl98Bu-{6rG14i?(VbF&t
    zyj85sYIo!T8!@s4pAp(UFc>nZ2kurl&WKZ4(@*>j{@(z69l%e(GYa`Jq!4fxd?d`as{#g`4!|4$cjMDLVj(T{Z{sc%p5KszlyVoz9t?m-l5oLpDEt0
    zYRl=AmM31fm)5R;wvy7u^wl)2lDY~BO}-qaMaxgRGUc&=k0Q;nSz86IPNk&q8!IfuM=0wcNN*_hgR}mfZhKbcGrIzH$|a80NPPeQdyTQM3X^~&66M$4W^L3
    zlKhJdW)qzi`J;G6AQGfINn8JavR0F}fx5Jf)ODSEQB#qISAcgYK`m6IkblOiP@BG(
    zTnuivo{Nv8HN5M5(l+tYr9DP2*#g-oE&BTpW^14qg1}9Vx>?vN3rZLzk5>r0Gp<@B
    z8Cs(ZvI$S+e#y!f@q^0xrue>XMW5SbpZJ3lc$T#
    zRag9bq;4feZPQyH8l#z%4BF#u-o-sA?&E$`R5#QrVbPBJ88OvRtK9@$pNKabYD(tT5!K_D
    SEMK0$A>Us%5#f)zy#527^Z};;
    
    diff --git a/src/test/samples/golden/imgview-debug.app b/src/test/samples/golden/imgview-debug.app
    index ab1138902bfc2a3b123298d95e646c281210d40d..78d8e583084d2fe3dd44baab8d6c35dcd6653d07 100644
    GIT binary patch
    delta 2166
    zcmY*b4@^~682|3&U3kFrKmLCpf8Jb%4oz*%%_+%K(A6&Q#RD`Xz#9*J3O%5i)qsHF
    z?e$c4aypHym95!qbL3)cCL1Ml3rk(P$i$j|E@|CbYKrOW_uc!Lbhh8Q=X~Gq`~PqTPu^Z2-q}TbS7C
    z$&{Kq8@;pGtm>y>Z2fv-=w?}8vb(~Q+$h^_8|Bo_wBY_}Ebsg@u4%|Tk&2J`$+;HZ>O&~09+
    z>Ld5f^7XgZ!Nmm&9eW<5Sk=Q_Q`jDH@ALX!;)sc=e(JvHvN`8}Y6XV+IF?wO18c1^OX3vo7nX;T^O
    z;WE3m-w*jp^tbM_6HVm(9e5X`^`XAf&A2Fw!3;TFdNxP6?fnXqx%D@5fHmR(ZcLOp
    zV2Mi~xf_jc2p{8m`t;v1!J$O7TZ?)(Y%CXVCM;+=3fpE1FXvjT{%Z&?*1JqzicfzB
    zgOb>(>Tg*Sz7VRu!=$iO55EYgmGzCv_Zjq`BweVWSam_CpGBz#(FxQi9N
    z`n(wP$YwkOCfOluGF5#Xe8UZ(jr#H*`TqLH%yd|V8x{8ImtjDb!}+-NyR9a6*=q`d
    zD34FyXnAp0s*6K?#bNhl9AyD(I9;KBeTC)jSqO4_v?0IWeFe!-#C@eu^}k{&I7Mz^
    zN@{`?qDSwH)--h2tip@Cdx}Lcu`RnMp2&%ar`G@QNOx2xYg6((W-0e8?@l+
    z;8rRY8A(eNIRW{K^+|0~w-`!#S~?{%lb5B8K*fyQ^zwG^mYTZadO9ZRl4s8?#2a`I
    zvX{q5=V_IV)p=UI7ObfV)a=mcwm6zRZ8~l(mDl^VKwyp5kXu(rBTyWQ6NAZ%r88o@
    zW7>*xtbG_b2^c`~sAy2D2n1@YXbV=eWEz6osw(QWs;zZ21+RV^kRYlY8SyeGi*4df
    zN4E0>e*8=}(tW0OQ)56|9n>_M0Odw8;3!R7hY>hbj;uUd{i;B1y+#L->KkyJ6M5ry
    zZM+SCHYCSk0Y7jA-Yupc-Gd<1+6^a4Y`6<`>+4-isO0DdRm0E57!#O?*-
    zk2;#zZ8aOVn;9E}Kt?(l`D7H3v6c)!84)r*CiRA7oFJo@j6PD+_|JiHk|c>PDw8SK
    z;~=BooOyTR#EOh>NnL4j1_=I1AfF~>3RNm8R5`$hDmU5K`W?S_Fjjiv$@resWHQdu
    zEF@{koF}!#lo{lEpiHH=m1#_GF@73jfJBRNag3oEmR!#QGJZCV(-AgloQahKG|Cg&b0
    zX=d?~NRo?;2c(vm_J^DU*D}f}N=dXl-R#&z66Y}onf}i#NlJP;o#rhXGE?}zl8gZ|
    z&Okwm44WiIoEwttlozO8RU4=cb{D0(;<_8tqq6j>Xw1ryI>aILd&LjvKNE^82Tyk?
    v`ZGd9|EJjF%2DRd#@!XCT-nk&aod&SoRC3uz+8+d&sX9(oJAD--+}%Ebjj{g
    
    delta 2074
    zcmY*Zdr(wW82|3Vg$0%XK?HfNf=r_|>$54w5{TuIqg{4mK@pL~8xPkY78a95(BO9w-h5q+2)yS8_Xk42hphnM@qFDd_Sxo2
    zxqWT++1#vXr(ohP2qn_%ln&C+VQ&uSc#
    z3)pMn8MAV@d1;om81G8oARnVcn|GHT3eWM3-MKqLFPwbMhlh^!a}B0D}-~r6;(e>QT))SX!GGiZ{rHwn2}ve4k(S6;99cN
    z#)gu`MxJiZZ81jN(z^1Vk#e8)5>BB9M$D^cO!EZXLpQ>Syi={kW7eyvlKj@IrAqx%
    zLSY^2h)75)3WR7Y0s)vn&mGbJi#vyAv5zCV7Ay)Qx&oPPi24HH1FCk`xC@(FHrrZl
    zYF&f7Rc$G3*-RO%CUU9#HTV|RAK4|jSw>X8)W@7rISEH$6_FcX*<#<+&|L1OZR}uF
    zN_H|X{Cddln#+Te&1Y^wWN
    zSCd<%I=D;V*v)#QcdY#j{&Nst2=O*}Jixm^3K(W!1E2zpKnJiL_y9Nn90ZO6W572+
    z1%~AX`hjna16vDx))OUiXgA0quT(my99
    zo&Q_wM@W+BCpnJtY$no&#h3jJ%&biRl9XJ*8727RKprD`207(;a-QU4HF!d~^(%_S
    zS;SGUEsXTzq(qT^f@Tv1WlodQA!N?-laLdrSDwk~9r}0E43Oy1&rLJ5!;)uvob>a;
    zn24}hBqxD&f#hVK(ky#slbLJAQMXb5o|I(LFHU3gvQj`xB6CNX{tNzpMIFBiN}!H*
    zQFj9Amq<<(cA<5dluXjEOeZ@3E56-%GLoXnAbE^s`ZZEaLRKR=kvhA|^ofU-KgeE;
    zqbSpFk}^YZ{v~t)
    
    diff --git a/src/test/samples/golden/imgview-release.app b/src/test/samples/golden/imgview-release.app
    index 3b75bcc912289e282ea0f492e9e144b62f651590..c0f1cebd8f4eb347b681477a2ec3da967352f27c 100644
    GIT binary patch
    delta 1980
    zcmYjR4Qv!e6n?wCZLfd)w58DY`pbogEh$(qNrV`3duQ(`P-15qm)H8C1Y$kEtDk(w$EiI5p!WaQKe7I=Sn5y_W^8bxr(kddv-iEkm
    z?{Si8;Rx%5^Ev-|w@cq(n6dhE@zw2+AN2Pp>u5**B
    z&)zl4Hx=xLiwhPS+ksKM>fzB$wnrStT;3NrVv(w!yDLU5wz5ynz|bV8QYK?V^r7o`Hskj<`_a)iS=r7)S>R6hrpBM#ukM41ED
    z+V$BxvDo(T7}wLS-^2um64Cx%GqHVVvpA5ns_!IhofO`{wN|~qA>5;n8oVsG{ssnH
    zZcg|@sQP|`!cyb>BB0hZIVa;Chb1oMG(3Son(>gn)J_kcF%t9|JE>mMW2X%%i%*ri{C0a;Gh;?^HAn>cLksPm&d
    zZhfce#a(IlgeE;<$2A;f9c$R^p(%Z%=^kGVaxm6YJLR~JWGLdeUaxvzG8Am0GC3uS}AK*GQx3&bTyZn2cJx;B?BhaJytNnY(
    zBZ^bjDsmF?728sVq^P)>@|1K=l%%fDnuUrHx$DJ!uH7Boo&b%B?$o>$^>_pCLiXA`
    z=_;+Yx4lXWX#S2CU&nrpZi$nr*-LS2sX5@)e7+j3r?R`7W}$dBL0n0#kS>Y^Y1tc_
    zvGymxS>O*Kbxzc;wfKBpZL}M!nKC{8y=^T4t!+;?W#ZNE1(HNtT5+NbN`*xnNGr9S
    z!OzcNBi$`syLx?EwO`X{A(VT?`LxD@r!fMDnvs=L3vBjv1vEO2R9}PRlBh~Qyz>_P
    zrI1X=0$$(*yq^PC02f$|z+1p^;6vaGU;?-bTnDCsy8s~-1>kq`EpP=`oE%-X;A9%_
    zC*Jx|krHt;H>Vpw9;wk<5ZS{y^NF`8c=dsq)5r5B6eiZuvN{T$Ke%h@aIs`
    zK!aMvn|ZiEKlq!R3rNZDGpt5*(R@PdJaX&#R9Zl?WNlEBjii>+;81+*Dn=z5)P;t<
    zz(_HLB)LJYo0onLNh!3U$mrt97h>CN``@S((FNWB_RUv-WDKzVV&S(nr&y`V*S@*S
    z*X561w1woTyRcW5UKZaKmrKLq9{QuAtfXA}Ozc1(6$jB@6knqMQ_PfhCW{U+{4>YI*zAGJOg6JuVy4kc#vd+QHWfGaozrW^Zt|Ub&hPvE
    zzTcy#?PtyRn(Wq2+nUpaka;}M;7tJuIGRgHx^ZsCn?H9lQ|}eV{#+XTVY31}o%vE^wx0^p_1%%UG$jQrgr4C(UMxOq!^^^`De$
    z%!8QNWM1^7LP#=p2qzv#6Jkp>!Fy)8i4Dn1D-NW0=yEDHHW{pp%;sq0JSZ}m?r^d(
    zxw;@>3)`ud;JTha5!c4<$*m4b6Sk-!NA*)k#tV(@F|uDn|9Z>c^Yig*yqH@!Qh0Nk
    zI`ujbS}_7*H}ayZIhVL>ddwne^)h+HK20l?)92XtX^qNuw6~3!(`!OIT!?%1F>f-*
    z&^g2VcT)W&VMX%YM)FzAHW~Bb#9NP#@5Js{B3MhbceycPJMQr8#|PD)Pjx9JnXnBf
    zZNuDM8|R&(yW>cBdoDHAscPQEvogAG(`>^H)S-WFP%$suIr|Q)P@b1U%&BzARdZ}y
    z*(P0Md5iSIpC1YlK3d^S^)Fdg#)iDNVb{7$nyCAIE8Ch;wq!4C@$wqIhjTVlQn9kP
    zGR!6Q+`PU!B84Wx&3;xpJ-iChqggD@GMTrUCyxd(f`-_635f
    zzsbLkm|1t$dgTlF*RhjXQ7OR8*_)+N=FYA!x&o_+-1Ei($KF0)M}Qn==d;Udm*K+i
    zg6!!9(pJ?y*lSe-s=u$>+jmeUNDg
    zEovap-K%aMI6!Jqv=0~oE&(?HA>miMz21QyvJR}*bzJeS6<=d3EfjPH=}MBiHm_6&N}CcmnZn@GHoEzR9lSqN?Th!X5-;AQVXKqbAP
    zTS1&)Be_Sn{|^3Jh_8nDAea#F9#94k3$Pndfqq~JI0AeKoCD4SSAZn&70`)c#ekE*
    zH>R;S7hlK;@${)Dj4rE;Jsb!0uexd7`D;#LZY
    zdzw#5PKPC5P!0ApLQP~jAx4Z>$bl;&Bg#@99U4Mbr5NrdL@6ah
    z$}+BPQI-=6Ckn{0i54&L;fc$@mlDfRJGK5lUl{%k_Qgag7ZE%-L@i>UDk$v!ubQxX
    zI*a5ISu(ZbKZc4bB2i=SsKS8SX`ZlGh|Vz_xt+EOOKK{mM9LyBTEt?^q!MK>v?(QK
    za-A1|y7^5ZGBzmR!Y-G)#-6T-%VQVK`(>$))vl~1APa5`I{aK!wC$bouv@kXfP(F>l
    zmMLesG&(ipDxt6}zCm#8i-Oon=Un`g3Nko9G4;~{X(F*rx?ifBfJk+i$C74QJbp))
    zMt#VMCO3~?H!+rqzK9j&#kHABR%eL!tsvy)FB`U$HW^!lLC=jQBV}VNR`qpC8cP~o
    zPM$E_S189Fqq3^``fedl=D!-6U2b>WF)pi@$<8Bw#AbBRXfKwuJ3~M;t*6tmtF$b^
    zQici9vRhPxP_vOhoDZ#X#y_u0^RcX2gMFP}$=YVvb%+0uRc~rTc^!O*(LUmBN8XeF
    zQju0QPCjZW)!*WFiGx}gyFZiJ-407?42n5v^oUZOM}0d`7LGwW-vfA6rVsJce@GUq
    zYg+qK(I6;lSZwBL%rae9r;8{acbs+ko}-KcSzKG=(?(m>$9iIHj!LOA;6N>^W8#qP
    z5>pu_8bB(sZ}6Xu4@r%@%+zUEUga;D+N9I`hDj8C@Q=1R9aj^}g7fdP4~%pou6COB
    z^S(z{A~kl_%vb@P@2$5nYmd#f_2Z({KpC^yMI~i8%qbIzX{1IeL7!0q4H6nCVZU9h
    z-cF}G)EVlhTl|g%UP@f~YhSXX%gayY>>0U;SU*z_QmS;0@()F^gs9K-&&>1GH7(zB^D7tmCH@I^^^MGXUzWQ5ZdDwT{?@q
    z(&vfIc~r+DUS%_R*zB>CsMG1$y%0y!k*1Vm3B|CuW2r^~c)g{#G)GSn>028;
    zjosF|@a*KQhlecVmHuqTY{<9_tOL|1@6qn?k-^Y_GvZYG{NacaIuaUS9=9xRl@vxre1|ygaYAcoi;MX77oUu4De7C(LGeFmHQB3%-GOV7qvib%SzrsINf@
    zD07?Zy0y-Wy*~o30)GH`
    z8>S(pI}jM`VaKqWZW9R|@97RJJp(~jgs=WMki&bdJF^Xt)EN0GYo+Z9{`@pG%6)9G
    zcPOA72`LKO4Cf*KwY9D6QOuxWUR32&!jA+7!wNfxQeOdaoj2s4>ADHN5|;Vczz19a
    z`x!6~xS(nS-UQA8?*o^BB(MN10n5M|z)*?_pzq{MU>+#6B=&5ow@NvQ!IDc7udwZs
    vUf@sIs-;mrVyiCvv;AWXw`ai$MxLcjWcdh>ez1?Rl
    
    delta 1490
    zcmYjRZA@EL7(Tb9P@o7C_5}s@fy6Kq_5sEb;|BIZ`LX5FYJst?D{!TBMQU#`I9-sm
    zH8hu-jhtvU66Zb={jtPMv4&(i$yjKdy2T~B7-KRW(L?0Nj7B$@*5};YCA-O!d(Qhj
    z@AIDb?YX_yw6fdf=n{9FA%tY`Jc+ji2*B|oLUOYEHoW<>m~XtdWY-tW&?Dvmcs_sb
    zTBlrFguPQE?s6WM*mVSBS4)!1-g)hlT5fRGIrU4U;GEeY)Cqf>5UHXyHfNRC{{IYV
    zB7mI4TycEbLP$FC26jA8()6XI>U?0Chv>LzZRKcACo>tjiMeQ1Y}ux!cv5UG$L*mr
    zrmbr<@lBVcYTP&QH}X2@Z>Dy)BxzziYbUYOD8>((=+9PPK>y77Zzhvm;D@=5tpqiv
    zB@e3yp{_$h;%0GtJLj~^)(13ZZZeS-`h|IeDQA`bU~aY?KzrS^I;Uo=$A`QZ9`Gh@
    zS)a4Ke~-kT)1xSUI9q%fvrQ+$*zwkrse@RZ(4w_OeU}F{@uWNO3Vx_yiNplMQWGcA
    z;slqgDc&i%JBfmKR*k97Z{=M)WkUBIsyMM%aaloDn^$Sd
    z(k(2|;#^kx(|z59k5(5;>?>N3*I4{E;;PrG>gPW1p!@SG9zTLu(q6;*I44@u76*Mj
    zuX=qG4>#1Xop|@CDLzIId*VL*D6eU3>0TxiB$7Ujz^~{)V&5YayISU9-(ca(j*HD<
    z(`8%$iOuMe5>5VVmS5SPLq6|(5Z4PM4a_2tb(%C}zuyWH)f;!KjkBQ`M`f+}I+m2j9?!e|4vOWj+fPB4vrFA$mrSeMxJ!-EJ>gjv-dZB^du-C5%mJ{+BZyS$8Y~Ki{19SL4-sQb+(eMo=e@T?PxADh^FGh}
    zob$f-++IAqdf3t2X+Q8bV=RlY4^J78fSV)n9j%J3SkzZ2DV8=m
    z9Xw^YXPca~kIAa$>%WaWng3#Fce-3j`?#!LAv?GD0h`f5H%Oc(JpcuW>+
    z>stFV@en9#SYqaJ%yL~hYj;o1f!TGoOheo}Kt6sBy
    z-uLKAWMb#djFr;)-gpzUuEd;Y5ErEZ%7oP>YAC~Q&X`C{BQr(``iv52kkCL02W?_)
    zHJj~LXQ-bp@f#L+DM{te1L^J_A3sx2H+mVdL8cz1Ud#TUk?t1Lny%a>&SBD-d#mCl
    zeL)vO7N<1{Sh$b>h~wF4TgJYGVp!6?)FJzy(HyM2sj$4VKu;0b+nYU2
    ztZH3&a&p$q!-eBDfqcfS$hZP*0MsYn@!m+waCpcObtnUYNK^^8gol`$?=CuMG8CX*
    z-c>X%rT9Y8gVJ?gU3{c`4K7+{|I??OCjudNgw60!@!fkm@D02L+r|G`H!H_S2AY+K
    z5)SkR1E&>si(f9@aTjha^F{nhFxaX@n?fPB2FHawK407*eZ#j{b~O61_lLkW;CG;S
    z(=@E~27|+W>;!hxZKC0meZ3K-Zz#md@YSCL3V5GocfJ9V1|vUXsj*(gpP!~ixsMF@
    zj|7#Lu%fW7a30}bSv*w_V+IZLp(=+Gc_=s>QP>5P`XY!Myt(9T&n@sZuq?p_e&7<=
    zPl0*B36%$U9k>9z4}1osfdya*SO(SshEhxbeJ5W6^T4*kRNa;)i&T&ru6Rx2M(bYb
    gSzc$Ym&Ul)T5tWNg0Xk0=@`wfPDiIzKP|2Q1yzA!(EtDd
    
    delta 1464
    zcmYjQeN0R2_admN3qL*0Guu%BrkVw!_L=Ap}FtOoiAF!hs_D_c)o6n
    z)9#(Zq3ID%B{$3L8jSI)WvMmalKxR07r5Y>{<%?b&2JTU3kO{wX_6RU6lHewUz3^)
    zA||<5nwYf_l1aXT8;_H9<0xx}Ke)zCbi%T!dNi+-SwiyA*>!pdk+^1g_@6oum*+TBp&#YT5c^mWxYm4m|%Il^zd9~v`e#AX0IFTdu8^NVMX$jx#Vl;Z6+DPjhCKEAIIs89;+kT8{DW%=RCpZ@u5OxG80TmU7E~D
    zlbo)ld8MfC91`BzFuS_6i&ycO1=V+G(&Qo8!#>Zsl3~!f_jOurdqNneUYpOdV}qt`
    z-NG_0&1dC5-7!RXYYno@zNAG3jiqN{*LR}zK1D(wZhB3#=)O!7hA-o
    z%Nzii%^Q?5UHNl?k8IBgzi%mo@j^?33ozuArcByzH$p`7B|KWALEu@uEMkcJjERz2
    zUyk`cm?Z{Fgs>sg!CK*5jp5bzQ@HN?WVRbV)V3k(1FytQU0@M7Pin23ln?x
    z!^5Mb4v&riQ^3c-b-<8_DuHl#xQ`q_Y78;)tFh>FeF0VJ8;X!0VSF5F`Scz84$Fp>
    zUbF9UT*g0SFj4oB;r@}Z(i&A1as!E
    r7p(K2RL%&r%TX)T(=kWA&_LgF)H_0zgv{}>v7Fv6x9Bim%J2UHuf;%Y
    
    diff --git a/src/test/samples/golden/inc-commdlg-debug.app b/src/test/samples/golden/inc-commdlg-debug.app
    index dc9a5ea6a984d6e391832cbfed11aa28aa12d4c4..efbfd13aa0ae43b62dd70fe89b30993b3e50c3e6 100644
    GIT binary patch
    delta 1621
    zcmYjSe@t6d6uz&tP@rrqu({B}9M)wM7KUuPELj+?9iY)XTGR4FsQh@NurjbMW=;)g
    z>7;q7gIuSJiqoixKe9g-L@{Qr3A$!~XrfD8Bu0oqy+jOI;^qWd{my$O?j~Q~z2`gU
    zJNKM-@9R$we!Ab~XmnP-%@|8z?7~w9B;Z;eW5)El7Ec=X9Q|3Eo=;{lHkQrU2te^P
    z_GFK{N~PiPL3a_EW$`VHk+1S&ORY2UPj-;NJEP;j?2|?(w@UX()uV7yo#sfwEQ`l(
    z>#k8hVxozSBUep~C8N(_MR9R$=9bkd;{8i7dH4&4&Bb-b24T=+qls`yWXYnQp`b{@
    z=yvf*!#$hixN}%mHC_Kr#L4`3L%rMMi91JR^&-i6#m`7a8;y2hN!b|;qHZmfie08<
    z5tcGUh?YH~0)(200OI^;l`H;bd5T9e4jAlf{8GkN!_M3M$BY_N6UuAh+l=-hUo+w!
    z|CfrisBzL!i>dwwk6Rqn%*g&^I=dZ`)EE?V!sr#nI*47rxd9t$P@SWP
    zWVaYkGtmG-iG7X#YTPf?@)A?4VR3~&Z)%cW=GRQ3;QfEJY@mez%%W7A$9%pNK0$z;rih>O4)Kz;Icw1**8jV?6`j5S!uyxz-&GxC3uNcK!}&hte_F
    z?NCBWu%|uHb5db9_=Vgpcj44BU&yZn0<}uGW?+D=KyfaE&*oN1-|-EWE!95c{s_1X
    zECadgqCurS5a{n@$B<2z2?vjNwuh9?z5!N%TYntL;+>Z5nFhF28Tlzone7rjewsJR
    zeYC%8FrXX>Dhk^OaqFb6CEi@++tP>Knld-63f3v9}s+`YkJxtC~t;ET@E
    zS4uN1-_4fm*m6Bvewe>n)RJRnjzIU}{y=|_UoWz9vvsHREU&ayND*l?mh4G
    zyw7{yx99fao}YF*?Oo#bvxJZg))RP2fB>8*CL}kzZ^M(X#RB8GIs1M-2YSTp0MF;^
    zwq?q>S=cu<;;P_biCsl7cBM4A?3vd-uH^>j98)(N1;@;KVVkhm0g)AuVTmZBu#&nRGkkj^AH_3t*IK#?PMk+H!&C87+bcgDV`LY%XPWw
    zjA_fGn)sSiQZ?=y_zQU*^jA~6OOiA(p0$(M85H9OO$=nKFQ9+s_$QM|F7U(Lz*d5q
    z(~_IjgHYEZA#trF-oQESlI1>)+=KSIX?1SRSg#j(&)w%u
    zTC+aqc>ivRJ*!7i{7AO=GG?1jgt6nTCsT*;c2bMh67_8!)WlP+z>D~yhDs$S7?zqi
    zkrpSoTut##(cLK&ytQgfb$$!);z<*_?@+~weUgiPo~05&$T|BatupTs#;DuuF>P9<
    zDRZ~5Kuhvi+0S=%6Fyp9EU_jadT{}IPZ+q-0V#&%H*3UW7k~Z7v
    zEBV#yns|6u4cmcdubSdxbhE8@>4*7EV@r23nIR(S(+K>64kY$HLa{65ZuT`^yxDQF
    zIc&O&3m~x>T~eyaf6nqN+k4pSnIFRS!bk(N2xOfm4cTwkhluKlyVOQqz^k}fSU30R
    zH%emtS>*>%7V9btjCGq1)(Ubdic>!*;dsDFY#TWAwj_bZj9hGk;k49_Ye~Pc47TsE
    z@N#GV*2joj>hDa>cdFtI{0d0~F|TBrjT!Xp?R-KKnW!TzF5;9#6Bj$Az|*?JAv#`A
    zUe#u#u)RhCCXl?NxBvXf*>&`zf_UBbJVI{4<~XuG1^9rxeLkf%6d86#ow9#WiOP}I
    z$S|pLZhS>hYkaiz(wF1pj$*_UoaTzCp%FZmza2!Xyk={pCb1Uhslo!J_WZt`o49O
    zX*GvlwboT$!Z)NV(f5JSz(`PTjmR>&j>6B-HO1W<%is--egvni><@+%nfO7sgE&nm
    zi%<0Y0scFfH^6)Z%oy+vV1wdu;0PcCL%=xjI`AHF9=HHp2GYP+Ko^FU0H%R&Ei*4Y
    zVz+MP<&A|uTms_%wNg!1n#oEFS=mo-R`iw>6MN7vMMA^jsLOXuraiU>p`NO?dSN&H
    e#8z(}t{~(sx?-!VwAhUQZf<3FcR8&O?f(Po4oqbL
    
    diff --git a/src/test/samples/golden/inc-commdlg-release.app b/src/test/samples/golden/inc-commdlg-release.app
    index 9dd895be951a4fcadbb2ff89d61b3cb3e6953f60..b16fa7c13e0f136ddc786586ebe10228fe30e0c5 100644
    GIT binary patch
    delta 1620
    zcmYjRZA@EL7(Oj6v`{t{*j%8nfXyPp!X}$DOBRM}`H0b6dQHoRP&>F%SfRBo%bXg}
    z(n)huH*z#CI-HA|7|nhxNW_>q5_HXeXrfD8_yX(L^jIj(xHJ%b+0B#pDX3DOc@ucC%*Pm6{`E(9r=dFy51C&o=
    zzje}GWf+?rkxMBoiLViie^HcN?VeA4yq63vPfY%J$S^Uz+wh>FVFDu6Wr@#OB=N*O
    zU783WCo#8u{Fa%qbm9f9C@-bWWJ#SNKCp_AhreXpS>9r56GlBZF&C?duiDhtDJec@
    zlHGjTc>fM501EJ<3fw%x#eNLmf1Lbw`JtoJfuM>Gs
    z{zFCD)D-!s~nYXYiOL
    z);F~Fr4wOL)Uf2tQRs%zqy
    zB#X%`lL#V}*f;o3rbi6Tyu#dVTv_KYnmY`y^4n%n{QlqC=5$<5ED6rP%{?^Qjkwxv
    z(a-xXU5Rx3f`zdnI^SCtG3!jucMRg9)I*uHJ46j-xGZTiiD{(AC_$f50u2%xDdC_)
    ztl!OKy3`r!r$_vT1zt)@`SZYBSC5aM&D%G66|q649;IGOfuE7?6;qn7#3NqAq&@pq
    z#Vh)PE`%h`XcDrTr7r?*-Sb;EfmuYdR&HJQ>vtgOKJBD^G|nv^2B&nWQ{*U%>a6n0
    zAU*;TSzxRwB~b`&B?j3R*YY3t$6s2dD-vjQSQbkNpvsjN-1_tS;O8s@tr4`xBYJce
    zcctH(ocF4(CA`X3@~}ISY0;q5v-=@VWMVC8*D{J>Dc5qF6nIW^u=7T1NqL^0A~I`R
    zJx$)#y70{TSuc-R$7=?18M7nf8n6japL{3$(WX#j$Q^Sl{R7dM5^0JIF)y#oKWH}Q
    zp=ji}{ZlZt|*vBPHu_(K7o^ot94yguPKV!@~t-d)x30ybar>|5-bg6C?dj
    zC8|UQ{K0`U3cJIv7VNqYx0d;$0VNo0R$`6eFk6S?Vh&#@s5gAWx7l_z_^|hfzzyJc
    zpkT{1qWFWsP%k@)-E^B+WVqKKReFcQtQcSYFp$T4ZI!u3Na{`etgXg=9e;kB8s$D7
    z>Kh3vO%X+5+u=OIzp`~yJcb!G%!jJnO7wUz6jj(Il=?D=TijWAuICQ;8dw%$g8*;^
    z>?gniAVbvwya`+a-UB`Z=72?D8CU@}0ESY`0DUK40t>(n>-4^DP8*#kJE+B0K31w!
    zTbMK0e>@ZnMG9#qMM6X2nCw5LY)R=k>>pD2WvksxRX)!v%jyhq-d0v;|Fo2`sjSlJ
    MX?J_d^g=20FUM(Z8UO$Q
    
    delta 1493
    zcmYjRZA@EL7(TbqQJ`29_C@({8xpfEVPA+6<38+#@?+%CngV02D{yUL#n#@kpmRae
    z*3evJD84Ha%XI!F-F(!*6dWig((ade2^*Q&J#FIQZ=e+Op
    zKJR(ozPI-q-)VF?I_2G$2q9Ulr?FE&0?w2WlApV`U>9q#(Ac-;&eM6|!{z`)yjb@v
    zldi4OfyrTag)mco6UNx}(&Tc_eCpFWA#m9_`AdW3oY^F8m-ahB(qv0)&O&+HzXmlC
    zKuluJ7N0T`l1aRQn~0++{YW+4A6OP9HfCC1HIm=KO-5{DE?OO1wrXh+6r0O;d)SO=
    z+v6$uO&8TtLN{<9aqa9kQ>&ZOlpN2=QGOB0#6uJPx$Fz5pF97_W|IrzF?VpKU<)_(
    z@On734G2iwvc-1^U+Su5mBsR!Oym*!B5#u^e}(;+chGzU<#n-|{MylOFXCQW6-8Qe
    zI+sQL9?Da}sKh#A;YjrZ^U$dfu2HQE2ifc(p?Wt0zoG)le}E}=z1+jU!NHqr7oWqW
    z(-;8CXLL$wO8IM6jBNKYZ_oT7#tSVC%)*dUnlNa;-yS4dPu#6F=me3)!^66Gk3Lb9
    z_vM%$fmx!nED+XZ+IcgaOHsUfEroNrP8$ELK&4$uaE5?$2YZ++WV-e{t
    z{;jtWQ|j+X&Ua|?9sCMO7}2j}hL0NV*?R?qB(hOwMqb1#NlIR9r-A2mg`;e&u)M0p
    z2;uvU0CXVvKrjF0v$Gr7Cx!7HyVntN7c!?2^*JCKG)IO=
    z4f7T4Gk*nsJ)0_uODd}=J|s=D-r}b6Yq08({U=AhikDzD-j(wV6HaHxMcs5D0uh1^Es=h^y_uIe&qgQGverKm3kL#jgjaJRy7flZX0
    zIr1m`-$Q&C#E0P-1>OVfU_1#N2NYls7z5q{J^-eHE5J1%1Ka>Q(X0e81$<|jdF?TW
    z)h5y#OB88gH!2ic32_AdG!hyLN8P@Y3Tw0Pl-zQJVli-P!a$86%`on&xfY~rT_(%95C>0YUJ3_+^H98H>K@#t+m
    zH0FmUmfSpg-NaZb_5wzvOK3fptWFc}U&fT1zhu}_)?{oE23;FVM#`hhR`qofMUzIC
    zlTR7$DU=hA5n0uI{VUMP{8vM>%k54$MrHLf*?Gi|Sd2Cr>%ov#XP6L8>*;j-Dh*38
    zlwm@&>=xA^)NBZd^P^PG#OGCM9?d#ru&?tgS=$V|Zu1|q>P%jw*Uq;a?Zdt{=pO%<
    zlC-J`@=?nu{}#7P9M-0>`cs+J?XaZ6Aeoa!k0{f5+qzmJ-luDHW8){J|@WZY&Km85W(Qzi3}3=rKT{7=t!4i&kbA_W7FX;Ruc6bLIaTqB
    zKA;OBi&I(%S;IQDeecSc-2XAbcx-W$cz1W
    z1QcY3u@)(dd~iz@Ey7PO@07?Zy0zAawLb)|0)GH`
    z8>S(pD-am$X2-FbZW9Te=V2q_BN4Cf*KwbfhxFnWk#K4j%o!Vd-p!wNf(R9^;ho!94|>AVTP5|;T`zztawpm*{mFb5P`ruJ;Aw@NuYR5Hd_N_I)l
    u^X;Y8Qj|YdT3z^Q2@V`WYW0qzPc-rimeRXvyA7rKAHT-9Tb&QxG5!znGiFu*
    
    delta 1489
    zcmYjQZA@EL7(Tb9P@o7C_C*Km1Buy8*cWaR<2LMtVzTAZYJst?D{!T-Vry?P_^}{q
    zYiKSv8#$TTNSvD{`eTWiVhw*pG8P)A&bUMuV@#$adWig((aa5|^*N_qvXeYH=e+Op
    zKJUjpcXIF5JuXLwyyH9}B!l%hb_I}tlO=@Yc*&3U*D(ujIS!yDH
    zn8a*pe9}TlI`IZ>JWeshQPG7zu*6Mt)U>*4IIo?VvayNTXmxDKrl)vNY&OsBp;M+U
    zs|@)~m!cb7H*g1WZS;3jt6NbFIi8bK*f}KQg(iA)+2_$ebN-vjBfzAWARuwQG`^ktj2Y{F8Z$SV$TI!Hyv~%jLVq+LupCBvowO#ecBIRPxaaTlCT%&L
    z3%q}i!k!UUBtMo*zJ%GP6JgwV>&es+oK6|hI-)ET2#F?>p$B0{f%KVM}hPWj)*{ThkT?eZ8P&
    zZ6i1Du4Oy1cj+lUMh|=J9{sqWab)psCNn@3F^#})=s;mVz!bYu;bGt4;LDAR&EnFL
    zeECf@&c+PhP=?G1fCKKN9br_MO8~S
    zgzd`)U;@dzqWu?7O|PY&6vnsh$S33$WKJOJbAS)X*X`GuLyL>0u!_juSBBlk`@`)TIYBQK+w0cj!5@X}`G=$(
    zYWGl|L)FwszdzXjno3gi&*C-ZB3=uq!C;FTJrE9)WrFid`e@kyxV(+_Q%OeFf@cJH53qx=5jX~@zyL4`yajv!Tm&uwmw`0!HPC@!C4fobJL}Y|k2q|S
    zna)&>(dX>7QaugX>!m&PU3>ki!Akt|D2y+M-1I^V{iV>pk(YW09+V@N!_(nfe(3%`
    D@P$O+
    
    diff --git a/src/test/samples/golden/inc-fileops-release.app b/src/test/samples/golden/inc-fileops-release.app
    index 8540159dd450601909d446ef128b83c32c7a33be..5e9d5472ac6c84c20ddebea6864a9429df35c079 100644
    GIT binary patch
    delta 1560
    zcmYjRVN6?97`?BwP@rrqut}h>44Y*W7B<7>5YL9WI{#c9;UX!c`4BF4-$LD%euCc4B$e#oMzmxx(R+?*h*=XKxl@yl6*9gYHC{3<(&!;}#Ne1ssO#OI3nwY7O?vt7(kfb`y@w8bMkKWZ&
    z69H%v>CNNUOpIj`&tXNnls0qA>MZer6@)zev|($t)7UNyx;BxH)x=jU>Z>G*r;TnG
    zpE2CKMNT=!WL5JG+=fo(zZ%-y9#6_KE~}Tw&MSVvW^~X*KbEvRLqIs!v)SYoT2^8y
    z!-QzrBN{-c`4A8nK&xD-&+D^1o_EM#Tj!VaDh#{s^6&GSO+J*@#kU)6qyA3l9{Y!i
    zw5TcaQLCx`c8^;e*238RiQMisSW;t9%t@nHRO>wI+m5nu49W}Kk5^^-06&9AWU;!Y
    zwJ(zhgQA8dXCKEb*L8KejN&QBId|YGQk2T#>KdOmTI)a3g~@p;rQU!8wX2SaBeGje
    z<(NbWQet1@KN}yET6vAB+pxUKpEvoWm-%&*D0}~JZF4%VmMja-zs)}|+Ksr{ZPw5G
    zE?tRC{G6GwQaaxoZ(`P&oc9glqBJ5iX|;(uQaH>R6D8BgjFCW}k$?sz8c49;CRXoc
    zvt8;e_0uDM#{w@UrTn=s-PPmgXA1U=UPNq=sfVf8a^M%py<%F+EBA<3FzLv>Rq>*}
    zpbH_3Q(6jH&CwSDx9<53o4_0*IV+c*_uF?M={{|xeKgJ`9ssBK)M;{bi`}`*3xoIw
    zD9H?CEmIaH;Fe=Z+u)r4aew@!Rk|#LMu*+v1_G#Z*@;^}sSkeE9BhlAJs#1cv$!h*
    z-sHSjb=<(KY$Fe=EtwHbIz6`+$%$;tnQ<(k7?yG@wabBLGzTkhE-bGu&?O>!d!whx
    zJ6ac>oSgIWNa1*0FrP6i6qkT?fcoS=)*EdZjtseCE@dDXjVY0q$Pn}L9Yy<1h62>f
    zJB!Ap8NOKbkaU&T79T2Kg^Q-_fBK~RcrfgZvRNK3uG-m-Z{RK1-uR!jT{$)~U{|6_
    zB-k4Yo>JH?ezADlJ-D^Z9}Os>P^%Ja4u{z)92fKWLUE(?4c}zh*5t?D9|Bi^n?Uh~
    zX+-G_g@*gsaqOns#3Cp9dZS9;P?(kBt3Lr0@IK3qd;?M%jr@$I&UzVtep)ulePpf%ECNfwGOz|PlwtztJNXh=0Jap)?Ac_uNCiAnd79s>+$BBB
    eE2?H_k*gYMtqgDE0Y5gzv)?)+!
    
    delta 1463
    zcmYjQZA@EL7(Tb9P@o73`=Wff4T;%I*cYP2xUXI)KeilNQ(&y?3S23yNbM~KKNciy
    z4bA0dBPTPA#AVY&e=IRmtl^JH#zN!Qj7xMe#$*wqhscka8QsRTKIh&p@gz^qIq&EF_g3s;trVL3w%qx$6@1uSfQT3CwtdR8
    zO*$|&;;j^B%CEy1zgCu7^DpY3)C+-2?x|lICHL%RX@|7m4U#6?;|q4mkN#^?lOe<;
    z7fKT|HbOGVH*gbiR5y-PGyI`7VPfN!4b`K09o%BZCKqBg@im8*7D4faJg<+eQw=s$y;YXS`XTep}j6vn^!m19YEX*4@8rWoX#cD
    zzmM|g4J(qL%q3sLY%|FSZld*6`Z!K!^;kX8-VsJ!p7w@b#)ld#qg*m2b$KEqPYAk}
    z7M-HIX(U`;H>bL|U3Br31>Lu6^27n^VD6~juto{I|W;+s*d*xpKQj@p~f^qzoG-me}E}|t-{B@!6A?v7hk}o(|iDw
    z&l;36UHNlPJlXCO0srD4o)<~m)><~6&+Ib6{t1(plFopY}kMbSxp|@2EH)iDJTTG?ZHatuAt$m6EP|If@IJR2Wt?asE
    zXVpCZA%ls&j|}&Xgq4=4qL7ZhlnvIZcdQZ=e@VYo#g2~=Y8Ji
    zIp=-vx&8d%R}VVtTOE7fWsIdTcHt=l5^y7zF=Kk&h$jtuw*D+n&!;mOo3Jo829Tb{
    z9?PV&TpFDmb`?`t7T+Qm`6@rQ(m5CZWCs~s7@z!kzcfC*ReDgW9Y>PtFh>$*Sv-DM
    zPmTJZi6-*KuA3N3Mqj{+ba8FwlGPdFeJcpL`Ade)rA@{bVbHbFM7S)nVpU%!Q6yn>
    zIr+5VfdV=17?oAc*LNE_ng42NcDdbg$C#{MCOeP#37gSDqrF(t?hFCZw4O@EuF|p?
    zOBp6a%WhExLd}GLI3HT&jDJ~~;*pFRgMFP}$=GVxd6)l~QDThwo#33z=-JeeHZigi`2F08(dPJ$tqrPn@3&)^5-$Qs+rVsJce^?f)
    zYg+q~(IHUOu-ME~n5Da}P8Cu-?l|Z2JxhvwSzKG=Q$}0m$GR{!N2OF6aG(~|F@9Kf
    ziODn*4M0ll8~hjJL8+0KnK}*2tNcY%n{3
    zyzkMKNJh??8Ox{hz4bO`?XkJGeq5AlWX5cEQ9%laIccI~8p%--=ra<~phN=+_Swbi
    zom8qrouPiZ#qU_)rNouL_9i;Iy!>?5?vYD~^)vMl^;-7bgxn*hw7ep>cny=*^jj6L
    z=nJ|KvN)-wkku@G5pe6A+pzJ?B9gXp>UqEY0Fv(0X4*&NoZ3{CezqLx2WzgubODrOQDwmsZ>nHTV&zk+sLA1v$x^xzI
    zrOy+a^QewRyvk?6`OUY=7^vP)^B?7JLKmz;^K->w4wra9_O=
    zQiA^Ofd8bzZt+VwTkgZHW!{ia2?QFIaNW=lTZQ972A|KVmcHYgtXpcm*!v^kD)0x8
    zvtb%kx&wj19(D}7={Diu@t*FG(lao`3h~t+2eNpNb$g}(Db+@P+FD_|f0dE55f%kzgfCR7rECI{F8o*GB383%fYhWHIuuSjXRA-g4_+arVezSO|
    vbc*MdR7s<}wxp`y(_+R(A*507IPzp8zhEi3pN@N=ME~R07&}~7|
    z)=)1ujGWADBrcmK`eTWiVhzza$yjLonsJFP#+Xb;=0W)}qtR_l>vQhylAYwqIp=+!
    z_jy0=xv%g3_zAbOQ`~Wu5R%1u6uSfnz+^EYmfXD+J70?h#=bdsK5qseHYdR2`MPDB
    za&H#)P7Ql1xLIOXV2ocbO)dN8wGV5#z&Y2{FAaigW}UEA*y9386>af3o5c42XHb(t
    z#3biR64O>fGRb4O@ikk+DY`p_gf~`&!*~izwM;^^BoT#fi1P6hV^ovXvZ1*9*Z$5nlVaO>>7_{H7g^21)c+>`+z_WN+L>KqzFG^y)
    zIp%v{7V9hF
    zk3L4c(*BOre1|Gt$5%+fh6ybKZ@3r>G4WX4u@OiSW_fI?h3TZzRPEcl;4Q!b#zx>UAOj&_40sKA2RIL004@O;;47dL!%70vz_+%Ummg}f
    z3ugLB#ZlVms1fR@-%%&j(@96&s!#+V7;q7gWMSxm2E~%jAnl&Zlx18_#EK6d*l~
    zefbm43Tb3w$YrIlEWSlBcC9$E(m9*_Y!?~4KQ{4mtu!{ZU3yrm8$*)nFvn76SscBu
    zr^fxz#8aC`ZrR#(S}(-5CO+c|DU!T&JZK
    zOBp6a%WhE(Ld}JMI6qqDOnz0B;jx@U2Kzd{nzP-o`#%3Mr{3g6c^!O*(LU^Jhwhnw
    zs7Q;NBpTh+s#9=Lr-Ji?uZigi`2F08-dPJGdqrM#|3&)^5|08%+rjPN{e?%6m
    zYg+r#@en9#SYrBF%(7iqCrc=vbewbfUm!)XEaukuq|sLOsV+>+QYlpi9H>=wj2)3(
    zVj|1LgOC#YHvh%=nAF6}O`V42RsO2UE4|5YnncM*_q5IFxLUF-IR8GkcDNI9wbQJh
    z_XD~T>DW0lW5sm7ci+RTJu&O;$3>|@X2NC{m85W((%Y3iq2{DB2tN>aJCKh@FYP^+
    zU(kh+#VIX?tY+wofLrJ6hK+v)k*t+d&-?uckaVB6(mopJ6bHd6UUiZjU1Co*^HM)P
    z0!lK&Sj&_}5xC_j(l)r}|G2;Y)+$|=L8HSiv4{YwTyDm#AJ+#zV-B>0(H^(x(plV<
    zK2KuSqdFGxDqG0IW>2I=olei}LvlP5ZB9FuPz*~tmRe>1OPYg?*XNg(<>?ZUxwFyJ
    z#9gfm&yUY|csPHwGLXxd4T>wkIzWB$9qW!X4u%JuQK!-uh(wieV|ajh_|Ad@CPN5dtW^1IvxmlB5az63Lo6nif`aNY#0A$-Jl#B>T6IU
    zN;uFR44hKfZGO3M+XJ|@%op)1!C;dTtq+CRDjXMb_9*$m2bhow)|2)EN0`OQr29{`|CTl>78x
    z?@&-_3@ZxT4Cf*Kjm2C36lTa_K2+sYB2NYfBMQ5KQeOvggEthN>ADTR5|%~Szz|{8*5*
    zH8hu-jhtyV5|>Sl{#atBSVLl*WDpv^W?Z6+F(!*6dWig(anWr|>vQhy5>N8vob$fV
    z`@A3b+*6M)?r}T2<(=mUAz7?Pu~R?-P81W8m%F!P7i+n|+_&V;7p&mJ<^)8%SU2s{
    z?k&>3=}}LmFjIa7#`t1sYSp)(f7~Dh&by|6X_j2Go1|^hUKdE3Y>&^|DL?qHNlgY3
    zlbkO}%-9IYBwxi%#8KTiQqAxOSA~g9Sk~8!<#ln38JnDs)y7vHT3Q6f=kq*XHf!1X
    zpf11WrkXBvgMT5ell^At@KCDDiJTneXOT=4njFYwUqb)f^-ngNS`x)v$CZLD+|DGBe;pyQ|ZGvoz!CuM0-mZb@{X>_yRsusFZTal+@+Pj65mm
    zT3U39?oK1&jWu(s3tL4Ok6F-trzTJCqaOZ6&Xo*M2yxTQ~nhz%x^Av19r`4*NnQ~ak7K?RSzA5Emd95`-M-oXKYUPN`BqO
    z7GZ9x=ew}?YH2Y>FMs3?`!K&{eC2jF8zR)0M(|g3K=}_a#TU!H{2LtnxpDD%T)NE%
    zK>4geDb*nX>bmPoYIs@`~6yoXugC;Yc>cXibBATb0_CEvX2T9+jp)bF9)Fd?a|u4unB&H9R3$}hueL=L<-<~cSL=~BsYcCo0cp#l&72xQOwPui*U
    zjSe~$Rf!G-!b2}BB+dRP+E8Z1t3f3kZdYQhkqEgfv0BG&?-^+0mm_{?A5c^^FsSSw
    z86yoSIs!}qp8!_@Ln5jK!r_sAvKy)Kh>2f~MW62vs7n8Eg!}~K9;mHj?>jbI)~xJ`
    zV|(=_{6hv4eIFPZ7!50JQAHuwkoYOKzPP8h4BFrrCAby!ned3JkU_XR;5fskiccK+
    z6aMcZz60W;@Qed*165!=3>*a%AOuVRuLJJ^7l4bvWgr854Rm8zNni%}&OZCn1Fa6p
    p%08`ZX16NqrAAg!)hIQwj;hA$P$eNPC`>G;x7+G#B*3tUP_y}Wp#%5zzRYh{(@m^MYFM081&pkI#wB9v8u09Qao*R
    zyZE%>?k#f4IU=i?Z{Q~KWd5t6#qIH=oTIXOk?g$UM{Gt1P4r?(yE6nt^LjR$yi7|Q
    zmNHC;mOY{tgqn{8;sR)uEA>T9mdEoB8yxHWQr>pM?%VwPyatmG<#q5KM#r$f9eI!a
    zOGR4M6#1wXRDY|-Eskhm?EXYbfCIIv&hewN
    zTTJGdLis{d6hqJ@<}iA>n2h9{y*C0bX-j=3(mjGKRDcpxY}vf
    z&-*T2iA?;gnXwW&-y3gY)}Ea6_2Z({L7B8WL^WkN%^4GkX=FwyL7!0q4H6nC;ebP|
    z-pXb>)EVlhNBoWjUP?;&Yk#_<%g@IO_6}b_te>ezsMm7f7o>Z|l%^~5h*vOa%e__c
    zlD?n|A&b+Rgsf)ii-23_+=fkH7LlBlOV|B&4J6&Ct+bEExx{_o6rVapj&89hr##<}
    zkAOsG7;8#d6oXriLAJp)|IhvTw^r%03>qDFi$w%b<#ID_{e(XF8FR2Dg7$bsm(Jp@
    z^m&tWUe&pXSJ^@yc1JQJ>UDZ{AH<1ltU2RcLNP4mTxyjA&u9*I-cVRpQJ|-Y?9Gjy
    zCU0q7cyeOa%Oi!O)xmtm?8vwXtOL|1|MBkVvBAiIE9O%Ag3*`~ITjgUUcR&FfXPsR
    zdU<=%s5H$NiXM=z@G8sUvQ@ZfnY~Y+a-Rr>y-_y9!R^hmi$LB3|(zkq*bz8k3dw&RA2L1pn
    z8>SJZI}{r1VJEPgZWD`~?CFjwJp*A@im(18P{4bvJM#^Y)EW7hwc36Oe}0-8x^954hGj7}2mt57
    zeg@0~Zm4{~>%ckSJ>YX74J-glz%sA~FqC2f=sWoem;s$<$<^%SoZ)ty4Z+iTgbE^g&jJYz!jow_`^S8=m1bFM@PbnbnZR$HEv#;M2RHEmv_
    zDNB#EL`(Bo`Oo)+2yd+*E9`4pRM1>{5_a8d)y2C%aL@w<_9u?QmaM8{{oE&8(-sGP
    ztDts63pY2_v7Oj^^%QTThdp(Vep1jfzH&E{2@*wgBk(IKP}mKaVpl6X>{}dsxpuJy
    zT)MLZps+bXDKpf+=K09>9`<<`gBUNgG%ycCPH8Gj`~7B+=-#+nZx#fe#lylv+$$zZ
    zVf{Jg$6%HSmKDN6rjxb7xe~>zAEt0V^eJo`e5h?jfg3$?vrSp0l@5$0{mwenvD3=a
    zo%u(!5mP$Ym0ax7B?oBBJ_3p^<*F3#l>0#HU3gulD;jwSPEFeuD8?sO8g-Y@1DM
    zW_r!mP<0vqkYJ+j{UZaTA+;@{s^lgTzd+ZO^wg9?8yxWjm#V!K8qriT2zLh@r|DG5
    z^ua&j{{iCLAwCMvIPgAT2jfZL2%rK%U;;P}dSu?+#8Rz
    v+9Wgmtg@Zv+v}u8+F);#nrN@R(GsjA#7U>^4OJI$LVB?ro^IErNACXvMp8jb
    
    diff --git a/src/test/samples/golden/inc-help-release.app b/src/test/samples/golden/inc-help-release.app
    index 6308c77c0a28fa7e91ce687276ce2bb979ca1edc..aba2e0abd86969f2d4fc5875f41a4d892409fe2a 100644
    GIT binary patch
    delta 1560
    zcmYjRe@t6d7`?Bwv_RQdU~}mT%dlBCVPTVPnk5UMlpisgM{8ONLS^uzWo6X1m^n3|
    zrIY5R4stavDxy&n%L`lqC6Lih~P@^+0@`n(EdWjfg;^qWdJ>PpJdXv+4?{~g)
    z?)|=ZU%xs!eV?nb-FfIO##k0(Kb|rm0XK>nGv?OKc+zkb>d(sDd?t^vi2}yP0MgSq
    zSTO0Tltw3q-8Kr#;!6bMpOqw6yXI3L?j?h_$0vV0B8|`NlD4sUD
    zU3|uH_cl4@9F(+GWwt
    z`wm@+O#Hlsu@XAp>u+Gzk(~Dp;G)zbGii5-DpEKt851Sb$c&OepOJtDB^pR@*df+#
    zXS1E^EcMePe!~JUC8hj%DBaoZ=V#3aMlK;Xz|`Z^YdP>U@yK5=X%)K6Q#5-C}<(^Wp$L
    z0!p&LSj&_}F}Rf&(l)u4|F}Q?(kfk%L8HTNv4jAsTxr3rpU?+CX9>1O&>oNI)>+(@
    zes6N#t2&qPDqG3J?nq`tolegkL~e4(gT`kHUC?x^!)?+<{h!0$lO
    zrfEd!35AAw*-7lC+r%QLdV8Wu?_ijf;;TOen0c>tPrd;uwMKr{T4le2KR+!SI)#Q^Ty(H-8aEk!Lk?|1b~ZR
    zKL!>6H?n-dYrr({F7OGE1{Q&3UFZ2P2V
    e__wwiX_Omx)!09>G1dwpjYdzqD~mN9md3xnFk@Q)
    
    delta 1463
    zcmYjQZA@EL7(Tb9P@o73`{L7WNX%x!K42;_4rVWuA6pLXQedp>3S22&k=k1fek@4Z
    z8k)<^Mou&viF4CLe=N}{){qz{84HbHTU?@xF(!*6dWig((dag&^*Q%;i6?n-&UxSG
    zecq3I?pu#v-Q#xl$UDyxLb6zoVyA!voG2kAFL&?2F4l6Pxo^px&s)KV%?XHjv2NLC
    z+*_p9nK4hLFjIa7#`xv3)T(b$|F}U2oO8|m+$_1~HcH#1y)KY6*&bi8Q-0uIlbQ@7
    zCb>|Wn6(j-Nxp)ch@-l3q?+Lmt_l;Iw5+Qc&+FzEGd8&ptBtQZw6q9{FXVZ=Y|gUv
    zL0x{;O*LKU2LD1_7yH%H>7i7Y6FE7`&mfs7G&z{dzJ&g{>z`~kwIqtUfhz@DxT%*n
    z!lA84K=N8?Vu$eQ7wz|0+}dIxciHFGjh4JM_Jg&}b_nhDvbwzbi9SE#4&D<@I&wPa
    zME_pOpEj&Wel(YS6|>DGBe;pyQ|ZGvoz!CuM0-;hb@`Mh_&h#TsEl&Sl+@*^j65ai
    zT3U39?oJ`$%{6nXi(5q(k6F-tmnKiOQV;(k=Sqe_7v48mjqM3(f_ZH|%jPwfw)IL&
    ztTdmO|8&O?5u-K8lz+vF3Ytq_hh6j8HKXo#oa{hB)kDW%OI6qN0pXMF8Jm;6Tu`^6
    zMVOoF`7Z2zT3U?J%OAPJJ}hXNSh=0eh6pvL5&Q)mQ2sqk@yiun{tXWP+_?AxEcBfoYJ&O`|Wy&XugC;Yc>cXibBATvnS^_u#XB8+jr&@auYJg5%n1$2ITJ#sO_WC5qHe33=OL>CE6YxA$2TJ
    z)MWb-{6;ogl#o_Z%CJbgSezyHH%!un7YvKy)Kh>2f~MV}i8sLH@dg!~BO9;oHB_Z*uoYgTr}
    zvAy~t{vm^jzW0v~j)j%>sG^YTNc5CSHF*MN6{^S}k*5|9DD26`~8BrpqnYoB}Zfi{O^
    oWuI2IvTS9&)X3_q8l@(7u&S{-R7r@8!o+fVd)y!3E(#UvKPa9*_W%F@
    
    diff --git a/src/test/samples/golden/inc-joystick-debug.app b/src/test/samples/golden/inc-joystick-debug.app
    index 39c2dd2092057fcdcd9c5ee24b80adb29a4b63e7..0d8760b7bfcdeece91acbaf043d6b9d48a1dad6f 100644
    GIT binary patch
    delta 1570
    zcmYjRe@t6d7`?BwP@rrqu(?2C88*u%ENrsrvSeW>VvNCF0%$yp~
    zwv*;r2e~sYDxy&nquCz|5{YK63A$!~sL>@Z@`n(EdWjfg;^qWdJ>PpJ?k1=2-tV06
    z-1~j+zJBxI)FDS>yM6!LjIj*H9z1100&WyBX3Vae@uXoZ(4SS=`E(9rrQ>6%M^yo(Ip8Jqa&pfonUUAj-IAA?A>o1-bSEFQh1
    zOXEJ|#8aDN*G-J2Nl+_vHeJcpL_zQ-u6)na#VbF8qsYqpX#iG7SNzs(i
    z>EP3bd$-6*`>3pHzP?+?lliZPR;SCAw8v!iGTFJskJyY38t=oBc4r8Pmi0^~ag~;3
    zSjsRVT6T$A5Na+Gi1VRUj^yVx86M3!Y_P5KD>>T@yYKKHavDq?l-J337;Pio4&)vG
    zmx{EgN%B!EsQxyWQykI4*!_v@?lxFbV^GX#qgzzyJnGwlvTzL2`R>Q7GJSxbfupim
    zUDMi^jt4eZeU1NYJR~*qN>i6%d6hqJ@<^}n8zxcm{y*C0bX-j=3(mjGJvh>ZxY}jb
    z&-*T2iFEXWnXzIz-y3gY){&U=4B(>FL7A}HL^WmD&1n;fX{1LfL7!0q4H6nC;ebu7
    z-p*t?)fwuiOZ<)nUP@B=Yk#V<+sn`9?H##<*Z@`d088kZV6pIL;%Hr+Zk@$l
    z>31jQ+^T&MudLHhcqb!FKU3>qh0oaDSr`
    zRzm)sfd7obZt_co+wQ@wW!|t)2?UyzNJB8lR^hmi!{-a@q;L5q%eHzi_WlsK3j6^S
    zZkUFYos<$Vs^bQ7D3BLMMKpyY4?94SlQfK66E!Ea5`18}$DEE<}
    zzTtq<6jBtn8P3D}D~qS{7-rBgFRF4V;fDi5VTDbh)E7Zq=Z!_@x^IH7hGh{p@BtUW
    zegez`PN+P<>%bK79`G5E0v3QJU>R5g7)mh#^qqVO%mZ8Ur}u8MTckW5E_<1;mFpV{C$tl~-GjmFs`>aAA$EcE@9Pt^WhwGG!D1
    
    delta 1473
    zcmYjQZA@EL7(Tb9qd*Z9_C@({8xk{2*cXb#xDR`w{Mfv-rodR&6}VDZ8MU_<{8*5*
    zH8hu-jhxJEB+gBZ{#atBSi>JTG8P)Ywzx#6F(!*6dWig((dag&^*QHu$xia*ob$fV
    z`@H9U?|a{Ay84vI)hTa1LkP)YJ&s)gB;Z&vA$hrbD|WsX3(S3E?tIP)K5Q<4$Mbc|
    zKIPdc?V1|#R&cYzuD}?-T$)<;&l{iCae=e$sb3l;_slwJv$WFoaluN#h-e!^!+<57!^Z}fX8?ic~zs-$?e9{|w2_I^(RAG`SX~+{9
    zd4kjRG_MrZokYT$D`r>cH}NW-u%P;OU7pybc-a>@S27Gb_r67|Y)#S__1XNE4J$Nl
    z>yj2|$tqU%^Bp0=TPw&4`-&FkHrJJlHjE{`Y9DCZX6NZn
    z|0CLnDedn_&3EYX4SakD6eWY
    zL)cC;03Ar(7UjQqe0DAUq##khbrm5uA#)T_p98!>fu5k&G8`T9#5`*MfEH7uEzu!T
    zLxY7oY+r)kK&J~6l16Ka_DEB-r>MF7609P!@8wbN;ekkpMvl_+Mb5hQ82E#bJ@bIH
    zOYIrycd42h9SDX8UR6n&{!z51OvGy;H5_hLW4j{}a#y0&j_tlv(8e!E0?^*8YFe;g
    z-7`E&>hS0YFbRAHTmyteR1Jp1!@XoXQe%ioK#N6>^aeGxcPK)Bg7HbHt)d?~Hdt1y
    z^opat@&f)L!9?AAhxz`KAGj7Nb(fC>x(+St~Wrn6p7@qVGE!Y)2~a)#(q;`pOlYkX|g8uhWx#;Ql`hCqu3P
    
    diff --git a/src/test/samples/golden/inc-joystick-release.app b/src/test/samples/golden/inc-joystick-release.app
    index dbb357b41716c51dd4c5ec13889b3492870aef98..5414a2413de76d44586843dc605d4876a5b815cf 100644
    GIT binary patch
    delta 1560
    zcmYjRe@t6d7`?BwP@rrqu(?2C88*u%ENrqlvt(gt%a0h%qctrBp)z>VvNCF0%$zfz
    zZ70o31#)LxRGdaljAnloU)$_eqqBl8x_uliJ
    zbMN=vdpmJt@rcvj;W+pXV=Rld4__IOfSVst9Ti4Z6%SaS9$^m0{Kr^{$Q<+$MTKTC>ISzKS|(?)CE$GR{%PpQ-yu%UL-=Zq!%_>cHgy|T*7%DiukT$Qe2TAv7JFTN}PVo>p#j8$}qf6||MP3@j
    zBcLENjI~Hvlz>}_B5adu{g3trD0iLhB7Dz4nqjyLc&Y?uCLZC6f=4A_;3
    z5)SkR17{R=n_n*8aUX6i^F{nhFxa9*n?fPB2FJxbzEIpCeap94b~O61_D8@q;CG;S
    z(=@E~27|+W>?BsxZKB~*eZ3K-Zz#md@aj(i1-#F)JKuni1|vUbskL6kpPv?ubRQq?
    z9|ws@-_Lk}^`hpe1RdPQ*aC^x_&u#Fvuq?p>e&7<=
    zPk{x%g(xrZ25=F0ANU+d1B<{iumY?D45^p^dM9523&6I*nf+TF7O8+oD$ep-6?>%@
    eczI>L6zBHJdh2HuIC}`G+1(w^#!7v&H2xRFYhvaA
    
    delta 1463
    zcmYjQZA@EL7(Tb9P@o73`=Wff4T;%I*cWaRBirnS@?*=PH3i1HuE3ST%Ba1?pl(6Z
    z*3evT7&&S-5|>RA{jtPMv4%fxWGpm(&A3DtV~mRXmlIX`kZ^a#FIQZ=e+Op
    zKJUjp_s88+yFIQ>dHXp+NEYjH>=clIsS-l+a`#s3Vl5S#`=;Faf)#w&T!4rd>$ZK`
    zvq{=JJ>sntX3DR@7{6MUTJbOFAJq$i^X}=N8YTDadTEQ)>;_4b?eTd#+-7}s_8;E^cUjV*)NthFQvMi$jMQD7Rf}R$-Z3nMfA_z|75eNMN!O6Tq)SXO?|up
    z4s9(0lGjTU+k{WQWWUei)+P(N$1YpfTk=-fch-HjLujvy)#lZWbq5gl+?`{~+YY-(clK{%
    zjCiH}9jS#5O}>e*kb)8ON@e(%>7Kh?Ku9thb7$lwR7rGsshx(NG87K8@xqGgRx^Y*
    zn*o?W>WpCE5}lBDE}7
    zw9EDx_zi5PC?Tn=ws^lZ&3cNPDz3n4ME1Qn>OD3P=}^f@cCpx5zX1>a2xQOwPuivQ
    zjP$z{Rf!G+!vilXB+Y&=UR!R&t05&EZdGFYA`x;=Vl|GPzBACqFGm8hD1~ehQq_XWG7PN5tD!#iyrR{s!H!rgnSRRPFR;}!s
    zV{6qV{6hv4eIFR^8wo2dQAHs)koak~rlhN;9NOR*C3qC|+3>Kckbbz^;5fr3OQsI}
    z0sl7;-v;p!c*cOY0Vf!b0!IM_7zDG=(JQK(q|0X17;q78@V$sDo&#&MzffuBO@_$P0%&_Lla%%A|^r%>Lp@~iJKE-^?dJ@xSO2z-tT

    xEp?^{90#ZMWwR<;>CghAKFQjx0YibZ{$MA4Md z>EKDjgInZ;eOy*GU*8?*Wd5t6-RW{A>=UwjmF(Q&M{Gt1jSXN)yE6nt+j=GwzfQ|? zEM=GwExSZL2sIZ1;(Tb8Bk_4%hDUP_8f@$QT27f^=RN*?PLs)l^16Ao(KhDog6^q* zsYr{OARo1o>hEwl#UU+>-Ji+sZi6K?2F08(x<#eVqrPgCg=0{j?_oSD(?|FjIxLIT zHLZQ=SP&F7EI#uzX4$T*(9H>LJPac+? zVk*nT0+15>2LIW3Kx*YxrXIucDu3DJkzV7sOrqq&f3(f%xLUF-IR7?x|5y*=YL8hz z@B4Hm($Vu~#)|2DZ@rCKSA5Phgp1OE%(&GiYDr-?r%jYhBRx(6eMSNrlxQHqKATv* zo5^&mGt^I)_#F#8l!Wrv-c)z5m!HntJ$4DPA*LRpUdz5;Aa{#tEw9ui&SBD-eX8PB zeL)vO7ALh7vYMqg0&YEX8#caKM6y;6J@2>gK+=8MO8aP>Lp%ab@u<_}=oGuMnHPuf z5>S#E##*KqDFibVua<#HQt{aJnRGiHB#2<>r+UY*5V z8Fa_z+^T&MkFuRSthRVsH0t#19wf&yk+!sb3B|C4eW^qCy`VW*c~gFAWu7h(nL8Uj zjo;O}@a)-HHxK1c)cSK7vqEtdSO=(2-lKitmXXk~BjQj7{o#lbY6%T9H{V{c&t%9$ zy}YYnLQ3+5f+NxmUQ>9mbQLa|a^U$B&SUQZ4cnqGH=+Y1Olx}q$wC=t8iS%;q!$J(l>mQWm}^cdw&dE2mSyG zH%voHUm!5j&yHa?-6j$`-rpBi`iFz81aJLuAdmN3w&xm<(qQDLEw$Eb`1onrDEEnx zfzg1{5>gbl8P22pD~qSC;5sQ?U43<|*T&>tC ny~rnt(!+)|yE+{U_pSc}3`S+s delta 1474 zcmYjRZA@EL7(TZjP@o73`+@>)Lt=&r`v9{M<2LMt@?*=PH3i1HuE3Q7MQU#`_^}{q zYiKSFBPW`T#AVY&e=N}{*6>3o84HbHGcM7^7_-F@Jw$%YXmlIX`kZ^aWH)(o&w1bH zecq40w;$}gzSr&Sl6RdYgk1kSl;e`%6j^P8m|(ta06nrw?L+9*H#pFvFo z5tCRfiO*RH$s}IIO~lcZex#c253UIln>1~x8n<+ElM$O(j8@0i>{?m`#TG3dFPk@Q ze>f$-=B8Ro=m!5rTnGEj)b61)CC77el%GK|QD~w+mwg%ibJxGwY;su?a|2fjws2D~ zZ-7JFh=9b^lK4*HOI@_xV=;5HiQHwMn>U**>+DDKLF*B;*Uf4ywG%yl#65dYG-=Q2 zoD=E8i z5_BysIz@MI!jx- zrDax<$IE`cql<{q>SW5lVukrlC2zp4`D~hA_d8B@IKT3dVc3$6THYsovMpnEvRCqJ zHZ==#V=do}caN49WAyUJ?ywK@neC4RiVi6M0jAjHaxeb|2Y+r{d=Zx} zV*x0i*D0kb<*x;?vOUNAzNI0o7e*RffFY+eZP0$dHbgXE+@m$=1d+wd!@9Un-zdua za?JO^EY?{z2eS>L_!W{cVqVD%pD^4DxAF-|WTUQ(yn-r8N?z%p!F{^IQ8rmnUe#uV z@cl*rCXl?XxBuM9g-z_Ef_VL|JVI_l<^-ZX1H^#*y#cj#G&16jx|M-JHL65fBO|1S z1qvIjUxMGj<_hDI%4&*Qq*>Nm)LecERz0%+#c|K@V7OByC)kCe%DOFB_+yYg`+&4l z=^YzzDykA041@+>R!ExtQM9p4k5_|ADAcAz4~E0!uEeVCd%UNijZcpFp?yeE)xdz# zGCEG`P;?lW20j6<0=h&*34}tUePj<(V~L4hjYgjD3#dxpNSOQtEf9q zf5QJg#CJk`44w(#ZJ-j2M}gyj0t^9@!0W)fzvp5>0Cf4X^unt#1o&_EC4z1GoBSx9T>FsjQJaGRXzh^;k diff --git a/src/test/samples/golden/inc-music-release.app b/src/test/samples/golden/inc-music-release.app index 01d50a9fa83b3f45586cb885d9f66faba647abec..f7822f9b31f64516de668433bb4d5b0fb3be4174 100644 GIT binary patch delta 1561 zcmYjRZA@EL7(TbOP@rr~wz<&4GHjMjSlDDUWXZyql#dwAr8O-Dp)$DAvNCF0%$y%E zp_AsO4stXuD$dVnH2bksG7>Y_1YNTqnz$tr`60xh9wNqQ+?*h*&wFo)p5*C0=Y5{{ zIp=-vxqY|xk6LF#o8!RSjIj*HUOZ($0_KVsGiKLKc+#*J=+BDmJekATL_T9<0Oixz zpFioWkVYnlT%{D2#n%Xqeo-7>>6}e`x|A+4jYFh5%%dr@EFQh9 zOJjcI#8R8bZkiZN$DYHA@)Fw2C9BiK`&STh^D~BRWzEJ`VbF79sc8A=ibZ{ul15WT zmy;(A_id3AjuBbaeEqkPC-a{TEiSh^;TV(ED`e*p-(xd6Xsj1Y+MOXFn%6U#_%&LV zVkyIfXxS~QL8!S%AkL3gITN2(W%y{$VS|00U(MNJ*mIYEms4-@qPz~i(`X;|wIlDb zf2c@{njjywjOuT7yTlPKjNPBe?rw)AH3r3;GI~Ur&ZEAaC=16Ro&N#6D$_^!=|3uq z)ites=~xI9H7q{;IA+JvRNK1-!k8E~Lh)iHil zc8SR>6AL1h*w^@v##*U~mzz2b%d7l(lUI6~&zVHY2Y+ju({VMiEI7ZBduX^5akbN| zpZ7hw66w+NX2y!?d~dvoS$lle+mDM<17+N17nPLZFsDr73}a0xiz0B#QOGv9=Kr|g|I#X5mO-P#F0qIJs$6cyt)I{bKW7fKgwYZ5KGfHs zM3iu#I~X{vuv`36;r4rRYnd|rOcn{E>gpX}+5C_MuqR)VkoB#_5@EW2_IkklCYSxcquD*pU5HOhTt zuy-h^G=>$0ZHDs@|H|Sme;6}pm=9GsmB>TE!HB{xpwt&Z+~f^K=ellzuY_e0Ht+)% z!CnUD0T)zW;C0{v@ILSvkOCHfC14p?0~ks%0rZ`G3Csgq@{{{E9kWPz$x!J%5`V$E iM|zfjXswpc@HuO>?Q$t&R%$v%gS*Xn!>XT_*8c()`D2{` delta 1464 zcmYjQZA@EL7(Tb9P@o77Ha5zK+YrrW!oDy_jN7mm%8xCF))W})x&l`U6sf(%;Kzd4 z*3ev!Swct#>q+bskbvVwgydxJt=Pp{&Nucg+4Chc_^>$u5iizl z>$Gc&v~POUT_MbrUxzVztt7GPT}XaZCj`zpr+;pcoUA%29p>$;3nBec9}bsGmFk$z&3X;xRXIrC!2l(WWuFz>e+~8v{olm{v|8OYbZVmyXLiO`n%tAu!DK_bwjWvDrkeXoqUhXz6&Dmlh37uxGKVc?HI_U!+p z9ZJt=ze7=#@PI!!@R~wW?DxVArFy&?P=di$C9*#hBKIX$ZQJcR4Q>2##0TvIimLki zmFAH#Qin%}fGOZ(UgM;M=mS}wb2+iY4h zv+K6)l~?c&=}gppV5Dy}sI-I?h1@{mXW9CquIf@~gQGverKrybM^uIM!`%kQX*N}K z{Lr89e+Tg$5FdqS95@fy!FUWf3MjxJFaf*`TmUWsmw~H58u$w6M6=?+4DhXW_SHw4 sZIXHRlZtf`Te8VvI4a&W=;)g z>7;q7gWMSw9ZsVrMzcQ_B$Ajp5_HY}(5OpX#vfxb=q?dsOx&CxtLJ;K#NFid-TR&M zoqNCU-Pfs_J2lRRHpjvD8DnXTeR#@%1l%ZK%$Qj>;Yq`ur$5Ux^O-EhCUO}Y11O)y zf!s-FximUC>?)zKEWSf9_H|)mwR0i)`5rR(V0`lD!_xT7PU$hJb{rzrVUEq4W%0}d zT^jczCqBP*?7E4uRQy$}C@-naT(UY#ynhuTH-Fu*y|mfbDhzsVd_GzhTeYZfQ&Mc+ z=yLKI!=u~eq+?W8HDCWd?;qIn~oPF$sB z36?TUh?d==3WS=C1mgT?l{5KOWtzvbju`A4{7TkN!`=t{W>&q)i}E`7E~9A0F$7M%Z>eR!l3akbN| zpZ7z$5~BxzIV@K)}C1K_T!>dLz%GIMFnLz%qbIzX{1IeL7!0q4H6nC;gDUd z-A|`G)LH7MTl|g%UP@B=>)?Dxmye&$**|g#v3{l=rC!VaUy<$+)0(c>EzV-nmU*k< zO?^QZLKdeq30ck27Xi1^_$Cy;cXw$nZu=M+zXQ@rXlIl9EYjPhbX zJ^~V%VXP@-Q2=fw3fU&t{6F{S-&&nHTV&zJ)(VYJ6Bx^xzI zrO%UC@TiX4c$F>WVY4SvqE@Hp4nQ1FN1IcQWfa4bj^$R_|FY&_8~JUpB`RuRZ%%!Z81zy?5l@*VGvG!BIaoKdIJ7l=faaASCYdHC+ULncEG z>gDZuW6}&?%6n3}#>?}M6tBTW%j|vWr0YZ=JAZK<$b!JnU|M!8Q9 z^$rJ>#;~HWt#BUZ-&(w7PhkcP^PwuI5;+zeiYV*?N__*wb>2{Lrt2>F3Ro6k13z#P z>=(cy;DX8vyaQYSJ_5c3=7A+(8CU_<0ftgc0DULl0E@u3+?oAb>MT+YA1t}ak6HIh mukfI?N*dwktyQMk62@-v@2wR!Zq@(j;ldi+ZO*BO*8c%y0%lwQ delta 1473 zcmYjRZA@EL7(Tb9P@o73`=T)Rfy4|GwlNG6A#lz){cDrtoLw(%lXg2n(qwCF-b(p_{|ss( zfSAO5X?(^)NG9s`RK;jicL$4pxAt#+rwr} zTOUoyue+$061suE5!cCXnL6B*rsQ}|j`A}|CJIdqmWAU0cud=kI zM_Od1`Mms>ySj)Ntxl%=YgSayRQe|Dn%Ao7b-(Li2MX+u9fd7fRm=N@Pqt<(4)$t6 z&AJw0-c`$Y;@zvI#TY&O@w@E9f|l{+JK5|Iq53oezo7%le}pM^xx&N0#le>w7oW$a z+gJd~XLU+hO8IL}tZeULpLbyh>xGd9=3vMvO&PS`uMH8+8+U6>IzeRd@USlK)i;Xr z{v7iIFiUin6~ekqCvSsuIf|+uq;Nj)QN9g6^tMdl#*Ey2gQ2wCfwg4cS%V!rts>o- zfAle8O9#7>3tgIg1HVENM$9Xj;p2vT?sfqoiEPxFk(W>paEMJ5R#dke zA$+$HfC(h;={U)ghK5~bWb=;?n!K;t7d9f}SEQ@|&{RX~@BDE?4rq@Oe*HI|t8)M(^|e!r^p4~NOmFg^vfeD=O=vuV}L zuGs3UF5(-~ndp1}$iQewX^SWdxrW5gur(z;8_S^$j$VRGQTK&LRD}$}-2umGHdS)` z;GgjS0P*b*ABATecn7e9u?aW=D8LXf0lWdc2b>2k0GEIa@D0$7VI_bW;Ct)r%a63$ xBs2T8@;WZ|tIjsIa(NgSSTSNlWv{{ec1Lihjx diff --git a/src/test/samples/golden/inc-net-release.app b/src/test/samples/golden/inc-net-release.app index c3f2172bc1762260e6da09207df60b18476caf25..b723ffeae724e10e320c45f6b24fa5eae87dda9f 100644 GIT binary patch delta 1560 zcmYjRVN6?97`?BwP@rrqu(?2C88*u%ENrsrvbX?Bp~Pq&t!XI;mBEvi6{u}7b80|K zC(TP8_um|eHvNyAp4KdZ9ynHJ!O z8uvpJPi-B)VPY&De*r7fCAFDLR%eO#uOj5;FB!I1IE-z=pljo)Xk~2GqMjpBEM;^# z`HbP-ZF15+CaapS|2A|o|HaVia=VlEaap}gb{_EqHlu^a`>>?l83Mwwk;x>k(y|Oo z874%_Zcz(D&4qwCKU(EXeqNK|v7Dm@+Xlaqv(vEmF8?m4(d0#WoqU(kHtOqu?umb> zNQ;^zAGLz&Z*#lEF)fVUpUUoTgC#Wv#hf&HM1{_yzFjB_$Dlm_{diTT5AZY4Ad9ti zt$pcu2oyCeG5aKD*{-Y8B@|EE&%6B3kfK-?*Vg&8(OUD7E=NGjJ#J+6i#Q}T- zlw^jnmMM!Oa4S)yZE}wPxIg~VDqWF5qr)z-gaE2sap2ZZ>Vuy#2U^2uk6U!>EbdCb zCo%6)?Mrx-t>j^~CDNi^r{@kJIi86+()MK(!;<#pHrfB2=3wQG`K1+kx!xsx4mag%t!lR{YaM6@~&z^Lh2!uQlHp@eW<$K!j4ZH>0rTArxbRJUn<;j4{j~X$Hg4JP*^8@!?##=)cdgahrm_fcc5_7 zG_3RlgTuY-1a{MHqT!+5o`}*r7-A*(>W6?l-fP*NYd}h!k)O6yTd&~HPs>KRj}7;Y z1eNBnqOh%S9^qeEyp@k)h8*TYRZb=HNN_l!unQ>lWe_)bQ_-33Ti~l^gs_e6RF8 eSITRpQGTwx);e3p*!K|9XmYnZC(HH2()wSjdt>GR delta 1463 zcmYjQZA@EL7(Tb9qd*Z9_C@({8xpgbu#KUU7{_KWlpk9Tttl|pbp@`ptVr!G20s=g zZ4J$(8RTSUBXQX@(H~0;iZ#Te&^$__nqtR_l>vQhy5>N8vob$fV z`@A3b+?j(bPq-YN^1e%ikSx}t*eM_ZXNw8R%iUYCi?v!{>^pPkD`xOva{wY*N%#`1NF?PK)x$a#^eOM<1E<2}wYLuL_+oavnAty+hY>mxZDL?kFK}`e@ zlbA1wPg@AdBwoc$#L<*~q?+yztP2wxGi|OK$?M=IBQ`M~t&XkRw6q9{&F8s2Y}T~v z!Ib=(i)tyM8~6)x?d(@mo14;<9M8#7ei6w;p^3g+_C@s1o&RLB$wg7j3a%7v;iev5 z4~Mn|0g0uO_+H^lUA5k0F>{lN+-0Adx0&)b*!Skcmg8uzi`C@Sj&}PH_v}52nB8;18J()g%(_|`IN3=JEF(sdO2VTI38Yrb)G9**-ct#!< zbS*79MR(_s@aBdw)rDQ6i^ojpzFm{Yo2i?BmUAUSpbPICtjcmw8f6}f*R*4Ur7d03 zA}iU%%YMA0i-^(cWXiu_h53yo=U~^oR!y(_Z3jD+Z-3}CY{|-6-Ya~vHDhtGSMqDN zHVN~ATD~89x0V)T^zcXSu=n$uM%Qj64k-T)rr7mz5B~}WUv6A{9+ytz z0Z=}xQ%Y0HpL62LcAxZl7Y6XWFw(#r3^}C3PZ>*8MhMN!_H zW4;GwvCgtWSeI$%EpV4&ig*fcf*I?)+pSVk(+Nfl-AntEZNuAVB3DHNO$pX zeT;ae{T<1L4ozOcS4hH$c_lM^)Ns$;$|od|jXE>(Dyk$Yd9|Gep41giu(5*js#YU} zA2I?kf#hwy{pTj&{IO0AX40ovH44Wyk*KNmxKMC1O|C4qo zJ;VJDMO7j}e<=8}LelK_qAg{5yc$qKp;je&I2C=n@gd9|{fik_M#4BPKpI8hO6guPVKRVe$ivk3($}d)KzZv|(m9 zYvQawxC>!k+vv%S7DSV71#3KPrW>2x*QjY7r!KP@0ZfdBvi diff --git a/src/test/samples/golden/inc-print-debug.app b/src/test/samples/golden/inc-print-debug.app index 617e181aa20548dbe32a30e1017aab47c9a7a774..31b448df6805891099d14559489f25eb2dc967cd 100644 GIT binary patch delta 1570 zcmYjRe@t6d7`?BwP@rrqu({B}GHjM@u&~Ld%aVmLDL-N~udTKegv#Ja%gU&2F>`8Q zLMP2jH^_ByQE?hwVl?|>K{66E*92X&KQz%r7x_boLA^wbF>!N(te)?^5_gl+ckg%3 zckcbZcVEAI?9xG3L#t!odyKIR#x6W%Kmu+TF=ouJoA9JzFVLTr+4)osW8?XZjRKTU zV{iV1t5O=C7<89WSQcL+IPyhtV!2~B`RNWaczM)O_%(8gu zzAlXikP}aB9KB^?EFFIZE6Pi1Gqt1oI?it8o!>i)v)tE{~@Q|S}R2y(0r|K9x zBD=*zmWhXuO6;5bXX8PsiC3693`;BgRg+IT%Ws=R$)$g^&FQ$BSQearn|olW197#( zte^J-x)SM;3ueZO>3na$gIQZ**4KxNQVV6mW*1eI;V`F7B&LxbrUZRP2{cG(poIN) zv2rhyX;-JIpC0i$7I-O1<*$9I_D(-PlecH+3SxasJxslp1HT~MD<(BvsYkqlNo)44 zir4i8T?kp6(j;UxLtg~kI%d~x0yBtYtz5e9x9>sHecDX>Xq-zt3QqB%#NnGhQCaAFT@JGG;@@HDC>(KKYMzMH>eq{jQix=?z9>N~AH;&%Atl!G4n= z5B2i4f>CLT&len(Zt}{)L!~Ql(K35pJn23j411$&nuiOmJDm6i-i7VL1J(`7vBBO3 zC8|V%U7_G9h27y-3b#CjTg&{>fD#HdDY5!+n61EZK8Md0)=J;-4VEo+e(e1*a0B=Q zC|oyYO4MY`o5v9Hc;udcxI@fszd=)HR383%fOJEMzls~m+!%>Tr$HQgkxn$ib mz07x8Yoyb>&01r+S_Wr+)>>tATJ=AAxUdFKtLvEu*8c(b%w$Ud delta 1473 zcmYjRZA@EL7(Tb9P@o73`=Wg81Bn?X>3o84HbHGcK8nF`C5@Jw$%YXmlIX`kZ^aWH)(o&w1bH zecq40x9{z|{09v^tscuUJ7|L-BFgHLq3E>wee4_T||h8-y)UQNw$LPqwBl4)%Ip z^_oUu-ciH1;oYUB#27vN@w@E9yvDJmJDJP?q53oezoG-mzlSM$rOd;>!NHdu7oWqW z!&m^yXLL$QQu%9EtZdgopLc!$>xGd9W?{%GO&YY{ZwwI48*^(7IzeRd@Q^O<)i;Xr zo-FeNFpG4S6~ekq8*helDT1mWq;Nj)QN9^I^tMFd#*Ey2ouRbUinV0lTKikKSw*_j z|L9}HmiDzL=G!&-CVqtkjF?v<&BqM)?Cm^4;+cpuEia-zZy}(&EX+Z z&HM#BEMJ0O&!!7vlFF(Jo1`h$UD#N58CE^A_tjDNV1KAxB}drBLVN8xEc`*po_R>x zp>&V*ITTe1_xpqWhZT}ye-y4R)#KHG5)8H|kv*XhxhJtI+jh?>XycP3K4|Y%RMp?7 zG!2iES`-}uCV@|YYk)2hR{X)>a1Ysz)L3HTQzPM*d;F@>GZZ2}!uS-_a@l#?2Gfd} zUA5I!T*5b`Gtu|n;ogy;(i~P4as!E`8Q zmQI?NZjd{(MP<{diP7wj1<6RvToZK7{?J61xX2$u4C*DK#>CB8Wc7USmFP`Q-@V`Y z&bjyd-hI7%@VA4G#&-L@cNk+CjNN$3fCStvV$7IbH{(geR-iwtvh(R2#wPL^8v{sB zV{iVXqe>c`9CnsbSQcL)82!9BzS1?9_+%#;ygNSm!vSf0daLxPR6mX+)ozX^&9XT5 zP*08dpot|nj@>aamWsWC73mV%%qgog#QRnda`7{U%@r-iHet}Uv1Ft&x?)k^AW<}F zbUOI7VaFypVIP%M&DVDyI+_1$Xmz?=3Hz9=-XJ@-_#T_lL1TSb((Via(Xy7w#Bb5E z3`-d%M9VHw3qs9>fH)so zonkV}!~&2Kdy)TWd_rpGm8LGka+<$t@<`|SU6UyJ;BRenIFAv12ZiE2{V%_$Ql(@2eyK%bF-1|=Fuu-_)q z4>Flfb%y%s62D@Bmy%Ha+?VX^_VTlNdq%DyHo(-w)N9%I6Xb3&rR9~n#05;+vu{mDIRr-9Gzl!HuLHL zJ_1TI!&u9dMG?5=2-4QMmjAdv{?aO4mO-P#PO*pps$6ctt)I{bKX3N8hR`0D=+;@> zm40`8&aK)P@hV%%!)lACM7>VW?nQDe6KP4=mrx8#*q7R5-%FZm z>g62;W70HVC^#zJ=2eA3o84HbHGcM7^7?Z^jJw#$=G`fws`kZ^a#FIQZ=e+Op zKJUjp_xF9X`#i2rdDj_2NEYi+>=clI6D5S?j_pQ0}c`Nv^xd0I_)=m4g zXRCBzdemDf%#>e&F@Cu$wd!BgKdKV~XWi33H%RWejnZ~$zZ)b?w#OIjl(*e8smTyx zk_)AY85<#)m1%~`fR zsLQW;sHO|u(BFt_XTMrny_D*5A}2@rERu;rlYP1DOX#1w|IKDoOQM+TxKgl%oBDV? z9NKyWB(Ihxb_$<<(SDc3t&J9Phka(Pvv)<4j-1X} z(Z7%KrwuETAIl|Q#cVUl2yUYFRQd=`C-qnz(cTtDT|VUvy?_rjSVp;IO6u}tMxGRO zEiF1lcc+l>#+o_R#ciUC$1Lc+U6UsdP%r;H=Sqe_7v8s6we1OMocU~i%a%2kwslEM ztTdmO|8(0B5u-K8lz+*J3K~k^gkAI7HKXoxF4k7yd}s)^R8=kS6+YRXvANhQ1vMKQ zg?Vo+-;KRnON%l3_#?O32L+AeE4Q-QK|+mbgnmH>lz$IX{Bnhle~m*RH!i+_OQ-n& zD4#PZWxDd$ym+$RM+5%FK|C*vG&B!GPHD=d{dR4TX#Rv(YcL2Ri;qVPali4RDDTZN z-v_hAVA&yT$h7ljI9Fn*`hE)ceIMo9;X`jL6mHDO%Qu-yE3J5z>>K+)>u$S9H~Wt< zM!eGgj?`j@CSS)_NWqACr80cnbkE-`AS9WMxij)IswBF++)hJJ8VX0)L}5jBiy6ZA zn*o?W>Xy;|b0_CFun!9pJ9gz0asx8Q5%nn`1{CNCs?8(OVNcAX^be>pCE6SvCN(Tr zwAc0p`1Ne2C?Tn=rnpI(WjaA5v5`*snB= zjFCDN9Ra3*kAbUzArV!A;qXW=*@M)0#3Z1`qR;mRRi$@0LVkqtaj509_Z(X+YgTr} zv7_oD{vm^jz7LJ`jfR!xsG^W-Nc!k*E&RJhIR7nU!VPd&_ot}HRi$cZv59=yG^8f$< diff --git a/src/test/samples/golden/inc-resource-debug.app b/src/test/samples/golden/inc-resource-debug.app index 8266be7e48e8fc3eff49e746f28226810486f12d..1452cc04e7085f7d7e73b467c6762d57024e602e 100644 GIT binary patch delta 1570 zcmYjRe@t6d7`?BwP@rrqu({B}GHjMjSlDFKWy!)&%8wY$qctrBp)z>VLK(F!W=;)g z>7;q78@V$sDo&#&i@HAu5{YKg1YNT~G|?q4@`n(EdWo3D#LWq^dcOBc+)Ym3z27?)(MEWSlB_Em9WwR1lC=^iq8Z+znC!_xTlPU!)uZX6=jVUDHDvUuXI zE{*$<6Hjd&yKZ7E9e)uk%1df9m#oeZ?_Wj8&0jWbFK;%s3WJ^-Pem(Ys}}WjN{Xe7 zE+?Nh+`mmuI!0wx^Y!0Gp3HwUw7A^vq+?80uaKQb{DjTupz&TTX?KQzXx_+V5?5(i zhNTP>qGh+J0iot1fjB=}KH#R zyTnA6i3gEN>>KUk6hiT|Ry$Z~w>?V*N}#M!lB(zarfuCN*8DTb#$FE&Eo* ztNMa2ge*>L60(}5F9L3z^P4vQSwyl{PF?r=4@$^ zd;}yi!&p(A+fpEUWVY4UFqE4r04?rBxM4Qu&Wfa4bj^$R_|Geg4PEnS0)mf8E=vIY+;JanE%QbEN-)@@MC(H#wg$(=9KKLkD}BeeSa#I;u=hv6Rp2I2 zxM>W@MA&J z*3evjsb@!Yab zy0%LDCWqY>!c6&97-I{i$rbN>>XSMlaKSnGbED*(-Xv|8_Buh*WNU2BO8KGx3~C~P zn8aL3e9A&dCh-bxB95l?Bh_?&U`3eNm}!00NL~jw8L^4EXmxDGrlmztY%b63Vbi8< z52oZ-T~td6-N4_7YiGZj+T4_;8EM{&tkvnYGyvdZe#(psGw;V=$U92XrcC_1vxM%N*CT%&L z3!;Ay&f&HoKB~rbwqnh7*p~Yci?&aP=lqEONL}h9?!_* zg07`Sr|9ks65d=hraHe(bn%!8-M4G<_&)09pXXeOAn3yTCabbENu$hT@tU@*v9zU2 zT4W{bc-c?4brCUIolNNmrx~1$xH1t@T9JAgpC!HSG5`; ze6JCJ2_#qb_MbaFvypvV5U=04j*y#>If7vMLrslvFVvYMg;(j@CCYA(M5s~*|+!if9$K)6FCC)uSUd);O%{87lBzfans z^bGeq6jg}~_=5v4DJ0GQDB4h_$EyJ)7;IId`@>;!M`G2s-JY}1#wSO7&_1ZBs=r@3 z5E>zMC^`&G0G|Ta09_)Y_=CYvFWHUMSYqN+qmdK6epTrm3X>mUd>m@)*n74urZqFW zYOAljjBiM1qVI#DzTu$K5>XU#9f_Y}>x;Xp%b*R8UV=+ep9zLkh4jPS2FF=8QGDv~ zpYVSN@f{E!hG!Ib8?b}%FmMb|fI(mkcnx?LxCmSVt^gU}E1(m@N&r*9x7O(wACPU5 vnax(b&qDTEse!$2Z;+bUg1y0VqJoeQ*s8t0(r!2YhfyVQcsgA*_uc;odYnS= diff --git a/src/test/samples/golden/inc-resource-release.app b/src/test/samples/golden/inc-resource-release.app index f14cdcc4e44bba28f7ec839da0c86e2b162d6573..6160e6366930d0e3c217e90f88380ee35e2016a4 100644 GIT binary patch delta 1560 zcmYjRe@t6d7`?BwP-xj$U~_@OGHjMjSlEO)vt(f?(VCWmP#HXFSsAr0W=;)g z+e!0M2e}#-9ZsW5jAk(l5{YK63A$!~XrfD83x#${FW_1}h0=D!%)U2b>E5tr2~Wakk-U^6;sVgO6pogpCFHnQ2|HCmQq zDZ_+l*)8fosQC~O=SQoYsn2V(JeGIFVBg?Z^L85c-sRutHJQ99ubb~O+Q)od&^`VS z6=_vd%$e zU29(^5duXGOU^!lS+48qbScGCj&m;m)1)Yo#r1VQZM4;Xs0)+xR7$M@2kKBA6USti zn94DUAf&{;&VMpKD7Eq`Q;%U~jlW>>O0V#nCQ*9vZ*6lru9hqd&cDq+G}eQ-+GEzw z`z~FHOzfPQu@XApo98j>O3r(Sa8c@!nY7tO4JjPvjERzIWX4IL&qzRn5)CA1u#2@j z*=)BuOZ{|<->|?-NhyCGOn3MC_?d$JW0w&dV(L-qwe0^Ha*vqS^2*%eRZKc_Z&kdk zFX%$Z;02BqfXBqKyo4*ZOb^8Q4C8tmOEtsvzmjAHx-su7U&X@y}jAf z@Cjuc)gw67hrD9J9zJa%3yYxTnX65+kV6zfY z!hybE;IzVS@ynJS_u$qtU&OBjgRM%mDHLLBa9qse3zmB68@|Q5qtS=GKLD-)zXO&{ z)3DMP436}(6WC3+iH1-1_eGTc;SejuSAP;H;QiL!`39ua8~GV)jqNJ_{IqP8`{>BP zXi#YhD+=2R=TZKZ)m!xlX2@YaROM764+lpg3cG+(UjlK1Hy5Any#>Aomc`h>4_pHK z39taTkmUv504@OU0iOYBU=dgbR)BSYp%fE9-^rK20cP zm}I6rF=Hbnn|vKN5l407NHxPBS{5cYVOdo>R?x{UW^6JOtBWr?w6q9{X9~PNHf!1V zs4lZf&uU`|OH!t)*av{b1c|JBaqWS$#pncuxRvFFX)UI`TT_ zMgKm^pEImTek7lK8MDnMBe;pyQ|UuEoz!EEM0-aVb@{Y6^b&rkp$f_+Q&N{Fv+|^% zYiZFbx;u@8w^z)m&TkZ5JY_-m9hyA3hkE(vc~>$Fy70cmYHhouaptr6E$dfU+SV;C zu<}A)`O`f^M2yxTQ~o6@DQYf11-s_AYewDgx!8ds=VM1+KH-z?S(}T!R#d;H zMVNOq@NL+8w6qwbk3VsbeO%NszH~R28zR)0M(7uGK>7DD#jjQQ_}4fD^5fzeT)NB$ zK>4gesnC@_=fsokIUMlM58-)Xq@g(&@=8-C?YEmlMDr)STC+hAS$sTVi2IEfMR{MI z`5~BP2Fng%L#Bhb!nqVf)elp+ANnZY1Rr`^qHtqIUcSy$T589$WZ&2a+qc<8y0d>9 zW5g>R=uFLbYVs}o3Mm*duT++go9?+gMT8`CF?Uv8M3qFB7dvR^8AIU^n<%cTZ8Jmo zZZiNANZmEsfAQqp8um$XV)NEQLT*FmIHEoS#DD_5LA7-xI_!yglz~AtrbJt#!=#=C zOLo}40KbXNlq4jT)tBy*rde-kOVw3ajmZ9&$Gk@eBb_Qa&MuZZ8`t5%ABOC?|4F-) z-q8VBAcr~Pi!);1zZzMwQORUbZ-FF7s_~b|c+WQq%4Gt*# zM#e}ZijDwNz^A|sz>tV4!Ekt_k8DS3JYo`1W6@)MK~?D+j*uT=d>U$n>;uPo%Zim< zcWkb?jBm(bqVN49{i9)}HL57&CK5l(R+V+vRYDsaqXdtlJ|7-Y6*2&KI~-@&RN0Ax zf5878#J50v6rOS5UBC&(L>0fvAH;7#Ct-~w+d pR(7TOZ5D7gNKGu^Y?5}dOU|a6W7YVZL}6mNd|jS$r&*}D{|B#&K!N}O diff --git a/src/test/samples/golden/inc-sound-debug.app b/src/test/samples/golden/inc-sound-debug.app index 547197b1c3ee25abe848ced0abc070dce65eb618..11013d19800a375be9eba3125f416b361379e24c 100644 GIT binary patch delta 1586 zcmYjRZA@EL7(TbO&_dZDu#fOHht0AH3!7}ZELr%F4v5iQT5Tx^mBE!3%BXEIb828h zC(TVA#FK_CTYr|P=hGRCO;{Kk14vI} zpJmcfE{#kMIg2PPi*FGe{VFfE(lHnRViy^lAD{gBpfo}1ss0w1QykI4*!}tR?lxFbV^GWqqg#~fJnGwlvTzK_^F4}JW%?99eYLV! zUDMi^j0Qnb!(ubfVV3T?I#odNxc#it_aZ6sWN~ASPZ>)pFX+P99FlDIRr-9GzlMI`d*5 zJ_1TI!&u9dMJ~AI2+}sVrvJG=|JEv9mO-P#PO*pps$6cut)I{bKV$YchtM9E=+s%< zm0ovj&aK)P@hY3iqr?_ViW;4s-G}68D$<+(_vvnJ8E%SzbN+8gvMCyV;whG6E3_hPzEq%u~S+~}BvG-@db>I&m zXTvn4bOi!~-K-zG={Awj@$Rm$(mfDl1^DWZ16jP=x--*&lxibCZLKJ|hCe?o8|6MV z*fSJRj)oM4ZHDs@|HkSmdlEC`FfXcdDB&jpgJFf8L#eNWxXBxG&vf1aUjfTpY~TYf zg1rpP15RXlfOmm&z{kK9AOS1@OTaR)1~8Oj0_Z#W8kh(2Ez^59)mx=39xS@Rw-xV} vUg5RHRnjRQEUwDGT*TPt5YlL{*FW3H&s&NgqT}`!>wo+j<7##EKd}A}aJXiC delta 1489 zcmYjRZA@EL7(Tb9qd*ZRY;1JEK9HErgnglt7`I_Blpk9ztri&Tx&l`U6sf(%;Kzcb zt)aOzgPdqK5|>RA{edx4tRYcnGZq@ZW?Z6+F(%UyJ!Jft(dag&^*QHu$xia*ob$fV z`@A1Lx1a30@tDieA@4d%2+3kSj9mdF;AAl&dAWNFcD|MhjD2hFJZlCYHV44t`MPPH zc5RgoOpmxLxmjVCVT@iXO{{nqk{{J^fpgC3UmGOn+y-g8wBHGmE?c8%tHKWdXHeq- z#KhAju^9^?nfNQX@i-+Zj*2e)ffa6|hPk?QD*O;7QlXgbgBp>w8f zYm@S;E=5mr-N4_7YoouLTHT70lw&zLg`Gh%UTC~GmwgfaGv~kAY+{iYa}8GoZ0=S( ztR4=1Jp$raN@6>?FL}XwmqyKvCUS>>6wZ4-g>8oqy)7$nV@7Va(NJ1$#j~W}TKijfTY0)O z|A;Z-mG-qK7TR_B8h(WYjF?v<&j3CkU$W=I{`y zq5i_XmM_7tr!$2yNuxDIP0}>&E@~{l2&;(fePPr+*dJ=w$O(GB$X>S*5B?Zr&)z5P zP`gL^9IB><`~AWGmsFCXe-y1R6Y*L=4F+4($iYyE+>vOtZI9dD=>onX!9?GOhI>bXYI9gs$yFqNg03s>tS*B#IHClXsy!7P)>P65cPkvH>16TA zBY(pGJ;ZlHd<32`;4Q!o#%AC+paKKHIPeEU zmQI?NI>^-KxWqk6-!_xT7PU#V;ZX8Lf!xBqbWbxDk zJvHu!CZ5_lcH6{QI{p$?q)Tcum#oeb?_Wj8%|{K}%bSg@!k}y8sc1!P)vCThqFBo4 za`G9&!`tMfV?Y3o$Nf~2W&RShR1S_8SESUM$S&d-Us~qoO+WN<#q5~M*Fa@9lEFg zp(3qnl6=&1s=w9k634YLc7HazyB(I)7!-5P=n>^QkNS3@EF6RK{Ey;QnLff#{|Q;F zt!wQ|$3vi~VTsviFw1sbohqhy(s9YPr6iR<52iZ0eEfoW|L`GLG!J*r~~ud;nG9ys z%iHtEq#3@Le^R=|D+`X5tieT7_C9~kbtVwT6IU zN;uFR44hZkT|Ql~<00Hy=8O21V6aJv)`voD4UUUBe4(IL`i5_@?x^!&?~j3-!0$l8 zrfFE|4h9E%*ct4m+eE`>d%7b^&p?P3%D zUIP{Y7qYy-Tfk-D1K=|t1uO!~zzVPqFqC2f=sWomSOB)=&FtTD(khvGsPqaiEZZx+ f$d8oONTd98S&i*lDVhNxjRtp{v%5?`EUo_qN~vOU delta 1463 zcmYjQZA@EL7(Tb9P@o7C_C@({8xpgburG8H<2LMt@?*=PH3i1HuE3Q7MQU#`_^}{q zyU<*2HgclbNL+>{`eTXN#4h~MNyb9s*NjVaF~($ZL=TZ4GaB8-v_9wFF7YH!&N=V< zywCe_&z;zJ>1mIvQ{Hu+5R%1u0y_mH;8Y1AdAWNlcCnTV&3$X`e8CDnY%V~=i*?67 z?b#|Fm>%_33Nz)`VT@lZORf4B^^fa?z^r@v=SImrw@KPA?RSHu$@chyo$|x~n$%$$PBh?IlXjPclgk^p8SY8LWn6b%)SWSG@p`}God?C;4V{?{m z>vZ`|57l&`8~O`z?d(@eo0n2uPUPe$KZj(Z&}3gO`x5%+?tik`)RHLX7OoU*;if*` z0Ee~#0m&PsiJiiyU$H-6ach%>+-ILzH(B!5*bml&wj*e-i`C}UjduqS_re3wq$8&@ zEBg0Q{+wY&^5ePWtC(#j8Np4oo=P9Z>9iiJC)zv0sLN-(p_lQY2FoayOi5jy%*d01 zuBAn%=-d;1Oy0}er@uUUaw`=m`0qW(S=UmA!=)(IptG4Zv#+lFNw`^HsXCXLa zj1jN2zazESp~<)K6;d!_Ua1TpH{J7h3J6JNWA2Q+j4FvPFSpasGls%ZHc?nn-D-yL z{bm3rkh*KM|KjQSjqIbs#ExD0gxrSA2}FGghyew9f@;f1bl4N~DE$L!Oo_Hche<69 z7VWit0e%CUDN0Bxt1WJprddyMQ^i$SjmW-N#=JuVkq(udU>A#>^_%hFk3shQ|D;_? z&uG6(QI+UGFg);@LelK_;tl0Syc$x%;Z`MfFcKm6C066u<2ws&{Bk4!?L&&H2K$xf zkug$_q9ec*a2dD(7!pw>7!Hs0l08U`M@#~0Ec#M!P*r+|BjiUIpMqLG`@pfqvSwx1 z9XqP7;2$!W==;z}-)LBAi7E=YiNw#c^(9?3<Tl?Irk2E_Z oEBmB!mIa-4QUg2bY>*n+WoJXxP$g7Qm{=}fr)QnhEL7b81MuBI4FCWD diff --git a/src/test/samples/golden/inc-sql-debug.app b/src/test/samples/golden/inc-sql-debug.app index 0d42a9778318a1c857eaad4a6fe2fc3f59b8c198..5da2f19e4a273d2e03b2d879a085ea0579c74e5a 100644 GIT binary patch delta 1586 zcmYjRe@t6d7`?Bw&_dZDu({B}95%})ENrqFvSa}!(Q3;N0U10gtUzsxnNtG_ zois0XkUMoj5f?Qvn*FgLk;Ke3LD%dLO>~Ki#0VPHi(-t4n-gU9eD9UGo1DITzw@1Q z@Atj?I#uy&g{!8?dFXA%SQ2ADo-!Z-H?kQsrq*?M(r{$z&*Id4HjS|f3u9vd>1iCa zOuCAtk;x%<0flApC4!@$KyUDHH19;MZ=E5dSjz7=-Nm;Tr|37RbM61Xx!*_ z@ma%txpK@oBCDFO?-q12|HaVY_IP5>FrSP|PW#R}|_z>f4R7a16@xJ%CqbdLKW1RkB#$ z(At-X1VB;4qH|ARmg>4Xok#JQ^Ss;lG%0dqaczT78||eZ>cZ#(l~QWJff`lkc$MrH zlPMHR-a8b&U8MQk^2`QZBgo%=ABt}S}&qzRn5)C9c>=5g> zlgUx&<6mMA?bc_9|%!_^a z2q?)6V=Yq_+2B^gNZaD-|L6YvTdQNpCu1b|@|bn*jBx<3wkub}-oQ3cHk^-cVQx)&~2Tm+#3uY%*k^ zUfz;9Ce8At%;VBEUYvC_e;qEGviq4+?vuR%Z-~wDK$dN9Bff#RV7q*mb&YajsHa8= zDZ$=OfA499-Q<_DcHVxqgtOHfKl+Z)|!H~jcQ0hw{uJfAgv+Xy*m%uU`8~A{W zU_S;H0XMRmfhk}Hco+BthyzQ&3a|=n01Ty=0Qyco2Nr=`%j|(|RaPm34-|aIYi;|a v=lGzlOd94hwzAy00>*xTkVcL3_~Ui_g2i?(9rrbx{>QH|o+j7SUF-h< delta 1489 zcmYjRZA@EL7(Tb9P@o7C_C?3o2NJWHu#I7m7{{;|ipiEks|CiouE3R+6{)?&;Kzcb zt)aOz8#&QzBrcmK`eTWiVhu5IBV(cQYsMwI7+o?Q(SwpWqtR_l>vQhylAYwqIp=+! z_jx~hZhvi@Zge@iJx-7`*&3U(QojE`gPI5+ zCNWnUpSBQ^NxXuah@&a}NHyIbSQaKWZdzS6n%BuqMr>j(x-qtF)6ya+Hkar2uo=_V zRVn#Z7u8ZiH}E&&I@m9!b~mLdIi8cF{1lRjLKFSD>$p;|g`0YK z9UR&k1SGDM#&-x`>b&(XinkVaO>>8noZ84ie29cWVtgL1gjpurBV^Uliqi zIp%v{mgp=igmsw?-U{bZ6jk3#;k@Ugd>ee|ZHdB-8M*m-LusiU&ysy*4Yu#Higc&` z(Z`5aI?$P%@6_b$_!W{cVqVD%A2ZyuHwy?!WTVcEyof4EN?z=sfyZ=(gKWI8qN>dZ z;d_h#OdxqnZ~xijvuoK0h4Jk>^9i{DnWKpM1P}xA_4?J;;mD9H>QV-RYE+4|Mutc= z^B3*5daqDAcAz_lCpdj>IOCi+jZcpFpuJB~RsVp} zGCWFZQFIuX1U>?;0J=m(@rOdgeWV_#@ra2}jYgj9^Q%hVP?&rVnt+&>ahS|f@=t|IZ{Y;{TZ#&T$bqnF@P)F(s3szL_fZinL}n=CnY z;7|C!f%p!HkH9kqyam|7*aRE~6kre-2VMi-0nP&FfQvu|_yXv{uoA#D@U?a3#Rr;g zl9_#6`2(A_*GP5jg1t_vXTRI)Rs|~wxrV~Ta>y;uw6Pxx?VCiYt#;#oS@(FlTuOltjM;Syo-}L)`m-uKPv$T-mCx7&KzbU7 z@~0hD()jeKvy8&B_zJ=3=f&~0?uEq1`^ey($>|@CN|VXm(tT3>B$8CSIhr!d;<3AW zYRm^sEVX^&x{0xL?0Kw6m(XTTS)C)^w}z05zi8N5(Q0fL23;FVMJl6f7WGvUMN>wn zgC`C5?vNAqaaq-TeYc^L`7eexr^}VFPsr+JvU7`@*o+Pu>&KFIX9$Sa%}geKm6l~# z$}k~Xc8OXLYAyuC`Oqpy;*9NiwlQxfbdUc- zMOxGZ`KT3Cf4j>mPH185{$zG{8!V|YDCV@$Eh=;#_3c4fI0ohU?#HV#eSn|A23f3c zXzfeKf}p5j@wq23%XVFzEunbAKI8N~O^RY!T-)HYMr+MSx-h;#rPLU3pmxT)WR!G-GcZ*pquhb>ZW73g*tKwyS zK^H<6XS5Wunx`)UZrux8Hokd8vQ`c~@7M1@(tX-V`)Hg)JOEDdsI%ng6bG`I7YFeX zP?8zOTBakTV?LqS%8uYMTF<9(LBxdx=v8TnaDwe<@A{IqP8`{+pj zXh3NWDGJ*T=TZKp#Z&nRX2@Y)ROL{@4+loV3cG+(UjlKRHx-@hxdpx&mPOdW2V4aE z39tw_k>vqi2QC2b0iOaXUcnB(d6oI@N)gIwEh>-QDgo9 delta 1463 zcmYjQZA@EL7(Tb9P@o73`=Wff4T;%I*v8OFjAL6blpk9Tttl|pbp@`ptVr!G20s=g zZ4J$(*~rPvM&hz*qCb|HDb^4ZXEPQWzh+!A7d0k}5IsbG%xH8Q)B2oyyTp?`Ip@6Z z^FHs#J$G);(o-%+kG%6dAtZ|!k!8~c|0`Jx$o*c^a}7wfil z#UI zViKA1_^gGHY~nTCL>x`&N2=-mz^X8@Nz=O8@q%t{GGY^%XkBd8rlmztEK}h2usPG# zhg0(FE~=%3Zs0G(b+KPfoo-4~ay&0b`8gyLg(e2`*%#42cm9*hB^O08w{WFk3pe%f zCOEY92uNHnkM9t^)Me{^7Bjb+$UXL%d84UdjeT$4XE}`adRcuz!$hACanIcsP1^E0 z7exOa%AeJ(NPaAzd=<0JCc?Oh)|2TYIGsvG8;SOoFs9@)?!b%qP{S3JONL}hp32Ho zg07`Sr|9ks5-zP7Q(f39x_H8b?z=R3s*Sq&=XqBm1iJ9P$!aZoqzUG+cukwvSlZGn zEwb`LUis5qT||slCsY0N|@BKsr9VN2FD@B!hItyznMy;@Yi zp;ef7H}GB9`?RzeqlZ6smwiywI?20M3IMY?l; z>tn<#9qLXlbZhc0e1#;8m{&5(Ck*%e?IJ=Fxu`QMFQZD5l9#(^;Avgq2%9Xfs_igB z_+BFb6G-0C+kgJl{08=6aeVvELPC}xa{^JH0%AbEe!tp28X0j#UCK~UjVh7$$Ox%t z{*v96FTihNvn6p!W%Z@|r5V;=+FEr5Ry}g?rE&N1V7OZ)C)lM@d*dcN_@j_L|37Jm z(myujP*f!n^oN44C?w5(FI``$$EyJ)6zWi-`@&&zPhxepX3tq@1U$ z&9=SfGX5c*iM|hx4vvMC_K2d8n@Ic&TUXXwR|#!!^b%Z(dLT5aDr5-mPB_l8>9UiD z|A7An#CJe^44w(#ZNLu3qrfph0fvD|;0@qi;39AdxB_H>uYev5D*?;`-&*HheyGhR onb{}RHyE=wNKI_s-Xt|MZf~jyRul3A3KPrW>2V#f8-N z%gLt=_imAsj!{|FeEqkOC-YwnEiSh^=@^sMOJwH}KVUODXuKCo+MOXFn%6U##8q0B zVJX9eXxS}lK&ZJ$AkL3gIg^*GGdz}a*kE7hS8}!+cHiOO=hU0LD6fO>Fxp3a?Z|uV zUn7=f5AX%JdQo8Ela8}4|5KDvEQ@Pve9CC6{#Z{;%uy-T1{|nWb&MaC zU1BoJ#DhpB_BH;K@sQNSD@>h+WVY4UFqE4r0_dy)bM4Qu&B^1Mwj-^)F|BU8fgDYPW70HVD0oP^#w!aCm#)G^%j|vnr0YZ=?XfZxa}U?TIP%Rm0++*iPncgY!!|RIefmbR{Dl-vTUpKVegNCtH2*X z;f85g=?(^mde{lRA{bPwvv4+HmWGpnkW?Z6+F(!*6dWgi#XxxKo{hfQeWHan~oZZcvM3(=a`s!dCapx8p5+rwr} zTOUrzue+$061stZ5ZB3mGj+HrP08_`9OY+`Oca_J$Yozb|J?a+Hk(`$#azdgf-T(C z!yDny)*~QswKTq6_)_!Mdn{&dF_F9MbMr=1-WvPS+-f<9_Ig-tUfp=F4{^`m6HVH3 zI_E|I9?GB9tw??(mwXko%_PFOiPn?pLpYsGMeB+7mN2H|)9%2F_@RPjluL$WN}kNf zlY*|LMW^WQG!oueGp4$@Rdn%$3Eg*U^5kCX=3nGoi4f?*`zEWl?3Tuv$Ko|@USnxX zkF>-}^LhEtcXSakTAfV!SFEU@x%3q5n%Ao7b-&|a2MX+u9EB}eRmb~X%ZmQ!u@$S{qVvHXC*d6v^LCg5c?QAwks6LIrujqjCA7F}IuJG`0aPZ~E#TRht zHWq;LS)EdrQvRM3E8Bb6=Uoh9y)e?i91J<7DTDU=wII>Faktj26GRpd59{JyeWNJv z&oSQzvqWcEA*{=E@-{eEqNw_Q3g>+v<=fyxZz~jT%*f3*8A>Z1SWEV;b*N*fRirz6 zOCKY)bg(PA*rmzW@hc=@#JrLjK5n?@ZWa)d$VQzRc^Orbl)T(Y15fJ;huB16MRmIo z!uJ>fm_YKj-u??G=Qgm93gZns@(H;Cnd6B13=jkI_4(Ddk;t$s>QV-W)Tk0^iwu)m z<}Yfpd)kD!(s9hj88%>pS@?>Y+5t3 zE4GHJd3-}U6MgR=85j*IZ4pHw*O2%bwyvb7rX1Sf=q0!m^|{c9s*pjrJK#9Orb!e1;?2S?rTeLS?f|ZD7x9km7m3HHQ7*!I7r`uKY!2N%0CPLl- diff --git a/src/test/samples/golden/inc-xfer-release.app b/src/test/samples/golden/inc-xfer-release.app index 194d5d4f52b97fa440eea7e47c80350119fb00cd..fd42560c3c66fc3e27518919b4e05ad67e384f9f 100644 GIT binary patch delta 1560 zcmYjRe@t6d7`^Y&LV>cez~%ylW!Nm6u&~MI%#wwnltGO8XqT3PP#HXFSsAr0W=;)g z>7;q7gWMVC4@9FTMzcQ_L`lqC6Lih~&_tKG$R9!s>Lp?p6E`Qw>iOO)(VLvUd%yFY zbMN=P`#N>#<{@Wen`8ewjIj*H9y}F50&WyBX3DOQ;Yq_@U_7g`^XVMM#`75)1xQb0 zU;c!%N*b9Ma+Oh75#Jyf`?5H((m9v>WEUB{J2vstL1}DyyL6vaKZYdDVTq+Iig@g< zks9|y6Hjd(y>4bK9e*Ax(k1noOVMVC_pc!2<}b=yE1FHMLN>JVRJ1a-V%1(HQ7mP0 zIr+4F?-nKL7*RCc*MA#2h5stIxZLifV^q;DlbuKWh|TDr@m?%xcZPsyUe9C_S7}*> zr2-S86}PAbq2)qAoFA=nCcmi3@L0}a*}l%NO}*@u1Y&>jDW zinMA;^3f`&{#Lh39MQwr{mJa^c34tlP|RtQM^qR*>f3>`a16@x-;Y;i{t!R?M-{QU zrnfI04}qeFC1#$$EZcQ$vV`JE#|4-FX;Ks`;@TRYG}&rCHiU^eDy2rofm$`k*ipqL zCbCRC2r03z@t;i(O2>Gmxl>+V$Di> zeUGk0I(EUrSTUXN&9^XXPt1AyaZ&1!nXuVKH7OjHw3(7=q(?|#%t%0k5@iw`u#43@ znM{W^L;ZA%-?6|;NvePDPjz(p_}RR@!rtY<2s2;6cMX&YSgf7~B`>6I=kpwVHMSVRC-E;r-Wj~j!ZvjkegXpdWT87%Hf zpC>Wr(Hx6-l`Z69vnSG`-k@jqAvvCjHm4m+D262+ORb9kS>3_L8}dsl@(hW{+}`ME z;*Q>hr^aVJJe)sT9mr+O2E}Dy9iTq>PIO0_2Ezl+s8j6=M51cADLlYDd}qM{vz&)| zd3(XAG|d+Z9+Ix{s=~vit8mekz0aI>oeYFL5jMj^h2^_i@eRBU+r|G`H>xLw`Wn@U z8V+;^17}ori(e|-b`NeX^F{n>FnCOjHiSZK6^;uze7>+w`j&69ZmaiU?~j11z)hfV z!!)dR2ZMt>>?C$GY@*>)J>3ztXCTB%@YSCJ@_3JRXReHtIuk!@t+rjkpP!zMavvS+ z9SW*VVO3?D;XK5@wt6cc!3;UfhpL=v`cA$A=7BBw(|b2HSfxBZST@er%63c7 faeH~KG{TRU*V;ZUW9)SZX*9aqoR60qho$wuk|twb delta 1463 zcmYjQZA@EL7(Tb9P@o73`=Wff4T;%I*atWz#*Ntv<;S)|YYL2YU4bj5D^h!l!H)$= zTSIec207WXkvKPJ^v4o2#TpXhBx9lRYsMwI7-Oc)C}hw4O{K!f7fMttZ+W!kCgzxdSiYLj}tymki02Jdu$n z1YJvuPSM>dB)qw5Om$(i=;ARGy6@2Bi9OWKKg+oiA<%{QB&)VOA&oJQ#cSHQ%F>o@ zX_1xY^YS0>=ptgYI+^k>SW!V!=?U01uT|6QKJQ@r3+xXag)LcC$NPj&wq`62_DVtR zx@KW+tm8Yd_h@M`Mh}1F4tu|#d2IQ1HX9^VpGM$kbU^ucFvYG^c=%U1_;Tao^SE>w z4}kJnol=%k{+ts}w&$?VyAZ_l!bk&iFyxdb4cf2QgGBSj-CC1Q5LrAttc!c~7e#qr zj`<##B|6IrVO^$!x5Bv`Mb-CHIPd!?-vS?cTc&VhMsB{rP+D%svt(ae2itd8MY=P8 z>tn<#9q3FhbZYVqe1#;8m{&5x#|-z}tpY+4*{CxkFQH13l9xJY;7MKK5F0P7sBSYt z_--Qr6G-0H+kb9qZXNreFurwrJ|QtFnSs`imd-2+GJzfncp-`I=Z3&0TU5VA$c6v@j8^0X!L3^K~s{R3G z@9-$8N6}$m68H$X2IvwI#UBa{_mQ1QjYmv;YBchEpI=q_hQj0r7$1jPK6}@;(X?u2 zS8ZFXF5w^2ndp1paQ{e1X^kifxsJq7u{9;#HRaF-M=!ypsLzCkRfP<|-44fTHd%7~ z;2-dR3-N6bAAx5Kcnh$D@i1@%P=FvX4!jP$16%+u0+)dd@FmcNVI_bW;2Z1gOAoZz qBs2TCa*7?W*GUa5YHyGl+1vJps$eA{Cs3GJ4o{aWi@PXPtp5P^{XqEu diff --git a/src/test/samples/golden/notepad-debug.app b/src/test/samples/golden/notepad-debug.app index 2e66a186736c3b23d7d59e808f09b6f9d38cb362..227a9be4edc4b92410a67bd601f83253031cb71c 100644 GIT binary patch delta 3323 zcmZWs3s6+&6+Y*(cUfM#D54_o;1d*6#iX`YP*#yN?a$rpA}S)_hQ*)|9yXe^ivk8_ zC2DS+rar2jCY@*}={UrxosN-~8e$r48)x#c51fomu2a*0I-|y5l%Dh7U38{*=G^<= zbH4MP$AA92@RypCn~c)6o=1-Zz!+U~=_yE%D7dsDhc+*%dtV^Wq`lGw=M)gqtTeFzp=e-9$EyCV`X*ds;_Yx@Vadi#4 zg!R4qHnb;5K0SjIyD#ei!#yw18~gREl~;sMvpsl&RDN@pHYKCXQDJGeZ_i+RX4j3x z@IiL!8gzIKb3l6}S@e54MOg6$ZTBg(#eN!ZQ9#L{*sF>#*l~#srr~0y}EJ}lM z!Eex3>95mmOR=y<@2d6<_q33S8`gJv1Fa%m4e1ul(`i82u(4Z_LWM6uomCqg?*RalD zzJ@#5!CtWgUkudPVR4={dS`5GefTu@({Fu2FSIHBft!yGuHRT=?unfrIz_rI5MIW; z7QxTRz0x|Wa4CN4$Fx`**;VUB`-KNWSUVMjsm}0;AhznEC{6GTX(CjHjr;e9WaN38 z!f}+%u$GW5P!&GPj9zPQ1o_4mIs^=I5wSuFD~W8^+X)u6l|8Wi_e)jjbwOt8u-Cdu z3aavY8BP6u`!e^r8rQT^d;Hc$+e)+2P}w(B8TMSItz5$#>3My_)^gi?W+8!l#@fq< zJ=drh==WT!5WyXaA>AyAOUa0}U97R&(Vq6*QC)a?|Cvg&HLg3q(Fu@F9%o7Jk#L`C z>T26oHMedx+KoU%V_SQmbye$Ds5Eop7wcLq)oZSc@5TXhBz^^6Fmn@@ri>Dc^4z@r zIqx%#EtPF>+H6UfF{^^kz|V;G>O<6}fx3={(m-3FwXwFTac2N-nWqv`r_$6yOwlq8mh_&w5@DvZVSLMDs?v*E}NxE`!?Ppdp=PnQGg)H8)W?r$rTbWp{hs@ksKp= zmE?CMgCrv)*GR6D+$8}jMJM5Na-QT0Npjr4{0YYsLu|IMPGX%OSx={@%m!Hoat+9G zkU^0BAb$y>!70yy{5{C?AcsMp1cwRvF31bf^?ML|^Du(E2x0~QT*ynjdHQ77tEYf- zH@f7X#}!i`gbrZc2Y?8FC%O_N5#(j{<}Gs|_~geR)+wS-I7BirUV(kY$XGYXzp0m0 z$R>-31Njforz+G4>_ae;KwbrX8h=n2AwN~Yr&Bg*tPAb($K$(a# z3As-cdV*TSf}Du4H#sXeKOG>G(Rnjj2RR8t4AnqRjzUxL;Oii#;81OH49HZZB;7H9 zozQ`DD!Y;UG$hrIa$^I?>Ac*@!P3yJgOLDo1_#kW&^~4$#=|li0Lmc?-A=HV+31di zB2D$*W2Au0VFlSJk=#fk;>ehs$>iJRtOwTFoBC(zq|>`YD@lsrofl zzb3!U(FwB*-5SJ2r#cHde{fcQhjrWK@<@WkD!~;fS8`sQV5tpQg?jcw35NbZ!*Ucw zD#Itx9`j;j0?6;8_Y{xElStzrzmG@g6Y!yiXt+QM^zvUjKIud8lydc}E)u*F^&F_v zbJgr=pgFjEYmr((Eh#pfA&^xlvOuoKv?yaT$PJ9(lU*I9u0!#dqOM2pJOngo%QvIQ z1^G0MseQh)m~@wH;Na{Wj5=>av06DdtGsCXTR2=ALdbgpH7sYgXBAu9Lu|BR4B^ilXtba+TtKJRCoh?sb2h}g zptPxBWph(=>%d@6x^v*;*#%B~-qaTqVwYJ&_m|B8-A|YY>Hem9p6>6Px9C1%&MYj{ or_2DzF{=vmwIy@RJ%tO>ugnG5L_e;4mHO8jOBVn{{(*u20a23qRsaA1 delta 3231 zcmZWs3vg8B6+Y)?@9ySBHY9|EgghX;mGBBE)sjFq1ZKoPyX+DkF>GL)he6V8f}nLa zFMOtrP`fN3n#R*Zy_T7_2HVjb-iE7i-CnI9`v5Zd&d|Lz86dS}kw z|DN-m?>zqV-<$k+>mM3CRdw!#M*(1ruGw@KBuKI^9l#cUUQKttE+t#{Y4P=|NrWd| z6$$(Eb=A4wGYu>DcX_98hmZMnUt&9ZW_Z-wxhZ^uoMHiEPD z`^HrL9t<0PyT667%7hOj02>P);psr@0Y5Kb&mXr z50kFHpw!&PHn($BuQDEY6ir^v>LsP-BD!ym4sjd(=8PN0amVuTUM_OsTsHM#(ND*fmuwegu zzZIhOecouf0&o<+*{;az)kFz%TO9c_LZ&N}lZX{ftyxXR%bk>UB1BEXCd|2HquMSD zGEnng|ycjPP8Mw-&54gDh|e)W%$*^zC>9<%zdd=G~cTjwirFh*;7|rF6Ii$fjZECU6uc>XHHBuUQ6zqx6lso3DG=3 zUT=_agBltGq3WF-J3O79U{gz|GuTnxu>*>XK*|#RcZ4r94yW{C$S6u(iTjPl)aBWy zNUJ=z@7nEsxTSqV2%a#GrRLAfqk(^jXpi1OT@`HXYN`r`f*mb^wwA|&Fl2n5nvV7aBm*R`lbk0}6dl1p zTiecUu$V&A5JN+#v*VF%flzSUj&}HvluL=tZoHgUqTNg~&ZW&OI7w$nA*t>Mc5d%# z3s!dogYZ5DzR#GHzOgWixCx_D@B~Av+IEJ5&_w1nWH@9Dr0?7G1=;^hmz{@yWk| z$W{R^##8?oyaUt#pZo`iLS^}sxz=*H&%mzAGZPpIVvoXH1^GFX2RO{XEC)NtFF^P} z{u}fu(0CTWL6ATu_2)})A!uP9tqM;e$gjY=i#9`ES81n!yunRiGLfU8JV})vGTcfS zD1X1yP|QN^a}r7$N*$#gxla_@p%xC1PIO%3tdjX|1DS&MYneL8R17mz1DTe9rsIKE zKu*AsKxPui45TDoNr2tZh0?`t{6tKE}ba(4~VX6=XIC(LvBY<{+lP z{pSEFhg@{o!CK~_%K_z@>c88Q4RQ)A$VQ3e#}g6HjmbhL-zAG~TZ=gZDx?I(0+7>D zmZHlB9-^ONVP~P9&)q843!wPW@nU@{6!Z9H!d|Cp*WD-%tL>VPvJ7Q-LA*5!`A!ut z!UJnTF6R0vn=K-rpn%ZyKInqP|&PC z9#&;rFPbpY%<@UyFQUvHIbm8&Tb#L&xz>b*Vg!`dmKy1o;g^_%#O_ zE$9r-qf_gXqsFn4ErU7J3haYRXV11{nb9!643`=E=(pNKMv1>JM#3rb&CzN-9no{M(_q*#ye EKRs~LJpcdz diff --git a/src/test/samples/golden/notepad-release.app b/src/test/samples/golden/notepad-release.app index 81aaf840c73bdde8d3df494c6f900f59ce97979b..9e68aea67f160f1ced1ddb4ded23e6954d800bf3 100644 GIT binary patch delta 2360 zcmZuy3v5$W82;~F@7CRIFa`|R+O3R&Yz29%;SrduFrwmJ3mpT-*x*VBg^g|)M0CJ5 zTvIELgAv6EMomzI2^qvC23ef4kO+y8NF+pvqTWS}!SHej_51JbR3az&&OPUUeE;K| z)3z_Zz1UZ>#5?H_5ydeI@znqcxR60)OYG<4%fqc&--5)tBZ+8tDp3o-{yZk8?(r2! z+xBeo=df5a&O+GyWoG1BWq0h@7$$hPZO;$Wq_&RXQjs*f4MtI~y}8S-84K@QrmaEv zv~~?_Ij<1)v~Ie%b(Y%74B7H+?P|JyY<{2U zkVg&DV%}|9)Z`7`f}bXSkr(&_fta^Ni=JS*a^rgh<3w9mW6RMA0;BX^JRUj4+Z=2) zkg!?{7!#m~Cc}YsLA1&j`+QtnG$&1$-S@=Fq~Y?|`{KK#Vx(r9yg5Tu_X`AN*+s=X6bD%@E>T^0cN($fK2Z zE8q9{O7t}Ew-aS@eh1#hYH6gqVl6Jp6j(+aZo|VKUVD$i#yonqv4gc@2OexFv%^%k z(RU{vUlu*i{R|ktVS_`7={F{IEn86~cBYJPIttlZiq7I*Yr&u4UT(CTc0&TjJ}j0b zPStqD+VF+Yj4h@KQ+4u-09$2uKTWU`k_45{vitR0DEU4OMO8?bezin39t3izp`;99QpvvA--1^yaU=R z|Di6?Yc{PZ(Hrz|ZFQ)2i%vJi(X{MgxV2Q(5Y$7V`FdksCDK@b) z-QzfkA3xI?kq`6>t$4l-ebz?~v>&dZzGpC`mRl1rl9SvZ&an zk}6^CuSt?2CQY?kcAFk}{m0O3^k+t*KjPUuXCj4-c@B@o>A#Bm?n6O{sVbxX2_QWX zpfhhqQ=;=os=uRdUReoem@&?z@m8`UXuc`vKf`?5R^zwXNUDKJI?1MqYUMS8np@zI z^?<`@+Cfb7uhhI6WBw=RvLAV*`pNUC?Ak29y-fE7JLf){7A+EM?X$DTq{*Om4w#JtqawKJGQ!BwsqWcr`eW6VZwQ delta 2208 zcmZuxYfKbZ6uz_Uu*(ASe(y^}s;nvEqqV42EQ^q+8FqES2O^787F-Htm&UeQMA(MS zvQcu=#z!>TCT$aw28%TOs7l00Y^zO8TGO;9rjm9@NorG5AK23G-WhC7JIR@Q?>XQ3 z&N<)Q-TPZkY;l#>I!jIwLT2zRz$(E6*r7y1^xAnnR`yK9scXKrKNAByeC05f&z`%+ z0aw1TcA(Rp%iNN39nPN1$>FKGvFN!%MsT`v;K$`cwqG^zKPNXU3-A5JV!it?=#;c?#-bJ1>HhGnn5M$xIcq0ykVXDT5w%#wOW^==Ox z(#=bWIuE#{NR;vV{y<(e{aIJ#mZYe&SEG}ZlPJa-4ei#dU%~uJ<)1S%;VZ0}+c-)< zv#{h*iV=upA|Z4msdq7pMK2g1)1H`89eG4A#$@aC)AYNTGI1Nm+e&Tvg0AiL$lLsw z4Vj?voM!WTB;`f!Me$u)@hNCK9%{#ljUFD}f!*O~u#iMfFlW?x#O-?(AF3r;QUsMU z>g*eL_Az*5m`#f5j-cS3X;sy+d2EVBI!s?3arUj1+{#rg6!HVkqVH0RxI*Zn9yzlqR=_W&7>K^#Q<^rXW#hk`* zv7EkXuw|7p_p$wn~s(6%k zPF}}vl%zCj$d7?cVU6P9|8f*U_M zQV;Hpav;#qB(H1ROA66+JFFk}DeMM}Q*_7;et%mdS&GuQ#H2nD?0BWIAs{!lwv+GS zd=6}}^uvU?y6G5tJ;7nVfZq^DV(yJ?yF2}IMTabtn<%`BW+ZO4rhyw6-hxXGyyR~S z$fOD3Dg=(w{=`GuenT z3$a45W3aD{L$A*{mC(h?tIsM_SfuT9u$t6IDiPU6Od-daPg@Q5jtj&jm9qb96XX$7 zNxzszVh;+&C&zRm=m~cSw2rLXVN8M%+JG_zs z@k})?5LY}-StJ>wvkX=@8+CK&t-Q^KT;jb(h=Hy!OGYrpGGcnzoXrk8ei$^j3MtfT zc{VeTBn!kYnu-1BS-WTd9DH`(HN9tXSk);+ zJMoHhc`G`i)3Y`K2ls00s(dDrl}*__tE2lIl!g>5IT>Kbtro3?^H#G%<#bQr3cB>r z+krXqpA-`9sL|)^Xi6nu}h@!TqzMhpF@VfPSB6tCbCpr!> zseNUWo?axRK9x{F4iZN5k2i}AIR8KHRC>{t!KZqL{%W(YTfpUsBZ3auo74)Ldbva} zk=1ton$>P0KF?E5v4~dNGX)3r*>?@yu)n1r+C1N&7mDeQg~h`2v>)#Z`WfDJbP8`T tH5U~N2j~X8Dcx68EMCneq=XI@In0j=@HeIX2Fg9Pu3HNT!QVH3{swb6rj!5x diff --git a/src/test/samples/golden/resedit-debug.app b/src/test/samples/golden/resedit-debug.app index 330d4321f98c8eff94a0f372bb6a8e77347b19fd..e81af31990a1001e73d72ed8e547ed728d1b8b0d 100644 GIT binary patch delta 4385 zcmZu#3vdk&bF$o=C?Ik2Z z(rW}lxg7#3br%yB{Nt>(Eaj8QshF>hn;QTFu7rj6#30Z8))QJxya<&L>BI z&Ot3b{!q~BGA5?Uj)F}xs`FNVjyP$3U=)Q)OFIf$W%PAUS7x8WWW3O}H5l^j1chCA zslUJdO&*TMP$CJNWvM*_hG-lD*j5dyf*o&6?YCNdHyBx$tbM*w#+Bb#$9;L`GQeA5 zjq_(k%9kVVwu=lhIoiQ_q8W_8xHM$X)oRRshdX-~QZg|BbGN_D&T#C&Hx96{4EC$O z8e3&PhugYbY4?4t>FaH)hlvSm-+McrT-Kv|hO)k+U{|R6E{;f*_Q9{MJ^u8mFF1ki z2N=p!0}CpS7HrLxA$x~wX{$vjTX$OT`L7d;tg+@w<6NKh6LXn($U0`)L!bRhPtMEL z&eFE5f5hcPRze?L8F1G743|W2%dP-XDz9(%FYvUy{lKzyD3saoY){X!vpJ$5&}*_Y zU%i_+zCEtXNfUZHb>2>4a__Mdvct zvig05m)U!?->_2q5j-q)ch!Et8E`>J`(EwBsk*r%kZt7wFHLnfG;XS(yS$j=Bd20 zqH*DdhPq%=Fub;=u_@fJu%V91tnrC+Od}q6t>uZWqRTp+IA0vJCM4Z3tPfdq*gU)D%_t@@-flZ(*P#9j5>-p(jn(1W z+C|}}y!v|TL&noS>qOFQ@s>3xdBnVO%>6v%O^AXdc}W|>6}7b+R?~XS=A>zAxN~(y zV|aC4Jq<0$y_6hbvvWO;s@;OrKgk?PEdg~>|F)4j z#U^tUJ!oD*W@I#N76Ely>&8HrPG$x*t9MBVI*G)2!DLeZ)}zn~AM$-bas|JJ)HzBO zWN=~Rvd@QPj-@no@by~H+-v=LGRJBCc&%s3wf++_Gbt^CfCN&Xk~yB8aO{+-3nZ`A zGkn1jg8cJ?X{5g7L8m!^GI^3Ar21Uz#7=XfR_fq>k{5NbA(T%jKVUduvuHB~l*!{N zWKeNp85V)GMNlNbD2C~ zo~&0WVx3aJSZFsAZuSLKis1QmCdbu?bsfa67aiB7nE8Qzm8w%a+q4alKcGykt~1lT zk}{js5D~fw^p>a*LcXCn7^N{X#8ITK@E|6VFw|&HRkb^U9ZeT*0c8xV+#zeM9*=aY zVhXBaoK{=~MK;AZt4uwd>J6D9GN*aN33@o)8&1^28Qw5U4`)Wa@gzNVD{vMym#8TM zr6jRa*#jsBk(&A~6w?K0s08^5;7OPJ3=z5>%P3K^g_JrxN2qIbaxVF+X%nmkpK%3T z3vGlkz#R^` z!8lTRLMD+~AV%ZZ;2tVbi-f#OXNeSw(2eL7wU`6Ai*MFbI;3-s9NyYnw9+9yu36ox zhYs-=F404Wcnl?{`W7k09w{Y4ChK`iooP!VYPqLcA^4zZEm7q{9??#BFuNXz3L#%3 zwZ=mo7UnfTeJx~!p3Fx+jl4==J8)=n-*9XZGq6_32u?5^jEYjgHKBysvUZ9K2i@0a~@{My>Yfx5Wen<`@~C4V zPYZdKP|phWD;?l=m_9#$%~go(8t`*M;yixA+yCF_CWgH#0*?6LWQtxA>bC<1==rSF z;9nN01{wTD$qSAo)fv0E`DokU3HdCky&grcIMAJ98O+H+QMb1GN&Qin3qW8#pUs(O zq<}IjU3PQ@!{b$Nn;GG2x(4?Rq)hdHs|I^qCilm#X`#-oYw$pNotwG6IFD{paVG1E`6w3Oc_DX_QXalfzYpJkh5DZ`Z*f+0D`n~u!Ax5#7~f~Q zYWO75GhYyLA-*C80HVN#kP*@S@RhrZeX%H(T;9ctvHBk|I$^j6sbRP;-8e&T)_CI$ z7jNvg1`N3xUk-*!<`&nld<`4#f0bgybjlz@-bgCVP(uv;ZGj#cXa)4hFau?)apnf` zP8n{5w%}t_21t!GLWl4%S)$Snd>wJ8W*DZ_n>ZXV{1P?ZkW00Gf&q^BB{k777Z;OA zq`8^;#=0zCj2R?HGoYSim?1jMWvV8#2R9?}1A6E-+8QWPS6bUA-JTer{Mxk(Hq>rt z=sG{?)%dPwrX7qE_gQswW{9Y@2lvOUL%4TZmvDc@nwB#|ylt(-{bQ>mXNEa(4ACiT ie@?a`rdyxnOiwSGiO+1_LtOc#OM_3(A#(rbp#K9i|E+ZZ delta 4257 zcmZuz3se;66~6!M4y=Fzz7-UFTQ%bQ9R*|&Y&E~Tg7`oLT|~r6SZqxiyCM*wD@V=r z5%tt++tenWV`B`hZBt`3l_rhPR!tj|dZMZ0iOp|n+88yV_uiSoN!vZ=?(Tj5|M&g( z{xi?4J6aXUDe{lq2LSE(8IHdaMWAf&4B)l(IrvMT>z%CMf%gA{9*jp@4hrSd=eBQW zV4%p{xgpq>%+frA#`@Qjnr{`KYan76>Vq2&jqZ1FeP<)pb67o&|fuCiA@h4{_9&)eIZPtj(s zqAD?(+_KQjKu5GECNy41Y#2knmSesLM!jd62KS6tJ-s#Wed8~lEPV;WTWq9x2S=8c zVBW$91Tw*{v!C!6N^>S#aq$)Q;ymsGW_2eKzF-k-93!?s}Oc^xauw9K+7*5{r8~yYt zB4QNk#oB=TMytM9oH7z)%x>>~#zlx)4wmL0jkwr}iQhwewAdHreg7oKm>-+cbuHSO zQwN(X$>;aA={d%?V$*s}BlCp8<~aOa8f_&S3(X;)8HZ!1MQ+}0Z?6Q&Y*hRm5lHhA zn(ANgUTB`iKP5I@=5bVttOk(g5e`Xe34L;uT6XF3lH!w<*j`9!#Zffam9|-7A6~A6 zXmLX@I*|itl|r+cix+c8Npqzgc?csrbCeGYbD0HZE;?`4;;x+~@*JNuN1zX}-IVA? zj)G<%tJ2MUY)j*;Z%zIGESboHX2>kDMXRIaehsO!|d-yAE&oMj~-1I>3Ce)*20y^NPSHH;h^18@z?Y{IPH zp%6hOWu@WV^)>4PwSmy;HR0M&O>WIPNHa>~Cg^Wse1@?*u0e#2wD@eX(caU|lNw|>=z zs!(oCCr;TcAc&WrnZ!`c|bTL^^0v#ZvJL$Dg%`RLeXZ0o#z$sP1x#NuPH_y+Vu zP=1V(f)VK`D^Nlxl_+&6Pow+{1k`k0i%`T$6$MY@8zV|xc3)(5hYlkb9jz{&SP zJplE22iQTd1$^oY(#x@eYKQGuye}X^ECvF#^3wou2$lUF<5+5+h!<(F$Qn%X<+ulm ziU-Y0yy+e8A-o&m4oX0Ff_t#u7LQNG{8yyrj_gd453I-{+||~0)rsRH){$(-Jq(3( z1b8@I9|q}lDqaLEM9uC z-qUJ;;n)DZt@XcxmJtwJr~0ry(oy@eI?7Q~SbfA%Q&}DDsQp;YaMb>++RZiw>I&3A zfn7sAHMj#r7f?gQ_J~C{qZlTThB74IM@K}hJzNB*BC-NCN=V81qlL=g>eSw2VKaLD zpU`QKaqS3ctW#rMdt$>lHdqkIP@pCW8Oh|blZiGeGezj*piCdn`@rt^l5wiQcnru4 z#DJP6WIU)D)L1+(1u9d>$GJ+D2u{bbP_rqX1~!M6v`D8C6X<09Y_xcfiPWiF)-B>= z>eL)oE#gtlr&?bkWv)ZYd?6Ef-N&qT3slgtEEIGnG!&>JA^*fNi-}wZ!xAB10ac8k zBaC{v&?h45Nhm#dF%Nhcd5OT|z{`>5mXh;{ffYy_-d0Q*9?~i+dGeQ&(GhZzDXCn@ zxuDkY5fdVuGeQ+f1dj-$gQ^t4cH(X=!?$~93ay)7C-HVz=eAF!)2k73oRF9jc0;Qb zGDWCwI_J4j=+pi$vL~#~;q~N(_N0*Sf!ZY0w}n0(^Jbtt3T4=3rMOugsVGlIWD&vl z7Gx8$%|gBas$QrD+nO0j1l1_`<}6Rv08dG66&hWPq^j@m<~*RHLXXVE6=pr`HneUN zNuG|AY7ufis2vY!)9{M&aC{PYnYo=dbI-UL%6!P&cinboZkLc*%-n7fadpKWAx8+c zSEwIxhCN|?SMH-OL>3tEejzoXe(Ie6b9@z{-?=x5p#wtw+-;|;lEFVOk~Glh;gJ>S zg-pKS-dz^X?Li@*19iyZ=oc1rt6RFzKq5tl+3N-MlF+k}z}a*+r|UV8US^ZS4U7RV zyL+&ed+=yS#Uw98s0p1O{I$*GZ`^zGb8YUySKRho>cQ89yvpNs!s$Wlj!(H zIwe#aC%Os!gH33J8GS>@U7+4_7(MM~j-Eus=vnq+`+Xj6K*%3}G97p~UGO6Oq$PYG3Uy29U$u7gHAv?kK}1cZgzq2RHgps5CH#?)x%kG= zfe;Td89R(Fl7HHVc*hN5-or!um)kx+Lg?-}*XDiaq1!Ix7RC#jJs|FL6`I6%HLs?0 znsI*9!bcGgN{r?jlvqtp2NkEOc#U5ztP!zB0TFlBl0?Lsh0FjxQ@Uuur}4#8`amUV z!PEFYS)h7o_$DOrlIyAIkMSvX#|M6a>Z{2jwx?*w5j~_*HC=*;gGbZ(i%k7!Gx`Fs zEFI?lnjU~NG|nom7lqI?0{y`m3PO{wKn*ai3|V$0d+2XtkBHH~j}aM0+k^};%lH(( z=NrQ&W{8ln2)`?ht@zz!{0hJK8#g9q=$G-ic+Nqt}fd+zMU z%93d(J?Hm*zJI^>dCx!ZI=(N^xXWMrBO+SFvl_oDM1s7VOJq&$x8s+eN{9Jdo?1^^ zh)!k`jYBw|pY_?N0_D=tQ%8cutXB0mp&a{de*9YJZ1NW?Il*%$PJOUhIx)RmS|e>a z0jGrDJ~m@l^@lz)U1MRy#Afow-&Tm`V&BG&zRaig&V z*m8A(LT|pgxEOzpx5e11NWx|{q}RZZupxkLVNexF{$}l>7_-#Ll{dw!mgVxQ&&2x{ zpV9$%dqkPFa-?fF;&y(@Af1UM=Sg@Ne`_eH*Bfe7-1CC1hFxaG%n|vj{`w7Z*6LpS zGczzg%TU(JXs9*eKhdBD^;0QJEP_zB{y_Z2dY`milq#L_{DSz7(jk3MysPL1FaFyo z&g~k`sxHJIY@0_qp-*($&3>Qfk(e7hZ70g-{?0su)$aIg$3YCrMtH{Em3kFN`0aBF zJM%MllmpBa2k^s=G6!s`)E7QkT-=j5&wPgT53#|aB(>|cGkf-SiQ`$-BU8{Fq(nV) zt%m=K@OJ&I;a3>azmLVP)T!!EnHwGmRe#)Y;Zzs+Mj%_~Y&uQ&0yL>q0n_j5A7SKq zTFP|{TR^`THmxIZmNN$R)hW+&2XP75$&SQ^r>f_|Ha`rvw5|D;?VtZLkj|?xgW;fl z8494BZ^qQ0G#@cx?{A5Mo{+xRv|?8J+T*kB3IAmrWeewUSH|b`EvEUxdN{`xhnwg8 zR{(~R{wu9&_%S1cTllgIJy~XmzW8yP)A%O_3%w^Vw2Nr=cvZiR$c>0sAU7e*Q&&&- zP}4y4P+&Nq_4N-8Ytg3YA!-*Z9Get53wXtD$G9{tE;+VJuZ!}Wy21ryF(MCq>(SuD z{e$g8bY2YRl&oyU9rz*AUd~9}sP!D_Yt)9cXn%L4|8b2z5>q)v%P_UnH5Aq&k?q>B zZ*Y(nknxg5T+G=hy(N}7i?(#3_D>TWVc^U$xq<6ww)k8gKB`dqyH<8I9Tb z72`8emeY5YFOX79$0QzBr38C7DIOY?B>Iz5BDE)eL^5;I5XdH_OdKvL zba)Mujg%E)qQui&PF{A+V^@i+6;n!up~2@Mq`t@hKIx*Bbd0!SFZ?IinS6mfUiH3# zvlG`#%J*<@Uo=$6#%d?Y)>NNtTWM60GCguhmVzOYq+=EX&Mh}N(`|`3>|R=EVx0i$ zFOe4=PI;A8V$$s$agZc)YfM*~JA@2HygFy=^TcBvT#h``q zp+u8qB_l4f-l<97U7P&$wkEF{Fj7a&mjmd4b}JL}`xG&JOQ23)30LY0KobO=dP` z-_GPuXW%mLw|Tge{{!EXcf~iteaWOWf6hF>Sxu1XIrDuE2gn&x0S}BP6MKsZ=>c4K z`M+jn2e4p+QN~{Al|JcIjG)iyi#{R zK;|~BQ-bs+=fsHd*Pdi7FbF$^x1!5or^ZO%wt>h%bb6#BVVkzCaag4%glk=mln_3= zC&gjBr^Iu3|5Ch#_d7zWsgXVuwKX+LMG28lXf;*t18ayr;6G42jiFtErgiwvnOph~ D1B*r3 delta 3234 zcma)7YitzP6+Sb&W3OL$9c*5Ht!?5`8?u;38r%?U;x+3g(D81(7>KjjX8dBQU9YJ~ z+jKGZQd`Rk&{Zk|Y6`8Is!ByeU03{RVuc8+O+(d439ZtqQG(Jz@eiUzZHQ^z@7&of zQwmX6Iy3j4dmi8U&bhNk_x>y#XlnP>oh70L{8r(uLlnsIJR)amzYTBxUCXlGm8tcM z4%kE21Yv*v-Ef@@R4NT84+dATS~ssiIrf@+a<1c2@|V>d;GF;Ddm9!1)H0<;+2V&$ z!si;Bap~rce_5vSKKR6E^2bkWM6>a4W5<4aQZDs`Jnx%hl^9ivN)I`AnyTd+pBX6| zo6Aj1v(wm&GZ+$6YE?nfcQ~LYk{q}1@9=9Ce^Xn6x}Nlnr^4yx^GL=(y*NlLfkXVw{PDGXmVDWDON=?{Rr*Z4;#j6S=f$5KjoNOM zw@Z{eSH|{);rGNXE;2V2=N#7`(#=Pt7RmRflFxzL+4vB4T>9knlUSWdj#N|P`>ag* zP6hkEjz$f*byKlICVdmLz6mx@Omn5E?i3QfKX0*msfw#ut)lwYgm0oj51PM89mOLs z=hN?rQtcrnCPG?=S}`xCwOz_(k)L5M{>x1ngsGLry7{`u&fJ*)EcA&ES3=_cQIpt_ zS$yAq=q5{6n%#WnbIoc^;#-;JOY2#^VWqha?>&iWrZHsRe^Xq@tdCv)bYWqDbje2F z2dF?d--c@JwZf431{UEIUFHln?bZP3=9DyXCyh@o^2qLaD%^2t0OJKp`z}I}ige5} z`|#=jB|64~iH*{LvxLkc8N5SIlx}vXEYHI-Pg=PUScYjen{jx31gO&~{OL2@tic&- zyRPFH90kqgR;24K7)$YcSAWYo7pHsvACg8)Y46U-OFI+3chQAOD8bj{tQoV8FW$%` ziZ6`#XMNXzl9Ik_t$N=k8R1DWnpIf3&2llfSPo!d@>5xU&xwmm#V@kPYwI$I-ba{c z;Pq<=6BO?1j5ZGr9|(*DjNbm}h%wwee1OVDXZ8l|x3Kq$)7j%nRFvmzRZfbooch97 zp_QKdzHunHzkg_Fl%5eUhcJ>-u2M+Xyi-3Nx~9Vj0}v<&f++zNHxA+F@smb{ERBrQ?*_Q8DzBS!PE zVbE11epnRc?J8T0*szfh0!H-F$Y9iR5A#uESJg*(mdjw;!=MFdDt_bjTCZLUC=6}#?9ZXKk|P? z8bcKwAqLO``{Qg(je&e#^}IopEgDKHV+?hrR3RFxb4YcjVDs7iGK*rj$R$+?hDcG4 zIAnFV%~ZQt9-IBxGHXZj6AQPgI1W;($>YYTUGQjM5FeCyVp*i9jFst3b1RulR{?${ zjjKQ-{J)4@4Q0x8t!=&9wq9dfKVVyXZR=EXVryx%O<%+GrK!r1<$x202FP7lx=;{9 z48D!-wCRm@y6fmLfcZWF+YHc3h^|P8R?76w0>-;Gy^dpOB}Lq2Z5?%K>t!a4g%cXs zU|>9D)%8~Syx%|`8wD9Q3aqt7LB=R-C1!EB%(DyAIo_uKR2IlFFT53#RjR9|FQ zZsMwM+yUgTX(09gCy?``eHo??LpGB*TfUhYbddH46kBdH=pZ+g09juAxSO1J1KDs- zkhkst`JpU>2fj}1^%TaCF?BUCxT(wY-x|IwJq0ScRqK{F1`6y{G}5U4U0JzD$H8 zVg6*Gb!g>Q?)rBmmb=wj${PQh4ZF)AZ@(LzJH!tv zmv6#NveW4Q9HgtQ52Q6Lx#FYBRl%>yW)|WL)26p@(aa~FIw{$-*&`oTFSJ&&wrQOf zpevjVed0e5Qn5g-K@q76PrX%@$e7w!li^gnV*dkPWs5k2=MM2Yo`#smb3oL3y~>2x mgXcMs@OrhYD~Q&MSG=_)*)>G(rTzt*LhXUMHT)H{e*Xq|&n%1p diff --git a/src/test/samples/golden/widshow-debug.app b/src/test/samples/golden/widshow-debug.app index 211e4275255583f1310999c732171f5c1f108b18..5da92b7c756410f7ffb3f4ec6929f52eb9f0c105 100644 GIT binary patch delta 1626 zcmYjSe@t6d6uz&tP@oJ5Y%ZfPhs`pCjZHS=Fmq#4e#B@Vt+xCSDuXA5m4R)Ub83b{ zC(TP8zW@PNAUD8N&z4V~8cLYwV-8>XC%i^hf zx@*Xbm{2Tx_^OGqc<2?ZC@!MSoU%GeymtvE7k|yLwy@6FAPjnJC>AUlTC%8bQ_xV% z=ydR?;elK^Vjq-MP1k!1aWenaQ15iPBKBcfy+m?u@gtJaMnj!gQg#M|s9Q-S!dGay z0ZSPoM9VHw4nj>u0C8Tl$`SdzEWw9TDh;+3emP~mVaq-KLrRsY3FWo$jYiusPc!14 z{)dXRs1edp3#tAFms9M~%*g&+GP@0u)EE?V#OM}3xABa%3@cmya8*W#{z3@HCZDX26CTRQt$2 z*(t`7Ovr~&V&CCE8y}Hsd6B8ru(-@$H8n|Z@@poM_wnCaa@wxuEDO%RP2F{@6?V1N ztnc?jIuh}rlV-+pXn*g%hgoxYx~T^Tr2?K|t4)+rgxwrBku#0>AO+|%3ZOxb1`60| z6U%oJi57K|`sotCV}XwnQU2NyYiaZF<7r!uO~BT})IHQ|+4~E^-C|tx%Xf*lFlkJF zRq=+tpaUU`qnZmzP0@{jTI=+xjCTr_q?AMV`|W#>be`5y9*uK|hruaL>NqJnMM={0 zd=D-GIhi4>dCDRS++q-Jt6bfG+>P5>rHe9Xbl558VL+9ObvX5-`rs$b-SvL7$0gcy z7H6f)9iDcp_IZ5DdeX4k!f~-zr>C~VIg|+2#qA3yhDGcP4YKzoO~J~m((?<`^bnD_ zwc6A09jyz`k50L{KYh5gJC!jjA}#?d0QJdppgmC2=kIj{9ZFYsAgK6j{JqT0H)ZTJ z8PZTMZ_XH&qI@o6zcj;(Gb{6#p`t~09zNnc*xm0Auu0yZS+KbQci?@<&i_xkS~)P# zRjmXRe|NjD`>4Wh@`=oK_uojs=uEtLvb#J&t_Ig-|#h-b$dO?{Rwad z_yfpX74<9aK3`u4JBVz$OwfO*qdlN>^!Bqn-1NrMM{g_-XOA~ z*SB2AkzVGbyUQh&$M8JG|HN~G-(OiSeadA#zvF|IEH}D@oquRdz mnOc4(y&#q9DKFuMgT$bOlS)Mw_+}Qdl%VFc zY1_j!|s zjL${hze8eAs!oYmLJ)U<1XjC7sm@D^~@rWaT-s{e!1Z#igVPiRI37B#liflxl=t+ z66?w+-+{6~Rq0`@+O)6+kjo+L`aucn15RQ)!J)Th2{dM8XPeSa%S||z^b37&(>^^f zcj0e!j5wt|tJ~=(mMza!+qCBnKJ- z{iK4r^A6}ff!;(X^G1art;nwzCTM5Ax%e`oYG(JzA$wo1zcols(V6^`s!cfX$6-75 zKWmHJIoM;7gL0tP?dyF}CJFjm{`w*{Kj@KtzD7B8$nPij1X^y`?>G-{{N#uW-bdtM z(A^`~4-AniY}yZu10Mj_0o5WPyM4ZaF0vn`afpd47z#YwXT@*;Rcqi(?_2M_xOf->m=-2~!1 z9WOY2^bhb~!+a0S2f+*jZvZ7wJPe!wWWWoI0IvdX0n@+?a2ZGfp8;(cRs@&?zS75E zSaa1NXyb=VSV7#hW5y_$=wgjYsHH};Nob~ZxXaX!yO&PkK1RPZn=~J;p~Au4Yk%KN v$glW|=9fjRf4Y(WkXw??d-;AlbsyYa+O~s`BBRO^i=)lfS3^i%dP)8RO2|_3 diff --git a/src/test/samples/golden/widshow-release.app b/src/test/samples/golden/widshow-release.app index c9752fc71ad241fc1f9dbb53969f6363094b416f..45e3992367e558f41e06493d26873827706ca4a5 100644 GIT binary patch delta 1619 zcmYjRe@t6d6uz&t(9$v>u(?3l95%!W8%zc>%8b{=UnWwGA?j&eDaqXVLZN5ctofhhm&m8k0$gYdwNrK zjd&0fN#u^*&=Hc1oWhF7#gv&{lxH~aS%t|#U(;+aY1XzfjT#$Cgm;aun&h{5&}c$y zx6!!fp?ooB9T8uV(DjJiJMN%Ba`5P+kY!t+foh+Y$Hd zKfFkj9OF8239rA^VP}UGGqOLQ&TfGuZw!h#p>?tnl}CNMQ5Lqr{XCE2Q|Ug&qyLD= z*4CBwB_jb)ykXIq=P*lmU7jl7_L%jY-SZ-k$P?N8I-Sy*D?U{NqjS8J3Jo^YDqF{o zh;}xaW+Fa>a`tWdtM)OWiSE*MYF5_ht2&o(hThb%f{*Si$@z8_XOU6*eP+#YC+u>k zUfu5p{7586&*=%tL=7e&+5G`LA1xg zx>OctrOz3ibIR5we99KCVYWn*tWKq8tKb|-g`1PsWfVhV*5y{w^Rl8~ruBxxk}NfZ zrS5F?G+8vVfmTi7P`c|&0-*ccojPP!-i zpiYy8dTD$1m=LFn*-r@bwA6U0a1AO-WbaES?8m(UXNb(ufU&r&6?fo0$S(a)xYzjw72Y6Aljcbcdv#fdDDMtsexkXpd=6rUow6T6)@4ZoZ12pW=;j zkNSIud{Sdjl1MI;hv>H^*RH29gAZ||Dw`B~(&rCJ)$Y%KycmbW2x*MQXKY8S d=tV>EgS?)(GD2!|6@oN4+HCi02ua^m@-KjnbzA@d delta 1522 zcmYjQdrVtZ7(chAP-uBf*pmT!U}H8T?7^nQ$R52={s1qnDX(>1fh(meYwaxtbrVTj zLvv|1ax$}#xNMqe%#vkVYcSDC#zHe6GcM7^7|nD<50O9SBf5=g{hiw`@g%?V_`cuw z`_A{>b3Y6$9keB3rp z**6J$rbZm=xLIP?V2oTTi!Zt667O5Mz&YF0PYr@CxklI`G}%B>MPp>vD6#f`v(%Uu zF|paw=(K^5bnGZjJWfhzTS?XY-X(6LW;MlwBC~l8Cr#=$ zuSkfm*d;Z=b-jNfu7m!fYjsFcLX76*Bz6YL_(fv_x$N_(pV|J&Wa9JuGB;-$w^2e3Pl2wRByIyWZ7Qx5NQ_)xwwi3wTBggB8F zCpcYA@k&wMDI~nLobBq|W?sdkI#l1GiW7Sz2fLbc#r&Xi?`yQm@VGEWod%a~<1$Sd zx`laKn$OC=zpIJx)@o#leMXB48cI*XuDXn>cJDW>w7sD6p~JAn*VnRs?h}n^gO$Ei zP_w#`n|Ic-?O1!&6mO%GJ$#qGThKVRcqfzb5lQQY_h(cfv2S3CT&ZxfFR*dv+QnvZ z=*m6-i6u2kSwjAEhCkWfgKpQH56=rN_0GVMQ<}`ue!bx%sw?VH8#DsX;$%Th+@-xJ ziS_507r-pmSVjnIG99cL&c!fp{UC+yflp#v;6rVT65QyKgRRXfEw3&HsX~IcE;yARq-aiLL5f)E1qU!S@+EC0zzV$uq`bv;FcsIE_6uVCp3ivbiA;l zswEr3nz8}tK>UtY{<9}$R@3(iqg%J-6LJeO#}M@~z#HW5^C-=M(6Bvhmj{QGupDX* z4U-z`DcWiH6#P0mT@)1*T4UNLOwm45W5s1ywa9_zM;(WUf}ILEMlYHwEo<@M4?_0r z|D>&Q-^idMSx@%sb)WCv2?5fir(4jt+DD02UBkbDQ@E~w?xx6K=M z%X)gvymkF0{6iWOb+-iuM*MPfNS4VBBz}^vEbgu@hc-Cc73{LI-ycw9G6;7o9H;4I z@$sHN;Qt!p^$;I{XAF1)s03pJa0rkAA21HQ3cLlJ2QC7afi&WAcTP zt7bu;++TUrl$^8llnQmUdRLv$MB6d9)0Z*J^a5rd{Q>g?E!kbC`>2FA?S5><@9U7x lgsWpTKW%TTr#}`}Zs4^j7IdOia}cYu%l=LSA^0J*@gLdZPyPS^ diff --git a/src/test/shell/appsTest.c b/src/test/shell/appsTest.c index a15cce73..575b15e8 100644 --- a/src/test/shell/appsTest.c +++ b/src/test/shell/appsTest.c @@ -247,7 +247,9 @@ static void caseProgmanViews(void); static void caseShellInfo(void); static void caseTempCopyEdges(void); static WidgetT *clickButton(WindowT *win, const char *caption); +static WidgetT *clickButtonIn(WidgetT *root, const char *caption); static void collectLeaves(WidgetT *w, WidgetT **arr, int32_t *count); +static void cpanelColorPick(AppContextT *ctx, WindowT *modal, void *userCtx); static void f1DuringIndex(AppContextT *ctx, WindowT *modal, void *userCtx); static void fireHandlers(WidgetT *w, bool clicksToo); static WidgetT *firstFocusable(WidgetT *root); @@ -335,7 +337,7 @@ static void caseCpanelCancelKeepsDblClick(void) { static void caseCpanelColors(void) { - tCase("cpanel colors tab: sliders, themes, reset"); + tCase("cpanel colors tab: color dialog, themes, reset"); fixShellUp(SCREEN_BPP); TEST_EQ(platformMkdirRecursive("CONFIG/THEMES"), 0); @@ -355,25 +357,26 @@ static void caseCpanelColors(void) { wgtTabControlSetActive(tabs, 1); tPump(SETTLE_FRAMES); - // Select a color, drive the RGB sliders, and confirm the scheme moved. + // Select a color, pick a new one in the color dialog (Change...), and + // confirm the scheme moved and the page shows it. Cancel in the + // dialog changes nothing. WidgetT *colorList = widgetAfterLabel(root, "System Colors:"); - WidgetT *redSldr = widgetAfterLabel(root, "Red:"); - WidgetT *greenSldr = widgetAfterLabel(root, "Green:"); - WidgetT *blueSldr = widgetAfterLabel(root, "Blue:"); + WidgetT *swatch = widgetAfterLabel(root, "Color:"); + WidgetT *redLbl = swatch ? swatch->nextSibling : NULL; + WidgetT *blueLbl = (redLbl && redLbl->nextSibling) ? redLbl->nextSibling->nextSibling : NULL; + uint32_t pick = DVX_RGB(11, 22, 33); - TEST_TRUE(colorList && redSldr && greenSldr && blueSldr); + TEST_TRUE(colorList && blueLbl); if (colorList && colorList->onChange) { wgtListBoxSetSelected(colorList, 2); colorList->onChange(colorList); } - if (redSldr && redSldr->onChange) { - wgtSliderSetValue(redSldr, 11); - wgtSliderSetValue(greenSldr, 22); - wgtSliderSetValue(blueSldr, 33); - redSldr->onChange(redSldr); - } + fixDialogsOn("OK"); + tDialogScript(cpanelColorPick, &pick); + TEST_TRUE(clickButton(win, "Change...") != NULL); + fixDialogsOff(); uint8_t r = 0; uint8_t g = 0; @@ -384,6 +387,17 @@ static void caseCpanelColors(void) { TEST_EQ(g, 22); TEST_EQ(b, 33); + if (blueLbl) { + TEST_STR_EQ(wgtGetText(redLbl), "Red 11"); + TEST_STR_EQ(wgtGetText(blueLbl), "Blue 33"); + } + + fixDialogsOn("Cancel"); + colorList->onDblClick(colorList); + fixDialogsOff(); + dvxGetColor(tCtx(), (ColorIdE)2, &r, &g, &b); + TEST_EQ(r, 11); + // Save the scheme as a theme through the file dialog, then round-trip. fixDialogsOn("OK"); fixDialogsType("TEST.THM", "Save"); @@ -577,7 +591,7 @@ static void caseCpanelFileTypes(void) { pmScriptStart(); pmStep("OK"); pmField("Program:", APP_CPANEL); - clickButton(win, "Change..."); + clickButtonIn(types->parent, "Change..."); TEST_STR_EQ(wgtListViewGetCell(types, 0, 2), APP_CPANEL); wgtListViewSetSelected(types, 1); clickButton(win, "Remove"); @@ -714,8 +728,11 @@ static void caseCpanelMouse(void) { if (pass == 0) { // Fire every onChange/onDblClick in the window: wheel and accel // dropdowns, all three sliders, the color plumbing, the - // double-click test button. + // double-click test button. A dialog one opens (the color + // list's double-click) is cancelled. + fixDialogsOn("Cancel"); fireHandlers(root, false); + fixDialogsOff(); tPump(SETTLE_FRAMES); // Reversed wheel via the dropdown next to its label. @@ -2494,7 +2511,14 @@ static void caseTempCopyEdges(void) { // Finds a button by caption (accelerator markers ignored) and clicks it. static WidgetT *clickButton(WindowT *win, const char *caption) { - WidgetT *w = win ? tFindByText(win->widgetRoot, caption) : NULL; + return clickButtonIn(win ? win->widgetRoot : NULL, caption); +} + + +// clickButton for the first button of that caption under root (a page +// whose caption another page also uses). +static WidgetT *clickButtonIn(WidgetT *root, const char *caption) { + WidgetT *w = root ? tFindByText(root, caption) : NULL; if (w && w->onClick) { w->onClick(w); @@ -2523,6 +2547,13 @@ static void collectLeaves(WidgetT *w, WidgetT **arr, int32_t *count) { } +// Dialog script: choose *(uint32_t *)userCtx in the color dialog. +static void cpanelColorPick(AppContextT *ctx, WindowT *modal, void *userCtx) { + (void)ctx; + TEST_TRUE(tColorDialogPick(modal, *(const uint32_t *)userCtx)); +} + + // Dialog script for caseHelpNewArgsInDialog: first frame, "press F1" (the // shell hands the viewer another file) and repaint the list; second frame, // close the dialog. diff --git a/src/widgets/kpunch/canvas/canvas.bhs b/src/widgets/kpunch/canvas/canvas.bhs index c4ccbb03..4fd71f89 100644 --- a/src/widgets/kpunch/canvas/canvas.bhs +++ b/src/widgets/kpunch/canvas/canvas.bhs @@ -84,4 +84,7 @@ If Picture1.Save("output.bmp") <> 0 Then End If .endcode +A PictureBox also has Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle): see Drawing on Forms and PictureBoxes. + .link ctrl.common.props Common Properties, Events, and Methods +.link ctrl.drawing Drawing on Forms and PictureBoxes