1# This file provides the runtime support for running a basic program
2# Assumes the program has been parsed using basparse.py
3
4import sys
5import math
6import random
7
8class BasicInterpreter:
9
10    # Initialize the interpreter. prog is a dictionary
11    # containing (line,statement) mappings
12    def __init__(self,prog):
13         self.prog = prog
14
15         self.functions = {           # Built-in function table
16             'SIN' : lambda z: math.sin(self.eval(z)),
17             'COS' : lambda z: math.cos(self.eval(z)),
18             'TAN' : lambda z: math.tan(self.eval(z)),
19             'ATN' : lambda z: math.atan(self.eval(z)),
20             'EXP' : lambda z: math.exp(self.eval(z)),
21             'ABS' : lambda z: abs(self.eval(z)),
22             'LOG' : lambda z: math.log(self.eval(z)),
23             'SQR' : lambda z: math.sqrt(self.eval(z)),
24             'INT' : lambda z: int(self.eval(z)),
25             'RND' : lambda z: random.random()
26         }
27
28    # Collect all data statements
29    def collect_data(self):
30         self.data = []
31         for lineno in self.stat:
32              if self.prog[lineno][0] == 'DATA':
33                  self.data = self.data + self.prog[lineno][1]
34         self.dc = 0                  # Initialize the data counter
35
36    # Check for end statements
37    def check_end(self):
38         has_end = 0
39         for lineno in self.stat:
40             if self.prog[lineno][0] == 'END' and not has_end:
41                  has_end = lineno
42         if not has_end:
43             print("NO END INSTRUCTION")
44             self.error = 1
45             return
46         if has_end != lineno:
47             print("END IS NOT LAST")
48             self.error = 1
49
50    # Check loops
51    def check_loops(self):
52         for pc in range(len(self.stat)):
53             lineno = self.stat[pc]
54             if self.prog[lineno][0] == 'FOR':
55                  forinst = self.prog[lineno]
56                  loopvar = forinst[1]
57                  for i in range(pc+1,len(self.stat)):
58                       if self.prog[self.stat[i]][0] == 'NEXT':
59                            nextvar = self.prog[self.stat[i]][1]
60                            if nextvar != loopvar: continue
61                            self.loopend[pc] = i
62                            break
63                  else:
64                       print("FOR WITHOUT NEXT AT LINE %s" % self.stat[pc])
65                       self.error = 1
66
67    # Evaluate an expression
68    def eval(self,expr):
69        etype = expr[0]
70        if etype == 'NUM': return expr[1]
71        elif etype == 'GROUP': return self.eval(expr[1])
72        elif etype == 'UNARY':
73             if expr[1] == '-': return -self.eval(expr[2])
74        elif etype == 'BINOP':
75             if expr[1] == '+': return self.eval(expr[2])+self.eval(expr[3])
76             elif expr[1] == '-': return self.eval(expr[2])-self.eval(expr[3])
77             elif expr[1] == '*': return self.eval(expr[2])*self.eval(expr[3])
78             elif expr[1] == '/': return float(self.eval(expr[2]))/self.eval(expr[3])
79             elif expr[1] == '^': return abs(self.eval(expr[2]))**self.eval(expr[3])
80        elif etype == 'VAR':
81             var,dim1,dim2 = expr[1]
82             if not dim1 and not dim2:
83                  if var in self.vars:
84                       return self.vars[var]
85                  else:
86                       print("UNDEFINED VARIABLE %s AT LINE %s" % (var, self.stat[self.pc]))
87                       raise RuntimeError
88             # May be a list lookup or a function evaluation
89             if dim1 and not dim2:
90                if var in self.functions:
91                      # A function
92                      return self.functions[var](dim1)
93                else:
94                      # A list evaluation
95                      if var in self.lists:
96                            dim1val = self.eval(dim1)
97                            if dim1val < 1 or dim1val > len(self.lists[var]):
98                                 print("LIST INDEX OUT OF BOUNDS AT LINE %s" % self.stat[self.pc])
99                                 raise RuntimeError
100                            return self.lists[var][dim1val-1]
101             if dim1 and dim2:
102                 if var in self.tables:
103                      dim1val = self.eval(dim1)
104                      dim2val = self.eval(dim2)
105                      if dim1val < 1 or dim1val > len(self.tables[var]) or dim2val < 1 or dim2val > len(self.tables[var][0]):
106                           print("TABLE INDEX OUT OUT BOUNDS AT LINE %s" % self.stat[self.pc])
107                           raise RuntimeError
108                      return self.tables[var][dim1val-1][dim2val-1]
109             print("UNDEFINED VARIABLE %s AT LINE %s" % (var, self.stat[self.pc]))
110             raise RuntimeError
111
112    # Evaluate a relational expression
113    def releval(self,expr):
114         etype = expr[1]
115         lhs   = self.eval(expr[2])
116         rhs   = self.eval(expr[3])
117         if etype == '<':
118             if lhs < rhs: return 1
119             else: return 0
120
121         elif etype == '<=':
122             if lhs <= rhs: return 1
123             else: return 0
124
125         elif etype == '>':
126             if lhs > rhs: return 1
127             else: return 0
128
129         elif etype == '>=':
130             if lhs >= rhs: return 1
131             else: return 0
132
133         elif etype == '=':
134             if lhs == rhs: return 1
135             else: return 0
136
137         elif etype == '<>':
138             if lhs != rhs: return 1
139             else: return 0
140
141    # Assignment
142    def assign(self,target,value):
143        var, dim1, dim2 = target
144        if not dim1 and not dim2:
145            self.vars[var] = self.eval(value)
146        elif dim1 and not dim2:
147            # List assignment
148            dim1val = self.eval(dim1)
149            if not var in self.lists:
150                 self.lists[var] = [0]*10
151
152            if dim1val > len(self.lists[var]):
153                 print ("DIMENSION TOO LARGE AT LINE %s" % self.stat[self.pc])
154                 raise RuntimeError
155            self.lists[var][dim1val-1] = self.eval(value)
156        elif dim1 and dim2:
157            dim1val = self.eval(dim1)
158            dim2val = self.eval(dim2)
159            if not var in self.tables:
160                 temp = [0]*10
161                 v = []
162                 for i in range(10): v.append(temp[:])
163                 self.tables[var] = v
164            # Variable already exists
165            if dim1val > len(self.tables[var]) or dim2val > len(self.tables[var][0]):
166                 print("DIMENSION TOO LARGE AT LINE %s" % self.stat[self.pc])
167                 raise RuntimeError
168            self.tables[var][dim1val-1][dim2val-1] = self.eval(value)
169
170    # Change the current line number
171    def goto(self,linenum):
172         if not linenum in self.prog:
173              print("UNDEFINED LINE NUMBER %d AT LINE %d" % (linenum, self.stat[self.pc]))
174              raise RuntimeError
175         self.pc = self.stat.index(linenum)
176
177    # Run it
178    def run(self):
179        self.vars   = { }            # All variables
180        self.lists  = { }            # List variables
181        self.tables = { }            # Tables
182        self.loops  = [ ]            # Currently active loops
183        self.loopend= { }            # Mapping saying where loops end
184        self.gosub  = None           # Gosub return point (if any)
185        self.error  = 0              # Indicates program error
186
187        self.stat = list(self.prog)  # Ordered list of all line numbers
188        self.stat.sort()
189        self.pc = 0                  # Current program counter
190
191        # Processing prior to running
192
193        self.collect_data()          # Collect all of the data statements
194        self.check_end()
195        self.check_loops()
196
197        if self.error: raise RuntimeError
198
199        while 1:
200            line  = self.stat[self.pc]
201            instr = self.prog[line]
202
203            op = instr[0]
204
205            # END and STOP statements
206            if op == 'END' or op == 'STOP':
207                 break           # We're done
208
209            # GOTO statement
210            elif op == 'GOTO':
211                 newline = instr[1]
212                 self.goto(newline)
213                 continue
214
215            # PRINT statement
216            elif op == 'PRINT':
217                 plist = instr[1]
218                 out = ""
219                 for label,val in plist:
220                     if out:
221                          out += ' '*(15 - (len(out) % 15))
222                     out += label
223                     if val:
224                          if label: out += " "
225                          eval = self.eval(val)
226                          out += str(eval)
227                 sys.stdout.write(out)
228                 end = instr[2]
229                 if not (end == ',' or end == ';'):
230                     sys.stdout.write("\n")
231                 if end == ',': sys.stdout.write(" "*(15-(len(out) % 15)))
232                 if end == ';': sys.stdout.write(" "*(3-(len(out) % 3)))
233
234            # LET statement
235            elif op == 'LET':
236                 target = instr[1]
237                 value  = instr[2]
238                 self.assign(target,value)
239
240            # READ statement
241            elif op == 'READ':
242                 for target in instr[1]:
243                      if self.dc < len(self.data):
244                          value = ('NUM',self.data[self.dc])
245                          self.assign(target,value)
246                          self.dc += 1
247                      else:
248                          # No more data.  Program ends
249                          return
250            elif op == 'IF':
251                 relop = instr[1]
252                 newline = instr[2]
253                 if (self.releval(relop)):
254                     self.goto(newline)
255                     continue
256
257            elif op == 'FOR':
258                 loopvar = instr[1]
259                 initval = instr[2]
260                 finval  = instr[3]
261                 stepval = instr[4]
262
263                 # Check to see if this is a new loop
264                 if not self.loops or self.loops[-1][0] != self.pc:
265                        # Looks like a new loop. Make the initial assignment
266                        newvalue = initval
267                        self.assign((loopvar,None,None),initval)
268                        if not stepval: stepval = ('NUM',1)
269                        stepval = self.eval(stepval)    # Evaluate step here
270                        self.loops.append((self.pc,stepval))
271                 else:
272                        # It's a repeat of the previous loop
273                        # Update the value of the loop variable according to the step
274                        stepval = ('NUM',self.loops[-1][1])
275                        newvalue = ('BINOP','+',('VAR',(loopvar,None,None)),stepval)
276
277                 if self.loops[-1][1] < 0: relop = '>='
278                 else: relop = '<='
279                 if not self.releval(('RELOP',relop,newvalue,finval)):
280                      # Loop is done. Jump to the NEXT
281                      self.pc = self.loopend[self.pc]
282                      self.loops.pop()
283                 else:
284                      self.assign((loopvar,None,None),newvalue)
285
286            elif op == 'NEXT':
287                 if not self.loops:
288                       print("NEXT WITHOUT FOR AT LINE %s" % line)
289                       return
290
291                 nextvar = instr[1]
292                 self.pc = self.loops[-1][0]
293                 loopinst = self.prog[self.stat[self.pc]]
294                 forvar = loopinst[1]
295                 if nextvar != forvar:
296                       print("NEXT DOESN'T MATCH FOR AT LINE %s" % line)
297                       return
298                 continue
299            elif op == 'GOSUB':
300                 newline = instr[1]
301                 if self.gosub:
302                       print("ALREADY IN A SUBROUTINE AT LINE %s" % line)
303                       return
304                 self.gosub = self.stat[self.pc]
305                 self.goto(newline)
306                 continue
307
308            elif op == 'RETURN':
309                 if not self.gosub:
310                      print("RETURN WITHOUT A GOSUB AT LINE %s" % line)
311                      return
312                 self.goto(self.gosub)
313                 self.gosub = None
314
315            elif op == 'FUNC':
316                 fname = instr[1]
317                 pname = instr[2]
318                 expr  = instr[3]
319                 def eval_func(pvalue,name=pname,self=self,expr=expr):
320                      self.assign((pname,None,None),pvalue)
321                      return self.eval(expr)
322                 self.functions[fname] = eval_func
323
324            elif op == 'DIM':
325                 for vname,x,y in instr[1]:
326                     if y == 0:
327                          # Single dimension variable
328                          self.lists[vname] = [0]*x
329                     else:
330                          # Double dimension variable
331                          temp = [0]*y
332                          v = []
333                          for i in range(x):
334                              v.append(temp[:])
335                          self.tables[vname] = v
336
337            self.pc += 1
338
339    # Utility functions for program listing
340    def expr_str(self,expr):
341        etype = expr[0]
342        if etype == 'NUM': return str(expr[1])
343        elif etype == 'GROUP': return "(%s)" % self.expr_str(expr[1])
344        elif etype == 'UNARY':
345             if expr[1] == '-': return "-"+str(expr[2])
346        elif etype == 'BINOP':
347             return "%s %s %s" % (self.expr_str(expr[2]),expr[1],self.expr_str(expr[3]))
348        elif etype == 'VAR':
349              return self.var_str(expr[1])
350
351    def relexpr_str(self,expr):
352         return "%s %s %s" % (self.expr_str(expr[2]),expr[1],self.expr_str(expr[3]))
353
354    def var_str(self,var):
355         varname,dim1,dim2 = var
356         if not dim1 and not dim2: return varname
357         if dim1 and not dim2: return "%s(%s)" % (varname, self.expr_str(dim1))
358         return "%s(%s,%s)" % (varname, self.expr_str(dim1),self.expr_str(dim2))
359
360    # Create a program listing
361    def list(self):
362         stat = list(self.prog)      # Ordered list of all line numbers
363         stat.sort()
364         for line in stat:
365             instr = self.prog[line]
366             op = instr[0]
367             if op in ['END','STOP','RETURN']:
368                   print("%s %s" % (line, op))
369                   continue
370             elif op == 'REM':
371                   print("%s %s" % (line, instr[1]))
372             elif op == 'PRINT':
373                   _out = "%s %s " % (line, op)
374                   first = 1
375                   for p in instr[1]:
376                         if not first: _out += ", "
377                         if p[0] and p[1]: _out += '"%s"%s' % (p[0],self.expr_str(p[1]))
378                         elif p[1]: _out += self.expr_str(p[1])
379                         else: _out += '"%s"' % (p[0],)
380                         first = 0
381                   if instr[2]: _out += instr[2]
382                   print(_out)
383             elif op == 'LET':
384                   print("%s LET %s = %s" % (line,self.var_str(instr[1]),self.expr_str(instr[2])))
385             elif op == 'READ':
386                   _out = "%s READ " % line
387                   first = 1
388                   for r in instr[1]:
389                         if not first: _out += ","
390                         _out += self.var_str(r)
391                         first = 0
392                   print(_out)
393             elif op == 'IF':
394                   print("%s IF %s THEN %d" % (line,self.relexpr_str(instr[1]),instr[2]))
395             elif op == 'GOTO' or op == 'GOSUB':
396                   print("%s %s %s" % (line, op, instr[1]))
397             elif op == 'FOR':
398                   _out = "%s FOR %s = %s TO %s" % (line,instr[1],self.expr_str(instr[2]),self.expr_str(instr[3]))
399                   if instr[4]: _out += " STEP %s" % (self.expr_str(instr[4]))
400                   print(_out)
401             elif op == 'NEXT':
402                   print("%s NEXT %s" % (line, instr[1]))
403             elif op == 'FUNC':
404                   print("%s DEF %s(%s) = %s" % (line,instr[1],instr[2],self.expr_str(instr[3])))
405             elif op == 'DIM':
406                   _out = "%s DIM " % line
407                   first = 1
408                   for vname,x,y in instr[1]:
409                         if not first: _out += ","
410                         first = 0
411                         if y == 0:
412                               _out += "%s(%d)" % (vname,x)
413                         else:
414                               _out += "%s(%d,%d)" % (vname,x,y)
415
416                   print(_out)
417             elif op == 'DATA':
418                   _out = "%s DATA " % line
419                   first = 1
420                   for v in instr[1]:
421                        if not first: _out += ","
422                        first = 0
423                        _out += v
424                   print(_out)
425
426    # Erase the current program
427    def new(self):
428         self.prog = {}
429
430    # Insert statements
431    def add_statements(self,prog):
432         for line,stat in prog.items():
433              self.prog[line] = stat
434
435    # Delete a statement
436    def del_line(self,lineno):
437         try:
438             del self.prog[lineno]
439         except KeyError:
440             pass
441
442