FoxDev StudioFor Visual FoxPro 9 applications
The runtime

Bytecode

The module the compiler writes and the VM reads - the container, the constant pool, function prototypes and lambda captures - and what real programs compile to, shown with the disassembler.

The bytecode is Visual FoxPro's p-code analogue: the serialisable output of the compiler and the only input of the VM. It is a stack machine with a deliberately fat instruction set. Where a small machine would spell REPLACE out as twenty loads and stores, this one has a ReplaceField instruction, so the interpreter loop is one match, a statement boundary is an explicit instruction, and a disassembly reads like the program it came from.

This page is the format at version 9 and a tour of what the compiler makes of ordinary programs. Every listing on it is the output of foxvm disasm. The instruction reference lists all 199 instructions with their operands and stack effects, generated from the VM's source.

From source to module

The pipeline/one crate, four stages, two places to stop
Source .prg, a method Lexer lexer.rs tokens Parser parser.rs, ast.rs AST Compiler compiler.rs Module The VM vm.rs Diagnostics, with line and column the editor linter and foxvm check stop here; the parser recovers and reads on encode() bytes in an .fxa every program takes these four stages: a .prg, a form method, a menu command, a Command Window line, EVALUATE() text, or the line a macro produced

The compiler produces a Module. In the IDE it is handed straight to load_module; Build App encodes it to bytes and stores them base64 in the .fxa bundle, and the player decodes them again. Nothing about a module depends on which road it took.

The container

A module is a byte string:

"FXVM"            4 bytes, the magic
version           u16, little-endian; 9 today
body              the Module, encoded with postcard

A module with the wrong magic is refused as not a FoxVM module. A module with another version is refused with "Module was built with bytecode version N; this runtime expects 9. Rebuild the project." - the format is not versioned for compatibility, it is versioned so that a stale .fxa says so plainly. An .fxa bundle also carries the crate version that built it, for the same reason.

postcard is the serde format for the body: compact, schemaless, and the crate the rest of the runtime already depends on rather than a wire format of our own. Two consequences follow that matter to anyone reading the format. An enum variant is encoded by its position in the enum, so an instruction can be added at the end but never moved; that is why a few instructions sit away from their family in the source and the reference shows them under the family instead. And integers are varints, so a small constant index or slot number costs one byte.

The module

What a Module holds/and what each part is indexed by
name, kind Program | Form | Snippet | Expression consts Vec<Constant> Const(i) pushes one; class property values live here too names Vec<String>, upper-cased variables, procedures, settings: LoadName, Do, SetCmd members Vec<String>, as written properties and methods: GetMember, SetMember, CallMethod funcs Vec<FuncProto> funcs[0] is main; calls and MakeLambda name one by index methods "OBJPATH.EVENT" -> func a form's Click finds its function here; ".INIT" is the form classes, queries, cursors, dlls DEFINE CLASS, SQL plans, CURSOR columns, DECLARE DLL

The four kinds say what funcs[0] is:

Kind What it holds
Program A .prg: funcs[0] is the implicit main, the rest are its PROCEDUREs and FUNCTIONs.
Form One function per method with source, listed in methods under "PGFMAIN.PAGE1.LBLGREETING.CLICK" or ".INIT" for the form itself. No main.
Snippet A menu command, an EXECSCRIPT() body, a Command Window line: funcs[0] is the body.
Expression EVALUATE(), &macro, a SKIP FOR condition: funcs[0] pushes one value and returns it.

Constants

Num(f64, chars, decimals)   a number and the width it was written in
Money(i64)                  $12.34 as written, in ten-thousandths
Str(String)
Date(Option<i32>)           days since 1970-01-01; None is the empty date
DateTime(Option<f64>)       seconds since 1970-01-01; None is the empty datetime
Bool(bool)                  only class property values need these; code pushes True/False
Null
Array(Vec<Constant>)        DIMENSION aRGB[3] in a class body

A numeric constant remembers how it was written because Visual FoxPro does: ? 001 prints three characters, and the width has to travel with the value for that to be true. In the listings below, Num(10.0, 2, 0) is the 10 of FOR i = 1 TO 10: two characters, none of them past the point.

Function prototypes

FuncProto
  name          upper-cased; MAIN for a program body, OBJPATH.EVENT for a method
  display_name  as error messages and PROGRAM() show it
  nparams       declared parameters, which occupy local slots 0..nparams
  locals        slot -> upper-cased name, so runtime-compiled code can find a local by name
  def_line      the source line LINENO(1) counts from
  captures      for a lambda: (from, to) slot pairs; empty for everything else
  code          the instructions

Classes

DEFINE CLASS ... ENDDEFINE becomes a ClassProto: the name and parent as written, the property values constant-folded, the ADD OBJECT members, and the methods as ("Init", func) or ("image1.Click", func). Inheritance is not flattened: a proto carries only what its own declaration says, and the host walks the chain when it instantiates.

