Home
Parsing
Eval

eval4

So far the expressions have consisted only of simple arithmetic operators. Functions are common in the real world though, so our next step is to add a few of these.
Eval4 source.
Eval4 header.
Sample application source.
Eval4 executable.
Syntax grammar 4
expression -> term [ ( "+" | "-" ) term ]*
term -> factor [ ( "*" | "/" ) factor ]*
factor -> [ "+" | "-" ] ( primary | "(" expression ")" )
primary -> number | ( name [ "(" ExpressionList ")" ] )
ExpressionList -> expression [ "," expression ]*
Lexical grammar 4
number -> [ digit ]+ [ "." [ digit ]* ] [ ( "e" | "E" ) [ "+" | "-" ] [ digit ]+ ]
name -> letter [ letter | digit ]*

Functions

Transcendental functions are generally of one variable, but a random number function takes no parameters and it is often convenient to have a two variable version of 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.

Modularization

Since we have arrived at something likely to be transplanted into someone else's program, the source for eval4 has been split into three files - the interpreter, an 'application', and a header file for the interpreter which is included into the application. The various items which up until now have been left global, must now have their presence hidden to avoid interfering with the application code. This can be accomplished in C and C++ by declaring them to be 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.

Run-time error checking

I didn't bother with any run-time error checking in eval3 because eval4 is the first version with enough facilities to be used seriously, but that means that the error checking should be the strongest possible.

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 like

   if (b == 0.0) err ("Division by zero.");
   if (fabs(a)/MAXFLOAT > fabs(b)) err ("Floating point overflow.");
where the second test indicates whether |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.


Eval
Parsing
Home
Valid HTML 4.01 Any comments or queries, write to me.
Site last updated 4 July, 2004