Adding Commands to BASIC! - RFO-BASIC/Basic GitHub Wiki
Introduction
A word of caution
Let's get started
Command keywords
The formatter
The Command class
Adding a top-level-command
Command groups
Adding a command to an existing group
Adding a new command group
Command execution functions - overview
Return value (error management)
Parsing the command line (parameter management)
Command execution (state and resource management)
Note: Between v01.80 and v01.81, there is a very large change in the way commands are managed. If you are adding commands to a version older than v01.81, get a previous version of this page from the page history. The last version that worked with v01.80 and before was edited on May 30, 2014.
It is not difficult to add a command to the BASIC! interpreter. However, if your command does something useful, it probably uses some resources. Sometimes, managing the resources is more difficult than adding the command.
This presentation will start with simple commands that do not open Android resources. Later it will cover more complex cases.
[TODO: This presentation describes how to add a command. It does not (yet!) describe how to add a built-in function.]
[TODO: Insert description of how BASIC! starts. Basic starts either the Editor or the interpreter. The Editor can run a program by starting the interpreter.]
[TODO: Insert description of how the BASIC! program is loaded into the Basic.lines ArrayList and the Run Activity is launched. Run starts the interpreter Thread, which first runs PreScan() and then RunLoop(). For each line of the program, RunLoop() extracts a command keyword and invokes a Run method that executes the command.]
[Summary: The Run Activity is both the framework that provides context for the interpreter and the Console by which the interpreter communicates to the user. The Run Activity creates a separate thread for the interpreter. That is, it creates an instance (theBackground) of an inner class (Background) that extends Thread, then invokes the Thread.start() method to start the interpreter running.]
There are several forum discussions (TODO: provide links) about when and whether to add commands. Please remember: commands are not free. Every command costs something.
- Speed: The command list search is linear. Adding commands makes the average search time longer.
- Bloat: The code for each command makes BASIC! bigger, both the installable APK and the installed footprint.
- Manual: De Re BASIC! gets bigger, too. Since we still do not have a "start-up guide", a new user is faced with a 170-page book.
- Complexity: Again, consider the new user, trying to learn all of those commands. Consider also that it is difficult to use variables that start with keywords. Your new command may break somebody's code.
If, in spite of the cost, you still want to add a command, read on.
To add even the simplest command to BASIC!, you must do these four things:
- Define a command keyword
- Make the keyword available to the code formatter
- Code a way for the interpreter to associate the keyword with a command execution function
- Write the command execution function
[TODO: naming conventions for command keywords and command function names. For now: it's pretty simple, folks, just copy what you see. But please do be careful with command names: keywords are forever. Once released they become canon.]
All keyword strings are defined as constants. A keyword constant's name starts with the prefix BKW_
. The string itself is all lower-case. A keyword may be upper- or mixed-case in BASIC! programs, but the PreScan() forces all unquoted program text to lower-case to make keyword matching faster.
private static final String BKW_DIM = "dim"; // single command
private static final String BKW_EMAIL_SEND = "email.send"; // single command
Commands that share a common prefix, such as "gr.", can be grouped. The groups define a command hierarchy, like a very wide, shallow tree, making keyword searches faster.
The interpreter compares the input stream to each top-level keyword. If the input matches a keyword, the interpreter runs the method that does the work of the matching command (its "execution function"). The "execution function" of a group keyword is a method that searches the commands in the group.
The formatter is a separate Activity launched from the Editor. Its code is in the file Format.java. The method StartOfLineKW()
must have access to all of the keyword String values. It gets them from two public data structures in Run.java. You do not make any changes to Format.java when you add a command.
If you add a top-level command, you put its keyword constant in the BasicKeyWords[]
array. If you add a command to a group, you put its keyword the the group's keyword array. If you add a new command group, you put the group keyword in BasicKeyWords[]
and create a new keyword array for the group. You put the keyword array name the keywordLists
map by adding a line to the list in the getKewordLists()
method.
Near the top of the Run class, you can find an nested static class called Command
; right below it is the method executeCommand(Command[], String)
and two similar findCommand()
methods.
Every command is encapsulated in an object of the Command
class. Each Command
object has three parts:
- a command's keyword (a
BKW_
constant) - an ID marker that can be used to tell the interpreter something about the command
- a
run()
method that runs the command's execution function. [*]
[*] A Command
object is a "function object", or "functor", so an array of Command
objects works as a table of function pointers.[]
When you create a group command table, you populate it with anonymous Command
objects whose run()
methods execute your command execution functions. The findCommand()
and executeCommand()
methods search your table. If the command line matches one of your keywords, executeCommand()
runs the corresponding Command.run()
method.
For example:
private final Command[] BASIC_cmd = new Command[] {
...
new Command(BKW_DIM) { public boolean run() { return executeDIM(); } },
new Command(BKW_TIMER_GROUP, CID_GROUP) { public boolean run() { return executeTIMER(); } },
...
};
The example shows a top-level command ("dim"), and the top-level entry point for a command group ("timer."). The second also shows how to add a CID_
marker.
All top-level commands are in the array BASIC_cmd[]
, as shown in the example. Each command group has its own Command
array. A group array is named with "_cmd[]" at the end.
The interpreter uses the String
constant in the Command
to find command keywords in the program text. statementExecuter()
invokes findCommand(BASIC_cmd)
to search the entire BASIC_cmd[]
array, stopping when it finds a match. It invokes the Command
object's run()
method to execute the command. If it does not find a match, it assumes an implied LET
and invokes CMD_LET.run()
.
The keyword search order is set by the order of the Command
s in the BASIC_cmd[]
array. Adding a keyword early in the array will let the interpreter find it fast. Everything after it will be slower.
Note: the keyword constants, the BasicKeyWords[]
and BASIC_cmd[]
arrays, and the execution functions are all members of the Run class, not the Background class
. StatementExecuter()
is also in Run
, but it runs in the Background
context. Since Background is an inner class of Run, it has access to Run's members.
Previously, I listed four things you have to do to add a command. Here is that list again, showing how to add a top-level command.
-
Define the keyword:
public final static String BKW_KEY_WORD = "key.word"
-
Add the keyword to the
BasicKeyWords[]
array for the formatter. -
Create a
Command
object in theBASIC_cmd[]
array:new Command(BKW_KEY_WORD) { public boolean run() { return executeKEY_WORD(); } },
-
Write the command's execution function:
private boolean executeKEY_WORD() { ...
Commands that start with the same word may be put in groups. One keyword names the group, and more keywords name the commands in the group. Group names always end with ".". For example, the "timer.set" command is part of the "timer." group:
private static final String BKW_TIMER_GROUP = "timer."; // name of a command group
private static final String BKW_TIMER_SET = "set"; // name of a command in a group
The group keyword acts like a top-level command. The keyword goes in BasicKeyWords[]
for the formatter, and its Command
object goes in BASIC_cmd[]
. Its execution function is the entry point into the command group. To continue with the "timer.set" example:
new Command(BKW_TIMER_GROUP, CID_GROUP) { public boolean run() { return executeTIMER(); } },
The execution function executeTimer()
is extremely simple. It tells the parser to search the Command[]
array that holds the commands of the timer group:
private boolean executeTIMER() { return executeCommand(Timer_cmd, "Timer"); }
Your group execution function can be more complex. For example, the entry to the graphics command group is executeGR()
This function performs an error check that is common to all graphics commands. You can't run any graphics command until you have run GR.Open. executeGR()
verifies that you have done things in the right order. This is another benefit of processing command groups separately from the other commands.
The BASIC! programmer does not see a difference between "email.send" (a simple command) and "timer.set" (a command within a group). You can define groups any way you want in the Java. At this time (v01.81), if three or more commands share a prefix, I have put them in a group.
Groups can contain subgroups. The commands in the subgroup go in their own Command[]
table. For example, look at the "GR.Text.Bold" command string. BKW_GR_GROUP
is "gr.", BKW_GR_TEXT_GROUP
is "text.", and BKW_GR_TEXT_BOLD
is "bold". executeGR()
searches GR_cmd[]
, executeGR_TEXT()
searches GrText_cmd[]
, and executeGR_TEXT_BOLD()
does the work of the GR.Text.Bold
command.
The code in Format.java can handle groups, but it can not handle subgroups. The <group>_KW[]
array must contain all of the commands of all of the subgroups. For example, GR_KW[]
contains the entry BKW_GR_TEXT_GROUP + BKW_GR_TEXT_BOLD
for GR.Text.Bold
.
The steps are nearly the same as for adding a top-level keyword:
- Define the keyword.
- Add the keyword to the correct
<group>_KW[]
array for the formatter. - Create a
Command
object in the correct<group>_cmd[]
array - Write the command's execution function.
If you are adding a command to a subgroup, remember that every subgroup has its own _cmd[]
array, but all of the subgroups must be combined in their parent group's _KW[]
array for the formatter.
Adding a command group is a lot like adding all of the group's commands separately, except you must also create the group. The four requirements for creating a command to enter a group are the same as always: create a keyword, tell the formatter, map the keyword to an execution function, write the execution function. For example, consider the Ringer.
group, new in v0.81.
-
Define the group keyword:
private static final String BKW_RINGER_GROUP = "ringer.";
-
Add the group keyword to the
BasicKeyWords[]
array for the formatter. The group keyword is a top-level command that provides an entry point to the commands in the group. -
Create a
Command
object for the group keyword in theBASIC_cmd[]
array:new Command(BKW_RINGER_GROUP,CID_GROUP) { public boolean run() { return executeRINGER(); } },
Note the CID_GROUP
marker. As of v01.81 it is not required. Please use it anyway, for future use.
-
Write the command's execution function:
private boolean executeRINGER() { return executeCommand(ringer_cmd, "Ringer"); }
For each command in the group, you will follow the steps described in "Adding a command to an existing group", but the keyword array for the formatter and the Command
array for execution do not yet exist. You must create them. You must also add the new keyword array to the keywordLists
map.
-
Define the keywords for the commands:
private static final String BKW_RINGER_GET_MODE = "get.mode";
-
Create a keyword group and put it in the formatter's map:
private static final String ringer_KW[] = { BKW_RINGER_GET_MODE, ... };
and in the Run
method getKeywordLists()
, add the line
keywordLists.put(BKW_RINGER_GROUP, ringer_KW);
-
Create a group
Command
array:private final Command[] ringer_cmd = new Command[] { new Command(BKW_RINGER_GET_MODE) { public boolean run() { return executeRINGER_GET_MODE(); } }, // ... other commands };
-
Write the execution functions for all of the commands.
If you are adding a new subgroup, remember that every subgroup has its own _cmd[]
array, but all of the subgroups must be combined in their parent group's _KW[]
array for the formatter.
Now you have all of the code in place to invoke your command execution function when the parser finds your command keyword. The next topic is how to write a command execution function. This is a Run member method that parses the command line to read command parameters, then operates on the parameters, the BASIC! state, and the Android system in some way that defines the functionality of the command.
This is a complex topic. The Run class defines a number of helper methods to make command line management easier. "Global" resources must be managed carefully: initialized properly, prevented from interfering with other commands, and released when your program completes -- or if BASIC! crashes.
Command execution functions are as different as the commands they implement, but they are all variations on a simple theme:
- All command execution functions are private methods of the Run class that return a boolean value.
- A few have parameters, but most have none. There are no rules governing use of method parameters[*].
- Almost all parse the program line for command parameters (not the same as the method parameters).
- All "do something", whatever the BASIC! command is supposed to do.
[*]Typically, a parameter is used when two or more keywords perform very similar functions. The StatementExecuter()
switch calls the same command execution function with different argument values to differentiate the commands. For example, both SU and SYSTEM invoke executeSU(boolean)
, but for SU the argument is true
and for SYSTEM it is false
.[]
Every command execution function returns a boolean value:
-
true
means the command interpreter should continue to the next statement. -
false
indicates an error condition that will normally stop the command interpreter.
A true
return value does not necessarily mean there was no error. Some errors are reported to the program so the program can decide what to do. For example, most functions that can get a "File Not Found" exception report the condition in a variable and then return true
.
A false
return value does not necessarily stop the program. An OnError:
trap may be able to clear the error condition and allow the interpreter to continue running.
If the command execution function returns false
, the error condition must be reported by a call to one of the RunTimeError()
methods. This sets the "global" SyntaxError flag and sets up the error message to be displayed on the Console when the program exits. It also sets up the pointers the Editor will use to highlight the error in the program text. The required call is usually made in one of three ways:
- The command execution function calls one of the
RunTimeError()
methods.RunTimeError(String...)
takes a String or String array that becomes the first part of the error message.RunTimeError(Exception)
gets a String from the Exception. - The command execution function calls
SyntaxError()
. This method callsRunTimeError(String...)
with the generic message string "Syntax Error". - The command execution function does nothing; it simply returns
false
. ThenStatementExecuter()
callsSyntaxError()
.
If you pass a String array to RunTimeError(String...)
, the entire array will be displayed on the Console. However, only the first element (the first line of the error message) will be reported by BASIC!'s GETERROR$() function.
The SyntaxError()
method may be called more than once. It will not repeat the "Syntax Error" message.
As a convenience, the RunTimeError()
methods return false
. It is a common idiom in the command execution functions to use this one-line shorthand form:
if (error) { return RunTimeError("message"); }
After reading any parameters from the program line, your command execution function should invoke the checkEOL()
method. If there is anything on the program line after the last parameter, this method returns false
, indicating a syntax error. There are still a few command execution functions that do not do this; in almost every case the checkEOL()
call should be added.
Whenever possible, it is recommended that you do all syntax error checks before doing any parameter validity checks. You should not change the state of the BASIC! program or the Android device before all error conditions have been checked. (You may see state changes in some error cases in existing code. The assumption may be that such changes are irrelevant because the program is going to exit. This is Bad Form and should be avoided whenever possible. You almost never know when an enterprising programmer is going to find a way to trap your error, allowing the program to continue to run.)
All command execution functions return a boolean that has meaning to the interpreter, not to the program it is interpreting. If a command execution function must return a result, it does so by placing a value into a variable given as one of its arguments.
[TODO: when there is a section describing lines
, ExecutingLineIndex
, ExecutingLineBuffer
, and LineIndex
, link to it, and modify this section accordingly.]
SUMMARY:
-
lines
in an ArrayList that contains all of the text of the BASIC! program. -
ExecutingLineIndex
is an index intolines
that specifies which line the interpreter is running. -
ExecutingLineBuffer
is a copy oflines[ExecutingLineIndex]
; all parsing takes place in this copy. -
LineIndex
is an index intoExecutingLineBuffer
that specifies where on the line the parser is looking.
lines
is "global"; it is a public static member of the Basic class. The others are private fields of Run.
The process of parsing the command line consists of examining the text in ExecutingLineBuffer
, using LineIndex
to keep track of where you are. Some command execution functions manipulate these variables directly, but the code is usually cleaner if you use helper methods. Parsing is too complex to have helpers for every possible operation, but there are methods to help with the most common operations.
Every line of a BASIC! program that is not a label starts with a command; either the beginning of the line is a command keyword or the line is an implicit LET command. The StatementExecuter()
switch starts the appropriate command execution function with LineIndex
pointing to the character after the end of the keyword.
Every command keyword is followed by zero or more command arguments. After a command execution function has parsed all of its arguments, it should usually check for end-of-line. Every line ends with a DOS-style newline (CR/LF); this condition is enforced by the program loader when it puts the program text in the Basic.lines
ArrayList.
Each command execution function dictates the number and format of its own arguments. There are few absolute rules governing the type or order of the arguments, when an argument is required or optional or positional, or anything else. However, it is good to try to conform to the style of existing commands, where such style is perceivable. The more consistent the language is, the easier it will be for programmers to learn and remember it.
A few commands, such as some of the SQL operations, have specialized format requirements for its arguments, parsed and enforced by the command execution function. However, almost every command argument is either a variable or an expression. An expression is a constant, a variable[*], or some combination of either or both along with operators. An expression is processed to yield a value, either a number or a string.
[*]An argument that looks like a variable may in fact be an expression. If the command execution function expects an expression, it invokes an expression parser. The parser finds the variable and returns its value.[]
An expression is normally used for input: it provides a value to a command use in an operation. A variable is normally used for output: it provides the command with a place to put the result of an operation.
The command execution function looks for arguments by calling a variable parser or an expression parser. When variable or expression parsing is complete, the parser returns with LineIndex
pointing to the character after the end of the variable or expression. The command execution function immediately looks for a separator or terminator. It does not look for another argument without first finding a separator.
Please be sure you understand that. First, you look for an argument, then you verify that there is a separator or terminator after the argument. The separator or terminator is not part of the expression.
Command arguments are almost always separated by commas. Arguments of PRINT and PRINT-like commands (for example, BYTE.WRITE.BUFFER) may be separated by semicolons, but semicolons may also be terminators.
Terminators are harder to describe than separators, because you always have to know what the terminator is terminating. Actually, the "terminator" tells your command execution function what to do next. There are several different kinds of terminators:
- EOL: always terminates the command line. There is nothing after an EOL. Parsing is complete. The command execution function verifies that the command syntax allows EOL at that point, and goes on to do what the command is supposed to do.
- Semicolon: only allowed in PRINT-like commands. A semicolon tells the command execution function to look for another argument. If there is another argument, the semicolon is a separator. If not, it terminates the line and the command.
- Keyword: only allowed in certain commands, such as IF and FOR. If an argument is followed by a keyword, the keyword gives the command execution function specific instructions. STEP tells FOR that to expect an optional argument. THEN tells IF to recursively invoke the command interpreter.
- Right parenthesis: terminates a list opened by a left parenthesis. Usually, the list consists of the arguments of a function call.
- Right bracket: terminates a list opened by a left bracket. Usually, the list specifies the index or indices of an array element.
- [TODO: is this list complete?]
Comment-markers ('%' and '!') and line-continuation operators ('~') are not seen in command execution functions.
There are two levels of parsing help. Low-level functions examine or operate directly on characters in the line buffer. High-level functions operate on command parameters or groups of parameters, using low-level functions to do their work.
Originally, almost all variable parsing was done by a single function called getVar()
. While this simplified argument parsing, it imposed restrictions that made some operations impossible. Now there is a whole family of primitive functions derived from the original getVar()
.
The "top-half" methods parseVar()
, searchVar()
, and getVarAndType()
find a variable on the command line and search the variable list, setting "globals" to indicate what they found. They do not create a new variable or return the value of an old one. The "bottom-half" methods getVarValue()
, createNewVar()
, and createNewScalar()
can create a new variable or return the value of an old one[*] (including the value of an array element, by calling GetArrayValue()
).
[*]These methods do not really return the value of a variable. They set "globals" that caller can use to locate the variable and write or read its value.[TODO: explain all of the globals set by the variable parsers][]
These primitives may be considered low-level helper functions. They are rarely called from command execution functions to handle special cases, usually involving arrays. The low-level functions are combined in various ways in high-level helper functions that command execution functions use to create and find variables [TODO: what does each of these do with LineIndex
?]:
-
getVar()
- exactly the same as the originalgetVar()
, but implemented as calls to the primitive methods. -
getNVar()
andgetSVar()
: likegetVar()
, but they returnfalse
if they do not find a variable of the correct type. -
getArrayVarForWrite()
: requires a new array name, undimensioned (empty brackets, "[]"), setting runtime errors if any required condition is not met. One form takes a type parameter (true
if numeric is required). Finds the name but does not create an entry in the variable list. -
getArrayVarForRead()
: requires an existing array name, setting runtime errors if any required condition is not met. -
getNewFNVar()
: requires a new user-defined function name, setting a runtime error if it finds an existing user-defined function name.
There are too many possible combinations to have helpers for all possibilities. For example, executeDIM()
requires the name of an array that does not already exist, but it requires dimensions in the brackets. executeSELECT()
requires a variable name that is either an existing array name, with empty brackets, or a numeric scalar (a LIST index). For these special one-time cases, the command execution functions call getVar()
primitives directly.
If you must write a command execution function with special requirements for its variables, you can either call the primitives directly or write a new helper. Either way, be careful. The getVar()
primitives are very brittle. They are laced with special-case code, such as checks for user-defined function names in places you probably would not expect them, which you might omit or delete accidentally.
In very rare cases you may need to refactor the existing primitives. If you touch these methods, be very careful, and test, test, test!!! (But if you can find a way to make them less fragile, go for it!)
If an argument is a variable, it is probably intended to receive a value resulting from an operation. The command execution function must write the value into the variable. To understand how this is done, you must know how BASIC! stores variables. [TODO: is this explanation one of the earlier TODOs? If not, it should be.]
[NOTE: This is subject to change. I am in the process of replacing VarNames and VarIndex with a hash map.]
Variables are stored in lists. Variables are created at runtime -- the first time the interpreter sees a variable name, a variable of the correct name and type is created. As each variable is created, information about it is put on the relevant lists. Variables are referred to by their list indices: incrementing integers in chronological order.
-
NumericVarValues
: a list of Double references. The Double objects contain the current values of all of the program's numeric variables. A numeric scalar is stored as a single element on this list. A numeric array is stored as a sequence of elements on this list. -
StringVarValues
: a list of String references. The String objects contain the current values of all of the program's string variables. A string scalar is stored as a single element on this list. An array of strings is stored as a sequence of elements on this list. -
ArrayTable
: a list of Bundle references. A Bundle object contains information about an array, including its dimensions, its starting point inNumericVarValues
orStringVarValues
, and its length (not its name or type). -
FunctionTable
: a list of Bundle references. A Bundle object contains information about a user-defined function. -
theLists
,theListsType
,theBundles
,theStacks
,theBundles
: these Java lists contain actual Java data structure objects representing BASIC! data structures, along with type information when needed.
I've saved the two most important lists for last. VarNames
is straightforward. VarIndex
is tricky.
-
VarNames
: a list of String references. The String objects contain the names of all of the variables created while executing the program. This includes numeric and string scalars, numeric and string arrays, and user-defined functions. It does not include lists, bundles, or stacks, which do not have names. -
VarIndex
: a list of Integer references. Each Integer object contains an index into one of the other lists:NumericVarValues
,StringVarValues
,ArrayTable
, orFunctionTable
. Which list is determined solely by the type of the variable name.
VarNames
and VarIndex
are parallel. the searchVar()
method searches for a variable name in VarNames
. The index of the name in VarNames
is also the index of the index in VarIndex
.
Example:
a = 5;
b = 6;
a$ = "abc"
ARRAY.LOAD a$[], "no", "pq", "rs"
b$ = "def"
NumericVarValues contains { 5, 6 }
StringVarValues contains { "abc", "no", "pq", "rs", "def" }
ArrayTable contains one Bundle: a 1D array, starts at StringVarValues index 1, length is 3
VarNames contains { "a", "b", "a$", "a$[", "b$" }
VarIndex contains { 0, 1, 0, 0, 4 }
To execute the line c$ = b$
, executeLet()
calls getVar()
to create a new variable name "c$". The name goes in as VarNames
element 6, the value (intialized to 0) as StringVarValues
element 5, and VarIndex
element 6 is set to 5. The part executeLet()
needs is the index into the value list, StringVarValues
. One of the getVar$()
primitives (createNewScalar()
) supplies that by setting the "global" theValueIndex
to 5. executeLet()
saves that value in a local variable, AssignToVarNumber
. (This is a bad name, it should not refer to VarNumber
but to theValueIndex
. It is historical, it probably comes from older code that read: AssignToVarNumber = VarIndex.get(VarNumber);
.)
Next, executeLet()
finds the "=" and knows it needs to evaluate everything after the "=" as a string expression, so it calls the helper getStringArg()
.
The helper calls the string expression parser evalStringExpression()
, which eventually calls the low-level variable parsing primitives. parseVar()
finds the string "b$" and sets the flags:
VarIsNumeric = false; VarIsArray = false; VarIsFunction = false;
LineIndex = 5; // one past "b$" in the line as seen by the parser: "c$=b$\n"
Then searchVar()
searches VarNames
and finds "b$" at index 4:
VarIsNew = false; VarNmber = 4;
theValueIndex = VarIndex.get(4); // sets theValueIndex to VarIndex element 4
Given these "global" values:
- if you want the variable name, use
VarNames.get(VarNumber)
- if you want the variable value, use
StringVarValues.get(VarIndex.get(theValueIndex))
- if you want to write the variable, use
StringVarValues.set(theValueIndex, newValue)
Of course, executeLET()
doesn't care about all that. It called getStringArg()
, which leaves the expression value in the "global" StringConstant
. It is holding the value index of "c$" in AssignToVarNumber
, so the code looks like this:
if (!getStringArg()) { return false; }
StringVarValues.set(AssignToVarNumber, StringConstant);
There are two different kinds of expressions: those that evaluate to a number (a numeric expression) and those that evaluate to a string (a string expression). Usually a command knows the type of each parameter, so it knows what kind of expression to look for. Some commands do not know. When PRINT starts parsing an expression, it can't tell if it is working on a numeric or string expression. It starts by calling the numeric expression parser, evalNumericExpression()
. If that fails, it calls the string expression parser, evalStringExpression()
.
PRINT can check both types of expression because evalNumericExpression()
does not advance LineIndex
unless it successfully evaluates a complete numeric expression. This is common among methods that examine the program line. They typically advance LineIndex
only if they find what they are looking for. As a programmer of command execution functions, you must always be aware when you call a parsing function what that function does to LineIndex
.
Low-level helpers:
-
isEOL()
:false
ifLineIndex
is out of bounds, or if it points to a newline, elsetrue
. -
checkEOL()
:true
ifisEOL()
returns true; otherwise posts a runtime-error and returnsfalse
. -
isNext(char)
:true
if there is another character on the line and it matches the expected character; bounds-checksLineIndex
, and advancesLineIndex
only when returningtrue
. This little method is surprisingly powerful, and because of its side-effects it can be tricky to use. Use it carefully, but do use it! -
evalStringExpression()
: not a helper, but the actual string expression parser. It leaves its result in the "global" String objectStringConstant
. Usually, this should not be called directly. -
getStringArg()
: a thin wrapper aroundevalStringExpression()
that verifies that the string expression is not also a logical expression -- a logical expression is a numeric type, not a string type. It also checks if the result isnull
-- this was put in to catch a serious bug that (probably!) no longer exists. You should almost always use this method instead of invokingevalStringExpression()
directly. Otherwise the tendency is to forget to checkSEisLE
. -
evalNumericExpression()
: not a helper, but the actual numeric expression parser. It leaves its result in the "global" Double objectEvalNumericExpressionValue
. There is no harm in calling this directly. -
getArgAsNum()
: a wrapper aroundevalNumericExpression()
written specifically for the BASIC!TIME()
function. I include it here as an example of what can be done. - [TODO: is this list complete?]
There are very few high-level functions for expressions, and they are rarely used -- this an area of BASIC! that needs some thought and planning. Today, most of the high-level functions are dedicated to a set of related commands. For example, the SQL commands have these helpers
- `getDbPtrArg(): checks if any databases are open, gets a numeric variable, and verifies that its value is in the range of the database list indices. The method sets runtime errors if any of these conditions are not met.
-
getVarAndDbPtrArgs(int[])
: likegetDbPtrArg()
, but expects two numeric variables, the second of which is a database pointer.
These examples demonstrate some of the reasons that there are so few high-level functions for expressions.
- Most functions return boolean status values, with actual information returned as side-effects in "globals". If two numeric variables are required, fetching the second overwrites the values obtained fetching the first. The parameter of
getVarAndDbPtrArgs(int[])
is a two-element array used to return values to the caller. The method copiestheValueIndex
set by the variable parser for each of the two numeric parameters into the two array elements. - It is almost true to say that every command is a special case. These helpers can't be used by any non-SQL commands because they perform database operations, and they can set runtime errors specific to SQL problems.
- The SQL commands that use these helpers take a numeric variable argument and use it as input. Strictly speaking, this could be considered a bug -- the database pointer should be an expression, not a variable. (However, if we decide to change it, using the helper saves us some work: changing the argument type in the helper changes it in several SQL commands.)
I believe there are enough common combinations of command parameters to justify building a set of high-level helpers for expressions.
[TODO: look up other high-level helpers; maybe they should be listed here.] [TODO: write more high-level helpers?]
This is a Work In Progress, what remains is TODO and TBD, pending whenever I can get to it.
For now I will make only one really crucial comment: be very careful with isNext(char)
. It has a side-effect -- it advances the global LineIndex
-- but only if it returns true
. If the next command line character does not match the target (the argument), the method returns false
and does not consume the character. It is surprisingly powerful, and there are lots of examples of how to use it correctly.
[TODO: continue with the "Command execution functions" section]
[TODO: describe appropriate handling of BASIC! and system resources]
[TODO: describe using an Intent to launch an Activity that is another part of BASIC!, like GR, Web, or CameraView]
[TODO: break this up into several hyperlinked pages]