The new keywords
LAMBDA and ENDLAMBDA, the FoxScript namespace, and the two value types Visual FoxPro has not got. Each rule was written down before it was coded, and each has a test that fails if it changes.
FoxScript is Visual FoxPro 9 with things added: a function value you can hand to something else to run later, a standard library that reaches the machine the way a program written this decade expects to, and tables that are not stopped by two gigabytes. It is a superset. Every program that runs in the product keeps running here and keeps meaning the same thing, and that is the rule every decision below answers to: where the product already has an answer, the product wins.
The whole project's method is to ask vfp9.exe. FoxScript has no vfp9.exe to ask, so where
there is nothing to measure the specification is written down first and the tests are the
specification. Every numbered rule on this page has one.
LAMBDA ... ENDLAMBDA
LOCAL f
f = LAMBDA(x, y)
RETURN x * y
ENDLAMBDA
? f(6, 7) && 42
Why this spelling
Every block in the language is a word and its matching end: IF/ENDIF, DO CASE/ENDCASE,
FOR/ENDFOR, SCAN/ENDSCAN, TRY/ENDTRY, WITH/ENDWITH, TEXT/ENDTEXT,
DEFINE CLASS/ENDDEFINE, PROCEDURE/ENDPROC. A brace block would be the only C-shaped
thing in it, and braces are not free anyway: {^2024-01-31} is a date literal and {} an empty
one. The word form costs nothing and reads like the language.
ENDLAMBDA rather than a bare END, and that was measured rather than chosen: END at the
start of a statement is already the beginning of END TRANSACTION, and the product answers a
bare one with error 1221, "Command is missing required clause." ENDLAMBDA collides with
nothing.
1. When LAMBDA opens a lambda
LAMBDA is not a reserved word. Measured in vfp9.exe, LAMBDA and ENDLAMBDA are legal
variable names, field names and property names, so LAMBDA = 5, DIMENSION lambda(3),
lambda(1) and tst.lambda all keep working.
LAMBDA opens a lambda only when it is followed by (, a possibly empty comma-separated list
of plain names, ), and then the end of the line. Anything else is an ordinary read of a
variable called LAMBDA. An array subscript is an expression; a parameter list is a list of
names; that tells the two apart without reserving the word. LAMBDA is matched as a whole word,
because it is read in expression position where LAMB has to stay available; ENDLAMBDA
abbreviates to four letters as every block terminator does.
2. The body
The body is the newline-terminated statements between the LAMBDA(...) line and ENDLAMBDA.
The expression parser stops treating a newline as the end of the statement for as long as the
body lasts, and recovers at the lambda's own ENDLAMBDA rather than swallowing the rest of the
file. A body that never closes is one error at the LAMBDA; a bad line inside a body is one
error, not a cascade. ENDLAMBDA) is the ordinary shape, because the lambda usually sits inside
an argument list that still has to close.
ENDLAMBDA outside a lambda body is not a keyword at all: ENDLAMBDA = 7 is legal FoxPro,
measured, and a word that closes nothing must not stop a program that uses it as a name.
LPARAMETERS and PARAMETERS inside a body are a syntax error. The parameters are written in
LAMBDA(...) and a second declaration has nowhere to mean anything.
3. Capture
A name in the body is resolved in this order, and the order is the whole of the scope rule:
- a parameter of the lambda;
- a
LOCALof the lambda; - a
LOCALof an enclosing routine, which is captured by value when the lambda is made; - anything else, which stays dynamic and is resolved when the lambda runs, against the private chain and the globals live then.
The third rule is forced. A LOCAL is a slot in a frame, the frame is gone by the time a
handler runs, and FoxPro has no reference to a local to keep instead. So the lambda gets a slot
of its own and the value is copied into it at the moment LAMBDA is evaluated. Two things
follow, and both are tested: a later change to the outer local is not seen by the lambda, and an
assignment to the captured name inside the lambda changes the lambda's copy, not the outer
variable.
LOCAL n, f
n = 1
f = LAMBDA()
n = n + 10
RETURN n
ENDLAMBDA
n = 2
? f() && 11: the lambda captured 1 when it was made
? n && 2: the lambda's copy is its own
Every LOCAL of the enclosing routine is captured, not only the ones the body mentions. It
costs a copy of each and it is what makes the chain work at any depth: a lambda inside a lambda
captures from the middle one's frame, and the middle one has the outer routine's locals in it.
The fourth rule is what dynamic scope already means: a PRIVATE set up before the lambda runs is
what the lambda sees, whoever set it up. It is also how recursion works - by way of a PRIVATE
or a PUBLIC, never a LOCAL, because a LOCAL holding the lambda is captured before the
lambda exists and the copy would be .F..
THIS is captured with the lambda, so a lambda written inside a method keeps its object.
THISFORM and THISFORMSET are worked out from the captured THIS when the lambda runs, which
is what they already are inside a method.
4. The value
Value gains a function: an id into a table the VM owns, holding the module, the function
index, the captured slot values and the captured THIS.
VARTYPE()answers "F". Measured, the lettersvfp9.execan return are C, N, Y, D, T, L, O, G, Q, X and U, so "F" is free; a Float field answers "N" there, so it is not that letter either.TYPE()answers "F" too, and the type name in an error message is "Function".EVALUATE(),STORE, an array element and an object property all hold one. A function value crossing to the host travels as its id, the way an object travels as its handle.=and==between two function values are true when they are the same function value, which mirrors objects, measured.- A function value is kept for the life of the runtime, like a compiled procedure;
RELEASEdoes not free one. That is a deliberate simplification, and the reason to make lambdas where a procedure would be written rather than inside a loop.
5. Calling one
f(a, b) where f holds a function value calls it. The name is read exactly as it is today: a
name holding an array is subscripted, a name holding a function is called, and a name holding
anything else still looks for a program of that name and still fails with error 1, "File 'f.prg'
does not exist.", measured.
Inside the body, a lambda is a procedure:
- missing arguments are padded with
.F.(measured);PCOUNT()answers how many were passed; RETURNwith no value returns.T.(measured), and so does running off the end;- a
@argument is by reference, because a lambda parameter is a parameter like any other.
Two shapes are worth knowing:
- A lambda that is not in a name is called where it stands.
laRoutes[1, 2](req, res)andmake()(20)both work. The rule is narrow on purpose: a(calls the value only after a subscript or a call, where the expression already ended in a bracket. Measured, a(there is error 36 in the product, so no program that runs there is read differently here. - A lambda kept in a property is read into a name first.
oForm.Handler(x)is a call of the methodHandlerin the product and stays one here, sof = oForm.Handlerand thenf(x)is the form that works.oForm.aHandlers[1](x)does work, because it ends in a bracket.
The FoxScript namespace
One global, FoxScript, and everything hangs off it. It is answered by the VM rather than the
host, and the reason is CursorToJson: a cursor is in the VM's own data engine and the host has
no way to read one. The VM makes the values; the host makes the machine.
? FoxScript.Version && the runtime's version
oServer = FoxScript.Http.CreateServer()
oData = FoxScript.Json.Parse('{"a": [1, 2, 3]}')
cText = FoxScript.Data.CursorToJson("c_cust")
The rules it follows
FoxScriptis a name only when nothing else in scope answers to it. A program that already has a variable, private or public calledFoxScriptkeeps working and keeps meaning what it meant; giving the variable up gives the namespace back. The same rule keepsLAMBDAa variable name.VARTYPE()answers "O". It is an object to the programmer.AMEMBERS()answers honestly, every member marked native and every property read-only, which they are.GETPEM()reads one the way it reads any other, so the namespace can be discovered from the Command Window.- A program may not write to it. A member it has raises 1743, "X is a read-only property."; a member it has not raises the product's own 1925, "Unknown member X.". Nothing reaches the host with a handle the host has never heard of.
- FoxScript's own errors are numbered from 3001. Visual FoxPro's numbers stop a long way below that, so a program can tell one of ours from one of the product's by the number alone.
The members
| Object | Members |
|---|---|
FoxScript |
Version, Http, Data, Json. |
FoxScript.Http |
CreateServer(); see the HTTP API. |
FoxScript.Data |
CursorToJson(cAlias), JsonToCursor(vJsonOrText, cAlias). |
FoxScript.Json |
Parse, Stringify, Count, Keys, Has, Get. |
FoxScript.Fs, FoxScript.Process and FoxScript.Crypto are later, and only where the product
has no answer already. FoxScript.Crypto in particular must not duplicate what a .fll does:
hosting libraries was done to keep the legacy road open.
The JSON value
A JSON value is not a FoxPro type and needs one. Empty plus ADDPROPERTY is close and is not
it: no arrays, no null, no order. So Value gains a JSON value - an ordered map, arrays,
numbers, strings, booleans and null - built on serde_json with key order preserved, because
that is the crate for this and hand-rolling a parser is the thing the project's rules forbid.
VARTYPE()answers "J", chosen the way "F" was: free in the product. The type name in a message is "Json".- A member is read by name, case-insensitively, because every other member read in this
language is:
oData.nameandoData.Nameare the same key. Where two keys differ only in case, the first in document order wins. - An element is read by subscript, one-based like everything else:
oData[1]andoData(1). Outside the array is the product's own invalid-subscript error. - What comes out is a FoxPro value where JSON has one: a string is character, a number is
numeric, true and false are logical, null is
.NULL.. An object or an array comes out as another JSON value, so reaching goes on:oData.items[1].name. - Two JSON values are equal when they hold the same thing. There is nothing to be identical
to, so
=compares what is in them. - Crossing to the host it goes as its text, so
res.Json(oData)works as readily asres.Json(cText).
FoxScript.Json has six members and no more:
| Member | Answers |
|---|---|
Parse(cText) |
The value, or error 3001 with the parser's own complaint. |
Stringify(vAny [, lPretty]) |
The text. It takes any FoxPro value, so a number, a logical, a date or an array can be written out. |
Count(vAny) |
How many members an object has or elements an array has; 0 for anything else. |
Keys(vObject) |
A JSON array of the names, in document order. |
Has(vObject, cName) |
A logical. |
Get(vObject, cName) |
The member, or .NULL. when there is none. oData.name for a name that is not there raises unknown-member, as a property would; Get asks rather than insists. |
The data bridge
FoxScript.Data.CursorToJson(cAlias) answers text, because the name says so and because what
a program does with it next is put it on a socket. FoxScript.Json.Parse() is there for a
program that wants to reach into it. JsonToCursor(vJsonOrText, cAlias) builds a cursor and
answers how many records went into it.
The mapping is written down because nothing can be measured for it.
A cursor to JSON. An array with one object per record, in record order. A deleted record is
left out when SET DELETED is ON, as SCAN already does. Key names are the field names
lower-cased, the same choice the product makes when it writes DIF and SYLK, measured.
Character and memo fields become strings with their trailing blanks trimmed. Numeric, float,
integer, double and currency become numbers. Logical becomes true or false. A date becomes
"YYYY-MM-DD" and a datetime "YYYY-MM-DDTHH:MM:SS". A general or blob field becomes null; a
null field is null.
JSON to a cursor. The value must be an array of objects, or one object, which becomes one
record. The fields are the union of the keys, in the order first seen. Each field's type comes
from the first non-null value seen for it: a string becomes character as wide as the widest
value, or a memo past 254; a number becomes numeric twenty wide with as many decimals as the
widest value needs; a boolean becomes logical; an object or array becomes a memo holding its
text. A field whose every value is null becomes logical, which is what vfp9.exe says a bare
.NULL. is, measured. A null value becomes the empty value of its field's type, because the
fields this makes are not nullable.
What did not change
Nothing about calls to procedures, built-ins or DLL functions. Nothing about PRIVATE,
PUBLIC or LOCAL. Nothing about any word that was a name before. The lexer gained no tokens:
LAMBDA and ENDLAMBDA are ordinary identifiers to it, as every keyword in this language is,
and the parser recognises them by position alone.