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