feat: establish first-class lexical language parity #2

Merged
trevon merged 14 commits from feat/language-parity-foundation into bench/yams-architecture-reproducibility 2026-08-30 22:09:40 +00:00
5 changed files with 95 additions and 12 deletions
Showing only changes of commit 664e503719 - Show all commits

feat(scan): separate C and C++ architecture forms

trvon 2026-08-30 15:48:10 -06:00
Signed by: trevon
GPG key ID: 38686EA797D7A8A7

View file

@ -9,14 +9,19 @@ The first-class parity targets are Dart, Rust, C/C++, and
JavaScript/TypeScript. Python and Swift remain secondary until their extraction
contracts and fixtures reach the same standard.
Plain `.h` files remain classified as C++ because their language cannot be
determined from the extension alone. C projects that require header symbol
resolution need compilation-database or include-context evidence in a future
frontend; Brick does not guess that ownership.
## Current coverage
| Language | Useful current coverage | Important gaps | Tier |
| --- | --- | --- | --- |
| Dart | Classes, mixins, enums, extensions, methods, named constructors, accessors, import/export/part directives, `extends`/`with`/`implements`/`on`, and ordinary calls | operators, generic relationship targets, conditional directives, and multiline declarations | first-class target |
| Rust | Structs, enums, traits, ordinary/async/const/unsafe/extern functions, simple inherent/trait impl ownership, ordinary/turbofish/macro calls, braced `use` resolution, and raw-string masking | module ownership, unions/type aliases, nested generic impl headers, glob imports, and macro definitions | first-class target |
| C | Files, basic includes, structs, and narrow function forms through the shared C/C++ profile | distinct C identity, enums/typedefs/function pointers, multiline declarations, macros, and declaration masking | first-class target |
| C++ | Basic classes/structs, narrow function forms, calls, and local include resolution | templates, constructors/destructors/operators, multiple inheritance, namespaces, overloads, lambdas, qualifiers, and multiline declarations | first-class target |
| C | Distinct `.c` identity, includes, structs/unions/enums, narrow function forms, and ordinary calls | anonymous typedef aggregates, function pointers, C23 attributes, macros, and multiline declarations | first-class target |
| C++ | Classes/structs/unions/enums, `final`, constructors/destructors/operators, override-qualified methods, multiple inheritance, ordinary calls, and local include resolution | namespaces, overload disambiguation, lambdas, concepts/requires, complex template bases, and multiline declarations | first-class target |
| JavaScript | Named functions, simple classes/extends, ES imports, and call-shaped expressions | methods, arrows/function expressions, CommonJS, dynamic imports, re-exports, decorators, and template literals | first-class target |
| TypeScript | JavaScript basics plus TypeScript file extensions and simple imports | interfaces, type aliases, enums/namespaces, generic/abstract declarations, overloads, decorators, and path mappings | first-class target |
| Python | Basic classes, functions/async functions, imports, calls, and indentation scopes | multiple bases, qualified ownership, decorators, multiline strings, relative/dynamic imports, and complete multi-import handling | secondary |

View file

