Source code for curry.backends.py.compiler

from .currylib.prelude.math import apply_unboxed
from .graph.node import check_int_range
from ...exceptions import CompileError
from ..generic import compiler, renderer
from ... import config, icurry
from ...utility.showflags import showflags
import collections, itertools, json, re, sys

__all__ = ['compile', 'write_module', 'FORMAT_VERSION']

# The format of the generated Python.  vEmitHeader writes it into every file
# as "# FORMAT: N".  The toolchain writes a cached file of another stamp, or
# none, again from the JSON file (Json2Py.is_stale): the emitter has changed
# what it writes since then.  A file without a stamp is format 1.  Raise the
# number when the generated code changes.  Format 2: the optimizer replaces
# calls of alias functions (interpreter.optimize.inline_aliases).  Format 5:
# the optimizer collapses an apply chain on a known head into the call it
# builds (interpreter.optimize.saturate_applies), a chain through Prelude.$
# included, and records the unfolding of a nullary function in its metadata;
# a file of format 2 is correct but slower, and it tells the modules that
# import it no unfoldings.  Formats 3 and 4 were earlier forms of the same
# pass in the working tree, never committed.  Format 6: the optimizer inlines
# calls of small non-recursive functions and calls of a single-case function
# on a known constructor (interpreter.optimize.inline_calls), and records the
# body of such a function in its metadata; a file of format 5 is correct but
# slower, and it tells the modules that import it no bodies.  Format 7: the
# arithmetic of Int, the integer divisions and the conversions of a Float to
# Int are the checked primitives of currylib.prelude.math (issues #105 and
# #106); a file of format 6 binds the operators of Python, which wrap no
# value but raise ZeroDivisionError and give a wider integer.
FORMAT_VERSION = 7

def source_file_expr(filename):
  '''
  The Python expression that names the source file of a module in the
  generated file.  A file under the installation is named relative to
  SPRITE_HOME and resolved when the generated file loads, so the file
  carries no path of the machine (see toolchain._filenames.installed_relpath).
  Another file is named as it is.
  '''
  from ...toolchain import _filenames
  relpath = _filenames.installed_relpath(filename)
  if relpath is None:
    return repr(filename)
  return 'curry.config.installed_path(%r)' % relpath

# The nesting at which a subexpression of the generated Python goes into a
# temporary of the step function (CompilerBase.hoist; issue #125): the
# parser of Python refuses more than 200 nested parentheses, a node is one
# call, and 64 leaves room under that limit for the calls that wrap a
# statement and the arguments of a node.
HOIST_DEPTH = 64

