User:Marinus/Gibberish interpreter

From Esolang
Jump to navigation Jump to search

This is my implementation of Gibberish. It should be a full implementation, though the reference interpreter isn't finished so I can't really be sure. Unless I missed a bug, it is compatible with the finished part of the official interpreter.

(I first wanted to finish the official interpreter, but this new one was a lot easier to do.)

######################################################################
# gibberish.py - python gibberish interpreter 
# 
# possible inconsistencies:
#   - q(3) stops the entire program, not just the currently running
#     piece of code, even when called from an exec'ed string
#     (I took "currently-running program" to mean the entire program)
#
#   - not in specification, but unfinished official interpreter seems to 
#     do it this way and/or given examples seem to rely on this:
#     - trying to copy/move/pop past the stack results in a run-time error
#     - numbers are floating point
#     - numbers used for stack indexes are rounded down
#     - type mismatches are run-time errors
#     - the stack and selected instruction set are global across "subroutines"
#     - pushing a string constant [like this] counts as one instruction 
#     - using x(0) to set the instruction set to one that doesn't exist
#       is a run-time error       
#
#   - neither in specification or in unfinished official interpreter
#     - but seems logical:
#       - exec'ing a string of malformed code is a run-time error
#
#       - numbers used for string indexes are rounded down
#         (like the stack)
# 
#       - reading past EOF returns -1 (char) or "" ([]?) (str)
#         (most other esolangs return -1 on EOF) 
#
#       - a line read from stdin still has its newline attached
#         (to distinguish [\n] (=empty line) from [] (=EOF), 
#          also this is how almost everyone else does it)
#
#       - logical operators consider 0.0000... to be false and everything
#         else to be true
#      
#     - and I have no idea how else to do it:
#       - lshift and rshift convert their arguments to ints first
#         (can't shift something, say, 2.3 bits to the left,
#          and bitshifting IEEE-754 floating point values seems monumentally
#          useless in comparison with bitshifting ints)
#
#       - arguments to binary and/or are converted to ints first
#         (fits with lshift/rshift)
#
##########################################


from operator import *
import sys

# types used for strings and numbers
STRT = str
NUMT = float

### helper functions ###
def typedesc(type):
   if type==STRT: return "string"
   if type==NUMT: return "number"
   return "invalid (%s)"%str(type)

# make error string
def errstr(code, error, position):
   amt = max(0, position - 5)
   prev = code[amt : position]
   if amt!=0: prev = "..." + prev
   
   amt = min(position + 6, len(code))
   next = code[position+1 : amt]
   if amt!=len(code): next += "..."
   
   return "%s at position %d (%s ->%s<- %s)" % \
         (error, position, prev, code[position], next)

# make error string with list of items
def items_errstr(code, error, position):
   prev = ",".join([str(c) for c in code[max(0, position - 5) : position]])
   next = ",".join([str(c) for c in code[position+1:position+6]])

   return "%s at ip %s (%s ->%s<- %s)" % \
         (error, position, prev, str(code[position]), next)
         

# return ordinal suffix (i.e. 1 => st and 4 => th)
def ordsuffix(num):
   if (int(num) % 100 in (11,12,13)): return "th"
   if (int(num) % 10 == 1): return "st"
   if (int(num) % 10 == 2): return "nd"
   if (int(num) % 10 == 3): return "rd"
   return "th"

#####

# stack class
class Stack:

   stack = None
   
   def __init__(self, copystack=None):
      if copystack:
         self.stack = copystack[:]
      else:
         self.stack = []


   def push(self, value):
      # make sure all numbers use the same type
      if type(value) in [bool, int]: value = NUMT(value)
      self.stack.append(value)
 
   def pop(self):
      return self.stack.pop()

   def swapn(self, n):
      n=int(n)
      foo = self.stack[-1-n]
      self.stack[-1-n] = self.stack[-1]
      self.stack[-1] = foo

   swap = lambda s: s.swapn(1)
   swap2 = lambda s: s.swapn(2)
   swap3 = lambda s: s.swapn(3)
 
   def copy(self, n):
      n=int(n)
      self.stack.append(self.stack[-1-n])

   dup = lambda s: s.copy(0)
   
   def move(self, n):
      n=int(n)
      self.stack.append(self.stack[-1-n])
      del self.stack[-1-n]

   def insert(self, n, value):
      n=int(n)
      self.stack.insert(-n-1, value)

   # 'inverted' copy/move = copy/move counting from the bottom of the stack
   def invcopy(self, n):
      n=int(n)
      self.stack.append(self.stack[n])
   
   def invmove(self, n):
      n=int(n)
      self.stack.append(self.stack[n])
      del self.stack[n]

   # getitem/setitem/len
   def __getitem__(self, n):
      return self.stack[-1-n]
   def __setitem__(self, n, val):
      self.insert(n, val)
   def __len__(self):
      return len(self.stack)


