0% found this document useful (0 votes)
54 views

Programming Savvy and Arithmetic Expressions

The document describes a program that reads and evaluates arithmetic expressions. It uses dynamic linkage to call different functions for each token type, avoiding large switch statements. The main loop gets input, scans for tokens, recognizes expressions using recursive descent, stores the expression tree, and processes it. Numbers and operators are tokenized. Expressions are represented as binary trees for flexible storage and later processing.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
54 views

Programming Savvy and Arithmetic Expressions

The document describes a program that reads and evaluates arithmetic expressions. It uses dynamic linkage to call different functions for each token type, avoiding large switch statements. The main loop gets input, scans for tokens, recognizes expressions using recursive descent, stores the expression tree, and processes it. Numbers and operators are tokenized. Expressions are represented as binary trees for flexible storage and later processing.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 9

1‌ 

‌P
‌ rogramming‌‌S
‌ avvy‌‌A
‌ nd‌‌A
‌ rithmetic‌‌‌Expressions‌  ‌
Dynamic‌‌linkage‌‌‌is‌‌a‌‌powerful‌‌programming‌‌technique‌‌in‌‌its‌‌own‌‌right.‌‌Rather‌  ‌
than‌‌writing‌‌a‌‌few‌‌functions,‌‌each‌‌with‌‌a‌‌big‌‌switch‌‌to‌‌handle‌‌many‌‌special‌‌cases,‌  ‌
we‌‌can‌‌write‌‌many‌‌small‌‌functions,‌‌one‌‌for‌‌each‌‌case,‌‌and‌‌arrange‌‌for‌‌the‌‌proper‌  ‌
function‌‌to‌‌be‌‌called‌‌by‌‌dynamic‌‌linkage.‌‌This‌‌often‌‌simplifies‌‌a‌‌routine‌‌job‌‌and‌‌it‌  ‌
usually‌‌results‌‌in‌‌code‌‌that‌‌can‌‌be‌‌extended‌‌easily.‌  ‌
As‌‌an‌‌example‌‌we‌‌will‌‌write‌‌a‌‌small‌‌program‌‌to‌‌read‌‌and‌‌evaluate‌‌arithmetic‌  ‌
expressions‌‌consisting‌‌of‌‌floating‌‌point‌‌numbers,‌‌parentheses‌‌and‌‌the‌‌usual‌‌operators‌‌for‌‌ 
addition,‌‌subtraction,‌‌and‌‌so‌‌on.‌‌Normally‌‌we‌‌would‌‌use‌‌the‌‌compiler‌‌generator‌‌tools‌‌lex‌‌and‌‌ 
yacc‌‌to‌‌build‌‌that‌‌part‌‌of‌‌the‌‌program‌‌which‌‌recognizes‌‌an‌‌arithmetic‌‌expression.‌‌This‌‌book‌‌is‌‌ 
not‌‌about‌‌compiler‌‌building,‌‌however,‌‌so‌‌just‌‌this‌  ‌
once‌‌we‌‌will‌‌write‌‌this‌‌code‌‌ourselves.‌  ‌
 ‌
 ‌
1.1‌‌The‌‌Main‌‌Loop‌  ‌
The‌‌main‌‌loop‌‌of‌‌the‌‌program‌‌reads‌‌a‌‌line‌‌from‌‌standard‌‌input,‌‌initializes‌‌things‌‌so‌  ‌
that‌‌numbers‌‌and‌‌operators‌‌can‌‌be‌‌extracted‌‌and‌‌white‌‌space‌‌is‌‌ignored,‌‌calls‌‌up‌‌a ‌ ‌
function‌‌to‌‌recognize‌‌a‌‌correct‌‌arithmetic‌‌expression‌‌and‌‌somehow‌‌store‌‌it,‌‌and‌  ‌
finally‌‌processes‌‌whatever‌‌was‌‌stored.‌‌If‌‌things‌‌go‌‌wrong,‌‌we‌‌simply‌‌read‌‌the‌  ‌
next‌‌input‌‌line.‌‌Here‌‌is‌‌the‌‌main‌‌loop:‌  ‌
#include‌‌<setjmp.h>‌  ‌
static‌‌enum‌‌tokens‌‌token;‌‌/*‌‌current‌‌input‌‌symbol‌‌*/‌ 
static‌‌jmp_buf‌‌onError;‌  ‌
int‌‌main‌‌(void)‌  ‌
{‌‌volatile‌‌int‌‌errors‌‌=‌‌0;‌  ‌
char‌‌buf‌‌[BUFSIZ];‌  ‌
if‌‌(setjmp(onError))‌  ‌
++‌‌errors;‌  ‌
while‌‌(gets(buf))‌  ‌
if‌‌(scan(buf))‌  ‌
{‌‌void‌‌*‌‌e‌‌=‌‌sum();‌  ‌
if‌‌(token)‌  ‌
error("trash‌‌after‌‌sum");‌  ‌
process(e);‌  ‌
delete(e);‌  ‌
}‌  ‌
return‌‌errors‌‌>‌‌0;‌  ‌
}‌  ‌
 ‌
 ‌
 ‌
