use rustyfi_syntax::version::RustyfiVersion;
use rustyfi_syntax::{lex, lex_with_version};
use std::path::{Path, PathBuf};
fn assert_same(src: &str, label: &str) {
let via_lex = lex(src);
let via_versioned = lex_with_version(src, RustyfiVersion::V0_0);
match (via_lex, via_versioned) {
(Ok(a), Ok(b)) => {
assert_eq!(a, b, "token stream mismatch for {label}");
}
(Err(a), Err(b)) => {
assert_eq!(
a.to_string(),
b.to_string(),
"lex error mismatch for {label}"
);
}
(a, b) => panic!(
"lex(..) and lex_with_version(.., V0_0) disagree on success/failure for \
{label}: lex={a:?} lex_with_version={b:?}"
),
}
}
fn collect_saty_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for entry in rd.flatten() {
let p = entry.path();
if p.is_dir() {
collect_saty_files(&p, out);
} else if matches!(
p.extension().and_then(|e| e.to_str()),
Some("saty") | Some("satyh") | Some("satyg")
) {
out.push(p);
}
}
}
#[test]
fn lex_and_lex_with_version_v0_0_agree_on_real_fixtures() {
let roots = [
concat!(env!("CARGO_MANIFEST_DIR"), "/../../lib-rustyfi/dist/packages"),
concat!(env!("CARGO_MANIFEST_DIR"), "/../../crates/rustyfi/tests/fixtures"),
];
let mut files = Vec::new();
for root in roots {
collect_saty_files(Path::new(root), &mut files);
}
files.sort();
assert!(
files.len() >= 5,
"expected to find at least 5 .saty/.satyh fixtures under {roots:?}, found {}",
files.len()
);
let mut checked = 0usize;
for f in &files {
let src = std::fs::read_to_string(f)
.unwrap_or_else(|e| panic!("{}: {e}", f.display()));
assert_same(&src, &f.display().to_string());
checked += 1;
}
assert!(checked >= 5, "expected to check at least 5 files, got {checked}");
}
#[test]
fn lex_and_lex_with_version_v0_0_agree_on_mode_coverage_snippets() {
let snippets = [
"let x = 3 in x",
"let-rec f n = f n in f",
"let-inline ctx \\emph it = read-inline ctx it",
"let-block ctx +p it = line-break true true ctx it",
"let-mutable c <- 0 in c",
"module M = struct\nlet x = 1\nend\nopen M in M.x",
"type t = | A | B of int\nin 0",
"@require: stdjabook\n@import: local\nlet x = 1 in x",
"@stage: 1\n3",
"{ Hello, \\emph{world}! #name; }",
"'< +p { hi } +q { there } >",
"${x^2 + \\frac{a}{b} + #y}",
"match x with | 0 -> `a` | n when n -> `b` | _ -> `c`",
"while !c < 3 do c <- !c + 1",
"a before b",
"(| title = {T}; size = 3pt |)",
"[1; 2; 3]",
"let rec = 1 in rec",
"let inline = 1 in inline",
"let block = 1 in block",
"rec + inline + block",
"let mutable = 1 in mutable",
"rec + inline + block + mutable",
"a :> b",
"x : > y",
"a ::> b",
"let signature = 1 in signature",
"let include = 1 in include",
"signature + include",
"A.B.C",
"Mod.x + A.b",
];
for src in snippets {
assert_same(src, src);
}
}
#[test]
fn v0_1_gates_the_bind_keywords() {
use rustyfi_syntax::token::Token;
let toks = lex_with_version("val mutable x <- 0", RustyfiVersion::V0_1).unwrap();
assert!(toks.iter().any(|a| matches!(a.slot, Token::Mutable)));
let toks = lex_with_version("val mutable x <- 0", RustyfiVersion::V0_0).unwrap();
assert!(toks.iter().all(|a| !matches!(a.slot, Token::Mutable)));
}
#[test]
fn v0_1_lexes_coerce_as_one_token() {
use rustyfi_syntax::token::Token;
let toks = lex_with_version("M :> S", RustyfiVersion::V0_1).unwrap();
assert!(toks.iter().any(|a| matches!(a.slot, Token::Coerce)));
let toks = lex_with_version("M : > S", RustyfiVersion::V0_1).unwrap();
assert!(toks.iter().all(|a| !matches!(a.slot, Token::Coerce)));
let toks = lex_with_version("a ::> b", RustyfiVersion::V0_1).unwrap();
assert!(toks.iter().any(|a| matches!(a.slot, Token::Cons)));
assert!(toks.iter().all(|a| !matches!(a.slot, Token::Coerce)));
let toks = lex_with_version("M :> S", RustyfiVersion::V0_0).unwrap();
assert!(toks.iter().all(|a| !matches!(a.slot, Token::Coerce)));
}
#[test]
fn v0_1_lexes_long_upper_paths() {
use rustyfi_syntax::token::Token;
let toks = lex_with_version("A.B.C", RustyfiVersion::V0_1).unwrap();
assert!(toks.iter().any(|a| matches!(
&a.slot,
Token::LongUpper(m, s) if m == &["A".to_string(), "B".to_string()] && s == "C"
)));
let toks = lex_with_version("A.B.c", RustyfiVersion::V0_1).unwrap();
assert!(toks.iter().any(|a| matches!(&a.slot, Token::VarWithMod(..))));
let err = lex_with_version("A.B.C", RustyfiVersion::V0_0).unwrap_err();
assert!(err.to_string().contains("module path must end with a variable name"));
}
#[test]
fn v0_1_gates_signature_and_include_keywords() {
use rustyfi_syntax::token::Token;
let toks = lex_with_version("signature include", RustyfiVersion::V0_1).unwrap();
assert!(toks.iter().any(|a| matches!(a.slot, Token::Signature)));
assert!(toks.iter().any(|a| matches!(a.slot, Token::Include)));
let toks = lex_with_version("signature include", RustyfiVersion::V0_0).unwrap();
assert!(toks.iter().all(|a| !matches!(a.slot, Token::Signature | Token::Include)));
}
#[test]
fn v0_1_lexes_row_var_as_one_token() {
use rustyfi_syntax::token::Token;
let toks = lex_with_version("(| x : int | ?'r |)", RustyfiVersion::V0_1).unwrap();
assert!(toks
.iter()
.any(|a| matches!(&a.slot, Token::RowVar(s) if s == "r")));
let toks = lex_with_version("? 'r", RustyfiVersion::V0_1).unwrap();
assert!(toks.iter().any(|a| matches!(a.slot, Token::OptionalType)));
assert!(toks
.iter()
.any(|a| matches!(&a.slot, Token::TypeVar(s) if s == "r")));
assert!(toks.iter().all(|a| !matches!(a.slot, Token::RowVar(_))));
let toks = lex_with_version("?'r", RustyfiVersion::V0_0).unwrap();
assert!(toks.iter().any(|a| matches!(a.slot, Token::OptionalType)));
assert!(toks
.iter()
.any(|a| matches!(&a.slot, Token::TypeVar(s) if s == "r")));
assert!(toks.iter().all(|a| !matches!(a.slot, Token::RowVar(_))));
}