Skip to content

Commit 91057df

Browse files
committed
feat: (x, y, z,...) notation for lists (= Arrays); subset transform tests
1 parent 9caf829 commit 91057df

File tree

9 files changed

+84
-29
lines changed

9 files changed

+84
-29
lines changed

docs/expressions/syntax.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ the lower level syntax of math.js. Differences are:
1414

1515
- No need to prefix functions and constants with the `math.*` namespace,
1616
you can just enter `sin(pi / 4)`.
17-
- Matrix indexes are one-based instead of zero-based.
17+
- By default, bracket notation `[1, 2, 3]` produces a Matrix object rather
18+
than an Array; and an Array can be written with list notation, as `(1, 2, 3)`.
19+
- Matrix and Array indexes are one-based instead of zero-based.
1820
- There are index and range operators which allow more conveniently getting
1921
and setting matrix indexes, like `A[2:4, 1]`.
2022
- Both indexes and ranges and have the upper-bound included.
2123
- There is a differing syntax for defining functions. Example: `f(x) = x^2`.
2224
- There are custom operators like `x + y` instead of `add(x, y)`.
23-
- Some operators are different. For example `^` is used for exponentiation,
25+
- Some operators are different. For example, `^` is used for exponentiation,
2426
not bitwise xor.
2527
- Implicit multiplication, like `2 pi`, is supported and has special rules.
2628
- Relational operators (`<`, `>`, `<=`, `>=`, `==`, and `!=`) are chained, so the expression `5 < x < 10` is equivalent to `5 < x and x < 10`.
@@ -58,7 +60,8 @@ Functions below.
5860
Operator | Name | Syntax | Associativity | Example | Result
5961
----------- |-----------------------------|-------------| ------------- |-----------------------| ---------------
6062
`(`, `)` | Grouping | `(x)` | None | `2 * (3 + 4)` | `14`
61-
`[`, `]` | Matrix, Index | `[...]` | None | `[[1,2],[3,4]]` | `[[1,2],[3,4]]`
63+
| Array, function arguments | `(x, y,...)`| None | `((), (1,), (1,2))` | `[[], [1], [1,2]]`
64+
`[`, `]` | Matrix, Index | `[...]` | None | `[[1,2],[3,4]]` | `matrix([[1,2],[3,4]])`
6265
`{`, `}` | Object | `{...}` | None | `{a: 1, b: 2}` | `{a: 1, b: 2}`
6366
`,` | Parameter separator | `x, y` | Left to right | `max(2, 1, 5)` | `5`
6467
`.` | Property accessor | `obj.prop` | Left to right | `obj={a: 12}; obj.a` | `12`
@@ -112,7 +115,7 @@ The operators have the following precedence, from highest to lowest:
112115

113116
Operators | Description
114117
--------------------------------- | --------------------
115-
`(...)`<br>`[...]`<br>`{...}` | Grouping<br>Matrix<br>Object
118+
`(...)`<br>`[...]`<br>`{...}` | Grouping/Array<br>Matrix<br>Object
116119
`x(...)`<br>`x[...]`<br>`obj.prop`<br>`:`| Function call<br>Matrix index<br>Property accessor<br>Key/value separator
117120
`'` | Matrix transpose
118121
`!` | Factorial