void‌‌error‌‌(const‌‌char‌‌*‌‌fmt,‌‌...)‌  ‌
{‌‌va_list‌‌ap;‌ 
va_start(ap,‌‌fmt);‌  ‌
vfprintf(stderr,‌‌fmt,‌‌ap),‌‌putc(’\n’,‌‌stderr);‌  ‌
va_end(ap);‌  ‌
longjmp(onError,‌‌1);‌ 
2‌  ‌

}‌  ‌
The‌‌error‌‌recovery‌‌point‌‌is‌‌defined‌‌with‌‌setjmp().‌‌If‌‌error()‌‌is‌‌called‌‌somewhere‌‌in‌  ‌
the‌‌program,‌‌longjmp()‌‌continues‌‌execution‌‌with‌‌another‌‌return‌‌from‌‌setjmp().‌‌In‌  ‌
this‌‌case,‌‌the‌‌result‌‌is‌‌the‌‌value‌‌passed‌‌to‌‌longjmp(),‌‌the‌‌error‌‌is‌‌counted,‌‌and‌‌the‌  ‌
next‌‌input‌‌line‌‌is‌‌read.‌‌The‌‌exit‌‌code‌‌of‌‌the‌‌program‌‌reports‌‌if‌‌any‌‌errors‌‌were‌  ‌
Encountered.‌  ‌
 ‌
 ‌
1.2‌‌The‌‌Scanner‌  ‌
In‌‌the‌‌main‌‌loop,‌‌once‌‌an‌‌input‌‌line‌‌has‌‌been‌‌read‌‌into‌‌buf[],‌‌it‌‌is‌‌passed‌‌to‌‌scan(),‌  ‌
which‌‌for‌‌each‌‌call‌‌places‌‌the‌‌next‌‌input‌‌symbol‌‌into‌‌the‌‌variable‌‌token.‌‌At‌‌the‌‌end‌  ‌
of‌‌a‌‌line‌‌token‌‌is‌‌zero:‌  ‌
#include‌‌<ctype.h>‌  ‌
#include‌‌<errno.h>‌  ‌
#include‌‌<stdlib.h>‌  ‌
#include‌‌"parse.h"‌‌/*‌‌defines‌‌NUMBER‌‌*/‌  ‌
static‌‌double‌‌number;‌‌/*‌‌if‌‌NUMBER:‌‌numerical‌‌value‌‌*/‌  ‌
static‌‌enum‌‌tokens‌‌scan‌‌(const‌‌char‌‌*‌‌buf)‌  ‌
/*‌‌return‌‌token‌‌=‌‌next‌‌input‌‌symbol‌‌*/‌  ‌
{‌‌static‌‌const‌‌char‌‌*‌‌bp;‌  ‌
if‌‌(buf)‌  ‌
bp‌‌=‌‌buf;‌‌/*‌‌new‌‌input‌‌line‌‌*/‌  ‌
while‌‌(isspace(*‌‌bp))‌  ‌
++‌‌bp;‌  ‌
if‌‌(isdigit(*‌‌bp)‌‌||‌‌*‌‌bp‌‌==‌‌’.’)‌  ‌
{‌‌errno‌‌=‌‌0;‌  ‌
token‌‌=‌‌NUMBER,‌‌number‌‌=‌‌strtod(bp,‌‌(char‌‌**)‌‌&‌‌bp);‌  ‌
if‌‌(errno‌‌==‌‌ERANGE)‌  ‌
error("bad‌‌value:‌‌%s",‌‌strerror(errno));‌  ‌
}‌  ‌
else‌  ‌
token‌‌=‌‌*‌‌bp‌‌?‌‌*‌‌bp‌‌++‌‌:‌‌0;‌  ‌
return‌‌token;‌  ‌
}‌  ‌
We‌‌call‌‌scan()‌‌with‌‌the‌‌address‌‌of‌‌an‌‌input‌‌line‌‌or‌‌with‌‌a‌‌null‌‌pointer‌‌to‌‌continue‌  ‌
work‌‌on‌‌the‌‌present‌‌line.‌‌White‌‌space‌‌is‌‌ignored‌‌and‌‌for‌‌a‌‌leading‌‌digit‌‌or‌‌decimal‌  ‌
point‌‌we‌‌extract‌‌a‌‌floating‌‌point‌‌number‌‌with‌‌the‌‌ANSI-C‌‌function‌‌strtod().‌‌Any‌  ‌
other‌‌character‌‌is‌‌returned‌‌as‌‌is,‌‌and‌‌we‌‌do‌‌not‌‌advance‌‌past‌‌a‌‌null‌‌byte‌‌at‌‌the‌‌end‌  ‌
of‌‌the‌‌input‌‌buffer.‌  ‌
 ‌
 ‌
1.3‌‌The‌‌Recognizer‌‌   ‌
The‌‌result‌‌of‌‌scan()‌‌is‌‌stored‌‌in‌‌the‌‌global‌‌variable‌‌token‌‌—‌‌this‌‌simplifies‌‌the‌  ‌
recognizer.‌‌If‌‌we‌‌have‌‌detected‌‌a‌‌number,‌‌we‌‌return‌‌the‌‌unique‌‌value‌‌NUMBER‌  ‌
and‌‌we‌‌make‌‌the‌‌actual‌‌value‌‌available‌‌in‌‌the‌‌global‌‌variable‌‌number.‌  ‌
 ‌
