mod common;
use common::get_test_context;
use codespan_reporting::term::{self, Config, termcolor::NoColor};
use compact_str::CompactString;
use insta::assert_snapshot;
use numbat::NumbatError;
use numbat::diagnostic::ErrorDiagnostic;
use numbat::markup::{Formatter, PlainTextFormatter};
use numbat::resolver::CodeSource;
use numbat::{Context, InterpreterResult, pretty_print::PrettyPrint};
#[track_caller]
fn expect_output_with_context(ctx: &mut Context, code: &str, expected_output: impl AsRef<str>) {
let expected_output = expected_output.as_ref();
println!("Expecting output {expected_output:?} for code\n{code}");
if let InterpreterResult::Value(val) = ctx.interpret(code, CodeSource::Internal).unwrap().1 {
let fmt = PlainTextFormatter {};
let actual_output = fmt.format(&val.pretty_print(), false);
assert_eq!(actual_output.trim(), expected_output);
} else {
panic!("Did not receive an interpreter result. Did you try to print the values?");
}
}
#[track_caller]
fn succeed(code: &str) -> CompactString {
let mut ctx = get_test_context();
let ret = ctx.interpret(code, CodeSource::Internal);
match ret {
Err(e) => panic!("was supposed to succeed but instead got:\n{}", e),
Ok((_stmts, ret)) => {
if let InterpreterResult::Value(val) = ret {
let fmt = PlainTextFormatter {};
fmt.format(&val.pretty_print(), false)
} else {
CompactString::const_new("")
}
}
}
}
#[track_caller]
fn fail(code: &str) -> NumbatError {
let mut ctx = get_test_context();
let ret = ctx.interpret(code, CodeSource::Internal);
match ret {
Err(e) => *e,
Ok((_stmts, ret)) => {
if let InterpreterResult::Value(val) = ret {
let fmt = PlainTextFormatter {};
let output = fmt.format(&val.pretty_print(), false);
panic!("was supposed to fail but instead got:\n{}", output.trim())
} else {
panic!("was supposed to fail but instead got:\n{ret:?}")
}
}
}
}
#[track_caller]
fn expect_output(code: &str, expected_output: impl AsRef<str>) {
let mut ctx = get_test_context();
expect_output_with_context(&mut ctx, code, expected_output)
}
#[track_caller]
fn expect_pretty_print(code: &str, expected_pretty_print_output: impl AsRef<str>) {
let mut ctx = get_test_context();
let (statements, _result) = ctx.interpret(code, CodeSource::Internal).unwrap();
assert_eq!(statements.len(), 1);
let statement = &statements[0];
assert_eq!(
statement.pretty_print().to_string(),
expected_pretty_print_output.as_ref()
);
}
#[track_caller]
fn expect_failure_with_context(ctx: &mut Context, code: &str, msg_part: &str) {
if let Err(e) = ctx.interpret(code, CodeSource::Internal) {
let error_message = e.to_string();
println!("{error_message}");
assert!(
error_message.contains(msg_part),
"Expected '{msg_part}' but got '{error_message}'"
);
} else {
panic!("Expected an error but the code '{code}' did not fail");
}
}
#[track_caller]
fn expect_failure(code: &str, msg_part: &str) {
let mut ctx = get_test_context();
expect_failure_with_context(&mut ctx, code, msg_part)
}
#[track_caller]
fn get_error_message(code: &str) -> String {
let mut ctx = get_test_context();
if let Err(e) = ctx.interpret(code, CodeSource::Internal) {
e.to_string()
} else {
panic!();
}
}
#[track_caller]
fn get_diagnostic_output(code: &str) -> String {
let mut ctx = get_test_context();
if let Err(e) = ctx.interpret(code, CodeSource::Internal) {
let mut output = NoColor::new(Vec::new());
let config = Config::default();
let diagnostics: Vec<_> = match &*e {
NumbatError::ResolverError(e) => e.diagnostics(),
NumbatError::NameResolutionError(e) => e.diagnostics(),
NumbatError::TypeCheckError(e) => e.diagnostics(),
NumbatError::RuntimeError(_) => {
panic!("RuntimeError diagnostics require ResolverDiagnostic wrapper")
}
};
for diagnostic in diagnostics {
term::emit(&mut output, &config, &ctx.resolver().files, &diagnostic).unwrap();
}
String::from_utf8(output.into_inner()).unwrap()
} else {
panic!("Expected error but code succeeded");
}
}
#[test]
fn simple_value() {
expect_output("0", "0");
expect_output("0_0", "0");
expect_output("0_0.0_0", "0");
expect_output(".0", "0");
expect_failure("_.0", "Unexpected character in identifier: '.'");
expect_output(".0_0", "0");
expect_failure(".0_", "Unexpected character in number literal: '_'");
expect_output("0b0", "0");
expect_output("0b01", "1");
expect_output("0b0_0", "0");
expect_failure("0b012", "Expected base-2 digit");
expect_failure("0b", "Expected base-2 digit");
expect_failure("0b_", "Expected base-2 digit");
expect_failure("0b_0", "Expected base-2 digit");
expect_failure("0b0_", "Expected base-2 digit");
expect_failure("0b0.0", "Expected base-2 digit");
expect_output("0o0", "0");
expect_output("0o01234567", "342_391");
expect_output("0o0_0", "0");
expect_failure("0o012345678", "Expected base-8 digit");
expect_failure("0o", "Expected base-8 digit");
expect_failure("0o_", "Expected base-8 digit");
expect_failure("0o_0", "Expected base-8 digit");
expect_failure("0o0_", "Expected base-8 digit");
expect_failure("0o0.0", "Expected base-8 digit");
expect_output("0x0", "0");
expect_output("0x0123456789abcdef", "8.19855e+16");
expect_output("0x0_0", "0");
expect_failure("0x0123456789abcdefg", "Expected base-16 digit");
expect_failure("0x", "Expected base-16 digit");
expect_failure("0x_", "Expected base-16 digit");
expect_failure("0x_0", "Expected base-16 digit");
expect_failure("0x0_", "Expected base-16 digit");
expect_failure("0x0.0", "Expected base-16 digit");
expect_output("NaN", "NaN");
expect_output("inf", "inf");
}
#[test]
fn test_factorial() {
expect_output("0!", "1");
expect_output("4!", "24");
expect_output("4.0!", "24");
expect_output("4.0!!", "8");
expect_output("4 !", "24");
expect_output("4 !!", "8");
expect_output(" 4 !", "24");
expect_output("(4)!", "24");
expect_output("3!^3", "216");
expect_output("(3!)^3", "216");
expect_output("3^3!", "729");
expect_output("-5!", "-120");
expect_output("-(5!)", "-120");
expect_output("-(5)!", "-120");
expect_output("-5!!", "-15");
expect_output("-(5!!)", "-15");
expect_output("-(5)!!", "-15");
expect_output("5!!", "15");
expect_output("5!!!", "10");
expect_output("5!!!!", "5");
expect_output("5!!!!!", "5");
expect_output("5!!!!!!", "5");
expect_output("(3!)!!!", "18");
expect_failure(
"(-1)!",
"Expected factorial argument to be a non-negative integer",
);
expect_failure(
"1.5!",
"Expected factorial argument to be a finite integer number",
);
expect_failure(
"(-1.5)!",
"Expected factorial argument to be a non-negative integer",
);
expect_failure(
"(2m)!",
"Argument of factorial needs to be dimensionless (got Length).",
);
}
#[test]
fn test_exponentiation() {
expect_output("3²*2", "18");
expect_output("3² 2", "18");
expect_output("3²·2", "18");
expect_output("3³*2", "54");
expect_output("3³(2)", "54");
expect_output("(1+2)²", "9");
expect_output("2²pi", "12.5664");
expect_output("2² pi", "12.5664");
expect_output("2²·pi", "12.5664");
expect_output("5m² to cm·m", "500 cm·m");
expect_output("2⁵", "32");
expect_output("-4¹", "-4");
expect_output("2⁻¹", "0.5");
expect_output("2⁻²", "0.25");
expect_output("10⁻⁵", "0.00001");
expect_failure("0^(-1)", "Division by zero");
expect_failure("0^(-2)", "Division by zero");
expect_failure("(0m)^(-2)", "Division by zero");
}
#[test]
fn test_conversions() {
expect_output("2in to cm", "5.08 cm");
expect_output("5m^2 -> m*cm", "500 m·cm");
expect_output("5m^2 -> cm*m", "500 cm·m");
expect_output("1 kB / 10 ms -> MB/s", "0.1 MB/s");
expect_output("55! / (6! (55 - 6)!) -> million", "28.9897 million");
let mut ctx = get_test_context();
let _ = ctx
.interpret("let x = 1 deg", CodeSource::Internal)
.unwrap();
expect_output_with_context(&mut ctx, "12 deg -> x", "12°");
}
#[test]
fn test_conversions_with_nontrivial_magnitude() {
expect_output("6 hours -> 45 min", "8 × 45 min");
expect_output("1 KiB -> 16 B", "64 × 16 B");
expect_output("10 m -> 2 m", "5 × 2 m");
expect_output("2 m -> 10 m", "0.2 × 10 m");
expect_output("10 m -> 0.5 m", "20 × 0.5 m");
expect_output("10 m -> (m/3)", "30 × 0.333333 m");
expect_output("1/(30 mpg) -> L/(100 km)", "7.84049 × 0.01 l/km");
expect_output("10 m -> m", "10 m");
expect_output("10 m -> 1 m", "10 m");
expect_output("(6 hours -> 45 min) + 1 min", "361 min");
expect_output("(1 KiB -> 16 B) + 1 B", "1025 B");
expect_output("3 / m -> 0.2 / m", "15 × 0.2 m⁻¹");
expect_output("10 / s -> 0.5 / s", "20 × 0.5 s⁻¹");
}
#[test]
fn test_implicit_conversion() {
let mut ctx = get_test_context();
let _ = ctx.interpret("let x = 5 m", CodeSource::Internal).unwrap();
expect_output_with_context(&mut ctx, "x", "5 m");
expect_output_with_context(&mut ctx, "2x", "10 m");
expect_output_with_context(&mut ctx, "2 x", "10 m");
expect_output_with_context(&mut ctx, "x x", "25 m²");
expect_output_with_context(&mut ctx, "x²", "25 m²");
expect_failure("x2", "Unknown identifier 'x2'");
}
#[test]
fn test_reset_after_runtime_error() {
let mut ctx = get_test_context();
let _ = ctx.interpret("let x = 1", CodeSource::Internal).unwrap();
let res = ctx.interpret("1/0", CodeSource::Internal);
assert!(res.is_err());
expect_output_with_context(&mut ctx, "x", "1");
}
#[test]
fn test_function_inverses() {
expect_output("sin(asin(0.1234))", "0.1234");
expect_output("cos(acos(0.1234))", "0.1234");
expect_output("tan(atan(0.1234))", "0.1234");
expect_output("sinh(asinh(0.1234))", "0.1234");
expect_output("cosh(acosh(1.1234))", "1.1234");
expect_output("tanh(atanh(0.1234))", "0.1234");
expect_output("log(exp(0.1234))", "0.1234");
expect_output("log10(10^0.1234)", "0.1234");
expect_output("log2(2^0.1234)", "0.1234");
expect_output("sqr(sqrt(0.1234))", "0.1234");
expect_output("asin(sin(0.1234))", "0.1234");
expect_output("acos(cos(0.1234))", "0.1234");
expect_output("atan(tan(0.1234))", "0.1234");
expect_output("asinh(sinh(0.1234))", "0.1234");
expect_output("acosh(cosh(1.1234))", "1.1234");
expect_output("atanh(tanh(0.1234))", "0.1234");
expect_output("exp(log(0.1234))", "0.1234");
expect_output("10^(log10(0.1234))", "0.1234");
expect_output("2^(log2(0.1234))", "0.1234");
expect_output("sqrt(sqr(0.1234))", "0.1234");
}
#[test]
fn test_algebra() {
let mut ctx = get_test_context();
let _ = ctx
.interpret("use extra::algebra", CodeSource::Internal)
.unwrap();
expect_output_with_context(&mut ctx, "quadratic_equation(1, 0, -1)", "[1, -1]");
expect_output_with_context(&mut ctx, "quadratic_equation(0, 9, 3)", "[-0.333333]");
expect_output_with_context(&mut ctx, "quadratic_equation(0, 0, 1)", "[]");
expect_output_with_context(&mut ctx, "quadratic_equation(9, -126, 441)", "[7]");
expect_output_with_context(&mut ctx, "quadratic_equation(1, -2, 1)", "[1]");
expect_output_with_context(&mut ctx, "quadratic_equation(0, 1, 1)", "[-1]");
expect_output_with_context(&mut ctx, "quadratic_equation(1, 0, 0)", "[0]");
expect_failure_with_context(
&mut ctx,
"quadratic_equation(0, 0, 0)",
"infinitely many solutions",
);
expect_output_with_context(&mut ctx, "quadratic_equation(1, 1, 1)", "[]");
expect_output_with_context(&mut ctx, "cubic_equation(1, -6, 11, -6)", "[1, 2, 3]");
expect_output_with_context(&mut ctx, "cubic_equation(1, 0, 0, -1)", "[1]");
expect_output_with_context(&mut ctx, "cubic_equation(1, 0, 0, 0)", "[0]");
expect_output_with_context(&mut ctx, "cubic_equation(0, 1, -3, 2)", "[1, 2]");
expect_output_with_context(&mut ctx, "cubic_equation(0, 0, 1, -4)", "[4]");
expect_output_with_context(&mut ctx, "cubic_equation(0, 0, 0, 5)", "[]");
expect_output_with_context(&mut ctx, "cubic_equation(1, 1, 1, 1)", "[-1]");
expect_failure_with_context(
&mut ctx,
"cubic_equation(0, 0, 0, 0)",
"infinitely many solutions",
);
}
#[test]
fn test_math() {
expect_output("sin(90°)", "1");
expect_output("sin(30°)", "0.5");
expect_output("sin(pi/2)", "1");
expect_output("atan2(10, 0) / (pi / 2)", "1");
expect_output("atan2(100 cm, 1 m) / (pi / 4)", "1");
expect_failure(
"atan2(100 cm, 1 m²)",
"Could not solve the following constraint",
);
expect_output("mod(5, 3)", "2");
expect_output("mod(-1, 4)", "3");
expect_output("mod(8 cm, 5 cm)", "3 cm");
expect_output("mod(235 cm, 1 m)", "35 cm");
expect_output("mod(2 m, 7 cm)", "0.04 m");
expect_failure("mod(8 m, 5 s)", "Could not solve the following constraint")
}
#[test]
fn test_incompatible_dimension_errors() {
assert_snapshot!(
get_error_message("kg m / s^2 + kg m^2"),
@r###"
left hand side: Length × Mass × Time⁻² [= Force]
right hand side: Length² × Mass [= MomentOfInertia]
"###
);
assert_snapshot!(
get_error_message("1 + m"),
@r###"
left hand side: Scalar [= Angle, Scalar, SolidAngle]
right hand side: Length
Suggested fix: divide the expression on the right hand side by a `Length` factor
"###
);
assert_snapshot!(
get_error_message("m / s + K A"),
@r###"
left hand side: Length / Time [= Velocity]
right hand side: Current × Temperature
"###
);
assert_snapshot!(
get_error_message("m + 1 / m"),
@r###"
left hand side: Length
right hand side: Length⁻¹ [= Wavenumber]
Suggested fix: invert the expression on the right hand side
"###
);
assert_snapshot!(
get_error_message("kW -> J"),
@r###"
left hand side: Length² × Mass × Time⁻³ [= Power]
right hand side: Length² × Mass × Time⁻² [= Energy, Torque]
Suggested fix: divide the expression on the right hand side by a `Time` factor
"###
);
assert_snapshot!(
get_error_message("sin(1 meter)"),
@r###"
parameter type: Scalar [= Angle, Scalar, SolidAngle]
argument type: Length
Suggested fix: divide the function argument by a `Length` factor
"###
);
assert_snapshot!(
get_error_message("let x: Acceleration = 4 m / s"),
@r###"
specified dimension: Length × Time⁻² [= Acceleration]
actual dimension: Length × Time⁻¹ [= Velocity]
Suggested fix: divide the right hand side expression by a `Time` factor
"###
);
assert_snapshot!(
get_error_message("unit x: Acceleration = 4 m / s"),
@r###"
specified dimension: Length × Time⁻² [= Acceleration]
actual dimension: Length × Time⁻¹ [= Velocity]
Suggested fix: divide the right hand side expression by a `Time` factor
"###
);
assert_snapshot!(
get_error_message("fn acceleration(length: Length, time: Time) -> Acceleration = length / time"),
@r###"
specified return type: Length × Time⁻² [= Acceleration]
actual return type: Length × Time⁻¹ [= Velocity]
Suggested fix: divide the expression in the function body by a `Time` factor
"###
);
}
#[test]
fn test_temperature_conversions() {
expect_output("11.5 °C", "284.65 K");
expect_output("89.3 °F", "304.983 K");
expect_output("0 K -> °C", "-273.15");
expect_output("fahrenheit(30 K)", "-405.67");
expect_output("100 °C -> °C", "100");
expect_output("100 °F -> °F", "100.0");
expect_output("(123 K -> °C) °C", "123 K");
expect_output("(123 K -> °F) °F", "123 K");
expect_output("-40 °F -> °C", "-40");
}
#[test]
fn test_other_functions() {
expect_output("sqrt(4)", "2");
expect_output("sqrt(-1)", "NaN");
expect_output("cbrt(27)", "3");
expect_output("cbrt(-64)", "-4");
expect_output("log10(100000)", "5");
expect_output("log(e^15)", "15");
expect_output("ln(e^15)", "15");
expect_output("ceil(3.1)", "4");
expect_output("floor(3.9)", "3");
expect_output("round(3.9)", "4");
expect_output("round(3.1)", "3");
expect_output("is_nan(NaN)", "true");
expect_output("is_nan(NaN cm)", "true");
expect_output("is_nan(ln(-1))", "true");
expect_output("is_nan(1)", "false");
expect_output("is_infinite(inf)", "true");
expect_output("is_infinite(-inf)", "true");
expect_output("is_infinite(1)", "false");
}
#[test]
fn test_last_result_identifier() {
let mut ctx = get_test_context();
let _ = ctx.interpret("2 + 3", CodeSource::Internal).unwrap();
expect_output_with_context(&mut ctx, "ans", "5");
let _ = ctx.interpret("1 + 2", CodeSource::Internal).unwrap();
expect_output_with_context(&mut ctx, "_", "3");
}
#[test]
fn test_addition_subtraction_commutativity() {
expect_output("2 min + 30 s", "150 s");
expect_output("30 s + 2 min", "150 s");
expect_output("1 m + 50 cm", "150 cm");
expect_output("50 cm + 1 m", "150 cm");
expect_output("2 min - 30 s", "90 s");
expect_output("30 s - 2 min", "-90 s");
expect_output("1 m - 50 cm", "50 cm");
expect_output("50 cm - 1 m", "-50 cm");
expect_output("1 hour + 30 min + 45 s", "5445 s");
expect_output("1 hour + 45 s + 30 min", "5445 s");
expect_output("30 min + 1 hour + 45 s", "5445 s");
expect_output("30 min + 45 s + 1 hour", "5445 s");
expect_output("45 s + 1 hour + 30 min", "5445 s");
expect_output("45 s + 30 min + 1 hour", "5445 s");
}
#[test]
fn test_issue_589_floor_in_precision() {
expect_output("1.5 weeks - floor_in(days, 1.5 weeks)", "0.5 day");
expect_output("1.5 weeks - floor_in(days, 1.5 weeks) -> hours", "12 h");
}
#[test]
fn test_misc_examples() {
expect_output("1920/16*9", "1080");
expect_output("2^32", "4_294_967_296");
expect_output("sqrt(1.4^2 + 1.5^2) * cos(pi/3)^2", "0.512957");
expect_output("2min + 30s", "150 s");
expect_output("30s + 2min", "150 s");
expect_output("4/3 * pi * (6000km)³", "9.04779e+11 km³");
expect_output("40kg * 9.8m/s^2 * 150cm", "588 J");
expect_output("sin(30°)", "0.5");
expect_output("60mph -> m/s", "26.8224 m/s");
expect_output("240km/day -> km/h", "10 km/h");
expect_output("1mrad -> °", "0.0572958°");
expect_output("52weeks -> days", "364 day");
expect_output("5in + 2ft -> cm", "73.66 cm");
expect_output("atan(30cm / 2m) -> deg", "8.53077°");
expect_output("6Mbit/s * 1.5h -> GB", "4.05 GB");
expect_output("6Mbit/s * 1.5h -> GiB", "3.77186 GiB");
expect_output("3m/4m", "0.75");
expect_output("4/2*2", "4");
expect_output("1/2 Hz -> s", "0.5 s");
expect_output("let b = \";\"; b", "\";\"");
expect_output("let b = \";\"\nb", "\";\"");
expect_output("let a = 3m; 5; a", "3 m");
expect_output("let a = 3m\n5\na", "3 m");
}
#[test]
fn test_bohr_radius_regression() {
expect_output("bohr_radius", "5.29177e-11 m");
}
#[test]
fn test_full_simplify() {
expect_output("5 cm/m", "0.05");
expect_output("hour/second", "3600");
expect_output("5 to cm/m", "500 cm/m");
expect_output(
"fn f(x: Scalar) -> Scalar = x to cm/m
f(5)",
"500 cm/m",
);
expect_output("1 Wh/W", "1 h");
expect_output("1 × (m/s)^2/(m/s)", "1 m/s");
expect_output("mph * s/m", "0.44704");
expect_output("3% * 1kg", "0.03 kg");
expect_output("m * g / cm", "100 g");
expect_output("gal/in", "231 in²");
expect_output(
"4 pi ε0 ℏ^2 / (electron_charge^2 electron_mass)",
"5.29177e-11 m",
);
expect_output("J/s", "1 W");
expect_output("Pa m²", "1 N");
expect_output("50 Ω * 2 A", "100 V");
expect_output("10 N / 5 Pa", "2 m²");
expect_output(
"sqrt(ℏ gravitational_constant / speed_of_light^3)",
"1.61626e-35 m",
);
expect_output("1 dpi * 1 in", "1 dot");
expect_output("1 mph * 1 hour", "1 mi");
expect_output("1 dot / 96 dpi", "0.0104167 in");
}
#[test]
fn test_prefixes() {
expect_output("hertz second", "1");
expect_output("kilohertz millisecond", "1");
expect_output("megahertz microsecond", "1");
expect_output("gigahertz nanosecond", "1");
expect_output("terahertz picosecond", "1");
expect_output("petahertz femtosecond", "1");
expect_output("exahertz attosecond", "1");
expect_output("zettahertz zeptosecond", "1");
expect_output("yottahertz yoctosecond", "1");
expect_output("ronnahertz rontosecond", "1");
expect_output("quettahertz quectosecond", "1");
}
#[test]
fn test_parse_errors() {
expect_failure(
"3kg+",
"Expected one of: number, identifier, parenthesized expression, struct instantiation",
);
expect_failure("let print=2", "Expected identifier after 'let' keyword");
expect_failure(
"fn print(x: Scalar) = 1",
"Expected identifier after 'fn' keyword",
);
}
#[test]
fn test_name_clash_errors() {
expect_failure("let kg=2", "Identifier is already in use: 'kg'");
expect_failure("fn kg(x: Scalar) = 1", "Identifier is already in use: 'kg'");
expect_failure("fn _()=0", "Reserved identifier");
}
#[test]
fn test_parameter_shadowing() {
expect_output(
"fn area(h: Length, w: Length) -> Area = h × w
area(3 m, 4 m)",
"12 m²",
);
expect_output(
"fn test(x: Scalar) -> Scalar = h where h = x + 1
test(2)",
"3",
);
expect_output(
"fn f(e: Scalar) -> Scalar = e + 1
f(2)",
"3",
);
}
#[test]
fn test_type_check_errors() {
expect_failure("foo", "Unknown identifier 'foo'");
expect_failure(
"let sin=2",
"Identifier is already in use by the foreign function: 'sin'",
);
expect_failure(
"fn pi() = 1",
"Identifier is already in use by the constant: 'pi'",
);
expect_failure(
"fn sin(x)=0",
"Identifier is already in use by the foreign function: 'sin'",
);
}
#[test]
fn test_unknown_dimension_error() {
assert_snapshot!(
get_diagnostic_output("unit my_length: Lenght"),
@r###"
error: while type checking
┌─ <internal:3>:1:17
│
1 │ unit my_length: Lenght
│ ^^^^^^ Unknown dimension 'Lenght'
│
= Did you mean 'Length'?
"###
);
assert_snapshot!(
get_diagnostic_output("dimension Foo = Lenght * Time"),
@r###"
error: while type checking
┌─ <internal:3>:1:17
│
1 │ dimension Foo = Lenght * Time
│ ^^^^^^ Unknown dimension 'Lenght'
│
= Did you mean 'Length'?
"###
);
assert_snapshot!(
get_diagnostic_output("fn speed(x: Velocty) -> Scalar = 1"),
@r###"
error: while type checking
┌─ <internal:3>:1:13
│
1 │ fn speed(x: Velocty) -> Scalar = 1
│ ^^^^^^^ Unknown dimension 'Velocty'
│
= Did you mean 'Velocity'?
"###
);
}
#[test]
fn test_runtime_errors() {
expect_failure("1/0", "Division by zero");
}
#[test]
fn test_comparisons() {
expect_output("2 < 3", "true");
expect_output("2 m < 3 m", "true");
expect_output("20 cm < 3 m", "true");
expect_output("2 m < 100 cm", "false");
expect_output("2 > 3", "false");
expect_output("2 m > 3 m", "false");
expect_output("20 cm > 3 m", "false");
expect_output("2 m > 100 cm", "true");
expect_output("2 <= 2", "true");
expect_output("2.1 <= 2", "false");
expect_output("2 >= 2", "true");
expect_output("2 >= 2.1", "false");
expect_output("NaN < NaN", "false");
expect_output("NaN < 0", "false");
expect_output("NaN < 0m", "false");
expect_output("0 < NaN", "false");
expect_output("0m < NaN", "false");
expect_output("NaN <= NaN", "false");
expect_output("NaN <= 0", "false");
expect_output("NaN <= 0m", "false");
expect_output("0 <= NaN", "false");
expect_output("0m <= NaN", "false");
expect_output("NaN > NaN", "false");
expect_output("NaN > 0", "false");
expect_output("NaN > 0m", "false");
expect_output("0 > NaN", "false");
expect_output("0m > NaN", "false");
expect_output("NaN >= NaN", "false");
expect_output("NaN >= 0", "false");
expect_output("NaN >= 0m", "false");
expect_output("0 >= NaN", "false");
expect_output("0m >= NaN", "false");
expect_output("200 cm == 2 m", "true");
expect_output("201 cm == 2 m", "false");
expect_output("200 cm != 2 m", "false");
expect_output("201 cm != 2 m", "true");
}
#[test]
fn test_logical() {
expect_output("!true", "false");
expect_output("!false", "true");
expect_output("true || false", "true");
expect_output("false || false", "false");
expect_output("true && false", "false");
expect_output("true && true", "true");
expect_output("false || true && false", "false");
expect_output("false || true && !false", "true");
insta::assert_snapshot!(fail("1 || 2"), @"Expected boolean value");
insta::assert_snapshot!(fail("true || 2"), @"Expected boolean value");
insta::assert_snapshot!(fail("1 || true"), @"Expected boolean value");
insta::assert_snapshot!(fail("1 && 2"), @"Expected boolean value");
insta::assert_snapshot!(fail("true && 2"), @"Expected boolean value");
insta::assert_snapshot!(fail("1 && true"), @"Expected boolean value");
insta::assert_snapshot!(fail("!1"), @"Expected boolean value");
insta::assert_snapshot!(fail("!1 || true"), @"Expected boolean value");
}
#[test]
fn test_conditionals() {
expect_output("if 1 < 2 then 3 else 4", "3");
expect_output("if 4 < 3 then 2 else 1", "1");
expect_output(
"if 4 > 3 then \"four is larger!\" else \"four is not larger!\"",
"\"four is larger!\"",
);
}
#[test]
fn test_string_interpolation() {
expect_output("\"pi = {pi}!\"", "\"pi = 3.14159!\"");
expect_output("\"1 + 2 = {1 + 2}\"", "\"1 + 2 = 3\"");
expect_output("\"{0.2:0.5}\"", "\"0.20000\"");
expect_output("\"pi ~= {pi:.3}\"", "\"pi ~= 3.142\"");
expect_output(
"\"both {pi:.3} and {e} are irrational and transcendental numbers\"",
"\"both 3.142 and 2.71828 are irrational and transcendental numbers\"",
);
expect_output(
"
let str = \"1234\"
\"{str:0.2}\"
",
"\"12\"",
);
expect_output("\"{1_000_300:+.3}\"", "\"+1000300.000\"");
expect_output(
"
let str = \"1234\"
\"a {str:^10} b\"
",
"\"a 1234 b\"",
);
expect_failure(
"\"{200:x}\"",
"Incorrect type for format specifiers: Unknown format code 'x' for type",
);
expect_failure(
"\"{200:.}\"",
"Invalid format specifiers: Format specifier missing precision",
);
expect_failure(
"
let str = \"1234\"
\"{str:.3f}\"
",
"Incorrect type for format specifiers: Unknown format code Some('f') for object of type 'str'",
);
expect_output(
"let power = 1 J/s
\"{power}\"",
"\"1 W\"",
);
}
#[test]
fn test_overwrite_regular_function() {
expect_output(
"
fn f(x)=0
fn f(x)=1
f(2)",
"1",
);
}
#[test]
fn test_overwrite_inner_function() {
expect_output(
"
fn inner() = 0
fn outer() = inner()
fn inner(x) = 1
outer()
",
"0",
);
}
#[test]
fn test_676_where_clause_does_not_leak() {
expect_output(
"
fn foo(x: Scalar) -> Scalar = x + something where something = 1
unit something
1 something
",
"1 something",
);
}
#[test]
fn test_override_constants() {
expect_output("let x = 1\nlet x = 2\nx", "2");
expect_output("let pi = 4\npi", "4");
}
#[test]
fn test_overwrite_captured_constant() {
expect_output(
"
let x = 1
fn f() = sin(x)
let x = 1 m
f()
",
"0.841471",
);
}
#[test]
fn test_pretty_print_prefixes() {
expect_output("1 megabarn", "1 megabarn");
}
#[test]
fn test_full_simplify_for_function_calls() {
expect_output("floor(1.2 hours / hour)", "1");
}
#[test]
fn test_datetime_runtime_errors() {
expect_failure(
"datetime(\"2000-01-99\")",
"Unrecognized datetime format: failed to parse day in date: parsed day is not valid: parameter 'day' with value 99 is not in the required range of 1..=31",
);
expect_failure("now() -> tz(\"Europe/NonExisting\")", "Unknown timezone");
expect_failure(
"date(\"2000-01-01\") + 1e100 years",
"Exceeded maximum size for time durations",
);
expect_failure(
"date(\"2000-01-01\") + 15_000 years",
"DateTime out of range",
);
expect_failure(
"format_datetime(\"%Y %;\", now())",
"strftime formatting failed: found unrecognized specifier directive `;`.",
);
expect_failure(
"format_datetime(\"%:\", now())",
"strftime formatting failed: expected to find specifier directive after colons",
);
}
#[test]
fn test_user_errors() {
expect_failure("error(\"test\")", "User error: test");
expect_failure("- error(\"test\")", "User error: test");
expect_failure("1 + error(\"test\")", "User error: test");
expect_failure("1 m + error(\"test\")", "User error: test");
expect_failure("if 3 < 2 then 2 m else error(\"test\")", "User error: test");
expect_failure(
"if true then error(\"test\") else \"foo\"",
"User error: test",
);
expect_failure(
"if false then \"foo\" else error(\"test\")",
"User error: test",
);
}
#[test]
fn test_recovery_after_runtime_error() {
{
let mut ctx = get_test_context();
expect_failure_with_context(&mut ctx, "let x = 1/0", "Division by zero");
expect_failure_with_context(&mut ctx, "x", "Unknown identifier 'x'");
}
{
let mut ctx = get_test_context();
expect_failure_with_context(&mut ctx, "let x = 1/0", "Division by zero");
assert!(
ctx.interpret("let x = 1", CodeSource::Internal)
.unwrap()
.1
.is_continue()
);
expect_output_with_context(&mut ctx, "x", "1");
}
}
#[test]
fn test_statement_pretty_printing() {
expect_pretty_print("let v = 10 m/s", "let v: Velocity = 10 metre / second");
expect_pretty_print(
"let v: Length * Frequency = 10 m/s",
"let v: Length × Frequency = 10 metre / second",
);
expect_pretty_print("let x = 0 + 1 m", "let x: Length = 0 + 1 metre");
expect_pretty_print("let x: Length = 0", "let x: Length = 0");
expect_pretty_print("let x = 0", "let x: forall A: Dim. A = 0");
expect_pretty_print(
"unit my_length_base_unit: Length",
"unit my_length_base_unit: Length",
);
expect_pretty_print(
"unit my_custom_base_unit",
"unit my_custom_base_unit: MyCustomBaseUnit",
);
expect_pretty_print(
"unit my_speed_unit = 10 m/s",
"unit my_speed_unit: Velocity = 10 metre / second",
);
expect_pretty_print(
"fn f(v)=(v+1 knot)/1s",
"fn f(v: Velocity) -> Acceleration = (v + 1 knot) / 1 second",
);
expect_pretty_print("fn f(x) = x", "fn f<A>(x: A) -> A = x");
expect_pretty_print("fn f(x, y) = y", "fn f<A, B>(x: A, y: B) -> B = y");
expect_pretty_print("fn f(x, y) = x", "fn f<A, B>(x: B, y: A) -> B = x");
expect_pretty_print("fn f(x) = 2 x", "fn f<A: Dim>(x: A) -> A = 2 x");
expect_pretty_print(
"fn f(x, y) = x * y",
"fn f<A: Dim, B: Dim>(x: A, y: B) -> A × B = x × y",
);
expect_pretty_print(
"fn f() -> Length * Frequency = c",
"fn f() -> Length × Frequency = c",
);
expect_pretty_print(
"fn f(v: Length * Frequency) = 1",
"fn f(v: Length × Frequency) -> Scalar = 1",
);
expect_pretty_print("fn f(x: Length) = 2 x", "fn f(x: Length) -> Length = 2 x");
expect_pretty_print("fn f(x) -> Length = 2 x", "fn f(x: Length) -> Length = 2 x");
expect_pretty_print("fn f<Z>(z: Z) = z", "fn f<Z>(z: Z) -> Z = z");
expect_pretty_print(
"fn f(x) = y where y = x",
"fn f<A>(x: A) -> A = y\n where y: A = x",
);
}
#[test]
fn test_parse() {
expect_output("let x: Scalar = parse(\"3.14\"); x", "3.14");
expect_output("let x: Scalar = parse(\" 42 \"); x", "42");
expect_output("let x: Scalar = parse(\"-1.5\"); x", "-1.5");
expect_output("let x: Scalar = parse(\"1e10\"); x", "10_000_000_000");
expect_output("let x: Scalar = parse(\"-2.5e-3\"); x", "-0.0025");
expect_output("let x: Scalar = parse(\"0xFF\"); x", "255");
expect_output("let x: Scalar = parse(\"0b1010\"); x", "10");
expect_output("let x: Scalar = parse(\"NaN\"); x", "NaN");
expect_output("let x: Scalar = parse(\"inf\"); x", "inf");
expect_output("let x: Scalar = parse(\"-inf\"); x", "-inf");
expect_output("let x: Length = parse(\"1.5 km\"); x", "1.5 km");
expect_output("let x: Length = parse(\"100 m\"); x", "100 m");
expect_output("let x: Length = parse(\"-50 cm\"); x", "-50 cm");
expect_output("let x: Mass = parse(\"2.5 kg\"); x", "2.5 kg");
expect_output("let x: Length = parse(\"inf m\"); x", "inf m");
expect_output("let x: Length = parse(\"NaN m\"); x", "NaN m");
expect_failure("let x: Scalar = parse(\"not a number\")", "Invalid pattern");
expect_failure("let x: Scalar = parse(\"1 1\")", "Invalid pattern");
expect_failure(
"let x: Length = parse(\"1 unknown_unit\")",
"Expected unit identifier",
);
expect_failure(
"parse(\"1.5\")",
"parse() requires a type annotation for the return type",
);
expect_failure(
"let x: Length = parse(\"1 kg\")",
"Type mismatch: expected `Length`, got `Mass`",
);
expect_failure(
"let x: Scalar = parse(\"1 kg\")",
"Type mismatch: expected `Scalar`, got `Mass`",
);
expect_failure(
"let x: Length = parse(\"1\")",
"Type mismatch: expected `Length`, got `Scalar`",
);
expect_output("let x: Length = parse(\"0\"); x", "0");
expect_output("let x: Mass = parse(\"0\"); x", "0");
expect_output("let x: Length = parse(\"0 kg\"); x", "0");
expect_output(
"unit jump: Length\nlet x: Length = parse(\"4 jump\"); x",
"4 jump",
);
expect_output(
"unit leap = 2 meter\nlet x: Length = parse(\"3 leap\"); x",
"3 leap",
);
}
#[test]
fn test_temperature_syntactic_sugar() {
expect_output("0 °C", "273.15 K");
expect_output("100 °C", "373.15 K");
expect_output("-5 °C", "268.15 K");
expect_output("32 °F", "273.15 K");
expect_output("212 °F", "373.15 K");
expect_output("273.15 K -> °C", "0");
expect_output("373.15 K -> °F", "212");
expect_output("100 °C -> °F", "212");
expect_output("212 °F -> °C", "100.0");
expect_output("-40 °C -> °F", "-40.0");
expect_output("-40 °F -> °C", "-40");
expect_output("(50 + 50) °C", "373.15 K");
expect_output("0 °C + 10 K -> °C", "10");
expect_output("20 celsius", "293.15 K");
expect_output("20 degree_celsius", "293.15 K");
expect_output("68 fahrenheit", "293.15 K");
expect_output("68 degree_fahrenheit", "293.15 K");
expect_pretty_print("let t = 20 °C", "let t: Temperature = 20 °C");
expect_pretty_print("let t = -5 °F", "let t: Temperature = (-5) °F");
expect_pretty_print("let v = 300 K -> °C", "let v: Scalar = 300 kelvin -> °C");
expect_pretty_print("let v = 300 K -> °F", "let v: Scalar = 300 kelvin -> °F");
expect_pretty_print("let t = 20 celsius", "let t: Temperature = 20 °C");
expect_pretty_print("let t = 68 fahrenheit", "let t: Temperature = 68 °F");
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
mod assert_eq {
use super::*;
#[test]
fn succeeds_when_eps_larger_than_diff() {
insta::assert_snapshot!(succeed("assert_eq(200cm, 201cm, 2cm)"), @"");
}
#[test]
fn succeeds_when_eps_is_equal_to_diff() {
insta::assert_snapshot!(succeed("assert_eq(200cm, 201cm, 1cm)"), @"");
}
#[test]
fn fails_when_types_mismatch() {
insta::assert_snapshot!(fail("assert_eq(200cm, 201cm, 2 watts)"), @"Argument types in assert_eq calls must match");
}
#[test]
fn fails_and_does_not_show_original_values_when_same_units() {
insta::assert_snapshot!(fail("assert_eq(200cm, 201cm, 0.1cm)"), @r###"
Assertion failed because the following two quantities differ by 1.0 cm, which is more than 0.1 cm:
200.0 cm
201.0 cm
"###);
}
#[test]
fn fails_and_show_original_values_for_rhs_only() {
insta::assert_snapshot!(fail("assert_eq(2000mm, 201cm, 0.1cm to mm)"), @r###"
Assertion failed because the following two quantities differ by 10 mm, which is more than 1 mm:
2000 mm
2010 mm (201 cm)
"###);
}
#[test]
fn fails_and_show_original_values_for_lhs_only() {
insta::assert_snapshot!(fail("assert_eq(2m, 2010mm, 0.1cm to mm)"), @r###"
Assertion failed because the following two quantities differ by 10 mm, which is more than 1 mm:
2000 mm (2 m)
2010 mm
"###);
}
#[test]
fn fails_and_show_original_values_for_lhs_and_rhs() {
insta::assert_snapshot!(fail("assert_eq(2m, 201cm, 0.1cm to mm)"), @r###"
Assertion failed because the following two quantities differ by 10 mm, which is more than 1 mm:
2000 mm (2 m)
2010 mm (201 cm)
"###);
}
#[test]
fn fails_and_formats_to_match_precision_of_eps() {
insta::assert_snapshot!(fail("assert_eq(212121000.123456cm, 212121001.123456cm, 0.900cm)"), @r###"
Assertion failed because the following two quantities differ by 1.0 cm, which is more than 0.9 cm:
212121000.1 cm
212121001.1 cm
"###);
insta::assert_snapshot!(fail("assert_eq(2.0000006cm, 243.311336cm, 0.0001mm)"), @r###"
Assertion failed because the following two quantities differ by 2413.1134 mm, which is more than 0.0001 mm:
0020.0000 mm (2.0 cm)
2433.1134 mm (243.311 cm)
"###);
insta::assert_snapshot!(fail("assert_eq(-2.0000006cm, -243cm, 0.0001mm)"), @r###"
Assertion failed because the following two quantities differ by 2410.0000 mm, which is more than 0.0001 mm:
-0020.0000 mm (-2.0 cm)
-2430.0000 mm (-243 cm)
"###);
insta::assert_snapshot!(fail("assert_eq(2.0000006cm, -243cm, 0.0001mm)"), @r###"
Assertion failed because the following two quantities differ by 2450.0000 mm, which is more than 0.0001 mm:
00020.0000 mm (2.0 cm)
-2430.0000 mm (-243 cm)
"###);
insta::assert_snapshot!(fail("assert_eq(212121000.123456cm, 212121001.123456cm, -0.900cm)"), @r###"
Assertion failed because the following two quantities differ by 1.0 cm, which is more than -0.9 cm:
212121000.1 cm
212121001.1 cm
"###);
}
#[test]
fn issue505_angles() {
insta::assert_snapshot!(fail("assert_eq(-77° + 0′ + 32″, -77.0089°, 1e-4°)"), @r###"
Assertion failed because the following two quantities differ by 0.0178°, which is more than 0.0001°:
-76.9911° (-277_168″)
-77.0089°
"###);
}
#[test]
fn test_floating_point_warning() {
insta::assert_snapshot!(fail("assert_eq(2+ 2, 2 + 1)"), @r###"
Assertion failed because the following two values are not the same:
4
3
"###);
insta::assert_snapshot!(fail("assert_eq(2 + 2e-12, 2 + 1e-12)"), @r###"
Assertion failed because the following two values are not the same:
2.0
2.0
Note: The two printed values appear to be the same, this may be due to floating point precision errors.
For dimension types you may want to test approximate equality instead: assert_eq(q1, q2, ε).
"###);
}
}
}