# -*- coding: utf-8 -*-
"""
This module contains classes defining types and methods which can be called by
the `PrattParser` class when checking types. It defines the class `TypeSig`
and the class `TypeObject`, as well as a dict class for storing
parameterized types.
All checking of type equivalence has been abstracted into this module, via
utility routines (often static methods of classes).
Terminology:
- Function **parameters** or **formal arguments** are the identifiers which appear
in the function definition (and the function signature). The type
specifications for parameters will be called **formal types**, since the
term parameter is used differently below.
The `original_sig` of a construct is the `TypeSig` of formal arguments.
The `expanded_sig` of a construct is the `original_sig` expanded to the
correct number of formal arguments (the first step of type matching). |br|
- Function **arguments** or **actual arguments** are the values which are actually
passed to the function on a function call. These are resolved at parse-time.
Their types will be referred to as **actual types**.
The `actual_sig` of a construct is the `TypeSig` of actual types, resolved
during parsing.
Note that the formal type of a parameter can possibly match more than one
actual type. At least one must match, however.
The each instance of the `PrattParser` class holds all of its defined types in
a `TypeTable` class, defined in this module.
"""
# For ideas on how to define composite types, etc., look at PEP-483:
# https://www.python.org/dev/peps/pep-0483/
# https://docs.python.org/3/library/typing.html
# Similarity to Python 3's type hinting would be good.
from __future__ import print_function, division, absolute_import
if __name__ == "__main__":
import pytest_helper
pytest_helper.script_run(["../../test/test_pratt_types.py",
"../../test/test_pratt_parser.py",
], pytest_args="-v")
from .shared_settings_and_exceptions import ParserException
#
# Formal and actual type specs for functions.
#
[docs]class Varargs(object):
"""This class is used to define type signatures which can check a variable
number of arguments. When used at the end of an argument list for a
`TypeSig` instance it will repeat any arguments inside it as many times as
necessary.
If `exact_repeat` is true (the default) then an exception will be raised if
no multiple of the repeated arguments matches the actual arguments.
Otherwise the arguments will be truncated to fit."""
# This hasn't been used or tested much, and may or may not be the best way
# to implement this feature.
def __init__(self, *args, **kwargs):
self.exact_repeat = kwargs.get("exact_repeat", True)
self.arg_types = args
[docs]class TypeSig(object):
"""The type specification for a function. Generally set at function
definition. Can also be used for actual types, since it is just a
container. The "functions" themselves can be any syntactic construct that
produces a node in the final parse tree, with of the node representing the
arguments.
A `TypeSig` instance is essentially just a tuple `(val_type, arg_types)`,
where `val_type` is a `TypeObjectSubclass` and the `arg_types` is a tuple
of them. Using a separate class instead of a tuple allows for additional
information to be stored with the data and produces better error messages.
The class also provides a convenient place to localize some routines which
operate on type signatures and lists of type signatures.Properties are
used automatically convert wildcard `None` arguments to the corresponding
`TypeObject` instances in the attributes `val_type` and `arg_types`.
For the purposes of equality comparison these objects are equivalent to the
tuple form. Equality is exact equality and **does not** hold for formal
signatures and their corresponding actual signatures. For that, use
`matches_formal_sig`. Equality also ignores any attributes (other than
`val_type` and `arg_types`) which might be added to or modified in a
`TypeSig` instance.
Note that `None` is a wildcard which matches any type argument, and `None`
for the `arg_types` list or tuple matches any arguments and any number of
arguments (it is expanded during parsing to as many `None` arguments as are
required). Note that `TypeSig() == TypeSig(None) == TypeSig(None, None)`.
To specify an object like a literal which takes no arguments an empty list
or tuple should be used for `arg_types`, as in `TypeSig(None, [])`; the
`val_type` argument can be set to the desired type if it is typed.
A single `TypeObject` as an `arg_types` argument (i.e., not an iterable) is
expanded to be a tuple of that type of objects, of any required length.
For example, a function might take an arbitrary number of arguments but
they must all have `Int` type.
The values type and the argument types can be accessed by indexing the
0 and 1 element of an instance, respectively, or by using the `val_type`
and `arg_types` attributes."""
def __init__(self, val_type=None, arg_types=None):
"""Initialize a type signature object.
The argument `val_type` should be either `None`, or a `TypeObject`
instance.
The argument `arg_types` should be a list, tuple, or other iterable of
`None` values and/or `TypeObject` instances.
The `None` value is treated as a wildcard that matches any
corresponding type; `None` alone for `arg_types` allows any number of
arguments of any type."""
self.val_type = val_type # Property, automatically converts None args.
self.repeat_index = None
self.exact_repeat = None
self.arg_types = arg_types # Propery, automatically converts None args.
#
# Properties are used for val_type and arg_types to automatically convert
# None args to TypeObject instances.
#
@property
def val_type(self):
"""Return the `val_type` property saved in `_val_type`."""
return self._val_type
@val_type.setter
def val_type(self, value):
"""Convert all `val_type` assignment values to `TypeObject` instance and
save the result in the attribute `_val_type`."""
self._val_type = self.convert_val_type_wildcards(value)
@property
def arg_types(self):
"""Return the `arg_types` property saved in `_arg_types`."""
return self._arg_types
@arg_types.setter
def arg_types(self, value):
"""Convert all `arg_types` assignment values to typles of `TypeObject`
instances and save the result in the `_arg_types` attribute."""
self._arg_types = self.convert_arg_types_wildcards(value)
[docs] def convert_val_type_wildcards(self, val_type):
"""Convert `val_type` argument to a `TypeObject` instance and set attribute."""
if isinstance(val_type, TypeObject):
pass # May need to do something at some point, not as of now.
elif val_type is None:
val_type = TypeObject(None)
else:
raise TypeModuleException("`TypeSig` initialized with invalid `val_type`"
" of '{0}', of Python type {1}. Must be a `TypeObject` instance."
.format(val_type, type(val_type)))
return val_type
[docs] def convert_arg_types_wildcards(self, arg_types):
"""Convert all `arg_types` arguments to `TypeObject` instances and return it."""
if arg_types is None: # None here represents accepting any number of any type.
arg_types = TypeObject(None)
elif isinstance(arg_types, TypeObject): # Single TypeObject, expands as needed.
pass
elif not arg_types: # Matches (), [], and anything else that bools to False
arg_types = () # Below case catches this, but this is clearer.
else:
# First handle any Varargs specifications.
arg_types = list(arg_types)
for count, arg in enumerate(arg_types):
if isinstance(arg, Varargs):
varargs = arg
if count != len(arg_types) - 1:
raise TypeModuleException("The Varargs specification can only"
"occur as the last component of an arg_types list.")
self.repeat_index = count
self.exact_repeat = arg.exact_repeat
arg_types = arg_types[:count] + list(varargs.arg_types)
# Now covert any None arguments to TypeObject(None).
for i in range(len(arg_types)):
if isinstance(arg_types[i], TypeObject): # Ignores None values.
pass # May take some action at some point, not now...
elif arg_types[i] is None:
arg_types[i] = TypeObject(None)
else:
raise TypeModuleException("`TypeSig` initialized with invalid"
" `arg_types` of '{0}', of Python types {1}. Must be"
" `TypeObject` instances.".format(list(arg_types),
[type(at) for at in arg_types]))
arg_types = tuple(arg_types)
return arg_types
#
# Static methods to do tasks related to TypeSig instances.
#
[docs] @staticmethod
def get_all_matching_expanded_sigs(sig_list, list_of_child_sig_lists, tnode=None,
raise_err_on_empty=True):
"""Return the list of all the signatures on `sig_list` whose arguments
match some choice of child/argument signatures from
`list_of_child_sig_lists` Note that this does not look at the return
types, and returns all signatures with matching arguments.
This is the main method of this class which is actually called from the
`PrattParser` to find all the argument-matching signatures. (If
overloading on return types is used then `PrattParser` also calls
`get_child_sigs_matching_return_arg_type` on the second pass, to check
return matches.)
The `sig_list` argument should be a list of `TypeSig` instances
representing formal types.
The `list_of_child_sig_lists` argument should be a list of lists, where
each sublist is a list of all the possible signatures for a child node.
The sublists should be in the same order as the children/arguments.
The returned list is a list of expanded formal type signatures (i.e.,
with `None` arguments expanded and with a fixed number of arguments).
The original, unexpanded formal signature which matched is saved as an
attribute `original_formal_sig` of each matching expanded formal
signature which is returned."""
num_args = len(list_of_child_sig_lists)
# Expand the signatures with wildcards to match the number of args.
sig_list = TypeSig.get_sigs_expanded_for_num_actual_args(num_args, sig_list)
# Filter the signatures by the number of arguments.
sig_list = TypeSig.get_sigs_matching_num_args(num_args, sig_list,
tnode, raise_err_on_empty)
# Now filter by sigs for which the actual value of the child type matches
# (as a type, not equality) the required formal type return type.
sig_list = TypeSig.filter_sigs_matching_child_types(sig_list,
list_of_child_sig_lists, tnode, raise_err_on_empty)
return sig_list
[docs] @staticmethod
def get_sigs_expanded_for_num_actual_args(num_args, sig_list):
"""Expand the signatures on list `sig_list` so that any `None` argument
not inside a tuple or list is converted to a tuple of `None` having
`num_args` of argument.
This routine also expands argument signatures consisting of a single
`TypeObject` to take any number of arguments of that type.
Each expanded version has the original, unexpanded signature saved with
it as an attribute called `original_formal_sig`."""
all_sigs_expanded = []
for sig in sig_list:
if isinstance(sig.arg_types, TypeObject): # TypeObject(None) here also.
new_sig = TypeSig(sig.val_type, (sig.arg_types,)*num_args)
elif not sig.repeat_index is None:
repeat_index = sig.repeat_index
new_args = list(sig.arg_types[:repeat_index-1])
while len(new_args) < num_args:
new_args.extend(sig.arg_types[repeat_index:])
if len(new_args) > num_args:
if sig.exact_repeat:
raise TypeModuleException("Repeating Varargs arguments does"
" not match the required number of arguments in"
" signature {0}".format(sig))
else:
new_args = new_args[:num_args]
new_sig = TypeSig(sig.val_type, new_args)
else:
new_sig = sig
new_sig.original_formal_sig = sig # Save formal sig as an attribute of expanded.
all_sigs_expanded.append(new_sig)
return all_sigs_expanded
[docs] @staticmethod
def get_sigs_matching_num_args(num_args, sig_list,
tnode=None, raise_err_on_empty=True):
"""Return a list of signatures from `sig_list` which match `num_args`
as the number of arguments. The optional token-tree node `tnode` is
only used for improved error reporting.
This routine assumes that a `formal_sig` attribute has been set for all
passed-in signatures (for example, as set by the method
`expand_sigs_with_None_args`."""
sigs_matching_numargs = []
for sig in sig_list:
sig_args_len = len(sig.arg_types)
if sig_args_len == num_args:
new_sig = sig
else:
continue
new_sig.original_formal_sig = sig.original_formal_sig # Copy over the formal sig.
sigs_matching_numargs.append(new_sig)
if raise_err_on_empty and not sigs_matching_numargs:
msg = "The number of arguments ({0}) does not match any signature.".format(
num_args)
if tnode:
tnode._raise_type_mismatch_error([], msg)
else:
raise TypeErrorInParsedLanguage(msg)
return sigs_matching_numargs
[docs] @staticmethod
def filter_sigs_matching_child_types(sig_list, list_of_child_sig_lists,
tnode=None, raise_err_on_empty=True):
"""Return a list of all signatures in `sig_list` which have arguments
that match (in order) the return type of some child signature in the
corresponding sublist of `list_of_child_sig_lists`. The latter should
be a list containing lists of all the expanded signatures for each
child, in corresponding order.
The number of arguments is assumed to already match (see
`get_sigs_matching_num_args`). This just filters the list, removing the
non-matches.
The optional token-tree node `tnode` is only used for improved error
reporting."""
matching_sigs = []
# Loop over each possible sig, testing for matches.
for sig in sig_list:
# Handle the case of literals, with no children/arguments.
if len(sig.arg_types) == 0:
matching_sigs.append(sig)
continue
# Handle the case with one or more children/arguments.
mismatch_with_sig = False
for child_sig_list, arg_type in zip(
list_of_child_sig_lists, sig.arg_types):
some_child_retval_matches = False
for child_sig in child_sig_list:
if arg_type == TypeObject(None):
some_child_retval_matches = True
break
if child_sig.val_type.matches_formal_type(arg_type):
some_child_retval_matches = True
break
if not some_child_retval_matches:
mismatch_with_sig = True
break
if not mismatch_with_sig:
matching_sigs.append(sig)
if raise_err_on_empty and not matching_sigs:
# TODO: Better msg, since now the construct abstraction is used.
msg = ("Type mismatch: The actual argument types do not match any type "
"signature registered with the token.")
if tnode:
tnode._raise_type_mismatch_error([], msg)
else:
raise TypeErrorInParsedLanguage(msg)
return matching_sigs
[docs] @staticmethod
def get_child_sigs_matching_return_arg_type(child, return_type, matching_sigs):
"""Called with a child node, the expected return type for that
child, and a list of matching sigs for the child. Returns all the
`child.matching_sigs` which also match in return type.
This is used in pass two of the type-checking. When called, the
`matching_sigs` parameter is passed the `child.matching_sigs` attribute
which was already set on pass one of the checking. If the result
is unique the return type of the child is known, and so the full
signature can be resolved and assigned."""
# TODO TODO: This needs to use actual_matches_formal routine instead!
return [s for s in matching_sigs
if s.val_type == return_type or return_type is None]
[docs] @staticmethod
def append_sig_to_list_replacing_if_identical(sig_list, sig):
"""Return `sig_list` either with `sig` appended or with any identical
signature (according only to type equality, not attributes) replaced
if one is there. This is called in `register_handler_funs`."""
try:
sig_list.remove(sig)
except ValueError:
pass
sig_list.append(sig)
return sig_list
#
# General indexing and equality testing methods.
#
def __getitem__(self, index):
"""Indexing works like a tuple."""
if index == 0:
return self.val_type
elif index == 1:
return self.arg_types
else:
raise IndexError
def __eq__(self, sig):
"""Note that equality is *only* based on `val_type` and `arg_type`
being *identical*. It ignores all other attributes, and does not
consider more sophisticated notions of equality."""
if not isinstance(sig, self.__class__):
raise TypeError(
"Comparing {0} with some other kind of object."
.format(self.__class__))
return self.val_type == sig.val_type and self.arg_types == sig.arg_types
def __ne__(self, sig):
return not self == sig
def __repr__(self):
if isinstance(self.arg_types, TypeObject):
arg_string = self.arg_types.short_repr()
else:
arg_string = "["
for count, t in enumerate(self.arg_types):
if count != 0:
arg_string += ", "
if count == self.repeat_index:
varargs = ", ".join(t.short_repr() for t in self.arg_types[count:])
arg_string += "Varargs(" + varargs + ")"
break
else:
arg_string += t.short_repr()
arg_string += "]"
return "TypeSig({0}, {1})".format(self.val_type.short_repr(), arg_string)
def __hash__(self):
"""Needed to index dicts and for use in Python sets."""
return hash(("TypeSig", self.val_type, self.arg_types))
#
# Type objects, representing individual types.
#
# NOTE: Consider if there is any advantage to having types themselves take
# parameters other than the string labels.
NONE = ("wildcard_none",) # A different representation for a type label of None.
[docs]class TypeObject(object):
"""Instances of this class represent types."""
# Be sure to update __hash__ if more type-defining components are added.
def __init__(self, type_label,
actual_matches_formal_fun=actual_matches_formal_default):
"""Instantiate a type object or a wildcare object with `None` argument."""
super(TypeObject, self).__init__() # Call base class __init__.
if type_label is None:
self.type_label = NONE
self.is_wildcard = True
else:
self.type_label = type_label
self.is_wildcard = False
self.conversions = {} # Dict keyed by to_type values.
self.actual_matches_formal_fun = actual_matches_formal_fun
def __hash__(self):
"""Define hashing for instances."""
return hash(("TypeObject", self.type_label))
[docs] def def_conversion(self, to_type, priority=0, tree_data=None):
"""Define an automatic conversion to be applied."""
# TODO might need to redefine priority mechanism, since matching across
# a full signature with possible conversions the greedy approach can
# miss some things a non-greedy approach would find. That may just be
# a limitation to document. Also, do you require uniqueness in
# resolution, or use a ranking/priority mechanism?
raise NotImplementedError("Conversions not implemented")
[docs] def undef_conversion(self, to_type):
"""Undefine a conversion."""
pass
def __eq__(self, type_obj):
"""Note that this defines equality between types. The `==` symbol
is defined as exact match only. Use `matches_formal_type` for
comparing actual to formal."""
if not isinstance(type_obj, TypeObject):
return False
return self.type_label == type_obj.type_label
def __ne__(self, type_object):
return not self == type_object
def __repr__(self):
if self.type_label == NONE:
str_label = "None"
else:
str_label = "'" + self.type_label + "'"
return "TypeObject({0})".format(str_label)
[docs] def short_repr(self):
"""This repr prints `None` types as simply "None" rather than
"TypeObject(None)"."""
if self.type_label == NONE:
str_label = "None"
else:
str_label = "'" + self.type_label + "'"
return str_label
#
# TypeTable.
#
[docs]class TypeTable(object):
"""A type table holding instances of the `TypeObject` class, one for each
defined type in the language. Each `PrattParser` instance has such a table,
which holds the objects which represent each type defined in the language
that the particular parser parses."""
aliases = {} # A static dict mapping defined aliases. TODO
def __init__(self, parser_instance):
"""Initialize the type table. The `parser_instance` parameter should be the
`PrattParser` instance which owns this type table. All type tables must
be associated with a parser instance (the types are part of the language
it parses)."""
self.parser_instance = parser_instance
self.type_object_dict = {}
def __contains__(self, type_label):
"""Test whether a `TypeObject` instance for `type_label` has been stored."""
return type_label in self.type_object_dict
def __getitem__(self, type_label):
"""Look up the `TypeObject` instance corresponding
to `type_label` in the type table and return it. Raises a
`TypeException` if no instance is found for the token label."""
if type_label in self.type_object_dict:
type_object = self.type_object_dict[type_label]
return type_object
[docs] def create_typeobject(self, type_label):
"""Create a `TypeObject` instance for a type with string label `type_label`
and store it in the type table. Return the new instance. Raises a
`TypeException` if an instance for `type_label` has already been
created."""
if type_label in self.type_object_dict:
raise TypeModuleException("Already created a type with type_label '{0}'."
.format(type_label))
# Create a new TypeObject instance for the formal type label.
#type_object = TypeObject(type_label, self.parser_instance.type_table) # TODO
type_object = TypeObject(type_label)
# Store the newly-created instance in the token_dict and then return it.
self.type_object_dict[type_label] = type_object
return type_object
[docs] def undef_typeobject(self, type_label):
"""Un-define the token with label type_label. The `TypeObject` instance
previously associated with that label is removed from the dictionary."""
try:
del self.type_object_dict[type_label]
except KeyError:
return # Not saved in dict, ignore.
#
# Exceptions.
#
[docs]class TypeErrorInParsedLanguage(ParserException):
"""Raised when the there is a type error in the language being parsed,
as opposed to a `TypeError` in the Python code."""
pass
[docs]class TypeModuleException(ParserException):
"""An exception in the code of the `pratt_types` module."""
pass