use rustyfi_backend::{FontKey, FontMetrics, Length};
use rustyfi_lang::value::Value;
use rustyfi_lang::{elaborate, eval, primitives, typecheck, v1::lower};
use rustyfi_syntax::leaf::KwIn;
use rustyfi_syntax::{cst, parse_file, parse_file_v1, RustyfiVersion, Span};
struct NoFonts;
impl FontMetrics for NoFonts {
fn advance(&self, _f: FontKey, _c: char, _size: Length) -> Option<Length> {
None
}
fn ascender(&self, _f: FontKey, size: Length) -> Length {
size
}
fn descender(&self, _f: FontKey, _size: Length) -> Length {
Length::pt(0.0)
}
}
fn eval_v01(src: &str) -> Result<Value, String> {
let doc_file = parse_file_v1(src).map_err(|e| format!("parse: {e}"))?;
let body =
lower::lower_document_v1(&doc_file).map_err(|e| format!("lower_document_v1: {e}"))?;
let eoi = match &doc_file {
rustyfi_syntax::cst_v1::FileV1::Document { eoi, .. } => eoi.clone(),
_ => return Err("entry must parse as a V0_1 document".to_string()),
};
let file = cst::File {
headers: Vec::new(),
prelude: Vec::new(),
in_kw: Some(KwIn(Span::default())),
body: Some(body),
eoi,
};
let env = primitives::base_env_with_version(RustyfiVersion::V0_1);
let store = rustyfi_lang::symbol::SymbolStore::new();
let scope = elaborate::Scope::new_with_version(&store, env.names(), RustyfiVersion::V0_1);
let elaborated =
elaborate::elaborate_program(&file, &scope).map_err(|e| format!("elaborate: {e}"))?;
typecheck::typecheck_with_version(&elaborated, RustyfiVersion::V0_1)
.map_err(|e| format!("typecheck: {e}"))?;
let mut interp = eval::Interp::new(&NoFonts);
interp
.eval(&env, &rustyfi_lang::ast::debrand(&elaborated.body, &store))
.map_err(|e| format!("eval: {e}"))
}
fn elaborate_v006_err(src: &str) -> String {
let file = parse_file(src).unwrap_or_else(|e| panic!("0.0.6 parse of {src:?}: {e}"));
let env = primitives::base_env();
let store = rustyfi_lang::symbol::SymbolStore::new();
let scope = elaborate::Scope::new(&store, env.names());
match elaborate::elaborate_program(&file, &scope) {
Ok(_) => panic!("expected a version-gate error, but {src:?} elaborated"),
Err(e) => e.to_string(),
}
}
fn as_int(v: Value) -> i64 {
match v {
Value::Int(n) => n,
other => panic!("expected an int, got {other:?}"),
}
}
fn as_tuple(v: Value) -> Vec<Value> {
match v {
Value::Tuple(xs) => xs,
other => panic!("expected a tuple, got {other:?}"),
}
}
#[test]
fn t1_defaulting_subset_and_order() {
let src = "\
let add ?(bias = b, scale = s) x =
let bv = match b with None -> 0 | Some v -> v end in
let sv = match s with None -> 1 | Some v -> v end in
x * sv + bv
in (add 1, add ?(bias = 10) 1, add ?(scale = 3, bias = 1) 2)";
let vals = as_tuple(eval_v01(src).expect("T1 should compile and evaluate"));
let got: Vec<i64> = vals.into_iter().map(as_int).collect();
assert_eq!(
got,
vec![1, 11, 7],
"plain-call default-all; subset (bias only); non-declaration order (scale, bias)"
);
}
#[test]
fn t2_higher_order_row_poly() {
let src = "\
let f ?(bias = b) x = x + (match b with None -> 0 | Some v -> v end) in
let apply-plain g y = g y in
(apply-plain f 5, f ?(bias = 2) 5)";
let vals = as_tuple(eval_v01(src).expect("T2 should compile and evaluate"));
let got: Vec<i64> = vals.into_iter().map(as_int).collect();
assert_eq!(got, vec![5, 7]);
}
#[test]
fn t3_record_row_poly() {
let src = "let getx r = r#x in (getx (| x = 1, y = 2 |)) + (getx (| x = 3, z = 4 |))";
assert_eq!(
as_int(eval_v01(src).expect("T3 should compile and evaluate")),
4
);
}
#[test]
fn t5_unknown_label_rejected() {
let err = eval_v01("let f ?(a = x) n = n in f ?(b = 1) 2")
.expect_err("supplying an undeclared optional label `b` must not typecheck");
assert!(
err.starts_with("typecheck:"),
"expected a typecheck error, got: {err}"
);
}
#[test]
fn t6_duplicate_label_rejected() {
let app_err = eval_v01("let f ?(a = x) n = n in f ?(a = 1, a = 2) 0")
.expect_err("a duplicate label in one `?(…)` application bundle must be rejected");
assert!(
app_err.contains("duplicate optional label"),
"got: {app_err}"
);
let param_err = eval_v01("let g = fun ?(a = x, a = y) n -> n in g 0")
.expect_err("a duplicate label in one `?(…)` binder list must be rejected");
assert!(
param_err.contains("duplicate optional label"),
"got: {param_err}"
);
}
#[test]
fn t7_v006_version_gate() {
let fun_err = elaborate_v006_err("let f = fun ?(a = x) p -> p in 0");
assert!(
fun_err.contains("SATySFi 0.1 syntax"),
"0.0.6 must reject a `?(…)` param bundle with a version error, got: {fun_err}"
);
let app_err = elaborate_v006_err("let f x = x in f ?(a = 1) 2");
assert!(
app_err.contains("SATySFi 0.1 syntax"),
"0.0.6 must reject a `?(…)` application bundle with a version error, got: {app_err}"
);
}
#[test]
fn t9_empty_bundle_is_error() {
let err = eval_v01("let f ?(a = x) n = n in f ?() 2")
.expect_err("an empty `?()` bundle must be a lower error");
assert!(err.contains("optional-argument bundle"), "got: {err}");
}
#[test]
fn ascribed_param_pattern_is_accepted() {
let v = eval_v01("let f (x : int) = x + 1 in f 1").expect("ascribed param should compile");
assert_eq!(as_int(v), 2);
}
#[test]
fn ascribed_param_takes_a_full_pattern_not_just_a_patbot() {
let v = eval_v01("let f (x :: xs : list int) = x in f [1, 2, 3]")
.expect("a cons-pattern ascription should compile");
assert_eq!(as_int(v), 1);
}
#[test]
fn ascribed_param_type_is_not_enforced() {
let v = eval_v01("let f (x : string) = x + 1 in f 1")
.expect("the ascription's type is dropped, so this must still compile");
assert_eq!(as_int(v), 2);
}
#[test]
fn t_opt_row_fun_type_domain_v006_version_gate() {
let file = parse_file("type adder = ?(bias : int) int -> int in 0")
.unwrap_or_else(|e| panic!("0.0.6 parse failed: {e}"));
let env = primitives::base_env();
let store = rustyfi_lang::symbol::SymbolStore::new();
let scope = elaborate::Scope::new(&store, env.names());
let program = elaborate::elaborate_program(&file, &scope)
.expect("elaborate carries the raw type decl through unchecked");
let err = typecheck::typecheck(&program)
.expect_err("a `?(l:ty)->` type domain must be rejected under 0.0.6");
assert!(
err.to_string().contains("SATySFi 0.1 syntax"),
"expected a version-gate message, got: {err}"
);
}
#[test]
fn t_opt_row_fun_type_domain_declares_cleanly_under_v01() {
let lib_file = parse_file_v1(
"module M = struct\n\
type adder = ?(bias : int) int -> int\n\
val x = 1\n\
end",
)
.unwrap_or_else(|e| panic!("lib parse failed: {e}"));
let prelude =
lower::lower_file_v1(&lib_file).unwrap_or_else(|e| panic!("lower lib failed: {e}"));
let doc_file = parse_file_v1("M.x").unwrap_or_else(|e| panic!("doc parse failed: {e}"));
let body =
lower::lower_document_v1(&doc_file).unwrap_or_else(|e| panic!("lower doc failed: {e}"));
let eoi = match &doc_file {
rustyfi_syntax::cst_v1::FileV1::Document { eoi, .. } => eoi.clone(),
_ => panic!("doc must parse as a V0_1 document"),
};
let file = cst::File {
headers: Vec::new(),
prelude,
in_kw: Some(KwIn(Span::default())),
body: Some(body),
eoi,
};
let env = primitives::base_env_with_version(RustyfiVersion::V0_1);
let store = rustyfi_lang::symbol::SymbolStore::new();
let scope = elaborate::Scope::new(&store, env.names());
let elaborated = elaborate::elaborate_program(&file, &scope)
.unwrap_or_else(|e| panic!("elaborate failed: {e}"));
typecheck::typecheck_with_version(&elaborated, RustyfiVersion::V0_1).unwrap_or_else(|e| {
panic!("expected the `?(bias:int) int -> int` synonym to typecheck under V0_1: {e}")
});
}