use std::sync::Arc;
use rex::{
Rex,
ast::{Expr, Symbol},
engine::{
Context, Engine, EngineError, FromRex, Handle, Heap, IntoRex, Module, RexDefault, Value,
virtual_export_name,
},
parser::parse as parse_rex,
typesystem::{BuiltinTypeId, Scheme, Type, TypeError, TypeKind},
};
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Rex)]
enum Side {
Left,
Right,
}
#[derive(Clone, Debug, PartialEq, Rex)]
enum Correctness {
Right,
Wrong,
}
#[derive(Clone, Debug, PartialEq, Rex)]
struct Label {
text: String,
side: Side,
}
fn render_label(label: Label) -> String {
match label.side {
Side::Left => format!("{:<12}", label.text),
Side::Right => format!("{:>12}", label.text),
}
}
fn inject_globals<State: Clone + Send + Sync + 'static>(
engine: &mut Engine<State>,
build: impl FnOnce(&mut Module<State>) -> Result<(), EngineError>,
) -> Result<(), EngineError> {
let mut module = Module::global();
build(&mut module)?;
engine.inject_module(module)
}
#[tokio::test]
async fn module_render_label_with_module_scoped_adts_left_and_right() {
let mut engine: Engine<()> = Engine::with_prelude(()).unwrap();
engine.add_default_resolvers();
let mut module = Module::new("sample");
module.add_rex_adt::<Side>().unwrap();
module.add_rex_adt::<Correctness>().unwrap();
module.add_rex_adt::<Label>().unwrap();
module
.export("render_label", |_: &(), label: Label| {
Ok::<String, EngineError>(render_label(label))
})
.unwrap();
engine.inject_module(module).unwrap();
let (value, ty) = engine
.into_evaluator()
.eval_snippet(
r#"
import sample (Label, Left, Right, Wrong, render_label);
import sample as Sample;
(
render_label (Label { text = "left", side = Left }),
render_label (Label { text = "right", side = (Right is Sample.Side) }),
(Right is Sample.Correctness),
(Wrong is Sample.Correctness)
)
"#,
)
.await
.unwrap();
let correctness_ty = Type::con(virtual_export_name("sample", "Correctness"), 0);
assert_eq!(
ty,
Type::tuple(vec![
Type::builtin(BuiltinTypeId::String),
Type::builtin(BuiltinTypeId::String),
correctness_ty.clone(),
correctness_ty,
])
);
let items = tuple_items(&value);
assert_eq!(items.len(), 4);
assert_eq!(
items[0].to_rust::<String>().unwrap(),
format!("{:<12}", "left")
);
assert_eq!(
items[1].to_rust::<String>().unwrap(),
format!("{:>12}", "right")
);
match items[2].value().unwrap() {
Value::Adt(tag, args) => {
assert_eq!(tag.as_ref(), "Right");
assert!(args.is_empty());
}
_ => panic!("expected ADT value for Correctness.Right"),
}
match items[3].value().unwrap() {
Value::Adt(tag, args) => {
assert_eq!(tag.as_ref(), "Wrong");
assert!(args.is_empty());
}
_ => panic!("expected ADT value for Correctness.Wrong"),
}
}
#[tokio::test]
async fn module_inject_rex_adt_registers_acyclic_dependency_closure() {
let mut engine: Engine<()> = Engine::with_prelude(()).unwrap();
engine.add_default_resolvers();
let mut module = Module::new("sample");
module.add_rex_adt::<Label>().unwrap();
module
.export("render_label", |_: &(), label: Label| {
Ok::<String, EngineError>(render_label(label))
})
.unwrap();
engine.inject_module(module).unwrap();
let (value, ty) = engine
.into_evaluator()
.eval_snippet(
r#"
import sample (Label, Left, render_label);
render_label (Label { text = "left", side = Left })
"#,
)
.await
.unwrap();
assert_eq!(ty, Type::builtin(BuiltinTypeId::String));
assert_eq!(
value.to_rust::<String>().unwrap(),
format!("{:<12}", "left")
);
}
#[tokio::test]
async fn match_ascribed_module_type_with_overlapping_constructor_is_ambiguous_regression() {
let mut engine: Engine<()> = Engine::with_prelude(()).unwrap();
engine.add_default_resolvers();
let mut module = Module::new("sample");
module.add_rex_adt::<Side>().unwrap();
module.add_rex_adt::<Correctness>().unwrap();
engine.inject_module(module).unwrap();
let err = engine
.into_evaluator()
.eval_snippet(
r#"
import sample (Right, Wrong);
import sample as Sample;
let x = (Right is Sample.Correctness) in
match (x is Sample.Correctness) with {
case Right -> true;
case Wrong -> false;
}
"#,
)
.await
.expect_err("expected ambiguity error for overlapping constructor in match pattern");
match err.into_engine_error() {
EngineError::Type(mut e) => {
while let TypeError::Spanned { error, .. } = e {
e = *error;
}
match e {
TypeError::AmbiguousOverload(name) => {
assert!(name.as_ref().ends_with(".Right"));
}
other => panic!("expected ambiguous overload error, got {other:?}"),
}
}
other => panic!("expected type error, got {other:?}"),
}
}
#[derive(Clone, Debug, PartialEq, Rex)]
struct Entity1 {
account_id: Uuid,
project_id: Uuid,
name: String,
description: Option<String>,
tags: Option<Vec<String>>,
numbers: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq, Rex)]
struct Entity2 {
account_id: Uuid,
project_id: Uuid,
name: String,
description: Option<String>,
tags: Option<Vec<String>>,
numbers: Vec<u32>,
}
impl Entity2 {
fn rex_new(state: &HostState, name: String, numbers: Vec<u32>) -> Result<Entity2, EngineError> {
Ok(Entity2 {
account_id: state.account_id,
project_id: state.project_id,
name,
description: None,
tags: None,
numbers,
})
}
}
impl RexDefault<HostState> for Entity1 {
fn rex_default(engine: Context<HostState>) -> Result<Handle, EngineError> {
let entity = Entity1 {
account_id: engine.state().account_id,
project_id: engine.state().project_id,
name: "".to_string(),
description: None,
tags: None,
numbers: vec![],
};
entity.into_rex(engine.heap())
}
}
#[derive(Clone)]
struct HostState {
account_id: Uuid,
project_id: Uuid,
is_admin: bool,
roles: Vec<String>,
}
fn current_account_id(state: &HostState) -> Result<Uuid, EngineError> {
Ok(state.account_id)
}
fn current_project_id(state: &HostState) -> Result<Uuid, EngineError> {
Ok(state.project_id)
}
fn is_admin(state: &HostState) -> Result<bool, EngineError> {
Ok(state.is_admin)
}
fn have_role(state: &HostState, role: String) -> Result<bool, EngineError> {
Ok(state.roles.iter().any(|r| r == &role))
}
async fn have_role_async(state: HostState, role: String) -> Result<bool, EngineError> {
Ok(state.roles.iter().any(|r| r == &role))
}
fn parse(code: &str) -> Arc<Expr> {
parse_rex(code).unwrap().body.unwrap()
}
fn tuple_items(value: &Handle) -> Vec<Handle> {
let Value::Tuple(items) = value.value().unwrap() else {
panic!("expected tuple, got {}", value.type_name().unwrap());
};
items
}
fn list_from_handles(heap: &Heap, values: Vec<Handle>) -> Result<Handle, EngineError> {
let mut list = heap.alloc_adt(Symbol::intern("Empty"), vec![])?;
for value in values.into_iter().rev() {
list = heap.alloc_adt(Symbol::intern("Cons"), vec![value, list])?;
}
Ok(list)
}
fn handle_as_list(handle: &Handle) -> Result<Vec<Handle>, EngineError> {
let mut out = Vec::new();
let mut cursor = handle.clone();
loop {
let Value::Adt(tag, args) = cursor.value()? else {
return Err(EngineError::NativeType {
expected: "List a".into(),
got: cursor.type_name()?.into(),
});
};
if tag == Symbol::intern("Empty") {
return Ok(out);
}
if tag == Symbol::intern("Cons") && args.len() == 2 {
out.push(args[0].clone());
cursor = args[1].clone();
continue;
}
return Err(EngineError::NativeType {
expected: "List a".into(),
got: cursor.type_name()?.into(),
});
}
}
fn is_i32_or_var(ty: &Type) -> bool {
matches!(ty.as_ref(), TypeKind::Con(tc) if tc.name_str() == "i32")
|| matches!(ty.as_ref(), TypeKind::Var(_))
}
fn assert_overload_tuple_type_shape(ty: &Type) {
let TypeKind::Tuple(items) = ty.as_ref() else {
panic!("expected tuple type, got {ty}");
};
assert_eq!(items.len(), 6);
assert!(
is_i32_or_var(&items[0]),
"expected i32/var at index 0, got {}",
items[0]
);
assert_eq!(items[1], Type::builtin(BuiltinTypeId::String));
assert_eq!(items[2], Type::builtin(BuiltinTypeId::Bool));
assert!(
is_i32_or_var(&items[3]),
"expected i32/var at index 3, got {}",
items[3]
);
assert_eq!(items[4], Type::builtin(BuiltinTypeId::Bool));
assert_eq!(items[5], Type::builtin(BuiltinTypeId::String));
}
#[derive(Clone, Debug, PartialEq, Rex)]
struct EmbedRecord {
n: i32,
}
#[tokio::test]
async fn injected_functions_can_read_shared_state_fields() {
let account_id = uuid::uuid!("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa");
let project_id = uuid::uuid!("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb");
let mut engine: Engine<HostState> = Engine::with_prelude(HostState {
account_id,
project_id,
is_admin: true,
roles: vec!["admin".to_string(), "editor".to_string()],
})
.unwrap();
inject_globals(&mut engine, |module| {
module.export("current_account_id", current_account_id)?;
module.export("current_project_id", current_project_id)?;
module.export("is_admin", is_admin)?;
module.export("have_role", have_role)?;
Ok(())
})
.unwrap();
let expr = parse(
"(current_account_id, current_project_id, is_admin, have_role \"admin\", have_role \"viewer\")",
);
let (value, ty) = engine.into_evaluator().eval(expr.as_ref()).await.unwrap();
assert_eq!(
ty,
Type::tuple(vec![
Type::builtin(BuiltinTypeId::Uuid),
Type::builtin(BuiltinTypeId::Uuid),
Type::builtin(BuiltinTypeId::Bool),
Type::builtin(BuiltinTypeId::Bool),
Type::builtin(BuiltinTypeId::Bool),
])
);
let items = tuple_items(&value);
assert_eq!(items.len(), 5);
assert_eq!(items[0].to_rust::<Uuid>().unwrap(), account_id);
assert_eq!(items[1].to_rust::<Uuid>().unwrap(), project_id);
assert!(items[2].to_rust::<bool>().unwrap());
assert!(items[3].to_rust::<bool>().unwrap());
assert!(!items[4].to_rust::<bool>().unwrap());
}
#[tokio::test]
async fn derived_rex_default_can_read_host_state() {
let account_id = uuid::uuid!("11111111-1111-4111-8111-111111111111");
let project_id = uuid::uuid!("22222222-2222-4222-8222-222222222222");
let mut engine: Engine<HostState> = Engine::with_prelude(HostState {
account_id,
project_id,
is_admin: true,
roles: vec!["admin".to_string()],
})
.unwrap();
Entity1::inject_rex_with_default(&mut engine).unwrap();
let expr = parse("let e: Entity1 = default in e");
let (value, ty) = engine.into_evaluator().eval(expr.as_ref()).await.unwrap();
assert_eq!(ty, Type::con("Entity1", 0));
let decoded = Entity1::from_rex(&value).unwrap();
assert_eq!(
decoded,
Entity1 {
account_id,
project_id,
name: String::new(),
description: None,
tags: None,
numbers: vec![],
}
);
}
#[tokio::test]
async fn derived_rex_default_record_update_can_override_fields() {
let account_id = uuid::uuid!("33333333-3333-4333-8333-333333333333");
let project_id = uuid::uuid!("44444444-4444-4444-8444-444444444444");
let mut engine: Engine<HostState> = Engine::with_prelude(HostState {
account_id,
project_id,
is_admin: false,
roles: vec!["reader".to_string()],
})
.unwrap();
Entity1::inject_rex_with_default(&mut engine).unwrap();
let expr = parse(
r#"let e: Entity1 = { default with { name = "sample", tags = Some (to_array ["x", "y"]), numbers = to_array [7, 11] } } in e"#,
);
let (value, ty) = engine.into_evaluator().eval(expr.as_ref()).await.unwrap();
assert_eq!(ty, Type::con("Entity1", 0));
let decoded = Entity1::from_rex(&value).unwrap();
assert_eq!(
decoded,
Entity1 {
account_id,
project_id,
name: "sample".to_string(),
description: None,
tags: Some(vec!["x".to_string(), "y".to_string()]),
numbers: vec![7, 11],
}
);
}
#[tokio::test]
async fn entity2_constructor_defaults_from_host_state_with_required_fields() {
let account_id = uuid::uuid!("55555555-5555-4555-8555-555555555555");
let project_id = uuid::uuid!("66666666-6666-4666-8666-666666666666");
let mut engine: Engine<HostState> = Engine::with_prelude(HostState {
account_id,
project_id,
is_admin: false,
roles: vec!["reader".to_string()],
})
.unwrap();
Entity2::inject_rex_with_constructor(&mut engine, Entity2::rex_new).unwrap();
let expr = parse(r#"Entity2 "sample" [7, 11]"#);
let (value, ty) = engine.into_evaluator().eval(expr.as_ref()).await.unwrap();
assert_eq!(ty, Type::con("Entity2", 0));
let decoded = Entity2::from_rex(&value).unwrap();
assert_eq!(
decoded,
Entity2 {
account_id,
project_id,
name: "sample".to_string(),
description: None,
tags: None,
numbers: vec![7, 11],
}
);
}
#[tokio::test]
async fn entity2_constructor_result_can_be_record_updated() {
let account_id = uuid::uuid!("77777777-7777-4777-8777-777777777777");
let project_id = uuid::uuid!("88888888-8888-4888-8888-888888888888");
let mut engine: Engine<HostState> = Engine::with_prelude(HostState {
account_id,
project_id,
is_admin: true,
roles: vec!["admin".to_string()],
})
.unwrap();
Entity2::inject_rex_with_constructor(&mut engine, Entity2::rex_new).unwrap();
let expr = parse(
r#"{
(Entity2 "sample" [7, 11])
with {
description = Some "desc",
tags = Some (to_array ["x", "y"])
}
}"#,
);
let (value, ty) = engine.into_evaluator().eval(expr.as_ref()).await.unwrap();
assert_eq!(ty, Type::con("Entity2", 0));
let decoded = Entity2::from_rex(&value).unwrap();
assert_eq!(
decoded,
Entity2 {
account_id,
project_id,
name: "sample".to_string(),
description: Some("desc".to_string()),
tags: Some(vec!["x".to_string(), "y".to_string()]),
numbers: vec![7, 11],
}
);
}
#[tokio::test]
async fn async_injected_functions_can_read_shared_state_fields() {
let mut engine: Engine<HostState> = Engine::with_prelude(HostState {
account_id: uuid::uuid!("cccccccc-cccc-4ccc-8ccc-cccccccccccc"),
project_id: uuid::uuid!("dddddddd-dddd-4ddd-8ddd-dddddddddddd"),
is_admin: false,
roles: vec!["reader".to_string(), "editor".to_string()],
})
.unwrap();
inject_globals(&mut engine, |module| {
module.export_async("have_role_async", |state: &HostState, role: String| {
have_role_async(state.clone(), role)
})
})
.unwrap();
let expr = parse("(have_role_async \"editor\", have_role_async \"admin\")");
let (value, ty) = engine.into_evaluator().eval(expr.as_ref()).await.unwrap();
assert_eq!(
ty,
Type::tuple(vec![
Type::builtin(BuiltinTypeId::Bool),
Type::builtin(BuiltinTypeId::Bool)
])
);
let items = tuple_items(&value);
assert_eq!(items.len(), 2);
assert!(items[0].to_rust::<bool>().unwrap());
assert!(!items[1].to_rust::<bool>().unwrap());
}
#[tokio::test]
async fn generic_export_can_repeat_a_value_into_a_list() {
let mut engine: Engine<()> = Engine::with_prelude(()).unwrap();
let t_var = engine.type_system.fresh_type_var(Some(Symbol::intern("T")));
let t = Type::var(t_var.clone());
let scheme = Scheme::new(
vec![t_var],
vec![],
Type::fun(
t.clone(),
Type::fun(Type::builtin(BuiltinTypeId::I32), Type::list(t)),
),
);
inject_globals(&mut engine, |module| {
module.export_native("repeat_value", scheme, 2, |engine, _, args| {
let value = args[0].clone();
let len = args[1].to_rust::<i32>()?;
let copies = (0..len.max(0)).map(|_| value.clone()).collect();
list_from_handles(engine.heap(), copies)
})
})
.unwrap();
let expr = parse(r#"(repeat_value "rex" 3, repeat_value true 2)"#);
let (value, ty) = engine.into_evaluator().eval(expr.as_ref()).await.unwrap();
assert_eq!(
ty,
Type::tuple(vec![
Type::list(Type::builtin(BuiltinTypeId::String)),
Type::list(Type::builtin(BuiltinTypeId::Bool)),
])
);
let items = tuple_items(&value);
let repeated_strings = handle_as_list(&items[0]).unwrap();
assert_eq!(repeated_strings.len(), 3);
assert_eq!(repeated_strings[0].to_rust::<String>().unwrap(), "rex");
assert_eq!(repeated_strings[1].to_rust::<String>().unwrap(), "rex");
assert_eq!(repeated_strings[2].to_rust::<String>().unwrap(), "rex");
let repeated_bools = handle_as_list(&items[1]).unwrap();
assert_eq!(repeated_bools.len(), 2);
assert!(repeated_bools[0].to_rust::<bool>().unwrap());
assert!(repeated_bools[1].to_rust::<bool>().unwrap());
}
#[tokio::test]
async fn generic_export_can_swap_two_values_of_different_types() {
let mut engine: Engine<()> = Engine::with_prelude(()).unwrap();
let p_var = engine.type_system.fresh_type_var(Some(Symbol::intern("P")));
let q_var = engine.type_system.fresh_type_var(Some(Symbol::intern("Q")));
let p = Type::var(p_var.clone());
let q = Type::var(q_var.clone());
let scheme = Scheme::new(
vec![p_var, q_var],
vec![],
Type::fun(p.clone(), Type::fun(q.clone(), Type::tuple(vec![q, p]))),
);
inject_globals(&mut engine, |module| {
module.export_native("swap_pair", scheme, 2, |engine, _, args| {
engine
.heap()
.alloc_tuple(vec![args[1].clone(), args[0].clone()])
})
})
.unwrap();
let expr = parse(r#"(swap_pair "left" 7, swap_pair true "right")"#);
let (value, ty) = engine.into_evaluator().eval(expr.as_ref()).await.unwrap();
assert_eq!(
ty,
Type::tuple(vec![
Type::tuple(vec![
Type::builtin(BuiltinTypeId::I32),
Type::builtin(BuiltinTypeId::String),
]),
Type::tuple(vec![
Type::builtin(BuiltinTypeId::String),
Type::builtin(BuiltinTypeId::Bool),
]),
])
);
let items = tuple_items(&value);
let first_swap = tuple_items(&items[0]);
assert_eq!(first_swap[0].to_rust::<i32>().unwrap(), 7);
assert_eq!(first_swap[1].to_rust::<String>().unwrap(), "left");
let second_swap = tuple_items(&items[1]);
assert_eq!(second_swap[0].to_rust::<String>().unwrap(), "right");
assert!(second_swap[1].to_rust::<bool>().unwrap());
}
#[tokio::test]
async fn overloaded_exports_types_and_values() {
let mut engine: Engine<()> = Engine::with_prelude(()).unwrap();
EmbedRecord::inject_rex(&mut engine).unwrap();
inject_globals(&mut engine, |module| {
module.export("over1", |_state: &(), x: i32| Ok(x + 1))?;
module.export("over1", |_state: &(), x: bool| {
Ok(if x {
"bool:true".to_string()
} else {
"bool:false".to_string()
})
})?;
module.export("over1", |_state: &(), rec: EmbedRecord| Ok(rec.n > 10))?;
module.export("over3", |_state: &(), a: i32, b: i32, c: i32| Ok(a + b + c))?;
module.export("over3", |_state: &(), a: String, b: String, c: String| {
Ok(a.len() < b.len() + c.len())
})?;
module.export(
"over3",
|_state: &(), a: EmbedRecord, b: EmbedRecord, c: EmbedRecord| {
Ok(format!("records:{}:{}:{}", a.n, b.n, c.n))
},
)?;
Ok(())
})
.unwrap();
let expr = r#"
(
over1 41,
over1 true,
over1 (EmbedRecord { n = 9 }),
over3 1 2 3,
over3 "a" "bb" "ccc",
over3 (EmbedRecord { n = 1 }) (EmbedRecord { n = 2 }) (EmbedRecord { n = 3 })
)
"#;
let (_, inferred) = engine.infer_snippet(expr).unwrap();
assert_overload_tuple_type_shape(&inferred);
let value = engine.into_evaluator().eval(parse(expr).as_ref()).await;
assert!(value.is_ok(), "evaluation failed: {value:?}");
let (value, ty) = value.unwrap();
assert_overload_tuple_type_shape(&ty);
let items = tuple_items(&value);
assert_eq!(items.len(), 6);
assert_eq!(items[0].to_rust::<i32>().unwrap(), 42);
assert_eq!(items[1].to_rust::<String>().unwrap(), "bool:true");
assert!(!items[2].to_rust::<bool>().unwrap());
assert_eq!(items[3].to_rust::<i32>().unwrap(), 6);
assert!(items[4].to_rust::<bool>().unwrap());
assert_eq!(items[5].to_rust::<String>().unwrap(), "records:1:2:3");
}
#[tokio::test]
async fn overloaded_async_exports_types_and_values() {
let mut engine: Engine<()> = Engine::with_prelude(()).unwrap();
EmbedRecord::inject_rex(&mut engine).unwrap();
inject_globals(&mut engine, |module| {
module.export_async("a1", |_state: &(), x: i32| async move { Ok(x + 1) })?;
module.export_async("a1", |_state: &(), x: bool| async move {
Ok(if x {
"bool:true".to_string()
} else {
"bool:false".to_string()
})
})?;
module.export_async("a1", |_state: &(), rec: EmbedRecord| async move {
Ok(rec.n > 10)
})?;
module.export_async("a3", |_state: &(), a: i32, b: i32, c: i32| async move {
Ok(a + b + c)
})?;
module.export_async(
"a3",
|_state: &(), a: String, b: String, c: String| async move {
Ok(a.len() < b.len() + c.len())
},
)?;
module.export_async(
"a3",
|_state: &(), a: EmbedRecord, b: EmbedRecord, c: EmbedRecord| async move {
Ok(format!("records:{}:{}:{}", a.n, b.n, c.n))
},
)?;
Ok(())
})
.unwrap();
let expr = r#"
(
a1 41,
a1 true,
a1 (EmbedRecord { n = 9 }),
a3 1 2 3,
a3 "a" "bb" "ccc",
a3 (EmbedRecord { n = 1 }) (EmbedRecord { n = 2 }) (EmbedRecord { n = 3 })
)
"#;
let (_, inferred) = engine.infer_snippet(expr).unwrap();
assert_overload_tuple_type_shape(&inferred);
let value = engine.into_evaluator().eval(parse(expr).as_ref()).await;
assert!(value.is_ok(), "evaluation failed: {value:?}");
let (value, ty) = value.unwrap();
assert_overload_tuple_type_shape(&ty);
let items = tuple_items(&value);
assert_eq!(items.len(), 6);
assert_eq!(items[0].to_rust::<i32>().unwrap(), 42);
assert_eq!(items[1].to_rust::<String>().unwrap(), "bool:true");
assert!(!items[2].to_rust::<bool>().unwrap());
assert_eq!(items[3].to_rust::<i32>().unwrap(), 6);
assert!(items[4].to_rust::<bool>().unwrap());
assert_eq!(items[5].to_rust::<String>().unwrap(), "records:1:2:3");
}