Get new drop: current VB9 Customer Technology Preview (CTP). Copy the database Saturn5_002.mdf and Saturn5_002_log.LDF to c:\. Zipped VB9 project directory tree.
The nice thing about this project is that we can explore technologies quite fully, but in the context of a concrete, small, completely understandable application: a programmable calculator. Anyone who can understand a Hewlett-Packard calculator can understand VB9, LINQ, ADO.NET, comprehensions, lambda expressions, and all the rest of these beautiful new technologies. Many ‘package’ samples are too large, offering no way to understand them piece-by-piece; or too artificial or abstract, leaving too big a gap to what one wants to learn to do; or too mundane to keep you awake. IQ97 is just big enough, just small enough, concrete, and fun.
Because people reason better from the concrete to the abstract rather than the other way around, many generalizations to other kinds of projects will be quite obvious, some so obvious that they don’t need pointing out, others I’ll point out when I notice them. Here’s one right off the bat: every technique we employ to simulate the HP97 can carry over to implementing any other virtual-machine (VM) specification. Any important VMs come to mind? How about the CLR itself? IQ97 will give you lots of ideas toward creating your own CLR / CLI implementation. Later on, we will talk about a virtual machine for executing LINQ comprehensions: a VM that can be implemented over a relational database, over XML, over objects in memory, over any kind of collections.
Because IQ97 is small and constrained, it’s practical to do some crazy things with it. Now, you’re not supposed to resist, saying “why would anyone ever do THAT?” You’re supposed to say “wow, I never thought of that!” then let your imagination go, trying to think of ways to use them in your own work. For instance, I now have planned out the following FIVE execution modes
1. Delegates to static functions
a. Development Mode: Includes disassembly and dehydration into the database
b. Deployment Mode: When development is completely done, the static functions can be erased from the simulator, relying entirely on 2b
2. Delegates to dynamic functions via Lightweight Code Gen (LCG)
a. generated in parallel with disassembly and dehydration of the static functions
b. or, in a completely separate phase, dynamically rehydrated, re-assembled, and generated from the database
3. Dynamically generated lambda expressions
4. Rehydrated lambda expressions
5. Stored procedures in VB through SQLCLR in the database
The first mode is the easiest to understand and implement, and is the basis for all the other modes. In the current drop, 1a and 2a are implemented. 2B just takes courage, but will be the last thing I ever do with this. Some scaffolding for 3 is in place, and the rest are just dreams at present. Let’s dig into mode 1.
The first task is to establish some variables to hold the durable state—that is, persisted to the database—of the machine, in a module named DelExec, short for Delegate Execution. There is a 4-position data stack, a 4-position address stack for GSB (go subroutine) instructions, 4 flags, 224 program-memory slots, 26 registers, and a special last-x register for storing whatever was last in the X slot of the stack. VB9 requires the largest-index value in the declaration of the array bounds, not the length of the array, which would be one more. So an array of length 4 is declared with a 3 in round brackets, and the index will range from 0 to 3, inclusive.
Public Module DelExec
Public dStk(3) As Double ' Data Stack
Public aStk(3) As Integer ' Address Stack
Public flags(3) As Boolean
Public pgm(224) As String ' Program Memory
Public reg(25) As Double
Public lastX As Double
We’re using just ordinary VB Doubles for values in the calculator. This is not precisely correct, as the actual calculator uses binary-coded decimal (BCD) and it’s easy to detect the difference, for instance, in different overflow behavior. However, VB Doubles are good enough for ordinary circumstances, in particular, for the Diagnostics and Primes programs. Furthermore, our goal is to simulate the calculator software, not its hardware. It would be a different project to precisely simulate the calculator numerics, and even the exact microcode (someone else has written such a simulation of the microcode for the HP45). But that would take us away from the point of the exercise: to show off fancy language and programming techniques.
Other aspects of durable state include the current display precision, trig mode and display style, which are loaded with the defaults below when the calculator powers up:
Public dpyPrecision As Integer = 2
Public dpyStyle As String = "FIX"
Public trigMode As String = "RAD"
For executing stored programs, we need an instruction pointer (ip), and we shall add a couple more instrumentation variables to count the total number of instructions executed and the real time taken. These are not in the original calculator, of course, but are just too good not to add to our sim. Since we can execute the sim out of the database, these will need to be durable, too:
Public ip As Integer
Public ic As Integer
Public it As New Stopwatch REM from System.Diagnostics
We’ll need exactly these things in the database. Use “Open Table Definition” and “Show Table Data” in Visual Studio’s Server Explorer under the connection to Saturn5_002.mdf and look over the tables “Stack,” “Registers,” “Flags,” and “Controls” (I don’t see a way to copy-paste the database schema here, so just take a look). We interact with these tables using LINQ / Dataset as follows:
Sub initVmdb()
dInstructionSet = New DataPackage("InstructionSet")
dFlags = New DataPackage("Flags")
dControls = New DataPackage("Controls")
dStack = New DataPackage("Stack")
dRegisters = New DataPackage("Registers")
End Sub
DataPackage is my abstraction over the LINQ / Dataset stuff, capturing the patterns I use over and over:
Public Class DataPackage
Implements IDisposable
Public ds As DataSet
Public da As SqlDataAdapter
Public dt As DataTable
Public dq As IQueryable
Public cb As SqlCommandBuilder
Every one of my data packages will need all five of these things. I make them public for my own convenience. Yeah, I know the conventional wisdom of private fields, public properties, yadda yadda, but this is for me, only, and I don’t need to overdo that discipline. A couple of trivial constructors:
Sub New()
'Nothing interesting here, but must exist
End Sub
Sub New(ByVal nym As String)
Me.create(nym)
End Sub
The following is where the real stuff occurs: this is how we handle each table in the database. Given a name, first we make a DataSet (I frequently use the pseudonym “nym” for variables containing names, because there are too many other names of names named name in a typical system). We also must make a data adapter, then just have it query up the whole table into the DataSet, which I always want to do. We also must have a SQL command builder, even though it’s not directly used. Since each of my DataSets contains just one table, for simplicity, I can set a variable of type DataTable always to the 0-th element of ds.Tables:
Public Sub create(ByVal nym As String)
ds = New DataSet(nym)
da = New SqlDataAdapter("SELECT * FROM " + nym, conn)
cb = New SqlCommandBuilder(da) REM must exist, but not used
da.Fill(ds)
dt = ds.Tables(0)
Now, we have another pattern: In every one of my tables (with one exception, later), the first column and only the first column contains the primary key, which sometimes happens to be one of those SQL autoincrement is-identity kinds. I found the following patterns to work, partly by trial-and-error and partly by asking around. I don’t understand the TableMappings, but I need them:
Dim fudge(1) As DataColumn
fudge(0) = dt.Columns.Item(0)
dt.PrimaryKey = fudge
dq = dt.ToQueryable()
da.TableMappings.Add(nym, "Table")
End Sub
Public Function update(ByVal nym As String) As Integer
Return da.Update(ds, nym)
End Function
The rest of the class is just standard IDisposable boilerplate support for releasing non-garbage-collected resources:
Protected Overrides Sub finalize()
Me.Dispose()
MyBase.Finalize()
End Sub
Private disposedValue As Boolean = False ' To detect redundant calls
Protected Overridable Sub Dispose(ByVal disposing As Boolean)
If Not Me.disposedValue Then
If disposing Then
cb.Dispose()
ds.Dispose()
da.Dispose()
dt.Dispose()
Me.disposedValue = True
End If
' TODO: free shared unmanaged resources
End If
Me.disposedValue = True
End Sub
End Class
The next seven variables are NON-durable, and track modes in the simulator rather than any documented modes in the calculator. Numeric entry, for instance, requires us to track whether the decimal or EEX key has been pressed. This isn’t the time to get into those vagaries, the point here being that just a handful of extra variables take care of them.
Public eex(1) As Integer
Public currexp As Double = 0
Public liftP As Boolean = True ' Will next entry lift the stack?
Public traceP As Boolean = True
Public eexmode As Boolean = False
Public efrac As Boolean = False ' Entering a fractional part?
Public progP As Boolean = False
Notice the Public access for most everything. Since this application is standalone, I break things up into modules and classes just for design modularity, and access control is not something I need to worry much about, so I am completely whimsical about it.
I’ll use the term command to mean a calculator function, whether executed by clicking a button on the faceplate, or by typing a shortcut key with the faceplate up, or by typing a calculator command in the console window, or by running a stored calculator program. Every command has a static function in the DelExec module (all functions in a VB module happen to be static functions). Executing a command works by first presenting a command string to the simulator. IQ97 commands do not take arguments, but variable information is often encoded in the command string. For instance, the STO4 command means “Store a copy of the value in the X slot of the data stack into register 4.” Likewise, STO3 means “Store a copy of the value in the X slot into register 3.” Today, it would be considered a more mainstream design, perhaps, to give the destination register as an argument to a generic STO command. However, that was not quite the way Hewlett-Packard did it back then.
There is a bit of irony to the story: the way to key in the STO4 command on the calculator faceplate is first to press the STO key and then to press the 4 key. However, internally, this combination is stored as a single byte in program memory. In fact, this merged keystroke encoding was a big improvement over the ancestral HP65, which, in fact, stored commands and arguments sequentially. However, that led to twisted code as programmers strove to save precious memory.
We need a CLR type, cmdel, for command delegates and a dictionary, staticDelegates, to look up command delegates given command strings:
Delegate Function cmdel(ByVal cmd As String, ByVal ip As Integer) _
As Integer
Private staticDelegates As New Dictionary(Of String, cmdel)
So, in our sim, we pick up the STO key, then a numeric key, and build a combined command string, “STO4,” and the static function for all STO commands then fishes the register back out of the string, again. This explains why every command function takes the actual command string as an argument—it often needs to fish out hidden pseudo-argument information.
Every command function also takes an integer representing the current instruction pointer, and is responsible for returning the next value of the instruction pointer, which is almost always just one more than the current ip, exceptions including branch and subroutine-call instructions. This is another advantage of the merged keystroke encoding: since all instructions are the same length, all command functions can increment ip the same way.
So, as you can probably guess, there are 256 possible opcodes. The calculator defines 236 of them, and I implemented all but WDATA and MRG, which are for writing data to mag cards. You can see all the command strings and their key scan codes in the table “InstructionSet.” The numeric opcodes are not used in IQ97, so we don’t record or store them anywhere.
It might seem most straightforward to start off by showing the numeric-entry commands. However, they are a bit messy due to internal modalities concerning the decimal point and EEX key, so let’s delay presenting them for a bit. The numeric operation commands are a good place to start. Consider the following, which implements the plus command, whenever the + key is pressed:
Function plus(ByVal c As String, ByVal m As Integer) As Integer
Savex()
dStk(0) += dStk(1)
dStk(1) = dStk(2)
dStk(2) = dStk(3)
updateStackUI()
Return m + 1
End Function
So, clearly, dStk(0) represents the X slot of the data stack, dStk(1) the Y slot, and so on. Savex is a ubiquitous utility; most of the command functions call it:
Sub Savex()
lastX = dStk(0)
EndInput()
End Sub
which just saves the last value of the X slot of the data stack in the special register lastX, where it can be fetched with the LSTX command. EndInput interacts with the numeric input modes, so we’ll talk about it later.
UpdateStackUI is one of a bunch of functions for operating the blinking lights. We get to all that later.
So there you go: we have laid out the endoskeleton of the calculator implementation. Next installment, we’ll fill up the dictionary with command delegates and start executing functions!