provable_contracts/schema/
parser.rs1use std::path::Path;
2
3use crate::error::ContractError;
4use crate::schema::types::{Contract, ContractKind, KaizenRecord, CONTRACT_TOP_LEVEL_FIELDS};
5
6pub fn parse_contract(path: &Path) -> Result<Contract, ContractError> {
16 let content = std::fs::read_to_string(path)?;
17 parse_contract_str(&content)
18}
19
20const NON_CONTRACT_FILENAMES: [&str; 3] = ["binding.yaml", "binding.yml", "external-corpora.yaml"];
34
35#[must_use]
44pub fn is_contract_yaml(path: &Path) -> bool {
45 if path.extension().and_then(|e| e.to_str()) != Some("yaml") {
46 return false;
47 }
48 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
49 return false;
50 };
51 !name.starts_with('.') && !NON_CONTRACT_FILENAMES.contains(&name)
52}
53
54pub fn parse_contract_str(yaml: &str) -> Result<Contract, ContractError> {
72 let mut contract: Contract = serde_yaml::from_str(yaml)?;
73 contract.unknown_top_level_keys = unknown_top_level_keys(yaml);
74 contract.strict_yaml_error = strict_yaml_error(yaml);
75 contract.kaizen_record = kaizen_record(yaml, contract.kind())?;
76 Ok(contract)
77}
78
79fn kaizen_record(yaml: &str, kind: ContractKind) -> Result<Option<KaizenRecord>, ContractError> {
94 if kind != ContractKind::Kaizen {
95 return Ok(None);
96 }
97 Ok(Some(serde_yaml::from_str::<KaizenRecord>(yaml)?))
98}
99
100fn unknown_top_level_keys(yaml: &str) -> Vec<String> {
115 use serde::de::IgnoredAny;
116 use std::collections::BTreeMap;
117
118 let Ok(map) = serde_yaml::from_str::<BTreeMap<String, IgnoredAny>>(yaml) else {
119 return Vec::new();
120 };
121 map.into_keys()
122 .filter(|k| !CONTRACT_TOP_LEVEL_FIELDS.contains(&k.as_str()))
123 .collect()
124}
125
126fn strict_yaml_error(yaml: &str) -> Option<String> {
137 serde_yaml::from_str::<serde_yaml::Value>(yaml)
138 .err()
139 .map(|e| e.to_string())
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 const MINIMAL_CONTRACT: &str = r#"
147metadata:
148 version: "1.0.0"
149 description: "Test contract"
150 references:
151 - "Test paper (2024)"
152equations:
153 test_eq:
154 formula: "f(x) = x + 1"
155proof_obligations: []
156falsification_tests: []
157"#;
158
159 #[test]
160 fn parse_minimal_contract() {
161 let contract = parse_contract_str(MINIMAL_CONTRACT).unwrap();
162 assert_eq!(contract.metadata.version, "1.0.0");
163 assert_eq!(contract.metadata.description, "Test contract");
164 assert_eq!(contract.equations.len(), 1);
165 assert!(contract.equations.contains_key("test_eq"));
166 }
167
168 #[test]
169 fn parse_contract_with_all_fields() {
170 let yaml = r#"
171metadata:
172 version: "1.0.0"
173 created: "2026-02-18"
174 author: "Test Author"
175 description: "Full contract"
176 references:
177 - "Paper A (2024)"
178 - "Paper B (2025)"
179equations:
180 softmax:
181 formula: "σ(x)_i = exp(x_i - max(x)) / Σ exp(x_j - max(x))"
182 domain: "x ∈ ℝ^n, n ≥ 1"
183 codomain: "σ(x) ∈ (0,1)^n"
184 invariants:
185 - "sum(output) = 1.0"
186 - "output_i > 0"
187proof_obligations:
188 - type: invariant
189 property: "Output sums to 1"
190 formal: "|sum(softmax(x)) - 1.0| < ε"
191 tolerance: 1.0e-6
192 applies_to: all
193 - type: equivalence
194 property: "SIMD matches scalar"
195 tolerance: 8.0
196 applies_to: simd
197kernel_structure:
198 phases:
199 - name: find_max
200 description: "Find max element"
201 invariant: "max >= all elements"
202 - name: exp_subtract
203 description: "Compute exp(x_i - max)"
204 invariant: "all values in (0, 1]"
205simd_dispatch:
206 softmax:
207 scalar: "softmax_scalar"
208 avx2: "softmax_avx2"
209enforcement:
210 normalization:
211 description: "Output sums to 1.0"
212 check: "contract_tests::FALSIFY-SM-001"
213 severity: "ERROR"
214falsification_tests:
215 - id: FALSIFY-SM-001
216 rule: "Normalization"
217 prediction: "sum(output) ≈ 1.0"
218 test: "proptest with random vectors"
219 if_fails: "Missing max-subtraction trick"
220kani_harnesses:
221 - id: KANI-SM-001
222 obligation: SM-INV-001
223 property: "Softmax sums to 1.0"
224 bound: 16
225 strategy: stub_float
226 solver: cadical
227 harness: verify_softmax_normalization
228qa_gate:
229 id: F-SM-001
230 name: "Softmax Contract"
231 checks:
232 - "normalization"
233 pass_criteria: "All falsification tests pass"
234 falsification: "Introduce off-by-one in max reduction"
235"#;
236
237 let contract = parse_contract_str(yaml).unwrap();
238 assert_eq!(contract.metadata.version, "1.0.0");
239 assert_eq!(contract.metadata.references.len(), 2);
240 assert_eq!(contract.equations.len(), 1);
241 assert_eq!(contract.proof_obligations.len(), 2);
242 assert!(contract.kernel_structure.is_some());
243 let ks = contract.kernel_structure.unwrap();
244 assert_eq!(ks.phases.len(), 2);
245 assert_eq!(contract.simd_dispatch.len(), 1);
246 assert_eq!(contract.enforcement.len(), 1);
247 assert_eq!(contract.falsification_tests.len(), 1);
248 assert_eq!(contract.falsification_tests[0].id, "FALSIFY-SM-001");
249 assert_eq!(contract.kani_harnesses.len(), 1);
250 assert_eq!(contract.kani_harnesses[0].bound, Some(16));
251 assert!(contract.qa_gate.is_some());
252 }
253
254 #[test]
255 fn parse_invalid_yaml_returns_error() {
256 let result = parse_contract_str("not: [valid: yaml: {{");
257 assert!(result.is_err());
258 }
259
260 #[test]
261 fn parse_missing_metadata_returns_error() {
262 let yaml = r#"
263equations:
264 test:
265 formula: "f(x) = x"
266"#;
267 let result = parse_contract_str(yaml);
268 assert!(result.is_err());
269 }
270
271 #[test]
272 fn parse_obligation_types() {
273 let yaml = r#"
274metadata:
275 version: "1.0.0"
276 description: "type test"
277equations:
278 f:
279 formula: "f(x) = x"
280proof_obligations:
281 - type: invariant
282 property: "test"
283 if_fails: ""
284 - type: equivalence
285 property: "test"
286 - type: bound
287 property: "test"
288 - type: monotonicity
289 property: "test"
290 - type: idempotency
291 property: "test"
292 - type: linearity
293 property: "test"
294 - type: symmetry
295 property: "test"
296 - type: associativity
297 property: "test"
298 - type: conservation
299 property: "test"
300falsification_tests: []
301"#;
302 let contract = parse_contract_str(yaml).unwrap();
303 assert_eq!(contract.proof_obligations.len(), 9);
304 }
305
306 #[test]
307 fn parse_dbc_obligation_types() {
308 use crate::schema::types::ObligationType;
309
310 let yaml = r#"
311metadata:
312 version: "1.0.0"
313 description: "DbC type test"
314 depends_on: ["parent-v1"]
315equations:
316 f:
317 formula: "f(x) = x"
318proof_obligations:
319 - type: precondition
320 property: "input finite"
321 formal: "isFinite(x)"
322 - type: postcondition
323 property: "output bounded"
324 requires: "PRE-001"
325 - type: frame
326 property: "input unchanged"
327 - type: loop_invariant
328 property: "max tracks true max"
329 applies_to_phase: "find_max"
330 - type: loop_variant
331 property: "remaining decreasing"
332 applies_to_phase: "accumulate"
333 - type: old_state
334 property: "cache grows"
335 - type: subcontract
336 property: "refines parent"
337 parent_contract: "parent-v1"
338falsification_tests: []
339"#;
340 let contract = parse_contract_str(yaml).unwrap();
341 assert_eq!(contract.proof_obligations.len(), 7);
342 assert_eq!(
343 contract.proof_obligations[0].obligation_type,
344 ObligationType::Precondition
345 );
346 assert_eq!(
347 contract.proof_obligations[1].obligation_type,
348 ObligationType::Postcondition
349 );
350 assert_eq!(
351 contract.proof_obligations[1].requires.as_deref(),
352 Some("PRE-001")
353 );
354 assert_eq!(
355 contract.proof_obligations[2].obligation_type,
356 ObligationType::Frame
357 );
358 assert_eq!(
359 contract.proof_obligations[3].obligation_type,
360 ObligationType::LoopInvariant
361 );
362 assert_eq!(
363 contract.proof_obligations[3].applies_to_phase.as_deref(),
364 Some("find_max")
365 );
366 assert_eq!(
367 contract.proof_obligations[4].obligation_type,
368 ObligationType::LoopVariant
369 );
370 assert_eq!(
371 contract.proof_obligations[5].obligation_type,
372 ObligationType::OldState
373 );
374 assert_eq!(
375 contract.proof_obligations[6].obligation_type,
376 ObligationType::Subcontract
377 );
378 assert_eq!(
379 contract.proof_obligations[6].parent_contract.as_deref(),
380 Some("parent-v1")
381 );
382 }
383
384 #[test]
385 fn parse_contract_with_kind_model_family() {
386 use crate::schema::types::ContractKind;
387
388 let yaml = r#"
392metadata:
393 version: "1.0.0"
394 description: "Google BERT architecture family metadata"
395 kind: model-family
396 references:
397 - "https://arxiv.org/abs/1810.04805"
398 - "https://huggingface.co/google-bert"
399# Custom top-level fields ignored by the kernel schema,
400# consumed by the downstream crate that owns the file.
401family: bert
402display_name: "Google BERT"
403vendor: Google
404architectures:
405 - BertModel
406 - BertForMaskedLM
407size_variants:
408 base:
409 parameters: "110M"
410 hidden_dim: 768
411"#;
412 let contract = parse_contract_str(yaml).unwrap();
413 assert_eq!(contract.kind(), ContractKind::ModelFamily);
414 assert!(!contract.requires_proofs());
415 assert!(!contract.is_registry());
416 let violations = crate::schema::validate_contract(&contract);
418 let errors: Vec<_> = violations
419 .iter()
420 .filter(|v| v.severity == crate::error::Severity::Error)
421 .collect();
422 assert!(
423 errors.is_empty(),
424 "model-family YAML should validate with no errors, got: {errors:?}",
425 );
426 }
427
428 #[test]
429 fn parse_contract_defaults_to_kernel_kind() {
430 use crate::schema::types::ContractKind;
431
432 let contract = parse_contract_str(MINIMAL_CONTRACT).unwrap();
433 assert_eq!(contract.kind(), ContractKind::Kernel);
434 assert!(contract.requires_proofs());
435 }
436
437 #[test]
438 fn parse_kani_strategies() {
439 use crate::schema::types::KaniStrategy;
440
441 let yaml = r#"
442metadata:
443 version: "1.0.0"
444 description: "kani test"
445equations:
446 f:
447 formula: "f(x) = x"
448kani_harnesses:
449 - id: K1
450 obligation: OBL-1
451 strategy: exhaustive
452 - id: K2
453 obligation: OBL-2
454 strategy: stub_float
455 - id: K3
456 obligation: OBL-3
457 strategy: compositional
458falsification_tests: []
459"#;
460 let contract = parse_contract_str(yaml).unwrap();
461 assert_eq!(contract.kani_harnesses.len(), 3);
462 assert_eq!(
463 contract.kani_harnesses[0].strategy,
464 Some(KaniStrategy::Exhaustive)
465 );
466 assert_eq!(
467 contract.kani_harnesses[1].strategy,
468 Some(KaniStrategy::StubFloat)
469 );
470 assert_eq!(
471 contract.kani_harnesses[2].strategy,
472 Some(KaniStrategy::Compositional)
473 );
474 }
475}