# parser
class Parser:
   CONSTANT, COMMAND = range(2)

   # program will be made up of a list of these
   class Item:
      def __init__(self, type, value):
         if type in (Parser.CONSTANT, Parser.COMMAND):
            self.type=type
            self.value=value
         else: raise TypeError, "item type invalid: %d" % type
      
      def __str__(self):
         if self.type==Parser.COMMAND: return self.value
         elif self.type==Parser.CONSTANT:
            if isinstance(self.value, str): return "[%s]" % self.value
            else: return str(self.value)

   # takes string, returns list of items
   @staticmethod
   def parse(string):
      items = []
      i = 0
      while i<len(string):
         # numbers
         if string[i] in "0123456789": 
            items.append(Parser.Item(Parser.CONSTANT, NUMT(string[i])))
         # strings
         elif string[i] == '[':
            curstr = ''
            lvl = 1
            while lvl>0:
               i += 1
               if (i>=len(string) and lvl>0):
                  raise ValueError(errstr(string, "unterminated [", i-1))
               if string[i] == ']': lvl -= 1
               elif string[i] == '[': lvl += 1
               if (lvl!=0): curstr += string[i]
            items.append(Parser.Item(Parser.CONSTANT, curstr))
         # whitespace is ignored
         elif string[i] in ' \n\t': pass
         # ] without [ == error
         elif string[i] == ']':
            raise ValueError(errstr(string, "] without [", i))
         # not one of these == command
         else:
            items.append(Parser.Item(Parser.COMMAND, string[i]))
         i += 1
   
      return items

