use rustyfi_backend::{FontKey, FontMetrics, Length, PureHorzBox, VertBox};
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> {
Some(size * 0.5)
}
fn ascender(&self, _f: FontKey, size: Length) -> Length {
size * 0.75
}
fn descender(&self, _f: FontKey, size: Length) -> Length {
size * 0.25
}
}
fn typecheck_str(src: &str) -> Result<(), 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)?;
Ok(())
}
fn assert_well_typed(src: &str) {
if let Err(e) = typecheck_str(src) {
panic!("expected {src:?} to type-check, got error: {e}");
}
}
fn assert_type_error(src: &str) {
match typecheck_str(src) {
Ok(()) => panic!("expected {src:?} to be rejected by the typechecker, but it passed"),
Err(CompileError::Type(_)) => {}
Err(other) => panic!("expected {src:?} to fail with a type error, got: {other}"),
}
}
fn eval_str(src: &str) -> Value {
let file = rustyfi_syntax::parse_file(src).expect("parse");
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).expect("elaborate");
typecheck::typecheck(&program).expect("typecheck");
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
interp
.eval(&env, &rustyfi_lang::ast::debrand(&program.body, &store))
.expect("eval")
}
const LENGTH_ABS_SRC: &str = "let length-abs len = if len <' 0pt then 0pt -' len else len
in
length-abs (0pt -' 5pt)";
#[test]
fn length_abs_formula_typechecks_with_existing_prims() {
assert_well_typed(LENGTH_ABS_SRC);
}
#[test]
fn length_abs_formula_evaluates_the_absolute_value() {
match eval_str(LENGTH_ABS_SRC) {
Value::Length(l) => assert!((l.0 - 5.0).abs() < 1e-9, "expected 5pt, got {}pt", l.0),
other => panic!("expected a length, got {other:?}"),
}
}
#[test]
fn embed_block_bottom_typechecks() {
assert_well_typed(
"let-inline ctx \\math m = inline-nil
in
embed-block-bottom (get-initial-context 100pt (command \\math)) 100pt (fun ctx -> block-nil)",
);
}
#[test]
fn embed_block_bottom_rejects_a_non_context_first_argument() {
assert_type_error(
"let-inline ctx \\math m = inline-nil
in
embed-block-bottom 5 100pt (fun ctx -> block-nil)",
);
}
#[test]
fn embed_block_bottom_wraps_the_solidified_block_at_the_given_width() {
let v = eval_str(
"let-inline ctx \\math m = inline-nil
in
embed-block-bottom (get-initial-context 100pt (command \\math)) 50pt (fun ctx -> block-skip 30pt)",
);
match v {
Value::InlineBoxes(boxes) => {
assert_eq!(boxes.len(), 1);
match &boxes[0] {
rustyfi_backend::HorzBox::Pure(PureHorzBox::EmbeddedBlock {
width,
height,
depth,
block,
..
}) => {
assert_eq!(*width, Length::pt(50.0));
assert_eq!(*height, Length::pt(30.0));
assert_eq!(*depth, Length::pt(0.0));
assert_eq!(block.len(), 1);
}
other => panic!("expected an EmbeddedBlock, got {other:?}"),
}
}
other => panic!("expected inline-boxes, got {other:?}"),
}
}
#[test]
fn line_stack_bottom_typechecks() {
assert_well_typed("line-stack-bottom [inline-skip 10pt; inline-skip 20pt]");
}
#[test]
fn line_stack_bottom_rejects_a_non_inline_boxes_list() {
assert_type_error("line-stack-bottom [1; 2; 3]");
}
#[test]
fn line_stack_bottom_stacks_each_element_as_one_line_at_the_widest_natural_width() {
let v = eval_str("line-stack-bottom [inline-skip 10pt; inline-skip 20pt]");
match v {
Value::InlineBoxes(boxes) => {
assert_eq!(boxes.len(), 1);
match &boxes[0] {
rustyfi_backend::HorzBox::Pure(PureHorzBox::EmbeddedBlock {
width, block, ..
}) => {
assert_eq!(*width, Length::pt(20.0), "wid should be the widest line");
assert_eq!(block.len(), 2, "each list element becomes its own line");
assert!(
block.iter().all(|vb| matches!(vb, VertBox::Line { .. })),
"every entry should be a stacked line, not a skip"
);
}
other => panic!("expected an EmbeddedBlock, got {other:?}"),
}
}
other => panic!("expected inline-boxes, got {other:?}"),
}
}
#[test]
fn line_stack_top_typechecks() {
assert_well_typed("line-stack-top [inline-skip 10pt; inline-skip 20pt]");
}
#[test]
fn line_stack_top_rejects_a_non_inline_boxes_list() {
assert_type_error("line-stack-top [1; 2; 3]");
}
#[test]
fn line_stack_top_and_bottom_differ_only_in_the_anchor() {
fn unwrap(v: Value) -> (Length, usize, bool) {
match v {
Value::InlineBoxes(boxes) => match &boxes[0] {
rustyfi_backend::HorzBox::Pure(PureHorzBox::EmbeddedBlock {
width,
block,
anchor_last,
..
}) => (*width, block.len(), *anchor_last),
other => panic!("expected an EmbeddedBlock, got {other:?}"),
},
other => panic!("expected inline-boxes, got {other:?}"),
}
}
let src = "[inline-skip 10pt; inline-skip 20pt]";
let (top_w, top_n, top_anchor) = unwrap(eval_str(&format!("line-stack-top {src}")));
let (bot_w, bot_n, bot_anchor) = unwrap(eval_str(&format!("line-stack-bottom {src}")));
assert_eq!((top_w, top_n), (bot_w, bot_n), "same block, same width");
assert!(!top_anchor, "`-top` anchors at the FIRST line");
assert!(bot_anchor, "`-bottom` anchors at the LAST line");
}
#[test]
fn add_footnote_typechecks() {
assert_well_typed("add-footnote (block-skip 10pt)");
}
#[test]
fn add_footnote_rejects_a_non_block_boxes_argument() {
assert_type_error("add-footnote 5");
}
#[test]
fn add_footnote_wraps_the_block_in_a_footnote_marker() {
match eval_str("add-footnote (block-skip 10pt)") {
Value::InlineBoxes(boxes) => {
assert_eq!(boxes.len(), 1);
match &boxes[0] {
rustyfi_backend::HorzBox::Pure(PureHorzBox::Footnote { block }) => {
assert_eq!(
block,
&vec![VertBox::Skip(Length::pt(10.0))],
"the footnote marker must carry the block unchanged"
);
}
other => panic!("expected a Footnote marker, got {other:?}"),
}
}
other => panic!("expected inline-boxes, got {other:?}"),
}
}