#[must_use]
pub fn fixed_decimal(digits: &str, scale: u8) -> String {
let (sign, magnitude) = match digits.strip_prefix('-') {
Some(rest) => ("-", rest),
None => ("", digits),
};
let scale = usize::from(scale);
if scale == 0 {
let whole = magnitude.trim_start_matches('0');
return format!("{sign}{}", if whole.is_empty() { "0" } else { whole });
}
let padded = format!("{magnitude:0>width$}", width = scale + 1);
let split = padded.len() - scale;
format!("{sign}{}.{}", &padded[..split], &padded[split..])
}
#[must_use]
pub fn float_text(mantissa: &str, exponent: Option<&str>) -> String {
let mut out = String::with_capacity(mantissa.len() + 4);
if mantissa.starts_with('.') {
out.push('0');
} else if let Some(rest) = mantissa.strip_prefix('-').filter(|r| r.starts_with('.')) {
out.push_str("-0");
out.push_str(rest);
return with_exponent(out, exponent);
}
out.push_str(mantissa);
if out.ends_with('.') {
out.push('0');
}
if exponent.is_none() && !out.contains('.') {
out.push_str(".0");
}
with_exponent(out, exponent)
}
fn with_exponent(mut text: String, exponent: Option<&str>) -> String {
if let Some(exponent) = exponent {
text.push('e');
text.push_str(exponent);
}
text
}
#[must_use]
pub fn wstring_utf8(points: &[u32]) -> Option<String> {
points.iter().map(|point| char::from_u32(*point)).collect()
}
#[must_use]
pub fn wchar_char(point: u32) -> Option<char> {
char::from_u32(point)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn a_fixed_puts_its_point_back_where_the_scale_says() {
assert_eq!(fixed_decimal("12345", 2), "123.45");
assert_eq!(fixed_decimal("12345", 0), "12345");
assert_eq!(fixed_decimal("5", 2), "0.05");
assert_eq!(fixed_decimal("5", 1), "0.5");
assert_eq!(fixed_decimal("-125", 2), "-1.25");
assert_eq!(fixed_decimal("0", 0), "0");
}
#[test]
fn a_float_keeps_the_source_s_digits_and_gains_only_what_rust_needs() {
assert_eq!(float_text("1.5", None), "1.5");
assert_eq!(float_text("1.5", Some("3")), "1.5e3");
assert_eq!(float_text("1.5", Some("-3")), "1.5e-3");
assert_eq!(float_text(".5", None), "0.5");
assert_eq!(float_text("-.5", None), "-0.5");
assert_eq!(float_text("1", None), "1.0");
assert_eq!(float_text("1.", None), "1.0");
assert_eq!(float_text("1", Some("3")), "1e3");
}
#[test]
fn a_wstring_that_is_not_scalar_values_is_refused_rather_than_mangled() {
assert_eq!(wstring_utf8(&[0x48, 0xE4]).as_deref(), Some("Hä"));
assert_eq!(wstring_utf8(&[0x1_D11E]).as_deref(), Some("\u{1d11e}"));
assert!(wstring_utf8(&[0xD800]).is_none());
assert!(wchar_char(0xDC00).is_none());
assert_eq!(wchar_char(0xE4), Some('ä'));
}
}
#[cfg(test)]
mod four_columns {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use crate::ModelGraph;
use crate::adapter::{c, cpp, cst, java};
use crate::apply::apply;
use crate::rule;
use zerodds_ast::Ast;
const SOURCE: &str = r#"
module spatial {
module sensing {
const string MODULE_ID = "spatial.sensing.lidar/1.5";
// `L"…"` and not `"…"`: IDL 4.2 §7.2.6.3 makes assigning a
// non-wide string literal to a `wstring` constant a
// compile-time diagnostic, and the front end reports it.
const wstring WIDE = L"wide";
const double RATE = 12.5;
const float NARROW = .5;
const fixed PRICE = 1.25d;
const char TAG = 'x';
const boolean ON = TRUE;
const long COUNT = 7;
};
};
"#;
fn model() -> Ast {
let parsed = zerodds_idl_parse::parse(SOURCE);
assert!(!parsed.has_errors(), "{:?}", parsed.diagnostics());
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "constants.idl");
assert!(lowered.diagnostics.is_empty(), "{:?}", lowered.diagnostics);
let mut ast = lowered.ast;
ast.resolve_all_by_name().expect("the model resolves");
ast
}
fn rendered() -> [(&'static str, String); 4] {
let ast = model();
let graph = ModelGraph::of(&ast);
let applied = apply(&graph, &rule::c::catalogue()).expect("the C rules apply");
let c_text = zerodds_lang_render::c::file(&zerodds_lang_cst::c::File {
doc: zerodds_lang_cst::common::Doc::none(),
guard: None,
includes: Vec::new(),
decls: c::c_items(&ast, &applied).expect("the C declarations"),
});
let applied = apply(&graph, &rule::cpp::catalogue()).expect("the C++ rules apply");
let cpp_text = zerodds_lang_render::cpp::file(&zerodds_lang_cst::cpp::File {
doc: zerodds_lang_cst::common::Doc::none(),
pragma_once: false,
includes: Vec::new(),
decls: cpp::cpp_items(&ast, &applied).expect("the C++ declarations"),
});
let applied = apply(&graph, &rule::java::catalogue()).expect("the Java rules apply");
let java_text = zerodds_lang_render::java::file(&zerodds_lang_cst::java::File {
doc: zerodds_lang_cst::common::Doc::none(),
package: None,
imports: Vec::new(),
types: java::java_items(&ast, &applied).expect("the Java declarations"),
});
let applied = apply(&graph, &rule::rust::catalogue()).expect("the Rust rules apply");
let rust_text = cst::rust_items(&ast, &applied)
.expect("the Rust declarations")
.iter()
.map(zerodds_lang_render::rust::item)
.collect::<Vec<_>>()
.join("\n");
[
("C", c_text),
("C++", cpp_text),
("Java", java_text),
("Rust", rust_text),
]
}
#[test]
fn a_string_constant_reaches_every_column() {
for (language, text) in rendered() {
assert!(
text.contains(r#""spatial.sensing.lidar/1.5""#),
"the {language} column dropped MODULE_ID:\n{text}"
);
}
}
#[test]
fn every_non_integral_form_reaches_every_column() {
for (language, text) in rendered() {
assert!(
text.contains(r#""wide""#),
"{language}: no wstring constant"
);
assert!(text.contains("12.5"), "{language}: no double constant");
assert!(text.contains("0.5"), "{language}: no float constant");
assert!(text.contains(r#""1.25""#), "{language}: no fixed constant");
assert!(text.contains("'x'"), "{language}: no char constant");
assert!(text.contains('7'), "{language}: no integer constant");
}
}
#[test]
fn a_thirty_two_bit_float_takes_the_suffix_its_language_needs() {
for (language, text) in rendered() {
let suffixed = text.contains("0.5f");
assert_eq!(
suffixed,
language != "Rust",
"{language}: the float suffix is wrong:\n{text}"
);
}
}
}