Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/fable-library-py/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Ignore compiled shared object files
*.so
.benchmarks/
9 changes: 9 additions & 0 deletions src/fable-library-py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ regex = "1.12.4"
[features]
# must be enabled when building with `cargo build`, maturin enables this automatically
extension-module = ["pyo3/extension-module"]

# Release profile tuned for the extension module. maturin builds with `--release`,
# so it honors these settings. opt-level defaults to 3 for release; do NOT set
# `panic = "abort"` — PyO3 relies on unwinding to convert boundary panics into a
# Python `PanicException` rather than aborting the interpreter process.
[profile.release]
lto = "fat"
codegen-units = 1
strip = true
69 changes: 50 additions & 19 deletions src/fable-library-py/src/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use pyo3::{
prelude::*,
types::{PyAnyMethods, PyList},
};
use std::sync::{Arc, Mutex};

#[pyclass(module = "fable", subclass, from_py_object)]
#[derive(Clone, Debug)]
Expand Down Expand Up @@ -237,8 +236,7 @@ impl FSharpArray {
}

// Fallback to PyObject storage if type extraction fails.
// This allows for generic or mixed-type arrays, at the cost of dynamic dispatch and locking.
// Arc<Mutex<...>> is used for thread safety and Python interop.
// This allows for generic or mixed-type arrays, at the cost of dynamic dispatch.
let len = elements.len().unwrap_or(0);
let mut vec = Vec::with_capacity(len);

Expand All @@ -248,7 +246,7 @@ impl FSharpArray {
})?;

Ok(FSharpArray {
storage: NativeArray::PyObject(Arc::new(Mutex::new(vec))),
storage: NativeArray::PyObject(vec),
})
}

Expand Down Expand Up @@ -369,7 +367,7 @@ impl FSharpArray {
}

Ok(FSharpArray {
storage: NativeArray::PyObject(Arc::new(Mutex::new(vec))),
storage: NativeArray::PyObject(vec),
})
}

