use std::collections::{HashMap, HashSet};
use rustyfi_lang::types::Stage;
use rustyfi_lang::value::Value;
use rustyfi_lang::{elaborate, eval, primitives, typecheck};
use rustyfi_syntax::RustyfiVersion;
fn eval_str(src: &str) -> Result<Value, String> {
let file = rustyfi_syntax::parse_file(src).map_err(|e| format!("parse: {e}"))?;
let env = primitives::base_env();
let store = rustyfi_lang::symbol::SymbolStore::new();
let scope = elaborate::Scope::new(&store, env.names());
let ast = elaborate::elaborate(&file, &scope).map_err(|e| format!("elaborate: {e}"))?;
let mut interp = eval::Interp::new(&NoMetrics);
interp
.eval(&env, &rustyfi_lang::ast::debrand(&ast, &store))
.map_err(|e| format!("eval: {e}"))
}
fn typecheck_str(src: &str, stages: &HashMap<usize, Stage>) -> Result<(), String> {
let file = rustyfi_syntax::parse_file(src).map_err(|e| format!("parse: {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_with_stages(&file, &scope, stages)
.map_err(|e| format!("elaborate: {e}"))?;
typecheck::typecheck_verbose(&program)
.map(|_| ())
.map_err(|e| format!("{e}"))
}
fn run_str(src: &str, stages: &HashMap<usize, Stage>) -> Result<Value, String> {
let file = rustyfi_syntax::parse_file(src).map_err(|e| format!("parse: {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_with_stages(&file, &scope, stages)
.map_err(|e| format!("elaborate: {e}"))?;
typecheck::typecheck_verbose(&program).map_err(|e| format!("typecheck: {e}"))?;
let mut interp = eval::Interp::new(&NoMetrics);
interp
.eval(&env, &rustyfi_lang::ast::debrand(&program.body, &store))
.map_err(|e| format!("eval: {e}"))
}
struct NoMetrics;
impl rustyfi_backend::FontMetrics for NoMetrics {
fn advance(
&self,
_f: rustyfi_backend::FontKey,
_c: char,
size: rustyfi_backend::Length,
) -> Option<rustyfi_backend::Length> {
Some(size * 0.5)
}
fn ascender(&self, _f: rustyfi_backend::FontKey, size: rustyfi_backend::Length) -> rustyfi_backend::Length {
size * 0.75
}
fn descender(&self, _f: rustyfi_backend::FontKey, size: rustyfi_backend::Length) -> rustyfi_backend::Length {
size * 0.25
}
}
#[test]
fn a_splice_of_a_quote_is_the_quoted_value() {
assert!(matches!(eval_str("~(&(1 + 1))").unwrap(), Value::Int(2)));
}
#[test]
fn a_quote_is_not_run_when_it_is_built() {
let v = eval_str("let c = &(1 / 0) in 7").unwrap();
assert!(matches!(v, Value::Int(7)), "an unforced quote must not run");
}
#[test]
fn a_quote_sees_the_environment_it_was_written_in() {
let v = eval_str("let f = (let x = 10 in ~(&(&(x)))) in let x = 99 in ~f");
assert!(matches!(v.unwrap(), Value::Int(10)));
}
#[test]
fn quotes_compose_through_a_stage_zero_function() {
let v = eval_str("let twice = ~(&( fun s -> ~(&(fun t -> t ^ t)) s )) in twice `ab`");
assert!(matches!(v.unwrap(), Value::Str(ref s) if s == "abab"));
}
#[test]
fn splicing_a_non_code_value_is_a_runtime_error_not_a_wrong_answer() {
let err = eval_str("~(1)").unwrap_err();
assert!(err.contains("code"), "expected a code-value error, got {err}");
}
#[test]
fn a_quote_at_the_document_stage_is_refused() {
let err = typecheck_str("let c = &(1) in 0", &HashMap::new()).unwrap_err();
assert!(
err.contains("only valid at stage 0"),
"expected a staging error, got {err}"
);
}
#[test]
fn a_splice_at_stage_zero_is_refused() {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Stage0);
let err = typecheck_str("let c = ~(1) in 0", &stages).unwrap_err();
assert!(
err.contains("only valid at stage 1"),
"expected a stage-1-only error, got {err}"
);
}
#[test]
fn a_splice_needs_a_code_value() {
let err = typecheck_str("let x = ~(1) in 0", &HashMap::new()).unwrap_err();
assert!(err.contains("code"), "expected a `code` mismatch, got {err}");
}
#[test]
fn a_stage_zero_binding_may_quote() {
let src = "let c = &(1) in 0";
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Stage0);
typecheck_str(src, &stages).expect("a stage-0 binding may quote");
}
#[test]
fn a_persistent_binding_may_not_quote() {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Persistent0);
let err = typecheck_str("let c = &(1) in 0", &stages).unwrap_err();
assert!(
err.contains("only valid at stage 0"),
"expected a staging error, got {err}"
);
}
#[test]
fn the_stage_does_not_leak_past_the_binding_it_was_declared_for() {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Stage0);
let err = typecheck_str("let a = &(1)\nlet b = &(2) in 0", &stages).unwrap_err();
assert!(
err.contains("only valid at stage 0"),
"the second binding is still stage 1: {err}"
);
}
#[test]
fn a_stage_zero_let_rec_may_quote() {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Stage0);
typecheck_str("let-rec f x = &(1) in 0", &stages).expect("a stage-0 `let-rec` may quote");
}
#[test]
fn a_stage_zero_let_mutable_may_quote() {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Stage0);
typecheck_str("let-mutable r <- &(1) in 0", &stages)
.expect("a stage-0 `let-mutable` may quote");
}
#[test]
fn a_stage_zero_command_binding_may_quote() {
for src in [
"let-inline \\c = let n = &(1) in { } in 0",
"let-block +c = let n = &(1) in '< > in 0",
"let-math \\c = let n = &(1) in ${} in 0",
] {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Stage0);
typecheck_str(src, &stages)
.unwrap_or_else(|e| panic!("the file's stage must reach this binding: {src} -> {e}"));
}
}
#[test]
fn a_staged_let_is_rejected_in_a_zero_zero_six_file() {
for src in [
"let ~x = 1 in 0",
"let-rec ~f x = x in 0",
"let-mutable ~r <- 1 in 0",
"let-inline ~ctx \\c = { } in 0",
"let-block ~ctx +c = '< > in 0",
"let-math ~\\c = ${} in 0",
] {
let err = typecheck_str(src, &HashMap::new()).unwrap_err();
assert!(
err.contains("SATySFi 0.1 syntax"),
"expected a version error for {src}, got {err}"
);
}
}
#[test]
fn the_code_type_has_no_zero_zero_six_spelling() {
let mut stages = HashMap::new();
stages.insert(1usize, Stage::Stage0);
let err = typecheck_str("type t = C of int code\nlet x = C (&(1)) in 0", &stages).unwrap_err();
assert!(
err.contains("mismatch"),
"`int code` must not name the code type under 0.0.6, got {err}"
);
}
#[test]
fn an_unstaged_program_is_unaffected() {
typecheck_str("let x = 1 + 1 in x", &HashMap::new()).expect("no staging, no change");
assert!(matches!(eval_str("let x = 1 + 1 in x").unwrap(), Value::Int(2)));
}
fn two_entries(bound: Option<Stage>, user: Option<Stage>) -> HashMap<usize, Stage> {
let mut stages = HashMap::new();
if let Some(st) = bound {
stages.insert(0usize, st);
}
if let Some(st) = user {
stages.insert(1usize, st);
}
stages
}
fn reference_across(bound: Option<Stage>, user: Option<Stage>) -> Result<(), String> {
typecheck_str("let a = 1\nlet b = a in 0", &two_entries(bound, user))
}
fn assert_stage_rejected(bound: Option<Stage>, user: Option<Stage>) {
let err = reference_across(bound, user).expect_err("this occurrence must be refused");
assert!(
err.contains("invalid occurrence") && err.contains("as to stage"),
"expected a staging-occurrence error, got {err}"
);
}
#[test]
fn a_stage_zero_binding_is_not_nameable_from_stage_one() {
assert_stage_rejected(Some(Stage::Stage0), None);
}
#[test]
fn a_stage_one_binding_is_not_nameable_from_stage_zero() {
assert_stage_rejected(None, Some(Stage::Stage0));
}
#[test]
fn a_stage_zero_binding_is_not_nameable_from_the_persistent_stage() {
assert_stage_rejected(Some(Stage::Stage0), Some(Stage::Persistent0));
}
#[test]
fn a_stage_one_binding_is_not_nameable_from_the_persistent_stage() {
assert_stage_rejected(None, Some(Stage::Persistent0));
}
#[test]
fn same_stage_references_are_accepted() {
reference_across(Some(Stage::Stage0), Some(Stage::Stage0))
.expect("stage 0 may name stage 0");
reference_across(Some(Stage::Persistent0), Some(Stage::Persistent0))
.expect("the persistent stage may name itself");
}
#[test]
fn a_persistent_binding_is_nameable_from_every_stage() {
reference_across(Some(Stage::Persistent0), None).expect("stage 1 may name persistent");
reference_across(Some(Stage::Persistent0), Some(Stage::Stage0))
.expect("stage 0 may name persistent");
reference_across(Some(Stage::Persistent0), Some(Stage::Persistent0))
.expect("persistent may name persistent");
}
#[test]
fn a_primitive_is_nameable_from_every_stage() {
for st in [Stage::Persistent0, Stage::Stage0, Stage::Stage1] {
let mut stages = HashMap::new();
stages.insert(0usize, st);
typecheck_str("let a = 1 + 1 in 0", &stages)
.unwrap_or_else(|e| panic!("`+` must be nameable at {}: {e}", st.as_str()));
}
}
#[test]
fn a_staged_binding_may_name_its_own_siblings_through_the_aliases_it_mints() {
let mut stages = HashMap::new();
for i in 0..2 {
stages.insert(i, Stage::Persistent0);
}
typecheck_str(
"module M : sig val twice : int -> int end = struct\n\
\x20 let-rec double x = x * 2\n\
\x20 let twice x = double x\n\
end\n\
module N : sig val four : int end = struct\n\
\x20 let four = M.twice 2\n\
end\n\
let n = N.four in n",
&stages,
)
.expect("a persistent module's members may name each other and be named from the document");
}
#[test]
fn a_persistent_binding_named_from_inside_a_quote_evaluates_to_its_value() {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Persistent0);
stages.insert(1usize, Stage::Stage0);
let v = run_str("let p = 10\nlet c = &(p) in ~c", &stages)
.expect("a quote may name a persistent binding");
assert!(matches!(v, Value::Int(10)), "expected 10, got {v:?}");
}
#[test]
fn a_quoted_persistent_reference_is_not_captured_at_the_splice_site() {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Persistent0);
stages.insert(1usize, Stage::Stage0);
for src in [
"let p = 10\nlet c = &(p)\nlet p = 99 in ~c",
"let p = 10\nlet c = &(p) in let p = 99 in ~c",
] {
let v = run_str(src, &stages).expect("a quote may name a persistent binding");
assert!(
matches!(v, Value::Int(10)),
"the quote's `p` is the persistent binding, not the splice site's: {src} -> {v:?}"
);
}
}
#[test]
fn a_quoted_persistent_reference_survives_being_carried_out_of_scope() {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Persistent0);
stages.insert(1usize, Stage::Stage0);
let v = run_str(
"let p = 10\nlet c = (let hide = 1 in &(p * 2)) in let p = `no` in ~c",
&stages,
)
.expect("a quote may name a persistent binding");
assert!(matches!(v, Value::Int(20)), "expected 20, got {v:?}");
}
#[test]
fn a_stage_header_survives_the_cross_version_splice() {
let mut stages = HashMap::new();
stages.insert(0usize, Stage::Stage0);
typecheck_str("let c = &(1)\nlet d = 2 in 0", &stages)
.expect("a spliced stage-0 binding may quote");
let err = typecheck_str("let c = &(1)\nlet d = &(2) in 0", &stages).unwrap_err();
assert!(
err.contains("only valid at stage 0"),
"the consumer's own bindings stay stage 1: {err}"
);
}
#[test]
fn a_staged_let_is_rejected_in_a_spliced_zero_zero_six_dependency() {
let file = rustyfi_syntax::parse_file("let ~x = 1 in 0").expect("parse");
let env = primitives::base_env();
let store = rustyfi_lang::symbol::SymbolStore::new();
let scope = elaborate::Scope::new_with_version(&store, env.names(), RustyfiVersion::V0_1);
elaborate::elaborate_program_with_versions(
&file,
&scope,
&HashSet::new(),
&HashMap::new(),
None,
)
.expect("a V0_1-authored `let ~x` elaborates");
let err = elaborate::elaborate_program_with_versions(
&file,
&scope,
&HashSet::from([0usize]),
&HashMap::new(),
None,
)
.expect_err("a V0_0-authored `let ~x` must be refused even under a V0_1 scope")
.to_string();
assert!(
err.contains("SATySFi 0.1 syntax"),
"expected a version error, got {err}"
);
}