Skip to content

Commit a30d1f8

Browse files
josdejonggwhitney
andauthored
fix: #2990 DenseMatrix can mutate input arrays (#2991)
* fix: #2990 DenseMatrix can mutate input arrays * chore: simplify internal function `preprocess` * chore: document ugly workaround of using `matrix.subset` to mutate a nested Array * chore: better solution for `assign` * chore: fix linting issue * chore: add a unit test for `multiply` testing whether the operation is immutable * chore: fix linting issue --------- Co-authored-by: Glen Whitney <[email protected]>
1 parent 563ff63 commit a30d1f8

File tree

4 files changed

+64
-11
lines changed

4 files changed

+64
-11
lines changed

src/expression/node/utils/assign.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ export function assignFactory ({ subset, matrix }) {
1919
return function assign (object, index, value) {
2020
try {
2121
if (Array.isArray(object)) {
22-
// we use matrix.subset here instead of the function subset because we must not clone the contents
23-
return matrix(object).subset(index, value).valueOf()
22+
const result = matrix(object).subset(index, value).valueOf()
23+
24+
// shallow copy all (updated) items into the original array
25+
result.forEach((item, index) => {
26+
object[index] = item
27+
})
28+
29+
return object
2430
} else if (object && typeof object.subset === 'function') { // Matrix
2531
return object.subset(index, value)
2632
} else if (typeof object === 'string') {

src/type/matrix/DenseMatrix.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -920,19 +920,18 @@ export const createDenseMatrixClass = /* #__PURE__ */ factory(name, dependencies
920920

921921
/**
922922
* Preprocess data, which can be an Array or DenseMatrix with nested Arrays and
923-
* Matrices. Replaces all nested Matrices with Arrays
923+
* Matrices. Clones all (nested) Arrays, and replaces all nested Matrices with Arrays
924924
* @memberof DenseMatrix
925-
* @param {Array} data
925+
* @param {Array | Matrix} data
926926
* @return {Array} data
927927
*/
928928
function preprocess (data) {
929-
for (let i = 0, ii = data.length; i < ii; i++) {
930-
const elem = data[i]
931-
if (isArray(elem)) {
932-
data[i] = preprocess(elem)
933-
} else if (elem && elem.isMatrix === true) {
934-
data[i] = preprocess(elem.valueOf())
935-
}
929+
if (isMatrix(data)) {
930+
return preprocess(data.valueOf())
931+
}
932+
933+
if (isArray(data)) {
934+
return data.map(preprocess)
936935
}
937936

938937
return data

test/unit-tests/function/arithmetic/multiply.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,26 @@ describe('multiply', function () {
875875
})
876876
})
877877

878+
describe('immutable operations', function () {
879+
it('should not mutate the input (arrays)', function () {
880+
const a = Object.freeze([[1, 2], [3, 4]])
881+
const b = Object.freeze([[5, 6], [7, 8]])
882+
883+
assert.deepStrictEqual(multiply(a, b), [[19, 22], [43, 50]])
884+
assert.deepStrictEqual(a, [[1, 2], [3, 4]])
885+
assert.deepStrictEqual(b, [[5, 6], [7, 8]])
886+
})
887+
888+
it('should not mutate the input (arrays with nested Matrices)', function () {
889+
const a = Object.freeze([math.matrix([1, 2]), math.matrix([3, 4])])
890+
const b = Object.freeze([math.matrix([5, 6]), math.matrix([7, 8])])
891+
892+
assert.deepStrictEqual(multiply(a, b), [[19, 22], [43, 50]])
893+
assert.deepStrictEqual(a, [math.matrix([1, 2]), math.matrix([3, 4])])
894+
assert.deepStrictEqual(b, [math.matrix([5, 6]), math.matrix([7, 8])])
895+
})
896+
})
897+
878898
it('should LaTeX multiply', function () {
879899
const expression = math.parse('multiply(2,3)')
880900
assert.strictEqual(expression.toTex(), '\\left(2\\cdot3\\right)')

test/unit-tests/type/matrix/DenseMatrix.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,34 @@ describe('DenseMatrix', function () {
158158
it('should throw an error when called with invalid datatype', function () {
159159
assert.throws(function () { console.log(new DenseMatrix([], 1)) })
160160
})
161+
162+
it('should not mutate the input data when creating a Matrix (1)', function () {
163+
const data = [[1, 2]]
164+
Object.freeze(data)
165+
166+
const matrix = new DenseMatrix(data) // should not throw "TypeError: Cannot assign to read only property '0' of object '[object Array]'"
167+
assert.deepStrictEqual(matrix.valueOf(), [[1, 2]])
168+
assert.notStrictEqual(matrix.valueOf(), data)
169+
})
170+
171+
it('should not mutate the input data when creating a Matrix (2)', function () {
172+
const nestedMatrix = new DenseMatrix([1, 2])
173+
const data = [nestedMatrix]
174+
175+
const matrix = new DenseMatrix(data)
176+
assert.deepStrictEqual(matrix._data, [[1, 2]])
177+
assert.deepStrictEqual(data, [nestedMatrix]) // should not have replaced the nestedMatrix in data itself
178+
})
179+
180+
it('should not mutate the input data operating on a Matrix', function () {
181+
const data = [[1, 2]]
182+
183+
const matrix = new DenseMatrix(data)
184+
matrix.set([0, 1], 42)
185+
186+
assert.deepStrictEqual(matrix, new DenseMatrix([[1, 42]]))
187+
assert.deepStrictEqual(data, [[1, 2]])
188+
})
161189
})
162190

163191
describe('size', function () {

0 commit comments

Comments
 (0)