Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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 AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ kyle-compute <[email protected]>
DongKwanKho <[email protected]>
Ikem Peter <[email protected]>
Josh Kelley <[email protected]>
Nils Dietrich <[email protected]>
Richard Taylor <[email protected]>

# Generated by tools/update-authors.js
7 changes: 7 additions & 0 deletions docs/expressions/expression_trees.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,20 @@ Construction:

```
new AccessorNode(object: Node, index: IndexNode)
new AccessorNode(object: Node, index: IndexNode, optionalChaining: boolean)
```

An optional property `optionalChaining` can be provided whether the accessor was
written as optional-chaining using `a?.b`, or `a?.["b"]` with bracket notation.
Default value is `false`. Forces evaluate to undefined if the given object is
undefined or null.

Properties:

- `object: Node`
- `index: IndexNode`
- `name: string` (read-only) The function or method name. Returns an empty string when undefined.
- `optionalChaining: boolean`

Examples:

Expand Down
97 changes: 49 additions & 48 deletions docs/expressions/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,54 +55,55 @@ interchangeably. For example, `x+y` will always evaluate identically to
Functions below.


Operator | Name | Syntax | Associativity | Example | Result
----------- | -------------------------- | ---------- | ------------- | --------------------- | ---------------
`(`, `)` | Grouping | `(x)` | None | `2 * (3 + 4)` | `14`
`[`, `]` | Matrix, Index | `[...]` | None | `[[1,2],[3,4]]` | `[[1,2],[3,4]]`
`{`, `}` | Object | `{...}` | None | `{a: 1, b: 2}` | `{a: 1, b: 2}`
`,` | Parameter separator | `x, y` | Left to right | `max(2, 1, 5)` | `5`
`.` | Property accessor | `obj.prop` | Left to right | `obj={a: 12}; obj.a` | `12`
`;` | Statement separator | `x; y` | Left to right | `a=2; b=3; a*b` | `[6]`
`;` | Row separator | `[x; y]` | Left to right | `[1,2;3,4]` | `[[1,2],[3,4]]`
`\n` | Statement separator | `x \n y` | Left to right | `a=2 \n b=3 \n a*b` | `[2,3,6]`
`+` | Add | `x + y` | Left to right | `4 + 5` | `9`
`+` | Unary plus | `+y` | Right to left | `+4` | `4`
`-` | Subtract | `x - y` | Left to right | `7 - 3` | `4`
`-` | Unary minus | `-y` | Right to left | `-4` | `-4`
`*` | Multiply | `x * y` | Left to right | `2 * 3` | `6`
`.*` | Element-wise multiply | `x .* y` | Left to right | `[1,2,3] .* [1,2,3]` | `[1,4,9]`
`/` | Divide | `x / y` | Left to right | `6 / 2` | `3`
`./` | Element-wise divide | `x ./ y` | Left to right | `[9,6,4] ./ [3,2,2]` | `[3,3,2]`
`%` | Percentage | `x%` | None | `8%` | `0.08`
`%` | Addition with Percentage | `x + y%` | Left to right | `100 + 3%` | `103`
`%` | Subtraction with Percentage| `x - y%` | Left to right | `100 - 3%` | `97`
`%` `mod` | Modulus | `x % y` | Left to right | `8 % 3` | `2`
`^` | Power | `x ^ y` | Right to left | `2 ^ 3` | `8`
`.^` | Element-wise power | `x .^ y` | Right to left | `[2,3] .^ [3,3]` | `[8,27]`
`'` | Transpose | `y'` | Left to right | `[[1,2],[3,4]]'` | `[[1,3],[2,4]]`
`!` | Factorial | `y!` | Left to right | `5!` | `120`
`&` | Bitwise and | `x & y` | Left to right | `5 & 3` | `1`
`~` | Bitwise not | `~x` | Right to left | `~2` | `-3`
<code>&#124;</code> | Bitwise or | <code>x &#124; y</code> | Left to right | <code>5 &#124; 3</code> | `7`
<code>^&#124;</code> | Bitwise xor | <code>x ^&#124; y</code> | Left to right | <code>5 ^&#124; 2</code> | `7`
`<<` | Left shift | `x << y` | Left to right | `4 << 1` | `8`
`>>` | Right arithmetic shift | `x >> y` | Left to right | `8 >> 1` | `4`
`>>>` | Right logical shift | `x >>> y` | Left to right | `-8 >>> 1` | `2147483644`
`and` | Logical and | `x and y` | Left to right | `true and false` | `false`
`not` | Logical not | `not y` | Right to left | `not true` | `false`
`or` | Logical or | `x or y` | Left to right | `true or false` | `true`
`xor` | Logical xor | `x xor y` | Left to right | `true xor true` | `false`
`=` | Assignment | `x = y` | Right to left | `a = 5` | `5`
`?` `:` | Conditional expression | `x ? y : z` | Right to left | `15 > 100 ? 1 : -1` | `-1`
`??` | Nullish coalescing | `x ?? y` | Left to right | `null ?? 2` | `2`
`:` | Range | `x : y` | Right to left | `1:4` | `[1,2,3,4]`
`to`, `in` | Unit conversion | `x to y` | Left to right | `2 inch to cm` | `5.08 cm`
`==` | Equal | `x == y` | Left to right | `2 == 4 - 2` | `true`
`!=` | Unequal | `x != y` | Left to right | `2 != 3` | `true`
`<` | Smaller | `x < y` | Left to right | `2 < 3` | `true`
`>` | Larger | `x > y` | Left to right | `2 > 3` | `false`
`<=` | Smallereq | `x <= y` | Left to right | `4 <= 3` | `false`
`>=` | Largereq | `x >= y` | Left to right | `2 + 4 >= 6` | `true`
Operator | Name | Syntax | Associativity | Example | Result
----------- |-----------------------------|-------------| ------------- |-----------------------| ---------------
`(`, `)` | Grouping | `(x)` | None | `2 * (3 + 4)` | `14`
`[`, `]` | Matrix, Index | `[...]` | None | `[[1,2],[3,4]]` | `[[1,2],[3,4]]`
`{`, `}` | Object | `{...}` | None | `{a: 1, b: 2}` | `{a: 1, b: 2}`
`,` | Parameter separator | `x, y` | Left to right | `max(2, 1, 5)` | `5`
`.` | Property accessor | `obj.prop` | Left to right | `obj={a: 12}; obj.a` | `12`
`;` | Statement separator | `x; y` | Left to right | `a=2; b=3; a*b` | `[6]`
`;` | Row separator | `[x; y]` | Left to right | `[1,2;3,4]` | `[[1,2],[3,4]]`
`\n` | Statement separator | `x \n y` | Left to right | `a=2 \n b=3 \n a*b` | `[2,3,6]`
`+` | Add | `x + y` | Left to right | `4 + 5` | `9`
`+` | Unary plus | `+y` | Right to left | `+4` | `4`
`-` | Subtract | `x - y` | Left to right | `7 - 3` | `4`
`-` | Unary minus | `-y` | Right to left | `-4` | `-4`
`*` | Multiply | `x * y` | Left to right | `2 * 3` | `6`
`.*` | Element-wise multiply | `x .* y` | Left to right | `[1,2,3] .* [1,2,3]` | `[1,4,9]`
`/` | Divide | `x / y` | Left to right | `6 / 2` | `3`
`./` | Element-wise divide | `x ./ y` | Left to right | `[9,6,4] ./ [3,2,2]` | `[3,3,2]`
`%` | Percentage | `x%` | None | `8%` | `0.08`
`%` | Addition with Percentage | `x + y%` | Left to right | `100 + 3%` | `103`
`%` | Subtraction with Percentage | `x - y%` | Left to right | `100 - 3%` | `97`
`%` `mod` | Modulus | `x % y` | Left to right | `8 % 3` | `2`
`^` | Power | `x ^ y` | Right to left | `2 ^ 3` | `8`
`.^` | Element-wise power | `x .^ y` | Right to left | `[2,3] .^ [3,3]` | `[8,27]`
`'` | Transpose | `y'` | Left to right | `[[1,2],[3,4]]'` | `[[1,3],[2,4]]`
`!` | Factorial | `y!` | Left to right | `5!` | `120`
`&` | Bitwise and | `x & y` | Left to right | `5 & 3` | `1`
`~` | Bitwise not | `~x` | Right to left | `~2` | `-3`
<code>&#124;</code> | Bitwise or | <code>x &#124; y</code> | Left to right | <code>5 &#124; 3</code> | `7`
<code>^&#124;</code> | Bitwise xor | <code>x ^&#124; y</code> | Left to right | <code>5 ^&#124; 2</code> | `7`
`<<` | Left shift | `x << y` | Left to right | `4 << 1` | `8`
`>>` | Right arithmetic shift | `x >> y` | Left to right | `8 >> 1` | `4`
`>>>` | Right logical shift | `x >>> y` | Left to right | `-8 >>> 1` | `2147483644`
`and` | Logical and | `x and y` | Left to right | `true and false` | `false`
`not` | Logical not | `not y` | Right to left | `not true` | `false`
`or` | Logical or | `x or y` | Left to right | `true or false` | `true`
`xor` | Logical xor | `x xor y` | Left to right | `true xor true` | `false`
`=` | Assignment | `x = y` | Right to left | `a = 5` | `5`
`?` `:` | Conditional expression | `x ? y : z` | Right to left | `15 > 100 ? 1 : -1` | `-1`
`??` | Nullish coalescing | `x ?? y` | Left to right | `null ?? 2` | `2`
`?.` | Optional chaining accessor | `obj?.prop` | Left to right | `obj={}; obj?.a` | `undefined`
`:` | Range | `x : y` | Right to left | `1:4` | `[1,2,3,4]`
`to`, `in` | Unit conversion | `x to y` | Left to right | `2 inch to cm` | `5.08 cm`
`==` | Equal | `x == y` | Left to right | `2 == 4 - 2` | `true`
`!=` | Unequal | `x != y` | Left to right | `2 != 3` | `true`
`<` | Smaller | `x < y` | Left to right | `2 < 3` | `true`
`>` | Larger | `x > y` | Left to right | `2 > 3` | `false`
`<=` | Smallereq | `x <= y` | Left to right | `4 <= 3` | `false`
`>=` | Largereq | `x >= y` | Left to right | `2 + 4 >= 6` | `true`


