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/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))