User:Marinus/LOCK interpreter

From Esolang
Jump to: navigation, search

This is a LOCK interpreter, implementing the specification as of 22 May 2010.

There are a few extra options for I/O for testing purposes.

-c ASCII (Brainfuck-style) I/O instead of numbers.
-r If the user enters something that is not a valid number, let the user retry instead of handling it as a failed I/O operation.
-f x Have failed input operations return the value x (must be an integer), the default is 0
-F Halt the program when I/O fails instead of returning a value
#!/usr/bin/env python

import sys, getopt

# lock.py
# lock interpreter

class ParseError(Exception): pass
class ProgramError(Exception): pass

class Block:
   def __init__(self, keys, code):
      self.keys = keys[:]
      self.code = code[:]
   
   # run the code in the block if possible
   # returns False if block can't run
   def run(self, env):
      cmds = { '>': env.addkey,
               '<': env.delkey,
               '?': env.condaddkey,
               '+': env.add,
               '-': env.sub,
               '*': env.mul,
               '/': env.div,
               '%': env.mod,
               '=': env.setvar,
               '^': env.setaccv,
               ':': env.readint,
               '$': env.outvar,
               '#': env.setaccn,
               '&': lambda foo: 0
      }
      
      # don't run if not all keys are present
      if False in [key in env.keys for key in self.keys]:
         return False
      
      for line in self.code:
         if line[0] in cmds:
            # it's a command
            try: cmds[line[0]](line[1:])
            except ZeroDivisionError:
                raise ProgramError("division by zero");
         else:
            # it's a string to be output
            env.outstr(line)
      
      return True

class Environment:

   # intio = read in and write out numbers (false=ASCII)
   # retry = if intio, have user retry inputting if input not a number
   # onfail = returned as read if read fails (None=stop program)
   
   def __init__(self, blocks, intio=True, retry=False, onfail=0):
      self.blocks = blocks
      self.keys = set(('main',))
      self.vars = {}
      self.acc = 0
      
      
      self.input, self.output = self.iofuncs(intio, retry, onfail)
      
   def iofuncs(self, intio, retry, onfail):
      if intio:
         # functions for integer input/output
         def i():
            while True:
               l = sys.stdin.readline()
               if l=='': # eof
                  if onfail!=None: return onfail
                  else: raise ProgramError("Read failed: end of file")
               try:
                  a = int(l)
                  return a
               except ValueError:
                  if onfail!=None: return onfail
                  if retry:
                     print "%s is not a number, please input a number:"%s ,
                  else:
                     raise ProgramError("Read failed: not a number")
         def o(val):
            sys.stdout.write(str(val))
      else:
         # functions for ASCII input/output
         def i():
            ch = sys.stdin.read(1)
            if ch == '':
               if onfail!=None: return onfail
               else: raise ProgramError("Read failed: end of file")
            return ord(ch)
            
         def o(val):
            sys.stdout.write(chr(val%256))
            
      return i, o 
      
   def gv(self,var): 
      if var in self.vars: return self.vars[var]
      else: return 0
      
   def sv(self,var,val):
      self.vars[var] = int(val)
      
   def addkey(self, key): self.keys.add(key)
   def delkey(self, key): 
       try: self.keys.remove(key)
       except KeyError: pass # removing a key that isn't there is not an error
   def condaddkey(self, key): self.acc and self.keys.add(key)
   def add(self, var): self.acc += self.gv(var)
   def sub(self, var): self.acc -= self.gv(var)
   def mul(self, var): self.acc *= self.gv(var)
   def div(self, var): self.acc /= self.gv(var)
   def mod(self, var): self.acc %= self.gv(var)
   def setvar(self, var): self.sv(var, self.acc)
   def setaccv(self, var): self.acc = self.gv(var)
   def readint(self, var): self.sv(var, self.input())
   def outvar(self, var): self.output(self.gv(var))
   def setaccn(self, num): 
      try:
         self.acc = int(num)
      except ValueError:
         raise ProgramError("Error: not a number: %s" % num)
   def outstr(self, str):
      sys.stdout.write(str.replace(r'\n',"\n")
                          .replace(r'\t',"\t")
                          .replace(r'\\',"\\")
                          .replace(r'\ ',' '))
      
   def run(self):
      t = True
      while t:
         for block in self.blocks:
            t = block.run(self)
            if t: break
      

def parse(code):
   lines = [line.lstrip() for line in code.split("\n")]
   
   blocks = []
   
   curkeys = None
   curcode = None
   curline = 0
   inblock = False
   for line in lines:
      curline += 1
      if line == '': continue
      
      if line[0] == '{':
         # new block
         if inblock:
            raise ParseError("line %d: nested blocks are not allowed"%curline)
         if line[1:] == '':
            raise ParseError("line %d: missing keys after {"%curline)
         curkeys = [key.strip() for key in line[1:].split(',')]
         curcode = []
         inblock = True
      elif line[0] == '}':
         # end of block
         if not inblock:
            raise ParseError("line %d: unbalanced }"%curline)
         blocks.append(Block(curkeys, curcode))
         inblock = False
      else:
         if not inblock:
            raise ParseError("line %d: code not allowed outside of block"%curline)
         curcode.append(line)
   
   if inblock:
      # block wasn't closed
      raise ParseError("unbalanced {");
   
   return blocks

def usage():
   print """Usage: lock.py [-h] [-c | -r] [-f x | -F] file
     -h: print this help
     -c: use ASCII i/o (Brainfuck-style) instead of integer i/o 
     -r: in integer mode, let the user retry if something other
         than a number is input
     -f x: return x on read failure (EOF or, if -r is not given,
         when the user inputs something that can't be parsed into
         an integer.)
         Default = 0.
     -F: halt program on read failure instead of returning a default
         value
"""

def main(argv):
   try:
      opts, args = getopt.getopt(argv[1:], "hcrFf:")
   except getopt.GetoptError, e:
      print e
      usage()
      sys.exit()
   
   onfail = 0
   retry = False
   intio = True
   
   for o, a in opts:
      if o == '-h':
         usage()
         sys.exit()
      elif o == '-c':
         intio = False
      elif o == '-r':
         retry = True
      elif o == '-F':
         onfail = None
      elif o == '-f':
         try:
            onfail = int(a)
         except ValueError:
            print "error: argument to -f must be a number."

   if len(args) != 1:
      print "argument error."
      usage()
      sys.exit()
   
   try:
      Environment(parse(file(args[0]).read()), intio, retry, onfail).run()
   except ParseError, e:
      print "parse error:", e
   except ProgramError, e:
      print "runtime error:", e
   except IOError, e:
      print "can't read file:", e
      
if __name__=='__main__': main(sys.argv)