Skip to content

Commit 4164134

Browse files
authored
Merge branch 'develop' into faster-map-mulitple
2 parents b3b477d + 25da3c9 commit 4164134

File tree

14 files changed

+597
-7
lines changed

14 files changed

+597
-7
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,5 +275,6 @@ lucaQ <lucaquercetti@gmail.com>
275275
EliaAlesiani <105418798+EliaAlesiani@users.noreply.github.com>
276276
kyle-compute <kyle@intelvis.ai>
277277
DongKwanKho <70864292+dodokw@users.noreply.github.com>
278+
Ikem Peter <ikempeter2020@gmail.com>
278279

279280
# Generated by tools/update-authors.js

HISTORY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# History
22

3+
# unpublished changes since, 14.7.0
4+
5+
- Feat: #3353 support for the nullish coalescing operator `??` in the
6+
expression parser (#3497). Thanks @ikemHood.
7+
38
# 2025-09-05, 14.7.0
49

510
- Feat: faster `DenseMatrix` symbol iterator (#3521). Thanks @dvd101x.

docs/expressions/syntax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ the lower level syntax of math.js. Differences are:
2424
not bitwise xor.
2525
- Implicit multiplication, like `2 pi`, is supported and has special rules.
2626
- Relational operators (`<`, `>`, `<=`, `>=`, `==`, and `!=`) are chained, so the expression `5 < x < 10` is equivalent to `5 < x and x < 10`.
27+
- The precedence of some operators is different; see a complete list at the precedence table below.
2728
- Multi-expression constructs like `a = 1; b = 2; a + b` or
2829
`"a = 1;\n cos(a)\n sin(a)"` (where `\n` denotes newline)
2930
produce a collection ("ResultSet") of values. Those expressions
@@ -53,6 +54,7 @@ interchangeably. For example, `x+y` will always evaluate identically to
5354
`add(x,y)`. For a full list of the equivalences, see the section on
5455
Functions below.
5556

57+
5658
Operator | Name | Syntax | Associativity | Example | Result
5759
----------- | -------------------------- | ---------- | ------------- | --------------------- | ---------------
5860
`(`, `)` | Grouping | `(x)` | None | `2 * (3 + 4)` | `14`
@@ -92,6 +94,7 @@ Operator | Name | Syntax | Associativity | Example
9294
`xor` | Logical xor | `x xor y` | Left to right | `true xor true` | `false`
9395
`=` | Assignment | `x = y` | Right to left | `a = 5` | `5`
9496
`?` `:` | Conditional expression | `x ? y : z` | Right to left | `15 > 100 ? 1 : -1` | `-1`
97+
`??` | Nullish coalescing | `x ?? y` | Left to right | `null ?? 2` | `2`
9598
`:` | Range | `x : y` | Right to left | `1:4` | `[1,2,3,4]`
9699
`to`, `in` | Unit conversion | `x to y` | Left to right | `2 inch to cm` | `5.08 cm`
97100
`==` | Equal | `x == y` | Left to right | `2 == 4 - 2` | `true`
@@ -112,6 +115,7 @@ Operators | Description
112115
`x(...)`<br>`x[...]`<br>`obj.prop`<br>`:`| Function call<br>Matrix index<br>Property accessor<br>Key/value separator
113116
`'` | Matrix transpose
114117
`!` | Factorial
118+
`??` | Nullish coalescing
115119
`^`, `.^` | Exponentiation
116120
`+`, `-`, `~`, `not` | Unary plus, unary minus, bitwise not, logical not
117121
See section below | Implicit multiplication
@@ -221,6 +225,7 @@ Operator Expression | Equivalent Function Expression
221225
`a \| b` |`bitOr(a,b)`
222226
`a ^\| b` |`bitXor(a,b)`
223227
`a & b` |`bitAnd(a,b)`
228+
`a ?? b` |`nullish(a,b)`
224229
`a == b` |`equal(a,b)`
225230
`a != b` |`unequal(a,b)`
226231
`a < b` |`smaller(a,b)`

src/expression/embeddedDocs/embeddedDocs.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ import { distanceDocs } from './function/geometry/distance.js'
112112
import { intersectDocs } from './function/geometry/intersect.js'
113113
import { andDocs } from './function/logical/and.js'
114114
import { notDocs } from './function/logical/not.js'
115+
import { nullishDocs } from './function/logical/nullish.js'
115116
import { orDocs } from './function/logical/or.js'
116117
import { xorDocs } from './function/logical/xor.js'
117118
import { mapSlicesDocs } from './function/matrix/mapSlices.js'
@@ -445,6 +446,7 @@ export const embeddedDocs = {
445446
// functions - logical
446447
and: andDocs,
447448
not: notDocs,
449+
nullish: nullishDocs,
448450
or: orDocs,
449451
xor: xorDocs,
450452

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const nullishDocs = {
2+
name: 'nullish',
3+
category: 'Logical',
4+
syntax: [
5+
'x ?? y',
6+
'nullish(x, y)'
7+
],
8+
description: 'Nullish coalescing operator. Returns the right-hand operand when the left-hand operand is null or undefined, and otherwise returns the left-hand operand.',
9+
examples: [
10+
'null ?? 42',
11+
'undefined ?? 42',
12+
'0 ?? 42',
13+
'false ?? 42',
14+
'null ?? undefined ?? 42'
15+
],
16+
seealso: [
17+
'and', 'or', 'not'
18+
]
19+
}

src/expression/operators.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export const properties = [
3939
associativity: 'left',
4040
associativeWith: []
4141
}
42-
4342
},
4443
{ // logical xor
4544
'OperatorNode:xor': {
@@ -239,6 +238,13 @@ export const properties = [
239238
associativeWith: []
240239
}
241240
},
241+
{ // nullish coalescing
242+
'OperatorNode:nullish': {
243+
op: '??',
244+
associativity: 'left',
245+
associativeWith: []
246+
}
247+
},
242248
{ // factorial
243249
'OperatorNode:factorial': {
244250
op: '!',

src/expression/parse.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
151151
'=': true,
152152
':': true,
153153
'?': true,
154+
'??': true,
154155

155156
'==': true,
156157
'!=': true,
@@ -1201,7 +1202,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
12011202
function parsePow (state) {
12021203
let node, name, fn, params
12031204

1204-
node = parseLeftHandOperators(state)
1205+
node = parseNullishCoalescing(state)
12051206

12061207
if (state.token === '^' || state.token === '.^') {
12071208
name = state.token
@@ -1215,6 +1216,22 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
12151216
return node
12161217
}
12171218

1219+
/**
1220+
* nullish coalescing operator
1221+
* @return {Node} node
1222+
* @private
1223+
*/
1224+
function parseNullishCoalescing (state) {
1225+
let node = parseLeftHandOperators(state)
1226+
1227+
while (state.token === '??') { // eslint-disable-line no-unmodified-loop-condition
1228+
getTokenSkipNewline(state)
1229+
node = new OperatorNode('??', 'nullish', [node, parseLeftHandOperators(state)])
1230+
}
1231+
1232+
return node
1233+
}
1234+
12181235
/**
12191236
* Left hand operators: factorial x!, ctranspose x'
12201237
* @return {Node} node
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createNullish } from '../../function/logical/nullish.js'
2+
import { factory } from '../../utils/factory.js'
3+
import { isCollection } from '../../utils/is.js'
4+
5+
const name = 'nullish'
6+
const dependencies = ['typed', 'matrix', 'size', 'flatten', 'deepEqual']
7+
8+
export const createNullishTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, size, flatten, deepEqual }) => {
9+
const nullish = createNullish({ typed, matrix, size, flatten, deepEqual })
10+
11+
function nullishTransform (args, math, scope) {
12+
const left = args[0].compile().evaluate(scope)
13+
14+
// If left is not a collection and not nullish, short-circuit and return it
15+
if (!isCollection(left) && left != null && left !== undefined) {
16+
return left
17+
}
18+
19+
// Otherwise evaluate right and apply full nullish semantics (incl. element-wise)
20+
const right = args[1].compile().evaluate(scope)
21+
return nullish(left, right)
22+
}
23+
24+
nullishTransform.rawArgs = true
25+
26+
return nullishTransform
27+
}, { isTransformFunction: true })

src/factoriesAny.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export { createConj } from './function/complex/conj.js'
6666
export { createIm } from './function/complex/im.js'
6767
export { createRe } from './function/complex/re.js'
6868
export { createNot } from './function/logical/not.js'
69+
export { createNullish } from './function/logical/nullish.js'
6970
export { createOr } from './function/logical/or.js'
7071
export { createXor } from './function/logical/xor.js'
7172
export { createConcat } from './function/matrix/concat.js'
@@ -364,5 +365,6 @@ export { createVarianceTransform } from './expression/transform/variance.transfo
364365
export { createPrintTransform } from './expression/transform/print.transform.js'
365366
export { createAndTransform } from './expression/transform/and.transform.js'
366367
export { createOrTransform } from './expression/transform/or.transform.js'
368+
export { createNullishTransform } from './expression/transform/nullish.transform.js'
367369
export { createBitAndTransform } from './expression/transform/bitAnd.transform.js'
368370
export { createBitOrTransform } from './expression/transform/bitOr.transform.js'

src/function/logical/nullish.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { factory } from '../../utils/factory.js'
2+
import { createMatAlgo03xDSf } from '../../type/matrix/utils/matAlgo03xDSf.js'
3+
import { createMatAlgo14xDs } from '../../type/matrix/utils/matAlgo14xDs.js'
4+
import { createMatAlgo13xDD } from '../../type/matrix/utils/matAlgo13xDD.js'
5+
import { DimensionError } from '../../error/DimensionError.js'
6+
7+
const name = 'nullish'
8+
const dependencies = ['typed', 'matrix', 'size', 'flatten', 'deepEqual']
9+
10+
export const createNullish = /* #__PURE__ */ factory(
11+
name,
12+
dependencies,
13+
({ typed, matrix, size, flatten, deepEqual }) => {
14+
const matAlgo03xDSf = createMatAlgo03xDSf({ typed })
15+
const matAlgo14xDs = createMatAlgo14xDs({ typed })
16+
const matAlgo13xDD = createMatAlgo13xDD({ typed })
17+
18+
/**
19+
* Nullish coalescing operator (??). Returns the right-hand side operand
20+
* when the left-hand side operand is null or undefined, and otherwise
21+
* returns the left-hand side operand.
22+
*
23+
* For matrices, the function is evaluated element wise.
24+
*
25+
* Syntax:
26+
*
27+
* math.nullish(x, y)
28+
*
29+
* Examples:
30+
*
31+
* math.nullish(null, 42) // returns 42
32+
* math.nullish(undefined, 42) // returns 42
33+
* math.nullish(0, 42) // returns 0
34+
* math.nullish(false, 42) // returns false
35+
* math.nullish('', 42) // returns ''
36+
*
37+
* // Object property access with fallback
38+
* const obj = {foo: 7, bar: 3}
39+
* math.nullish(obj.baz, 0) // returns 0
40+
*
41+
* See also:
42+
*
43+
* and, or, not
44+
*
45+
* @param {*} x First value to check
46+
* @param {*} y Fallback value
47+
* @return {*} Returns y when x is null or undefined, otherwise returns x
48+
*/
49+
50+
return typed(
51+
name,
52+
{
53+
// Scalar and SparseMatrix-first short-circuit handlers
54+
'number|bigint|Complex|BigNumber|Fraction|Unit|string|boolean|SparseMatrix, any': (x, _y) => x,
55+
'null, any': (_x, y) => y,
56+
'undefined, any': (_x, y) => y,
57+
58+
// SparseMatrix-first with collection RHS: enforce exact shape match
59+
'SparseMatrix, Array | Matrix': (x, y) => {
60+
const sx = flatten(size(x).valueOf()) // work around #3529/#3530
61+
const sy = flatten(size(y).valueOf())
62+
if (deepEqual(sx, sy)) return x
63+
throw new DimensionError(sx, sy)
64+
},
65+
66+
// DenseMatrix-first handlers (no broadcasting between collections)
67+
'DenseMatrix, DenseMatrix': typed.referToSelf(self => (x, y) => matAlgo13xDD(x, y, self)),
68+
'DenseMatrix, SparseMatrix': typed.referToSelf(self => (x, y) => matAlgo03xDSf(x, y, self, false)),
69+
'DenseMatrix, Array': typed.referToSelf(self => (x, y) => matAlgo13xDD(x, matrix(y), self)),
70+
'DenseMatrix, any': typed.referToSelf(self => (x, y) => matAlgo14xDs(x, y, self, false)),
71+
72+
// Array-first handlers (bridge via matrix() where needed)
73+
'Array, Array': typed.referToSelf(self => (x, y) => matAlgo13xDD(matrix(x), matrix(y), self).valueOf()),
74+
'Array, DenseMatrix': typed.referToSelf(self => (x, y) => matAlgo13xDD(matrix(x), y, self)),
75+
'Array, SparseMatrix': typed.referToSelf(self => (x, y) => matAlgo03xDSf(matrix(x), y, self, false)),
76+
'Array, any': typed.referToSelf(self => (x, y) => matAlgo14xDs(matrix(x), y, self, false).valueOf())
77+
}
78+
)
79+
}
80+
)

0 commit comments

Comments
 (0)