A frame

Every function call pushes a frame, and the frame is where the instructions' operands point.

One frame, and the value stack beside it/what LoadLocal, LoadName and LoadThis reach
Frame module 0, func 1, pc 4, line 9 locals: one slot each, .F. until written 0 PN 1 I 2 TOTAL 3 #FOR.. 4 N (capt.) params first, then LOCALs, then the compiler's temporaries, then a lambda's captured slots. A callee never sees them. privates: { X: 5 } PRIVATE and undeclared: dynamic this: handle 12 LoadThis; THISFORM comes from it with_stack: [ 12 ] PushWith / LoadWith / PopWith args, stack_base, stmt_sp, catch, finally Value stack shared by the fiber Number(2, w1.0) top Ref -> caller's slot 0 Str("customer") stack_base: cut back to here on return or unwind the caller's values, untouched LoadLocal(2) slot 2 of this frame LoadName(0) privates here, then each caller's, then PUBLIC Ref(Local(0)) a cell over slot 0, for a @ argument

A LOCAL is a slot and is never visible to a callee. A PRIVATE or an undeclared name goes in the frame's privates map and is found by walking the callers, which is what dynamic scope has always meant here. #FOR_END1 and #FOR_STEP2 in the listings below are the compiler's own slots: a FOR loop's bounds are evaluated once, as Visual FoxPro evaluates them, and have to be kept somewhere a program cannot name.

What programs compile to

Each listing is foxvm disasm on the program above it. Read [before] -> [after] with the top of the stack on the right.

A statement, a local, a print

LOCAL n
n = 3
? n * 2
func 0 MAIN (demo) nparams=0 locals=["N"]
     0  Stmt(1)
     1  DeclLocal(0)
     2  Stmt(2)
     3  Const(0)                              ; Num(3.0, 1, 0)
     4  StoreLocal(0)
     5  Stmt(3)
     6  Print { newline: true, argc: 0 }
     7  LoadLocal(0)
     8  Const(1)                              ; Num(2.0, 1, 0)
     9  Mul
    10  Print { newline: false, argc: 1 }
    11  True
    12  EndOfCode

Every statement begins with Stmt(line): it records the line for error messages and LINENO(), and it is where a breakpoint hooks. ? is two prints, the newline it starts with and then the item, because ?? is the same instruction without the first. True then EndOfCode is the .T. a routine answers when it runs off the end, which is what Visual FoxPro answers, measured. EndOfCode is not Return: a written RETURN also has to leave a line a macro put together, and the routine that line belongs to.

IF, and a short-circuit AND

LOCAL a, b
a = 3
b = 4
IF a > 2 AND b < 5
    ? "both"
ELSE
    ? "not both"
ENDIF
    10  LoadLocal(0)
    11  Const(2)
    12  Gt
    13  JumpIfFalseKeep(18)
    14  LoadLocal(1)
    15  Const(3)
    16  Lt
    17  And
    18  JumpIfFalse(24)
    19  Stmt(5)
    20  Print { newline: true, argc: 0 }
    21  Const(4)                              ; Str("both")
    22  Print { newline: false, argc: 1 }
    23  Jump(28)
    24  Stmt(7)
    25  Print { newline: true, argc: 0 }
    26  Const(5)                              ; Str("not both")
    27  Print { newline: false, argc: 1 }
    28  True

a AND b compiles to a; JumpIfFalseKeep(end); b; And; end:. JumpIfFalseKeep peeks rather than pops, so when the left side is .F. it stays on the stack as the answer and the right side is never evaluated; when it is .T. the right side is evaluated and And combines the two, three-valued, because either may be .NULL.. Jump targets are absolute program counters.

FOR, with its bounds evaluated once

LOCAL i, total
total = 0
FOR i = 1 TO 10 STEP 2
    total = total + i
ENDFOR
? total
func 0 MAIN (loop) nparams=0 locals=["I", "TOTAL", "#FOR_END1", "#FOR_STEP2"]
     7  Const(1)                              ; Num(1.0, 1, 0)
     8  StoreLocal(0)
     9  Const(2)                              ; Num(10.0, 2, 0)
    10  StoreLocal(2)
    11  Const(3)                              ; Num(2.0, 1, 0)
    12  StoreLocal(3)
    13  LoadLocal(0)
    14  LoadLocal(2)
    15  LoadLocal(3)
    16  ForTest(27)
    17  Stmt(4)
    18  LoadLocal(1)
    19  LoadLocal(0)
    20  Add
    21  StoreLocal(1)
    22  LoadLocal(0)
    23  LoadLocal(3)
    24  Add
    25  StoreLocal(0)
    26  Jump(13)
    27  Stmt(6)

