difftreelog
ci fix clippy warnings
in: master
3 files changed
Makefilediffbeforeafterboth--- a/Makefile
+++ /dev/null
@@ -1,78 +0,0 @@
-.PHONY: test build build-wasi
-jsonnet-cpp:
- git clone https://github.com/google/jsonnet jsonnet-cpp
-.ONESHELL:
-jsonnet-sjsonnet:
- mkdir jsonnet-sjsonnet && cd jsonnet-sjsonnet
- wget https://github.com/databricks/sjsonnet/releases/download/0.2.4/sjsonnet.jar
- echo "#!/bin/sh" > sjsonnet
- echo "java -Xss400m -jar $(PWD)/jsonnet-sjsonnet/sjsonnet.jar $@" >> sjsonnet
- chmod +x sjsonnet
-
-.ONESHELL:
-test-examples: jsonnet-cpp
- export JSONNET_BIN="$(PWD)/target/release/jrsonnet"
- export EXAMPLES_DIR="$(PWD)/jsonnet-cpp/examples/"
- cd ./jsonnet-cpp/examples/
- ./check.sh
-
-.ONESHELL:
-test-tests: jsonnet-cpp
- export JSONNET_BIN="$(PWD)/target/release/jrsonnet --trace-format=go --thread-stack-size=96"
- cd ./jsonnet-cpp/test_suite/
- ./run_tests.sh
-
-test:
- cargo test
-build:
- RUSTFLAGS="-Zmutable-noalias=yes -C link-arg=-s" cargo build --release -p jrsonnet
-build-wasi:
- cd ./bindings/ && cargo build --release -p jsonnet --target wasm32-wasi
-
-bench = hyperfine --export-markdown "result.$(1).md" "jrsonnet $(1)" "gojsonnet $(1)" "jsonnet $(1)" "sjsonnet $(1)"
-bench-larger-stack = hyperfine --export-markdown "result.$(1).md" "jrsonnet $(1)" "gojsonnet -s 200000 $(1)" "jsonnet -s 200000 $(1)" "sjsonnet $(1)"
-bench-no-scala = hyperfine --export-markdown "result.$(1).md" "jrsonnet $(1)" "gojsonnet $(1)" "jsonnet $(1)"
-bench-no-go = hyperfine --export-markdown "result.$(1).md" "jrsonnet $(1)" "jsonnet $(1)" "sjsonnet $(1)"
-
-.PHONY: benchmarks
-.ONESHELL:
-benchmarks: jsonnet-cpp jsonnet-sjsonnet
- export PATH=$(PWD)/target/release/:$(PWD)/jsonnet-sjsonnet/:$(PATH)
-
- mkdir -p $(PWD)/benchmarks
-
- cd jsonnet-cpp/benchmarks/
-
- jrsonnet -S gen_big_object.jsonnet > bench.05.gen.jsonnet
-
- $(call bench,bench.01.jsonnet)
- $(call bench,bench.02.jsonnet)
- $(call bench,bench.03.jsonnet)
- $(call bench,bench.04.jsonnet)
- $(call bench,bench.05.gen.jsonnet)
- # std.reverse not implemented in sjsonnet
- $(call bench-no-scala,bench.06.jsonnet)
- $(call bench-larger-stack,bench.07.jsonnet)
- $(call bench,bench.08.jsonnet)
-
- rm -f result.md
- find . | /usr/bin/grep -oE "[a-z_0-9.]+.jsonnet$$" | grep -v gen_big_object | xargs -n1 -i sh -c 'printf "## {}\n\n" >> result.md && cat result.{}.md >> result.md && printf "\n" >> result.md'
- cp result.md $(PWD)/benchmarks/benchmarks.md
-
- cd ../perf_tests/
-
- $(call bench,large_string_join.jsonnet)
- # golang overflows os stack on this benchmark
- $(call bench-no-go,large_string_template.jsonnet)
- $(call bench,realistic1.jsonnet)
- $(call bench,realistic2.jsonnet)
-
- rm -f result.md
- find . | /usr/bin/grep -oE "[a-z_0-9.]+.jsonnet$$" | xargs -n1 -i sh -c 'printf "## {}\n\n" >> result.md && cat result.{}.md >> result.md && printf "\n" >> result.md'
- cp result.md $(PWD)/benchmarks/perf_tests.md
-
- cd $(PWD)/benchmarks/
-
- rm -f result.md
- printf "# Benchmark results\n\n" > result.md
- cat benchmarks.md perf_tests.md >> result.md
README.mddiffbeforeafterboth--- a/README.md
+++ b/README.md
@@ -32,8 +32,6 @@
Official benchmark report are available [in this gist](https://gist.github.com/CertainLach/5770d7ad4836066f8e0bd91e823e451b), and updated sometimes. Here it tested against golang, C++, and scala impl. As you can see, it is a lot faster
-You can generate this report by calling `make benchmarks`, but it probally won't work in standard setup, you need to link golang jsonnet impl to gojsonnet, and c++ impl to jsonnet.
-
TODO: Create docker container for easier benchmarking and/or benchmark in CI
Also, there is some ideas to improve performance even further, by i.e:
crates/jrsonnet-evaluator/src/builtin/format.rsdiffbeforeafterboth1//! faster std.format impl2#![allow(clippy::too_many_arguments)]34use crate::{error::Error::*, throw, LocError, ObjValue, Result, Val, ValType};56#[derive(Debug, Clone)]7pub enum FormatError {8 TruncatedFormatCode,9 UnrecognizedConversionType(char),1011 NotEnoughValues,1213 CannotUseStarWidthWithObject,14 MappingKeysRequired,15 NoSuchFormatField(Rc<str>),16}1718impl From<FormatError> for LocError {19 fn from(e: FormatError) -> Self {20 Self::new(Format(e))21 }22}2324use std::rc::Rc;25use FormatError::*;2627type ParseResult<'t, T> = std::result::Result<(T, &'t str), FormatError>;2829pub fn try_parse_mapping_key(str: &str) -> ParseResult<&str> {30 if str.is_empty() {31 return Err(TruncatedFormatCode);32 }33 let bytes = str.as_bytes();34 if bytes[0] == b'(' {35 let mut i = 1;36 while i < bytes.len() {37 if bytes[i] == b')' {38 return Ok((&str[1..i as usize], &str[i as usize + 1..]));39 }40 i += 1;41 }42 Err(TruncatedFormatCode)43 } else {44 Ok(("", str))45 }46}4748#[cfg(test)]49pub mod tests_key {50 use super::*;5152 #[test]53 fn parse_key() {54 assert_eq!(55 try_parse_mapping_key("(hello ) world").unwrap(),56 ("hello ", " world")57 );58 assert_eq!(try_parse_mapping_key("() world").unwrap(), ("", " world"));59 assert_eq!(try_parse_mapping_key(" world").unwrap(), ("", " world"));60 assert_eq!(61 try_parse_mapping_key(" () world").unwrap(),62 ("", " () world")63 );64 }6566 #[test]67 #[should_panic]68 fn parse_key_missing_start() {69 try_parse_mapping_key("").unwrap();70 }7172 #[test]73 #[should_panic]74 fn parse_key_missing_end() {75 try_parse_mapping_key("( ").unwrap();76 }77}7879#[derive(Default, Debug)]80pub struct CFlags {81 pub alt: bool,82 pub zero: bool,83 pub left: bool,84 pub blank: bool,85 pub sign: bool,86}8788pub fn try_parse_cflags(str: &str) -> ParseResult<CFlags> {89 if str.is_empty() {90 return Err(TruncatedFormatCode);91 }92 let bytes = str.as_bytes();93 let mut i = 0;94 let mut out = CFlags::default();95 loop {96 if bytes.len() == i {97 return Err(TruncatedFormatCode);98 }99 match bytes[i] {100 b'#' => out.alt = true,101 b'0' => out.zero = true,102 b'-' => out.left = true,103 b' ' => out.blank = true,104 b'+' => out.sign = true,105 _ => break,106 }107 i += 1;108 }109 Ok((out, &str[i..]))110}111112#[derive(Debug, PartialEq)]113pub enum Width {114 Star,115 Fixed(usize),116}117pub fn try_parse_field_width(str: &str) -> ParseResult<Width> {118 if str.is_empty() {119 return Err(TruncatedFormatCode);120 }121 let bytes = str.as_bytes();122 if bytes[0] == b'*' {123 return Ok((Width::Star, &str[1..]));124 }125 let mut out: usize = 0;126 let mut digits = 0;127 while let Some(digit) = (bytes[digits] as char).to_digit(10) {128 out *= 10;129 out += digit as usize;130 digits += 1;131 if digits == bytes.len() {132 return Err(TruncatedFormatCode);133 }134 }135 Ok((Width::Fixed(out), &str[digits..]))136}137138pub fn try_parse_precision(str: &str) -> ParseResult<Option<Width>> {139 if str.is_empty() {140 return Err(TruncatedFormatCode);141 }142 let bytes = str.as_bytes();143 if bytes[0] == b'.' {144 try_parse_field_width(&str[1..]).map(|(r, s)| (Some(r), s))145 } else {146 Ok((None, str))147 }148}149150// Only skips151pub fn try_parse_length_modifier(str: &str) -> ParseResult<()> {152 if str.is_empty() {153 return Err(TruncatedFormatCode);154 }155 let bytes = str.as_bytes();156 let mut idx = 0;157 while bytes[idx] == b'h' || bytes[idx] == b'l' || bytes[idx] == b'L' {158 idx += 1;159 if bytes.len() == idx {160 return Err(TruncatedFormatCode);161 }162 }163 Ok(((), &str[idx..]))164}165166#[derive(Debug, PartialEq)]167pub enum ConvTypeV {168 Decimal,169 Octal,170 Hexadecimal,171 Scientific,172 Float,173 Shorter,174 Char,175 String,176 Percent,177}178pub struct ConvType {179 v: ConvTypeV,180 caps: bool,181}182183pub fn parse_conversion_type(str: &str) -> ParseResult<ConvType> {184 if str.is_empty() {185 return Err(TruncatedFormatCode);186 }187188 let code = str.as_bytes()[0];189 let v: (ConvTypeV, bool) = match code {190 b'd' | b'i' | b'u' => (ConvTypeV::Decimal, false),191 b'o' => (ConvTypeV::Octal, false),192 b'x' => (ConvTypeV::Hexadecimal, false),193 b'X' => (ConvTypeV::Hexadecimal, true),194 b'e' => (ConvTypeV::Scientific, false),195 b'E' => (ConvTypeV::Scientific, true),196 b'f' => (ConvTypeV::Float, false),197 b'F' => (ConvTypeV::Float, true),198 b'g' => (ConvTypeV::Shorter, false),199 b'G' => (ConvTypeV::Shorter, true),200 b'c' => (ConvTypeV::Char, false),201 b's' => (ConvTypeV::String, false),202 b'%' => (ConvTypeV::Percent, false),203 c => return Err(UnrecognizedConversionType(c as char)),204 };205206 Ok((ConvType { v: v.0, caps: v.1 }, &str[1..]))207}208209#[derive(Debug)]210pub struct Code<'s> {211 mkey: &'s str,212 cflags: CFlags,213 width: Width,214 precision: Option<Width>,215 convtype: ConvTypeV,216 caps: bool,217}218pub fn parse_code(str: &str) -> ParseResult<Code> {219 if str.is_empty() {220 return Err(TruncatedFormatCode);221 }222 let (mkey, str) = try_parse_mapping_key(str)?;223 let (cflags, str) = try_parse_cflags(str)?;224 let (width, str) = try_parse_field_width(str)?;225 let (precision, str) = try_parse_precision(str)?;226 let (_, str) = try_parse_length_modifier(str)?;227 let (convtype, str) = parse_conversion_type(str)?;228229 Ok((230 Code {231 mkey,232 cflags,233 width,234 precision,235 convtype: convtype.v,236 caps: convtype.caps,237 },238 str,239 ))240}241242#[derive(Debug)]243pub enum Element<'s> {244 String(&'s str),245 Code(Code<'s>),246}247pub fn parse_codes(mut str: &str) -> Result<Vec<Element>> {248 let mut bytes = str.as_bytes();249 let mut out = vec![];250 let mut offset = 0;251252 loop {253 while offset != bytes.len() && bytes[offset] != b'%' {254 offset += 1;255 }256 if offset != 0 {257 out.push(Element::String(&str[0..offset]));258 }259 if offset == bytes.len() {260 return Ok(out);261 }262 str = &str[offset + 1..];263 let (code, nstr) = parse_code(str)?;264 str = nstr;265 bytes = str.as_bytes();266 offset = 0;267268 out.push(Element::Code(code))269 }270}271272const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";273274#[inline]275pub fn render_integer(276 out: &mut String,277 iv: i64,278 padding: usize,279 precision: usize,280 blank: bool,281 sign: bool,282 radix: i64,283 prefix: &str,284 caps: bool,285) {286 // Digit char indexes in reverse order, i.e287 // for radix = 16 and n = 12f: [15, 2, 1]288 let digits = if iv == 0 {289 vec![0u8]290 } else {291 let mut v = iv.abs();292 let mut nums = Vec::with_capacity(1);293 while v > 0 {294 nums.push((v % radix) as u8);295 v /= radix;296 }297 nums298 };299 let neg = iv < 0;300 let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 });301 let zp2 = zp302 .max(precision)303 .saturating_sub(prefix.len() + digits.len());304305 if neg {306 out.push('-')307 } else if sign {308 out.push('+');309 } else if blank {310 out.push(' ');311 }312313 out.reserve(zp2);314 for _ in 0..zp2 {315 out.push('0');316 }317 out.push_str(&prefix);318319 for digit in digits.into_iter().rev() {320 let ch = NUMBERS[digit as usize] as char;321 out.push(if caps { ch.to_ascii_uppercase() } else { ch });322 }323}324325pub fn render_decimal(326 out: &mut String,327 iv: i64,328 padding: usize,329 precision: usize,330 blank: bool,331 sign: bool,332) {333 render_integer(out, iv, padding, precision, blank, sign, 10, "", false)334}335pub fn render_octal(336 out: &mut String,337 iv: i64,338 padding: usize,339 precision: usize,340 alt: bool,341 blank: bool,342 sign: bool,343) {344 render_integer(345 out,346 iv,347 padding,348 precision,349 blank,350 sign,351 8,352 if alt && iv != 0 { "0" } else { "" },353 false,354 )355}356pub fn render_hexadecimal(357 out: &mut String,358 iv: i64,359 padding: usize,360 precision: usize,361 alt: bool,362 blank: bool,363 sign: bool,364 caps: bool,365) {366 render_integer(367 out,368 iv,369 padding,370 precision,371 blank,372 sign,373 16,374 match (alt, caps) {375 (true, true) => "0X",376 (true, false) => "0x",377 (false, _) => "",378 },379 caps,380 )381}382383pub fn render_float(384 out: &mut String,385 n: f64,386 mut padding: usize,387 precision: usize,388 blank: bool,389 sign: bool,390 ensure_pt: bool,391 trailing: bool,392) {393 let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 };394 padding = padding.saturating_sub(dot_size + precision);395 render_decimal(out, n.floor() as i64, padding, 0, blank, sign);396 if precision == 0 {397 if ensure_pt {398 out.push('.');399 }400 return;401 }402 let frac = (n.fract() * 10.0_f64.powf(precision as f64) + 0.5).floor();403 if trailing || frac > 0.0 {404 out.push('.');405 let mut frac_str = String::new();406 render_decimal(&mut frac_str, frac as i64, precision, 0, false, false);407 let mut trim = frac_str.len();408 if !trailing {409 for b in frac_str.as_bytes().iter().rev() {410 if *b == b'0' {411 trim -= 1;412 }413 }414 }415 out.push_str(&frac_str[..trim]);416 } else if ensure_pt {417 out.push('.');418 }419}420421pub fn render_float_sci(422 out: &mut String,423 n: f64,424 mut padding: usize,425 precision: usize,426 blank: bool,427 sign: bool,428 ensure_pt: bool,429 trailing: bool,430 caps: bool,431) {432 let exponent = n.log10().floor();433 let mantissa = if exponent as i16 == -324 {434 n * 10.0 / 10.0_f64.powf(exponent + 1.0)435 } else {436 n / 10.0_f64.powf(exponent)437 };438 let mut exponent_str = String::new();439 render_decimal(&mut exponent_str, exponent as i64, 3, 0, false, true);440441 // +1 for e442 padding = padding.saturating_sub(exponent_str.len() + 1);443444 render_float(445 out, mantissa, padding, precision, blank, sign, ensure_pt, trailing,446 );447 out.push(if caps { 'E' } else { 'e' });448 out.push_str(&exponent_str);449}450451pub fn format_code(452 out: &mut String,453 value: &Val,454 code: &Code,455 width: usize,456 precision: Option<usize>,457) -> Result<()> {458 let clfags = &code.cflags;459 let (fpprec, iprec) = match precision {460 Some(v) => (v, v),461 None => (6, 0),462 };463 let padding = if clfags.zero && !clfags.left {464 width465 } else {466 0467 };468469 // TODO: If left padded, can optimize by writing directly to out470 let mut tmp_out = String::new();471472 match code.convtype {473 ConvTypeV::String => tmp_out.push_str(&value.clone().to_string()?),474 ConvTypeV::Decimal => {475 let value = value.clone().try_cast_num("%d/%u/%i requires number")?;476 render_decimal(477 &mut tmp_out,478 value as i64,479 padding,480 iprec,481 clfags.blank,482 clfags.sign,483 );484 }485 ConvTypeV::Octal => {486 let value = value.clone().try_cast_num("%o requires number")?;487 render_octal(488 &mut tmp_out,489 value as i64,490 padding,491 iprec,492 clfags.alt,493 clfags.blank,494 clfags.sign,495 );496 }497 ConvTypeV::Hexadecimal => {498 let value = value.clone().try_cast_num("%x/%X requires number")?;499 render_hexadecimal(500 &mut tmp_out,501 value as i64,502 padding,503 iprec,504 clfags.alt,505 clfags.blank,506 clfags.sign,507 code.caps,508 );509 }510 ConvTypeV::Scientific => {511 let value = value.clone().try_cast_num("%e/%E requires number")?;512 render_float_sci(513 &mut tmp_out,514 value,515 padding,516 fpprec,517 clfags.blank,518 clfags.sign,519 clfags.alt,520 true,521 code.caps,522 );523 }524 ConvTypeV::Float => {525 let value = value.clone().try_cast_num("%e/%E requires number")?;526 render_float(527 &mut tmp_out,528 value,529 padding,530 fpprec,531 clfags.blank,532 clfags.sign,533 clfags.alt,534 true,535 );536 }537 ConvTypeV::Shorter => {538 let value = value.clone().try_cast_num("%g/%G requires number")?;539 let exponent = value.log10().floor();540 if exponent < -4.0 || exponent >= fpprec as f64 {541 render_float_sci(542 &mut tmp_out,543 value,544 padding,545 fpprec - 1,546 clfags.blank,547 clfags.sign,548 clfags.alt,549 clfags.alt,550 code.caps,551 );552 } else {553 let digits_before_pt = 1.max(exponent as usize + 1);554 render_float(555 &mut tmp_out,556 value,557 padding,558 fpprec - digits_before_pt,559 clfags.blank,560 clfags.sign,561 clfags.alt,562 clfags.alt,563 );564 }565 }566 ConvTypeV::Char => match value.clone().unwrap_if_lazy()? {567 Val::Num(n) => tmp_out.push(568 std::char::from_u32(n as u32)569 .ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?,570 ),571 Val::Str(s) => {572 if s.chars().count() != 1 {573 throw!(RuntimeError(574 format!("%c expected 1 char string, got {}", s.chars().count()).into(),575 ));576 }577 tmp_out.push_str(&s);578 }579 _ => {580 throw!(TypeMismatch(581 "%c requires number/string",582 vec![ValType::Num, ValType::Str],583 value.value_type()?,584 ));585 }586 },587 ConvTypeV::Percent => tmp_out.push('%'),588 };589590 let padding = width.saturating_sub(tmp_out.len());591592 if !clfags.left {593 for _ in 0..padding {594 out.push(' ');595 }596 }597 out.push_str(&tmp_out);598 if clfags.left {599 for _ in 0..padding {600 out.push(' ');601 }602 }603604 Ok(())605}606607pub fn format_arr(str: &str, mut values: &[Val]) -> Result<String> {608 let codes = parse_codes(&str)?;609 let mut out = String::new();610611 for code in codes {612 match code {613 Element::String(s) => {614 out.push_str(s);615 }616 Element::Code(c) => {617 let width = match c.width {618 Width::Star => {619 if values.is_empty() {620 throw!(NotEnoughValues);621 }622 let value = &values[0];623 values = &values[1..];624 value.clone().try_cast_num("field width")? as usize625 }626 Width::Fixed(n) => n,627 };628 let precision = match c.precision {629 Some(Width::Star) => {630 if values.is_empty() {631 throw!(NotEnoughValues);632 }633 let value = &values[0];634 values = &values[1..];635 Some(value.clone().try_cast_num("field precision")? as usize)636 }637 Some(Width::Fixed(n)) => Some(n),638 None => None,639 };640641 // %% should not consume a value642 let value = if c.convtype == ConvTypeV::Percent {643 &Val::Null644 } else {645 if values.is_empty() {646 throw!(NotEnoughValues);647 }648 let value = &values[0];649 values = &values[1..];650 value651 };652653 format_code(&mut out, value, &c, width, precision)?;654 }655 }656 }657658 Ok(out)659}660661pub fn format_obj(str: &str, values: &ObjValue) -> Result<String> {662 let codes = parse_codes(&str)?;663 let mut out = String::new();664665 for code in codes {666 match code {667 Element::String(s) => {668 out.push_str(s);669 }670 Element::Code(c) => {671 // TODO: Operate on ref672 let f: Rc<str> = c.mkey.into();673 let width = match c.width {674 Width::Star => {675 throw!(CannotUseStarWidthWithObject);676 }677 Width::Fixed(n) => n,678 };679 let precision = match c.precision {680 Some(Width::Star) => {681 throw!(CannotUseStarWidthWithObject);682 }683 Some(Width::Fixed(n)) => Some(n),684 None => None,685 };686687 let value = if c.convtype == ConvTypeV::Percent {688 Val::Null689 } else {690 if f.is_empty() {691 throw!(MappingKeysRequired);692 }693 let value = if let Some(v) = values.get(f.clone())? {694 v695 } else {696 throw!(NoSuchFormatField(f));697 };698 value699 };700701 format_code(&mut out, &value, &c, width, precision)?;702 }703 }704 }705706 Ok(out)707}708709#[cfg(test)]710pub mod test_format {711 use super::*;712713 #[test]714 fn parse() {715 assert_eq!(716 parse_codes(717 "How much error budget is left looking at our %.3f%% availability gurantees?"718 )719 .unwrap()720 .len(),721 4722 );723 }724725 #[test]726 fn octals() {727 assert_eq!(format_arr("%#o", &[Val::Num(8.0)]).unwrap(), "010");728 assert_eq!(format_arr("%#4o", &[Val::Num(8.0)]).unwrap(), " 010");729 assert_eq!(format_arr("%4o", &[Val::Num(8.0)]).unwrap(), " 10");730 assert_eq!(format_arr("%04o", &[Val::Num(8.0)]).unwrap(), "0010");731 assert_eq!(format_arr("%+4o", &[Val::Num(8.0)]).unwrap(), " +10");732 assert_eq!(format_arr("%+04o", &[Val::Num(8.0)]).unwrap(), "+010");733 assert_eq!(format_arr("%-4o", &[Val::Num(8.0)]).unwrap(), "10 ");734 assert_eq!(format_arr("%+-4o", &[Val::Num(8.0)]).unwrap(), "+10 ");735 assert_eq!(format_arr("%+-04o", &[Val::Num(8.0)]).unwrap(), "+10 ");736 }737738 #[test]739 fn percent_doesnt_consumes_values() {740 assert_eq!(741 format_arr(742 "How much error budget is left looking at our %.3f%% availability gurantees?",743 &[Val::Num(4.0)]744 )745 .unwrap(),746 "How much error budget is left looking at our 4.000% availability gurantees?"747 );748 }749}1//! faster std.format impl2#![allow(clippy::too_many_arguments)]34use crate::{error::Error::*, throw, LocError, ObjValue, Result, Val, ValType};56#[derive(Debug, Clone)]7pub enum FormatError {8 TruncatedFormatCode,9 UnrecognizedConversionType(char),1011 NotEnoughValues,1213 CannotUseStarWidthWithObject,14 MappingKeysRequired,15 NoSuchFormatField(Rc<str>),16}1718impl From<FormatError> for LocError {19 fn from(e: FormatError) -> Self {20 Self::new(Format(e))21 }22}2324use std::rc::Rc;25use FormatError::*;2627type ParseResult<'t, T> = std::result::Result<(T, &'t str), FormatError>;2829pub fn try_parse_mapping_key(str: &str) -> ParseResult<&str> {30 if str.is_empty() {31 return Err(TruncatedFormatCode);32 }33 let bytes = str.as_bytes();34 if bytes[0] == b'(' {35 let mut i = 1;36 while i < bytes.len() {37 if bytes[i] == b')' {38 return Ok((&str[1..i as usize], &str[i as usize + 1..]));39 }40 i += 1;41 }42 Err(TruncatedFormatCode)43 } else {44 Ok(("", str))45 }46}4748#[cfg(test)]49pub mod tests_key {50 use super::*;5152 #[test]53 fn parse_key() {54 assert_eq!(55 try_parse_mapping_key("(hello ) world").unwrap(),56 ("hello ", " world")57 );58 assert_eq!(try_parse_mapping_key("() world").unwrap(), ("", " world"));59 assert_eq!(try_parse_mapping_key(" world").unwrap(), ("", " world"));60 assert_eq!(61 try_parse_mapping_key(" () world").unwrap(),62 ("", " () world")63 );64 }6566 #[test]67 #[should_panic]68 fn parse_key_missing_start() {69 try_parse_mapping_key("").unwrap();70 }7172 #[test]73 #[should_panic]74 fn parse_key_missing_end() {75 try_parse_mapping_key("( ").unwrap();76 }77}7879#[derive(Default, Debug)]80pub struct CFlags {81 pub alt: bool,82 pub zero: bool,83 pub left: bool,84 pub blank: bool,85 pub sign: bool,86}8788pub fn try_parse_cflags(str: &str) -> ParseResult<CFlags> {89 if str.is_empty() {90 return Err(TruncatedFormatCode);91 }92 let bytes = str.as_bytes();93 let mut i = 0;94 let mut out = CFlags::default();95 loop {96 if bytes.len() == i {97 return Err(TruncatedFormatCode);98 }99 match bytes[i] {100 b'#' => out.alt = true,101 b'0' => out.zero = true,102 b'-' => out.left = true,103 b' ' => out.blank = true,104 b'+' => out.sign = true,105 _ => break,106 }107 i += 1;108 }109 Ok((out, &str[i..]))110}111112#[derive(Debug, PartialEq)]113pub enum Width {114 Star,115 Fixed(usize),116}117pub fn try_parse_field_width(str: &str) -> ParseResult<Width> {118 if str.is_empty() {119 return Err(TruncatedFormatCode);120 }121 let bytes = str.as_bytes();122 if bytes[0] == b'*' {123 return Ok((Width::Star, &str[1..]));124 }125 let mut out: usize = 0;126 let mut digits = 0;127 while let Some(digit) = (bytes[digits] as char).to_digit(10) {128 out *= 10;129 out += digit as usize;130 digits += 1;131 if digits == bytes.len() {132 return Err(TruncatedFormatCode);133 }134 }135 Ok((Width::Fixed(out), &str[digits..]))136}137138pub fn try_parse_precision(str: &str) -> ParseResult<Option<Width>> {139 if str.is_empty() {140 return Err(TruncatedFormatCode);141 }142 let bytes = str.as_bytes();143 if bytes[0] == b'.' {144 try_parse_field_width(&str[1..]).map(|(r, s)| (Some(r), s))145 } else {146 Ok((None, str))147 }148}149150// Only skips151pub fn try_parse_length_modifier(str: &str) -> ParseResult<()> {152 if str.is_empty() {153 return Err(TruncatedFormatCode);154 }155 let bytes = str.as_bytes();156 let mut idx = 0;157 while bytes[idx] == b'h' || bytes[idx] == b'l' || bytes[idx] == b'L' {158 idx += 1;159 if bytes.len() == idx {160 return Err(TruncatedFormatCode);161 }162 }163 Ok(((), &str[idx..]))164}165166#[derive(Debug, PartialEq)]167pub enum ConvTypeV {168 Decimal,169 Octal,170 Hexadecimal,171 Scientific,172 Float,173 Shorter,174 Char,175 String,176 Percent,177}178pub struct ConvType {179 v: ConvTypeV,180 caps: bool,181}182183pub fn parse_conversion_type(str: &str) -> ParseResult<ConvType> {184 if str.is_empty() {185 return Err(TruncatedFormatCode);186 }187188 let code = str.as_bytes()[0];189 let v: (ConvTypeV, bool) = match code {190 b'd' | b'i' | b'u' => (ConvTypeV::Decimal, false),191 b'o' => (ConvTypeV::Octal, false),192 b'x' => (ConvTypeV::Hexadecimal, false),193 b'X' => (ConvTypeV::Hexadecimal, true),194 b'e' => (ConvTypeV::Scientific, false),195 b'E' => (ConvTypeV::Scientific, true),196 b'f' => (ConvTypeV::Float, false),197 b'F' => (ConvTypeV::Float, true),198 b'g' => (ConvTypeV::Shorter, false),199 b'G' => (ConvTypeV::Shorter, true),200 b'c' => (ConvTypeV::Char, false),201 b's' => (ConvTypeV::String, false),202 b'%' => (ConvTypeV::Percent, false),203 c => return Err(UnrecognizedConversionType(c as char)),204 };205206 Ok((ConvType { v: v.0, caps: v.1 }, &str[1..]))207}208209#[derive(Debug)]210pub struct Code<'s> {211 mkey: &'s str,212 cflags: CFlags,213 width: Width,214 precision: Option<Width>,215 convtype: ConvTypeV,216 caps: bool,217}218pub fn parse_code(str: &str) -> ParseResult<Code> {219 if str.is_empty() {220 return Err(TruncatedFormatCode);221 }222 let (mkey, str) = try_parse_mapping_key(str)?;223 let (cflags, str) = try_parse_cflags(str)?;224 let (width, str) = try_parse_field_width(str)?;225 let (precision, str) = try_parse_precision(str)?;226 let (_, str) = try_parse_length_modifier(str)?;227 let (convtype, str) = parse_conversion_type(str)?;228229 Ok((230 Code {231 mkey,232 cflags,233 width,234 precision,235 convtype: convtype.v,236 caps: convtype.caps,237 },238 str,239 ))240}241242#[derive(Debug)]243pub enum Element<'s> {244 String(&'s str),245 Code(Code<'s>),246}247pub fn parse_codes(mut str: &str) -> Result<Vec<Element>> {248 let mut bytes = str.as_bytes();249 let mut out = vec![];250 let mut offset = 0;251252 loop {253 while offset != bytes.len() && bytes[offset] != b'%' {254 offset += 1;255 }256 if offset != 0 {257 out.push(Element::String(&str[0..offset]));258 }259 if offset == bytes.len() {260 return Ok(out);261 }262 str = &str[offset + 1..];263 let (code, nstr) = parse_code(str)?;264 str = nstr;265 bytes = str.as_bytes();266 offset = 0;267268 out.push(Element::Code(code))269 }270}271272const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";273274#[inline]275pub fn render_integer(276 out: &mut String,277 iv: i64,278 padding: usize,279 precision: usize,280 blank: bool,281 sign: bool,282 radix: i64,283 prefix: &str,284 caps: bool,285) {286 // Digit char indexes in reverse order, i.e287 // for radix = 16 and n = 12f: [15, 2, 1]288 let digits = if iv == 0 {289 vec![0u8]290 } else {291 let mut v = iv.abs();292 let mut nums = Vec::with_capacity(1);293 while v > 0 {294 nums.push((v % radix) as u8);295 v /= radix;296 }297 nums298 };299 let neg = iv < 0;300 let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 });301 let zp2 = zp302 .max(precision)303 .saturating_sub(prefix.len() + digits.len());304305 if neg {306 out.push('-')307 } else if sign {308 out.push('+');309 } else if blank {310 out.push(' ');311 }312313 out.reserve(zp2);314 for _ in 0..zp2 {315 out.push('0');316 }317 out.push_str(&prefix);318319 for digit in digits.into_iter().rev() {320 let ch = NUMBERS[digit as usize] as char;321 out.push(if caps { ch.to_ascii_uppercase() } else { ch });322 }323}324325pub fn render_decimal(326 out: &mut String,327 iv: i64,328 padding: usize,329 precision: usize,330 blank: bool,331 sign: bool,332) {333 render_integer(out, iv, padding, precision, blank, sign, 10, "", false)334}335pub fn render_octal(336 out: &mut String,337 iv: i64,338 padding: usize,339 precision: usize,340 alt: bool,341 blank: bool,342 sign: bool,343) {344 render_integer(345 out,346 iv,347 padding,348 precision,349 blank,350 sign,351 8,352 if alt && iv != 0 { "0" } else { "" },353 false,354 )355}356pub fn render_hexadecimal(357 out: &mut String,358 iv: i64,359 padding: usize,360 precision: usize,361 alt: bool,362 blank: bool,363 sign: bool,364 caps: bool,365) {366 render_integer(367 out,368 iv,369 padding,370 precision,371 blank,372 sign,373 16,374 match (alt, caps) {375 (true, true) => "0X",376 (true, false) => "0x",377 (false, _) => "",378 },379 caps,380 )381}382383pub fn render_float(384 out: &mut String,385 n: f64,386 mut padding: usize,387 precision: usize,388 blank: bool,389 sign: bool,390 ensure_pt: bool,391 trailing: bool,392) {393 let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 };394 padding = padding.saturating_sub(dot_size + precision);395 render_decimal(out, n.floor() as i64, padding, 0, blank, sign);396 if precision == 0 {397 if ensure_pt {398 out.push('.');399 }400 return;401 }402 let frac = (n.fract() * 10.0_f64.powf(precision as f64) + 0.5).floor();403 if trailing || frac > 0.0 {404 out.push('.');405 let mut frac_str = String::new();406 render_decimal(&mut frac_str, frac as i64, precision, 0, false, false);407 let mut trim = frac_str.len();408 if !trailing {409 for b in frac_str.as_bytes().iter().rev() {410 if *b == b'0' {411 trim -= 1;412 }413 }414 }415 out.push_str(&frac_str[..trim]);416 } else if ensure_pt {417 out.push('.');418 }419}420421pub fn render_float_sci(422 out: &mut String,423 n: f64,424 mut padding: usize,425 precision: usize,426 blank: bool,427 sign: bool,428 ensure_pt: bool,429 trailing: bool,430 caps: bool,431) {432 let exponent = n.log10().floor();433 let mantissa = if exponent as i16 == -324 {434 n * 10.0 / 10.0_f64.powf(exponent + 1.0)435 } else {436 n / 10.0_f64.powf(exponent)437 };438 let mut exponent_str = String::new();439 render_decimal(&mut exponent_str, exponent as i64, 3, 0, false, true);440441 // +1 for e442 padding = padding.saturating_sub(exponent_str.len() + 1);443444 render_float(445 out, mantissa, padding, precision, blank, sign, ensure_pt, trailing,446 );447 out.push(if caps { 'E' } else { 'e' });448 out.push_str(&exponent_str);449}450451pub fn format_code(452 out: &mut String,453 value: &Val,454 code: &Code,455 width: usize,456 precision: Option<usize>,457) -> Result<()> {458 let clfags = &code.cflags;459 let (fpprec, iprec) = match precision {460 Some(v) => (v, v),461 None => (6, 0),462 };463 let padding = if clfags.zero && !clfags.left {464 width465 } else {466 0467 };468469 // TODO: If left padded, can optimize by writing directly to out470 let mut tmp_out = String::new();471472 match code.convtype {473 ConvTypeV::String => tmp_out.push_str(&value.clone().to_string()?),474 ConvTypeV::Decimal => {475 let value = value.clone().try_cast_num("%d/%u/%i requires number")?;476 render_decimal(477 &mut tmp_out,478 value as i64,479 padding,480 iprec,481 clfags.blank,482 clfags.sign,483 );484 }485 ConvTypeV::Octal => {486 let value = value.clone().try_cast_num("%o requires number")?;487 render_octal(488 &mut tmp_out,489 value as i64,490 padding,491 iprec,492 clfags.alt,493 clfags.blank,494 clfags.sign,495 );496 }497 ConvTypeV::Hexadecimal => {498 let value = value.clone().try_cast_num("%x/%X requires number")?;499 render_hexadecimal(500 &mut tmp_out,501 value as i64,502 padding,503 iprec,504 clfags.alt,505 clfags.blank,506 clfags.sign,507 code.caps,508 );509 }510 ConvTypeV::Scientific => {511 let value = value.clone().try_cast_num("%e/%E requires number")?;512 render_float_sci(513 &mut tmp_out,514 value,515 padding,516 fpprec,517 clfags.blank,518 clfags.sign,519 clfags.alt,520 true,521 code.caps,522 );523 }524 ConvTypeV::Float => {525 let value = value.clone().try_cast_num("%e/%E requires number")?;526 render_float(527 &mut tmp_out,528 value,529 padding,530 fpprec,531 clfags.blank,532 clfags.sign,533 clfags.alt,534 true,535 );536 }537 ConvTypeV::Shorter => {538 let value = value.clone().try_cast_num("%g/%G requires number")?;539 let exponent = value.log10().floor();540 if exponent < -4.0 || exponent >= fpprec as f64 {541 render_float_sci(542 &mut tmp_out,543 value,544 padding,545 fpprec - 1,546 clfags.blank,547 clfags.sign,548 clfags.alt,549 clfags.alt,550 code.caps,551 );552 } else {553 let digits_before_pt = 1.max(exponent as usize + 1);554 render_float(555 &mut tmp_out,556 value,557 padding,558 fpprec - digits_before_pt,559 clfags.blank,560 clfags.sign,561 clfags.alt,562 clfags.alt,563 );564 }565 }566 ConvTypeV::Char => match value.clone().unwrap_if_lazy()? {567 Val::Num(n) => tmp_out.push(568 std::char::from_u32(n as u32)569 .ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?,570 ),571 Val::Str(s) => {572 if s.chars().count() != 1 {573 throw!(RuntimeError(574 format!("%c expected 1 char string, got {}", s.chars().count()).into(),575 ));576 }577 tmp_out.push_str(&s);578 }579 _ => {580 throw!(TypeMismatch(581 "%c requires number/string",582 vec![ValType::Num, ValType::Str],583 value.value_type()?,584 ));585 }586 },587 ConvTypeV::Percent => tmp_out.push('%'),588 };589590 let padding = width.saturating_sub(tmp_out.len());591592 if !clfags.left {593 for _ in 0..padding {594 out.push(' ');595 }596 }597 out.push_str(&tmp_out);598 if clfags.left {599 for _ in 0..padding {600 out.push(' ');601 }602 }603604 Ok(())605}606607pub fn format_arr(str: &str, mut values: &[Val]) -> Result<String> {608 let codes = parse_codes(&str)?;609 let mut out = String::new();610611 for code in codes {612 match code {613 Element::String(s) => {614 out.push_str(s);615 }616 Element::Code(c) => {617 let width = match c.width {618 Width::Star => {619 if values.is_empty() {620 throw!(NotEnoughValues);621 }622 let value = &values[0];623 values = &values[1..];624 value.clone().try_cast_num("field width")? as usize625 }626 Width::Fixed(n) => n,627 };628 let precision = match c.precision {629 Some(Width::Star) => {630 if values.is_empty() {631 throw!(NotEnoughValues);632 }633 let value = &values[0];634 values = &values[1..];635 Some(value.clone().try_cast_num("field precision")? as usize)636 }637 Some(Width::Fixed(n)) => Some(n),638 None => None,639 };640641 // %% should not consume a value642 let value = if c.convtype == ConvTypeV::Percent {643 &Val::Null644 } else {645 if values.is_empty() {646 throw!(NotEnoughValues);647 }648 let value = &values[0];649 values = &values[1..];650 value651 };652653 format_code(&mut out, value, &c, width, precision)?;654 }655 }656 }657658 Ok(out)659}660661pub fn format_obj(str: &str, values: &ObjValue) -> Result<String> {662 let codes = parse_codes(&str)?;663 let mut out = String::new();664665 for code in codes {666 match code {667 Element::String(s) => {668 out.push_str(s);669 }670 Element::Code(c) => {671 // TODO: Operate on ref672 let f: Rc<str> = c.mkey.into();673 let width = match c.width {674 Width::Star => {675 throw!(CannotUseStarWidthWithObject);676 }677 Width::Fixed(n) => n,678 };679 let precision = match c.precision {680 Some(Width::Star) => {681 throw!(CannotUseStarWidthWithObject);682 }683 Some(Width::Fixed(n)) => Some(n),684 None => None,685 };686687 let value = if c.convtype == ConvTypeV::Percent {688 Val::Null689 } else {690 if f.is_empty() {691 throw!(MappingKeysRequired);692 }693 if let Some(v) = values.get(f.clone())? {694 v695 } else {696 throw!(NoSuchFormatField(f));697 }698 };699700 format_code(&mut out, &value, &c, width, precision)?;701 }702 }703 }704705 Ok(out)706}707708#[cfg(test)]709pub mod test_format {710 use super::*;711712 #[test]713 fn parse() {714 assert_eq!(715 parse_codes(716 "How much error budget is left looking at our %.3f%% availability gurantees?"717 )718 .unwrap()719 .len(),720 4721 );722 }723724 #[test]725 fn octals() {726 assert_eq!(format_arr("%#o", &[Val::Num(8.0)]).unwrap(), "010");727 assert_eq!(format_arr("%#4o", &[Val::Num(8.0)]).unwrap(), " 010");728 assert_eq!(format_arr("%4o", &[Val::Num(8.0)]).unwrap(), " 10");729 assert_eq!(format_arr("%04o", &[Val::Num(8.0)]).unwrap(), "0010");730 assert_eq!(format_arr("%+4o", &[Val::Num(8.0)]).unwrap(), " +10");731 assert_eq!(format_arr("%+04o", &[Val::Num(8.0)]).unwrap(), "+010");732 assert_eq!(format_arr("%-4o", &[Val::Num(8.0)]).unwrap(), "10 ");733 assert_eq!(format_arr("%+-4o", &[Val::Num(8.0)]).unwrap(), "+10 ");734 assert_eq!(format_arr("%+-04o", &[Val::Num(8.0)]).unwrap(), "+10 ");735 }736737 #[test]738 fn percent_doesnt_consumes_values() {739 assert_eq!(740 format_arr(741 "How much error budget is left looking at our %.3f%% availability gurantees?",742 &[Val::Num(4.0)]743 )744 .unwrap(),745 "How much error budget is left looking at our 4.000% availability gurantees?"746 );747 }748}