use rex::{
engine::{Engine, Module, ValueDisplayOptions},
parser::parse as parse_rex,
typesystem::{BuiltinTypeId, Type, TypeKind},
};
fn type_compatible(actual: &Type, expected: &Type) -> bool {
match (actual.as_ref(), expected.as_ref()) {
(TypeKind::Var(_), TypeKind::Con(tc)) if tc.name_str() == "i32" => true,
(TypeKind::Con(a), TypeKind::Con(b)) => a == b,
(TypeKind::App(af, aa), TypeKind::App(ef, ea))
| (TypeKind::Fun(af, aa), TypeKind::Fun(ef, ea)) => {
type_compatible(af, ef) && type_compatible(aa, ea)
}
(TypeKind::Tuple(as_), TypeKind::Tuple(es)) if as_.len() == es.len() => as_
.iter()
.zip(es.iter())
.all(|(a, e)| type_compatible(a, e)),
(TypeKind::Record(as_), TypeKind::Record(es)) if as_.len() == es.len() => as_
.iter()
.zip(es.iter())
.all(|((an, at), (en, et))| an == en && type_compatible(at, et)),
_ => false,
}
}
async fn eval_to_string(code: &str, expected_ty: Type) -> Result<String, String> {
let program = parse_rex(code).map_err(|errs| format!("parse error: {errs:?}"))?;
let mut engine = Engine::with_prelude(()).unwrap();
let mut module = Module::global();
module.add_decls(program.decls.clone());
engine.inject_module(module).map_err(|e| format!("{e}"))?;
let (handle, ty) = engine
.into_evaluator()
.eval(program.body.as_ref().unwrap().as_ref())
.await
.map_err(|e| format!("{e}"))?;
assert!(
type_compatible(&ty, &expected_ty),
"eval returned unexpected type for: {code}\nactual: {ty}\nexpected: {expected_ty}"
);
let opts = ValueDisplayOptions {
include_numeric_suffixes: true,
..ValueDisplayOptions::default()
};
handle.display_with(opts).map_err(|e| format!("{e}"))
}
async fn assert_eval(code: &str, expected: &str, expected_ty: Type) {
let actual = eval_to_string(code, expected_ty)
.await
.unwrap_or_else(|e| panic!("expected ok, got error: {e}"));
assert_eq!(actual, expected);
}
async fn assert_err_contains(code: &str, needle: &str) {
let err = eval_to_string(code, Type::builtin(BuiltinTypeId::I32))
.await
.unwrap_err();
assert!(
err.contains(needle),
"expected error containing {needle:?}, got: {err}"
);
}
#[tokio::test]
async fn default_record_dispatch() {
assert_eval(
r#"
type Foo = Foo { x: i32, y: i32 } | Bar { z: f32 };
instance Default Foo where {
default = Bar { z = 0.0 };
}
let x: Foo = default in x
"#,
"Bar {z = 0f32}",
Type::con("Foo", 0),
)
.await;
}
#[tokio::test]
async fn default_nested_context_list() {
assert_eval(
r#"
let xs: List i32 = default in xs
"#,
"[]",
Type::list(Type::builtin(BuiltinTypeId::I32)),
)
.await;
}
#[tokio::test]
async fn pattern_field_renaming() {
assert_eval(
r#"
type Point = Point { x: f32, y: f32 };
instance AdditiveMonoid Point where {
zero = Point { x = 0.0, y = 0.0 };
+ = \p q -> match (p, q) with {
case (Point { x: x1, y: y1 }, Point { x: x2, y: y2 }) ->
Point { x = x1 + x2, y = y1 + y2 };
};
}
(Point { x = 1.0, y = 2.0 }) + (Point { x = 3.0, y = 4.0 })
"#,
"Point {x = 4f32, y = 6f32}",
Type::con("Point", 0),
)
.await;
}
#[tokio::test]
async fn default_nested_context_option() {
assert_eval(
r#"
let x: Option i32 = default in x
"#,
"None",
Type::option(Type::builtin(BuiltinTypeId::I32)),
)
.await;
}
#[tokio::test]
async fn default_custom_adt_single_ctor_unnamed_fields() {
assert_eval(
r#"
type Pair = Pair i32 bool;
instance Default Pair where {
default = Pair 42 true;
}
let x: Pair = default in x
"#,
"Pair 42i32 true",
Type::con("Pair", 0),
)
.await;
}
#[tokio::test]
async fn default_custom_adt_single_ctor_named_fields() {
assert_eval(
r#"
type Config = Config { retries: i32, enabled: bool };
instance Default Config where {
default = Config { retries = 3, enabled = false };
}
let x: Config = default in x
"#,
"Config {enabled = false, retries = 3i32}",
Type::con("Config", 0),
)
.await;
}
#[tokio::test]
async fn default_custom_adt_enum_unit_variants() {
assert_eval(
r#"
type Mode = Fast | Safe | Debug;
instance Default Mode where {
default = Safe;
}
let x: Mode = default in x
"#,
"Safe",
Type::con("Mode", 0),
)
.await;
}
#[tokio::test]
async fn default_custom_adt_enum_mixed_variant_payloads() {
assert_eval(
r#"
type Token = Eof | IntLit i32 | Meta { line: i32, col: i32 };
instance Default Token where {
default = Meta { line = 1, col = 1 };
}
let x: Token = default in x
"#,
"Meta {col = 1i32, line = 1i32}",
Type::con("Token", 0),
)
.await;
}
#[tokio::test]
async fn default_custom_adt_generic_instance_uses_constraint() {
assert_eval(
r#"
type Box a = Box a | Missing;
instance Default (Box a) <= Default a where {
default = Box default;
}
let x: Box i32 = default in x
"#,
"Box 0i32",
Type::app(Type::con("Box", 1), Type::builtin(BuiltinTypeId::I32)),
)
.await;
}
#[tokio::test]
async fn default_multiple_adts_same_named_fields_then_record_update_without_is_fails() {
assert_err_contains(
r#"
type A = A { x: i32, y: i32 };
type B = B { x: i32, y: i32 };
instance Default A where {
default = A { x = 1, y = 2 };
}
instance Default B where {
default = B { x = 10, y = 20 };
}
let
a = { default with { x = 9 } },
b = { default with { y = 8 } }
in
(a, b)
"#,
"field `x` is not definitely available",
)
.await;
}
#[tokio::test]
async fn default_multiple_adts_same_named_fields_then_record_update_uses_let_annotations() {
assert_eval(
r#"
type A = A { x: i32, y: i32 };
type B = B { x: i32, y: i32 };
instance Default A where {
default = A { x = 1, y = 2 };
}
instance Default B where {
default = B { x = 10, y = 20 };
}
let
a: A = { default with { x = 9 } },
b: B = { default with { y = 8 } }
in
(a, b)
"#,
"(A {x = 9i32, y = 2i32}, B {x = 10i32, y = 8i32})",
Type::tuple(vec![Type::con("A", 0), Type::con("B", 0)]),
)
.await;
}
#[tokio::test]
async fn default_multiple_adts_same_named_fields_then_record_update() {
assert_eval(
r#"
type A = A { x: i32, y: i32 };
type B = B { x: i32, y: i32 };
instance Default A where {
default = A { x = 1, y = 2 };
}
instance Default B where {
default = B { x = 10, y = 20 };
}
let
a: A = { (default is A) with { x = 9 } },
b: B = { (default is B) with { y = 8 } }
in
(a, b)
"#,
"(A {x = 9i32, y = 2i32}, B {x = 10i32, y = 8i32})",
Type::tuple(vec![Type::con("A", 0), Type::con("B", 0)]),
)
.await;
}
#[tokio::test]
async fn default_multiple_adts_same_named_fields_with_is_disambiguates_without_let_types() {
assert_eval(
r#"
type A = A { x: i32, y: i32 };
type B = B { x: i32, y: i32 };
instance Default A where {
default = A { x = 1, y = 2 };
}
instance Default B where {
default = B { x = 10, y = 20 };
}
let
a = { (default is A) with { x = 9 } },
b = { (default is B) with { y = 8 } }
in
(a, b)
"#,
"(A {x = 9i32, y = 2i32}, B {x = 10i32, y = 8i32})",
Type::tuple(vec![Type::con("A", 0), Type::con("B", 0)]),
)
.await;
}
#[tokio::test]
async fn methods_can_call_other_methods() {
assert_eval(
r#"
class PairOps p where {
first : p -> i32;
second : p -> i32;
sum_pair : p -> i32;
}
type Pair = Pair { a: i32, b: i32 };
instance PairOps Pair where {
first = \p -> p.a;
second = \p -> p.b;
sum_pair = \p -> (first p) + (second p);
}
sum_pair (Pair { a = 19, b = 23 })
"#,
"42i32",
Type::builtin(BuiltinTypeId::I32),
)
.await;
}
#[tokio::test]
async fn method_can_return_function() {
assert_eval(
r#"
class Builder a where {
make_adder : a -> i32 -> i32;
}
instance Builder i32 where {
make_adder = \n x -> x + n;
}
let f = make_adder (5 is i32) in f (37 is i32)
"#,
"42i32",
Type::builtin(BuiltinTypeId::I32),
)
.await;
}
#[tokio::test]
async fn instance_method_can_reference_global_fn() {
assert_eval(
r#"
fn inc (x: i32) -> i32 = x + 1;
class Bump a where {
bump : a -> a;
}
instance Bump i32 where {
bump = inc;
}
bump 41
"#,
"42i32",
Type::builtin(BuiltinTypeId::I32),
)
.await;
}
#[tokio::test]
async fn hkt_functor_option_and_result() {
assert_eval(
r#"
class MyFunctor f where {
fmap : (a -> b) -> f a -> f b;
}
instance MyFunctor Option where {
fmap = \f x ->
match x with {
case Some v -> Some (f v);
case None -> None;
};
}
instance MyFunctor (Result e) where {
fmap = \f x ->
match x with {
case Ok v -> Ok (f v);
case Err err -> Err err;
};
}
let
inc = \x -> x + 1,
a = fmap inc (Some 1),
b = fmap inc (None is Option i32),
c = fmap inc ((Ok 1) is Result i32 string),
d = fmap inc ((Err "bad") is Result i32 string)
in
(a, b, c, d)
"#,
r#"(Some 2i32, None, Ok 2i32, Err "bad")"#,
Type::tuple(vec![
Type::option(Type::builtin(BuiltinTypeId::I32)),
Type::option(Type::builtin(BuiltinTypeId::I32)),
Type::result(
Type::builtin(BuiltinTypeId::I32),
Type::builtin(BuiltinTypeId::String),
),
Type::result(
Type::builtin(BuiltinTypeId::I32),
Type::builtin(BuiltinTypeId::String),
),
]),
)
.await;
}
#[tokio::test]
async fn pattern_match_inside_method_body() {
assert_eval(
r#"
class Head a where {
head_or : a -> List a -> a;
}
instance Head i32 where {
head_or = \fallback xs ->
match xs with {
case [] -> fallback;
case x::rest -> x;
};
}
(head_or 0 [1, 2, 3], head_or 7 [])
"#,
"(1i32, 7i32)",
Type::tuple(vec![
Type::builtin(BuiltinTypeId::I32),
Type::builtin(BuiltinTypeId::I32),
]),
)
.await;
}
#[tokio::test]
async fn superclass_and_instance_context() {
assert_eval(
r#"
class MyEq a where {
eq : a -> a -> bool;
}
class MyOrd a <= MyEq a where {
my_cmp : a -> a -> i32;
}
type Color = Red | Green | Blue;
instance MyEq Color where {
eq = \x y ->
match x with {
case Red ->
let r = match y with { case Red -> true; case _ -> false; } in r;
case Green ->
let r = match y with { case Green -> true; case _ -> false; } in r;
case Blue ->
let r = match y with { case Blue -> true; case _ -> false; } in r;
};
}
instance MyOrd Color <= MyEq Color where {
my_cmp = \x y ->
if eq x y then 0 else
match x with {
case Red -> -1;
case Green -> if eq y Red then 1 else -1;
case Blue -> 1;
};
}
(eq Red Blue, eq Blue Blue, my_cmp Red Green, my_cmp Blue Red)
"#,
"(false, true, -1i32, 1i32)",
Type::tuple(vec![
Type::builtin(BuiltinTypeId::Bool),
Type::builtin(BuiltinTypeId::Bool),
Type::builtin(BuiltinTypeId::I32),
Type::builtin(BuiltinTypeId::I32),
]),
)
.await;
}
#[tokio::test]
async fn missing_instance_method_is_error() {
assert_err_contains(
r#"
class NeedsMethod a where {
needs : a;
}
instance NeedsMethod i32;
0
"#,
"missing implementation of `needs`",
)
.await;
}
#[tokio::test]
async fn unknown_instance_method_is_error() {
assert_err_contains(
r#"
class NeedsMethod a where {
needs : a;
}
instance NeedsMethod i32 where {
not_a_method = 0;
}
0
"#,
"unknown method `not_a_method`",
)
.await;
}
#[tokio::test]
async fn missing_instance_constraint_is_error() {
assert_err_contains(
r#"
class NeedsCtx a where {
make : a;
}
instance NeedsCtx (List a) where {
make = [make];
}
0
"#,
"not in the instance context",
)
.await;
}
#[tokio::test]
async fn duplicate_instances_are_rejected() {
assert_err_contains(
r#"
class Dup a where {
dup : a;
}
instance Dup i32 where {
dup = 0;
}
instance Dup i32 where {
dup = 1;
}
0
"#,
"duplicate type class instance",
)
.await;
}
#[tokio::test]
async fn ambiguous_class_method_use_is_error() {
assert_err_contains(
r#"
class Pick a where {
pick : a;
}
instance Pick i32 where {
pick = 0;
}
pick
"#,
"ambiguous overload",
)
.await;
}