Skip to content

Scratch/riel/identity v12 - #107

Merged
rikvanriel merged 10 commits into
facebookexperimental:mainfrom
rikvanriel:scratch/riel/identity-v12
Nov 7, 2026
Merged

rikvanriel merged 10 commits into
facebookexperimental:mainfrom
rikvanriel:scratch/riel/identity-v12

Conversation

@rikvanriel

Copy link
Copy Markdown
Contributor

Semcode conflated same-named functions across lines, overloads, preprocessor arms, and languages because the functions merge key couldn't tell them apart. This series bumps the schema to v12 and fixes identity end to end: line_start joins the merge key so definitions on different lines coexist while declarations still fold into the definition they announce; each language fills qualifiers in its own spelling ( Rust modules, C++ namespaces/classes, Python pkg.mod.Class, Zig mod.Struct) with the C++ frontend moving to tree-sitter-cpp; declarations match definitions by canonicalised parameter types; preprocessor arms are recorded as written; callee spellings are stored verbatim in typed edges with this/self calls tagged by their enclosing owner; and each extractor records its version with header routing. Verified with cargo fmt --check and cargo clippy --all-targets clean and the full suite passing

Version 12 changes what a row means: line_start joins the functions
merge key and types gains a qualifier column, so rows an older build
wrote cannot be merged correctly and start again. The drop happens
only when a processed record still names an older extractor, which
proves old rows are in the tables; a lost mark on its own restamps
without dropping, so current rows are never left without the files
that produced them. The staleness decision is taken before the
processed migration empties the table: a real v11 index carries an
Int64 extractor_version, which the migration drops, so checking after
it would find nothing and keep stale rows beside their re-read
replacements. Covered by a test rebuilding the real v11 Int64 shape
and asserting both tables start again.
Two definitions at different lines are two rows now: the merge key
grows line_start, and the within-file dedup folds a declaration into
its same-key definition -- unconditionally, since no language here
overloads -- while never merging two definitions. Names differ between
a prototype and its body anyway, so `baz()` meets `baz(void)` and
`int a[]` meets `int *a`.
Rows that share a guard and qualifier coexist instead of collapsing;
the chain walk learns the same distinction, gathering only arms whose
guard differs from the chosen row's. one_per_file keeps one voice per
file per owner by line order, so a stub arm with a body still cannot
outrank the file's first definition; arms beside it carry the guard
rule instead, with siblings reported next to the chosen row rather
than dropped.
C++ files now parse with the C++ grammar, so member declarations the
C grammar never read are extracted. .cpp and friends map to a new
Language::Cpp with its own parser, queries and header routing;
headers stay C by default, reading as C++ only where the index
records it.
Rust keeps its module-and-owner qualifier. C++ takes the owner from
the qualified-id out of line and from the enclosing namespaces and
classes in line, keeping explicit-specialisation arguments and
stripping a primary template's own parameters so the definition folds
into its declaration. Python carries pkg.mod.Class with <locals> for
nested functions, Zig carries mod.Struct, and C carries none.
Only C++ overloads, so only there does a declaration match the
definition it announces, comparing canonicalised parameter types
(*/\& spacing folded away); an overload with no body in this file
survives beside the definitions. C++ reference declarators carry no
declarator field, so the name is stripped from them explicitly:
const Box &o in the class folds into const Box& o out of line.

A declarator returning a pointer or a reference wraps the function
(char *name(), S &self()), so each arm repeats its declarator bare
and wrapped in a pointer once and twice and in a reference; the name,
return type and parameters capture identically in each, and the
qualified-id scope read walks through the wrapper the same way. Both
declaration arms capture the return type instead of storing the void
default. A brace inside the parameter list is a default argument
rather than a body, so the prototype test ignores parenthesised
groups; a const or volatile after the last * or & is top-level and
canonicalises away.

A friend is a non-member: the enclosing walk skips the class a friend
is written in, so the declaration takes the namespace owner and folds
into its definition. A conversion operator has no operator_name, so
the operator_cast itself is captured as the name with its target as
the return type. A function-try-block body reads as a definition, and
reference-to-pointer declarators (int *&slot()) have their own arm. A
C++20 constrained declaration keeps its requirement braces outside
every parenthesis and still reads as a definition; that limit is
documented and pinned.
A types row gains a qualifier column in its merge key, filled from
the same owner function that writes functions.qualifier: one file's
ns1::Box and ns2::Box coexist, and a receiver type joins to its
methods' qualifier. The column is appended and read by name, so
older rows read back without it.
Rust reads its #[cfg] predicates from the attributes above each
item, C++ shares the C preprocessor reading, Python records if
conditions and try arms opaquely, and Zig records comptime
conditions. Guards filter only: nothing but Kconfig atoms ever
contributes a fact, so opaque guards never prune a chain.
A call stores as written: qualified paths and template arguments
kept, so the full path matches first and the last segment after.
The calls column becomes an array of typed edges; this commit writes
plain spellings only, and lookups project each edge to the callee it
names.
The sole rewrite is Rust Self::, resolved through the enclosing impl.
A member call through this or self, and a bare call in scope, carries
its enclosing owner beside it in the same JSON column; lookups
project the tag away or read both halves, with arguments optional.
Each file records the version of the language that read it, so a
tree-sitter bump re-reads only its language while shared logic
still moves them all together. A header records the language it
was read as, which is what makes flipping the routing rescan the
headers; the run's choice is recorded beside the other build
options in schema_meta. Header routing is not configurable yet:
the analyzer always reads .h as C, so the record pins false, the
same value the row versions consult. When the option is wired,
plumb that value to both the analyzer and the record.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 7, 2026
@rikvanriel
rikvanriel merged commit fec42ee into facebookexperimental:main Nov 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant