Support switch range cases for floating-point values.

This commit is contained in:
Stephen Chung 2023-02-12 23:20:14 +08:00
parent d0a47d7f66
commit 10089c5cb0
7 changed files with 68 additions and 45 deletions

View File

@ -17,6 +17,7 @@ Enhancements
------------ ------------
* The functions `min` and `max` are added for numbers. * The functions `min` and `max` are added for numbers.
* Range cases in `switch` statements now also match floating-point and decimal values. In order to support this, however, small numeric ranges cases are no longer unrolled.
Version 1.12.0 Version 1.12.0

View File

@ -4,8 +4,9 @@ use super::{ASTFlags, ASTNode, BinaryExpr, Expr, FnCallExpr, Ident};
use crate::engine::{KEYWORD_EVAL, OP_EQUALS}; use crate::engine::{KEYWORD_EVAL, OP_EQUALS};
use crate::func::StraightHashMap; use crate::func::StraightHashMap;
use crate::tokenizer::Token; use crate::tokenizer::Token;
use crate::types::dynamic::Union;
use crate::types::Span; use crate::types::Span;
use crate::{calc_fn_hash, Position, StaticVec, INT}; use crate::{calc_fn_hash, Dynamic, Position, StaticVec, INT};
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
use std::{ use std::{
@ -257,7 +258,7 @@ impl IntoIterator for RangeCase {
type Item = INT; type Item = INT;
type IntoIter = Box<dyn Iterator<Item = Self::Item>>; type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
#[inline(always)] #[inline]
#[must_use] #[must_use]
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
match self { match self {
@ -269,7 +270,7 @@ impl IntoIterator for RangeCase {
impl RangeCase { impl RangeCase {
/// Returns `true` if the range contains no items. /// Returns `true` if the range contains no items.
#[inline(always)] #[inline]
#[must_use] #[must_use]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
match self { match self {
@ -278,7 +279,7 @@ impl RangeCase {
} }
} }
/// Size of the range. /// Size of the range.
#[inline(always)] #[inline]
#[must_use] #[must_use]
pub fn len(&self) -> INT { pub fn len(&self) -> INT {
match self { match self {
@ -288,15 +289,56 @@ impl RangeCase {
Self::InclusiveInt(r, ..) => *r.end() - *r.start() + 1, Self::InclusiveInt(r, ..) => *r.end() - *r.start() + 1,
} }
} }
/// Is the specified number within this range? /// Is the specified value within this range?
#[inline(always)] #[inline]
#[must_use] #[must_use]
pub fn contains(&self, n: INT) -> bool { pub fn contains(&self, value: &Dynamic) -> bool {
match value {
Dynamic(Union::Int(v, ..)) => self.contains_int(*v),
#[cfg(not(feature = "no_float"))]
Dynamic(Union::Float(v, ..)) => self.contains_float(**v),
#[cfg(feature = "decimal")]
Dynamic(Union::Decimal(v, ..)) => self.contains_decimal(**v),
_ => false,
}
}
/// Is the specified number within this range?
#[inline]
#[must_use]
pub fn contains_int(&self, n: INT) -> bool {
match self { match self {
Self::ExclusiveInt(r, ..) => r.contains(&n), Self::ExclusiveInt(r, ..) => r.contains(&n),
Self::InclusiveInt(r, ..) => r.contains(&n), Self::InclusiveInt(r, ..) => r.contains(&n),
} }
} }
/// Is the specified floating-point number within this range?
#[cfg(not(feature = "no_float"))]
#[inline]
#[must_use]
pub fn contains_float(&self, n: crate::FLOAT) -> bool {
use crate::FLOAT;
match self {
Self::ExclusiveInt(r, ..) => ((r.start as FLOAT)..(r.end as FLOAT)).contains(&n),
Self::InclusiveInt(r, ..) => ((*r.start() as FLOAT)..=(*r.end() as FLOAT)).contains(&n),
}
}
/// Is the specified decimal number within this range?
#[cfg(feature = "decimal")]
#[inline]
#[must_use]
pub fn contains_decimal(&self, n: rust_decimal::Decimal) -> bool {
use rust_decimal::Decimal;
match self {
Self::ExclusiveInt(r, ..) => {
(Into::<Decimal>::into(r.start)..Into::<Decimal>::into(r.end)).contains(&n)
}
Self::InclusiveInt(r, ..) => {
(Into::<Decimal>::into(*r.start())..=Into::<Decimal>::into(*r.end())).contains(&n)
}
}
}
/// Is the specified range inclusive? /// Is the specified range inclusive?
#[inline(always)] #[inline(always)]
#[must_use] #[must_use]

View File

@ -3,7 +3,8 @@
use super::{Caches, EvalContext, GlobalRuntimeState, Target}; use super::{Caches, EvalContext, GlobalRuntimeState, Target};
use crate::api::events::VarDefInfo; use crate::api::events::VarDefInfo;
use crate::ast::{ use crate::ast::{
ASTFlags, BinaryExpr, Expr, FlowControl, OpAssignment, Stmt, SwitchCasesCollection, ASTFlags, BinaryExpr, ConditionalExpr, Expr, FlowControl, OpAssignment, Stmt,
SwitchCasesCollection,
}; };
use crate::func::{get_builtin_op_assignment_fn, get_hasher}; use crate::func::{get_builtin_op_assignment_fn, get_hasher};
use crate::types::dynamic::{AccessMode, Union}; use crate::types::dynamic::{AccessMode, Union};
@ -359,15 +360,13 @@ impl Engine {
break; break;
} }
} }
} else if value.is_int() && !ranges.is_empty() { } else if !ranges.is_empty() {
// Then check integer ranges // Then check integer ranges
let value = value.as_int().expect("`INT`"); for r in ranges.iter().filter(|r| r.contains(&value)) {
let ConditionalExpr { condition, expr } = &expressions[r.index()];
for r in ranges.iter().filter(|r| r.contains(value)) { let cond_result = match condition {
let block = &expressions[r.index()]; Expr::BoolConstant(b, ..) => *b,
let cond_result = match block.condition {
Expr::BoolConstant(b, ..) => b,
ref c => self ref c => self
.eval_expr(global, caches, scope, this_ptr.as_deref_mut(), c)? .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), c)?
.as_bool() .as_bool()
@ -377,7 +376,7 @@ impl Engine {
}; };
if cond_result { if cond_result {
result = Some(&block.expr); result = Some(expr);
break; break;
} }
} }

View File

@ -28,6 +28,7 @@ impl Hasher for StraightHasher {
self.0 self.0
} }
#[cold] #[cold]
#[inline(never)]
fn write(&mut self, _bytes: &[u8]) { fn write(&mut self, _bytes: &[u8]) {
panic!("StraightHasher can only hash u64 values"); panic!("StraightHasher can only hash u64 values");
} }

View File

@ -563,16 +563,14 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
} }
// Then check ranges // Then check ranges
if value.is_int() && !ranges.is_empty() { if !ranges.is_empty() {
let value = value.as_int().unwrap();
// Only one range or all ranges without conditions // Only one range or all ranges without conditions
if ranges.len() == 1 if ranges.len() == 1
|| ranges || ranges
.iter() .iter()
.all(|r| expressions[r.index()].is_always_true()) .all(|r| expressions[r.index()].is_always_true())
{ {
if let Some(r) = ranges.iter().find(|r| r.contains(value)) { if let Some(r) = ranges.iter().find(|r| r.contains(&value)) {
let range_block = &mut expressions[r.index()]; let range_block = &mut expressions[r.index()];
if range_block.is_always_true() { if range_block.is_always_true() {
@ -619,7 +617,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
let old_ranges_len = ranges.len(); let old_ranges_len = ranges.len();
ranges.retain(|r| r.contains(value)); ranges.retain(|r| r.contains(&value));
if ranges.len() != old_ranges_len { if ranges.len() != old_ranges_len {
state.set_dirty(); state.set_dirty();

View File

@ -19,7 +19,7 @@ use crate::types::StringsInterner;
use crate::{ use crate::{
calc_fn_hash, Dynamic, Engine, EvalAltResult, EvalContext, ExclusiveRange, FnArgsVec, calc_fn_hash, Dynamic, Engine, EvalAltResult, EvalContext, ExclusiveRange, FnArgsVec,
Identifier, ImmutableString, InclusiveRange, LexError, OptimizationLevel, ParseError, Position, Identifier, ImmutableString, InclusiveRange, LexError, OptimizationLevel, ParseError, Position,
Scope, Shared, SmartString, StaticVec, AST, INT, PERR, Scope, Shared, SmartString, StaticVec, AST, PERR,
}; };
use bitflags::bitflags; use bitflags::bitflags;
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
@ -42,9 +42,6 @@ const SCOPE_SEARCH_BARRIER_MARKER: &str = "$ BARRIER $";
/// The message: `TokenStream` never ends /// The message: `TokenStream` never ends
const NEVER_ENDS: &str = "`Token`"; const NEVER_ENDS: &str = "`Token`";
/// Unroll `switch` ranges no larger than this.
const SMALL_SWITCH_RANGE: INT = 16;
/// _(internals)_ A type that encapsulates the current state of the parser. /// _(internals)_ A type that encapsulates the current state of the parser.
/// Exported under the `internals` feature only. /// Exported under the `internals` feature only.
pub struct ParseState<'e, 's> { pub struct ParseState<'e, 's> {
@ -1216,7 +1213,6 @@ impl Engine {
let stmt_block: StmtBlock = stmt.into(); let stmt_block: StmtBlock = stmt.into();
(Expr::Stmt(stmt_block.into()), need_comma) (Expr::Stmt(stmt_block.into()), need_comma)
}; };
let has_condition = !matches!(condition, Expr::BoolConstant(true, ..));
expressions.push((condition, action_expr).into()); expressions.push((condition, action_expr).into());
let index = expressions.len() - 1; let index = expressions.len() - 1;
@ -1240,23 +1236,9 @@ impl Engine {
if let Some(mut r) = range_value { if let Some(mut r) = range_value {
if !r.is_empty() { if !r.is_empty() {
// Do not unroll ranges if there are previous non-unrolled ranges // Other range
if !has_condition && ranges.is_empty() && r.len() <= SMALL_SWITCH_RANGE r.set_index(index);
{ ranges.push(r);
// Unroll small range
r.into_iter().for_each(|n| {
let hasher = &mut get_hasher();
Dynamic::from_int(n).hash(hasher);
cases
.entry(hasher.finish())
.and_modify(|cases| cases.push(index))
.or_insert_with(|| [index].into());
});
} else {
// Other range
r.set_index(index);
ranges.push(r);
}
} }
continue; continue;
} }

View File

@ -50,8 +50,8 @@ fn test_expressions() -> Result<(), Box<EvalAltResult>> {
" "
switch x { switch x {
0 => 1, 0 => 1,
1..10 => 123,
10 => 42, 10 => 42,
1..10 => 123,
} }
" "
)?, )?,
@ -63,11 +63,11 @@ fn test_expressions() -> Result<(), Box<EvalAltResult>> {
" "
switch x { switch x {
0 => 1, 0 => 1,
10 => 42,
1..10 => { 1..10 => {
let y = 123; let y = 123;
y y
} }
10 => 42,
} }
" "
) )