Repository navigation
expression: refuse a bracket after a call, which ExprTk reads as a second statement (#1415) - #1419
Merged
Merged
Conversation
…cond statement (lanl#1415) ExprTk multiplies a variable or a bracketed group against a bracket after it, but after a call the bracket opened a second statement, and the expression's value was that statement's: k*exp(-A)(B) gave B, and a .net rate law written so ran as its last factor with no word. compile() now refuses such an expression, naming the call, as BNG2.pl rejects it as a syntax error. A(B), (A)(B), 2(B) and a word operator before a bracket are read as before. No corpus or repository model is written so.
The scanner took any name that no variable held, and no word operator on a list, for a call. Two of the words were wrong: mand and mor are ExprTk calls, so k*mand(A > 1, B > 1)(B) still loaded and gave B. Its own token rules were wrong too: it split k.g at the dot, so a model with a parameter k.g had k.g(A)(B), the product, refused as a call to g. And an undefined name was refused as a call, with advice to write foo(A)*(...), which fails just the same. A call is now what ExprTk's parse_symbol calls: a built-in function, if, not, or a function in the symbol table. Names and numbers are read with the lexer rules exact_literals already follows (number_token_end, and symbol_token_end, factored out of it), in one pass. The scan reads the text as written, so the refusal quotes it: if(A>1 && B>1,1,0) rather than the engine's "if(A>1 and B>1,1,0)". That needed exact_literals fixed first: it respells a literal ExprTk misreads as a bracketed quotient, which after a call opened the same second statement. abs(B)0.49999999999999997, a product to ExprTk's lexer, gave the literal alone on main, and the PR refused it, naming a bracket nobody wrote. The respelling now writes the '*' the lexer would read. The check moves to expr_compat, the place both ExprTk layers share, and NFsim's function parser runs it too: a hand-written XML function 3*exp(0)(2) gave 2, for 6. Every new test fails on the PR's head.
wshlavacek
added a commit
that referenced
this pull request
Nov 2, 2026
nfsim: carry #1419's shim check, and run the vendor tripwire in CI
wshlavacek
added a commit
to Montana/bngsim
that referenced
this pull request
Nov 2, 2026
lanl#1419 added the bracket-after-call refusal to NFsim's mu::Parser shim (nfsim_funcparser.h) as a direct edit to third_party/nfsim/. A refresh replaces that tree with what upstream plus scripts/nfsim_vendor_patches/ produce, so the next one would have dropped the check without a word: the 20-patch candidate's export differs from main's tree in that file alone. It is now carry 0021, bngsim/carry-shim-bracket-after-call, and the re-vendored tree is byte-identical to main's. It got in because nothing ran the tripwire. NFSIM_VENDORING.md said a paused vendor_verify job in build.yml would, but no such job exists; the check ran only in a manual pre-commit stage. lint.yml gains an nfsim-vendor job that replays the patch series onto the base VENDOR.json records and runs vendor_nfsim.py --verify-clean, about a minute. On main's tree it fails, naming nfsim_funcparser.h.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1415.
What was wrong
ExprTk multiplies a variable, and a bracketed group, against a bracket after it:
A(B)and(A)(B)are A·B. After a function call it does not. The bracket opens a second statement, and the expression's value is the last statement's. So the call, and everything before it, was dropped with no word.A
.netwith A = 3, B = 5,f()read through the right-hand side, on main:k*exp(-A)(B)(k = 0.5)sin(A)(B)2*abs(A)(B) + 1time()(B),rint(A)(B),if(A>1,1,0)(B),sin(A)[B],sin(A) (B),sin (A)(B),tfun(...)(B)k*(1 + exp(-A)(B))ERR244 - Expected ')'BNG2.pl rejects the same text (
ABORT: Syntax error at (Bobs)), so a network BioNetGen writes never holds one.The change
ExprTkEvaluator::compilerefuses an expression where a call's closing)is followed by(,[or{, and names the call:time(), andif/not.and,or,xor,nand,nor,xnor,mand,mor,in,like,ilike) are not calls:(A > 1) and (B)(C)is (A > 1) and B·C, as codegen: write C for what ExprTk means, or decline before compiling (#1212) #1348's product table has it.1e1(B)is a number times a group.set_function_eval_expressionboth go throughcompile.The issue offered refusing or reading the product. I chose to refuse because BioNetGen does, and because a text that silently dropped half its value is better fixed by its author than guessed at.
The diff (
src/expression.cpp, abridged)Tests
test_codegen_exprtk_functions.py(#1348's) is updated where it asserted the old reading.test_a_bracket_after_a_call_declinesnow expects the interpreter to refuse (it asserted B), and still checks codegen's own decline on the text. The table case becomestest_a_bracket_after_a_table_is_refused.expleaves_NO_C, since that law no longer loads.python/tests/test_bracket_after_a_call.py(25 tests):sin (A)(B), an inline table (the loader writes it astfun_f__tfun0(), which is named), and the bracketedk*(1 + exp(-A)(B)). All 11 fail on main.(A > 1) and (B)(A)= 1:A(B)= 15,(A)(B),A (B),2(B)= 10,k(B)= 2.5,k*exp(-A)*(B),exp(-A)+(B),sin(A),1e-1(B),1e1(B)= 50,A(B)(A)= 45,k(B)(A)= 7.5,1e1(B)(A)= 150.Mutation tests (the core rebuilt for each):
A(B)(A))1e1(B)(A))(only, not[sin(A)[B])sin(A) (B))sin (A)(B))(A > 1) and (B)(A))define_constanthas no caller); parameters are variables. The check stays for the API.Corpus
1,323 BioModels plus the repository's 177
.netfiles: 1,500 loads, identical on main and here (1,468 load, and the same 32 fail on both). No model is written this way.Checks (macOS arm64)
python/tests: 17,204 passed, 0 failed.parity_checks/tests: 1,015 passed. The 2 failures (test_bngsim_provenanceand the reference-archive replay) also fail on main here: their subprocess can't importbngsimwithout my local import shim.pre-commit run --all-files,sphinx -W, andci/changelog.py check/required(changelog.d/1415.fixed.md) are clean.