Skip to content

Conversation

@codegiyu
Copy link

@codegiyu codegiyu commented Jun 30, 2025

  • Parser extended to accept numeric literals as object keys in object expressions.
  • Parser extended to accept numeric literals as object accessors including negative numbers.
  • Parser extended to accept operations and expressions resulting in numbers as object accessors.
  • If a number is parsed as an object key or accessor, it is converted to a string before being used as an object key / accessor.
  • If an operation or expression resulting in a number is parsed as an object accessor, the result of the operation / expression is evaluated before being converted to a string and used as an object accessor
  • Numeric literals, operations and expressions resulting in numbers are now accepted in square brackets for object assigments. The resulting number is first converted to a string before being used as an object key.
  • Care was taken to manage numbers with leading zeros either as object keys or accessors. eg 05. They're run through a Number constructor before being converted to strings to ensure there are no preceeding zeros
  • Dot notation for object assignment and accessing remains unchanged

Closes #3352

@codegiyu
Copy link
Author

@josdejong ready for review

@gwhitney
Copy link
Collaborator

This is just a preliminary review; I have not had a chance to go over the core code changes that implement the new functionality. However, there are no new test cases to demonstrate the new functionality (and help prevent future regressions on this feature), and a few irrelevant changes that do not belong in this PR, which I have commented. Please remove the irrelevant changes and add a thorough suite of unit tests for the new feature, and we can move on to a more thorough code review. Thanks!

@codegiyu
Copy link
Author

codegiyu commented Jul 1, 2025

Understood...I'll remove the unnecessary eslint, prettier suggestions and whitespace improvement and add tests for the parser adjustments I've made

@codegiyu codegiyu requested a review from gwhitney July 1, 2025 18:00
@codegiyu
Copy link
Author

codegiyu commented Jul 1, 2025

@gwhitney unnecessary changes removed and unit test suites added....ready for review

@gwhitney
Copy link
Collaborator

gwhitney commented Jul 1, 2025

@josdejong the contributor codegiyu has quite reasonably reflected the idiosyncrasy from JavaScript into the MathJS Expressions language that {2: 3} is a valid object literal with property '2' but {-2: 3} is a syntax error. Is that what MathJS would prefer? Or would you rather that {-2: 3}, for example, be allowed, as a literal for a plain object with property '-2'? I personally am a bit unclear on the motivation for JavaScript to disallow negative numeric literal keys in an object.

Copy link
Owner

@josdejong josdejong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for picking this up @codegiyu! Nice.

I made a couple of inline comments, can you have a look at those?

const evalIndex = this.index._compile(math, argNames)

// If index contains operator node, evaluate result of the operation and access object with result
if (isOperatorNode(this.index.dimensions[0]) && isObjectNode(this.object)) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new section in AccessorNode adds support for operators inside the accessor like {2: 6}[2 * 1], but it doesn't allow any expression in general, for example it fails with {2: 6}[multiply(2, 1)].

I think the right place to update this behavior is not here in AccessorNode, but in IndexNode.isObjectProperty and IndexNode.getObjectProperty, see also my other comment in parse.js.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having trouble updating this behaviour in IndexNode.getObjectProperty

I was able to compile the result of the operation because this was happening in a _compile method and I had access to math and argNames, so could call the compile method of the OperatorNode

In IndexNode.getObjectProperty, I don't have access to math and argNames, so can't use the _compile method on an OperatorNode in there. And I can't find another way to compile a result to the operation in an OperatorNode

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've looked into this a bit more, it's more complex than I had expected.

In AccessorNode._compile, there is an if (this.index.isObjectProperty()) { ... }. I think that part of the code must be removed, since it only works for static strings and not expressions inside the brackets. And also, we cannot detect anymore whether we're dealing with an object key (vs a matrix subset) by just looking at the index: when the index now contains just one dimension with one number, it could be an object key too. Therefore, this should be handled by the access(...) function. The logic in the function access that handles the case of an object can be extended to correctly evaluate numeric properties too. To do that, instead of the index.getObjectProperty() we need a method like index.evalObjectProperty(scope, args, context). We cannot use the normal IndexNode._compile(...) because that changes numeric indexes from 1-based to 0-based, and we don't want that in case of an object key.

Similarly, we need to update/extend the logic in AssignmentNode.

Maybe we should also get rid of the "smart" name function of AccessorNode, see get name() { ... }. It's not really needed in practice I think, and is not reliable anymore when it only works in "some" cases.

Does this direction sort of make sense?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it all make sense. Thank you for the directions. I will try to work it out and revert


