#!/usr/bin/env python """ interpreter.py - Pluvo Interpreter Author: Sean B. Palmer, inamidst.com """ import sys, re from datatypes import Table, Variable, Symbol, String, Number, Regexp, URI r_name = re.compile(r'(?:(?:\A|(?<=\W))\$([A-Za-z]+))|(?:\$\{([A-Za-z]+)\})') class Interpreter(object): """Take a compiled Pluvo program and run it. >>> i = Interpreter(program) >>> i.run() """ def __init__(self, program, verbose=False): # @@ Check that everything in the program, keys, values, and items, are # valid Pluvo datatypes, i.e. not Python specific ones. self.program = program self.verbose = verbose self.stack = [] def root(self): """Return the outermost scope, the root program block.""" if self.stack: return self.stack[0] return None def stem(self): """Return the second outermost scope, the first level of nesting.""" if len(self.stack) > 1: return self.stack[1] return None def current(self): """Return the current scope.""" if self.stack: return self.stack[-1] return None def varbind(self, block, name, condition, value): """Bind name in block's variables to value with condition. This is what actually does the binding so that we can have the dynamic typing. It checks for any strict conditions, and tests them on the new value if they exist. """ variables = block[String("variables")] if variables.has_key(name): # print name, 'exists' oldcond, oldval = variables[name] # print 'oldcond:', oldcond if condition is None: condition = oldcond elif oldcond is not True: raise ValueError("Not yet implemented") elif condition is None: condition = True if condition in (String, Number): if not isinstance(value, condition): typename = {String:'String', Number:'Number'}[condition] print "Error: expected %s when setting %s" % (typename, name) sys.exit(1) elif condition is not True: raise ValueError("Not yet implemented: %s" % condition) # print ('setting condition for %s:' % name), condition variables[name] = (condition, value) def bind(self, name, value, scope=None, condition=None): """Bind name, a raw string, to value, an object of any kind. If the name exists in a parent scope, bind it in that scope; otherwise if scope is given bind it in that scope, otherwise bind it in root, the outermost scope. The optional condition specifies a condition that the value must pass, i.e. strict typing. This only applies to user space variables and is ignored if passed with program space variables. """ # Handle the simple case first if name.startswith('@'): root = self.root() # @@! root[String("basics")][String(name[1:])] = value root[String("basics")][name[1:]] = value return elif name.startswith('$'): name = name[1:] higher = False for block in self.stack: if block.has_key(String("variables")): if block[String("variables")].has_key(name): if not higher: # block[String("variables")][name] = value # print 'VAR A' self.varbind(block, name, condition, value) higher = True else: del block[String("variables")][name] if not higher: # if True: if scope is None: # If we're not given an explicit scope, do a Pythonic scope if self.current() == self.root(): scope = self.root() else: scope = self.stem() setvalue = False if scope is not None: if scope.has_key(String("variables")): # scope[String("variables")][name] = value # print 'VAR B' self.varbind(scope, name, condition, value) else: vars = Table({"variables": Table()}) scope[String("variables")] = vars # scope[String("variables")][name] = value # print 'VAR C' self.varbind(scope, name, condition, value) setvalue = True if (setvalue is False) and self.stack: root = self.root() # root[String("variables")][name] = value # print 'VAR D' self.varbind(root, name, condition, value) def lookup(self, name): # @@ Path should be dependent on @path itself # Of course, that means a recursive lookup at the moment # so it needs to be separated into lookupBasic etc. # The Pluvo standard library prefix is "%". # User libraries/modules could be referenced with "$"; that way one could # still use $-less variables for barenames without fear of a long and # expensive lookup. The modules wouldn't be copied to closure variables. # if name[0] in '$@': sigil, name = name[0], name[1:] else: sigil = '' if sigil == '': # We look at $ first, then @. We don't import library for block in reversed(self.stack): if block.has_key(String("variables")): variables = block[String("variables")] if variables.has_key(name): return variables[name][1] basics = self.program[String("basics")] if basics.has_key(name): return basics[name] elif sigil == '$': for block in reversed(self.stack): if block.has_key(String("variables")): variables = block[String("variables")] if variables.has_key(name): return variables[name][1] elif sigil == '@': basics = self.program[String("basics")] if basics.has_key(name): return basics[name] import library return library.library(name) return Symbol(name) def run(self): self.evaluate(self.program) def evaluate(self, function, args=None): # Look through the argspec for symbols, and assign their values # in locals to the respective args. This provides dynamic scoping. # # function = function.copy() # @@ argspecs args should be resolved by this stage. They aren't yet # Actually, they can be resolved with the other stuff, in perform, which # is probably just as well given that we want to get varnames etc. # argspec = function.get(String('argspec')) if argspec and args: if not function.has_key(String("variables")): function[String("variables")] = Table() for var, value in zip(argspec, args): if hasattr(var, 'name'): # isinstance(var, Variable): self.varbind(function, var.name, None, value) elif isinstance(var, Table) and var.get('kind') == 'Function': var = var[0][0] # @@ assert length, etc. if hasattr(var, 'name'): self.varbind(function, var.name, None, value) else: raise ValueError("Argspec args must have names") self.stack.append(function) for command in function: result, status, control = self.perform(command) function[String("status")] = status function[String("result")] = result function[String("control")] = control # @@ result, status, control # if control == "return/exit": break # rather... # if function.get("control") == "return/exit": # break # otherwise, assign, then check again? if control == "return/exit": break self.stack.pop() return function[String("result")] def resolve(self, arg): # @@ Functions should be able to decline lookup, interpolation, etc. if isinstance(arg, Variable): resolved = self.lookup(arg.name) try: resolved.name = arg.name except AttributeError: pass # Argh, None etc. won't let us return resolved # String interpolation elif isinstance(arg, String): def interpolate(m): name = m.group(1) or m.group(2) try: return str(self.lookup(name)) except: return name itpl = String(r_name.sub(interpolate, str(arg))) return itpl # Regexp interpolation elif isinstance(arg, Regexp): def interpolate(m): name = m.group(1) or m.group(2) try: return str(self.lookup(name)) except: return name itpl = Regexp(r_name.sub(interpolate, str(arg.pattern))) return itpl # URI interpolation elif isinstance(arg, URI): def interpolate(m): name = m.group(1) or m.group(2) try: return str(self.lookup(name)) except: return name itpl = URI(r_name.sub(interpolate, str(arg.uri))) return itpl # Anonymous closure initialisation # # Cf. "Correct implementation of static scope in languages with # first-class nested functions can be subtle, as it requires each # function value to carry with it a record of the values of the # variables that it depends on (the pair of the function and this # environment is called a closure)." # - http://en.wikipedia.org/wiki/Scope_%28programming%29 # elif isinstance(arg, Table) and (arg.get('kind') == 'Function'): # Cf. basics.equals # if hasattr(arg, 'name'): continue for block in self.stack: if block.has_key(String("variables")): for (key, value) in block[String("variables")].iteritems(): # @@ These should be resolved properly! self.varbind(arg, key, None, value[1]) return arg # Resolve table args, recursively elif isinstance(arg, Table) and (not hasattr(arg, 'name')): new = Table([]) skip = 0 for i, oldarg in enumerate(arg): if skip: skip = skip - 1 continue newarg = self.resolve(oldarg) if ((i <= (len(arg) - 2)) and hasattr(arg[i+1], 'name') and (arg[i+1].name == u'=')): new[newarg] = self.resolve(arg[i+2]) skip = 2 else: new.append(newarg) return new return arg def perform(self, command, argspec=None): if self.verbose: print >> sys.stderr, 'EVAL:', command # We have a command, containing variables and other things. Look up all # of the variables that we need to, and add everything to the argv list. # The varname of resolved is saved in resolved.name for safekeeping. # argv = [] for arg in command: res = self.resolve(arg) argv.append(res) function, args = self.coordinate(argv) if hasattr(function, '__call__'): retval = function(self, *args) else: # Automatically run block arguments to Pluvo functions # @@ Unless the arg is {escaped} in the argspec rargs = [] argspec = function[String("argspec")] for i, arg in enumerate(args): if isinstance(arg, Table) and (arg.get('kind') == 'Function'): if (argspec is None) or hasattr(argspec[i], 'name'): rargs.append(self.evaluate(arg)) else: rargs.append(arg) else: rargs.append(arg) retval = self.evaluate(function, tuple(rargs)) # Return a (result, status, control) tuple if isinstance(retval, tuple): return { 1: retval + (String(""), String("")), 2: retval + (String(""),), 3: retval }[len(retval)] else: return (retval, String(""), String("")) def coordinate(self, argv): # Coordination Happens Here # # Coordination is the process of finding which argument in a command line # to use as the actual command itself. Usually a language requires it to # be the first in the command. But say that we have the following: # # "." join ("p" "q" "r") # # It's obvious what it should do. So with coordination we look through # the args until we find a callable one, then we use that as the # callable. Quite simple really. # # Another feature is that functions can decline coordination if the # arguments being passed do not meet their argspec. So given the # following: # # def chain (first second) { first; second } # def date () { say "2006-05-13" } # def time () { say "11:31 AM" } # date chain time # # We look along the line and find that date is the first callable. But # its argspec is "()", and it's being passed two arguments. So it # declines to be used, and we go along and find that chain is both # callable and has the right argspec. # for i, arg in enumerate(argv): # First, we force-decline the first argument if the second argument is # =, which you can think of as a global argspec constraint. if len(argv) > 1: if hasattr(argv[1], 'name') and (argv[1].name in ('=', '.')): function = argv[1] args = tuple(argv[:1] + argv[2:]) break # There are two types of callable: builtin Python functions, and # callable blocks (i.e. Functions, or Closures) of Pluvo commands. # # Python functions can't decline, unless they have func.argspec set # Or perhaps func.accept, returning a boolean, would be better if hasattr(arg, '__call__'): function = arg args = tuple(argv[:i] + argv[i+1:]) # Check to see whether it declines or accepts if hasattr(function, 'accept'): if function.accept(args): break else: break elif isinstance(arg, Table): if ((arg.get(String('kind')) == String("Function")) and arg.has_key(String('argspec'))): function = arg args = tuple(argv[:i] + argv[i+1:]) # It's a function, but is its argspec right? # This is a naive check for length at the moment # Eventually argspecs could contain conditions # argspec = arg.get(String('argspec')) if len(argspec) == len(args): break else: msg = "No callable function found: %s" % argv raise ValueError(msg) return function, args def trim(string): return string.strip(' \t\r\n') if __name__ == '__main__': print trim(__doc__)