At‌‌the‌‌top‌‌level‌‌expressions‌‌are‌‌recognized‌‌by‌‌the‌‌function‌‌sum()‌‌which‌‌internally‌  ‌
calls‌‌on‌‌scan()‌‌and‌‌returns‌‌a‌‌representation‌‌that‌‌can‌‌be‌‌manipulated‌‌by‌‌process()‌  ‌
3‌  ‌

and‌‌reclaimed‌‌by‌‌delete().‌  ‌
If‌‌we‌‌do‌‌not‌‌use‌‌yacc‌‌we‌‌recognize‌‌expressions‌‌by‌‌the‌‌method‌‌of‌‌recursive‌  ‌
descent‌‌where‌‌grammatical‌‌rules‌‌are‌‌translated‌‌into‌‌equivalent‌‌C‌‌functions.‌‌For‌  ‌
example:‌‌a‌‌sum‌‌is‌‌a‌‌product,‌‌followed‌‌by‌‌zero‌‌or‌‌more‌‌groups,‌‌each‌‌consisting‌‌of‌  ‌
an‌‌addition‌‌operator‌‌and‌‌another‌‌product.‌‌A‌‌grammatical‌‌rule‌‌like‌  ‌
sum‌‌:‌‌product‌‌{‌‌+|—‌‌product‌‌}...‌  ‌
is‌‌translated‌‌into‌‌a‌‌C‌‌function‌‌like‌  ‌
void‌‌sum‌‌(void)‌  ‌
{‌  ‌
product();‌  ‌
for‌‌(;;)‌  ‌
{‌‌switch‌‌(token)‌‌{ ‌ ‌
case‌‌’+’:‌  ‌
case‌‌’—’:‌  ‌
scan(0),‌‌product();‌‌continue;‌  ‌
}‌  ‌
return;‌  ‌
}‌  ‌
}‌  ‌
There‌‌is‌‌a‌‌C‌‌function‌‌for‌‌each‌‌grammatical‌‌rule‌‌so‌‌that‌‌rules‌‌can‌‌call‌‌each‌‌other.‌  ‌
Alternatives‌‌are‌‌translated‌‌into‌‌switch‌‌or‌‌if‌‌statements,‌‌iterations‌‌in‌‌the‌‌grammar‌  ‌
produce‌‌loops‌‌in‌‌C.‌‌The‌‌only‌‌problem‌‌is‌‌that‌‌we‌‌must‌‌avoid‌‌infinite‌‌recursion.‌  ‌
token‌‌always‌‌contains‌‌the‌‌next‌‌input‌‌symbol.‌‌If‌‌we‌‌recognize‌‌it,‌‌we‌‌must‌‌call‌  ‌
scan(0)‌‌to‌‌advance‌‌in‌‌the‌‌input‌‌and‌‌store‌‌a‌‌new‌‌symbol‌‌in‌‌token.‌  ‌
 ‌
 ‌
1.4‌‌The‌‌Processor‌  ‌
How‌‌do‌‌we‌‌process‌‌an‌‌expression?‌‌If‌‌we‌‌only‌‌want‌‌to‌‌perform‌‌simple‌‌arithmetic‌  ‌
on‌‌numerical‌‌values,‌‌we‌‌can‌‌extend‌‌the‌‌recognition‌‌functions‌‌and‌‌compute‌‌the‌  ‌
result‌‌as‌‌soon‌‌as‌‌we‌‌recognize‌‌the‌‌operators‌‌and‌‌the‌‌operands:‌‌sum()‌‌would‌  ‌
expect‌‌a‌‌double‌‌result‌‌from‌‌each‌‌call‌‌to‌‌product(),‌‌perform‌‌addition‌‌or‌‌subtraction‌  ‌
as‌‌soon‌‌as‌‌possible,‌‌and‌‌return‌‌the‌‌result,‌‌again‌‌as‌‌a‌‌double‌‌function‌‌value.‌  ‌
If‌‌we‌‌want‌‌to‌‌build‌‌a‌‌system‌‌that‌‌can‌‌handle‌‌more‌‌complicated‌‌expressions‌‌we‌  ‌
need‌‌to‌‌store‌‌expressions‌‌for‌‌later‌‌processing.‌‌In‌‌this‌‌case,‌‌we‌‌can‌‌not‌‌only‌‌perform‌‌ 
arithmetic,‌‌but‌‌we‌‌can‌‌permit‌‌decisions‌‌and‌‌conditionally‌‌evaluate‌‌only‌‌part‌‌of‌  ‌
an‌‌expression,‌‌and‌‌we‌‌can‌‌use‌‌stored‌‌expressions‌‌as‌‌user‌‌functions‌‌within‌‌other‌  ‌
expressions.‌‌All‌‌we‌‌need‌‌is‌‌a‌‌reasonably‌‌general‌‌way‌‌to‌‌represent‌‌an‌‌expression.‌  ‌
The‌‌conventional‌‌technique‌‌is‌‌to‌‌use‌‌a‌‌binary‌‌tree‌‌and‌‌store‌‌token‌‌in‌‌each‌‌node:‌  ‌
struct‌‌Node‌‌{ ‌ ‌
enum‌‌tokens‌‌token;‌  ‌
struct‌‌Node‌‌*‌‌left,‌‌*‌‌right;‌  ‌
};‌  ‌
This‌‌is‌‌not‌‌very‌‌flexible,‌‌however.‌‌We‌‌need‌‌to‌‌introduce‌‌a‌‌union‌‌to‌‌create‌‌a‌‌node‌  ‌
in‌‌which‌‌we‌‌can‌‌store‌‌a‌‌numerical‌‌value‌‌and‌‌we‌‌waste‌‌space‌‌in‌‌nodes‌‌representing‌  ‌
unary‌‌operators.‌‌Additionally,‌‌process()‌‌and‌‌delete()‌‌will‌‌contain‌‌switch‌‌statements‌‌which‌‌ 
grow‌‌with‌‌every‌‌new‌‌token‌‌which‌‌we‌‌invent.‌  ‌
 ‌
 ‌
