1use std::collections::BTreeSet;
15
16use crate::concept_records::{ConceptRecord, SourceAlias, concept_records};
17use crate::foundation_models::{FoundationRegister, foundation_register};
18use crate::grammar::{Grammar, GrammarExpr, RuleKind};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct RequiredDistinction {
23 pub concepts: [&'static str; 2],
25 pub reason: &'static str,
27}
28
29pub const REQUIRED_CONCEPT_DISTINCTIONS: &[RequiredDistinction] = &[
31 RequiredDistinction {
32 concepts: ["grammar.ordered-choice", "grammar.unordered-choice"],
33 reason: "An ordered choice commits to the first alternative that matches, while an unordered choice accepts every alternative that matches, so the two accept different languages.",
34 },
35 RequiredDistinction {
36 concepts: ["grammar.lexical-precedence", "grammar.syntactic-precedence"],
37 reason: "Lexical precedence decides which token the lexer produces, while syntactic precedence decides between parse alternatives over tokens already produced.",
38 },
39 RequiredDistinction {
40 concepts: ["binding", "assignment"],
41 reason: "A binding introduces a name for a value in a scope, while an assignment updates the value stored in a location that already exists.",
42 },
43 RequiredDistinction {
44 concepts: ["grammar.list", "grammar.linked-list"],
45 reason: "A list rule is a delimited sequence of items, while a linked list is built from pairs and may end in a dotted pair, so a linked list has no counterpart among plain sequences.",
46 },
47 RequiredDistinction {
48 concepts: ["grammar.string", "grammar.symbol"],
49 reason: "A string literal denotes text, while a symbol denotes an interned identifier that compares by identity, so the same characters read as different data.",
50 },
51 RequiredDistinction {
52 concepts: ["grammar.member", "grammar.ini.setting"],
53 reason: "An object member pairs a name with any nested value, while an INI setting is one key and value line of a section whose value is raw text.",
54 },
55 RequiredDistinction {
56 concepts: ["grammar.object", "grammar.racket.hash-table"],
57 reason: "An object maps names to values with no stated key comparison, while a Racket hash table maps any datum keys under its equality, so the two key sets differ.",
58 },
59 RequiredDistinction {
60 concepts: ["grammar.value", "grammar.datum"],
61 reason: "A value of a data grammar is data only, while a datum is the external representation of a program value, which the reader may also read as code.",
62 },
63 RequiredDistinction {
64 concepts: ["grammar.identifier", "grammar.identifier-name"],
65 reason: "An identifier rule accepts letters, digits and underscores that start with a letter, while an ECMAScript identifier name also accepts dollar signs, Unicode letters and escapes.",
66 },
67 RequiredDistinction {
68 concepts: ["grammar.document", "grammar.program"],
69 reason: "A document is the whole text of a data or document format, while a program is source text the reader reads as code.",
70 },
71 RequiredDistinction {
72 concepts: ["grammar.boolean-value", "grammar.true-value"],
73 reason: "A boolean value rule accepts both truth values, while a true value rule accepts the literal true alone, so one cannot replace the other.",
74 },
75 RequiredDistinction {
76 concepts: ["grammar.program", "grammar.module"],
77 reason: "A program is source text the reader reads as code as a whole, while a TypeScript module declaration is one statement that opens a namespace inside a program.",
78 },
79 RequiredDistinction {
80 concepts: ["grammar.true-constant", "grammar.true-value"],
81 reason: "A Lean true constant accepts the proposition True as well as the Boolean literal true, while a true value rule accepts the literal true alone.",
82 },
83 RequiredDistinction {
84 concepts: ["grammar.symbol", "grammar.keyword"],
85 reason: "A symbol may name a variable, while a keyword evaluates to itself and is never a variable.",
86 },
87 RequiredDistinction {
88 concepts: ["grammar.comment", "grammar.block-comment"],
89 reason: "A comment rule covers every comment its format allows, while a block comment runs between nesting delimiters, so it does not end at the end of its line.",
90 },
91];
92
93pub const REQUIRED_FOUNDATION_DISTINCTIONS: &[[&str; 2]] = &[
95 [
96 "integer-model.natural-number",
97 "integer-model.unbounded-integer",
98 ],
99 ["overflow-model.abort", "overflow-model.wrapping"],
100 [
101 "effect-model.abort",
102 "effect-model.panic-with-default-value",
103 ],
104 [
105 "universe-model.non-cumulative-sort-hierarchy",
106 "universe-model.cumulative-type-hierarchy",
107 ],
108 [
109 "proof-system.bounded-property-check",
110 "proof-system.lean-kernel",
111 ],
112 [
113 "logic-model.constructive-propositions",
114 "logic-model.classical-propositions",
115 ],
116];
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum CorrespondenceRelation {
121 Shared,
123 Distinct,
125 Ambiguous,
127 Unknown,
129}
130
131impl CorrespondenceRelation {
132 #[must_use]
134 pub const fn as_str(self) -> &'static str {
135 match self {
136 Self::Shared => "shared",
137 Self::Distinct => "distinct",
138 Self::Ambiguous => "ambiguous",
139 Self::Unknown => "unknown",
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct ConceptCorrespondence {
147 pub relation: CorrespondenceRelation,
149 pub first: Vec<String>,
151 pub second: Vec<String>,
153 pub shared: Option<String>,
155 pub justification: Option<String>,
157 pub correspondence: Option<String>,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct ConceptDistinctionProblem {
164 pub kind: &'static str,
166 pub subject: String,
168 pub message: String,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct PrecedenceUse {
175 pub rule: String,
177 pub label: String,
179 pub concept: &'static str,
181}
182
183fn within_matches(id: &str, within: Option<&str>) -> bool {
184 within.is_none_or(|prefix| {
185 id == prefix
186 || id
187 .strip_prefix(prefix)
188 .is_some_and(|rest| rest.starts_with('.'))
189 })
190}
191
192#[must_use]
198pub fn source_meanings_in(
199 records: &[ConceptRecord],
200 register: &FoundationRegister,
201 alias: &SourceAlias,
202 within: Option<&str>,
203) -> Vec<String> {
204 let concepts = records.iter().filter(|record| {
205 record
206 .source_aliases
207 .iter()
208 .any(|candidate| candidate.source == alias.source && candidate.name == alias.name)
209 });
210 let models = register.models.iter().filter(|model| {
211 model.source_aliases.iter().any(|candidate| {
212 candidate.source == alias.source
213 && candidate
214 .name
215 .split(", ")
216 .any(|spelling| spelling == alias.name)
217 })
218 });
219 concepts
220 .map(|record| record.id.clone())
221 .chain(models.map(|model| model.id.clone()))
222 .filter(|id| within_matches(id, within))
223 .collect()
224}
225
226fn same_pair(pair: &[impl AsRef<str>], first: &str, second: &str) -> bool {
227 pair.len() == 2
228 && ((pair[0].as_ref() == first && pair[1].as_ref() == second)
229 || (pair[0].as_ref() == second && pair[1].as_ref() == first))
230}
231
232fn meaning_definition(
233 records: &[ConceptRecord],
234 register: &FoundationRegister,
235 id: &str,
236) -> Option<String> {
237 records
238 .iter()
239 .find(|record| record.id == id)
240 .map(|record| record.definition.clone())
241 .or_else(|| {
242 register
243 .models
244 .iter()
245 .find(|model| model.id == id)
246 .map(|model| model.definition.clone())
247 })
248}
249
250#[must_use]
252pub fn concept_correspondence_in(
253 records: &[ConceptRecord],
254 register: &FoundationRegister,
255 first: &SourceAlias,
256 second: &SourceAlias,
257 within: Option<&str>,
258) -> ConceptCorrespondence {
259 let first_meanings = source_meanings_in(records, register, first, within);
260 let second_meanings = source_meanings_in(records, register, second, within);
261 let mut result = ConceptCorrespondence {
262 relation: CorrespondenceRelation::Unknown,
263 first: first_meanings.clone(),
264 second: second_meanings.clone(),
265 shared: None,
266 justification: None,
267 correspondence: None,
268 };
269 if first_meanings.is_empty() || second_meanings.is_empty() {
270 return result;
271 }
272 if first_meanings.len() > 1 || second_meanings.len() > 1 {
273 result.relation = CorrespondenceRelation::Ambiguous;
274 return result;
275 }
276 let (one, other) = (&first_meanings[0], &second_meanings[0]);
277 if one == other {
278 result.relation = CorrespondenceRelation::Shared;
279 result.shared = Some(one.clone());
280 result.justification = meaning_definition(records, register, one);
281 return result;
282 }
283 result.relation = CorrespondenceRelation::Distinct;
284 result.justification = REQUIRED_CONCEPT_DISTINCTIONS
285 .iter()
286 .find(|entry| same_pair(&entry.concepts, one, other))
287 .map(|entry| entry.reason.to_owned())
288 .or_else(|| {
289 register
290 .distinctions
291 .iter()
292 .find(|entry| same_pair(&entry.models, one, other))
293 .map(|entry| entry.reason.clone())
294 });
295 result.correspondence = register
296 .correspondences
297 .iter()
298 .find(|entry| same_pair(&[&entry.from, &entry.to], one, other))
299 .map(|entry| entry.kind.clone());
300 result
301}
302
303#[must_use]
305pub fn concept_correspondence(
306 first: &SourceAlias,
307 second: &SourceAlias,
308 within: Option<&str>,
309) -> ConceptCorrespondence {
310 concept_correspondence_in(
311 concept_records(),
312 foundation_register(),
313 first,
314 second,
315 within,
316 )
317}
318
319fn shared_sources(aliases: &[SourceAlias]) -> usize {
320 aliases
321 .iter()
322 .map(|alias| alias.source.as_str())
323 .filter(|source| *source != "meta-language" && !source.ends_with(" grammar surface"))
324 .collect::<BTreeSet<_>>()
325 .len()
326}
327
328fn is_justification(text: &str) -> bool {
330 let text = text.trim();
331 text.ends_with('.') && text.split_whitespace().count() >= 3
332}
333
334#[must_use]
344pub fn check_concept_distinctions(
345 records: &[ConceptRecord],
346 register: &FoundationRegister,
347) -> Vec<ConceptDistinctionProblem> {
348 let mut problems = Vec::new();
349 let mut report = |kind, subject: &str, message: String| {
350 problems.push(ConceptDistinctionProblem {
351 kind,
352 subject: subject.to_owned(),
353 message,
354 });
355 };
356 for entry in REQUIRED_CONCEPT_DISTINCTIONS {
357 let subject = entry.concepts.join(" / ");
358 let found: Vec<_> = entry
359 .concepts
360 .iter()
361 .map(|id| records.iter().find(|record| record.id == *id))
362 .collect();
363 for (id, record) in entry.concepts.iter().zip(&found) {
364 if record.is_none() {
365 report(
366 "unknown-concept",
367 &subject,
368 format!("{subject} must stay distinct, but {id} is not recorded"),
369 );
370 }
371 }
372 let [Some(first), Some(second)] = found[..] else {
373 continue;
374 };
375 if first.definition.trim().to_lowercase() == second.definition.trim().to_lowercase() {
376 report(
377 "indistinct-concepts",
378 &subject,
379 format!("{subject} record the same definition, which merges two meanings"),
380 );
381 }
382 for (record, other) in [(first, second), (second, first)] {
383 if record.former_names.contains(&other.id)
384 || record.represents.as_deref() == Some(other.id.as_str())
385 {
386 report(
387 "conflated-distinction",
388 &subject,
389 format!(
390 "{} records {} as itself, which merges two meanings",
391 record.id, other.id
392 ),
393 );
394 }
395 }
396 }
397 for pair in REQUIRED_FOUNDATION_DISTINCTIONS {
398 let subject = pair.join(" / ");
399 let found: Vec<_> = pair
400 .iter()
401 .map(|id| register.models.iter().find(|model| model.id == *id))
402 .collect();
403 for (id, model) in pair.iter().zip(&found) {
404 if model.is_none() {
405 report(
406 "unknown-model",
407 &subject,
408 format!("{subject} must stay distinct, but {id} is not recorded"),
409 );
410 }
411 }
412 if let [Some(first), Some(second)] = found[..]
413 && first.properties == second.properties
414 {
415 report(
416 "indistinct-concepts",
417 &subject,
418 format!("{subject} record the same properties, which merges two meanings"),
419 );
420 }
421 let recorded = register.distinctions.iter().any(|entry| {
422 same_pair(&entry.models, pair[0], pair[1]) && is_justification(&entry.reason)
423 });
424 if !recorded {
425 report(
426 "unrecorded-distinction",
427 &subject,
428 format!("the foundation register records no reason why {subject} differ"),
429 );
430 }
431 }
432 for record in records {
433 if shared_sources(&record.source_aliases) > 1
434 && (!is_justification(&record.definition) || record.constraints.is_empty())
435 {
436 report(
437 "unjustified-sharing",
438 &record.id,
439 format!(
440 "{} is shared by several sources but states no meaning that justifies the correspondence",
441 record.id
442 ),
443 );
444 }
445 }
446 for model in ®ister.models {
447 if shared_sources(&model.source_aliases) > 1
448 && (!is_justification(&model.definition) || model.properties.is_empty())
449 {
450 report(
451 "unjustified-sharing",
452 &model.id,
453 format!(
454 "{} is shared by several languages but states no meaning that justifies the correspondence",
455 model.id
456 ),
457 );
458 }
459 }
460 problems
461}
462
463fn is_precedence_label(label: &str) -> bool {
464 ["prec", "prec_left", "prec_right", "prec_dynamic"]
465 .iter()
466 .any(|name| {
467 label == *name
468 || label
469 .strip_prefix(name)
470 .is_some_and(|rest| rest.starts_with('='))
471 })
472}
473
474fn collect_precedence(
475 rule: &str,
476 expr: &GrammarExpr,
477 in_token: bool,
478 uses: &mut Vec<PrecedenceUse>,
479) {
480 match expr {
481 GrammarExpr::Capture { label, expr } => {
482 let label = label.as_deref().unwrap_or_default();
483 if is_precedence_label(label) {
484 let lexical = in_token && !label.starts_with("prec_dynamic");
485 uses.push(PrecedenceUse {
486 rule: rule.to_owned(),
487 label: label.to_owned(),
488 concept: if lexical {
489 "grammar.lexical-precedence"
490 } else {
491 "grammar.syntactic-precedence"
492 },
493 });
494 }
495 let token = in_token || label == "token" || label == "immediate_token";
496 collect_precedence(rule, expr, token, uses);
497 }
498 GrammarExpr::Choice { alternatives, .. } => {
499 for alternative in alternatives {
500 collect_precedence(rule, alternative, in_token, uses);
501 }
502 }
503 GrammarExpr::Sequence(items) => {
504 for item in items {
505 collect_precedence(rule, item, in_token, uses);
506 }
507 }
508 GrammarExpr::Optional(item)
509 | GrammarExpr::ZeroOrMore(item)
510 | GrammarExpr::OneOrMore(item)
511 | GrammarExpr::Repeat { expr: item, .. }
512 | GrammarExpr::And(item)
513 | GrammarExpr::Not(item) => collect_precedence(rule, item, in_token, uses),
514 _ => {}
515 }
516}
517
518#[must_use]
525pub fn grammar_precedence_concepts(grammar: &Grammar) -> Vec<PrecedenceUse> {
526 let mut uses = Vec::new();
527 for rule in grammar.rules() {
528 collect_precedence(
529 rule.name(),
530 rule.expr(),
531 rule.kind() == RuleKind::Token,
532 &mut uses,
533 );
534 }
535 uses
536}