class Interpreter:

   # exception to be caught if something goes wrong with the code
   class CodeError(Exception): pass;

   world = None
   code = None
   stack = None
   ip = 0
   activeset = 0
   sets = None # command sets, see init
   parent = None

   def __init__(self,world,code,stack=None,ip=0,activeset=0,parent = None):
      self.world = world
      self.code = code
      if stack==None:
         self.stack = Stack()
      else:
         self.stack = stack
      self.ip = ip 
      self.activeset = activeset
      self.parent = None

      s=self # makes the huge list less long
      s.sets = [
          # instruction set 0 #
          { 'e' : s.activateSet1,
            'f' : s.activateSet2,
            'g' : s.activateSet3,
            'x' : s.cActivateSet,
            'j' : s.cGetSet,
            'z' : s.cNop
          },
          # instruction set 1 #
          { 'u' : s.cDuplicate,
            'a' : s.cAdd,
            's' : s.cSub,
            'm' : s.cMul,
            'd' : s.cDiv,
            't' : s.cToStr,
            'i' : s.cToNum,
            'c' : s.cConcatenate,
            'o' : s.cOutput,
            'q' : s.cInlineOutput,
            'n' : s.cReadChar,
            'l' : s.cReadLine,
            'h' : s.cSubstring,
            'y' : s.cStrLen,
            'v' : s.cDiscard,
            'p' : s.cCopy,
            'k' : s.cMove,
            'r' : s.cStackSize
          },
          # instruction set 2 #
          { 'u' : s.cGT,
            'd' : s.cLT,
            's' : s.cSkip, 
            't' : s.cSkipTwo,
            'p' : s.cInsert,
            'a' : s.cAnd,
            'o' : s.cOr,
            'n' : s.cNot,
            'c' : s.cExec,
            'w' : s.cWhile,
            'q' : s.cEqual,
            'l' : s.cLshift,
            'r' : s.cRshift
          },
          # instruction set 3 #
          { 'q' : s.cQuit,
            'w' : s.cRecallWhile,
            'n' : s.cIsNumber,
            's' : s.cIsString,
            'a' : s.cBinAnd,
            'o' : s.cBinOr,
            'i' : s.cInteger,
            'm' : s.cMod,
            't' : s.cToChar,
            'c' : s.cCharAt,
            'r' : s.cReplaceChar, 
            'p' : s.cInvertedCopy,
            'k' : s.cInvertedMove, 
            'b' : s.cSwap,
            'd' : s.cSwap2,
            'h' : s.cSwap3,
          }
      ]
 
   # step function
   # returns True if there are more steps, False if not.
   def step(self):
      if self.ip >= len(self.code):
         return False # at the end of the code
      
      curitem = self.code[self.ip]
 
      # if there's a trace variable defined and true, give a trace
      try: 
         global trace
         if trace: 
            print "\x1b[7m",  
            print "trace: cur=", self.ip, str(curitem), "set=", self.activeset, "stack=", self.stack.stack,
            print "\x1b[0m"
      except: pass

      if curitem.type == Parser.CONSTANT:
         # this is a constant value, push it
         self.sf(self.stack.push, [curitem.value])
      else:
         # it's a command
         if curitem.value in self.sets[0]:
            # command set 0 has priority above everything
            self.sets[0][curitem.value]()
         else:
            # get it from selected set
            if self.activeset<0 or self.activeset>=len(self.sets):
               # selected set is invalid
               raise Interpreter.CodeError(self.err("no such set: %d",
                                               self.activeset))
            else:
               if not curitem.value in self.sets[self.activeset]:
                  #invalid command for selected set
                  raise Interpreter.CodeError(self.err(
                         "set %d has no command '%s'"%( self.activeset, \
                                                   curitem.value)))           
               else:
                  #yay it's good, run it
                  try:
                     self.sets[self.activeset][curitem.value]()
                  except ZeroDivisionError, complaint:
                     raise Interpreter.CodeError(self.err(
                         "division by zero (%s)"%complaint))
      self.ip += 1  
      return True   

   # make an error string with error & current state
   def err(self, error):
      return items_errstr(self.code, error, self.ip)


   # call stack function and catch its errors
   def sf(self, func, args):
      rv=None
      try: rv = func(*args)
      except IndexError, complaint:
         raise Interpreter.CodeError(self.err("stack error (%s)"%complaint))
      return rv

   # raise a type error
   def raiseTypeErr(self, expected, got):
      raise Interpreter.CodeError(
          self.err("wrong type: expected %s instead of %s" % \
                      (typedesc(expected), typedesc(got))))
     
   ### commands ###
   
   # convert a function f(a) into push(f(pop())) + type checking
   def unarystackf(self, f, ntype=None, pushresult=True):
      def stackfunc():
         a=self.sf(self.stack.pop,[])
         if ntype and not isinstance(a,ntype):
            self.raiseTypeErr(ntype, type(a))

         res = f(a)
         if pushresult: self.sf(self.stack.push,[res])
      return stackfunc

   # convert a function f(a,b) into b=pop(),a=pop(),push(f(a,b)) + type checking
   def binstackf(self, f, atype=None, btype=None, pushresult=True):
      def stackfunc():
         b = self.sf(self.stack.pop,[])
         a = self.sf(self.stack.pop,[])
         if btype and not isinstance(b, btype):
            self.raiseTypeErr(btype, type(b))
         if atype and not isinstance(a, atype):
            self.raiseTypeErr(atype, type(a))
         res=f(a,b)
         if pushresult: self.sf(self.stack.push,[res])
      return stackfunc
   
   # and now one with three arguments f(a,b,c) 
   def tristackf(self, f, atype=None, btype=None, ctype=None, pushresult=True):
      x = self.sf
      y = self.stack.pop
      def stackfunc():
         c = x(y,[])
         b = x(y,[])
         a = x(y,[])
         if ctype and not isinstance(c,ctype):
            self.raiseTypeErr(ctype, type(c))
         if btype and not isinstance(b,btype):
            self.raiseTypeErr(btype, type(b))
         if atype and not isinstance(a,atype): 
            self.raiseTypeErr(atype, type(a))
         res = f(a,b,c)
         if pushresult: self.sf(self.stack.push,[res])
      return stackfunc 
        
   ## set 0 ##
   def cActivateSet(self):
      set = self.sf(self.stack.pop,[])
      if not isinstance(set, NUMT):
         raise Interpreter.CodeError(
                               self.err("wrong type: expected number"))
      if len(self.sets)<=int(set) or int(set)<0: 
         raise Interpreter.CodeError(
                               self.err("no instruction set %d exists" % set))
      self.activeset = int(set)
   
   def activateSet1(self): self.activeset = 1
   def activateSet2(self): self.activeset = 2
   def activateSet3(self): self.activeset = 3

   def cGetSet(self):
      self.sf(self.stack.push,[self.activeset])
 
   def cNop(self): pass

   ## set 1 ##
   def cDuplicate(self): self.sf(self.stack.dup,[])
   def cAdd(self): self.binstackf(add, NUMT, NUMT)()
   def cSub(self): self.binstackf(sub, NUMT, NUMT)()
   def cMul(self): self.binstackf(mul, NUMT, NUMT)()
   def cDiv(self): self.binstackf(div, NUMT, NUMT)()

   # shortest representation of value in string
   def v2str(self, n):
      if n=="": return n
      if isinstance(n,str) and (n[0]==' ' or n[-1]==' '): return n
      try: v=str(int(n))
      except:
         try: v=str(float(n))
         except: v=str(n)
      return v

   def cToStr(self): self.unarystackf(self.v2str, NUMT)()
   def cToNum(self): 
      def tonum(s):
         # return a number from the string if possible,
         # if not a valid number then push the string back
         try: n = NUMT(s)
         except ValueError: n = s
         return n
      self.unarystackf(tonum, STRT)()
   def cConcatenate(self): self.binstackf(add, STRT, STRT)() 
   def cOutput(self):             
      self.unarystackf(
             (lambda s:self.world.out(self.v2str(s)+"\n")),pushresult=False)()
   def cInlineOutput(self): 
      self.unarystackf(
             (lambda s:self.world.out(self.v2str(s))),pushresult=False)()
   def cReadChar(self): 
      self.sf(self.stack.push, [NUMT(self.world.readchar())])
   def cReadLine(self):
      self.sf(self.stack.push, [self.world.readline()])
     
   def cSubstring(self):
      self.tristackf((lambda strn,start,end:strn[int(start):int(end)]),
                             STRT,NUMT,NUMT)()
   def cStrLen(self): self.unarystackf(len, STRT)()
   def cDiscard(self): self.sf(self.stack.pop, [])
   def cCopy(self): self.unarystackf(self.stack.copy, NUMT, False)()
   def cMove(self): self.unarystackf(self.stack.move, NUMT, False)()
   def cStackSize(self):
      self.sf(self.stack.push, NUMT(len(self.stack)))
   
   ## set 2 ##
   def cGT(self): self.binstackf(gt)()
   def cLT(self): self.binstackf(lt)()
   def cSkip(self):
      def skip(n): self.ip += int(n)
      self.unarystackf(skip,NUMT,False)()
   def cSkipTwo(self):
      def skip2(n): self.ip += int(n*2)
      self.unarystackf(skip2,NUMT,False)()
   def cInsert(self):
      def insert(thing, where): self.stack.insert(where, thing)
      self.binstackf(insert, btype=NUMT, pushresult=False)()
   def cAnd(self): self.binstackf(lambda a,b:NUMT(a and b and 1 or 0))()
   def cOr(self): self.binstackf(lambda a,b:NUMT((a or b) and 1 or 0))()
   def cNot(self): self.unarystackf(lambda a:NUMT(not a and 1 or 0))()
   
  
   def execstr(self, codestr):
      self.world.recurse(self, codestr)

   def cExec(self): self.unarystackf(self.execstr,STRT,False)()
   def cWhile(self):
      while True:
         # pop a value
         val = self.sf(self.stack.pop,[])
         if val: 
            # pop code, run it
            code = self.sf(self.stack.pop,[])
            if not isinstance(code, STRT):
               self.raiseTypeErr(STRT, type(code))
            else:
               self.execstr(code)
         else:
            break
   def cEqual(self): self.binstackf(eq)()
   def cLshift(self): self.binstackf((lambda a,b:int(a)<<int(b)),NUMT,NUMT)()
   def cRshift(self): self.binstackf((lambda a,b:int(a)>>int(b)),NUMT,NUMT)()
   
   ## set 3 ##
  
   def cQuit(self): self.world.quit()
   def cRecallWhile(self):
      # pop code
      code = self.sf(self.stack.pop,[])
      if not isinstance(code, STRT): self.raiseTypeErr(STRT, type(code))
      # while
      while self.sf(self.stack.pop,[]):
         self.execstr(code)
   
   def cIsNumber(self):
      self.unarystackf(lambda v: isinstance(v,NUMT) and 1 or 0)()
   def cIsString(self):
      self.unarystackf(lambda v: isinstance(v,STRT) and 1 or 0)()
   # binary and/or use integers again
   def binop(self,op): return lambda a,b: op(int(a), int(b))
   def cBinAnd(self): self.binstackf(self.binop(and_), NUMT, NUMT)()
   def cBinOr(self): self.binstackf(self.binop(or_), NUMT, NUMT)()
   def cInteger(self): self.unarystackf(int, NUMT)()
   def cMod(self): self.binstackf(mod, NUMT, NUMT)()
   def cToChar(self): self.unarystackf((lambda k:chr(int(k)%256)), NUMT)()
   def cCharAt(self): 
      def charat(strn, idx):
         idx=int(idx)
         if (idx<0 or idx>=len(strn)):
            # nowhere in the spec it says python-style negative indexes
            # are allowed...
            raise Interpreter.CodeError(self.err(
                   ("index out of bounds: tried to get %d-char string's " +\
                    "%d%s character") % (len(strn), idx, ordsuffix(idx)) ))
         return NUMT(ord(strn[idx]))
      self.binstackf(charat, STRT, NUMT)()
   def cReplaceChar(self):
      def replacechar(strn, idx, repl):
         idx=int(idx)
         if (idx<0 or idx>=len(strn)):
            raise Interpreter.CodeError(self.err(
                   ("index out of bounds: tried to change %d-char string's "+\
                    "%d%s character") % (len(strn), idx, ordsuffix(idx)) ))
         return strn[:idx] + repl[0] + strn[idx+1:]
      self.tristackf(replacechar, STRT, NUMT, STRT)()
   def cInvertedCopy(self):
      self.unarystackf(self.stack.invcopy, NUMT, False)()
   def cInvertedMove(self):
      self.unarystackf(self.stack.invmove, NUMT, False)()
   def cSwap(self): self.sf(self.stack.swap, [])
   def cSwap2(self): self.sf(self.stack.swap2, [])
   def cSwap3(self): self.sf(self.stack.swap3, [])


     
