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')