Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Labels and Macros #65

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to decide how we want to manage signatures here.

Ideally it should be easy to either type this by hand or use the signature of an existing function. For the tests, I've been using inspect.signature(lambda <sig>: None) as an easy way to get the signature I want.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one idea is to allow a function which would likely look like: lambda a, b, c: ...

"""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):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an earlier draft of this I was allowing people to pass Macros as arguments to jumps, and I replaced jumps to macros with jumps to the first instruction here. Now that we have labels, I can't think of a use-case where passing a macro is a significant improvement over passing a label that's right before a label, though it does mean you have to make sure nothing gets put between your label and your macro. One thing we could consider is adding start and end labels to all macros and have __iter__ bookend the results of assemble with those.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that adding the start/end labels is easier to support elsewhere, but we can implement that later.

"""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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a test

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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be an |=, but that can go in a separate PR.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remember to do this

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