summaryrefslogtreecommitdiffstats
path: root/contrib/python/pythran/pythran/syntax.py
blob: 2137d6ba33e4c0b670a43bf16042bbcb4e77dc2c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
'''
This module performs a few early syntax check on the input AST.
It checks the conformance of the input code to Pythran specific
constraints.
'''

from pythran.errors import PythranSyntaxError
from pythran.tables import MODULES
from pythran.intrinsic import Class
from pythran.typing import Tuple, List, Set, Dict
from pythran.utils import isstr
from pythran import metadata

import beniget
import gast as ast
import logging
import numpy as np

logger = logging.getLogger('pythran')

# NB: this purposely ignores OpenMP metadata
class ExtendedDefUseChains(beniget.DefUseChains):

    def __init__(self, ancestors):
        super(ExtendedDefUseChains, self).__init__()
        self.unbounds = dict()
        self.ancestors = ancestors

    def unbound_identifier(self, name, node):
        for n in reversed(self.ancestors.parents(node)):
            if hasattr(n, 'lineno'):
                break
        self.unbounds.setdefault(name, []).append(n)


class SyntaxChecker(ast.NodeVisitor):

    """
    Visit an AST and raise a PythranSyntaxError upon unsupported construct.

    Attributes
    ----------
    attributes : {str}
        Possible attributes from Pythonic modules/submodules.
    """

    def __init__(self):
        """ Gather attributes from MODULES content. """
        self.attributes = set()

        def save_attribute(module):
            """ Recursively save Pythonic keywords as possible attributes. """
            self.attributes.update(module.keys())
            for signature in module.values():
                if isinstance(signature, dict):
                    save_attribute(signature)
                elif isinstance(signature, Class):
                    save_attribute(signature.fields)

        for module in MODULES.values():
            save_attribute(module)

    def visit_Module(self, node):
        err = ("Top level statements can only be assignments, strings,"
               "functions, comments, or imports")
        WhiteList = ast.FunctionDef, ast.Import, ast.ImportFrom, ast.Assign
        for n in node.body:
            if isinstance(n, ast.Expr) and isstr(n.value):
                continue
            if isinstance(n, WhiteList):
                continue
            raise PythranSyntaxError(err, n)
        ancestors = beniget.Ancestors()
        ancestors.visit(node)
        duc = ExtendedDefUseChains(ancestors)
        duc.visit(node)
        for k, v in duc.unbounds.items():
            raise PythranSyntaxError("Unbound identifier {}".format(k), v[0])
        self.generic_visit(node)

    def visit_Interactive(self, node):
        raise PythranSyntaxError("Interactive session not supported", node)

    def visit_Expression(self, node):
        raise PythranSyntaxError("Interactive expressions not supported", node)

    def visit_Suite(self, node):
        raise PythranSyntaxError(
            "Suites are specific to Jython and not supported", node)

    def visit_ClassDef(self, _):
        raise PythranSyntaxError("Classes not supported")

    def visit_Print(self, node):
        self.generic_visit(node)
        if node.dest:
            raise PythranSyntaxError(
                "Printing to a specific stream not supported", node.dest)

    def visit_With(self, node):
        raise PythranSyntaxError("With statements not supported", node)

    def visit_Starred(self, node):
        raise PythranSyntaxError("Call with star arguments not supported",
                                 node)

    def visit_keyword(self, node):
        if node.arg is None:
            raise PythranSyntaxError("Call with kwargs not supported", node)

    def visit_Call(self, node):
        self.generic_visit(node)

    def visit_Constant(self, node):
        if node.value is Ellipsis:
            if hasattr(node, 'lineno'):
                args = [node]
            else:
                args = []
            raise PythranSyntaxError("Ellipsis are not supported", *args)
        iinfo = np.iinfo(int)
        if isinstance(node.value, int) and not (iinfo.min <= node.value
                                                <= iinfo.max):
            raise PythranSyntaxError("large int not supported", node)

    def visit_FunctionDef(self, node):
        if node.decorator_list:
            raise PythranSyntaxError("decorators not supported", node)
        if node.args.vararg:
            raise PythranSyntaxError("Varargs not supported", node)
        if node.args.kwarg:
            raise PythranSyntaxError("Keyword arguments not supported",
                                     node)
        self.generic_visit(node)

    def visit_Raise(self, node):
        self.generic_visit(node)
        if node.cause:
            raise PythranSyntaxError(
                "Cause in raise statements not supported",
                node)

    def visit_Attribute(self, node):
        self.generic_visit(node)
        if node.attr not in self.attributes:
            raise PythranSyntaxError(
                "Attribute '{0}' unknown".format(node.attr),
                node)

    def visit_NamedExpr(self, node):
        raise PythranSyntaxError(
            "named expression are not supported yet, please open an issue :-)",
            node)

    def visit_Import(self, node):
        """ Check if imported module exists in MODULES. """
        for alias in node.names:
            current_module = MODULES
            # Recursive check for submodules
            for path in alias.name.split('.'):
                if path not in current_module:
                    raise PythranSyntaxError(
                        "Module '{0}' unknown.".format(alias.name),
                        node)
                else:
                    current_module = current_module[path]

    def visit_ImportFrom(self, node):
        """
            Check validity of imported functions.

            Check:
                - no level specific value are provided.
                - a module is provided
                - module/submodule exists in MODULES
                - imported function exists in the given module/submodule
        """
        if node.level:
            raise PythranSyntaxError("Relative import not supported", node)
        if not node.module:
            raise PythranSyntaxError("import from without module", node)
        module = node.module
        current_module = MODULES
        # Check if module exists
        for path in module.split('.'):
            if path not in current_module:
                raise PythranSyntaxError(
                    "Module '{0}' unknown.".format(module),
                    node)
            else:
                current_module = current_module[path]

        # Check if imported functions exist
        for alias in node.names:
            if alias.name == '*':
                continue
            elif alias.name not in current_module:
                raise PythranSyntaxError(
                    "identifier '{0}' not found in module '{1}'".format(
                        alias.name,
                        module),
                    node)

    def visit_Exec(self, node):
        raise PythranSyntaxError("'exec' statements are not supported", node)

    def visit_Global(self, node):
        raise PythranSyntaxError("'global' statements are not supported", node)


