Add built-in NumPy support#248
Conversation
bf87cce to
1dffa8a
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
0b57170 to
6bfc6ce
Compare
|
Thank you for this PR. As you note, it's absolutely massive. I can understand the use case however I'm also reluctant to add such a huge feature set to the codebase, it's also not a complete feature set of numpy / pandas so I would imagine that merging this will set precedent for:
I worry this will quickly become unsustainable to maintain. I wonder if there's a few different approaches to explore here:
|
|
I certainly would like to help here but my rust knowledge is next to nothing, I done my own sandbox project but it use pyodide which is different approach to monty here https://github.com/auto-medica-labs/vivarium |
|
Hi thanks so much for this @trevorprater I agree with one of @davidhewitt's suggestions: we should add Rationale:
LMK what you think? |
|
Thanks @samuelcolvin, that makes sense. I agree – numpy is a much cleaner fit for Monty’s scope. The pandas surface area is large and would inevitably pull toward needing a real query engine for completeness, which isn’t worth the binary bloat. I’ll split the PR to numpy-only. Plan is:
@davidhewitt – apologies for the slow response, things have been hectic on my end. I really appreciate the thoughtful review and the suggestions around a plugin/extension mechanism (great idea). |
|
Great, no worries and no rush. FYI, I recently merged #265 which was a huge refactoring that'll likely hit this PR hard (but makes it MUCH easier to avoid copying data, satisfy borrow checker etc). Happy to take pings if you need any comments about how to get this PR up-to-date with main for the numpy bindings. |
Implements a built-in numpy module with ndarray type for running LLM-generated numeric Python code in the Monty sandbox. Module functions: array, zeros, ones, arange, linspace, sum, mean, min, max, abs, sqrt, log, exp, round, clip, where, maximum, minimum, sort, unique, concatenate, cumsum, dot, ceil, floor, log10, std. NdArray methods: sum, mean, min, max, std, flatten, tolist, copy, sort, argsort, argmin, argmax, all, any, cumsum, reshape, round, clip, dot, astype. Element-wise binary ops (+, -, *, /, //, %, **) and comparisons (==, !=, >, <, >=, <=) between arrays and scalars. All tests verified against real CPython + numpy.
Parity test with ~200 assertions covering every numpy function, method, attribute, binary op, comparison, and edge case — verified against both real CPython+numpy and Monty. Bugs found and fixed: - argmax() returned last max on ties instead of first (numpy returns first) - .T attribute not resolved (single-char "T" uses ASCII interning, not StaticStrings) - Float repr wrote "1.0" instead of numpy's "1." - len() on 2D arrays returned total elements instead of shape[0]
6bfc6ce to
529777b
Compare
… functions - Pre-check allocation size in np.zeros, np.ones, np.arange, np.linspace before allocating Vec to prevent memory exhaustion from user-controlled sizes - Accept plain list arguments in call_elementwise (np.abs, np.sqrt, etc.) matching real NumPy behavior - Improve reshape error message to include actual size and requested shape - Add tests for elementwise functions on plain lists
- Fix `~` on int arrays to use bitwise NOT (e.g. ~1 = -2) instead of logical NOT - Fix `~` on float arrays to raise TypeError, matching NumPy - Fix `~` on bool arrays to correctly flip True/False with Bool dtype - Track bool vs int dtype in array creation so np.array([True, False]) gets dtype='bool' instead of 'int64' - Validate that np.where x/y array lengths match condition length, raising ValueError on mismatch instead of silently producing inconsistent arrays
NaN/Inf correctness: - Fix min()/max() to propagate NaN (was silently ignoring NaN values) - Fix sort/argsort/unique to put NaN values last, matching NumPy - Fix float repr to use lowercase 'nan'/'inf' instead of Rust's 'NaN'/'inf' Empty array handling: - Fix empty np.array([]) to default to float64 dtype (was int64) 2D array correctness: - Fix tolist() to produce nested lists for 2D+ arrays (was flattening) Dtype promotion: - Track scalar_is_float through binary operations so int_arr * 1.0 correctly promotes to float64 (was staying int64 because 1.0.fract() == 0) - Pass is_float flag from Value::Float through value_to_f64() and all scalar operation dispatch Resource safety: - Add check_array_alloc_size() to np.concatenate (was unchecked) - Use checked_mul in reshape to prevent usize overflow from user input Tests: - Add ~60 new assertions for NaN/Inf, empty arrays, dtype, and 2D ops
…overage
Add comprehensive numpy ndarray support covering all operations commonly
generated by LLMs. Implementation spans 8 categories:
Phase 1 - Math: np.sin, np.cos, np.tan, np.log2, np.power, np.diff
Phase 2 - Creation: np.full, np.eye, np.copy, np.empty, np.zeros((m,n)),
np.ones((m,n)), np.zeros_like, np.ones_like
Phase 3 - Testing: np.isnan, np.isinf, np.isfinite, np.array_equal,
np.count_nonzero, np.all, np.any (module-level)
Phase 4 - Aggregation: .prod(), np.prod, .var(), np.var, np.median,
np.argmin, np.argmax (module-level)
Phase 5 - Manipulation: np.reshape, np.transpose (module-level),
np.append, np.vstack, np.hstack, np.stack, .ravel()
Phase 6 - Indexing: np.nonzero, np.argwhere, fancy indexing with
integer arrays, slice indexing (arr[1:3], arr[::2], arr[::-1])
Phase 7 - Utilities: np.tile, np.repeat, np.split,
.astype("int32"/"float32"/"int"/"float")
Phase 8 - Validation: 560 assertions verified against NumPy 2.x,
edge cases for empty arrays, NaN/Inf, single elements
All 918 integration tests pass, clippy clean, ref-count-panic clean.
- Validate negative arguments for np.linspace (num), np.tile (reps), np.repeat (repeats), and np.split (sections) — previously these would silently wrap to huge usize values via cast_sign_loss - Fix np.split error message for sections=0 to match NumPy's wording - Use defer_drop! for reps_val in tile/repeat for cleaner ref counting - Update module-level docstring to list all ~40 supported functions - Add doc comments about np.stack/np.hstack 1D-only limitation - Add doc comment about np.array_equal NaN behavior - Add comment about ref-count leak acceptability in call_split (resource exhaustion is terminal per project convention) - Replace temporal "New functions" section comment with descriptive label
Phase 1: Constants (np.pi, np.e, np.inf, np.nan, np.newaxis) and dtype type objects (np.float64, np.int64, np.bool_, np.float32, np.int32). Phase 3: Inverse trig (arcsin, arccos, arctan, arctan2), hyperbolic (sinh, cosh, tanh, arcsinh, arccosh, arctanh), and remaining element-wise math (sign, square, cbrt, reciprocal, log1p, exp2, expm1, deg2rad, rad2deg, degrees, radians, hypot, nan_to_num, fmin, fmax, fmod, rint, fabs, positive, negative). Phase 4: NaN-aware aggregations (nansum, nanmean, nanmin, nanmax, nanstd, nanvar, nanprod, nanmedian, nanargmin, nanargmax, nancumsum, nancumprod) and statistics (ptp, cumprod, percentile, quantile, average). Phase 5: Logical functions (logical_and, logical_or, logical_not, logical_xor, allclose, isclose, isin). Phase 6: Array manipulation (flip, fliplr, flipud, roll, expand_dims, squeeze, ravel, delete, insert, diag, diagonal, trace, flatnonzero, asarray, column_stack, array_split, full_like, empty_like). Phase 7: Sorting/searching/set ops (argsort, searchsorted, extract, intersect1d, union1d, setdiff1d, setxor1d, bincount, digitize). Phase 8: Linear algebra (outer, cross). Phase 9-10: Creation functions (logspace, geomspace, tri, tril, triu, identity, meshgrid, gradient, convolve, correlate, interp, select). Also fixes ref-counting bugs in functions using into_pos_only + .next() pattern, and fixes np.sign to return 0.0 for zero (matching NumPy). Test assertions: 560 → 735 (+175 new).
… ops, matmul Implements the most critical missing operators for ndarray parity: - Bitwise operators (&, |, ^) on bool and int arrays - __setitem__ with int index, bool mask, and slice assignment - __iter__ for iterating over array elements in for loops - __contains__ for 'val in arr' membership testing - In-place operators (+=, -=, *=, /=) for scalar and array operands - @ (matmul) operator and np.matmul function for dot/matrix products Updates the matmul test expectation from NotImplementedError to TypeError since @ is now implemented for ndarray but not for plain int/float. Test assertions: 735 → 779 (+44 new).
… etc.) and attributes (nbytes, itemsize) Adds missing ndarray methods and attributes: - .item() - extract single element as scalar - .cumprod() - cumulative product - .squeeze() - remove size-1 dimensions - .take(indices) - take elements at given indices - .diagonal() - extract diagonal of 2D array - .trace() - sum of diagonal elements - .fill(value) - fill array in-place - .compress(condition) - select elements by bool mask - .swapaxes(a, b) - swap two axes - .nbytes - total bytes (size * 8) - .itemsize - bytes per element (8) Test assertions: 779 → 803 (+24 new).
…+ test assertions - Fix ndarray.sort() to mutate in-place and return None (was creating new array) - Fix slice setitem to handle array RHS values, not just scalars - Add comprehensive test assertions (total now 1002) covering: - In-place sort, setitem with slices/masks/arrays - ndarray methods: item, cumprod, squeeze, take, diagonal, trace, fill, compress, swapaxes - ndarray attributes: nbytes, itemsize - Additional edge cases for existing functions
…nzero() methods - Add FloorDiv, Mod, Pow variants to NdArrayInplaceOp for true in-place mutation - Wire InplaceFloorDiv/InplaceMod/InplacePow to new inplace methods - Add .flat attribute returning flattened 1D copy of the array - Add ndarray.repeat(n) method for element-wise repetition - Add ndarray.nonzero() method returning tuple of index arrays - Total assertions: 1014
# Conflicts: # crates/monty-datatest/src/main.rs # crates/monty/src/bytecode/vm/binary.rs # crates/monty/src/bytecode/vm/compare.rs # crates/monty/src/bytecode/vm/mod.rs # crates/monty/src/heap.rs # crates/monty/src/heap_data.rs # crates/monty/src/intern.rs # crates/monty/src/modules/mod.rs # crates/monty/src/object.rs
There was a problem hiding this comment.
16 issues found across 29 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/monty/test_cases/numpy__arithmetic.py">
<violation number="1" location="crates/monty/test_cases/numpy__arithmetic.py:75">
P2: The test claims to cover in-place addition, but it uses normal addition and reassignment instead, so `__iadd__`/mutation semantics are never tested.</violation>
</file>
<file name="crates/monty/src/types/iter.rs">
<violation number="1" location="crates/monty/src/types/iter.rs:602">
P2: 0-D ndarrays are treated as empty iterables instead of raising `TypeError`, which diverges from NumPy/Python iteration semantics.</violation>
</file>
<file name="crates/monty/src/modules/mod.rs">
<violation number="1" location="crates/monty/src/modules/mod.rs:53">
P2: Adding `Numpy` here renumbers `StandardLib`'s `u8` discriminants, which breaks the bytecode/module-ID ABI for previously compiled or serialized code.</violation>
</file>
<file name="crates/monty/test_cases/numpy__methods.py">
<violation number="1" location="crates/monty/test_cases/numpy__methods.py:62">
P3: `np.abs` on a list of ints should not produce floats; this expected value conflicts with NumPy semantics and will mis-test correct behavior.</violation>
</file>
<file name="crates/monty/src/types/ndarray.rs">
<violation number="1" location="crates/monty/src/types/ndarray.rs:222">
P2: Broadcast allocation accounting omits the result vector that callers allocate after broadcasting, so large broadcasted ops can pass the precheck even when peak memory is ~3x the element count.</violation>
<violation number="2" location="crates/monty/src/types/ndarray.rs:426">
P1: Large ndarray arithmetic results are materialized without a pre-allocation resource check, so broadcasting/scalar ops can allocate untracked Rust Vecs beyond the intended sandbox limits.</violation>
<violation number="3" location="crates/monty/src/types/ndarray.rs:1331">
P1: repeat_array allocates and fills the repeated Vec before checking resource limits, so large inputs can OOM or overflow before enforcement.</violation>
<violation number="4" location="crates/monty/src/types/ndarray.rs:1768">
P2: Fancy indexing accepts float ndarrays via truncation and indexes flattened storage instead of NumPy-style advanced indexing.</violation>
<violation number="5" location="crates/monty/src/types/ndarray.rs:1855">
P1: ndarray slice assignment from another ndarray silently pads/truncates instead of validating the RHS length, which can corrupt data without raising an error.</violation>
<violation number="6" location="crates/monty/src/types/ndarray.rs:2260">
P2: Unsupported numeric arguments are silently coerced to `0.0` instead of raising a type error, so invalid inputs can be accepted and produce incorrect ndarray results.</violation>
</file>
<file name="crates/monty/src/bytecode/vm/binary.rs">
<violation number="1" location="crates/monty/src/bytecode/vm/binary.rs:886">
P2: In-place ndarray ops only compare flat data length, so arrays with different shapes but the same element count are incorrectly treated as compatible and updated elementwise in storage order.</violation>
<violation number="2" location="crates/monty/src/bytecode/vm/binary.rs:917">
P2: In-place ndarray ops mutate element storage without promoting/casting `dtype`, so arrays can hold fractional data while still reporting an integer/bool dtype.</violation>
</file>
<file name="crates/monty/test_cases/numpy__aliases.py">
<violation number="1" location="crates/monty/test_cases/numpy__aliases.py:388">
P2: The test over-asserts NumPy partition behavior by requiring a fully sorted result for `kth=-1`, but partition only guarantees the kth element's position and leaves the rest unordered.</violation>
</file>
<file name="crates/monty/src/types/type.rs">
<violation number="1" location="crates/monty/src/types/type.rs:118">
P2: `Type::NdArray` formats as `ndarray` instead of the documented `numpy.ndarray`.</violation>
</file>
<file name="crates/monty-datatest/src/main.rs">
<violation number="1" location="crates/monty-datatest/src/main.rs:182">
P1: `skip-cpython-windows` is accidentally parsed as `skip-cpython`, causing CPython tests to be skipped on all platforms.</violation>
</file>
<file name="crates/monty/src/value.rs">
<violation number="1" location="crates/monty/src/value.rs:1652">
P2: NdArray membership only accepts Int/Float/Bool needles and returns false for numeric LongInt values, causing false negatives for numerically equal inputs.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
2 issues found across 16 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/monty/test_cases/numpy__parity.py">
<violation number="1" location="crates/monty/test_cases/numpy__parity.py:2079">
P1: These `+=` tests expect int arrays to upcast to float64, but NumPy in-place ufuncs preserve the original dtype and raise on float RHS instead.</violation>
</file>
<file name="crates/monty/src/bytecode/vm/binary.rs">
<violation number="1" location="crates/monty/src/bytecode/vm/binary.rs:1012">
P2: In-place pow still leaves integer dtype unchanged for negative integer exponents, so fractional f64 results can be stored under Int64 and later truncated on readback.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
09ca97e to
31be82e
Compare
|
@samuelcolvin @davidhewitt, I think this PR is now at the point where I should stop expanding it and ask for project-owner direction on what you want next. Since the earlier review discussion, I have reshaped this to the scope Sam suggested: built-in NumPy support only, no pandas. The current PR is intended to be a constrained Monty-native NumPy subset, not a general third-party package mechanism and not a bridge to CPython's C-backed NumPy. Current state:
My opinion: this is the right place to freeze the PR's scope. I do not think it is wise to keep adding more NumPy surface area here. If the project is comfortable owning a constrained built-in NumPy runtime, I think this is now a reasonable "NumPy core support" PR. Further work should be separate PRs, probably in this order:
What I need from you at this stage:
I am intentionally pausing feature expansion until I know which direction you want. Thank you |
Summary
Adds built-in NumPy support to Monty for sandbox-safe numeric array workloads.
This is implemented inside Monty's runtime, so user code can run ordinary
import numpy as npsnippets without loading CPython's C-backed NumPy package, exposing host filesystem/network access, or adding general third-party package support. The PR closes the source-derived pure-core backlog without adding host-boundary APIs, unsafe memory access, or out-of-scope Python modules.This PR now covers:
NdArrayruntime support with shape, dtype, indexing, conversion, display, arithmetic, comparison, aggregation, manipulation, selection, and iterator-style helpers.broadcast_shapes,broadcast_to,broadcast_arrays, and a sandbox-safe materializedbroadcastsubset.np.ufunctype compatibility for implemented ufunc-like callables.astype, scalar float formatting, and publicnp.ndarray/np.dtypecompatibility.generic,number,signedinteger,unsignedinteger,complexfloating,flexible,character), metadata-only dtype/scalar names (complex64,complex128,cdouble,csingle,clongdouble,str_,bytes_,void,object_,datetime64,timedelta64,ScalarType), and pure numeric index-trick helpers (index_exp,s_,mgrid,ogrid,r_,c_).block,tensordot,einsum,einsum_path,apply_along_axis,apply_over_axes,piecewise, andpad.ndarray.flatnow has a distinctnp.flatitertype marker while remaining backed by Monty's existing materialized 1-D ndarray behavior.NumPy Surface Audit
Local audit against NumPy 2.4.2 on PR head
ec5da8b:That means the audit has no remaining names in the categories we can honestly implement inside Monty's current safe core runtime. Remaining public NumPy names are excluded because they require one of:
Performance
The first CodSpeed regression was traced to fixed parser startup overhead from probing the enlarged
StaticStringstable for arbitrary filename labels, not to NumPy execution itself. Commit66e1466routes filename labels through dynamic-only interning while preserving static interning for identifiers, attributes, imports, and literals.CodSpeed passed on pushed head
1f7d54d, then failed on5b155a9with a regression in non-NumPy hot-path benchmarks (dict_comp__monty,fib__monty, andlist_comp__monty). Commit7c0d15eremoves the newValue::FlatIterenum arm from the hot value path and representsndarray.flatas a markedNdArrayinstead.Code head
7c0d15epassed the CodSpeed performance gate againstmain(ffd29f0): 17 benchmarks unchanged and 15 skipped benchmarks using baseline results. Heads331da18and3e7447bwere green on GitHub CI/CodSpeed, including Cubic. Current headec5da8bis green on GitHub CI/CodSpeed, including Cubic.Latest Commits
ec5da8bImplement NumPy metadata dtype markers3e7447bFix NumPy ndarray type displaycd7bcb7Implement NumPy compatibility markers331da18Document built-in NumPy support7c0d15eKeep flatiter off the Value hot path5b155a9Complete NumPy core runtime coverageTest Plan
Local verification completed on head
ec5da8b:make format-rsRUSTUP_TOOLCHAIN=nightly make lint-rsuvx ruff==0.14.7 format --checkuvx ruff==0.14.7 checkRUSTUP_TOOLCHAIN=nightly cargo run -p monty-datatest --features memory-model-checks numpy__aliasesRUSTUP_TOOLCHAIN=nightly cargo run -p monty-datatest --features memory-model-checks numpy__parityRUSTUP_TOOLCHAIN=nightly RUST_TEST_THREADS=1 make test-cases(954 passed)python3 playground/numpy_surface_audit.pygit diff --checkReview feedback:
3e7447bwhile preservingnp.ndarray.__name__ == "ndarray".GitHub verification:
1f7d54d: required checks green, includingCodSpeed Performance Analysis.5b155a9: functionally green locally, butCodSpeed Performance Analysisfailed with a non-NumPy hot-path regression.7c0d15e:CodSpeed Performance Analysispassed againstmain(ffd29f0) and GitHub CI checks were green.331da18: GitHub CI/CodSpeed checks were green.3e7447b: GitHub CI/CodSpeed checks green.ec5da8b: GitHub CI/CodSpeed checks green.Working Document