use monosashi::{
Blindspot, ByteRange, Evidence, Factor, FactorKind, Ladder, Measured, Reading, Step,
};
use tatara_lisp::binding_shapes::{DEFINE_HEADS, QUOTE_HEADS};
use tatara_lisp::{Span, Spanned, SpannedForm};
use crate::build_check::{check_program, TypeDiagnostic, TypeDiagnosticKind};
pub static TATARA_LADDER: Ladder = Ladder::new(
"tatara-lisp gradual typing",
&[
Step::new(
"untyped",
"no annotation anywhere — every expression infers `:any`, which conforms both ways, so only argument COUNTS are checked",
),
Step::new(
"annotated",
"some annotations exist; definitions without one infer `:any` and conform to everything",
),
Step::new(
"checked",
"every definition has a declared type, so the conformance walk reaches all of them",
),
],
);
const UNTYPED: usize = 0;
const ANNOTATED: usize = 1;
const CHECKED: usize = 2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TataraFactor {
Declaration,
Annotation,
UndeclaredDefinition,
Mismatch,
BadTypeSpec,
Arity,
}
impl FactorKind for TataraFactor {
fn label(self) -> &'static str {
match self {
TataraFactor::Declaration => "declaration",
TataraFactor::Annotation => "annotation",
TataraFactor::UndeclaredDefinition => "undeclared definition",
TataraFactor::Mismatch => "type mismatch",
TataraFactor::BadTypeSpec => "bad type spec",
TataraFactor::Arity => "arity mismatch",
}
}
fn shifts_forward(self) -> bool {
matches!(self, TataraFactor::Declaration | TataraFactor::Annotation)
}
}
#[must_use]
pub fn evidence_of_span(span: Span) -> Evidence {
if span.is_synthetic() {
Evidence::Unlocated(Blindspot::Synthetic)
} else {
Evidence::At(ByteRange::new(span.start, span.end))
}
}
#[must_use]
pub fn reading_of(forms: &[Spanned]) -> Reading<TataraFactor> {
let mut census = Census::default();
for form in forms {
census.walk(form);
}
let mut out = Reading::default();
for (name, span) in &census.declarations {
out.factors.push(Factor::new(
TataraFactor::Declaration,
name.clone(),
evidence_of_span(*span),
detail(name, "has a declared type, so the checker walks it"),
));
}
for (subject, span) in &census.annotations {
out.factors.push(Factor::new(
TataraFactor::Annotation,
subject.clone(),
evidence_of_span(*span),
"an inline `(the …)` — this one expression is checked",
));
}
let mut qualified = 0usize;
for (name, span) in &census.definitions {
if census.declarations.iter().any(|(d, _)| d == name) {
qualified += 1;
} else {
out.factors.push(Factor::new(
TataraFactor::UndeclaredDefinition,
name.clone(),
evidence_of_span(*span),
detail(name, "has no `(declare …)` — declare it to shift further"),
));
}
}
let annotation_sites = census.annotations.len() + census.declarations.len();
out.measured = Measured {
analysed: annotation_sites,
qualified,
considered: census.definitions.len(),
};
out.rung = if census.definitions.is_empty() {
None
} else if annotation_sites == 0 {
TATARA_LADDER.rung(UNTYPED)
} else if qualified < census.definitions.len() {
TATARA_LADDER.rung(ANNOTATED)
} else {
TATARA_LADDER.rung(CHECKED)
};
for diag in check_program(forms) {
out.factors.push(factor_of_diagnostic(&diag));
}
out
}
fn factor_of_diagnostic(diag: &TypeDiagnostic) -> Factor<TataraFactor> {
let evidence = evidence_of_span(diag.span);
match &diag.kind {
TypeDiagnosticKind::Mismatch {
expected,
got,
context,
} => {
let mut d = String::from("expected ");
d.push_str(&expected.render());
d.push_str(", got ");
d.push_str(&got.render());
Factor::new(TataraFactor::Mismatch, context.clone(), evidence, d)
}
TypeDiagnosticKind::BadTypeSpec(msg) => Factor::new(
TataraFactor::BadTypeSpec,
msg.clone(),
evidence,
"the annotation was written and the checker could not read it",
),
TypeDiagnosticKind::Arity {
expected,
got,
context,
} => {
let mut d = String::from("expected ");
d.push_str(&expected.to_string());
d.push_str(" argument(s), got ");
d.push_str(&got.to_string());
Factor::new(TataraFactor::Arity, context.clone(), evidence, d)
}
}
}
fn detail(name: &str, tail: &str) -> String {
let mut d = String::with_capacity(name.len() + tail.len() + 3);
d.push('`');
d.push_str(name);
d.push_str("` ");
d.push_str(tail);
d
}
#[derive(Default)]
struct Census {
definitions: Vec<(String, Span)>,
declarations: Vec<(String, Span)>,
annotations: Vec<(String, Span)>,
}
impl Census {
fn walk(&mut self, form: &Spanned) {
let SpannedForm::List(items) = &form.form else {
return;
};
if let Some(head) = items.first().and_then(Spanned::as_symbol) {
if QUOTE_HEADS.contains(&head) {
return;
}
if head == "declare" && items.len() == 3 {
if let Some(name) = items[1].as_symbol() {
self.declarations.push((name.to_string(), form.span));
}
return;
}
if head == "the" && items.len() == 3 {
self.annotations.push((brief(&items[1]), form.span));
self.walk(&items[2]);
return;
}
if DEFINE_HEADS.contains(&head) && items.len() >= 3 {
if let Some(name) = define_name(items) {
self.definitions.push((name, form.span));
}
}
}
for item in items {
self.walk(item);
}
}
}
fn define_name(items: &[Spanned]) -> Option<String> {
match &items[1].form {
SpannedForm::List(sig) => sig.first()?.as_symbol().map(ToString::to_string),
SpannedForm::Atom(_) => items[1].as_symbol().map(ToString::to_string),
_ => None,
}
}
fn brief(form: &Spanned) -> String {
form.as_symbol()
.map_or_else(|| String::from("<type>"), ToString::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
use tatara_lisp::read_spanned;
fn reading(src: &str) -> Reading<TataraFactor> {
let forms = read_spanned(src).expect("test source must parse");
reading_of(&forms)
}
fn subjects(factors: &[&Factor<TataraFactor>]) -> Vec<String> {
factors.iter().map(|f| f.subject.clone()).collect()
}
const UNANNOTATED: &str = "(define (add a b) (+ a b))";
const DECLARED: &str = "(declare counter :int) (define counter 0)";
#[test]
fn an_unannotated_program_is_untyped_and_buys_no_type_analysis() {
let r = reading(UNANNOTATED);
assert_eq!(r.rung.map(|x| x.label()), Some("untyped"));
assert_eq!(
r.measured.analysed, 0,
"the promise is zero type analysis, and the reading must show it"
);
assert_eq!(r.measured.considered, 1);
assert_eq!(r.measured.qualified, 0);
}
#[test]
fn a_fully_declared_program_is_checked() {
let r = reading(DECLARED);
assert_eq!(r.rung.map(|x| x.label()), Some("checked"));
assert!(r.measured.all_qualified());
assert!(
r.measured.analysed > 0,
"declaring must buy real type analysis"
);
}
#[test]
fn one_undeclared_definition_holds_the_whole_program_back() {
let r = reading("(declare counter :int) (define counter 0) (define (other x) x)");
assert_eq!(r.rung.map(|x| x.label()), Some("annotated"), "not checked");
assert_eq!(r.measured.qualified, 1);
assert_eq!(r.measured.considered, 2);
}
#[test]
fn the_reading_names_the_definition_holding_it_back() {
let r = reading("(declare counter :int) (define counter 0) (define (other x) x)");
let held = subjects(
&r.holding_back()
.into_iter()
.filter(|f| f.kind == TataraFactor::UndeclaredDefinition)
.collect::<Vec<_>>(),
);
assert_eq!(held, vec!["other"]);
}
#[test]
fn a_declaration_after_its_definition_still_qualifies_it() {
let r = reading("(define counter 0) (declare counter :int)");
assert_eq!(r.rung.map(|x| x.label()), Some("checked"));
assert_eq!(r.measured.qualified, 1);
}
#[test]
fn an_inline_annotation_lifts_off_untyped_but_does_not_reach_checked() {
let r = reading("(define (add a b) (the :int (+ 1 2)))");
assert_eq!(r.rung.map(|x| x.label()), Some("annotated"));
assert_eq!(r.measured.qualified, 0);
assert!(r.factors.iter().any(|f| f.kind == TataraFactor::Annotation));
}
#[test]
fn a_quoted_definition_is_data_not_a_definition() {
for src in ["(display '(define x 1))", "(display (quote (define x 1)))"] {
let r = reading(src);
assert_eq!(r.rung, None, "nothing was defined by {src}");
assert_eq!(r.measured.considered, 0, "{src}");
}
}
#[test]
fn a_program_with_no_definitions_has_no_rung() {
assert_eq!(reading("(+ 1 2)").rung, None);
}
#[test]
fn diagnostics_survive_a_program_with_no_rung() {
let r = reading("(the :int \"oops\")");
assert_eq!(r.rung, None, "nothing was defined");
assert!(
r.factors.iter().any(|f| f.kind == TataraFactor::Mismatch),
"the mismatch must still be reported: {:?}",
r.factors
);
}
#[test]
fn a_declared_mismatch_is_a_factor_that_holds_back() {
let r = reading("(declare counter :int) (define counter \"oops\")");
let mismatches: Vec<_> = r
.factors
.iter()
.filter(|f| f.kind == TataraFactor::Mismatch)
.collect();
assert_eq!(mismatches.len(), 1, "{:?}", r.factors);
assert!(!mismatches[0].kind.shifts_forward());
assert!(
mismatches[0].detail.contains("expected"),
"{:?}",
mismatches[0]
);
}
#[test]
fn an_arity_error_is_a_factor_that_holds_back() {
let r = reading("(define (add a b) (+ a b)) (add 1 2 3)");
assert!(r.factors.iter().any(|f| f.kind == TataraFactor::Arity));
assert!(!TataraFactor::Arity.shifts_forward());
}
#[test]
fn a_malformed_type_spec_is_a_factor_that_holds_back() {
let r = reading("(define x 1) (the (:list-of) 1)");
assert!(
r.factors
.iter()
.any(|f| f.kind == TataraFactor::BadTypeSpec),
"{:?}",
r.factors
);
}
#[test]
fn every_factor_from_real_source_carries_a_real_byte_range() {
let src = "(declare counter :int) (define counter \"oops\") (define (other x) x)";
let r = reading(src);
assert!(!r.factors.is_empty());
assert!(r.is_fully_located(), "blind spots: {:?}", r.blind_spots());
for f in &r.factors {
let range = f.evidence.range().expect("located");
assert!(!range.is_empty(), "{f:?} must point at real bytes");
assert!(
range.end <= src.len(),
"{f:?} must be inside the {} bytes of source",
src.len()
);
}
}
#[test]
fn a_synthetic_span_becomes_a_stated_blind_spot() {
assert_eq!(
evidence_of_span(Span::synthetic()),
Evidence::Unlocated(Blindspot::Synthetic)
);
assert_eq!(
evidence_of_span(Span::new(3, 9)),
Evidence::At(ByteRange::new(3, 9))
);
}
#[test]
fn the_reading_changes_as_the_program_shifts() {
let labels: Vec<Option<&str>> = [
"(+ 1 2)",
UNANNOTATED,
"(declare counter :int) (define counter 0) (define (other x) x)",
DECLARED,
]
.iter()
.map(|s| reading(s).rung.map(|r| r.label()))
.collect();
assert_eq!(
labels,
vec![None, Some("untyped"), Some("annotated"), Some("checked")],
"each step must move the reading"
);
}
#[test]
fn the_ladder_is_ordered_untyped_to_checked() {
assert_eq!(TATARA_LADDER.height(), 3);
assert!(TATARA_LADDER.bottom() < TATARA_LADDER.top());
assert_eq!(TATARA_LADDER.bottom().label(), "untyped");
assert_eq!(TATARA_LADDER.top().label(), "checked");
let r = reading(DECLARED).rung.expect("has a rung");
assert_eq!(r, TATARA_LADDER.top());
assert!(r >= TATARA_LADDER.rung(1).unwrap());
}
#[test]
fn the_summary_is_one_line_carrying_the_ramp_and_the_denominator() {
let line = reading(DECLARED).summary();
assert!(line.starts_with("███"), "{line}");
assert!(line.contains("checked"), "{line}");
assert!(line.contains("1/1 qualified"), "{line}");
}
}