Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expr: Optimizing for integer values #5614

Merged
merged 12 commits into from
Dec 14, 2023
4 changes: 2 additions & 2 deletions src/uu/expr/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.map(|v| v.into_iter().map(|s| s.as_ref()).collect::<Vec<_>>())
.unwrap_or_default();

let res = AstNode::parse(&token_strings)?.eval()?;
let res: String = AstNode::parse(&token_strings)?.eval()?.eval_as_string();
println!("{res}");
if !is_truthy(&res) {
if !is_truthy(&res.into()) {
return Err(1.into());
}
Ok(())
Expand Down
166 changes: 115 additions & 51 deletions src/uu/expr/src/syntax_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

// spell-checker:ignore (ToDO) ints paren prec multibytes

use num_bigint::BigInt;
use num_bigint::{BigInt, ParseBigIntError};
use num_traits::ToPrimitive;
use onig::{Regex, RegexOptions, Syntax};

use crate::{ExprError, ExprResult};
Expand Down Expand Up @@ -45,7 +46,7 @@
}

impl BinOp {
fn eval(&self, left: &AstNode, right: &AstNode) -> ExprResult<String> {
fn eval(&self, left: &AstNode, right: &AstNode) -> ExprResult<NumOrStr> {
match self {
Self::Relation(op) => op.eval(left, right),
Self::Numeric(op) => op.eval(left, right),
Expand All @@ -55,10 +56,10 @@
}

impl RelationOp {
fn eval(&self, a: &AstNode, b: &AstNode) -> ExprResult<String> {
fn eval(&self, a: &AstNode, b: &AstNode) -> ExprResult<NumOrStr> {
let a = a.eval()?;
let b = b.eval()?;
let b = if let (Ok(a), Ok(b)) = (a.parse::<BigInt>(), b.parse::<BigInt>()) {
let b = if let (Ok(a), Ok(b)) = (&a.to_bigint(), &b.to_bigint()) {
match self {
Self::Lt => a < b,
Self::Leq => a <= b,
Expand All @@ -79,24 +80,18 @@
}
};
if b {
Ok("1".into())
Ok(1.into())
} else {
Ok("0".into())
Ok(0.into())
}
}
}

impl NumericOp {
fn eval(&self, left: &AstNode, right: &AstNode) -> ExprResult<String> {
let a: BigInt = left
.eval()?
.parse()
.map_err(|_| ExprError::NonIntegerArgument)?;
let b: BigInt = right
.eval()?
.parse()
.map_err(|_| ExprError::NonIntegerArgument)?;
Ok(match self {
fn eval(&self, left: &AstNode, right: &AstNode) -> ExprResult<NumOrStr> {
let a = left.eval()?.eval_as_bigint()?;
let b = right.eval()?.eval_as_bigint()?;
Ok(NumOrStr::Num(match self {
Self::Add => a + b,
Self::Sub => a - b,
Self::Mul => a * b,
Expand All @@ -110,13 +105,12 @@
};
a % b
}
}
.to_string())
}))
}
}

impl StringOp {
fn eval(&self, left: &AstNode, right: &AstNode) -> ExprResult<String> {
fn eval(&self, left: &AstNode, right: &AstNode) -> ExprResult<NumOrStr> {
match self {
Self::Or => {
let left = left.eval()?;
Expand All @@ -127,23 +121,23 @@
if is_truthy(&right) {
return Ok(right);
}
Ok("0".into())
Ok(0.into())
}
Self::And => {
let left = left.eval()?;
if !is_truthy(&left) {
return Ok("0".into());
return Ok(0.into());
}
let right = right.eval()?;
if !is_truthy(&right) {
return Ok("0".into());
return Ok(0.into());

Check warning on line 133 in src/uu/expr/src/syntax_tree.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/expr/src/syntax_tree.rs#L133

Added line #L133 was not covered by tests
}
Ok(left)
}
Self::Match => {
let left = left.eval()?;
let right = right.eval()?;
let re_string = format!("^{}", &right);
let left = left.eval()?.eval_as_string();
let right = right.eval()?.eval_as_string();
let re_string = format!("^{}", right);
let re = Regex::with_options(
&re_string,
RegexOptions::REGEX_OPTION_NONE,
Expand All @@ -158,19 +152,20 @@
} else {
re.find(&left)
.map_or("0".to_string(), |(start, end)| (end - start).to_string())
})
}
.into())
}
Self::Index => {
let left = left.eval()?;
let right = right.eval()?;
let left = left.eval()?.eval_as_string();
let right = right.eval()?.eval_as_string();
for (current_idx, ch_h) in left.chars().enumerate() {
for ch_n in right.chars() {
for ch_n in right.to_string().chars() {
if ch_n == ch_h {
return Ok((current_idx + 1).to_string());
return Ok((current_idx + 1).into());
}
}
}
Ok("0".to_string())
Ok(0.into())
}
}
}
Expand Down Expand Up @@ -200,6 +195,55 @@
&[(":", BinOp::String(StringOp::Match))],
];

#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]
pub enum NumOrStr {
Num(BigInt),

Check warning on line 200 in src/uu/expr/src/syntax_tree.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/expr/src/syntax_tree.rs#L200

Added line #L200 was not covered by tests
Str(String),
}

impl From<usize> for NumOrStr {
fn from(num: usize) -> Self {
Self::Num(BigInt::from(num))
}
}

impl From<BigInt> for NumOrStr {
fn from(num: BigInt) -> Self {
Self::Num(num)
}

Check warning on line 213 in src/uu/expr/src/syntax_tree.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/expr/src/syntax_tree.rs#L211-L213

Added lines #L211 - L213 were not covered by tests
}

impl From<String> for NumOrStr {
fn from(str: String) -> Self {
Self::Str(str)
}
}

impl NumOrStr {
pub fn to_bigint(&self) -> Result<BigInt, ParseBigIntError> {
match self {
Self::Num(num) => Ok(num.clone()),
Self::Str(str) => str.parse::<BigInt>(),
}
}

pub fn eval_as_bigint(self) -> ExprResult<BigInt> {
match self {
Self::Num(num) => Ok(num),
Self::Str(str) => str
.parse::<BigInt>()
.map_err(|_| ExprError::NonIntegerArgument),
}
}

pub fn eval_as_string(self) -> String {
match self {
Self::Num(num) => num.to_string(),
Self::Str(str) => str,
}
}
}

#[derive(Debug, PartialEq, Eq)]
pub enum AstNode {
Leaf {
Expand All @@ -225,9 +269,9 @@
Parser::new(input).parse()
}

pub fn eval(&self) -> ExprResult<String> {
pub fn eval(&self) -> ExprResult<NumOrStr> {
match self {
Self::Leaf { value } => Ok(value.into()),
Self::Leaf { value } => Ok(value.to_string().into()),
Self::BinOp {
op_type,
left,
Expand All @@ -238,7 +282,7 @@
pos,
length,
} => {
let string = string.eval()?;
let string: String = string.eval()?.eval_as_string();

// The GNU docs say:
//
Expand All @@ -247,16 +291,31 @@
//
// So we coerce errors into 0 to make that the only case we
// have to care about.
let pos: usize = pos.eval()?.parse().unwrap_or(0);
let length: usize = length.eval()?.parse().unwrap_or(0);
let pos = pos
.eval()?

Check warning on line 295 in src/uu/expr/src/syntax_tree.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/expr/src/syntax_tree.rs#L295

Added line #L295 was not covered by tests
.eval_as_bigint()
.ok()
.and_then(|n| n.to_usize())
.unwrap_or(0);
let length = length
.eval()?

Check warning on line 301 in src/uu/expr/src/syntax_tree.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/expr/src/syntax_tree.rs#L301

Added line #L301 was not covered by tests
.eval_as_bigint()
.ok()
.and_then(|n| n.to_usize())
.unwrap_or(0);

let (Some(pos), Some(_)) = (pos.checked_sub(1), length.checked_sub(1)) else {
return Ok(String::new());
return Ok(String::new().into());
};

Ok(string.chars().skip(pos).take(length).collect())
Ok(string
.chars()
.skip(pos)
.take(length)
.collect::<String>()
.into())
}
Self::Length { string } => Ok(string.eval()?.chars().count().to_string()),
Self::Length { string } => Ok(string.eval()?.eval_as_string().chars().count().into()),
}
}
}
Expand Down Expand Up @@ -399,21 +458,26 @@
/// Determine whether `expr` should evaluate the string as "truthy"
///
/// Truthy strings are either empty or match the regex "-?0+".
pub fn is_truthy(s: &str) -> bool {
// Edge case: `-` followed by nothing is truthy
if s == "-" {
return true;
}
pub fn is_truthy(s: &NumOrStr) -> bool {
match s {
NumOrStr::Num(num) => num != &BigInt::from(0),
NumOrStr::Str(str) => {
// Edge case: `-` followed by nothing is truthy
if str == "-" {
return true;

Check warning on line 467 in src/uu/expr/src/syntax_tree.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/expr/src/syntax_tree.rs#L467

Added line #L467 was not covered by tests
}

let mut bytes = s.bytes();
let mut bytes = str.bytes();

// Empty string is falsy
let Some(first) = bytes.next() else {
return false;
};
// Empty string is falsy
let Some(first) = bytes.next() else {
return false;
};

let is_zero = (first == b'-' || first == b'0') && bytes.all(|b| b == b'0');
!is_zero
let is_zero = (first == b'-' || first == b'0') && bytes.all(|b| b == b'0');
!is_zero
}
}
}

#[cfg(test)]
Expand Down
Loading