# class World
# handles stepping of interpreter and input and output
#   World().quit() -> ends program
#   World().out("blah") -> stdout.write("blah")
#   World().readchar() -> read 1 char from stdin
#   World().readline() -> read 1 line from stdin

class World:
   interpreter = None

   def __init__(self, codestr, prevint=None):
      # parse the code
       
      self.code = Parser.parse(codestr)
      
  
      if prevint:
         self.interpreter = Interpreter( self, self.code, 
                                         parent=prevint,
                                         stack=prevint.stack,
                                         activeset=prevint.activeset )
      else:
         self.interpreter = Interpreter( self, self.code )
   
   # run the interpreter until it fails or quits
   def run(self):
      
      while self.interpreter.step(): pass
     

   # the interpreter calls these for program control
   def quit(self):
      sys.exit(0)
   
   def out(self, string):
      sys.stdout.write(string)

   def readchar(self):
      v = sys.stdin.read(1)
      if not v: return -1
      else: return ord(v)

   def readline(self):
      return sys.stdin.readline()

   def recurse(self, prevint, codestr):
      try:
         # run the sub-interpreter in its own world
         w = World(codestr, prevint=prevint)
         w.run()
      except ValueError, complaint:
         # parsing failed
         raise Interpreter.CodeError(prevint.err(
                  "exec: parsing of string failed: \n\t%s\n" % complaint))
      except Interpreter.CodeError, complaint:
         # run-time error in code
         raise Interpreter.CodeError(prevint.err(
                  "exec: sub-interpreter runtime error: \n\t%s\n" % complaint))
      except Exception, complaint:
         # something else went wrong
         raise Interpreter.CodeError(prevint.err(
                  "exec: sub-interpreter failed: %s" % str(complaint)))


###############################################################

# start the program
def main(argv):
   global trace
   if (not len(argv) in (2,3)) or (len(argv)==3 and \
                      (not argv[1] == '-trace')): 
      print "usage: %s [-trace] filename | -" % argv[0]
      sys.exit(2)
   else:
      if argv[1]=='-trace': 
         trace=True
         fname = argv[2]
      else:
         trace = False
         fname = argv[1]
      try:
         if fname=='-': f = sys.stdin
         else: f = file(fname, 'r')
      except:
         print "Can't open file %s" % argv[1]
         sys.exit(3)
      code = f.read()
      f.close()
         
      try:
         w = World(code)
         w.run()
      except Interpreter.CodeError, complaint:
         print "Run-time error: %s" % complaint
      except ValueError, complaint:
         print "Parse error: %s" % complaint
      except Exception, complaint:
         print "An exception occured: %s" % complaint

if __name__=="__main__": main(sys.argv)