| expression | -> term [ ( "+" | "-" ) term ]* | |
| term | -> factor [ ( "*" | "/" ) factor ]* | |
| factor | -> [ "+" | "-" ] ( primary | "(" expression ")" ) | |
| primary | -> number | ( name [ "(" ExpressionList ")" ] ) | |
| ExpressionList | -> expression [ "," expression ]* |
| number | -> [ digit ]+ [ "." [ digit ]* ] [ ( "e" | "E" ) [ "+" | "-" ] [ digit ]+ ] | |
| name | -> letter [ letter | digit ]* |
atan(), usually called atan2(), which
returns a result in the correct quadrant. To cope with these possibilities we
use a generalised grammar for functions which copes with an arbitrary number of
arguments. This in turn brings about the need to check that the correct number
of arguments were used for that particular function. With the grammar
presented, this is a matter of semantics, not syntax. Although the grammar
could be extended to explicitly mention every function we want and the number
of arguments each takes, I want to keep the grammar and parser as simple as
possible.
Other standard functions such as square root, log, exponential, 'ceiling' and 'floor' functions, can easily be added, and so can any other function you find convenient in your application. For instance, polynomial evaluation with fixed coefficients would be easy, although if the coefficients are to be determined at run-time they will have to be input using expressions which have been entered separately.
If we had variables which we could assign values to, then our problem would be largely solved, but then we would have a language which was difficult to use as a 'smart' routine for reading numbers. Knowing when the value entered is intended to be THE number would be awkward. Nevertheless, there are places where having a tiny programming language to plug into our own programs would be convenient, and we will come to that later.
static (local to the file they were declared in) unless
they should actually be available to other modules, in which case they are
declared to be extern. By #includeing the header file
into the interpreter module, we ensure that everything we want to export is
declared consistently with what the aplication will be looking for.
static means other things in other contexts in C/C++, but the
'local to file' interpretation is what we want here.
Attempting to divide by zero is obviously not a good idea but you might be
tempted to ignore the possibility and trust the user not to do stupid things.
After all, it's lots of hassle to check everything every time and will slow the
program down into the bargain. The checks for division could look likewhere the second test indicates whether
if (b == 0.0) err ("Division by zero.");
if (fabs(a)/MAXFLOAT > fabs(b)) err ("Floating point overflow.");
|a/b| will
produce a result greater than MAXFLOAT - i.e. an overflow. The
division by MAXFLOAT will at worst cause an underflow which we can
ignore. All this will double the time it takes to do a division. But addition,
subtraction and multiplication can cause overflows too. Adding all these checks
will significantly clutter up the code.
The solution for some errors is to add a signal handler. This allows us to
specify a procedure which is to be called when a floating point error happens,
and which might even be told what sort of error occurred - division by zero,
overflow, etc. We can then provide an error message and stop execution.
We can't quit the run by throwing an exception from within the handler though.
Signals and exceptions do not mix well. We have to use a global variable,
SigCode to pass back information to the run() routine.
The checking overhead now reduces to checking at the end that
SigCode has not been set. The use of a subcode is a Borland
extension that gives a little more information than just 'an error has
occurred'. The cast applied to the handler parameter of signal()
can be dispensed with if you use a standard version of the handler.
C library calls set the global variable errno instead of
signalling, so we must check errno too. Sin(), cos()
etc. produce domain and range errors if the function parameter
is outside the domain over which the function operates or if the result is
outside the range that can be represented in the type of numbers the functions
use. If the function produces no errors, it should leave errno
alone, so that a long expression with several function calls will not overwrite
earlier error reports with an OK status. Note that this is very similar to what
we're doing with signals.
If you want a report of exactly where in the expression the error occurred, the distributed nature of the evaluation amongst the parser makes checking everything step by step awkward, but that situation will improve in eval5.