This commit is contained in:
gr
2026-11-11 14:22:02 +08:00
parent 245618dcdc
commit a62311a8da
64878 changed files with 3747172 additions and 66 deletions
@@ -0,0 +1,98 @@
# -----------------------------------------------------------------
# pycparser: __init__.py
#
# This package file exports some convenience functions for
# interacting with pycparser
#
# Eli Bendersky [https://eli.thegreenplace.net/]
# License: BSD
# -----------------------------------------------------------------
__all__ = ["c_ast", "c_lexer", "c_parser"]
__version__ = "3.12"
from subprocess import check_output
from . import c_parser
CParser = c_parser.CParser
def preprocess_file(filename, cpp_path="cpp", cpp_args=""):
"""Preprocess a file using cpp.
filename:
Name of the file you want to preprocess.
cpp_path:
cpp_args:
Refer to the documentation of parse_file for the meaning of these
arguments.
When successful, returns the preprocessed file's contents.
Errors from cpp will be printed out.
"""
path_list = [cpp_path]
if isinstance(cpp_args, list):
path_list += cpp_args
elif cpp_args != "":
path_list += [cpp_args]
path_list += [filename]
try:
# Note the use of universal_newlines to treat all newlines
# as \n for Python's purpose
text = check_output(path_list, universal_newlines=True)
except OSError as e:
raise RuntimeError(
"Unable to invoke 'cpp'. "
+ "Make sure its path was passed correctly\n"
+ f"Original error: {e}"
)
return text
def parse_file(
filename, use_cpp=False, cpp_path="cpp", cpp_args="", parser=None, encoding=None
):
"""Parse a C file using pycparser.
filename:
Name of the file you want to parse.
use_cpp:
Set to True if you want to execute the C pre-processor
on the file prior to parsing it.
cpp_path:
If use_cpp is True, this is the path to 'cpp' on your
system. If no path is provided, it attempts to just
execute 'cpp', so it must be in your PATH.
cpp_args:
If use_cpp is True, set this to the command line arguments strings
to cpp. Be careful with quotes - it's best to pass a raw string
(r'') here. For example:
r'-I../utils/fake_libc_include'
If several arguments are required, pass a list of strings.
encoding:
Encoding to use for the file to parse
parser:
Optional parser object to be used instead of the default CParser
When successful, an AST is returned. ParseError can be
thrown if the file doesn't parse successfully.
Errors from cpp will be printed out.
"""
if use_cpp:
text = preprocess_file(filename, cpp_path, cpp_args)
else:
with open(filename, encoding=encoding) as f:
text = f.read()
if parser is None:
parser = CParser()
return parser.parse(text, filename)
@@ -0,0 +1,385 @@
# -----------------------------------------------------------------
# _ast_gen.py
#
# Generates the AST Node classes from a specification given in
# a configuration file. Run 'make regen-ast' from the repository root
# to regenerate c_ast.py from _c_ast.cfg and format the code.
# This module can also be run as a script (from the repo root or the
# pycparser/ directory); then run 'make format' from the repo root.
#
# The design of this module was inspired by astgen.py from the
# Python 2.5 code-base.
#
# Eli Bendersky [https://eli.thegreenplace.net/]
# License: BSD
# -----------------------------------------------------------------
import os
from string import Template
from typing import IO
class ASTCodeGenerator:
def __init__(self, cfg_filename="_c_ast.cfg"):
"""Initialize the code generator from a configuration
file.
"""
self.cfg_filename = cfg_filename
self.node_cfg = [
NodeCfg(name, contents)
for (name, contents) in self.parse_cfgfile(cfg_filename)
]
def generate(self, file: IO[str]) -> None:
"""Generates the code into file, an open file buffer."""
src = Template(_PROLOGUE_COMMENT).substitute(cfg_filename=self.cfg_filename)
src += _PROLOGUE_CODE
for node_cfg in self.node_cfg:
src += node_cfg.generate_source() + "\n\n"
file.write(src)
def parse_cfgfile(self, filename):
"""Parse the configuration file and yield pairs of
(name, contents) for each node.
"""
with open(filename, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
colon_i = line.find(":")
lbracket_i = line.find("[")
# A field's default may contain []; find the outer closing bracket.
rbracket_i = line.rfind("]")
if colon_i < 1 or lbracket_i <= colon_i or rbracket_i <= lbracket_i:
raise RuntimeError(f"Invalid line in {filename}:\n{line}\n")
name = line[:colon_i]
val = line[lbracket_i + 1 : rbracket_i]
vallist = [v.strip() for v in val.split(",")] if val else []
yield name, vallist
class NodeCfg:
"""Node configuration.
name: node name
contents: a list of contents - attributes and child nodes
See comment at the top of the configuration file for details.
"""
def __init__(self, name, contents):
self.name = name
self.all_entries = []
self.attr = []
self.child = []
self.seq_child = []
self.defaults = {}
for entry in contents:
entry, has_default, default = entry.partition("=")
entry = entry.strip()
clean_entry = entry.rstrip("*")
self.all_entries.append(clean_entry)
if has_default:
default = default.strip()
if default not in ("None", "[]"):
raise RuntimeError(
f"Unsupported default {default!r} for {name}.{clean_entry}; "
"expected None or []"
)
self.defaults[clean_entry] = default
if entry.endswith("**"):
self.seq_child.append(clean_entry)
elif entry.endswith("*"):
self.child.append(clean_entry)
else:
self.attr.append(entry)
def generate_source(self):
src = self._gen_init()
src += "\n" + self._gen_children()
src += "\n" + self._gen_iter()
src += "\n" + self._gen_attr_names()
return src
def _gen_init(self):
src = f"class {self.name}(Node):\n"
args = ["self"]
args.extend(name for name in self.all_entries if name not in self.defaults)
args.append("coord=None")
# Defaulted fields are keyword-only so adding one doesn't shift coord
# in existing positional calls. Both supported defaults use None here;
# list defaults are initialized separately for each instance below.
if self.defaults:
args.append("*")
args.extend(f"{name}=None" for name in self.defaults)
arglist = f"({', '.join(args)})"
# Keep coord and __weakref__ last: Node.__repr__ relies on their
# positions when iterating over the node's fields.
slots = ", ".join(
repr(name) for name in self.all_entries + ["coord", "__weakref__"]
)
src += f" __slots__ = ({slots})\n"
src += f" def __init__{arglist}:\n"
for name in self.all_entries + ["coord"]:
if self.defaults.get(name) == "[]":
src += f" self.{name} = [] if {name} is None else {name}\n"
else:
src += f" self.{name} = {name}\n"
return src
def _gen_children(self):
src = " def children(self):\n"
if self.all_entries:
src += " nodelist = []\n"
for child in self.child:
src += f" if self.{child} is not None:\n"
src += f' nodelist.append(("{child}", self.{child}))\n'
for seq_child in self.seq_child:
src += f" for i, child in enumerate(self.{seq_child} or []):\n"
src += f' nodelist.append((f"{seq_child}[{{i}}]", child))\n'
src += " return tuple(nodelist)\n"
else:
src += " return ()\n"
return src
def _gen_iter(self):
src = " def __iter__(self):\n"
if self.all_entries:
for child in self.child:
src += f" if self.{child} is not None:\n"
src += f" yield self.{child}\n"
for seq_child in self.seq_child:
src += f" yield from self.{seq_child} or []\n"
if not (self.child or self.seq_child):
# Empty generator
src += " return\n" + " yield\n"
else:
# Empty generator
src += " return\n" + " yield\n"
return src
def _gen_attr_names(self):
src = " attr_names = (" + "".join(f"{nm!r}, " for nm in self.attr) + ")"
return src
_PROLOGUE_COMMENT = r"""#-----------------------------------------------------------------
# ** ATTENTION **
# This code was automatically generated from _c_ast.cfg
#
# Do not modify it directly. Modify the configuration file and
# run 'make regen-ast' from the repository root to regenerate and format it.
# ** ** *** ** **
#
# The order of generated __slots__ is significant to Node.__repr__.
# ruff: noqa: RUF023
#
# pycparser: c_ast.py
#
# AST Node classes.
#
# Eli Bendersky [https://eli.thegreenplace.net/]
# License: BSD
#-----------------------------------------------------------------
"""
_PROLOGUE_CODE = r'''
import sys
from typing import IO, Any, ClassVar
def _repr(obj):
"""
Get the representation of an object, with dedicated pprint-like format for lists.
"""
if isinstance(obj, list):
return '[' + (',\n '.join(_repr(e).replace('\n', '\n ') for e in obj)) + '\n]'
else:
return repr(obj)
class Node:
__slots__ = ()
""" Abstract base class for AST nodes.
"""
attr_names: ClassVar[tuple[str, ...]] = ()
coord: Any | None
def __repr__(self):
""" Generates a python representation of the current node
"""
result = self.__class__.__name__ + '('
indent = ''
separator = ''
for name in self.__slots__[:-2]:
result += separator
result += indent
result += name + '=' + (_repr(getattr(self, name)).replace('\n', '\n ' + (' ' * (len(name) + len(self.__class__.__name__)))))
separator = ','
indent = '\n ' + (' ' * len(self.__class__.__name__))
result += indent + ')'
return result
def children(self):
"""Return a tuple of (name, child) pairs for immediate child nodes.
Names identify the field and, for list fields, the index, such as
"type" or "ext[0]". Optional children that are None are omitted.
The returned tuple supports len() and indexing. Iterating over the
node itself yields just the child nodes, without their names.
"""
def show(
self,
buf: IO[str] = sys.stdout,
offset: int = 0,
attrnames: bool = False,
showemptyattrs: bool = True,
nodenames: bool = False,
showcoord: bool = False,
_my_node_name: str | None = None,
):
""" Pretty print the Node and all its attributes and
children (recursively) to a buffer.
buf:
Open IO buffer into which the Node is printed.
offset:
Initial offset (amount of leading spaces)
attrnames:
True if you want to see the attribute names in
name=value pairs. False to only see the values.
showemptyattrs:
False if you want to suppress printing empty attributes.
nodenames:
True if you want to see the actual node names
within their parents.
showcoord:
Do you want the coordinates of each Node to be
displayed.
"""
lead = ' ' * offset
if nodenames and _my_node_name is not None:
buf.write(lead + self.__class__.__name__+ ' <' + _my_node_name + '>: ')
else:
buf.write(lead + self.__class__.__name__+ ': ')
if self.attr_names:
def is_empty(v):
return v is None or (hasattr(v, '__len__') and len(v) == 0)
nvlist = [(n, getattr(self,n)) for n in self.attr_names \
if showemptyattrs or not is_empty(getattr(self,n))]
if attrnames:
attrstr = ', '.join(f'{name}={value}' for name, value in nvlist)
else:
attrstr = ', '.join(f'{value}' for _, value in nvlist)
buf.write(attrstr)
if showcoord:
buf.write(f' (at {self.coord})')
buf.write('\n')
for (child_name, child) in self.children():
child.show(
buf,
offset=offset + 2,
attrnames=attrnames,
showemptyattrs=showemptyattrs,
nodenames=nodenames,
showcoord=showcoord,
_my_node_name=child_name)
class NodeVisitor:
""" A base NodeVisitor class for visiting c_ast nodes.
Subclass it and define your own visit_XXX methods, where
XXX is the class name you want to visit with these
methods.
For example:
class ConstantVisitor(NodeVisitor):
def __init__(self):
self.values = []
def visit_Constant(self, node):
self.values.append(node.value)
Creates a list of values of all the constant nodes
encountered below the given node. To use it:
cv = ConstantVisitor()
cv.visit(node)
Notes:
* generic_visit() will be called for AST nodes for which
no visit_XXX method was defined.
* The children of nodes for which a visit_XXX was
defined will not be visited - if you need this, call
generic_visit() on the node.
You can use:
NodeVisitor.generic_visit(self, node)
* Modeled after Python's own AST visiting facilities
(the ast module of Python 3.0)
"""
_method_cache = None
def visit(self, node: Node):
""" Visit a node.
"""
if self._method_cache is None:
self._method_cache = {}
visitor = self._method_cache.get(node.__class__.__name__, None)
if visitor is None:
method = 'visit_' + node.__class__.__name__
visitor = getattr(self, method, self.generic_visit)
self._method_cache[node.__class__.__name__] = visitor
return visitor(node)
def generic_visit(self, node: Node):
""" Called if no explicit visitor function exists for a
node. Implements preorder visiting of the node.
"""
for _, c in node.children():
self.visit(c)
'''
if __name__ == "__main__":
base_dir = os.path.dirname(os.path.abspath(__file__))
cfg_path = os.path.join(base_dir, "_c_ast.cfg")
out_path = os.path.join(base_dir, "c_ast.py")
ast_gen = ASTCodeGenerator(cfg_path)
with open(out_path, "w") as out:
ast_gen.generate(out)
@@ -0,0 +1,268 @@
#-----------------------------------------------------------------
# pycparser: _c_ast.cfg
#
# Defines the AST Node classes used in pycparser.
# After editing, run 'make regen-ast' from the repository root to
# regenerate pycparser/c_ast.py and format the code.
#
# Each entry is a Node sub-class name, listing the attributes
# and child nodes of the class:
# <name>* - a child node
# <name>** - a sequence of child nodes
# <name> - an attribute
#
# Append =None or =[] to any field to give it a constructor default, e.g.:
# Example: [name, child*=None, items**=[], flags=[]]
# Defaulted fields are keyword-only arguments after coord; fields without
# defaults remain required positional-or-keyword arguments in the listed order.
# A [] default creates a fresh list when omitted or passed None, and otherwise
# keeps the supplied value. Defaults do not change slots or child traversal.
#
# Fields named attributes contain AttributeSpecifier groups. Their owning node
# determines placement: declarations/parameters and labels use prefixes, tags
# put them after struct/union/enum, and declarators put them after the
# identifier, '*', ']', or ')' that the node represents.
#
# Eli Bendersky [https://eli.thegreenplace.net/]
# License: BSD
#-----------------------------------------------------------------
# ArrayDecl is a nested declaration of an array with the given type.
# dim: the dimension (for example, constant 42)
# dim_quals: list of dimension qualifiers, to support C99's allowing 'const'
# and 'static' within the array dimension in function declarations.
ArrayDecl: [type*, dim*, dim_quals, attributes**=[]]
# An array designator [expr] in a NamedInitializer. The wrapper distinguishes
# an identifier used as an array index from a field designator such as .field.
ArrayDesignator: [expr*]
ArrayRef: [name*, subscript*]
# op: =, +=, /= etc.
#
Assignment: [op, lvalue*, rvalue*]
Alignas: [alignment*]
# Generic C23 attribute; arguments is raw text inside parentheses, or None
# when no argument clause was written. namespace is None for unprefixed names.
Attribute: [name, namespace, arguments]
# One [[...]] group; empty attribute-list entries are ignored.
AttributeSpecifier: [attributes**]
# A standalone attribute declaration, including [[fallthrough]];.
AttributeDeclaration: [attributes**]
# Attribute prefixes on statements other than labels (which have their own
# attributes field to retain the existing switch-case AST structure).
AttributedStatement: [attributes**, stmt*]
# Attributes following a declaration's type specifiers and qualifiers.
AttributedType: [type*, attributes**]
BinaryOp: [op, left*, right*]
# A C23 bit-precise integer type, used as TypeDecl.type.
# width: the constant expression specifying the bit width, left unevaluated
# signedness: 'signed', 'unsigned', or None when omitted
#
BitIntType: [width*, signedness]
Break: []
Case: [expr*, stmts**, attributes**=[]]
Cast: [to_type*, expr*]
# Compound statement in C99 is a list of block items (declarations or
# statements).
#
Compound: [block_items**]
# Compound literal (anonymous object), with optional storage specifiers in C23.
# (storage-class-specifiers? type-name) {initializer_list}
# type: the typename
# init: InitList for the initializer list
# storage: list of storage-class specifiers, empty when omitted
#
CompoundLiteral: [type*, init*, storage]
# type: int, char, float, string, etc.
# value: source spelling. A string may retain adjacent quoted fragments to
# preserve escape boundaries, e.g. "\x1" "a".
#
Constant: [type, value]
Continue: []
# name: the variable being declared
# quals: list of qualifiers (const, volatile)
# funcspec: list function specifiers (i.e. inline in C99)
# storage: list of storage specifiers (extern, register, etc.)
# type: declaration type (probably nested with all the modifiers)
# init: initialization value, or None
# bitsize: bit field size, or None
#
Decl: [name, quals, align, storage, funcspec, type*, init*, bitsize*, attributes**=[]]
# Declarations sharing specifiers, optionally followed by Pragma nodes before
# the semicolon (in a for initializer).
DeclList: [decls**]
Default: [stmts**, attributes**=[]]
DoWhile: [cond*, stmt*]
# Represents the ellipsis (...) parameter in a function
# declaration
#
EllipsisParam: []
# An empty statement (a semicolon ';' on its own)
#
EmptyStatement: []
# Enumeration type specifier
# name: an optional ID
# values: an EnumeratorList
# underlying_type: an optional Typename for a C23 fixed underlying type
#
Enum: [name, values*, underlying_type*, attributes**=[]]
# A name/value pair for enumeration values
#
Enumerator: [name, value*, attributes**=[]]
# A list of Enumerator and Pragma nodes in source order.
#
EnumeratorList: [enumerators**]
# A list of expressions separated by the comma operator.
#
ExprList: [exprs**]
# This is the top of the AST, representing a single C file (a
# translation unit in K&R jargon). It contains a list of
# "external-declaration"s, which is either declarations (Decl),
# Typedef or function definitions (FuncDef).
#
FileAST: [ext**]
# for (init; cond; next) stmt
#
For: [init*, cond*, next*, stmt*]
# name: Id
# args: ExprList
#
FuncCall: [name*, args*]
# type <decl>(args)
#
FuncDecl: [args*, type*, attributes**=[]]
# Function definition: a declarator for the function name and
# a body, which is a compound statement.
# There's an optional list of parameter declarations for old
# K&R-style definitions
#
FuncDef: [decl*, param_decls**, body*]
# One association in a C11 generic selection.
# type: Typename, or None for the default association
# expr: the result expression for this association
#
GenericAssociation: [type*, expr*]
# A C11 _Generic expression. All associations are preserved in source order.
# expr: the controlling expression (not evaluated by C)
# associations: a nonempty sequence of GenericAssociation nodes
#
GenericSelection: [expr*, associations**]
Goto: [name]
ID: [name]
# Holder for types that are a simple identifier (e.g. the built
# ins void, char etc. and typedef-defined types)
#
IdentifierType: [names]
If: [cond*, iftrue*, iffalse*]
# The unresolved type of a C23 auto declaration. Used as TypeDecl.type;
# the initializer remains on Decl and "auto" remains in Decl.storage.
#
InferredType: []
# An initialization list used for compound literals.
#
InitList: [exprs**]
Label: [name, stmt*, attributes**=[]]
# A named initializer for C99.
# name is a sequence of ID nodes for .field designators and ArrayDesignator
# nodes for [expr] designators, allowing combinations such as [i].field[j].
#
NamedInitializer: [name**, expr*]
# a list of comma separated function parameter declarations
#
ParamList: [params**]
PtrDecl: [quals, type*, attributes**=[]]
Return: [expr*]
# A static assertion declaration. The condition is preserved without evaluation.
# message: a string Constant, or None for the message-less extension
#
StaticAssert: [cond*, message*]
# name: struct tag name
# decls: declaration of members
#
Struct: [name, decls**, attributes**=[]]
# type: . or ->
# name.field or name->field
#
StructRef: [name*, type, field*]
Switch: [cond*, stmt*]
# cond ? iftrue : iffalse
#
TernaryOp: [cond*, iftrue*, iffalse*]
# A base type declaration
#
TypeDecl: [declname, quals, align, type*, attributes**=[]]
# A typedef declaration.
# Very similar to Decl, but without some attributes
#
Typedef: [name, quals, storage, type*, attributes**=[]]
Typename: [name, quals, align, type*, attributes**=[]]
# A C23 typeof specifier, used as TypeDecl.type without resolving its type.
# op: typeof or typeof_unqual
# expr: a Typename for a type operand, otherwise an expression
#
TypeOf: [op, expr*]
UnaryOp: [op, expr*]
# name: union tag name
# decls: declaration of members
#
Union: [name, decls**, attributes**=[]]
While: [cond*, stmt*]
Pragma: [string]
@@ -0,0 +1,201 @@
# ------------------------------------------------------------------------------
# pycparser: ast_transforms.py
#
# Some utilities used by the parser to create a friendlier AST.
#
# Eli Bendersky [https://eli.thegreenplace.net/]
# License: BSD
# ------------------------------------------------------------------------------
from copy import deepcopy
from typing import Any, cast
from . import c_ast
def fix_switch_cases(switch_node: c_ast.Switch) -> c_ast.Switch:
"""The 'case' statements in a 'switch' come out of parsing with one
child node, so subsequent statements are just tucked to the parent
Compound. Additionally, consecutive (fall-through) case statements
come out messy. This is a peculiarity of the C grammar. The following:
switch (myvar) {
case 10:
k = 10;
p = k + 1;
return 10;
case 20:
case 30:
return 20;
default:
break;
}
Creates this tree (pseudo-dump):
Switch
ID: myvar
Compound:
Case 10:
k = 10
p = k + 1
return 10
Case 20:
Case 30:
return 20
Default:
break
The goal of this transform is to fix this mess, turning it into the
following:
Switch
ID: myvar
Compound:
Case 10:
k = 10
p = k + 1
return 10
Case 20:
Case 30:
return 20
Default:
break
A fixed AST node is returned. The argument may be modified.
"""
assert isinstance(switch_node, c_ast.Switch)
if isinstance(switch_node.stmt, c_ast.AttributedStatement):
attributed = switch_node.stmt
switch_node.stmt = attributed.stmt
fix_switch_cases(switch_node)
attributed.stmt = switch_node.stmt
switch_node.stmt = attributed
return switch_node
if not isinstance(switch_node.stmt, c_ast.Compound):
return switch_node
# The new Compound child for the Switch, which will collect children in the
# correct order
new_compound = c_ast.Compound([], switch_node.stmt.coord)
# The last Case/Default node
last_case: c_ast.Case | c_ast.Default | None = None
# Goes over the children of the Compound below the Switch, adding them
# either directly below new_compound or below the last Case as appropriate
# (for `switch(cond) {}`, block_items would have been None)
for child in switch_node.stmt.block_items or []:
if isinstance(child, (c_ast.Case, c_ast.Default)):
# If it's a Case/Default:
# 1. Add it to the Compound and mark as "last case"
# 2. If its immediate child is also a Case or Default, promote it
# to a sibling.
new_compound.block_items.append(child)
_extract_nested_case(child, new_compound.block_items)
last_case = new_compound.block_items[-1]
else:
# Other statements are added as children to the last case, if it
# exists.
if last_case is None:
new_compound.block_items.append(child)
else:
last_case.stmts.append(child)
switch_node.stmt = new_compound
return switch_node
def _extract_nested_case(
case_node: c_ast.Case | c_ast.Default, stmts_list: list[c_ast.Node]
) -> None:
"""Recursively extract consecutive Case statements that are made nested
by the parser and add them to the stmts_list.
"""
if isinstance(case_node.stmts[0], (c_ast.Case, c_ast.Default)):
nested = case_node.stmts.pop()
stmts_list.append(nested)
_extract_nested_case(cast(Any, nested), stmts_list)
def fix_atomic_specifiers(
decl: c_ast.Decl | c_ast.Typedef | c_ast.Typename,
) -> c_ast.Decl | c_ast.Typedef | c_ast.Typename:
"""Atomic specifiers like _Atomic(type) are unusually structured,
conferring a qualifier upon the contained type.
This function fixes a decl with atomic specifiers to have a sane AST
structure, by removing spurious Typename->TypeDecl pairs and attaching
the _Atomic qualifier in the right place.
"""
# There can be multiple levels of _Atomic in a decl; fix them until a
# fixed point is reached.
while True:
decl, found = _fix_atomic_specifiers_once(decl)
if not found:
break
# Match the declaration's qualifiers to the innermost TypeDecl, as for
# ordinary declarations. Qualifiers moved onto pointers belong only on
# those PtrDecl nodes. Also restore the name lost with the wrapper TypeDecl.
typ: Any = decl
while not isinstance(typ, c_ast.TypeDecl):
try:
typ = typ.type
except AttributeError:
return decl
decl.quals = typ.quals[:]
if typ.declname is None:
typ.declname = decl.name
return decl
def _fix_atomic_specifiers_once(
decl: c_ast.Decl | c_ast.Typedef | c_ast.Typename,
) -> tuple[c_ast.Decl | c_ast.Typedef | c_ast.Typename, bool]:
"""Performs one 'fix' round of atomic specifiers.
Returns (modified_decl, found) where found is True iff a fix was made.
"""
parent: Any = decl
grandparent: Any = None
node: Any = decl.type
while node is not None:
if isinstance(node, c_ast.Typename) and "_Atomic" in node.quals:
break
try:
grandparent = parent
parent = node
node = node.type
except AttributeError:
# If we've reached a node without a `type` field, it means we won't
# find what we're looking for at this point; give up the search
# and return the original decl unmodified.
return decl, False
assert isinstance(parent, c_ast.TypeDecl)
assert grandparent is not None
# _Atomic(T) cannot wrap an array or function type (C11 6.7.2.4p3). Such a T
# has no `quals` to merge below, so report it instead of letting the attach
# raise AttributeError out of parse().
if isinstance(node.type, (c_ast.ArrayDecl, c_ast.FuncDecl)):
# Imported here to avoid a circular import at module load time.
from .c_parser import ParseError
raise ParseError(
f"{node.coord}: _Atomic cannot be applied to array or function type"
)
# Declaration specifiers are shared by comma-separated declarators. Copy
# the contained type before attaching qualifiers, coordinates, and a name.
typ = deepcopy(node.type)
if typ.coord is None:
# Preserve the declarator coord for _Atomic(T) so TypeDecl doesn't lose
# its location when we replace the wrapper Typename.
typ.coord = parent.coord
# Qualifiers outside _Atomic(T) apply to T itself. In particular, const in
# const _Atomic(int *) qualifies the pointer, not the pointed-to int.
typ.quals = parent.quals + typ.quals
if "_Atomic" not in typ.quals:
typ.quals.append("_Atomic")
cast(Any, grandparent).type = typ
return decl, True
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,866 @@
# ------------------------------------------------------------------------------
# pycparser: c_generator.py
#
# C code generator from pycparser AST nodes.
#
# Eli Bendersky [https://eli.thegreenplace.net/]
# License: BSD
# ------------------------------------------------------------------------------
from collections.abc import Callable
from typing import ClassVar
from . import c_ast
class CGenerator:
"""Uses the same visitor pattern as c_ast.NodeVisitor, but modified to
return a value from each visit method, using string accumulation in
generic_visit.
"""
indent_level: int
reduce_parentheses: bool
def __init__(self, reduce_parentheses: bool = False) -> None:
"""Constructs C-code generator
reduce_parentheses:
if True, eliminates needless parentheses on binary operators
"""
# Statements start with indentation of self.indent_level spaces, using
# the _make_indent method.
self.indent_level = 0
self.reduce_parentheses = reduce_parentheses
def _make_indent(self) -> str:
return " " * self.indent_level
def visit(self, node: c_ast.Node) -> str:
method = "visit_" + node.__class__.__name__
return getattr(self, method, self.generic_visit)(node)
def generic_visit(self, node: c_ast.Node | None) -> str:
if node is None:
return ""
else:
return "".join(self.visit(c) for c_name, c in node.children())
def visit_Constant(self, n: c_ast.Constant) -> str:
return n.value
def visit_ID(self, n: c_ast.ID) -> str:
return n.name
def _generate_attributes(self, attributes: list[c_ast.AttributeSpecifier]) -> str:
return " ".join(self.visit(attribute) for attribute in attributes)
def visit_Attribute(self, n: c_ast.Attribute) -> str:
s = (n.namespace + "::" if n.namespace is not None else "") + n.name
if n.arguments is not None:
s += "(" + n.arguments + ")"
return s
def visit_AttributeSpecifier(self, n: c_ast.AttributeSpecifier) -> str:
return "[[" + ", ".join(self.visit(a) for a in n.attributes) + "]]"
def visit_AttributeDeclaration(self, n: c_ast.AttributeDeclaration) -> str:
return self._generate_attributes(n.attributes)
def visit_AttributedType(self, n: c_ast.AttributedType) -> str:
return self.visit(n.type) + " " + self._generate_attributes(n.attributes)
def visit_AttributedStatement(self, n: c_ast.AttributedStatement) -> str:
return (
self._generate_attributes(n.attributes)
+ " "
+ self._generate_stmt(n.stmt).lstrip()
)
def visit_Pragma(self, n: c_ast.Pragma) -> str:
if isinstance(n.string, c_ast.Constant):
return "_Pragma(" + self.visit(n.string) + ")"
ret = "#pragma"
if n.string:
ret += " " + n.string
return ret
def visit_ArrayRef(self, n: c_ast.ArrayRef) -> str:
arrref = self._parenthesize_unless_simple(n.name)
return arrref + "[" + self._visit_expr(n.subscript, allow_comma=True) + "]"
def visit_StructRef(self, n: c_ast.StructRef) -> str:
sref = self._parenthesize_unless_simple(n.name)
return sref + n.type + self.visit(n.field)
def visit_FuncCall(self, n: c_ast.FuncCall) -> str:
fref = self._parenthesize_unless_simple(n.name)
# ExprList here separates arguments; visit_ExprList handles each argument.
args = self.visit(n.args) if n.args is not None else ""
return fref + "(" + args + ")"
def visit_GenericSelection(self, n: c_ast.GenericSelection) -> str:
# The controlling and association expressions are assignment-expressions:
# preserve parentheses around comma expressions with _visit_expr.
expr = self._visit_expr(n.expr)
associations = ", ".join(self.visit(a) for a in n.associations)
return f"_Generic({expr}, {associations})"
def visit_GenericAssociation(self, n: c_ast.GenericAssociation) -> str:
typ = self.visit(n.type) if n.type is not None else "default"
return f"{typ}: {self._visit_expr(n.expr)}"
def visit_UnaryOp(self, n: c_ast.UnaryOp) -> str:
match n.op:
case "sizeof":
# Always parenthesize the argument of sizeof since it can be
# a name.
return f"sizeof({self._visit_expr(n.expr, allow_comma=True)})"
case "p++":
operand = self._parenthesize_unless_simple(n.expr)
return f"{operand}++"
case "p--":
operand = self._parenthesize_unless_simple(n.expr)
return f"{operand}--"
case _:
operand = self._parenthesize_unless_simple(n.expr)
return f"{n.op}{operand}"
# Precedence map of binary operators:
precedence_map: ClassVar[dict[str, int]] = {
# Should be in sync with c_parser.CParser.precedence
# Higher numbers are stronger binding
"||": 0, # weakest binding
"&&": 1,
"|": 2,
"^": 3,
"&": 4,
"==": 5,
"!=": 5,
">": 6,
">=": 6,
"<": 6,
"<=": 6,
">>": 7,
"<<": 7,
"+": 8,
"-": 8,
"*": 9,
"/": 9,
"%": 9, # strongest binding
}
def visit_BinaryOp(self, n: c_ast.BinaryOp) -> str:
# Note: all binary operators are left-to-right associative
# Unary expressions bind more tightly than every binary operator.
#
# If `n.left.op` has a stronger or equally binding precedence in
# comparison to `n.op`, no parenthesis are needed for the left:
# e.g., `(a*b) + c` is equivalent to `a*b + c`, as well as
# `(a+b) - c` is equivalent to `a+b - c` (same precedence).
# If the left operator is weaker binding than the current, then
# parentheses are necessary:
# e.g., `(a+b) * c` is NOT equivalent to `a+b * c`.
lval_str = self._parenthesize_if(
n.left,
lambda d: (
not (
self._is_simple_node(d)
or self.reduce_parentheses
and (
isinstance(d, c_ast.UnaryOp)
or isinstance(d, c_ast.BinaryOp)
and self.precedence_map[d.op] >= self.precedence_map[n.op]
)
)
),
)
# If `n.right.op` has a stronger -but not equal- binding precedence,
# parenthesis can be omitted on the right:
# e.g., `a + (b*c)` is equivalent to `a + b*c`.
# If the right operator is weaker or equally binding, then parentheses
# are necessary:
# e.g., `a * (b+c)` is NOT equivalent to `a * b+c` and
# `a - (b+c)` is NOT equivalent to `a - b+c` (same precedence).
rval_str = self._parenthesize_if(
n.right,
lambda d: (
not (
self._is_simple_node(d)
or self.reduce_parentheses
and (
isinstance(d, c_ast.UnaryOp)
or isinstance(d, c_ast.BinaryOp)
and self.precedence_map[d.op] > self.precedence_map[n.op]
)
)
),
)
return f"{lval_str} {n.op} {rval_str}"
def visit_Assignment(self, n: c_ast.Assignment) -> str:
rval_str = self._parenthesize_if(
n.rvalue, lambda n: isinstance(n, c_ast.Assignment)
)
return f"{self._visit_expr(n.lvalue)} {n.op} {rval_str}"
def visit_IdentifierType(self, n: c_ast.IdentifierType) -> str:
return " ".join(n.names)
def visit_InferredType(self, n: c_ast.InferredType) -> str:
# The auto storage specifier is emitted by the enclosing declaration.
return ""
def visit_TypeOf(self, n: c_ast.TypeOf) -> str:
return f"{n.op}({self._visit_expr(n.expr)})"
def visit_BitIntType(self, n: c_ast.BitIntType) -> str:
sign = n.signedness + " " if n.signedness else ""
return f"{sign}_BitInt({self._visit_expr(n.width)})"
def _visit_expr(self, n: c_ast.Node, *, allow_comma: bool = False) -> str:
"""Render an expression or initializer with its required delimiters.
Full expression contexts (returns, conditions, subscripts, etc.) allow
bare commas. Individual arguments and initializers need comma grouping.
Statement expressions always need their own parentheses.
"""
match n:
case c_ast.InitList():
return "{" + self.visit(n) + "}"
case c_ast.ExprList() if allow_comma:
return self.visit(n)
case c_ast.ExprList() | c_ast.Compound():
return "(" + self.visit(n) + ")"
case _:
return self.visit(n)
def visit_Decl(self, n: c_ast.Decl, no_type: bool = False) -> str:
# Later declarations in a DeclList omit the shared specifiers, but
# still need their own pointer, array, and function declarator parts.
s = (
self._generate_type(n.type, emit_base_type=False)
if no_type
else self._generate_decl(n)
)
if n.bitsize:
s += " : " + self._visit_expr(n.bitsize)
if n.init:
s += " = " + self._visit_expr(n.init)
return s
def visit_DeclList(self, n: c_ast.DeclList) -> str:
s = self.visit(n.decls[0])
for decl in n.decls[1:]:
if isinstance(decl, c_ast.Pragma):
# A for initializer can have pragmas before its semicolon.
# Keep directives on their own lines, without a comma.
s += "\n" + self._make_indent() + self.visit(decl)
else:
s += ", " + self.visit_Decl(decl, no_type=True)
if isinstance(n.decls[-1], c_ast.Pragma):
s += "\n" + self._make_indent()
return s
def _defined_type(
self, n: c_ast.Node
) -> c_ast.Struct | c_ast.Union | c_ast.Enum | None:
"""Return the struct, union, or enum body this declarator defines.
``union { struct a b; } u, *uu`` both return that same union node.
``struct Foo x`` has no body, so it returns None. Callers compare
the result with ``is``: two anonymous structs that only look alike
are different nodes. Attribute wrappers are skipped too, since
each declarator can have its own wrapper around the shared body.
"""
if not isinstance(n, (c_ast.Decl, c_ast.Typedef)):
return None
typ = n.type
while isinstance(
typ,
(
c_ast.PtrDecl,
c_ast.ArrayDecl,
c_ast.FuncDecl,
c_ast.TypeDecl,
c_ast.AttributedType,
),
):
typ = typ.type
if isinstance(typ, (c_ast.Struct, c_ast.Union)) and typ.decls is not None:
return typ
if isinstance(typ, c_ast.Enum) and typ.values is not None:
return typ
return None
def _same_decl_specs(self, left: c_ast.Node, right: c_ast.Node) -> bool:
"""Return whether two declarators have the same specifiers.
``static struct { int a; } x, y`` matches: both names are
``static``. ``static struct { int a; } x`` followed by
``struct { int a; } y`` does not, because only ``x`` is ``static``.
Qualifiers, alignment, and function specifiers are compared too.
"""
if type(left) is not type(right):
return False
if isinstance(left, c_ast.Decl) and isinstance(right, c_ast.Decl):
return (
left.quals == right.quals
and left.align == right.align
and left.storage == right.storage
and left.funcspec == right.funcspec
)
if isinstance(left, c_ast.Typedef) and isinstance(right, c_ast.Typedef):
return left.quals == right.quals and left.storage == right.storage
return False
def _coalesce_defined_types(
self, nodes: list[c_ast.Node]
) -> list[c_ast.Node | list[c_ast.Node]]:
"""Group consecutive declarators that share one type definition.
``union { struct a b; } u, *uu = &u`` is one group: both names
point at that union and their specifiers match.
``struct { int a; } x; struct { int a; } y;`` stays two items,
because those are different definition nodes. One name on its
own, or a name whose specifiers differ from the next, is left
alone.
"""
grouped: list[c_ast.Node | list[c_ast.Node]] = []
index = 0
while index < len(nodes):
node = nodes[index]
defined = self._defined_type(node)
if defined is None:
grouped.append(node)
index += 1
continue
end = index + 1
while end < len(nodes) and self._same_decl_specs(node, nodes[end]):
if self._defined_type(nodes[end]) is not defined:
break
end += 1
if end == index + 1:
grouped.append(node)
else:
grouped.append(list(nodes[index:end]))
index = end
return grouped
def _visit_decl_group(self, nodes: list[c_ast.Node]) -> str:
"""Emit a group as one declaration, with the body on the first name.
``typedef struct { int a; } B, C`` keeps a single ``typedef`` and
one body. ``struct { int a; } x, *y`` keeps ``*y`` on that same
declaration instead of repeating the struct.
"""
first = nodes[0]
if isinstance(first, c_ast.Typedef):
extras: list[str] = []
for decl in nodes[1:]:
if not isinstance(decl, c_ast.Typedef):
raise TypeError("typedef group contains a non-typedef")
extras.append(self._generate_type(decl.type, emit_base_type=False))
return self.visit(first) + ", " + ", ".join(extras)
decls: list[c_ast.Decl] = []
for decl in nodes:
if not isinstance(decl, c_ast.Decl):
raise TypeError("declaration group contains a non-declaration")
decls.append(decl)
return self.visit(c_ast.DeclList(decls))
def visit_Typedef(self, n: c_ast.Typedef) -> str:
s = self._generate_attributes(n.attributes) + " " if n.attributes else ""
if n.storage:
s += " ".join(n.storage) + " "
s += self._generate_type(n.type)
return s
def visit_Cast(self, n: c_ast.Cast) -> str:
s = "(" + self._generate_type(n.to_type, emit_declname=False) + ")"
return s + " " + self._parenthesize_unless_simple(n.expr)
def visit_ExprList(self, n: c_ast.ExprList) -> str:
visited_subexprs = []
for expr in n.exprs:
visited_subexprs.append(self._visit_expr(expr))
return ", ".join(visited_subexprs)
def visit_InitList(self, n: c_ast.InitList) -> str:
visited_subexprs = []
for expr in n.exprs:
visited_subexprs.append(self._visit_expr(expr))
return ", ".join(visited_subexprs)
def visit_Enum(self, n: c_ast.Enum) -> str:
return self._generate_struct_union_enum(n, name="enum")
def visit_Alignas(self, n: c_ast.Alignas) -> str:
return f"_Alignas({self._visit_expr(n.alignment)})"
def visit_Enumerator(self, n: c_ast.Enumerator) -> str:
name = n.name
if n.attributes:
name += " " + self._generate_attributes(n.attributes)
if not n.value:
return f"{self._make_indent()}{name},\n"
else:
return f"{self._make_indent()}{name} = {self._visit_expr(n.value)},\n"
def visit_FuncDef(self, n: c_ast.FuncDef) -> str:
decl = self.visit(n.decl)
self.indent_level = 0
body = self.visit(n.body)
if n.param_decls:
knrdecls = "".join(self._generate_stmt(p) for p in n.param_decls)
return decl + "\n" + knrdecls + body + "\n"
else:
return decl + "\n" + body + "\n"
def visit_FileAST(self, n: c_ast.FileAST) -> str:
s = ""
for ext in self._coalesce_defined_types(n.ext):
if isinstance(ext, list):
s += self._visit_decl_group(ext) + ";\n"
continue
match ext:
case c_ast.FuncDef():
s += self.visit(ext)
case c_ast.Pragma():
s += self.visit(ext) + "\n"
case _:
s += self.visit(ext) + ";\n"
return s
def visit_Compound(self, n: c_ast.Compound) -> str:
s = self._make_indent() + "{\n"
self.indent_level += 2
if n.block_items:
s += self._generate_body(n.block_items)
self.indent_level -= 2
s += self._make_indent() + "}\n"
return s
def visit_CompoundLiteral(self, n: c_ast.CompoundLiteral) -> str:
storage = " ".join(n.storage) + " " if n.storage else ""
return "(" + storage + self.visit(n.type) + "){" + self.visit(n.init) + "}"
def visit_EmptyStatement(self, n: c_ast.EmptyStatement) -> str:
return ";"
def visit_ParamList(self, n: c_ast.ParamList) -> str:
return ", ".join(self.visit(param) for param in n.params)
def visit_Return(self, n: c_ast.Return) -> str:
s = "return"
if n.expr:
s += " " + self._visit_expr(n.expr, allow_comma=True)
return s + ";"
def visit_Break(self, n: c_ast.Break) -> str:
return "break;"
def visit_Continue(self, n: c_ast.Continue) -> str:
return "continue;"
def visit_TernaryOp(self, n: c_ast.TernaryOp) -> str:
s = "(" + self._visit_expr(n.cond) + ") ? "
s += "(" + self._visit_expr(n.iftrue) + ") : "
s += "(" + self._visit_expr(n.iffalse) + ")"
return s
def visit_If(self, n: c_ast.If) -> str:
s = "if ("
if n.cond:
s += self._visit_expr(n.cond, allow_comma=True)
s += ")\n"
s += self._generate_stmt(n.iftrue, add_indent=True)
if n.iffalse:
s += self._make_indent() + "else\n"
s += self._generate_stmt(n.iffalse, add_indent=True)
return s
def visit_For(self, n: c_ast.For) -> str:
s = "for ("
if n.init:
s += self._visit_expr(n.init, allow_comma=True)
s += ";"
if n.cond:
s += " " + self._visit_expr(n.cond, allow_comma=True)
s += ";"
if n.next:
s += " " + self._visit_expr(n.next, allow_comma=True)
s += ")\n"
s += self._generate_stmt(n.stmt, add_indent=True)
return s
def visit_While(self, n: c_ast.While) -> str:
s = "while ("
if n.cond:
s += self._visit_expr(n.cond, allow_comma=True)
s += ")\n"
s += self._generate_stmt(n.stmt, add_indent=True)
return s
def visit_DoWhile(self, n: c_ast.DoWhile) -> str:
s = "do\n"
s += self._generate_stmt(n.stmt, add_indent=True)
s += self._make_indent() + "while ("
if n.cond:
s += self._visit_expr(n.cond, allow_comma=True)
s += ");"
return s
def visit_StaticAssert(self, n: c_ast.StaticAssert) -> str:
s = "_Static_assert("
s += self._visit_expr(n.cond)
if n.message:
s += ", "
s += self.visit(n.message)
s += ")"
return s
def visit_Switch(self, n: c_ast.Switch) -> str:
s = "switch (" + self._visit_expr(n.cond, allow_comma=True) + ")\n"
s += self._generate_stmt(n.stmt, add_indent=True)
return s
def visit_Case(self, n: c_ast.Case) -> str:
s = self._generate_attributes(n.attributes) + " " if n.attributes else ""
s += "case " + self._visit_expr(n.expr) + ":\n"
return s + self._generate_body(n.stmts or [], add_indent=True)
def visit_Default(self, n: c_ast.Default) -> str:
s = self._generate_attributes(n.attributes) + " " if n.attributes else ""
s += "default:\n"
return s + self._generate_body(n.stmts or [], add_indent=True)
def visit_Label(self, n: c_ast.Label) -> str:
s = self._generate_attributes(n.attributes) + " " if n.attributes else ""
return s + n.name + ":\n" + self._generate_stmt(n.stmt)
def visit_Goto(self, n: c_ast.Goto) -> str:
return "goto " + n.name + ";"
def visit_EllipsisParam(self, n: c_ast.EllipsisParam) -> str:
return "..."
def visit_Struct(self, n: c_ast.Struct) -> str:
return self._generate_struct_union_enum(n, "struct")
def visit_Typename(self, n: c_ast.Typename) -> str:
s = self._generate_attributes(n.attributes) + " " if n.attributes else ""
return s + self._generate_type(n.type)
def visit_Union(self, n: c_ast.Union) -> str:
return self._generate_struct_union_enum(n, "union")
def visit_ArrayDesignator(self, n: c_ast.ArrayDesignator) -> str:
return "[" + self._visit_expr(n.expr) + "]"
def visit_NamedInitializer(self, n: c_ast.NamedInitializer) -> str:
s = ""
for name in n.name:
if isinstance(name, c_ast.ID):
s += "." + name.name
else:
s += self.visit(name)
s += " = " + self._visit_expr(n.expr)
return s
def visit_FuncDecl(self, n: c_ast.FuncDecl) -> str:
return self._generate_type(n)
def visit_ArrayDecl(self, n: c_ast.ArrayDecl) -> str:
return self._generate_type(n, emit_declname=False)
def visit_TypeDecl(self, n: c_ast.TypeDecl) -> str:
return self._generate_type(n, emit_declname=False)
def visit_PtrDecl(self, n: c_ast.PtrDecl) -> str:
return self._generate_type(n, emit_declname=False)
def _generate_struct_union_enum(
self, n: c_ast.Struct | c_ast.Union | c_ast.Enum, name: str
) -> str:
"""Generates code for structs, unions, and enums. name should be
'struct', 'union', or 'enum'.
"""
if name in ("struct", "union"):
assert isinstance(n, (c_ast.Struct, c_ast.Union))
members = n.decls
body_function = self._generate_body
else:
assert name == "enum"
assert isinstance(n, c_ast.Enum)
members = None if n.values is None else n.values.enumerators
body_function = self._generate_enum_body
attributes = (
self._generate_attributes(n.attributes) + " " if n.attributes else ""
)
s = name + " " + attributes + (n.name or "")
if isinstance(n, c_ast.Enum) and n.underlying_type is not None:
s = s.rstrip() + " : " + self.visit(n.underlying_type)
if members is not None:
# None means no members
# Empty sequence means an empty list of members
s += "\n"
s += self._make_indent()
self.indent_level += 2
s += "{\n"
s += body_function(members)
self.indent_level -= 2
s += self._make_indent() + "}"
return s
def _generate_body(self, nodes: list[c_ast.Node], add_indent: bool = False) -> str:
"""Emit statements or members, grouping shared type definitions.
Callers handle braces and indentation levels; _generate_stmt supplies the
punctuation for individual statements, declarations, and pragmas.
"""
indent = self._make_indent()
if add_indent:
indent += " "
parts: list[str] = []
for item in self._coalesce_defined_types(nodes):
if isinstance(item, list):
parts.append(indent + self._visit_decl_group(item) + ";\n")
else:
parts.append(self._generate_stmt(item, add_indent=add_indent))
return "".join(parts)
def _generate_enum_body(
self, members: list[c_ast.Enumerator | c_ast.Pragma]
) -> str:
parts = []
last_enumerator = None
for member in members:
if isinstance(member, c_ast.Pragma):
parts.append(self._generate_stmt(member))
else:
last_enumerator = len(parts)
parts.append(self.visit(member))
# Remove the comma from the last enumerator, which may be followed by
# pragmas whose text and newlines must remain intact.
if last_enumerator is not None:
parts[last_enumerator] = parts[last_enumerator][:-2] + "\n"
return "".join(parts)
def _generate_stmt(self, n: c_ast.Node, add_indent: bool = False) -> str:
"""Generation from a statement node. This method exists as a wrapper
for individual visit_* methods to handle different treatment of
some statements in this context.
"""
if add_indent:
self.indent_level += 2
indent = self._make_indent()
if add_indent:
self.indent_level -= 2
match n:
case (
c_ast.Decl()
| c_ast.Assignment()
| c_ast.Cast()
| c_ast.UnaryOp()
| c_ast.BinaryOp()
| c_ast.TernaryOp()
| c_ast.FuncCall()
| c_ast.ArrayRef()
| c_ast.StructRef()
| c_ast.Constant()
| c_ast.ID()
| c_ast.Typedef()
| c_ast.ExprList()
| c_ast.CompoundLiteral()
| c_ast.GenericSelection()
| c_ast.StaticAssert()
| c_ast.AttributeDeclaration()
):
# These visitors leave the semicolon to the enclosing context,
# so they can also be used in expressions or for clauses.
return indent + self.visit(n) + ";\n"
case c_ast.Compound():
# No extra indentation required before the opening brace of a
# compound - because it consists of multiple lines it has to
# compute its own indentation.
#
return self.visit(n)
case c_ast.AttributedStatement():
return indent + self.visit(n)
case c_ast.If():
return indent + self.visit(n)
case _:
return indent + self.visit(n) + "\n"
def _generate_decl(self, n: c_ast.Decl) -> str:
"""Generation from a Decl node."""
s = self._generate_attributes(n.attributes) + " " if n.attributes else ""
if n.funcspec:
s += " ".join(n.funcspec) + " "
if n.storage:
s += " ".join(n.storage) + " "
if n.align:
s += " ".join(self.visit(align) for align in n.align) + " "
if isinstance(
n.type, (c_ast.TypeDecl, c_ast.PtrDecl, c_ast.ArrayDecl, c_ast.FuncDecl)
):
s += self._generate_type(n.type)
else:
# Members without declarators carry their specifier directly;
# their qualifiers have not been copied onto a TypeDecl.
if n.quals:
s += " ".join(n.quals) + " "
s += self._generate_type_specifier(n.type)
return s
def _generate_type_specifier(self, n: c_ast.Node) -> str:
if isinstance(n, c_ast.AttributedType):
return (
self._generate_type_specifier(n.type).rstrip()
+ " "
+ self._generate_attributes(n.attributes)
)
if isinstance(n, c_ast.Typename) and "_Atomic" in n.quals:
# A bare _Atomic(T) still has its Typename wrapper. Preserve the
# specifier syntax, which also keeps atomic pointers unambiguous.
return "_Atomic(" + self.visit(n) + ")"
return self._generate_type(n)
def _generate_type(
self,
n: c_ast.Node,
modifiers: list[c_ast.Node] | None = None,
emit_declname: bool = True,
emit_base_type: bool = True,
) -> str:
"""Recursive generation from a type node. n is the type node.
modifiers collects the PtrDecl, ArrayDecl and FuncDecl modifiers
encountered on the way down to a TypeDecl, to allow proper
generation from it.
emit_base_type controls whether the shared type specifier and its
qualifiers are emitted, independently of the declarator parts.
"""
if modifiers is None:
modifiers = []
# ~ print(n, modifiers)
match n:
case c_ast.TypeDecl():
s = ""
if emit_base_type:
if n.quals:
s += " ".join(n.quals) + " "
s += self.visit(n.type)
nstr = n.declname if n.declname and emit_declname else ""
if n.attributes:
nstr += " " + self._generate_attributes(n.attributes)
# Resolve modifiers.
# Wrap in parens to distinguish pointer to array and pointer to
# function syntax.
#
for i, modifier in enumerate(modifiers):
match modifier:
case c_ast.ArrayDecl():
if i != 0 and isinstance(modifiers[i - 1], c_ast.PtrDecl):
nstr = "(" + nstr + ")"
nstr += "["
if modifier.dim_quals:
nstr += " ".join(modifier.dim_quals) + " "
if modifier.dim is not None:
nstr += self._visit_expr(modifier.dim)
nstr += "]"
if modifier.attributes:
nstr += " " + self._generate_attributes(
modifier.attributes
)
case c_ast.FuncDecl():
if i != 0 and isinstance(modifiers[i - 1], c_ast.PtrDecl):
nstr = "(" + nstr + ")"
args = (
self.visit(modifier.args)
if modifier.args is not None
else ""
)
nstr += "(" + args + ")"
if modifier.attributes:
nstr += " " + self._generate_attributes(
modifier.attributes
)
case c_ast.PtrDecl():
prefix = "*"
if modifier.attributes:
prefix += " " + self._generate_attributes(
modifier.attributes
)
if modifier.quals:
prefix += " " + " ".join(modifier.quals)
separator = (
" "
if nstr and (modifier.attributes or modifier.quals)
else ""
)
nstr = prefix + separator + nstr
if not emit_base_type:
return nstr
if nstr:
s = s.rstrip()
s += (" " if s else "") + nstr
return s
case c_ast.Decl():
return self._generate_decl(n.type)
case c_ast.Typename():
prefix = (
self._generate_attributes(n.attributes) + " "
if n.attributes
else ""
)
return prefix + self._generate_type(
n.type,
emit_declname=emit_declname,
emit_base_type=emit_base_type,
)
case c_ast.IdentifierType():
return " ".join(n.names) + " "
case c_ast.ArrayDecl() | c_ast.PtrDecl() | c_ast.FuncDecl():
return self._generate_type(
n.type,
modifiers + [n],
emit_declname=emit_declname,
emit_base_type=emit_base_type,
)
case _:
return self.visit(n)
def _parenthesize_if(
self, n: c_ast.Node, condition: Callable[[c_ast.Node], bool]
) -> str:
"""Visits 'n' and returns its string representation, parenthesized
if the condition function applied to the node returns True.
"""
s = self._visit_expr(n)
if condition(n):
return "(" + s + ")"
else:
return s
def _parenthesize_unless_simple(self, n: c_ast.Node) -> str:
"""Common use case for _parenthesize_if"""
return self._parenthesize_if(n, lambda d: not self._is_simple_node(d))
def _is_simple_node(self, n: c_ast.Node) -> bool:
"""Returns True for nodes that are "simple" - i.e. nodes that always
have higher precedence than operators.
"""
return isinstance(
n,
(
c_ast.Constant,
c_ast.ID,
c_ast.ArrayRef,
c_ast.StructRef,
c_ast.FuncCall,
c_ast.GenericSelection,
),
)
@@ -0,0 +1,861 @@
# ------------------------------------------------------------------------------
# pycparser: c_lexer.py
#
# CLexer class: lexer for the C language
#
# Eli Bendersky [https://eli.thegreenplace.net/]
# License: BSD
# ------------------------------------------------------------------------------
import re
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
from typing import Literal, assert_never
Dialect = Literal["legacy", "c23"]
@dataclass(slots=True)
class Token:
type: str
value: str
lineno: int
column: int
filename: str
class CLexer:
"""A standalone lexer for C.
Constructor parameters:
error_func:
Called with (msg, line, column) on lexing errors.
dialect:
"legacy" preserves the existing keyword set. "c23" also recognizes
C23 keywords and predefined constants.
Call input(text) to initialize lexing, and then keep calling token() to
get the next token, until it returns None (at end of input).
Non-keyword identifiers are emitted as ID tokens. The parser handles
scopes and classifies typedef names as TYPEID.
"""
def __init__(
self,
error_func: Callable[[str, int, int], None],
*,
dialect: Dialect = "legacy",
) -> None:
if dialect not in ("legacy", "c23"):
raise ValueError(f"Unknown dialect {dialect!r}; expected 'legacy' or 'c23'")
# Select keywords once per lexer. input() resets the scanning state
# without changing this map or affecting lexers using another dialect.
self._keyword_map = _keyword_map.copy()
if dialect == "c23":
self._keyword_map.update(_c23_keywords)
self.error_func = error_func
self._init_state()
def input(self, text: str, filename: str = "") -> None:
"""Initialize the lexer to the given input text.
filename is an optional name identifying the initial file from which the
input comes. The lexer may modify it if #line directives are
encountered.
"""
self._init_state()
self._lexdata = text
self._filename = filename
def _init_state(self) -> None:
self._lexdata = ""
self._filename = ""
self._pos = 0
self._line_start = 0
self._pending_tok: Token | None = None
self._lineno = 1
self._in_attribute = False
@property
def filename(self) -> str:
return self._filename
def token(self) -> Token | None:
# Lexing strategy overview:
#
# - We maintain a current position (self._pos), line number, and the
# byte offset of the current line start. The lexer is a simple loop
# that skips whitespace/newlines and emits one token per call.
#
# - A small amount of logic is handled manually before regex matching:
#
# * Preprocessor-style directives: if we see '#', we check whether
# it's a #line or #pragma directive and consume it inline. #line
# updates lineno/filename and produces no tokens. #pragma can yield
# both PPPRAGMA and PPPRAGMASTR, but token() returns a single token,
# so we stash the PPPRAGMASTR as _pending_tok to return on the next
# token() call. Otherwise we return PPHASH.
# * Newlines update lineno/line-start tracking so tokens can record
# accurate columns.
#
# - The bulk of tokens are recognized in _match_token:
#
# * _regex_rules: regex patterns for identifiers, literals, and other
# complex tokens (including error-producing patterns). The lexer
# uses a combined _regex_master to scan options at the same time.
# * _fixed_tokens: exact string matches for operators and punctuation,
# resolved by longest match.
#
# - Error patterns call the error callback and advance minimally, which
# keeps lexing resilient while reporting useful diagnostics.
text = self._lexdata
n = len(text)
if self._pending_tok is not None:
tok = self._pending_tok
self._pending_tok = None
return tok
while self._pos < n:
match text[self._pos]:
case " " | "\t" | "\f" | "\v":
self._pos += 1
case "\n":
self._lineno += 1
self._pos += 1
self._line_start = self._pos
case "#":
if _line_pattern.match(text, self._pos + 1):
self._pos += 1
self._handle_ppline()
continue
if _pragma_pattern.match(text, self._pos + 1):
self._pos += 1
toks = self._handle_pppragma()
if len(toks) > 1:
self._pending_tok = toks[1]
if len(toks) > 0:
return toks[0]
continue
tok = self._make_token("PPHASH", "#", self._pos)
self._pos += 1
return tok
case _:
if tok := self._match_token():
return tok
else:
continue
def _match_token(self) -> Token | None:
"""Match one token at the current position.
Returns a Token on success, or None if no token could be matched and
an error was reported. This method always advances _pos by the matched
length, or by 1 on error/no-match.
"""
text = self._lexdata
pos = self._pos
# Attribute arguments are balanced token sequences, not C expressions.
# Keep them opaque so the parser does not interpret their contents as
# C expressions or declarations.
if self._in_attribute:
if text[pos] == "(":
return self._attribute_arguments()
if match := _attribute_end.match(text, pos):
self._in_attribute = False
return self._attribute_token("ATTRIBUTE_END", match.end())
# Only run regexp check if we are starting an attribute, to avoid
# costly regex matching for every token.
elif text[pos] == "[" and (match := _attribute_begin.match(text, pos)):
self._in_attribute = True
return self._attribute_token("ATTRIBUTE_BEGIN", match.end())
# We pick the longest match between:
# - the master regex (identifiers, literals, error patterns, etc.)
# - fixed operator/punctuator literals from the bucket for text[pos]
#
# The longest match is required to ensure we properly lex something
# like ".123" (a floating-point constant) as a single entity (with
# FLOAT_CONST), rather than a PERIOD followed by a number.
#
# The fixed-literal buckets are already length-sorted, so within that
# bucket we can take the first match. However, we still compare its
# length to the regex match because the regex may have matched a longer
# token that should take precedence.
best = None
if m := _regex_master.match(text, pos):
tok_type = m.lastgroup
# All master-regex alternatives are named; lastgroup shouldn't be None.
assert tok_type is not None
value = m.group(tok_type)
length = len(value)
action, msg = _regex_actions[tok_type]
best = (length, tok_type, value, action, msg)
if bucket := _fixed_tokens_by_first.get(text[pos]):
for entry in bucket:
if text.startswith(entry.literal, pos):
length = len(entry.literal)
if best is None or length > best[0]:
best = (
length,
entry.tok_type,
entry.literal,
_RegexAction.TOKEN,
None,
)
break
if best is None:
self._error(f"Illegal character {text[pos]!r}", pos)
self._pos += 1
return None
length, tok_type, value, action, msg = best
match action:
case _RegexAction.TOKEN:
pass
case _RegexAction.ERROR:
if tok_type == "BAD_CHAR_CONST":
msg = f"Invalid char constant {value}"
# All other ERROR rules provide a message.
assert msg is not None
self._error(msg, pos)
self._pos += max(1, length)
return None
case _RegexAction.ID:
tok_type = (
"ID" if self._in_attribute else self._keyword_map.get(value, "ID")
)
case _:
assert_never(action)
tok = self._make_token(tok_type, value, pos)
self._pos += length
return tok
def _attribute_token(self, kind: str, end: int) -> Token:
"""Emit the text from _pos up to end and advance lexer state.
Attribute delimiters can contain whitespace, and argument clauses can
span lines. Record the token's starting coordinates before updating
line tracking for the entire consumed span.
"""
value = self._lexdata[self._pos : end]
tok = self._make_token(kind, value, self._pos)
self._lineno += value.count("\n")
if "\n" in value:
self._line_start = self._pos + value.rfind("\n") + 1
self._pos = end
return tok
def _attribute_arguments(self) -> Token | None:
"""Scan an attribute argument clause with _pos at its opening '('.
Return one ATTRIBUTE_ARGUMENTS token containing the raw text inside
the outer parentheses. A stack tracks the expected closing delimiters
for nested (), [], and {}. Comments and quoted literals are skipped
while balancing, but preserved in the token value. This bypasses normal
token classification, keeping arbitrary attribute arguments separate
from C expressions and scope tracking.
Report malformed clauses at the opening parenthesis. If the error
callback returns, consume the remaining input and return None.
"""
text = self._lexdata
start = self._pos
pos = start + 1
closers = [")"]
pairs = {"(": ")", "[": "]", "{": "}"}
error = "Unterminated attribute arguments"
while pos < len(text):
ch = text[pos]
if text.startswith("/*", pos):
end = text.find("*/", pos + 2)
if end < 0:
break
pos = end + 2
continue
if text.startswith("//", pos):
end = text.find("\n", pos + 2)
pos = len(text) if end < 0 else end
continue
# Consume whole identifiers and preprocessing numbers first, so
# digits in literal prefixes aren't confused with numbers and
# digit separators aren't mistaken for character-literal quotes.
if match := _attribute_word.match(text, pos):
pos = match.end()
continue
if ch in {"'", '"'}:
quote = ch
pos += 1
while pos < len(text) and text[pos] not in {quote, "\n"}:
pos += 2 if text[pos] == "\\" else 1
if pos >= len(text) or text[pos] != quote:
break
pos += 1
continue
if ch in pairs:
closers.append(pairs[ch])
elif ch in ")]}":
if ch != closers[-1]:
error = "Mismatched delimiter in attribute arguments"
break
closers.pop()
if not closers:
tok = self._attribute_token("ATTRIBUTE_ARGUMENTS", pos + 1)
tok.value = tok.value[1:-1]
return tok
pos += 1
self._error(error, start)
self._pos = len(text)
return None
def _make_token(self, tok_type: str, value: str, pos: int) -> Token:
"""Create a Token at an absolute input position.
Expects tok_type/value and the absolute byte offset pos in the current
input. Does not advance lexer state; callers manage _pos themselves.
Returns a Token with the current filename and line/column coordinates.
"""
column = pos - self._line_start + 1
tok = Token(tok_type, value, self._lineno, column, self._filename)
return tok
def _error(self, msg: str, pos: int) -> None:
column = pos - self._line_start + 1
self.error_func(msg, self._lineno, column)
def _handle_ppline(self) -> None:
# Since #line directives aren't supposed to return tokens but should
# only affect the lexer's state (update line/filename for coords), this
# method does a bit of parsing on its own. It doesn't return anything,
# but its side effect is to update self._pos past the directive, and
# potentially update self._lineno and self._filename, based on the
# directive's contents.
#
# Accepted #line forms from preprocessors:
# - "#line 66 \"kwas\\df.h\""
# - "# 9"
# - "#line 10 \"include/me.h\" 1 2 3" (extra numeric flags)
# - "# 1 \"file.h\" 3"
# Errors we must report:
# - "#line \"file.h\"" (filename before line number)
# - "#line df" (garbage instead of number/string)
#
# We scan the directive line once (after an optional 'line' keyword),
# validating the order: NUMBER, optional STRING, then any NUMBERs.
# The NUMBERs tail is only accepted if a filename STRING was present.
text = self._lexdata
n = len(text)
line_end = text.find("\n", self._pos)
if line_end == -1:
line_end = n
line = text[self._pos : line_end]
pos = 0
line_len = len(line)
def skip_ws() -> None:
nonlocal pos
while pos < line_len and line[pos] in " \t":
pos += 1
skip_ws()
if line.startswith("line", pos):
pos += 4
def success(pp_line: str | None, pp_filename: str | None) -> None:
if pp_line is None:
self._error("line number missing in #line", self._pos + line_len)
else:
try:
self._lineno = int(pp_line.replace("'", ""))
except ValueError:
self._error("invalid #line directive", self._pos + line_len)
return
if pp_filename is not None:
self._filename = pp_filename
self._pos = line_end + 1
self._line_start = self._pos
def fail(msg: str, offset: int) -> None:
self._error(msg, self._pos + offset)
self._pos = line_end + 1
self._line_start = self._pos
skip_ws()
if pos >= line_len:
success(None, None)
return
if line[pos] == '"':
fail("filename before line number in #line", pos)
return
# Line numbers allow digit separators, but no integer suffixes.
m = re.match(_decimal_digits, line[pos:])
if not m:
fail("invalid #line directive", pos)
return
pp_line = m.group(0)
pos += len(pp_line)
skip_ws()
if pos >= line_len:
success(pp_line, None)
return
if line[pos] != '"':
fail("invalid #line directive", pos)
return
m = re.match(_string_literal, line[pos:])
if not m:
fail("invalid #line directive", pos)
return
pp_filename = m.group(0).lstrip('"').rstrip('"')
pos += len(m.group(0))
# Consume arbitrary sequence of numeric flags after the directive
while True:
skip_ws()
if pos >= line_len:
break
m = re.match(_decimal_digits, line[pos:])
if not m:
fail("invalid #line directive", pos)
return
pos += len(m.group(0))
success(pp_line, pp_filename)
def _handle_pppragma(self) -> list[Token]:
# Parse a full #pragma line; returns a list of tokens with 1 or 2
# tokens - PPPRAGMA and an optional PPPRAGMASTR. If an empty list is
# returned, it means an error occurred, or we're at the end of input.
#
# Examples:
# - "#pragma" -> PPPRAGMA only
# - "#pragma once" -> PPPRAGMA, PPPRAGMASTR("once")
# - "# pragma omp parallel private(th_id)" -> PPPRAGMA, PPPRAGMASTR("omp parallel private(th_id)")
# - "#\tpragma {pack: 2, smack: 3}" -> PPPRAGMA, PPPRAGMASTR("{pack: 2, smack: 3}")
text = self._lexdata
n = len(text)
pos = self._pos
while pos < n and text[pos] in " \t":
pos += 1
if pos >= n:
self._pos = pos
return []
if not text.startswith("pragma", pos):
self._error("invalid #pragma directive", pos)
self._pos = pos + 1
return []
pragma_pos = pos
pos += len("pragma")
toks = [self._make_token("PPPRAGMA", "pragma", pragma_pos)]
while pos < n and text[pos] in " \t":
pos += 1
start = pos
while pos < n and text[pos] != "\n":
pos += 1
if pos > start:
toks.append(self._make_token("PPPRAGMASTR", text[start:pos], start))
if pos < n and text[pos] == "\n":
self._lineno += 1
pos += 1
self._line_start = pos
self._pos = pos
return toks
##
## Reserved keywords
##
_keywords: tuple[str, ...] = (
"AUTO",
"BREAK",
"CASE",
"CHAR",
"CONST",
"CONTINUE",
"DEFAULT",
"DO",
"DOUBLE",
"ELSE",
"ENUM",
"EXTERN",
"FLOAT",
"FOR",
"GOTO",
"IF",
"INLINE",
"INT",
"LONG",
"REGISTER",
"OFFSETOF",
"RESTRICT",
"RETURN",
"SHORT",
"SIGNED",
"SIZEOF",
"STATIC",
"STRUCT",
"SWITCH",
"TYPEDEF",
"UNION",
"UNSIGNED",
"VOID",
"VOLATILE",
"WHILE",
"__BUILTIN_VA_ARG",
"__INT128",
"_BOOL",
"_COMPLEX",
"_GENERIC",
"_NORETURN",
"_THREAD_LOCAL",
"_STATIC_ASSERT",
"_ATOMIC",
"_ALIGNOF",
"_ALIGNAS",
"_PRAGMA",
)
_keyword_map: dict[str, str] = {}
for keyword in _keywords:
# Keywords from new C standard are mixed-case, like _Bool, _Alignas, etc.
if keyword.startswith("_") and len(keyword) > 1 and keyword[1].isalpha():
_keyword_map[keyword[:2].upper() + keyword[2:].lower()] = keyword
else:
_keyword_map[keyword.lower()] = keyword
# Alternate spellings share token types with the existing grammar. The other
# keywords are reserved even where the parser does not yet support their syntax.
_c23_keywords = {
"alignas": "_ALIGNAS",
"alignof": "_ALIGNOF",
"bool": "_BOOL",
"static_assert": "_STATIC_ASSERT",
"thread_local": "_THREAD_LOCAL",
"true": "BOOL_CONST",
"false": "BOOL_CONST",
"nullptr": "NULLPTR",
"constexpr": "CONSTEXPR",
"typeof": "TYPEOF",
"typeof_unqual": "TYPEOF_UNQUAL",
"_BitInt": "_BITINT",
"_Decimal32": "_DECIMAL32",
"_Decimal64": "_DECIMAL64",
"_Decimal128": "_DECIMAL128",
"_Imaginary": "_IMAGINARY",
}
##
## Regexes for use in tokens
##
# valid C identifiers (K&R2: A.2.3), plus '$' (supported by some compilers)
_identifier = r"[a-zA-Z_$][0-9a-zA-Z_$]*"
_attribute_begin = re.compile(r"\[\s*\[")
_attribute_end = re.compile(r"\]\s*\]")
_attribute_word = re.compile(rf"{_identifier}|(?:[0-9]|\.[0-9])(?:[eEpP][+-]|[\w.'])*")
# Accept digit separators in all dialects, retaining them in token values.
# A quote must be between two digits of the appropriate base; it cannot
# touch a prefix, decimal point, exponent marker/sign, or suffix.
_decimal_digits = "[0-9](?:'?[0-9])*"
_hex_prefix = "0[xX]"
_hex_digits = "[0-9a-fA-F](?:'?[0-9a-fA-F])*"
_bin_prefix = "0[bB]"
_bin_digits = "[01](?:'?[01])*"
# integer constants (K&R2: A.2.5.1)
# C23 wb/WB allows u/U before or after it, but no long suffix. Recognize it
# in every dialect and let the parser enforce C23. Keep these alternatives
# ahead of bare u/U so that an unsigned bit-precise suffix is consumed whole.
_integer_suffix_opt = (
r"(([uU](wb|WB))|((wb|WB)[uU]?)|"
r"([uU]ll)|([uU]LL)|(ll[uU]?)|(LL[uU]?)|([uU][lL])|([lL][uU]?)|[uU])?"
)
_decimal_constant = (
"(0" + _integer_suffix_opt + ")|([1-9](?:'?[0-9])*" + _integer_suffix_opt + ")"
)
_octal_constant = "0(?:'?[0-7])*" + _integer_suffix_opt
_hex_constant = _hex_prefix + _hex_digits + _integer_suffix_opt
_bin_constant = _bin_prefix + _bin_digits + _integer_suffix_opt
_bad_octal_constant = "0(?:'?[0-7])*'?[89]"
# comments are not supported
_unsupported_c_style_comment = r"\/\*"
_unsupported_cxx_style_comment = r"\/\/"
# Character constants and escape sequences.
#
# Tokens preserve their source spelling; the lexer does not decode escapes or
# validate Unicode code points. Each complete escape must match as one character
# in _cconst_char, which is used for both single and multicharacter constants.
# Matching is deliberately permissive: simple escapes accept extra letters and
# punctuation, and numeric escapes accept decimal digits, not just octal digits.
#
# _cconst_char is also repeated in the error patterns. If an escape can match in
# several ways, long malformed constants can cause exponential backtracking
# (https://github.com/eliben/pycparser/issues/61). The lookaheads prevent this:
# - Decimal and hexadecimal escapes consume the whole run of digits. For
# example, \123 cannot be split into \1 followed by the characters 2 and 3.
# - A simple \x escape is allowed only when no hexadecimal digit follows it.
# - A complete Unicode escape cannot fall back to a simple \u or \U escape.
# _bad_escape also excludes all decimal digits to agree with this permissiveness.
# Universal character names consume exactly four hex digits after u or eight
# after U. A following hex digit is a separate character, unlike with \x.
_unicode_escape = r"(u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})"
# This lookahead runs immediately after the backslash. If a complete Unicode
# escape follows, only _unicode_escape may match it: \u00e9 cannot be read as
# \u followed by 0, 0, e, 9. Incomplete forms retain the permissive behavior
# of simple escapes.
_simple_escape = (
rf"(?!{_unicode_escape})" r"""([a-wyzA-Z._~!=&\^\-\\?'"]|x(?![0-9a-fA-F]))"""
)
_decimal_escape = r"""(\d+)(?!\d)"""
_hex_escape = r"""(x[0-9a-fA-F]+)(?![0-9a-fA-F])"""
_bad_escape = r"""([\\][^a-zA-Z._~^!=&\^\-\\?'"x0-9])"""
_escape_sequence = (
r"""(\\("""
+ _simple_escape
+ "|"
+ _decimal_escape
+ "|"
+ _hex_escape
+ "|"
+ _unicode_escape
+ "))"
)
# Strings do not need to count the characters represented by escapes. Match
# just the backslash and a permitted following character; _string_char consumes
# any remaining numeric or Unicode digits as ordinary text. This avoids matching
# complete escape sequences repeatedly. The permissive spellings also support
# Windows paths in #line filenames, which are matched with _string_literal.
_escape_sequence_start_in_string = r"""(\\[0-9a-zA-Z._~!=&\^\-\\?'"])"""
_cconst_char = r"""([^'\\\n]|""" + _escape_sequence + ")"
_char_const = "'" + _cconst_char + "'"
_wchar_const = "L" + _char_const
_u8char_const = "u8" + _char_const
_u16char_const = "u" + _char_const
_u32char_const = "U" + _char_const
_multicharacter_constant = "'" + _cconst_char + "{2,4}'"
_unmatched_quote = "('" + _cconst_char + "*\\n)|('" + _cconst_char + "*$)"
_bad_char_const = (
r"""('""" + _cconst_char + """[^'\n]+')|('')|('""" + _bad_escape + r"""[^'\n]*')"""
)
# String literals, including wide and Unicode prefixes.
_string_char = r"""([^"\\\n]|""" + _escape_sequence_start_in_string + ")"
_string_literal = '"' + _string_char + '*"'
_wstring_literal = "L" + _string_literal
_u8string_literal = "u8" + _string_literal
_u16string_literal = "u" + _string_literal
_u32string_literal = "U" + _string_literal
_bad_string_literal = '"' + _string_char + "*" + _bad_escape + _string_char + '*"'
# floating constants (K&R2: A.2.5.3)
_exponent_part = "([eE][-+]?" + _decimal_digits + ")"
_fractional_constant = (
"(("
+ _decimal_digits
+ r")?\."
+ _decimal_digits
+ ")|("
+ _decimal_digits
+ r"\.)"
)
_unsuffixed_floating_constant = (
"((("
+ _fractional_constant
+ ")"
+ _exponent_part
+ "?)|("
+ _decimal_digits
+ _exponent_part
+ "))"
)
# Recognize C23 decimal suffixes in every dialect; the parser rejects them
# outside C23. Each suffix must be entirely lowercase or uppercase.
# The hexadecimal float pattern below keeps only f/F/l/L.
_floating_constant = _unsuffixed_floating_constant + "(df|dd|dl|DF|DD|DL|[FfLl])?"
_binary_exponent_part = "([pP][+-]?" + _decimal_digits + ")"
_hex_fractional_constant = (
"(((" + _hex_digits + r""")?\.""" + _hex_digits + ")|(" + _hex_digits + r"""\.))"""
)
_hex_floating_constant = (
"("
+ _hex_prefix
+ "("
+ _hex_digits
+ "|"
+ _hex_fractional_constant
+ ")"
+ _binary_exponent_part
+ "[FfLl]?)"
)
class _RegexAction(Enum):
TOKEN = 0
ID = 1
ERROR = 2
@dataclass(frozen=True)
class _RegexRule:
# tok_type: name of the token emitted for a match
# regex_pattern: the raw regex (no anchors) to match at the current position
# action: TOKEN for normal tokens, ID for identifiers, ERROR to report
# error_message: message used for ERROR entries
tok_type: str
regex_pattern: str
action: _RegexAction
error_message: str | None
_regex_rules: list[_RegexRule] = [
_RegexRule(
"UNSUPPORTED_C_STYLE_COMMENT",
_unsupported_c_style_comment,
_RegexAction.ERROR,
"Comments are not supported, see https://github.com/eliben/pycparser#3using.",
),
_RegexRule(
"UNSUPPORTED_CXX_STYLE_COMMENT",
_unsupported_cxx_style_comment,
_RegexAction.ERROR,
"Comments are not supported, see https://github.com/eliben/pycparser#3using.",
),
_RegexRule(
"BAD_STRING_LITERAL",
_bad_string_literal,
_RegexAction.ERROR,
"String contains invalid escape code",
),
_RegexRule("WSTRING_LITERAL", _wstring_literal, _RegexAction.TOKEN, None),
_RegexRule("U8STRING_LITERAL", _u8string_literal, _RegexAction.TOKEN, None),
_RegexRule("U16STRING_LITERAL", _u16string_literal, _RegexAction.TOKEN, None),
_RegexRule("U32STRING_LITERAL", _u32string_literal, _RegexAction.TOKEN, None),
_RegexRule("STRING_LITERAL", _string_literal, _RegexAction.TOKEN, None),
_RegexRule("HEX_FLOAT_CONST", _hex_floating_constant, _RegexAction.TOKEN, None),
_RegexRule("FLOAT_CONST", _floating_constant, _RegexAction.TOKEN, None),
_RegexRule("INT_CONST_HEX", _hex_constant, _RegexAction.TOKEN, None),
_RegexRule("INT_CONST_BIN", _bin_constant, _RegexAction.TOKEN, None),
_RegexRule(
"BAD_CONST_OCT",
_bad_octal_constant,
_RegexAction.ERROR,
"Invalid octal constant",
),
_RegexRule("INT_CONST_OCT", _octal_constant, _RegexAction.TOKEN, None),
_RegexRule("INT_CONST_DEC", _decimal_constant, _RegexAction.TOKEN, None),
_RegexRule("INT_CONST_CHAR", _multicharacter_constant, _RegexAction.TOKEN, None),
_RegexRule("CHAR_CONST", _char_const, _RegexAction.TOKEN, None),
_RegexRule("WCHAR_CONST", _wchar_const, _RegexAction.TOKEN, None),
_RegexRule("U8CHAR_CONST", _u8char_const, _RegexAction.TOKEN, None),
_RegexRule("U16CHAR_CONST", _u16char_const, _RegexAction.TOKEN, None),
_RegexRule("U32CHAR_CONST", _u32char_const, _RegexAction.TOKEN, None),
_RegexRule("UNMATCHED_QUOTE", _unmatched_quote, _RegexAction.ERROR, "Unmatched '"),
_RegexRule("BAD_CHAR_CONST", _bad_char_const, _RegexAction.ERROR, None),
_RegexRule("ID", _identifier, _RegexAction.ID, None),
]
_regex_actions: dict[str, tuple[_RegexAction, str | None]] = {}
_regex_pattern_parts: list[str] = []
for _rule in _regex_rules:
_regex_actions[_rule.tok_type] = (_rule.action, _rule.error_message)
_regex_pattern_parts.append(f"(?P<{_rule.tok_type}>{_rule.regex_pattern})")
# The master regex is a single alternation of all token patterns, each wrapped
# in a named group. We match once at the current position and then use
# `lastgroup` to recover which token kind fired; this avoids iterating over all
# regexes on every character while keeping the same token-level semantics.
_regex_master: re.Pattern[str] = re.compile("|".join(_regex_pattern_parts))
@dataclass(frozen=True)
class _FixedToken:
tok_type: str
literal: str
_fixed_tokens: list[_FixedToken] = [
_FixedToken("ELLIPSIS", "..."),
_FixedToken("LSHIFTEQUAL", "<<="),
_FixedToken("RSHIFTEQUAL", ">>="),
_FixedToken("PLUSPLUS", "++"),
_FixedToken("MINUSMINUS", "--"),
_FixedToken("ARROW", "->"),
_FixedToken("LAND", "&&"),
_FixedToken("LOR", "||"),
_FixedToken("LSHIFT", "<<"),
_FixedToken("RSHIFT", ">>"),
_FixedToken("LE", "<="),
_FixedToken("GE", ">="),
_FixedToken("EQ", "=="),
_FixedToken("NE", "!="),
_FixedToken("TIMESEQUAL", "*="),
_FixedToken("DIVEQUAL", "/="),
_FixedToken("MODEQUAL", "%="),
_FixedToken("PLUSEQUAL", "+="),
_FixedToken("MINUSEQUAL", "-="),
_FixedToken("ANDEQUAL", "&="),
_FixedToken("OREQUAL", "|="),
_FixedToken("XOREQUAL", "^="),
_FixedToken("EQUALS", "="),
_FixedToken("PLUS", "+"),
_FixedToken("MINUS", "-"),
_FixedToken("TIMES", "*"),
_FixedToken("DIVIDE", "/"),
_FixedToken("MOD", "%"),
_FixedToken("OR", "|"),
_FixedToken("AND", "&"),
_FixedToken("NOT", "~"),
_FixedToken("XOR", "^"),
_FixedToken("LNOT", "!"),
_FixedToken("LT", "<"),
_FixedToken("GT", ">"),
_FixedToken("CONDOP", "?"),
_FixedToken("LPAREN", "("),
_FixedToken("RPAREN", ")"),
_FixedToken("LBRACKET", "["),
_FixedToken("RBRACKET", "]"),
_FixedToken("LBRACE", "{"),
_FixedToken("RBRACE", "}"),
_FixedToken("COMMA", ","),
_FixedToken("PERIOD", "."),
_FixedToken("SEMI", ";"),
_FixedToken("COLON", ":"),
_FixedToken("COLONCOLON", "::"),
]
# To avoid scanning all fixed tokens on every character, we bucket them by the
# first character. When matching at position i, we only look at the bucket for
# text[i], and we pre-sort that bucket by token length so the first match is
# also the longest. This preserves longest-match semantics (e.g. '>>=' before
# '>>' before '>') while reducing the number of comparisons.
_fixed_tokens_by_first: dict[str, list[_FixedToken]] = {}
for _entry in _fixed_tokens:
_fixed_tokens_by_first.setdefault(_entry.literal[0], []).append(_entry)
for _bucket in _fixed_tokens_by_first.values():
_bucket.sort(key=lambda item: len(item.literal), reverse=True)
_line_pattern: re.Pattern[str] = re.compile(r"([ \t]*line\W)|([ \t]*\d+)")
_pragma_pattern: re.Pattern[str] = re.compile(r"[ \t]*pragma\W")
File diff suppressed because it is too large Load Diff