4‌  ‌

1.5‌‌Information‌‌Hiding‌  ‌
Applying‌‌what‌‌we‌‌have‌‌learned‌‌thus‌‌far,‌‌we‌‌do‌‌not‌‌reveal‌‌the‌‌structure‌‌of‌‌a‌‌node‌‌at‌  ‌
all.‌‌Instead,‌‌we‌‌place‌‌some‌‌declarations‌‌in‌‌a‌‌header‌‌file‌‌value.h:‌  ‌
const‌‌void‌‌*‌‌Add;‌  ‌
...‌  ‌
void‌‌*‌‌new‌‌(const‌‌void‌‌*‌‌type,‌‌...);‌  ‌
void‌‌process‌‌(const‌‌void‌‌*‌‌tree);‌  ‌
void‌‌delete‌‌(void‌‌*‌‌tree);‌  ‌
Now‌‌we‌‌can‌‌code‌‌sum()‌‌as‌‌follows:‌  ‌
#include‌‌"value.h"‌  ‌
static‌‌void‌‌*‌‌sum‌‌(void)‌  ‌
{‌‌void‌‌*‌‌result‌‌=‌‌product();‌  ‌
const‌‌void‌‌*‌‌type;‌  ‌
for‌‌(;;)‌  ‌
{‌‌switch‌‌(token)‌‌{ ‌ ‌
case‌‌’+’:‌  ‌
type‌‌=‌‌Add;‌  ‌
break;‌  ‌
case‌‌’—’:‌  ‌
type‌‌=‌‌Sub;‌  ‌
break;‌  ‌
default:‌  ‌
return‌‌result;‌  ‌
}‌  ‌
scan(0);‌  ‌
result‌‌=‌‌new(type,‌‌result,‌‌product());‌  ‌
}‌  ‌
}‌  ‌
product()‌‌has‌‌the‌‌same‌‌architecture‌‌as‌‌sum()‌‌and‌‌calls‌‌on‌‌a‌‌function‌‌factor()‌‌to‌  ‌
recognize‌‌numbers,‌‌signs,‌‌and‌‌a‌‌sum‌‌enclosed‌‌in‌‌parentheses:‌  ‌
static‌‌void‌‌*‌‌sum‌‌(void);‌  ‌
static‌‌void‌‌*‌‌factor‌‌(void)‌  ‌
{‌‌void‌‌*‌‌result;‌  ‌
switch‌‌(token)‌‌{ ‌ ‌
case‌‌’+’:‌  ‌
scan(0);‌  ‌
return‌‌factor();‌  ‌
 ‌
1.6‌‌Dynamic‌‌Linkage‌‌   ‌
case‌‌’—’:‌  ‌
scan(0);‌  ‌
return‌‌new(Minus,‌‌factor());‌  ‌
default:‌  ‌
error("bad‌‌factor:‌‌’%c’‌‌0x%x",‌‌token,‌‌token);‌  ‌
case‌‌NUMBER:‌  ‌
result‌‌=‌‌new(Value,‌‌number);‌  ‌
break;‌  ‌
case‌‌’(’:‌  ‌
5‌  ‌

scan(0);‌  ‌
result‌‌=‌‌sum();‌  ‌
if‌‌(token‌‌!=‌‌’)’)‌  ‌
error("expecting‌‌)");‌  ‌
}‌  ‌
scan(0);‌  ‌
return‌‌result;‌  ‌
}‌  ‌
Especially‌‌in‌‌factor()‌‌we‌‌need‌‌to‌‌be‌‌very‌‌careful‌‌to‌‌maintain‌‌the‌‌scanner‌‌invariant:‌  ‌
token‌‌must‌‌always‌‌contain‌‌the‌‌next‌‌input‌‌symbol.‌‌As‌‌soon‌‌as‌‌token‌‌is‌‌consumed‌  ‌
we‌‌need‌‌to‌‌call‌‌scan(0).‌  ‌
 ‌
 ‌