src/expression/node/ArrayNode.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ export const createArrayNode = /* #__PURE__ */ factory(name, dependencies, ({ No
1313
* @constructor ArrayNode
1414
* @extends {Node}
1515
* Holds an 1-dimensional array with items
16-
* @param {Node[]} [items] 1 dimensional array with items
16+
* @param {Node[]} [items] 1 dimensional array with items
17+
* @param {boolean} [forceArray]
18+
* Should the result always be Array regardless of config? (default
19+
* is false)
1720
*/
18-
constructor (items) {
21+
constructor (items, forceArray = false) {
1922
super()
2023
this.items = items || []
24+
this.forceArray = forceArray
2125

2226
// validate input
2327
if (!Array.isArray(this.items) || !this.items.every(isNode)) {
@@ -47,7 +51,7 @@ export const createArrayNode = /* #__PURE__ */ factory(name, dependencies, ({ No
4751
return item._compile(math, argNames)
4852
})
4953

50-
const asMatrix = (math.config.matrix !== 'Array')
54+
const asMatrix = !this.forceArray && (math.config.matrix !== 'Array')
5155
if (asMatrix) {
5256
const matrix = math.matrix
5357
return function evalArrayNode (scope, args, context) {

src/expression/parse.js

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,28 +1745,37 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
17451745
* @private
17461746
*/
17471747
function parseParentheses (state) {
1748-
let node
1749-
17501748
// check if it is a parenthesized expression
1751-
if (state.token === '(') {
1752-
// parentheses (...)
1753-
openParams(state)
1754-
getToken(state)
1755-
1756-
node = parseAssignment(state) // start again
1749+
if (state.token !== '(') return parseEnd(state)
1750+
// Yes, we have parentheses (...)
1751+
openParams(state)
1752+
getToken(state)
17571753

1758-
if (state.token !== ')') {
1759-
throw createSyntaxError(state, 'Parenthesis ) expected')
1760-
}
1754+
if (state.token === ')') { // `()` is empty array
17611755
closeParams(state)
17621756
getToken(state)
1757+
return parseAccessors(state, new ArrayNode([], true))
1758+
}
17631759

1764-
node = new ParenthesisNode(node)
1765-
node = parseAccessors(state, node)
1766-
return node
1760+
let node = parseAssignment(state) // start again
1761+
1762+
if (state.token === ',') { // Array notation
1763+
const items = [node]
1764+
do {
1765+
getToken(state)
1766+
if (state.token === ')') break
1767+
items.push(parseAssignment(state))
1768+
} while (state.token === ',')
1769+
node = new ArrayNode(items, true)
1770+
} else node = new ParenthesisNode(node)
1771+
1772+
if (state.token !== ')') {
1773+
throw createSyntaxError(state, 'Parenthesis ) expected')
17671774
}
1775+
closeParams(state)
1776+
getToken(state)
17681777

1769-
return parseEnd(state)
1778+
return parseAccessors(state, node)
17701779
}
17711780

17721781
/**

src/expression/transform/subset.transform.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const createSubsetTransform = /* #__PURE__ */ factory(
3333
// supplied an array instead of an index for the 2nd argument, so
3434
// have to turn that into an Index with indexTransform rather than
3535
// just plain index, as subset will if we just let it have at it.
36-
args[1] = indexTransform.apply(null, args)
36+
args[1] = indexTransform.apply(null, args[1])
3737
}
3838
return subset.apply(null, args)
3939
} catch (err) {

src/type/matrix/Matrix.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export const createMatrixClass = /* #__PURE__ */ factory(name, dependencies, ()
146146
if (fields.start === '') fields.start = index.shiftPosition
147147
if (fields.end === '') fields.end = size[dim] + index.shiftPosition - 1
148148
fields.start -= index.shiftPosition
149-
fields.end -= index
149+
fields.end -= index.shiftPosition
150150
const attributes = index.includeEnd
151151
? { start: fields.start, last: fields.end, step: fields.step }
152152
: fields

src/type/matrix/Range.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,9 @@ export const createRangeClass = /* #__PURE__ */ factory(name, dependencies, ({
729729
callback, skipZeros = false, isUnary = false
730730
) {
731731
if (!Number.isFinite(this.length)) {
732-
throw new Error('Attempt to infinite loop')
732+
throw new Error(
733+
'Attempt to infinite loop Range with ' +
734+
`start=${this.start} step=${this.step}`)
733735
}
734736
const array = []
735737
let x = this.start

test/unit-tests/expression/parse.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,19 @@ describe('parse', function () {
890890
const scope = {}
891891
assert.throws(function () { parseAndEval('c=concat(a, [1,2,3])', scope) })
892892
})
893+
894+
it(
895+
'should interpret comma-separated expressions in parentheses as arrays',
896+
function () {
897+
assert.deepStrictEqual(parseAndEval('(3,4,5)'), [3, 4, 5])
898+
assert.deepStrictEqual(parseAndEval('(5,12,13,)'), [5, 12, 13])
899+
assert.deepStrictEqual(parseAndEval('(5,)'), [5])
900+
assert.deepStrictEqual(parseAndEval('()'), [])
901+
assert.strictEqual(parseAndEval('(7,24,25)[2]'), 24)
902+
assert.deepStrictEqual(parseAndEval('size((8, 15, 17))'), [3])
903+
assert.deepStrictEqual(
904+
parseAndEval('((),(0,),(0,1))'), [[], [0], [0, 1]])
905+
})
893906
})
894907

895908
describe('objects', function () {

test/unit-tests/expression/security.test.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,17 @@ describe('security', function () {
248248
it('should not allow replacing validateSafeMethod with a local variant', function () {
249249
assert.throws(function () {
250250
math.evaluate("evaluate(\"f(validateSafeMethod)=cos.constructor(\\\"return evaluate\\\")()\")(evaluate(\"f(x,y)=0\"))(\"console.log('hacked...')\")")
251-
}, /SyntaxError: Value expected/)
251+
}, /TypeError: Unexpected type of argument/)
252252
})
253253

254+
it(
255+
'should not allow replacing validateSafeMethod with a local variant (2)',
256+
function () {
257+
assert.throws(function () {
258+
math.evaluate("evaluate(\"f(validateSafeMethod)=cos.constructor(\\\"return evaluate\\\")()\")?.(evaluate(\"f(x,y)=0\"))(\"console.log('hacked...')\")")
259+
}, /No access to method .constructor/)
260+
})
261+
254262
it('should not allow abusing toString', function () {
255263
assert.throws(function () {
256264
math.evaluate("badToString = evaluate(\"f() = 1\"); badReplace = evaluate(\"f(a, b) = \\\"evaluate\\\"\"); badNumber = {toString:badToString, replace:badReplace}; badNode = {\"isNode\": true, \"type\": \"ConstantNode\", \"valueType\": \"number\", \"value\": badNumber}; x = evaluate(\"f(child, path, parent) = badNode\", {badNode:badNode}); parse(\"(1)\").map(x).compile().evaluate()(\"console.log('hacked...')\")")
@@ -260,13 +268,13 @@ describe('security', function () {
260268
it('should not allow creating a bad FunctionAssignmentNode', function () {
261269
assert.throws(function () {
262270
math.evaluate("badNode={isNode:true,type:\"FunctionAssignmentNode\",expr:parse(\"1\"),types:{join:evaluate(\"f(a)=\\\"\\\"\")},params:{\"forEach\":evaluate(\"f(x)=1\"),\"join\":evaluate(\"f(x)=\\\"){return evaluate;}});return fn;})())}});return fn;})());}};//\\\"\")}};parse(\"f()=x\").map(evaluate(\"f(a,b,c)=badNode\",{\"badNode\":badNode})).compile().evaluate()()()(\"console.log('hacked...')\")")
263-
}, /SyntaxError: Value expected/)
271+
}, /TypeError: Callback function must return a Node/)
264272
})
265273

266274
it('should not allow creating a bad OperatorNode (1)', function () {
267275
assert.throws(function () {
268276
math.evaluate("badNode={isNode:true,type:\"FunctionAssignmentNode\",expr:parse(\"1\"),types:{join:evaluate(\"f(a)=\\\"\\\"\")},params:{\"forEach\":evaluate(\"f(x)=1\"),\"join\":evaluate(\"f(x)=\\\"){return evaluate;}});return fn;})())}});return fn;})());}};//\\\"\")}};parse(\"f()=x\").map(evaluate(\"f(a,b,c)=badNode\",{\"badNode\":badNode})).compile().evaluate()()()(\"console.log('hacked...')\")")
269-
}, /SyntaxError: Value expected/)
277+
}, /TypeError: Callback function must return a Node/)
270278
})
271279

272280
it('should not allow creating a bad OperatorNode (2)', function () {
@@ -316,7 +324,7 @@ describe('security', function () {
316324
'evilMath=x.create().done();' +
317325
'evilMath.import({"_compile":f(a,b,c)="evaluate","isNode":f()=true}); ' +
318326
"parse(\"(1)\").map(g(a,b,c)=evilMath.chain()).compile().evaluate()(\"console.log('hacked...')\")")
319-
}, /SyntaxError: Value expected/)
327+
}, /Error: Undefined symbol Chain/)
320328
})
321329

322330
it('should not allow passing a function name containg bad contents', function () {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import assert from 'assert'
2+
import math from '../../../../src/defaultInstance.js'
3+
4+
const ev = math.evaluate
5+
const matrix = math.matrix
6+
const nummat = x => matrix(x, 'dense', 'number')
7+
8+
describe('subset.transform', function () {
9+
it('should obey indexing conventions of expressions', function () {
10+
assert.strictEqual(ev('subset([1,2;3,4], (2, 1))'), 3)
11+
assert.deepStrictEqual(
12+
ev('subset(range(2,10), ([9,7,5],))'), nummat([10, 8, 6]))
13+
assert.deepStrictEqual(
14+
ev("subset([1,2,3;4,5,6], ('1:2', '2:3'))"), matrix([[2, 3], [5, 6]]))
15+
})
16+
})

0 commit comments

Comments
 (0)