use rustyfi_backend::{FontKey, FontMetrics, Length};
use rustyfi_lang::value::Value;
use rustyfi_lang::{elaborate, eval, primitives, typecheck, CompileError};
struct Mono;
impl FontMetrics for Mono {
fn advance(&self, _f: FontKey, c: char, size: Length) -> Option<Length> {
if c.is_ascii() {
Some(size * 0.5)
} else {
None
}
}
fn ascender(&self, _f: FontKey, size: Length) -> Length {
size * 0.75
}
fn descender(&self, _f: FontKey, size: Length) -> Length {
size * 0.25
}
}
fn run(src: &str) -> Result<Value, CompileError> {
let file = rustyfi_syntax::parse_file(src)?;
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)?;
typecheck::typecheck(&program)?;
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
Ok(interp.eval(&env, &rustyfi_lang::ast::debrand(&program.body, &store))?)
}
fn int(src: &str) -> i64 {
match run(src).unwrap() {
Value::Int(n) => n,
other => panic!("{src:?} evaluated to {other:?}, not an int"),
}
}
const GREET_CMD: &str = "let-inline ctx \\greet ?:name =
read-inline ctx (
match name with
| Some(_) -> { Hello there, my dear friend! }
| None -> { Hi. }
)
let-inline ctx \\math m = inline-nil
in
";
#[test]
fn inline_command_optional_arg_supplied_and_omitted_typecheck_and_evaluate() {
let src = format!(
"{GREET_CMD}\
let base = get-initial-context 200pt (command \\math) in
let ib-yes = read-inline base {{ \\greet?:(1); }} in
let ib-no = read-inline base {{ \\greet?*; }} in
let (w-yes, _, _) = get-natural-metrics ib-yes in
let (w-no, _, _) = get-natural-metrics ib-no in
if w-yes >' w-no then 1 else 0"
);
assert_eq!(
int(&src),
1,
"supplied greeting should render wider than the omitted fallback"
);
}
#[test]
fn inline_command_with_only_an_omission_marker_still_evaluates() {
let src = format!(
"{GREET_CMD}\
let base = get-initial-context 200pt (command \\math) in
let ib = read-inline base {{ \\greet?*; }} in
let (w, _, _) = get-natural-metrics ib in
if w >' 0pt then 1 else 0"
);
assert_eq!(int(&src), 1);
}
#[test]
fn inline_command_marker_less_bare_call_pads_the_same_as_explicit_omission() {
let src = format!(
"{GREET_CMD}\
let base = get-initial-context 200pt (command \\math) in
let ib-bare = read-inline base {{ \\greet; }} in
let ib-omitted = read-inline base {{ \\greet?*; }} in
let (w-bare, _, _) = get-natural-metrics ib-bare in
let (w-omitted, _, _) = get-natural-metrics ib-omitted in
(w-bare, w-omitted)"
);
match run(&src).unwrap() {
Value::Tuple(vs) if vs.len() == 2 => {
let mut it = vs.into_iter();
let (Value::Length(w_bare), Value::Length(w_omitted)) =
(it.next().unwrap(), it.next().unwrap())
else {
panic!("expected two lengths");
};
assert_eq!(
w_bare, w_omitted,
"marker-less `{{ \\greet; }}` should elaborate exactly like explicit `{{ \\greet?*; }}`"
);
}
other => panic!("expected a (length * length) tuple, got {other:?}"),
}
}
#[test]
fn optional_arrow_type_unifies_with_option_domain_and_call_sites() {
let src = "type greeter = string ?-> string -> int
type box-ty = | MkBox of greeter
let combine prefix-opt s = match prefix-opt with
| Some(p) -> string-length p + string-length s
| None -> string-length s
let apply-supplied b p s = match b with | MkBox(f) -> f ?:(p) s
let apply-omitted b s = match b with | MkBox(f) -> f ?* s
let bx = MkBox combine
in
let via-some = apply-supplied bx `ab` `cde` in
let via-none = apply-omitted bx `cde` in
if via-some == 5 then
(if via-none == 3 then 1 else 0)
else 0";
assert_eq!(int(src), 1);
}
#[test]
fn optional_arrow_type_declaration_alone_typechecks() {
let src = "type greeter = string ?-> string -> int
type section-cmd = [string?; string?; inline-text; block-text] block-cmd
in
0";
assert_eq!(int(src), 0);
}