merged in latest cython-devel
[cython.git] / Cython / Compiler / Scanning.py
1 #
2 #   Pyrex Scanner
3 #
4
5 #import pickle
6 import cPickle as pickle
7
8 import os
9 import platform
10 import stat
11 import sys
12 from time import time
13
14 import cython
15 cython.declare(EncodedString=object, string_prefixes=object, raw_prefixes=object, IDENT=object)
16
17 from Cython import Plex, Utils
18 from Cython.Plex.Scanners import Scanner
19 from Cython.Plex.Errors import UnrecognizedInput
20 from Errors import CompileError, error
21 from Lexicon import string_prefixes, raw_prefixes, make_lexicon, IDENT
22
23 from StringEncoding import EncodedString
24
25 try:
26     plex_version = Plex._version
27 except AttributeError:
28     plex_version = None
29 #print "Plex version:", plex_version ###
30
31 debug_scanner = 0
32 trace_scanner = 0
33 scanner_debug_flags = 0
34 scanner_dump_file = None
35 binary_lexicon_pickle = 1
36 notify_lexicon_unpickling = 0
37 notify_lexicon_pickling = 1
38
39 lexicon = None
40
41 #-----------------------------------------------------------------
42
43 def hash_source_file(path):
44     # Try to calculate a hash code for the given source file.
45     # Returns an empty string if the file cannot be accessed.
46     #print "Hashing", path ###
47     try:
48         from hashlib import md5 as new_md5
49     except ImportError:
50         from md5 import new as new_md5
51     f = None
52     try:
53         try:
54             f = open(path, "rU")
55             text = f.read()
56         except IOError, e:
57             print("Unable to hash scanner source file (%s)" % e)
58             return ""
59     finally:
60         if f:
61             f.close()
62     # Normalise spaces/tabs. We don't know what sort of
63     # space-tab substitution the file may have been
64     # through, so we replace all spans of spaces and
65     # tabs by a single space.
66     import re
67     text = re.sub("[ \t]+", " ", text)
68     hash = new_md5(text.encode("ASCII")).hexdigest()
69     return hash
70
71 def open_pickled_lexicon(expected_hash):
72     # Try to open pickled lexicon file and verify that
73     # it matches the source file. Returns the opened
74     # file if successful, otherwise None. ???
75     global lexicon_pickle
76     f = None
77     result = None
78     if os.path.exists(lexicon_pickle):
79         try:
80             f = open(lexicon_pickle, "rb")
81             actual_hash = pickle.load(f)
82             if actual_hash == expected_hash:
83                 result = f
84                 f = None
85             else:
86                 print("Lexicon hash mismatch:")       ###
87                 print("   expected " + expected_hash) ###
88                 print("   got     " + actual_hash)    ###
89         except (IOError, pickle.UnpicklingError), e:
90             print("Warning: Unable to read pickled lexicon " + lexicon_pickle)
91             print(e)
92     if f:
93         f.close()
94     return result
95
96 def try_to_unpickle_lexicon():
97     global lexicon, lexicon_pickle, lexicon_hash
98     dir = os.path.dirname(__file__)
99     source_file = os.path.join(dir, "Lexicon.py")
100     lexicon_hash = hash_source_file(source_file)
101     lexicon_pickle = os.path.join(dir, "Lexicon.pickle")
102     f = open_pickled_lexicon(lexicon_hash)
103     if f:
104         if notify_lexicon_unpickling:
105             t0 = time()
106             print("Unpickling lexicon...")
107         try:
108             lexicon = pickle.load(f)
109         except Exception, e:
110             print "WARNING: Exception while loading lexicon pickle, regenerating"
111             print e
112             lexicon = None
113         f.close()
114         if notify_lexicon_unpickling:
115             t1 = time()
116             print("Done (%.2f seconds)" % (t1 - t0))
117
118 def create_new_lexicon():
119     global lexicon
120     t0 = time()
121     print("Creating lexicon...")
122     lexicon = make_lexicon()
123     t1 = time()
124     print("Done (%.2f seconds)" % (t1 - t0))
125
126 def pickle_lexicon():
127     f = None
128     try:
129         f = open(lexicon_pickle, "wb")
130     except IOError:
131         print("Warning: Unable to save pickled lexicon in " + lexicon_pickle)
132     if f:
133         if notify_lexicon_pickling:
134             t0 = time()
135             print("Pickling lexicon...")
136         pickle.dump(lexicon_hash, f, binary_lexicon_pickle)
137         pickle.dump(lexicon, f, binary_lexicon_pickle)
138         f.close()
139         if notify_lexicon_pickling:
140             t1 = time()
141             print("Done (%.2f seconds)" % (t1 - t0))
142
143 def get_lexicon():
144     global lexicon
145     if not lexicon and plex_version is None:
146         try_to_unpickle_lexicon()
147     if not lexicon:
148         create_new_lexicon()
149         if plex_version is None:
150             pickle_lexicon()
151     return lexicon
152     
153 #------------------------------------------------------------------
154
155 reserved_words = [
156     "global", "include", "ctypedef", "cdef", "def", "class",
157     "print", "del", "pass", "break", "continue", "return",
158     "raise", "import", "exec", "try", "except", "finally",
159     "while", "if", "elif", "else", "for", "in", "assert",
160     "and", "or", "not", "is", "in", "lambda", "from", "yield",
161     "cimport", "by", "with", "cpdef", "DEF", "IF", "ELIF", "ELSE"
162 ]
163
164 class Method(object):
165
166     def __init__(self, name):
167         self.name = name
168         self.__name__ = name # for Plex tracing
169     
170     def __call__(self, stream, text):
171         return getattr(stream, self.name)(text)
172
173 #------------------------------------------------------------------
174
175 def build_resword_dict():
176     d = {}
177     for word in reserved_words:
178         d[word] = 1
179     return d
180
181 cython.declare(resword_dict=object)
182 resword_dict = build_resword_dict()
183
184 #------------------------------------------------------------------
185
186 class CompileTimeScope(object):
187
188     def __init__(self, outer = None):
189         self.entries = {}
190         self.outer = outer
191     
192     def declare(self, name, value):
193         self.entries[name] = value
194     
195     def lookup_here(self, name):
196         return self.entries[name]
197         
198     def __contains__(self, name):
199         return name in self.entries
200     
201     def lookup(self, name):
202         try:
203             return self.lookup_here(name)
204         except KeyError:
205             outer = self.outer
206             if outer:
207                 return outer.lookup(name)
208             else:
209                 raise
210
211 def initial_compile_time_env():
212     benv = CompileTimeScope()
213     names = ('UNAME_SYSNAME', 'UNAME_NODENAME', 'UNAME_RELEASE',
214         'UNAME_VERSION', 'UNAME_MACHINE')
215     for name, value in zip(names, platform.uname()):
216         benv.declare(name, value)
217     import __builtin__ as builtins
218     names = ('False', 'True',
219         'abs', 'bool', 'chr', 'cmp', 'complex', 'dict', 'divmod', 'enumerate',
220         'float', 'hash', 'hex', 'int', 'len', 'list', 'long', 'map', 'max', 'min',
221         'oct', 'ord', 'pow', 'range', 'reduce', 'repr', 'round', 'slice', 'str',
222         'sum', 'tuple', 'xrange', 'zip')
223     for name in names:
224         try:
225             benv.declare(name, getattr(builtins, name))
226         except AttributeError:
227             # ignore, likely Py3
228             pass
229     denv = CompileTimeScope(benv)
230     return denv
231
232 #------------------------------------------------------------------
233
234 class SourceDescriptor(object):
235     """
236     A SourceDescriptor should be considered immutable.
237     """
238     _escaped_description = None
239     _cmp_name = ''
240     def __str__(self):
241         assert False # To catch all places where a descriptor is used directly as a filename
242     
243     def get_escaped_description(self):
244         if self._escaped_description is None:
245             self._escaped_description = \
246                 self.get_description().encode('ASCII', 'replace').decode("ASCII")
247         return self._escaped_description
248
249     def __gt__(self, other):
250         # this is only used to provide some sort of order
251         try:
252             return self._cmp_name > other._cmp_name
253         except AttributeError:
254             return False
255
256     def __lt__(self, other):
257         # this is only used to provide some sort of order
258         try:
259             return self._cmp_name < other._cmp_name
260         except AttributeError:
261             return False
262
263     def __le__(self, other):
264         # this is only used to provide some sort of order
265         try:
266             return self._cmp_name <= other._cmp_name
267         except AttributeError:
268             return False
269
270 class FileSourceDescriptor(SourceDescriptor):
271     """
272     Represents a code source. A code source is a more generic abstraction
273     for a "filename" (as sometimes the code doesn't come from a file).
274     Instances of code sources are passed to Scanner.__init__ as the
275     optional name argument and will be passed back when asking for
276     the position()-tuple.
277     """
278     def __init__(self, filename):
279         self.filename = filename
280         self._cmp_name = filename
281     
282     def get_lines(self):
283         return Utils.open_source_file(self.filename)
284     
285     def get_description(self):
286         return self.filename
287     
288     def get_filenametable_entry(self):
289         return self.filename
290     
291     def __eq__(self, other):
292         return isinstance(other, FileSourceDescriptor) and self.filename == other.filename
293
294     def __hash__(self):
295         return hash(self.filename)
296
297     def __repr__(self):
298         return "<FileSourceDescriptor:%s>" % self.filename
299
300 class StringSourceDescriptor(SourceDescriptor):
301     """
302     Instances of this class can be used instead of a filenames if the
303     code originates from a string object.
304     """
305     def __init__(self, name, code):
306         self.name = name
307         self.codelines = [x + "\n" for x in code.split("\n")]
308         self._cmp_name = name
309     
310     def get_lines(self):
311         return self.codelines
312     
313     def get_description(self):
314         return self.name
315
316     def get_filenametable_entry(self):
317         return "stringsource"
318
319     def __hash__(self):
320         return hash(self.name)
321
322     def __eq__(self, other):
323         return isinstance(other, StringSourceDescriptor) and self.name == other.name
324
325     def __repr__(self):
326         return "<StringSourceDescriptor:%s>" % self.name
327
328 #------------------------------------------------------------------
329
330 class PyrexScanner(Scanner):
331     #  context            Context  Compilation context
332     #  included_files     [string] Files included with 'include' statement
333     #  compile_time_env   dict     Environment for conditional compilation
334     #  compile_time_eval  boolean  In a true conditional compilation context
335     #  compile_time_expr  boolean  In a compile-time expression context
336
337     def __init__(self, file, filename, parent_scanner = None, 
338                  scope = None, context = None, source_encoding=None, parse_comments=True, initial_pos=None):
339         Scanner.__init__(self, get_lexicon(), file, filename, initial_pos)
340         if parent_scanner:
341             self.context = parent_scanner.context
342             self.included_files = parent_scanner.included_files
343             self.compile_time_env = parent_scanner.compile_time_env
344             self.compile_time_eval = parent_scanner.compile_time_eval
345             self.compile_time_expr = parent_scanner.compile_time_expr
346         else:
347             self.context = context
348             self.included_files = scope.included_files
349             self.compile_time_env = initial_compile_time_env()
350             self.compile_time_eval = 1
351             self.compile_time_expr = 0
352         self.parse_comments = parse_comments
353         self.source_encoding = source_encoding
354         self.trace = trace_scanner
355         self.indentation_stack = [0]
356         self.indentation_char = None
357         self.bracket_nesting_level = 0
358         self.begin('INDENT')
359         self.sy = ''
360         self.next()
361
362     def commentline(self, text):
363         if self.parse_comments:
364             self.produce('commentline', text)    
365     
366     def current_level(self):
367         return self.indentation_stack[-1]
368
369     def open_bracket_action(self, text):
370         self.bracket_nesting_level = self.bracket_nesting_level + 1
371         return text
372
373     def close_bracket_action(self, text):
374         self.bracket_nesting_level = self.bracket_nesting_level - 1
375         return text
376
377     def newline_action(self, text):
378         if self.bracket_nesting_level == 0:
379             self.begin('INDENT')
380             self.produce('NEWLINE', '')
381     
382     string_states = {
383         "'":   'SQ_STRING',
384         '"':   'DQ_STRING',
385         "'''": 'TSQ_STRING',
386         '"""': 'TDQ_STRING'
387     }
388     
389     def begin_string_action(self, text):
390         if text[:1] in string_prefixes:
391             text = text[1:]
392         if text[:1] in raw_prefixes:
393             text = text[1:]
394         self.begin(self.string_states[text])
395         self.produce('BEGIN_STRING')
396     
397     def end_string_action(self, text):
398         self.begin('')
399         self.produce('END_STRING')
400     
401     def unclosed_string_action(self, text):
402         self.end_string_action(text)
403         self.error("Unclosed string literal")
404
405     def indentation_action(self, text):
406         self.begin('')
407         # Indentation within brackets should be ignored.
408         #if self.bracket_nesting_level > 0:
409         #    return
410         # Check that tabs and spaces are being used consistently.
411         if text:
412             c = text[0]
413             #print "Scanner.indentation_action: indent with", repr(c) ###
414             if self.indentation_char is None:
415                 self.indentation_char = c
416                 #print "Scanner.indentation_action: setting indent_char to", repr(c)
417             else:
418                 if self.indentation_char != c:
419                     self.error("Mixed use of tabs and spaces")
420             if text.replace(c, "") != "":
421                 self.error("Mixed use of tabs and spaces")
422         # Figure out how many indents/dedents to do
423         current_level = self.current_level()
424         new_level = len(text)
425         #print "Changing indent level from", current_level, "to", new_level ###
426         if new_level == current_level:
427             return
428         elif new_level > current_level:
429             #print "...pushing level", new_level ###
430             self.indentation_stack.append(new_level)
431             self.produce('INDENT', '')
432         else:
433             while new_level < self.current_level():
434                 #print "...popping level", self.indentation_stack[-1] ###
435                 self.indentation_stack.pop()
436                 self.produce('DEDENT', '')
437             #print "...current level now", self.current_level() ###
438             if new_level != self.current_level():
439                 self.error("Inconsistent indentation")
440
441     def eof_action(self, text):
442         while len(self.indentation_stack) > 1:
443             self.produce('DEDENT', '')
444             self.indentation_stack.pop()
445         self.produce('EOF', '')
446
447     def next(self):
448         try:
449             sy, systring = self.read()
450         except UnrecognizedInput:
451             self.error("Unrecognized character")
452         if sy == IDENT:
453             if systring in resword_dict:
454                 sy = systring
455             else:
456                 systring = EncodedString(systring)
457                 systring.encoding = self.source_encoding
458         self.sy = sy
459         self.systring = systring
460         if False: # debug_scanner:
461             _, line, col = self.position()
462             if not self.systring or self.sy == self.systring:
463                 t = self.sy
464             else:
465                 t = "%s %s" % (self.sy, self.systring)
466             print("--- %3d %2d %s" % (line, col, t))
467     
468     def put_back(self, sy, systring):
469         self.unread(self.sy, self.systring)
470         self.sy = sy
471         self.systring = systring
472     
473     def unread(self, token, value):
474         # This method should be added to Plex
475         self.queue.insert(0, (token, value))
476     
477     def error(self, message, pos = None, fatal = True):
478         if pos is None:
479             pos = self.position()
480         if self.sy == 'INDENT':
481             err = error(pos, "Possible inconsistent indentation")
482         err = error(pos, message)
483         if fatal: raise err
484         
485     def expect(self, what, message = None):
486         if self.sy == what:
487             self.next()
488         else:
489             self.expected(what, message)
490     
491     def expect_keyword(self, what, message = None):
492         if self.sy == IDENT and self.systring == what:
493             self.next()
494         else:
495             self.expected(what, message)
496     
497     def expected(self, what, message = None):
498         if message:
499             self.error(message)
500         else:
501             self.error("Expected '%s'" % what)
502         
503     def expect_indent(self):
504         self.expect('INDENT',
505             "Expected an increase in indentation level")
506
507     def expect_dedent(self):
508         self.expect('DEDENT',
509             "Expected a decrease in indentation level")
510
511     def expect_newline(self, message = "Expected a newline"):
512         # Expect either a newline or end of file
513         if self.sy != 'EOF':
514             self.expect('NEWLINE', message)