Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions interpreter/src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ use parser::{AriadneSpan, BuiltinFunction};

use crate::{
InterpreterError,
vm::{Interpreter, types::Value},
vm::{
Interpreter,
types::{Value, str_to_num},
},
};

use rc_vec::RcVec as Vec;
Expand Down Expand Up @@ -242,18 +245,27 @@ fn to_bits(v: &Value) -> u64 {

fn strtonum(v: &Value) -> f64 {
let bytes = value_bytes(v);
let Ok(s) = std::str::from_utf8(&bytes) else {
return 0.;
};
let s = s.trim_start();
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
return u64::from_str_radix(hex, 16).map_or(0., |n| n as f64);
}
if s.len() > 1 && s.starts_with('0') && s.bytes().all(|b| (b'0'..=b'7').contains(&b)) {
// Leading zero and only octal digits → octal, as in gawk `strtonum`.
return u64::from_str_radix(s, 8).map_or(0., |n| n as f64);
// As in gawk, a hex or octal prefix only counts at the very start; an 8,
// 9, `.` or exponent after the leading 0 makes it a decimal number.
match bytes.as_slice() {
[b'0', b'x' | b'X', rest @ ..] => rest
.iter()
.map_while(|&b| char::from(b).to_digit(16))
.fold(0., |n, d| n * 16. + f64::from(d)),
[b'0', rest @ ..] => {
let octal = rest
.iter()
.take_while(|b| (b'0'..=b'7').contains(*b))
.count();
match rest.get(octal) {
Some(b'8' | b'9' | b'.' | b'e' | b'E') => str_to_num(&bytes),
_ => rest[..octal]
.iter()
.fold(0., |n, &d| n * 8. + f64::from(d - b'0')),
}
}
_ => str_to_num(&bytes),
}
s.parse().unwrap_or(0.)
}

fn value_bytes(v: &Value) -> Vec<u8> {
Expand All @@ -278,6 +290,11 @@ mod tests {
f(&mut intrp);
}

#[test]
fn str_value_reads_the_leading_number() {
assert_eq!(Value::new_str(b" 3x").to_num(), 3.);
}

#[test]
fn int_truncates_toward_zero() {
with_intrp(|intrp| {
Expand Down
65 changes: 61 additions & 4 deletions interpreter/src/vm/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,64 @@ enum AwkValue<'a> {
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Display, From)]
pub struct AwkNum(f64);

/// The numeric value of a string, read the way awk reads it: leading blanks
/// are skipped and the longest prefix that is a decimal number counts, so
/// `" 12 "` is 12, `"3x"` is 3 and `"x"` is 0. Like gawk, `inf` and `nan`
/// are only recognized with a sign and nothing else but blanks around them.
pub(crate) fn str_to_num(s: &[u8]) -> f64 {
let is_blank = |b: &u8| matches!(b, b' ' | b'\t' | b'\n' | 0x0B | 0x0C | b'\r');
let s = &s[s.iter().take_while(|b| is_blank(b)).count()..];
let digits = |from: usize| {
s.get(from..)
.map_or(0, |t| t.iter().take_while(|b| b.is_ascii_digit()).count())
};

let mut end = usize::from(matches!(s.first(), Some(b'+' | b'-')));
let int = digits(end);
end += int;
let mut frac = 0;
if s.get(end) == Some(&b'.') {
frac = digits(end + 1);
end += 1 + frac;
}
if int + frac == 0 {
return signed_special(s, &is_blank).unwrap_or(0.);
}
if matches!(s.get(end), Some(b'e' | b'E')) {
let sign = usize::from(matches!(s.get(end + 1), Some(b'+' | b'-')));
let exp = digits(end + 1 + sign);
if exp > 0 {
end += 1 + sign + exp;
}
}
str::from_utf8(&s[..end])
.ok()
.and_then(|n| n.parse().ok())
.unwrap_or(0.)
}

/// `+inf`, `-inf`, `+nan` or `-nan`, in any case, followed only by blanks.
fn signed_special(s: &[u8], is_blank: &impl Fn(&u8) -> bool) -> Option<f64> {
let (sign, rest) = s.split_first()?;
let negative = match sign {
b'+' => false,
b'-' => true,
_ => return None,
};
let (word, tail) = rest.split_at_checked(3)?;
if !tail.iter().all(is_blank) {
return None;
}
let value = if word.eq_ignore_ascii_case(b"inf") {
f64::INFINITY
} else if word.eq_ignore_ascii_case(b"nan") {
f64::NAN
} else {
return None;
};
Some(if negative { -value } else { value })
}

#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AwkStr([u8]);
Expand Down Expand Up @@ -275,10 +333,9 @@ impl<'a> AwkValue<'a> {
&Self::Float(AwkNum(f)) => f,
&Self::Int(AwkInt(n)) => n as f64,
&Self::Bool(AwkBool(b)) => b as usize as f64,
Self::String(s) => str::from_utf8(s)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0.),
Self::String(s) => str_to_num(s),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about Str and StrNum? they still hit _ => 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in ae2eb12: Str goes through str_to_num, and StrNum returns its stored num. Nothing builds a StrNum yet, so only Str has a test.

Self::Str(s) => str_to_num(s),
Self::StrNum(s) => s.num.0,
_ => 0.,
}
}
Expand Down
38 changes: 38 additions & 0 deletions tests/by-util/test_awk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1020,3 +1020,41 @@ fn integral_numbers_print_as_integers() {
.succeeds()
.stdout_only("500000000000000000000\n");
}

#[test]
fn string_to_number_uses_leading_numeric_prefix() {
ucmd()
.arg(
r#"BEGIN {
print "3x" + 0, " 12 " + 1, "1e5x" + 0, "-.5" + 0, "1.2.3" + 0
print "x" + 0, "1e" + 0, "1e+" + 0, "inf" + 0, "0x1A" + 0
print ("+inf" + 0 > 1e308), ("-INF" + 0 < -1e308), ("\v\f\r 7" + 0)
}"#,
)
.succeeds()
.stdout_only("3 13 100000 -0.5 1.2\n0 1 1 0 0\n1 1 7\n");
}

#[test]
fn fields_with_blanks_around_numbers_sum() {
ucmd()
.arg(r#"BEGIN { FS = "," } { s += $2 } END { print s }"#)
.pipe_in("a, 12 \nb, 4\nc,10abc\n")
.succeeds()
.stdout_only("26\n");
}

#[test]
fn strtonum_reads_the_leading_number() {
ucmd()
.arg(
r#"BEGIN {
print strtonum("3x"), strtonum(" 12 "), strtonum("-3x"), strtonum(".5x"), strtonum("1e3x")
print strtonum("0x1Fz"), strtonum("0X1g"), strtonum("0x"), strtonum(" 0x10"), strtonum("+0x10")
print strtonum("017x"), strtonum("0778"), strtonum("0189"), strtonum("01.5"), strtonum("017e1")
print strtonum("x"), strtonum(""), strtonum("0"), strtonum("00"), strtonum("0xFFFFFFFF")
}"#,
)
.succeeds()
.stdout_only("3 12 -3 0.5 1000\n31 1 0 0 0\n15 778 189 1.5 170\n0 0 0 0 4294967295\n");
}
Loading