1.6‌‌Dynamic‌‌Linkage‌  ‌
The‌‌recognizer‌‌is‌‌complete.‌‌value.h‌‌completely‌‌hides‌‌the‌‌evaluator‌‌for‌‌arithmetic‌‌expressions‌‌ 
and‌‌at‌‌the‌‌same‌‌time‌‌specifies‌‌what‌‌we‌‌have‌‌to‌‌implement.‌  ‌
new()‌‌takes‌‌a‌‌description‌‌such‌‌as‌‌Add‌‌and‌‌suitable‌‌arguments‌‌such‌‌as‌‌pointers‌‌to‌  ‌
the‌‌operands‌‌of‌‌the‌‌addition‌‌and‌‌returns‌‌a‌‌pointer‌‌representing‌‌the‌‌sum:‌  ‌
struct‌‌Type‌‌{ ‌ ‌
void‌‌*‌‌(*‌‌new)‌‌(va_list‌‌ap);‌  ‌
double‌‌(*‌‌exec)‌‌(const‌‌void‌‌*‌‌tree);‌  ‌
void‌‌(*‌‌delete)‌‌(void‌‌*‌‌tree);‌  ‌
};‌  ‌
void‌‌*‌‌new‌‌(const‌‌void‌‌*‌‌type,‌‌...)‌  ‌
{‌‌va_list‌‌ap;‌ 
void‌‌*‌‌result;‌ 
assert(type‌‌&&‌‌((struct‌‌Type‌‌*)‌‌type)‌‌—>‌‌new);‌  ‌
va_start(ap,‌‌type);‌  ‌
result‌‌=‌‌((struct‌‌Type‌‌*)‌‌type)‌‌—>‌‌new(ap);‌  ‌
*‌‌(const‌‌struct‌‌Type‌‌**)‌‌result‌‌=‌‌type;‌  ‌
va_end(ap);‌  ‌
return‌‌result;‌  ‌
}‌  ‌
We‌‌use‌‌dynamic‌‌linkage‌‌and‌‌pass‌‌the‌‌call‌‌to‌‌a‌‌node-specific‌‌routine‌‌which,‌‌in‌‌the‌  ‌
case‌‌of‌‌Add,‌‌has‌‌to‌‌create‌‌the‌‌node‌‌and‌‌enter‌‌the‌‌two‌‌pointers:‌  ‌
struct‌‌Bin‌‌{ ‌ ‌
const‌‌void‌‌*‌‌type;‌  ‌
void‌‌*‌‌left,‌‌*‌‌right;‌  ‌
};‌  ‌
 ‌
static‌‌void‌‌*‌‌mkBin‌‌(va_list‌‌ap)‌  ‌
{‌‌struct‌‌Bin‌‌*‌‌node‌‌=‌‌malloc(sizeof(struct‌‌Bin));‌  ‌
assert(node);‌  ‌
node‌‌—>‌‌left‌‌=‌‌va_arg(ap,‌‌void‌‌*);‌  ‌
node‌‌—>‌‌right‌‌=‌‌va_arg(ap,‌‌void‌‌*);‌  ‌
return‌‌node;‌  ‌
}‌  ‌
6‌  ‌

Note‌‌that‌‌only‌‌mkBin()‌‌knows‌‌what‌‌node‌‌it‌‌creates.‌‌All‌‌we‌‌require‌‌is‌‌that‌‌the‌‌various‌‌nodes‌‌ 
start‌‌with‌‌a‌‌pointer‌‌for‌‌the‌‌dynamic‌‌linkage.‌‌This‌‌pointer‌‌is‌‌entered‌‌by‌  ‌
new()‌‌so‌‌that‌‌delete()‌‌can‌‌reach‌‌its‌‌node-specific‌‌function:‌  ‌
void‌‌delete‌‌(void‌‌*‌‌tree)‌  ‌
{‌  ‌
assert(tree‌‌&&‌‌*‌‌(struct‌‌Type‌‌**)‌‌tree‌  ‌
&&‌‌(*‌‌(struct‌‌Type‌‌**)‌‌tree)‌‌—>‌‌delete);‌  ‌
(*‌‌(struct‌‌Type‌‌**)‌‌tree)‌‌—>‌‌delete(tree);‌  ‌
}‌  ‌
static‌‌void‌‌freeBin‌‌(void‌‌*‌‌tree)‌  ‌
{‌  ‌
delete(((struct‌‌Bin‌‌*)‌‌tree)‌‌—>‌‌left);‌  ‌
delete(((struct‌‌Bin‌‌*)‌‌tree)‌‌—>‌‌right);‌  ‌
free(tree);‌  ‌
}‌  ‌
Dynamic‌‌linkage‌‌elegantly‌‌avoids‌‌complicated‌‌nodes.‌‌.new()‌‌creates‌‌precisely‌  ‌
the‌‌right‌‌node‌‌for‌‌each‌‌type‌‌description:‌‌binary‌‌operators‌‌have‌‌two‌‌descendants,‌  ‌
unary‌‌operators‌‌have‌‌one,‌‌and‌‌a‌‌value‌‌node‌‌only‌‌contains‌‌the‌‌value.‌‌delete()‌‌is‌‌a ‌ ‌
very‌‌simple‌‌function‌‌because‌‌each‌‌node‌‌handles‌‌its‌‌own‌‌destruction:‌‌binary‌‌operators‌‌delete‌‌ 
two‌‌subtrees‌‌and‌‌free‌‌their‌‌own‌‌node,‌‌unary‌‌operators‌‌delete‌‌only‌‌one‌  ‌
subtree,‌‌and‌‌a‌‌value‌‌node‌‌will‌‌only‌‌free‌‌itself.‌‌Variables‌‌or‌‌constants‌‌can‌‌even‌  ‌
remain‌‌behind‌‌—‌‌they‌‌simply‌‌would‌‌do‌‌nothing‌‌in‌‌response‌‌to‌‌delete().‌ 
 ‌
 ‌
 ‌
