Skip to content

fix: give ^ higher precedence than unary - and + - #17

Merged
chrispcampbell merged 6 commits into
mainfrom
todd/16-op-precedence
Aug 8, 2026
Merged

fix: give ^ higher precedence than unary - and +#17
chrispcampbell merged 6 commits into
mainfrom
todd/16-op-precedence

Conversation

@ToddFincannonEI

@ToddFincannonEI ToddFincannonEI commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16

Problem

In grammar/Expr.g4, the Negative and Positive alternatives were listed above Power. In an ANTLR left-recursive rule, alternative order sets precedence, so unary - bound tighter than ^.

That makes -x^2 parse as (-x)^2 instead of -(x^2), which is a silent numerical error rather than a parse failure. The Gaussian kernel is the case that surfaced it:

EXP(-x^2/2)

parsed as EXP(((-x)^2)/2) — the exponent lost its sign, so the expression evaluated as EXP(x^2/2), growing instead of decaying.

Change

Move the Power alternative above Negative and Positive:

     |   ':NOT:' expr                      # Not
+    |   expr '^' expr                     # Power
     |   '-' expr                          # Negative
     |   '+' expr                          # Positive
-    |   expr '^' expr                     # Power
     |   expr op=('*'|'/') expr            # MulDiv

One line moved; no other grammar changes. This matches standard mathematical convention and Vensim's own operator precedence, where ^ outranks unary sign.

parser/ModelParser.js is regenerated with ANTLR 4.12.0. The generated diff is confined to the expr rule: the Power precedence predicate moves from 12 to 14 and its right operand from expr(13) to expr(15), while Negative drops from expr(14) to expr(13) and Positive from expr(13) to expr(12).

Effect

Measured by parsing each expression with the pre-fix and post-fix parsers:

Expression Before After
-x^2 (-x)^2 -(x^2)
+x^2 (+x)^2 +(x^2)
-2^2 4 -4
EXP(-x^2/2) EXP(x^2/2) EXP(-(x^2)/2)
-x^2*y ((-x)^2)*y (-(x^2))*y
x^-2 x^(-2) x^(-2) (unchanged)
(-x)^2 (-x)^2 (-x)^2 (unchanged)
-x*y (-x)*y (-x)*y (unchanged)
2^3^2 (2^3)^2 (2^3)^2 (unchanged)

x^-2 still parses because ANTLR emits prefix-unary alternatives in the rule's primary section with no precedence predicate — only the left-recursive loop is guarded. Raising Power above Negative therefore changes which operator wins when both could apply to the left, without making a negated right operand unreachable.

^ remains left-associative, consistent with Vensim. That is pre-existing behavior, unchanged here, and now covered by a test so it stays that way.

Tests

This adds the repository's first test suite, using Vitest.

  • tests/parse-expr.js — parses an expression and renders the tree as a fully parenthesized infix string, so expectations read as grouping rather than as ANTLR internals: -x^2/2 becomes ((-(x^2))/2). It fails loudly on syntax errors and on input that parses only in part, since expr otherwise stops silently at the first token it cannot use.
  • tests/expr-precedence.test.js — 22 cases covering power against unary sign, power against the binary operators, unary sign against the binary operators, associativity of ^, *// and +/-, and precedence among *, +, relational, :NOT:, :AND: and :OR:.

Run with:

pnpm test

The suite was checked against the pre-fix parser to confirm it catches the regression: exactly the five power-vs-unary-sign cases fail, and the other 17 pass. That makes it a real guard on alternative ordering in Expr.g4, not just a restatement of current behavior.

README.md gains a short section on running the tests and on why alternative order in the grammar needs test coverage.

Open items for review

  • No version bump. package.json is still at 0.6.3. This is a behavior change for consumers, so it wants at least a patch bump before publishing — arguably a minor bump, since any model relying on the old grouping will silently produce different numbers.
  • tests/ will be included in the published package. There is no files field and no .npmignore, so grammar/ and tools/ already ship too. Consistent with existing practice, but worth deciding on if you want to trim the tarball.

Base branch note

todd/op-precedence is based on dependabot/npm_and_yarn/js-yaml-3.14.2, not on origin/main. As a result the diff against main also carries an unrelated pnpm-lock.yaml hunk (adds the settings: block with autoInstallPeers / excludeLinksFromLockfile). Rebase before opening if you want the PR to stand alone:

git rebase --onto origin/main dependabot/npm_and_yarn/js-yaml-3.14.2 todd/op-precedence

Verification status

All groupings in the table above were measured by running the actual parsers, not derived. pnpm test passes 22/22 on the regenerated parser and fails 5/22 on the pre-fix parser.

dependabot Bot and others added 3 commits November 17, 2025 16:59
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](nodeca/js-yaml@3.14.1...3.14.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Regenerate the parser for the power precedence fix and add a Vitest suite
that pins down operator grouping.

Precedence in an ANTLR left-recursive rule comes from the order of the
alternatives, so reordering them in Expr.g4 changes how expressions
evaluate without causing a parse error. The tests render each parse tree
as a fully parenthesized infix string so the expectations read as
grouping rather than as ANTLR internals.

Checked against the pre-fix parser: exactly the five power-vs-unary-sign
cases fail and the other 17 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ToddFincannonEI ToddFincannonEI added the bug Something isn't working label Aug 7, 2026
@chrispcampbell
chrispcampbell changed the base branch from dependabot/npm_and_yarn/js-yaml-3.14.2 to main August 7, 2026 22:55

@chrispcampbell chrispcampbell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good to me. I moved the lock file and vitest changes out into separate issues/PRs that have already been merged to main, and this branch was updated to be based on the latest main, so it only has the minimal set of changes.

I locally linked this package in my copy of SDE and verified that the compile package tests and test models are still passing after this change.

@chrispcampbell
chrispcampbell merged commit 4824902 into main Aug 8, 2026
3 checks passed
@chrispcampbell
chrispcampbell deleted the todd/16-op-precedence branch August 8, 2026 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exponentiation binds less tightly than unary minus, silently changing the sign of an exponent

2 participants