The end and the step go into two slots the program cannot name, so changing total inside the body cannot change how many times it runs. ForTest takes [var, end, step] and jumps out when the loop is finished, in either direction, which is why the step is on the stack too.

DO WITH a by-reference argument, and a FUNCTION call

LOCAL n
n = 5
DO double WITH n
? n
? twice(4)

PROCEDURE double
    LPARAMETERS pn
    pn = pn * 2
ENDPROC

FUNCTION twice(x)
    RETURN x * 2
ENDFUNC
func 0 MAIN (call) nparams=0 locals=["N"]
     6  Ref(Local(0))
     7  Do { name: 0, argc: 1, in_prog: false }     ; name 0: DOUBLE
    ...
    14  Const(1)                                    ; Num(4.0, 1, 0)
    15  IndexOrCall { name: 1, argc: 1 }            ; name 1: TWICE
func 1 DOUBLE (double) nparams=1 locals=["PN"]
     0  Stmt(9)
     1  LoadLocal(0)
     2  Const(2)
     3  Mul
     4  StoreLocal(0)
     5  True
     6  EndOfCode
func 2 TWICE (twice) nparams=1 locals=["X"]
     0  Stmt(13)
     1  LoadLocal(0)
     2  Const(2)
     3  Mul
     4  Return

DO ... WITH passes by reference, so the argument is Ref(Local(0)): a cell over the caller's slot that the callee's StoreLocal(0) writes through, which is why ? n prints 10. A function call in an expression is IndexOrCall, the one question asked of a name: an array of that name is subscripted, a function is called, and anything else looks for a program of that name. A parameter is slot 0 of the callee; nparams=1 says how many slots the arguments fill.

TRY, CATCH TO, FINALLY

TRY
    x = 1 / 0
CATCH TO oErr
    ? oErr.Message
FINALLY
    ? "done"
ENDTRY
     1  TryPush { catch: 9, finally: 17 }
     2  Stmt(2)
     3  Const(0)
     4  Const(1)
     5  Div
     6  StoreName(0)                          ; name 0: X
     7  TryPop
     8  Jump(17)
     9  CatchObject
    10  StoreName(1)                          ; name 1: OERR
    11  Stmt(4)
    12  Print { newline: true, argc: 0 }
    13  LoadField { area: Some(1), field: 0 } ; oErr.Message
    14  Print { newline: false, argc: 1 }
    15  TryPop
    16  Jump(17)
    17  Stmt(6)
    18  Print { newline: true, argc: 0 }
    19  Const(2)                              ; Str("done")
    20  Print { newline: false, argc: 1 }
    21  EndFinally

TryPush installs a handler naming both targets. When Div raises error 1307, the VM pops any frames above the one that installed the handler, cuts the value stack back to where it was at TryPush, and jumps to catch. CatchObject yields CreateException so the host can build the Exception object, and StoreName puts it in oErr. Both paths end at finally, and EndFinally re-raises whatever was still propagating when the block was entered - nothing, on the paths shown. oErr.Message is LoadField and not GetMember, because a bare name followed by a dot is only known at run time to be an object variable, an m. prefix, or a table alias.

A lambda and its capture

LOCAL n, f
n = 1
f = LAMBDA(x)
    RETURN x + n
ENDLAMBDA
? f(10)
func 0 MAIN (lambda) nparams=0 locals=["N", "F"]
     4  Const(0)                              ; Num(1.0, 1, 0)
     5  StoreLocal(0)
     6  Stmt(3)
     7  MakeLambda(1)
     8  StoreLocal(1)
     9  Stmt(6)
    10  Print { newline: true, argc: 0 }
    11  LoadLocal(1)
    12  Const(1)                              ; Num(10.0, 2, 0)
    13  IndexOrCallValue(1)
    14  Print { newline: false, argc: 1 }
func 1 #LAMBDA1 (lambda LAMBDA at line 3) nparams=1 locals=["X", "N", "F"]
     0  Stmt(4)
     1  LoadLocal(0)
     2  LoadLocal(1)
     3  Add
     4  Return

The lambda is an ordinary function of the module, #LAMBDA1, with a parameter in slot 0 and the enclosing routine's two locals in slots 1 and 2; its proto's captures say [(0, 1), (1, 2)]. MakeLambda(1) copies slot 0 and slot 1 of the current frame into a new function value and pushes it. IndexOrCallValue(1) asks of a value what IndexOrCall asks of a name, and when it is a function the VM pushes a frame for #LAMBDA1, writes the argument into slot 0 and the captured values into slots 1 and 2, and carries on round the loop. Nothing recurses.

