-
Notifications
You must be signed in to change notification settings - Fork 479
introducing dexpr #8649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
introducing dexpr #8649
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| name: dexpr | ||
|
|
||
| # dexpr is plain C++ with no Kokkos, MPI, netCDF or EKAT dependency, so unlike | ||
| # the rest of E3SM it builds and tests on a stock GitHub runner in about a | ||
| # minute. That is why this workflow does not need the self-hosted ghci-snl | ||
| # machines the eamxx workflows run on, and checks out no submodules. | ||
|
|
||
| on: | ||
| # Runs on PRs against master, but only if they touch the parser | ||
| pull_request: | ||
| branches: [ master ] | ||
| types: [opened, synchronize, ready_for_review, reopened] | ||
| paths: | ||
| - 'share/dexpr/**' | ||
| - '.github/workflows/dexpr-testing.yml' | ||
|
|
||
| # Also guard master itself, so a bad merge is caught immediately | ||
| push: | ||
| branches: [ master ] | ||
| paths: | ||
| - 'share/dexpr/**' | ||
| - '.github/workflows/dexpr-testing.yml' | ||
|
|
||
| # Manual run for debug purposes only | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| # Two runs are in the same group if they are testing the same git ref | ||
| # - if trigger=pull_request, the ref is refs/pull/<PR_NUMBER>/merge | ||
| # - for other triggers, the ref is the branch tested | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| test: | ||
| name: ${{ matrix.compiler }} / ${{ matrix.build_type }} | ||
| runs-on: ubuntu-latest | ||
|
|
||
| strategy: | ||
| # One compiler failing should not hide what the others would have said | ||
| fail-fast: false | ||
| matrix: | ||
| compiler: [gcc, clang] | ||
| build_type: [Debug, Release] | ||
|
|
||
| steps: | ||
| - name: Check out the repository | ||
| uses: actions/checkout@v7 | ||
| with: | ||
| persist-credentials: false | ||
| show-progress: false | ||
|
|
||
| - name: Show action trigger | ||
| uses: ./.github/actions/show-workflow-trigger | ||
|
|
||
| - name: Select the compiler | ||
| run: | | ||
| if [ "${{ matrix.compiler }}" = "gcc" ]; then | ||
| echo "CC=gcc" >> "$GITHUB_ENV" | ||
| echo "CXX=g++" >> "$GITHUB_ENV" | ||
| else | ||
| echo "CC=clang" >> "$GITHUB_ENV" | ||
| echo "CXX=clang++" >> "$GITHUB_ENV" | ||
| fi | ||
|
|
||
| - name: Configure | ||
| run: | | ||
| cmake -S share/dexpr -B build \ | ||
| -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ | ||
| -DDEXPR_WERROR=ON | ||
|
|
||
| - name: Build | ||
| run: cmake --build build --parallel | ||
|
|
||
| - name: Test | ||
| run: ctest --test-dir build --output-on-failure --parallel | ||
|
|
||
| - name: Smoke test the tool | ||
| run: ./build/dexpr functions |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| **/build/** | ||
| **/.cache/** |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| cmake_minimum_required(VERSION 3.20) | ||
|
|
||
| project(dexpr | ||
| VERSION 0.1.0 | ||
| DESCRIPTION "Lexer and parser for the diagnostics expression language" | ||
| LANGUAGES CXX) | ||
|
|
||
| # This library is deliberately standalone: it has no Kokkos, MPI, netCDF or | ||
| # EKAT dependency, and is not wired into the CIME/csm_share build. That is | ||
| # what lets it be configured and tested anywhere cmake and a C++20 compiler | ||
| # exist, including a stock GitHub runner. | ||
| option(DEXPR_ENABLE_TESTS "Build the unit tests" ON) | ||
| option(DEXPR_ENABLE_TOOL "Build the command line tool" ON) | ||
|
|
||
| add_library(dexpr | ||
| src/ast_print.cpp | ||
| src/lexer.cpp | ||
| src/parser.cpp | ||
| src/precedences.cpp | ||
| src/tokens.cpp | ||
| ) | ||
|
|
||
| target_compile_features(dexpr PUBLIC cxx_std_20) | ||
|
|
||
| target_include_directories(dexpr | ||
| PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) | ||
|
|
||
| option(DEXPR_WERROR "Treat compiler warnings as errors" OFF) | ||
|
|
||
| # Warnings stay on: with no external headers in the include path there is | ||
| # nothing here that anyone has to fight, so the library should stay clean. | ||
| # CI turns DEXPR_WERROR on to keep it that way. The flags are set here rather | ||
| # than per target so tests/ inherits exactly the same set. | ||
| set(DEXPR_WARNINGS) | ||
| if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") | ||
| set(DEXPR_WARNINGS -Wall -Wextra -Wpedantic) | ||
| if (DEXPR_WERROR) | ||
| list(APPEND DEXPR_WARNINGS -Werror) | ||
| endif() | ||
| endif() | ||
|
|
||
| target_compile_options(dexpr PRIVATE ${DEXPR_WARNINGS}) | ||
|
|
||
| if (DEXPR_ENABLE_TOOL) | ||
| # The target is dexpr_cli because the library already owns the name dexpr; | ||
| # the binary it produces is still just `dexpr`. | ||
| add_executable(dexpr_cli tools/dexpr.cpp) | ||
| set_target_properties(dexpr_cli PROPERTIES OUTPUT_NAME dexpr) | ||
| target_link_libraries(dexpr_cli PRIVATE dexpr) | ||
| endif() | ||
|
|
||
| if (DEXPR_ENABLE_TESTS) | ||
| include(CTest) | ||
| if (BUILD_TESTING) | ||
| add_subdirectory(tests) | ||
| endif() | ||
| endif() |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2026 Peter Schwartz | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| # dexpr | ||
|
|
||
| A lexer and parser for the diagnostics expression language: the small DSL used | ||
| to describe derived diagnostics as expressions over model fields, rather than | ||
| as hand-written code per diagnostic. | ||
|
|
||
| Given a string like | ||
|
|
||
| ```text | ||
| x*y.derivative(dx=dy,['col']).where(x>0) | ||
| ``` | ||
|
|
||
| which will be grouped as | ||
|
|
||
| ```text | ||
| (x*y.derivative((dx=dy), ['col']).where((x>0))) | ||
| ``` | ||
|
|
||
| `dexpr` produces an abstract syntax tree. It does not evaluate anything, and it | ||
| knows nothing about fields, grids or timesteps -- turning an AST into an actual | ||
| diagnostic is the caller's job. the Parser::parse function returns a pointer to the | ||
| root node of the AST. | ||
|
|
||
| Example Usage: | ||
|
|
||
| ```c++ | ||
| parser::Parser parser{Lexer{string_input}}; | ||
| const auto expr = parser.parse(); // expr is a std::unique_ptr | ||
| auto string_representation = ast::to_string(*expr); //to_string implmented as a Vistor | ||
| ``` | ||
|
|
||
| ## Layout | ||
|
|
||
| | Path | Contents | | ||
| | --- | --- | | ||
| | `include/dexpr/` | Public headers | | ||
| | `src/` | Lexer, parser, AST printing, token and precedence tables | | ||
| | `tests/` | Catch2 unit tests | | ||
| | `tools/` | `dexpr`, a small command line helper | | ||
|
|
||
| The grammar is handled by a hand-written lexer feeding a Pratt (top-down operator precedence) parser. | ||
| AST nodes are represented by a `std::variant`, visited through | ||
| `Expression::visit`, which serves as a generic wrapper around `std::visit`. | ||
| This design means the nodes should be stable and transformations acting on the AST can be easily added. | ||
|
|
||
| The basic set of callable functions lives in one place, `supported_functions.hpp`. | ||
| Nothing else in the library is diagnostics-specific. | ||
|
|
||
| ### Parser terminology | ||
|
|
||
| The expression parser uses a hand-written | ||
| [Pratt parser](https://tdop.github.io/), | ||
| also known as *top-down operator-precedence parsing*. Pratt parsing associates | ||
| parsing behavior with tokens and uses operator precedence to determine how an | ||
| expression is grouped. | ||
|
|
||
| Some terminology used throughout the implementation: | ||
|
|
||
| - **Prefix expression** — an expression beginning with an operator | ||
| and consumes an expression to its right, e.g. `-x` or `!x`. | ||
| Literals and identifers are considered Prefix expressions for parser-function dispatch | ||
| but are stored in the AST as a more specific node. | ||
|
|
||
| - **Infix expression** — an expression that contains operator located **in**-between a left- and right-hand | ||
| expression, e.g. `x + y` or `x < y`. | ||
|
|
||
| - **Precedence / binding power** — determines how tightly an operator binds | ||
| relative to surrounding operators. For example, multiplication has higher | ||
| precedence than addition, so `x + y * z` is parsed as `x + (y * z)`. | ||
| Exponentiation is right-associative: `x ** y ** z` is parsed as | ||
| `x ** (y ** z)`. This is represented by conditionally modifying its Precedence to | ||
| reflect right-binding power. | ||
|
|
||
| - **Prefix parse function** — parses an expression beginning with a token that can begin an expression, | ||
| such as an identifier, literal, unary operator, or opening parenthesis. | ||
|
|
||
| - **Infix parse function** — extends an expression based on the `Precedence` of the following token. | ||
| It receives the expression to its left and parses the required expression(s) to its right. | ||
| Example tokens include: aritmetic operators, logical operators, opening parenthesis, etc... | ||
|
|
||
| ## Building | ||
|
|
||
| `dexpr` is deliberately standalone. It has no Kokkos, MPI, netCDF or EKAT | ||
| dependency, it is not part of the CIME or `csm_share` build, and it requires | ||
| only CMake 3.20 and a C++20 compiler: | ||
|
|
||
| ```shell | ||
| ./run_tests.sh | ||
| ``` | ||
|
|
||
| which is shorthand for | ||
|
|
||
| ```shell | ||
| cmake -S . -B build -DDEXPR_ENABLE_TESTS=ON -DDEXPR_ENABLE_TOOL=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON | ||
| cmake --build build --parallel | ||
| ctest --test-dir build --output-on-failure --parallel | ||
| ``` | ||
|
|
||
| Catch2 v3 is used for the tests. An installed copy is used if there is one; | ||
| otherwise CMake fetches a pinned version. | ||
|
|
||
| ### Compiler requirement | ||
|
|
||
| C++20, and in practice GCC 11 or newer: numeric literals are parsed and | ||
| printed with floating-point `<charconv>`, which is the binding constraint. | ||
|
|
||
| ### Options | ||
|
|
||
| | Option | Default | Effect | | ||
| | --- | --- | --- | | ||
| | `DEXPR_ENABLE_TESTS` | `ON` | Build the unit tests | | ||
| | `DEXPR_ENABLE_TOOL` | `ON` | Build the `dexpr` command line tool | | ||
| | `DEXPR_WERROR` | `OFF` | Treat compiler warnings as errors; CI sets this | | ||
|
|
||
| ## Testing | ||
|
|
||
| Every `TEST_CASE` is registered with ctest individually, so a failure names | ||
| itself. The same commands run in CI, across gcc and clang in both Debug and | ||
| Release, on every pull request touching this directory. | ||
|
|
||
| ## Planned work | ||
|
|
||
| Not implemented here, recorded so it is not rediscovered: | ||
|
|
||
| - **Component-supplied functions.** The callable set is fixed in | ||
| `supported_functions.hpp` and nothing consults it, so `nope(x)` parses and is | ||
| never rejected. The plan is a registry a component fills in at init -- each | ||
| function's name, its parameters in positional order, and whether the call is | ||
| written free (`where(...)`) or as a method (`T_mid.interp(...)`) -- plus a | ||
| pass over the AST that checks calls against it. That pass stays out of the | ||
| parser on purpose, so `foo(a, b=c)` parses the same whether or not `foo` | ||
| exists. | ||
| - **Operator syntax is fixed.** A registry would let a component add functions | ||
| but not operators. A new operator means editing the token enum, the lexer, | ||
| the precedence table and the parser's dispatch tables. | ||
|
|
||
| ## Provenance | ||
|
|
||
| Vendored from [peterdschwartz/e3sm_diags_parser](https://github.com/peterdschwartz/e3sm_diags_parser) | ||
| at `d680d50`, MIT licensed; see `LICENSE`. The upstream name, and its `edp` | ||
| abbreviation, were dropped in favour of `dexpr` once the code moved here. | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.