Two defects that fed each other, fixed together because the first alone would
let the new Field nodes attract bare-name calls.
1. An anonymous aggregate under a typedef had no nodes.
`typedef struct { ... } X;` produced no Class node and none of its members a
Field node: the struct_specifier carries no name of its own, and
extract_class_def gave up on it. That is the dominant C spelling of a type
(37 of the 38 structs in internal/cbm/cbm.h). The enclosing type_definition
now names the aggregate through its declarator; only a plain type_identifier
declarator counts, since a pointer, array or function typedef names another
type. Applies to struct, union and enum specifiers in C and C++ (`.h` is
classified C++).
2. A C member access was resolved by its bare member name.
The member half of `a.b` / `a->b` reached the short-name registry as `b`,
which returned whichever Field (or function, or variable) was called `b`: on
this repository 9,150 USAGE and 2,810 WRITES edges onto Fields from C
sources, 3,976 of the USAGE edges onto a name several structs share.
The C LSP already types the object of a field_expression. It now publishes
the owner as a FIELD_REFERENCE row, one per (enclosing function, object type,
field): the pipeline edge is function -> Field, so a row per occurrence would
only add a record per member access. The usage and read/write resolvers
(sequential and parallel) join those rows by function and member name; a C
member name with no typed owner binds nothing.
Four things had to hold for the owner to be found:
- the cross-file registry folded Field defs into their owning type for Go
only, so every C/C++ struct from another file (every header) registered
with zero fields. The fold now covers C and C++;
- c_register_lsp_defs kept the first 63 fields of a type. It is sized from
the list now: a cap there silently dropped edges onto later fields;
- a struct is registered under the header that declares it while the use
site knows its short name. The owner type resolves as written, then
module-qualified, then by short name from this module, a directly included
header, or as the single candidate. Two same-named structs with no
evidence between them resolve to nothing;
- the parallel resolver ran the cross-file pass only for files with
unqualified call sites. A C member access or member call that still has no
owner now requests it too.
Calls landing on a Field follow the same rule (cbm_call_onto_field_policy):
dropped across languages (the #1928 reference rule; a Bash `command` or a
Python `sorted()` is not a C struct member), dropped for a bare C call
(`socket(...)`, a function-pointer parameter), and bound by owner for a C or
C++ member call. Bare C++ calls and every other language are unchanged.
Measured on this repository, old binary vs new:
C/H Class nodes 155 -> 1000
C/H Field nodes 1047 -> 5478
enumerators of formerly anonymous enums +911 Variable nodes
USAGE onto the new types, enums and constants +23k edges
USAGE -> Field from .c, owner type named in the file 58.1% -> 97.6%
USAGE onto a non-Field, name only used as a member 146 -> 0
WRITES -> Field from .c 2981 -> 6534
CALLS 159 removed, 3 added
index wall time unchanged within noise; peak footprint +2.8%
full macOS suite: 8309 passed, 0 failed, 10 skipped
Tests: extract_c_anonymous_typedef_aggregate_is_named_by_its_typedef,
pipeline_c_member_access_binds_field_by_object_type (+ _parallel),
call_onto_field_follows_language_and_shape,
c_member_access_binds_by_owner_only. Each was RED on the unchanged tree; the
pipeline tests are RED again with the call policy neutralized.
Not in this change: a function-pointer member declared inline
(`void (*f)(int);`) has no Field node; a `p->f()` call is not resolved (the
registry splits on `.` and `::` only); inline functions in `.h` files keep
the bare-name member binding; designated initializers are not typed.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>