@ -316,7 +316,7 @@ impl ScanState {
.flat_map(|names| names.as_str().split(','))
.map(str::trim)
.filter(|name| !name.is_empty())
.map(clean_target)
.map(|name| clean_relationship_target(name, language))
.collect();
if bases.is_empty()
&& let Some(base) = captures.name("base")
@ -406,7 +406,8 @@ impl ScanState {
let member_call = prefix.ends_with('.') || prefix.ends_with("->");
let definition_call_name = definition_name
.as_deref()
.and_then(|name| name.rsplit('.').next());
.and_then(|name| name.rsplit('.').next())
.map(|name| name.trim_start_matches('~'));
if definition_call_name != Some(target) && !is_keyword(target) {
self.pending.push(PendingRelationship {
source: source.clone(),
@ -645,6 +646,22 @@ impl Patterns {
r"^\s*use\s+(?P<name>[A-Za-z_][\w:]*)",
None,
),
"c" => (
r"^\s*(?:typedef\s+)?(?:struct|union|enum)\s+(?P<name>[A-Za-z_]\w*)",
vec![r"^\s*(?:[\w*&]+\s+)+(?P<name>[A-Za-z_]\w*)\s*\([^;{}]*\)\s*\{?\s*$"],
r#"^\s*#\s*include\s*[<\"](?P<name>[^>\"]+)[>\"]"#,
None,
),
"cpp" => (
r"^\s*(?:class|struct|union|enum(?:\s+class)?)\s+(?P<name>[A-Za-z_]\w*)(?:\s+final)?",
vec![
r"^\s*(?:[\w:<>,*&]+\s+)+(?P<name>[A-Za-z_]\w*(?:::\w+)*)\s*\([^;{}]*\)\s*(?:const\s*)?(?:noexcept\s*)?(?:(?:override|final)\s*)*(?:\{|=\s*(?:default|delete)\s*;)?\s*$",
r"^\s*(?P<name>~?[A-Za-z_]\w*)\s*\([^;{}]*\)\s*(?::[^{}]*)?\s*(?:noexcept\s*)?(?:\{\s*\}|\{|=\s*(?:default|delete)\s*;)\s*$",
r"^\s*(?:[\w:<>,*&]+\s+)+(?P<name>operator\s*(?:[^\s(]+|\(\)))\s*\([^;{}]*\)\s*(?:const\s*)?(?:noexcept\s*)?(?:\{|=\s*(?:default|delete)\s*;)\s*$",
],
r#"^\s*#\s*include\s*[<\"](?P<name>[^>\"]+)[>\"]"#,
Some(r":\s*(?P<names>[^\{]+)"),
),
"javascript" | "typescript" => (
r"^\s*(?:export\s+)?class\s+(?P<name>[A-Za-z_$][\w$]*)(?:\s+extends\s+(?P<base>[A-Za-z_$][\w$]*))?",
vec![r"^\s*(?:export\s+)?(?:async\s+)?function\s+(?P<name>[A-Za-z_$][\w$]*)\s*\("],
@ -707,7 +724,8 @@ fn language_for(path: &Path) -> Option<&'static str> {
match path.extension()?.to_str()? {
"rs" => Some("rust"),
"py" => Some("python"),
"c" | "h" | "cc" | "cpp" | "cxx" | "hh" | "hpp" | "hxx" => Some("cpp"),
"c" => Some("c"),
"h" | "cc" | "cpp" | "cxx" | "hh" | "hpp" | "hxx" => Some("cpp"),
"js" | "jsx" | "mjs" | "cjs" => Some("javascript"),
"ts" | "tsx" | "mts" | "cts" => Some("typescript"),
"swift" => Some("swift"),
@ -794,6 +812,19 @@ fn clean_import_target(value: &str, language: &str) -> String {
}
}
fn clean_relationship_target(value: &str, language: &str) -> String {
let target = clean_target(value);
if language == "cpp" {
target
.split_whitespace()
.filter(|part| !matches!(*part, "public" | "protected" | "private" | "virtual"))
.collect::<Vec<_>>()
.join(" ")
} else {
target
}
}
fn import_keys(name: &str, language: &str) -> Vec<String> {
let mut keys = vec![name.to_owned()];
match language {

View file

@ -4,6 +4,15 @@ struct Worker {
int state;
};
union Payload {
int integer;
double decimal;
};
enum State {
ready
};
int start() {
return helper();
}

View file

@ -1,10 +1,19 @@
#include "support.hpp"
class Worker : public Base {
class Base {};
class Runnable {};
template <typename T>
class Worker final : public Base, private Runnable {
public:
void start() {
Worker() {}
~Worker() {}
void start() override {
helper();
}
Worker& operator=(const Worker&) = default;
};
int helper() {

View file

@ -42,8 +42,8 @@ fn first_class_language_fixtures_have_stable_baseline_evidence() {
},
FixtureCase {
directory: "c",
language: "cpp",
classes: &["Worker"],
language: "c",
classes: &["Payload", "State", "Worker"],
functions: &["helper", "start"],
imports: 1,
inherits: 0,
@ -52,10 +52,10 @@ fn first_class_language_fixtures_have_stable_baseline_evidence() {
FixtureCase {
directory: "cpp",
language: "cpp",
classes: &["Worker"],
functions: &["helper", "start"],
classes: &["Base", "Runnable", "Worker"],
functions: &["Worker", "helper", "operator=", "start", "~Worker"],
imports: 1,
inherits: 1,
inherits: 2,
calls: 1,
},
FixtureCase {
@ -112,6 +112,35 @@ fn rust_fixture_models_impl_ownership_and_local_imports() {
);
}
#[test]
fn cpp_fixture_models_member_ownership_without_declaration_calls() {
let root = fixture_root("cpp");
let building = scan(&root).unwrap();
let worker = building
.nodes
.iter()
.find(|node| node.kind == NodeKind::Class && node.name == "Worker")
.unwrap();
let members: Vec<_> = building
.nodes
.iter()
.filter(|node| node.kind == NodeKind::Function && node.parent.as_ref() == Some(&worker.id))
.map(|node| node.name.as_str())
.collect();
assert_eq!(members.len(), 4);
assert!(building.edges.iter().all(|edge| {
edge.kind != EdgeKind::Calls
|| !building.nodes.iter().any(|node| {
node.id == edge.target
&& matches!(
node.name.as_str(),
"Worker" | "~Worker" | "operator=" | "start"
)
})
}));
}
fn assert_fixture(case: FixtureCase) {
let root = fixture_root(case.directory);
let building = scan(&root).unwrap();