1use std::sync::{Arc, OnceLock};
8
9use harn_vm::VmValue;
10use serde::Deserialize;
11
12use super::language::Language;
13
14const RECEIPT_JSON: &str = include_str!("../../data/grammar-fitness/receipt.v1.json");
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ParserOperation {
19 Parse,
21 Search,
23 SafeEdit,
25}
26
27impl ParserOperation {
28 pub const fn as_str(self) -> &'static str {
30 match self {
31 Self::Parse => "parse",
32 Self::Search => "search",
33 Self::SafeEdit => "safe_edit",
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SourceObservation {
41 Clean,
43 Errors,
45 NotObserved,
47}
48
49impl SourceObservation {
50 const fn as_str(self) -> &'static str {
51 match self {
52 Self::Clean => "clean",
53 Self::Errors => "errors",
54 Self::NotObserved => "not_observed",
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum Coverage {
62 Verified,
64 Partial,
66 Unavailable,
68 Inconclusive,
70}
71
72impl Coverage {
73 pub const fn as_str(self) -> &'static str {
75 match self {
76 Self::Verified => "verified",
77 Self::Partial => "partial",
78 Self::Unavailable => "unavailable",
79 Self::Inconclusive => "inconclusive",
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
85pub struct ParserHealth {
87 operation: ParserOperation,
88 language: Option<Language>,
89 observation: SourceObservation,
90 source_authority: Option<String>,
91 coverage: Coverage,
92 support: &'static str,
93 receipt: Option<&'static LanguageReceipt>,
94 abi_version: Option<usize>,
95}
96
97impl ParserHealth {
98 pub fn observed(language: Language, operation: ParserOperation, had_errors: bool) -> Self {
100 let observation = if had_errors {
101 SourceObservation::Errors
102 } else {
103 SourceObservation::Clean
104 };
105 Self::new(Some(language), operation, observation)
106 }
107
108 pub fn not_observed(language: Language, operation: ParserOperation) -> Self {
110 Self::new(Some(language), operation, SourceObservation::NotObserved)
111 }
112
113 pub fn fitted(language: Language, operation: ParserOperation) -> Self {
116 let mut health = Self::new(Some(language), operation, SourceObservation::NotObserved);
117 if health.support == "supported" {
118 health.coverage = Coverage::Verified;
119 }
120 health
121 }
122
123 pub fn unavailable(language: Option<Language>, operation: ParserOperation) -> Self {
125 let receipt = language.and_then(receipt_for);
126 let ts_language = language.and_then(Language::ts_language);
127 let fitted = receipt.is_some_and(|row| {
128 row.operations
129 .iter()
130 .any(|candidate| candidate == operation.as_str())
131 });
132 Self {
133 operation,
134 language,
135 observation: SourceObservation::NotObserved,
136 source_authority: None,
137 coverage: Coverage::Unavailable,
138 support: if ts_language.is_some() && fitted {
139 "supported"
140 } else {
141 "unsupported"
142 },
143 receipt,
144 abi_version: ts_language.as_ref().map(tree_sitter::Language::abi_version),
145 }
146 }
147
148 fn new(
149 language: Option<Language>,
150 operation: ParserOperation,
151 observation: SourceObservation,
152 ) -> Self {
153 let receipt = language.and_then(receipt_for);
154 let ts_language = language.and_then(Language::ts_language);
155 let fitted = receipt.is_some_and(|row| {
156 row.operations
157 .iter()
158 .any(|candidate| candidate == operation.as_str())
159 });
160 let supported = ts_language.is_some() && fitted;
161 let coverage = if !supported {
162 Coverage::Unavailable
163 } else {
164 match (operation, observation) {
165 (_, SourceObservation::Clean) => Coverage::Verified,
166 (_, SourceObservation::NotObserved) => Coverage::Inconclusive,
167 (ParserOperation::Search, SourceObservation::Errors) => Coverage::Partial,
168 (ParserOperation::Parse | ParserOperation::SafeEdit, SourceObservation::Errors) => {
169 Coverage::Inconclusive
170 }
171 }
172 };
173 Self {
174 operation,
175 language,
176 observation,
177 source_authority: (!matches!(observation, SourceObservation::NotObserved))
178 .then(|| "tree_sitter".to_string()),
179 coverage,
180 support: if supported {
181 "supported"
182 } else {
183 "unsupported"
184 },
185 receipt,
186 abi_version: ts_language.as_ref().map(tree_sitter::Language::abi_version),
187 }
188 }
189
190 pub const fn coverage(&self) -> Coverage {
192 self.coverage
193 }
194
195 pub const fn is_verified(&self) -> bool {
197 matches!(self.coverage, Coverage::Verified)
198 }
199
200 pub fn externally_verified(
204 language: Language,
205 operation: ParserOperation,
206 authority: impl Into<String>,
207 ) -> Self {
208 let mut health = Self::new(Some(language), operation, SourceObservation::Errors);
209 if health.support == "supported" {
210 health.coverage = Coverage::Verified;
211 health.source_authority = Some(authority.into());
212 }
213 health
214 }
215
216 pub(crate) fn attach_to(&self, response: VmValue) -> VmValue {
217 match response {
218 VmValue::Dict(map) => {
219 let mut fields = (*map).clone();
220 fields.insert("health".into(), self.to_vm_value());
221 VmValue::dict(fields)
222 }
223 other => other,
224 }
225 }
226
227 pub fn to_vm_value(&self) -> VmValue {
229 let receipt = fitness_receipt();
230 let grammar = self.receipt.map_or_else(
231 || {
232 dict([
233 (
234 "language",
235 self.language
236 .map_or(VmValue::Nil, |language| VmValue::string(language.name())),
237 ),
238 ("package", VmValue::Nil),
239 (
240 "runtime_version",
241 VmValue::string(&receipt.tree_sitter_runtime),
242 ),
243 ("abi_version", VmValue::Nil),
244 ("enabled_features", VmValue::List(Arc::new(Vec::new()))),
245 ])
246 },
247 |language_receipt| {
248 let package = &language_receipt.package;
249 dict([
250 ("language", VmValue::string(&language_receipt.language)),
251 (
252 "package",
253 dict([
254 ("name", VmValue::string(&package.name)),
255 ("version", VmValue::string(&package.version)),
256 ("source", VmValue::string(&package.source)),
257 (
258 "checksum",
259 package
260 .checksum
261 .as_deref()
262 .map_or(VmValue::Nil, VmValue::string),
263 ),
264 ("artifact_digest", VmValue::string(&package.artifact_digest)),
265 ]),
266 ),
267 (
268 "runtime_version",
269 VmValue::string(&receipt.tree_sitter_runtime),
270 ),
271 (
272 "abi_version",
273 self.abi_version
274 .map_or(VmValue::Nil, |abi| VmValue::Int(abi as i64)),
275 ),
276 (
277 "enabled_features",
278 VmValue::List(Arc::new(vec![VmValue::string(grammar_feature(
279 self.language.expect("receipt has language"),
280 ))])),
281 ),
282 ])
283 },
284 );
285 let corpus_fitness = if self.receipt.is_some_and(|row| {
286 row.operations
287 .iter()
288 .any(|candidate| candidate == self.operation.as_str())
289 }) {
290 "verified"
291 } else {
292 "unavailable"
293 };
294 dict([
295 ("schema_version", VmValue::Int(receipt.schema_version)),
296 ("operation", VmValue::string(self.operation.as_str())),
297 ("grammar", grammar),
298 (
299 "corpus",
300 dict([
301 (
302 "schema_version",
303 VmValue::Int(receipt.corpus.schema_version),
304 ),
305 ("digest", VmValue::string(&receipt.corpus.digest)),
306 ("fitness", VmValue::string(corpus_fitness)),
307 (
308 "authority",
309 self.receipt
310 .map_or(VmValue::Nil, |row| VmValue::string(&row.authority)),
311 ),
312 ]),
313 ),
314 (
315 "source",
316 dict([
317 ("observation", VmValue::string(self.observation.as_str())),
318 (
319 "authority",
320 self.source_authority
321 .as_deref()
322 .map_or(VmValue::Nil, VmValue::string),
323 ),
324 ]),
325 ),
326 ("support", VmValue::string(self.support)),
327 ("coverage", VmValue::string(self.coverage.as_str())),
328 ])
329 }
330}
331
332#[derive(Debug, Deserialize)]
333#[serde(deny_unknown_fields)]
334struct FitnessReceipt {
335 schema_version: i64,
336 corpus: CorpusReceipt,
337 tree_sitter_runtime: String,
338 languages: Vec<LanguageReceipt>,
339}
340
341#[derive(Debug, Deserialize)]
342#[serde(deny_unknown_fields)]
343struct CorpusReceipt {
344 schema_version: i64,
345 digest: String,
346}
347
348#[derive(Debug, Deserialize)]
349#[serde(deny_unknown_fields)]
350struct LanguageReceipt {
351 language: String,
352 package: PackageReceipt,
353 operations: Vec<String>,
354 authority: String,
355}
356
357#[derive(Debug, Deserialize)]
358#[serde(deny_unknown_fields)]
359struct PackageReceipt {
360 name: String,
361 version: String,
362 source: String,
363 checksum: Option<String>,
364 artifact_digest: String,
365}
366
367fn fitness_receipt() -> &'static FitnessReceipt {
368 static RECEIPT: OnceLock<FitnessReceipt> = OnceLock::new();
369 RECEIPT.get_or_init(|| {
370 let receipt: FitnessReceipt =
371 serde_json::from_str(RECEIPT_JSON).expect("grammar fitness receipt must be valid");
372 assert_eq!(receipt.schema_version, 1, "unsupported fitness receipt");
373 assert!(
374 !receipt.tree_sitter_runtime.is_empty(),
375 "fitness receipt must identify the tree-sitter runtime"
376 );
377 receipt
378 })
379}
380
381fn receipt_for(language: Language) -> Option<&'static LanguageReceipt> {
382 fitness_receipt()
383 .languages
384 .iter()
385 .find(|row| row.language == language.name())
386}
387
388const fn grammar_feature(language: Language) -> &'static str {
389 match language {
390 Language::Harn => "grammar-harn",
391 Language::TypeScript
392 | Language::Tsx
393 | Language::JavaScript
394 | Language::Jsx
395 | Language::Html
396 | Language::Css => "grammar-web",
397 Language::Rust | Language::C | Language::Cpp | Language::Go | Language::Zig => {
398 "grammar-systems"
399 }
400 Language::Python
401 | Language::Ruby
402 | Language::Bash
403 | Language::Lua
404 | Language::Php
405 | Language::R => "grammar-scripting",
406 Language::Java | Language::Kotlin | Language::Scala => "grammar-jvm",
407 Language::CSharp | Language::Swift | Language::Elixir | Language::Haskell => {
408 "grammar-enterprise"
409 }
410 Language::Json | Language::Yaml | Language::Toml | Language::Sql | Language::Markdown => {
411 "grammar-data"
412 }
413 }
414}
415
416fn dict<const N: usize>(entries: [(&str, VmValue); N]) -> VmValue {
417 VmValue::dict(
418 entries
419 .into_iter()
420 .map(|(key, value)| (key.to_string(), value))
421 .collect::<harn_vm::value::DictMap>(),
422 )
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn receipt_covers_every_registered_language_and_operation() {
431 for language in Language::all() {
432 let receipt = receipt_for(*language)
433 .unwrap_or_else(|| panic!("missing receipt for {}", language.name()));
434 for operation in [
435 ParserOperation::Parse,
436 ParserOperation::Search,
437 ParserOperation::SafeEdit,
438 ] {
439 assert!(
440 receipt
441 .operations
442 .iter()
443 .any(|candidate| candidate == operation.as_str()),
444 "{} lacks {} fitness",
445 language.name(),
446 operation.as_str()
447 );
448 }
449 }
450 }
451
452 #[test]
453 fn coverage_policy_is_operation_specific() {
454 let parse = ParserHealth::observed(Language::Swift, ParserOperation::Parse, true);
455 let search = ParserHealth::observed(Language::Swift, ParserOperation::Search, true);
456 let edit = ParserHealth::observed(Language::Swift, ParserOperation::SafeEdit, false);
457 assert_eq!(parse.coverage(), Coverage::Inconclusive);
458 assert_eq!(search.coverage(), Coverage::Partial);
459 assert_eq!(edit.coverage(), Coverage::Verified);
460 let VmValue::Dict(parse_health) = parse.to_vm_value() else {
461 panic!("health dict")
462 };
463 let Some(VmValue::Dict(source)) = parse_health.get("source") else {
464 panic!("source dict")
465 };
466 assert!(matches!(
467 source.get("authority"),
468 Some(VmValue::String(authority)) if authority.as_str() == "tree_sitter"
469 ));
470 }
471
472 #[test]
473 fn only_a_named_external_authority_can_verify_a_parser_limitation() {
474 let tree_sitter_only =
475 ParserHealth::observed(Language::Scala, ParserOperation::SafeEdit, true);
476 let externally_verified = ParserHealth::externally_verified(
477 Language::Scala,
478 ParserOperation::SafeEdit,
479 "scalac 3.7.1",
480 );
481 assert_eq!(tree_sitter_only.coverage(), Coverage::Inconclusive);
482 assert_eq!(externally_verified.coverage(), Coverage::Verified);
483 let value = externally_verified.to_vm_value();
484 let VmValue::Dict(health) = value else {
485 panic!("health dict")
486 };
487 let Some(VmValue::Dict(source)) = health.get("source") else {
488 panic!("source dict")
489 };
490 assert!(matches!(
491 source.get("authority"),
492 Some(VmValue::String(authority)) if authority.as_str() == "scalac 3.7.1"
493 ));
494 }
495}