From 09224237e25be1d43fbbdcacafec483739642341 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 10 Dec 2025 23:23:09 -0800 Subject: [PATCH 1/4] fix: parse unary not used with function-call syntax as a function call --- src/expression/parse.js | 13 +++++++------ test/unit-tests/expression/parse.test.js | 10 ++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/expression/parse.js b/src/expression/parse.js index c300b8cf98..0221339256 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -1217,7 +1217,6 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ * @private */ function parseUnary (state) { - let name, params, fn const operators = { '-': 'unaryMinus', '+': 'unaryPlus', @@ -1226,13 +1225,15 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ } if (hasOwnProperty(operators, state.token)) { - fn = operators[state.token] - name = state.token + const fn = operators[state.token] + const name = state.token + const saveState = Object.assign({}, state) getTokenSkipNewline(state) - params = [parseUnary(state)] - - return new OperatorNode(name, fn, params) + if (name !== 'not' || state.token !== '(') { + const params = [parseUnary(state)] + return new OperatorNode(name, fn, params) + } else Object.assign(state, saveState) } return parsePow(state) diff --git a/test/unit-tests/expression/parse.test.js b/test/unit-tests/expression/parse.test.js index 444247244f..58a078f8ef 100644 --- a/test/unit-tests/expression/parse.test.js +++ b/test/unit-tests/expression/parse.test.js @@ -2054,6 +2054,16 @@ describe('parse', function () { assert.strictEqual(parseAndEval('4 + not 2'), 4) assert.strictEqual(parseAndEval('10+not not 3'), 11) + + assert.strictEqual(parseAndEval('not(2)'), false) + // not used in function call syntax has function-call precedence + assert.deepStrictEqual( + parseAndEval('not([0, 1]).map(_(x) = equal(x, true) ? 7 : -1)'), + math.matrix([7, -1])) + // not used as operator has unary-prefix-operator precedence + assert.deepStrictEqual( + parseAndEval('not [0, 1].map(_(x) = equal(x, true) ? 7 : -1)'), + math.matrix([false, false])) }) it('should parse minus -', function () { From d92e00a48a7a7a46c2128f3ca1fd48c57f5d7c03 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 11 Dec 2025 09:34:16 -0800 Subject: [PATCH 2/4] doc: Explanatory comments for parseUnary change --- src/expression/parse.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/expression/parse.js b/src/expression/parse.js index 0221339256..7dfe085ad1 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -1230,6 +1230,11 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ const saveState = Object.assign({}, state) getTokenSkipNewline(state) + // If the current expression looks like `not(stuff...`, i.e. appears + // in the guise of function-call syntax, then we pass on it here and + // let it be picked up by the function-call parsing + // (the combo of parseSymbol/parseAccessors) later, so that it will + // have the exact behavior/precedence of a function call. if (name !== 'not' || state.token !== '(') { const params = [parseUnary(state)] return new OperatorNode(name, fn, params) From cecfd2067df064a9d3d01de0c8915df92c133962 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 17 Dec 2025 07:44:41 -0800 Subject: [PATCH 3/4] refactor: reverse sense of test for `not` as function call to clarify behavior --- src/expression/parse.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/expression/parse.js b/src/expression/parse.js index 7dfe085ad1..b85456886f 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -1230,15 +1230,16 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ const saveState = Object.assign({}, state) getTokenSkipNewline(state) - // If the current expression looks like `not(stuff...`, i.e. appears - // in the guise of function-call syntax, then we pass on it here and - // let it be picked up by the function-call parsing - // (the combo of parseSymbol/parseAccessors) later, so that it will - // have the exact behavior/precedence of a function call. - if (name !== 'not' || state.token !== '(') { + if (name === 'not' && state.token === '(') { + // This is the syntax of a unary function call with symbol `not`, + // so rather than handling here, we let it fall through to be handled + // by function-call parsing later. + Object.assign(state, saveState) + } else { + // Bona-finde "unary operator" application const params = [parseUnary(state)] return new OperatorNode(name, fn, params) - } else Object.assign(state, saveState) + } } return parsePow(state) From a66e16318f16a1008d4cbb98b97167b2de0cbb2d Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 22 Dec 2025 13:56:43 -0800 Subject: [PATCH 4/4] refactor: If detecting not(...), parse symbol directly rather than just continue --- src/expression/parse.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/expression/parse.js b/src/expression/parse.js index b85456886f..e01e7c85bf 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -1231,10 +1231,11 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ getTokenSkipNewline(state) if (name === 'not' && state.token === '(') { - // This is the syntax of a unary function call with symbol `not`, - // so rather than handling here, we let it fall through to be handled - // by function-call parsing later. + // OK, this appears to be using `not` in the syntax of an ordinary + // function call, rather than as a unary operator, so re-parse the + // `not` as a Symbol and continue from there: Object.assign(state, saveState) + return parseSymbol(state) } else { // Bona-finde "unary operator" application const params = [parseUnary(state)]