use lex_types::stdlib_spec::{
declared_modules, defs_for, lookup, module_record, parse_signature, BuiltinKind, BUILTINS,
IndexConvention,
};
use lex_types::types::{EffectSet, Ty};
use lex_types::env::TypeEnv;
#[test]
fn every_signature_parses() {
for (i, def) in BUILTINS.iter().enumerate() {
let ty = parse_signature(def, 1000 + i as u32)
.unwrap_or_else(|e| panic!("{}.{}: {e}", def.module, def.name));
assert!(matches!(ty, Ty::Function { .. }), "{}.{} is not a function", def.module, def.name);
}
}
#[test]
fn no_duplicate_definitions() {
let mut seen = std::collections::HashSet::new();
for def in BUILTINS {
assert!(seen.insert((def.module, def.name)), "duplicate {}.{}", def.module, def.name);
}
}
#[test]
fn declared_modules_are_str_and_list() {
assert_eq!(declared_modules(), vec!["str", "list"]);
}
#[test]
fn module_scope_is_served_from_the_catalogue() {
let env = TypeEnv::default();
for m in declared_modules() {
let from_scope = lex_types::builtins::module_scope(m, &env).expect("scope");
let from_spec = module_record(m).expect("record");
assert_eq!(from_scope, from_spec, "{m}");
}
}
#[test]
fn str_record_lists_every_builtin_in_order() {
let Some(Ty::Record(fields)) = module_record("str") else { panic!("str record") };
let names: Vec<&str> = fields.keys().map(|k| k.as_str()).collect();
assert_eq!(names, vec![
"is_empty", "to_int", "to_float", "concat", "len", "char_at", "split", "join",
"starts_with", "ends_with", "contains", "cmp", "replace", "trim", "to_upper",
"to_lower", "strip_prefix", "strip_suffix", "slice", "is_ascii", "find", "find_any",
]);
}
#[test]
fn list_record_lists_every_builtin_in_order() {
let Some(Ty::Record(fields)) = module_record("list") else { panic!("list record") };
let names: Vec<&str> = fields.keys().map(|k| k.as_str()).collect();
assert_eq!(names, vec![
"map", "par_map", "sort_by", "filter", "fold", "len", "is_empty", "range", "head",
"tail", "concat", "reverse", "cons", "enumerate",
]);
}
#[test]
fn concrete_signatures_match_the_old_tables() {
let Some(Ty::Record(fields)) = module_record("str") else { panic!() };
assert_eq!(
fields["find"],
Ty::function(
vec![Ty::str(), Ty::str(), Ty::int()],
EffectSet::empty(),
Ty::Con("Option".into(), vec![Ty::int()]),
)
);
assert_eq!(
fields["split"],
Ty::function(vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::List(Box::new(Ty::str()))),
);
let Some(Ty::Record(fields)) = module_record("list") else { panic!() };
assert_eq!(
fields["enumerate"],
Ty::function(
vec![Ty::List(Box::new(Ty::Var(0)))],
EffectSet::empty(),
Ty::List(Box::new(Ty::Tuple(vec![Ty::int(), Ty::Var(0)]))),
)
);
assert_eq!(
fields["cons"],
Ty::function(
vec![Ty::Var(0), Ty::List(Box::new(Ty::Var(0)))],
EffectSet::empty(),
Ty::List(Box::new(Ty::Var(0))),
)
);
}
#[test]
fn higher_order_rows_are_shared_within_and_distinct_across_builtins() {
let Some(Ty::Record(fields)) = module_record("list") else { panic!() };
let mut rows = Vec::new();
for name in ["map", "par_map", "sort_by", "filter", "fold"] {
let Ty::Function { params, effects, ret } = &fields[name] else { panic!("{name}") };
let outer = effects.var.expect("open row on the call");
let closure = params.iter().find_map(|p| match p {
Ty::Function { effects, .. } => effects.var,
_ => None,
}).expect("closure param with a row");
assert_eq!(outer, closure, "{name}: closure row must be the call's row");
assert!(effects.concrete.is_empty(), "{name}: no concrete effects");
assert!(!rows.contains(&outer), "{name}: row id {outer} reused");
rows.push(outer);
match name {
"map" | "par_map" => assert_eq!(**ret, Ty::List(Box::new(Ty::Var(1)))),
"sort_by" | "filter" => assert_eq!(**ret, Ty::List(Box::new(Ty::Var(0)))),
"fold" => assert_eq!(**ret, Ty::Var(1)),
_ => unreachable!(),
}
}
for r in rows {
assert!(r >= 100, "row id {r} collides with type-variable ids");
}
}
#[test]
fn a_bare_bracketed_name_is_an_effect_not_a_row() {
for def in BUILTINS {
assert!(!def.ty.contains("[E]"), "{}.{}: write `[| E]` for an open row", def.module, def.name);
}
}
#[test]
fn a_program_still_checks_against_the_catalogue() {
let src = r#"
import "std.str" as str
import "std.list" as list
fn shout(xs :: List[Str]) -> List[Str] {
list.map(xs, fn (s :: Str) -> Str { str.to_upper(s) })
}
fn first_pos(s :: Str) -> Option[Int] { str.find(s, ",", 0) }
"#;
let prog = lex_syntax::parse_source(src).expect("parse");
let stages = lex_ast::canonicalize_program(&prog);
lex_types::check_program(&stages).expect("type-check");
}
#[test]
fn index_conventions_are_declared_for_every_positional_builtin() {
let positional = ["len", "char_at", "slice", "find", "find_any"];
for def in defs_for("str") {
if positional.contains(&def.name) {
assert_ne!(def.index, IndexConvention::None,
"str.{}: takes or returns a position, declare Byte or Codepoint", def.name);
} else {
assert_eq!(def.index, IndexConvention::None,
"str.{}: declares an index convention but has no positions", def.name);
}
}
assert_eq!(lookup("str", "len").unwrap().index, IndexConvention::Byte);
assert_eq!(lookup("str", "slice").unwrap().index, IndexConvention::Codepoint);
assert_eq!(lookup("list", "map").unwrap().kind, BuiltinKind::VmNative);
assert!(lookup("str", "nope").is_none());
}