Skip to content

Commit

Permalink
ENH: Add labels and macros.
Browse files Browse the repository at this point in the history
  • Loading branch information
Scott Sanderson committed Mar 7, 2018
1 parent 3cb7737 commit 3bf5451
Show file tree
Hide file tree
Showing 6 changed files with 567 additions and 45 deletions.
99 changes: 99 additions & 0 deletions codetransformer/assembler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import sys
import types
from toolz import mapcat

from . import instructions as instrs
from .code import Code


Label = instrs.Label


def assemble_function(signature, objs, code_kwargs=None, function_kwargs=None):
"""TODO
"""
if code_kwargs is None:
code_kwargs = {}
if function_kwargs is None:
function_kwargs = {}

code_kwargs.setdefault('argnames', list(gen_argnames_for_code(signature)))

# Default to using the globals of the calling stack frame.
function_kwargs.setdefault('globals', sys._getframe(1).f_globals)

function_kwargs.setdefault('argdefs', tuple(extract_defaults(signature)))

code = assemble_code(objs, **code_kwargs).to_pycode()

return types.FunctionType(code, **function_kwargs)


def assemble_code(objs, **code_kwargs):
"""TODO
"""
instrs = resolve_labels(assemble_instructions(objs))
return Code(instrs, **code_kwargs)


def assemble_instructions(objs):
"""Assemble a sequence of Instructions or iterables of instructions.
"""
return list(mapcat(_validate_instructions, objs))


def resolve_labels(objs):
"""TODO
"""
out = []
last_instr = None
for i in reversed(objs):
if isinstance(i, Label):
if last_instr is None:
# TODO: Better error here.
raise ValueError("Can't end with a Label!")
# Make any jumps to `i` resolve to `last_instr`.
last_instr.steal(i)
elif isinstance(i, instrs.Instruction):
last_instr = i
out.append(i)
else:
raise TypeError("Unknown type: {}", i)

for i in out:
if isinstance(i.arg, Label):
raise ValueError("Unresolved label for {}".format(i))

return reversed(out)


def _validate_instructions(obj):
"""TODO
"""
Instruction = instrs.Instruction
if isinstance(obj, (Label, Instruction)):
yield obj
else:
for instr in obj:
if not isinstance(instr, (Instruction, Label)):
raise TypeError(
"Expected an Instruction or Label. Got %s" % obj,
)
yield instr


def gen_argnames_for_code(sig):
"""Get argnames from an inspect.signature to pass to a Code object. """
for name, param in sig.parameters.items():
if param.kind == param.VAR_POSITIONAL:
yield '*' + name
elif param.kind == param.VAR_KEYWORD:
yield '**' + name
else:
yield name