[docs] def compile(interp, iobj): compileM = PyCompiler(interp, iobj) return compileM.compile()
class PyCompiler(compiler.CompilerBase): CODE_TYPE = 'Python' HOIST_DEPTH = HOIST_DEPTH SYNTH_TAGS = 'py.boxedfunc', 'py.rawfunc', 'py.unboxedfunc' EXCLUDED_METADATA = set(SYNTH_TAGS + ('py.material',)) def __init__(self, interp, iobj): compiler.CompilerBase.__init__(self, interp, iobj) self.counts = collections.defaultdict(itertools.count) def vIsBuiltin(self, iobj): return False def vIsSynthesized(self, ifun): return any(md in ifun.metadata for md in self.SYNTH_TAGS) def vBackendFunctionKey(self, func): return hex(id(func)) def vEmitHeader(self): curry = config.python_package_name() yield '# FORMAT: %d' % FORMAT_VERSION yield 'import %s' % curry yield 'from %s.common import *' % curry yield 'from %s.backends.py.graph import DataType, InfoTable' % curry yield 'from %s.backends.py.currylib.prelude.string import codepoints' % curry if self.root_isa_module: yield 'from %s.icurry import IModule, PUBLIC, PRIVATE' % curry yield "if 'interp' not in globals():" yield " interp = %s.getInterpreter()" % curry def vEmitFooter(self): return [] def vEmitImported(self, modulename): yield 'interp.import_(%r)' % modulename def vEmitStepfuncLink(self, ifun, h_stepfunc): return [] def vEmitInfotabLink(self, isym, h_info): if self.isExternal(isym): yield '%s = interp.symbol(%r).info' % (h_info, isym.fullname) def vEmitDataTypeLink(self, itype, h_datatype): if self.isExternal(itype): yield '%s = interp.type(%r).datatype' % (h_datatype, itype.fullname) def vEmitStepfuncHeader(self, ifun, h_stepfunc): yield 'def %s(rts, _0):' % h_stepfunc, ifun.fullname def vEmitStepfuncEntry(self): return [] def vEmitHoisted(self, name, text): yield '%s = %s' % (name, text) def vEmitImportBackendFunction(self, func, h_func): yield 'from %s import %s as %s' % (func.__module__, func.__name__, h_func) def vEmitSynthesizedStepfunc(self, ibuiltin, h_stepfunc): if 'py.boxedfunc' in ibuiltin.metadata: boxedfunc = ibuiltin.metadata['py.boxedfunc'] assert importable(boxedfunc) h_func = self.internBackendFunction(boxedfunc) yield 'args = (rts.variable(_0, i).hnf() for i in range(len(_0.successors)))' yield '_0.rewrite(%s(rts, *args))' % h_func elif 'py.rawfunc' in ibuiltin.metadata: rawfunc = ibuiltin.metadata['py.rawfunc'] assert importable(rawfunc) h_func = self.internBackendFunction(rawfunc) yield 'rts.Node(%s(rts, _0), target=_0.target)' % h_func elif 'py.unboxedfunc' in ibuiltin.metadata: unboxedfunc = ibuiltin.metadata['py.unboxedfunc'] assert importable(unboxedfunc) h_func = self.internBackendFunction(unboxedfunc) h_apply = self.internBackendFunction(apply_unboxed) yield 'return %s(rts, %s, _0)' % (h_apply, h_func) else: raise CompileError('no built-in Python definition found') def vEmitFunctionInfotab(self, ifun, h_info, h_stepfunc): yield '%s = InfoTable(%r, %r, T_FUNC, %s, %s, %r)' % ( h_info, ifun.name, ifun.arity , showflags(ifun.metadata.get('all.flags', 0)) , h_stepfunc , getattr(ifun.metadata, 'py.format', None) ) def vEmitConstructorInfotab(self, ictor, h_info, h_datatype): builtin = 'all.tag' in ictor.metadata yield '%s = InfoTable(%r, %r, %r, %s, None, %r)' % ( h_info, ictor.name, ictor.arity , ictor.index if not builtin else ictor.metadata['all.tag'] , showflags(ictor.metadata.get('all.flags', 0)) , getattr(ictor.metadata, 'py.format', None) ) def vEmitDataType(self, itype, h_datatype, ctor_handles): if self.root_isa_module: yield '%s = DataType(%r, [%s])' % ( h_datatype, itype.name, ', '.join(str(h) for h in ctor_handles) ) def vEmitStringLiteral(self, string, h_string): # The code points of the literal, in the form _biString walks. The # literal is spelled with ASCII escapes, so the module file stays ASCII. yield '%s = codepoints(%s)' % (h_string, ascii(string)) def vEmitValueSetLiteral(self, values, h_valueset, h_valueset_data): yield '%s = %r' % (h_valueset, values) def vEmitMetadata(self, metadata, h_md): yield '%s = %r' % (h_md, metadata) def vEmitModuleDefinition(self, imodule, h_module): py = renderer.PY_RENDERER def _close(level, string): return (2 * level + 1) * py.INDENT * ' ' + string yield '%s = IModule.fromBOM(' % h_module yield ' fullname=%r' % imodule.fullname yield ' , filename=%s' % source_file_expr(imodule.filename) yield ' , imports=%s' % repr(imodule.imports) yield ' , metadata=%s' % self.internMetadata(imodule.metadata) yield ' , mdkey=%r' % 'py.material' yield ' , aliases=%r' % imodule.aliases if not imodule.types: yield ' , types=[]' else: yield ' , types=[' types = imodule.types.values() for prefix, itype in py.prettylist(types, level=1): h_type = self.vGetSymbolName(itype, compiler.DATA_TYPE) type_md = self.internMetadata(itype.metadata) ctor_mds = tuple( self.internMetadata(ictor.metadata) for ictor in itype.constructors ) yield '%s(%s, [%s], %s)' % (prefix, type_md, ', '.join(ctor_mds), h_type) yield _close(1, ']') if not imodule.functions: yield ' , functions=[]' else: yield ' , functions=[' functions = imodule.functions.values() for prefix, ifun in py.prettylist(functions, level=1): vis = 'PRIVATE' if ifun.is_private else 'PUBLIC ' h_info = self.vGetSymbolName(ifun, compiler.INFO_TABLE) h_md = self.internMetadata(ifun.metadata) yield '%s(%s, %s, %s)' % (prefix, vis, h_md, h_info) yield _close(1, ']') yield _close(0, ')') def vEmitModuleImport(self, imodule, h_module): yield '_module_ = interp.import_(%s)' % h_module def vEmit_compileS_IVarDecl(self, vardecl, varname): yield '%s = None' % varname def vEmit_compileS_IFreeDecl(self, vardecl, varname): yield '%s = rts.freshvar()' % varname def vEmit_compileS_IVarAssign(self, assign, lhs, rhs): yield '%s = %s' % (lhs, rhs) def vEmit_compileS_INodeAssign(self, assign, lhs, rhs): # Patch a forward reference of a recursive let. Index to the parent of # the slot, then write the slot through Node.__setitem__, which takes one # integer index. The rendered ``lhs`` is not used: ``_1[1,0] = _2`` would # pass a tuple index to __setitem__. This mirrors the C++ emission. path = list(assign.path) assert path var = self.vEmit_compileE_IVar(assign.lhs.var) parent = var + ('[%s]' % ','.join(map(str, path[:-1])) if path[:-1] else '') yield '%s[%s] = %s' % (parent, path[-1], rhs) def vEmit_compileS_IExempt(self, exempt): yield '_0.rewrite(rts.Failure)' def vEmit_compileS_IReturn(self, iret, expr): if isinstance(iret.expr, icurry.IReference): yield '_0.rewrite(rts.Fwd, %s)' % expr else: yield '_0.rewrite(%s)' % expr def vEmit_compileS_ICaseCons(self, icase, h_datatype, varident): yield '%s.hnf(typedef=%s)' % (varident, h_datatype) yield 'selector = %s.tag' % varident el = '' for branch in icase.branches[:-1]: rhs = self.interp.symbol(branch.symbolname).info.tag yield '%sif selector == %s:' % (el, rhs), branch.symbolname yield list(self.compileS(branch.block)) el = 'el' if el: yield 'else:', icase.branches[-1].symbolname yield list(self.compileS(icase.branches[-1].block)) else: for line in self.compileS(icase.branches[-1].block): yield line def vEmit_compileS_ICaseLit(self, icase, h_sel, h_values): h_datatype = self.importDataType(icase.branches[0].lit.fullname) yield '%s.hnf(typedef=%s, values=%s)' % (h_sel, h_datatype, h_values) yield 'selector = %s.unboxed_value' % h_sel el = '' for branch in icase.branches: # An Int literal outside the range is refused when the code is made, # as the node refuses it (issue #105): a comparison with it would # match nothing in silence. check_int_range(branch.lit.value) rhs = repr(branch.lit.value) yield '%sif selector == %s:' % (el, rhs) yield list(self.compileS(branch.block)) el = 'el' last_line = '_0.rewrite(rts.Failure)' if el: yield 'else:' yield [last_line] else: yield last_line def vEmit_compileE_IVar(self, ivar): return '_%s' % ivar.vid def vEmit_compileE_IVarAccess(self, ivaraccess, var): return '%s[%s]' % (var, ','.join(map(str, ivaraccess.path))) def vEmit_compileE_ILiteral(self, iliteral, h_ctor, primary): check_int_range(iliteral.value) text = '%s, %r' % (h_ctor, iliteral.value) return 'rts.Node(%s)' % text if primary else text def vEmit_compileE_IString(self, istring, h_string, primary): text = 'rts.prelude._biString, memoryview(%s)' % h_string return 'rts.Node(%s)' % text if primary else text def vEmit_compileE_IUnboxedLiteral(self, iunboxed, primary): return repr(iunboxed) def vEmit_compileE_ICall(self, icall, h_info, args, primary): text = '%s%s' % (h_info, ''.join(', ' + e for e in args)) return 'rts.Node(%s)' % text if primary else text def vEmit_compileE_IPartialCall(self, ipcall, h_info, args, primary): text = 'rts.PartApplic, %s, rts.Node(%s%s, partial=True)' % ( self.compileE(ipcall.missing) , h_info , ''.join(', ' + e for e in args) ) return 'rts.Node(%s)' % text if primary else text def vEmit_compileE_IOr(self, ior, lhs, rhs, primary): h_choice = self.importSymbol('Prelude.?') text = "%s, %s, %s" % (h_choice, lhs, rhs) return 'rts.Node(%s)' % text if primary else text def importable(obj): modulename = getattr(obj, '__module__', None) name = getattr(obj, '__name__', None) module = sys.modules.get(modulename, None) found = getattr(module, name, None) return found is not None
[docs] def write_module( target_object, stream, goal=None, section_headers=True, module_main=True , goalscheme=None ): ''' Writes a compiled module as Python. With ``module_main``, the file ends in a footer that evaluates ``goal`` when the file runs as a program; ``goalscheme`` is the FlatCurry type of the goal as text, recorded so that a goal with class constraints runs without its interface file. ''' render = renderer.PY_RENDERER.renderLines SECTIONS = ( '.header' , '.imports' , '.strings' , '.valuesets' , '.primitives' , '.metadata' , '.stepfuncs' , '.infotabs' , '.datatypes' , '.stepfuncs.link' , '.infotabs.link' , '.datatypes.link' , '.moduledef' , '.moduleimp' , '.footer' ) assert sorted(SECTIONS) == sorted(compiler.TargetObject.SECTIONS) for section_name in SECTIONS: if section_headers: stream.write('# SECTION: %s\n' % section_name) section_data = target_object[section_name] section_text = render(section_data) stream.write(section_text) if section_text: stream.write('\n\n') if module_main: stream.write('''if __name__ == '__main__':\n''') stream.write( ' from %s import __main__\n' % config.python_package_name()) scheme = '' if goalscheme is None else ', goalscheme=%r' % goalscheme stream.write( ' __main__.moduleMain(__file__, %r, goal=%r%s)\n' % (target_object.unitname, goal, scheme) ) stream.write('\n\n')