Expand Down Expand Up @@ -946,8 +944,7 @@ impl FSharpArray {
NativeArray::Bool(vec) => {
vec[idx as usize] = value.extract()?;
}
NativeArray::PyObject(arc_mutex_vec) => {
let mut vec = arc_mutex_vec.lock().unwrap(); // Acquire the lock
NativeArray::PyObject(vec) => {
vec[idx as usize] = value.clone().into();
}
}
Expand Down Expand Up @@ -1162,12 +1159,10 @@ impl FSharpArray {
let mut results = NativeArray::new(&original_type, None); // No initial capacity needed

for i in 0..len {
// Avoid cloning item_obj if possible, only clone for predicate call
let item_obj = self.get_item_at_index(i as isize, py)?;
let keep = predicate.call1((item_obj.clone_ref(py),))?.is_truthy()?;

if keep {
// Push the original item (by index) into the results collector
// The predicate consumes the fetched item; the kept value is copied
// straight from storage by index, so no extra clone is needed.
let item = self.storage.get(py, i)?;
if predicate.call1((item,))?.is_truthy()? {
results.push_from_storage(&self.storage, i, py);
}
}
Expand Down Expand Up @@ -1274,12 +1269,12 @@ impl FSharpArray {
if len == 0 {
// Return an empty array
return Ok(FSharpArray {
storage: NativeArray::PyObject(Arc::new(Mutex::new(vec![]))),
storage: NativeArray::PyObject(vec![]),
});
}

// Create an array of arrays (chunks)
let mut chunks = NativeArray::PyObject(Arc::new(Mutex::new(vec![])));
let mut chunks = NativeArray::PyObject(vec![]);

// Create each chunk
for x in 0..len.div_ceil(chunk_size) {
Expand Down Expand Up @@ -1651,6 +1646,15 @@ impl FSharpArray {
}

pub fn sum(&self, py: Python<'_>, adder: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
// Fast path: numeric storage sums natively without per-element Python calls.
// This bypasses `adder` entirely, which is safe because Fable only ever
// injects the standard numeric adder for primitive-typed arrays (custom
// monoids reach the runtime through `sum_by`, not `sum`). If that invariant
// ever changes, this fast path must be gated accordingly.
if let Some(result) = self.storage.try_native_sum(py) {
return result;
}

let len = self.storage.len();
let mut acc = adder.call_method0("GetZero")?;

Expand All @@ -1670,6 +1674,14 @@ impl FSharpArray {
));
}

// Fast path: sum numeric storage natively, then delegate the final
// DivideByInt to the averager so division/truncation semantics match exactly.
if let Some(sum_res) = self.storage.try_native_sum(py) {
let acc = sum_res?;
let result = averager.call_method1("DivideByInt", (acc, len))?;
return Ok(result.into());
}

let mut acc = averager.call_method0("GetZero")?;

for i in 0..len {
Expand Down Expand Up @@ -1931,7 +1943,7 @@ impl FSharpArray {
let len = self.storage.len();
if len == 0 {
return Ok(FSharpArray {
storage: NativeArray::PyObject(Arc::new(Mutex::new(vec![]))),
storage: NativeArray::PyObject(vec![]),
});
}

Expand Down Expand Up @@ -2852,6 +2864,14 @@ impl FSharpArray {
value: &Bound<'_, PyAny>,
eq: Option<&Bound<'_, PyAny>>,
) -> PyResult<bool> {
// Fast path: with no custom equality comparer, numeric/bool storage scans
// natively (falls back when `value` isn't representable as the element type).
if eq.is_none() {
if let Some(result) = self.storage.try_native_contains(value) {
return result;
}
}

let len = self.storage.len();
for i in 0..len {
let item = self.get_item_at_index(i as isize, py)?;
Expand All @@ -2877,6 +2897,14 @@ impl FSharpArray {

// let max (xs: 'a[]) ([<Inject>] comparer: IComparer<'a>) : 'a =
pub fn max(&self, py: Python<'_>, comparer: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
// Fast path: integer storage compares natively (floats fall back to the
// comparer path to preserve its NaN ordering; empty falls back to the
// canonical empty-array error in reduce_impl). Bypassing `comparer` is safe
// because Fable always injects the standard comparer here; a custom key
// comparer reaches the runtime through `max_by`, which is not fast-pathed.
if let Some(result) = self.storage.try_native_max(py) {
return result;
}
reduce_impl(self, py, |acc, item, py| {
let comparison =
comparer.call_method1("Compare", (acc.clone_ref(py), item.clone_ref(py)))?;
Expand All @@ -2886,6 +2914,10 @@ impl FSharpArray {
}

pub fn min(&self, py: Python<'_>, comparer: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
// Fast path: integer storage compares natively (see `max`).
if let Some(result) = self.storage.try_native_min(py) {
return result;
}
reduce_impl(self, py, |acc, item, py| {
let comparison =
comparer.call_method1("Compare", (acc.clone_ref(py), item.clone_ref(py)))?;
Expand Down Expand Up @@ -2947,7 +2979,7 @@ impl FSharpArray {
let mut results = NativeArray::new(&target_type, None);
for i in 0..len {
let item = self.get_item_at_index(i as isize, py)?;
let chosen = chooser.call1((item.clone_ref(py),))?;
let chosen = chooser.call1((item,))?;
if !chosen.is_none() {
results.push_value(&chosen, py)?;
}
Expand Down Expand Up @@ -3282,8 +3314,7 @@ impl FSharpArray {
NativeArray::Float32(vec) => vec.swap(i, j),
NativeArray::Float64(vec) => vec.swap(i, j),
NativeArray::Bool(vec) => vec.swap(i, j),
NativeArray::PyObject(arc) => {
let mut vec = arc.lock().unwrap();
NativeArray::PyObject(vec) => {
vec.swap(i, j);
}
}
Expand Down
Loading
Loading