def extract_defaults(sig):
"""Get default parameters from an inspect.signature.
"""
return (p.default for p in sig.parameters.values() if p.default != p.empty)
7 changes: 2 additions & 5 deletions codetransformer/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,21 +344,18 @@ def __init__(self,
if kwarg is not None:
raise ValueError('cannot specify **kwargs more than once')
kwarg = argname[2:]
append_argname(argname)
continue
elif argname.startswith('*'):
if varg is not None:
raise ValueError('cannot specify *args more than once')
varg = argname[1:]
argcounter = kwonlyargcount # all following args are kwonly.
append_argname(argname)
continue
argcounter[0] += 1
append_argname(argname)

if varg is not None:
append_argname(varg)
if kwarg is not None:
append_argname(kwarg)

cellvar_names = set(cellvars)
freevar_names = set(freevars)
for instr in filter(op.attrgetter('uses_free'), instrs):
Expand Down
99 changes: 60 additions & 39 deletions codetransformer/instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,30 +64,29 @@ def _vartype(self):


class InstructionMeta(ABCMeta, matchable):
_marker = object() # sentinel
_type_cache = {}

def __init__(self, *args, opcode=None):
def __init__(self, *args, opcode=None, synthetic=False):
return super().__init__(*args)

def __new__(mcls, name, bases, dict_, *, opcode=None):
def __new__(mcls, name, bases, dict_, *, opcode=None, synthetic=False):
try:
return mcls._type_cache[opcode]
except KeyError:
pass

if len(bases) != 1:
if len(bases) > 1:
raise TypeError(
'{} does not support multiple inheritance'.format(
mcls.__name__,
),
)

if bases[0] is mcls._marker:
if synthetic:
dict_['_reprname'] = immutableattr(name)
for attr in ('absjmp', 'have_arg', 'opcode', 'opname', 'reljmp'):
dict_[attr] = _notimplemented(attr)
return super().__new__(mcls, name, (object,), dict_)
return super().__new__(mcls, name, bases, dict_)

if opcode not in opmap.values():
raise TypeError('Invalid opcode: {}'.format(opcode))
Expand Down Expand Up @@ -123,7 +122,45 @@ def __repr__(self):
__str__ = __repr__


class Instruction(InstructionMeta._marker, metaclass=InstructionMeta):
class JumpTarget:
"""Base class for objects that can be targets of jump instructions.
This is the base for both Instruction and Label.
"""

def __init__(self):
self._target_of = set()
self._stolen_by = None # used for lnotab recalculation

def steal(self, instr):
"""Steal the jump index off of `instr`.
This makes anything that would have jumped to `instr` jump to
this Instruction instead.
Parameters
----------
instr : JumpTarget
The target to steal the jump sources from.
Returns
-------
self : JumpTarget
The object that owns this method.
Notes
-----
This mutates self and ``instr`` inplace.
"""
instr._stolen_by = self
for jmp in instr._target_of:
jmp.arg = self
self._target_of = instr._target_of
instr._target_of = set()
return self


class Instruction(JumpTarget, metaclass=InstructionMeta, synthetic=True):
"""
Base class for all instruction types.
Expand All @@ -139,13 +176,12 @@ class Instruction(InstructionMeta._marker, metaclass=InstructionMeta):
_no_arg = no_default

def __init__(self, arg=_no_arg):
super().__init__()
if self.have_arg and arg is self._no_arg:
raise TypeError(
"{} missing 1 required argument: 'arg'".format(self.opname),
)
self.arg = self._normalize_arg(arg)
self._target_of = set()
self._stolen_by = None # used for lnotab recalculation

def __repr__(self):
arg = self.arg
Expand All @@ -158,33 +194,6 @@ def __repr__(self):
def _normalize_arg(arg):
return arg

def steal(self, instr):
"""Steal the jump index off of `instr`.
This makes anything that would have jumped to `instr` jump to
this Instruction instead.
Parameters
----------
instr : Instruction
The instruction to steal the jump sources from.
Returns
-------
self : Instruction
The instruction that owns this method.
Notes
-----
This mutates self and ``instr`` inplace.
"""
instr._stolen_by = self
for jmp in instr._target_of:
jmp.arg = self
self._target_of = instr._target_of
instr._target_of = set()
return self

@classmethod
def from_opcode(cls, opcode, arg=_no_arg):
"""
Expand Down Expand Up @@ -302,13 +311,13 @@ def _call_repr(self):


def _check_jmp_arg(self, arg):
if not isinstance(arg, (Instruction, _RawArg)):
if not isinstance(arg, (JumpTarget, _RawArg)):
raise TypeError(
'argument to %s must be an instruction, got: %r' % (
'argument to %s must be a valid jump target, got: %r' % (
type(self).__name__, arg,
),
)
if isinstance(arg, Instruction):
if isinstance(arg, JumpTarget):
arg._target_of.add(self)
return arg

Expand Down Expand Up @@ -424,6 +433,18 @@ def __get__(self, instance, owner):
del class_


class Label(JumpTarget):
"""A "pseudo-instruction" that can be the target of a jump instruction.
"""

def __init__(self, debug_name='anonymous'):
super().__init__()
self.debug_name = debug_name

def __repr__(self):
return "Label({!r})".format(self.debug_name)


# Clean up the namespace
del name
del globals_
Expand Down
Loading

0 comments on commit 3bf5451

Please sign in to comment.