1.7‌‌A‌‌Postfix‌‌Writer‌  ‌
So‌‌far‌‌we‌‌have‌‌not‌‌really‌‌decided‌‌what‌‌process()‌‌is‌‌going‌‌to‌‌do.‌‌If‌‌we‌‌want‌‌to‌‌emit‌  ‌
a‌‌postfix‌‌version‌‌of‌‌the‌‌expression,‌‌we‌‌would‌‌add‌‌a‌‌character‌‌string‌‌to‌‌the‌‌struct‌  ‌
Type‌‌to‌‌show‌‌the‌‌actual‌‌operator‌‌and‌‌process()‌‌would‌‌arrange‌‌for‌‌a‌‌single‌‌output‌  ‌
line‌‌indented‌‌by‌‌a‌‌tab:‌  ‌
void‌‌process‌‌(const‌‌void‌‌*‌‌tree)‌  ‌
{‌  ‌
putchar(’\t’);‌  ‌
exec(tree);‌  ‌
putchar(’\n’);‌  ‌
}‌  ‌
exec()‌‌handles‌‌the‌‌dynamic‌‌linkage:‌  ‌
static‌‌void‌‌exec‌‌(const‌‌void‌‌*‌‌tree)‌  ‌
{‌  ‌
assert(tree‌‌&&‌‌*‌‌(struct‌‌Type‌‌**)‌‌tree‌  ‌
&&‌‌(*‌‌(struct‌‌Type‌‌**)‌‌tree)‌‌—>‌‌exec);‌  ‌
(*‌‌(struct‌‌Type‌‌**)‌‌tree)‌‌—>‌‌exec(tree);‌  ‌
}‌  ‌
and‌‌every‌‌binary‌‌operator‌‌is‌‌emitted‌‌with‌‌the‌‌following‌‌function:‌  ‌
static‌‌void‌‌doBin‌‌(const‌‌void‌‌*‌‌tree)‌  ‌
{‌  ‌
exec(((struct‌‌Bin‌‌*)‌‌tree)‌‌—>‌‌left);‌  ‌
exec(((struct‌‌Bin‌‌*)‌‌tree)‌‌—>‌‌right);‌  ‌
7‌  ‌

printf("‌‌%s",‌‌(*‌‌(struct‌‌Type‌‌**)‌‌tree)‌‌—>‌‌name);‌  ‌
}‌  ‌
The‌‌type‌‌descriptions‌‌tie‌‌everything‌‌together:‌  ‌
static‌‌struct‌‌Type‌‌_Add‌‌=‌‌{‌‌"+",‌‌mkBin,‌‌doBin,‌‌freeBin‌‌};‌  ‌
static‌‌struct‌‌Type‌‌_Sub‌‌=‌‌{‌‌"—",‌‌mkBin,‌‌doBin,‌‌freeBin‌‌};‌  ‌
const‌‌void‌‌*‌‌Add‌‌=‌‌&‌‌_Add;‌  ‌
const‌‌void‌‌*‌‌Sub‌‌=‌‌&‌‌_Sub;‌  ‌
It‌‌should‌‌be‌‌easy‌‌to‌‌guess‌‌how‌‌a‌‌numerical‌‌value‌‌is‌‌implemented.‌‌It‌‌is‌‌represented‌  ‌
as‌‌a‌‌structure‌‌with‌‌a‌‌double‌‌information‌‌field:‌  ‌
struct‌‌Val‌‌{ ‌ ‌
const‌‌void‌‌*‌‌type;‌  ‌
double‌‌value;‌  ‌
};‌  ‌
static‌‌void‌‌*‌‌mkVal‌‌(va_list‌‌ap)‌  ‌
{‌‌struct‌‌Val‌‌*‌‌node‌‌=‌‌malloc(sizeof(struct‌‌Val));‌  ‌
assert(node);‌  ‌
node‌‌—>‌‌value‌‌=‌‌va_arg(ap,‌‌double);‌  ‌
return‌‌node;‌  ‌
}‌  ‌
Processing‌‌consists‌‌of‌‌printing‌‌the‌‌value:‌  ‌
static‌‌void‌‌doVal‌‌(const‌‌void‌‌*‌‌tree)‌  ‌
{‌  ‌
printf("‌‌%g",‌‌((struct‌‌Val‌‌*)‌‌tree)‌‌—>‌‌value);‌  ‌
}‌  ‌
We‌‌are‌‌done‌‌—‌‌there‌‌is‌‌no‌‌subtree‌‌to‌‌delete,‌‌so‌‌we‌‌can‌‌use‌‌the‌‌library‌‌function‌  ‌
free()‌‌directly‌‌to‌‌delete‌‌the‌‌value‌‌node:‌  ‌
static‌‌struct‌‌Type‌‌_Value‌‌=‌‌{‌‌"",‌‌mkVal,‌‌doVal,‌‌free‌‌};‌  ‌
const‌‌void‌‌*‌‌Value‌‌=‌‌&‌‌_Value;‌  ‌
A‌‌unary‌‌operator‌‌such‌‌as‌‌Minus‌‌is‌‌left‌‌as‌‌an‌‌exercise.‌  ‌
 ‌
 ‌
 ‌