def check_syntax(node):
    '''Does nothing but raising PythranSyntaxError when needed'''
    SyntaxChecker().visit(node)


def check_specs(specs, types):
    '''
    Does nothing but raising PythranSyntaxError if specs
    are incompatible with the actual code
    '''
    from pythran.types.tog import unify, clone, tr
    from pythran.types.tog import Function, TypeVariable, InferenceError

    for fname, signatures in specs.functions.items():
        ftype = types[fname]
        for signature in signatures:
            sig_type = Function([tr(p) for p in signature], TypeVariable())
            try:
                unify(clone(sig_type), clone(ftype))
            except InferenceError:
                raise PythranSyntaxError(
                    "Specification for `{}` does not match inferred type:\n"
                    "expected `{}`\n"
                    "got `Callable[[{}], ...]`".format(
                        fname,
                        ftype,
                        ", ".join(map(str, sig_type.types[:-1])))
                )


def check_exports(pm, mod, specs):
    '''
    Does nothing but raising PythranSyntaxError if specs
    references an undefined global
    '''
    from pythran.analyses.argument_effects import ArgumentEffects
    mod_functions = {node.name: node for node in mod.body
                     if isinstance(node, ast.FunctionDef)}

    argument_effects = pm.gather(ArgumentEffects, mod)

    for fname, signatures in specs.functions.items():
        try:
            fnode = mod_functions[fname]
        except KeyError:
            raise PythranSyntaxError(
                "Invalid spec: exporting undefined function `{}`"
                .format(fname))

        is_global =  metadata.get(fnode.body[0], metadata.StaticReturn)

        if is_global and signatures:
            raise PythranSyntaxError(
                "Invalid spec: exporting global `{}` as a function"
                .format(fname))

        if not is_global and not signatures:
            raise PythranSyntaxError(
                "Invalid spec: exporting function `{}` as a global"
                .format(fname))


        ae = argument_effects[fnode]

        for signature in signatures:
            args_count = len(fnode.args.args)
            if len(signature) > args_count:
                raise PythranSyntaxError(
                    "Too many arguments when exporting `{}`"
                    .format(fname))
            elif len(signature) < args_count - len(fnode.args.defaults):
                raise PythranSyntaxError(
                    "Not enough arguments when exporting `{}`"
                    .format(fname))
            for i, ty in enumerate(signature):
                if ae[i] and isinstance(ty, (List, Tuple, Dict, Set)):
                    logger.warning(
                        ("Exporting function '{}' that modifies its {} "
                         "argument. Beware that this argument won't be "
                         "modified at Python call site").format(
                             fname,
                             ty.__class__.__qualname__),
                    )