## Precedence
Expand Down
51 changes: 42 additions & 9 deletions src/expression/node/AccessorNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
* @param {Node} object The object from which to retrieve
* a property or subset.
* @param {IndexNode} index IndexNode containing ranges
* @param {boolean} [optionalChaining=false]
* Optional property, if the accessor was written as optional-chaining
* using `a?.b`, or `a?.["b"] with bracket notation.
* Forces evaluate to undefined if the given object is undefined or null.
*/
constructor (object, index) {
constructor (object, index, optionalChaining = false) {
super()
if (!isNode(object)) {
throw new TypeError('Node expected for parameter "object"')
Expand All @@ -59,6 +63,7 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({

this.object = object
this.index = index
this.optionalChaining = optionalChaining
}

// readonly property name
Expand Down Expand Up @@ -93,15 +98,41 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
const evalObject = this.object._compile(math, argNames)
const evalIndex = this.index._compile(math, argNames)

const optionalChaining = this.optionalChaining
const prevOptionalChaining = isAccessorNode(this.object) && this.object.optionalChaining

if (this.index.isObjectProperty()) {
const prop = this.index.getObjectProperty()
return function evalAccessorNode (scope, args, context) {
const ctx = context || {}
const object = evalObject(scope, args, ctx)

if (optionalChaining && object == null) {
ctx.optionalShortCircuit = true
return undefined
}

if (prevOptionalChaining && ctx?.optionalShortCircuit) {
return undefined
}

// get a property from an object evaluated using the scope.
return getSafeProperty(evalObject(scope, args, context), prop)
return getSafeProperty(object, prop)
}
} else {
return function evalAccessorNode (scope, args, context) {
const object = evalObject(scope, args, context)
const ctx = context || {}
const object = evalObject(scope, args, ctx)

if (optionalChaining && object == null) {
ctx.optionalShortCircuit = true
return undefined
}

if (prevOptionalChaining && ctx?.optionalShortCircuit) {
return undefined
}

// we pass just object here instead of context:
const index = evalIndex(scope, args, object)
return access(object, index)
Expand All @@ -127,7 +158,8 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
map (callback) {
return new AccessorNode(
this._ifNode(callback(this.object, 'object', this)),
this._ifNode(callback(this.index, 'index', this))
this._ifNode(callback(this.index, 'index', this)),
this.optionalChaining
)
}

Expand All @@ -136,7 +168,7 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
* @return {AccessorNode}
*/
clone () {
return new AccessorNode(this.object, this.index)
return new AccessorNode(this.object, this.index, this.optionalChaining)
}

/**
Expand All @@ -149,8 +181,8 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
if (needParenthesis(this.object)) {
object = '(' + object + ')'
}

return object + this.index.toString(options)
const optionalChaining = this.optionalChaining ? (this.index.dotNotation ? '?' : '?.') : ''
return object + optionalChaining + this.index.toString(options)
}

/**
Expand Down Expand Up @@ -192,7 +224,8 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
return {
mathjs: name,
object: this.object,
index: this.index
index: this.index,
optionalChaining: this.optionalChaining
}
}

Expand All @@ -205,7 +238,7 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
* @returns {AccessorNode}
*/
static fromJSON (json) {
return new AccessorNode(json.object, json.index)
return new AccessorNode(json.object, json.index, json.optionalChaining)
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/expression/node/FunctionNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({
_compile (math, argNames) {
// compile arguments
const evalArgs = this.args.map((arg) => arg._compile(math, argNames))
const fromOptionalChaining = isAccessorNode(this.fn) && this.fn.optionalChaining

if (isSymbolNode(this.fn)) {
const name = this.fn.name
Expand Down Expand Up @@ -242,6 +243,12 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({

return function evalFunctionNode (scope, args, context) {
const object = evalObject(scope, args, context)

// Optional chaining: if the base object is nullish, short-circuit to undefined
if (fromOptionalChaining && object == null) {
return undefined
}

const fn = getSafeMethod(object, prop)

if (fn?.rawArgs) {
Expand Down
Loading