1.8‌‌Arithmetic‌  ‌
If‌‌we‌‌want‌‌to‌‌do‌‌arithmetic,‌‌we‌‌let‌‌the‌‌execute‌‌functions‌‌return‌‌double‌‌values‌‌to‌  ‌
be‌‌printed‌‌in‌‌process():‌  ‌
static‌‌double‌‌exec‌‌(const‌‌void‌‌*‌‌tree)‌  ‌
{‌  ‌
return‌‌(*‌‌(struct‌‌Type‌‌**)‌‌tree)‌‌—>‌‌exec(tree);‌  ‌
}‌  ‌
void‌‌process‌‌(const‌‌void‌‌*‌‌tree)‌  ‌
{‌  ‌
printf("\t%g\n",‌‌exec(tree));‌  ‌
}‌  ‌
For‌‌each‌‌type‌‌of‌‌node‌‌we‌‌need‌‌one‌‌execution‌‌function‌‌which‌‌computes‌‌and‌‌returns‌  ‌
the‌‌value‌‌for‌‌the‌‌node.‌‌Here‌‌are‌‌two‌‌examples:‌  ‌
static‌‌double‌‌doVal‌‌(const‌‌void‌‌*‌‌tree)‌  ‌
{‌  ‌
return‌‌((struct‌‌Val‌‌*)‌‌tree)‌‌—>‌‌value;‌  ‌
8‌  ‌

}‌  ‌
static‌‌double‌‌doAdd‌‌(const‌‌void‌‌*‌‌tree)‌  ‌
{‌  ‌
return‌‌exec(((struct‌‌Bin‌‌*)‌‌tree)‌‌—>‌‌left)‌‌+ ‌ ‌
exec(((struct‌‌Bin‌‌*)‌‌tree)‌‌—>‌‌right);‌  ‌
}‌  ‌
static‌‌struct‌‌Type‌‌_Add‌‌=‌‌{‌‌mkBin,‌‌doAdd,‌‌freeBin‌‌};‌  ‌
static‌‌struct‌‌Type‌‌_Value‌‌=‌‌{‌‌mkVal,‌‌doVal,‌‌free‌‌};‌  ‌
const‌‌void‌‌*‌‌Add‌‌=‌‌&‌‌_Add;‌  ‌
const‌‌void‌‌*‌‌Value‌‌=‌‌&‌‌_Value;‌  ‌
 ‌
 ‌
 ‌
1.9‌‌Infix‌‌Output‌  ‌
Perhaps‌‌the‌‌highlight‌‌of‌‌processing‌‌arithmetic‌‌expressions‌‌is‌‌to‌‌print‌‌them‌‌with‌‌a ‌ ‌
minimal‌‌number‌‌of‌‌parentheses.‌‌This‌‌is‌‌usually‌‌a‌‌bit‌‌tricky,‌‌depending‌‌on‌‌who‌‌is‌  ‌
responsible‌‌for‌‌emitting‌‌the‌‌parentheses.‌‌In‌‌addition‌‌to‌‌the‌‌operator‌‌name‌‌used‌‌for‌  ‌
postfix‌‌output‌‌we‌‌add‌‌two‌‌numbers‌‌to‌‌the‌‌struct‌‌Type:‌  ‌
struct‌‌Type‌‌{ ‌ ‌
const‌‌char‌‌*‌‌name;‌‌/*‌‌node’s‌‌name‌‌*/‌  ‌
char‌‌rank,‌‌rpar;‌  ‌
void‌‌*‌‌(*‌‌new)‌‌(va_list‌‌ap);‌  ‌
void‌‌(*‌‌exec)‌‌(const‌‌void‌‌*‌‌tree,‌‌int‌‌rank,‌‌int‌‌par);‌  ‌
void‌‌(*‌‌delete)‌‌(void‌‌*‌‌tree);‌  ‌
};‌  ‌
.rank‌‌is‌‌the‌‌precedence‌‌of‌‌the‌‌operator,‌‌starting‌‌with‌‌1‌‌for‌‌addition.‌‌.rpar‌‌is‌‌set‌‌for‌  ‌
operators‌‌such‌‌as‌‌subtraction,‌‌which‌‌require‌‌their‌‌right‌‌operand‌‌to‌‌be‌‌enclosed‌‌in‌  ‌
parentheses‌‌if‌‌it‌‌uses‌‌an‌‌operator‌‌of‌‌equal‌‌precedence.‌‌As‌‌an‌‌example‌‌consider‌ 
 ‌
 ‌
