mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-01 22:56:54 -06:00
Merge PR #3038
This commit is contained in:
@@ -106,3 +106,6 @@ sync_github_name = true
|
||||
|
||||
[people.Plutonium-fream]
|
||||
sync_github_name = true
|
||||
|
||||
[people.maxm11]
|
||||
sync_github_name = true
|
||||
|
||||
@@ -89,8 +89,14 @@ pub fn zip2(a: &Value, b: &Value, f: impl Fn(&Value, &Value) -> Value) -> Value
|
||||
/// Cell count of a `rows`×`cols` broadcast result, or `None` when it would exceed [`MAX_CELLS`].
|
||||
/// Each operand passes its own size cap, but their broadcast product can be far larger
|
||||
/// (`SEQUENCE(1048576)*SEQUENCE(1,16384)` is 17 billion cells), so it is checked before allocating.
|
||||
/// The product is formed in `u64`: on wasm32 `usize` is 32 bits, where a `usize` product above
|
||||
/// `u32::MAX` would wrap (2^32 cells → 0) and slip past the cap.
|
||||
pub(super) fn broadcast_len(rows: usize, cols: usize) -> Option<usize> {
|
||||
rows.checked_mul(cols).filter(|&n| n as u64 <= MAX_CELLS)
|
||||
let cells = u64::try_from(rows).ok()?.checked_mul(u64::try_from(cols).ok()?)?;
|
||||
if cells > MAX_CELLS {
|
||||
return None;
|
||||
}
|
||||
usize::try_from(cells).ok()
|
||||
}
|
||||
|
||||
fn broadcast_dim(a: usize, b: usize) -> usize {
|
||||
@@ -124,7 +130,7 @@ pub(super) fn zip3(c: &Array, t: &Value, f: &Value, g: impl Fn(&Value, &Value, &
|
||||
/// can't nest, so the whole helper gives `#CALC!`, as in Excel.
|
||||
pub(super) fn single(v: Value) -> Option<Value> {
|
||||
match v {
|
||||
Value::Array(a) if a.rows * a.cols > 1 => None,
|
||||
Value::Array(a) if a.data.len() > 1 => None,
|
||||
v => Some(v.scalar()),
|
||||
}
|
||||
}
|
||||
@@ -178,3 +184,24 @@ fn binary_scalar(op: BinOp, a: &Value, b: &Value, sys: gridcraft_core::DateSyste
|
||||
BinOp::Range | BinOp::Union | BinOp::Intersect => Value::Error(CellError::Value),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MAX_CELLS, broadcast_len};
|
||||
|
||||
#[test]
|
||||
fn broadcast_len_caps_at_max_cells() {
|
||||
assert_eq!(broadcast_len(4_000, 4_000), Some(MAX_CELLS as usize));
|
||||
assert_eq!(broadcast_len(4_000, 4_001), None);
|
||||
assert_eq!(broadcast_len(1, MAX_CELLS as usize), Some(MAX_CELLS as usize));
|
||||
}
|
||||
|
||||
/// `65_536 * 65_536` is 2^32. Multiplied as `usize` on wasm32 it wraps to 0, which a
|
||||
/// `> MAX_CELLS` test would let through; the guard must reject the true product.
|
||||
#[test]
|
||||
fn broadcast_len_rejects_a_product_that_wraps_to_zero_on_32_bits() {
|
||||
assert_eq!(65_536u32.wrapping_mul(65_536), 0);
|
||||
assert_eq!(broadcast_len(65_536, 65_536), None);
|
||||
assert_eq!(broadcast_len(1_048_576, 16_384), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1251,6 +1251,25 @@ fn array_broadcast_result_is_capped() {
|
||||
assert_eq!(t.num("C3"), 5.0);
|
||||
}
|
||||
|
||||
/// MAP refuses a result past `MAX_CELLS` by its checked product, before allocating it (the
|
||||
/// 2^32-cell wrap that a 32-bit `usize` product would turn into 0 is pinned in
|
||||
/// `broadcast_len_rejects_a_product_that_wraps_to_zero_on_32_bits`; `SEQUENCE` tops out at
|
||||
/// 16384 columns, so the widest product it can build is 16384 squared).
|
||||
#[test]
|
||||
fn map_caps_its_result_size() {
|
||||
let mut t = T::new();
|
||||
for (at, formula) in
|
||||
[("A1", "=MAP(SEQUENCE(4001),SEQUENCE(1,4000),LAMBDA(a,b,a))"), ("A2", "=MAP(SEQUENCE(16384),SEQUENCE(1,16384),LAMBDA(a,b,a))")]
|
||||
{
|
||||
t.set(at, formula);
|
||||
assert_eq!(t.get(at), Value::Error(CellError::Num), "{formula}");
|
||||
}
|
||||
// Within the cap the broadcast shape is unchanged.
|
||||
t.set("A3", "=ROWS(MAP(SEQUENCE(3),SEQUENCE(1,2),LAMBDA(a,b,a+b)))");
|
||||
t.set("A4", "=COLUMNS(MAP(SEQUENCE(3),SEQUENCE(1,2),LAMBDA(a,b,a+b)))");
|
||||
assert_eq!((t.num("A3"), t.num("A4")), (3.0, 2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_function() {
|
||||
let mut t = T::new();
|
||||
|
||||
Reference in New Issue
Block a user