Skip to content

[DRAFT] [clang] translate block-scope enum, function and local label declarations - #2153

Draft
VladimirMakaev wants to merge 1 commit into
facebook:mainfrom
VladimirMakaev:clang-block-scope-decls
Draft

VladimirMakaev wants to merge 1 commit into
facebook:mainfrom
VladimirMakaev:clang-block-scope-decls

Conversation

@VladimirMakaev

@VladimirMakaev VladimirMakaev commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

A block-scope DeclStmt of an unhandled kind (an enum, a function declaration, _Static_assert,
__label__, C++20 using enum) raised Unimplemented, so the whole enclosing function was
dropped and nothing was reported in it. For example (from c/bufferoverrun/trivial.c):

void block_scope_enum_array_size_Bad(void) {
  enum { kLen = 4 }; // before: Unimplemented, the whole function was dropped
  char buf[kLen];
  buf[kLen] = 0;     // Infer missed: buffer overrun (offset 4, size 4)
                     // now: BUFFER_OVERRUN_L1 reported here
}

The fix:

  • Enum and function declarations go to collect_all_decl, which still initializes variables
    declared in the same statement; the other kinds are no-ops.
  • ClangPointers records the enum of each enum constant, so an unknown constant no longer
    evaluates to 0 (also fixes enums nested in structs not yet translated).
  • The AST exporter exports the label declaration for label statements, and the frontend
    identifies labels by it, so local labels with the same name get distinct CFG nodes.

Test plan

New tests in c/pulse/enum.c, c/pulse/frontend.c, c/bufferoverrun/trivial.c,
cpp/pulse/frontend.cpp and cpp/pulse-20/using_enum.cpp: the bad cases are now reported and
the ok controls stay silent. New frontend tests block_scope_enum.c and local_labels.c check
enum values and one CFG node per local label. The C, C++, Java, Kotlin and SIL codetoanalyze tests
pass; the AST exporter ObjCTest.m expected outputs are updated for the label change.

…ions

The clang frontend translates a `DeclStmt` according to its first
declaration. Variables, bindings and records were translated, typedefs,
using declarations and namespace aliases were no-ops, and any other kind
raised `Unimplemented`. That aborted the translation of the whole
enclosing function, which was then replaced by a bodyless declaration:
nothing was reported in it, not even for the statements before the
declaration, and its callers saw an unknown callee. The kinds that reach
this in ordinary code are enums (`enum { kLen = 4 };`), function
declarations (`extern int helper(int);`, or `X x();` in C++),
`_Static_assert` (also `static_assert` in C once `<assert.h>` is
included), GNU local labels (`__label__ l;`) and C++20 `using enum`.
For instance, no overrun was reported in

    void f(void) {
      enum { kLen = 4 };
      char buf[kLen];
      buf[kLen] = 0;
    }

Enum and function declarations now go to `collect_all_decl`, which skips
declarations that are not variables but still initializes the variables
that can follow them in the same statement (`enum E { A } e = A;`,
`int g(void), x = 3;`). Static assertions, local label declarations and
using-enum declarations are no-ops. Other unknown kinds still raise
`Unimplemented`.

Local labels need more than a no-op. They exist so that a macro can
define a label in each of its expansions, which gives distinct labels
with the same name in one function:

    #define FREE_UNLESS(cond, p) \
      do { __label__ skip; if (cond) goto skip; free(p); skip:; } while (0)

    FREE_UNLESS(c, p);
    FREE_UNLESS(c, q);

The frontend identified labels by name, so both `skip` labels would
share one CFG node, whose successor would be the code after only one of
them: here a false double free of `q`, and no path to the exit of the
function. A goto statement already exports the declaration of its
label, so the AST exporter now exports it for label statements too, and
the frontend identifies labels by that declaration.

Routing block-scope enums is not enough on its own either. The values of
enum constants are looked up in a map that only contains the enums
added to it so far, and an unknown constant evaluates to 0. Enums are
added when they are declared at the top level, when the record that
contains them is translated, or when their type is translated. In C,
enum constants have type `int`, so using one does not add its enum, and
`buf[kLen]` above would become `buf[0]`. The same already happened,
without any abort, for an enum nested in a struct that is not
translated before the constant is used: a block-scope struct
(statements are translated last to first, so a use of the constant is
translated before any variable of that type is declared), or a struct
declared in a header other than the one of the current source file.

`ClangPointers` now records the enum that declares each enum constant
while it visits the AST, and `get_enum_constant_expr` adds the enum of
an unknown constant to the map before looking the constant up again.

Some cases remain. The AST exporter does not export enums declared
inside a type name (`sizeof(enum { N = 1 })`) or a parameter list, so
their constants still evaluate to 0. And as for top-level enums, the
value of a constant is computed by translating its initializer, so
initializers other than literal arithmetic (`?:`, calls to `constexpr`
functions, static members) still give an unknown value. Exporting the
value clang computes for each constant would fix both.

## Test plan

New tests, whose `_bad`/`_Bad` cases are not reported on main (the
function is dropped or the constant evaluates to 0), while the `_ok` and
`_Good` controls stay silent:
- c/pulse/enum.c: values of block-scope enum constants, an enum and a
  variable declared in the same statement, a typedef of a block-scope
  enum, an enum in a block-scope struct, an enum in a struct declared in
  another header, statements before an enum declaration
- c/pulse/frontend.c: a block-scope function declaration (resolved to
  the definition that follows), a function and a variable declared in
  the same statement, `_Static_assert`, `__label__`, and two expansions
  of a macro with a local label (no false double free, and a real double
  free after them is found)
- c/bufferoverrun/trivial.c: array indexed with a block-scope enum
  constant
- c/frontend/enumeration/block_scope_enum.c: the CFG shows the values of
  block-scope enum constants
- c/frontend/gotostmt/local_labels.c: the CFG has one node per local
  label, also when local labels share a name with each other or with a
  label of the function
- cpp/pulse/frontend.cpp (also issues.exp-11): block-scope enum class,
  function declaration from the "most vexing parse"
- cpp/pulse-20/using_enum.cpp: `using enum`

Ran the C, C++, Java, Kotlin and SIL codetoanalyze tests: the only
expected output changes are the new tests. The ObjC tests were not run;
none of them has a block-scope declaration of the newly handled kinds,
a local label or an enum nested in a record.

The only AST exporter test with a label statement is ObjCTest.m, which
needs the macOS SDK. I ran the old and the new plugin on it on Linux:
the outputs differ only in the label statement, which now has the same
record as the goto statement before it. I applied that change to the
Json and Yojson expected outputs, and to the Biniou one by hand
(`bdump` was not available), copying the encoding of the goto
statement's record. The Json and Yojson outputs of the other exporter
tests are unchanged.
@meta-cla meta-cla Bot added the CLA Signed label Oct 2, 2026
@VladimirMakaev VladimirMakaev changed the title [clang] translate block-scope enum, function and local label declarations [DRAFT] [clang] translate block-scope enum, function and local label declarations Oct 2, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant