mod common;
use serde_json::json;
use tabnas::{Tabnas, Value as EngineValue};
use tabnas_expr::{ExprOptions, OpDef, PrevalDef};
use tabnas_support::{load_spec, Runner, SpecOptions, Value};
use common::{parse_simplified, parser_default, parser_for, parser_with, spec_dir, to_failure};
fn run_spec(name: &str, parser: Tabnas) {
let path = spec_dir().join(name);
assert!(path.is_file(), "missing shared fixture {}", path.display());
hold_to_canonical_json(&path, &parser);
Runner::new(move |input| {
parse_simplified(&parser, input)
.map(Value::from)
.map_err(to_failure)
})
.file(&path);
}
#[test]
fn spec_happy() {
run_spec("happy.tsv", parser_default());
}
#[test]
fn spec_binary() {
run_spec("binary.tsv", parser_default());
}
#[test]
fn spec_dangling_operator() {
run_spec("dangling-operator.tsv", parser_default());
}
#[test]
fn spec_arithmetic_mixed() {
run_spec("arithmetic-mixed.tsv", parser_default());
}
#[test]
fn spec_prefix_infix_mixed() {
run_spec("prefix-infix-mixed.tsv", parser_default());
}
#[test]
fn spec_paren_deep_nest() {
run_spec("paren-deep-nest.tsv", parser_default());
}
#[test]
fn spec_structure_arith() {
run_spec("structure-arith.tsv", parser_default());
}
#[test]
fn spec_structure() {
run_spec("structure.tsv", parser_default());
}
#[test]
fn spec_render() {
run_spec("render.tsv", parser_default());
}
#[test]
fn spec_unary_prefix_basic() {
run_spec("unary-prefix-basic.tsv", parser_default());
}
#[test]
fn spec_paren_basic() {
run_spec("paren-basic.tsv", parser_default());
}
#[test]
fn spec_implicit_list_top_basic() {
run_spec("implicit-list-top-basic.tsv", parser_default());
}
#[test]
fn spec_json_base() {
run_spec("json-base.tsv", parser_default());
}
#[test]
fn spec_jsonic_base() {
run_spec("jsonic-base.tsv", parser_default());
}
#[test]
fn spec_implicit_list_top_paren() {
run_spec("implicit-list-top-paren.tsv", parser_default());
}
#[test]
fn spec_paren_implicit_list() {
run_spec("paren-implicit-list.tsv", parser_default());
}
#[test]
fn spec_paren_implicit_map() {
run_spec("paren-implicit-map.tsv", parser_default());
}
#[test]
fn spec_map_implicit_list_paren() {
run_spec("map-implicit-list-paren.tsv", parser_default());
}
#[test]
fn spec_paren_list_implicit_structure_comma() {
run_spec("paren-list-implicit-structure-comma.tsv", parser_default());
}
#[test]
fn spec_paren_list_implicit_structure_space() {
run_spec("paren-list-implicit-structure-space.tsv", parser_default());
}
#[test]
fn spec_paren_map_implicit_structure_comma() {
run_spec("paren-map-implicit-structure-comma.tsv", parser_default());
}
#[test]
fn spec_paren_map_implicit_structure_space() {
run_spec("paren-map-implicit-structure-space.tsv", parser_default());
}
#[test]
fn spec_infix_in_paren_map() {
run_spec("infix-in-paren-map.tsv", parser_default());
}
fn suffix_ops() -> serde_json::Value {
json!({
"op": {
"factorial": { "suffix": true, "left": 6000000, "src": "!" },
"question": { "suffix": true, "left": 3500000, "src": "?" },
}
})
}
#[test]
fn spec_binding_power_zero() {
run_spec(
"binding-power-zero.tsv",
parser_for(json!({
"op": {
"zero": { "infix": true, "left": 0, "right": 0, "src": "~" },
"below": { "infix": true, "left": -2000000, "right": -1900000, "src": "@" },
}
})),
);
}
#[test]
fn spec_unary_prefix_edge() {
run_spec(
"unary-prefix-edge.tsv",
parser_for(json!({
"op": {
"at": { "prefix": true, "right": 5000000, "src": "@" },
"tight": { "infix": true, "left": 7000000, "right": 7100000, "src": "~" },
}
})),
);
}
#[test]
fn spec_unary_suffix_basic() {
run_spec("unary-suffix-basic.tsv", parser_for(suffix_ops()));
}
#[test]
fn spec_unary_suffix_arith() {
run_spec("unary-suffix-arith.tsv", parser_for(suffix_ops()));
}
#[test]
fn spec_unary_suffix_structure() {
run_spec("unary-suffix-structure.tsv", parser_for(suffix_ops()));
}
#[test]
fn spec_unary_suffix_prefix() {
run_spec("unary-suffix-prefix.tsv", parser_for(suffix_ops()));
}
#[test]
fn spec_unary_suffix_paren() {
run_spec("unary-suffix-paren.tsv", parser_for(suffix_ops()));
}
#[test]
fn spec_unary_suffix_edge() {
run_spec(
"unary-suffix-edge.tsv",
parser_for(json!({
"op": {
"factorial": { "suffix": true, "left": 6000000, "src": "!" },
"question": { "suffix": true, "left": 3500000, "src": "?" },
"tight": { "infix": true, "left": 7000000, "right": 7100000, "src": "~" },
}
})),
);
}
fn ternary_ops() -> serde_json::Value {
json!({
"op": {
"factorial": { "suffix": true, "src": "!", "left": 6000000 },
"ternary": { "ternary": true, "src": ["?", ":"] },
}
})
}
#[test]
fn spec_ternary_basic() {
run_spec("ternary-basic.tsv", parser_for(ternary_ops()));
}
#[test]
fn spec_ternary_implicit_list() {
run_spec("ternary-implicit-list.tsv", parser_for(ternary_ops()));
}
#[test]
fn spec_ternary_many_2() {
run_spec(
"ternary-many-2.tsv",
parser_for(json!({
"op": {
"foo": { "ternary": true, "src": ["?", ":"] },
"bar": { "ternary": true, "src": ["QQ", "CC"] },
}
})),
);
}
#[test]
fn spec_ternary_many_3() {
run_spec(
"ternary-many-3.tsv",
parser_for(json!({
"op": {
"foo": { "ternary": true, "src": ["?", ":"] },
"bar": { "ternary": true, "src": ["QQ", "CC"] },
"zed": { "ternary": true, "src": ["%%", "@@"] },
}
})),
);
}
#[test]
fn spec_ternary_paren_preval() {
run_spec(
"ternary-paren-preval.tsv",
parser_for(json!({
"op": {
"ternary": { "ternary": true, "src": ["?", ":"] },
"plain": {
"paren": true, "osrc": "(", "csrc": ")",
"preval": { "active": true },
},
}
})),
);
}
#[test]
fn spec_paren_preval_chain() {
run_spec(
"paren-preval-chain.tsv",
parser_for(json!({
"op": {
"index": {
"paren": true, "osrc": "[", "csrc": "]",
"preval": { "required": true },
},
"call": {
"paren": true, "osrc": "(", "csrc": ")",
"preval": { "active": true },
},
"plain": null,
}
})),
);
}
#[test]
fn spec_add_infix() {
run_spec(
"add-infix.tsv",
parser_for(json!({
"op": {
"foo": { "infix": true, "left": 3500000, "right": 3600000, "src": "foo" },
}
})),
);
}
#[test]
fn spec_add_paren() {
run_spec(
"add-paren.tsv",
parser_for(json!({
"op": { "angle": { "paren": true, "osrc": "<", "csrc": ">" } }
})),
);
}
#[test]
fn spec_paren_preval_basic() {
run_spec(
"paren-preval-basic.tsv",
parser_for(json!({
"op": {
"angle": {
"osrc": "<", "csrc": ">", "paren": true,
"preval": { "active": true },
},
}
})),
);
}
#[test]
fn spec_paren_preval_overload() {
run_spec(
"paren-preval-overload.tsv",
parser_for(json!({
"op": {
"factorial": { "suffix": true, "left": 6000000, "src": "!" },
"square": {
"osrc": "[", "csrc": "]", "paren": true,
"preval": { "required": true },
},
"brace": {
"osrc": "{", "csrc": "}", "paren": true,
"preval": { "required": true },
},
}
})),
);
}
#[test]
fn spec_paren_preval_implicit() {
run_spec(
"paren-preval-implicit.tsv",
parser_for(json!({ "op": { "plain": { "preval": true } } })),
);
}
fn number_at(terms: &[EngineValue], index: usize) -> f64 {
match terms.get(index) {
Some(EngineValue::Number(number)) => *number,
_ => 0.0,
}
}
fn factorial(n: f64) -> f64 {
if n <= 1.0 {
return 1.0;
}
let mut out = 1.0;
let mut step = 2.0;
while step <= n {
out *= step;
step += 1.0;
}
out
}
#[test]
fn spec_evaluate_math() {
let options = ExprOptions::new()
.with_op("addition", OpDef::infix("+", 140, 150))
.with_op("subtraction", OpDef::infix("-", 140, 150))
.with_op("multiplication", OpDef::infix("*", 160, 170))
.with_op("division", OpDef::infix("/", 160, 170))
.with_op("negative", OpDef::prefix("-", 200))
.with_op("positive", OpDef::prefix("+", 200))
.with_op("factorial", OpDef::suffix("!", 300))
.with_op(
"func",
OpDef::paren("(", ")").with_preval(PrevalDef {
active: true,
required: false,
allow: None,
}),
)
.with_evaluate(|_site, op, terms| {
let left = number_at(terms, 0);
let right = number_at(terms, 1);
match op.name.as_str() {
"addition-infix" => EngineValue::Number(left + right),
"subtraction-infix" => EngineValue::Number(left - right),
"multiplication-infix" => EngineValue::Number(left * right),
"division-infix" => {
EngineValue::Number(if 0.0 == right { 0.0 } else { left / right })
}
"negative-prefix" => EngineValue::Number(-left),
"positive-prefix" => EngineValue::Number(left),
"factorial-suffix" => EngineValue::Number(factorial(left)),
"func-paren" => {
let EngineValue::String(name) =
terms.first().unwrap_or(&EngineValue::Undefined)
else {
return terms.first().cloned().unwrap_or(EngineValue::Undefined);
};
let rest = &terms[1..];
let args: Vec<EngineValue> = match rest {
[EngineValue::Array(members)] => members.to_vec(),
other => other.to_vec(),
};
match name.as_str() {
"min" | "max" => {
let mut best = number_at(&args, 0);
for index in 1..args.len() {
let candidate = number_at(&args, index);
if ("min" == name) == (candidate < best) && candidate != best {
best = candidate;
}
}
EngineValue::Number(best)
}
_ => EngineValue::Number(number_at(&args, 0)),
}
}
"plain-paren" => EngineValue::Number(left),
_ => EngineValue::Number(left),
}
});
run_spec("evaluate-math.tsv", parser_with(options));
}
fn hold_to_canonical_json(path: &std::path::Path, parser: &Tabnas) {
let spec = load_spec(path, &SpecOptions::default())
.unwrap_or_else(|error| panic!("{}: {}", path.display(), error.0));
let mut failures = Vec::new();
for row in &spec.rows {
let value = tabnas_expr::parse_simplified(parser, &row.unesc(0))
.unwrap_or_else(|error| panic!("{}: {error}", row.location()));
let got = canonical(&value);
if got != row.col(1) {
failures.push(format!(
"{}\n got {got}\n expected {}",
row.location(),
row.col(1)
));
}
}
assert!(
failures.is_empty(),
"{} of {} rows differ from the canonical JSON:\n{}",
failures.len(),
spec.rows.len(),
failures.join("\n")
);
}
fn canonical(value: &EngineValue) -> String {
let mut out = String::new();
write_canonical(&mut out, value);
out
}
fn write_canonical(out: &mut String, value: &EngineValue) {
let string = |text: &str| serde_json::to_string(text).expect("a string is JSON");
match value {
EngineValue::Undefined | EngineValue::Null => out.push_str("null"),
EngineValue::Bool(flag) => out.push_str(if *flag { "true" } else { "false" }),
EngineValue::Number(number) => out.push_str(&js_number(*number)),
EngineValue::String(text) => out.push_str(&string(text)),
EngineValue::Text(text) => out.push_str(&string(&text.string)),
EngineValue::Array(items) => write_items(out, items),
EngineValue::ListRef(list) => write_items(out, &list.value),
EngineValue::Object(members) => write_members(out, members.iter()),
EngineValue::MapRef(map) => write_members(out, map.value.iter()),
}
}
fn write_items(out: &mut String, items: &[EngineValue]) {
out.push('[');
for (at, item) in items.iter().enumerate() {
if 0 < at {
out.push(',');
}
write_canonical(out, item);
}
out.push(']');
}
fn write_members<'a>(
out: &mut String,
members: impl Iterator<Item = (&'a String, &'a EngineValue)>,
) {
out.push('{');
let mut first = true;
for (name, member) in members {
if member.is_undefined() {
continue;
}
if !first {
out.push(',');
}
first = false;
out.push_str(&serde_json::to_string(name).expect("a key is JSON"));
out.push(':');
write_canonical(out, member);
}
out.push('}');
}
fn js_number(number: f64) -> String {
if !number.is_finite() {
return "null".to_string();
}
if 0.0 == number {
return "0".to_string();
}
let magnitude = number.abs();
let (exact, n) = exact_decimal(magnitude);
let reads_back = |digits: &[u8], n: i32| {
let text: String = digits
.iter()
.map(|digit| char::from(b'0' + digit))
.collect();
format!("0.{text}e{n}").parse::<f64>() == Ok(magnitude)
};
let mut body = None;
for k in 1..=exact.len() {
let low = &exact[..k];
let rest = &exact[k..];
if rest.iter().all(|digit| 0 == *digit) {
body = Some(js_layout(low, n));
break;
}
let mut high = low.to_vec();
let mut high_n = n;
match high.iter().rposition(|digit| 9 != *digit) {
Some(at) => {
high[at] += 1;
high[at + 1..].fill(0);
}
None => {
high.fill(0);
high[0] = 1;
high_n += 1;
}
}
let take_high = match (reads_back(low, n), reads_back(&high, high_n)) {
(false, false) => continue,
(true, false) => false,
(false, true) => true,
(true, true) => match rest[0].cmp(&5) {
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Equal => {
rest[1..].iter().any(|digit| 0 != *digit) || 1 == low[k - 1] % 2
}
},
};
body = Some(if take_high {
js_layout(&high, high_n)
} else {
js_layout(low, n)
});
break;
}
let body = body.expect("the exact digits read back");
if number < 0.0 {
format!("-{body}")
} else {
body
}
}
fn exact_decimal(magnitude: f64) -> (Vec<u8>, i32) {
const BASE: u64 = 1_000_000_000;
let bits = magnitude.to_bits();
let fraction = bits & ((1 << 52) - 1);
let biased = ((bits >> 52) & 0x7ff) as i32;
let (mantissa, exponent) = if 0 == biased {
(fraction, -1074)
} else {
(fraction | (1 << 52), biased - 1075)
};
let mut limbs = vec![
mantissa % BASE,
mantissa / BASE % BASE,
mantissa / BASE / BASE,
];
let mut multiply = |factor: u64| {
let mut carry = 0;
for limb in limbs.iter_mut() {
let product = *limb * factor + carry;
*limb = product % BASE;
carry = product / BASE;
}
while 0 < carry {
limbs.push(carry % BASE);
carry /= BASE;
}
};
let (base, step) = if 0 <= exponent {
(2u64, 30)
} else {
(5u64, 13)
};
let mut count = exponent.unsigned_abs();
while 0 < count {
let now = count.min(step);
multiply(base.pow(now));
count -= now;
}
let mut text = String::new();
for limb in limbs.iter().rev() {
text.push_str(&format!("{limb:09}"));
}
let digits: Vec<u8> = text
.trim_start_matches('0')
.bytes()
.map(|byte| byte - b'0')
.collect();
let n = digits.len() as i32 + exponent.min(0);
(digits, n)
}
fn js_layout(digits: &[u8], n: i32) -> String {
let text: String = digits
.iter()
.map(|digit| char::from(b'0' + digit))
.collect();
let digits = text.trim_end_matches('0');
let k = digits.len() as i32;
if k <= n && n <= 21 {
format!("{digits}{}", "0".repeat((n - k) as usize))
} else if 0 < n && n <= 21 {
format!("{}.{}", &digits[..n as usize], &digits[n as usize..])
} else if -6 < n && n <= 0 {
format!("0.{}{digits}", "0".repeat(n.unsigned_abs() as usize))
} else {
let sign = if n < 1 { '-' } else { '+' };
let power = (n - 1).abs();
if 1 == k {
format!("{digits}e{sign}{power}")
} else {
format!("{}.{}e{sign}{power}", &digits[..1], &digits[1..])
}
}
}
#[test]
fn js_number_spells_as_javascript_does() {
for (number, want) in [
(1.0, "1"),
(-0.0, "0"),
(1.5, "1.5"),
(-2.25, "-2.25"),
(100.0, "100"),
(1e20, "100000000000000000000"),
(1e21, "1e+21"),
(1.5e300, "1.5e+300"),
(0.000001, "0.000001"),
(1e-7, "1e-7"),
(-1.25e-7, "-1.25e-7"),
(0.1 + 0.2, "0.30000000000000004"),
(f64::NAN, "null"),
(f64::INFINITY, "null"),
(f64::from_bits(0x4305edd45e85c45a), "771558860699787.2"),
(f64::from_bits(0x4305edd45e85c45e), "771558860699787.8"),
(f64::from_bits(0x4310000000000001), "1125899906842624.2"),
(f64::from_bits(0x3e60000000000000), "2.9802322387695312e-8"),
(f64::from_bits(0x0060000000000000), "7.120236347223045e-307"),
(f64::from_bits(0x0000000000000001), "5e-324"),
(f64::MAX, "1.7976931348623157e+308"),
(-f64::MAX, "-1.7976931348623157e+308"),
] {
assert_eq!(js_number(number), want, "for {number:?}");
}
}
#[test]
fn every_fixture_row_is_held_to_its_canonical_json() {
let rows: usize = std::fs::read_dir(spec_dir())
.expect("the shared fixture directory is readable")
.map(|entry| entry.expect("a readable directory entry").path())
.filter(|path| path.extension().is_some_and(|ext| ext == "tsv"))
.map(|path| {
load_spec(&path, &SpecOptions::default())
.unwrap_or_else(|error| panic!("{}: {}", path.display(), error.0))
.rows
.len()
})
.sum();
assert_eq!(
rows, 1136,
"the shared fixtures hold {rows} rows, not the 1136 measured"
);
}
#[test]
fn every_shared_fixture_is_run() {
let source = std::fs::read_to_string(
common::repo_root()
.join("rs")
.join("tests")
.join("parity_test.rs"),
)
.expect("this test file is readable");
let mut fixtures: Vec<String> = std::fs::read_dir(spec_dir())
.expect("the shared fixture directory is readable")
.map(|entry| entry.expect("a readable directory entry").file_name())
.map(|name| name.to_string_lossy().into_owned())
.filter(|name| name.ends_with(".tsv"))
.collect();
fixtures.sort();
assert!(
!fixtures.is_empty(),
"no fixtures in {}; the coverage gate would pass over nothing",
spec_dir().display()
);
let missing: Vec<&String> = fixtures
.iter()
.filter(|name| !source.contains(&format!("\"{name}\"")))
.collect();
assert!(
missing.is_empty(),
"no Rust runner for {} of {} shared fixtures: {:?}. \
Add a `run_spec` test with the operator table the file assumes, \
matching the TypeScript and Go runners.",
missing.len(),
fixtures.len(),
missing
);
}