Repository: gerlero/foamlib

Issue:
div((nuEff*dev2(T(grad(U))))) parsing
Hi again, so I think I have found another issue with the parsing in the `fvSchemes` file for the `divSchemes` when trying to parse the following line `div((nuEff*dev2(T(grad(U)))))`. I think this is a valid keyword?

```py
from pathlib import Path
from foamlib import FoamFile

Path("./system").mkdir(exist_ok=True)
file = open('./system/fvSchemes', 'w')
file.close()
fv_schemes = {
    'divSchemes': {
        'default': 'none',
        'div((nuEff*dev2(T(grad(U)))))': 'Gauss linear',
        'div(phi,U)': 'bounded Gauss upwind'
    },
}
file = FoamFile(path='./system/fvSchemes')
file.update(fv_schemes)
```

```sh
Traceback (most recent call last):
  File ".../test.py", line 15, in <module>
    file.update(fv_schemes)
  File ".../.venv/lib/python3.12/site-packages/foamlib/_files/_files.py", line 359, in update
    super().update(*args, **kwargs)
  File "<frozen _collections_abc>", line 982, in update
  File ".../.venv/lib/python3.12/site-packages/foamlib/_files/_files.py", line 307, in __setitem__
    self[(*keywords, k)] = v
    ~~~~^^^^^^^^^^^^^^^^
  File ".../.venv/lib/python3.12/site-packages/foamlib/_files/_files.py", line 200, in __setitem__
    raise ValueError(msg)
ValueError: Invalid keyword: div((nuEff*dev2(T(grad(U)))))
~``

Hints from issue comments:
@gerlero: @bevanwsjones just to confirm, shouldn't it be ```div(nuEff*dev2(T(grad(U))))```? Doesn't it have an extra set of parentheses?
@gerlero: In any case, I can update the parsing grammar to accept such a (multiple-parenthesized) expression.@bevanwsjones: Hi @gerlero , so yes, it does parse correctly with only one parenthesis. However, if I run this with openFOAM I get 

```sh
...
[4] --> FOAM FATAL IO ERROR: (openfoam-2412)
[4] Entry 'div((nuEff*dev2(T(grad(U)))))' not found in dictionary "stream/divSchemes"
...
```

So I assume it wants the double parenthesis? 
@gerlero: Yeah, something is writing with double parentheses. My doubt actually is if _that_ something has a bug.

No need to answer that question for now; I'll simply merge #329 and it should work as is.@gerlero: Should be fixed in v0.8.5@bevanwsjones: Thanks again. 😄

Solution patch:
diff --git a/foamlib/_files/_parsing.py b/foamlib/_files/_parsing.py
index 31be86c..1ecf502 100644
--- a/foamlib/_files/_parsing.py
+++ b/foamlib/_files/_parsing.py
@@ -235,11 +235,12 @@ _TENSOR = (
     | _tensor(TensorKind.SYMM_TENSOR)
     | _tensor(TensorKind.TENSOR)
 )
-_IDENTIFIER = Forward()
-_IDENTIFIER <<= Combine(
-    Word(_IDENTCHARS, _IDENTBODYCHARS, exclude_chars="()")
-    + Opt(Literal("(") + _IDENTIFIER + Literal(")"))
+_PARENTHESIZED = Forward()
+_IDENTIFIER = Combine(
+    Word(_IDENTCHARS, _IDENTBODYCHARS, exclude_chars="()") + Opt(_PARENTHESIZED)
 )
+_PARENTHESIZED <<= Combine(Literal("(") + (_PARENTHESIZED | _IDENTIFIER) + Literal(")"))
+
 _DIMENSIONED = (Opt(_IDENTIFIER) + _DIMENSIONS + _TENSOR).set_parse_action(
     lambda tks: Dimensioned(*reversed(tks.as_list()))
 )


Test patch:
diff --git a/tests/test_files/test_parsing.py b/tests/test_files/test_parsing.py
index 140b899..486a22c 100644
--- a/tests/test_files/test_parsing.py
+++ b/tests/test_files/test_parsing.py
@@ -93,6 +93,7 @@ def test_parse_value() -> None:
     assert Parsed(b"(water oil mercury air)")[()] == ["water", "oil", "mercury", "air"]
     assert Parsed(b"div(phi,U)")[()] == "div(phi,U)"
     assert Parsed(b"div(nuEff*dev(T(grad(U))))")[()] == "div(nuEff*dev(T(grad(U))))"
+    assert Parsed(b"div((nuEff*dev(T(grad(U)))))")[()] == "div((nuEff*dev(T(grad(U)))))"
     assert Parsed(b"((air and water) { type constant; sigma 0.07; })")[()] == [
         (["air", "and", "water"], {"type": "constant", "sigma": 0.07})
     ]