$‌‌infix‌  ‌
1‌‌+‌‌(2‌‌—‌‌3)‌  ‌
1+2‌‌—‌‌3 ‌ ‌
1‌‌—‌‌(2‌‌—‌‌3)‌  ‌
1‌‌—‌‌(2‌‌—‌‌3)‌  ‌
This‌‌demonstrates‌‌that‌‌we‌‌have‌‌the‌‌following‌‌initialization:‌  ‌
static‌‌struct‌‌Type‌‌_Add‌‌=‌‌{"+",‌‌1,‌‌0,‌‌mkBin,‌‌doBin,‌‌freeBin};‌  ‌
static‌‌struct‌‌Type‌‌_Sub‌‌=‌‌{"—",‌‌1,‌‌1,‌‌mkBin,‌‌doBin,‌‌freeBin};‌  ‌
The‌‌tricky‌‌part‌‌is‌‌for‌‌a‌‌binary‌‌node‌‌to‌‌decide‌‌if‌‌it‌‌must‌‌surround‌‌itself‌‌with‌  ‌
parentheses.‌‌A‌‌binary‌‌node‌‌such‌‌as‌‌an‌‌addition‌‌is‌‌given‌‌the‌‌precedence‌‌of‌‌its‌  ‌
superior‌‌and‌‌a‌‌flag‌‌indicating‌‌whether‌‌parentheses‌‌are‌‌needed‌‌in‌‌the‌‌case‌‌of‌‌equal‌  ‌
precedence.‌‌doBin()‌‌decides‌‌if‌‌it‌‌will‌‌use‌‌parentheses:‌  ‌
static‌‌void‌‌doBin‌‌(const‌‌void‌‌*‌‌tree,‌‌int‌‌rank,‌‌int‌‌par)‌  ‌
{‌‌const‌‌struct‌‌Type‌‌*‌‌type‌‌=‌‌*‌‌(struct‌‌Type‌‌**)‌‌tree;‌  ‌
par‌‌=‌‌type‌‌—>‌‌rank‌‌<‌‌rank‌  ‌
||‌‌(par‌‌&&‌‌type‌‌—>‌‌rank‌‌==‌‌rank);‌  ‌
if‌‌(par)‌‌putchar(’(’);‌  ‌
If‌‌our‌‌node‌‌has‌‌less‌‌precedence‌‌than‌‌its‌‌superior,‌‌or‌‌if‌‌we‌‌are‌‌asked‌‌to‌‌put‌‌up‌  ‌
9‌  ‌

parentheses‌‌on‌‌equal‌‌precedence,‌‌we‌‌print‌‌parentheses.‌‌In‌‌any‌‌case,‌‌if‌‌our‌  ‌
description‌‌has‌‌.rpar‌‌set,‌‌we‌‌require‌‌only‌‌of‌‌our‌‌right‌‌operand‌‌that‌‌it‌‌put‌‌up‌‌extra‌  ‌
parentheses:‌  ‌
exec(((struct‌‌Bin‌‌*)‌‌tree)‌‌—>‌‌left,‌‌type‌‌—>‌‌rank,‌‌0);‌  ‌
printf("‌‌%s‌‌",‌‌type‌‌—>‌‌name);‌  ‌
exec(((struct‌‌Bin‌‌*)‌‌tree)‌‌—>‌‌right,‌  ‌
type‌‌—>‌‌rank,‌‌type‌‌—>‌‌rpar);‌  ‌
if‌‌(par)‌‌putchar(’)’);‌  ‌
}‌  ‌
The‌‌remaining‌‌printing‌‌routines‌‌are‌‌significantly‌‌simpler‌‌to‌‌write.‌  ‌
 ‌
 ‌
 ‌
 ‌
REFERENCES‌  ‌
[ANSI]‌‌American‌‌National‌‌Standard‌‌for‌‌Information‌‌Systems‌‌—‌‌Programming‌  ‌
Language‌‌C‌‌X3.159-1989.‌  ‌
[AWK88]‌‌A.‌‌V.‌‌Aho,‌‌B.‌‌W.‌‌Kernighan‌‌und‌‌P.‌‌J.‌‌Weinberger‌‌The‌‌awk‌‌Programming‌  ‌
Language‌‌Addison-Wesley‌‌1988,‌‌ISBN‌‌0-201-07981-X.‌  ‌
 ‌
AUTHOR‌  ‌
Guruprasad‌‌Davangave‌  ‌
Software‌‌Engineer‌  ‌
Computer‌‌Science‌‌and‌‌Engineering.‌‌[2021]‌  ‌
 ‌
 ‌
 ‌

You might also like