Files
clang-p2996/clang/lib/StaticAnalyzer
Tom Honermann 8fb42300a0 [SYCL] AST support for SYCL kernel entry point functions. (#122379)
A SYCL kernel entry point function is a non-member function or a static
member function declared with the `sycl_kernel_entry_point` attribute.
Such functions define a pattern for an offload kernel entry point
function to be generated to enable execution of a SYCL kernel on a
device. A SYCL library implementation orchestrates the invocation of
these functions with corresponding SYCL kernel arguments in response to
calls to SYCL kernel invocation functions specified by the SYCL 2020
specification.

The offload kernel entry point function (sometimes referred to as the
SYCL kernel caller function) is generated from the SYCL kernel entry
point function by a transformation of the function parameters followed
by a transformation of the function body to replace references to the
original parameters with references to the transformed ones. Exactly how
parameters are transformed will be explained in a future change that
implements non-trivial transformations. For now, it suffices to state
that a given parameter of the SYCL kernel entry point function may be
transformed to multiple parameters of the offload kernel entry point as
needed to satisfy offload kernel argument passing requirements.
Parameters that are decomposed in this way are reconstituted as local
variables in the body of the generated offload kernel entry point
function.

For example, given the following SYCL kernel entry point function
definition:
```
template<typename KernelNameType, typename KernelType>
[[clang::sycl_kernel_entry_point(KernelNameType)]]
void sycl_kernel_entry_point(KernelType kernel) {
  kernel();
}
```

and the following call:
```
struct Kernel {
  int dm1;
  int dm2;
  void operator()() const;
};
Kernel k;
sycl_kernel_entry_point<class kernel_name>(k);
```

the corresponding offload kernel entry point function that is generated
might look as follows (assuming `Kernel` is a type that requires
decomposition):
```
void offload_kernel_entry_point_for_kernel_name(int dm1, int dm2) {
  Kernel kernel{dm1, dm2};
  kernel();
}
```

Other details of the generated offload kernel entry point function, such
as its name and calling convention, are implementation details that need
not be reflected in the AST and may differ across target devices. For
that reason, only the transformation described above is represented in
the AST; other details will be filled in during code generation.

These transformations are represented using new AST nodes introduced
with this change. `OutlinedFunctionDecl` holds a sequence of
`ImplicitParamDecl` nodes and a sequence of statement nodes that
correspond to the transformed parameters and function body.
`SYCLKernelCallStmt` wraps the original function body and associates it
with an `OutlinedFunctionDecl` instance. For the example above, the AST
generated for the `sycl_kernel_entry_point<kernel_name>` specialization
would look as follows:
```
FunctionDecl 'sycl_kernel_entry_point<kernel_name>(Kernel)'
  TemplateArgument type 'kernel_name'
  TemplateArgument type 'Kernel'
  ParmVarDecl kernel 'Kernel'
  SYCLKernelCallStmt
    CompoundStmt
      <original statements>
    OutlinedFunctionDecl
      ImplicitParamDecl 'dm1' 'int'
      ImplicitParamDecl 'dm2' 'int'
      CompoundStmt
        VarDecl 'kernel' 'Kernel'
          <initialization of 'kernel' with 'dm1' and 'dm2'>
        <transformed statements with redirected references of 'kernel'>
```

Any ODR-use of the SYCL kernel entry point function will (with future
changes) suffice for the offload kernel entry point to be emitted. An
actual call to the SYCL kernel entry point function will result in a
call to the function. However, evaluation of a `SYCLKernelCallStmt`
statement is a no-op, so such calls will have no effect other than to
trigger emission of the offload kernel entry point.

Additionally, as a related change inspired by code review feedback,
these changes disallow use of the `sycl_kernel_entry_point` attribute
with functions defined with a _function-try-block_. The SYCL 2020
specification prohibits the use of C++ exceptions in device functions.
Even if exceptions were not prohibited, it is unclear what the semantics
would be for an exception that escapes the SYCL kernel entry point
function; the boundary between host and device code could be an implicit
noexcept boundary that results in program termination if violated, or
the exception could perhaps be propagated to host code via the SYCL
library. Pending support for C++ exceptions in device code and clear
semantics for handling them at the host-device boundary, this change
makes use of the `sycl_kernel_entry_point` attribute with a function
defined with a _function-try-block_ an error.
2025-01-22 16:39:08 -05:00
..

//===----------------------------------------------------------------------===//
// Clang Static Analyzer
//===----------------------------------------------------------------------===//

= Library Structure =

The analyzer library has two layers: a (low-level) static analysis
engine (ExprEngine.cpp and friends), and some static checkers
(*Checker.cpp).  The latter are built on top of the former via the
Checker and CheckerVisitor interfaces (Checker.h and
CheckerVisitor.h).  The Checker interface is designed to be minimal
and simple for checker writers, and attempts to isolate them from much
of the gore of the internal analysis engine.

= How It Works =

The analyzer is inspired by several foundational research papers ([1],
[2]).  (FIXME: kremenek to add more links)

In a nutshell, the analyzer is basically a source code simulator that
traces out possible paths of execution.  The state of the program
(values of variables and expressions) is encapsulated by the state
(ProgramState).  A location in the program is called a program point
(ProgramPoint), and the combination of state and program point is a
node in an exploded graph (ExplodedGraph).  The term "exploded" comes
from exploding the control-flow edges in the control-flow graph (CFG).

Conceptually the analyzer does a reachability analysis through the
ExplodedGraph.  We start at a root node, which has the entry program
point and initial state, and then simulate transitions by analyzing
individual expressions.  The analysis of an expression can cause the
state to change, resulting in a new node in the ExplodedGraph with an
updated program point and an updated state.  A bug is found by hitting
a node that satisfies some "bug condition" (basically a violation of a
checking invariant).

The analyzer traces out multiple paths by reasoning about branches and
then bifurcating the state: on the true branch the conditions of the
branch are assumed to be true and on the false branch the conditions
of the branch are assumed to be false.  Such "assumptions" create
constraints on the values of the program, and those constraints are
recorded in the ProgramState object (and are manipulated by the
ConstraintManager).  If assuming the conditions of a branch would
cause the constraints to be unsatisfiable, the branch is considered
infeasible and that path is not taken.  This is how we get
path-sensitivity.  We reduce exponential blow-up by caching nodes.  If
a new node with the same state and program point as an existing node
would get generated, the path "caches out" and we simply reuse the
existing node.  Thus the ExplodedGraph is not a DAG; it can contain
cycles as paths loop back onto each other and cache out.

ProgramState and ExplodedNodes are basically immutable once created.  Once
one creates a ProgramState, you need to create a new one to get a new
ProgramState.  This immutability is key since the ExplodedGraph represents
the behavior of the analyzed program from the entry point.  To
represent these efficiently, we use functional data structures (e.g.,
ImmutableMaps) which share data between instances.

Finally, individual Checkers work by also manipulating the analysis
state.  The analyzer engine talks to them via a visitor interface.
For example, the PreVisitCallExpr() method is called by ExprEngine
to tell the Checker that we are about to analyze a CallExpr, and the
checker is asked to check for any preconditions that might not be
satisfied.  The checker can do nothing, or it can generate a new
ProgramState and ExplodedNode which contains updated checker state.  If it
finds a bug, it can tell the BugReporter object about the bug,
providing it an ExplodedNode which is the last node in the path that
triggered the problem.

= Notes about C++ =

Since now constructors are seen before the variable that is constructed
in the CFG, we create a temporary object as the destination region that
is constructed into. See ExprEngine::VisitCXXConstructExpr().

In ExprEngine::processCallExit(), we always bind the object region to the
evaluated CXXConstructExpr. Then in VisitDeclStmt(), we compute the
corresponding lazy compound value if the variable is not a reference, and
bind the variable region to the lazy compound value. If the variable
is a reference, just use the object region as the initializer value.

Before entering a C++ method (or ctor/dtor), the 'this' region is bound
to the object region. In ctors, we synthesize 'this' region with
CXXRecordDecl*, which means we do not use type qualifiers. In methods, we
synthesize 'this' region with CXXMethodDecl*, which has getThisType()
taking type qualifiers into account. It does not matter we use qualified
'this' region in one method and unqualified 'this' region in another
method, because we only need to ensure the 'this' region is consistent
when we synthesize it and create it directly from CXXThisExpr in a single
method call.

= Working on the Analyzer =

If you are interested in bringing up support for C++ expressions, the
best place to look is the visitation logic in ExprEngine, which
handles the simulation of individual expressions.  There are plenty of
examples there of how other expressions are handled.

If you are interested in writing checkers, look at the Checker and
CheckerVisitor interfaces (Checker.h and CheckerVisitor.h).  Also look
at the files named *Checker.cpp for examples on how you can implement
these interfaces.

= Debugging the Analyzer =

There are some useful command-line options for debugging.  For example:

$ clang -cc1 -help | grep analyze
 -analyze-function <value>
 -analyzer-display-progress
 -analyzer-viz-egraph-graphviz
 ...

The first allows you to specify only analyzing a specific function.
The second prints to the console what function is being analyzed.  The
third generates a graphviz dot file of the ExplodedGraph.  This is
extremely useful when debugging the analyzer and viewing the
simulation results.

Of course, viewing the CFG (Control-Flow Graph) is also useful:

$ clang -cc1 -analyzer-checker-help-developer

 -analyzer-checker=debug.DumpCFG   Display Control-Flow Graphs
 -analyzer-checker=debug.ViewCFG   View Control-Flow Graphs using GraphViz
(outdated below?)
 -cfg-add-implicit-dtors           Add C++ implicit destructors to CFGs for all analyses
 -cfg-add-initializers             Add C++ initializers to CFGs for all analyses
 -unoptimized-cfg                  Generate unoptimized CFGs for all analyses

debug.DumpCFG dumps a textual representation of the CFG to the console, and
debug.ViewCFG creates a GraphViz representation.

= References =

[1] Precise interprocedural dataflow analysis via graph reachability,
    T Reps, S Horwitz, and M Sagiv, POPL '95,
    http://portal.acm.org/citation.cfm?id=199462

[2] A memory model for static analysis of C programs, Z Xu, T
    Kremenek, and J Zhang, http://lcs.ios.ac.cn/~xzx/memmodel.pdf