• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import re
2import sys
3
4def negate(condition):
5    """
6    Returns a CPP conditional that is the opposite of the conditional passed in.
7    """
8    if condition.startswith('!'):
9        return condition[1:]
10    return "!" + condition
11
12class Monitor:
13    """
14    A simple C preprocessor that scans C source and computes, line by line,
15    what the current C preprocessor #if state is.
16
17    Doesn't handle everything--for example, if you have /* inside a C string,
18    without a matching */ (also inside a C string), or with a */ inside a C
19    string but on another line and with preprocessor macros in between...
20    the parser will get lost.
21
22    Anyway this implementation seems to work well enough for the CPython sources.
23    """
24
25    is_a_simple_defined = re.compile(r'^defined\s*\(\s*[A-Za-z0-9_]+\s*\)$').match
26
27    def __init__(self, filename=None, *, verbose=False):
28        self.stack = []
29        self.in_comment = False
30        self.continuation = None
31        self.line_number = 0
32        self.filename = filename
33        self.verbose = verbose
34
35    def __repr__(self):
36        return ''.join((
37            '<Monitor ',
38            str(id(self)),
39            " line=", str(self.line_number),
40            " condition=", repr(self.condition()),
41            ">"))
42
43    def status(self):
44        return str(self.line_number).rjust(4) + ": " + self.condition()
45
46    def condition(self):
47        """
48        Returns the current preprocessor state, as a single #if condition.
49        """
50        return " && ".join(condition for token, condition in self.stack)
51
52    def fail(self, *a):
53        if self.filename:
54            filename = " " + self.filename
55        else:
56            filename = ''
57        print("Error at" + filename, "line", self.line_number, ":")
58        print("   ", ' '.join(str(x) for x in a))
59        sys.exit(-1)
60
61    def close(self):
62        if self.stack:
63            self.fail("Ended file while still in a preprocessor conditional block!")
64
65    def write(self, s):
66        for line in s.split("\n"):
67            self.writeline(line)
68
69    def writeline(self, line):
70        self.line_number += 1
71        line = line.strip()
72
73        def pop_stack():
74            if not self.stack:
75                self.fail("#" + token + " without matching #if / #ifdef / #ifndef!")
76            return self.stack.pop()
77
78        if self.continuation:
79            line = self.continuation + line
80            self.continuation = None
81
82        if not line:
83            return
84
85        if line.endswith('\\'):
86            self.continuation = line[:-1].rstrip() + " "
87            return
88
89        # we have to ignore preprocessor commands inside comments
90        #
91        # we also have to handle this:
92        #     /* start
93        #     ...
94        #     */   /*    <-- tricky!
95        #     ...
96        #     */
97        # and this:
98        #     /* start
99        #     ...
100        #     */   /* also tricky! */
101        if self.in_comment:
102            if '*/' in line:
103                # snip out the comment and continue
104                #
105                # GCC allows
106                #    /* comment
107                #    */ #include <stdio.h>
108                # maybe other compilers too?
109                _, _, line = line.partition('*/')
110                self.in_comment = False
111
112        while True:
113            if '/*' in line:
114                if self.in_comment:
115                    self.fail("Nested block comment!")
116
117                before, _, remainder = line.partition('/*')
118                comment, comment_ends, after = remainder.partition('*/')
119                if comment_ends:
120                    # snip out the comment
121                    line = before.rstrip() + ' ' + after.lstrip()
122                    continue
123                # comment continues to eol
124                self.in_comment = True
125                line = before.rstrip()
126            break
127
128        # we actually have some // comments
129        # (but block comments take precedence)
130        before, line_comment, comment = line.partition('//')
131        if line_comment:
132            line = before.rstrip()
133
134        if not line.startswith('#'):
135            return
136
137        line = line[1:].lstrip()
138        assert line
139
140        fields = line.split()
141        token = fields[0].lower()
142        condition = ' '.join(fields[1:]).strip()
143
144        if token in {'if', 'ifdef', 'ifndef', 'elif'}:
145            if not condition:
146                self.fail("Invalid format for #" + token + " line: no argument!")
147            if token in {'if', 'elif'}:
148                if not self.is_a_simple_defined(condition):
149                    condition = "(" + condition + ")"
150                if token == 'elif':
151                    previous_token, previous_condition = pop_stack()
152                    self.stack.append((previous_token, negate(previous_condition)))
153            else:
154                fields = condition.split()
155                if len(fields) != 1:
156                    self.fail("Invalid format for #" + token + " line: should be exactly one argument!")
157                symbol = fields[0]
158                condition = 'defined(' + symbol + ')'
159                if token == 'ifndef':
160                    condition = '!' + condition
161                token = 'if'
162
163            self.stack.append((token, condition))
164
165        elif token == 'else':
166            previous_token, previous_condition = pop_stack()
167            self.stack.append((previous_token, negate(previous_condition)))
168
169        elif token == 'endif':
170            while pop_stack()[0] != 'if':
171                pass
172
173        else:
174            return
175
176        if self.verbose:
177            print(self.status())
178
179if __name__ == '__main__':
180    for filename in sys.argv[1:]:
181        with open(filename, "rt") as f:
182            cpp = Monitor(filename, verbose=True)
183            print()
184            print(filename)
185            for line_number, line in enumerate(f.read().split('\n'), 1):
186                cpp.writeline(line)
187