it('should throw an error when negative numbers are applied as keys in an object expression', function () {
assert.throws(function () {
parseAndEval('{-1: 34}')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Glen: I cannot come up with a reason why we wouldn't support negative numbers. Can you implement support for negative values too? Or do you have an other opinion?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I believe I can. I left it out earlier because javascript also didn't seem to accept that

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josdejong I was able to make this happen as well by modifying a part of the parseObject function.

Right before it would throw an error due to not finding a symbol, string or number as an object key, if the current token is '-', it would try to parseUnary and if the resulting node is an operator node with just a constant node with a number value, then it would proceed to use the number preceeded by a '-' as the key

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're idea is the same as Glen implemented for the % operator just yesterday in #3505, that indeed works out nicely.

// If param value is number and node is object node, make param value a string
if (typeof params[0].value === 'number' && isObjectNode(node) && params.length === 1 && isConstantNode(params[0])) {
// Number constructor is first used to manage situations of numbers with preceding zero digit(s)
params[0].value = String(Number(params[0].value))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it can be the case that the actual (numeric or string) key is evaluated at runtime and not parse time, I think this logic should not be located here but when evaluating the actual key, in functions like Indexnode.isObjectProperty and IndexNode.getObjectProperty. However, these functions currently determine whether dealing with an object node statically. I think this needs to be changed to determine this after evaluating it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I will look into effecting the change from IndexNode instead

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right...making the changes in IndexNode will affect assignments too, which is something I overlooked.

I'm thinking of introducing an optional property like 'forObjectNode'. But since there's already an optional property for dot notation, I'm now thinking of having an optional options object that would contain dotNotation and forObjectNode.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been able to mostly move this logic to IndexNode.isObjectProperty and .getObjectProperty

I did attach a boolean property, 'forObjectNode' to params[0] here

I opted for this against modifying the structure of arguments to the IndexNode constructor as I initially intended in my earlier comment, because this felt more controlled and still got the job done

However, using that forObjectNode property I attached to the nodes being fed into the IndexNode constructor, I've been able to recreate this logic you commented on here in IndexNode's isObjectProperty and getObjectProperty

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josdejong Can you please review this update?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to await reviewing until you've tried out the latest ideas, or is there a specific part of the code on which you already would like to have more feedback?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josdejong I've pushed my latest changes and I would like your review.

The earlier logic I had in parseAccessor has been completely moved out.
IndexNode.isObjectProperty and getObjectProperty have been restored to only recognize and return strings like before

To be able to handle numbers, operations and expressions in matrix indexes for object accessing and assignments, I relied on a singular DenseMatrix being present in the index._dimensions array recieved in the access and assign functions

Problems are that

  1. Somewhere in the Index transform process, it passes through the _createImmutableMatrix method in MatrixIndex and throws an error if non-integers are used
  2. The tests for getting names accurately for AccessorNode, AssignmentNode, FunctionNode are failing
  3. A few security tests related to not allowing calling Functions via various things (bind, constructor, etc) are failing...however they're failing because the errors gotten now don't exactly match the errors expected. But trying out the test cases still results in an error being thrown

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates, I'll do some testing and debugging with the latest version of your PR soon (I expect tomorrow).

It can indeed be that some of the security tests now give differing error messages, that is fine.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About (2): the tests for names will need to change if we drop support for these "smart" names. It will be a breaking change.

Thinking about it however, it may be better to leave it as it is for now and mark it deprecated, then we don't introduce a breaking change right now, and it may be better to try focus on getting numbers working as index, which is complex enough already. Let's try one step at a time 😅 .

@gwhitney
Copy link
Collaborator

gwhitney commented Jul 2, 2025

OK, now that @josdejong has reviewed the code in more detail, I will leave feedback on your changes to him. But @codegiyu you may want to push your changes so far so that Jos can see them. If you don't think they're ready for merging yet, you should click the "convert to draft" link, and then you can mark it "ready for review" when you think it's back in shape. Thanks!

@codegiyu codegiyu marked this pull request as draft July 3, 2025 13:00
@codegiyu codegiyu requested a review from josdejong July 3, 2025 13:00
@josdejong
Copy link
Owner

josdejong commented Jul 11, 2025

I'll reply here in the main thread since I get a bit lost in the inline conversations 😅.

Let me try to explain in a bit more detail where I think we need to go. It may be the case that this resolves some of the current questions/issues already.

  1. The method access needs to change from:

    access(object, index)

    To:

    access(object, evalIndex, evalProperty, scope, args)

    Because access now needs to determine whether to evaluate as index or as property. Depending on whether dealing with an Array, Matrix, or Object, the function access will first call evalIndex(scope, args, context) or evalProperty(scope, args) and then apply it to the array/object.

  2. The AccessorNode._compile method will then look something like this:

    _compile (math, argNames) {
      const evalObject = this.object._compile(math, argNames)
      const evalIndex = this.index._compile(math, argNames)
      const evalProperty = this.index._compileProperty(math, argNames)  // DOES NOT YET EXIST
    
      return function evalAccessorNode (scope, args, context) {
        const object = evalObject(scope, args, context)
        return access(object, evalIndex, evalProperty, scope, args)
      }
    }
  3. The AssignmentNode._compile method needs a similar change as AccessorNode._compile

  4. We need a new method IndexNode._compileProperty, which replaces the need for our old isObjectProperty and getObjectProperty. This needs to return an evaluation function which tries to evaluate the index as a property, which can be a string or number. If that is not successful, the function throws an exception.

Does this help?

@josdejong
Copy link
Owner

@codegiyu did you see my last feedback on this PR? Please let me know if you need more pointers or help.

@codegiyu
Copy link
Author

@codegiyu did you see my last feedback on this PR? Please let me know if you need more pointers or help.

I'm so sorry, I didn't notice your feedback earlier. I'm checking it out now

@codegiyu
Copy link
Author

Thank you for the guide on a direction on modifying a few of the methods....I'll get it sorted as much as I can before midnight tonight

@josdejong
Copy link
Owner

Thanks for the update! There is no need to hurry, I just wanted to know if it's still on your radar. Take your time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Numeric keys disallowed in object expressions

3 participants