Skip to content

Commit 314ebda

Browse files
committed
接通C与C++统一原生差分并保留语义与上下文未知
1 parent 3716b6e commit 314ebda

12 files changed

Lines changed: 3597 additions & 12 deletions

File tree

‎crates/codeguard-cli/src/clang_syntax_probe.rs‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ pub(crate) fn observe(
1212
standard: &str,
1313
source: &[u8],
1414
deadline: Instant,
15+
) -> Value {
16+
observe_with_cancellation(
17+
tool,
18+
language,
19+
standard,
20+
source,
21+
deadline,
22+
&AtomicBool::new(false),
23+
)
24+
}
25+
26+
/// 对固定标准源码执行共享取消令牌的原生观察;参数与observe一致,额外令牌贯穿两个进程阶段。
27+
pub(crate) fn observe_with_cancellation(
28+
tool: &Path,
29+
language: &str,
30+
standard: &str,
31+
source: &[u8],
32+
deadline: Instant,
33+
cancelled: &AtomicBool,
1534
) -> Value {
1635
let mut report = json!({"status":"incomplete","reason":"clang_tool_unavailable","version":null,"tool_sha256":null,"diagnostics":[]});
1736
if Instant::now() >= deadline {
@@ -44,7 +63,6 @@ pub(crate) fn observe(
4463
report["reason"] = json!("clang_scratch_unavailable");
4564
return report;
4665
};
47-
let cancelled = AtomicBool::new(false);
4866
let invoke = |args: Vec<OsString>, stdin| {
4967
run_process(
5068
&ProcessSpec {
@@ -56,7 +74,7 @@ pub(crate) fn observe(
5674
deadline,
5775
output_limit_bytes: 64 * 1024,
5876
},
59-
&cancelled,
77+
cancelled,
6078
)
6179
};
6280
let version = invoke(vec![OsString::from("--version")], None);

‎crates/codeguard-cli/src/grammar_native_checker.rs‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ pub(crate) enum GrammarNativeChecker {
1717
Go,
1818
/// 显式固定edition2024的Rustfmt解析;不是Clippy lint或项目编译。
1919
Rust,
20+
/// 固定AppleClang21的C11独立源码观察。
21+
C,
22+
/// 固定AppleClang21的C++17独立源码观察。
23+
Cpp,
2024
}
2125

2226
impl GrammarNativeChecker {
@@ -32,6 +36,8 @@ impl GrammarNativeChecker {
3236
"ruby" => Some(Self::Ruby),
3337
"go" => Some(Self::Go),
3438
"rust" => Some(Self::Rust),
39+
"c" => Some(Self::C),
40+
"cpp" => Some(Self::Cpp),
3541
_ => None,
3642
}
3743
}
@@ -44,12 +50,14 @@ impl GrammarNativeChecker {
4450
Self::Ruby => "ruby 2.6.10p210",
4551
Self::Go => "go1.23.4",
4652
Self::Rust => "rustfmt 1.9.0-stable",
53+
Self::C | Self::Cpp => "Apple clang version 21.0.0 (clang-2100.3.34.2)",
4754
}
4855
}
4956
/// 返回对应原生制品的有界字节预算;Node 独立预算不扩张其它工具权限。
5057
pub(crate) fn artifact_budget(self) -> u64 {
5158
match self {
5259
Self::Javascript => crate::javascript_syntax_probe::NODE_ARTIFACT_BUDGET,
60+
Self::C | Self::Cpp => 256 * 1024 * 1024,
5361
_ => 64 * 1024 * 1024,
5462
}
5563
}
@@ -60,6 +68,32 @@ impl GrammarNativeChecker {
6068
_ => Ok(None),
6169
}
6270
}
71+
/// 保留语法与语义的边界;Clang仅明确解析规则可分类为语法无效,警告不是语法错误。
72+
pub(crate) fn classify(self, native: &Value) -> Option<bool> {
73+
if matches!(self, Self::C | Self::Cpp) {
74+
if native["status"] == "completed" {
75+
return Some(true);
76+
}
77+
if native["status"] != "diagnostics_observed" {
78+
return None;
79+
}
80+
let rows = native["diagnostics"].as_array()?;
81+
let mut syntax_error = false;
82+
for row in rows {
83+
match row["level"].as_str()? {
84+
"warning" | "note" => {}
85+
"error" if row["rule_id"] == "clang.err_expected_expression" => {
86+
syntax_error = true
87+
}
88+
// 类型、名称、扩展诊断及未审计解析规则不能被解释为grammar漏报。
89+
_ => return None,
90+
}
91+
}
92+
Some(!syntax_error)
93+
} else {
94+
crate::grammar_native_differential::classify_native(native)
95+
}
96+
}
6397
/// 对冻结源码执行隔离语法观察;工具、源码与预算由差分入口绑定。
6498
pub(crate) fn observe(
6599
self,
@@ -77,6 +111,18 @@ impl GrammarNativeChecker {
77111
Self::Ruby => crate::ruby_syntax_probe::observe(tool, source, deadline, cancelled),
78112
Self::Go => crate::go_syntax_probe::observe(tool, source, deadline, cancelled),
79113
Self::Rust => crate::rustfmt_syntax_probe::observe(tool, source, deadline, cancelled),
114+
Self::C | Self::Cpp => crate::clang_syntax_probe::observe_with_cancellation(
115+
tool,
116+
if matches!(self, Self::C) { "c" } else { "cpp" },
117+
if matches!(self, Self::C) {
118+
"c11"
119+
} else {
120+
"c++17"
121+
},
122+
source,
123+
deadline,
124+
cancelled,
125+
),
80126
Self::Javascript => {
81127
crate::javascript_syntax_probe::observe(tool, source, deadline, cancelled)
82128
}
@@ -98,6 +144,41 @@ mod tests {
98144
time::{Duration, Instant},
99145
};
100146

147+
#[test]
148+
fn clang_syntax_labels_preserve_warning_and_semantic_boundaries() {
149+
for checker in [GrammarNativeChecker::C, GrammarNativeChecker::Cpp] {
150+
for (rows, expected) in [
151+
(
152+
serde_json::json!([{"level":"warning","rule_id":"clang.warn_unused_variable"}]),
153+
Some(true),
154+
),
155+
(
156+
serde_json::json!([{"level":"error","rule_id":"clang.err_expected_expression"}]),
157+
Some(false),
158+
),
159+
(
160+
serde_json::json!([{"level":"error","rule_id":"clang.err_unknown_typename"}]),
161+
None,
162+
),
163+
(
164+
serde_json::json!([{"level":"error","rule_id":"clang.err_expected_expression"},{"level":"error","rule_id":"clang.err_unknown_typename"}]),
165+
None,
166+
),
167+
(
168+
serde_json::json!([{"rule_id":"clang.err_expected_expression"}]),
169+
None,
170+
),
171+
] {
172+
assert_eq!(
173+
checker.classify(
174+
&serde_json::json!({"status":"diagnostics_observed","diagnostics":rows})
175+
),
176+
expected
177+
);
178+
}
179+
}
180+
}
181+
101182
#[test]
102183
fn every_native_observer_cancels_version_and_scan_in_flight() {
103184
for language in [
@@ -110,6 +191,8 @@ mod tests {
110191
"ruby",
111192
"go",
112193
"rust",
194+
"c",
195+
"cpp",
113196
] {
114197
for phase in ["version", "scan"] {
115198
let root = std::env::temp_dir().canonicalize().unwrap().join(format!(
@@ -125,6 +208,7 @@ mod tests {
125208
"kotlin" => "printf 'info: kotlinc-jvm 2.4.10 (JRE fixture)\\n' >&2",
126209
"python" => "printf 'ruff 0.16.8\\n'",
127210
"ruby" => "printf 'ruby 2.6.10p210 (fixture) [fixture]\\n'",
211+
"c" | "cpp" => "printf 'Apple clang version 21.0.0 (clang-2100.3.34.2)\\n'",
128212
"rust" => "printf 'rustfmt 1.9.0-stable (fixture)\\n'",
129213
"go" => {
130214
"if [ \"$#\" = 1 ]; then printf 'go version go1.23.4 fixture/fixture\\n'; else printf '%s: go1.23.4\\n' \"$2\"; fi"
@@ -200,6 +284,8 @@ mod tests {
200284
"ruby",
201285
"go",
202286
"rust",
287+
"c",
288+
"cpp",
203289
] {
204290
for mode in ["alias", "replace"] {
205291
let root = std::env::temp_dir().canonicalize().unwrap().join(format!(
@@ -223,6 +309,7 @@ mod tests {
223309
"kotlin" => "printf 'info: kotlinc-jvm 2.4.10 (JRE fixture)\\n' >&2",
224310
"python" => "printf 'ruff 0.16.8\\n'",
225311
"ruby" => "printf 'ruby 2.6.10p210 (fixture) [fixture]\\n'",
312+
"c" | "cpp" => "printf 'Apple clang version 21.0.0 (clang-2100.3.34.2)\\n'",
226313
"rust" => "printf 'rustfmt 1.9.0-stable (fixture)\\n'",
227314
"go" => {
228315
"if [ \"$#\" = 1 ]; then printf 'go version go1.23.4 fixture/fixture\\n'; else printf '%s: go1.23.4\\n' \"$2\"; fi"

‎crates/codeguard-cli/src/grammar_native_differential.rs‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ pub fn replay_native_corpus(
6969
let ruby_selected = tools.contains_key("ruby");
7070
let go_selected = tools.contains_key("go");
7171
let rust_selected = tools.contains_key("rust");
72-
let measure_structure = tools.contains_key("python")
72+
let clang_selected = tools.contains_key("c") || tools.contains_key("cpp");
73+
let measure_structure = clang_selected
74+
|| tools.contains_key("python")
7375
|| javascript_selected
7476
|| ruby_selected
7577
|| go_selected
@@ -118,7 +120,7 @@ pub fn replay_native_corpus(
118120
&& checker
119121
.companion_identity(&tools[&case.language])
120122
.is_ok_and(|current| current == *companion);
121-
let native_class = tool_current.then(|| classify_native(&native)).flatten();
123+
let native_class = tool_current.then(|| checker.classify(&native)).flatten();
122124
let wasm_started = Instant::now();
123125
let observation =
124126
if !admitted || cancelled.load(Ordering::Relaxed) || Instant::now() >= deadline {
@@ -166,6 +168,10 @@ pub fn replay_native_corpus(
166168
"native_elapsed_us":native_us,"wasm_classification":name(wasm_class),"wasm_recovery_count":recovery_count,"wasm_reason":wasm_reason,
167169
"wasm_elapsed_us":elapsed_us(wasm_started),"comparison":comparison(native_class,wasm_class),
168170
"fixture_native_disagreement":native_class.map(|valid|valid!=case.expected_valid)});
171+
if matches!(case.language.as_str(), "c" | "cpp") {
172+
row["native_standard"] = json!(if case.language == "c" { "c11" } else { "c++17" });
173+
row["native_classification_policy"] = json!("clang-parse-expression-only-v1");
174+
}
169175
if case.language == "javascript" {
170176
// Node开发观察器固定--input-type=module,不向未知项目模式推断。
171177
row["javascript_mode"] = json!("module");
@@ -249,7 +255,7 @@ pub fn replay_native_corpus(
249255
inventory.push(json!({"language":language,"grammar_sha256":asset["sha256"],"native_selected":false,"reason":if checker(language).is_some(){"explicit_native_tool_not_selected"}else{"native_differential_adapter_unavailable"},"grammar_qualified":false}));
250256
}
251257
}
252-
let mut report = json!({"schema_version":if javascript_selected {"0.10.0"}else if rust_selected {"0.8.0"}else if go_selected {"0.7.0"}else if ruby_selected {"0.5.0"}else if measure_structure {"0.3.0"}else{"0.1.0"},"report_type":"native_grammar_differential","status":"incomplete","delivery_decision":"not_evaluated",
258+
let mut report = json!({"schema_version":if clang_selected {"0.11.0"}else if javascript_selected {"0.10.0"}else if rust_selected {"0.8.0"}else if go_selected {"0.7.0"}else if ruby_selected {"0.5.0"}else if measure_structure {"0.3.0"}else{"0.1.0"},"report_type":"native_grammar_differential","status":"incomplete","delivery_decision":"not_evaluated",
253259
"authority":"development_native_differential_only","native_adapter_reused":!(javascript_selected || ruby_selected || go_selected || rust_selected),"independent_holdout":false,"grammar_qualified_count":0,
254260
"corpus_sha256":digest(corpus_bytes),"manifest_sha256":corpus.manifest_sha256,"program_sha256":program_sha,"program_stable":program_stable,
255261
"language_count":inventory.len(),"selected_language_count":frozen.len(),"sample_count":cases.len(),"languages":inventory,"cases":cases});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#![cfg(all(feature = "wasm-precheck", unix))]
2+
use codeguard_cli::grammar_native_differential::replay_native_corpus;
3+
use serde_json::{Value, json};
4+
use sha2::{Digest, Sha256};
5+
use std::{
6+
collections::BTreeMap,
7+
fs,
8+
os::unix::fs::PermissionsExt,
9+
path::PathBuf,
10+
sync::atomic::AtomicBool,
11+
time::{Duration, Instant},
12+
};
13+
14+
fn corpus() -> Vec<u8> {
15+
let mut corpus: Value = serde_json::from_slice(include_bytes!(
16+
"../../../tests/fixtures/grammar_regression_v0_2.json"
17+
))
18+
.unwrap();
19+
codeguard_cli::grammar_evaluation::validate_corpus_against_manifest(
20+
&serde_json::to_vec(&corpus).unwrap(),
21+
include_bytes!("../../../tests/fixtures/grammar_manifests/manifest_2026_10_04.json"),
22+
)
23+
.unwrap();
24+
corpus["manifest_sha256"] = json!(format!(
25+
"{:x}",
26+
Sha256::digest(include_bytes!("../../../grammars/manifest.json"))
27+
));
28+
corpus["cases"]
29+
.as_array_mut()
30+
.unwrap()
31+
.retain(|c| c["language"] != "c" && c["language"] != "cpp");
32+
for language in ["c", "cpp"] {
33+
for (name, source, valid) in [
34+
("plain", "int x=1;\n", true),
35+
("warning", "int f(void) { int unused; return 0; }\n", true),
36+
("literal", "const char *s=\"#literal\";\n", true),
37+
("syntax", "int x = ;\n", false),
38+
("semantic", "UnknownType x;\n", true),
39+
("context", "%:include \"missing.h\"\n", true),
40+
] {
41+
corpus["cases"].as_array_mut().unwrap().push(json!({"id":format!("{language}-replay-{name}"),"language":language,"source":source,"source_sha256":format!("{:x}",Sha256::digest(source)),"expected_valid":valid,"label":"regression","cohort":"repository_regression","origin":"crates/codeguard-cli/tests/c_family_native_replay.rs"}));
42+
}
43+
}
44+
serde_json::to_vec(&corpus).unwrap()
45+
}
46+
47+
fn replay(tool: PathBuf, budget: u64) -> Value {
48+
replay_native_corpus(
49+
&PathBuf::from(env!("CARGO_BIN_EXE_codeguard")),
50+
&corpus(),
51+
&BTreeMap::from([("c".into(), tool.clone()), ("cpp".into(), tool)]),
52+
Instant::now() + Duration::from_secs(budget),
53+
&AtomicBool::new(false),
54+
)
55+
.expect("C11/C++17 must use explicit native Clang observers")
56+
}
57+
58+
#[test]
59+
fn selected_clang_wrong_version_retains_unknown_cases_and_full_inventory() {
60+
let root = std::env::temp_dir().join(format!("cg-clang-replay-control-{}", std::process::id()));
61+
fs::create_dir(&root).unwrap();
62+
let tool = root.join("clang");
63+
fs::write(&tool, "#!/bin/sh\nprintf 'unknown compiler\\n'\n").unwrap();
64+
fs::set_permissions(&tool, fs::Permissions::from_mode(0o700)).unwrap();
65+
let result = std::panic::catch_unwind(|| replay(tool, 90));
66+
fs::remove_dir_all(root).unwrap();
67+
let report = result.unwrap();
68+
assert_eq!(report["schema_version"], "0.11.0");
69+
assert_eq!(report["native_adapter_reused"], true);
70+
assert_eq!(report["language_count"], 32);
71+
assert_eq!(report["sample_count"], 12);
72+
assert_eq!(report["grammar_qualified_count"], 0);
73+
assert!(
74+
report["cases"]
75+
.as_array()
76+
.unwrap()
77+
.iter()
78+
.all(|c| c["native_classification"] == "unknown" && c["comparison"] == "unknown")
79+
);
80+
}
81+
82+
#[test]
83+
#[ignore = "requires explicit existing Apple Clang21 via CODEGUARD_CLANG_BIN; never installs"]
84+
fn actual_clang_warnings_semantics_and_context_do_not_fabricate_syntax_labels() {
85+
let tool = PathBuf::from(std::env::var_os("CODEGUARD_CLANG_BIN").unwrap());
86+
let report = replay(tool, 180);
87+
assert_eq!(report["schema_version"], "0.11.0");
88+
assert_eq!(report["native_adapter_reused"], true);
89+
for row in report["cases"].as_array().unwrap() {
90+
let id = row["id"].as_str().unwrap();
91+
let expected = if id.ends_with("semantic") || id.ends_with("context") {
92+
"unknown"
93+
} else if id.ends_with("syntax") {
94+
"invalid"
95+
} else {
96+
"valid"
97+
};
98+
assert_eq!(row["native_classification"], expected, "{row}");
99+
assert_eq!(
100+
row["native_standard"],
101+
if row["language"] == "c" {
102+
"c11"
103+
} else {
104+
"c++17"
105+
}
106+
);
107+
assert_eq!(
108+
row["fixture_native_disagreement"],
109+
if expected == "unknown" {
110+
Value::Null
111+
} else {
112+
json!(false)
113+
}
114+
);
115+
}
116+
if let Some(path) = std::env::var_os("CODEGUARD_CLANG_REPLAY_EVIDENCE") {
117+
fs::write(path, serde_json::to_vec_pretty(&report).unwrap()).unwrap();
118+
}
119+
}

‎docs/Codeguard-Grammar-Evaluation.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,6 @@ flowchart LR
200200
I --> J[Agent feedback and original native verification]
201201
J --> K[Closure requires accepted native repair evidence]
202202
```
203+
204+
205+
Uniform development replay now reuses the fixed Clang service for C11/C++17 with shared cancellation. Warnings do not count as syntax errors; only the audited expected-expression parsing rule classifies invalid syntax. Semantic/mixed/context diagnostics remain unknown. Report0.11 records standard and policy, preserves the32-language inventory, and grants no qualification. See [bounded acceptance](../tests/acceptance/c-family-native-replay.md).

‎docs/Codeguard-Grammar-Evaluation.zh_CN.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,6 @@ flowchart LR
212212
I --> J[智能体反馈与原工具复检]
213213
J --> K[关闭仍需通过原生修复验收]
214214
```
215+
216+
217+
C11/C++17开发期统一回放现复用固定Clang原生服务与共享取消令牌。警告不算语法错误;仅已审计缺表达式解析规则可分类,语义/混合/预处理上下文保持unknown。新0.11协议记录标准与策略,32库存及零资格保持;见[局部验收](../tests/acceptance/c-family-native-replay.md)。

‎openspec/changes/introduce-rust-codeguard-cli/specs/native-tool-adapters/spec.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,3 +969,8 @@ Model capture, original task execution and report ownership observation SHALL sh
969969
- **THEN** CodeGuard SHALL preserve native syntax checking rather than infer missing header/macro context from those literal characters. Real line-start preprocessing directives, including digraph spellings and C11 trigraphs, SHALL remain context-unresolved before native execution. Line splicing and raw-string translation boundaries SHALL not allow header reads or fabricate source findings. This guard SHALL not establish a complete preprocessing model or production qualification.
970970

971971
- **AND** the fixed Clang guard SHALL account for initial UTF-8 BOM and LF, CR, CRLF, LFCR and horizontal-space splices within the 1MiB input budget. Uncertain leading Unicode recovery SHALL remain explicitly context-unresolved without source violations or qualification.
972+
973+
974+
#### Scenario: C11 and C++17 uniform native grammar replay keeps semantic diagnostics unknown
975+
- **WHEN** development corpus replay explicitly selects an existing fixed Apple Clang21 tool for c or cpp
976+
- **THEN** it SHALL reuse the isolated native service with c11 or c++17, shared deadline/cancellation and frozen original entry/artifact identity. It SHALL retain all 32 inventory rows, selected samples and unknown denominators. Version/tool/context failures SHALL NOT become syntax failures; warnings alone SHALL NOT become invalid syntax. Only independently audited parsing diagnostic rules MAY classify syntax errors, and mixed semantic/unknown errors SHALL remain unknown. Standard and classification policy SHALL be recorded in a new versioned report without changing historical report semantics, granting grammar qualification or claiming independent holdout.

0 commit comments

Comments
 (0)