Capture by value/the two moments a captured LOCAL is copied
MAIN's frame 0 N = 1 1 F slots 0 and 1 MakeLambda(1) copies Function value #7 module 0, func 1 this: none captured: [1, .F.] kept for the run f(10) copies #LAMBDA1's frame X=10 N=1 F slots 0, 1 and 2 n = 2 after the lambda was made: it still adds 1, because slot 0 was copied at MakeLambda. n = n + 10 inside the lambda: changes slot 1 of the lambda's frame, never MAIN's slot 0. A PRIVATE is not in any slot, so it is not copied: it is looked up when the lambda runs. THIS travels with the value, so a lambda written in a method keeps its object.

SCAN FOR

USE customer
SCAN FOR country = "UK"
    ? company
ENDSCAN
USE
     1  Const(0)                              ; Str("customer")
     2  Use { alias: None, named_alias: false, exclusive: false, online: false, in_area: false }
     3  OpenIndex
     4  Stmt(2)
     5  Go(Top)
     6  CallBuiltin { id: 145, argc: 0 }      ; EOF()
     7  JumpIfTrue(19)
     8  LoadName(0)                           ; name 0: COUNTRY
     9  Const(1)                              ; Str("UK")
    10  Eq
    11  JumpIfFalse(16)
    12  Stmt(3)
    13  Print { newline: true, argc: 0 }
    14  LoadName(1)                           ; name 1: COMPANY
    15  Print { newline: false, argc: 1 }
    16  Const(2)                              ; Num(1.0, 1, 0)
    17  Skip
    18  Jump(6)
    19  Stmt(5)
    20  Const(3)                              ; Str("")
    21  Use { alias: None, ... }

A SCAN is nothing special: Go(Top), then a loop of EOF(), the FOR condition, the body, Skip 1. Use yields a request to open the file and OpenIndex reads the structural .cdx beside it if the header says one is there. A field is read by LoadName, the same instruction a variable uses, because Visual FoxPro looks a bare name up the same way: a variable first, then a field of the selected area. USE with an empty path closes the area.

SELECT-SQL

SELECT custno, SUM(amount) AS total ;
    FROM orders ;
    GROUP BY custno ;
    INTO CURSOR c_top
     1  SqlOpen { table: 0, alias: 0, named: false }
     2  SqlBegin(0)
     3  SelectSource(0)
     4  Go(Top)
     5  SelectSource(0)
     6  CallBuiltin { id: 145, argc: 0 }      ; EOF()
     7  JumpIfTrue(16)
     8  LoadName(1)                           ; CUSTNO, the group key
     9  LoadName(2)                           ; AMOUNT, for SUM
    10  LoadName(1)                           ; CUSTNO, the select list
    11  SqlRow(3)
    12  SelectSource(0)
    13  Const(1)
    14  Skip
    15  Jump(5)
    16  SqlEnd

A query is compiled to the same loop a hand-written SCAN uses: Go(Top), EOF(), Skip. What holds it together is Module::queries[0], a QueryPlan that says what the three values SqlRow(3) collects per record are for - a group key, an aggregate input, a column - and SqlEnd finishes the grouping, applies HAVING and ORDER BY, and writes the cursor. Several sources are nested loops with SelectSource(n) choosing which is current, and an outer join's miss is JoinMiss. That a query is bytecode over the same primitives is why it can read a table larger than memory the way a SCAN does.

Objects and WITH

oForm.Caption = "Hi"
WITH oForm
    .Left = 10
    .Show()
ENDWITH
     1  Const(0)                              ; Str("Hi")
     2  LoadName(0)                           ; OFORM
     3  SetMember(0)                          ; member 0: Caption
     4  Stmt(2)
     5  LoadName(0)
     6  PushWith
     7  Stmt(3)
     8  Const(1)
     9  LoadWith
    10  SetMember(1)                          ; member 1: Left
    11  Stmt(4)
    12  LoadWith
    13  CallMethod { name: 2, argc: 0 }       ; member 2: Show
    14  Pop
    15  PopWith

SetMember takes [value, obj] and yields SetProp to the host, which owns the object; the fiber is resumed once the property is written and any ProgrammaticChange it fired has run. CallMethod yields CallMethod and its result is pushed, so a call whose value is not wanted is followed by Pop. WITH is a stack in the frame: PushWith saves the object, LoadWith reads it for each .member, PopWith lets it go. Member names are stored as written and the host compares them case-insensitively; variable names are upper-cased in the module because the language is.

Which instructions leave the machine

About a third of the instructions can yield a host request; the rest complete inside the VM. The ones that yield are the ones whose effect is outside the machine's own memory: every object write and method call, every file and table operation, every dialog, DoForm, DoMenu, CreateObject, the DLL and library calls. An instruction that yields is resumed with a value and either pushes it, discards it, or - for the data instructions, which can take several round trips to read a record - rewinds the program counter and runs again with the reply in hand. The virtual machine page is about that loop; the instruction reference says which request each one yields.