python-cc 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pcc/__init__.py +0 -0
- pcc/__main__.py +3 -0
- pcc/ast/__init__.py +0 -0
- pcc/ast/ast.py +179 -0
- pcc/ast/ast_transforms.py +106 -0
- pcc/ast/c_ast.py +800 -0
- pcc/codegen/__init__.py +0 -0
- pcc/codegen/c_codegen.py +4177 -0
- pcc/evaluater/__init__.py +0 -0
- pcc/evaluater/c_evaluator.py +238 -0
- pcc/generator/__init__.py +0 -0
- pcc/generator/c_generator.py +399 -0
- pcc/lex/__init__.py +0 -0
- pcc/lex/c_lexer.py +495 -0
- pcc/lex/lexer.py +68 -0
- pcc/lex/token.py +24 -0
- pcc/parse/__init__.py +0 -0
- pcc/parse/c_parser.py +1700 -0
- pcc/parse/file_parser.py +82 -0
- pcc/parse/parser.py +300 -0
- pcc/parse/plyparser.py +56 -0
- pcc/pcc.py +38 -0
- pcc/ply/__init__.py +5 -0
- pcc/ply/cpp.py +908 -0
- pcc/ply/ctokens.py +133 -0
- pcc/ply/lex.py +1097 -0
- pcc/ply/yacc.py +3471 -0
- pcc/ply/ygen.py +74 -0
- pcc/preprocessor.py +509 -0
- pcc/project.py +78 -0
- pcc/util.py +121 -0
- python_cc-0.0.2.dist-info/METADATA +182 -0
- python_cc-0.0.2.dist-info/RECORD +36 -0
- python_cc-0.0.2.dist-info/WHEEL +4 -0
- python_cc-0.0.2.dist-info/entry_points.txt +2 -0
- python_cc-0.0.2.dist-info/licenses/LICENSE +25 -0
pcc/ply/ygen.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# ply: ygen.py
|
|
2
|
+
#
|
|
3
|
+
# This is a support program that auto-generates different versions of the YACC parsing
|
|
4
|
+
# function with different features removed for the purposes of performance.
|
|
5
|
+
#
|
|
6
|
+
# Users should edit the method LParser.parsedebug() in yacc.py. The source code
|
|
7
|
+
# for that method is then used to create the other methods. See the comments in
|
|
8
|
+
# yacc.py for further details.
|
|
9
|
+
|
|
10
|
+
import os.path
|
|
11
|
+
import shutil
|
|
12
|
+
|
|
13
|
+
def get_source_range(lines, tag):
|
|
14
|
+
srclines = enumerate(lines)
|
|
15
|
+
start_tag = '#--! %s-start' % tag
|
|
16
|
+
end_tag = '#--! %s-end' % tag
|
|
17
|
+
|
|
18
|
+
for start_index, line in srclines:
|
|
19
|
+
if line.strip().startswith(start_tag):
|
|
20
|
+
break
|
|
21
|
+
|
|
22
|
+
for end_index, line in srclines:
|
|
23
|
+
if line.strip().endswith(end_tag):
|
|
24
|
+
break
|
|
25
|
+
|
|
26
|
+
return (start_index + 1, end_index)
|
|
27
|
+
|
|
28
|
+
def filter_section(lines, tag):
|
|
29
|
+
filtered_lines = []
|
|
30
|
+
include = True
|
|
31
|
+
tag_text = '#--! %s' % tag
|
|
32
|
+
for line in lines:
|
|
33
|
+
if line.strip().startswith(tag_text):
|
|
34
|
+
include = not include
|
|
35
|
+
elif include:
|
|
36
|
+
filtered_lines.append(line)
|
|
37
|
+
return filtered_lines
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
dirname = os.path.dirname(__file__)
|
|
41
|
+
shutil.copy2(os.path.join(dirname, 'yacc.py'), os.path.join(dirname, 'yacc.py.bak'))
|
|
42
|
+
with open(os.path.join(dirname, 'yacc.py'), 'r') as f:
|
|
43
|
+
lines = f.readlines()
|
|
44
|
+
|
|
45
|
+
parse_start, parse_end = get_source_range(lines, 'parsedebug')
|
|
46
|
+
parseopt_start, parseopt_end = get_source_range(lines, 'parseopt')
|
|
47
|
+
parseopt_notrack_start, parseopt_notrack_end = get_source_range(lines, 'parseopt-notrack')
|
|
48
|
+
|
|
49
|
+
# Get the original source
|
|
50
|
+
orig_lines = lines[parse_start:parse_end]
|
|
51
|
+
|
|
52
|
+
# Filter the DEBUG sections out
|
|
53
|
+
parseopt_lines = filter_section(orig_lines, 'DEBUG')
|
|
54
|
+
|
|
55
|
+
# Filter the TRACKING sections out
|
|
56
|
+
parseopt_notrack_lines = filter_section(parseopt_lines, 'TRACKING')
|
|
57
|
+
|
|
58
|
+
# Replace the parser source sections with updated versions
|
|
59
|
+
lines[parseopt_notrack_start:parseopt_notrack_end] = parseopt_notrack_lines
|
|
60
|
+
lines[parseopt_start:parseopt_end] = parseopt_lines
|
|
61
|
+
|
|
62
|
+
lines = [line.rstrip()+'\n' for line in lines]
|
|
63
|
+
with open(os.path.join(dirname, 'yacc.py'), 'w') as f:
|
|
64
|
+
f.writelines(lines)
|
|
65
|
+
|
|
66
|
+
print('Updated yacc.py')
|
|
67
|
+
|
|
68
|
+
if __name__ == '__main__':
|
|
69
|
+
main()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
pcc/preprocessor.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""C preprocessor for pcc.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
#include <stdio.h> - system headers: silently ignored
|
|
5
|
+
#include "file.h" - user headers: read and inline
|
|
6
|
+
#define NAME VALUE - object-like macros
|
|
7
|
+
#define NAME(a,b) ((a)+(b)) - function-like macros
|
|
8
|
+
#define NAME - flag macros
|
|
9
|
+
#undef NAME
|
|
10
|
+
#ifdef / #ifndef / #if / #elif / #else / #endif
|
|
11
|
+
#if defined(X) && (X > 5) - full expression evaluation
|
|
12
|
+
defined(NAME) - in #if expressions
|
|
13
|
+
## token pasting - in macro bodies
|
|
14
|
+
# stringification - in function macro bodies
|
|
15
|
+
__LINE__, __FILE__ - built-in macros
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
IDENTIFIER_RE = re.compile(r"[a-zA-Z_]\w*")
|
|
22
|
+
|
|
23
|
+
# System headers silently ignored (libc functions auto-declared from LIBC_FUNCTIONS)
|
|
24
|
+
SYSTEM_HEADERS = {
|
|
25
|
+
"stdio.h",
|
|
26
|
+
"stdlib.h",
|
|
27
|
+
"string.h",
|
|
28
|
+
"strings.h",
|
|
29
|
+
"ctype.h",
|
|
30
|
+
"math.h",
|
|
31
|
+
"time.h",
|
|
32
|
+
"unistd.h",
|
|
33
|
+
"fcntl.h",
|
|
34
|
+
"errno.h",
|
|
35
|
+
"assert.h",
|
|
36
|
+
"stdarg.h",
|
|
37
|
+
"stddef.h",
|
|
38
|
+
"stdint.h",
|
|
39
|
+
"stdbool.h",
|
|
40
|
+
"limits.h",
|
|
41
|
+
"float.h",
|
|
42
|
+
"signal.h",
|
|
43
|
+
"setjmp.h",
|
|
44
|
+
"locale.h",
|
|
45
|
+
"inttypes.h",
|
|
46
|
+
"iso646.h",
|
|
47
|
+
"wchar.h",
|
|
48
|
+
"wctype.h",
|
|
49
|
+
"sys/types.h",
|
|
50
|
+
"sys/stat.h",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
BUILTIN_DEFINES = {
|
|
54
|
+
"NULL": "0",
|
|
55
|
+
"EOF": "(-1)",
|
|
56
|
+
"EXIT_SUCCESS": "0",
|
|
57
|
+
"EXIT_FAILURE": "1",
|
|
58
|
+
"RAND_MAX": "2147483647",
|
|
59
|
+
"INT_MAX": "9223372036854775807",
|
|
60
|
+
"INT_MIN": "(-9223372036854775807-1)",
|
|
61
|
+
"CHAR_BIT": "8",
|
|
62
|
+
"CHAR_MAX": "127",
|
|
63
|
+
"UCHAR_MAX": "255",
|
|
64
|
+
"SHRT_MAX": "32767",
|
|
65
|
+
"USHRT_MAX": "65535",
|
|
66
|
+
"UINT_MAX": "4294967295",
|
|
67
|
+
"LONG_MAX": "9223372036854775807",
|
|
68
|
+
"ULONG_MAX": "18446744073709551615",
|
|
69
|
+
"LLONG_MAX": "9223372036854775807",
|
|
70
|
+
"LLONG_MIN": "(-9223372036854775807-1)",
|
|
71
|
+
"ULLONG_MAX": "18446744073709551615",
|
|
72
|
+
"SIZE_MAX": "18446744073709551615",
|
|
73
|
+
"INTPTR_MAX": "9223372036854775807",
|
|
74
|
+
"FLT_MAX": "3.402823466e+38",
|
|
75
|
+
"DBL_MAX": "1.7976931348623158e+308",
|
|
76
|
+
"FLT_MIN": "1.175494351e-38",
|
|
77
|
+
"DBL_MIN": "2.2250738585072014e-308",
|
|
78
|
+
"FLT_MANT_DIG": "24",
|
|
79
|
+
"DBL_MANT_DIG": "53",
|
|
80
|
+
"FLT_MAX_EXP": "128",
|
|
81
|
+
"DBL_MAX_EXP": "1024",
|
|
82
|
+
"HUGE_VAL": "1e309",
|
|
83
|
+
"HUGE_VALF": "1e39f",
|
|
84
|
+
"true": "1",
|
|
85
|
+
"false": "0",
|
|
86
|
+
"__STDC__": "1",
|
|
87
|
+
"__STDC_VERSION__": "201112",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Type definitions injected before user code (like stddef.h / stdint.h)
|
|
91
|
+
TYPE_PREAMBLE = """
|
|
92
|
+
typedef long size_t;
|
|
93
|
+
typedef long ssize_t;
|
|
94
|
+
typedef long ptrdiff_t;
|
|
95
|
+
typedef long intptr_t;
|
|
96
|
+
typedef unsigned long uintptr_t;
|
|
97
|
+
typedef void *va_list;
|
|
98
|
+
typedef long long intmax_t;
|
|
99
|
+
typedef unsigned long long uintmax_t;
|
|
100
|
+
typedef int sig_atomic_t;
|
|
101
|
+
typedef int wchar_t;
|
|
102
|
+
typedef int wint_t;
|
|
103
|
+
typedef long off_t;
|
|
104
|
+
typedef long clock_t;
|
|
105
|
+
typedef long time_t;
|
|
106
|
+
typedef int pid_t;
|
|
107
|
+
typedef unsigned int mode_t;
|
|
108
|
+
typedef int FILE;
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Macro:
|
|
113
|
+
"""Represents a #define macro (object-like or function-like)."""
|
|
114
|
+
|
|
115
|
+
__slots__ = ("name", "params", "body", "is_function", "_pattern")
|
|
116
|
+
|
|
117
|
+
def __init__(self, name, body, params=None):
|
|
118
|
+
self.name = name
|
|
119
|
+
self.body = body
|
|
120
|
+
self.params = params # None for object-like, list for function-like
|
|
121
|
+
self.is_function = params is not None
|
|
122
|
+
self._pattern = (
|
|
123
|
+
re.compile(r"\b" + re.escape(name) + r"\b")
|
|
124
|
+
if not self.is_function
|
|
125
|
+
else None
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Preprocessor:
|
|
130
|
+
def __init__(self, base_dir=None, defines=None):
|
|
131
|
+
self.base_dir = base_dir or "."
|
|
132
|
+
self.macros = {}
|
|
133
|
+
self._expand_cache = {}
|
|
134
|
+
self._identifier_cache = {}
|
|
135
|
+
self.included_files = set()
|
|
136
|
+
self._line_no = 0
|
|
137
|
+
self._file = "<string>"
|
|
138
|
+
|
|
139
|
+
# Load built-in defines as object-like macros
|
|
140
|
+
for name, value in BUILTIN_DEFINES.items():
|
|
141
|
+
self.macros[name] = Macro(name, value)
|
|
142
|
+
if defines:
|
|
143
|
+
for name, value in defines.items():
|
|
144
|
+
self.macros[name] = Macro(name, str(value))
|
|
145
|
+
|
|
146
|
+
def preprocess(self, source):
|
|
147
|
+
# Inject type preamble for common typedefs
|
|
148
|
+
self._expand_cache.clear()
|
|
149
|
+
self._identifier_cache.clear()
|
|
150
|
+
source = TYPE_PREAMBLE + source
|
|
151
|
+
lines = source.splitlines()
|
|
152
|
+
return self._process_lines(lines, self.base_dir)
|
|
153
|
+
|
|
154
|
+
def _invalidate_expand_cache(self):
|
|
155
|
+
self._expand_cache.clear()
|
|
156
|
+
|
|
157
|
+
def _process_lines(self, lines, base_dir):
|
|
158
|
+
output = []
|
|
159
|
+
i = 0
|
|
160
|
+
skip_stack = [] # stack of (skipping: bool, branch_taken: bool)
|
|
161
|
+
|
|
162
|
+
while i < len(lines):
|
|
163
|
+
self._line_no = i + 1
|
|
164
|
+
line = lines[i]
|
|
165
|
+
stripped = line.strip()
|
|
166
|
+
|
|
167
|
+
# Line continuation
|
|
168
|
+
while stripped.endswith("\\") and i + 1 < len(lines):
|
|
169
|
+
i += 1
|
|
170
|
+
next_line = lines[i].strip()
|
|
171
|
+
stripped = stripped[:-1] + " " + next_line
|
|
172
|
+
line = stripped
|
|
173
|
+
|
|
174
|
+
skipping = any(s[0] for s in skip_stack)
|
|
175
|
+
|
|
176
|
+
if stripped.startswith("#"):
|
|
177
|
+
# Strip inline C comments from directives
|
|
178
|
+
directive = re.sub(r"/\*.*?\*/", "", stripped[1:]).strip()
|
|
179
|
+
directive = re.sub(r"//.*$", "", directive).strip()
|
|
180
|
+
handled = self._handle_directive(
|
|
181
|
+
directive, output, skip_stack, skipping, base_dir
|
|
182
|
+
)
|
|
183
|
+
i += 1
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
if skipping:
|
|
187
|
+
i += 1
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
# Apply macro expansion
|
|
191
|
+
processed = self._expand_line(line)
|
|
192
|
+
output.append(processed)
|
|
193
|
+
i += 1
|
|
194
|
+
|
|
195
|
+
return "\n".join(output)
|
|
196
|
+
|
|
197
|
+
def _handle_directive(self, directive, output, skip_stack, skipping, base_dir):
|
|
198
|
+
# --- Conditional directives (always processed for nesting) ---
|
|
199
|
+
if directive.startswith("ifdef "):
|
|
200
|
+
name = directive[6:].strip()
|
|
201
|
+
if skipping:
|
|
202
|
+
skip_stack.append((True, False))
|
|
203
|
+
else:
|
|
204
|
+
cond = name in self.macros
|
|
205
|
+
skip_stack.append((not cond, cond))
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if directive.startswith("ifndef "):
|
|
209
|
+
name = directive[7:].strip()
|
|
210
|
+
if skipping:
|
|
211
|
+
skip_stack.append((True, False))
|
|
212
|
+
else:
|
|
213
|
+
cond = name not in self.macros
|
|
214
|
+
skip_stack.append((not cond, cond))
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
if directive.startswith("if "):
|
|
218
|
+
expr = directive[3:].strip()
|
|
219
|
+
if skipping:
|
|
220
|
+
skip_stack.append((True, False))
|
|
221
|
+
else:
|
|
222
|
+
cond = self._eval_condition(expr)
|
|
223
|
+
skip_stack.append((not cond, cond))
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
if directive.startswith("elif "):
|
|
227
|
+
expr = directive[5:].strip()
|
|
228
|
+
if not skip_stack:
|
|
229
|
+
return
|
|
230
|
+
parent_skip = (
|
|
231
|
+
any(s[0] for s in skip_stack[:-1]) if len(skip_stack) > 1 else False
|
|
232
|
+
)
|
|
233
|
+
_, branch_taken = skip_stack[-1]
|
|
234
|
+
if parent_skip or branch_taken:
|
|
235
|
+
skip_stack[-1] = (True, branch_taken)
|
|
236
|
+
else:
|
|
237
|
+
cond = self._eval_condition(expr)
|
|
238
|
+
skip_stack[-1] = (not cond, cond)
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
if directive.startswith("else"):
|
|
242
|
+
if skip_stack:
|
|
243
|
+
parent_skip = (
|
|
244
|
+
any(s[0] for s in skip_stack[:-1]) if len(skip_stack) > 1 else False
|
|
245
|
+
)
|
|
246
|
+
_, branch_taken = skip_stack[-1]
|
|
247
|
+
if parent_skip or branch_taken:
|
|
248
|
+
skip_stack[-1] = (True, branch_taken)
|
|
249
|
+
else:
|
|
250
|
+
skip_stack[-1] = (False, True)
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
if directive.startswith("endif"):
|
|
254
|
+
if skip_stack:
|
|
255
|
+
skip_stack.pop()
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
if skipping:
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
# --- Non-conditional directives (only when not skipping) ---
|
|
262
|
+
|
|
263
|
+
# #include <header>
|
|
264
|
+
m = re.match(r"include\s*<(.+?)>", directive)
|
|
265
|
+
if m:
|
|
266
|
+
return # System header: silently skip
|
|
267
|
+
|
|
268
|
+
# #include "header"
|
|
269
|
+
m = re.match(r'include\s*"(.+?)"', directive)
|
|
270
|
+
if m:
|
|
271
|
+
filename = m.group(1)
|
|
272
|
+
filepath = os.path.join(base_dir, filename)
|
|
273
|
+
filepath = os.path.normpath(filepath)
|
|
274
|
+
if filepath not in self.included_files:
|
|
275
|
+
self.included_files.add(filepath)
|
|
276
|
+
try:
|
|
277
|
+
with open(filepath, "r") as f:
|
|
278
|
+
header_lines = f.read().splitlines()
|
|
279
|
+
header_dir = os.path.dirname(filepath)
|
|
280
|
+
result = self._process_lines(header_lines, header_dir)
|
|
281
|
+
output.append(result)
|
|
282
|
+
except FileNotFoundError:
|
|
283
|
+
pass
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# #define NAME(params) body -- function-like macro
|
|
287
|
+
m = re.match(r"define\s+(\w+)\(([^)]*)\)\s*(.*)", directive)
|
|
288
|
+
if m:
|
|
289
|
+
name = m.group(1)
|
|
290
|
+
params = [p.strip() for p in m.group(2).split(",") if p.strip()]
|
|
291
|
+
body = m.group(3).strip()
|
|
292
|
+
self.macros[name] = Macro(name, body, params)
|
|
293
|
+
self._invalidate_expand_cache()
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# #define NAME body -- object-like macro
|
|
297
|
+
m = re.match(r"define\s+(\w+)\s*(.*)", directive)
|
|
298
|
+
if m:
|
|
299
|
+
name = m.group(1)
|
|
300
|
+
body = m.group(2).strip()
|
|
301
|
+
self.macros[name] = Macro(name, body)
|
|
302
|
+
self._invalidate_expand_cache()
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
# #undef NAME
|
|
306
|
+
m = re.match(r"undef\s+(\w+)", directive)
|
|
307
|
+
if m:
|
|
308
|
+
self.macros.pop(m.group(1), None)
|
|
309
|
+
self._invalidate_expand_cache()
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# #error, #warning, #pragma, #line: silently ignore
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
def _eval_condition(self, expr):
|
|
316
|
+
"""Evaluate a #if / #elif expression. Returns True/False."""
|
|
317
|
+
# Strip C comments from expression
|
|
318
|
+
expr = re.sub(r"/\*.*?\*/", "", expr).strip()
|
|
319
|
+
expr = re.sub(r"//.*$", "", expr).strip()
|
|
320
|
+
# Handle defined(NAME) and defined NAME BEFORE macro expansion
|
|
321
|
+
# to avoid expanding the name away
|
|
322
|
+
expanded = re.sub(
|
|
323
|
+
r"\bdefined\s*\(\s*(\w+)\s*\)",
|
|
324
|
+
lambda m: "1" if m.group(1) in self.macros else "0",
|
|
325
|
+
expr,
|
|
326
|
+
)
|
|
327
|
+
expanded = re.sub(
|
|
328
|
+
r"\bdefined\s+(\w+)",
|
|
329
|
+
lambda m: "1" if m.group(1) in self.macros else "0",
|
|
330
|
+
expanded,
|
|
331
|
+
)
|
|
332
|
+
# Now expand macros
|
|
333
|
+
expanded = self._expand_line(expanded)
|
|
334
|
+
# Replace any remaining identifiers with 0 (C standard behavior)
|
|
335
|
+
expanded = re.sub(r"\b[a-zA-Z_]\w*\b", "0", expanded)
|
|
336
|
+
# Evaluate using Python
|
|
337
|
+
try:
|
|
338
|
+
# C uses && || ! instead of and or not, but Python handles these as bitwise
|
|
339
|
+
# Convert C logical operators
|
|
340
|
+
py_expr = (
|
|
341
|
+
expanded.replace("&&", " and ")
|
|
342
|
+
.replace("||", " or ")
|
|
343
|
+
.replace("!", " not ")
|
|
344
|
+
)
|
|
345
|
+
return bool(eval(py_expr, {"__builtins__": {}}, {}))
|
|
346
|
+
except Exception:
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
def _expand_line(self, line):
|
|
350
|
+
"""Expand all macros in a line, handling both object and function macros."""
|
|
351
|
+
prev = None
|
|
352
|
+
iterations = 0
|
|
353
|
+
while line != prev and iterations < 30:
|
|
354
|
+
prev = line
|
|
355
|
+
line = self._expand_once(line)
|
|
356
|
+
iterations += 1
|
|
357
|
+
return line
|
|
358
|
+
|
|
359
|
+
def _expand_once(self, line):
|
|
360
|
+
"""One pass of macro expansion — optimized."""
|
|
361
|
+
original_line = line
|
|
362
|
+
cached = self._expand_cache.get(line)
|
|
363
|
+
if cached is not None:
|
|
364
|
+
return cached
|
|
365
|
+
|
|
366
|
+
# Extract identifiers from the line once
|
|
367
|
+
ids_in_line = self._identifier_cache.get(line)
|
|
368
|
+
if ids_in_line is None:
|
|
369
|
+
ids_in_line = frozenset(IDENTIFIER_RE.findall(line))
|
|
370
|
+
self._identifier_cache[line] = ids_in_line
|
|
371
|
+
# Only check macros that appear in the line
|
|
372
|
+
matching = [self.macros[name] for name in ids_in_line if name in self.macros]
|
|
373
|
+
if not matching:
|
|
374
|
+
self._expand_cache[line] = line
|
|
375
|
+
return line
|
|
376
|
+
# Sort by name length (longest first) for correct replacement
|
|
377
|
+
matching.sort(key=lambda m: len(m.name), reverse=True)
|
|
378
|
+
for macro in matching:
|
|
379
|
+
if macro.is_function:
|
|
380
|
+
line = self._expand_func_macro(line, macro)
|
|
381
|
+
else:
|
|
382
|
+
# Use a callback replacement so Python's regex engine does not
|
|
383
|
+
# interpret C string escapes like \xNN or \n in macro bodies.
|
|
384
|
+
line = macro._pattern.sub(lambda _m, body=macro.body: body, line)
|
|
385
|
+
self._expand_cache[original_line] = line
|
|
386
|
+
return line
|
|
387
|
+
|
|
388
|
+
def _expand_func_macro(self, line, macro):
|
|
389
|
+
"""Expand function-like macro invocations in line."""
|
|
390
|
+
result = []
|
|
391
|
+
pos = 0
|
|
392
|
+
while pos < len(line):
|
|
393
|
+
match = self._find_func_macro_call(line, macro.name, pos)
|
|
394
|
+
if match is None:
|
|
395
|
+
result.append(line[pos:])
|
|
396
|
+
break
|
|
397
|
+
start, args_start = match
|
|
398
|
+
result.append(line[pos:start])
|
|
399
|
+
args, end = self._find_macro_args(line, args_start)
|
|
400
|
+
if args is not None:
|
|
401
|
+
expanded = self._substitute_params(macro, args)
|
|
402
|
+
result.append(expanded)
|
|
403
|
+
pos = end
|
|
404
|
+
else:
|
|
405
|
+
result.append(line[pos:args_start])
|
|
406
|
+
pos = args_start
|
|
407
|
+
return "".join(result)
|
|
408
|
+
|
|
409
|
+
def _find_func_macro_call(self, line, name, start):
|
|
410
|
+
"""Find the next function-like macro invocation using string scanning."""
|
|
411
|
+
name_len = len(name)
|
|
412
|
+
pos = start
|
|
413
|
+
|
|
414
|
+
while True:
|
|
415
|
+
idx = line.find(name, pos)
|
|
416
|
+
if idx == -1:
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
# The match must start at an identifier boundary.
|
|
420
|
+
if idx > 0:
|
|
421
|
+
prev = line[idx - 1]
|
|
422
|
+
if prev == "_" or prev.isalnum():
|
|
423
|
+
pos = idx + name_len
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
arg_pos = idx + name_len
|
|
427
|
+
while arg_pos < len(line) and line[arg_pos].isspace():
|
|
428
|
+
arg_pos += 1
|
|
429
|
+
|
|
430
|
+
if arg_pos < len(line) and line[arg_pos] == "(":
|
|
431
|
+
return idx, arg_pos + 1
|
|
432
|
+
|
|
433
|
+
pos = idx + name_len
|
|
434
|
+
|
|
435
|
+
def _find_macro_args(self, line, start):
|
|
436
|
+
"""Find comma-separated arguments within balanced parentheses.
|
|
437
|
+
Returns (list_of_args, end_position) or (None, 0)."""
|
|
438
|
+
depth = 1
|
|
439
|
+
pos = start
|
|
440
|
+
args = []
|
|
441
|
+
arg_start = start
|
|
442
|
+
|
|
443
|
+
while pos < len(line) and depth > 0:
|
|
444
|
+
c = line[pos]
|
|
445
|
+
if c == "(":
|
|
446
|
+
depth += 1
|
|
447
|
+
elif c == ")":
|
|
448
|
+
depth -= 1
|
|
449
|
+
if depth == 0:
|
|
450
|
+
args.append(line[arg_start:pos].strip())
|
|
451
|
+
return args, pos + 1
|
|
452
|
+
elif c == "," and depth == 1:
|
|
453
|
+
args.append(line[arg_start:pos].strip())
|
|
454
|
+
arg_start = pos + 1
|
|
455
|
+
elif c == '"':
|
|
456
|
+
# Skip string literal
|
|
457
|
+
pos += 1
|
|
458
|
+
while pos < len(line) and line[pos] != '"':
|
|
459
|
+
if line[pos] == "\\":
|
|
460
|
+
pos += 1
|
|
461
|
+
pos += 1
|
|
462
|
+
elif c == "'":
|
|
463
|
+
pos += 1
|
|
464
|
+
while pos < len(line) and line[pos] != "'":
|
|
465
|
+
if line[pos] == "\\":
|
|
466
|
+
pos += 1
|
|
467
|
+
pos += 1
|
|
468
|
+
pos += 1
|
|
469
|
+
|
|
470
|
+
return None, 0
|
|
471
|
+
|
|
472
|
+
def _substitute_params(self, macro, args):
|
|
473
|
+
"""Replace parameter names with arguments in macro body."""
|
|
474
|
+
body = macro.body
|
|
475
|
+
if not macro.params:
|
|
476
|
+
return body
|
|
477
|
+
# Handle __VA_ARGS__
|
|
478
|
+
if macro.params[-1] == "...":
|
|
479
|
+
regular = macro.params[:-1]
|
|
480
|
+
va_args = (
|
|
481
|
+
", ".join(args[len(regular) :]) if len(args) > len(regular) else ""
|
|
482
|
+
)
|
|
483
|
+
for i, param in enumerate(regular):
|
|
484
|
+
if i < len(args):
|
|
485
|
+
arg = args[i]
|
|
486
|
+
body = re.sub(
|
|
487
|
+
r"\b" + re.escape(param) + r"\b",
|
|
488
|
+
lambda _m, arg=arg: arg,
|
|
489
|
+
body,
|
|
490
|
+
)
|
|
491
|
+
body = body.replace("__VA_ARGS__", va_args)
|
|
492
|
+
else:
|
|
493
|
+
for i, param in enumerate(macro.params):
|
|
494
|
+
if i < len(args):
|
|
495
|
+
arg = args[i]
|
|
496
|
+
body = re.sub(
|
|
497
|
+
r"\b" + re.escape(param) + r"\b",
|
|
498
|
+
lambda _m, arg=arg: arg,
|
|
499
|
+
body,
|
|
500
|
+
)
|
|
501
|
+
# Handle ## token pasting
|
|
502
|
+
body = re.sub(r"\s*##\s*", "", body)
|
|
503
|
+
return body
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def preprocess(source, base_dir=None, defines=None):
|
|
507
|
+
"""Preprocess C source code."""
|
|
508
|
+
pp = Preprocessor(base_dir=base_dir, defines=defines)
|
|
509
|
+
return pp.preprocess(source)
|
pcc/project.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""C project builder: collect and merge multiple .c files for compilation.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
pcc <file.c> - compile and run a single file
|
|
5
|
+
pcc <directory> - collect all .c files, merge, compile and run
|
|
6
|
+
|
|
7
|
+
For a directory project, the builder:
|
|
8
|
+
1. Finds all .c files in the directory (non-recursive)
|
|
9
|
+
2. Puts the file containing main() last
|
|
10
|
+
3. Merges all source files into a single compilation unit
|
|
11
|
+
4. #include "xxx.h" is resolved by the preprocessor
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def collect_project(path):
|
|
19
|
+
"""Collect C source from a file or directory.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
path: path to a .c file or a directory
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
(merged_source, base_dir)
|
|
26
|
+
"""
|
|
27
|
+
path = os.path.abspath(path)
|
|
28
|
+
|
|
29
|
+
if os.path.isfile(path):
|
|
30
|
+
base_dir = os.path.dirname(path)
|
|
31
|
+
with open(path, 'r') as f:
|
|
32
|
+
return f.read(), base_dir
|
|
33
|
+
|
|
34
|
+
if os.path.isdir(path):
|
|
35
|
+
return _collect_directory(path), path
|
|
36
|
+
|
|
37
|
+
raise FileNotFoundError(f"Not found: {path}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _collect_directory(dirpath):
|
|
41
|
+
"""Merge all .c files in a directory into one source string."""
|
|
42
|
+
c_files = sorted(f for f in os.listdir(dirpath) if f.endswith('.c'))
|
|
43
|
+
|
|
44
|
+
if not c_files:
|
|
45
|
+
raise FileNotFoundError(f"No .c files found in {dirpath}")
|
|
46
|
+
|
|
47
|
+
# Separate: main file goes last, others go first
|
|
48
|
+
main_file = None
|
|
49
|
+
other_files = []
|
|
50
|
+
|
|
51
|
+
for fname in c_files:
|
|
52
|
+
fpath = os.path.join(dirpath, fname)
|
|
53
|
+
with open(fpath, 'r') as f:
|
|
54
|
+
content = f.read()
|
|
55
|
+
if _has_main(content):
|
|
56
|
+
main_file = (fname, content)
|
|
57
|
+
else:
|
|
58
|
+
other_files.append((fname, content))
|
|
59
|
+
|
|
60
|
+
if main_file is None:
|
|
61
|
+
raise ValueError(f"No main() function found in {dirpath}/*.c")
|
|
62
|
+
|
|
63
|
+
# Merge: other files first, then main file
|
|
64
|
+
parts = []
|
|
65
|
+
for fname, content in other_files:
|
|
66
|
+
parts.append(f"// --- {fname} ---")
|
|
67
|
+
parts.append(content)
|
|
68
|
+
fname, content = main_file
|
|
69
|
+
parts.append(f"// --- {fname} (main) ---")
|
|
70
|
+
parts.append(content)
|
|
71
|
+
|
|
72
|
+
return '\n'.join(parts)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _has_main(source):
|
|
76
|
+
"""Check if source contains a main() function definition."""
|
|
77
|
+
# Match: int main(, void main(, main( at start of line
|
|
78
|
+
return bool(re.search(r'^\s*(?:int|void)\s+main\s*\(', source, re.MULTILINE))
|