def isbool(*args): """Checks if value is boolean.""" true_or_false = [ instructions.lookup(instructions.true_), instructions.lookup(instructions.false_) ] return all(map(lambda c: isinstance(c, bool) or c in true_or_false, args))
def to_bool(instr): if isinstance(instr, bool): return instr elif instr == instructions.lookup(instructions.true_): return True elif instr == instructions.lookup(instructions.false_): return False else: raise CompileError("Unknown instruction: %s" % instr)
def check(code): """Checks code for obvious errors.""" def safe_lookup(op): try: return instructions.lookup(op) except Exception: return op for i, a in enumerate(code): b = code[i + 1] if i + 1 < len(code) else None # Does instruction exist? if not isconstant(a): try: instructions.lookup(a) except KeyError as err: # Skip embedded push closures if not (len(err.args) == 1 and is_embedded_push(err.args[0])): raise CompileError( "Instruction at index %d is unknown: %s" % (i, a)) # Invalid: <str> int if isstring(a) and safe_lookup(b) == instructions.cast_int: raise CompileError( "Cannot convert string to integer (index %d): %s %s" % (i, a, b)) # Invalid: <int> <binary op> boolean_ops = [ instructions.boolean_not, instructions.boolean_or, instructions.boolean_and ] if not isbool(a) and safe_lookup(b) in boolean_ops: raise CompileError( "Can only use binary operators on booleans (index %d): %s %s" % (i, a, b)) return code
def code_to_string(code): from crianza import compiler s = [] for op in code: if isconstant(op): if isstring(op): s.append(repr(op)) else: s.append(str(op)) elif compiler.is_embedded_push(op): v = compiler.get_embedded_push_value(op) s.append('"%s"' % repr(v)[1:-1] if isinstance(v, str) else repr(v)) else: s.append(str(instructions.lookup(op))) return " ".join(s)
def native_types(code): """Convert code elements from strings to native Python types.""" out = [] for c in code: if isconstant(c, quoted=True): if isstring(c, quoted=True): v = c[1:-1] elif isbool(c): v = to_bool(c) elif isnumber(c): v = c else: raise CompileError("Unknown type %s: %s" % (type(c).__name__, c)) # Instead of pushing constants in the code, we always push callable # Python functions, for fast dispatching: out.append(make_embedded_push(v)) else: try: out.append(instructions.lookup(c)) except KeyError: raise CompileError("Unknown word '%s'" % c) return out
"""Compiles to native Python bytecode and runs program, returning the topmost value on the stack. Args: optimize: Whether to optimize the code after parsing it. Returns: None: If the stack is empty obj: If the stack contains a single value [obj, obj, ...]: If the stack contains many values """ native = xcompile(source, optimize=optimize) return native() opmap = { cr.lookup("%"): mod, cr.lookup("&"): bitwise_and, cr.lookup("*"): mul, cr.lookup("+"): add, cr.lookup("-"): sub, cr.lookup("."): dot, cr.lookup("/"): div, cr.lookup("<"): less, cr.lookup("<="): greater_equal, cr.lookup("<>"): not_equal, cr.lookup("="): equal, cr.lookup(">"): greater, cr.lookup(">="): greater_equal, cr.lookup("@"): at, cr.lookup("^"): bitwise_xor, cr.lookup("abs"): abs_,
topmost value on the stack. Args: optimize: Whether to optimize the code after parsing it. Returns: None: If the stack is empty obj: If the stack contains a single value [obj, obj, ...]: If the stack contains many values """ native = xcompile(source, optimize=optimize) return native() opmap = { cr.lookup("%"): mod, cr.lookup("&"): bitwise_and, cr.lookup("*"): mul, cr.lookup("+"): add, cr.lookup("-"): sub, cr.lookup("."): dot, cr.lookup("/"): div, cr.lookup("<"): less, cr.lookup("<="): greater_equal, cr.lookup("<>"): not_equal, cr.lookup("="): equal, cr.lookup(">"): greater, cr.lookup(">="): greater_equal, cr.lookup("@"): at, cr.lookup("^"): bitwise_xor, cr.lookup("abs"): abs_,
def compile(code, silent=True, ignore_errors=False, optimize=True): """Compiles subroutine-forms into a complete working code. A program such as: : sub1 <sub1 code ...> ; : sub2 <sub2 code ...> ; sub1 foo sub2 bar is compiled into: <sub1 address> call foo <sub2 address> call exit <sub1 code ...> return <sub2 code ...> return Optimizations are first done on subroutine bodies, then on the main loop and finally, symbols are resolved (i.e., placeholders for subroutine addresses are replaced with actual addresses). Args: silent: If set to False, will print optimization messages. ignore_errors: Only applies to the optimization engine, if set to False it will not raise any exceptions. The actual compilatio will still raise errors. optimize: Flag to control whether to optimize code. Raises: CompilationError - Raised if invalid code is detected. Returns: An array of code that can be run by a Machine. Typically, you want to pass this to a Machine without doing optimizations. Usage: source = parse("<source code>") code = compile(source) machine = Machine(code, optimize=False) machine.run() """ assert (isinstance(code, list)) output = [] subroutine = {} builtins = Machine([]).instructions # Gather up subroutines try: it = code.__iter__() while True: word = next(it) if word == ":": name = next(it) if name in builtins: raise CompileError( "Cannot shadow internal word definition '%s'." % name) if name in [":", ";"]: raise CompileError("Invalid word name '%s'." % name) subroutine[name] = [] while True: op = next(it) if op == ";": subroutine[name].append( instructions.lookup(instructions.return_)) break else: subroutine[name].append(op) else: output.append(word) except StopIteration: pass # Expand all subroutine words to ["<name>", "call"] for name, code in subroutine.items(): # For subroutines xcode = [] for op in code: xcode.append(op) if op in subroutine: xcode.append(instructions.lookup(instructions.call)) subroutine[name] = xcode # Compile main code (code outside of subroutines) xcode = [] for op in output: xcode.append(op) if op in subroutine: xcode.append(instructions.lookup(instructions.call)) # Because main code comes before subroutines, we need to explicitly add an # exit instruction output = xcode if len(subroutine) > 0: output += [instructions.lookup(instructions.exit)] # Optimize main code if optimize: output = optimizer.optimized(output, silent=silent, ignore_errors=False) # Add subroutines to output, track their locations location = {} for name, code in subroutine.items(): location[name] = len(output) if optimize: output += optimizer.optimized(code, silent=silent, ignore_errors=False) else: output += code # Resolve all subroutine references for i, op in enumerate(output): if op in location: output[i] = location[op] output = native_types(output) if not ignore_errors: check(output) return output
def safe_lookup(op): try: return instructions.lookup(op) except Exception: return op
def lookup(self, instruction): """Looks up name-to-function or function-to-name.""" return instructions.lookup(instruction, self.instructions)
def isfunction(op): try: instructions.lookup(op) return True except KeyError: return False
def constant_fold(code, silent=True, ignore_errors=True): """Constant-folds simple expressions like 2 3 + to 5. Args: code: Code in non-native types. silent: Flag that controls whether to print optimizations made. ignore_errors: Whether to raise exceptions on found errors. """ # Loop until we haven't done any optimizations. E.g., "2 3 + 5 *" will be # optimized to "5 5 *" and in the next iteration to 25. Yes, this is # extremely slow, big-O wise. We'll fix that some other time. (TODO) arithmetic = list( map(instructions.lookup, [ instructions.add, instructions.bitwise_and, instructions.bitwise_or, instructions.bitwise_xor, instructions.div, instructions.equal, instructions.greater, instructions.less, instructions.mod, instructions.mul, instructions.sub, ])) divzero = map(instructions.lookup, [ instructions.div, instructions.mod, ]) lookup = instructions.lookup def isfunction(op): try: instructions.lookup(op) return True except KeyError: return False def isconstant(op): return op is None or interpreter.isconstant( op, quoted=True) or not isfunction(op) keep_running = True while keep_running: keep_running = False # Find two consecutive numbes and an arithmetic operator for i, a in enumerate(code): b = code[i + 1] if i + 1 < len(code) else None c = code[i + 2] if i + 2 < len(code) else None # Constant fold arithmetic operations (TODO: Move to check-func) if interpreter.isnumber(a, b) and c in arithmetic: # Although we can detect division by zero at compile time, we # don't report it here, because the surrounding system doesn't # handle that very well. So just leave it for now. (NOTE: If # we had an "error" instruction, we could actually transform # the expression to an error, or exit instruction perhaps) if b == 0 and c in divzero: if ignore_errors: continue else: raise errors.CompileError( ZeroDivisionError("Division by zero")) # Calculate result by running on a machine (lambda vm: ... is # embedded pushes, see compiler) result = interpreter.Machine([ lambda vm: vm.push(a), lambda vm: vm.push(b), instructions.lookup(c) ]).run().top del code[i:i + 3] code.insert(i, result) if not silent: print("Optimizer: Constant-folded %s %s %s to %s" % (a, b, c, result)) keep_running = True break # Translate <constant> dup to <constant> <constant> if isconstant(a) and b == lookup(instructions.dup): code[i + 1] = a if not silent: print("Optimizer: Translated %s %s to %s %s" % (a, b, a, a)) keep_running = True break # Dead code removal: <constant> drop if isconstant(a) and b == lookup(instructions.drop): del code[i:i + 2] if not silent: print("Optimizer: Removed dead code %s %s" % (a, b)) keep_running = True break if a == lookup(instructions.nop): del code[i] if not silent: print("Optimizer: Removed dead code %s" % a) keep_running = True break # Dead code removal: <integer> cast_int if isinstance(a, int) and b == lookup(instructions.cast_int): del code[i + 1] if not silent: print("Optimizer: Translated %s %s to %s" % (a, b, a)) keep_running = True break # Dead code removal: <float> cast_float if isinstance(a, float) and b == lookup(instructions.cast_float): del code[i + 1] if not silent: print("Optimizer: Translated %s %s to %s" % (a, b, a)) keep_running = True break # Dead code removal: <string> cast_str if isinstance(a, str) and b == lookup(instructions.cast_str): del code[i + 1] if not silent: print("Optimizer: Translated %s %s to %s" % (a, b, a)) keep_running = True break # Dead code removal: <boolean> cast_bool if isinstance(a, bool) and b == lookup(instructions.cast_bool): del code[i + 1] if not silent: print("Optimizer: Translated %s %s to %s" % (a, b, a)) keep_running = True break # <c1> <c2> swap -> <c2> <c1> if isconstant(a) and isconstant(b) and c == lookup( instructions.swap): del code[i:i + 3] code = code[:i] + [b, a] + code[i:] if not silent: print("Optimizer: Translated %s %s %s to %s %s" % (a, b, c, b, a)) keep_running = True break # a b over -> a b a if isconstant(a) and isconstant(b) and c == lookup( instructions.over): code[i + 2] = a if not silent: print("Optimizer: Translated %s %s %s to %s %s %s" % (a, b, c, a, b, a)) keep_running = True break # "123" cast_int -> 123 if interpreter.isstring(a) and b == lookup(instructions.cast_int): try: number = int(a) del code[i:i + 2] code.insert(i, number) if not silent: print("Optimizer: Translated %s %s to %s" % (a, b, number)) keep_running = True break except ValueError: pass if isconstant(a) and b == lookup(instructions.cast_str): del code[i:i + 2] code.insert(i, str(a)) # TODO: Try-except here if not silent: print("Optimizer: Translated %s %s to %s" % (a, b, str(a))) keep_running = True break if isconstant(a) and b == lookup(instructions.cast_bool): del code[i:i + 2] code.insert(i, bool(a)) # TODO: Try-except here if not silent: print("Optimizer: Translated %s %s to %s" % (a, b, bool(a))) keep_running = True break if isconstant(a) and b == lookup(instructions.cast_float): try: v = float(a) del code[i:i + 2] code.insert(i, v) if not silent: print("Optimizer: Translated %s %s to %s" % (a, b, v)) keep_running = True break except ValueError: pass return code