commit 2dfed7464e2a34e40b1d21f5bc4c5295b2fc9c50 Author: small-red-hat <603606739@qq.com> Date: Mon Dec 29 13:59:13 2025 +0800 first diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml new file mode 100644 index 0000000..98a2ae1 --- /dev/null +++ b/.github/workflows/maven-publish.yml @@ -0,0 +1,28 @@ +name: Maven Central Repo Deployment +on: + release: + types: [released, prereleased] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout Git Repo + uses: actions/checkout@v2 + - name: Set up Maven Central Repo + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + server-id: ossrh + server-username: 'OSSRH_USER' + server-password: 'OSSRH_PASSWORD' + gpg-passphrase: 'MAVEN_GPG_PASSPHRASE' + gpg-private-key: ${{ secrets.GPG_SECRET }} + - name: debug settings.xml + run: cat /home/runner/.m2/settings.xml + - name: Publish to Maven Central Repo + run: mvn clean deploy --batch-mode --activate-profiles deploy + env: + OSSRH_USER: ${{ secrets.OSSRH_TOKEN_USER }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_TOKEN_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/reduce-adoc.yml b/.github/workflows/reduce-adoc.yml new file mode 100644 index 0000000..7cc2945 --- /dev/null +++ b/.github/workflows/reduce-adoc.yml @@ -0,0 +1,42 @@ +name: Reduce Adoc +on: + push: + paths: + - '**/*-source.adoc' + branches: ['**'] + workflow_dispatch: + +permissions: + contents: write + +jobs: + reduce: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + + - name: Install asciidoctor-reducer + run: gem install --no-document asciidoctor-reducer + + - name: Reduce all *-source.adoc files + shell: bash + run: | + set -euo pipefail + mapfile -d '' -t sources < <(find . -type f -name '*-source.adoc' -print0) + for src in "${sources[@]}"; do + out="${src%-source.adoc}.adoc" + mkdir -p "$(dirname "$out")" + echo "Reducing $src -> $out" + asciidoctor-reducer --preserve-conditionals -o "$out" "$src" + done + + - name: Commit reduced files + uses: EndBug/add-and-commit@v9 \ No newline at end of file diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..2239b9f --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,20 @@ +name: Java Unit Test with Maven + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Run tests with Maven + run: mvn -B clean test --file pom.xml + - name: Upload results to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d51290c --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Created by .ignore support plugin (hsz.mobi) +### Java template +*.class + +target/* +*.iml +.idea/ +.spec/ +openspec/ +AGENTS.md +gen/ + +.vscode/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +!src/test/resources/test-plugins/*.jar +dependency-reduced-pom.xml + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +test.log +test-push.sh + +.DS_Store + +src/main/antlr4/.antlr \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README-EN-source.adoc b/README-EN-source.adoc new file mode 100644 index 0000000..9bf3304 --- /dev/null +++ b/README-EN-source.adoc @@ -0,0 +1,1108 @@ +:toc: + += QLExpress + +image::images/logo.png[] + +link:README.adoc[【中文版】] | [English] + +image::https://api.star-history.com/svg?repos=alibaba/QLExpress&type=Date[Star History Chart] + +== Background Introduction + +QLExpress is an embedded Java dynamic scripting tool evolved from Alibaba's e-commerce business rules. It has strong influence within Alibaba Group, and was open-sourced in 2012 to continuously optimize itself and promote the spirit of open source contribution. + +Based on basic expression calculation, it has the following features: + +* Flexible customization capabilities - through Java API custom functions and operators, you can quickly implement business rule DSLs +* Compatible with the latest Java syntax, making it easy for Java programmers to get familiar quickly. Business personnel familiar with C-like languages will also find it very convenient to use +* Native JSON support for quickly defining complex data structures +* Friendly error reporting - whether compilation or runtime errors, it can precisely and friendly indicate error locations +* Unique expression tracing functionality that can trace the calculated values at intermediate nodes of expressions, helping business personnel or AI perform root cause analysis of online rule calculation results +* Secure by default - scripts are not allowed to interact with application code by default, but if interaction is needed, you can also define secure interaction methods yourself +* Interpreted execution, doesn't occupy JVM metaspace, can enable caching to improve interpretation performance +* Lightweight code with minimal dependencies, suitable for all Java runtime environments + +QLExpress4, as the latest evolution of QLExpress, rewrote the parsing engine based on Antlr4, further enhancing the original advantages and fully embracing functional programming, with further improvements in both performance and expressive power. + +Usage scenarios: + +* E-commerce coupon rule configuration: Quickly implement coupon rule DSLs through QLExpress custom functions and operators, allowing operations personnel to dynamically configure according to requirements +* Form building control association rule configuration: Form building platforms allow users to drag and drop controls to build custom forms, using QLExpress scripts to configure associations between different controls +* Workflow engine condition rule configuration +* Advertising system billing rule configuration + +\...\... + +== New Version Features + +The new version is not a simple functional refactoring of the old version, but our exploration of the next generation rule expression engine based on insights into user needs. It has many very practical but missing important features in other engines. + +=== Expression Calculation Tracing + +After business personnel complete rule script configuration, it's difficult to perceive their online execution situation. For example, in e-commerce promotion rules requiring users to satisfy the rule `isVip && not logged in for more than 10 days`. How many online users are blocked by the vip condition, and how many users are blocked by the login condition? This is just a simple rule with only two conditions, but the actual online situation is much more complex. + +Tracing online rule execution not only helps business personnel understand the actual online situation and troubleshoot and fix problems, but the accumulated data is also very valuable and can be used for subsequent rule optimization and business decisions. Below is a simplified product diagram of a rule platform performing root cause analysis and annotation decisions on rules based on QLExpress4's expression tracing capability: + +image::images/order_rules_cn.png[] + +The principle of root cause analysis lies in using QLExpress4's expression tracing capability to obtain the value of each intermediate result during expression calculation, and accordingly determine the cause of the final execution result. + +For specific usage methods, refer to: link:#expression-calculation-tracing-1[Expression Calculation Tracing] + +=== Native JSON Syntax Support + +QLExpress4 natively supports JSON syntax and can quickly define complex data structures. + +JSON arrays represent lists (List), while JSON objects represent mappings (Map), and complex objects can also be directly defined. + +Products can implement JSON mapping rules based on this feature, allowing users to conveniently define mapping relationships from one model to another. Below is a simplified product diagram of a rule platform implementing model mapping based on this capability: + +image::images/json_map.png[] + +For specific usage methods, refer to: link:#convenient-syntax-elements[Convenient Syntax Elements] + +=== Convenient String Processing + +QLExpress4 has targeted enhancements to string processing capabilities, allowing expressions to be directly embedded in strings through `$\{expression}`. + +For specific usage methods, refer to: link:#dynamic-strings[Dynamic Strings] + +=== Attachment Pass-through + +Normally, all information needed for script execution is in the `context`. Keys in the context can be referenced as variables in scripts and ultimately passed to custom functions or operators. + +However, for security or convenience reasons, some information is not wanted to be referenced by users through variables, such as tenant names, passwords, etc. + +At this time, this information can be passed to custom functions or operators through attachments. + +For specific usage methods, refer to: link:#adding-custom-functions-and-operators[Adding Custom Functions and Operators], where the `hello` custom function returns different welcome messages according to different tenants in the attachment. + +=== Functional Programming + +Functions are elevated to first-class citizens in QLExpress4, can be used as variables, and can also be returned as function return values. They can also be easily combined with common functional APIs in Java (such as Stream). + +Here's a simple QLExpress example script: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/lambda/lambda_doc.ql[] +---- + +For more usage methods, refer to: + +* link:#lambda-expressions[Lambda Expressions] +* link:#list-filtering-and-mapping[List Filtering and Mapping] +* link:#stream-api[Stream API] +* link:#functional-interfaces[Functional Interfaces] + +=== Semicolon Simplification + +QLExpress4 supports omitting semicolons, making expressions more concise. For details, refer to link:#semicolons[Semicolons] + +== API Quick Start + +=== Adding Dependencies + +[source,xml] +---- + + com.alibaba + qlexpress4 + 4.0.7 + +---- + +Requirements + +* JDK 8 or higher + + +=== First QLExpress Program + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl] +---- + +More expression execution methods can be found in the documentation link:docs/execute-en.adoc[Expression Execution] + +=== Adding Custom Functions and Operators + +The simplest way is to quickly define function/operator logic through Java Lambda expressions: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addFunctionAndOperator] +---- + +If the logic of custom functions is complex, or you need to obtain script context information, you can also implement it by inheriting `CustomFunction`. + +For example, the following `hello` custom function returns different welcome messages according to different tenants: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/test/function/HelloFunction.java[] +---- + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=customComplexFunction] +---- + +QLExpress4 also supports adding custom functions through QLExpress scripts. Note that variables defined outside of functions (such as defineTime in the example) are initialized when the function is defined and will not be recalculated when the function is subsequently called. + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addFunctionsDefinedInScript] +---- + +It is recommended to use Java to define custom functions as much as possible, as this can provide better performance and stability. + +For more ways to customize syntax elements, see the documentation link:docs/custom-item-en.adoc[Custom Syntax Elements]. + +=== Validating Syntax Correctness + +To validate syntax correctness without executing scripts, including operator restriction validation: call `check` and catch exceptions. If `QLSyntaxException` is caught, it indicates syntax errors exist. + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=checkSyntax] +---- + +You can use `CheckOptions` to configure more fine-grained syntax validation rules, supporting the following two options: + +1. `operatorCheckStrategy`: Operator validation strategy, used to restrict which operators can be used in the script +2. `disableFunctionCalls`: Whether to disable function calls, default is false + +Example 1: Using Operator Validation Strategy (Whitelist) + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/OperatorLimitTest.java[tag=operatorCheckStrategyExample] +---- + +Example 2: Disabling Function Calls + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=disableFunctionCallsExample] +---- + +=== Parsing External Variables Required by Scripts + +Some variables used in scripts are generated within the script, while others need to be passed in from outside through `context`. + +QLExpress4 provides a method to parse all variables that need to be passed in from outside in the script: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=getOutVarNames] +---- + +More script-dependency parsing tools: + +* `getOutFunctions` – parses all functions that must be defined externally +* `getOutVarAttrs` – parses all variables (and the attributes they reference) that must be supplied from outside; an enhanced version of `getOutVarNames` + +=== High-Precision Calculation + +QLExpress internally uses BigDecimal to represent all numbers that cannot be precisely represented by double, to represent calculation precision as much as possible: + +> Example: 0.1 cannot be precisely represented in double + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=bigDecimalForPrecise] +---- + +This approach can solve some calculation precision problems: + +For example, 0.1+0.2 is not equal to 0.3 in Java due to precision issues. +QLExpress can automatically identify that 0.1 and 0.2 cannot be precisely represented by double precision, and change to use BigDecimal representation to ensure the result equals 0.3 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=preciseComparisonWithJava] +---- + +In addition to the default precision guarantee, there's also a `precise` switch. When turned on, all calculations use BigDecimal to prevent problems caused by low-precision numbers passed in from outside: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=preciseSwitch] +---- + +=== Security Strategy + +QLExpress4 adopts isolation security strategy by default, not allowing scripts to access Java object fields and methods, which ensures script execution security. If you need to access Java objects, you can configure through different security strategies. + +Assuming the application has the following Java class: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/inport/MyDesk.java[] +---- + +The script execution context is set as follows: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyContextSetup] +---- + +QLExpress4 provides four security strategies: + +==== 1. Isolation Strategy (Default) + +By default, QLExpress4 adopts isolation strategy, not allowing access to any fields and methods: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyIsolation] +---- + +==== 2. Blacklist Strategy + +Through blacklist strategy, you can prohibit access to specific fields or methods, while other fields and methods can be accessed normally: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyBlackList] +---- + +==== 3. Whitelist Strategy + +Through whitelist strategy, only specified fields or methods are allowed to be accessed, while other fields and methods are prohibited: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyWhiteList] +---- + +==== 4. Open Strategy + +Open strategy allows access to all fields and methods, similar to QLExpress3 behavior, but security risks need to be noted: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyOpen] +---- + +> Note: While open strategy provides maximum flexibility, it also brings security risks. It's recommended to use only in trusted environments and not recommended for processing end-user input scripts. + +==== Strategy Recommendations + +It's recommended to directly adopt the default strategy and not directly call Java object fields and methods in scripts. Instead, provide system functionality to embedded scripts through custom functions and operators (refer to link:#adding-custom-functions-and-operators[Adding Custom Functions and Operators]). This can ensure both script security and flexibility, with better user experience. + +If you do need to call Java object fields and methods, at least use whitelist strategy to provide limited access permissions to scripts. + +As for blacklist and open strategies, they're not recommended for external input script scenarios unless you ensure each script will be reviewed. + +=== Calling Java Classes in Applications + +> Requires relaxing security strategy, not recommended for end-user input + +Assuming the application has the following Java class (`com.alibaba.qlexpress4.QLImportTester`): + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/QLImportTester.java[] +---- + +In QLExpress, there are two calling methods. + +==== 1. Using `import` Statement in Scripts to Import Classes and Use + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=importJavaCls] +---- + +==== 2. Default Import When Creating `Express4Runner`, No Additional `import` Statement Needed in Scripts + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=defaultImport] +---- + +In addition to using `ImportManager.importCls` to import individual classes, there are other more convenient import methods: + + * `ImportManager.importPack` directly imports all classes under a package path, such as `ImportManager.importPack("java.util")` will import all classes under the `java.util` package. QLExpress defaults to importing the following packages: + ** `ImportManager.importPack("java.lang")` + ** `ImportManager.importPack("java.util")` + ** `ImportManager.importPack("java.math")` + ** `ImportManager.importPack("java.util.stream")` + ** `ImportManager.importPack("java.util.function")` + * `ImportManager.importInnerCls` imports all inner classes in a given class path + +=== Custom ClassLoader + +QLExpress4 supports specifying class loaders through custom `ClassSupplier`, which is very useful in scenarios like plugin architecture and modular applications. Through custom class loaders, QLExpress scripts can access classes in specific ClassLoaders. + +The following example shows how to integrate with the link:https://pf4j.org/[PF4J] plugin framework to allow QLExpress scripts to access classes in plugins: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/pf4j/Pf4jClassSupplierTest.java[tag=pluginClassSupplier] +---- + +Typical application scenarios for custom ClassSupplier: + +* **Plugin Architecture**: Allow scripts to access classes and interfaces defined in plugins +* **Modular Applications**: In modular frameworks like OSGi, allow scripts to access classes in specific modules +* **Dynamic Class Loading**: Load classes from remote repositories or dynamically generated bytecode +* **Class Isolation**: Use different ClassLoaders to achieve class isolation + +=== Expression Caching + +Through the `cache` option, you can enable expression caching, so the same expressions won't be recompiled, greatly improving performance. + +Note that this cache has no size limit and is only suitable for use when expressions are in limited quantities: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=cacheSwitch] +---- + +However, when scripts are executed for the first time, they're still relatively slow because there's no cache. + +You can cache scripts before first execution using the following method to ensure first execution speed: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=parseToCache] +---- + +Note that this cache has an unlimited size; be sure to control its size in your application. You can periodically clear the compilation cache by calling the `clearCompileCache` method. + +=== Clearing DFA Cache + +QLExpress uses ANTLR4 as its parsing engine. ANTLR4 builds a DFA (Deterministic Finite Automaton) cache at runtime to accelerate subsequent syntax parsing. This cache consumes a certain amount of memory. + +In some memory-sensitive scenarios, you can call the `clearDFACache` method to clear the DFA cache and release memory. + +> **IMPORTANT WARNING**: Clearing the DFA cache will cause a significant compilation performance degradation. It is NOT recommended for normal use cases. + +==== Use Cases + +* **Memory-sensitive applications**: When memory usage is a critical concern and you can tolerate slower compilation times +* **Infrequently changing scripts**: When scripts are relatively stable and not frequently recompiled + +==== Best Practice + +Call this method immediately after parsing and caching your expression, and ensure all subsequent executions use the cached option to avoid recompilation. Example code: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/ClearDfaCacheTest.java[tag=clearDFACacheBestPractice] +---- + +By following this approach, you can minimize memory usage while maintaining performance. + +=== Setting Timeout + +You can set a timeout for scripts to prevent infinite loops or other reasons from causing excessive consumption of application resources. + +The following example code sets a 10ms timeout for the script: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=scripTimeout] +---- + +> Note: For system performance considerations, QLExpress's detection of timeout is not accurate. Especially for timeouts occurring in Java code callbacks (such as custom functions or operators), they won't be detected immediately. Only after execution is complete and back to QLExpress runtime will they be detected and execution interrupted. + +=== Extension Functions + +Using QLExpress's extension function capability, you can add additional member methods to Java classes. + +Extension functions are implemented based on QLExpress runtime, so they're only effective in QLExpress scripts. + +The following example code adds a `hello()` extension function to the String class: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=extensionFunction] +---- + +=== Java Class Object, Field, and Method Aliases + +QLExpress supports defining one or more aliases for objects, fields, or methods through the `QLAlias` annotation, making it convenient for non-technical personnel to use expressions to define rules. + +In the following example, the final order amount is calculated based on whether the user is a vip. + +User class definition: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/test/qlalias/User.java[] +---- + +Order class definition: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/test/qlalias/Order.java[] +---- + +Calculate final order amount through QLExpress script rules: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=qlAlias] +---- + +=== Keyword, Operator, and Function Aliases + +To further facilitate non-technical personnel in writing rules, QLExpress provides `addAlias` to add aliases to original keywords, operators, and functions, making the entire script expression more natural language-like. + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addAlias] +---- + +Keywords that support setting aliases include: + + * if + * then + * else + * for + * while + * break + * continue + * return + * function + * macro + * new + * null + * true + * false + +> Note: Some familiar usages are actually operators, not keywords, such as the `in` operator. All operators and functions support aliases by default + +=== Macros + +Macros are a powerful code reuse mechanism in QLExpress that allows users to define reusable script fragments and call them when needed. Unlike simple text replacement, QLExpress macros are implemented based on instruction replay mechanism, providing better performance and semantic accuracy. + +Macros are particularly suitable for the following scenarios: + +* **Code Reuse**: Encapsulate commonly used script fragments into macros to avoid repeatedly writing the same logic +* **Business Rule Templates**: Define standard business rule templates such as price calculation, permission checking, etc. +* **Flow Control**: Encapsulate complex control flows such as conditional judgment, loop logic, etc. +* **DSL Construction**: As basic components for building domain-specific languages + +Macros can be defined in two ways: + +**1. Using `macro` keyword in scripts** + +[source,java] +---- +include::./src/test/resources/testsuite/independent/macro/macro.ql[tag=addMacroInScript] +---- + +**2. Adding through Java API** + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addMacro] +---- + +Differences between macros and functions: + +[cols="1,1,1"] +|=== +| Feature | Macro | Function +| Parameter Passing | No parameters, relies on context variables | Supports parameter passing +| Performance | Direct instruction insertion, no call overhead | Has function call overhead +| Scope | Shares caller's scope | Independent scope +| Applicable Scenarios | Code fragment reuse | Logic encapsulation and parameterization +|=== + +Macros are particularly suitable for code fragment reuse scenarios that don't require parameter passing and mainly rely on context variables, while functions are more suitable for scenarios requiring parameterization and independent scope. + +**Changes in macro features in QLExpress4 compared to version 3**: + + * Version 4's macro implementation is closer to the definition of macros in common programming languages, equivalent to inserting predefined code fragments at the macro's location, sharing the same scope as the call point. `return`, `continue`, and `break` in macros can affect the control flow of the caller. However, version 3's implementation is actually closer to parameterless function calls. + * Version 4's macros cannot be used as variables, only when standing alone as a line statement can they be macro-replaced. Because macros can be arbitrary scripts, not necessarily expressions with return values, using them as variables would have semantic issues. Version 3's macros are essentially parameterless function calls, so they're often used as variables. + +If you want to be compatible with version 3's macro features, it's recommended to use link:#dynamic-variables[Dynamic Variables] + +=== Dynamic Variables + +Regular "static variables" are fixed values associated with keys in the context. Dynamic variables can be expressions calculated from other variables. Dynamic variables support nesting, meaning dynamic variables can depend on another dynamic variable for calculation. + +Example: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=dynamicVar] +---- + +=== Expression Calculation Tracing + +If relevant options are enabled, QLExpress4 will return an expression trace tree along with the rule script calculation result. The structure of the expression trace tree is similar to a syntax tree, with the difference that it records intermediate results of this execution at each node. + +For example, for the expression `!true || myTest(a, 1)`, the structure of the expression trace tree is roughly as follows: + +[source] +---- + || true + / \ + ! false myTest + / / \ + true a 10 1 +---- + +It can be applied to various scenarios: + + * Help business personnel analyze and troubleshoot rule calculation results + * Sample and categorize rules judged as false online + * AI automatic diagnosis and repair of rules + +Node calculation results are placed in the `value` field of the `ExpressionTrace` object. If short-circuit occurs in the middle causing some expressions to not be calculated, the `evaluated` field of the `ExpressionTrace` object will be set to false. Code example: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=expressionTrace] +---- + +> Note: You must set the `InitOptions.traceExpression` option to true when creating `Express4Runner`, and set `QLOptions.traceExpression` to true when executing scripts for this feature to take effect. + +You can also get all expression trace points without executing scripts: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=getExpressionTracePoints] +---- + +Supported expression trace point types and corresponding child node meanings are as follows: + +[cols="1,1,1"] +|=== +| Node Type | Node Meaning | Child Node Meaning +| OPERATOR | Operator | Both operands +| FUNCTION | Function | Function parameters +| METHOD | Method | Method parameters +| FIELD | Field | Target object for field access +| LIST | List | List elements +| MAP | Field | None +| IF | Conditional branch | Condition expression, then logic block and else logic block +| RETURN | Return statement | Return expression +| VARIABLE | Variable | None +| VALUE | Literal value | None +| DEFINE_FUNCTION | Define function | None +| DEFINE_MACRO | Define macro | None +| PRIMARY | Other composite values not yet drilled down (such as dictionaries, if, etc.) | None +| STATEMENT | Other composite statements not yet drilled down (such as while, for, etc.) | None +|=== + +=== Spring Integration + +QLExpress doesn't need special integration with Spring, just a `Express4Runner` singleton is sufficient. + +The "integration" example provided here allows direct reference to any Spring Bean in QLExpress scripts. + +While this approach is very convenient, it gives scripts too much permission and freedom. It's no longer recommended. It's still recommended to only put objects that users are allowed to access in the context. + +Core integration components: + +* link:src/test/java/com/alibaba/qlexpress4/spring/QLSpringContext.java[QLSpringContext]: Implements the `ExpressContext` interface, providing access capability to the Spring container. It first looks for variables from the passed context, and if not found, tries to get beans with the same name from the Spring container. +* link:src/test/java/com/alibaba/qlexpress4/spring/QLExecuteService.java[QLExecuteService]: Encapsulates QLExpress execution logic, integrates with Spring container, convenient for use in Spring applications. + +Assuming there's a Spring Bean named `helloService`: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/spring/HelloService.java[] +---- + +Call this Bean in scripts: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/spring/SpringDemoTest.java[] +---- + + +== Syntax Introduction + +QLExpress4 is compatible with Java8 syntax while also providing many more flexible and loose syntax patterns to help users write expressions more quickly. + +Based on expression-first syntax design, complex conditional judgment statements can also be directly used as expressions. + +Code snippets appearing in this chapter are all qlexpress scripts. +`assert` is an assertion method injected by the test framework into the engine, ensuring its parameter is `true`. +`assertErrCode` ensures that the lambda parameter expression execution will definitely throw a QLException containing the second parameter error code. + +=== Variable Declaration + +Supports both static typing and dynamic typing: + + * When declaring variables without writing types, the variable is dynamic type and also an assignment expression + * When declaring variables with types, it's static type, and at this time it's a variable declaration statement + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/dynamic_typing.ql[] +---- + +=== Convenient Syntax Elements + +Common syntax elements like lists (List), mappings (Map), etc. all have very convenient construction syntax sugar in QLExpress: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql[] +---- + +Through the `*.` operator, you can quickly process lists and mappings, such as taking properties of list elements, or getting key lists and value lists of mappings: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/spread/list_spread.ql[tag=spreadExample] +---- + +In mappings, by specifying the fully qualified name of the type through the `@class` key, you can directly use JSON to create complex Java objects. For example, the following MyHome is a Java class with complex nested types: + +[source,java] +---- +include::./src/test/java/com/alibaba/qlexpress4/inport/MyHome.java[] +---- + +You can conveniently create it through the following QLExpress script: + +> Note: This feature requires opening security options as referenced in link:#security-strategy[Security Strategy] to execute normally. + +[source,java] +---- +include::./src/test/resources/testsuite/java/map/classified_json.ql[tag=classifiedJson] +---- + +=== Numbers + +For numbers without declared types, +QLExpress will automatically select the most appropriate one from data types like int, long, BigInteger, double, BigDecimal based on their range: + +[source,java] +---- +include::./src/test/resources/testsuite/java/number/number_auto_type.ql[] +---- + +Therefore, when writing custom functions or operators, it's recommended to use Number type for receiving, because numeric types cannot be determined in advance. + +=== Dynamic Strings + +Dynamic strings are a new capability introduced in QLExpress version 4 to enhance string processing capabilities. + +Supports inserting expression calculations in strings through the `$\{expression}` format: + +> If you want to keep `$\{expression}` as-is in strings, you can escape `$` using `\$` + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/dynamic_string.ql[] +---- + +If you want QLExpress4 strings to maintain compatibility with version 3 and not process interpolation expressions, you can directly turn off this feature when creating `Express4Runner`: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=disableInterpolation] +---- + +=== Template Rendering + +Leveraging dynamic strings, QLExpress4 can also be used as a lightweight template engine. + +You don't need to manually add string quotes in the script. Just call `executeTemplate` to render the template string: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=templateEngine] +---- + +=== Placeholders + +Placeholders are used to extract values of arbitrary keys from the context. + +Global variables can also extract values from the context, but are limited by QLExpress keywords and syntax, so the keys that can be extracted are limited. +For example, the value corresponding to key "0" in the context cannot be extracted through variables, because 0 is not a legal variable in QLExpress, but a numeric constant. +At this time, you can use the default placeholder `$\{0}` to extract it. + +> Note the distinction from interpolation in dynamic strings. Placeholders are written outside strings. Dynamic string interpolation is `$\{expression}`, where expressions are written by default, and the running result of `"${0}"` is `"0"`. Placeholders are `$\{placeholder}`, where keys in the context are written by default, and the running result of `${0}` is the value corresponding to key "0" in the context. + +QLExpress uses `${placeholder}` format placeholders by default, where: + +* `${` is the start marker +* `}` is the end marker +* `placeholder` is the placeholder content, corresponding to the key in the context + +In addition to default placeholders, QLExpress also supports customizing the start and end markers of placeholders: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=customSelector] +---- + +Custom placeholders are not arbitrary, with the following restrictions: + +* **Start Marker Restriction**: `selectorStart` must be one of the following four formats: + ** `${` (default) + ** `$[` + ** `#{` + ** `#[` +* **End Marker Restriction**: `selectorEnd` must be a string of 1 or more characters + +=== Semicolons + +Expression statements can omit ending semicolons, and the return value of the entire script is the calculation result of the last expression. + +The return value of the following script is 2: + +[source,java] +---- +a = 1 +b = 2 +// last express +1+1 +---- + +Equivalent to the following写法: + +[source,java] +---- +a = 1 +b = 2 +// return statment +return 1+1; +---- + +Because semicolons can be omitted, QLExpress4's handling of line breaks is stricter compared to version 3 or Java language. If you want to split multi-line expressions into multiple lines, it's recommended to keep operators on the current line and move the right operand to the next line. + +The following multi-line expression will report a syntax error (counterexample): + +[source,java] +---- +// syntax error +a ++ b +---- + +The following is a correct line break example (positive example): + +[source,java] +---- +a + +b +---- + +Other syntax habits remain consistent with Java. + +=== Expressions + +QLExpress adopts expression-first design, where almost everything is an expression except for import, return, and loop structures. + +if statements are also expressions: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/if_as_expr.ql[] +---- + +try catch structures are also expressions: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql[] +---- + +=== Short-Circuit Evaluation + +Similar to Java, `&&` and `||` logical operations are all short-circuit evaluations. + +For example, the expression `false && (1/0)` won't cause a division by zero error because `&&` short-circuits at the initial `false`. + +Short-circuit evaluation is enabled by default, and the engine also provides options to turn off short-circuit for a specific execution: + +> One scenario for turning off short-circuit is to ensure full warm-up of expressions + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=disableShortCircuit] +---- + + +=== Control Structures + +==== if Branch + +In addition to being fully compatible with Java's `if` syntax, it also supports rule engine-like `if ... then ... else ...` syntax, where `then` can be treated as an omittable keyword: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/if.ql[] +---- + +==== while Loop + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/while.ql[] +---- + +==== for Loop + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/for.ql[] +---- + +==== for-each Loop + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/for_each.ql[] +---- + +==== try-catch + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/try_catch.ql[] +---- + +=== Function Definition + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/function.ql[] +---- + +=== Lambda Expressions + +In QLExpress4, Lambda expressions as first-class citizens can be passed as variables or returned. + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/lambda.ql[] +---- + +=== List Filtering and Mapping + +Supports direct functional filtering and mapping of list types through filter, map methods. + +Implemented by adding link:#extension-functions[Extension Functions] to list types, note the distinction from methods with the same names in Stream API. + +Compared to Stream API, it can directly operate on lists, and return values are also directly lists, making it more convenient. + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/list_map_filter.ql[] +---- + +=== Java8 Syntax Compatibility + +QLExpress can be compatible with common Java8 syntax. + +Such as link:#for-each-loop[for each loops], Stream API, functional interfaces, etc. + +==== Stream API + +You can directly use stream APIs in Java collections to operate on collections. + +Because these stream APIs are all from methods in Java, refer to link:#security-strategy[Security Strategy] to open security options for the following scripts to execute normally. + +[source,java] +---- +include::./src/test/resources/testsuite/java/stream/java_stream.ql[] +---- + +==== Functional Interfaces + +Java8 introduced functional interfaces like Function, Consumer, Predicate, etc. QLExpress's link:#lambda-expressions[Lambda Expressions] can be assigned to these interfaces or used as method parameters that receive these interfaces: + +[source,java] +---- +include::./src/test/resources/testsuite/java/lambda/java_functional_interface.ql[] +---- + +== Appendix I Upgrade Guide + +QLExpress's previous version had significant gaps with the industry in various features due to years of iteration stagnation. + +One of QLExpress4's goals is to make up for these gaps at once, so it chose to make bold upgrades while intentionally abandoning some compatibility. Of course, basic functionality and experience still align with the previous version. + +If your system already uses the old version of QLExpress, you must conduct a comprehensive regression test before upgrading to ensure all these scripts can execute normally in the new version before upgrading. + +If you don't have time or methods to verify them one by one, then upgrading is not recommended. + +If it's a new system, it's recommended to directly adopt QLExpress4. QLExpress4's ecosystem will become increasingly perfect in the future, while version 3 will be gradually abandoned. + +Below lists the main differences between the new and old versions to help users upgrade existing scripts. If anything is missing, feedback is welcome: + +=== Default Security Strategy + +If you completely use default options, accessing Java object fields (`o.field`) or calling member methods (`o.method()`) will throw `FIELD_NOT_FOUND` and `METHOD_NOT_FOUND` errors respectively. + +This is because version 3 could unrestrictedly access any fields and methods in Java application systems through reflection, which is considered unsafe in embedded scripts. + +If you want to be compatible with version 3's behavior, when creating `Express4Runner`, you need to set the security strategy to "open", refer to the following code: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyOpen] +---- + +For details, refer to the link:#security-strategy[Security Strategy] chapter. + +=== Defining Mappings + +QLExpress old version supported quickly creating mappings through `NewMap(key:value)` syntax. Although not discussed in detail in documentation, many users learned and used this syntax through unit tests and inquiries. + +However, this syntax was too customized and differed greatly from industry standards, so it was removed in the new version. + +The new version natively supports JSON syntax and directly adopts JSON dictionary format (`{key:value}`) to quickly create mappings, making it more intuitive. + +For details, refer to link:#convenient-syntax-elements[Convenient Syntax Elements] + +=== Global Variable Context Pollution + +QLExpress supports passing in a global context when executing scripts, i.e., the context parameter. + +In the old version, if global variables were defined in scripts, these variables would also be written to the context. After script execution, you could obtain the values of global variables defined in scripts through the context. + +An old version example is as follows: + +[source,java] +---- +// only for QLExpress 3.x + +String express = "a=3;a+1"; +ExpressRunner runner = new ExpressRunner(false, true); +DefaultContext context = new DefaultContext<>(); + +Object res = runner.execute(express, context, null, true, true); +// The result of the script execution should be 4 (a+1) +Assert.assertEquals(4, res); +// The variable 'a' defined in the script is also stored in the context +Assert.assertEquals(3, context.get("a")); +---- + +Based on research and feedback, we believe this would cause the global context to be "polluted" by scripts, posing security risks. + +Therefore, in QLExpress4, global variables are not written to the context by default. + +If you want to be compatible with version 3's features, you need to set the `polluteUserContext` option to `true`, refer to the following code: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=polluteUserContext] +---- + +=== Semicolons Can Be Omitted + +"Omittable semicolons" is already a standard feature of modern scripting languages, and QLExpress4 has also followed this trend - semicolons can be omitted. + +For details, refer to the link:#semicolons[Semicolons] chapter. + +=== Strict Newline Mode + +Since QLExpress4 supports omitting semicolons, the interpreter needs to use newlines to determine whether a statement has ended. Therefore, QLExpress4 has stricter requirements for newlines than QLExpress3. + +The following script is valid in QLExpress3 but not in QLExpress4: + +[source,java] +---- +// Valid in QLExpress3, but not in QLExpress4 +商家应收= + 价格 + - 饭卡商家承担 + + 平台补贴 +---- + +In QLExpress4, the above script will be parsed as two separate statements: +1. `商家应收 = 价格` +2. `- 饭卡商家承担 + 平台补贴` (the second statement will result in a syntax error) + +To achieve the same effect in QLExpress4, you need to place the operator at the end of the line rather than at the beginning: + +[source,java] +---- +// Correct way in QLExpress4 +商家应收= + 价格 - + 饭卡商家承担 + + 平台补贴 +---- + +This way, the interpreter knows that the current line's expression is not yet complete and will continue reading the next line. + +If you need to be compatible with QLExpress3's newline behavior, you can set the `strictNewLines` option to `false`: + +[source,java] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=notStrictNewLinesTest] +---- + +Note: Non-strict newline mode causes the interpreter to ignore all newlines, which may affect code readability and the accuracy of error messages. It is recommended to use this only when you need to be compatible with legacy code. + +=== Obtaining char Type + +In QLExpress 3, single characters wrapped in single quotes were parsed as char type, not String. + +This caused much confusion for users, for example, the result of `"a"=='a'` would be `false`. + +So later in QLExpress 3, the `ExpressRunner.setIgnoreConstChar` option was added. When set to `true`, all characters wrapped in single quotes and double quotes would be parsed as String type. However, this option was turned off by default and required users to manually enable it. + +Considering that script users rarely use the `char` primitive type, we directly removed this option in QLExpress 4. All characters wrapped in single quotes and double quotes are now parsed as String type. + +If you still need to use `char` type in scripts, you can obtain it through two methods: + +* Type casting: `(char) 'a'` +* Type declaration: `char a = 'a'` + +== Appendix II How to Contribute? + +QLExpress is completely open to community changes. Any suggestions and modifications will be welcome, and after discussion, reasonable ones will be accepted into the main branch. + +First, you need to clone the code to local. Before formally modifying the code, you need to prepare as follows: + +1. Execute `mvn compile` in the project root directory: When the project is first downloaded locally, there will be many classes not found, so you need to generate Antlr4 runtime code first +2. Configure code formatting: QLExpress project has unified code format specifications, and automatic formatting needs to be configured before git commits during development + +Create a new file `.git/hooks/pre-commit` in the project directory with the following content: + +[source,bash] +---- +#!/bin/sh +mvn spotless:apply +git add -u +exit 0 +---- + +This way, before each git commit, maven's spotless plugin will automatically execute code formatting. For specific code format configuration, see link:spotless_eclipse_formatter.xml[] + +3. Run unit tests: After completing code development, run all unit tests locally to ensure code quality + - JDK 8 environment: Execute `mvn test` + - JDK 9 and above environment: Execute `mvn test -DargLine="--add-opens java.base/java.util.stream=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED"` + +== Appendix III QLExpress4 Performance Improvements + +link:https://www.yuque.com/xuanheng-ffjti/iunlps/pgfzw46zel2xfnie?singleDoc#%20%E3%80%8AQLExpress3%E4%B8%8E4%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94%E3%80%8B[QLExpress4 vs 3 Performance Comparison] + +Summary: In common scenarios, without compilation cache, QLExpress4 can achieve nearly 10x performance improvement over version 3; with compilation cache, there's also a 1x performance improvement. + +== Appendix IV Developer Contact Information + + * Email: + ** qinyuan.dqy@alibaba-inc.com + ** yumin.pym@taobao.com + ** 704643716@qq.com + * WeChat: + ** xuanheng: dqy932087612 + ** binggou: pymbupt + ** linxiang: tkk33362 + * DingTalk Support Group + +image::images/qlexpress_support_group_qr_2026.jpg[] diff --git a/README-EN.adoc b/README-EN.adoc new file mode 100644 index 0000000..1d72485 --- /dev/null +++ b/README-EN.adoc @@ -0,0 +1,1893 @@ +:toc: + += QLExpress + +image::images/logo.png[] + +link:README.adoc[【中文版】] | [English] + +image::https://api.star-history.com/svg?repos=alibaba/QLExpress&type=Date[Star History Chart] + +== Background Introduction + +QLExpress is an embedded Java dynamic scripting tool evolved from Alibaba's e-commerce business rules. It has strong influence within Alibaba Group, and was open-sourced in 2012 to continuously optimize itself and promote the spirit of open source contribution. + +Based on basic expression calculation, it has the following features: + +* Flexible customization capabilities - through Java API custom functions and operators, you can quickly implement business rule DSLs +* Compatible with the latest Java syntax, making it easy for Java programmers to get familiar quickly. Business personnel familiar with C-like languages will also find it very convenient to use +* Native JSON support for quickly defining complex data structures +* Friendly error reporting - whether compilation or runtime errors, it can precisely and friendly indicate error locations +* Unique expression tracing functionality that can trace the calculated values at intermediate nodes of expressions, helping business personnel or AI perform root cause analysis of online rule calculation results +* Secure by default - scripts are not allowed to interact with application code by default, but if interaction is needed, you can also define secure interaction methods yourself +* Interpreted execution, doesn't occupy JVM metaspace, can enable caching to improve interpretation performance +* Lightweight code with minimal dependencies, suitable for all Java runtime environments + +QLExpress4, as the latest evolution of QLExpress, rewrote the parsing engine based on Antlr4, further enhancing the original advantages and fully embracing functional programming, with further improvements in both performance and expressive power. + +Usage scenarios: + +* E-commerce coupon rule configuration: Quickly implement coupon rule DSLs through QLExpress custom functions and operators, allowing operations personnel to dynamically configure according to requirements +* Form building control association rule configuration: Form building platforms allow users to drag and drop controls to build custom forms, using QLExpress scripts to configure associations between different controls +* Workflow engine condition rule configuration +* Advertising system billing rule configuration + +\...\... + +== New Version Features + +The new version is not a simple functional refactoring of the old version, but our exploration of the next generation rule expression engine based on insights into user needs. It has many very practical but missing important features in other engines. + +=== Expression Calculation Tracing + +After business personnel complete rule script configuration, it's difficult to perceive their online execution situation. For example, in e-commerce promotion rules requiring users to satisfy the rule `isVip && not logged in for more than 10 days`. How many online users are blocked by the vip condition, and how many users are blocked by the login condition? This is just a simple rule with only two conditions, but the actual online situation is much more complex. + +Tracing online rule execution not only helps business personnel understand the actual online situation and troubleshoot and fix problems, but the accumulated data is also very valuable and can be used for subsequent rule optimization and business decisions. Below is a simplified product diagram of a rule platform performing root cause analysis and annotation decisions on rules based on QLExpress4's expression tracing capability: + +image::images/order_rules_cn.png[] + +The principle of root cause analysis lies in using QLExpress4's expression tracing capability to obtain the value of each intermediate result during expression calculation, and accordingly determine the cause of the final execution result. + +For specific usage methods, refer to: link:#expression-calculation-tracing-1[Expression Calculation Tracing] + +=== Native JSON Syntax Support + +QLExpress4 natively supports JSON syntax and can quickly define complex data structures. + +JSON arrays represent lists (List), while JSON objects represent mappings (Map), and complex objects can also be directly defined. + +Products can implement JSON mapping rules based on this feature, allowing users to conveniently define mapping relationships from one model to another. Below is a simplified product diagram of a rule platform implementing model mapping based on this capability: + +image::images/json_map.png[] + +For specific usage methods, refer to: link:#convenient-syntax-elements[Convenient Syntax Elements] + +=== Convenient String Processing + +QLExpress4 has targeted enhancements to string processing capabilities, allowing expressions to be directly embedded in strings through `$\{expression}`. + +For specific usage methods, refer to: link:#dynamic-strings[Dynamic Strings] + +=== Attachment Pass-through + +Normally, all information needed for script execution is in the `context`. Keys in the context can be referenced as variables in scripts and ultimately passed to custom functions or operators. + +However, for security or convenience reasons, some information is not wanted to be referenced by users through variables, such as tenant names, passwords, etc. + +At this time, this information can be passed to custom functions or operators through attachments. + +For specific usage methods, refer to: link:#adding-custom-functions-and-operators[Adding Custom Functions and Operators], where the `hello` custom function returns different welcome messages according to different tenants in the attachment. + +=== Functional Programming + +Functions are elevated to first-class citizens in QLExpress4, can be used as variables, and can also be returned as function return values. They can also be easily combined with common functional APIs in Java (such as Stream). + +Here's a simple QLExpress example script: + +[source,java] +---- +add = (a, b) -> { + return a + b; +}; +i = add(1,2); +assert(i == 3); +---- + +For more usage methods, refer to: + +* link:#lambda-expressions[Lambda Expressions] +* link:#list-filtering-and-mapping[List Filtering and Mapping] +* link:#stream-api[Stream API] +* link:#functional-interfaces[Functional Interfaces] + +=== Semicolon Simplification + +QLExpress4 supports omitting semicolons, making expressions more concise. For details, refer to link:#semicolons[Semicolons] + +== API Quick Start + +=== Adding Dependencies + +[source,xml] +---- + + com.alibaba + qlexpress4 + 4.0.7 + +---- + +Requirements + +* JDK 8 or higher + + +=== First QLExpress Program + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map context = new HashMap<>(); + context.put("a", 1); + context.put("b", 2); + context.put("c", 3); + Object result = express4Runner.execute("a + b * c", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(7, result); +---- + +More expression execution methods can be found in the documentation link:docs/execute-en.adoc[Expression Execution] + +=== Adding Custom Functions and Operators + +The simplest way is to quickly define function/operator logic through Java Lambda expressions: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // custom function + express4Runner.addVarArgsFunction("join", + params -> Arrays.stream(params).map(Object::toString).collect(Collectors.joining(","))); + Object resultFunction = + express4Runner.execute("join(1,2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultFunction); + + // custom operator + express4Runner.addOperatorBiFunction("join", (left, right) -> left + "," + right); + Object resultOperator = + express4Runner.execute("1 join 2 join 3", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultOperator); +---- + +If the logic of custom functions is complex, or you need to obtain script context information, you can also implement it by inheriting `CustomFunction`. + +For example, the following `hello` custom function returns different welcome messages according to different tenants: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.test.function; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +public class HelloFunction implements CustomFunction { + @Override + public Object call(QContext qContext, Parameters parameters) + throws Throwable { + String tenant = (String)qContext.attachment().get("tenant"); + return "hello," + tenant; + } +} +---- + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addFunction("hello", new HelloFunction()); + String resultJack = (String)express4Runner.execute("hello()", + Collections.emptyMap(), + // Additional information(tenant for example) can be brought into the custom function from outside via attachments + QLOptions.builder().attachments(Collections.singletonMap("tenant", "jack")).build()).getResult(); + assertEquals("hello,jack", resultJack); + String resultLucy = + (String)express4Runner + .execute("hello()", + Collections.emptyMap(), + QLOptions.builder().attachments(Collections.singletonMap("tenant", "lucy")).build()) + .getResult(); + assertEquals("hello,lucy", resultLucy); +---- + +QLExpress4 also supports adding custom functions through QLExpress scripts. Note that variables defined outside of functions (such as defineTime in the example) are initialized when the function is defined and will not be recalculated when the function is subsequently called. + +[source,java,indent=0] +---- + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + BatchAddFunctionResult addResult = express4Runner.addFunctionsDefinedInScript( + "function myAdd(a,b) {\n" + " return a+b;" + "}\n" + "\n" + "function getCurrentTime() {\n" + + " return System.currentTimeMillis();\n" + "}" + "\n" + "defineTime=System.currentTimeMillis();\n" + + "function defineTime() {\n" + " return defineTime;" + "}\n", + ExpressContext.EMPTY_CONTEXT, + QLOptions.DEFAULT_OPTIONS); + assertEquals(3, addResult.getSucc().size()); + QLResult result = express4Runner.execute("myAdd(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertEquals(3, result.getResult()); + + QLResult resultCurTime1 = + express4Runner.execute("getCurrentTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + Thread.sleep(1000); + QLResult resultCurTime2 = + express4Runner.execute("getCurrentTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertNotSame(resultCurTime1.getResult(), resultCurTime2.getResult()); + + /* + * The defineTime variable is defined outside the function and is initialized when the function is defined; + * it is not recalculated afterward, so the value returned is always the time at which the function was defined. + */ + QLResult resultDefineTime1 = + express4Runner.execute("defineTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + Thread.sleep(1000); + QLResult resultDefineTime2 = + express4Runner.execute("defineTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertSame(resultDefineTime1.getResult(), resultDefineTime2.getResult()); +---- + +It is recommended to use Java to define custom functions as much as possible, as this can provide better performance and stability. + +For more ways to customize syntax elements, see the documentation link:docs/custom-item-en.adoc[Custom Syntax Elements]. + +=== Validating Syntax Correctness + +To validate syntax correctness without executing scripts, including operator restriction validation: call `check` and catch exceptions. If `QLSyntaxException` is caught, it indicates syntax errors exist. + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + express4Runner.check("a+b;\n(a+b"); + fail(); + } + catch (QLSyntaxException e) { + assertEquals(2, e.getLineNo()); + assertEquals(4, e.getColNo()); + assertEquals("SYNTAX_ERROR", e.getErrorCode()); + // represents the end of script + assertEquals( + "[Error SYNTAX_ERROR: mismatched input '' expecting ')']\n" + "[Near: a+b; (a+b]\n" + + " ^^^^^\n" + "[Line: 2, Column: 4]", + e.getMessage()); + } +---- + +You can use `CheckOptions` to configure more fine-grained syntax validation rules, supporting the following two options: + +1. `operatorCheckStrategy`: Operator validation strategy, used to restrict which operators can be used in the script +2. `disableFunctionCalls`: Whether to disable function calls, default is false + +Example 1: Using Operator Validation Strategy (Whitelist) + +[source,java,indent=0] +---- + // Create a whitelist of allowed operators + Set allowedOps = new HashSet<>(Arrays.asList("+", "*")); + + // Configure check options with operator whitelist + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build(); + + // Create runner and check script with custom options + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.check("a + b * c", checkOptions); // This will pass as + and * are allowed +---- + +Example 2: Disabling Function Calls + +[source,java,indent=0] +---- + // Create a runner + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Create options with function calls disabled + CheckOptions options = CheckOptions.builder().disableFunctionCalls(true).build(); + + // Script with function call + String scriptWithFunctionCall = "Math.max(1, 2)"; + + // Use custom options to check script + try { + runner.check(scriptWithFunctionCall, options); + } + catch (QLSyntaxException e) { + // Will throw exception as function calls are disabled + } +---- + +=== Parsing External Variables Required by Scripts + +Some variables used in scripts are generated within the script, while others need to be passed in from outside through `context`. + +QLExpress4 provides a method to parse all variables that need to be passed in from outside in the script: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Set outVarNames = + express4Runner.getOutVarNames("int a = 1, b = 10;\n" + "c = 11\n" + "e = a + b + c + d\n" + "f+e"); + Set expectSet = new HashSet<>(); + expectSet.add("d"); + expectSet.add("f"); + assertEquals(expectSet, outVarNames); +---- + +More script-dependency parsing tools: + +* `getOutFunctions` – parses all functions that must be defined externally +* `getOutVarAttrs` – parses all variables (and the attributes they reference) that must be supplied from outside; an enhanced version of `getOutVarNames` + +=== High-Precision Calculation + +QLExpress internally uses BigDecimal to represent all numbers that cannot be precisely represented by double, to represent calculation precision as much as possible: + +> Example: 0.1 cannot be precisely represented in double + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = express4Runner.execute("0.1", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertTrue(result instanceof BigDecimal); +---- + +This approach can solve some calculation precision problems: + +For example, 0.1+0.2 is not equal to 0.3 in Java due to precision issues. +QLExpress can automatically identify that 0.1 and 0.2 cannot be precisely represented by double precision, and change to use BigDecimal representation to ensure the result equals 0.3 + +[source,java,indent=0] +---- + assertNotEquals(0.3, 0.1 + 0.2, 0.0); + assertTrue((Boolean)express4Runner.execute("0.3==0.1+0.2", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); +---- + +In addition to the default precision guarantee, there's also a `precise` switch. When turned on, all calculations use BigDecimal to prevent problems caused by low-precision numbers passed in from outside: + +[source,java,indent=0] +---- + Map context = new HashMap<>(); + context.put("a", 0.1); + context.put("b", 0.2); + assertFalse((Boolean)express4Runner.execute("0.3==a+b", context, QLOptions.DEFAULT_OPTIONS).getResult()); + // open precise switch + assertTrue((Boolean)express4Runner.execute("0.3==a+b", context, QLOptions.builder().precise(true).build()) + .getResult()); +---- + +=== Security Strategy + +QLExpress4 adopts isolation security strategy by default, not allowing scripts to access Java object fields and methods, which ensures script execution security. If you need to access Java objects, you can configure through different security strategies. + +Assuming the application has the following Java class: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.inport; + +/** + * Author: DQinYuan + */ +public class MyDesk { + + private String book1; + + private String book2; + + public String getBook1() { + return book1; + } + + public void setBook1(String book1) { + this.book1 = book1; + } + + public String getBook2() { + return book2; + } + + public void setBook2(String book2) { + this.book2 = book2; + } +} +---- + +The script execution context is set as follows: + +[source,java,indent=0] +---- + MyDesk desk = new MyDesk(); + desk.setBook1("Thinking in Java"); + desk.setBook2("Effective Java"); + Map context = Collections.singletonMap("desk", desk); +---- + +QLExpress4 provides four security strategies: + +==== 1. Isolation Strategy (Default) + +By default, QLExpress4 adopts isolation strategy, not allowing access to any fields and methods: + +[source,java,indent=0] +---- + // default isolation strategy, no field or method can be found + Express4Runner express4RunnerIsolation = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + assertErrorCode(express4RunnerIsolation, context, "desk.book1", "FIELD_NOT_FOUND"); + assertErrorCode(express4RunnerIsolation, context, "desk.getBook2()", "METHOD_NOT_FOUND"); +---- + +==== 2. Blacklist Strategy + +Through blacklist strategy, you can prohibit access to specific fields or methods, while other fields and methods can be accessed normally: + +[source,java,indent=0] +---- + // black list security strategy + Set memberList = new HashSet<>(); + memberList.add(MyDesk.class.getMethod("getBook2")); + Express4Runner express4RunnerBlackList = new Express4Runner( + InitOptions.builder().securityStrategy(QLSecurityStrategy.blackList(memberList)).build()); + assertErrorCode(express4RunnerBlackList, context, "desk.book2", "FIELD_NOT_FOUND"); + Object resultBlack = + express4RunnerBlackList.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertEquals("Thinking in Java", resultBlack); +---- + +==== 3. Whitelist Strategy + +Through whitelist strategy, only specified fields or methods are allowed to be accessed, while other fields and methods are prohibited: + +[source,java,indent=0] +---- + // white list security strategy + Express4Runner express4RunnerWhiteList = new Express4Runner( + InitOptions.builder().securityStrategy(QLSecurityStrategy.whiteList(memberList)).build()); + Object resultWhite = + express4RunnerWhiteList.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertEquals("Effective Java", resultWhite); + assertErrorCode(express4RunnerWhiteList, context, "desk.getBook1()", "METHOD_NOT_FOUND"); +---- + +==== 4. Open Strategy + +Open strategy allows access to all fields and methods, similar to QLExpress3 behavior, but security risks need to be noted: + +[source,java,indent=0] +---- + // open security strategy + Express4Runner express4RunnerOpen = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Assert.assertEquals("Thinking in Java", + express4RunnerOpen.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Effective Java", + express4RunnerOpen.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult()); +---- + +> Note: While open strategy provides maximum flexibility, it also brings security risks. It's recommended to use only in trusted environments and not recommended for processing end-user input scripts. + +==== Strategy Recommendations + +It's recommended to directly adopt the default strategy and not directly call Java object fields and methods in scripts. Instead, provide system functionality to embedded scripts through custom functions and operators (refer to link:#adding-custom-functions-and-operators[Adding Custom Functions and Operators]). This can ensure both script security and flexibility, with better user experience. + +If you do need to call Java object fields and methods, at least use whitelist strategy to provide limited access permissions to scripts. + +As for blacklist and open strategies, they're not recommended for external input script scenarios unless you ensure each script will be reviewed. + +=== Calling Java Classes in Applications + +> Requires relaxing security strategy, not recommended for end-user input + +Assuming the application has the following Java class (`com.alibaba.qlexpress4.QLImportTester`): + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4; + +public class QLImportTester { + + public static int add(int a, int b) { + return a + b; + } + +} +---- + +In QLExpress, there are two calling methods. + +==== 1. Using `import` Statement in Scripts to Import Classes and Use + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.builder() + // open security strategy, which allows access to all Java classes within the application. + .securityStrategy(QLSecurityStrategy.open()) + .build()); + // Import Java classes using the import statement. + Map params = new HashMap<>(); + params.put("a", 1); + params.put("b", 2); + Object result = + express4Runner + .execute("import com.alibaba.qlexpress4.QLImportTester;" + "QLImportTester.add(a,b)", + params, + QLOptions.DEFAULT_OPTIONS) + .getResult(); + Assert.assertEquals(3, result); +---- + +==== 2. Default Import When Creating `Express4Runner`, No Additional `import` Statement Needed in Scripts + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.builder() + .addDefaultImport( + Collections.singletonList(ImportManager.importCls("com.alibaba.qlexpress4.QLImportTester"))) + .securityStrategy(QLSecurityStrategy.open()) + .build()); + Object result = + express4Runner.execute("QLImportTester.add(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult(); + Assert.assertEquals(3, result); +---- + +In addition to using `ImportManager.importCls` to import individual classes, there are other more convenient import methods: + + * `ImportManager.importPack` directly imports all classes under a package path, such as `ImportManager.importPack("java.util")` will import all classes under the `java.util` package. QLExpress defaults to importing the following packages: + ** `ImportManager.importPack("java.lang")` + ** `ImportManager.importPack("java.util")` + ** `ImportManager.importPack("java.math")` + ** `ImportManager.importPack("java.util.stream")` + ** `ImportManager.importPack("java.util.function")` + * `ImportManager.importInnerCls` imports all inner classes in a given class path + +=== Custom ClassLoader + +QLExpress4 supports specifying class loaders through custom `ClassSupplier`, which is very useful in scenarios like plugin architecture and modular applications. Through custom class loaders, QLExpress scripts can access classes in specific ClassLoaders. + +The following example shows how to integrate with the link:https://pf4j.org/[PF4J] plugin framework to allow QLExpress scripts to access classes in plugins: + +[source,java,indent=0] +---- + // Specify plugin directory (test-plugins directory under test resources) + Path pluginsDir = new File("src/test/resources/test-plugins").toPath(); + PluginManager pluginManager = new DefaultPluginManager(pluginsDir); + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + + // Get the PluginClassLoader of the first plugin + PluginWrapper plugin = pluginManager.getPlugins().get(0); + ClassLoader pluginClassLoader = plugin.getPluginClassLoader(); + + // Custom class supplier using plugin ClassLoader + ClassSupplier pluginClassSupplier = clsName -> { + try { + return Class.forName(clsName, true, pluginClassLoader); + } + catch (ClassNotFoundException | NoClassDefFoundError e) { + return null; + } + }; + + InitOptions options = InitOptions.builder() + .securityStrategy(QLSecurityStrategy.open()) + .classSupplier(pluginClassSupplier) + .build(); + Express4Runner runner = new Express4Runner(options); + + String script = "import com.alibaba.qlexpress4.pf4j.TestPluginInterface; TestPluginInterface.TEST_CONSTANT"; + Object result = runner.execute(script, Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + + Assert.assertEquals("Hello from PF4J Plugin!", result.toString()); +---- + +Typical application scenarios for custom ClassSupplier: + +* **Plugin Architecture**: Allow scripts to access classes and interfaces defined in plugins +* **Modular Applications**: In modular frameworks like OSGi, allow scripts to access classes in specific modules +* **Dynamic Class Loading**: Load classes from remote repositories or dynamically generated bytecode +* **Class Isolation**: Use different ClassLoaders to achieve class isolation + +=== Expression Caching + +Through the `cache` option, you can enable expression caching, so the same expressions won't be recompiled, greatly improving performance. + +Note that this cache has no size limit and is only suitable for use when expressions are in limited quantities: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // open cache switch + express4Runner.execute("1+2", new HashMap<>(), QLOptions.builder().cache(true).build()); +---- + +However, when scripts are executed for the first time, they're still relatively slow because there's no cache. + +You can cache scripts before first execution using the following method to ensure first execution speed: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.parseToDefinitionWithCache("a+b"); +---- + +Note that this cache has an unlimited size; be sure to control its size in your application. You can periodically clear the compilation cache by calling the `clearCompileCache` method. + +=== Clearing DFA Cache + +QLExpress uses ANTLR4 as its parsing engine. ANTLR4 builds a DFA (Deterministic Finite Automaton) cache at runtime to accelerate subsequent syntax parsing. This cache consumes a certain amount of memory. + +In some memory-sensitive scenarios, you can call the `clearDFACache` method to clear the DFA cache and release memory. + +> **IMPORTANT WARNING**: Clearing the DFA cache will cause a significant compilation performance degradation. It is NOT recommended for normal use cases. + +==== Use Cases + +* **Memory-sensitive applications**: When memory usage is a critical concern and you can tolerate slower compilation times +* **Infrequently changing scripts**: When scripts are relatively stable and not frequently recompiled + +==== Best Practice + +Call this method immediately after parsing and caching your expression, and ensure all subsequent executions use the cached option to avoid recompilation. Example code: + +[source,java,indent=0] +---- + /* + * When the expression changes, parse it and add it to the expression cache; + * after parsing is complete, call clearDFACache. + */ + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.parseToDefinitionWithCache(exampleExpress); + runner.clearDFACache(); + + /* + * All subsequent runs of this script must enable the cache option to ensure that re-compilation does not occur. + */ + for (int i = 0; i < 3; i++) { + runner.execute(exampleExpress, ExpressContext.EMPTY_CONTEXT, QLOptions.builder().cache(true).build()); + } +---- + +By following this approach, you can minimize memory usage while maintaining performance. + +=== Setting Timeout + +You can set a timeout for scripts to prevent infinite loops or other reasons from causing excessive consumption of application resources. + +The following example code sets a 10ms timeout for the script: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + express4Runner.execute("while (true) {\n" + " 1+1\n" + "}", + Collections.emptyMap(), + QLOptions.builder().timeoutMillis(10L).build()); + fail("should timeout"); + } + catch (QLTimeoutException e) { + assertEquals(QLErrorCodes.SCRIPT_TIME_OUT.name(), e.getErrorCode()); + } +---- + +> Note: For system performance considerations, QLExpress's detection of timeout is not accurate. Especially for timeouts occurring in Java code callbacks (such as custom functions or operators), they won't be detected immediately. Only after execution is complete and back to QLExpress runtime will they be detected and execution interrupted. + +=== Extension Functions + +Using QLExpress's extension function capability, you can add additional member methods to Java classes. + +Extension functions are implemented based on QLExpress runtime, so they're only effective in QLExpress scripts. + +The following example code adds a `hello()` extension function to the String class: + +[source,java,indent=0] +---- + ExtensionFunction helloFunction = new ExtensionFunction() { + @Override + public Class[] getParameterTypes() { + return new Class[0]; + } + + @Override + public String getName() { + return "hello"; + } + + @Override + public Class getDeclaringClass() { + return String.class; + } + + @Override + public Object invoke(Object obj, Object[] args) { + String originStr = (String)obj; + return "Hello," + originStr; + } + }; + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addExtendFunction(helloFunction); + Object result = + express4Runner.execute("'jack'.hello()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("Hello,jack", result); + + // simpler way to define extension function + express4Runner.addExtendFunction("add", + Number.class, + params -> ((Number)params[0]).intValue() + ((Number)params[1]).intValue()); + QLResult resultAdd = express4Runner.execute("1.add(2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertEquals(3, resultAdd.getResult()); + +---- + +=== Java Class Object, Field, and Method Aliases + +QLExpress supports defining one or more aliases for objects, fields, or methods through the `QLAlias` annotation, making it convenient for non-technical personnel to use expressions to define rules. + +In the following example, the final order amount is calculated based on whether the user is a vip. + +User class definition: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.test.qlalias; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +@QLAlias("用户") +public class User { + + @QLAlias("是vip") + private boolean vip; + + @QLAlias("用户名") + private String name; + + public boolean isVip() { + return vip; + } + + public void setVip(boolean vip) { + this.vip = vip; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +---- + +Order class definition: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.test.qlalias; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +@QLAlias("订单") +public class Order { + + @QLAlias("订单号") + private String orderNum; + + @QLAlias("金额") + private int amount; + + public String getOrderNum() { + return orderNum; + } + + public void setOrderNum(String orderNum) { + this.orderNum = orderNum; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } +} +---- + +Calculate final order amount through QLExpress script rules: + +[source,java,indent=0] +---- + Order order = new Order(); + order.setOrderNum("OR123455"); + order.setAmount(100); + + User user = new User(); + user.setName("jack"); + user.setVip(true); + + // Calculate the Final Order Amount + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Number result = (Number)express4Runner + .executeWithAliasObjects("用户.是vip? 订单.金额 * 0.8 : 订单.金额", QLOptions.DEFAULT_OPTIONS, order, user) + .getResult(); + assertEquals(80, result.intValue()); +---- + +=== Keyword, Operator, and Function Aliases + +To further facilitate non-technical personnel in writing rules, QLExpress provides `addAlias` to add aliases to original keywords, operators, and functions, making the entire script expression more natural language-like. + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // add custom function zero + express4Runner.addFunction("zero", (String ignore) -> 0); + + // keyword alias + assertTrue(express4Runner.addAlias("如果", "if")); + assertTrue(express4Runner.addAlias("则", "then")); + assertTrue(express4Runner.addAlias("否则", "else")); + assertTrue(express4Runner.addAlias("返回", "return")); + // operator alias + assertTrue(express4Runner.addAlias("大于", ">")); + // function alias + assertTrue(express4Runner.addAlias("零", "zero")); + + Map context = new HashMap<>(); + context.put("语文", 90); + context.put("数学", 90); + context.put("英语", 90); + + Object result = express4Runner + .execute("如果 (语文 + 数学 + 英语 大于 270) 则 {返回 1;} 否则 {返回 零();}", context, QLOptions.DEFAULT_OPTIONS) + .getResult(); + assertEquals(0, result); +---- + +Keywords that support setting aliases include: + + * if + * then + * else + * for + * while + * break + * continue + * return + * function + * macro + * new + * null + * true + * false + +> Note: Some familiar usages are actually operators, not keywords, such as the `in` operator. All operators and functions support aliases by default + +=== Macros + +Macros are a powerful code reuse mechanism in QLExpress that allows users to define reusable script fragments and call them when needed. Unlike simple text replacement, QLExpress macros are implemented based on instruction replay mechanism, providing better performance and semantic accuracy. + +Macros are particularly suitable for the following scenarios: + +* **Code Reuse**: Encapsulate commonly used script fragments into macros to avoid repeatedly writing the same logic +* **Business Rule Templates**: Define standard business rule templates such as price calculation, permission checking, etc. +* **Flow Control**: Encapsulate complex control flows such as conditional judgment, loop logic, etc. +* **DSL Construction**: As basic components for building domain-specific languages + +Macros can be defined in two ways: + +**1. Using `macro` keyword in scripts** + +[source,java] +---- +macro add { + c = a + b; +} + +a = 1; +b = 2; +add; +assert(c == 3); +---- + +**2. Adding through Java API** + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addMacro("rename", "name='haha-'+name"); + Map context = Collections.singletonMap("name", "wuli"); + Object result = express4Runner.execute("rename", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("haha-wuli", result); + + // replace macro + express4Runner.addOrReplaceMacro("rename", "name='huhu-'+name"); + Object result1 = express4Runner.execute("rename", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("huhu-wuli", result1); +---- + +Differences between macros and functions: + +[cols="1,1,1"] +|=== +| Feature | Macro | Function +| Parameter Passing | No parameters, relies on context variables | Supports parameter passing +| Performance | Direct instruction insertion, no call overhead | Has function call overhead +| Scope | Shares caller's scope | Independent scope +| Applicable Scenarios | Code fragment reuse | Logic encapsulation and parameterization +|=== + +Macros are particularly suitable for code fragment reuse scenarios that don't require parameter passing and mainly rely on context variables, while functions are more suitable for scenarios requiring parameterization and independent scope. + +**Changes in macro features in QLExpress4 compared to version 3**: + + * Version 4's macro implementation is closer to the definition of macros in common programming languages, equivalent to inserting predefined code fragments at the macro's location, sharing the same scope as the call point. `return`, `continue`, and `break` in macros can affect the control flow of the caller. However, version 3's implementation is actually closer to parameterless function calls. + * Version 4's macros cannot be used as variables, only when standing alone as a line statement can they be macro-replaced. Because macros can be arbitrary scripts, not necessarily expressions with return values, using them as variables would have semantic issues. Version 3's macros are essentially parameterless function calls, so they're often used as variables. + +If you want to be compatible with version 3's macro features, it's recommended to use link:#dynamic-variables[Dynamic Variables] + +=== Dynamic Variables + +Regular "static variables" are fixed values associated with keys in the context. Dynamic variables can be expressions calculated from other variables. Dynamic variables support nesting, meaning dynamic variables can depend on another dynamic variable for calculation. + +Example: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + Map staticContext = new HashMap<>(); + staticContext.put("语文", 88); + staticContext.put("数学", 99); + staticContext.put("英语", 95); + + QLOptions defaultOptions = QLOptions.DEFAULT_OPTIONS; + DynamicVariableContext dynamicContext = + new DynamicVariableContext(express4Runner, staticContext, defaultOptions); + dynamicContext.put("平均成绩", "(语文+数学+英语)/3.0"); + dynamicContext.put("是否优秀", "平均成绩>90"); + + // dynamic var + assertTrue((Boolean)express4Runner.execute("是否优秀", dynamicContext, defaultOptions).getResult()); + assertEquals(94, + ((Number)express4Runner.execute("平均成绩", dynamicContext, defaultOptions).getResult()).intValue()); + // static var + assertEquals(187, + ((Number)express4Runner.execute("语文+数学", dynamicContext, defaultOptions).getResult()).intValue()); +---- + +=== Expression Calculation Tracing + +If relevant options are enabled, QLExpress4 will return an expression trace tree along with the rule script calculation result. The structure of the expression trace tree is similar to a syntax tree, with the difference that it records intermediate results of this execution at each node. + +For example, for the expression `!true || myTest(a, 1)`, the structure of the expression trace tree is roughly as follows: + +[source] +---- + || true + / \ + ! false myTest + / / \ + true a 10 1 +---- + +It can be applied to various scenarios: + + * Help business personnel analyze and troubleshoot rule calculation results + * Sample and categorize rules judged as false online + * AI automatic diagnosis and repair of rules + +Node calculation results are placed in the `value` field of the `ExpressionTrace` object. If short-circuit occurs in the middle causing some expressions to not be calculated, the `evaluated` field of the `ExpressionTrace` object will be set to false. Code example: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.builder().traceExpression(true).build()); + express4Runner.addFunction("myTest", (Predicate)i -> i > 10); + + Map context = new HashMap<>(); + context.put("a", true); + QLResult result = express4Runner + .execute("a && (!myTest(11) || false)", context, QLOptions.builder().traceExpression(true).build()); + Assert.assertFalse((Boolean)result.getResult()); + + List expressionTraces = result.getExpressionTraces(); + Assert.assertEquals(1, expressionTraces.size()); + ExpressionTrace expressionTrace = expressionTraces.get(0); + Assert.assertEquals("OPERATOR && false\n" + " | VARIABLE a true\n" + " | OPERATOR || false\n" + + " | OPERATOR ! false\n" + " | FUNCTION myTest true\n" + " | VALUE 11 11\n" + + " | VALUE false false\n", expressionTrace.toPrettyString(0)); + + // short circuit + context.put("a", false); + QLResult resultShortCircuit = express4Runner.execute("(a && true) && (!myTest(11) || false)", + context, + QLOptions.builder().traceExpression(true).build()); + Assert.assertFalse((Boolean)resultShortCircuit.getResult()); + ExpressionTrace expressionTraceShortCircuit = resultShortCircuit.getExpressionTraces().get(0); + Assert.assertEquals( + "OPERATOR && false\n" + " | OPERATOR && false\n" + " | VARIABLE a false\n" + " | VALUE true \n" + + " | OPERATOR || \n" + " | OPERATOR ! \n" + " | FUNCTION myTest \n" + + " | VALUE 11 \n" + " | VALUE false \n", + expressionTraceShortCircuit.toPrettyString(0)); + Assert.assertTrue(expressionTraceShortCircuit.getChildren().get(0).isEvaluated()); + Assert.assertFalse(expressionTraceShortCircuit.getChildren().get(1).isEvaluated()); + + // in + QLResult resultIn = express4Runner + .execute("'ab' in ['cc', 'dd', 'ff']", context, QLOptions.builder().traceExpression(true).build()); + Assert.assertFalse((Boolean)resultIn.getResult()); + ExpressionTrace expressionTraceIn = resultIn.getExpressionTraces().get(0); + Assert + .assertEquals( + "OPERATOR in false\n" + " | VALUE 'ab' ab\n" + " | LIST [ [cc, dd, ff]\n" + " | VALUE 'cc' cc\n" + + " | VALUE 'dd' dd\n" + " | VALUE 'ff' ff\n", + expressionTraceIn.toPrettyString(0)); +---- + +> Note: You must set the `InitOptions.traceExpression` option to true when creating `Express4Runner`, and set `QLOptions.traceExpression` to true when executing scripts for this feature to take effect. + +You can also get all expression trace points without executing scripts: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + TracePointTree tracePointTree = express4Runner.getExpressionTracePoints("1+3+5*ab+9").get(0); + Assert.assertEquals("OPERATOR +\n" + " | OPERATOR +\n" + " | OPERATOR +\n" + " | VALUE 1\n" + + " | VALUE 3\n" + " | OPERATOR *\n" + " | VALUE 5\n" + " | VARIABLE ab\n" + + " | VALUE 9\n", tracePointTree.toPrettyString(0)); +---- + +Supported expression trace point types and corresponding child node meanings are as follows: + +[cols="1,1,1"] +|=== +| Node Type | Node Meaning | Child Node Meaning +| OPERATOR | Operator | Both operands +| FUNCTION | Function | Function parameters +| METHOD | Method | Method parameters +| FIELD | Field | Target object for field access +| LIST | List | List elements +| MAP | Field | None +| IF | Conditional branch | Condition expression, then logic block and else logic block +| RETURN | Return statement | Return expression +| VARIABLE | Variable | None +| VALUE | Literal value | None +| DEFINE_FUNCTION | Define function | None +| DEFINE_MACRO | Define macro | None +| PRIMARY | Other composite values not yet drilled down (such as dictionaries, if, etc.) | None +| STATEMENT | Other composite statements not yet drilled down (such as while, for, etc.) | None +|=== + +=== Spring Integration + +QLExpress doesn't need special integration with Spring, just a `Express4Runner` singleton is sufficient. + +The "integration" example provided here allows direct reference to any Spring Bean in QLExpress scripts. + +While this approach is very convenient, it gives scripts too much permission and freedom. It's no longer recommended. It's still recommended to only put objects that users are allowed to access in the context. + +Core integration components: + +* link:src/test/java/com/alibaba/qlexpress4/spring/QLSpringContext.java[QLSpringContext]: Implements the `ExpressContext` interface, providing access capability to the Spring container. It first looks for variables from the passed context, and if not found, tries to get beans with the same name from the Spring container. +* link:src/test/java/com/alibaba/qlexpress4/spring/QLExecuteService.java[QLExecuteService]: Encapsulates QLExpress execution logic, integrates with Spring container, convenient for use in Spring applications. + +Assuming there's a Spring Bean named `helloService`: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.spring; + +import org.springframework.stereotype.Service; + +/** + * Spring Bean example service class + */ +@Service +public class HelloService { + + /** + * Hello method that returns a greeting string + * @return greeting string + */ + public String hello(String name) { + return "Hello, " + name + "!"; + } +} +---- + +Call this Bean in scripts: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.spring; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.HashMap; +import java.util.Map; + +/** + * HelloService unit test class + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = SpringTestConfig.class) +public class SpringDemoTest { + + @Autowired + private QLExecuteService qlExecuteService; + + @Test + public void qlExecuteWithSpringContextTest() { + Map context = new HashMap<>(); + context.put("name", "Wang"); + String result = (String)qlExecuteService.execute("helloService.hello(name)", context); + Assert.assertEquals("Hello, Wang!", result); + } +} +---- + + +== Syntax Introduction + +QLExpress4 is compatible with Java8 syntax while also providing many more flexible and loose syntax patterns to help users write expressions more quickly. + +Based on expression-first syntax design, complex conditional judgment statements can also be directly used as expressions. + +Code snippets appearing in this chapter are all qlexpress scripts. +`assert` is an assertion method injected by the test framework into the engine, ensuring its parameter is `true`. +`assertErrCode` ensures that the lambda parameter expression execution will definitely throw a QLException containing the second parameter error code. + +=== Variable Declaration + +Supports both static typing and dynamic typing: + + * When declaring variables without writing types, the variable is dynamic type and also an assignment expression + * When declaring variables with types, it's static type, and at this time it's a variable declaration statement + +[source,java] +---- +// Dynamic Typeing +a = 1; +a = "1"; +// Static Typing +int b = 2; +// throw QLException with error code INCOMPATIBLE_ASSIGNMENT_TYPE when assign with incompatible type String +assertErrorCode(() -> b = "1", "INCOMPATIBLE_ASSIGNMENT_TYPE") + +---- + +=== Convenient Syntax Elements + +Common syntax elements like lists (List), mappings (Map), etc. all have very convenient construction syntax sugar in QLExpress: + +[source,java] +---- +// list +l = [1,2,3] +assert(l[0]==1) +assert(l[-1]==3) +// Underlying data type of list is ArrayList in Java +assert(l instanceof ArrayList) +// map +m = { + "aa": 10, + "bb": { + "cc": "cc1", + "dd": "dd1" + } +} +assert(m['aa']==10) +// Underlying data type of map is ArrayList in Java +assert(m instanceof LinkedHashMap) +// empty map +emMap = {:} +emMap['haha']='huhu' +assert(emMap['haha']=='huhu') +---- + +Through the `*.` operator, you can quickly process lists and mappings, such as taking properties of list elements, or getting key lists and value lists of mappings: + +[source,java] +---- +list = [ + { + "name": "Li", + "age": 10 + }, + { + "name": "Wang", + "age": 15 + } +] + +// get field from list +assert(list*.age==[10,15]) + +mm = { + "aaa": 1, + "bbb": 2 +} + +// get map key value list +assert(mm*.key==["aaa", "bbb"]) +assert(mm*.value==[1, 2]) +---- + +In mappings, by specifying the fully qualified name of the type through the `@class` key, you can directly use JSON to create complex Java objects. For example, the following MyHome is a Java class with complex nested types: + +[source,java] +---- +package com.alibaba.qlexpress4.inport; + +/** + * Author: DQinYuan + */ +public class MyHome { + + private String sofa; + + private String chair; + + private MyDesk myDesk; + + private String bed; + + public String getSofa() { + return sofa; + } + + public void setSofa(String sofa) { + this.sofa = sofa; + } + + public String getChair() { + return chair; + } + + public MyDesk getMyDesk() { + return myDesk; + } + + public void setMyDesk(MyDesk myDesk) { + this.myDesk = myDesk; + } + + public void setChair(String chair) { + this.chair = chair; + } + + public String getBed() { + return bed; + } +} +---- + +You can conveniently create it through the following QLExpress script: + +> Note: This feature requires opening security options as referenced in link:#security-strategy[Security Strategy] to execute normally. + +[source,java] +---- +myHome = { + '@class': 'com.alibaba.qlexpress4.inport.MyHome', + 'sofa': 'a-sofa', + 'chair': 'b-chair', + 'myDesk': { + 'book1': 'Then Moon and Sixpence', + '@class': 'com.alibaba.qlexpress4.inport.MyDesk' + }, + // ignore field that don't exist + 'notexist': 1234 +} +assert(myHome.getSofa()=='a-sofa') +assert(myHome instanceof com.alibaba.qlexpress4.inport.MyHome) +assert(myHome.getMyDesk().getBook1()=='Then Moon and Sixpence') +assert(myHome.getMyDesk() instanceof com.alibaba.qlexpress4.inport.MyDesk) +---- + +=== Numbers + +For numbers without declared types, +QLExpress will automatically select the most appropriate one from data types like int, long, BigInteger, double, BigDecimal based on their range: + +[source,java] +---- +assert(2147483647 instanceof Integer); +assert(9223372036854775807 instanceof Long); +assert(18446744073709552000 instanceof BigInteger); +// 0.25 can be precisely presented with double +assert(0.25 instanceof Double); +assert(2.7976931348623157E308 instanceof BigDecimal); +---- + +Therefore, when writing custom functions or operators, it's recommended to use Number type for receiving, because numeric types cannot be determined in advance. + +=== Dynamic Strings + +Dynamic strings are a new capability introduced in QLExpress version 4 to enhance string processing capabilities. + +Supports inserting expression calculations in strings through the `$\{expression}` format: + +> If you want to keep `$\{expression}` as-is in strings, you can escape `$` using `\$` + +[source,java] +---- +a = 123 +assert("hello,${a-1}" == "hello,122") + +// escape $ with \$ +assert("hello,\${a-1}" == "hello,\${a-1}") + +b = "test" +assert("m xx ${ + if (b like 't%') { + 'YYY' + } +}" == "m xx YYY") +---- + +If you want QLExpress4 strings to maintain compatibility with version 3 and not process interpolation expressions, you can directly turn off this feature when creating `Express4Runner`: + +[source,java,indent=0] +---- + Express4Runner express4RunnerDisable = new Express4Runner( + // disable string interpolation + InitOptions.builder().interpolationMode(InterpolationMode.DISABLE).build()); + Assert.assertEquals("Hello,${ a + 1 }", + express4RunnerDisable.execute("\"Hello,${ a + 1 }\"", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Hello,${lll", + express4RunnerDisable.execute("\"Hello,${lll\"", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Hello,aaa $ lll\"\n\b", + express4RunnerDisable.execute("\"Hello,aaa $ lll\\\"\n\b\"", context, QLOptions.DEFAULT_OPTIONS) + .getResult()); +---- + +=== Template Rendering + +Leveraging dynamic strings, QLExpress4 can also be used as a lightweight template engine. + +You don't need to manually add string quotes in the script. Just call `executeTemplate` to render the template string: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map context = new HashMap<>(); + context.put("a", 1); + context.put("b", 2); + context.put("c", "test"); + QLResult simpleTemplate = express4Runner.executeTemplate("a ${a};b ${b+2}", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("a 1;b 4", simpleTemplate.getResult()); + QLResult conditionTemplate = + express4Runner.executeTemplate("m xx ${\n" + " if (c like 't%') {\n" + " 'YYY'\n" + " }\n" + "}", + context, + QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("m xx YYY", conditionTemplate.getResult()); + QLResult multiLineTemplate = express4Runner.executeTemplate("m\n ${a}\n c", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("m\n 1\n c", multiLineTemplate.getResult()); + QLResult escapeStringTemplate = + express4Runner.executeTemplate("m \n\"haha\" d\"", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("m \n\"haha\" d\"", escapeStringTemplate.getResult()); +---- + +=== Placeholders + +Placeholders are used to extract values of arbitrary keys from the context. + +Global variables can also extract values from the context, but are limited by QLExpress keywords and syntax, so the keys that can be extracted are limited. +For example, the value corresponding to key "0" in the context cannot be extracted through variables, because 0 is not a legal variable in QLExpress, but a numeric constant. +At this time, you can use the default placeholder `$\{0}` to extract it. + +> Note the distinction from interpolation in dynamic strings. Placeholders are written outside strings. Dynamic string interpolation is `$\{expression}`, where expressions are written by default, and the running result of `"${0}"` is `"0"`. Placeholders are `$\{placeholder}`, where keys in the context are written by default, and the running result of `${0}` is the value corresponding to key "0" in the context. + +QLExpress uses `${placeholder}` format placeholders by default, where: + +* `${` is the start marker +* `}` is the end marker +* `placeholder` is the placeholder content, corresponding to the key in the context + +In addition to default placeholders, QLExpress also supports customizing the start and end markers of placeholders: + +[source,java,indent=0] +---- + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().selectorStart("#[").selectorEnd("]").build()); + + Map context = new HashMap<>(); + context.put("0", "World"); + + QLResult result = express4Runner.execute("'Hello ' + #[0]", context, QLOptions.DEFAULT_OPTIONS); + assertEquals("Hello World", result.getResult()); +---- + +Custom placeholders are not arbitrary, with the following restrictions: + +* **Start Marker Restriction**: `selectorStart` must be one of the following four formats: + ** `${` (default) + ** `$[` + ** `#{` + ** `#[` +* **End Marker Restriction**: `selectorEnd` must be a string of 1 or more characters + +=== Semicolons + +Expression statements can omit ending semicolons, and the return value of the entire script is the calculation result of the last expression. + +The return value of the following script is 2: + +[source,java] +---- +a = 1 +b = 2 +// last express +1+1 +---- + +Equivalent to the following写法: + +[source,java] +---- +a = 1 +b = 2 +// return statment +return 1+1; +---- + +Because semicolons can be omitted, QLExpress4's handling of line breaks is stricter compared to version 3 or Java language. If you want to split multi-line expressions into multiple lines, it's recommended to keep operators on the current line and move the right operand to the next line. + +The following multi-line expression will report a syntax error (counterexample): + +[source,java] +---- +// syntax error +a ++ b +---- + +The following is a correct line break example (positive example): + +[source,java] +---- +a + +b +---- + +Other syntax habits remain consistent with Java. + +=== Expressions + +QLExpress adopts expression-first design, where almost everything is an expression except for import, return, and loop structures. + +if statements are also expressions: + +[source,java] +---- +assert(if (11 == 11) { + 10 +} else { + 20 + 2 +} + 1 == 11) +---- + +try catch structures are also expressions: + +[source,java] +---- +assert(1 + try { + 100 + 1/0 +} catch(e) { + // Throw a zero-division exception + 11 +} == 12) +---- + +=== Short-Circuit Evaluation + +Similar to Java, `&&` and `||` logical operations are all short-circuit evaluations. + +For example, the expression `false && (1/0)` won't cause a division by zero error because `&&` short-circuits at the initial `false`. + +Short-circuit evaluation is enabled by default, and the engine also provides options to turn off short-circuit for a specific execution: + +> One scenario for turning off short-circuit is to ensure full warm-up of expressions + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // execute when enable short circuit (default) + // `1/0` is short-circuited by the preceding `false`, so it won't throw an error. + assertFalse((Boolean)express4Runner.execute("false && (1/0)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + try { + // execute when disable short circuit + express4Runner.execute("false && (1/0)", + Collections.emptyMap(), + QLOptions.builder().shortCircuitDisable(true).build()); + fail(); + } + catch (QLException e) { + Assert.assertEquals("INVALID_ARITHMETIC", e.getErrorCode()); + Assert.assertEquals("Division by zero", e.getReason()); + } +---- + + +=== Control Structures + +==== if Branch + +In addition to being fully compatible with Java's `if` syntax, it also supports rule engine-like `if ... then ... else ...` syntax, where `then` can be treated as an omittable keyword: + +[source,java] +---- +a = 11; +// if ... else ... +assert(if (a >= 0 && a < 5) { + true +} else if (a >= 5 && a < 10) { + false +} else if (a >= 10 && a < 15) { + true +} == true) + +// if ... then ... else ... +r = if (a == 11) then true else false +assert(r == true) +---- + +==== while Loop + +[source,java] +---- +i = 0; +while (i < 5) { + if (++i == 2) { + break; + } +} +assert(i==2) +---- + +==== for Loop + +[source,java] +---- +l = []; +for (int i = 3; i < 6; i++) { + l.add(i); +} +assert(l==[3,4,5]) +---- + +==== for-each Loop + +[source,java] +---- +sum = 0; +for (i: [0,1,2,3,4]) { + if (i == 2) { + continue; + } + sum += i; +} +assert(sum==8) +---- + +==== try-catch + +[source,java] +---- +assert(try { + 100 + 1/0 +} catch(e) { + // Throw a zero-division exception + 11 +} == 11) +---- + +=== Function Definition + +[source,java] +---- +function sub(a, b) { + return a-b; +} +assert(sub(3,1)==2) +---- + +=== Lambda Expressions + +In QLExpress4, Lambda expressions as first-class citizens can be passed as variables or returned. + +[source,java] +---- +add = (a, b) -> { + return a + b; +} +assert(add(1,2)==3) +---- + +=== List Filtering and Mapping + +Supports direct functional filtering and mapping of list types through filter, map methods. + +Implemented by adding link:#extension-functions[Extension Functions] to list types, note the distinction from methods with the same names in Stream API. + +Compared to Stream API, it can directly operate on lists, and return values are also directly lists, making it more convenient. + +[source,java] +---- +l = ["a-111", "a-222", "b-333", "c-888"] +assert(l.filter(i -> i.startsWith("a-")) + .map(i -> i.split("-")[1]) == ["111", "222"]) +---- + +=== Java8 Syntax Compatibility + +QLExpress can be compatible with common Java8 syntax. + +Such as link:#for-each-loop[for each loops], Stream API, functional interfaces, etc. + +==== Stream API + +You can directly use stream APIs in Java collections to operate on collections. + +Because these stream APIs are all from methods in Java, refer to link:#security-strategy[Security Strategy] to open security options for the following scripts to execute normally. + +[source,java] +---- +l = ["a-111", "a-222", "b-333", "c-888"] + +l2 = l.stream() + .filter(i -> i.startsWith("a-")) + .map(i -> i.split("-")[1]) + .collect(Collectors.toList()); +assert(l2 == ["111", "222"]); +---- + +==== Functional Interfaces + +Java8 introduced functional interfaces like Function, Consumer, Predicate, etc. QLExpress's link:#lambda-expressions[Lambda Expressions] can be assigned to these interfaces or used as method parameters that receive these interfaces: + +[source,java] +---- +Runnable r = () -> a = 8; +r.run(); +assert(a == 8); + +Supplier s = () -> "test"; +assert(s.get() == 'test'); + +Consumer c = (a) -> b = a + "-te"; +c.accept("ccc"); +assert(b == 'ccc-te'); + +Function f = a -> a + 3; +assert(f.apply(1) == 4); + +Function f1 = (a, b) -> a + b; +assert(f1.apply("test-") == "test-null"); +---- + +== Appendix I Upgrade Guide + +QLExpress's previous version had significant gaps with the industry in various features due to years of iteration stagnation. + +One of QLExpress4's goals is to make up for these gaps at once, so it chose to make bold upgrades while intentionally abandoning some compatibility. Of course, basic functionality and experience still align with the previous version. + +If your system already uses the old version of QLExpress, you must conduct a comprehensive regression test before upgrading to ensure all these scripts can execute normally in the new version before upgrading. + +If you don't have time or methods to verify them one by one, then upgrading is not recommended. + +If it's a new system, it's recommended to directly adopt QLExpress4. QLExpress4's ecosystem will become increasingly perfect in the future, while version 3 will be gradually abandoned. + +Below lists the main differences between the new and old versions to help users upgrade existing scripts. If anything is missing, feedback is welcome: + +=== Default Security Strategy + +If you completely use default options, accessing Java object fields (`o.field`) or calling member methods (`o.method()`) will throw `FIELD_NOT_FOUND` and `METHOD_NOT_FOUND` errors respectively. + +This is because version 3 could unrestrictedly access any fields and methods in Java application systems through reflection, which is considered unsafe in embedded scripts. + +If you want to be compatible with version 3's behavior, when creating `Express4Runner`, you need to set the security strategy to "open", refer to the following code: + +[source,java,indent=0] +---- + // open security strategy + Express4Runner express4RunnerOpen = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Assert.assertEquals("Thinking in Java", + express4RunnerOpen.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Effective Java", + express4RunnerOpen.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult()); +---- + +For details, refer to the link:#security-strategy[Security Strategy] chapter. + +=== Defining Mappings + +QLExpress old version supported quickly creating mappings through `NewMap(key:value)` syntax. Although not discussed in detail in documentation, many users learned and used this syntax through unit tests and inquiries. + +However, this syntax was too customized and differed greatly from industry standards, so it was removed in the new version. + +The new version natively supports JSON syntax and directly adopts JSON dictionary format (`{key:value}`) to quickly create mappings, making it more intuitive. + +For details, refer to link:#convenient-syntax-elements[Convenient Syntax Elements] + +=== Global Variable Context Pollution + +QLExpress supports passing in a global context when executing scripts, i.e., the context parameter. + +In the old version, if global variables were defined in scripts, these variables would also be written to the context. After script execution, you could obtain the values of global variables defined in scripts through the context. + +An old version example is as follows: + +[source,java] +---- +// only for QLExpress 3.x + +String express = "a=3;a+1"; +ExpressRunner runner = new ExpressRunner(false, true); +DefaultContext context = new DefaultContext<>(); + +Object res = runner.execute(express, context, null, true, true); +// The result of the script execution should be 4 (a+1) +Assert.assertEquals(4, res); +// The variable 'a' defined in the script is also stored in the context +Assert.assertEquals(3, context.get("a")); +---- + +Based on research and feedback, we believe this would cause the global context to be "polluted" by scripts, posing security risks. + +Therefore, in QLExpress4, global variables are not written to the context by default. + +If you want to be compatible with version 3's features, you need to set the `polluteUserContext` option to `true`, refer to the following code: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + QLOptions populateOption = QLOptions.builder().polluteUserContext(true).build(); + Map populatedMap = new HashMap<>(); + populatedMap.put("b", 10); + express4Runner.execute("a = 11;b = a", populatedMap, populateOption); + assertEquals(11, populatedMap.get("a")); + assertEquals(11, populatedMap.get("b")); + + // no population + Map noPopulatedMap1 = new HashMap<>(); + express4Runner.execute("a = 11", noPopulatedMap1, QLOptions.DEFAULT_OPTIONS); + assertFalse(noPopulatedMap1.containsKey("a")); + + Map noPopulatedMap2 = new HashMap<>(); + noPopulatedMap2.put("a", 10); + assertEquals(19, express4Runner.execute("a = 19;a", noPopulatedMap2, QLOptions.DEFAULT_OPTIONS).getResult()); + assertEquals(10, noPopulatedMap2.get("a")); +---- + +=== Semicolons Can Be Omitted + +"Omittable semicolons" is already a standard feature of modern scripting languages, and QLExpress4 has also followed this trend - semicolons can be omitted. + +For details, refer to the link:#semicolons[Semicolons] chapter. + +=== Strict Newline Mode + +Since QLExpress4 supports omitting semicolons, the interpreter needs to use newlines to determine whether a statement has ended. Therefore, QLExpress4 has stricter requirements for newlines than QLExpress3. + +The following script is valid in QLExpress3 but not in QLExpress4: + +[source,java] +---- +// Valid in QLExpress3, but not in QLExpress4 +商家应收= + 价格 + - 饭卡商家承担 + + 平台补贴 +---- + +In QLExpress4, the above script will be parsed as two separate statements: +1. `商家应收 = 价格` +2. `- 饭卡商家承担 + 平台补贴` (the second statement will result in a syntax error) + +To achieve the same effect in QLExpress4, you need to place the operator at the end of the line rather than at the beginning: + +[source,java] +---- +// Correct way in QLExpress4 +商家应收= + 价格 - + 饭卡商家承担 + + 平台补贴 +---- + +This way, the interpreter knows that the current line's expression is not yet complete and will continue reading the next line. + +If you need to be compatible with QLExpress3's newline behavior, you can set the `strictNewLines` option to `false`: + +[source,java] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.builder().strictNewLines(false).build()); + String script = "商家应收=\n 价格\n - 饭卡商家承担\n + 平台补贴"; + Map context = new HashMap<>(); + context.put("价格", 10); + context.put("饭卡商家承担", 3); + context.put("平台补贴", 5); + QLResult result = express4Runner.execute(script, context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(12, ((Number)result.getResult()).intValue()); +---- + +Note: Non-strict newline mode causes the interpreter to ignore all newlines, which may affect code readability and the accuracy of error messages. It is recommended to use this only when you need to be compatible with legacy code. + +=== Obtaining char Type + +In QLExpress 3, single characters wrapped in single quotes were parsed as char type, not String. + +This caused much confusion for users, for example, the result of `"a"=='a'` would be `false`. + +So later in QLExpress 3, the `ExpressRunner.setIgnoreConstChar` option was added. When set to `true`, all characters wrapped in single quotes and double quotes would be parsed as String type. However, this option was turned off by default and required users to manually enable it. + +Considering that script users rarely use the `char` primitive type, we directly removed this option in QLExpress 4. All characters wrapped in single quotes and double quotes are now parsed as String type. + +If you still need to use `char` type in scripts, you can obtain it through two methods: + +* Type casting: `(char) 'a'` +* Type declaration: `char a = 'a'` + +== Appendix II How to Contribute? + +QLExpress is completely open to community changes. Any suggestions and modifications will be welcome, and after discussion, reasonable ones will be accepted into the main branch. + +First, you need to clone the code to local. Before formally modifying the code, you need to prepare as follows: + +1. Execute `mvn compile` in the project root directory: When the project is first downloaded locally, there will be many classes not found, so you need to generate Antlr4 runtime code first +2. Configure code formatting: QLExpress project has unified code format specifications, and automatic formatting needs to be configured before git commits during development + +Create a new file `.git/hooks/pre-commit` in the project directory with the following content: + +[source,bash] +---- +#!/bin/sh +mvn spotless:apply +git add -u +exit 0 +---- + +This way, before each git commit, maven's spotless plugin will automatically execute code formatting. For specific code format configuration, see link:spotless_eclipse_formatter.xml[] + +3. Run unit tests: After completing code development, run all unit tests locally to ensure code quality + - JDK 8 environment: Execute `mvn test` + - JDK 9 and above environment: Execute `mvn test -DargLine="--add-opens java.base/java.util.stream=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED"` + +== Appendix III QLExpress4 Performance Improvements + +link:https://www.yuque.com/xuanheng-ffjti/iunlps/pgfzw46zel2xfnie?singleDoc#%20%E3%80%8AQLExpress3%E4%B8%8E4%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94%E3%80%8B[QLExpress4 vs 3 Performance Comparison] + +Summary: In common scenarios, without compilation cache, QLExpress4 can achieve nearly 10x performance improvement over version 3; with compilation cache, there's also a 1x performance improvement. + +== Appendix IV Developer Contact Information + + * Email: + ** qinyuan.dqy@alibaba-inc.com + ** yumin.pym@taobao.com + ** 704643716@qq.com + * WeChat: + ** xuanheng: dqy932087612 + ** binggou: pymbupt + ** linxiang: tkk33362 + * DingTalk Support Group + +image::images/qlexpress_support_group_qr_2026.jpg[] diff --git a/README-source.adoc b/README-source.adoc new file mode 100644 index 0000000..e376c52 --- /dev/null +++ b/README-source.adoc @@ -0,0 +1,1109 @@ +:toc: + += QLExpress + +image::images/logo.png[] + +【中文版】| link:README-EN.adoc[[English\]] + +image::https://api.star-history.com/svg?repos=alibaba/QLExpress&type=Date[Star History Chart] + +== 背景介绍 + +由阿里的电商业务规则演化而来的嵌入式Java动态脚本工具,在阿里集团有很强的影响力,同时为了自身不断优化、发扬开源贡献精神,于2012年开源。 + +在基本的表达式计算的基础上,还有以下特色: + +* 灵活的自定义能力,通过 Java API 自定义函数和操作符,可以快速实现业务规则的 DSL +* 兼容最新的 Java 语法,方便 Java 程序员快速熟悉。熟悉类 C 语言的业务人员使用起来也会非常顺手 +* 原生支持 JSON,快捷定义复杂数据结构 +* 友好的报错提示,无论是编译还是运行时错误,都能精确友好地提示错误位置 +* 独一无二的表达式追踪功能,可以追踪表达式在中间节点计算的值,方便业务人员或者 AI 对线上规则的计算结果进行归因分析 +* 默认安全,脚本默认不允许和应用代码进行交互,如果需要交互,也可以自行定义安全的交互方式 +* 解释执行,不占用 JVM 元空间,可以开启缓存提升解释性能 +* 代码精简,依赖最小,适合所有java的运行环境 + +QLExpress4 作为 QLExpress 的最新演进版本,基于 Antlr4 重写了解析引擎,将原先的优点进一步发扬光大,新增了大量特色功能,彻底拥抱函数式编程,在性能和表达能力上都进行了进一步增强。 + +> 如果项目还是使用旧版本的 QLExpress,可以跳转 link:https://github.com/alibaba/QLExpress/tree/branch_version_3.x.x[branch_version_3.x.x] 维护分支查看旧版文档。如需升级可以参考 link:#附录一-升级指南[附录一-升级指南] + +场景举例: + +* 电商优惠券规则配置:通过 QLExpress 自定义函数和操作符快速实现优惠规则 DSL,供运营人员根据需求自行动态配置 +* 表单搭建控件关联规则配置:表单搭建平台允许用户拖拽控件搭建自定义的表单,利用 QLExpress 脚本配置不同控件间的关联关系 +* 流程引擎条件规则配置 +* 广告系统计费规则配置 + +\...\... + +== 新版特色 + +新版本并不是对旧版本的简单功能重构,而是我们基于对用户需求的洞察,对下一代规则表达式引擎的探索。拥有许多非常实用,但是在其他引擎中缺失的重要功能。 + +=== 表达式计算追踪 + +在业务人员完成规则脚本的配置后,很难对其线上执行情况进行感知。比如电商的促销规则,要求用户满足规则 `isVip && 未登录10天以上`。到底有多少线上用户是被 vip 条件拦截,又有多少用户是因为登录条件被拦截?这还是只是仅仅两个条件的简单规则,实际线上情况则更加复杂。 + +线上规则执行情况的追踪,不仅仅可以帮助业务人员了解线上的实际情况,排查和修复问题。其沉淀的数据也非常有价值,可以用于后续的规则优化和业务决策。以下是某个规则平台,基于 QLExpress4 的表达式追踪能力,对规则进行归因分析与附注的决策的产品简化图: + +image::images/order_rules_cn.png[] + +归因分析的原理在于利用 QLExpress4 的表达式追踪能力,获得表达式在计算过程中每个中间结果的值, 据此判断表达式最终运行结果产生的原因。 + +具体使用方法参考:link:#表达式计算追踪-1[表达式计算追踪] + +=== 原生支持 JSON 语法 + +QLExpress4 原生支持 JSON 语法,可以快捷定义复杂的数据结构。 + +JSON 数组代表列表(List),而 JSON 对象代表映射(Map),也可以直接定义复杂对象。 + +产品上可以基于该特性实现 JSON 映射规则。让用户可以便捷地定义从一个模型向另一个模型的映射关系。以下是某个规则平台,基于该能力实现的模型映射产品简化图: + +image::images/json_map.png[] + +具体使用方法参考:link:#方便语法元素[方便语法元素] + +=== 便捷字符串处理 + +QLExpress4 对字符串处理能力进行针对性的增强,在字符串中可以直接通过 `$\{expression}` 嵌入表达式计算结果。 + +具体使用方法参考:link:#动态字符串[动态字符串] + +=== 附件透传 + +正常情况下,脚本执行需要的全部信息都在 `context` 中。context 中的 key 可以在脚本中作为变量引用,最终传递给自定义函数或者操作符。 + +但是出于安全,或者方便使用等因素考虑。有些信息并不希望用户通过变量引用到,比如租户名,密码等等。 + +此时可以通过附件(attachments)将这部分信息传递给自定义函数或者操作符使用。 + +具体使用方法参考:link:#添加自定义函数与操作符[添加自定义函数与操作符] 其中 `hello` 自定义函数根据附件中租户不同,返回不同的欢迎信息的示例。 + +=== 函数式编程 + +函数被提升为 QLExpress4 中的第一等公民,可以作为变量使用,也可以作为函数的返回值。并且可以很容易地和 Java 中常见的函数式 API(比如 Stream) 结合使用。 + +以下是一个简单的 QLExpress 示例脚本: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/lambda/lambda_doc.ql[] +---- + +更多使用方法参考: + +* link:#lambda-表达式[Lambda表达式] +* link:#列表过滤和映射[列表过滤和映射] +* link:#stream-api[Stream API] +* link:#函数式接口[函数式接口] + +=== 分号简化 + +QLExpress4 支持省略分号,让表达式更加简洁。具体参考 link:#分号[分号] + +== API 快速入门 + +=== 引入依赖 + +[source,xml] +---- + + com.alibaba + qlexpress4 + 4.0.7 + +---- + +环境要求: + +* JDK 8 或更高版本 + +=== 第一个 QLExpress 程序 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl] +---- + +更多的表达式执行方式见文档 link:docs/execute.adoc[表达式执行] + +=== 添加自定义函数与操作符 + +最简单的方式是通过 Java Lambda 表达式快速定义函数/操作符的逻辑: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addFunctionAndOperator] +---- + +如果自定义函数的逻辑比较复杂,或者需要获得脚本的上下文信息,也可以通过继承 `CustomFunction` 的方式实现。 + +比如下面的 `hello` 自定义函数,根据租户不同,返回不同的欢迎信息: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/test/function/HelloFunction.java[] +---- + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=customComplexFunction] +---- + +QLExpress4还支持通过QLExpress脚本添加自定义函数。需要注意的是,在函数外定义的变量(如示例中的defineTime)在函数定义时就已初始化完成,后续调用函数时不会重新计算该变量的值。 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addFunctionsDefinedInScript] +---- + +建议尽可能使用Java方式定义自定义函数,这样可以获得更好的性能和稳定性。 + +更多自定义语法元素的方式见文档 link:docs/custom-item.adoc[自定义语法元素] + +=== 校验语法正确性 + +在不执行脚本的情况下,单纯校验语法的正确性,其中包含了操作符的限制校验,调用 `check` 并且捕获异常,如果捕获到 `QLSyntaxException`,则说明存在语法错误 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=checkSyntax] +---- + +你可以使用 `CheckOptions` 配置更精细的语法校验规则,主要支持以下两个选项: + +1. `operatorCheckStrategy`: 操作符校验策略,用于限制脚本中可以使用的操作符 +2. `disableFunctionCalls`: 是否禁用函数调用,默认为 false + +示例1:使用操作符校验策略(白名单) + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/OperatorLimitTest.java[tag=operatorCheckStrategyExample] +---- + +示例2:禁用函数调用 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=disableFunctionCallsExample] +---- + +=== 解析脚本所需外部变量 + +脚本中使用的变量有的是脚本内生,有的是需要从外部通过 `context` 传入的。 + +QLExpress4 提供了一个方法,可以解析出脚本中所有需要从外部传入的变量: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=getOutVarNames] +---- + +更多脚本依赖解析工具: + +* `getOutFunctions`: 解析所有需要从外部定义的函数 +* `getOutVarAttrs`:解析所有需要从外部传入变量及其涉及的属性,`getOutVarNames` 的增强版本 + +=== 高精度计算 + +QLExpress 内部会用 BigDecimal 表示所有无法用 double 精确表示数字,来尽可能地表示计算精度: + +> 举例:0.1 在 double 中无法精确表示 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=bigDecimalForPrecise] +---- + +通过这种方式能够解决一些计算精度问题: + +比如 0.1+0.2 因为精度问题,在 Java 中是不等于 0.3 的。 +而 QLExpress 能够自动识别出 0.1 和 0.2 无法用双精度精确表示,改成用 BigDecimal 表示,确保其结果等于0.3 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=preciseComparisonWithJava] +---- + +除了默认的精度保证外,还提供了 `precise` 开关,打开后所有的计算都使用BigDecimal,防止外部传入的低精度数字导致的问题: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=preciseSwitch] +---- + +=== 安全策略 + +QLExpress4 默认采用隔离安全策略,不允许脚本访问 Java 对象的字段和方法,这确保了脚本执行的安全性。如果需要访问 Java 对象,可以通过不同的安全策略进行配置。 + +假设应用中有如下的 Java 类: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/inport/MyDesk.java[] +---- + +脚本执行的上下文设置如下: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyContextSetup] +---- + +QLExpress4 提供了四种安全策略: + +==== 1. 隔离策略(默认) + +默认情况下,QLExpress4 采用隔离策略,不允许访问任何字段和方法: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyIsolation] +---- + +==== 2. 黑名单策略 + +通过黑名单策略,可以禁止访问特定的字段或方法,其他字段和方法可以正常访问: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyBlackList] +---- + +==== 3. 白名单策略 + +通过白名单策略,只允许访问指定的字段或方法,其他字段和方法都会被禁止: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyWhiteList] +---- + +==== 4. 开放策略 + +开放策略允许访问所有字段和方法,类似于 QLExpress3 的行为,但需要注意安全风险: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyOpen] +---- + +> 注意:开放策略虽然提供了最大的灵活性,但也带来了安全风险。建议只在受信任的环境中使用,不建议用于处理终端用户输入的脚本。 + +==== 策略建议 + +建议直接采用默认策略,在脚本中不要直接调用 Java 对象的字段和方法。而是通过自定义函数和操作符的方式(参考 link:#添加自定义函数与操作符[添加自定义函数与操作符]),对嵌入式脚本提供系统功能。这样能同时保证脚本的安全性和灵活性,用户体验还更好。 + +如果确实需要调用 Java 对象的字段和方法,至少应该使用白名单策略,只提供脚本有限的访问权限。 + +至于黑名单和开放策略,不建议在外部输入脚本的场景使用,除非确保每个脚本都会经过审核。 + +=== 调用应用中的 Java 类 + +> 需要放开安全策略,不建议用于终端用户输入 + +假设应用中有如下的 Java 类(`com.alibaba.qlexpress4.QLImportTester`): + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/QLImportTester.java[] +---- + +在 QLExpress 中有如下两种调用方式。 + +==== 1. 在脚本中使用 `import` 语句导入类并且使用 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=importJavaCls] +---- + +==== 2. 在创建 `Express4Runner` 时默认导入该类,此时脚本中就不需要额外的 `import` 语句 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=defaultImport] +---- + +除了用 `ImportManager.importCls` 导入单个类外,还有其他更方便的导入方式: + + * `ImportManager.importPack` 直接导入包路径下的所有类,比如 `ImportManager.importPack("java.util")` 会导入 `java.util` 包下的所有类,QLExpress 默认就会导入下面的包 + ** `ImportManager.importPack("java.lang")` + ** `ImportManager.importPack("java.util")` + ** `ImportManager.importPack("java.math")` + ** `ImportManager.importPack("java.util.stream")` + ** `ImportManager.importPack("java.util.function")` + * `ImportManager.importInnerCls` 导入给定类路径里的所有内部类 + +=== 自定义 ClassLoader + +QLExpress4 支持通过自定义 `ClassSupplier` 来指定类加载器,这在插件化架构、模块化应用等场景中非常有用。通过自定义类加载器,可以让 QLExpress 脚本访问特定 ClassLoader 中的类。 + +下面的示例展示了如何与 link:https://pf4j.org/[PF4J] 插件框架集成,让 QLExpress 脚本能够访问插件中的类: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/pf4j/Pf4jClassSupplierTest.java[tag=pluginClassSupplier] +---- + +自定义 ClassSupplier 的典型应用场景: + +* **插件化架构**:让脚本能够访问插件中定义的类和接口 +* **模块化应用**:在 OSGi 等模块化框架中,让脚本访问特定模块的类 +* **动态类加载**:从远程仓库或动态生成的字节码中加载类 +* **类隔离**:使用不同的 ClassLoader 来实现类的隔离 + +=== 表达式缓存 + +通过 `cache` 选项可以开启表达式缓存,这样相同的表达式就不会重新编译,能够大大提升性能。 + +注意该缓存没有限制大小,只适合在表达式为有限数量的情况下使用: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=cacheSwitch] +---- + +但是当脚本首次执行时,因为没有缓存,依旧会比较慢。 + +可以通过下面的方法在首次执行前就将脚本缓存起来,保证首次执行的速度: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=parseToCache] +---- + +注意该缓存的大小是无限的,业务上注意控制大小,可以调用 `clearCompileCache` 方法定期清空编译缓存。 + +=== 清除 DFA 缓存 + +QLExpress 使用 ANTLR4 作为解析引擎,ANTLR4 在运行时会构建 DFA (确定有限状态自动机) 缓存来加速后续的语法解析。这个缓存会占用一定的内存空间。 + +在某些内存敏感的场景下,可以通过调用 `clearDFACache` 方法来清除 DFA 缓存,释放内存。 + +> **重要警告**: 清除 DFA 缓存会导致编译性能大幅下降,正常情况下不建议使用此方法。 + +==== 适用场景 + +* **内存敏感型应用**: 当内存使用是关键考虑因素,且可以容忍较慢的编译时间时 +* **脚本不经常变更**: 当脚本相对稳定且不会频繁重新编译时 + +==== 最佳实践 + +在解析并缓存表达式后立即调用此方法,并确保后续所有执行都打开缓存选项以避免重新编译。示例代码如下: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/ClearDfaCacheTest.java[tag=clearDFACacheBestPractice] +---- + +通过这种方式,可以在保证性能的同时,最大限度地降低内存占用。 + +=== 设置超时时间 + +可以给脚本设置一个超时时间,防止其中存在死循环或者其他原因导致应用资源被过量消耗。 + +下面的示例代码给脚本给脚本设置了一个 10ms 的超时时间: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=scripTimeout] +---- + +> 注意,出于系统性能的考虑,QLExpress 对于超时时间的检测是不准确的。特别是在回调Java代码中(比如自定义函数或者操作符)出现的超时,不会立刻被检测到。只有在执行完,回到 QLExpress 运行时后才会被检测到并中断执行。 + +=== 扩展函数 + +利用 QLExpress 提供的扩展函数能力,可以给Java类中添加额外的成员方法。 + +扩展函数是基于 QLExpress 运行时实现的,因此仅仅在 QLExpress 脚本中有效。 + +下面的示例代码给 String 类添加了一个 `hello()` 扩展函数: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=extensionFunction] +---- + +=== Java类的对象,字段和方法别名 + +QLExpress 支持通过 `QLAlias` 注解给对象,字段或者方法定义一个或多个别名,方便非技术人员使用表达式定义规则。 + +下面的例子中,根据用户是否 vip 计算订单最终金额。 + +用户类定义: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/test/qlalias/User.java[] +---- + +订单类定义: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/test/qlalias/Order.java[] +---- + +通过 QLExpress 脚本规则计算最终订单金额: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=qlAlias] +---- + +=== 关键字,操作符和函数别名 + +为了进一步方面非技术人员编写规则,QLExpress 提供 `addAlias` 给原始关键字,操作符和函数增加别名。让整个脚本的表述更加贴近自然语言。 + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addAlias] +---- + +支持设置别名的关键字有: + + * if + * then + * else + * for + * while + * break + * continue + * return + * function + * macro + * new + * null + * true + * false + +> 注意:部分大家熟悉的用法其实是操作符,而不是关键字,比如 `in` 操作符。而所有的操作符和函数默认就是支持别名的 + +=== 宏 + +宏是QLExpress中一个强大的代码复用机制,它允许用户定义一段可重用的脚本片段,并在需要时进行调用。与简单的文本替换不同,QLExpress的宏是基于指令回放的机制实现的,具有更好的性能和语义准确性。 + +宏特别适用于以下场景: + +* **代码复用**:将常用的脚本片段封装成宏,避免重复编写相同的逻辑 +* **业务规则模板**:定义标准的业务规则模板,如价格计算、权限检查等 +* **流程控制**:封装复杂的控制流程,如条件判断、循环逻辑等 +* **DSL构建**:作为构建领域特定语言的基础组件 + +宏可以通过两种方式定义: + +**1. 在脚本中使用 `macro` 关键字定义** + +[source,java] +---- +include::./src/test/resources/testsuite/independent/macro/macro.ql[tag=addMacroInScript] +---- + +**2. 通过Java API添加** + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addMacro] +---- + +宏与函数的区别: + +[cols="1,1,1"] +|=== +| 特性 | 宏 | 函数 +| 参数传递 | 无参数,依赖上下文变量 | 支持参数传递 +| 性能 | 指令直接插入,无调用开销 | 有函数调用开销 +| 作用域 | 共享调用者作用域 | 独立的作用域 +| 适用场景 | 代码片段复用 | 逻辑封装和参数化 +|=== + +宏特别适合那些不需要参数传递、主要依赖上下文变量的代码片段复用场景,而函数更适合需要参数化和独立作用域的场景。 + +**QLExpress4 相比 3 版本,宏特性的变化**: + + * 4 的宏实现更加接近通常编程语言中宏的定义,相当于将预定义的代码片段插入到宏所在的位置,与调用点位于同一作用域,宏中的 `return`, `contine` 和 `break` 等可以影响调用方的控制流。但是 3 中的实现其实更加接近无参函数调用。 + * 4 的宏无法作为变量使用,只有单独作为一行语句时才能被宏替换。因为宏可以是任意脚本,不一定是有返回值的表达式,作为变量时会存在语义问题。3 的宏本质是一个无参函数调用,所以常常被作为变量使用 + +如果想兼容 3 中的宏特性,建议使用 link:#动态变量[动态变量] + +=== 动态变量 + +常规的 “静态变量”,是 context 中和 key 关联的固定的值。而动态变量可以是一个表达式,由另外一些变量计算而得。动态变量支持嵌套,即动态变量可以依赖另一个动态变量计算得到。 + +示例如下: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=dynamicVar] +---- + +=== 表达式计算追踪 + +如果打开相关选项,QLExpress4 就会在返回规则脚本计算结果的同时,返回一颗表达式追踪树。表达式追踪树的结构类似语法树,不同之处在于,它会在每个节点上记录本次执行的中间结果。 + +比如对于表达式 `!true || myTest(a, 1)`,表达式追踪树的结构大概如下: + +[source] +---- + || true + / \ + ! false myTest + / / \ + true a 10 1 +---- + +可应用于多种场景: + + * 方便业务人员对规则的计算结果进行分析排查 + * 对线上判断为 false 的规则进行采样归类 + * AI 自动诊断和修复规则 + +节点计算结果会被放置到 `ExpressionTrace` 对象的 `value` 字段中。如果中间发生短路导致部分表达式未被计算,则 `ExpressionTrace` 对象的 `evaluated` 字段会被设置为 false。代码示例如下: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=expressionTrace] +---- + +> 注意,必须在新建 `Express4Runner` 时将 `InitOptions.traceExpression` 选项设置为 true,同时在执行脚本时将 `QLOptions.traceExpression` 设置为 true,该功能才能生效。 + +也可以在不执行脚本的情况下获得所有表达式追踪点: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=getExpressionTracePoints] +---- + +支持的表达式追踪点类型以及对应子节点的含义如下: + +[cols="1,1,1"] +|=== +| 节点类型 | 节点含义 | 子节点含义 +| OPERATOR | 操作符 | 两侧操作数 +| FUNCTION | 函数 | 函数参数 +| METHOD | 方法 | 方法参数 +| FIELD | 字段 | 取字段的目标对象 +| LIST | 列表 | 列表元素 +| MAP | 字段 | 无 +| IF | 条件分支 | condition表达式,then逻辑块和else逻辑块 +| RETURN | 返回语句 | 返回表达式 +| VARIABLE | 变量 | 无 +| VALUE | 字面值 | 无 +| DEFINE_FUNCTION | 定义函数 | 无 +| DEFINE_MACRO | 定义宏 | 无 +| PRIMARY | 暂时未继续下钻的其他复合值(比如字典,if等等)| 无 +| STATEMENT | 暂未继续下钻的其他复合语句(比如 while, for 等等)| 无 +|=== + +=== 与 Spring 集成 + +QLExpress 并不需要专门与 Spring 集成,只需要一个 `Express4Runner` 单例,即可使用。 + +这里提供的 “集成” 示例,可以在 QLExpress 脚本中直接引用任意 Spring Bean。 + +这种方式虽然很方便,但是脚本权限过大,自由度太高。不再推荐使用,还是建议在 context 只放入允许用户访问的对象。 + +核心集成组件: + +* link:src/test/java/com/alibaba/qlexpress4/spring/QLSpringContext.java[QLSpringContext]: 实现了 `ExpressContext` 接口,提供了对 Spring 容器的访问能力。它会优先从传入的 context 中查找变量,如果找不到则尝试从 Spring 容器中获取同名的 Bean。 +* link:src/test/java/com/alibaba/qlexpress4/spring/QLExecuteService.java[QLExecuteService]: 封装了 QLExpress 的执行逻辑,集成了 Spring 容器,方便在 Spring 应用中使用。 + +假设存在一个 Spring Bean, 名为 `helloService`: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/spring/HelloService.java[] +---- + +在脚本中调用该 Bean: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/spring/SpringDemoTest.java[] +---- + + +== 语法入门 + +QLExpress4 兼容 Java8 语法的同时,也提供了很多更加灵活宽松的语法模式,帮助用户更快捷地编写表达式。 + +基于表达式优先的语法设计,复杂的条件判断语句也可以直接当作表达式使用。 + +在本章节中出现的代码片段都是 qlexpress 脚本, +`assert` 是测试框架往引擎中注入的断言方法,会确保其参数为 `true`。 +`assertErrCode` 会确保其 lambda 参数表达式的执行一定会抛出含第二个参数 error code 的 QLException。 + +=== 变量声明 + +同时支持静态类型和动态类型: + + * 变量声明时不写类型,则变量是动态类型,也同时是一个赋值表达式 + * 变量声明如果写类型,则是静态类型,此时是一个变量声明语句 + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/dynamic_typing.ql[] +---- + +=== 方便语法元素 + +列表(List),映射(Map)等常用语法元素在 QLExpress 中都有非常方便的构造语法糖: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql[] +---- + +通过 `*.` 操作符,可以快捷地对列表和映射进行处理,比如对列表元素进行取属性,或者获得映射的 key 列表和 value 列表: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/spread/list_spread.ql[tag=spreadExample] +---- + +在映射中通过 `@class` key 指定类型的全限定名,就可以直接使用 JSON 创建复杂Java对象。比如下面的 MyHome , 是一个含有复杂嵌套类型 Java 类: + +[source,java] +---- +include::./src/test/java/com/alibaba/qlexpress4/inport/MyHome.java[] +---- + +可以通过下面的 QLExpress 脚本,便捷创建: + +> 注意,该特性需要参考 link:#安全策略[安全策略] 打开安全选项,才能正常执行。 + +[source,java] +---- +include::./src/test/resources/testsuite/java/map/classified_json.ql[tag=classifiedJson] +---- + +=== 数字 + +对于未声明类型的数字, +QLExpress会根据其所属范围自动从 int, long, BigInteger, double, BigDecimal 等数据类型中选择一个最合适的: + +[source,java] +---- +include::./src/test/resources/testsuite/java/number/number_auto_type.ql[] +---- + +因此在自定义函数或者操作符时,建议使用 Number 类型进行接收,因为数字类型是无法事先确定的。 + +=== 动态字符串 + +动态字符串是 QLExpress 为了增强字符串处理能力,在 4 版本新引入的能力。 + +支持 `$\{expression}` 的格式在字符串中插入表达式计算: + +> 如果想在字符串中原样保持 `$\{expression}`,可以使用 `\$` 对 `$` 进行转义 + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/dynamic_string.ql[] +---- + +如果还想让 QLExpress4 的字符串和 3 保持兼容性,不对插值表达式进行处理,可以在新建 `Express4Runner` 时直接关闭该特性: + +[source,java] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=disableInterpolation] +---- + +=== 模板语言 + +利用动态字符串能力,QLExpress4 也可以作为一个轻量级模板引擎使用。 + +无需在脚本中手动添加字符串引号,直接调用 `executeTemplate` 渲染模板字符串: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=templateEngine] +---- + +=== 占位符 + +占位符用于从 context 中提取任意 key 的值。 + +全局变量也可以从 context 中提取值,但是收到 QLExpress 关键词和语法的限制,能提取的 key 有限。 +比如 context 中 "0" key 对应的值就无法通过变量提取,因为 0 不是 QLExpress 中的合法变量,而是一个数字常量。 +此时可以用默认占位符 `$\{0}` 来提取。 + +> 注意和动态字符串中插值区分,占位符是写在字符串之外。动态字符串插值是 `$\{expression}`,其中默认写的是表达式,`"${0}"` 的运行结果是 `"0"`。而占位符是 `$\{placeholder}`,其中默认写的是 context 中的 key,`${0}` 的运行结果是 context 中 "0" key 对应的值。 + +QLExpress 默认使用 `$\{placeholder}` 格式的占位符,其中: + +* `${` 是起始标记 +* `}` 是结束标记 +* `placeholder` 是占位符内容,对应 cotext 中的 key + +除了默认的占位符外,QLExpress 还支持自定义占位符的起始和结束标记: + +[source,java] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=customSelector] +---- + +自定义占位符并不是任意的,限制条件如下: + +* **起始标记限制**:`selectorStart` 必须是以下四种格式之一: + ** `${` (默认) + ** `$[` + ** `#{` + ** `#[` +* **结束标记限制**:`selectorEnd` 必须是 1 个或更多字符的字符串 + +=== 分号 + +表达式语句可以省略结尾的分号,整个脚本的返回值就是最后一个表达式的计算结果。 + +以下脚本的返回值为 2: + +[source,java] +---- +a = 1 +b = 2 +// last express +1+1 +---- + +等价于以下写法: + +[source,java] +---- +a = 1 +b = 2 +// return statment +return 1+1; +---- + +因为分号可以省略,QLExpress4 对于换行的处理相比 3 或者 Java 语言更加严格。如果想要将多行表达式拆成多行,建议将操作符保留在当前行,而将右操作数换到下一行。 + +以下多行表达式会报语法错误(反例): + +[source,java] +---- +// syntax error +a ++ b +---- + +以下是正确的换行示例(正例): + +[source,java] +---- +a + +b +---- + +其他的语法习惯保持和 Java 一致即可。 + +=== 表达式 + +QLExpress 采用表达式优先的设计,其中 除了 import, return 和循环等结构外,几乎都是表达式。 + +if 语句也是一个表达式: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/if_as_expr.ql[] +---- + +try catch 结构也是一个表达式: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql[] +---- + +=== 短路计算 + +和 Java 类似,`&&` 和 `||` 逻辑运算都是短路运算的。 + +比如表达式 `false && (1/0)` 不会发生除 0 错误,因为 `&&` 短路在了最开始的 `false` 处。 + +短路计算默认是开启的,引擎也提供了选项,可以在某次执行时将短路关闭: + +> 关闭短路的一个场景是保证表达式的充分预热 + +[source,java] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=disableShortCircuit] +---- + + +=== 控制结构 + +==== if 分支 + +除了完全兼容 Java 中的 `if` 写法,还支持类似规则引擎的 `if ... then ... else ...` 的写法,其中 `then` 可以当成一个可以省略的关键字: + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/if.ql[] +---- + +==== while 循环 + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/while.ql[] +---- + +==== for 循环 + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/for.ql[] +---- + +==== for-each 循环 + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/for_each.ql[] +---- + +==== try-catch + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/try_catch.ql[] +---- + +=== 函数定义 + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/function.ql[] +---- + +=== Lambda 表达式 + +QLExpress4 中,Lambda 表达式作为一等公民,可以作为变量进行传递或者返回。 + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/lambda.ql[] +---- + +=== 列表过滤和映射 + +支持通过 filter, map 方法直接对列表类型进行函数式过滤和映射。 + +底层通过在列表类型添加 link:#扩展函数[扩展函数] 实现,注意和 Stream API 中同名方法区分。 + +相比 Stream Api,它可以直接对列表进行操作,返回值也直接就是列表,更加方便。 + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/list_map_filter.ql[] +---- + +=== 兼容 Java8 语法 + +QLExpress 可以兼容 Java8 的常见语法。 + +比如 link:#for-each-循环[for each循环], Stream API, 函数式接口等等。 + +==== Stream API + +可以直接使用 Java 集合中的 stream api 对集合进行操作。 + +因为此时的 stream api 都是来自 Java 中的方法,参考 link:#安全策略[安全策略] 打开安全选项,以下脚本才能正常执行。 + +[source,java] +---- +include::./src/test/resources/testsuite/java/stream/java_stream.ql[] +---- + +==== 函数式接口 + +Java8 中引入了 Function, Consumer, Predicate 等函数式接口,QLExpress 中的 link:#lambda-表达式[Lambda表达式] 可以赋值给这些接口,或者作为接收这些接口的方法参数: + +[source,java] +---- +include::./src/test/resources/testsuite/java/lambda/java_functional_interface.ql[] +---- + +== 附录一 升级指南 + +QLExpress 的上一版本因为多年的迭代停滞,在各项特性上和业界产生了较大差距。 + +QLExpress4 的目标之一就是一次性弥补这些差距,因此选择进行了大刀阔斧的升级,而有意放弃了部分兼容性。当然,基础的功能和体验还是和上一版本保持了对齐。 + +如果系统已经使用老版本的 QLExpress,升级之前务必要进行一次全面的回归测试,确保这些脚本都能在新版中正常执行,再进行升级。 + +如果没有时间或者方法对它们一一验证,那么不建议进行升级。 + +如果是新系统,建议直接采用 QLExpress4,未来 QLExpress4 的生态建设会越来越完善,而 3 会被逐渐抛弃。 + +下面将列表新版和旧版的主要不同,方便用户对已有脚本进行升级。如有遗漏,欢迎反馈: + +=== 默认安全策略 + +如果完全使用默认选项,获取 Java 对象的字段(`o.field`),或者调用成员方法(`o.method()`),则会分别抛出 `FIELD_NOT_FOUND` 和 `METHOD_NOT_FOUND` 错误。 + +这是因为 3 可以没有限制地通过反射访问 Java 应用系统中的任意字段和方法,这在嵌入式脚本中被认为是不安全的。 + +如果想兼容 3 的行为,则在新建 `Express4Runner` 时, 要将安全策略设置为 “开放”,参考代码如下: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyOpen] +---- + +详细参考 link:#安全策略[安全策略] 章节。 + +=== 定义映射 + +QLExpress 老版本支持通过 `NewMap(key:value)` 的方式快速创建映射,虽然在文档中没有详细讨论,但是很多用户通过单元测试和询问的方式,知晓并使用了这个语法。 + +不过这种语法过于定制,也和业界的规范相差很大,因此在新版中将其移除。 + +新版原生支持 JSON 语法,直接采用 JSON 字典的格式(`{key:value}`)即可快速创建映射,更加直观。 + +详细参考 link:#方便语法元素[方便语法元素] + +=== 全局变量污染上下文 + +QLExpress 支持在执行脚本时传入一个全局的上下文,即 context 参数。 + +在老版本中,如果脚本中定义了全局变量,则这些变量也会写入到 context。在脚本执行结束后,可以通过 context 获取到脚本中定义的全局变量的值。 + +一个老版本的列子如下: + +[source,java] +---- +// only for QLExpress 3.x + +String express = "a=3;a+1"; +ExpressRunner runner = new ExpressRunner(false, true); +DefaultContext context = new DefaultContext<>(); + +Object res = runner.execute(express, context, null, true, true); +// The result of the script execution should be 4 (a+1) +Assert.assertEquals(4, res); +// The variable 'a' defined in the script is also stored in the context +Assert.assertEquals(3, context.get("a")); +---- + +根据调研和反馈,我们认为这会导致全局上下文被脚本 “污染”,存在安全性问题。 + +因此在 QLExpress4 中,全局变量默认不会写入到 context 中。 + +如果想要兼容 3 的特性,需要将 `polluteUserContext` 选项设置为 `true`,参考代码如下: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=polluteUserContext] +---- + +=== 分号可省略 + +"分号可省略" 已经是现代脚本语言的一个标配,QLExpress4 也跟进了这个特性,分号是可以省略的。 + +具体参考 link:#分号[分号] 章节。 + +=== 严格换行模式 + +由于 QLExpress4 支持分号可省略,解释器需要通过换行符来判断语句是否结束。因此,QLExpress4 对换行的要求比 QLExpress3 更加严格。 + +下面的脚本在 QLExpress3 中是合法的,但在 QLExpress4 中不是: + +[source,java] +---- +// 在 QLExpress3 中合法的脚本,但在 QLExpress4 中不是 +商家应收= + 价格 + - 饭卡商家承担 + + 平台补贴 +---- + +在 QLExpress4 中,上述脚本会被解析为两个独立的语句: +1. `商家应收 = 价格` +2. `- 饭卡商家承担 + 平台补贴`(第二个语句会报语法错误) + +如果要在 QLExpress4 中实现同样的效果,需要将操作符放在行尾,而不是行首: + +[source,java] +---- +// QLExpress4 中正确的写法 +商家应收= + 价格 - + 饭卡商家承担 + + 平台补贴 +---- + +这样解释器就知道当前行的表达式还未结束,会继续读取下一行。 + +如果您需要兼容 QLExpress3 的换行特性,可以设置 `strictNewLines` 选项为 `false`: + +[source,java] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=notStrictNewLinesTest] +---- + +注意:非严格换行模式会让解释器忽略所有换行符,可能会影响代码的可读性和错误提示的准确性。建议仅在需要兼容旧代码时使用。 + +=== 获得 char 类型 + +在 QLExpress 3 中,单引号包裹的单个字符会被解析为 char 类型,而不是 String。 + +这个给用户带来了不少困惑,比如 `"a"=='a'` 的判断结果是 `false`。 + +所以后来 QLExpress 3 中新增了 `ExpressRunner.setIgnoreConstChar` 选项,设置为 `true` 后,所有的单引号和双引号包裹的字符都会被解析为 String 类型。但是这个选项默认是关闭的,需要用户手动开启。 + +考虑到脚本用户很少会使用到 `char` 这种底层类型,我们在 QLExpress 4 中直接取消了这个选项,所有的单引号和双引号包裹的字符都会被解析为 String 类型。 + +如果您在脚本还是需要使用 `char` 类型,可以通过两种方法获得: + +* 类型强转:`(char) 'a'` +* 类型声明:`char a = 'a'` + +== 附录二 如何贡献? + +QLExpress 对社区的更改完全开放,任何建议和修改,都会受到欢迎,讨论后合理最后会被接纳到主干中。 + +首先需要将代码 clone 到本地,在正式修改代码前,需要先进行如下准备: + +1. 项目根目录执行 `mvn compile`:项目刚刚下载到本地时,会有大量的类找不到,需要先生成 Antlr4 的运行时代码 +2. 配置代码格式化:QLExpress 项目有统一的代码格式规范,开发前需要配置在 git 提交前的自动格式化 + +在项目目录下新建文件 `.git/hooks/pre-commit`,内容如下: + +[source,bash] +---- +#!/bin/sh +mvn spotless:apply +git add -u +exit 0 +---- + +这样在每次 git commit 之前,就会自动执行 maven 的 spotless 插件执行代码格式化,具体代码格式配置见 link:spotless_eclipse_formatter.xml[] + +3. 执行单元测试:在开发完成代码之后,先在本地全量执行单元测试,确保代码质量 + - JDK 8 环境:执行 `mvn test` + - JDK 9 及以上环境:执行 `mvn test -DargLine="--add-opens java.base/java.util.stream=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED"` + +== 附录三 QLExpress4性能提升 + +link:https://www.yuque.com/xuanheng-ffjti/iunlps/pgfzw46zel2xfnie?singleDoc#%20%E3%80%8AQLExpress3%E4%B8%8E4%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94%E3%80%8B[QLExpress4与3性能对比] + +总结:常见场景下,无编译缓存时,QLExpress4能比3有接近10倍性能提升;有编译缓存,也有一倍性能提升。 + +== 附录四 开发者联系方式 + + * Email: + ** qinyuan.dqy@alibaba-inc.com + ** yumin.pym@taobao.com + ** 704643716@qq.com + * WeChat: + ** xuanheng: dqy932087612 + ** binggou: pymbupt + ** linxiang: tkk33362 + * DingTalk Support Group + +image::images/qlexpress_support_group_qr_2026.jpg[] diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..0a2ebf9 --- /dev/null +++ b/README.adoc @@ -0,0 +1,1894 @@ +:toc: + += QLExpress + +image::images/logo.png[] + +【中文版】| link:README-EN.adoc[[English\]] + +image::https://api.star-history.com/svg?repos=alibaba/QLExpress&type=Date[Star History Chart] + +== 背景介绍 + +由阿里的电商业务规则演化而来的嵌入式Java动态脚本工具,在阿里集团有很强的影响力,同时为了自身不断优化、发扬开源贡献精神,于2012年开源。 + +在基本的表达式计算的基础上,还有以下特色: + +* 灵活的自定义能力,通过 Java API 自定义函数和操作符,可以快速实现业务规则的 DSL +* 兼容最新的 Java 语法,方便 Java 程序员快速熟悉。熟悉类 C 语言的业务人员使用起来也会非常顺手 +* 原生支持 JSON,快捷定义复杂数据结构 +* 友好的报错提示,无论是编译还是运行时错误,都能精确友好地提示错误位置 +* 独一无二的表达式追踪功能,可以追踪表达式在中间节点计算的值,方便业务人员或者 AI 对线上规则的计算结果进行归因分析 +* 默认安全,脚本默认不允许和应用代码进行交互,如果需要交互,也可以自行定义安全的交互方式 +* 解释执行,不占用 JVM 元空间,可以开启缓存提升解释性能 +* 代码精简,依赖最小,适合所有java的运行环境 + +QLExpress4 作为 QLExpress 的最新演进版本,基于 Antlr4 重写了解析引擎,将原先的优点进一步发扬光大,新增了大量特色功能,彻底拥抱函数式编程,在性能和表达能力上都进行了进一步增强。 + +> 如果项目还是使用旧版本的 QLExpress,可以跳转 link:https://github.com/alibaba/QLExpress/tree/branch_version_3.x.x[branch_version_3.x.x] 维护分支查看旧版文档。如需升级可以参考 link:#附录一-升级指南[附录一-升级指南] + +场景举例: + +* 电商优惠券规则配置:通过 QLExpress 自定义函数和操作符快速实现优惠规则 DSL,供运营人员根据需求自行动态配置 +* 表单搭建控件关联规则配置:表单搭建平台允许用户拖拽控件搭建自定义的表单,利用 QLExpress 脚本配置不同控件间的关联关系 +* 流程引擎条件规则配置 +* 广告系统计费规则配置 + +\...\... + +== 新版特色 + +新版本并不是对旧版本的简单功能重构,而是我们基于对用户需求的洞察,对下一代规则表达式引擎的探索。拥有许多非常实用,但是在其他引擎中缺失的重要功能。 + +=== 表达式计算追踪 + +在业务人员完成规则脚本的配置后,很难对其线上执行情况进行感知。比如电商的促销规则,要求用户满足规则 `isVip && 未登录10天以上`。到底有多少线上用户是被 vip 条件拦截,又有多少用户是因为登录条件被拦截?这还是只是仅仅两个条件的简单规则,实际线上情况则更加复杂。 + +线上规则执行情况的追踪,不仅仅可以帮助业务人员了解线上的实际情况,排查和修复问题。其沉淀的数据也非常有价值,可以用于后续的规则优化和业务决策。以下是某个规则平台,基于 QLExpress4 的表达式追踪能力,对规则进行归因分析与附注的决策的产品简化图: + +image::images/order_rules_cn.png[] + +归因分析的原理在于利用 QLExpress4 的表达式追踪能力,获得表达式在计算过程中每个中间结果的值, 据此判断表达式最终运行结果产生的原因。 + +具体使用方法参考:link:#表达式计算追踪-1[表达式计算追踪] + +=== 原生支持 JSON 语法 + +QLExpress4 原生支持 JSON 语法,可以快捷定义复杂的数据结构。 + +JSON 数组代表列表(List),而 JSON 对象代表映射(Map),也可以直接定义复杂对象。 + +产品上可以基于该特性实现 JSON 映射规则。让用户可以便捷地定义从一个模型向另一个模型的映射关系。以下是某个规则平台,基于该能力实现的模型映射产品简化图: + +image::images/json_map.png[] + +具体使用方法参考:link:#方便语法元素[方便语法元素] + +=== 便捷字符串处理 + +QLExpress4 对字符串处理能力进行针对性的增强,在字符串中可以直接通过 `$\{expression}` 嵌入表达式计算结果。 + +具体使用方法参考:link:#动态字符串[动态字符串] + +=== 附件透传 + +正常情况下,脚本执行需要的全部信息都在 `context` 中。context 中的 key 可以在脚本中作为变量引用,最终传递给自定义函数或者操作符。 + +但是出于安全,或者方便使用等因素考虑。有些信息并不希望用户通过变量引用到,比如租户名,密码等等。 + +此时可以通过附件(attachments)将这部分信息传递给自定义函数或者操作符使用。 + +具体使用方法参考:link:#添加自定义函数与操作符[添加自定义函数与操作符] 其中 `hello` 自定义函数根据附件中租户不同,返回不同的欢迎信息的示例。 + +=== 函数式编程 + +函数被提升为 QLExpress4 中的第一等公民,可以作为变量使用,也可以作为函数的返回值。并且可以很容易地和 Java 中常见的函数式 API(比如 Stream) 结合使用。 + +以下是一个简单的 QLExpress 示例脚本: + +[source,java] +---- +add = (a, b) -> { + return a + b; +}; +i = add(1,2); +assert(i == 3); +---- + +更多使用方法参考: + +* link:#lambda-表达式[Lambda表达式] +* link:#列表过滤和映射[列表过滤和映射] +* link:#stream-api[Stream API] +* link:#函数式接口[函数式接口] + +=== 分号简化 + +QLExpress4 支持省略分号,让表达式更加简洁。具体参考 link:#分号[分号] + +== API 快速入门 + +=== 引入依赖 + +[source,xml] +---- + + com.alibaba + qlexpress4 + 4.0.7 + +---- + +环境要求: + +* JDK 8 或更高版本 + +=== 第一个 QLExpress 程序 + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map context = new HashMap<>(); + context.put("a", 1); + context.put("b", 2); + context.put("c", 3); + Object result = express4Runner.execute("a + b * c", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(7, result); +---- + +更多的表达式执行方式见文档 link:docs/execute.adoc[表达式执行] + +=== 添加自定义函数与操作符 + +最简单的方式是通过 Java Lambda 表达式快速定义函数/操作符的逻辑: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // custom function + express4Runner.addVarArgsFunction("join", + params -> Arrays.stream(params).map(Object::toString).collect(Collectors.joining(","))); + Object resultFunction = + express4Runner.execute("join(1,2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultFunction); + + // custom operator + express4Runner.addOperatorBiFunction("join", (left, right) -> left + "," + right); + Object resultOperator = + express4Runner.execute("1 join 2 join 3", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultOperator); +---- + +如果自定义函数的逻辑比较复杂,或者需要获得脚本的上下文信息,也可以通过继承 `CustomFunction` 的方式实现。 + +比如下面的 `hello` 自定义函数,根据租户不同,返回不同的欢迎信息: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.test.function; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +public class HelloFunction implements CustomFunction { + @Override + public Object call(QContext qContext, Parameters parameters) + throws Throwable { + String tenant = (String)qContext.attachment().get("tenant"); + return "hello," + tenant; + } +} +---- + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addFunction("hello", new HelloFunction()); + String resultJack = (String)express4Runner.execute("hello()", + Collections.emptyMap(), + // Additional information(tenant for example) can be brought into the custom function from outside via attachments + QLOptions.builder().attachments(Collections.singletonMap("tenant", "jack")).build()).getResult(); + assertEquals("hello,jack", resultJack); + String resultLucy = + (String)express4Runner + .execute("hello()", + Collections.emptyMap(), + QLOptions.builder().attachments(Collections.singletonMap("tenant", "lucy")).build()) + .getResult(); + assertEquals("hello,lucy", resultLucy); +---- + +QLExpress4还支持通过QLExpress脚本添加自定义函数。需要注意的是,在函数外定义的变量(如示例中的defineTime)在函数定义时就已初始化完成,后续调用函数时不会重新计算该变量的值。 + +[source,java,indent=0] +---- + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + BatchAddFunctionResult addResult = express4Runner.addFunctionsDefinedInScript( + "function myAdd(a,b) {\n" + " return a+b;" + "}\n" + "\n" + "function getCurrentTime() {\n" + + " return System.currentTimeMillis();\n" + "}" + "\n" + "defineTime=System.currentTimeMillis();\n" + + "function defineTime() {\n" + " return defineTime;" + "}\n", + ExpressContext.EMPTY_CONTEXT, + QLOptions.DEFAULT_OPTIONS); + assertEquals(3, addResult.getSucc().size()); + QLResult result = express4Runner.execute("myAdd(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertEquals(3, result.getResult()); + + QLResult resultCurTime1 = + express4Runner.execute("getCurrentTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + Thread.sleep(1000); + QLResult resultCurTime2 = + express4Runner.execute("getCurrentTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertNotSame(resultCurTime1.getResult(), resultCurTime2.getResult()); + + /* + * The defineTime variable is defined outside the function and is initialized when the function is defined; + * it is not recalculated afterward, so the value returned is always the time at which the function was defined. + */ + QLResult resultDefineTime1 = + express4Runner.execute("defineTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + Thread.sleep(1000); + QLResult resultDefineTime2 = + express4Runner.execute("defineTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertSame(resultDefineTime1.getResult(), resultDefineTime2.getResult()); +---- + +建议尽可能使用Java方式定义自定义函数,这样可以获得更好的性能和稳定性。 + +更多自定义语法元素的方式见文档 link:docs/custom-item.adoc[自定义语法元素] + +=== 校验语法正确性 + +在不执行脚本的情况下,单纯校验语法的正确性,其中包含了操作符的限制校验,调用 `check` 并且捕获异常,如果捕获到 `QLSyntaxException`,则说明存在语法错误 + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + express4Runner.check("a+b;\n(a+b"); + fail(); + } + catch (QLSyntaxException e) { + assertEquals(2, e.getLineNo()); + assertEquals(4, e.getColNo()); + assertEquals("SYNTAX_ERROR", e.getErrorCode()); + // represents the end of script + assertEquals( + "[Error SYNTAX_ERROR: mismatched input '' expecting ')']\n" + "[Near: a+b; (a+b]\n" + + " ^^^^^\n" + "[Line: 2, Column: 4]", + e.getMessage()); + } +---- + +你可以使用 `CheckOptions` 配置更精细的语法校验规则,主要支持以下两个选项: + +1. `operatorCheckStrategy`: 操作符校验策略,用于限制脚本中可以使用的操作符 +2. `disableFunctionCalls`: 是否禁用函数调用,默认为 false + +示例1:使用操作符校验策略(白名单) + +[source,java,indent=0] +---- + // Create a whitelist of allowed operators + Set allowedOps = new HashSet<>(Arrays.asList("+", "*")); + + // Configure check options with operator whitelist + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build(); + + // Create runner and check script with custom options + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.check("a + b * c", checkOptions); // This will pass as + and * are allowed +---- + +示例2:禁用函数调用 + +[source,java,indent=0] +---- + // Create a runner + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Create options with function calls disabled + CheckOptions options = CheckOptions.builder().disableFunctionCalls(true).build(); + + // Script with function call + String scriptWithFunctionCall = "Math.max(1, 2)"; + + // Use custom options to check script + try { + runner.check(scriptWithFunctionCall, options); + } + catch (QLSyntaxException e) { + // Will throw exception as function calls are disabled + } +---- + +=== 解析脚本所需外部变量 + +脚本中使用的变量有的是脚本内生,有的是需要从外部通过 `context` 传入的。 + +QLExpress4 提供了一个方法,可以解析出脚本中所有需要从外部传入的变量: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Set outVarNames = + express4Runner.getOutVarNames("int a = 1, b = 10;\n" + "c = 11\n" + "e = a + b + c + d\n" + "f+e"); + Set expectSet = new HashSet<>(); + expectSet.add("d"); + expectSet.add("f"); + assertEquals(expectSet, outVarNames); +---- + +更多脚本依赖解析工具: + +* `getOutFunctions`: 解析所有需要从外部定义的函数 +* `getOutVarAttrs`:解析所有需要从外部传入变量及其涉及的属性,`getOutVarNames` 的增强版本 + +=== 高精度计算 + +QLExpress 内部会用 BigDecimal 表示所有无法用 double 精确表示数字,来尽可能地表示计算精度: + +> 举例:0.1 在 double 中无法精确表示 + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = express4Runner.execute("0.1", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertTrue(result instanceof BigDecimal); +---- + +通过这种方式能够解决一些计算精度问题: + +比如 0.1+0.2 因为精度问题,在 Java 中是不等于 0.3 的。 +而 QLExpress 能够自动识别出 0.1 和 0.2 无法用双精度精确表示,改成用 BigDecimal 表示,确保其结果等于0.3 + +[source,java,indent=0] +---- + assertNotEquals(0.3, 0.1 + 0.2, 0.0); + assertTrue((Boolean)express4Runner.execute("0.3==0.1+0.2", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); +---- + +除了默认的精度保证外,还提供了 `precise` 开关,打开后所有的计算都使用BigDecimal,防止外部传入的低精度数字导致的问题: + +[source,java,indent=0] +---- + Map context = new HashMap<>(); + context.put("a", 0.1); + context.put("b", 0.2); + assertFalse((Boolean)express4Runner.execute("0.3==a+b", context, QLOptions.DEFAULT_OPTIONS).getResult()); + // open precise switch + assertTrue((Boolean)express4Runner.execute("0.3==a+b", context, QLOptions.builder().precise(true).build()) + .getResult()); +---- + +=== 安全策略 + +QLExpress4 默认采用隔离安全策略,不允许脚本访问 Java 对象的字段和方法,这确保了脚本执行的安全性。如果需要访问 Java 对象,可以通过不同的安全策略进行配置。 + +假设应用中有如下的 Java 类: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.inport; + +/** + * Author: DQinYuan + */ +public class MyDesk { + + private String book1; + + private String book2; + + public String getBook1() { + return book1; + } + + public void setBook1(String book1) { + this.book1 = book1; + } + + public String getBook2() { + return book2; + } + + public void setBook2(String book2) { + this.book2 = book2; + } +} +---- + +脚本执行的上下文设置如下: + +[source,java,indent=0] +---- + MyDesk desk = new MyDesk(); + desk.setBook1("Thinking in Java"); + desk.setBook2("Effective Java"); + Map context = Collections.singletonMap("desk", desk); +---- + +QLExpress4 提供了四种安全策略: + +==== 1. 隔离策略(默认) + +默认情况下,QLExpress4 采用隔离策略,不允许访问任何字段和方法: + +[source,java,indent=0] +---- + // default isolation strategy, no field or method can be found + Express4Runner express4RunnerIsolation = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + assertErrorCode(express4RunnerIsolation, context, "desk.book1", "FIELD_NOT_FOUND"); + assertErrorCode(express4RunnerIsolation, context, "desk.getBook2()", "METHOD_NOT_FOUND"); +---- + +==== 2. 黑名单策略 + +通过黑名单策略,可以禁止访问特定的字段或方法,其他字段和方法可以正常访问: + +[source,java,indent=0] +---- + // black list security strategy + Set memberList = new HashSet<>(); + memberList.add(MyDesk.class.getMethod("getBook2")); + Express4Runner express4RunnerBlackList = new Express4Runner( + InitOptions.builder().securityStrategy(QLSecurityStrategy.blackList(memberList)).build()); + assertErrorCode(express4RunnerBlackList, context, "desk.book2", "FIELD_NOT_FOUND"); + Object resultBlack = + express4RunnerBlackList.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertEquals("Thinking in Java", resultBlack); +---- + +==== 3. 白名单策略 + +通过白名单策略,只允许访问指定的字段或方法,其他字段和方法都会被禁止: + +[source,java,indent=0] +---- + // white list security strategy + Express4Runner express4RunnerWhiteList = new Express4Runner( + InitOptions.builder().securityStrategy(QLSecurityStrategy.whiteList(memberList)).build()); + Object resultWhite = + express4RunnerWhiteList.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertEquals("Effective Java", resultWhite); + assertErrorCode(express4RunnerWhiteList, context, "desk.getBook1()", "METHOD_NOT_FOUND"); +---- + +==== 4. 开放策略 + +开放策略允许访问所有字段和方法,类似于 QLExpress3 的行为,但需要注意安全风险: + +[source,java,indent=0] +---- + // open security strategy + Express4Runner express4RunnerOpen = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Assert.assertEquals("Thinking in Java", + express4RunnerOpen.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Effective Java", + express4RunnerOpen.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult()); +---- + +> 注意:开放策略虽然提供了最大的灵活性,但也带来了安全风险。建议只在受信任的环境中使用,不建议用于处理终端用户输入的脚本。 + +==== 策略建议 + +建议直接采用默认策略,在脚本中不要直接调用 Java 对象的字段和方法。而是通过自定义函数和操作符的方式(参考 link:#添加自定义函数与操作符[添加自定义函数与操作符]),对嵌入式脚本提供系统功能。这样能同时保证脚本的安全性和灵活性,用户体验还更好。 + +如果确实需要调用 Java 对象的字段和方法,至少应该使用白名单策略,只提供脚本有限的访问权限。 + +至于黑名单和开放策略,不建议在外部输入脚本的场景使用,除非确保每个脚本都会经过审核。 + +=== 调用应用中的 Java 类 + +> 需要放开安全策略,不建议用于终端用户输入 + +假设应用中有如下的 Java 类(`com.alibaba.qlexpress4.QLImportTester`): + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4; + +public class QLImportTester { + + public static int add(int a, int b) { + return a + b; + } + +} +---- + +在 QLExpress 中有如下两种调用方式。 + +==== 1. 在脚本中使用 `import` 语句导入类并且使用 + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.builder() + // open security strategy, which allows access to all Java classes within the application. + .securityStrategy(QLSecurityStrategy.open()) + .build()); + // Import Java classes using the import statement. + Map params = new HashMap<>(); + params.put("a", 1); + params.put("b", 2); + Object result = + express4Runner + .execute("import com.alibaba.qlexpress4.QLImportTester;" + "QLImportTester.add(a,b)", + params, + QLOptions.DEFAULT_OPTIONS) + .getResult(); + Assert.assertEquals(3, result); +---- + +==== 2. 在创建 `Express4Runner` 时默认导入该类,此时脚本中就不需要额外的 `import` 语句 + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.builder() + .addDefaultImport( + Collections.singletonList(ImportManager.importCls("com.alibaba.qlexpress4.QLImportTester"))) + .securityStrategy(QLSecurityStrategy.open()) + .build()); + Object result = + express4Runner.execute("QLImportTester.add(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult(); + Assert.assertEquals(3, result); +---- + +除了用 `ImportManager.importCls` 导入单个类外,还有其他更方便的导入方式: + + * `ImportManager.importPack` 直接导入包路径下的所有类,比如 `ImportManager.importPack("java.util")` 会导入 `java.util` 包下的所有类,QLExpress 默认就会导入下面的包 + ** `ImportManager.importPack("java.lang")` + ** `ImportManager.importPack("java.util")` + ** `ImportManager.importPack("java.math")` + ** `ImportManager.importPack("java.util.stream")` + ** `ImportManager.importPack("java.util.function")` + * `ImportManager.importInnerCls` 导入给定类路径里的所有内部类 + +=== 自定义 ClassLoader + +QLExpress4 支持通过自定义 `ClassSupplier` 来指定类加载器,这在插件化架构、模块化应用等场景中非常有用。通过自定义类加载器,可以让 QLExpress 脚本访问特定 ClassLoader 中的类。 + +下面的示例展示了如何与 link:https://pf4j.org/[PF4J] 插件框架集成,让 QLExpress 脚本能够访问插件中的类: + +[source,java,indent=0] +---- + // Specify plugin directory (test-plugins directory under test resources) + Path pluginsDir = new File("src/test/resources/test-plugins").toPath(); + PluginManager pluginManager = new DefaultPluginManager(pluginsDir); + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + + // Get the PluginClassLoader of the first plugin + PluginWrapper plugin = pluginManager.getPlugins().get(0); + ClassLoader pluginClassLoader = plugin.getPluginClassLoader(); + + // Custom class supplier using plugin ClassLoader + ClassSupplier pluginClassSupplier = clsName -> { + try { + return Class.forName(clsName, true, pluginClassLoader); + } + catch (ClassNotFoundException | NoClassDefFoundError e) { + return null; + } + }; + + InitOptions options = InitOptions.builder() + .securityStrategy(QLSecurityStrategy.open()) + .classSupplier(pluginClassSupplier) + .build(); + Express4Runner runner = new Express4Runner(options); + + String script = "import com.alibaba.qlexpress4.pf4j.TestPluginInterface; TestPluginInterface.TEST_CONSTANT"; + Object result = runner.execute(script, Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + + Assert.assertEquals("Hello from PF4J Plugin!", result.toString()); +---- + +自定义 ClassSupplier 的典型应用场景: + +* **插件化架构**:让脚本能够访问插件中定义的类和接口 +* **模块化应用**:在 OSGi 等模块化框架中,让脚本访问特定模块的类 +* **动态类加载**:从远程仓库或动态生成的字节码中加载类 +* **类隔离**:使用不同的 ClassLoader 来实现类的隔离 + +=== 表达式缓存 + +通过 `cache` 选项可以开启表达式缓存,这样相同的表达式就不会重新编译,能够大大提升性能。 + +注意该缓存没有限制大小,只适合在表达式为有限数量的情况下使用: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // open cache switch + express4Runner.execute("1+2", new HashMap<>(), QLOptions.builder().cache(true).build()); +---- + +但是当脚本首次执行时,因为没有缓存,依旧会比较慢。 + +可以通过下面的方法在首次执行前就将脚本缓存起来,保证首次执行的速度: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.parseToDefinitionWithCache("a+b"); +---- + +注意该缓存的大小是无限的,业务上注意控制大小,可以调用 `clearCompileCache` 方法定期清空编译缓存。 + +=== 清除 DFA 缓存 + +QLExpress 使用 ANTLR4 作为解析引擎,ANTLR4 在运行时会构建 DFA (确定有限状态自动机) 缓存来加速后续的语法解析。这个缓存会占用一定的内存空间。 + +在某些内存敏感的场景下,可以通过调用 `clearDFACache` 方法来清除 DFA 缓存,释放内存。 + +> **重要警告**: 清除 DFA 缓存会导致编译性能大幅下降,正常情况下不建议使用此方法。 + +==== 适用场景 + +* **内存敏感型应用**: 当内存使用是关键考虑因素,且可以容忍较慢的编译时间时 +* **脚本不经常变更**: 当脚本相对稳定且不会频繁重新编译时 + +==== 最佳实践 + +在解析并缓存表达式后立即调用此方法,并确保后续所有执行都打开缓存选项以避免重新编译。示例代码如下: + +[source,java,indent=0] +---- + /* + * When the expression changes, parse it and add it to the expression cache; + * after parsing is complete, call clearDFACache. + */ + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.parseToDefinitionWithCache(exampleExpress); + runner.clearDFACache(); + + /* + * All subsequent runs of this script must enable the cache option to ensure that re-compilation does not occur. + */ + for (int i = 0; i < 3; i++) { + runner.execute(exampleExpress, ExpressContext.EMPTY_CONTEXT, QLOptions.builder().cache(true).build()); + } +---- + +通过这种方式,可以在保证性能的同时,最大限度地降低内存占用。 + +=== 设置超时时间 + +可以给脚本设置一个超时时间,防止其中存在死循环或者其他原因导致应用资源被过量消耗。 + +下面的示例代码给脚本给脚本设置了一个 10ms 的超时时间: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + express4Runner.execute("while (true) {\n" + " 1+1\n" + "}", + Collections.emptyMap(), + QLOptions.builder().timeoutMillis(10L).build()); + fail("should timeout"); + } + catch (QLTimeoutException e) { + assertEquals(QLErrorCodes.SCRIPT_TIME_OUT.name(), e.getErrorCode()); + } +---- + +> 注意,出于系统性能的考虑,QLExpress 对于超时时间的检测是不准确的。特别是在回调Java代码中(比如自定义函数或者操作符)出现的超时,不会立刻被检测到。只有在执行完,回到 QLExpress 运行时后才会被检测到并中断执行。 + +=== 扩展函数 + +利用 QLExpress 提供的扩展函数能力,可以给Java类中添加额外的成员方法。 + +扩展函数是基于 QLExpress 运行时实现的,因此仅仅在 QLExpress 脚本中有效。 + +下面的示例代码给 String 类添加了一个 `hello()` 扩展函数: + +[source,java,indent=0] +---- + ExtensionFunction helloFunction = new ExtensionFunction() { + @Override + public Class[] getParameterTypes() { + return new Class[0]; + } + + @Override + public String getName() { + return "hello"; + } + + @Override + public Class getDeclaringClass() { + return String.class; + } + + @Override + public Object invoke(Object obj, Object[] args) { + String originStr = (String)obj; + return "Hello," + originStr; + } + }; + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addExtendFunction(helloFunction); + Object result = + express4Runner.execute("'jack'.hello()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("Hello,jack", result); + + // simpler way to define extension function + express4Runner.addExtendFunction("add", + Number.class, + params -> ((Number)params[0]).intValue() + ((Number)params[1]).intValue()); + QLResult resultAdd = express4Runner.execute("1.add(2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertEquals(3, resultAdd.getResult()); + +---- + +=== Java类的对象,字段和方法别名 + +QLExpress 支持通过 `QLAlias` 注解给对象,字段或者方法定义一个或多个别名,方便非技术人员使用表达式定义规则。 + +下面的例子中,根据用户是否 vip 计算订单最终金额。 + +用户类定义: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.test.qlalias; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +@QLAlias("用户") +public class User { + + @QLAlias("是vip") + private boolean vip; + + @QLAlias("用户名") + private String name; + + public boolean isVip() { + return vip; + } + + public void setVip(boolean vip) { + this.vip = vip; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +---- + +订单类定义: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.test.qlalias; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +@QLAlias("订单") +public class Order { + + @QLAlias("订单号") + private String orderNum; + + @QLAlias("金额") + private int amount; + + public String getOrderNum() { + return orderNum; + } + + public void setOrderNum(String orderNum) { + this.orderNum = orderNum; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } +} +---- + +通过 QLExpress 脚本规则计算最终订单金额: + +[source,java,indent=0] +---- + Order order = new Order(); + order.setOrderNum("OR123455"); + order.setAmount(100); + + User user = new User(); + user.setName("jack"); + user.setVip(true); + + // Calculate the Final Order Amount + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Number result = (Number)express4Runner + .executeWithAliasObjects("用户.是vip? 订单.金额 * 0.8 : 订单.金额", QLOptions.DEFAULT_OPTIONS, order, user) + .getResult(); + assertEquals(80, result.intValue()); +---- + +=== 关键字,操作符和函数别名 + +为了进一步方面非技术人员编写规则,QLExpress 提供 `addAlias` 给原始关键字,操作符和函数增加别名。让整个脚本的表述更加贴近自然语言。 + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // add custom function zero + express4Runner.addFunction("zero", (String ignore) -> 0); + + // keyword alias + assertTrue(express4Runner.addAlias("如果", "if")); + assertTrue(express4Runner.addAlias("则", "then")); + assertTrue(express4Runner.addAlias("否则", "else")); + assertTrue(express4Runner.addAlias("返回", "return")); + // operator alias + assertTrue(express4Runner.addAlias("大于", ">")); + // function alias + assertTrue(express4Runner.addAlias("零", "zero")); + + Map context = new HashMap<>(); + context.put("语文", 90); + context.put("数学", 90); + context.put("英语", 90); + + Object result = express4Runner + .execute("如果 (语文 + 数学 + 英语 大于 270) 则 {返回 1;} 否则 {返回 零();}", context, QLOptions.DEFAULT_OPTIONS) + .getResult(); + assertEquals(0, result); +---- + +支持设置别名的关键字有: + + * if + * then + * else + * for + * while + * break + * continue + * return + * function + * macro + * new + * null + * true + * false + +> 注意:部分大家熟悉的用法其实是操作符,而不是关键字,比如 `in` 操作符。而所有的操作符和函数默认就是支持别名的 + +=== 宏 + +宏是QLExpress中一个强大的代码复用机制,它允许用户定义一段可重用的脚本片段,并在需要时进行调用。与简单的文本替换不同,QLExpress的宏是基于指令回放的机制实现的,具有更好的性能和语义准确性。 + +宏特别适用于以下场景: + +* **代码复用**:将常用的脚本片段封装成宏,避免重复编写相同的逻辑 +* **业务规则模板**:定义标准的业务规则模板,如价格计算、权限检查等 +* **流程控制**:封装复杂的控制流程,如条件判断、循环逻辑等 +* **DSL构建**:作为构建领域特定语言的基础组件 + +宏可以通过两种方式定义: + +**1. 在脚本中使用 `macro` 关键字定义** + +[source,java] +---- +macro add { + c = a + b; +} + +a = 1; +b = 2; +add; +assert(c == 3); +---- + +**2. 通过Java API添加** + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addMacro("rename", "name='haha-'+name"); + Map context = Collections.singletonMap("name", "wuli"); + Object result = express4Runner.execute("rename", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("haha-wuli", result); + + // replace macro + express4Runner.addOrReplaceMacro("rename", "name='huhu-'+name"); + Object result1 = express4Runner.execute("rename", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("huhu-wuli", result1); +---- + +宏与函数的区别: + +[cols="1,1,1"] +|=== +| 特性 | 宏 | 函数 +| 参数传递 | 无参数,依赖上下文变量 | 支持参数传递 +| 性能 | 指令直接插入,无调用开销 | 有函数调用开销 +| 作用域 | 共享调用者作用域 | 独立的作用域 +| 适用场景 | 代码片段复用 | 逻辑封装和参数化 +|=== + +宏特别适合那些不需要参数传递、主要依赖上下文变量的代码片段复用场景,而函数更适合需要参数化和独立作用域的场景。 + +**QLExpress4 相比 3 版本,宏特性的变化**: + + * 4 的宏实现更加接近通常编程语言中宏的定义,相当于将预定义的代码片段插入到宏所在的位置,与调用点位于同一作用域,宏中的 `return`, `contine` 和 `break` 等可以影响调用方的控制流。但是 3 中的实现其实更加接近无参函数调用。 + * 4 的宏无法作为变量使用,只有单独作为一行语句时才能被宏替换。因为宏可以是任意脚本,不一定是有返回值的表达式,作为变量时会存在语义问题。3 的宏本质是一个无参函数调用,所以常常被作为变量使用 + +如果想兼容 3 中的宏特性,建议使用 link:#动态变量[动态变量] + +=== 动态变量 + +常规的 “静态变量”,是 context 中和 key 关联的固定的值。而动态变量可以是一个表达式,由另外一些变量计算而得。动态变量支持嵌套,即动态变量可以依赖另一个动态变量计算得到。 + +示例如下: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + Map staticContext = new HashMap<>(); + staticContext.put("语文", 88); + staticContext.put("数学", 99); + staticContext.put("英语", 95); + + QLOptions defaultOptions = QLOptions.DEFAULT_OPTIONS; + DynamicVariableContext dynamicContext = + new DynamicVariableContext(express4Runner, staticContext, defaultOptions); + dynamicContext.put("平均成绩", "(语文+数学+英语)/3.0"); + dynamicContext.put("是否优秀", "平均成绩>90"); + + // dynamic var + assertTrue((Boolean)express4Runner.execute("是否优秀", dynamicContext, defaultOptions).getResult()); + assertEquals(94, + ((Number)express4Runner.execute("平均成绩", dynamicContext, defaultOptions).getResult()).intValue()); + // static var + assertEquals(187, + ((Number)express4Runner.execute("语文+数学", dynamicContext, defaultOptions).getResult()).intValue()); +---- + +=== 表达式计算追踪 + +如果打开相关选项,QLExpress4 就会在返回规则脚本计算结果的同时,返回一颗表达式追踪树。表达式追踪树的结构类似语法树,不同之处在于,它会在每个节点上记录本次执行的中间结果。 + +比如对于表达式 `!true || myTest(a, 1)`,表达式追踪树的结构大概如下: + +[source] +---- + || true + / \ + ! false myTest + / / \ + true a 10 1 +---- + +可应用于多种场景: + + * 方便业务人员对规则的计算结果进行分析排查 + * 对线上判断为 false 的规则进行采样归类 + * AI 自动诊断和修复规则 + +节点计算结果会被放置到 `ExpressionTrace` 对象的 `value` 字段中。如果中间发生短路导致部分表达式未被计算,则 `ExpressionTrace` 对象的 `evaluated` 字段会被设置为 false。代码示例如下: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.builder().traceExpression(true).build()); + express4Runner.addFunction("myTest", (Predicate)i -> i > 10); + + Map context = new HashMap<>(); + context.put("a", true); + QLResult result = express4Runner + .execute("a && (!myTest(11) || false)", context, QLOptions.builder().traceExpression(true).build()); + Assert.assertFalse((Boolean)result.getResult()); + + List expressionTraces = result.getExpressionTraces(); + Assert.assertEquals(1, expressionTraces.size()); + ExpressionTrace expressionTrace = expressionTraces.get(0); + Assert.assertEquals("OPERATOR && false\n" + " | VARIABLE a true\n" + " | OPERATOR || false\n" + + " | OPERATOR ! false\n" + " | FUNCTION myTest true\n" + " | VALUE 11 11\n" + + " | VALUE false false\n", expressionTrace.toPrettyString(0)); + + // short circuit + context.put("a", false); + QLResult resultShortCircuit = express4Runner.execute("(a && true) && (!myTest(11) || false)", + context, + QLOptions.builder().traceExpression(true).build()); + Assert.assertFalse((Boolean)resultShortCircuit.getResult()); + ExpressionTrace expressionTraceShortCircuit = resultShortCircuit.getExpressionTraces().get(0); + Assert.assertEquals( + "OPERATOR && false\n" + " | OPERATOR && false\n" + " | VARIABLE a false\n" + " | VALUE true \n" + + " | OPERATOR || \n" + " | OPERATOR ! \n" + " | FUNCTION myTest \n" + + " | VALUE 11 \n" + " | VALUE false \n", + expressionTraceShortCircuit.toPrettyString(0)); + Assert.assertTrue(expressionTraceShortCircuit.getChildren().get(0).isEvaluated()); + Assert.assertFalse(expressionTraceShortCircuit.getChildren().get(1).isEvaluated()); + + // in + QLResult resultIn = express4Runner + .execute("'ab' in ['cc', 'dd', 'ff']", context, QLOptions.builder().traceExpression(true).build()); + Assert.assertFalse((Boolean)resultIn.getResult()); + ExpressionTrace expressionTraceIn = resultIn.getExpressionTraces().get(0); + Assert + .assertEquals( + "OPERATOR in false\n" + " | VALUE 'ab' ab\n" + " | LIST [ [cc, dd, ff]\n" + " | VALUE 'cc' cc\n" + + " | VALUE 'dd' dd\n" + " | VALUE 'ff' ff\n", + expressionTraceIn.toPrettyString(0)); +---- + +> 注意,必须在新建 `Express4Runner` 时将 `InitOptions.traceExpression` 选项设置为 true,同时在执行脚本时将 `QLOptions.traceExpression` 设置为 true,该功能才能生效。 + +也可以在不执行脚本的情况下获得所有表达式追踪点: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + TracePointTree tracePointTree = express4Runner.getExpressionTracePoints("1+3+5*ab+9").get(0); + Assert.assertEquals("OPERATOR +\n" + " | OPERATOR +\n" + " | OPERATOR +\n" + " | VALUE 1\n" + + " | VALUE 3\n" + " | OPERATOR *\n" + " | VALUE 5\n" + " | VARIABLE ab\n" + + " | VALUE 9\n", tracePointTree.toPrettyString(0)); +---- + +支持的表达式追踪点类型以及对应子节点的含义如下: + +[cols="1,1,1"] +|=== +| 节点类型 | 节点含义 | 子节点含义 +| OPERATOR | 操作符 | 两侧操作数 +| FUNCTION | 函数 | 函数参数 +| METHOD | 方法 | 方法参数 +| FIELD | 字段 | 取字段的目标对象 +| LIST | 列表 | 列表元素 +| MAP | 字段 | 无 +| IF | 条件分支 | condition表达式,then逻辑块和else逻辑块 +| RETURN | 返回语句 | 返回表达式 +| VARIABLE | 变量 | 无 +| VALUE | 字面值 | 无 +| DEFINE_FUNCTION | 定义函数 | 无 +| DEFINE_MACRO | 定义宏 | 无 +| PRIMARY | 暂时未继续下钻的其他复合值(比如字典,if等等)| 无 +| STATEMENT | 暂未继续下钻的其他复合语句(比如 while, for 等等)| 无 +|=== + +=== 与 Spring 集成 + +QLExpress 并不需要专门与 Spring 集成,只需要一个 `Express4Runner` 单例,即可使用。 + +这里提供的 “集成” 示例,可以在 QLExpress 脚本中直接引用任意 Spring Bean。 + +这种方式虽然很方便,但是脚本权限过大,自由度太高。不再推荐使用,还是建议在 context 只放入允许用户访问的对象。 + +核心集成组件: + +* link:src/test/java/com/alibaba/qlexpress4/spring/QLSpringContext.java[QLSpringContext]: 实现了 `ExpressContext` 接口,提供了对 Spring 容器的访问能力。它会优先从传入的 context 中查找变量,如果找不到则尝试从 Spring 容器中获取同名的 Bean。 +* link:src/test/java/com/alibaba/qlexpress4/spring/QLExecuteService.java[QLExecuteService]: 封装了 QLExpress 的执行逻辑,集成了 Spring 容器,方便在 Spring 应用中使用。 + +假设存在一个 Spring Bean, 名为 `helloService`: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.spring; + +import org.springframework.stereotype.Service; + +/** + * Spring Bean example service class + */ +@Service +public class HelloService { + + /** + * Hello method that returns a greeting string + * @return greeting string + */ + public String hello(String name) { + return "Hello, " + name + "!"; + } +} +---- + +在脚本中调用该 Bean: + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.spring; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.HashMap; +import java.util.Map; + +/** + * HelloService unit test class + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = SpringTestConfig.class) +public class SpringDemoTest { + + @Autowired + private QLExecuteService qlExecuteService; + + @Test + public void qlExecuteWithSpringContextTest() { + Map context = new HashMap<>(); + context.put("name", "Wang"); + String result = (String)qlExecuteService.execute("helloService.hello(name)", context); + Assert.assertEquals("Hello, Wang!", result); + } +} +---- + + +== 语法入门 + +QLExpress4 兼容 Java8 语法的同时,也提供了很多更加灵活宽松的语法模式,帮助用户更快捷地编写表达式。 + +基于表达式优先的语法设计,复杂的条件判断语句也可以直接当作表达式使用。 + +在本章节中出现的代码片段都是 qlexpress 脚本, +`assert` 是测试框架往引擎中注入的断言方法,会确保其参数为 `true`。 +`assertErrCode` 会确保其 lambda 参数表达式的执行一定会抛出含第二个参数 error code 的 QLException。 + +=== 变量声明 + +同时支持静态类型和动态类型: + + * 变量声明时不写类型,则变量是动态类型,也同时是一个赋值表达式 + * 变量声明如果写类型,则是静态类型,此时是一个变量声明语句 + +[source,java] +---- +// Dynamic Typeing +a = 1; +a = "1"; +// Static Typing +int b = 2; +// throw QLException with error code INCOMPATIBLE_ASSIGNMENT_TYPE when assign with incompatible type String +assertErrorCode(() -> b = "1", "INCOMPATIBLE_ASSIGNMENT_TYPE") + +---- + +=== 方便语法元素 + +列表(List),映射(Map)等常用语法元素在 QLExpress 中都有非常方便的构造语法糖: + +[source,java] +---- +// list +l = [1,2,3] +assert(l[0]==1) +assert(l[-1]==3) +// Underlying data type of list is ArrayList in Java +assert(l instanceof ArrayList) +// map +m = { + "aa": 10, + "bb": { + "cc": "cc1", + "dd": "dd1" + } +} +assert(m['aa']==10) +// Underlying data type of map is ArrayList in Java +assert(m instanceof LinkedHashMap) +// empty map +emMap = {:} +emMap['haha']='huhu' +assert(emMap['haha']=='huhu') +---- + +通过 `*.` 操作符,可以快捷地对列表和映射进行处理,比如对列表元素进行取属性,或者获得映射的 key 列表和 value 列表: + +[source,java] +---- +list = [ + { + "name": "Li", + "age": 10 + }, + { + "name": "Wang", + "age": 15 + } +] + +// get field from list +assert(list*.age==[10,15]) + +mm = { + "aaa": 1, + "bbb": 2 +} + +// get map key value list +assert(mm*.key==["aaa", "bbb"]) +assert(mm*.value==[1, 2]) +---- + +在映射中通过 `@class` key 指定类型的全限定名,就可以直接使用 JSON 创建复杂Java对象。比如下面的 MyHome , 是一个含有复杂嵌套类型 Java 类: + +[source,java] +---- +package com.alibaba.qlexpress4.inport; + +/** + * Author: DQinYuan + */ +public class MyHome { + + private String sofa; + + private String chair; + + private MyDesk myDesk; + + private String bed; + + public String getSofa() { + return sofa; + } + + public void setSofa(String sofa) { + this.sofa = sofa; + } + + public String getChair() { + return chair; + } + + public MyDesk getMyDesk() { + return myDesk; + } + + public void setMyDesk(MyDesk myDesk) { + this.myDesk = myDesk; + } + + public void setChair(String chair) { + this.chair = chair; + } + + public String getBed() { + return bed; + } +} +---- + +可以通过下面的 QLExpress 脚本,便捷创建: + +> 注意,该特性需要参考 link:#安全策略[安全策略] 打开安全选项,才能正常执行。 + +[source,java] +---- +myHome = { + '@class': 'com.alibaba.qlexpress4.inport.MyHome', + 'sofa': 'a-sofa', + 'chair': 'b-chair', + 'myDesk': { + 'book1': 'Then Moon and Sixpence', + '@class': 'com.alibaba.qlexpress4.inport.MyDesk' + }, + // ignore field that don't exist + 'notexist': 1234 +} +assert(myHome.getSofa()=='a-sofa') +assert(myHome instanceof com.alibaba.qlexpress4.inport.MyHome) +assert(myHome.getMyDesk().getBook1()=='Then Moon and Sixpence') +assert(myHome.getMyDesk() instanceof com.alibaba.qlexpress4.inport.MyDesk) +---- + +=== 数字 + +对于未声明类型的数字, +QLExpress会根据其所属范围自动从 int, long, BigInteger, double, BigDecimal 等数据类型中选择一个最合适的: + +[source,java] +---- +assert(2147483647 instanceof Integer); +assert(9223372036854775807 instanceof Long); +assert(18446744073709552000 instanceof BigInteger); +// 0.25 can be precisely presented with double +assert(0.25 instanceof Double); +assert(2.7976931348623157E308 instanceof BigDecimal); +---- + +因此在自定义函数或者操作符时,建议使用 Number 类型进行接收,因为数字类型是无法事先确定的。 + +=== 动态字符串 + +动态字符串是 QLExpress 为了增强字符串处理能力,在 4 版本新引入的能力。 + +支持 `$\{expression}` 的格式在字符串中插入表达式计算: + +> 如果想在字符串中原样保持 `$\{expression}`,可以使用 `\$` 对 `$` 进行转义 + +[source,java] +---- +a = 123 +assert("hello,${a-1}" == "hello,122") + +// escape $ with \$ +assert("hello,\${a-1}" == "hello,\${a-1}") + +b = "test" +assert("m xx ${ + if (b like 't%') { + 'YYY' + } +}" == "m xx YYY") +---- + +如果还想让 QLExpress4 的字符串和 3 保持兼容性,不对插值表达式进行处理,可以在新建 `Express4Runner` 时直接关闭该特性: + +[source,java] +---- + Express4Runner express4RunnerDisable = new Express4Runner( + // disable string interpolation + InitOptions.builder().interpolationMode(InterpolationMode.DISABLE).build()); + Assert.assertEquals("Hello,${ a + 1 }", + express4RunnerDisable.execute("\"Hello,${ a + 1 }\"", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Hello,${lll", + express4RunnerDisable.execute("\"Hello,${lll\"", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Hello,aaa $ lll\"\n\b", + express4RunnerDisable.execute("\"Hello,aaa $ lll\\\"\n\b\"", context, QLOptions.DEFAULT_OPTIONS) + .getResult()); +---- + +=== 模板语言 + +利用动态字符串能力,QLExpress4 也可以作为一个轻量级模板引擎使用。 + +无需在脚本中手动添加字符串引号,直接调用 `executeTemplate` 渲染模板字符串: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map context = new HashMap<>(); + context.put("a", 1); + context.put("b", 2); + context.put("c", "test"); + QLResult simpleTemplate = express4Runner.executeTemplate("a ${a};b ${b+2}", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("a 1;b 4", simpleTemplate.getResult()); + QLResult conditionTemplate = + express4Runner.executeTemplate("m xx ${\n" + " if (c like 't%') {\n" + " 'YYY'\n" + " }\n" + "}", + context, + QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("m xx YYY", conditionTemplate.getResult()); + QLResult multiLineTemplate = express4Runner.executeTemplate("m\n ${a}\n c", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("m\n 1\n c", multiLineTemplate.getResult()); + QLResult escapeStringTemplate = + express4Runner.executeTemplate("m \n\"haha\" d\"", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("m \n\"haha\" d\"", escapeStringTemplate.getResult()); +---- + +=== 占位符 + +占位符用于从 context 中提取任意 key 的值。 + +全局变量也可以从 context 中提取值,但是收到 QLExpress 关键词和语法的限制,能提取的 key 有限。 +比如 context 中 "0" key 对应的值就无法通过变量提取,因为 0 不是 QLExpress 中的合法变量,而是一个数字常量。 +此时可以用默认占位符 `$\{0}` 来提取。 + +> 注意和动态字符串中插值区分,占位符是写在字符串之外。动态字符串插值是 `$\{expression}`,其中默认写的是表达式,`"${0}"` 的运行结果是 `"0"`。而占位符是 `$\{placeholder}`,其中默认写的是 context 中的 key,`${0}` 的运行结果是 context 中 "0" key 对应的值。 + +QLExpress 默认使用 `$\{placeholder}` 格式的占位符,其中: + +* `${` 是起始标记 +* `}` 是结束标记 +* `placeholder` 是占位符内容,对应 cotext 中的 key + +除了默认的占位符外,QLExpress 还支持自定义占位符的起始和结束标记: + +[source,java] +---- + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().selectorStart("#[").selectorEnd("]").build()); + + Map context = new HashMap<>(); + context.put("0", "World"); + + QLResult result = express4Runner.execute("'Hello ' + #[0]", context, QLOptions.DEFAULT_OPTIONS); + assertEquals("Hello World", result.getResult()); +---- + +自定义占位符并不是任意的,限制条件如下: + +* **起始标记限制**:`selectorStart` 必须是以下四种格式之一: + ** `${` (默认) + ** `$[` + ** `#{` + ** `#[` +* **结束标记限制**:`selectorEnd` 必须是 1 个或更多字符的字符串 + +=== 分号 + +表达式语句可以省略结尾的分号,整个脚本的返回值就是最后一个表达式的计算结果。 + +以下脚本的返回值为 2: + +[source,java] +---- +a = 1 +b = 2 +// last express +1+1 +---- + +等价于以下写法: + +[source,java] +---- +a = 1 +b = 2 +// return statment +return 1+1; +---- + +因为分号可以省略,QLExpress4 对于换行的处理相比 3 或者 Java 语言更加严格。如果想要将多行表达式拆成多行,建议将操作符保留在当前行,而将右操作数换到下一行。 + +以下多行表达式会报语法错误(反例): + +[source,java] +---- +// syntax error +a ++ b +---- + +以下是正确的换行示例(正例): + +[source,java] +---- +a + +b +---- + +其他的语法习惯保持和 Java 一致即可。 + +=== 表达式 + +QLExpress 采用表达式优先的设计,其中 除了 import, return 和循环等结构外,几乎都是表达式。 + +if 语句也是一个表达式: + +[source,java] +---- +assert(if (11 == 11) { + 10 +} else { + 20 + 2 +} + 1 == 11) +---- + +try catch 结构也是一个表达式: + +[source,java] +---- +assert(1 + try { + 100 + 1/0 +} catch(e) { + // Throw a zero-division exception + 11 +} == 12) +---- + +=== 短路计算 + +和 Java 类似,`&&` 和 `||` 逻辑运算都是短路运算的。 + +比如表达式 `false && (1/0)` 不会发生除 0 错误,因为 `&&` 短路在了最开始的 `false` 处。 + +短路计算默认是开启的,引擎也提供了选项,可以在某次执行时将短路关闭: + +> 关闭短路的一个场景是保证表达式的充分预热 + +[source,java] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // execute when enable short circuit (default) + // `1/0` is short-circuited by the preceding `false`, so it won't throw an error. + assertFalse((Boolean)express4Runner.execute("false && (1/0)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + try { + // execute when disable short circuit + express4Runner.execute("false && (1/0)", + Collections.emptyMap(), + QLOptions.builder().shortCircuitDisable(true).build()); + fail(); + } + catch (QLException e) { + Assert.assertEquals("INVALID_ARITHMETIC", e.getErrorCode()); + Assert.assertEquals("Division by zero", e.getReason()); + } +---- + + +=== 控制结构 + +==== if 分支 + +除了完全兼容 Java 中的 `if` 写法,还支持类似规则引擎的 `if ... then ... else ...` 的写法,其中 `then` 可以当成一个可以省略的关键字: + +[source,java] +---- +a = 11; +// if ... else ... +assert(if (a >= 0 && a < 5) { + true +} else if (a >= 5 && a < 10) { + false +} else if (a >= 10 && a < 15) { + true +} == true) + +// if ... then ... else ... +r = if (a == 11) then true else false +assert(r == true) +---- + +==== while 循环 + +[source,java] +---- +i = 0; +while (i < 5) { + if (++i == 2) { + break; + } +} +assert(i==2) +---- + +==== for 循环 + +[source,java] +---- +l = []; +for (int i = 3; i < 6; i++) { + l.add(i); +} +assert(l==[3,4,5]) +---- + +==== for-each 循环 + +[source,java] +---- +sum = 0; +for (i: [0,1,2,3,4]) { + if (i == 2) { + continue; + } + sum += i; +} +assert(sum==8) +---- + +==== try-catch + +[source,java] +---- +assert(try { + 100 + 1/0 +} catch(e) { + // Throw a zero-division exception + 11 +} == 11) +---- + +=== 函数定义 + +[source,java] +---- +function sub(a, b) { + return a-b; +} +assert(sub(3,1)==2) +---- + +=== Lambda 表达式 + +QLExpress4 中,Lambda 表达式作为一等公民,可以作为变量进行传递或者返回。 + +[source,java] +---- +add = (a, b) -> { + return a + b; +} +assert(add(1,2)==3) +---- + +=== 列表过滤和映射 + +支持通过 filter, map 方法直接对列表类型进行函数式过滤和映射。 + +底层通过在列表类型添加 link:#扩展函数[扩展函数] 实现,注意和 Stream API 中同名方法区分。 + +相比 Stream Api,它可以直接对列表进行操作,返回值也直接就是列表,更加方便。 + +[source,java] +---- +l = ["a-111", "a-222", "b-333", "c-888"] +assert(l.filter(i -> i.startsWith("a-")) + .map(i -> i.split("-")[1]) == ["111", "222"]) +---- + +=== 兼容 Java8 语法 + +QLExpress 可以兼容 Java8 的常见语法。 + +比如 link:#for-each-循环[for each循环], Stream API, 函数式接口等等。 + +==== Stream API + +可以直接使用 Java 集合中的 stream api 对集合进行操作。 + +因为此时的 stream api 都是来自 Java 中的方法,参考 link:#安全策略[安全策略] 打开安全选项,以下脚本才能正常执行。 + +[source,java] +---- +l = ["a-111", "a-222", "b-333", "c-888"] + +l2 = l.stream() + .filter(i -> i.startsWith("a-")) + .map(i -> i.split("-")[1]) + .collect(Collectors.toList()); +assert(l2 == ["111", "222"]); +---- + +==== 函数式接口 + +Java8 中引入了 Function, Consumer, Predicate 等函数式接口,QLExpress 中的 link:#lambda-表达式[Lambda表达式] 可以赋值给这些接口,或者作为接收这些接口的方法参数: + +[source,java] +---- +Runnable r = () -> a = 8; +r.run(); +assert(a == 8); + +Supplier s = () -> "test"; +assert(s.get() == 'test'); + +Consumer c = (a) -> b = a + "-te"; +c.accept("ccc"); +assert(b == 'ccc-te'); + +Function f = a -> a + 3; +assert(f.apply(1) == 4); + +Function f1 = (a, b) -> a + b; +assert(f1.apply("test-") == "test-null"); +---- + +== 附录一 升级指南 + +QLExpress 的上一版本因为多年的迭代停滞,在各项特性上和业界产生了较大差距。 + +QLExpress4 的目标之一就是一次性弥补这些差距,因此选择进行了大刀阔斧的升级,而有意放弃了部分兼容性。当然,基础的功能和体验还是和上一版本保持了对齐。 + +如果系统已经使用老版本的 QLExpress,升级之前务必要进行一次全面的回归测试,确保这些脚本都能在新版中正常执行,再进行升级。 + +如果没有时间或者方法对它们一一验证,那么不建议进行升级。 + +如果是新系统,建议直接采用 QLExpress4,未来 QLExpress4 的生态建设会越来越完善,而 3 会被逐渐抛弃。 + +下面将列表新版和旧版的主要不同,方便用户对已有脚本进行升级。如有遗漏,欢迎反馈: + +=== 默认安全策略 + +如果完全使用默认选项,获取 Java 对象的字段(`o.field`),或者调用成员方法(`o.method()`),则会分别抛出 `FIELD_NOT_FOUND` 和 `METHOD_NOT_FOUND` 错误。 + +这是因为 3 可以没有限制地通过反射访问 Java 应用系统中的任意字段和方法,这在嵌入式脚本中被认为是不安全的。 + +如果想兼容 3 的行为,则在新建 `Express4Runner` 时, 要将安全策略设置为 “开放”,参考代码如下: + +[source,java,indent=0] +---- + // open security strategy + Express4Runner express4RunnerOpen = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Assert.assertEquals("Thinking in Java", + express4RunnerOpen.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Effective Java", + express4RunnerOpen.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult()); +---- + +详细参考 link:#安全策略[安全策略] 章节。 + +=== 定义映射 + +QLExpress 老版本支持通过 `NewMap(key:value)` 的方式快速创建映射,虽然在文档中没有详细讨论,但是很多用户通过单元测试和询问的方式,知晓并使用了这个语法。 + +不过这种语法过于定制,也和业界的规范相差很大,因此在新版中将其移除。 + +新版原生支持 JSON 语法,直接采用 JSON 字典的格式(`{key:value}`)即可快速创建映射,更加直观。 + +详细参考 link:#方便语法元素[方便语法元素] + +=== 全局变量污染上下文 + +QLExpress 支持在执行脚本时传入一个全局的上下文,即 context 参数。 + +在老版本中,如果脚本中定义了全局变量,则这些变量也会写入到 context。在脚本执行结束后,可以通过 context 获取到脚本中定义的全局变量的值。 + +一个老版本的列子如下: + +[source,java] +---- +// only for QLExpress 3.x + +String express = "a=3;a+1"; +ExpressRunner runner = new ExpressRunner(false, true); +DefaultContext context = new DefaultContext<>(); + +Object res = runner.execute(express, context, null, true, true); +// The result of the script execution should be 4 (a+1) +Assert.assertEquals(4, res); +// The variable 'a' defined in the script is also stored in the context +Assert.assertEquals(3, context.get("a")); +---- + +根据调研和反馈,我们认为这会导致全局上下文被脚本 “污染”,存在安全性问题。 + +因此在 QLExpress4 中,全局变量默认不会写入到 context 中。 + +如果想要兼容 3 的特性,需要将 `polluteUserContext` 选项设置为 `true`,参考代码如下: + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + QLOptions populateOption = QLOptions.builder().polluteUserContext(true).build(); + Map populatedMap = new HashMap<>(); + populatedMap.put("b", 10); + express4Runner.execute("a = 11;b = a", populatedMap, populateOption); + assertEquals(11, populatedMap.get("a")); + assertEquals(11, populatedMap.get("b")); + + // no population + Map noPopulatedMap1 = new HashMap<>(); + express4Runner.execute("a = 11", noPopulatedMap1, QLOptions.DEFAULT_OPTIONS); + assertFalse(noPopulatedMap1.containsKey("a")); + + Map noPopulatedMap2 = new HashMap<>(); + noPopulatedMap2.put("a", 10); + assertEquals(19, express4Runner.execute("a = 19;a", noPopulatedMap2, QLOptions.DEFAULT_OPTIONS).getResult()); + assertEquals(10, noPopulatedMap2.get("a")); +---- + +=== 分号可省略 + +"分号可省略" 已经是现代脚本语言的一个标配,QLExpress4 也跟进了这个特性,分号是可以省略的。 + +具体参考 link:#分号[分号] 章节。 + +=== 严格换行模式 + +由于 QLExpress4 支持分号可省略,解释器需要通过换行符来判断语句是否结束。因此,QLExpress4 对换行的要求比 QLExpress3 更加严格。 + +下面的脚本在 QLExpress3 中是合法的,但在 QLExpress4 中不是: + +[source,java] +---- +// 在 QLExpress3 中合法的脚本,但在 QLExpress4 中不是 +商家应收= + 价格 + - 饭卡商家承担 + + 平台补贴 +---- + +在 QLExpress4 中,上述脚本会被解析为两个独立的语句: +1. `商家应收 = 价格` +2. `- 饭卡商家承担 + 平台补贴`(第二个语句会报语法错误) + +如果要在 QLExpress4 中实现同样的效果,需要将操作符放在行尾,而不是行首: + +[source,java] +---- +// QLExpress4 中正确的写法 +商家应收= + 价格 - + 饭卡商家承担 + + 平台补贴 +---- + +这样解释器就知道当前行的表达式还未结束,会继续读取下一行。 + +如果您需要兼容 QLExpress3 的换行特性,可以设置 `strictNewLines` 选项为 `false`: + +[source,java] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.builder().strictNewLines(false).build()); + String script = "商家应收=\n 价格\n - 饭卡商家承担\n + 平台补贴"; + Map context = new HashMap<>(); + context.put("价格", 10); + context.put("饭卡商家承担", 3); + context.put("平台补贴", 5); + QLResult result = express4Runner.execute(script, context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(12, ((Number)result.getResult()).intValue()); +---- + +注意:非严格换行模式会让解释器忽略所有换行符,可能会影响代码的可读性和错误提示的准确性。建议仅在需要兼容旧代码时使用。 + +=== 获得 char 类型 + +在 QLExpress 3 中,单引号包裹的单个字符会被解析为 char 类型,而不是 String。 + +这个给用户带来了不少困惑,比如 `"a"=='a'` 的判断结果是 `false`。 + +所以后来 QLExpress 3 中新增了 `ExpressRunner.setIgnoreConstChar` 选项,设置为 `true` 后,所有的单引号和双引号包裹的字符都会被解析为 String 类型。但是这个选项默认是关闭的,需要用户手动开启。 + +考虑到脚本用户很少会使用到 `char` 这种底层类型,我们在 QLExpress 4 中直接取消了这个选项,所有的单引号和双引号包裹的字符都会被解析为 String 类型。 + +如果您在脚本还是需要使用 `char` 类型,可以通过两种方法获得: + +* 类型强转:`(char) 'a'` +* 类型声明:`char a = 'a'` + +== 附录二 如何贡献? + +QLExpress 对社区的更改完全开放,任何建议和修改,都会受到欢迎,讨论后合理最后会被接纳到主干中。 + +首先需要将代码 clone 到本地,在正式修改代码前,需要先进行如下准备: + +1. 项目根目录执行 `mvn compile`:项目刚刚下载到本地时,会有大量的类找不到,需要先生成 Antlr4 的运行时代码 +2. 配置代码格式化:QLExpress 项目有统一的代码格式规范,开发前需要配置在 git 提交前的自动格式化 + +在项目目录下新建文件 `.git/hooks/pre-commit`,内容如下: + +[source,bash] +---- +#!/bin/sh +mvn spotless:apply +git add -u +exit 0 +---- + +这样在每次 git commit 之前,就会自动执行 maven 的 spotless 插件执行代码格式化,具体代码格式配置见 link:spotless_eclipse_formatter.xml[] + +3. 执行单元测试:在开发完成代码之后,先在本地全量执行单元测试,确保代码质量 + - JDK 8 环境:执行 `mvn test` + - JDK 9 及以上环境:执行 `mvn test -DargLine="--add-opens java.base/java.util.stream=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED"` + +== 附录三 QLExpress4性能提升 + +link:https://www.yuque.com/xuanheng-ffjti/iunlps/pgfzw46zel2xfnie?singleDoc#%20%E3%80%8AQLExpress3%E4%B8%8E4%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94%E3%80%8B[QLExpress4与3性能对比] + +总结:常见场景下,无编译缓存时,QLExpress4能比3有接近10倍性能提升;有编译缓存,也有一倍性能提升。 + +== 附录四 开发者联系方式 + + * Email: + ** qinyuan.dqy@alibaba-inc.com + ** yumin.pym@taobao.com + ** 704643716@qq.com + * WeChat: + ** xuanheng: dqy932087612 + ** binggou: pymbupt + ** linxiang: tkk33362 + * DingTalk Support Group + +image::images/qlexpress_support_group_qr_2026.jpg[] diff --git a/docs/custom-item-en-source.adoc b/docs/custom-item-en-source.adoc new file mode 100644 index 0000000..5437670 --- /dev/null +++ b/docs/custom-item-en-source.adoc @@ -0,0 +1,152 @@ +:toc: + += Custom Syntax Elements + +This section systematically outlines the interfaces and capabilities in QLExpress4 for customizing syntax elements. + +== Overview + +* Custom functions +** Implement the CustomFunction interface +** Use Java functional interfaces +** Register via annotation scanning +** Add via QLExpress script +** Implement QLFunctionalVarargs +* Custom operators +** Implement CustomBinaryOperator +** Replace built-in operators +** Use Java functional interfaces +** Add aliases +** Implement QLFunctionalVarargs +* Extension functions +** Extend ExtensionFunction +** Implement QLFunctionalVarargs +* Aliases for operators and functions + +== Custom Functions + +=== Implement the CustomFunction interface + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/test/function/HelloFunction.java[] +---- + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=customComplexFunction] +---- + +=== Use Java functional interfaces + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addFunctionWithJavaFunctional] +---- + +=== Register via annotations + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=annoObj] +---- + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addFunctionByAnnotationObject] +---- + +=== Add via QLExpress script + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=joinFunctionVarargsObj] +---- + +=== Implement QLFunctionalVarargs + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addFunctionByVarargs] +---- + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addFunctionByVarargs] +---- + +== Custom Operators + +=== Implement CustomBinaryOperator and set precedence + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addOperatorWithPrecedence] +---- + +=== Replace built-in operators + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=replaceDefaultOperator] +---- + +=== Use Java functional interfaces + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addOperatorBiFunction] +---- + +=== Implement QLFunctionalVarargs + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addOperatorByVarargs] +---- + +== Extension Functions + +=== Extend ExtensionFunction + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=defineExtensionFunctionByExtensionFunction] +---- + +=== Implement QLFunctionalVarargs + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=defineExtensionFunctionByQLFunctionalVarargs] +---- + +== QLFunctionalVarargs: One object defines three kinds of operations + +The same QLFunctionalVarargs object can serve as the implementation of a function, an operator, and an extension function at the same time, making it easy to reuse unified semantics and implementation across multiple places. This capability comes from the interface’s varargs design; see the example and interface definition below. For background discussion, refer to link:https://github.com/alibaba/QLExpress/issues/407[issue407]: + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=qlfunctionalvarargsAllInOne] +---- + +=== Interface definition + +[source,java,indent=0] +---- +include::../src/main/java/com/alibaba/qlexpress4/api/QLFunctionalVarargs.java[] +---- + +== Aliases for operators and functions (keyword aliases also supported) + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addAlias] +---- + +== Notes and recommendations + +- Under the QLFunctionalVarargs calling convention, when invoking an extension function the first actual argument is the receiver object, followed by the call arguments; functions and operators do not include a receiver. +- Set the precedence of custom operators according to the intended expression semantics to avoid confusion with existing rules. +- Annotation-based registration only processes public methods, and duplicate names will fail to register; the batch registration result contains both success and failure lists. + diff --git a/docs/custom-item-en.adoc b/docs/custom-item-en.adoc new file mode 100644 index 0000000..66e23bf --- /dev/null +++ b/docs/custom-item-en.adoc @@ -0,0 +1,334 @@ +:toc: + += Custom Syntax Elements + +This section systematically outlines the interfaces and capabilities in QLExpress4 for customizing syntax elements. + +== Overview + +* Custom functions +** Implement the CustomFunction interface +** Use Java functional interfaces +** Register via annotation scanning +** Add via QLExpress script +** Implement QLFunctionalVarargs +* Custom operators +** Implement CustomBinaryOperator +** Replace built-in operators +** Use Java functional interfaces +** Add aliases +** Implement QLFunctionalVarargs +* Extension functions +** Extend ExtensionFunction +** Implement QLFunctionalVarargs +* Aliases for operators and functions + +== Custom Functions + +=== Implement the CustomFunction interface + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.test.function; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +public class HelloFunction implements CustomFunction { + @Override + public Object call(QContext qContext, Parameters parameters) + throws Throwable { + String tenant = (String)qContext.attachment().get("tenant"); + return "hello," + tenant; + } +} +---- + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addFunction("hello", new HelloFunction()); + String resultJack = (String)express4Runner.execute("hello()", + Collections.emptyMap(), + // Additional information(tenant for example) can be brought into the custom function from outside via attachments + QLOptions.builder().attachments(Collections.singletonMap("tenant", "jack")).build()).getResult(); + assertEquals("hello,jack", resultJack); + String resultLucy = + (String)express4Runner + .execute("hello()", + Collections.emptyMap(), + QLOptions.builder().attachments(Collections.singletonMap("tenant", "lucy")).build()) + .getResult(); + assertEquals("hello,lucy", resultLucy); +---- + +=== Use Java functional interfaces + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // Function + runner.addFunction("inc", (Function)x -> x + 1); + // Predicate + runner.addFunction("isPos", (Predicate)x -> x > 0); + // Runnable + runner.addFunction("notify", () -> { + }); + // Consumer + runner.addFunction("print", (Consumer)System.out::println); + + Object r1 = runner.execute("inc(1)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + Object r2 = runner.execute("isPos(1)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(2, r1); + assertEquals(true, r2); +---- + +=== Register via annotations + +[source,java,indent=0] +---- + public static class MyFunctionUtil { + @QLFunction({"myAdd", "iAdd"}) + public int add(int a, int b) { + return a + b; + } + + @QLFunction("arr3") + public static int[] array3(int a, int b, int c) { + return new int[] {a, b, c}; + } + + @QLFunction("concat") + public String concat(String a, String b) { + return a + b; + } + } +---- + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + BatchAddFunctionResult addResult = express4Runner.addObjFunction(new MyFunctionUtil()); + assertEquals(4, addResult.getSucc().size()); + Object result = + express4Runner.execute("myAdd(1,2) + iAdd(5,6)", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(14, result); + express4Runner.addStaticFunction(MyFunctionUtil.class); + Object result1 = + express4Runner.execute("arr3(5,9,10)[2]", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(10, result1); + + Object result2 = + express4Runner.execute("concat('aa', null)", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("aanull", result2); +---- + +=== Add via QLExpress script + +[source,java,indent=0] +---- + public static class JoinFunction implements QLFunctionalVarargs { + @Override + public Object call(Object... params) { + return Arrays.stream(params).map(Object::toString).collect(Collectors.joining(",")); + } + } +---- + +=== Implement QLFunctionalVarargs + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addVarArgsFunction("join", new JoinFunction()); + Object resultFunction = + express4Runner.execute("join(1,2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultFunction); +---- + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addVarArgsFunction("join", new JoinFunction()); + Object resultFunction = + express4Runner.execute("join(1,2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultFunction); +---- + +== Custom Operators + +=== Implement CustomBinaryOperator and set precedence + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.addOperator("?><", (left, right) -> left.get().toString() + right.get().toString(), QLPrecedences.ADD); + Object r = runner.execute("1 ?>< 2 * 3", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + // precedence set to ADD, so multiply first, then custom operator: "1" + "6" => "16" + assertEquals("16", r); +---- + +=== Replace built-in operators + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + boolean ok = runner.replaceDefaultOperator("+", + (left, right) -> Double.parseDouble(left.get().toString()) + Double.parseDouble(right.get().toString())); + assertTrue(ok); + Object r = runner.execute("'1.2' + '2.3'", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(3.5d, r); +---- + +=== Use Java functional interfaces + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addOperatorBiFunction("join", (left, right) -> left + "," + right); + Object resultOperator = + express4Runner.execute("1 join 2 join 3", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultOperator); +---- + +=== Implement QLFunctionalVarargs + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.addOperator("join", params -> params[0] + "," + params[1]); + Object r = runner.execute("1 join 2", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2", r); +---- + +== Extension Functions + +=== Extend ExtensionFunction + +[source,java,indent=0] +---- + ExtensionFunction helloFunction = new ExtensionFunction() { + @Override + public Class[] getParameterTypes() { + return new Class[0]; + } + + @Override + public String getName() { + return "hello"; + } + + @Override + public Class getDeclaringClass() { + return String.class; + } + + @Override + public Object invoke(Object obj, Object[] args) { + String originStr = (String)obj; + return "Hello," + originStr; + } + }; + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addExtendFunction(helloFunction); + Object result = + express4Runner.execute("'jack'.hello()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("Hello,jack", result); +---- + +=== Implement QLFunctionalVarargs + +[source,java,indent=0] +---- + // simpler way to define extension function + express4Runner.addExtendFunction("add", + Number.class, + params -> ((Number)params[0]).intValue() + ((Number)params[1]).intValue()); + QLResult resultAdd = express4Runner.execute("1.add(2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertEquals(3, resultAdd.getResult()); +---- + +== QLFunctionalVarargs: One object defines three kinds of operations + +The same QLFunctionalVarargs object can serve as the implementation of a function, an operator, and an extension function at the same time, making it easy to reuse unified semantics and implementation across multiple places. This capability comes from the interface’s varargs design; see the example and interface definition below. For background discussion, refer to link:https://github.com/alibaba/QLExpress/issues/407[issue407]: + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + QLFunctionalVarargs allInOne = params -> { + // sum numbers no matter how many args + double sum = 0d; + for (Object p : params) { + if (p instanceof Number) { + sum += ((Number)p).doubleValue(); + } + } + return sum; + }; + + // as function + runner.addVarArgsFunction("sumAll", allInOne); + // as operator + runner.addOperator("+&", allInOne); + // as extension function: first arg is the receiver, followed by call arguments + runner.addExtendFunction("plusAll", Number.class, allInOne); + + Map ctx = new HashMap<>(); + Object rf = runner.execute("sumAll(1,2,3)", ctx, QLOptions.DEFAULT_OPTIONS).getResult(); + Object ro = runner.execute("1 +& 4", ctx, QLOptions.DEFAULT_OPTIONS).getResult(); + Object re = runner.execute("1.plusAll(5)", ctx, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(6d, rf); + assertEquals(5d, ro); + assertEquals(6d, re); +---- + +=== Interface definition + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.api; + +/** + * Author: TaoKan + */ +@FunctionalInterface +public interface QLFunctionalVarargs { + Object call(Object... params); +} +---- + +== Aliases for operators and functions (keyword aliases also supported) + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // add custom function zero + express4Runner.addFunction("zero", (String ignore) -> 0); + + // keyword alias + assertTrue(express4Runner.addAlias("如果", "if")); + assertTrue(express4Runner.addAlias("则", "then")); + assertTrue(express4Runner.addAlias("否则", "else")); + assertTrue(express4Runner.addAlias("返回", "return")); + // operator alias + assertTrue(express4Runner.addAlias("大于", ">")); + // function alias + assertTrue(express4Runner.addAlias("零", "zero")); + + Map context = new HashMap<>(); + context.put("语文", 90); + context.put("数学", 90); + context.put("英语", 90); + + Object result = express4Runner + .execute("如果 (语文 + 数学 + 英语 大于 270) 则 {返回 1;} 否则 {返回 零();}", context, QLOptions.DEFAULT_OPTIONS) + .getResult(); + assertEquals(0, result); +---- + +== Notes and recommendations + +- Under the QLFunctionalVarargs calling convention, when invoking an extension function the first actual argument is the receiver object, followed by the call arguments; functions and operators do not include a receiver. +- Set the precedence of custom operators according to the intended expression semantics to avoid confusion with existing rules. +- Annotation-based registration only processes public methods, and duplicate names will fail to register; the batch registration result contains both success and failure lists. diff --git a/docs/custom-item-source.adoc b/docs/custom-item-source.adoc new file mode 100644 index 0000000..da4b932 --- /dev/null +++ b/docs/custom-item-source.adoc @@ -0,0 +1,152 @@ +:toc: + += 自定义语法元素 + +本节系统梳理在 QLExpress4 在自定义语法元素方面的接口和能力。 + +== 总览 + +* 自定义函数 +** 实现CustomFunction接口 +** 使用 Java 函数式接口 +** 通过注解扫描注册 +** 通过QLExpress脚本添加 +** 实现QLFunctionalVarargs +* 自定义操作符 +** 实现CustomBinaryOperator +** 替换内置操作符 +** 使用 Java 函数式接口 +** 添加别名 +** 实现QLFunctionalVarargs +* 扩展函数 +** 继承ExtensionFunction +** 实现QLFunctionalVarargs +* 操作符与函数别名 + +== 自定义函数 + +=== 实现CustomFunction接口 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/test/function/HelloFunction.java[] +---- + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=customComplexFunction] +---- + +=== 使用 Java 函数式接口 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addFunctionWithJavaFunctional] +---- + +=== 注解方式注册 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=annoObj] +---- + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addFunctionByAnnotationObject] +---- + +=== 通过QLExpress脚本添加 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=joinFunctionVarargsObj] +---- + +=== 实现QLFunctionalVarargs + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addFunctionByVarargs] +---- + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addFunctionByVarargs] +---- + +== 自定义操作符 + +=== 实现CustomBinaryOperator并设置优先级 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addOperatorWithPrecedence] +---- + +=== 替换内置操作符 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=replaceDefaultOperator] +---- + +=== 使用 Java 函数式接口 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addOperatorBiFunction] +---- + +=== 实现QLFunctionalVarargs + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=addOperatorByVarargs] +---- + +== 扩展函数 + +=== 继承ExtensionFunction + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=defineExtensionFunctionByExtensionFunction] +---- + +=== 实现QLFunctionalVarargs + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=defineExtensionFunctionByQLFunctionalVarargs] +---- + +== QLFunctionalVarargs:一个对象同时定义三类操作 + +同一个QLFunctionalVarargs对象可同时用作函数、操作符与扩展函数的实现,便于在多处复用统一的语义与实现。该能力来源于接口的可变参数设计,详见下方示例与接口定义。背景讨论参考 link:https://github.com/alibaba/QLExpress/issues/407[issue407]: + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java[tag=qlfunctionalvarargsAllInOne] +---- + +=== 接口定义 + +[source,java,indent=0] +---- +include::../src/main/java/com/alibaba/qlexpress4/api/QLFunctionalVarargs.java[] +---- + +== 操作符与函数别名(亦支持关键字别名) + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addAlias] +---- + +== 说明与建议 + +- QLFunctionalVarargs 的定义模式下,扩展函数调用时实参列表首位是接收者对象,其后为调用参数;函数与操作符不含接收者。 +- 自定义操作符的优先级需根据表达式期望进行设置,避免与已有运算规则产生混淆。 +- 注解方式注册仅会处理公开方法,且重复名称将注册失败;批量注册返回结果中包含成功与失败清单。 + diff --git a/docs/custom-item.adoc b/docs/custom-item.adoc new file mode 100644 index 0000000..ad131a1 --- /dev/null +++ b/docs/custom-item.adoc @@ -0,0 +1,334 @@ +:toc: + += 自定义语法元素 + +本节系统梳理在 QLExpress4 在自定义语法元素方面的接口和能力。 + +== 总览 + +* 自定义函数 +** 实现CustomFunction接口 +** 使用 Java 函数式接口 +** 通过注解扫描注册 +** 通过QLExpress脚本添加 +** 实现QLFunctionalVarargs +* 自定义操作符 +** 实现CustomBinaryOperator +** 替换内置操作符 +** 使用 Java 函数式接口 +** 添加别名 +** 实现QLFunctionalVarargs +* 扩展函数 +** 继承ExtensionFunction +** 实现QLFunctionalVarargs +* 操作符与函数别名 + +== 自定义函数 + +=== 实现CustomFunction接口 + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.test.function; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +public class HelloFunction implements CustomFunction { + @Override + public Object call(QContext qContext, Parameters parameters) + throws Throwable { + String tenant = (String)qContext.attachment().get("tenant"); + return "hello," + tenant; + } +} +---- + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addFunction("hello", new HelloFunction()); + String resultJack = (String)express4Runner.execute("hello()", + Collections.emptyMap(), + // Additional information(tenant for example) can be brought into the custom function from outside via attachments + QLOptions.builder().attachments(Collections.singletonMap("tenant", "jack")).build()).getResult(); + assertEquals("hello,jack", resultJack); + String resultLucy = + (String)express4Runner + .execute("hello()", + Collections.emptyMap(), + QLOptions.builder().attachments(Collections.singletonMap("tenant", "lucy")).build()) + .getResult(); + assertEquals("hello,lucy", resultLucy); +---- + +=== 使用 Java 函数式接口 + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // Function + runner.addFunction("inc", (Function)x -> x + 1); + // Predicate + runner.addFunction("isPos", (Predicate)x -> x > 0); + // Runnable + runner.addFunction("notify", () -> { + }); + // Consumer + runner.addFunction("print", (Consumer)System.out::println); + + Object r1 = runner.execute("inc(1)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + Object r2 = runner.execute("isPos(1)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(2, r1); + assertEquals(true, r2); +---- + +=== 注解方式注册 + +[source,java,indent=0] +---- + public static class MyFunctionUtil { + @QLFunction({"myAdd", "iAdd"}) + public int add(int a, int b) { + return a + b; + } + + @QLFunction("arr3") + public static int[] array3(int a, int b, int c) { + return new int[] {a, b, c}; + } + + @QLFunction("concat") + public String concat(String a, String b) { + return a + b; + } + } +---- + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + BatchAddFunctionResult addResult = express4Runner.addObjFunction(new MyFunctionUtil()); + assertEquals(4, addResult.getSucc().size()); + Object result = + express4Runner.execute("myAdd(1,2) + iAdd(5,6)", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(14, result); + express4Runner.addStaticFunction(MyFunctionUtil.class); + Object result1 = + express4Runner.execute("arr3(5,9,10)[2]", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(10, result1); + + Object result2 = + express4Runner.execute("concat('aa', null)", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("aanull", result2); +---- + +=== 通过QLExpress脚本添加 + +[source,java,indent=0] +---- + public static class JoinFunction implements QLFunctionalVarargs { + @Override + public Object call(Object... params) { + return Arrays.stream(params).map(Object::toString).collect(Collectors.joining(",")); + } + } +---- + +=== 实现QLFunctionalVarargs + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addVarArgsFunction("join", new JoinFunction()); + Object resultFunction = + express4Runner.execute("join(1,2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultFunction); +---- + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addVarArgsFunction("join", new JoinFunction()); + Object resultFunction = + express4Runner.execute("join(1,2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultFunction); +---- + +== 自定义操作符 + +=== 实现CustomBinaryOperator并设置优先级 + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.addOperator("?><", (left, right) -> left.get().toString() + right.get().toString(), QLPrecedences.ADD); + Object r = runner.execute("1 ?>< 2 * 3", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + // precedence set to ADD, so multiply first, then custom operator: "1" + "6" => "16" + assertEquals("16", r); +---- + +=== 替换内置操作符 + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + boolean ok = runner.replaceDefaultOperator("+", + (left, right) -> Double.parseDouble(left.get().toString()) + Double.parseDouble(right.get().toString())); + assertTrue(ok); + Object r = runner.execute("'1.2' + '2.3'", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(3.5d, r); +---- + +=== 使用 Java 函数式接口 + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addOperatorBiFunction("join", (left, right) -> left + "," + right); + Object resultOperator = + express4Runner.execute("1 join 2 join 3", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultOperator); +---- + +=== 实现QLFunctionalVarargs + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.addOperator("join", params -> params[0] + "," + params[1]); + Object r = runner.execute("1 join 2", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2", r); +---- + +== 扩展函数 + +=== 继承ExtensionFunction + +[source,java,indent=0] +---- + ExtensionFunction helloFunction = new ExtensionFunction() { + @Override + public Class[] getParameterTypes() { + return new Class[0]; + } + + @Override + public String getName() { + return "hello"; + } + + @Override + public Class getDeclaringClass() { + return String.class; + } + + @Override + public Object invoke(Object obj, Object[] args) { + String originStr = (String)obj; + return "Hello," + originStr; + } + }; + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addExtendFunction(helloFunction); + Object result = + express4Runner.execute("'jack'.hello()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("Hello,jack", result); +---- + +=== 实现QLFunctionalVarargs + +[source,java,indent=0] +---- + // simpler way to define extension function + express4Runner.addExtendFunction("add", + Number.class, + params -> ((Number)params[0]).intValue() + ((Number)params[1]).intValue()); + QLResult resultAdd = express4Runner.execute("1.add(2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertEquals(3, resultAdd.getResult()); +---- + +== QLFunctionalVarargs:一个对象同时定义三类操作 + +同一个QLFunctionalVarargs对象可同时用作函数、操作符与扩展函数的实现,便于在多处复用统一的语义与实现。该能力来源于接口的可变参数设计,详见下方示例与接口定义。背景讨论参考 link:https://github.com/alibaba/QLExpress/issues/407[issue407]: + +[source,java,indent=0] +---- + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + QLFunctionalVarargs allInOne = params -> { + // sum numbers no matter how many args + double sum = 0d; + for (Object p : params) { + if (p instanceof Number) { + sum += ((Number)p).doubleValue(); + } + } + return sum; + }; + + // as function + runner.addVarArgsFunction("sumAll", allInOne); + // as operator + runner.addOperator("+&", allInOne); + // as extension function: first arg is the receiver, followed by call arguments + runner.addExtendFunction("plusAll", Number.class, allInOne); + + Map ctx = new HashMap<>(); + Object rf = runner.execute("sumAll(1,2,3)", ctx, QLOptions.DEFAULT_OPTIONS).getResult(); + Object ro = runner.execute("1 +& 4", ctx, QLOptions.DEFAULT_OPTIONS).getResult(); + Object re = runner.execute("1.plusAll(5)", ctx, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(6d, rf); + assertEquals(5d, ro); + assertEquals(6d, re); +---- + +=== 接口定义 + +[source,java,indent=0] +---- +package com.alibaba.qlexpress4.api; + +/** + * Author: TaoKan + */ +@FunctionalInterface +public interface QLFunctionalVarargs { + Object call(Object... params); +} +---- + +== 操作符与函数别名(亦支持关键字别名) + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // add custom function zero + express4Runner.addFunction("zero", (String ignore) -> 0); + + // keyword alias + assertTrue(express4Runner.addAlias("如果", "if")); + assertTrue(express4Runner.addAlias("则", "then")); + assertTrue(express4Runner.addAlias("否则", "else")); + assertTrue(express4Runner.addAlias("返回", "return")); + // operator alias + assertTrue(express4Runner.addAlias("大于", ">")); + // function alias + assertTrue(express4Runner.addAlias("零", "zero")); + + Map context = new HashMap<>(); + context.put("语文", 90); + context.put("数学", 90); + context.put("英语", 90); + + Object result = express4Runner + .execute("如果 (语文 + 数学 + 英语 大于 270) 则 {返回 1;} 否则 {返回 零();}", context, QLOptions.DEFAULT_OPTIONS) + .getResult(); + assertEquals(0, result); +---- + +== 说明与建议 + +- QLFunctionalVarargs 的定义模式下,扩展函数调用时实参列表首位是接收者对象,其后为调用参数;函数与操作符不含接收者。 +- 自定义操作符的优先级需根据表达式期望进行设置,避免与已有运算规则产生混淆。 +- 注解方式注册仅会处理公开方法,且重复名称将注册失败;批量注册返回结果中包含成功与失败清单。 diff --git a/docs/execute-en-source.adoc b/docs/execute-en-source.adoc new file mode 100644 index 0000000..4b25134 --- /dev/null +++ b/docs/execute-en-source.adoc @@ -0,0 +1,50 @@ +:toc: + += Expression Execution + +This section describes the different ways to execute expressions in QLExpress4. + +== Use Map as the context + +- `execute(String, Map, QLOptions)` +- Description: According to the method comments, use a `Map` as the context; the keys in the `Map` are the variable names used in the script, and the values are the corresponding variable values. +- Typical scenario: Build a `Map` from external business data and pass it in so the script can access variables directly by name. + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl] +---- + +== Use object fields as the context + +- `execute(String, Object, QLOptions)` +- Description: Use a plain Java object as the context; variable names in the script correspond to the object's field names (or accessible getters). +- Typical scenario: When you already have a DTO/POJO that carries the context data, pass the object directly so the script can read its fields. + +// Note: This references the test code for execution with an object context. +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=executeWithObject] +---- + +== Use a set of aliased objects + +- `executeWithAliasObjects(String, QLOptions, Object...)` +- Description: Pass objects annotated with `@QLAlias`; the annotation value becomes the variable name in the context. Objects without the annotation are ignored. +- Typical scenario: Expose object properties/methods with Chinese (or domain-specific) aliases so rule authors can write scripts more intuitively. + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=qlAlias] +---- + +== Use a custom `ExpressContext` + +- `execute(String, ExpressContext, QLOptions)` +- Description: This overload allows passing a custom context that implements `ExpressContext` (for example, a dynamic variable context). The other `execute` overloads ultimately delegate to this method. +- Typical scenario: When you need on-demand, by-name evaluation of variable values, read/write isolation, or integration with external storage, a custom context is the most flexible choice. + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=dynamicVar] +---- diff --git a/docs/execute-en.adoc b/docs/execute-en.adoc new file mode 100644 index 0000000..fe5e879 --- /dev/null +++ b/docs/execute-en.adoc @@ -0,0 +1,95 @@ +:toc: + += Expression Execution + +This section describes the different ways to execute expressions in QLExpress4. + +== Use Map as the context + +- `execute(String, Map, QLOptions)` +- Description: According to the method comments, use a `Map` as the context; the keys in the `Map` are the variable names used in the script, and the values are the corresponding variable values. +- Typical scenario: Build a `Map` from external business data and pass it in so the script can access variables directly by name. + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map context = new HashMap<>(); + context.put("a", 1); + context.put("b", 2); + context.put("c", 3); + Object result = express4Runner.execute("a + b * c", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(7, result); +---- + +== Use object fields as the context + +- `execute(String, Object, QLOptions)` +- Description: Use a plain Java object as the context; variable names in the script correspond to the object's field names (or accessible getters). +- Typical scenario: When you already have a DTO/POJO that carries the context data, pass the object directly so the script can read its fields. + +// Note: This references the test code for execution with an object context. +[source,java,indent=0] +---- + MyObj myObj = new MyObj(); + myObj.a = 1; + myObj.b = "test"; + + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = express4Runner.execute("a+b", myObj, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1test", result); +---- + +== Use a set of aliased objects + +- `executeWithAliasObjects(String, QLOptions, Object...)` +- Description: Pass objects annotated with `@QLAlias`; the annotation value becomes the variable name in the context. Objects without the annotation are ignored. +- Typical scenario: Expose object properties/methods with Chinese (or domain-specific) aliases so rule authors can write scripts more intuitively. + +[source,java,indent=0] +---- + Order order = new Order(); + order.setOrderNum("OR123455"); + order.setAmount(100); + + User user = new User(); + user.setName("jack"); + user.setVip(true); + + // Calculate the Final Order Amount + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Number result = (Number)express4Runner + .executeWithAliasObjects("用户.是vip? 订单.金额 * 0.8 : 订单.金额", QLOptions.DEFAULT_OPTIONS, order, user) + .getResult(); + assertEquals(80, result.intValue()); +---- + +== Use a custom `ExpressContext` + +- `execute(String, ExpressContext, QLOptions)` +- Description: This overload allows passing a custom context that implements `ExpressContext` (for example, a dynamic variable context). The other `execute` overloads ultimately delegate to this method. +- Typical scenario: When you need on-demand, by-name evaluation of variable values, read/write isolation, or integration with external storage, a custom context is the most flexible choice. + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + Map staticContext = new HashMap<>(); + staticContext.put("语文", 88); + staticContext.put("数学", 99); + staticContext.put("英语", 95); + + QLOptions defaultOptions = QLOptions.DEFAULT_OPTIONS; + DynamicVariableContext dynamicContext = + new DynamicVariableContext(express4Runner, staticContext, defaultOptions); + dynamicContext.put("平均成绩", "(语文+数学+英语)/3.0"); + dynamicContext.put("是否优秀", "平均成绩>90"); + + // dynamic var + assertTrue((Boolean)express4Runner.execute("是否优秀", dynamicContext, defaultOptions).getResult()); + assertEquals(94, + ((Number)express4Runner.execute("平均成绩", dynamicContext, defaultOptions).getResult()).intValue()); + // static var + assertEquals(187, + ((Number)express4Runner.execute("语文+数学", dynamicContext, defaultOptions).getResult()).intValue()); +---- diff --git a/docs/execute-source.adoc b/docs/execute-source.adoc new file mode 100644 index 0000000..9c1b724 --- /dev/null +++ b/docs/execute-source.adoc @@ -0,0 +1,50 @@ +:toc: + += 表达式执行 + +本节说明在 QLExpress4 中执行表达式的多种方式。 + +== 使用 Map 作为上下文 + +- `execute(String, Map, QLOptions)` +- 说明:根据方法注释,使用 `Map` 作为上下文,`Map` 的 key 即为脚本中的变量名,value 为对应变量值。 +- 典型场景:从外部业务数据构造一个 `Map` 传入,直接在脚本里以变量名访问。 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl] +---- + +== 使用对象字段作为上下文 + +- `execute(String, Object, QLOptions)` +- 说明:根据方法注释,使用一个普通 Java 对象作为上下文,脚本中的变量名对应该对象的字段名(或可访问的 getter)。 +- 典型场景:已有一个承载上下文数据的 DTO/POJO,直接将对象传入以便脚本读取其字段。 + +// 说明:此处引用对象上下文执行的测试代码。 +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=executeWithObject] +---- + +== 使用带别名的对象集合 + +- `executeWithAliasObjects(String, QLOptions, Object...)` +- 说明:根据方法注释,传入标记了 `@QLAlias` 的对象,注解值将作为上下文中的变量名;未标记的对象会被忽略。 +- 典型场景:以中文(或领域语言)别名暴露对象属性/方法,供规则编写者直观地编写脚本。 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=qlAlias] +---- + +== 使用自定义 `ExpressContext` + +- `execute(String, ExpressContext, QLOptions)` +- 说明:该重载允许传入实现了 `ExpressContext` 的自定义上下文(例如动态变量上下文)。其它 `execute` 重载最终也会委托到此方法。 +- 典型场景:需要按需按名计算变量值、隔离读写、或接入外部存储等高级能力时,自定义上下文最灵活。 + +[source,java,indent=0] +---- +include::../src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=dynamicVar] +---- \ No newline at end of file diff --git a/docs/execute.adoc b/docs/execute.adoc new file mode 100644 index 0000000..66740cb --- /dev/null +++ b/docs/execute.adoc @@ -0,0 +1,95 @@ +:toc: + += 表达式执行 + +本节说明在 QLExpress4 中执行表达式的多种方式。 + +== 使用 Map 作为上下文 + +- `execute(String, Map, QLOptions)` +- 说明:根据方法注释,使用 `Map` 作为上下文,`Map` 的 key 即为脚本中的变量名,value 为对应变量值。 +- 典型场景:从外部业务数据构造一个 `Map` 传入,直接在脚本里以变量名访问。 + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map context = new HashMap<>(); + context.put("a", 1); + context.put("b", 2); + context.put("c", 3); + Object result = express4Runner.execute("a + b * c", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(7, result); +---- + +== 使用对象字段作为上下文 + +- `execute(String, Object, QLOptions)` +- 说明:根据方法注释,使用一个普通 Java 对象作为上下文,脚本中的变量名对应该对象的字段名(或可访问的 getter)。 +- 典型场景:已有一个承载上下文数据的 DTO/POJO,直接将对象传入以便脚本读取其字段。 + +// 说明:此处引用对象上下文执行的测试代码。 +[source,java,indent=0] +---- + MyObj myObj = new MyObj(); + myObj.a = 1; + myObj.b = "test"; + + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = express4Runner.execute("a+b", myObj, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1test", result); +---- + +== 使用带别名的对象集合 + +- `executeWithAliasObjects(String, QLOptions, Object...)` +- 说明:根据方法注释,传入标记了 `@QLAlias` 的对象,注解值将作为上下文中的变量名;未标记的对象会被忽略。 +- 典型场景:以中文(或领域语言)别名暴露对象属性/方法,供规则编写者直观地编写脚本。 + +[source,java,indent=0] +---- + Order order = new Order(); + order.setOrderNum("OR123455"); + order.setAmount(100); + + User user = new User(); + user.setName("jack"); + user.setVip(true); + + // Calculate the Final Order Amount + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Number result = (Number)express4Runner + .executeWithAliasObjects("用户.是vip? 订单.金额 * 0.8 : 订单.金额", QLOptions.DEFAULT_OPTIONS, order, user) + .getResult(); + assertEquals(80, result.intValue()); +---- + +== 使用自定义 `ExpressContext` + +- `execute(String, ExpressContext, QLOptions)` +- 说明:该重载允许传入实现了 `ExpressContext` 的自定义上下文(例如动态变量上下文)。其它 `execute` 重载最终也会委托到此方法。 +- 典型场景:需要按需按名计算变量值、隔离读写、或接入外部存储等高级能力时,自定义上下文最灵活。 + +[source,java,indent=0] +---- + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + Map staticContext = new HashMap<>(); + staticContext.put("语文", 88); + staticContext.put("数学", 99); + staticContext.put("英语", 95); + + QLOptions defaultOptions = QLOptions.DEFAULT_OPTIONS; + DynamicVariableContext dynamicContext = + new DynamicVariableContext(express4Runner, staticContext, defaultOptions); + dynamicContext.put("平均成绩", "(语文+数学+英语)/3.0"); + dynamicContext.put("是否优秀", "平均成绩>90"); + + // dynamic var + assertTrue((Boolean)express4Runner.execute("是否优秀", dynamicContext, defaultOptions).getResult()); + assertEquals(94, + ((Number)express4Runner.execute("平均成绩", dynamicContext, defaultOptions).getResult()).intValue()); + // static var + assertEquals(187, + ((Number)express4Runner.execute("语文+数学", dynamicContext, defaultOptions).getResult()).intValue()); +---- diff --git a/images/json_map.png b/images/json_map.png new file mode 100644 index 0000000..1c56329 Binary files /dev/null and b/images/json_map.png differ diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000..03de603 Binary files /dev/null and b/images/logo.png differ diff --git a/images/order_rules_cn.png b/images/order_rules_cn.png new file mode 100644 index 0000000..ffe954c Binary files /dev/null and b/images/order_rules_cn.png differ diff --git a/images/qlexpress_support_group_qr_2026.jpg b/images/qlexpress_support_group_qr_2026.jpg new file mode 100644 index 0000000..2780117 Binary files /dev/null and b/images/qlexpress_support_group_qr_2026.jpg differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..83ef4de --- /dev/null +++ b/pom.xml @@ -0,0 +1,312 @@ + + + + 4.0.0 + com.fantaibao + fantaibao-qlexpress4 + jar + 4.0.7 + QLExpress + QLExpress is a powerful, lightweight, dynamic language for the Java platform aimed at improving + developers’ productivity in different business scenes. + + https://github.com/alibaba/QLExpress + 2012 + + + + Apache 2 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + + scm:git:git@github.com:alibaba/QLExpress.git + scm:git:git@github.com:alibaba/QLExpress.git + git@github.com:alibaba/QLExpress.git + + + + https://github.com/alibaba/QLExpress/issues + GitHub Issues + + + + Alibaba Group + https://github.com/alibaba + + + + + tianqiao + tianqiao + tianqiao@alibaba-inc.com + + + baoxingjie + baoxingjie + baoxingjie@126.com + + + qianghui + qianghui + qhlhl2010@gmail.com + + + + + UTF-8 + + + + + org.antlr + antlr4-runtime + 4.9.3 + + + junit + junit + 4.13.2 + test + + + com.alibaba.fastjson2 + fastjson2 + 2.0.9 + test + + + + org.springframework + spring-test + 5.3.30 + test + + + org.springframework + spring-context + 5.3.30 + test + + + org.pf4j + pf4j + 3.13.0 + test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.0.3 + + + + + src/main/java/**/*.java + src/test/java/**/*.java + + + true + 4 + + + + + + spotless_eclipse_formatter.xml + + + + + + org.apache.maven.plugins + maven-source-plugin + 2.1 + + true + + + + compile + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.1 + + true + + + + attach-javadocs + + jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + + + + + + + + + + + org.antlr + antlr4-maven-plugin + 4.9.3 + + true + true + ${project.build.directory}/generated-sources/antlr4/com/alibaba/qlexpress4/aparser + + + + + + antlr4 + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + org.antlr.v4.runtime + com.alibaba.qlexpress4.a4runtime + + + + + + + + + + + + + + + + deploy + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + + + + + + + + + + + + fantaibao-fantaibao-maven-repository + maven-repository + http://192.168.3.25:18081/nexus/repository/maven-releases/ + + + + + diff --git a/spotless_eclipse_formatter.xml b/spotless_eclipse_formatter.xml new file mode 100644 index 0000000..e1075ff --- /dev/null +++ b/spotless_eclipse_formatter.xml @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/antlr4/QLParser.g4 b/src/main/antlr4/QLParser.g4 new file mode 100644 index 0000000..9514560 --- /dev/null +++ b/src/main/antlr4/QLParser.g4 @@ -0,0 +1,436 @@ +parser grammar QLParser; + +options { + tokenVocab = QLexer; +} + +@header { + package com.alibaba.qlexpress4.aparser; + import com.alibaba.qlexpress4.aparser.ParserOperatorManager.OpType; + import static com.alibaba.qlexpress4.aparser.ParserOperatorManager.OpType.*; + import static com.alibaba.qlexpress4.QLPrecedences.*; + import static com.alibaba.qlexpress4.aparser.InterpolationMode.*; +} + +@members { + protected boolean isOpType(String lexeme, OpType opType) { + return false; + } + protected Integer precedence(String lexeme) { + return 0; + } + protected InterpolationMode getInterpolationMode() { + return SCRIPT; + } + protected boolean isStrictNewLines() { + return true; + } +} + +// grammar + +program + : (newlines? importDeclaration)* newlines? blockStatements? EOF + ; + +blockStatements + : blockStatement+ + ; + +newlines : NEWLINE+; + +nextStatement + : {_input.LA(1) == Token.EOF || _input.LA(1) == QLexer.RBRACE}? | ';' | {isStrictNewLines()}? NEWLINE; + +blockStatement + : localVariableDeclaration ';' # localVariableDeclarationStatement + | THROW expression nextStatement # throwStatement + | WHILE '(' newlines? expression newlines? ')' '{' newlines? blockStatements? newlines? '}' # whileStatement + | FOR '(' newlines? forInit (forCondition=expression)? ';' newlines? (forUpdate=expression)? newlines? ')' '{' newlines? blockStatements? newlines? '}' # traditionalForStatement + | FOR '(' newlines? declType? varId ':' expression newlines? ')' '{' newlines? blockStatements? newlines? '}' # forEachStatement + | FUNCTION varId '(' newlines? formalOrInferredParameterList? newlines? ')' LBRACE newlines? blockStatements? newlines? RBRACE # functionStatement + | MACRO varId LBRACE newlines? blockStatements? newlines? RBRACE # macroStatement + | (BREAK | CONTINUE) nextStatement # breakContinueStatement + | RETURN expression? nextStatement # returnStatement + | (';' | NEWLINE) # emptyStatement + | expression nextStatement # expressionStatement + ; + +localVariableDeclaration + : declType variableDeclaratorList + ; + +forInit + : localVariableDeclaration ';' + | expression ';' + | ';' + ; + +variableDeclaratorList + : variableDeclarator (newlines? ',' newlines? variableDeclarator)* + ; + +variableDeclarator + : variableDeclaratorId (EQ newlines? variableInitializer)? + ; + +variableDeclaratorId + : varId dims? + ; + +variableInitializer + : expression + | arrayInitializer + ; + +arrayInitializer + : LBRACE newlines? variableInitializerList? newlines? RBRACE + ; + +variableInitializerList + : variableInitializer (newlines? ',' newlines? variableInitializer)* ','? + ; + +// decl type + +declType + : primitiveType dims? + | clsType dims? + ; + +declTypeNoArr + : primitiveType + | clsType + ; + +primitiveType + : 'byte' + | 'short' + | 'int' + | 'long' + | 'float' + | 'double' + | 'boolean' + | 'char' + ; + +referenceType + : clsType dims? + | primitiveType dims + ; + +dims + : LBRACK RBRACK (LBRACK RBRACK)* + ; + +clsTypeNoTypeArguments + : varId ('.' varId)* + ; + +clsType + : varId ('.' varId)* typeArguments? + ; + +typeArguments + : LT newlines? typeArgumentList? newlines? (GT | RIGHSHIFT | URSHIFT)? + | NOEQ + ; + +typeArgumentList + : typeArgument (newlines? ',' newlines? typeArgument)* + ; + +typeArgument + : referenceType + | wildcard + ; + +wildcard + : '?' wildcardBounds? + ; + +wildcardBounds + : 'extends' referenceType + | 'super' referenceType + ; + +// expression + +expression + : leftHandSide assignOperator newlines? expression + | ternaryExpr + ; + +leftHandSide + : varId (newlines? pathPart)* + ; + +ternaryExpr + : condition=baseExpr[1] (QUESTION newlines? thenExpr=baseExpr[0] COLON newlines? elseExpr=expression)? + ; + +baseExpr [int p] + : primary ({_input.LA(1) != Token.EOF && (!isStrictNewLines() || _input.LA(1) != QLexer.NEWLINE) && + isOpType(_input.LT(1).getText(), MIDDLE) && precedence(_input.LT(1).getText()) >= $p}? leftAsso)* + ; + +leftAsso + : binaryop newlines? baseExpr[precedence(_localctx.binaryop().getStart().getText()) + 1]; + +binaryop + : opId | varId + ; + +// primary + +primary + : (prefixExpress)? primaryNoFixPathable (newlines? pathPart)* (suffixExpress)? + | primaryNoFixNonPathable + ; + +prefixExpress + : {_input.LT(1).getType() != Token.EOF && isOpType(_input.LT(1).getText(), PREFIX)}? opId + ; + +suffixExpress + : {_input.LT(1).getType() != Token.EOF && isOpType(_input.LT(1).getText(), SUFFIX)}? opId + ; + +primaryNoFixPathable + : literal # constExpr + | '(' newlines? declType newlines? ')' primary # castExpr + | '(' newlines? expression newlines? ')' # groupExpr + | NEW varId ('.' varId)* typeArguments? '(' newlines? argumentList? newlines? ')' # newObjExpr + | NEW declTypeNoArr dimExprs # newEmptyArrExpr + | NEW declTypeNoArr dims arrayInitializer # newInitArrExpr + | varId # varIdExpr + | primitiveType # typeExpr + | '[' newlines? listItems? newlines? ']' # listExpr + | LBRACE newlines? mapEntries newlines? RBRACE # mapExpr + | LBRACE newlines? blockStatements? newlines? RBRACE # blockExpr + | SELECTOR_START SelectorVariable_VANME # contextSelectExpr + ; + +primaryNoFixNonPathable + : qlIf # ifExpr + | TRY LBRACE newlines? blockStatements? newlines? RBRACE tryCatches? (newlines? tryFinally)? # tryCatchExpr + | lambdaParameters ARROW newlines? ( LBRACE newlines? blockStatements? newlines? RBRACE | expression) # lambdaExpr + ; + +qlIf : IF '(' newlines? condition=expression newlines? ')' newlines? THEN? newlines? thenBody (newlines? ELSE newlines? elseBody)?; + +thenBody + : LBRACE newlines? blockStatements? newlines? RBRACE + | expression + | blockStatement + ; + +elseBody + : LBRACE newlines? blockStatements? newlines? RBRACE + // if ... else ... if ... + | qlIf + | expression + | blockStatement + ; + +listItems + : expression (newlines? ',' newlines? expression)* ','? + ; + +dimExprs + : ('[' newlines? expression newlines? ']')+ + ; + +tryCatches + : tryCatch (newlines? tryCatch)* + ; + +tryCatch + : 'catch' '(' catchParams ')' LBRACE newlines? blockStatements? newlines? RBRACE + ; + +catchParams + : (declType ('|' declType)*)? varId + ; + +tryFinally + : FINALLY LBRACE newlines? blockStatements? newlines? RBRACE + ; + +mapEntries + : ':' + | mapEntry (',' newlines? mapEntry)* ','? + ; + +mapEntry + : mapKey newlines? ':' newlines? mapValue + ; + +mapValue + : {_input.LT(-2).getText().equals("'@class'")}? QuoteStringLiteral # clsValue + | expression # eValue + ; + +mapKey + : idMapKey # idKey + | doubleQuoteStringLiteral # stringKey + | QuoteStringLiteral # quoteStringKey + ; + +idMapKey + : varId + | FOR + | IF + | ELSE + | WHILE + | BREAK + | CONTINUE + | RETURN + | FUNCTION + | MACRO + | IMPORT + | STATIC + | NEW + | BYTE + | SHORT + | INT + | LONG + | FLOAT + | DOUBLE + | CHAR + | BOOL + | NULL + | TRUE + | FALSE + | EXTENDS + | SUPER + | TRY + | CATCH + | FINALLY + | THROW + | CLASS + | THIS + ; + +pathPart + : '.' varId '(' newlines? argumentList? newlines? ')' # methodInvoke + | OPTIONAL_CHAINING varId '(' newlines? argumentList? newlines? ')' # optionalMethodInvoke + | SPREAD_CHAINING varId '(' newlines? argumentList? newlines? ')' # spreadMethodInvoke + | '.' fieldId # fieldAccess + | OPTIONAL_CHAINING fieldId # optionalFieldAccess + | SPREAD_CHAINING fieldId # spreadFieldAccess + | DCOLON varId # methodAccess + | '(' newlines? argumentList? newlines? ')' # callExpr + | '[' newlines? indexValueExpr? newlines? ']' # indexExpr + | {isOpType(_input.LT(1).getText(), MIDDLE) && precedence(_input.LT(1).getText()) == GROUP}? opId newlines? varId # customPath + ; + +fieldId + : varId + | CLASS + | QuoteStringLiteral + ; + +indexValueExpr + : expression # singleIndex + | start=expression? newlines? ':' newlines? end=expression? # sliceIndex + ; + +argumentList + : expression (newlines? ',' newlines? expression)* + ; + +literal + : IntegerLiteral + | FloatingPointLiteral + | IntegerOrFloatingLiteral + | boolenLiteral + | QuoteStringLiteral + | doubleQuoteStringLiteral + | NULL + ; + +doubleQuoteStringLiteral + : {getInterpolationMode() == DISABLE}? DOUBLE_QUOTE StaticStringCharacters? DOUBLE_QUOTE + | DOUBLE_QUOTE (DyStrText | stringExpression)* DOUBLE_QUOTE + ; + +stringExpression + : {getInterpolationMode() == SCRIPT}? DyStrExprStart newlines? expression newlines? RBRACE + | {getInterpolationMode() == VARIABLE}? DyStrExprStart SelectorVariable_VANME + ; + +boolenLiteral + : TRUE + | FALSE + ; + +lambdaParameters + : varId + | '(' formalOrInferredParameterList? ')' + ; + +formalOrInferredParameterList + : formalOrInferredParameter (newlines? ',' newlines? formalOrInferredParameter)* + ; + +formalOrInferredParameter + : declType? varId + ; + +// import (not support import static now) + +importDeclaration + // import xxx + : IMPORT varId ('.' varId)* ';' # importCls + // import .* + | IMPORT varId ('.' varId)* (DOT MUL | DOTMUL) ';' # importPack + ; + +// id + +assignOperator + : EQ + | RIGHSHIFT_ASSGIN + | URSHIFT_ASSGIN + | LSHIFT_ASSGIN + | ADD_ASSIGN + | SUB_ASSIGN + | AND_ASSIGN + | OR_ASSIGN + | MUL_ASSIGN + | MOD_ASSIGN + | DIV_ASSIGN + | XOR_ASSIGN + ; + +opId + : GT + | LT + | GE + | LE + | BANG + | TILDE + | ADD + | SUB + | MUL + | DIV + | INC + | DEC + | DOTMUL + | NOEQ + | RIGHSHIFT + | URSHIFT + | LEFTSHIFT + | BIT_AND + | BIT_OR + | MOD + | CARET + | assignOperator + | OPID + ; + +varId + : ID + | FUNCTION + ; \ No newline at end of file diff --git a/src/main/antlr4/QLexer.g4 b/src/main/antlr4/QLexer.g4 new file mode 100644 index 0000000..8b611a3 --- /dev/null +++ b/src/main/antlr4/QLexer.g4 @@ -0,0 +1,1196 @@ +lexer grammar QLexer; + +@header { + package com.alibaba.qlexpress4.aparser; + import static com.alibaba.qlexpress4.aparser.InterpolationMode.*; +} + +@members { + protected String getSelectorStart() { + return "${"; + } + + protected InterpolationMode getInterpolationMode() { + return SCRIPT; + } + + protected void consumeSelectorVariable() { } + + protected void throwScannerException(String lexeme, String reason) { } +} + +// lexer + +// Keywords + +FOR: 'for'; +IF: 'if'; +ELSE: 'else'; +WHILE: 'while'; +BREAK: 'break'; +CONTINUE: 'continue'; +RETURN: 'return'; +FUNCTION: 'function'; +MACRO: 'macro'; +IMPORT: 'import'; +STATIC: 'static'; +NEW: 'new'; +BYTE: 'byte'; +SHORT: 'short'; +INT: 'int'; +LONG: 'long'; +FLOAT: 'float'; +DOUBLE: 'double'; +CHAR: 'char'; +BOOL: 'boolean'; +NULL: 'null'; +TRUE: 'true'; +FALSE: 'false'; +EXTENDS: 'extends'; +SUPER: 'super'; +TRY: 'try'; +CATCH: 'catch'; +FINALLY: 'finally'; +THROW: 'throw'; +THEN: 'then'; + +// unuseful now, but reserve them for future +CLASS: 'class'; +THIS: 'this'; + +// String Literals + +QuoteStringLiteral + : '\'' QuoteStringCharacters? '\'' + ; + +fragment QuoteStringCharacters + : QuoteStringCharacter+ + ; + +fragment QuoteStringCharacter + : ~['\\] + | '\\' '\''? + ; + +fragment ZeroToThree + : [0-3] + ; + +fragment OctalDigit + : [0-7] + ; + +fragment HexDigit + : [0-9a-fA-F] + ; + +// Number Literals + +IntegerLiteral + : HexIntegerLiteral + | OctalIntegerLiteral + | BinaryIntegerLiteral + ; + +FloatingPointLiteral + : '.' Digits ExponentPart? FloatTypeSuffix? + | DecimalNumeral ExponentPart FloatTypeSuffix? + | DecimalNumeral FloatTypeSuffix + ; + +IntegerOrFloatingLiteral + // 1l 1. 1.2 1.2e3 1.2e3f + : DecimalNumeral IntegerOrFloating? + ; + +fragment IntegerOrFloating + : IntegerTypeSuffix + | { + !( + ( (_input.LA(2) >= 'a' && _input.LA(2) <= 'z') || (_input.LA(2) >= 'A' && _input.LA(2) <= 'Z') ) + && + ( (_input.LA(3) >= 'a' && _input.LA(3) <= 'z') || (_input.LA(3) >= 'A' && _input.LA(3) <= 'Z') ) + ) + }? '.' Digits? ExponentPart? FloatTypeSuffix? + ; + +fragment BinaryIntegerLiteral + : BinaryNumeral IntegerTypeSuffix? + ; + +fragment BinaryNumeral + : '0' [bB] BinaryDigits + ; + +fragment BinaryDigits + : BinaryDigit (BinaryDigitsAndUnderscores? BinaryDigit)? + ; + +fragment BinaryDigit + : [01] + ; + +fragment BinaryDigitsAndUnderscores + : BinaryDigitOrUnderscore+ + ; + +fragment BinaryDigitOrUnderscore + : BinaryDigit + | '_' + ; + +fragment OctalIntegerLiteral + : OctalNumeral IntegerTypeSuffix? + ; + +fragment OctalNumeral + : '0' Underscores? OctalDigits + ; + +fragment OctalDigits + : OctalDigit (OctalDigitsAndUnderscores? OctalDigit)? + ; + +fragment OctalDigitsAndUnderscores + : OctalDigitOrUnderscore+ + ; + +fragment OctalDigitOrUnderscore + : OctalDigit + | '_' + ; + +fragment HexIntegerLiteral + : HexNumeral IntegerTypeSuffix? + ; + +fragment HexNumeral + : '0' [xX] HexDigits + ; + +fragment HexDigits + : HexDigit (HexDigitsAndUnderscores? HexDigit)? + ; + +fragment HexDigitsAndUnderscores + : HexDigitOrUnderscore+ + ; + +fragment HexDigitOrUnderscore + : HexDigit + | '_' + ; + +fragment DecimalIntegerLiteral + : DecimalNumeral IntegerTypeSuffix? + ; + +fragment IntegerTypeSuffix + : [lL] + ; + +fragment DecimalNumeral + : '0' + | NonZeroDigit (Digits? | Underscores Digits) + ; + +fragment Underscores + : '_'+ + ; + + +fragment NonZeroDigit + : [1-9] + ; + +fragment Digits + : Digit (DigitsAndUnderscores? Digit)? + ; + +fragment Digit + : '0' + | NonZeroDigit + ; + +fragment DigitsAndUnderscores + : DigitOrUnderscore+ + ; + +fragment DigitOrUnderscore + : Digit + | '_' + ; + +fragment HexSignificand + : HexNumeral '.'? + | '0' [xX] HexDigits? '.' HexDigits + ; + +fragment BinaryExponent + : BinaryExponentIndicator SignedInteger + ; + +fragment BinaryExponentIndicator + : [pP] + ; + +fragment FloatTypeSuffix + : [fFdD] + ; + +fragment ExponentPart + : ExponentIndicator SignedInteger + ; + +fragment SignedInteger + : Sign? Digits + ; + +fragment Sign + : [+-] + ; + +fragment ExponentIndicator + : [eE] + ; + +// operator + +LPAREN : '('; +RPAREN : ')'; +LBRACE : '{'; +RBRACE : '}'; +LBRACK : '['; +RBRACK : ']'; + + +// system operator + +DOT : '.'; +ARROW : '->'; +SEMI : ';'; +COMMA : ','; +QUESTION : '?'; +COLON : ':'; +DCOLON: '::'; +GT : '>'; +LT : '<'; +EQ : '='; +NOEQ: '<>'; +RIGHSHIFT_ASSGIN: '>>='; +RIGHSHIFT: '>>'; +OPTIONAL_CHAINING: '?.'; +SPREAD_CHAINING: '*.'; +URSHIFT_ASSGIN: '>>>='; +URSHIFT: '>>>'; +LSHIFT_ASSGIN: '<<='; +LEFTSHIFT: '<<'; +GE: '>='; +LE: '<='; +DOTMUL: '.*'; +CARET: '^'; +ADD_ASSIGN: '+='; +SUB_ASSIGN: '-='; +AND_ASSIGN: '&='; +OR_ASSIGN: '|='; +MUL_ASSIGN: '*='; +MOD_ASSIGN: '%='; +DIV_ASSIGN: '/='; +XOR_ASSIGN: '^='; + +// prefix suffix operator +BANG : '!'; +TILDE : '~'; + +ADD: '+'; +SUB: '-'; +MUL: '*'; +DIV: '/'; +BIT_AND: '&'; +BIT_OR: '|'; +MOD: '%'; + +INC: '++'; +DEC: '--'; + +// Whitespace and comments + +NEWLINE : '\r' '\n'? | '\n'; + +WS : [ \t\u000C]+ -> skip + ; + +COMMENT + : '/*' .*? '*/' -> skip + ; + +LINE_COMMENT + : '//' ~[\r\n]* -> skip + ; + +// custom operator + +OPID: OpIdItemStart OpIdItem+; + +fragment OpIdItemStart + : CARET + | TILDE + | BIT_AND + | BIT_OR + | MUL + | MOD + | EQ + | BANG + | DIV + | ADD + | SUB + | QUESTION + | DOT + ; + +fragment OpIdItem + : CARET + | TILDE + | BIT_AND + | BIT_OR + | MUL + | MOD + | EQ + | BANG + | LT + | GT + | DIV + | COLON + | QUESTION + | DOT + ; + +// Selector + +SELECTOR_START: ('${' | '$[' | '#{' | '#[') { + if (getSelectorStart().equals(getText())) { + pushMode(SelectorVariable); + } else { + throwScannerException(getText(), "invalid selector start, expect '" + getSelectorStart() + "'"); + } +}; + +// Java Idetifiers specification +// https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8 + +ID + : IdStart IdPart* + ; + +// unicode chart +// https://www.ssec.wisc.edu/~tomw/java/unicode.html +fragment IdStart + // # + : [\u0023] + // A-Z + | [\u0041-\u005A] + // _ + | [\u005F] + // @ + | [\u0040] + // $ + | [\u0024] + // a-z + | [\u0061-\u007A] + // ¢-¥ + | [\u00A2-\u00A5] + | [\u00AA] + | [\u00B5] + | [\u00BA] + | [\u00C0-\u00D6] + | [\u00D8-\u00F6] + | [\u00F8-\u02C1] + | [\u02C6-\u02D1] + | [\u02E0-\u02E4] + | [\u02EC] + | [\u02EE] + | [\u0370-\u0374] + | [\u0376-\u0377] + | [\u037A-\u037D] + | [\u037F] + | [\u0386] + | [\u0388-\u038A] + | [\u038C] + | [\u038E-\u03A1] + | [\u03A3-\u03F5] + | [\u03F7-\u0481] + | [\u048A-\u052F] + | [\u0531-\u0556] + | [\u0559] + | [\u0561-\u0587] + | [\u058F] + | [\u05D0-\u05EA] + | [\u05F0-\u05F2] + | [\u060B] + | [\u0620-\u064A] + | [\u066E-\u066F] + | [\u0671-\u06D3] + | [\u06D5] + | [\u06E5-\u06E6] + | [\u06EE-\u06EF] + | [\u06FA-\u06FC] + | [\u06FF] + | [\u0710] + | [\u0712-\u072F] + | [\u074D-\u07A5] + | [\u07B1] + | [\u07CA-\u07EA] + | [\u07F4-\u07F5] + | [\u07FA] + | [\u0800-\u0815] + | [\u081A] + | [\u0824] + | [\u0828] + | [\u0840-\u0858] + | [\u0860-\u086A] + | [\u08A0-\u08B4] + | [\u08B6-\u08BD] + | [\u0904-\u0939] + [\u093D] + | [\u0950] + | [\u0958-\u0961] + | [\u0971-\u0980] + | [\u0985-\u098C] + | [\u098F-\u0990] + | [\u0993-\u09A8] + | [\u09AA-\u09B0] + | [\u09B2] + | [\u09B6-\u09B9] + | [\u09BD] + | [\u09CE] + | [\u09DC-\u09DD] + | [\u09DF-\u09E1] + | [\u09F0-\u09F3] + | [\u09FB-\u09FC] + | [\u0A05-\u0A0A] + | [\u0A0F-\u0A10] + | [\u0A13-\u0A28] + | [\u0A2A-\u0A30] + | [\u0A32-\u0A33] + | [\u0A35-\u0A36] + | [\u0A38-\u0A39] + | [\u0A59-\u0A5C] + | [\u0A5E] + | [\u0A72-\u0A74] + | [\u0A85-\u0A8D] + | [\u0A8F-\u0A91] + | [\u0A93-\u0AA8] + | [\u0AAA-\u0AB0] + | [\u0AB2-\u0AB3] + | [\u0AB5-\u0AB9] + | [\u0ABD] + | [\u0AD0] + | [\u0AE0-\u0AE1] + | [\u0AF1] + | [\u0AF9] + | [\u0B05-\u0B0C] + | [\u0B0F-\u0B10] + | [\u0B13-\u0B28] + | [\u0B2A-\u0B30] + | [\u0B32-\u0B33] + | [\u0B35-\u0B39] + | [\u0B3D] + | [\u0B5C-\u0B5D] + | [\u0B5F-\u0B61] + | [\u0B71] + | [\u0B83] + | [\u0B85-\u0B8A] + | [\u0B8E-\u0B90] + | [\u0B92-\u0B95] + | [\u0B99-\u0B9A] + | [\u0B9C] + | [\u0B9E-\u0B9F] + | [\u0BA3-\u0BA4] + | [\u0BA8-\u0BAA] + | [\u0BAE-\u0BB9] + | [\u0BD0] + | [\u0BF9] + | [\u0C05-\u0C0C] + | [\u0C0E-\u0C10] + | [\u0C12-\u0C28] + | [\u0C2A-\u0C39] + | [\u0C3D] + | [\u0C58-\u0C5A] + | [\u0C60-\u0C61] + | [\u0C80] + | [\u0C85-\u0C8C] + | [\u0C8E-\u0C90] + | [\u0C92-\u0CA8] + | [\u0CAA-\u0CB3] + | [\u0CB5-\u0CB9] + | [\u0CBD] + | [\u0CDE] + | [\u0CE0-\u0CE1] + | [\u0CF1-\u0CF2] + | [\u0D05-\u0D0C] + | [\u0D0E-\u0D10] + | [\u0D12-\u0D3A] + | [\u0D3D] + | [\u0D4E] + | [\u0D54-\u0D56] + | [\u0D5F-\u0D61] + | [\u0D7A-\u0D7F] + | [\u0D85-\u0D96] + | [\u0D9A-\u0DB1] + | [\u0DB3-\u0DBB] + | [\u0DBD] + | [\u0DC0-\u0DC6] + | [\u0E01-\u0E30] + | [\u0E32-\u0E33] + | [\u0E3F-\u0E46] + | [\u0E81-\u0E82] + | [\u0E84] + | [\u0E87-\u0E88] + | [\u0E8A] + | [\u0E8D] + | [\u0E94-\u0E97] + | [\u0E99-\u0E9F] + | [\u0EA1-\u0EA3] + | [\u0EA5] + | [\u0EA7] + | [\u0EAA-\u0EAB] + | [\u0EAD-\u0EB0] + | [\u0EB2-\u0EB3] + | [\u0EBD] + | [\u0EC0-\u0EC4] + | [\u0EC6] + | [\u0EDC-\u0EDF] + | [\u0F00] + | [\u0F40-\u0F47] + | [\u0F49-\u0F6C] + | [\u0F88-\u0F8C] + | [\u1000-\u102A] + | [\u103F] + | [\u1050-\u1055] + | [\u105A-\u105D] + | [\u1061] + | [\u1065-\u1066] + | [\u106E-\u1070] + | [\u1075-\u1081] + | [\u108E] + | [\u10A0-\u10C5] + | [\u10C7] + | [\u10CD] + | [\u10D0-\u10FA] + | [\u10FC-\u1248] + | [\u124A-\u124D] + | [\u1250-\u1256] + | [\u1258] + | [\u125A-\u125D] + | [\u1260-\u1288] + | [\u128A-\u128D] + | [\u1290-\u12B0] + | [\u12B2-\u12B5] + | [\u12B8-\u12BE] + | [\u12C0] + | [\u12C2-\u12C5] + | [\u12C8-\u12D6] + | [\u12D8-\u1310] + | [\u1312-\u1315] + | [\u1318-\u135A] + | [\u1380-\u138F] + | [\u13A0-\u13F5] + | [\u13F8-\u13FD] + | [\u1401-\u166C] + | [\u166F-\u167F] + | [\u1681-\u169A] + | [\u16A0-\u16EA] + | [\u16EE-\u16F8] + | [\u1700-\u170C] + | [\u170E-\u1711] + | [\u1720-\u1731] + | [\u1740-\u1751] + | [\u1760-\u176C] + | [\u176E-\u1770] + | [\u1780-\u17B3] + | [\u17D7] + | [\u17DB-\u17DC] + | [\u1820-\u1877] + | [\u1880-\u1884] + | [\u1887-\u18A8] + | [\u18AA] + | [\u18B0-\u18F5] + | [\u1900-\u191E] + | [\u1950-\u196D] + | [\u1970-\u1974] + | [\u1980-\u19AB] + | [\u19B0-\u19C9] + | [\u1A00-\u1A16] + | [\u1A20-\u1A54] + | [\u1AA7] + | [\u1B05-\u1B33] + | [\u1B45-\u1B4B] + | [\u1B83-\u1BA0] + | [\u1BAE-\u1BAF] + | [\u1BBA-\u1BE5] + | [\u1C00-\u1C23] + | [\u1C4D-\u1C4F] + | [\u1C5A-\u1C7D] + | [\u1C80-\u1C88] + | [\u1CE9-\u1CEC] + | [\u1CEE-\u1CF1] + | [\u1CF5-\u1CF6] + | [\u1D00-\u1DBF] + | [\u1E00-\u1F15] + | [\u1F18-\u1F1D] + | [\u1F20-\u1F45] + | [\u1F48-\u1F4D] + | [\u1F50-\u1F57] + | [\u1F59] + | [\u1F5B] + | [\u1F5D] + | [\u1F5F-\u1F7D] + | [\u1F80-\u1FB4] + | [\u1FB6-\u1FBC] + | [\u1FBE] + | [\u1FC2-\u1FC4] + | [\u1FC6-\u1FCC] + | [\u1FD0-\u1FD3] + | [\u1FD6-\u1FDB] + | [\u1FE0-\u1FEC] + | [\u1FF2-\u1FF4] + | [\u1FF6-\u1FFC] + | [\u203F-\u2040] + | [\u2054] + | [\u2071] + | [\u207F] + | [\u2090-\u209C] + | [\u20A0-\u20BF] + | [\u2102] + | [\u2107] + | [\u210A-\u2113] + | [\u2115] + | [\u2119-\u211D] + | [\u2124] + | [\u2126] + | [\u2128] + | [\u212A-\u212D] + | [\u212F-\u2139] + | [\u213C-\u213F] + | [\u2145-\u2149] + | [\u214E] + | [\u2160-\u2188] + | [\u2C00-\u2C2E] + | [\u2C30-\u2C5E] + | [\u2C60-\u2CE4] + | [\u2CEB-\u2CEE] + | [\u2CF2-\u2CF3] + | [\u2D00-\u2D25] + | [\u2D27] + | [\u2D2D] + | [\u2D30-\u2D67] + | [\u2D6F] + | [\u2D80-\u2D96] + | [\u2DA0-\u2DA6] + | [\u2DA8-\u2DAE] + | [\u2DB0-\u2DB6] + | [\u2DB8-\u2DBE] + | [\u2DC0-\u2DC6] + | [\u2DC8-\u2DCE] + | [\u2DD0-\u2DD6] + | [\u2DD8-\u2DDE] + | [\u2E2F] + | [\u3005-\u3007] + | [\u3021-\u3029] + | [\u3031-\u3035] + | [\u3038-\u303C] + | [\u3041-\u3096] + | [\u309D-\u309F] + | [\u30A1-\u30FA] + | [\u30FC-\u30FF] + | [\u3105-\u312E] + | [\u3131-\u318E] + | [\u31A0-\u31BA] + | [\u31F0-\u31FF] + | [\u3400-\u4DB5] + // chinese + | [\u4E00-\u9FEA] + | [\uA000-\uA48C] + | [\uA4D0-\uA4FD] + | [\uA500-\uA60C] + | [\uA610-\uA61F] + | [\uA62A-\uA62B] + | [\uA640-\uA66E] + | [\uA67F-\uA69D] + | [\uA6A0-\uA6EF] + | [\uA717-\uA71F] + | [\uA722-\uA788] + | [\uA78B-\uA7AE] + | [\uA7B0-\uA7B7] + | [\uA7F7-\uA801] + | [\uA803-\uA805] + | [\uA807-\uA80A] + | [\uA80C-\uA822] + | [\uA838] + | [\uA840-\uA873] + | [\uA882-\uA8B3] + | [\uA8F2-\uA8F7] + | [\uA8FB] + | [\uA8FD] + | [\uA90A-\uA925] + | [\uA930-\uA946] + | [\uA960-\uA97C] + | [\uA984-\uA9B2] + | [\uA9CF] + | [\uA9E0-\uA9E4] + | [\uA9E6-\uA9EF] + | [\uA9FA-\uA9FE] + | [\uAA00-\uAA28] + | [\uAA40-\uAA42] + | [\uAA44-\uAA4B] + | [\uAA60-\uAA76] + | [\uAA7A] + | [\uAA7E-\uAAAF] + | [\uAAB1] + | [\uAAB5-\uAAB6] + | [\uAAB9-\uAABD] + | [\uAAC0] + | [\uAAC2] + | [\uAADB-\uAADD] + | [\uAAE0-\uAAEA] + | [\uAAF2-\uAAF4] + | [\uAB01-\uAB06] + | [\uAB09-\uAB0E] + | [\uAB11-\uAB16] + | [\uAB20-\uAB26] + | [\uAB28-\uAB2E] + | [\uAB30-\uAB5A] + | [\uAB5C-\uAB65] + | [\uAB70-\uABE2] + | [\uAC00-\uD7A3] + | [\uD7B0-\uD7C6] + | [\uD7CB-\uD7FB] + | [\uF900-\uFA6D] + | [\uFA70-\uFAD9] + | [\uFB00-\uFB06] + | [\uFB13-\uFB17] + | [\uFB1D] + | [\uFB1F-\uFB28] + | [\uFB2A-\uFB36] + | [\uFB38-\uFB3C] + | [\uFB3E] + | [\uFB40-\uFB41] + | [\uFB43-\uFB44] + | [\uFB46-\uFBB1] + | [\uFBD3-\uFD3D] + | [\uFD50-\uFD8F] + | [\uFD92-\uFDC7] + | [\uFDF0-\uFDFC] + | [\uFE33-\uFE34] + | [\uFE4D-\uFE4F] + | [\uFE69] + | [\uFE70-\uFE74] + | [\uFE76-\uFEFC] + | [\uFF04] + | [\uFF21-\uFF3A] + | [\uFF3F] + | [\uFF41-\uFF5A] + | [\uFF66-\uFFBE] + | [\uFFC2-\uFFC7] + | [\uFFCA-\uFFCF] + | [\uFFD2-\uFFD7] + | [\uFFDA-\uFFDC] + | [\uFFE0-\uFFE1] + | [\uFFE5-\uFFE6] + ; + +fragment IdPart + : IdStart + // 、 + | [\u3001] + // 0-9 + | [\u0030-\u0039] + | [\u3010\u3011] // 【】 + | [\u2260] // ≠ + | [\u007F-\u009F] + | [\u00AD] + | [\u0300-\u036F] + | [\u0483-\u0487] + | [\u0591-\u05BD] + | [\u05BF] + | [\u05C1-\u05C2] + | [\u05C4-\u05C5] + | [\u05C7] + | [\u0600-\u0605] + | [\u0610-\u061A] + | [\u061C] + | [\u064B-\u0669] + | [\u0670] + | [\u06D6-\u06DD] + | [\u06DF-\u06E4] + | [\u06E7-\u06E8] + | [\u06EA-\u06ED] + | [\u06F0-\u06F9] + | [\u070F] + | [\u0711] + | [\u0730-\u074A] + | [\u07A6-\u07B0] + | [\u07C0-\u07C9] + | [\u07EB-\u07F3] + | [\u0816-\u0819] + | [\u081B-\u0823] + | [\u0825-\u0827] + | [\u0829-\u082D] + | [\u0859-\u085B] + | [\u08D4-\u0903] + | [\u093A-\u093C] + | [\u093E-\u094F] + | [\u0951-\u0957] + | [\u0962-\u0963] + | [\u0966-\u096F] + | [\u0981-\u0983] + | [\u09BC] + | [\u09BE-\u09C4] + | [\u09C7-\u09C8] + | [\u09CB-\u09CD] + | [\u09D7] + | [\u09E2-\u09E3] + | [\u09E6-\u09EF] + | [\u0A01-\u0A03] + | [\u0A3C] + | [\u0A3E-\u0A42] + | [\u0A47-\u0A48] + | [\u0A4B-\u0A4D] + | [\u0A51] + | [\u0A66-\u0A71] + | [\u0A75] + | [\u0A81-\u0A83] + | [\u0ABC] + | [\u0ABE-\u0AC5] + | [\u0AC7-\u0AC9] + | [\u0ACB-\u0ACD] + | [\u0AE2-\u0AE3] + | [\u0AE6-\u0AEF] + | [\u0AFA-\u0AFF] + | [\u0B01-\u0B03] + | [\u0B3C] + | [\u0B3E-\u0B44] + | [\u0B47-\u0B48] + | [\u0B4B-\u0B4D] + | [\u0B56-\u0B57] + | [\u0B62-\u0B63] + | [\u0B66-\u0B6F] + | [\u0B82] + | [\u0BBE-\u0BC2] + | [\u0BC6-\u0BC8] + | [\u0BCA-\u0BCD] + | [\u0BD7] + | [\u0BE6-\u0BEF] + | [\u0C00-\u0C03] + | [\u0C3E-\u0C44] + | [\u0C46-\u0C48] + | [\u0C4A-\u0C4D] + | [\u0C55-\u0C56] + | [\u0C62-\u0C63] + | [\u0C66-\u0C6F] + | [\u0C81-\u0C83] + | [\u0CBC] + | [\u0CBE-\u0CC4] + | [\u0CC6-\u0CC8] + | [\u0CCA-\u0CCD] + | [\u0CD5-\u0CD6] + | [\u0CE2-\u0CE3] + | [\u0CE6-\u0CEF] + | [\u0D00-\u0D03] + | [\u0D3B-\u0D3C] + | [\u0D3E-\u0D44] + | [\u0D46-\u0D48] + | [\u0D4A-\u0D4D] + | [\u0D57] + | [\u0D62-\u0D63] + | [\u0D66-\u0D6F] + | [\u0D82-\u0D83] + | [\u0DCA] + | [\u0DCF-\u0DD4] + | [\u0DD6] + | [\u0DD8-\u0DDF] + | [\u0DE6-\u0DEF] + | [\u0DF2-\u0DF3] + | [\u0E31] + | [\u0E34-\u0E3A] + | [\u0E47-\u0E4E] + | [\u0E50-\u0E59] + | [\u0EB1] + | [\u0EB4-\u0EB9] + | [\u0EBB-\u0EBC] + | [\u0EC8-\u0ECD] + | [\u0ED0-\u0ED9] + | [\u0F18-\u0F19] + | [\u0F20-\u0F29] + | [\u0F35] + | [\u0F37] + | [\u0F39] + | [\u0F3E-\u0F3F] + | [\u0F71-\u0F84] + | [\u0F86-\u0F87] + | [\u0F8D-\u0F97] + | [\u0F99-\u0FBC] + | [\u0FC6] + | [\u102B-\u103E] + | [\u1040-\u1049] + | [\u1056-\u1059] + | [\u105E-\u1060] + | [\u1062-\u1064] + | [\u1067-\u106D] + | [\u1071-\u1074] + | [\u1082-\u108D] + | [\u108F-\u109D] + | [\u135D-\u135F] + | [\u1712-\u1714] + | [\u1732-\u1734] + | [\u1752-\u1753] + | [\u1772-\u1773] + | [\u17B4-\u17D3] + | [\u17DD] + | [\u17E0-\u17E9] + | [\u180B-\u180E] + | [\u1810-\u1819] + | [\u1885-\u1886] + | [\u18A9] + | [\u1920-\u192B] + | [\u1930-\u193B] + | [\u1946-\u194F] + | [\u19D0-\u19D9] + | [\u1A17-\u1A1B] + | [\u1A55-\u1A5E] + | [\u1A60-\u1A7C] + | [\u1A7F-\u1A89] + | [\u1A90-\u1A99] + | [\u1AB0-\u1ABD] + | [\u1B00-\u1B04] + | [\u1B34-\u1B44] + | [\u1B50-\u1B59] + | [\u1B6B-\u1B73] + | [\u1B80-\u1B82] + | [\u1BA1-\u1BAD] + | [\u1BB0-\u1BB9] + | [\u1BE6-\u1BF3] + | [\u1C24-\u1C37] + | [\u1C40-\u1C49] + | [\u1C50-\u1C59] + | [\u1CD0-\u1CD2] + | [\u1CD4-\u1CE8] + | [\u1CED] + | [\u1CF2-\u1CF4] + | [\u1CF7-\u1CF9] + | [\u1DC0-\u1DF9] + | [\u1DFB-\u1DFF] + | [\u200B-\u200F] + | [\u202A-\u202E] + | [\u2060-\u2064] + | [\u2066-\u206F] + | [\u20D0-\u20DC] + | [\u20E1] + | [\u20E5-\u20F0] + | [\u2CEF-\u2CF1] + | [\u2D7F] + | [\u2DE0-\u2DFF] + | [\u302A-\u302F] + | [\u3099-\u309A] + | [\uA620-\uA629] + | [\uA66F] + | [\uA674-\uA67D] + | [\uA69E-\uA69F] + | [\uA6F0-\uA6F1] + | [\uA802] + | [\uA806] + | [\uA80B] + | [\uA823-\uA827] + | [\uA880-\uA881] + | [\uA8B4-\uA8C5] + | [\uA8D0-\uA8D9] + | [\uA8E0-\uA8F1] + | [\uA900-\uA909] + | [\uA926-\uA92D] + | [\uA947-\uA953] + | [\uA980-\uA983] + | [\uA9B3-\uA9C0] + | [\uA9D0-\uA9D9] + | [\uA9E5] + | [\uA9F0-\uA9F9] + | [\uAA29-\uAA36] + | [\uAA43] + | [\uAA4C-\uAA4D] + | [\uAA50-\uAA59] + | [\uAA7B-\uAA7D] + | [\uAAB0] + | [\uAAB2-\uAAB4] + | [\uAAB7-\uAAB8] + | [\uAABE-\uAABF] + | [\uAAC1] + | [\uAAEB-\uAAEF] + | [\uAAF5-\uAAF6] + | [\uABE3-\uABEA] + | [\uABEC-\uABED] + | [\uABF0-\uABF9] + | [\uFB1E] + | [\uFE00-\uFE0F] + | [\uFE20-\uFE2F] + | [\uFEFF] + | [\uFF10-\uFF19] + | [\uFFF9-\uFFFB] + ; + +// string expression + +DOUBLE_QUOTE: '"' { + if (getInterpolationMode() == DISABLE) { + pushMode(StaticString); + } else { + pushMode(DynamicString); + } +}; + +CATCH_ALL: . ; + +mode StaticString; + +StaticStringCharacters: StaticStringCharacter+; + +fragment StaticStringCharacter + : ~["\\] + | '\\' '"'? + ; + +STATIC_STRING_CLOSE: DOUBLE_QUOTE -> popMode, type(DOUBLE_QUOTE); + +mode DynamicString; + +DyStrExprStart: '${' { + if (getInterpolationMode() == SCRIPT) { + pushMode(StringExpression); + } else if (getInterpolationMode() == VARIABLE) { + pushMode(SelectorVariable); + } +}; + +DyStrText: DyStringCharacter+; + +fragment DyStringCharacter + : ~["\\$] + | '\\' ('"' | '$')? + ; + +DYNAMIC_STRING_CLOSE: DOUBLE_QUOTE -> popMode, type(DOUBLE_QUOTE); + +mode SelectorVariable; + +SelectorVariable_VANME: { + consumeSelectorVariable(); +}; + +mode StringExpression; + +StrExpr_THEN: THEN -> type(THEN); +StrExpr_FOR: FOR -> type(FOR); +StrExpr_IF: IF -> type(IF); +StrExpr_ELSE: ELSE -> type(ELSE); +StrExpr_WHILE: WHILE -> type(WHILE); +StrExpr_BREAK: BREAK -> type(BREAK); +StrExpr_CONTINUE: CONTINUE -> type(CONTINUE); +StrExpr_RETURN: RETURN -> type(RETURN); +StrExpr_FUNCTION: FUNCTION -> type(FUNCTION); +StrExpr_MACRO: MACRO -> type(MACRO); +StrExpr_IMPORT: IMPORT -> type(IMPORT); +StrExpr_STATIC: STATIC -> type(STATIC); +StrExpr_NEW: NEW -> type(NEW); +StrExpr_BYTE: BYTE -> type(BYTE); +StrExpr_SHORT: SHORT -> type(SHORT); +StrExpr_INT: INT -> type(INT); +StrExpr_LONG: LONG -> type(LONG); +StrExpr_FLOAT: FLOAT -> type(FLOAT); +StrExpr_DOUBLE: DOUBLE -> type(DOUBLE); +StrExpr_CHAR: CHAR -> type(CHAR); +StrExpr_BOOL: BOOL -> type(BOOL); +StrExpr_NULL: NULL -> type(NULL); +StrExpr_TRUE: TRUE -> type(TRUE); +StrExpr_FALSE: FALSE -> type(FALSE); +StrExpr_EXTENDS: EXTENDS -> type(EXTENDS); +StrExpr_SUPER: SUPER -> type(SUPER); +StrExpr_TRY: TRY -> type(TRY); +StrExpr_CATCH: CATCH -> type(CATCH); +StrExpr_FINALLY: FINALLY -> type(FINALLY); +StrExpr_THROW: THROW -> type(THROW); + +StrExpr_CLASS: CLASS -> type(CLASS); +StrExpr_THIS: THIS -> type(THIS); + +StrExpr_QuoteStringLiteral: QuoteStringLiteral -> type(QuoteStringLiteral); + +StrExpr_IntegerLiteral: IntegerLiteral -> type(IntegerLiteral); +StrExpr_FloatingPointLiteral: FloatingPointLiteral -> type(FloatingPointLiteral); +StrExpr_IntegerOrFloatingLiteral: IntegerOrFloatingLiteral -> type(IntegerOrFloatingLiteral); + +StrExpr_LPAREN: LPAREN -> type(LPAREN); +StrExpr_RPAREN: RPAREN -> type(RPAREN); +StrExpr_LBRACE: LBRACE -> pushMode(StringExpression), type(LBRACE); +StrExpr_RBRACE: RBRACE -> popMode, type(RBRACE); +StrExpr_LBRACK: LBRACK -> type(LBRACK); +StrExpr_RBRACK: RBRACK -> type(RBRACK); + +StrExpr_DOT: DOT -> type(DOT); +StrExpr_ARROW: ARROW -> type(ARROW); +StrExpr_SEMI: SEMI -> type(SEMI); +StrExpr_COMMA: COMMA -> type(COMMA); +StrExpr_QUESTION: QUESTION -> type(QUESTION); +StrExpr_COLON: COLON -> type(COLON); +StrExpr_DCOLON: DCOLON -> type(DCOLON); +StrExpr_GT: GT -> type(GT); +StrExpr_LT: LT -> type(LT); +StrExpr_EQ: EQ -> type(EQ); +StrExpr_NOEQ: NOEQ -> type(NOEQ); +StrExpr_RIGHSHIFT_ASSGIN: RIGHSHIFT_ASSGIN -> type(RIGHSHIFT_ASSGIN); +StrExpr_RIGHSHIFT: RIGHSHIFT -> type(RIGHSHIFT); +StrExpr_OPTIONAL_CHAINING: OPTIONAL_CHAINING -> type(OPTIONAL_CHAINING); +StrExpr_SPREAD_CHAINING: SPREAD_CHAINING -> type(SPREAD_CHAINING); +StrExpr_URSHIFT_ASSGIN: URSHIFT_ASSGIN -> type(URSHIFT_ASSGIN); +StrExpr_URSHIFT: URSHIFT -> type(URSHIFT); +StrExpr_LSHIFT_ASSGIN: LSHIFT_ASSGIN -> type(LSHIFT_ASSGIN); +StrExpr_LEFTSHIFT: LEFTSHIFT -> type(LEFTSHIFT); +StrExpr_GE: GE -> type(GE); +StrExpr_LE: LE -> type(LE); +StrExpr_DOTMUL: DOTMUL -> type(DOTMUL); +StrExpr_CARET: CARET -> type(CARET); +StrExpr_ADD_ASSIGN: ADD_ASSIGN -> type(ADD_ASSIGN); +StrExpr_SUB_ASSIGN: SUB_ASSIGN -> type(SUB_ASSIGN); +StrExpr_AND_ASSIGN: AND_ASSIGN -> type(AND_ASSIGN); +StrExpr_OR_ASSIGN: OR_ASSIGN -> type(OR_ASSIGN); +StrExpr_MUL_ASSIGN: MUL_ASSIGN -> type(MUL_ASSIGN); +StrExpr_MOD_ASSIGN: MOD_ASSIGN -> type(MOD_ASSIGN); +StrExpr_DIV_ASSIGN: DIV_ASSIGN -> type(DIV_ASSIGN); +StrExpr_XOR_ASSIGN: XOR_ASSIGN -> type(XOR_ASSIGN); + +StrExpr_BANG: BANG -> type(BANG); +StrExpr_TILDE: TILDE -> type(TILDE); + +StrExpr_ADD: ADD -> type(ADD); +StrExpr_SUB: SUB -> type(SUB); +StrExpr_MUL: MUL -> type(MUL); +StrExpr_DIV: DIV -> type(DIV); +StrExpr_BIT_AND: BIT_AND -> type(BIT_AND); +StrExpr_BIT_OR: BIT_OR -> type(BIT_OR); +StrExpr_MOD: MOD -> type(MOD); + +StrExpr_INC: INC -> type(INC); +StrExpr_DEC: DEC -> type(DEC); + +StrExpr_NEWLINE: NEWLINE -> type(NEWLINE); + +StrExpr_WS: WS -> skip; + +StrExpr_COMMENT: COMMENT -> type(COMMENT); + +StrExpr_LINE_COMMENT: LINE_COMMENT -> type(LINE_COMMENT); + +StrExpr_OPID: OPID -> type(OPID); + +StrExpr_ID: ID -> type(ID); + +StrExpr_DOUBLE_QUOTE: DOUBLE_QUOTE -> type(DOUBLE_QUOTE), pushMode(DynamicString); +StrExpr_SELECTOR_START: SELECTOR_START -> type(SELECTOR_START); \ No newline at end of file diff --git a/src/main/antlr4/QLexer.tokens b/src/main/antlr4/QLexer.tokens new file mode 100644 index 0000000..22b8048 --- /dev/null +++ b/src/main/antlr4/QLexer.tokens @@ -0,0 +1,180 @@ +FOR=1 +IF=2 +ELSE=3 +WHILE=4 +BREAK=5 +CONTINUE=6 +RETURN=7 +FUNCTION=8 +MACRO=9 +IMPORT=10 +STATIC=11 +NEW=12 +BYTE=13 +SHORT=14 +INT=15 +LONG=16 +FLOAT=17 +DOUBLE=18 +CHAR=19 +BOOL=20 +NULL=21 +TRUE=22 +FALSE=23 +EXTENDS=24 +SUPER=25 +TRY=26 +CATCH=27 +FINALLY=28 +THROW=29 +THEN=30 +CLASS=31 +THIS=32 +QuoteStringLiteral=33 +IntegerLiteral=34 +FloatingPointLiteral=35 +IntegerOrFloatingLiteral=36 +LPAREN=37 +RPAREN=38 +LBRACE=39 +RBRACE=40 +LBRACK=41 +RBRACK=42 +DOT=43 +ARROW=44 +SEMI=45 +COMMA=46 +QUESTION=47 +COLON=48 +DCOLON=49 +GT=50 +LT=51 +EQ=52 +NOEQ=53 +RIGHSHIFT_ASSGIN=54 +RIGHSHIFT=55 +OPTIONAL_CHAINING=56 +SPREAD_CHAINING=57 +URSHIFT_ASSGIN=58 +URSHIFT=59 +LSHIFT_ASSGIN=60 +LEFTSHIFT=61 +GE=62 +LE=63 +DOTMUL=64 +CARET=65 +ADD_ASSIGN=66 +SUB_ASSIGN=67 +AND_ASSIGN=68 +OR_ASSIGN=69 +MUL_ASSIGN=70 +MOD_ASSIGN=71 +DIV_ASSIGN=72 +XOR_ASSIGN=73 +BANG=74 +TILDE=75 +ADD=76 +SUB=77 +MUL=78 +DIV=79 +BIT_AND=80 +BIT_OR=81 +MOD=82 +INC=83 +DEC=84 +NEWLINE=85 +WS=86 +COMMENT=87 +LINE_COMMENT=88 +OPID=89 +SELECTOR_START=90 +ID=91 +DOUBLE_QUOTE=92 +CATCH_ALL=93 +StaticStringCharacters=94 +DyStrExprStart=95 +DyStrText=96 +SelectorVariable_VANME=97 +StrExpr_WS=98 +'for'=1 +'if'=2 +'else'=3 +'while'=4 +'break'=5 +'continue'=6 +'return'=7 +'function'=8 +'macro'=9 +'import'=10 +'static'=11 +'new'=12 +'byte'=13 +'short'=14 +'int'=15 +'long'=16 +'float'=17 +'double'=18 +'char'=19 +'boolean'=20 +'null'=21 +'true'=22 +'false'=23 +'extends'=24 +'super'=25 +'try'=26 +'catch'=27 +'finally'=28 +'throw'=29 +'then'=30 +'class'=31 +'this'=32 +'('=37 +')'=38 +'{'=39 +'}'=40 +'['=41 +']'=42 +'.'=43 +'->'=44 +';'=45 +','=46 +'?'=47 +':'=48 +'::'=49 +'>'=50 +'<'=51 +'='=52 +'<>'=53 +'>>='=54 +'>>'=55 +'?.'=56 +'*.'=57 +'>>>='=58 +'>>>'=59 +'<<='=60 +'<<'=61 +'>='=62 +'<='=63 +'.*'=64 +'^'=65 +'+='=66 +'-='=67 +'&='=68 +'|='=69 +'*='=70 +'%='=71 +'/='=72 +'^='=73 +'!'=74 +'~'=75 +'+'=76 +'-'=77 +'*'=78 +'/'=79 +'&'=80 +'|'=81 +'%'=82 +'++'=83 +'--'=84 +'"'=92 +'${'=95 diff --git a/src/main/java/com/alibaba/qlexpress4/CheckOptions.java b/src/main/java/com/alibaba/qlexpress4/CheckOptions.java new file mode 100644 index 0000000..ea876d2 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/CheckOptions.java @@ -0,0 +1,69 @@ +package com.alibaba.qlexpress4; + +import com.alibaba.qlexpress4.operator.OperatorCheckStrategy; + +/** + * Script validation configuration class + * Used to configure restriction rules during script validation + * + * @author QLExpress Team + */ +public class CheckOptions { + + /** + * Operator check strategy for script validation + * default OperatorCheckStrategy.allowAll() + */ + private final OperatorCheckStrategy operatorCheckStrategy; + + /** + * Whether to disable function calls in the script + * default false + */ + private final boolean disableFunctionCalls; + + /** + * Default validation options + */ + public static final CheckOptions DEFAULT_OPTIONS = CheckOptions.builder().build(); + + private CheckOptions(OperatorCheckStrategy operatorCheckStrategy, boolean disableFunctionCalls) { + this.operatorCheckStrategy = operatorCheckStrategy; + this.disableFunctionCalls = disableFunctionCalls; + } + + public OperatorCheckStrategy getCheckStrategy() { + return operatorCheckStrategy; + } + + public boolean isDisableFunctionCalls() { + return disableFunctionCalls; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private OperatorCheckStrategy operatorCheckStrategy = OperatorCheckStrategy.allowAll(); + + private boolean disableFunctionCalls = false; + + private Builder() { + } + + public Builder operatorCheckStrategy(OperatorCheckStrategy operatorCheckStrategy) { + this.operatorCheckStrategy = operatorCheckStrategy; + return this; + } + + public Builder disableFunctionCalls(boolean disableFunctionCalls) { + this.disableFunctionCalls = disableFunctionCalls; + return this; + } + + public CheckOptions build() { + return new CheckOptions(operatorCheckStrategy, disableFunctionCalls); + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/ClassSupplier.java b/src/main/java/com/alibaba/qlexpress4/ClassSupplier.java new file mode 100644 index 0000000..02c1810 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/ClassSupplier.java @@ -0,0 +1,11 @@ +package com.alibaba.qlexpress4; + +/** + * Author: DQinYuan + */ +@FunctionalInterface +public interface ClassSupplier { + + Class loadCls(String clsQualifiedName); + +} \ No newline at end of file diff --git a/src/main/java/com/alibaba/qlexpress4/DefaultClassSupplier.java b/src/main/java/com/alibaba/qlexpress4/DefaultClassSupplier.java new file mode 100644 index 0000000..ce8b4aa --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/DefaultClassSupplier.java @@ -0,0 +1,39 @@ +package com.alibaba.qlexpress4; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Author: DQinYuan + */ +public class DefaultClassSupplier implements ClassSupplier { + + private static final DefaultClassSupplier INSTANCE = new DefaultClassSupplier(); + + public static DefaultClassSupplier getInstance() { + return INSTANCE; + } + + private final Map>> cache = new ConcurrentHashMap<>(); + + /** + * @param clsQualifiedName qualified name of class + * @return loaded class + */ + @Override + public Class loadCls(String clsQualifiedName) { + Optional> clsOp = cache.computeIfAbsent(clsQualifiedName, this::loadClsInner); + return clsOp.orElse(null); + } + + private Optional> loadClsInner(String clsQualifiedName) { + try { + Class aClass = Class.forName(clsQualifiedName); + return Optional.of(aClass); + } + catch (ClassNotFoundException | NoClassDefFoundError e) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/Express4Runner.java b/src/main/java/com/alibaba/qlexpress4/Express4Runner.java new file mode 100644 index 0000000..3c8839c --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/Express4Runner.java @@ -0,0 +1,751 @@ +package com.alibaba.qlexpress4; + +import com.alibaba.qlexpress4.aparser.CheckVisitor; +import com.alibaba.qlexpress4.aparser.GeneratorScope; +import com.alibaba.qlexpress4.aparser.ImportManager; +import com.alibaba.qlexpress4.aparser.MacroDefine; +import com.alibaba.qlexpress4.aparser.OutFunctionVisitor; +import com.alibaba.qlexpress4.aparser.OutVarAttrsVisitor; +import com.alibaba.qlexpress4.aparser.OutVarNamesVisitor; +import com.alibaba.qlexpress4.aparser.QCompileCache; +import com.alibaba.qlexpress4.aparser.QLParser; +import com.alibaba.qlexpress4.aparser.QvmInstructionVisitor; +import com.alibaba.qlexpress4.aparser.SyntaxTreeFactory; +import com.alibaba.qlexpress4.aparser.TraceExpressionVisitor; +import com.alibaba.qlexpress4.aparser.compiletimefunction.CompileTimeFunction; +import com.alibaba.qlexpress4.api.BatchAddFunctionResult; +import com.alibaba.qlexpress4.api.QLFunctionalVarargs; +import com.alibaba.qlexpress4.exception.PureErrReporter; +import com.alibaba.qlexpress4.exception.QLException; +import com.alibaba.qlexpress4.exception.QLSyntaxException; +import com.alibaba.qlexpress4.runtime.DelegateQContext; +import com.alibaba.qlexpress4.runtime.QLambda; +import com.alibaba.qlexpress4.runtime.QLambdaDefinitionInner; +import com.alibaba.qlexpress4.runtime.QLambdaTrace; +import com.alibaba.qlexpress4.runtime.QvmGlobalScope; +import com.alibaba.qlexpress4.runtime.QvmRuntime; +import com.alibaba.qlexpress4.runtime.ReflectLoader; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.context.ExpressContext; +import com.alibaba.qlexpress4.runtime.context.MapExpressContext; +import com.alibaba.qlexpress4.runtime.context.ObjectFieldExpressContext; +import com.alibaba.qlexpress4.runtime.context.QLAliasContext; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; +import com.alibaba.qlexpress4.runtime.function.ExtensionFunction; +import com.alibaba.qlexpress4.runtime.function.QMethodFunction; +import com.alibaba.qlexpress4.runtime.instruction.QLInstruction; +import com.alibaba.qlexpress4.runtime.operator.CustomBinaryOperator; +import com.alibaba.qlexpress4.runtime.operator.OperatorManager; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.runtime.trace.QTraces; +import com.alibaba.qlexpress4.runtime.trace.TracePointTree; +import com.alibaba.qlexpress4.utils.BasicUtil; +import com.alibaba.qlexpress4.utils.QLFunctionUtil; +import org.antlr.v4.runtime.dfa.DFA; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Author: DQinYuan + */ +public class Express4Runner { + private final OperatorManager operatorManager = new OperatorManager(); + + private final Map> compileCache = new ConcurrentHashMap<>(); + + private final Map userDefineFunction = new ConcurrentHashMap<>(); + + private final Map compileTimeFunctions = new ConcurrentHashMap<>(); + + private final GeneratorScope globalScope = new GeneratorScope(null, "global", new ConcurrentHashMap<>()); + + private final ReflectLoader reflectLoader; + + private final InitOptions initOptions; + + public Express4Runner(InitOptions initOptions) { + this.initOptions = initOptions; + this.reflectLoader = new ReflectLoader(initOptions.getSecurityStrategy(), initOptions.isAllowPrivateAccess()); + SyntaxTreeFactory.warmUp(); + } + + public CustomFunction getFunction(String functionName) { + return userDefineFunction.get(functionName); + } + + public CompileTimeFunction getCompileTimeFunction(String functionName) { + return compileTimeFunctions.get(functionName); + } + + /** + * Execute the script with variables set in the context; the map key corresponds to the + * variable name referenced in the script. + * + * @param script the script content to execute + * @param context variables for execution, keyed by variable name + * @param qlOptions execution options (e.g. interpolation, debug) + * @return result of script execution and related traces + * @throws QLException if a script or runtime error occurs + */ + public QLResult execute(String script, Map context, QLOptions qlOptions) + throws QLException { + return execute(script, new MapExpressContext(context), qlOptions); + } + + /** + * Execute a template string by wrapping it as a dynamic string literal. + * Template does not support newlines in this mode. + * + * @param template the template text to evaluate as a dynamic string + * @param context variables available to the template + * @param qlOptions execution options + * @return result of template evaluation + * @throws QLException if compilation or execution fails + */ + public QLResult executeTemplate(String template, Map context, QLOptions qlOptions) + throws QLException { + String script = wrapAsDynamicString(template); + return execute(script, context, qlOptions); + } + + private String wrapAsDynamicString(String template) { + if (template == null) { + return "\"\""; + } + String escaped = template.replace("\"", "\\\""); + return "\"" + escaped + "\""; + } + + /** + * Execute the script with variables resolved from the fields of the given context object. + * The variable name in the script corresponds to the field name on the object. + * + * @param script the script content to execute + * @param context the object whose public fields/properties are exposed as variables + * @param qlOptions execution options + * @return result of script execution + * @throws QLException if a script or runtime error occurs + */ + public QLResult execute(String script, Object context, QLOptions qlOptions) + throws QLException { + return execute(script, new ObjectFieldExpressContext(context, this), qlOptions); + } + + /** + * Execute the script using objects annotated with {@code @QLAlias}. + * The {@code QLAlias.value} serves as the variable name for each object. + * Objects without the annotation are ignored. + * + * @param script the script content to execute + * @param qlOptions execution options + * @param objects objects annotated with {@code @QLAlias} + * @return result of script execution + * @throws QLException if a script or runtime error occurs + */ + public QLResult executeWithAliasObjects(String script, QLOptions qlOptions, Object... objects) { + return execute(script, new QLAliasContext(objects), qlOptions); + } + + public QLResult execute(String script, ExpressContext context, QLOptions qlOptions) { + QLambdaTrace mainLambdaTrace; + if (initOptions.isDebug()) { + long start = System.currentTimeMillis(); + mainLambdaTrace = parseToLambda(script, context, qlOptions); + initOptions.getDebugInfoConsumer() + .accept("Compile consume time: " + (System.currentTimeMillis() - start) + " ms"); + } + else { + mainLambdaTrace = parseToLambda(script, context, qlOptions); + } + QLambda mainLambda = mainLambdaTrace.getqLambda(); + try { + Object result; + if (initOptions.isDebug()) { + long start = System.currentTimeMillis(); + result = mainLambda.call().getResult().get(); + initOptions.getDebugInfoConsumer() + .accept("Execute consume time: " + (System.currentTimeMillis() - start) + " ms"); + } + else { + result = mainLambda.call().getResult().get(); + } + + return new QLResult(result, mainLambdaTrace.getTraces().getExpressionTraces()); + } + catch (QLException e) { + throw e; + } + catch (Throwable nuKnown) { + // should not run here + throw new RuntimeException(nuKnown); + } + } + + private QTraces convertPoints2QTraces(List expressionTracePoints) { + Map traceMap = new HashMap<>(); + List expressionTraces = expressionTracePoints.stream() + .map(tracePoint -> convertPoint2Trace(tracePoint, traceMap)) + .collect(Collectors.toList()); + return new QTraces(expressionTraces, traceMap); + } + + private ExpressionTrace convertPoint2Trace(TracePointTree tree, Map traceMap) { + if (tree.getChildren().isEmpty()) { + ExpressionTrace result = new ExpressionTrace(tree.getType(), tree.getToken(), Collections.emptyList(), + tree.getLine(), tree.getCol(), tree.getPosition()); + traceMap.put(result.getPosition(), result); + return result; + } + List mergedChildren = + tree.getChildren().stream().map(child -> convertPoint2Trace(child, traceMap)).collect(Collectors.toList()); + ExpressionTrace result = new ExpressionTrace(tree.getType(), tree.getToken(), mergedChildren, tree.getLine(), + tree.getCol(), tree.getPosition()); + traceMap.put(result.getPosition(), result); + return result; + } + + /** + * Get external variables (those that must be provided via context) referenced by the script. + * + * @param script the script content + * @return names of external variables referenced in the script + */ + public Set getOutVarNames(String script) { + QLParser.ProgramContext programContext = parseToSyntaxTree(script); + OutVarNamesVisitor outVarNamesVisitor = new OutVarNamesVisitor(inheritDefaultImport()); + programContext.accept(outVarNamesVisitor); + return outVarNamesVisitor.getOutVars(); + } + + /** + * Get external variable attribute access paths referenced by the script. + * + * @param script the script content + * @return attribute chains accessed on external variables + */ + public Set> getOutVarAttrs(String script) { + QLParser.ProgramContext programContext = parseToSyntaxTree(script); + OutVarAttrsVisitor outVarAttrsVisitor = new OutVarAttrsVisitor(inheritDefaultImport()); + programContext.accept(outVarAttrsVisitor); + return outVarAttrsVisitor.getOutVarAttrs(); + } + + /** + * Get external functions (those that must be provided via context) referenced by the script. + * + * @param script the script content + * @return names of external functions referenced in the script + */ + public Set getOutFunctions(String script) { + QLParser.ProgramContext programContext = parseToSyntaxTree(script); + OutFunctionVisitor outFunctionVisitor = new OutFunctionVisitor(); + programContext.accept(outFunctionVisitor); + return outFunctionVisitor.getOutFunctions(); + } + + /** + * Get the expression trace trees for the script without executing it. + * + * @param script the script content + * @return trace trees for each expression + */ + public List getExpressionTracePoints(String script) { + QLParser.ProgramContext programContext = parseToSyntaxTree(script); + TraceExpressionVisitor traceExpressionVisitor = new TraceExpressionVisitor(); + programContext.accept(traceExpressionVisitor); + return traceExpressionVisitor.getExpressionTracePoints(); + } + + /** + * add user defined global macro to QLExpress engine + * @param name macro name + * @param macroScript script for macro + * @return true if add macro successfully. fail if macro name already exists. + */ + public boolean addMacro(String name, String macroScript) { + return globalScope.defineMacroIfAbsent(name, parseMacroDefine(name, macroScript)); + } + + /** + * add or replace user defined global macro to QLExpress engine + * @param name macro name + * @param macroScript script for macro + */ + public void addOrReplaceMacro(String name, String macroScript) { + globalScope.defineMacro(name, parseMacroDefine(name, macroScript)); + } + + private MacroDefine parseMacroDefine(String name, String macroScript) { + QLParser.ProgramContext macroProgram = parseToSyntaxTree(macroScript); + QvmInstructionVisitor macroVisitor = new QvmInstructionVisitor(macroScript, inheritDefaultImport(), + new GeneratorScope("MACRO_" + name, globalScope), operatorManager, QvmInstructionVisitor.Context.MACRO, + compileTimeFunctions, initOptions); + macroProgram.accept(macroVisitor); + List macroInstructions = macroVisitor.getInstructions(); + List blockStatementContexts = macroProgram.blockStatements().blockStatement(); + boolean lastStmtExpress = !blockStatementContexts.isEmpty() && blockStatementContexts + .get(blockStatementContexts.size() - 1) instanceof QLParser.ExpressionStatementContext; + return new MacroDefine(macroInstructions, lastStmtExpress); + } + + /** + * add user defined function to QLExpress engine + * @param name function name + * @param function function definition + * @return true if add function successfully. fail if function name already exists. + */ + public boolean addFunction(String name, CustomFunction function) { + CustomFunction preFunction = userDefineFunction.putIfAbsent(name, function); + return preFunction == null; + } + + public boolean addFunction(String name, Function function) { + return addFunction(name, (qContext, parameters) -> { + T t = parameters.size() > 0 ? (T)parameters.get(0).get() : null; + return function.apply(t); + }); + } + + public boolean addVarArgsFunction(String name, QLFunctionalVarargs functionalVarargs) { + return addFunction(name, (qContext, parameters) -> { + Object[] paramArr = new Object[parameters.size()]; + for (int i = 0; i < paramArr.length; i++) { + paramArr[i] = parameters.get(i).get(); + } + return functionalVarargs.call(paramArr); + }); + } + + public boolean addFunction(String name, Predicate predicate) { + return addFunction(name, (qContext, parameters) -> { + T t = parameters.size() > 0 ? (T)parameters.get(0).get() : null; + return predicate.test(t); + }); + } + + public boolean addFunction(String name, Runnable runnable) { + return addFunction(name, (qContext, parameters) -> { + runnable.run(); + return null; + }); + } + + public boolean addFunction(String name, Consumer consumer) { + return addFunction(name, (qContext, parameters) -> { + T t = parameters.size() > 0 ? (T)parameters.get(0).get() : null; + consumer.accept(t); + return null; + }); + } + + /** + * Add a user-defined function backed by a specific Java service instance method. + * + * @param name function name exposed in QLExpress scripts + * @param serviceObject target service instance, must not be {@code null} + * @param methodName Java method name on the service instance + * @param parameterClassTypes parameter type signature of the Java method; use an empty array for no-arg methods + * @return true if the function was added successfully; false if a function with the same name already exists + * @throws IllegalArgumentException if {@code serviceObject} or {@code methodName} is null, or if no matching + * public method is found on the service type + */ + public boolean addFunctionOfServiceMethod(String name, Object serviceObject, String methodName, + Class[] parameterClassTypes) { + if (serviceObject == null) { + throw new IllegalArgumentException("serviceObject must not be null"); + } + if (methodName == null) { + throw new IllegalArgumentException("methodName must not be null"); + } + + Class[] parameterTypes = parameterClassTypes == null ? new Class[0] : parameterClassTypes; + Method method; + try { + method = serviceObject.getClass().getMethod(methodName, parameterTypes); + } + catch (NoSuchMethodException e) { + throw new IllegalArgumentException("No such public method '" + methodName + "' with parameter types " + + java.util.Arrays.toString(parameterTypes) + " on service object class '" + + serviceObject.getClass().getName() + "'", e); + } + + return addFunction(name, new QMethodFunction(serviceObject, method)); + } + + /** + * execute `scriptWithFunctionDefine` and add functions defined in script + * @param scriptWithFunctionDefine script with function define + * @param context context when execute script + * @param qlOptions qlOptions when execute script + * @return succ and fail functions. fail if function name already exists + */ + public BatchAddFunctionResult addFunctionsDefinedInScript(String scriptWithFunctionDefine, ExpressContext context, + QLOptions qlOptions) { + BatchAddFunctionResult batchResult = new BatchAddFunctionResult(); + QLambdaTrace mainLambdaTrace = parseToLambda(scriptWithFunctionDefine, context, qlOptions); + try { + Map functionTableInScript = mainLambdaTrace.getqLambda().getFunctionDefined(); + for (Map.Entry entry : functionTableInScript.entrySet()) { + boolean addResult = addFunction(entry.getKey(), entry.getValue()); + (addResult ? batchResult.getSucc() : batchResult.getFail()).add(entry.getKey()); + } + return batchResult; + } + catch (QLException e) { + throw e; + } + catch (Throwable e) { + // should not run here + throw new RuntimeException(e); + } + } + + /** + * add object member method with annotation {@link com.alibaba.qlexpress4.annotation.QLFunction} as function + * @param object object with member method with annotation {@link com.alibaba.qlexpress4.annotation.QLFunction} + * @return succ and fail functions. fail if function name already exists or method is not public + */ + public BatchAddFunctionResult addObjFunction(Object object) { + return addFunctionByAnnotation(object.getClass(), object); + } + + /** + * add class static method with annotation {@link com.alibaba.qlexpress4.annotation.QLFunction} as function + * @param clazz class with static method with annotation {@link com.alibaba.qlexpress4.annotation.QLFunction} + * @return succ and fail functions. fail if function name already exists or method is not public + */ + public BatchAddFunctionResult addStaticFunction(Class clazz) { + return addFunctionByAnnotation(clazz, null); + } + + private BatchAddFunctionResult addFunctionByAnnotation(Class clazz, Object object) { + BatchAddFunctionResult result = new BatchAddFunctionResult(); + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (!BasicUtil.isPublic(method)) { + result.getFail().add(method.getName()); + continue; + } + if (QLFunctionUtil.containsQLFunctionForMethod(method)) { + for (String functionName : QLFunctionUtil.getQLFunctionValue(method)) { + boolean addResult = addFunction(functionName, new QMethodFunction(object, method)); + (addResult ? result.getSucc() : result.getFail()).add(method.getName()); + } + } + } + return result; + } + + /** + * add compile time function + * @param name function name + * @param compileTimeFunction definition + * @return true if successful + */ + public boolean addCompileTimeFunction(String name, CompileTimeFunction compileTimeFunction) { + return compileTimeFunctions.putIfAbsent(name, compileTimeFunction) == null; + } + + /** + * add extension function + * @param extensionFunction definition of extansion function + */ + public void addExtendFunction(ExtensionFunction extensionFunction) { + this.reflectLoader.addExtendFunction(extensionFunction); + } + + /** + * add an extension function with variable arguments. + * @param name the name of the extension function + * @param bindingClass the receiver type (class) + * @param functionalVarargs custom logic + */ + public void addExtendFunction(String name, Class bindingClass, QLFunctionalVarargs functionalVarargs) { + this.reflectLoader.addExtendFunction(new ExtensionFunction() { + @Override + public Class[] getParameterTypes() { + return new Class[] {Object[].class}; + } + + @Override + public boolean isVarArgs() { + return true; + } + + @Override + public String getName() { + return name; + } + + @Override + public Class getDeclaringClass() { + return bindingClass; + } + + @Override + public Object invoke(Object obj, Object[] args) + throws InvocationTargetException, IllegalAccessException { + Object[] varArgs = (Object[])args[0]; + Object[] extArgs = new Object[varArgs.length + 1]; + extArgs[0] = obj; + System.arraycopy(varArgs, 0, extArgs, 1, varArgs.length); + return functionalVarargs.call(extArgs); + } + }); + } + + public QLParser.ProgramContext parseToSyntaxTree(String script) { + return SyntaxTreeFactory.buildTree(script, + operatorManager, + initOptions.isDebug(), + false, + initOptions.getDebugInfoConsumer(), + initOptions.getInterpolationMode(), + initOptions.getSelectorStart(), + initOptions.getSelectorEnd(), + initOptions.isStrictNewLines()); + } + + public void check(String script, CheckOptions checkOptions) + throws QLSyntaxException { + // 1. Parse syntax tree (reuse existing parseToSyntaxTree logic) + QLParser.ProgramContext programContext = parseToSyntaxTree(script); + + // 2. Create CheckVisitor and pass validation configuration and script content + CheckVisitor checkVisitor = new CheckVisitor(checkOptions, script); + + // 3. Traverse syntax tree and perform operator validation during traversal + programContext.accept(checkVisitor); + } + + public void check(String script) + throws QLSyntaxException { + check(script, CheckOptions.DEFAULT_OPTIONS); + } + + public QLambdaTrace parseToLambda(String script, ExpressContext context, QLOptions qlOptions) { + QCompileCache mainLambdaDefine = + qlOptions.isCache() ? parseToDefinitionWithCache(script) : parseDefinition(script); + if (initOptions.isDebug()) { + initOptions.getDebugInfoConsumer().accept("\nInstructions:"); + mainLambdaDefine.getQLambdaDefinition().println(0, initOptions.getDebugInfoConsumer()); + } + + QTraces qTraces = initOptions.isTraceExpression() && qlOptions.isTraceExpression() + ? convertPoints2QTraces(mainLambdaDefine.getExpressionTracePoints()) + : new QTraces(null, null); + + QvmRuntime qvmRuntime = + new QvmRuntime(qTraces, qlOptions.getAttachments(), reflectLoader, System.currentTimeMillis()); + QvmGlobalScope globalScope = new QvmGlobalScope(context, userDefineFunction, qlOptions); + QLambda qLambda = mainLambdaDefine.getQLambdaDefinition() + .toLambda(new DelegateQContext(qvmRuntime, globalScope), qlOptions, true); + return new QLambdaTrace(qLambda, qTraces); + } + + /** + * parse script with cache + * @param script script to parse + * @return QLambdaDefinition and TracePointTrees + */ + public QCompileCache parseToDefinitionWithCache(String script) { + try { + return getParseFuture(script).get(); + } + catch (Exception e) { + Throwable compileException = e.getCause(); + throw compileException instanceof QLSyntaxException ? (QLSyntaxException)compileException + : new RuntimeException(compileException); + } + } + + public Value loadField(Object object, String fieldName) { + return reflectLoader.loadField(object, fieldName, true, PureErrReporter.INSTANCE); + } + + private static final String PARSER_CLASS_NAME = "com.alibaba.qlexpress4.aparser.QLParser"; + + private static final String DFA_FIELD_NAME = "_decisionToDFA"; + + /** + * Clears the DFA (Deterministic Finite Automaton) cache used by the ANTLR parser. + *

+ * WARNING: Calling this method will cause a significant compilation performance degradation. + * It is NOT recommended for normal use cases. + *

+ * + *

Use Cases:

+ *
    + *
  • Memory-sensitive applications: When memory usage is a critical concern and you can + * tolerate slower compilation times
  • + *
  • Infrequently changing scripts: When scripts are relatively stable and not frequently + * recompiled
  • + *
+ * + *

Best Practice:

+ *

+ * Call this method immediately after parsing and caching your expression, and ensure all subsequent + * executions use the cached version to avoid recompilation. Example: + *

+ *
{@code
+     * /*
+     *  * When the expression changes, parse it and add it to the expression cache;
+     *  * after parsing is complete, call clearDFACache.
+     *  *\/
+     * Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
+     * runner.parseToDefinitionWithCache(complexDataProcessingExpress);
+     * runner.clearDFACache();
+     *
+     * /*
+     *  * All subsequent runs of this script must enable the cache option to ensure that re-compilation does not occur.
+     *  *\/
+     * for (int i = 0; i < 3; i++) {
+     *     runner.execute(complexDataProcessingExpress, Collections.emptyMap(), QLOptions
+     *             .builder().cache(true).build());
+     * }
+     * }
+ */ + public void clearDFACache() { + DFA[] decisionToDFA = getDecisionToDFA(); + + for (int d = 0; d < decisionToDFA.length; d++) { + decisionToDFA[d] = new DFA(QLParser._ATN.getDecisionState(d), d); + } + } + + private DFA[] getDecisionToDFA() { + try { + Class parserClass = Class.forName(PARSER_CLASS_NAME); + Field dfaField = parserClass.getDeclaredField(DFA_FIELD_NAME); + dfaField.setAccessible(true); + return (DFA[])dfaField.get(null); + } + catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * Clear the compilation cache. + * This method clears the cache that stores compiled scripts for performance optimization. + * When the cache is cleared, subsequent script executions will need to recompile the scripts, + * which may temporarily impact performance until the cache is rebuilt. + */ + public void clearCompileCache() { + compileCache.clear(); + } + + private Future getParseFuture(String script) { + Future parseFuture = compileCache.get(script); + if (parseFuture != null) { + return parseFuture; + } + FutureTask parseTask = new FutureTask<>(() -> parseDefinition(script)); + Future preTask = compileCache.putIfAbsent(script, parseTask); + if (preTask == null) { + parseTask.run(); + return parseTask; + } + return preTask; + } + + private QCompileCache parseDefinition(String script) { + QLParser.ProgramContext program = parseToSyntaxTree(script); + QvmInstructionVisitor qvmInstructionVisitor = new QvmInstructionVisitor(script, inheritDefaultImport(), + globalScope, operatorManager, compileTimeFunctions, initOptions); + program.accept(qvmInstructionVisitor); + + QLambdaDefinitionInner qLambdaDefinition = new QLambdaDefinitionInner("main", + qvmInstructionVisitor.getInstructions(), Collections.emptyList(), qvmInstructionVisitor.getMaxStackSize()); + if (initOptions.isTraceExpression()) { + TraceExpressionVisitor traceExpressionVisitor = new TraceExpressionVisitor(); + program.accept(traceExpressionVisitor); + List tracePoints = traceExpressionVisitor.getExpressionTracePoints(); + return new QCompileCache(qLambdaDefinition, tracePoints); + } + else { + return new QCompileCache(qLambdaDefinition, Collections.emptyList()); + } + } + + private ImportManager inheritDefaultImport() { + return new ImportManager(initOptions.getClassSupplier(), initOptions.getDefaultImport()); + } + + public boolean addOperatorBiFunction(String operator, BiFunction biFunction) { + return operatorManager.addBinaryOperator(operator, + (left, right) -> biFunction.apply((T)left.get(), (U)right.get()), + QLPrecedences.MULTI); + } + + public boolean addOperator(String operator, QLFunctionalVarargs functionalVarargs) { + return addOperator(operator, (left, right) -> functionalVarargs.call(left.get(), right.get())); + } + + /** + * add operator with multi precedences + * @param operator operator name + * @param customBinaryOperator operator implement + * @return true if add operator successfully; false if operator already exist + */ + public boolean addOperator(String operator, CustomBinaryOperator customBinaryOperator) { + return operatorManager.addBinaryOperator(operator, customBinaryOperator, QLPrecedences.MULTI); + } + + /** + * add operator + * @param operator operator name + * @param customBinaryOperator operator implement + * @param precedence precedence, see {@link QLPrecedences} + * @return true if add operator successfully; false if operator already exist + */ + public boolean addOperator(String operator, CustomBinaryOperator customBinaryOperator, int precedence) { + return operatorManager.addBinaryOperator(operator, customBinaryOperator, precedence); + } + + /** + * @param operator operator name + * @param customBinaryOperator operator implement + * @return true if replace operator successfully; false if default operator not exists + */ + public boolean replaceDefaultOperator(String operator, CustomBinaryOperator customBinaryOperator) { + return operatorManager.replaceDefaultOperator(operator, customBinaryOperator); + } + + /** + * add alias for keyWord, operator and function + * @param alias must be a valid id + * @param originToken key word in qlexpress + * @return true if add alias successfully + */ + public boolean addAlias(String alias, String originToken) { + boolean addKeyWordAliasResult = operatorManager.addKeyWordAlias(alias, originToken); + boolean addOperatorAliasResult = operatorManager.addOperatorAlias(alias, originToken); + boolean addFunctionAliasResult = addFunctionAlias(alias, originToken); + + return addKeyWordAliasResult || addOperatorAliasResult || addFunctionAliasResult; + } + + private boolean addFunctionAlias(String alias, String originToken) { + CustomFunction customFunction = userDefineFunction.get(originToken); + if (customFunction != null) { + return userDefineFunction.putIfAbsent(alias, customFunction) == null; + } + return false; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/InitOptions.java b/src/main/java/com/alibaba/qlexpress4/InitOptions.java new file mode 100644 index 0000000..944a20a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/InitOptions.java @@ -0,0 +1,245 @@ +package com.alibaba.qlexpress4; + +import com.alibaba.qlexpress4.aparser.ImportManager; +import com.alibaba.qlexpress4.aparser.InterpolationMode; +import com.alibaba.qlexpress4.security.QLSecurityStrategy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +/** + * Author: TaoKan + */ +public class InitOptions { + + public static InitOptions DEFAULT_OPTIONS = InitOptions.builder().build(); + + private final ClassSupplier classSupplier; + + /** + * default import java packages for script + * default + * ImportManager.importPack("java.lang"), + * ImportManager.importPack("java.util"), + * ImportManager.importPack("java.math"), + * ImportManager.importPack("java.util.stream") + * ImportManager.importPack("java.util.function") + */ + private final List defaultImport; + + /** + * enable debug mode + * default false + */ + private final boolean debug; + + /** + * consume all debug info, valid when debug is true + * default is print in standard output, can not be null + */ + private final Consumer debugInfoConsumer; + + /** + * qlexpress security strategy + * default is isolation, not allow any access to java + */ + private final QLSecurityStrategy securityStrategy; + + /** + * allow access private field and method + * default false + */ + private final boolean allowPrivateAccess; + + /** + * How to manage string interpolation, for instance, "a ${t-c} b" + * default SCRIPT + */ + private final InterpolationMode interpolationMode; + + /** + * track the execution process of all expressions and return the path to the `execute` caller. + * default false + */ + private final boolean traceExpression; + + /** + * selector start token, must be one of: "${", "$[", "#{", or "#[". + * default is "${". + */ + private final String selectorStart; + + /** + * selector end token, must be 1 or more characters + * default is "}" + */ + private final String selectorEnd; + + /** + * Strictly require a line break between any two statements (since semicolons can be omitted in QLExpress4). + * default is true + */ + private final boolean strictNewLines; + + private InitOptions(ClassSupplier classSupplier, List defaultImport, boolean debug, + Consumer debugInfoConsumer, QLSecurityStrategy securityStrategy, boolean allowPrivateAccess, + InterpolationMode interpolationMode, boolean traceExpression, String selectorStart, String selectorEnd, + boolean strictNewLines) { + this.classSupplier = classSupplier; + this.defaultImport = defaultImport; + this.debug = debug; + this.debugInfoConsumer = debugInfoConsumer; + this.securityStrategy = securityStrategy; + this.allowPrivateAccess = allowPrivateAccess; + this.interpolationMode = interpolationMode; + this.traceExpression = traceExpression; + this.selectorStart = selectorStart; + this.selectorEnd = selectorEnd; + this.strictNewLines = strictNewLines; + } + + public static InitOptions.Builder builder() { + return new Builder(); + } + + public List getDefaultImport() { + return defaultImport; + } + + public ClassSupplier getClassSupplier() { + return classSupplier; + } + + public boolean isDebug() { + return debug; + } + + public Consumer getDebugInfoConsumer() { + return debugInfoConsumer; + } + + public QLSecurityStrategy getSecurityStrategy() { + return securityStrategy; + } + + public boolean isAllowPrivateAccess() { + return allowPrivateAccess; + } + + public InterpolationMode getInterpolationMode() { + return interpolationMode; + } + + public boolean isTraceExpression() { + return traceExpression; + } + + public String getSelectorStart() { + return selectorStart; + } + + public String getSelectorEnd() { + return selectorEnd; + } + + public boolean isStrictNewLines() { + return strictNewLines; + } + + public static class Builder { + private ClassSupplier classSupplier = DefaultClassSupplier.getInstance(); + + private final List defaultImport = + new ArrayList<>(Arrays.asList(ImportManager.importPack("java.lang"), + ImportManager.importPack("java.util"), + ImportManager.importPack("java.math"), + ImportManager.importPack("java.util.stream"), + ImportManager.importPack("java.util.function"))); + + private boolean debug = false; + + private Consumer debugInfoConsumer = System.out::println; + + private QLSecurityStrategy securityStrategy = QLSecurityStrategy.isolation(); + + private boolean allowPrivateAccess = false; + + private InterpolationMode interpolationMode = InterpolationMode.SCRIPT; + + private boolean traceExpression = false; + + private String selectorStart = "${"; + + private String selectorEnd = "}"; + + private boolean strictNewLines = true; + + public Builder classSupplier(ClassSupplier classSupplier) { + this.classSupplier = classSupplier; + return this; + } + + public Builder addDefaultImport(List defaultImport) { + this.defaultImport.addAll(defaultImport); + return this; + } + + public Builder debug(boolean debug) { + this.debug = debug; + return this; + } + + public Builder debugInfoConsumer(Consumer debugInfoConsumer) { + this.debugInfoConsumer = debugInfoConsumer; + return this; + } + + public Builder securityStrategy(QLSecurityStrategy securityStrategy) { + this.securityStrategy = securityStrategy; + return this; + } + + public Builder allowPrivateAccess(boolean allowPrivateAccess) { + this.allowPrivateAccess = allowPrivateAccess; + return this; + } + + public Builder interpolationMode(InterpolationMode interpolationMode) { + this.interpolationMode = interpolationMode; + return this; + } + + public Builder traceExpression(boolean traceExpression) { + this.traceExpression = traceExpression; + return this; + } + + public Builder selectorStart(String selectorStart) { + if (!Arrays.asList("${", "$[", "#{", "#[").contains(selectorStart)) { + throw new IllegalArgumentException("Custom selector start must in '${' | '$[' | '#{' | '#['"); + } + this.selectorStart = selectorStart; + return this; + } + + public Builder selectorEnd(String selectorEnd) { + if (selectorEnd == null || selectorEnd.isEmpty()) { + throw new IllegalArgumentException("Custom selector end must be 1 or more characters"); + } + this.selectorEnd = selectorEnd; + return this; + } + + public Builder strictNewLines(boolean strictNewLines) { + this.strictNewLines = strictNewLines; + return this; + } + + public InitOptions build() { + return new InitOptions(classSupplier, defaultImport, debug, debugInfoConsumer, securityStrategy, + allowPrivateAccess, interpolationMode, traceExpression, selectorStart, selectorEnd, strictNewLines); + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/QLOptions.java b/src/main/java/com/alibaba/qlexpress4/QLOptions.java new file mode 100644 index 0000000..845d00d --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/QLOptions.java @@ -0,0 +1,205 @@ +package com.alibaba.qlexpress4; + +import java.util.Collections; +import java.util.Map; + +/** + * Author: DQinYuan + * date 2022/1/12 2:35 下午 + */ +public class QLOptions { + + public static QLOptions DEFAULT_OPTIONS = QLOptions.builder().build(); + + /** + * precise evaluate based on BigDecimal + * default false + */ + private final boolean precise; + + /** + * define global symbol in user context + * default false + */ + private final boolean polluteUserContext; + + /** + * script timeout millisecond, default is -1, namely time unlimited + * <= 0, time unlimited + * default -1 + */ + private final long timeoutMillis; + + /** + * attachments will be carried to user defined function/operator/macro + * only used to pass data, not as variable value + * + * default empty map + */ + private final Map attachments; + + /** + * allow cache compile result of script + * + * default false + */ + private final boolean cache; + + /** + * avoid null pointer + * default false + */ + private final boolean avoidNullPointer; + + /** + * max length of arrays allowed to be created + * -1 means no limit + * default -1 + */ + private final int maxArrLength; + + /** + * Track the execution process of all expressions and return the path to the `execute` caller. + * To enable expression tracing, please ensure that the InitOptions.traceExpression is alse set to true. + * default false + */ + private final boolean traceExpression; + + /** + * disable short circuit in logic operator + * default false + */ + private final boolean shortCircuitDisable; + + private QLOptions(boolean precise, boolean polluteUserContext, long timeoutMillis, Map attachments, + boolean cache, boolean avoidNullPointer, int maxArrLength, boolean traceExpression, + boolean shortCircuitDisable) { + this.precise = precise; + this.polluteUserContext = polluteUserContext; + this.timeoutMillis = timeoutMillis; + this.attachments = attachments; + this.cache = cache; + this.avoidNullPointer = avoidNullPointer; + this.maxArrLength = maxArrLength; + this.traceExpression = traceExpression; + this.shortCircuitDisable = shortCircuitDisable; + } + + public static Builder builder() { + return new Builder(); + } + + public boolean isPrecise() { + return precise; + } + + public boolean isPolluteUserContext() { + return polluteUserContext; + } + + public long getTimeoutMillis() { + return timeoutMillis; + } + + public Map getAttachments() { + return attachments; + } + + public boolean isCache() { + return cache; + } + + public boolean isAvoidNullPointer() { + return avoidNullPointer; + } + + public int getMaxArrLength() { + return maxArrLength; + } + + /** + * @param newArrLen new arr length in runtime + * @return true if less or equal to max arr len + */ + public boolean checkArrLen(int newArrLen) { + return maxArrLength == -1 || newArrLen <= maxArrLength; + } + + public boolean isTraceExpression() { + return traceExpression; + } + + public boolean isShortCircuitDisable() { + return shortCircuitDisable; + } + + public static class Builder { + private boolean precise = false; + + private boolean polluteUserContext = false; + + private long timeoutMillis = -1; + + private Map attachments = Collections.emptyMap(); + + private boolean cache = false; + + private boolean avoidNullPointer = false; + + private int maxArrLength = -1; + + private boolean traceExpression = false; + + private boolean shortCircuitDisable = false; + + public Builder precise(boolean precise) { + this.precise = precise; + return this; + } + + public Builder polluteUserContext(boolean polluteUserContext) { + this.polluteUserContext = polluteUserContext; + return this; + } + + public Builder timeoutMillis(long timeoutMillis) { + this.timeoutMillis = timeoutMillis; + return this; + } + + public Builder attachments(Map attachments) { + this.attachments = attachments; + return this; + } + + public Builder cache(boolean cache) { + this.cache = cache; + return this; + } + + public Builder avoidNullPointer(boolean avoidNullPointer) { + this.avoidNullPointer = avoidNullPointer; + return this; + } + + public Builder maxArrLength(int maxArrLength) { + this.maxArrLength = maxArrLength; + return this; + } + + public Builder traceExpression(boolean traceExpression) { + this.traceExpression = traceExpression; + return this; + } + + public Builder shortCircuitDisable(boolean shortCircuitDisable) { + this.shortCircuitDisable = shortCircuitDisable; + return this; + } + + public QLOptions build() { + return new QLOptions(precise, polluteUserContext, timeoutMillis, attachments, cache, avoidNullPointer, + maxArrLength, traceExpression, shortCircuitDisable); + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/QLPrecedences.java b/src/main/java/com/alibaba/qlexpress4/QLPrecedences.java new file mode 100644 index 0000000..090c86a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/QLPrecedences.java @@ -0,0 +1,89 @@ +package com.alibaba.qlexpress4; + +/** + * Author: DQinYuan + * date 2022/1/12 2:31 下午 + */ +public class QLPrecedences { + + /* + * = += -= &= |= *= /= %= <<= >>= + */ + public static final int ASSIGN = 0; + + /* + * ?: + */ + public static final int TERNARY = 1; + + /* + * || or + */ + public static final int OR = 2; + + /* + * && and + */ + public static final int AND = 3; + + /* + * | + */ + public static final int BIT_OR = 4; + + /* + * ^ + */ + public static final int XOR = 5; + + /* + * & + */ + public static final int BIT_AND = 6; + + /* + * == != + */ + public static final int EQUAL = 7; + + /* + * < <= > >= instanceof + */ + public static final int COMPARE = 8; + + /* + * << >> >>> + */ + public static final int BIT_MOVE = 9; + + /* + * in like + */ + public static final int IN_LIKE = 10; + + /* + * + - + */ + public static final int ADD = 11; + + /* + * * / % + */ + public static final int MULTI = 12; + + /* + * ! ++ -- ~ + - + */ + public static final int UNARY = 13; + + /* + * ++ -- in suffix, like i++ + */ + public static final int UNARY_SUFFIX = 14; + + /* + * . () + */ + public static final int GROUP = 15; + +} diff --git a/src/main/java/com/alibaba/qlexpress4/QLResult.java b/src/main/java/com/alibaba/qlexpress4/QLResult.java new file mode 100644 index 0000000..0d9f6ec --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/QLResult.java @@ -0,0 +1,25 @@ +package com.alibaba.qlexpress4; + +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; + +import java.util.List; + +public class QLResult { + + private final Object result; + + private final List expressionTraces; + + public QLResult(Object result, List expressionTraces) { + this.result = result; + this.expressionTraces = expressionTraces; + } + + public Object getResult() { + return result; + } + + public List getExpressionTraces() { + return expressionTraces; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/annotation/QLAlias.java b/src/main/java/com/alibaba/qlexpress4/annotation/QLAlias.java new file mode 100644 index 0000000..0aa9983 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/annotation/QLAlias.java @@ -0,0 +1,15 @@ +package com.alibaba.qlexpress4.annotation; + +import java.lang.annotation.*; + +import static java.lang.annotation.ElementType.*; + +@Inherited +@Target({TYPE, FIELD, METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface QLAlias { + /** + * @return aliases + */ + String[] value(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/annotation/QLFunction.java b/src/main/java/com/alibaba/qlexpress4/annotation/QLFunction.java new file mode 100644 index 0000000..517753f --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/annotation/QLFunction.java @@ -0,0 +1,20 @@ +package com.alibaba.qlexpress4.annotation; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.METHOD; + +/** + * Author: TaoKan + */ +@Inherited +@Target({METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface QLFunction { + /** + * @return function names + */ + String[] value(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/AliasTokenSource.java b/src/main/java/com/alibaba/qlexpress4/aparser/AliasTokenSource.java new file mode 100644 index 0000000..9e4c6c8 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/AliasTokenSource.java @@ -0,0 +1,53 @@ +package com.alibaba.qlexpress4.aparser; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenFactory; +import org.antlr.v4.runtime.TokenSource; + +public class AliasTokenSource implements TokenSource { + + private final TokenSource tokenSource; + + private final ParserOperatorManager operatorManager; + + AliasTokenSource(TokenSource tokenSource, ParserOperatorManager operatorManager) { + this.tokenSource = tokenSource; + this.operatorManager = operatorManager; + } + + @Override + public Token nextToken() { + return SyntaxTreeFactory.preHandleToken(tokenSource.nextToken(), operatorManager); + } + + @Override + public int getLine() { + return tokenSource.getLine(); + } + + @Override + public int getCharPositionInLine() { + return tokenSource.getCharPositionInLine(); + } + + @Override + public CharStream getInputStream() { + return tokenSource.getInputStream(); + } + + @Override + public String getSourceName() { + return tokenSource.getSourceName(); + } + + @Override + public void setTokenFactory(TokenFactory factory) { + tokenSource.setTokenFactory(factory); + } + + @Override + public TokenFactory getTokenFactory() { + return tokenSource.getTokenFactory(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/AliasTokenStream.java b/src/main/java/com/alibaba/qlexpress4/aparser/AliasTokenStream.java new file mode 100644 index 0000000..72487cb --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/AliasTokenStream.java @@ -0,0 +1,95 @@ +package com.alibaba.qlexpress4.aparser; + +import org.antlr.v4.runtime.BufferedTokenStream; +import org.antlr.v4.runtime.RuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenSource; +import org.antlr.v4.runtime.TokenStream; +import org.antlr.v4.runtime.misc.Interval; + +public class AliasTokenStream implements TokenStream { + + private final BufferedTokenStream stream; + + private final ParserOperatorManager operatorManager; + + public AliasTokenStream(BufferedTokenStream stream, ParserOperatorManager operatorManager) { + this.stream = stream; + this.operatorManager = operatorManager; + } + + @Override + public Token LT(int k) { + return SyntaxTreeFactory.preHandleToken(stream.LT(k), operatorManager); + } + + @Override + public Token get(int index) { + return SyntaxTreeFactory.preHandleToken(stream.get(index), operatorManager); + } + + @Override + public TokenSource getTokenSource() { + return new AliasTokenSource(stream.getTokenSource(), operatorManager); + } + + @Override + public String getText(Interval interval) { + return stream.getText(interval); + } + + @Override + public String getText() { + return stream.getText(); + } + + @Override + public String getText(RuleContext ctx) { + return stream.getText(ctx); + } + + @Override + public String getText(Token start, Token stop) { + return stream.getText(start, stop); + } + + @Override + public void consume() { + stream.consume(); + } + + @Override + public int LA(int i) { + return SyntaxTreeFactory.preHandleToken(LT(i), operatorManager).getType(); + } + + @Override + public int mark() { + return stream.mark(); + } + + @Override + public void release(int marker) { + stream.release(marker); + } + + @Override + public int index() { + return stream.index(); + } + + @Override + public void seek(int index) { + stream.seek(index); + } + + @Override + public int size() { + return stream.size(); + } + + @Override + public String getSourceName() { + return stream.getSourceName(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/BuiltInTypesSet.java b/src/main/java/com/alibaba/qlexpress4/aparser/BuiltInTypesSet.java new file mode 100644 index 0000000..1a3c16e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/BuiltInTypesSet.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.aparser; + +public class BuiltInTypesSet { + + public static final String BYTE = "byte"; + + public static final String SHORT = "short"; + + public static final String INT = "int"; + + public static final String LONG = "long"; + + public static final String FLOAT = "float"; + + public static final String DOUBLE = "double"; + + public static final String BOOLEAN = "boolean"; + + public static final String CHAR = "char"; + + public static Class getCls(String lexeme) { + switch (lexeme) { + case BYTE: + return Byte.class; + case SHORT: + return Short.class; + case INT: + return Integer.class; + case LONG: + return Long.class; + case FLOAT: + return Float.class; + case DOUBLE: + return Double.class; + case BOOLEAN: + return Boolean.class; + case CHAR: + return Character.class; + default: + return null; + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/CheckVisitor.java b/src/main/java/com/alibaba/qlexpress4/aparser/CheckVisitor.java new file mode 100644 index 0000000..d067783 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/CheckVisitor.java @@ -0,0 +1,142 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.CheckOptions; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLException; +import com.alibaba.qlexpress4.exception.QLSyntaxException; +import com.alibaba.qlexpress4.operator.OperatorCheckStrategy; +import org.antlr.v4.runtime.Token; + +/** + * @author zhoutao + */ +public class CheckVisitor extends QLParserBaseVisitor { + + /** + * Operator restriction strategy + */ + private final OperatorCheckStrategy operatorCheckStrategy; + + /** + * Whether to disable function calls + */ + private final boolean disableFunctionCalls; + + /** + * Script content for error reporting + */ + private final String script; + + public CheckVisitor(CheckOptions checkOptions) { + this(checkOptions, ""); + } + + public CheckVisitor(CheckOptions checkOptions, String script) { + this.operatorCheckStrategy = checkOptions.getCheckStrategy(); + this.disableFunctionCalls = checkOptions.isDisableFunctionCalls(); + this.script = script; + } + + private void checkOperator(String operatorString, Token token) + throws QLSyntaxException { + if (null != operatorCheckStrategy && !operatorCheckStrategy.isAllowed(operatorString)) { + String reason = String.format(QLErrorCodes.OPERATOR_NOT_ALLOWED.getErrorMsg(), + operatorString, + operatorCheckStrategy.getOperators()); + throw QLException.reportScannerErr(script, + token.getStartIndex(), + token.getLine(), + token.getCharPositionInLine(), + operatorString, + QLErrorCodes.OPERATOR_NOT_ALLOWED.name(), + reason); + } + } + + private void checkFunctionCall(Token token) + throws QLSyntaxException { + if (disableFunctionCalls) { + String reason = "Function calls are not allowed in this context"; + throw QLException.reportScannerErr(script, + token.getStartIndex(), + token.getLine(), + token.getCharPositionInLine(), + token.getText(), + "FUNCTION_CALL_NOT_ALLOWED", + reason); + } + } + + @Override + public Void visitLeftAsso(QLParser.LeftAssoContext ctx) { + // Get operator + QLParser.BinaryopContext binaryopContext = ctx.binaryop(); + if (binaryopContext != null) { + String operator = binaryopContext.getText(); + checkOperator(operator, binaryopContext.getStart()); // Validate here, may throw exception + } + + // Continue traversing child nodes + return super.visitLeftAsso(ctx); + } + + @Override + public Void visitPrefixExpress(QLParser.PrefixExpressContext ctx) { + // Get prefix operator + if (ctx.opId() != null) { + String operator = ctx.opId().getText(); + checkOperator(operator, ctx.opId().getStart()); // Validate here, may throw exception + } + + return super.visitPrefixExpress(ctx); + } + + @Override + public Void visitSuffixExpress(QLParser.SuffixExpressContext ctx) { + // Get suffix operator + if (ctx.opId() != null) { + String operator = ctx.opId().getText(); + checkOperator(operator, ctx.opId().getStart()); + } + + return super.visitSuffixExpress(ctx); + } + + @Override + public Void visitExpression(QLParser.ExpressionContext ctx) { + // Check assignment operator + if (ctx.assignOperator() != null) { + checkOperator(ctx.assignOperator().getText(), ctx.assignOperator().getStart()); + } + + return super.visitExpression(ctx); + } + + @Override + public Void visitCallExpr(QLParser.CallExprContext ctx) { + // Check if function calls are disabled + checkFunctionCall(ctx.getStart()); + return super.visitCallExpr(ctx); + } + + @Override + public Void visitMethodInvoke(QLParser.MethodInvokeContext ctx) { + // Check if function calls are disabled + checkFunctionCall(ctx.getStart()); + return super.visitMethodInvoke(ctx); + } + + @Override + public Void visitOptionalMethodInvoke(QLParser.OptionalMethodInvokeContext ctx) { + // Check if function calls are disabled + checkFunctionCall(ctx.getStart()); + return super.visitOptionalMethodInvoke(ctx); + } + + @Override + public Void visitSpreadMethodInvoke(QLParser.SpreadMethodInvokeContext ctx) { + // Check if function calls are disabled + checkFunctionCall(ctx.getStart()); + return super.visitSpreadMethodInvoke(ctx); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/ExistStack.java b/src/main/java/com/alibaba/qlexpress4/aparser/ExistStack.java new file mode 100644 index 0000000..9adcc0b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/ExistStack.java @@ -0,0 +1,12 @@ +package com.alibaba.qlexpress4.aparser; + +public interface ExistStack { + + ExistStack push(); + + ExistStack pop(); + + boolean exist(String varName); + + void add(String varName); +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/GeneratorScope.java b/src/main/java/com/alibaba/qlexpress4/aparser/GeneratorScope.java new file mode 100644 index 0000000..c623e17 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/GeneratorScope.java @@ -0,0 +1,50 @@ +package com.alibaba.qlexpress4.aparser; + +import java.util.HashMap; +import java.util.Map; + +/** + * Author: DQinYuan + */ +public class GeneratorScope { + + private final GeneratorScope parent; + + private final String name; + + private final Map macroDefineMap; + + public GeneratorScope(GeneratorScope parent, String name, Map macroDefineMap) { + this.parent = parent; + this.name = name; + this.macroDefineMap = macroDefineMap; + } + + public GeneratorScope(String name, GeneratorScope parent) { + this.parent = parent; + this.name = name; + this.macroDefineMap = new HashMap<>(); + } + + /** + * @param name macro name + * @param macroDefine macro definition + * @return true if define macro successfully. fail if macro name already exists + */ + public boolean defineMacroIfAbsent(String name, MacroDefine macroDefine) { + return macroDefineMap.putIfAbsent(name, macroDefine) == null; + } + + public void defineMacro(String name, MacroDefine macroDefine) { + macroDefineMap.put(name, macroDefine); + } + + public MacroDefine getMacroInstructions(String macroName) { + MacroDefine qlInstructions = macroDefineMap.get(macroName); + return qlInstructions != null ? qlInstructions : parent != null ? parent.getMacroInstructions(macroName) : null; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/ImportManager.java b/src/main/java/com/alibaba/qlexpress4/aparser/ImportManager.java new file mode 100644 index 0000000..c1995bc --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/ImportManager.java @@ -0,0 +1,231 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.ClassSupplier; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Author: DQinYuan + */ +public class ImportManager { + + private final ClassSupplier classSupplier; + + private final List importedPacks; + + private final Map> importedClses; + + public ImportManager(ClassSupplier classSupplier, List imports) { + this.classSupplier = classSupplier; + this.importedPacks = new ArrayList<>(); + this.importedClses = new HashMap<>(); + imports.forEach(this::addImport); + } + + public ImportManager(ClassSupplier classSupplier, List importedPacks, + Map> importedClses) { + this.classSupplier = classSupplier; + this.importedPacks = importedPacks; + this.importedClses = importedClses; + } + + public boolean addImport(QLImport anImport) { + switch (anImport.getScope()) { + case PACK: + case InnerCls: + importedPacks.add(anImport); + return true; + case CLS: + Class importCls = classSupplier.loadCls(anImport.getTarget()); + if (importCls == null) { + return false; + } + String[] split = anImport.getTarget().split("\\."); + importedClses.put(split[split.length - 1], importCls); + return true; + default: + return false; + } + } + + public Class loadQualified(String qualifiedCls) { + return classSupplier.loadCls(qualifiedCls); + } + + public LoadPartQualifiedResult loadPartQualified(List fieldIds) { + Class qualifiedCls = null; + List qualifiedPath = null; + String innerClsId = null; + final byte initState = 0; + final byte continueState = 1; + final byte loadClsState = 2; + final byte loadInnerClsState = 3; + final byte preLoadInnerClsState = 4; + byte state = initState; + nextField: for (int i = 0; i < fieldIds.size(); i++) { + String fieldId = fieldIds.get(i); + switch (state) { + case initState: + // load from imported class + Class aCls = importedClses.get(fieldId); + if (aCls != null) { + qualifiedCls = aCls; + state = preLoadInnerClsState; + continue; + } + // load from imported packs + if (!Character.isLowerCase(fieldId.charAt(0))) { + for (QLImport importedPack : importedPacks) { + switch (importedPack.getScope()) { + case PACK: + Class packCls = classSupplier.loadCls(importedPack.getTarget() + "." + fieldId); + if (packCls != null) { + qualifiedCls = packCls; + state = preLoadInnerClsState; + continue nextField; + } + break; + case InnerCls: + Class innerCls = classSupplier.loadCls(importedPack.getTarget() + "$" + fieldId); + if (innerCls != null) { + qualifiedCls = innerCls; + state = preLoadInnerClsState; + continue nextField; + } + break; + } + } + return new LoadPartQualifiedResult(null, 0); + } + state = continueState; + qualifiedPath = new ArrayList<>(); + qualifiedPath.add(fieldId); + break; + case preLoadInnerClsState: + if (!Character.isLowerCase(fieldId.charAt(0))) { + state = loadInnerClsState; + innerClsId = fieldId; + } + else { + return new LoadPartQualifiedResult(qualifiedCls, i); + } + break; + case continueState: + qualifiedPath.add(fieldId); + if (!Character.isLowerCase(fieldId.charAt(0))) { + state = loadClsState; + } + break; + case loadClsState: + qualifiedCls = classSupplier.loadCls(String.join(".", qualifiedPath)); + if (qualifiedCls == null) { + return new LoadPartQualifiedResult(null, 0); + } + if (!Character.isLowerCase(fieldId.charAt(0))) { + qualifiedPath = null; + innerClsId = fieldId; + state = loadInnerClsState; + } + else { + return new LoadPartQualifiedResult(qualifiedCls, i); + } + break; + case loadInnerClsState: + Class innerCls = classSupplier.loadCls(qualifiedCls.getName() + "$" + innerClsId); + if (innerCls == null) { + return new LoadPartQualifiedResult(qualifiedCls, i - 1); + } + if (!Character.isLowerCase(fieldId.charAt(0))) { + qualifiedCls = innerCls; + innerClsId = fieldId; + } + else { + return new LoadPartQualifiedResult(innerCls, i); + } + break; + } + } + + switch (state) { + case continueState: + return new LoadPartQualifiedResult(null, 0); + case loadClsState: + qualifiedCls = classSupplier.loadCls(String.join(".", qualifiedPath)); + return qualifiedCls == null ? new LoadPartQualifiedResult(null, fieldIds.size()) + : new LoadPartQualifiedResult(qualifiedCls, fieldIds.size()); + case preLoadInnerClsState: + return new LoadPartQualifiedResult(qualifiedCls, fieldIds.size()); + case loadInnerClsState: + Class innerCls = classSupplier.loadCls(qualifiedCls.getName() + "$" + innerClsId); + return innerCls == null ? new LoadPartQualifiedResult(qualifiedCls, fieldIds.size() - 1) + : new LoadPartQualifiedResult(innerCls, fieldIds.size()); + default: + return new LoadPartQualifiedResult(null, 0); + } + } + + public static class LoadPartQualifiedResult { + private final Class cls; + + /** + * first no class path field index + */ + private final int restIndex; + + public LoadPartQualifiedResult(Class cls, int restIndex) { + this.cls = cls; + this.restIndex = restIndex; + } + + public Class getCls() { + return cls; + } + + public int getRestIndex() { + return restIndex; + } + } + + enum ImportScope { + // import java.lang.*; + PACK, + // import a.b.Cls.* + InnerCls, + // import java.lang.String; + CLS + } + + public static QLImport importInnerCls(String clsPath) { + return new QLImport(ImportScope.InnerCls, clsPath); + } + + public static QLImport importPack(String packPath) { + return new QLImport(ImportScope.PACK, packPath); + } + + public static QLImport importCls(String clsPath) { + return new QLImport(ImportScope.CLS, clsPath); + } + + public static class QLImport { + private final ImportScope scope; + + private final String target; + + public QLImport(ImportScope scope, String target) { + this.scope = scope; + this.target = target; + } + + public ImportScope getScope() { + return scope; + } + + public String getTarget() { + return target; + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/InterpolationMode.java b/src/main/java/com/alibaba/qlexpress4/aparser/InterpolationMode.java new file mode 100644 index 0000000..69a6989 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/InterpolationMode.java @@ -0,0 +1,19 @@ +package com.alibaba.qlexpress4.aparser; + +/** + * How to manage string interpolation, for instance, "a ${t-c} b" + */ +public enum InterpolationMode { + /** + * Implement interpolation using a QLExpress script. + */ + SCRIPT, + /** + * Implement interpolation using a variable name in context. + */ + VARIABLE, + /** + * Disable interpolation, `${xxx}` will be rendered verbatim. + */ + DISABLE +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/MacroDefine.java b/src/main/java/com/alibaba/qlexpress4/aparser/MacroDefine.java new file mode 100644 index 0000000..93daf38 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/MacroDefine.java @@ -0,0 +1,28 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.runtime.instruction.QLInstruction; + +import java.util.List; + +/** + * Author: DQinYuan + */ +public class MacroDefine { + + private final List instructions; + + private final boolean lastStmtExpress; + + public MacroDefine(List instructions, boolean lastStmtExpress) { + this.instructions = instructions; + this.lastStmtExpress = lastStmtExpress; + } + + public List getMacroInstructions() { + return instructions; + } + + public boolean isLastStmtExpress() { + return lastStmtExpress; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/OperatorFactory.java b/src/main/java/com/alibaba/qlexpress4/aparser/OperatorFactory.java new file mode 100644 index 0000000..3c9ee2a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/OperatorFactory.java @@ -0,0 +1,17 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.runtime.operator.BinaryOperator; +import com.alibaba.qlexpress4.runtime.operator.unary.UnaryOperator; + +/** + * Author: DQinYuan + */ +public interface OperatorFactory { + + BinaryOperator getBinaryOperator(String operatorLexeme); + + UnaryOperator getPrefixUnaryOperator(String operatorLexeme); + + UnaryOperator getSuffixUnaryOperator(String operatorLexeme); + +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/OutFunctionVisitor.java b/src/main/java/com/alibaba/qlexpress4/aparser/OutFunctionVisitor.java new file mode 100644 index 0000000..610aac4 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/OutFunctionVisitor.java @@ -0,0 +1,112 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.exception.PureErrReporter; +import com.alibaba.qlexpress4.runtime.instruction.PopInstruction; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class OutFunctionVisitor extends ScopeStackVisitor { + + private final Set outFunctions = new HashSet<>(); + + public OutFunctionVisitor() { + super(new ExistFunctionStack(null)); + } + + private static class ExistFunctionStack implements ExistStack { + private final ExistFunctionStack parent; + + private final Set existVars = new HashSet<>(); + + private ExistFunctionStack(ExistFunctionStack parent) { + this.parent = parent; + } + + public void add(String varName) { + existVars.add(varName); + } + + public boolean exist(String varName) { + if (existVars.contains(varName)) { + return true; + } + return parent != null && parent.exist(varName); + } + + public ExistFunctionStack push() { + return new ExistFunctionStack(this); + } + + public ExistFunctionStack pop() { + return parent; + } + } + + @Override + public Void visitBlockStatements(QLParser.BlockStatementsContext ctx) { + List nonEmptyChildren = ctx.blockStatement() + .stream() + .filter(bs -> !(bs instanceof QLParser.EmptyStatementContext)) + .collect(Collectors.toList()); + // process all function definitions to support forward references + for (QLParser.BlockStatementContext child : nonEmptyChildren) { + if (child instanceof QLParser.FunctionStatementContext) { + child.accept(this); + } + } + + for (QLParser.BlockStatementContext child : nonEmptyChildren) { + if (!(child instanceof QLParser.FunctionStatementContext)) { + child.accept(this); + } + } + + return null; + } + + @Override + public Void visitPrimary(QLParser.PrimaryContext ctx) { + QLParser.PrimaryNoFixPathableContext primaryNoFixPathableContext = ctx.primaryNoFixPathable(); + if (primaryNoFixPathableContext != null) { + List pathPartContexts = ctx.pathPart(); + if (primaryNoFixPathableContext instanceof QLParser.VarIdExprContext && !pathPartContexts.isEmpty() + && pathPartContexts.get(0) instanceof QLParser.CallExprContext) { + // function call + String functionName = primaryNoFixPathableContext.getText(); + if (!getStack().exist(functionName)) { + outFunctions.add(functionName); + } + } + } + return super.visitPrimary(ctx); + } + + @Override + public Void visitFunctionStatement(QLParser.FunctionStatementContext ctx) { + String functionName = ctx.varId().getText(); + getStack().add(functionName); + + QLParser.FormalOrInferredParameterListContext paramList = ctx.formalOrInferredParameterList(); + if (paramList != null) { + paramList.accept(this); + } + + QLParser.BlockStatementsContext functionBlockStatements = ctx.blockStatements(); + if (functionBlockStatements != null) { + push(); + // recur scene + getStack().add(functionName); + functionBlockStatements.accept(this); + pop(); + } + + return null; + } + + public Set getOutFunctions() { + return outFunctions; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/OutVarAttrsVisitor.java b/src/main/java/com/alibaba/qlexpress4/aparser/OutVarAttrsVisitor.java new file mode 100644 index 0000000..e88e554 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/OutVarAttrsVisitor.java @@ -0,0 +1,199 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.utils.QLStringUtils; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class OutVarAttrsVisitor extends ScopeStackVisitor { + + private final Set> outVarAttrs = new HashSet<>(); + + private final ImportManager importManager; + + public OutVarAttrsVisitor(ImportManager importManager) { + super(new ExistVarStack(null)); + this.importManager = importManager; + } + + private static class ExistVarStack implements ExistStack { + private final ExistVarStack parent; + + private final Set existVars = new HashSet<>(); + + private ExistVarStack(ExistVarStack parent) { + this.parent = parent; + } + + public void add(String varName) { + existVars.add(varName); + } + + public boolean exist(String varName) { + if (existVars.contains(varName)) { + return true; + } + return parent != null && parent.exist(varName); + } + + public ExistVarStack push() { + return new ExistVarStack(this); + } + + public ExistVarStack pop() { + return parent; + } + } + + // handle import + + @Override + public Void visitImportCls(QLParser.ImportClsContext ctx) { + String importClsPath = ctx.varId() + .stream() + .map(QLParser.VarIdContext::getStart) + .map(Token::getText) + .collect(Collectors.joining(".")); + importManager.addImport(ImportManager.importCls(importClsPath)); + return null; + } + + @Override + public Void visitImportPack(QLParser.ImportPackContext ctx) { + List importPackPathTokens = ctx.varId(); + boolean isInnerCls = + !Character.isLowerCase(importPackPathTokens.get(importPackPathTokens.size() - 1).getText().charAt(0)); + String importPath = importPackPathTokens.stream() + .map(QLParser.VarIdContext::getStart) + .map(Token::getText) + .collect(Collectors.joining(".")); + importManager + .addImport(isInnerCls ? ImportManager.importInnerCls(importPath) : ImportManager.importPack(importPath)); + return null; + } + + // collect exist variable name + + /** + * @param ctx int a = 10; + * @return a + */ + @Override + public Void visitVariableDeclaratorId(QLParser.VariableDeclaratorIdContext ctx) { + QLParser.VarIdContext varIdContext = ctx.varId(); + getStack().add(varIdContext.getText()); + return null; + } + + @Override + public Void visitLeftHandSide(QLParser.LeftHandSideContext ctx) { + List pathPartContexts = ctx.pathPart(); + String leftVarName = ctx.varId().getText(); + if (pathPartContexts.isEmpty()) { + getStack().add(leftVarName); + } + else if (!getStack().exist(leftVarName)) { + addAttrs(leftVarName, pathPartContexts); + } + return null; + } + + // exclude function call + + @Override + public Void visitPrimary(QLParser.PrimaryContext ctx) { + QLParser.PrimaryNoFixPathableContext primaryNoFixPathableContext = ctx.primaryNoFixPathable(); + if (primaryNoFixPathableContext != null) { + List pathPartContexts = ctx.pathPart(); + if (primaryNoFixPathableContext instanceof QLParser.VarIdExprContext && !pathPartContexts.isEmpty() + && pathPartContexts.get(0) instanceof QLParser.CallExprContext) { + // function call + for (QLParser.PathPartContext pathPartContext : pathPartContexts) { + pathPartContext.accept(this); + } + return null; + } + if (primaryNoFixPathableContext instanceof QLParser.VarIdExprContext) { + int restIndex = parseOutVarAttrInPath(((QLParser.VarIdExprContext)primaryNoFixPathableContext).varId(), + pathPartContexts); + for (int i = restIndex; i < pathPartContexts.size(); i++) { + pathPartContexts.get(i).accept(this); + } + return null; + } + } + + return super.visitPrimary(ctx); + } + + private int parseOutVarAttrInPath(QLParser.VarIdContext idContext, + List pathPartContexts) { + List headPartIds = new ArrayList<>(); + String primaryId = idContext.getText(); + headPartIds.add(primaryId); + for (QLParser.PathPartContext pathPartContext : pathPartContexts) { + if (pathPartContext instanceof QLParser.FieldAccessContext) { + headPartIds.add(parseFieldId(((QLParser.FieldAccessContext)pathPartContext).fieldId())); + } + else { + break; + } + } + ImportManager.LoadPartQualifiedResult loadPartQualifiedResult = importManager.loadPartQualified(headPartIds); + if (loadPartQualifiedResult.getCls() != null) { + return loadPartQualifiedResult.getRestIndex() - 1; + } + else { + return getStack().exist(primaryId) ? 0 : addAttrs(primaryId, pathPartContexts); + } + } + + private String parseFieldId(QLParser.FieldIdContext ctx) { + TerminalNode quoteStringLiteral = ctx.QuoteStringLiteral(); + if (quoteStringLiteral != null) { + return QLStringUtils.parseStringEscape(quoteStringLiteral.getText()); + } + return ctx.getStart().getText(); + } + + private String getFieldId(QLParser.PathPartContext pathPartContext) { + if (pathPartContext instanceof QLParser.FieldAccessContext) { + return parseFieldId(((QLParser.FieldAccessContext)pathPartContext).fieldId()); + } + else if (pathPartContext instanceof QLParser.OptionalFieldAccessContext) { + return parseFieldId(((QLParser.OptionalFieldAccessContext)pathPartContext).fieldId()); + } + else if (pathPartContext instanceof QLParser.SpreadFieldAccessContext) { + return parseFieldId(((QLParser.SpreadFieldAccessContext)pathPartContext).fieldId()); + } + else { + return null; + } + } + + private int addAttrs(String primaryId, List pathPartContexts) { + List attrs = new ArrayList<>(); + attrs.add(primaryId); + + int i = 0; + for (; i < pathPartContexts.size(); i++) { + String fieldId = getFieldId(pathPartContexts.get(i)); + if (fieldId == null) { + break; + } + attrs.add(fieldId); + } + + outVarAttrs.add(attrs); + return i; + } + + public Set> getOutVarAttrs() { + return outVarAttrs; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/OutVarNamesVisitor.java b/src/main/java/com/alibaba/qlexpress4/aparser/OutVarNamesVisitor.java new file mode 100644 index 0000000..0a6fff8 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/OutVarNamesVisitor.java @@ -0,0 +1,194 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.utils.QLStringUtils; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Author: DQinYuan + */ +public class OutVarNamesVisitor extends ScopeStackVisitor { + + private final Set outVars = new HashSet<>(); + + private final ImportManager importManager; + + public OutVarNamesVisitor(ImportManager importManager) { + super(new ExistVarStack(null)); + this.importManager = importManager; + } + + private static class ExistVarStack implements ExistStack { + private final ExistVarStack parent; + + private final Set existVars = new HashSet<>(); + + private ExistVarStack(ExistVarStack parent) { + this.parent = parent; + } + + public void add(String varName) { + existVars.add(varName); + } + + public boolean exist(String varName) { + if (existVars.contains(varName)) { + return true; + } + return parent != null && parent.exist(varName); + } + + public ExistVarStack push() { + return new ExistVarStack(this); + } + + public ExistVarStack pop() { + return parent; + } + } + + // handle import + + @Override + public Void visitImportCls(QLParser.ImportClsContext ctx) { + String importClsPath = ctx.varId() + .stream() + .map(QLParser.VarIdContext::getStart) + .map(Token::getText) + .collect(Collectors.joining(".")); + importManager.addImport(ImportManager.importCls(importClsPath)); + return null; + } + + @Override + public Void visitImportPack(QLParser.ImportPackContext ctx) { + List importPackPathTokens = ctx.varId(); + boolean isInnerCls = + !Character.isLowerCase(importPackPathTokens.get(importPackPathTokens.size() - 1).getText().charAt(0)); + String importPath = importPackPathTokens.stream() + .map(QLParser.VarIdContext::getStart) + .map(Token::getText) + .collect(Collectors.joining(".")); + importManager + .addImport(isInnerCls ? ImportManager.importInnerCls(importPath) : ImportManager.importPack(importPath)); + return null; + } + + // collect exist variable name + + /** + * @param ctx int a = 10; + * @return a + */ + @Override + public Void visitVariableDeclaratorId(QLParser.VariableDeclaratorIdContext ctx) { + QLParser.VarIdContext varIdContext = ctx.varId(); + getStack().add(varIdContext.getText()); + return null; + } + + @Override + public Void visitLeftHandSide(QLParser.LeftHandSideContext ctx) { + List pathPartContexts = ctx.pathPart(); + String leftVarName = ctx.varId().getText(); + if (pathPartContexts.isEmpty()) { + getStack().add(leftVarName); + } + else if (!getStack().exist(leftVarName)) { + outVars.add(leftVarName); + } + return null; + } + + // selector + + @Override + public Void visitContextSelectExpr(QLParser.ContextSelectExprContext ctx) { + String variableName = ctx.SelectorVariable_VANME().getText().trim(); + if (!getStack().exist(variableName)) { + outVars.add(variableName); + } + return null; + } + + // exclude function call + + @Override + public Void visitPrimary(QLParser.PrimaryContext ctx) { + QLParser.PrimaryNoFixPathableContext primaryNoFixPathableContext = ctx.primaryNoFixPathable(); + if (primaryNoFixPathableContext != null) { + List pathPartContexts = ctx.pathPart(); + if (primaryNoFixPathableContext instanceof QLParser.VarIdExprContext && !pathPartContexts.isEmpty() + && pathPartContexts.get(0) instanceof QLParser.CallExprContext) { + // function call + for (QLParser.PathPartContext pathPartContext : pathPartContexts) { + pathPartContext.accept(this); + } + return null; + } + if (primaryNoFixPathableContext instanceof QLParser.VarIdExprContext) { + int restIndex = parseVarIdInPath(((QLParser.VarIdExprContext)primaryNoFixPathableContext).varId(), + pathPartContexts); + for (int i = restIndex; i < pathPartContexts.size(); i++) { + pathPartContexts.get(i).accept(this); + } + return null; + } + } + + return super.visitPrimary(ctx); + } + + private int parseVarIdInPath(QLParser.VarIdContext idContext, List pathPartContexts) { + List headPartIds = new ArrayList<>(); + String primaryId = idContext.getText(); + headPartIds.add(primaryId); + for (QLParser.PathPartContext pathPartContext : pathPartContexts) { + if (pathPartContext instanceof QLParser.FieldAccessContext) { + headPartIds.add(parseFieldId(((QLParser.FieldAccessContext)pathPartContext).fieldId())); + } + else { + break; + } + } + ImportManager.LoadPartQualifiedResult loadPartQualifiedResult = importManager.loadPartQualified(headPartIds); + if (loadPartQualifiedResult.getCls() != null) { + return loadPartQualifiedResult.getRestIndex() - 1; + } + else { + if (!getStack().exist(primaryId)) { + outVars.add(primaryId); + } + return 0; + } + } + + private String parseFieldId(QLParser.FieldIdContext ctx) { + TerminalNode quoteStringLiteral = ctx.QuoteStringLiteral(); + if (quoteStringLiteral != null) { + return QLStringUtils.parseStringEscape(quoteStringLiteral.getText()); + } + return ctx.getStart().getText(); + } + + // collect out variables name + + @Override + public Void visitVarIdExpr(QLParser.VarIdExprContext ctx) { + String varName = ctx.varId().getText(); + if (!getStack().exist(varName)) { + outVars.add(varName); + } + return null; + } + + public Set getOutVars() { + return outVars; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/ParserOperatorManager.java b/src/main/java/com/alibaba/qlexpress4/aparser/ParserOperatorManager.java new file mode 100644 index 0000000..e5230e5 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/ParserOperatorManager.java @@ -0,0 +1,34 @@ +package com.alibaba.qlexpress4.aparser; + +/** + * Author: DQinYuan + */ +public interface ParserOperatorManager { + + enum OpType { + PREFIX, SUFFIX, MIDDLE + } + + /** + * determine whether lexeme is opType or not + * @param lexeme lexeme + * @param opType type of operator + * @return true if lexeme is opType + */ + boolean isOpType(String lexeme, OpType opType); + + /** + * get binary operator precedence + * @param lexeme lexeme + * @return null if lexeme not a operator + */ + Integer precedence(String lexeme); + + /** + * Get the aliased token type of lexeme. + * + * @param lexeme the source text of the token + * @return alias token type (or {@code null} if none) + */ + Integer getAlias(String lexeme); +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/QCompileCache.java b/src/main/java/com/alibaba/qlexpress4/aparser/QCompileCache.java new file mode 100644 index 0000000..43bf810 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/QCompileCache.java @@ -0,0 +1,26 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.runtime.QLambdaDefinition; +import com.alibaba.qlexpress4.runtime.trace.TracePointTree; + +import java.util.List; + +public class QCompileCache { + + private final QLambdaDefinition qLambdaDefinition; + + private final List expressionTracePoints; + + public QCompileCache(QLambdaDefinition qLambdaDefinition, List expressionTracePoints) { + this.qLambdaDefinition = qLambdaDefinition; + this.expressionTracePoints = expressionTracePoints; + } + + public QLambdaDefinition getQLambdaDefinition() { + return qLambdaDefinition; + } + + public List getExpressionTracePoints() { + return expressionTracePoints; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/QLErrorListener.java b/src/main/java/com/alibaba/qlexpress4/aparser/QLErrorListener.java new file mode 100644 index 0000000..04d4a2f --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/QLErrorListener.java @@ -0,0 +1,50 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLException; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.Token; + +public class QLErrorListener extends BaseErrorListener { + + private final String script; + + public QLErrorListener(String script) { + this.script = script; + } + + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, + String msg, RecognitionException e) { + Token currentToken = (Token)offendingSymbol; + String tokenText = currentToken.getType() == Token.EOF ? "" : currentToken.getText(); + String preHandledScript = currentToken.getType() == Token.EOF ? script + "" : script; + String preHandledMsg = errMsg(((Parser)recognizer).getContext(), currentToken, msg); + + throw QLException.reportScannerErr(preHandledScript, + currentToken.getStartIndex(), + currentToken.getLine(), + currentToken.getCharPositionInLine(), + tokenText, + QLErrorCodes.SYNTAX_ERROR.name(), + preHandledMsg); + } + + private String errMsg(ParserRuleContext ruleContext, Token currentToken, String msg) { + if ("'".equals(currentToken.getText()) || "\"".equals(currentToken.getText()) + || ruleContext.getRuleIndex() == QLParser.RULE_doubleQuoteStringLiteral) { + return "unterminated string literal"; + } + if ("import".equals(currentToken.getText())) { + return "Import statement is not at the beginning of the file."; + } + if (ruleContext.getRuleIndex() == QLParser.RULE_importDeclaration && "static".equals(currentToken.getText())) { + return "'import static' not supported temporarily"; + } + return msg; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/QLErrorStrategy.java b/src/main/java/com/alibaba/qlexpress4/aparser/QLErrorStrategy.java new file mode 100644 index 0000000..92a9579 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/QLErrorStrategy.java @@ -0,0 +1,61 @@ +package com.alibaba.qlexpress4.aparser; + +import org.antlr.v4.runtime.DefaultErrorStrategy; +import org.antlr.v4.runtime.InputMismatchException; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.Vocabulary; +import org.antlr.v4.runtime.misc.Interval; +import org.antlr.v4.runtime.misc.IntervalSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * Author: DQinYuan + */ +public class QLErrorStrategy extends DefaultErrorStrategy { + + @Override + protected void reportInputMismatch(Parser recognizer, InputMismatchException e) { + String msg = "mismatched input " + getTokenErrorDisplay(e.getOffendingToken()) + " expecting " + + intervalSetString(e.getExpectedTokens(), recognizer.getVocabulary()); + recognizer.notifyErrorListeners(e.getOffendingToken(), msg, e); + } + + private static String intervalSetString(IntervalSet expectedTokens, Vocabulary vocabulary) { + if (expectedTokens.getIntervals() == null || expectedTokens.getIntervals().isEmpty()) { + return "{}"; + } + + List eleNames = new ArrayList<>(); + for (Interval I : expectedTokens.getIntervals()) { + int a = I.a; + int b = I.b; + for (int i = a; i <= b; i++) { + if (i != QLexer.NEWLINE) { + eleNames.add(elementName(vocabulary, i)); + } + } + } + if (eleNames.isEmpty()) { + return "{}"; + } + if (eleNames.size() == 1) { + return eleNames.get(0); + } + return String.join(",", eleNames); + } + + private static String elementName(Vocabulary vocabulary, int a) { + if (a == Token.EOF) { + return ""; + } + else if (a == Token.EPSILON) { + return ""; + } + else { + return vocabulary.getDisplayName(a); + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/QLExtendLexer.java b/src/main/java/com/alibaba/qlexpress4/aparser/QLExtendLexer.java new file mode 100644 index 0000000..f717664 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/QLExtendLexer.java @@ -0,0 +1,103 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLException; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Token; + +public class QLExtendLexer extends QLexer { + + private final String script; + + private final InterpolationMode interpolationMode; + + private final String selectorStart; + + private final String selectorEnd; + + private final boolean strictNewLines; + + public QLExtendLexer(CharStream input, String script, InterpolationMode interpolationMode, String selectorStart, + String selectorEnd, boolean strictNewLines) { + super(input); + this.script = script; + this.interpolationMode = interpolationMode; + this.selectorStart = selectorStart; + this.selectorEnd = selectorEnd; + this.strictNewLines = strictNewLines; + } + + @Override + public Token nextToken() { + Token token = super.nextToken(); + // In non-strict mode, skip NEWLINE tokens + if (!strictNewLines && token.getType() == QLexer.NEWLINE) { + // Skip NEWLINEs by recursively calling nextToken() + return nextToken(); + } + return token; + } + + @Override + protected InterpolationMode getInterpolationMode() { + return interpolationMode; + } + + @Override + protected String getSelectorStart() { + return selectorStart; + } + + @Override + protected void consumeSelectorVariable() { + StringBuilder t = new StringBuilder(); + int selectorEndLength = selectorEnd.length(); + char lastCharOfSelector = selectorEnd.charAt(selectorEndLength - 1); + + t.ensureCapacity(selectorEndLength * 2); + + while (true) { + int curChInt = _input.LA(1); + if (curChInt == Token.EOF || curChInt == '\n') { + // mismatch + throwScannerException(t.toString(), "unterminated selector"); + } + char curCh = (char)curChInt; + t.append(curCh); + _input.consume(); + + if (curCh == lastCharOfSelector && t.length() >= selectorEndLength) { + if (checkEndsWith(t, selectorEnd)) { + // match + String text = t.toString(); + setText(text.substring(0, text.length() - selectorEndLength)); + popMode(); + break; + } + } + } + } + + @Override + protected void throwScannerException(String lexeme, String reason) { + throw QLException.reportScannerErr(script, + this._tokenStartCharIndex, + this._tokenStartLine, + this._tokenStartCharPositionInLine, + lexeme, + QLErrorCodes.SYNTAX_ERROR.name(), + reason); + } + + private boolean checkEndsWith(StringBuilder sb, String suffix) { + int suffixLength = suffix.length(); + int sbLength = sb.length(); + + for (int i = 0; i < suffixLength; i++) { + if (sb.charAt(sbLength - suffixLength + i) != suffix.charAt(i)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/QLExtendParser.java b/src/main/java/com/alibaba/qlexpress4/aparser/QLExtendParser.java new file mode 100644 index 0000000..0f6f20f --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/QLExtendParser.java @@ -0,0 +1,40 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.aparser.ParserOperatorManager.OpType; + +public class QLExtendParser extends QLParser { + + private final ParserOperatorManager opM; + + private final InterpolationMode interpolationMode; + + private final boolean strictNewLines; + + public QLExtendParser(AliasTokenStream input, ParserOperatorManager opM, InterpolationMode interpolationMode, + boolean strictNewLines) { + super(input); + this.opM = opM; + this.interpolationMode = interpolationMode; + this.strictNewLines = strictNewLines; + } + + @Override + protected boolean isOpType(String lexeme, OpType opType) { + return opM.isOpType(lexeme, opType); + } + + @Override + protected Integer precedence(String lexeme) { + return opM.precedence(lexeme); + } + + @Override + protected InterpolationMode getInterpolationMode() { + return interpolationMode; + } + + @Override + protected boolean isStrictNewLines() { + return strictNewLines; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/QvmInstructionVisitor.java b/src/main/java/com/alibaba/qlexpress4/aparser/QvmInstructionVisitor.java new file mode 100644 index 0000000..dc62090 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/QvmInstructionVisitor.java @@ -0,0 +1,1752 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.DefaultClassSupplier; +import com.alibaba.qlexpress4.InitOptions; +import com.alibaba.qlexpress4.aparser.compiletimefunction.CodeGenerator; +import com.alibaba.qlexpress4.aparser.compiletimefunction.CompileTimeFunction; +import com.alibaba.qlexpress4.exception.*; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.instruction.*; +import com.alibaba.qlexpress4.runtime.operator.BinaryOperator; +import com.alibaba.qlexpress4.runtime.operator.OperatorManager; +import com.alibaba.qlexpress4.runtime.operator.unary.UnaryOperator; +import com.alibaba.qlexpress4.utils.QLStringUtils; +import org.antlr.v4.runtime.RuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; +import java.util.stream.Collectors; + +import static com.alibaba.qlexpress4.aparser.QLParser.*; + +/** + * Author: DQinYuan + */ +public class QvmInstructionVisitor extends QLParserBaseVisitor { + private static final String SCOPE_SEPARATOR = "$"; + + private static final String BLOCK_LAMBDA_NAME_PREFIX = "BLOCK_"; + + private static final String IF_PREFIX = "IF_"; + + private static final String THEN_SUFFIX = "_THEN"; + + private static final String ELSE_SUFFIX = "_ELSE"; + + private static final String MACRO_PREFIX = "MACRO_"; + + private static final String LAMBDA_PREFIX = "LAMBDA_"; + + private static final String TRY_PREFIX = "TRY_"; + + private static final String CATCH_SUFFIX = "_CATCH"; + + private static final String FINAL_SUFFIX = "_FINAL"; + + private static final String FOR_PREFIX = "FOR_"; + + private static final String INIT_SUFFIX = "_INIT"; + + private static final String CONDITION_SUFFIX = "_CONDITION"; + + private static final String UPDATE_SUFFIX = "_UPDATE"; + + private static final String BODY_SUFFIX = "_BODY"; + + private static final String WHILE_PREFIX = "WHILE_"; + + private static final String LBRACE = "{"; + + private static final String RBRACE = "}"; + + private static final BigInteger MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); + + private static final BigInteger MAX_INTEGER = BigInteger.valueOf(Integer.MAX_VALUE); + + private static final BigDecimal MAX_DOUBLE = new BigDecimal(String.valueOf(Double.MAX_VALUE)); + + private static final int TIMEOUT_CHECK_GAP = 5; + + public enum Context { + BLOCK, MACRO + } + + private final String script; + + private final ImportManager importManager; + + private final GeneratorScope generatorScope; + + private final OperatorFactory operatorFactory; + + private final Map compileTimeFunctions; + + private final InitOptions initOptions; + + private final Context context; + + private final List instructionList = new ArrayList<>(); + + private int stackSize; + + private int maxStackSize; + + private int ifCounter = 0; + + private int blockCounter = 0; + + private int macroCounter = 0; + + private int lambdaCounter = 0; + + private int tryCounter = 0; + + private int forCounter = 0; + + private int whileCounter = 0; + + private int timeoutCheckPoint = -1; + + /* + * main constructor + */ + public QvmInstructionVisitor(String script, ImportManager importManager, GeneratorScope globalScope, + OperatorFactory operatorFactory, Map compileTimeFunctions, + InitOptions initOptions) { + this.script = script; + this.importManager = importManager; + this.generatorScope = new GeneratorScope("main", globalScope); + this.operatorFactory = operatorFactory; + this.context = Context.BLOCK; + this.compileTimeFunctions = compileTimeFunctions; + this.initOptions = initOptions; + } + + /* + * for recursion + */ + public QvmInstructionVisitor(String script, ImportManager importManager, GeneratorScope generatorScope, + OperatorFactory operatorFactory, Context context, Map compileTimeFunctions, + InitOptions initOptions) { + this.script = script; + this.importManager = importManager; + this.generatorScope = generatorScope; + this.operatorFactory = operatorFactory; + this.context = context; + this.compileTimeFunctions = compileTimeFunctions; + this.initOptions = initOptions; + } + + /* + * visible for testing + */ + public QvmInstructionVisitor(String script) { + this.script = script; + this.importManager = new ImportManager(DefaultClassSupplier.getInstance(), new ArrayList<>(), new HashMap<>()); + this.generatorScope = new GeneratorScope("test-main", null); + this.operatorFactory = new OperatorManager(); + this.context = Context.BLOCK; + this.compileTimeFunctions = new HashMap<>(); + this.initOptions = InitOptions.DEFAULT_OPTIONS; + } + + @Override + public Void visitImportCls(ImportClsContext ctx) { + String importClsPath = + ctx.varId().stream().map(VarIdContext::getStart).map(Token::getText).collect(Collectors.joining(".")); + importManager.addImport(ImportManager.importCls(importClsPath)); + return null; + } + + @Override + public Void visitImportPack(ImportPackContext ctx) { + List importPackPathTokens = ctx.varId(); + boolean isInnerCls = + !Character.isLowerCase(importPackPathTokens.get(importPackPathTokens.size() - 1).getText().charAt(0)); + String importPath = importPackPathTokens.stream() + .map(VarIdContext::getStart) + .map(Token::getText) + .collect(Collectors.joining(".")); + importManager + .addImport(isInnerCls ? ImportManager.importInnerCls(importPath) : ImportManager.importPack(importPath)); + return null; + } + + @Override + public Void visitBlockStatements(BlockStatementsContext blockStatementsContext) { + boolean isPreExpress = false; + List nonEmptyChildren = blockStatementsContext.blockStatement() + .stream() + .filter(bs -> !(bs instanceof EmptyStatementContext)) + .collect(Collectors.toList()); + + // First pass: process macro definitions to ensure they are available for functions + for (BlockStatementContext child : nonEmptyChildren) { + if (child instanceof MacroStatementContext) { + child.accept(this); + } + } + + // Second pass: process all function definitions to support forward references + for (BlockStatementContext child : nonEmptyChildren) { + if (child instanceof FunctionStatementContext) { + child.accept(this); + } + } + + // Third pass: process all other statements + for (BlockStatementContext child : nonEmptyChildren) { + if (!(child instanceof FunctionStatementContext) && !(child instanceof MacroStatementContext)) { + if (isPreExpress) { + addInstruction(new PopInstruction(PureErrReporter.INSTANCE)); + } + isPreExpress = handleStmt(child); + } + } + + if (context == Context.BLOCK) { + if (isPreExpress) { + addInstruction(new ReturnInstruction(PureErrReporter.INSTANCE, QResult.ResultType.CONTINUE, null)); + } + } + return null; + } + + private void visitBodyExpression(ExpressionContext expressionContext) { + BlockExprContext blockExprContext = blockExpr(expressionContext); + if (blockExprContext != null) { + BlockStatementsContext blockStatementsContext = blockExprContext.blockStatements(); + if (blockStatementsContext == null) { + return; + } + blockStatementsContext.accept(this); + return; + } + expressionContext.accept(this); + addInstruction( + new ReturnInstruction(newReporterWithToken(expressionContext.getStart()), QResult.ResultType.RETURN, null)); + } + + private BlockExprContext blockExpr(ExpressionContext expressionContext) { + Token startToken = expressionContext.getStart(); + Token stopToken = expressionContext.getStop(); + if (!(LBRACE.equals(startToken.getText()) && RBRACE.equals(stopToken.getText()))) { + // fast fail + return null; + } + TernaryExprContext ternaryExprContext = expressionContext.ternaryExpr(); + if (ternaryExprContext == null) { + return null; + } + if (ternaryExprContext.QUESTION() != null) { + return null; + } + BaseExprContext baseExprContext = ternaryExprContext.baseExpr(0); + if (!baseExprContext.leftAsso().isEmpty()) { + return null; + } + PrimaryContext primaryContext = baseExprContext.primary(); + if (primaryContext.primaryNoFixNonPathable() != null) { + return null; + } + + PrimaryNoFixPathableContext primaryNoFixPathableContext = primaryContext.primaryNoFixPathable(); + return primaryNoFixPathableContext instanceof BlockExprContext ? (BlockExprContext)primaryNoFixPathableContext + : null; + } + + @Override + public Void visitExpression(ExpressionContext ctx) { + TernaryExprContext ternaryExprContext = ctx.ternaryExpr(); + if (ternaryExprContext != null) { + ternaryExprContext.accept(this); + return null; + } + + ctx.leftHandSide().accept(this); + ctx.expression().accept(this); + + AssignOperatorContext assignOperatorContext = ctx.assignOperator(); + BinaryOperator assignOperator = operatorFactory.getBinaryOperator(assignOperatorContext.getText()); + addInstruction(new OperatorInstruction(newReporterWithToken(assignOperatorContext.getStart()), assignOperator, + assignOperatorContext.getStart().getStartIndex())); + return null; + } + + @Override + public Void visitTraditionalForStatement(TraditionalForStatementContext ctx) { + int forCount = forCount(); + ErrorReporter forErrReporter = newReporterWithToken(ctx.FOR().getSymbol()); + + // for init + ForInitContext forInitContext = ctx.forInit(); + QLambdaDefinitionInner forInitLambda = + forInitContext == null ? null : generateForInitLambda(forCount, forInitContext); + + // condition + ExpressionContext forConditionContext = ctx.forCondition; + QLambdaDefinitionInner forConditionLambda = forConditionContext == null ? null + : generateForExpressLambda(forCount, CONDITION_SUFFIX, forConditionContext); + + // for update + ExpressionContext forUpdateContext = ctx.forUpdate; + QLambdaDefinitionInner forUpdateLambda = + forUpdateContext == null ? null : generateForExpressLambda(forCount, UPDATE_SUFFIX, forUpdateContext); + + // for body + QLambdaDefinition forBodyLambda = loopBodyVisitorDefinition(ctx.blockStatements(), + generatorScope.getName() + SCOPE_SEPARATOR + FOR_PREFIX + forCount + BODY_SUFFIX, + Collections.emptyList(), + forErrReporter); + + int forInitSize = forInitLambda == null ? 0 : forInitLambda.getMaxStackSize(); + int forConditionSize = forConditionLambda == null ? 0 : forConditionLambda.getMaxStackSize(); + int forUpdateSize = forUpdateLambda == null ? 0 : forUpdateLambda.getMaxStackSize(); + int forScopeMaxStackSize = Math.max(forInitSize, Math.max(forConditionSize, forUpdateSize)); + + if (initOptions.isTraceExpression()) { + pureAddInstruction(new TraceEvaludatedInstruction(forErrReporter, ctx.FOR().getSymbol().getStartIndex())); + } + + addInstruction(new ForInstruction(forErrReporter, forInitLambda, forConditionLambda, + forConditionContext != null ? newReporterWithToken(forConditionContext.getStart()) : null, forUpdateLambda, + forScopeMaxStackSize, forBodyLambda)); + return null; + } + + private QLambdaDefinitionInner generateForInitLambda(int forCount, ForInitContext forInitContext) { + if (forInitContext.localVariableDeclaration() != null) { + String scopeName = generatorScope.getName() + SCOPE_SEPARATOR + FOR_PREFIX + forCount + INIT_SUFFIX; + QvmInstructionVisitor subVisitor = parseWithSubVisitor(forInitContext.localVariableDeclaration(), + new GeneratorScope(scopeName, generatorScope), + Context.MACRO); + return new QLambdaDefinitionInner(scopeName, subVisitor.getInstructions(), Collections.emptyList(), + subVisitor.getMaxStackSize()); + } + else if (forInitContext.expression() != null) { + return generateForExpressLambda(forCount, INIT_SUFFIX, forInitContext.expression()); + } + else { + return null; + } + } + + private QLambdaDefinitionInner generateForExpressLambda(int forCount, String scopeSuffix, + ExpressionContext expressionContext) { + String scopeName = generatorScope.getName() + SCOPE_SEPARATOR + FOR_PREFIX + forCount + scopeSuffix; + QvmInstructionVisitor subVisitor = parseExprBodyWithSubVisitor(expressionContext, + new GeneratorScope(scopeName, generatorScope), + Context.BLOCK); + return new QLambdaDefinitionInner(scopeName, subVisitor.getInstructions(), Collections.emptyList(), + subVisitor.getMaxStackSize()); + } + + @Override + public Void visitForEachStatement(ForEachStatementContext ctx) { + ExpressionContext targetExprContext = ctx.expression(); + targetExprContext.accept(this); + + DeclTypeContext declTypeContext = ctx.declType(); + Class itVarCls = declTypeContext == null ? Object.class : parseDeclType(declTypeContext); + + ErrorReporter forEachErrReporter = newReporterWithToken(ctx.FOR().getSymbol()); + QLambdaDefinition bodyDefinition = loopBodyVisitorDefinition(ctx.blockStatements(), + generatorScope.getName() + SCOPE_SEPARATOR + FOR_PREFIX + forCount() + BODY_SUFFIX, + Collections.singletonList(new QLambdaDefinitionInner.Param(ctx.varId().getText(), itVarCls)), + forEachErrReporter); + + if (initOptions.isTraceExpression()) { + pureAddInstruction( + new TraceEvaludatedInstruction(forEachErrReporter, ctx.FOR().getSymbol().getStartIndex())); + } + + addInstruction(new ForEachInstruction(forEachErrReporter, bodyDefinition, itVarCls, + newReporterWithToken(targetExprContext.getStart()))); + + return null; + } + + @Override + public Void visitWhileStatement(WhileStatementContext ctx) { + int whileCount = whileCount(); + + String whileConditionScope = + generatorScope.getName() + SCOPE_SEPARATOR + WHILE_PREFIX + whileCount + CONDITION_SUFFIX; + QvmInstructionVisitor conditionSubVisitor = parseExprBodyWithSubVisitor(ctx.expression(), + new GeneratorScope(whileConditionScope, generatorScope), + Context.BLOCK); + QLambdaDefinitionInner conditionLambda = new QLambdaDefinitionInner(whileConditionScope, + conditionSubVisitor.getInstructions(), Collections.emptyList(), conditionSubVisitor.getMaxStackSize()); + + ErrorReporter whileErrReporter = newReporterWithToken(ctx.WHILE().getSymbol()); + QLambdaDefinition whileBodyLambda = loopBodyVisitorDefinition(ctx.blockStatements(), + generatorScope.getName() + SCOPE_SEPARATOR + WHILE_PREFIX + whileCount + BODY_SUFFIX, + Collections.emptyList(), + whileErrReporter); + + if (initOptions.isTraceExpression()) { + pureAddInstruction( + new TraceEvaludatedInstruction(whileErrReporter, ctx.WHILE().getSymbol().getStartIndex())); + } + + addInstruction(new WhileInstruction(whileErrReporter, conditionLambda, whileBodyLambda, + whileBodyLambda instanceof QLambdaDefinitionInner + ? Math.max(conditionLambda.getMaxStackSize(), + ((QLambdaDefinitionInner)whileBodyLambda).getMaxStackSize()) + : conditionLambda.getMaxStackSize())); + return null; + } + + private QLambdaDefinition loopBodyVisitorDefinition(BlockStatementsContext bodyCtx, String scopeName, + List paramsType, ErrorReporter errorReporter) { + if (bodyCtx == null) { + return QLambdaDefinitionEmpty.INSTANCE; + } + QvmInstructionVisitor bodyVisitor = + parseWithSubVisitor(bodyCtx, new GeneratorScope(scopeName, generatorScope), Context.MACRO); + List bodyInstructions = bodyVisitor.getInstructions(); + + List resultInstructions = new ArrayList<>(); + resultInstructions.add(new CheckTimeOutInstruction(errorReporter)); + resultInstructions.addAll(bodyInstructions); + + return new QLambdaDefinitionInner(scopeName, resultInstructions, paramsType, bodyVisitor.getMaxStackSize()); + } + + @Override + public Void visitThrowStatement(ThrowStatementContext ctx) { + ctx.expression().accept(this); + addInstruction(new ThrowInstruction(newReporterWithToken(ctx.THROW().getSymbol()))); + return null; + } + + @Override + public Void visitReturnStatement(ReturnStatementContext ctx) { + ErrorReporter errorReporter = newReporterWithToken(ctx.getStart()); + ExpressionContext expression = ctx.expression(); + if (expression == null) { + addInstruction(new ConstInstruction(errorReporter, null, null)); + } + else { + expression.accept(this); + } + + addInstruction(new ReturnInstruction(errorReporter, QResult.ResultType.RETURN, ctx.getStart().getStartIndex())); + return null; + } + + @Override + public Void visitFunctionStatement(FunctionStatementContext ctx) { + FormalOrInferredParameterListContext formalOrInferredParameterList = ctx.formalOrInferredParameterList(); + List params = formalOrInferredParameterList == null ? Collections.emptyList() + : parseFormalOrInferredParameterList(formalOrInferredParameterList); + VarIdContext functionNameCtx = ctx.varId(); + QLambdaDefinition functionDefinition = parseFunctionDefinition(functionNameCtx.getText(), ctx, params); + + ErrorReporter errorReporter = newReporterWithToken(functionNameCtx.getStart()); + + if (initOptions.isTraceExpression()) { + pureAddInstruction( + new TraceEvaludatedInstruction(errorReporter, functionNameCtx.getStart().getStartIndex())); + } + + addInstruction(new DefineFunctionInstruction(errorReporter, functionDefinition.getName(), functionDefinition)); + return null; + } + + private QLambdaDefinition parseFunctionDefinition(String functionName, FunctionStatementContext ctx, + List params) { + BlockStatementsContext blockStatementsContext = ctx.blockStatements(); + if (blockStatementsContext == null) { + return new QLambdaDefinitionInner(functionName, Collections.emptyList(), params, 0); + } + + QvmInstructionVisitor functionSubVisitor = parseWithSubVisitor(blockStatementsContext, + new GeneratorScope(functionName, generatorScope), + Context.BLOCK); + return new QLambdaDefinitionInner(functionName, functionSubVisitor.getInstructions(), params, + functionSubVisitor.getMaxStackSize()); + } + + @Override + public Void visitCastExpr(CastExprContext ctx) { + DeclTypeContext castDeclTypeContext = ctx.declType(); + Class castCls = parseDeclType(castDeclTypeContext); + ErrorReporter errorReporter = newReporterWithToken(castDeclTypeContext.getStart()); + addInstruction(new ConstInstruction(errorReporter, castCls, null)); + ctx.primary().accept(this); + addInstruction(new CastInstruction(errorReporter)); + return null; + } + + @Override + public Void visitTernaryExpr(TernaryExprContext ctx) { + ctx.condition.accept(this); + + if (ctx.QUESTION() != null) { + QvmInstructionVisitor thenVisitor = parseWithSubVisitor(ctx.thenExpr, generatorScope, Context.MACRO); + QvmInstructionVisitor elseVisitor = parseWithSubVisitor(ctx.elseExpr, generatorScope, Context.MACRO); + ifElseInstructions(newReporterWithToken(ctx.QUESTION().getSymbol()), + thenVisitor.getInstructions(), + null, + elseVisitor.getInstructions(), + null, + ctx.QUESTION().getSymbol().getStartIndex()); + } + + return null; + } + + @Override + public Void visitBlockExpr(BlockExprContext ctx) { + ErrorReporter blockErrReporter = newReporterWithToken(ctx.getStart()); + BlockStatementsContext blockStatementsContext = ctx.blockStatements(); + if (blockStatementsContext == null) { + addInstruction(new ConstInstruction(blockErrReporter, null, null)); + return null; + } + + String blockScopeName = blockScopeName(); + QvmInstructionVisitor blockSubVisitor = parseWithSubVisitor(blockStatementsContext, + new GeneratorScope(blockScopeName, generatorScope), + Context.MACRO); + + addInstruction(new NewScopeInstruction(blockErrReporter, blockScopeName)); + blockSubVisitor.getInstructions().forEach(this::addInstruction); + addInstruction(new CloseScopeInstruction(blockErrReporter, blockScopeName)); + if (initOptions.isTraceExpression()) { + pureAddInstruction(new TracePeekInstruction(blockErrReporter, ctx.getStart().getStartIndex())); + } + return null; + } + + @Override + public Void visitQlIf(QlIfContext qlIfContext) { + qlIfContext.condition.accept(this); + + int ifCount = ifCount(); + ErrorReporter ifErrorReporter = newReporterWithToken(qlIfContext.IF().getSymbol()); + String ifScopeName = generatorScope.getName() + SCOPE_SEPARATOR + IF_PREFIX + ifCount; + addInstruction(new NewScopeInstruction(ifErrorReporter, ifScopeName)); + + String thenScopeName = generatorScope.getName() + SCOPE_SEPARATOR + IF_PREFIX + ifCount + THEN_SUFFIX; + ThenBodyContext thenBodyContext = qlIfContext.thenBody(); + List thenInstructions = + parseWithSubVisitor(thenBodyContext, new GeneratorScope(thenScopeName, generatorScope), Context.MACRO) + .getInstructions(); + if (ifBodyFillConst(thenBodyContext.expression(), + thenBodyContext.blockStatement(), + thenBodyContext.blockStatements())) { + thenInstructions.add(new ConstInstruction(ifErrorReporter, null, null)); + } + Integer thenTraceKey = thenBodyContext.LBRACE() == null ? null : thenBodyContext.getStart().getStartIndex(); + + String elseScopeName = generatorScope.getName() + SCOPE_SEPARATOR + IF_PREFIX + ifCount + ELSE_SUFFIX; + ElseBodyContext elseBodyContext = qlIfContext.elseBody(); + List elseInstructions = + elseBodyContext == null ? Collections.singletonList(new ConstInstruction(ifErrorReporter, null, null)) + : parseWithSubVisitor(elseBodyContext, new GeneratorScope(elseScopeName, generatorScope), Context.MACRO) + .getInstructions(); + if (elseBodyContext != null && elseBodyContext.qlIf() == null + && ifBodyFillConst(elseBodyContext.expression(), + elseBodyContext.blockStatement(), + elseBodyContext.blockStatements())) { + elseInstructions.add(new ConstInstruction(ifErrorReporter, null, null)); + } + Integer elseTraceKey = elseBodyContext == null ? null + : (elseBodyContext.LBRACE() == null ? null : elseBodyContext.getStart().getStartIndex()); + + ifElseInstructions(ifErrorReporter, + thenInstructions, + thenTraceKey, + elseInstructions, + elseTraceKey, + qlIfContext.getStart().getStartIndex()); + + addInstruction(new CloseScopeInstruction(ifErrorReporter, ifScopeName)); + return null; + } + + private boolean ifBodyFillConst(ExpressionContext expressionContext, BlockStatementContext blockStatementContext, + BlockStatementsContext blockStatementsContext) { + if (expressionContext != null) { + return false; + } + if (blockStatementContext != null) { + return stmtFillConst(blockStatementContext); + } + if (blockStatementsContext != null) { + List statementList = blockStatementsContext.blockStatement() + .stream() + .filter(bs -> !(bs instanceof EmptyStatementContext)) + .collect(Collectors.toList()); + return statementList.isEmpty() || stmtFillConst(statementList.get(statementList.size() - 1)); + } + return true; + } + + private boolean stmtFillConst(BlockStatementContext blockStatementContext) { + return !(blockStatementContext instanceof ExpressionStatementContext) + && !(blockStatementContext instanceof ReturnStatementContext); + } + + @Override + public Void visitBreakContinueStatement(BreakContinueStatementContext ctx) { + TerminalNode aBreak = ctx.BREAK(); + QResult qResult = aBreak == null ? QResult.LOOP_CONTINUE_RESULT : QResult.LOOP_BREAK_RESULT; + + if (initOptions.isTraceExpression()) { + pureAddInstruction( + new TraceEvaludatedInstruction(newReporterWithToken(ctx.getStart()), ctx.getStart().getStartIndex())); + } + + addInstruction(new BreakContinueInstruction(newReporterWithToken(ctx.getStart()), qResult)); + return null; + } + + @Override + public Void visitListExpr(ListExprContext ctx) { + visitListExprInner(ctx.listItems(), newReporterWithToken(ctx.getStart())); + return null; + } + + private void visitListExprInner(ListItemsContext listItemsContext, ErrorReporter listErrorReporter) { + if (listItemsContext == null) { + addInstruction(new NewListInstruction(listErrorReporter, 0)); + return; + } + List expressions = listItemsContext.expression(); + for (ExpressionContext expression : expressions) { + expression.accept(this); + } + addInstruction(new NewListInstruction(listErrorReporter, expressions.size())); + } + + @Override + public Void visitMapExpr(MapExprContext ctx) { + MapEntriesContext mapEntriesContext = ctx.mapEntries(); + List mapEntryContexts = mapEntriesContext.mapEntry(); + List keys = new ArrayList<>(mapEntryContexts.size()); + Class cls = null; + for (MapEntryContext mapEntryContext : mapEntryContexts) { + MapValueContext valueContext = mapEntryContext.mapValue(); + if (valueContext instanceof EValueContext) { + EValueContext eValueContext = (EValueContext)valueContext; + keys.add(parseMapKey(mapEntryContext.mapKey())); + eValueContext.expression().accept(this); + continue; + } + if (valueContext instanceof ClsValueContext) { + ClsValueContext clsValueContext = (ClsValueContext)valueContext; + TerminalNode clsLiteral = clsValueContext.QuoteStringLiteral(); + String clsText = clsLiteral.getText(); + String clsName = clsText.substring(1, clsText.length() - 1); + Class mayBeCls = importManager.loadQualified(clsName); + if (mayBeCls == null) { + String clsKeyText = mapEntryContext.mapKey().getText(); + keys.add(clsKeyText.substring(1, clsKeyText.length() - 1)); + addInstruction(new ConstInstruction(newReporterWithToken(clsLiteral.getSymbol()), + QLStringUtils.parseStringEscape(clsText), null)); + // @class override + cls = null; + } + else { + cls = mayBeCls; + } + } + } + if (cls == null) { + addInstruction(new NewMapInstruction(newReporterWithToken(ctx.getStart()), keys)); + } + else { + addInstruction(new NewFilledInstanceInstruction(newReporterWithToken(ctx.getStart()), cls, keys)); + } + return null; + } + + private String parseMapKey(MapKeyContext mapKeyContext) { + if (mapKeyContext instanceof IdKeyContext) { + return mapKeyContext.getText(); + } + else if (mapKeyContext instanceof StringKeyContext || mapKeyContext instanceof QuoteStringKeyContext) { + return QLStringUtils.parseStringEscape(mapKeyContext.getText()); + } + // shouldn't run here + throw new IllegalStateException(); + } + + @Override + public Void visitNewObjExpr(NewObjExprContext ctx) { + Class newCls = parseClsIds(ctx.varId()); + ArgumentListContext argumentListContext = ctx.argumentList(); + if (argumentListContext != null) { + argumentListContext.accept(this); + } + int argNum = argumentListContext == null ? 0 : argumentListContext.expression().size(); + addInstruction(new NewInstanceInstruction(newReporterWithToken(ctx.NEW().getSymbol()), newCls, argNum)); + return null; + } + + @Override + public Void visitNewEmptyArrExpr(NewEmptyArrExprContext ctx) { + ctx.dimExprs().accept(this); + int dims = ctx.dimExprs().expression().size(); + Class arrCls = parseDeclTypeNoArr(ctx.declTypeNoArr()); + addInstruction(new MultiNewArrayInstruction(newReporterWithToken(ctx.NEW().getSymbol()), arrCls, dims)); + return null; + } + + @Override + public Void visitNewInitArrExpr(NewInitArrExprContext ctx) { + Class cls = parseDeclTypeNoArr(ctx.declTypeNoArr()); + ArrayInitializerContext arrayInitializerContext = ctx.arrayInitializer(); + newArrWithInitializers(embedClsInDims(cls, ctx.dims().LBRACK().size() - 1), arrayInitializerContext); + return null; + } + + private Class embedClsInDims(Class cls, int dims) { + for (int i = 0; i < dims; i++) { + cls = Array.newInstance(cls, 0).getClass(); + } + return cls; + } + + @Override + public Void visitLambdaExpr(LambdaExprContext ctx) { + List lambdaParams = parseLambdaParams(ctx.lambdaParameters()); + String lambdaScopeName = lambdaScopeName(); + + QvmInstructionVisitor subVisitor = null; + ExpressionContext expression = ctx.expression(); + ErrorReporter arrowErrorReporter = newReporterWithToken(ctx.ARROW().getSymbol()); + if (expression != null) { + subVisitor = parseExprBodyWithSubVisitor(expression, + new GeneratorScope(lambdaScopeName, generatorScope), + Context.BLOCK); + } + else { + BlockStatementsContext blockStatementsContext = ctx.blockStatements(); + if (blockStatementsContext != null) { + subVisitor = parseWithSubVisitor(blockStatementsContext, + new GeneratorScope(lambdaScopeName, generatorScope), + Context.BLOCK); + } + } + + if (subVisitor == null) { + addInstruction(new LoadLambdaInstruction(arrowErrorReporter, QLambdaDefinitionEmpty.INSTANCE)); + } + else { + QLambdaDefinition lambdaDefinition = new QLambdaDefinitionInner(lambdaScopeName, + subVisitor.getInstructions(), lambdaParams, subVisitor.getMaxStackSize()); + addInstruction(new LoadLambdaInstruction(arrowErrorReporter, lambdaDefinition)); + } + return null; + } + + private List parseLambdaParams(LambdaParametersContext lambdaParametersContext) { + VarIdContext varIdContext = lambdaParametersContext.varId(); + if (varIdContext != null) { + return Collections.singletonList(new QLambdaDefinitionInner.Param(varIdContext.getText(), Object.class)); + } + FormalOrInferredParameterListContext formalOrInferredParameterList = + lambdaParametersContext.formalOrInferredParameterList(); + if (formalOrInferredParameterList == null) { + return Collections.emptyList(); + } + return formalOrInferredParameterList.formalOrInferredParameter() + .stream() + .map(this::formalOrInferredParameter2Param) + .collect(Collectors.toList()); + } + + private List parseFormalOrInferredParameterList( + FormalOrInferredParameterListContext formalOrInferredParameterList) { + return formalOrInferredParameterList.formalOrInferredParameter() + .stream() + .map(this::formalOrInferredParameter2Param) + .collect(Collectors.toList()); + } + + private QLambdaDefinitionInner.Param formalOrInferredParameter2Param( + FormalOrInferredParameterContext formalOrInferredParameterContext) { + String paramName = formalOrInferredParameterContext.varId().getText(); + DeclTypeContext declTypeContext = formalOrInferredParameterContext.declType(); + Class paramCls = declTypeContext == null ? Object.class : parseDeclType(declTypeContext); + return new QLambdaDefinitionInner.Param(paramName, paramCls); + } + + @Override + public Void visitTryCatchExpr(TryCatchExprContext ctx) { + BlockStatementsContext blockStatementsContext = ctx.blockStatements(); + if (blockStatementsContext == null) { + addInstruction(new ConstInstruction(newReporterWithToken(ctx.TRY().getSymbol()), null, + ctx.getStart().getStartIndex())); + return null; + } + + int tryCount = tryCount(); + String tryScopeName = generatorScope.getName() + SCOPE_SEPARATOR + TRY_PREFIX + tryCount; + QvmInstructionVisitor bodySubVisitor = parseWithSubVisitor(blockStatementsContext, + new GeneratorScope(tryScopeName, generatorScope), + Context.BLOCK); + + QLambdaDefinition bodyLambdaDefinition = new QLambdaDefinitionInner(tryScopeName, + bodySubVisitor.getInstructions(), Collections.emptyList(), bodySubVisitor.getMaxStackSize()); + List, QLambdaDefinition>> exceptionTable = parseExceptionTable(tryCount, ctx); + QLambdaDefinition finalBodyDefinition = parseFinalBodyDefinition(tryCount, ctx); + + addInstruction(new TryCatchInstruction(newReporterWithToken(ctx.TRY().getSymbol()), bodyLambdaDefinition, + exceptionTable, finalBodyDefinition)); + return null; + } + + private QLambdaDefinition parseFinalBodyDefinition(int tryCount, TryCatchExprContext ctx) { + TryFinallyContext tryFinallyContext = ctx.tryFinally(); + if (tryFinallyContext == null) { + return null; + } + + BlockStatementsContext blockStatementsContext = tryFinallyContext.blockStatements(); + if (blockStatementsContext == null) { + return null; + } + + String finalScopeName = generatorScope.getName() + SCOPE_SEPARATOR + TRY_PREFIX + tryCount + FINAL_SUFFIX; + QvmInstructionVisitor finalBodySubVisitor = parseWithSubVisitor(blockStatementsContext, + new GeneratorScope(finalScopeName, generatorScope), + Context.BLOCK); + return new QLambdaDefinitionInner(finalScopeName, finalBodySubVisitor.getInstructions(), + Collections.emptyList(), finalBodySubVisitor.getMaxStackSize()); + } + + private List, QLambdaDefinition>> parseExceptionTable(int tryCount, TryCatchExprContext ctx) { + TryCatchesContext tryCatchesContext = ctx.tryCatches(); + if (tryCatchesContext == null) { + return Collections.emptyList(); + } + List tryCatchContexts = tryCatchesContext.tryCatch(); + int catchSize = tryCatchContexts.size(); + List, QLambdaDefinition>> exceptionTable = new ArrayList<>(catchSize); + for (TryCatchContext tryCatchContext : tryCatchContexts) { + CatchParamsContext catchParamsContext = tryCatchContext.catchParams(); + String eName = catchParamsContext.varId().getText(); + String catchBodyName = generatorScope.getName() + SCOPE_SEPARATOR + TRY_PREFIX + tryCount + CATCH_SUFFIX; + QvmInstructionVisitor catchSubVisitor = tryCatchContext.blockStatements() == null ? null + : parseWithSubVisitor(tryCatchContext.blockStatements(), + new GeneratorScope(catchBodyName, generatorScope), + Context.BLOCK); + + List catchDeclTypes = catchParamsContext.declType(); + if (catchDeclTypes.isEmpty()) { + QLambdaDefinitionInner.Param param = new QLambdaDefinitionInner.Param(eName, Object.class); + QLambdaDefinition exceptionHandlerDefinition = catchSubVisitor == null ? QLambdaDefinitionEmpty.INSTANCE + : new QLambdaDefinitionInner(catchBodyName, catchSubVisitor.getInstructions(), + Collections.singletonList(param), catchSubVisitor.getMaxStackSize()); + exceptionTable.add(new AbstractMap.SimpleEntry<>(Object.class, exceptionHandlerDefinition)); + } + for (DeclTypeContext declTypeContext : catchDeclTypes) { + Class exceptionType = parseDeclType(declTypeContext); + QLambdaDefinitionInner.Param param = new QLambdaDefinitionInner.Param(eName, exceptionType); + QLambdaDefinition exceptionHandlerDefinition = catchSubVisitor == null ? QLambdaDefinitionEmpty.INSTANCE + : new QLambdaDefinitionInner(catchBodyName, catchSubVisitor.getInstructions(), + Collections.singletonList(param), catchSubVisitor.getMaxStackSize()); + exceptionTable.add(new AbstractMap.SimpleEntry<>(exceptionType, exceptionHandlerDefinition)); + } + } + return exceptionTable; + } + + @Override + public Void visitVarIdExpr(VarIdExprContext ctx) { + addInstruction(new LoadInstruction(newReporterWithToken(ctx.getStart()), ctx.varId().getText(), + ctx.getStart().getStartIndex())); + return null; + } + + private int parsePathHeadPart(PrimaryNoFixPathableContext primaryNoFixPathableContext, + List pathPartContexts) { + if (primaryNoFixPathableContext instanceof TypeExprContext) { + Class cls = BuiltInTypesSet.getCls(primaryNoFixPathableContext.getStart().getText()); + int dimPartNum = parseDimParts(0, pathPartContexts); + addInstruction(new ConstInstruction(newReporterWithToken(primaryNoFixPathableContext.getStart()), + new MetaClass(dimPartNum > 0 ? wrapInArray(cls, dimPartNum) : cls), null)); + return dimPartNum; + } + else if (primaryNoFixPathableContext instanceof VarIdExprContext) { + VarIdExprContext idContext = (VarIdExprContext)primaryNoFixPathableContext; + return parseIdHeadPart(idContext.varId(), pathPartContexts); + } + else { + primaryNoFixPathableContext.accept(this); + return 0; + } + } + + private int parseIdHeadPart(VarIdContext idContext, List pathPartContexts) { + if (!pathPartContexts.isEmpty() && pathPartContexts.get(0) instanceof CallExprContext) { + // function call + CallExprContext callExprContext = (CallExprContext)pathPartContexts.get(0); + ArgumentListContext argumentListContext = callExprContext.argumentList(); + visitCallFunction(idContext, argumentListContext); + return 1; + } + List headPartIds = new ArrayList<>(); + headPartIds.add(idContext.getText()); + for (PathPartContext pathPartContext : pathPartContexts) { + if (pathPartContext instanceof FieldAccessContext) { + headPartIds.add(parseFieldId(((FieldAccessContext)pathPartContext).fieldId())); + } + else { + break; + } + } + ImportManager.LoadPartQualifiedResult loadPartQualifiedResult = importManager.loadPartQualified(headPartIds); + if (loadPartQualifiedResult.getCls() != null) { + int restIndex = loadPartQualifiedResult.getRestIndex() - 1; + Token clsReportToken = + restIndex == 0 ? idContext.getStart() : pathPartContexts.get(restIndex - 1).getStop(); + int dimPartNum = parseDimParts(restIndex, pathPartContexts); + Class cls = dimPartNum > 0 ? wrapInArray(loadPartQualifiedResult.getCls(), dimPartNum) + : loadPartQualifiedResult.getCls(); + + addInstruction(new ConstInstruction(newReporterWithToken(clsReportToken), new MetaClass(cls), null)); + return restIndex + dimPartNum; + } + else { + addInstruction(new LoadInstruction(newReporterWithToken(idContext.getStart()), idContext.getText(), + idContext.getStart().getStartIndex())); + return 0; + } + } + + private int parseDimParts(int start, List pathPartContexts) { + int i = start; + for (; i < pathPartContexts.size(); i++) { + PathPartContext pathPartContext = pathPartContexts.get(i); + if (!isEmptyIndex(pathPartContext)) { + break; + } + } + return i - start; + } + + private boolean isEmptyIndex(PathPartContext pathPartContext) { + if (!(pathPartContext instanceof IndexExprContext)) { + return false; + } + IndexExprContext indexExprContext = (IndexExprContext)pathPartContext; + return indexExprContext.indexValueExpr() == null; + } + + @Override + public Void visitMethodInvoke(MethodInvokeContext ctx) { + visitMethodInvokeInner(ctx.argumentList(), ctx.varId(), false); + return null; + } + + @Override + public Void visitOptionalMethodInvoke(OptionalMethodInvokeContext ctx) { + visitMethodInvokeInner(ctx.argumentList(), ctx.varId(), true); + return null; + } + + @Override + public Void visitSpreadMethodInvoke(SpreadMethodInvokeContext ctx) { + ArgumentListContext argumentListContext = ctx.argumentList(); + if (argumentListContext != null) { + argumentListContext.accept(this); + } + VarIdContext methodName = ctx.varId(); + int argNum = argumentListContext == null ? 0 : argumentListContext.expression().size(); + addInstruction(new SpreadMethodInvokeInstruction(newReporterWithToken(methodName.getStart()), + methodName.getText(), argNum)); + return null; + } + + private void visitMethodInvokeInner(ArgumentListContext argumentListContext, VarIdContext methodName, + boolean optional) { + if (argumentListContext != null) { + argumentListContext.accept(this); + } + int argNum = argumentListContext == null ? 0 : argumentListContext.expression().size(); + addInstruction(new MethodInvokeInstruction(newReporterWithToken(methodName.getStart()), methodName.getText(), + argNum, optional)); + } + + @Override + public Void visitFieldAccess(FieldAccessContext ctx) { + Token fieldNameToken = ctx.getStop(); + addInstruction( + new GetFieldInstruction(newReporterWithToken(fieldNameToken), parseFieldId(ctx.fieldId()), false)); + return null; + } + + @Override + public Void visitOptionalFieldAccess(OptionalFieldAccessContext ctx) { + Token fieldNameToken = ctx.getStop(); + addInstruction( + new GetFieldInstruction(newReporterWithToken(fieldNameToken), parseFieldId(ctx.fieldId()), true)); + return null; + } + + @Override + public Void visitSpreadFieldAccess(SpreadFieldAccessContext ctx) { + Token fieldNameToken = ctx.getStop(); + addInstruction( + new SpreadGetFieldInstruction(newReporterWithToken(fieldNameToken), parseFieldId(ctx.fieldId()))); + return null; + } + + private String parseFieldId(FieldIdContext ctx) { + TerminalNode quoteStringLiteral = ctx.QuoteStringLiteral(); + if (quoteStringLiteral != null) { + return QLStringUtils.parseStringEscape(quoteStringLiteral.getText()); + } + return ctx.getStart().getText(); + } + + @Override + public Void visitMethodAccess(MethodAccessContext ctx) { + VarIdContext methodName = ctx.varId(); + addInstruction(new GetMethodInstruction(newReporterWithToken(ctx.DCOLON().getSymbol()), methodName.getText())); + return null; + } + + @Override + public Void visitCallExpr(CallExprContext ctx) { + ArgumentListContext argumentListContext = ctx.argumentList(); + if (argumentListContext != null) { + argumentListContext.accept(this); + } + int argNum = argumentListContext == null ? 0 : argumentListContext.expression().size(); + addInstruction(new CallInstruction(newReporterWithToken(ctx.getStart()), argNum)); + return null; + } + + @Override + public Void visitIndexExpr(IndexExprContext ctx) { + IndexValueExprContext indexValueExprContext = ctx.indexValueExpr(); + if (indexValueExprContext == null) { + throw reportParseErr(ctx.getStop(), + QLErrorCodes.MISSING_INDEX.name(), + QLErrorCodes.MISSING_INDEX.getErrorMsg()); + } + ErrorReporter errorReporter = newReporterWithToken(ctx.getStart()); + if (indexValueExprContext instanceof SingleIndexContext) { + ((SingleIndexContext)indexValueExprContext).expression().accept(this); + addInstruction(new IndexInstruction(errorReporter)); + } + else if (indexValueExprContext instanceof SliceIndexContext) { + SliceIndexContext sliceIndexContext = (SliceIndexContext)indexValueExprContext; + if (sliceIndexContext.start == null && sliceIndexContext.end == null) { + addInstruction(new SliceInstruction(errorReporter, SliceInstruction.Mode.COPY)); + } + else if (sliceIndexContext.start == null) { + sliceIndexContext.end.accept(this); + addInstruction(new SliceInstruction(errorReporter, SliceInstruction.Mode.LEFT)); + } + else if (sliceIndexContext.end == null) { + sliceIndexContext.start.accept(this); + addInstruction(new SliceInstruction(errorReporter, SliceInstruction.Mode.RIGHT)); + } + else { + sliceIndexContext.start.accept(this); + sliceIndexContext.end.accept(this); + addInstruction(new SliceInstruction(errorReporter, SliceInstruction.Mode.BOTH)); + } + } + return null; + } + + @Override + public Void visitCustomPath(CustomPathContext ctx) { + ErrorReporter errorReporter = newReporterWithToken(ctx.getStart()); + String path = ctx.varId().getText(); + addInstruction(new ConstInstruction(errorReporter, path, null)); + + String operatorId = ctx.opId().getText(); + BinaryOperator binaryOperator = operatorFactory.getBinaryOperator(operatorId); + addInstruction(new OperatorInstruction(errorReporter, binaryOperator, ctx.opId().getStart().getStartIndex())); + return null; + } + + @Override + public Void visitLeftAsso(LeftAssoContext ctx) { + BinaryopContext binaryopContext = ctx.binaryop(); + ErrorReporter opErrReporter = newReporterWithToken(binaryopContext.getStart()); + String operatorId = binaryopContext.getText(); + BaseExprContext rightExpr = ctx.baseExpr(); + // short circuit operator + if ("&&".equals(operatorId)) { + jumpRightIfExpect(false, opErrReporter, rightExpr, operatorId, binaryopContext.getStart().getStartIndex()); + } + else if ("||".equals(operatorId)) { + jumpRightIfExpect(true, opErrReporter, rightExpr, operatorId, binaryopContext.getStart().getStartIndex()); + } + else { + rightExpr.accept(this); + BinaryOperator binaryOperator = operatorFactory.getBinaryOperator(operatorId); + addInstruction( + new OperatorInstruction(opErrReporter, binaryOperator, binaryopContext.getStart().getStartIndex())); + } + return null; + } + + @Override + public Void visitMacroStatement(MacroStatementContext ctx) { + String macroId = ctx.varId().getText(); + + BlockStatementsContext macroBlockStatementsContext = ctx.blockStatements(); + + BlockStatementContext lastStmt = getMacroLastStmt(ctx, macroBlockStatementsContext); + generatorScope.defineMacro(macroId, + new MacroDefine(getMacroInstructions(macroBlockStatementsContext), + lastStmt instanceof ExpressionStatementContext)); + + if (initOptions.isTraceExpression()) { + pureAddInstruction(new TraceEvaludatedInstruction(newReporterWithToken(ctx.varId().getStart()), + ctx.varId().getStart().getStartIndex())); + } + + return null; + } + + private BlockStatementContext getMacroLastStmt(MacroStatementContext macroCtx, + BlockStatementsContext macroBlockStatementsCtx) { + if (macroBlockStatementsCtx == null) { + return macroCtx; + } + List blockStatementContexts = macroBlockStatementsCtx.blockStatement() + .stream() + .filter(bs -> !(bs instanceof EmptyStatementContext)) + .collect(Collectors.toList()); + return blockStatementContexts.isEmpty() ? macroCtx + : blockStatementContexts.get(blockStatementContexts.size() - 1); + } + + private List getMacroInstructions(BlockStatementsContext macroBlockStatementsContext) { + if (macroBlockStatementsContext == null) { + return Collections.emptyList(); + } + else { + QvmInstructionVisitor subVisitor = parseWithSubVisitor(macroBlockStatementsContext, + new GeneratorScope(macroScopeName(), generatorScope), + Context.MACRO); + return subVisitor.getInstructions(); + } + } + + @Override + public Void visitLocalVariableDeclaration(LocalVariableDeclarationContext ctx) { + if (initOptions.isTraceExpression()) { + addInstruction( + new TraceEvaludatedInstruction(newReporterWithToken(ctx.getStart()), ctx.getStart().getStartIndex())); + } + + Class declCls = parseDeclType(ctx.declType()); + List variableDeclaratorContexts = ctx.variableDeclaratorList().variableDeclarator(); + for (VariableDeclaratorContext variableDeclarator : variableDeclaratorContexts) { + VariableInitializerContext variableInitializer = variableDeclarator.variableInitializer(); + if (variableInitializer == null) { + addInstruction(new ConstInstruction(newReporterWithToken(variableDeclarator.getStop()), null, null)); + } + else { + parseInitializer(variableInitializer, declCls); + } + VariableDeclaratorIdContext variableDeclaratorIdContext = variableDeclarator.variableDeclaratorId(); + addInstruction(new DefineLocalInstruction(newReporterWithToken(variableDeclaratorIdContext.getStart()), + variableDeclaratorIdContext.getText(), declCls)); + } + return null; + } + + private void parseInitializer(VariableInitializerContext variableInitializer, Class declCls) { + ExpressionContext expression = variableInitializer.expression(); + if (expression != null) { + expression.accept(this); + return; + } + ArrayInitializerContext arrayInitializerContext = variableInitializer.arrayInitializer(); + newArrWithInitializers(declCls, arrayInitializerContext); + } + + private void newArrWithInitializers(Class componentClz, ArrayInitializerContext arrayInitializerContext) { + VariableInitializerListContext variableInitializerListContext = + arrayInitializerContext.variableInitializerList(); + List initializerContexts = + variableInitializerListContext == null ? Collections.emptyList() + : variableInitializerListContext.variableInitializer(); + for (VariableInitializerContext variableInitializerContext : initializerContexts) { + variableInitializerContext.accept(this); + } + addInstruction(new NewArrayInstruction(newReporterWithToken(arrayInitializerContext.getStart()), componentClz, + initializerContexts.size())); + } + + @Override + public Void visitLeftHandSide(LeftHandSideContext ctx) { + VarIdContext idContext = ctx.varId(); + List pathPartContexts = ctx.pathPart(); + if (pathPartContexts.size() == 1 && pathPartContexts.get(0) instanceof CallExprContext) { + CallExprContext callExprContext = (CallExprContext)pathPartContexts.get(0); + ArgumentListContext argumentListContext = callExprContext.argumentList(); + visitCallFunction(idContext, argumentListContext); + } + else { + int tailPartStart = parseIdHeadPart(idContext, pathPartContexts); + for (int i = tailPartStart; i < pathPartContexts.size(); i++) { + pathPartContexts.get(i).accept(this); + } + } + return null; + } + + private void visitCallFunction(VarIdContext functionNameContext, ArgumentListContext argumentListContext) { + String functionName = functionNameContext.getText(); + CompileTimeFunction compileTimeFunction = compileTimeFunctions.get(functionName); + if (compileTimeFunction != null) { + ErrorReporter functionNameReporter = newReporterWithToken(functionNameContext.getStart()); + compileTimeFunction.createFunctionInstruction(functionName, + argumentListContext == null ? Collections.emptyList() : argumentListContext.expression(), + operatorFactory, + new CodeGenerator() { + @Override + public void addInstruction(QLInstruction qlInstruction) { + QvmInstructionVisitor.this.addInstruction(qlInstruction); + } + + @Override + public void addInstructionsByTree(ParseTree tree) { + tree.accept(QvmInstructionVisitor.this); + } + + @Override + public QLSyntaxException reportParseErr(String errCode, String errReason) { + return QvmInstructionVisitor.this + .reportParseErr(functionNameContext.getStart(), errCode, errReason); + } + + @Override + public QLambdaDefinition generateLambdaDefinition(ExpressionContext expressionContext, + List params) { + QvmInstructionVisitor subVisitor = + parseExprBodyWithSubVisitor(expressionContext, generatorScope, context); + return new QLambdaDefinitionInner(functionName, subVisitor.getInstructions(), params, + subVisitor.getMaxStackSize()); + } + + @Override + public ErrorReporter getErrorReporter() { + return functionNameReporter; + } + + @Override + public ErrorReporter newReporterWithToken(Token token) { + return QvmInstructionVisitor.this.newReporterWithToken(token); + } + } + + ); + return; + } + + if (argumentListContext != null) { + argumentListContext.accept(this); + } + int argSize = argumentListContext == null ? 0 : argumentListContext.expression().size(); + addInstruction(new CallFunctionInstruction(newReporterWithToken(functionNameContext.getStart()), functionName, + argSize, functionNameContext.getStart().getStartIndex())); + } + + @Override + public Void visitPrimary(PrimaryContext ctx) { + if (ctx.primaryNoFixNonPathable() != null) { + ctx.primaryNoFixNonPathable().accept(this); + return null; + } + PrimaryNoFixPathableContext primaryNoFixPathableContext = ctx.primaryNoFixPathable(); + List pathPartContexts = ctx.pathPart(); + + // path + // head part + int tailPartStart = parsePathHeadPart(primaryNoFixPathableContext, pathPartContexts); + + // tail part + for (int i = tailPartStart; i < pathPartContexts.size(); i++) { + pathPartContexts.get(i).accept(this); + } + + SuffixExpressContext suffixExpressContext = ctx.suffixExpress(); + if (suffixExpressContext != null) { + String suffixOperator = suffixExpressContext.getText(); + UnaryOperator suffixUnaryOperator = operatorFactory.getSuffixUnaryOperator(suffixOperator); + addInstruction(new UnaryInstruction(newReporterWithToken(suffixExpressContext.getStart()), + suffixUnaryOperator, suffixExpressContext.getStart().getStartIndex())); + } + + PrefixExpressContext prefixExpressContext = ctx.prefixExpress(); + if (prefixExpressContext != null) { + String prefixOperator = prefixExpressContext.getText(); + UnaryOperator prefixUnaryOperator = operatorFactory.getPrefixUnaryOperator(prefixOperator); + addInstruction(new UnaryInstruction(newReporterWithToken(prefixExpressContext.getStart()), + prefixUnaryOperator, prefixExpressContext.getStart().getStartIndex())); + } + return null; + } + + @Override + public Void visitTypeExpr(TypeExprContext ctx) { + Class cls = BuiltInTypesSet.getCls(ctx.primitiveType().getText()); + addInstruction(new ConstInstruction(newReporterWithToken(ctx.getStart()), new MetaClass(cls), null)); + return null; + } + + @Override + public Void visitArrayInitializer(ArrayInitializerContext ctx) { + return super.visitArrayInitializer(ctx); + } + + @Override + public Void visitConstExpr(ConstExprContext ctx) { + ctx.literal().accept(this); + return null; + } + + @Override + public Void visitContextSelectExpr(ContextSelectExprContext ctx) { + String variableName = ctx.SelectorVariable_VANME().getText().trim(); + addInstruction( + new LoadInstruction(newReporterWithToken(ctx.getStart()), variableName, ctx.getStart().getStartIndex())); + return null; + } + + @Override + public Void visitLiteral(LiteralContext literal) { + TerminalNode integerLiteral = literal.IntegerLiteral(); + if (integerLiteral != null) { + try { + Number intResult = parseInteger(remove(integerLiteral.getText(), '_')); + addInstruction(new ConstInstruction(newReporterWithToken(integerLiteral.getSymbol()), intResult, + integerLiteral.getSymbol().getStartIndex())); + } + catch (NumberFormatException nfe) { + throw reportParseErr(integerLiteral.getSymbol(), + QLErrorCodes.INVALID_NUMBER.name(), + QLErrorCodes.INVALID_NUMBER.getErrorMsg()); + } + return null; + } + TerminalNode floatingPointLiteral = literal.FloatingPointLiteral(); + if (floatingPointLiteral != null) { + try { + Number floatingResult = parseFloating(remove(floatingPointLiteral.getText(), '_')); + addInstruction(new ConstInstruction(newReporterWithToken(floatingPointLiteral.getSymbol()), + floatingResult, floatingPointLiteral.getSymbol().getStartIndex())); + } + catch (NumberFormatException nfe) { + throw reportParseErr(floatingPointLiteral.getSymbol(), + QLErrorCodes.INVALID_NUMBER.name(), + QLErrorCodes.INVALID_NUMBER.getErrorMsg()); + } + return null; + } + TerminalNode integerOrFloatingLiteral = literal.IntegerOrFloatingLiteral(); + if (integerOrFloatingLiteral != null) { + try { + String numberText = integerOrFloatingLiteral.getText(); + Number numberResult = numberText.contains(".") ? parseFloating(remove(numberText, '_')) + : parseInteger(remove(numberText, '_')); + addInstruction(new ConstInstruction(newReporterWithToken(integerOrFloatingLiteral.getSymbol()), + numberResult, integerOrFloatingLiteral.getSymbol().getStartIndex())); + } + catch (NumberFormatException nfe) { + throw reportParseErr(integerOrFloatingLiteral.getSymbol(), + QLErrorCodes.INVALID_NUMBER.name(), + QLErrorCodes.INVALID_NUMBER.getErrorMsg()); + } + return null; + } + BoolenLiteralContext booleanLiteral = literal.boolenLiteral(); + if (booleanLiteral != null) { + boolean boolValue = Boolean.parseBoolean(booleanLiteral.getText()); + addInstruction(new ConstInstruction(newReporterWithToken(booleanLiteral.getStart()), boolValue, + booleanLiteral.getStart().getStartIndex())); + return null; + } + TerminalNode quoteStringLiteral = literal.QuoteStringLiteral(); + if (quoteStringLiteral != null) { + String escapedStr = QLStringUtils.parseStringEscape(quoteStringLiteral.getText()); + addInstruction(new ConstInstruction(newReporterWithToken(quoteStringLiteral.getSymbol()), escapedStr, + quoteStringLiteral.getSymbol().getStartIndex())); + return null; + } + DoubleQuoteStringLiteralContext doubleQuoteStringLiteral = literal.doubleQuoteStringLiteral(); + if (doubleQuoteStringLiteral != null) { + visitDoubleQuoteStringLiteral(doubleQuoteStringLiteral); + return null; + } + TerminalNode nullLiteral = literal.NULL(); + if (nullLiteral != null) { + addInstruction(new ConstInstruction(newReporterWithToken(nullLiteral.getSymbol()), null, + nullLiteral.getSymbol().getStartIndex())); + return null; + } + return null; + } + + @Override + public Void visitDoubleQuoteStringLiteral(DoubleQuoteStringLiteralContext ctx) { + if (initOptions.getInterpolationMode() == InterpolationMode.DISABLE) { + TerminalNode characters = ctx.StaticStringCharacters(); + if (characters == null) { + addInstruction(new ConstInstruction(newReporterWithToken(ctx.getStart()), "", null)); + return null; + } + String originText = characters.getText(); + addInstruction(new ConstInstruction(newReporterWithToken(ctx.getStart()), + QLStringUtils.parseStringEscapeStartEnd(originText, 0, originText.length()), null)); + return null; + } + int childCount = ctx.getChildCount(); + for (int i = 1; i < childCount - 1; i++) { + ParseTree child = ctx.getChild(i); + if (child instanceof StringExpressionContext) { + StringExpressionContext stringExpression = (StringExpressionContext)child; + ExpressionContext expression = stringExpression.expression(); + if (expression != null) { + // SCRIPT + visitExpression(expression); + } + else { + // VARIABLE + TerminalNode varTerminalNode = stringExpression.SelectorVariable_VANME(); + String varName = varTerminalNode.getText().trim(); + addInstruction( + new LoadInstruction(newReporterWithToken(varTerminalNode.getSymbol()), varName, null)); + } + } + else if (child instanceof TerminalNode) { + TerminalNode terminalNode = (TerminalNode)child; + String originStr = terminalNode.getText(); + addInstruction(new ConstInstruction(newReporterWithToken(terminalNode.getSymbol()), + QLStringUtils.parseStringEscapeStartEnd(originStr, 0, originStr.length()), + ctx.getStart().getStartIndex())); + } + } + addInstruction(new StringJoinInstruction(newReporterWithToken(ctx.getStart()), childCount - 2)); + return null; + } + + private Number parseFloating(String floatingText) { + char floatingTypeFlag = floatingText.charAt(floatingText.length() - 1); + switch (floatingTypeFlag) { + case 'f': + case 'F': + return new BigDecimal(floatingText.substring(0, floatingText.length() - 1)).floatValue(); + case 'd': + case 'D': + return new BigDecimal(floatingText.substring(0, floatingText.length() - 1)).doubleValue(); + default: + BigDecimal baseDecimal = new BigDecimal(floatingText); + return baseDecimal.compareTo(MAX_DOUBLE) <= 0 ? maybePresentWithDouble(baseDecimal) : baseDecimal; + } + } + + private Number maybePresentWithDouble(BigDecimal origin) { + double doubleValue = origin.doubleValue(); + BigDecimal reference = new BigDecimal(doubleValue); + return reference.compareTo(origin) == 0 ? doubleValue : origin; + } + + private Number parseInteger(String intText) { + char intTypeFlag = intText.charAt(intText.length() - 1); + switch (intTypeFlag) { + case 'l': + case 'L': + String baseIntText = intText.substring(0, intText.length() - 1); + BigInteger baseInt = parseBaseInteger(baseIntText); + return baseInt.longValue(); + default: + // auto type + baseInt = parseBaseInteger(intText); + if (baseInt.compareTo(MAX_INTEGER) <= 0) { + return baseInt.intValue(); + } + else if (baseInt.compareTo(MAX_LONG) <= 0) { + return baseInt.longValue(); + } + else { + return baseInt; + } + } + } + + private BigInteger parseBaseInteger(String intText) { + String radixPrefix = subString(intText, 2); + switch (radixPrefix) { + case "0x": + case "0X": + // radix 16 + return new BigInteger(intText.substring(2), 16); + case "0b": + case "0B": + // radix 2 + return new BigInteger(intText.substring(2), 2); + default: + if (radixPrefix.startsWith("0")) { + // radix 8 + return new BigInteger(intText, 8); + } + else { + // radix 10 + return new BigInteger(intText); + } + } + } + + private Class parseDeclTypeNoArr(DeclTypeNoArrContext declTypeNoArrContext) { + PrimitiveTypeContext primitiveTypeContext = declTypeNoArrContext.primitiveType(); + if (primitiveTypeContext != null) { + return BuiltInTypesSet.getCls(primitiveTypeContext.getText()); + } + + ClsTypeContext clsTypeContext = declTypeNoArrContext.clsType(); + return parseClsIds(clsTypeContext.varId()); + } + + private Class parseDeclType(DeclTypeContext declTypeContext) { + Class baseCls = parseDeclBaseCls(declTypeContext); + DimsContext dims = declTypeContext.dims(); + int layers = dims == null ? 0 : dims.LBRACK().size(); + return wrapInArray(baseCls, layers); + } + + private Class parseDeclBaseCls(DeclTypeContext declTypeContext) { + PrimitiveTypeContext primitiveTypeContext = declTypeContext.primitiveType(); + if (primitiveTypeContext != null) { + return BuiltInTypesSet.getCls(primitiveTypeContext.getText()); + } + ClsTypeContext clsTypeContext = declTypeContext.clsType(); + return parseClsIds(clsTypeContext.varId()); + } + + private String remove(String target, char c) { + StringBuilder builder = new StringBuilder(target.length()); + for (int i = 0; i < target.length(); i++) { + char iChar = target.charAt(i); + if (iChar != c) { + builder.append(iChar); + } + } + return builder.toString(); + } + + private String subString(String target, int end) { + if (end > target.length()) { + return target; + } + return target.substring(0, end); + } + + private Class wrapInArray(Class baseType, int layers) { + for (int i = 0; i < layers; i++) { + baseType = Array.newInstance(baseType, 0).getClass(); + } + return baseType; + } + + private Class parseClsIds(List varIdContexts) { + List fieldIds = varIdContexts.stream().map(RuleContext::getText).collect(Collectors.toList()); + ImportManager.LoadPartQualifiedResult loadPartQualifiedResult = importManager.loadPartQualified(fieldIds); + if (loadPartQualifiedResult.getCls() == null || loadPartQualifiedResult.getRestIndex() != fieldIds.size()) { + Token lastIdToken = varIdContexts.get(varIdContexts.size() - 1).getStart(); + throw reportParseErr(lastIdToken, + QLErrorCodes.CLASS_NOT_FOUND.name(), + String.format(QLErrorCodes.CLASS_NOT_FOUND.getErrorMsg(), String.join(".", fieldIds))); + } + return loadPartQualifiedResult.getCls(); + } + + private QLSyntaxException reportParseErr(Token token, String errCode, String errReason) { + return QLException.reportScannerErr(script, + token.getStartIndex(), + token.getLine(), + token.getCharPositionInLine(), + token.getText(), + errCode, + errReason); + } + + public List getInstructions() { + return instructionList; + } + + public int getMaxStackSize() { + return maxStackSize; + } + + private void ifElseInstructions(ErrorReporter conditionReporter, List thenInstructions, + Integer thenTraceKey, List elseInstructions, Integer elseTraceKey, int traceKey) { + JumpIfPopInstruction jumpIf = new JumpIfPopInstruction(conditionReporter, false, -1); + pureAddInstruction(jumpIf); + int jumpStart = instructionList.size(); + thenInstructions.forEach(this::pureAddInstruction); + if (initOptions.isTraceExpression()) { + if (thenTraceKey != null) { + pureAddInstruction(new TracePeekInstruction(conditionReporter, thenTraceKey)); + } + pureAddInstruction(new TracePeekInstruction(conditionReporter, traceKey)); + } + addTimeoutInstruction(); + + JumpInstruction jump = new JumpInstruction(conditionReporter, -1); + pureAddInstruction(jump); + + jumpIf.setPosition(instructionList.size() - jumpStart); + + jumpStart = instructionList.size(); + elseInstructions.forEach(this::pureAddInstruction); + if (initOptions.isTraceExpression()) { + if (elseTraceKey != null) { + pureAddInstruction(new TracePeekInstruction(conditionReporter, elseTraceKey)); + } + pureAddInstruction(new TracePeekInstruction(conditionReporter, traceKey)); + } + addTimeoutInstruction(); + jump.setPosition(instructionList.size() - jumpStart); + } + + private void jumpRightIfExpect(boolean expect, ErrorReporter opErrReporter, RuleContext right, String operatorId, + int traceKey) { + QvmInstructionVisitor rightVisitor = parseWithSubVisitor(right, generatorScope, Context.MACRO); + List rightInstructions = rightVisitor.getInstructions(); + + JumpIfInstruction jumpIf = new JumpIfInstruction(opErrReporter, expect, -1, traceKey); + pureAddInstruction(jumpIf); + + int jumpStart = instructionList.size(); + + rightInstructions.forEach(this::pureAddInstruction); + BinaryOperator binaryOperator = operatorFactory.getBinaryOperator(operatorId); + addInstruction(new OperatorInstruction(opErrReporter, binaryOperator, traceKey)); + addTimeoutInstruction(); + + jumpIf.setPosition(instructionList.size() - jumpStart); + } + + private QvmInstructionVisitor parseWithSubVisitor(RuleContext ruleContext, GeneratorScope generatorScope, + Context context) { + QvmInstructionVisitor subVisitor = new QvmInstructionVisitor(script, importManager, generatorScope, + operatorFactory, context, compileTimeFunctions, initOptions); + ruleContext.accept(subVisitor); + return subVisitor; + } + + private QvmInstructionVisitor parseExprBodyWithSubVisitor(ExpressionContext expressionContext, + GeneratorScope generatorScope, Context context) { + QvmInstructionVisitor subVisitor = new QvmInstructionVisitor(script, importManager, generatorScope, + operatorFactory, context, compileTimeFunctions, initOptions); + // reduce the level of syntax tree when expression is a block + subVisitor.visitBodyExpression(expressionContext); + return subVisitor; + } + + private boolean handleStmt(BlockStatementContext statementContext) { + if (maybeMacroCall(statementContext)) { + String macroName = statementContext.getStart().getText(); + MacroDefine macroDefine = generatorScope.getMacroInstructions(macroName); + if (macroDefine != null) { + macroDefine.getMacroInstructions().forEach(this::pureAddInstruction); + addTimeoutInstruction(); + return macroDefine.isLastStmtExpress(); + } + } + statementContext.accept(this); + return statementContext instanceof ExpressionStatementContext; + } + + private boolean maybeMacroCall(BlockStatementContext statementContext) { + if (statementContext instanceof ExpressionStatementContext) { + ExpressionContext expressionContext = ((ExpressionStatementContext)statementContext).expression(); + if (expressionContext != null) { + if (expressionContext.getStart() == expressionContext.getStop()) { + return expressionContext.getStart().getType() == QLexer.ID; + } + } + } + return false; + } + + private void pureAddInstruction(QLInstruction qlInstruction) { + int stackExpandSize = qlInstruction.stackOutput() - qlInstruction.stackInput(); + expandStackSize(stackExpandSize); + instructionList.add(qlInstruction); + } + + private void addInstruction(QLInstruction qlInstruction) { + if (instructionList.size() - timeoutCheckPoint > TIMEOUT_CHECK_GAP) { + addTimeoutInstruction(); + } + pureAddInstruction(qlInstruction); + if (qlInstruction instanceof MethodInvokeInstruction || qlInstruction instanceof CallFunctionInstruction + || qlInstruction instanceof CallConstInstruction || qlInstruction instanceof CallInstruction) { + addTimeoutInstruction(); + } + } + + private void addTimeoutInstruction() { + QLInstruction lastInstruction = instructionList.get(instructionList.size() - 1); + if (lastInstruction instanceof CheckTimeOutInstruction) { + return; + } + this.timeoutCheckPoint = instructionList.size(); + instructionList.add(new CheckTimeOutInstruction(lastInstruction.getErrorReporter())); + } + + private void expandStackSize(int stackExpandSize) { + stackSize += stackExpandSize; + if (stackSize > maxStackSize) { + maxStackSize = stackSize; + } + } + + private ErrorReporter newReporterWithToken(Token token) { + return new DefaultErrReporter(script, token.getStartIndex(), token.getLine(), token.getCharPositionInLine(), + token.getText()); + } + + private int whileCount() { + return whileCounter++; + } + + private int forCount() { + return forCounter++; + } + + private int ifCount() { + return ifCounter++; + } + + private int tryCount() { + return tryCounter++; + } + + private String blockScopeName() { + return generatorScope.getName() + SCOPE_SEPARATOR + BLOCK_LAMBDA_NAME_PREFIX + blockCounter++; + } + + private String macroScopeName() { + return generatorScope.getName() + SCOPE_SEPARATOR + MACRO_PREFIX + macroCounter++; + } + + private String lambdaScopeName() { + return generatorScope.getName() + SCOPE_SEPARATOR + LAMBDA_PREFIX + lambdaCounter++; + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/ScopeStackVisitor.java b/src/main/java/com/alibaba/qlexpress4/aparser/ScopeStackVisitor.java new file mode 100644 index 0000000..9b0f191 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/ScopeStackVisitor.java @@ -0,0 +1,99 @@ +package com.alibaba.qlexpress4.aparser; + +public abstract class ScopeStackVisitor extends QLParserBaseVisitor { + + private ExistStack existStack; + + protected ScopeStackVisitor(ExistStack existStack) { + this.existStack = existStack; + } + + // scope + @Override + public Void visitBlockExpr(QLParser.BlockExprContext ctx) { + push(); + super.visitBlockExpr(ctx); + pop(); + return null; + } + + @Override + public Void visitQlIf(QLParser.QlIfContext qlIfContext) { + qlIfContext.condition.accept(this); + + push(); + qlIfContext.thenBody().accept(this); + pop(); + + QLParser.ElseBodyContext elseBodyContext = qlIfContext.elseBody(); + if (elseBodyContext != null) { + push(); + elseBodyContext.accept(this); + pop(); + } + + return null; + } + + @Override + public Void visitTryCatchExpr(QLParser.TryCatchExprContext ctx) { + QLParser.BlockStatementsContext blockStatementsContext = ctx.blockStatements(); + if (blockStatementsContext != null) { + push(); + blockStatementsContext.accept(this); + pop(); + } + + QLParser.TryCatchesContext tryCatchesContext = ctx.tryCatches(); + if (tryCatchesContext != null) { + tryCatchesContext.accept(this); + } + + QLParser.TryFinallyContext tryFinallyContext = ctx.tryFinally(); + if (tryFinallyContext != null) { + push(); + tryFinallyContext.accept(this); + pop(); + } + + return null; + } + + @Override + public Void visitTryCatch(QLParser.TryCatchContext ctx) { + push(); + super.visitTryCatch(ctx); + pop(); + return null; + } + + @Override + public Void visitFunctionStatement(QLParser.FunctionStatementContext ctx) { + ctx.varId().accept(this); + QLParser.FormalOrInferredParameterListContext paramList = ctx.formalOrInferredParameterList(); + if (paramList != null) { + paramList.accept(this); + } + + QLParser.BlockStatementsContext functionBlockStatements = ctx.blockStatements(); + if (functionBlockStatements != null) { + push(); + functionBlockStatements.accept(this); + pop(); + } + + return null; + } + + public void push() { + this.existStack = this.existStack.push(); + } + + public void pop() { + this.existStack = this.existStack.pop(); + } + + public ExistStack getStack() { + return existStack; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactory.java b/src/main/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactory.java new file mode 100644 index 0000000..33256da --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactory.java @@ -0,0 +1,101 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.runtime.operator.OperatorManager; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.WritableToken; +import org.antlr.v4.runtime.atn.DecisionInfo; +import org.antlr.v4.runtime.atn.DecisionState; +import org.antlr.v4.runtime.atn.PredictionMode; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Author: DQinYuan + */ +public class SyntaxTreeFactory { + + private static final AtomicBoolean IS_WARM_UP = new AtomicBoolean(); + + public static void warmUp() { + if (IS_WARM_UP.compareAndSet(false, true)) { + // warm up + warmUpExpress("1+1"); + warmUpExpress("a = b + c"); + } + } + + private static void warmUpExpress(String script) { + buildTree(script, new OperatorManager(), false, false, s -> { + }, InterpolationMode.SCRIPT, "${", "}", true); + } + + public static QLParser.ProgramContext buildTree(String script, ParserOperatorManager operatorManager, + boolean printTree, boolean profile, Consumer printer, InterpolationMode interpolationMode, + String selectorStart, String selectorEnd, boolean strictNewLines) { + QLexer lexer = new QLExtendLexer(CharStreams.fromString(script), script, interpolationMode, selectorStart, + selectorEnd, strictNewLines); + CommonTokenStream tokens = new CommonTokenStream(lexer); + QLParser qlGrammarParser = new QLExtendParser(new AliasTokenStream(tokens, operatorManager), operatorManager, + interpolationMode, strictNewLines); + if (!printTree) { + qlGrammarParser.removeErrorListeners(); + } + qlGrammarParser.addErrorListener(new QLErrorListener(script)); + qlGrammarParser.setErrorHandler(new QLErrorStrategy()); + qlGrammarParser.getInterpreter().setPredictionMode(PredictionMode.SLL); + if (profile) { + qlGrammarParser.setProfile(true); + } + QLParser.ProgramContext programContext = qlGrammarParser.program(); + if (printTree) { + printer.accept(tokens.getTokens().stream().map(Token::getText).collect(Collectors.joining(" | "))); + printer.accept(programContext.toStringTree(qlGrammarParser)); + } + if (profile) { + profileParser(qlGrammarParser); + } + return programContext; + } + + private static void profileParser(Parser parser) { + System.out.printf("%-" + 35 + "s", "rule"); + System.out.printf("%-" + 15 + "s", "time"); + System.out.printf("%-" + 15 + "s", "invocations"); + System.out.printf("%-" + 15 + "s", "lookahead"); + System.out.printf("%-" + 15 + "s", "lookahead(max)"); + System.out.printf("%-" + 15 + "s", "ambiguities"); + System.out.printf("%-" + 15 + "s", "errors"); + System.out.printf("%-" + 15 + "s", "fallBack"); + System.out.println(); + for (DecisionInfo decisionInfo : parser.getParseInfo().getDecisionInfo()) { + DecisionState ds = parser.getATN().getDecisionState(decisionInfo.decision); + String rule = parser.getRuleNames()[ds.ruleIndex]; + if (decisionInfo.timeInPrediction > 0) { + System.out.printf("%-" + 35 + "s", rule); + System.out.printf("%-" + 15 + "s", decisionInfo.timeInPrediction); + System.out.printf("%-" + 15 + "s", decisionInfo.invocations); + System.out.printf("%-" + 15 + "s", decisionInfo.SLL_TotalLook); + System.out.printf("%-" + 15 + "s", decisionInfo.SLL_MaxLook); + System.out.printf("%-" + 15 + "s", decisionInfo.ambiguities.size()); + System.out.printf("%-" + 15 + "s", decisionInfo.errors); + System.out.printf("%-" + 15 + "s", decisionInfo.LL_Fallback); + System.out.println(); + } + } + } + + public static Token preHandleToken(Token originToken, ParserOperatorManager operatorManager) { + if (originToken instanceof WritableToken && originToken.getType() == QLexer.ID) { + Integer aliasId = operatorManager.getAlias(originToken.getText()); + if (aliasId != null && originToken.getType() != aliasId) { + ((WritableToken)originToken).setType(aliasId); + } + } + return originToken; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/TraceExpressionVisitor.java b/src/main/java/com/alibaba/qlexpress4/aparser/TraceExpressionVisitor.java new file mode 100644 index 0000000..78c6d44 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/TraceExpressionVisitor.java @@ -0,0 +1,441 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.aparser.QLParser.EmptyStatementContext; +import com.alibaba.qlexpress4.aparser.QLParser.ExpressionContext; +import com.alibaba.qlexpress4.aparser.QLParser.LocalVariableDeclarationStatementContext; +import com.alibaba.qlexpress4.aparser.QLParser.PathPartContext; +import com.alibaba.qlexpress4.aparser.QLParser.VarIdContext; +import com.alibaba.qlexpress4.runtime.trace.TracePointTree; +import com.alibaba.qlexpress4.runtime.trace.TraceType; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class TraceExpressionVisitor extends QLParserBaseVisitor { + + private final List expressionTracePoints = new ArrayList<>(); + + public List getExpressionTracePoints() { + return expressionTracePoints; + } + + // ==================== Statement ==================== + + @Override + public TracePointTree visitThrowStatement(QLParser.ThrowStatementContext ctx) { + TracePointTree tracePoint = newPoint(TraceType.STATEMENT, Collections.emptyList(), ctx.getStart()); + expressionTracePoints.add(tracePoint); + return null; + } + + @Override + public TracePointTree visitLocalVariableDeclarationStatement(LocalVariableDeclarationStatementContext ctx) { + TracePointTree tracePoint = newPoint(TraceType.STATEMENT, Collections.emptyList(), ctx.getStart()); + expressionTracePoints.add(tracePoint); + return null; + } + + @Override + public TracePointTree visitExpressionStatement(QLParser.ExpressionStatementContext ctx) { + TracePointTree expressionTrace = visitExpression(ctx.expression()); + expressionTracePoints.add(expressionTrace); + return null; + } + + @Override + public TracePointTree visitWhileStatement(QLParser.WhileStatementContext ctx) { + TracePointTree tracePoint = newPoint(TraceType.STATEMENT, Collections.emptyList(), ctx.getStart()); + expressionTracePoints.add(tracePoint); + return null; + } + + @Override + public TracePointTree visitTraditionalForStatement(QLParser.TraditionalForStatementContext ctx) { + TracePointTree tracePoint = newPoint(TraceType.STATEMENT, Collections.emptyList(), ctx.getStart()); + expressionTracePoints.add(tracePoint); + return null; + } + + @Override + public TracePointTree visitForEachStatement(QLParser.ForEachStatementContext ctx) { + TracePointTree tracePoint = newPoint(TraceType.STATEMENT, Collections.emptyList(), ctx.getStart()); + expressionTracePoints.add(tracePoint); + return null; + } + + @Override + public TracePointTree visitFunctionStatement(QLParser.FunctionStatementContext ctx) { + TracePointTree tracePoint = + newPoint(TraceType.DEFINE_FUNCTION, Collections.emptyList(), ctx.varId().getStart()); + expressionTracePoints.add(tracePoint); + return null; + } + + @Override + public TracePointTree visitMacroStatement(QLParser.MacroStatementContext ctx) { + TracePointTree tracePoint = newPoint(TraceType.DEFINE_MACRO, Collections.emptyList(), ctx.varId().getStart()); + expressionTracePoints.add(tracePoint); + return null; + } + + @Override + public TracePointTree visitBreakContinueStatement(QLParser.BreakContinueStatementContext ctx) { + TracePointTree tracePoint = newPoint(TraceType.STATEMENT, Collections.emptyList(), ctx.getStart()); + expressionTracePoints.add(tracePoint); + return null; + } + + @Override + public TracePointTree visitReturnStatement(QLParser.ReturnStatementContext ctx) { + ExpressionContext returnExpressionContext = ctx.expression(); + if (returnExpressionContext != null) { + TracePointTree returnExpressionTrace = returnExpressionContext.accept(this); + TracePointTree tracePoint = + newPoint(TraceType.RETURN, Collections.singletonList(returnExpressionTrace), ctx.getStart()); + expressionTracePoints.add(tracePoint); + } + else { + TracePointTree tracePoint = newPoint(TraceType.RETURN, Collections.emptyList(), ctx.getStart()); + expressionTracePoints.add(tracePoint); + } + return null; + } + + @Override + public TracePointTree visitEmptyStatement(QLParser.EmptyStatementContext ctx) { + expressionTracePoints.add(newPoint(TraceType.STATEMENT, Collections.emptyList(), ctx.getStart())); + return null; + } + + @Override + public TracePointTree visitBlockStatements(QLParser.BlockStatementsContext ctx) { + List emptyChildren = ctx.blockStatement() + .stream() + .filter(bs -> bs instanceof EmptyStatementContext) + .collect(Collectors.toList()); + if (emptyChildren.size() == ctx.blockStatement().size()) { + // all emtpty + emptyChildren.get(0).accept(this); + return null; + } + + for (QLParser.BlockStatementContext blockStatementContext : ctx.blockStatement()) { + if (!(blockStatementContext instanceof EmptyStatementContext)) { + blockStatementContext.accept(this); + } + } + return null; + } + + // ==================== Expression ==================== + + @Override + public TracePointTree visitExpression(QLParser.ExpressionContext ctx) { + QLParser.TernaryExprContext ternaryExprContext = ctx.ternaryExpr(); + if (ternaryExprContext != null) { + return visitTernaryExpr(ternaryExprContext); + } + + TracePointTree leftChildTree = visitLeftHandSide(ctx.leftHandSide()); + TracePointTree rightChildTree = visitExpression(ctx.expression()); + return newPoint(TraceType.OPERATOR, + Arrays.asList(leftChildTree, rightChildTree), + ctx.assignOperator().getStart()); + } + + @Override + public TracePointTree visitLeftHandSide(QLParser.LeftHandSideContext ctx) { + VarIdContext varIdContext = ctx.varId(); + ; + List pathParts = ctx.pathPart(); + TracePointTree leftChildTree = null; + int start = 0; + if (!pathParts.isEmpty() && pathParts.get(0) instanceof QLParser.CallExprContext) { + QLParser.CallExprContext callExprContext = (QLParser.CallExprContext)pathParts.get(0); + leftChildTree = newPoint(TraceType.FUNCTION, + traceArgumentList(callExprContext.argumentList()), + varIdContext.getStart()); + start = 1; + } + else { + leftChildTree = newPoint(TraceType.VARIABLE, Collections.emptyList(), ctx.getStart()); + } + return pathParts(leftChildTree, pathParts.subList(start, pathParts.size())); + } + + @Override + public TracePointTree visitTernaryExpr(QLParser.TernaryExprContext ctx) { + if (ctx.thenExpr == null) { + return ctx.condition.accept(this); + } + + TracePointTree conditionPoint = ctx.condition.accept(this); + TracePointTree thenPoint = ctx.thenExpr.accept(this); + TracePointTree elsePoint = ctx.elseExpr.accept(this); + + Token keyToken = ctx.QUESTION().getSymbol(); + return newPoint(TraceType.OPERATOR, Arrays.asList(conditionPoint, thenPoint, elsePoint), keyToken); + } + + @Override + public TracePointTree visitBaseExpr(QLParser.BaseExprContext ctx) { + TracePointTree leftChildTree = visitPrimary(ctx.primary()); + for (QLParser.LeftAssoContext leftAssoContext : ctx.leftAsso()) { + Token keyToken = leftAssoContext.binaryop().getStart(); + TracePointTree rightChildTree = visitBaseExpr(leftAssoContext.baseExpr()); + leftChildTree = newPoint(TraceType.OPERATOR, Arrays.asList(leftChildTree, rightChildTree), keyToken); + } + return leftChildTree; + } + + @Override + public TracePointTree visitPrimary(QLParser.PrimaryContext ctx) { + TracePointTree leftChildTree = primaryBaseTrace(ctx); + + // suffix + QLParser.SuffixExpressContext suffixExpressContext = ctx.suffixExpress(); + if (suffixExpressContext != null) { + leftChildTree = + newPoint(TraceType.OPERATOR, Collections.singletonList(leftChildTree), suffixExpressContext.getStart()); + } + + // prefix + QLParser.PrefixExpressContext prefixExpressContext = ctx.prefixExpress(); + if (prefixExpressContext != null) { + leftChildTree = + newPoint(TraceType.OPERATOR, Collections.singletonList(leftChildTree), prefixExpressContext.getStart()); + } + + return leftChildTree; + } + + @Override + public TracePointTree visitConstExpr(QLParser.ConstExprContext ctx) { + return newPoint(TraceType.VALUE, Collections.emptyList(), ctx.getText(), ctx.getStart()); + } + + @Override + public TracePointTree visitCastExpr(QLParser.CastExprContext ctx) { + return ctx.primary().accept(this); + } + + @Override + public TracePointTree visitGroupExpr(QLParser.GroupExprContext ctx) { + return ctx.expression().accept(this); + } + + @Override + public TracePointTree visitNewObjExpr(QLParser.NewObjExprContext ctx) { + return newPoint(TraceType.PRIMARY, Collections.emptyList(), ctx.getText(), ctx.getStart()); + } + + @Override + public TracePointTree visitNewEmptyArrExpr(QLParser.NewEmptyArrExprContext ctx) { + return newPoint(TraceType.PRIMARY, Collections.emptyList(), ctx.getText(), ctx.getStart()); + } + + @Override + public TracePointTree visitNewInitArrExpr(QLParser.NewInitArrExprContext ctx) { + return newPoint(TraceType.PRIMARY, Collections.emptyList(), ctx.getText(), ctx.getStart()); + } + + @Override + public TracePointTree visitLambdaExpr(QLParser.LambdaExprContext ctx) { + return newPoint(TraceType.PRIMARY, Collections.emptyList(), ctx.ARROW().getSymbol()); + } + + @Override + public TracePointTree visitVarIdExpr(QLParser.VarIdExprContext ctx) { + return newPoint(TraceType.VARIABLE, Collections.emptyList(), ctx.getStart()); + } + + @Override + public TracePointTree visitTypeExpr(QLParser.TypeExprContext ctx) { + return newPoint(TraceType.VALUE, Collections.emptyList(), ctx.getStart()); + } + + @Override + public TracePointTree visitListExpr(QLParser.ListExprContext ctx) { + QLParser.ListItemsContext listItemsContext = ctx.listItems(); + if (listItemsContext == null) { + return newPoint(TraceType.LIST, Collections.emptyList(), ctx.getStart()); + } + List children = listItemsContext.expression() + .stream() + .map(expression -> expression.accept(this)) + .collect(Collectors.toList()); + return newPoint(TraceType.LIST, children, ctx.getStart()); + } + + @Override + public TracePointTree visitMapExpr(QLParser.MapExprContext ctx) { + return newPoint(TraceType.MAP, Collections.emptyList(), ctx.getStart()); + } + + @Override + public TracePointTree visitBlockExpr(QLParser.BlockExprContext ctx) { + TraceExpressionVisitor traceExpressionVisitor = new TraceExpressionVisitor(); + ctx.blockStatements().accept(traceExpressionVisitor); + List children = traceExpressionVisitor.getExpressionTracePoints(); + return newPoint(TraceType.BLOCK, children, ctx.getStart()); + } + + @Override + public TracePointTree visitQlIf(QLParser.QlIfContext ctx) { + List children = new ArrayList<>(3); + children.add(ctx.condition.accept(this)); + // thenBody + children.add(visitThenBody(ctx.thenBody())); + // elseBody + if (ctx.elseBody() != null) { + children.add(visitElseBody(ctx.elseBody())); + } + return newPoint(TraceType.IF, children, "if", ctx.getStart()); + } + + @Override + public TracePointTree visitThenBody(QLParser.ThenBodyContext thenBody) { + if (thenBody.blockStatements() != null) { + TraceExpressionVisitor blockVisitor = new TraceExpressionVisitor(); + thenBody.blockStatements().accept(blockVisitor); + return newPoint(TraceType.BLOCK, blockVisitor.getExpressionTracePoints(), thenBody.getStart()); + } + else if (thenBody.expression() != null) { + return thenBody.expression().accept(this); + } + else if (thenBody.blockStatement() != null) { + TraceExpressionVisitor blockVisitor = new TraceExpressionVisitor(); + thenBody.blockStatement().accept(blockVisitor); + return blockVisitor.getExpressionTracePoints().get(0); + } + return newPoint(TraceType.BLOCK, Collections.emptyList(), thenBody.getStart()); + } + + @Override + public TracePointTree visitElseBody(QLParser.ElseBodyContext elseBody) { + if (elseBody.blockStatements() != null) { + TraceExpressionVisitor blockVisitor = new TraceExpressionVisitor(); + elseBody.blockStatements().accept(blockVisitor); + return newPoint(TraceType.BLOCK, blockVisitor.getExpressionTracePoints(), elseBody.getStart()); + } + else if (elseBody.expression() != null) { + return elseBody.expression().accept(this); + } + else if (elseBody.blockStatement() != null) { + TraceExpressionVisitor blockVisitor = new TraceExpressionVisitor(); + elseBody.blockStatement().accept(blockVisitor); + return blockVisitor.getExpressionTracePoints().get(0); + } + else if (elseBody.qlIf() != null) { + return elseBody.qlIf().accept(this); + } + return newPoint(TraceType.BLOCK, Collections.emptyList(), elseBody.getStart()); + } + + @Override + public TracePointTree visitTryCatchExpr(QLParser.TryCatchExprContext ctx) { + return newPoint(TraceType.PRIMARY, Collections.emptyList(), ctx.getStart()); + } + + @Override + public TracePointTree visitContextSelectExpr(QLParser.ContextSelectExprContext ctx) { + return newPoint(TraceType.PRIMARY, Collections.emptyList(), ctx.getStart()); + } + + // ==================== Private Helper ==================== + + private TracePointTree primaryBaseTrace(QLParser.PrimaryContext ctx) { + QLParser.PrimaryNoFixNonPathableContext primaryNoFixNonPathableContext = ctx.primaryNoFixNonPathable(); + if (primaryNoFixNonPathableContext != null) { + return primaryNoFixNonPathableContext.accept(this); + } + + QLParser.PrimaryNoFixPathableContext primaryNoFixPathableContext = ctx.primaryNoFixPathable(); + List pathPartContexts = ctx.pathPart(); + TracePointTree leftChildTree = null; + int start = 0; + if (primaryNoFixPathableContext instanceof QLParser.VarIdExprContext && !pathPartContexts.isEmpty() + && pathPartContexts.get(0) instanceof QLParser.CallExprContext) { + // function call + QLParser.VarIdExprContext functionNameContext = (QLParser.VarIdExprContext)primaryNoFixPathableContext; + QLParser.CallExprContext callExprContext = (QLParser.CallExprContext)pathPartContexts.get(0); + leftChildTree = newPoint(TraceType.FUNCTION, + traceArgumentList(callExprContext.argumentList()), + functionNameContext.getStart()); + start = 1; + } + else { + leftChildTree = primaryNoFixPathableContext.accept(this); + } + + return pathParts(leftChildTree, pathPartContexts.subList(start, pathPartContexts.size())); + } + + private TracePointTree pathParts(TracePointTree pathRoot, List pathPartContexts) { + TracePointTree leftChildTree = pathRoot; + for (QLParser.PathPartContext current : pathPartContexts) { + if (current instanceof QLParser.MethodInvokeContext + || current instanceof QLParser.OptionalMethodInvokeContext + || current instanceof QLParser.SpreadMethodInvokeContext) { + QLParser.ArgumentListContext argumentList = current.getChild(QLParser.ArgumentListContext.class, 0); + List argumentsChildren = traceArgumentList(argumentList); + List methodChildren = new ArrayList<>(1 + argumentsChildren.size()); + methodChildren.add(leftChildTree); + methodChildren.addAll(argumentsChildren); + Token keyToken = current.getChild(QLParser.VarIdContext.class, 0).getStart(); + leftChildTree = newPoint(TraceType.METHOD, methodChildren, keyToken); + } + else if (current instanceof QLParser.CallExprContext) { + QLParser.ArgumentListContext argumentList = current.getChild(QLParser.ArgumentListContext.class, 0); + List argumentsChildren = traceArgumentList(argumentList); + List callChildren = new ArrayList<>(1 + argumentsChildren.size()); + callChildren.add(leftChildTree); + callChildren.addAll(argumentsChildren); + leftChildTree = newPoint(TraceType.OPERATOR, callChildren, current.getStart()); + } + else if (current instanceof QLParser.IndexExprContext) { + QLParser.IndexValueExprContext indexValueExprContext = + current.getChild(QLParser.IndexValueExprContext.class, 0); + List indexArgChildren = + indexValueExprContext.getRuleContexts(QLParser.ExpressionContext.class) + .stream() + .map(expression -> expression.accept(this)) + .collect(Collectors.toList()); + List indexChildren = new ArrayList<>(1 + indexArgChildren.size()); + indexChildren.add(leftChildTree); + indexChildren.addAll(indexArgChildren); + leftChildTree = newPoint(TraceType.OPERATOR, indexChildren, current.getStart()); + } + else { + // field + leftChildTree = newPoint(TraceType.FIELD, Collections.singletonList(leftChildTree), current.getStop()); + } + } + return leftChildTree; + } + + private List traceArgumentList(QLParser.ArgumentListContext argumentListContext) { + if (argumentListContext == null || argumentListContext.isEmpty()) { + return Collections.emptyList(); + } + return argumentListContext.expression() + .stream() + .map(expression -> expression.accept(this)) + .collect(Collectors.toList()); + } + + private TracePointTree newPoint(TraceType traceType, List children, Token keyToken) { + return new TracePointTree(traceType, keyToken.getText(), children, keyToken.getLine(), + keyToken.getCharPositionInLine(), keyToken.getStartIndex()); + } + + private TracePointTree newPoint(TraceType traceType, List children, String text, Token keyToken) { + return new TracePointTree(traceType, text, children, keyToken.getLine(), keyToken.getCharPositionInLine(), + keyToken.getStartIndex()); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/compiletimefunction/CodeGenerator.java b/src/main/java/com/alibaba/qlexpress4/aparser/compiletimefunction/CodeGenerator.java new file mode 100644 index 0000000..1638280 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/compiletimefunction/CodeGenerator.java @@ -0,0 +1,31 @@ +package com.alibaba.qlexpress4.aparser.compiletimefunction; + +import com.alibaba.qlexpress4.aparser.QLParser; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLSyntaxException; +import com.alibaba.qlexpress4.runtime.QLambdaDefinition; +import com.alibaba.qlexpress4.runtime.QLambdaDefinitionInner; +import com.alibaba.qlexpress4.runtime.instruction.QLInstruction; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; + +import java.util.List; + +/** + * Author: DQinYuan + */ +public interface CodeGenerator { + + void addInstruction(QLInstruction qlInstruction); + + void addInstructionsByTree(ParseTree tree); + + QLSyntaxException reportParseErr(String errCode, String errReason); + + QLambdaDefinition generateLambdaDefinition(QLParser.ExpressionContext expressionContext, + List params); + + ErrorReporter getErrorReporter(); + + ErrorReporter newReporterWithToken(Token token); +} diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/compiletimefunction/CompileTimeFunction.java b/src/main/java/com/alibaba/qlexpress4/aparser/compiletimefunction/CompileTimeFunction.java new file mode 100644 index 0000000..7444227 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/aparser/compiletimefunction/CompileTimeFunction.java @@ -0,0 +1,23 @@ +package com.alibaba.qlexpress4.aparser.compiletimefunction; + +import com.alibaba.qlexpress4.aparser.OperatorFactory; +import com.alibaba.qlexpress4.aparser.QLParser; + +import java.util.List; + +/** + * Author: DQinYuan + */ +public interface CompileTimeFunction { + + /** + * create instructions for function in compile time + * @param functionName function name + * @param arguments arguments syntax tree + * @param operatorFactory operator factory + * @param codeGenerator tool for code generate + */ + void createFunctionInstruction(String functionName, List arguments, + OperatorFactory operatorFactory, CodeGenerator codeGenerator); + +} diff --git a/src/main/java/com/alibaba/qlexpress4/api/BatchAddFunctionResult.java b/src/main/java/com/alibaba/qlexpress4/api/BatchAddFunctionResult.java new file mode 100644 index 0000000..ad84a41 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/api/BatchAddFunctionResult.java @@ -0,0 +1,27 @@ +package com.alibaba.qlexpress4.api; + +import java.util.ArrayList; +import java.util.List; + +/** + * Author: DQinYuan + */ +public class BatchAddFunctionResult { + + private final List succ; + + private final List fail; + + public BatchAddFunctionResult() { + this.succ = new ArrayList<>(); + this.fail = new ArrayList<>(); + } + + public List getSucc() { + return succ; + } + + public List getFail() { + return fail; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/api/QLFunctionalVarargs.java b/src/main/java/com/alibaba/qlexpress4/api/QLFunctionalVarargs.java new file mode 100644 index 0000000..5d373c4 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/api/QLFunctionalVarargs.java @@ -0,0 +1,9 @@ +package com.alibaba.qlexpress4.api; + +/** + * Author: TaoKan + */ +@FunctionalInterface +public interface QLFunctionalVarargs { + Object call(Object... params); +} diff --git a/src/main/java/com/alibaba/qlexpress4/enums/AccessMode.java b/src/main/java/com/alibaba/qlexpress4/enums/AccessMode.java new file mode 100644 index 0000000..0ffd7cf --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/enums/AccessMode.java @@ -0,0 +1,8 @@ +package com.alibaba.qlexpress4.enums; + +/** + * Author: TaoKan + */ +public enum AccessMode { + WRITE, READ, +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/DefaultErrReporter.java b/src/main/java/com/alibaba/qlexpress4/exception/DefaultErrReporter.java new file mode 100644 index 0000000..04aeee9 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/DefaultErrReporter.java @@ -0,0 +1,37 @@ +package com.alibaba.qlexpress4.exception; + +/** + * Author: DQinYuan + */ +public class DefaultErrReporter implements ErrorReporter { + + private final String script; + + private final int tokenStartPos; + + private final int line; + + private final int col; + + private final String lexeme; + + public DefaultErrReporter(String script, int tokenStartPos, int line, int col, String lexeme) { + this.script = script; + this.tokenStartPos = tokenStartPos; + this.line = line; + this.col = col; + this.lexeme = lexeme; + } + + @Override + public QLRuntimeException reportFormatWithCatch(Object catchObj, String errorCode, String format, Object... args) { + return QLException.reportRuntimeErrWithAttach(script, + tokenStartPos, + line, + col, + lexeme, + errorCode, + String.format(format, args), + catchObj); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/ErrorReporter.java b/src/main/java/com/alibaba/qlexpress4/exception/ErrorReporter.java new file mode 100644 index 0000000..d9e7b69 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/ErrorReporter.java @@ -0,0 +1,18 @@ +package com.alibaba.qlexpress4.exception; + +public interface ErrorReporter { + + default QLRuntimeException report(Object catchObj, String errorCode, String reason) { + return reportFormatWithCatch(catchObj, errorCode, reason); + } + + default QLRuntimeException report(String errorCode, String reason) { + return reportFormatWithCatch(null, errorCode, reason); + } + + default QLRuntimeException reportFormat(String errorCode, String format, Object... args) { + return reportFormatWithCatch(null, errorCode, format, args); + } + + QLRuntimeException reportFormatWithCatch(Object catchObj, String errorCode, String format, Object... args); +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/ExMessageUtil.java b/src/main/java/com/alibaba/qlexpress4/exception/ExMessageUtil.java new file mode 100644 index 0000000..b0aeb33 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/ExMessageUtil.java @@ -0,0 +1,67 @@ +package com.alibaba.qlexpress4.exception; + +import java.text.MessageFormat; + +/** + * Author: DQinYuan + */ +public class ExMessageUtil { + + private static final String REPORT_TEMPLATE = "[Error {0}: {1}]\n[Near: {2}]\n{3}\n[Line: {4}, Column: {5}]"; + + private static final int SNIPPET_EXTENSION_LEN = 20; + + public static class ExMessage { + private final String message; + + private final String snippet; + + public ExMessage(String message, String snippet) { + this.message = message; + this.snippet = snippet; + } + + public String getMessage() { + return message; + } + + public String getSnippet() { + return snippet; + } + } + + public static ExMessage format(String script, int tokenStartPos, int tokenLine, int tokenCol, String lexeme, + String errorCode, String reason) { + int startReportPos = Math.max(tokenStartPos - SNIPPET_EXTENSION_LEN, 0); + int endReportPos = Math.min(tokenStartPos + lexeme.length() + SNIPPET_EXTENSION_LEN, script.length()); + + StringBuilder snippetBuilder = new StringBuilder(); + if (startReportPos > 0) { + snippetBuilder.append("..."); + } + for (int i = startReportPos; i < endReportPos; i++) { + char codeChar = script.charAt(i); + snippetBuilder.append(codeChar < ' ' ? ' ' : codeChar); + } + if (endReportPos < script.length()) { + snippetBuilder.append("..."); + } + + StringBuilder carteBuilder = new StringBuilder().append(" "); + if (startReportPos > 0) { + carteBuilder.append(" "); + } + for (int i = startReportPos; i < tokenStartPos; i++) { + carteBuilder.append(' '); + } + for (int i = 0; i < lexeme.length(); i++) { + carteBuilder.append('^'); + } + + String snippet = snippetBuilder.toString(); + String message = MessageFormat + .format(REPORT_TEMPLATE, errorCode, reason, snippet, carteBuilder.toString(), tokenLine, tokenCol); + return new ExMessage(message, snippet); + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/ExceptionFactory.java b/src/main/java/com/alibaba/qlexpress4/exception/ExceptionFactory.java new file mode 100644 index 0000000..72cf33e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/ExceptionFactory.java @@ -0,0 +1,11 @@ +package com.alibaba.qlexpress4.exception; + +/** + * Author: DQinYuan + */ +public interface ExceptionFactory { + + T newException(String message, int lineNo, int colNo, String errLexeme, String errorCode, String reason, + String snippet); + +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/PureErrReporter.java b/src/main/java/com/alibaba/qlexpress4/exception/PureErrReporter.java new file mode 100644 index 0000000..333a03b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/PureErrReporter.java @@ -0,0 +1,17 @@ +package com.alibaba.qlexpress4.exception; + +/** + * Author: DQinYuan + */ +public class PureErrReporter implements ErrorReporter { + + public static PureErrReporter INSTANCE = new PureErrReporter(); + + private PureErrReporter() { + } + + @Override + public QLRuntimeException reportFormatWithCatch(Object catchObj, String errorCode, String format, Object... args) { + return new QLRuntimeException(null, String.format(format, args), errorCode); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/QLErrorCodes.java b/src/main/java/com/alibaba/qlexpress4/exception/QLErrorCodes.java new file mode 100644 index 0000000..8e2c7b7 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/QLErrorCodes.java @@ -0,0 +1,83 @@ +package com.alibaba.qlexpress4.exception; + +/** + * Author: DQinYuan + */ +public enum QLErrorCodes { + // @formatter:off + // syntax error + SYNTAX_ERROR(""), + MISSING_INDEX("missing index expression"), + INVALID_NUMBER("invalid number"), + CLASS_NOT_FOUND("can not find class: %s"), + + // runtime error + INVALID_INDEX("index can only be number"), + INDEX_OUT_BOUND("index out of bound"), + NONINDEXABLE_OBJECT("object of class %s is not indexable"), + NONTRAVERSABLE_OBJECT("object of class %s is not traversable"), + NULL_FIELD_ACCESS("can not access field from null"), + NULL_METHOD_ACCESS("can not access method from null"), + FIELD_NOT_FOUND("'%s' field not found"), + SET_FIELD_UNKNOWN_ERROR("unknown error when setting field '%s' value"), + GET_FIELD_UNKNOWN_ERROR("unknown error when getting field '%s' value"), + INVOKE_METHOD_WITH_WRONG_ARGUMENTS("invoke method '%s' with wrong arguments"), + INVOKE_METHOD_INNER_ERROR("exception from inner when invoking method '%s'"), + INVOKE_METHOD_UNKNOWN_ERROR("unknown error when invoking method '%s'"), + INVOKE_FUNCTION_INNER_ERROR("exception from inner when invoking function '%s', error message: %s"), + FUNCTION_NOT_FOUND("function '%s' not found"), + FUNCTION_TYPE_MISMATCH("symbol '%s' is not a function type"), + INVOKE_LAMBDA_ERROR("error when invoking lambda"), + NULL_CALL("can not call null"), + OBJECT_NOT_CALLABLE("type '%s' is not callable"), + METHOD_NOT_FOUND("no suitable method '%s' found for args %s"), + INVOKE_CONSTRUCTOR_UNKNOWN_ERROR("unknown error when invoking constructor"), + INVOKE_CONSTRUCTOR_INNER_ERROR("exception from inner when invoking constructor"), + NO_SUITABLE_CONSTRUCTOR("no suitable constructor for types %s"), + EXECUTE_BLOCK_ERROR("error when executing block"), + INCOMPATIBLE_TYPE_CAST("incompatible cast from type: %s to type: %s"), + INVALID_CAST_TARGET("target for type cast must be a class, but accept %s"), + SCRIPT_TIME_OUT("script exceeds timeout milliseconds, which is %d ms"), + INCOMPATIBLE_ASSIGNMENT_TYPE("variable declared type %s, assigned with incompatible value type %s"), + FOR_EACH_ITERABLE_REQUIRED("for-each can only be applied to iterable"), + FOR_EACH_TYPE_MISMATCH("for-each type mismatch, required %s, but %s provided"), + FOR_EACH_UNKNOWN_ERROR("unknown error when executing for-each"), + FOR_INIT_ERROR("error when executing for init"), + FOR_BODY_ERROR("error when executing for body"), + FOR_UPDATE_ERROR("error when executing for update"), + FOR_CONDITION_ERROR("error when executing for condition"), + FOR_CONDITION_BOOL_REQUIRED("result of for condition must be bool"), + WHILE_CONDITION_BOOL_REQUIRED("result of while condition must be bool"), + WHILE_CONDITION_ERROR("error when executing while condition"), + CONDITION_BOOL_REQUIRED("result of condition expression must be bool"), + ARRAY_SIZE_NUM_REQUIRED("size of array must be number"), + EXCEED_MAX_ARR_LENGTH("array length %d, exceed max allowed length %d"), + INCOMPATIBLE_ARRAY_ITEM_TYPE("item %d with type %s incompatible with array type %s"), + INVALID_ASSIGNMENT("value %s is not assignable"), + EXECUTE_OPERATOR_EXCEPTION("exception when executing '%s %s %s'"), + INVALID_ARITHMETIC(""), + INVALID_BINARY_OPERAND("the '%s' operator can not be applied to leftType:%s with leftValue:%s and rightType:%s with rightValue:%s"), + INVALID_UNARY_OPERAND("the '%s' operator can not be applied to type %s with value %s"), + EXECUTE_FINAL_BLOCK_ERROR("error when executing final block in try...catch...final..."), + EXECUTE_TRY_BLOCK_ERROR("error when executing try... block"), + EXECUTE_CATCH_HANDLER_ERROR("error when executing handler of '%s'"), + + // operator restriction error + OPERATOR_NOT_ALLOWED("Script uses disallowed operator: %s"), + + // user defined exception + INVALID_ARGUMENT(""), + BIZ_EXCEPTION(""), + QL_THROW("qlexpress throw statement"); + // @formatter:on + + private final String errorMsg; + + QLErrorCodes(String errorMsg) { + this.errorMsg = errorMsg; + } + + public String getErrorMsg() { + return errorMsg; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/QLException.java b/src/main/java/com/alibaba/qlexpress4/exception/QLException.java new file mode 100644 index 0000000..84a8140 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/QLException.java @@ -0,0 +1,79 @@ +package com.alibaba.qlexpress4.exception; + +import com.alibaba.qlexpress4.exception.lsp.Diagnostic; +import com.alibaba.qlexpress4.exception.lsp.Position; +import com.alibaba.qlexpress4.exception.lsp.Range; + +public class QLException extends RuntimeException { + + private final Diagnostic diagnostic; + + protected QLException(String message, Diagnostic diagnostic) { + super(message); + this.diagnostic = diagnostic; + } + + public Diagnostic getDiagnostic() { + return diagnostic; + } + + public int getPos() { + return diagnostic.getPos(); + } + + public String getReason() { + return diagnostic.getMessage(); + } + + /** + * @return line no based 1 + */ + public int getLineNo() { + return diagnostic.getRange().getStart().getLine() + 1; + } + + /** + * @return col no based 1 + */ + public int getColNo() { + return diagnostic.getRange().getStart().getCharacter() + 1; + } + + public String getErrLexeme() { + return diagnostic.getLexeme(); + } + + public String getErrorCode() { + return diagnostic.getCode(); + } + + public static QLSyntaxException reportScannerErr(String script, int tokenStartPos, int line, int col, String lexeme, + String errorCode, String reason) { + ExMessageUtil.ExMessage exMessage = + ExMessageUtil.format(script, tokenStartPos, line, col, lexeme, errorCode, reason); + Diagnostic diagnostic = + toDiagnostic(tokenStartPos, line, col, lexeme, errorCode, reason, exMessage.getSnippet()); + return new QLSyntaxException(exMessage.getMessage(), diagnostic); + } + + public static QLRuntimeException reportRuntimeErrWithAttach(String script, int tokenStartPos, int line, int col, + String lexeme, String errorCode, String reason, Object catchObj) { + ExMessageUtil.ExMessage exMessage = + ExMessageUtil.format(script, tokenStartPos, line, col, lexeme, errorCode, reason); + Diagnostic diagnostic = + toDiagnostic(tokenStartPos, line, col, lexeme, errorCode, reason, exMessage.getSnippet()); + return errorCode.equals(QLErrorCodes.SCRIPT_TIME_OUT.name()) + ? new QLTimeoutException(catchObj, exMessage.getMessage(), diagnostic) + : new QLRuntimeException(catchObj, exMessage.getMessage(), diagnostic); + } + + private static Diagnostic toDiagnostic(int startPos, int line, int col, String lexeme, String errorCode, + String reason, String snippet) { + int zeroBasedLine = line - 1; + int zeroBasedCol = col - 1; + Position start = new Position(zeroBasedLine, zeroBasedCol); + Position end = new Position(zeroBasedLine, zeroBasedCol + lexeme.length()); + Range range = new Range(start, end); + return new Diagnostic(startPos, range, lexeme, errorCode, reason, snippet); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/QLRuntimeException.java b/src/main/java/com/alibaba/qlexpress4/exception/QLRuntimeException.java new file mode 100644 index 0000000..d486bda --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/QLRuntimeException.java @@ -0,0 +1,32 @@ +package com.alibaba.qlexpress4.exception; + +import com.alibaba.qlexpress4.exception.lsp.Diagnostic; +import com.alibaba.qlexpress4.exception.lsp.Range; + +public class QLRuntimeException extends QLException { + + /** + * catchObj can be catched at QLExpress catch clause + */ + private final Object catchObj; + + /* + * Visible for test + */ + protected QLRuntimeException(Object catchObj, String reason, String errorCode) { + super("", new Diagnostic(0, new Range(null, null), "", errorCode, reason, "")); + this.catchObj = catchObj; + } + + protected QLRuntimeException(Object catchObj, String message, Diagnostic diagnostic) { + super(message, diagnostic); + this.catchObj = catchObj; + if (catchObj instanceof Throwable) { + super.initCause((Throwable)catchObj); + } + } + + public Object getCatchObj() { + return catchObj; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/QLSyntaxException.java b/src/main/java/com/alibaba/qlexpress4/exception/QLSyntaxException.java new file mode 100644 index 0000000..764dff7 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/QLSyntaxException.java @@ -0,0 +1,12 @@ +package com.alibaba.qlexpress4.exception; + +import com.alibaba.qlexpress4.exception.lsp.Diagnostic; + +/** + * Author: DQinYuan + */ +public class QLSyntaxException extends QLException { + protected QLSyntaxException(String message, Diagnostic diagnostic) { + super(message, diagnostic); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/QLTimeoutException.java b/src/main/java/com/alibaba/qlexpress4/exception/QLTimeoutException.java new file mode 100644 index 0000000..cdb197d --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/QLTimeoutException.java @@ -0,0 +1,13 @@ +package com.alibaba.qlexpress4.exception; + +import com.alibaba.qlexpress4.exception.lsp.Diagnostic; + +public class QLTimeoutException extends QLRuntimeException { + protected QLTimeoutException(Object catchObj, String reason, String errorCode) { + super(catchObj, reason, errorCode); + } + + protected QLTimeoutException(Object catchObj, String message, Diagnostic diagnostic) { + super(catchObj, message, diagnostic); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/UserDefineException.java b/src/main/java/com/alibaba/qlexpress4/exception/UserDefineException.java new file mode 100644 index 0000000..3d3a891 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/UserDefineException.java @@ -0,0 +1,27 @@ +package com.alibaba.qlexpress4.exception; + +/** + * user define error message for custom function/operator + * Author: DQinYuan + */ +public class UserDefineException extends Exception { + + public enum ExceptionType { + INVALID_ARGUMENT, BIZ_EXCEPTION + }; + + private final ExceptionType type; + + public UserDefineException(String message) { + this(ExceptionType.BIZ_EXCEPTION, message); + } + + public UserDefineException(ExceptionType type, String message) { + super(message); + this.type = type; + } + + public ExceptionType getType() { + return type; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/lsp/Diagnostic.java b/src/main/java/com/alibaba/qlexpress4/exception/lsp/Diagnostic.java new file mode 100644 index 0000000..2cdd888 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/lsp/Diagnostic.java @@ -0,0 +1,70 @@ +package com.alibaba.qlexpress4.exception.lsp; + +/** + * Author: DQinYuan + */ +public class Diagnostic { + + /** + * start position in script + */ + private final int pos; + + /** + * The range at which the message applies. + */ + private final Range range; + + /** + * The diagnostic's code, which might appear in the user interface. + */ + private final String code; + + /** + * The diagnostic's message. + */ + private final String message; + + /** + * snippet near error position + */ + private final String snippet; + + /** + * lexeme in range + */ + private final String lexeme; + + public Diagnostic(int pos, Range range, String lexeme, String code, String message, String snippet) { + this.pos = pos; + this.range = range; + this.lexeme = lexeme; + this.code = code; + this.message = message; + this.snippet = snippet; + } + + public int getPos() { + return pos; + } + + public Range getRange() { + return range; + } + + public String getLexeme() { + return lexeme; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public String getSnippet() { + return snippet; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/lsp/Position.java b/src/main/java/com/alibaba/qlexpress4/exception/lsp/Position.java new file mode 100644 index 0000000..6567f88 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/lsp/Position.java @@ -0,0 +1,32 @@ +package com.alibaba.qlexpress4.exception.lsp; + +/** + * Author: DQinYuan + */ +public class Position { + + /** + * Line position in a document (zero-based). + */ + private final int line; + + /** + * Character offset on a line in a document (zero-based). + * If the character value is greater than the line length it defaults back + * to the line length. + */ + private final int character; + + public Position(int line, int character) { + this.line = line; + this.character = character; + } + + public int getLine() { + return line; + } + + public int getCharacter() { + return character; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/exception/lsp/Range.java b/src/main/java/com/alibaba/qlexpress4/exception/lsp/Range.java new file mode 100644 index 0000000..9da5a48 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/exception/lsp/Range.java @@ -0,0 +1,24 @@ +package com.alibaba.qlexpress4.exception.lsp; + +/** + * Author: DQinYuan + */ +public class Range { + + private final Position start; + + private final Position end; + + public Range(Position start, Position end) { + this.start = start; + this.end = end; + } + + public Position getStart() { + return start; + } + + public Position getEnd() { + return end; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/member/FieldHandler.java b/src/main/java/com/alibaba/qlexpress4/member/FieldHandler.java new file mode 100644 index 0000000..f22ecd3 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/member/FieldHandler.java @@ -0,0 +1,45 @@ +package com.alibaba.qlexpress4.member; + +import com.alibaba.qlexpress4.annotation.QLAlias; +import com.alibaba.qlexpress4.utils.QLAliasUtils; + +import java.lang.reflect.Field; + +/** + * Author: TaoKan + */ +public class FieldHandler { + public static class Preferred { + + public static String preHandleAlias(Class baseClass, String propertyName) { + Field[] fields = baseClass.getDeclaredFields(); + for (Field field : fields) { + if (QLAliasUtils.matchQLAlias(propertyName, field.getAnnotationsByType(QLAlias.class))) { + return field.getName(); + } + } + Class superclass = baseClass.getSuperclass(); + if (superclass != null) { + return preHandleAlias(superclass, propertyName); + } + return propertyName; + } + + public static Field gatherFieldRecursive(Class baseClass, String propertyName) { + Field[] fields = baseClass.getDeclaredFields(); + for (Field field : fields) { + if (propertyName.equals(field.getName())) { + return field; + } + if (QLAliasUtils.matchQLAlias(propertyName, field.getAnnotationsByType(QLAlias.class))) { + return field; + } + } + Class superclass = baseClass.getSuperclass(); + if (superclass != null) { + return gatherFieldRecursive(superclass, propertyName); + } + return null; + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/member/MethodHandler.java b/src/main/java/com/alibaba/qlexpress4/member/MethodHandler.java new file mode 100644 index 0000000..ae89ac3 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/member/MethodHandler.java @@ -0,0 +1,106 @@ +package com.alibaba.qlexpress4.member; + +import com.alibaba.qlexpress4.runtime.IMethod; +import com.alibaba.qlexpress4.utils.BasicUtil; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * Author: TaoKan + */ +public class MethodHandler { + + public static Method getGetter(Class clazz, String property) { + String isGet = BasicUtil.getIsGetter(property); + String getter = BasicUtil.getGetter(property); + GetterCandidateMethod mGetCandidate = null; + for (Method method : clazz.getMethods()) { + if ((isGet.equals(method.getName())) && method.getReturnType() == boolean.class + && BasicUtil.isPublic(method) && method.getParameterTypes().length == 0) { + GetterCandidateMethod isGetMethod = new GetterCandidateMethod(method, 2); + if (isPreferredGetter(mGetCandidate, isGetMethod)) { + mGetCandidate = isGetMethod; + } + } + else if (getter.equals(method.getName()) && BasicUtil.isPublic(method) + && method.getParameterTypes().length == 0) { + GetterCandidateMethod getterMethod = new GetterCandidateMethod(method, 1); + if (isPreferredGetter(mGetCandidate, getterMethod)) { + mGetCandidate = getterMethod; + } + } + } + return mGetCandidate == null ? null : mGetCandidate.getMethod(); + } + + public static boolean isPreferredGetter(GetterCandidateMethod before, GetterCandidateMethod after) { + if (before == null) { + return true; + } + return after.getPriority() >= before.getPriority(); + } + + public static Method getSetter(Class clazz, String property) { + property = BasicUtil.getSetter(property); + + for (Method method : clazz.getMethods()) { + if (BasicUtil.isPublic(method) && method.getParameterTypes().length == 1 + && property.equals(method.getName())) { + return method; + } + } + return null; + } + + public static boolean hasOnlyOneAbstractMethod(Method[] methods) { + int count = 0; + for (Method method : methods) { + if (Modifier.isAbstract(method.getModifiers())) { + count++; + if (count > 1) { + return false; + } + } + } + return count == 1; + } + + public static class Access { + public static Object accessMethodValue(IMethod method, Object bean, Object[] args) + throws IllegalArgumentException, InvocationTargetException, IllegalAccessException { + if (!method.isAccess()) { + method.setAccessible(true); + } + return method.invoke(bean, args); + } + } + + static class GetterCandidateMethod { + public Method getMethod() { + return method; + } + + public void setMethod(Method method) { + this.method = method; + } + + private Method method; + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + private int priority; + + public GetterCandidateMethod(Method method, int priority) { + this.method = method; + this.priority = priority; + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/operator/BlackOperatorCheckStrategy.java b/src/main/java/com/alibaba/qlexpress4/operator/BlackOperatorCheckStrategy.java new file mode 100644 index 0000000..66f21f5 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/operator/BlackOperatorCheckStrategy.java @@ -0,0 +1,34 @@ +package com.alibaba.qlexpress4.operator; + +import java.util.Collections; +import java.util.Set; + +/** + * Blacklist strategy that forbids specified operators + * If blackOperators is empty, all operators are allowed + * + * @author QLExpress Team + */ +public class BlackOperatorCheckStrategy implements OperatorCheckStrategy { + + private final Set blackOperators; + + public BlackOperatorCheckStrategy(Set blackOperators) { + if (blackOperators == null) { + this.blackOperators = Collections.emptySet(); + } + else { + this.blackOperators = Collections.unmodifiableSet(blackOperators); + } + } + + @Override + public boolean isAllowed(String operator) { + return !blackOperators.contains(operator); + } + + @Override + public Set getOperators() { + return blackOperators; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/operator/DefaultOperatorCheckStrategy.java b/src/main/java/com/alibaba/qlexpress4/operator/DefaultOperatorCheckStrategy.java new file mode 100644 index 0000000..261873f --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/operator/DefaultOperatorCheckStrategy.java @@ -0,0 +1,26 @@ +package com.alibaba.qlexpress4.operator; + +import java.util.Collections; +import java.util.Set; + +/** + * @author zhoutao + */ +public class DefaultOperatorCheckStrategy implements OperatorCheckStrategy { + + private static final DefaultOperatorCheckStrategy INSTANCE = new DefaultOperatorCheckStrategy(); + + public static DefaultOperatorCheckStrategy getInstance() { + return INSTANCE; + } + + @Override + public boolean isAllowed(String operator) { + return true; + } + + @Override + public Set getOperators() { + return Collections.emptySet(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/operator/OperatorCheckStrategy.java b/src/main/java/com/alibaba/qlexpress4/operator/OperatorCheckStrategy.java new file mode 100644 index 0000000..4b85213 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/operator/OperatorCheckStrategy.java @@ -0,0 +1,51 @@ +package com.alibaba.qlexpress4.operator; + +import java.util.Set; + +/** + * Operator restriction strategy interface. + * Defines the contract for checking whether an operator is allowed. + * + * Supported operators (organized by category): + * - arithmetic: {@code +, -, *, /, %, mod, +=, -=, *=, /=, %=} + * - assign: {@code =} + * - bit: {@code &, |, ^, ~, <<, >>, >>>, &=, |=, ^=, <<=, >>=, >>>=} + * - collection: {@code in, not_in} + * - compare: {@code ==, !=, <>, <, <=, >, >=} + * - logic: {@code &&, ||, !, and, or} + * - string: {@code like, not_like} + * - unary: {@code ++, --, +, - (unary)} + * - root: {@code instanceof} + * + * Usage example: + *
{@code
+ *   // Whitelist strategy - only allow + and *
+ *   Set allowed = new HashSet<>(Arrays.asList("+", "*"));
+ *   OperatorCheckStrategy strategy = OperatorCheckStrategy.whitelist(allowed);
+ *
+ *   // Blacklist strategy - forbid assignment operator
+ *   Set forbidden = new HashSet<>(Arrays.asList("="));
+ *   OperatorCheckStrategy strategy = OperatorCheckStrategy.blacklist(forbidden);
+ *
+ *   // Allow all operators
+ *   OperatorCheckStrategy strategy = OperatorCheckStrategy.allowAll();
+ * }
+ */ +public interface OperatorCheckStrategy { + + static OperatorCheckStrategy allowAll() { + return DefaultOperatorCheckStrategy.getInstance(); + } + + static OperatorCheckStrategy whitelist(Set allowedOperators) { + return new WhiteOperatorCheckStrategy(allowedOperators); + } + + static OperatorCheckStrategy blacklist(Set forbiddenOperators) { + return new BlackOperatorCheckStrategy(forbiddenOperators); + } + + boolean isAllowed(String operator); + + Set getOperators(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/operator/WhiteOperatorCheckStrategy.java b/src/main/java/com/alibaba/qlexpress4/operator/WhiteOperatorCheckStrategy.java new file mode 100644 index 0000000..b983263 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/operator/WhiteOperatorCheckStrategy.java @@ -0,0 +1,34 @@ +package com.alibaba.qlexpress4.operator; + +import java.util.Collections; +import java.util.Set; + +/** + * Whitelist strategy that only allows specified operators + * If allowedOperators is empty, no operators are allowed + * + * @author QLExpress Team + */ +public class WhiteOperatorCheckStrategy implements OperatorCheckStrategy { + + private final Set allowedOperators; + + public WhiteOperatorCheckStrategy(Set allowedOperators) { + if (allowedOperators == null) { + this.allowedOperators = Collections.emptySet(); + } + else { + this.allowedOperators = Collections.unmodifiableSet(allowedOperators); + } + } + + @Override + public boolean isAllowed(String operator) { + return allowedOperators.contains(operator); + } + + @Override + public Set getOperators() { + return allowedOperators; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/proxy/QLambdaInvocationHandler.java b/src/main/java/com/alibaba/qlexpress4/proxy/QLambdaInvocationHandler.java new file mode 100644 index 0000000..e6854f5 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/proxy/QLambdaInvocationHandler.java @@ -0,0 +1,26 @@ +package com.alibaba.qlexpress4.proxy; + +import com.alibaba.qlexpress4.runtime.QLambda; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * Author: TaoKan + */ +public class QLambdaInvocationHandler implements InvocationHandler { + private final QLambda qLambda; + + public QLambdaInvocationHandler(QLambda qLambda) { + this.qLambda = qLambda; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + return Modifier.isAbstract(method.getModifiers()) ? qLambda.call(args).getResult().get() + : method.getReturnType() == String.class && "toString".equals(method.getName()) ? "QLambdaProxy" + : method.invoke(args); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/DelegateQContext.java b/src/main/java/com/alibaba/qlexpress4/runtime/DelegateQContext.java new file mode 100644 index 0000000..73a2b28 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/DelegateQContext.java @@ -0,0 +1,113 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.function.CustomFunction; +import com.alibaba.qlexpress4.runtime.scope.QScope; +import com.alibaba.qlexpress4.runtime.trace.QTraces; + +import java.util.Collections; +import java.util.Map; + +/** + * Author: DQinYuan + */ +public class DelegateQContext implements QContext { + + private final QRuntime qRuntime; + + private QScope qScope; + + public DelegateQContext(QRuntime qRuntime, QScope qScope) { + this.qRuntime = qRuntime; + this.qScope = qScope; + } + + @Override + public long scriptStartTimeStamp() { + return qRuntime.scriptStartTimeStamp(); + } + + @Override + public Map attachment() { + return qRuntime.attachment(); + } + + @Override + public ReflectLoader getReflectLoader() { + return qRuntime.getReflectLoader(); + } + + @Override + public QTraces getTraces() { + return qRuntime.getTraces(); + } + + @Override + public Value getSymbol(String varName) { + return qScope.getSymbol(varName); + } + + @Override + public Object getSymbolValue(String varName) { + return qScope.getSymbolValue(varName); + } + + @Override + public void defineLocalSymbol(String varName, Class varClz, Object value) { + qScope.defineLocalSymbol(varName, varClz, value); + } + + @Override + public void defineFunction(String functionName, CustomFunction function) { + qScope.defineFunction(functionName, function); + } + + @Override + public CustomFunction getFunction(String functionName) { + return qScope.getFunction(functionName); + } + + @Override + public Map getFunctionTable() { + return qScope.getFunctionTable(); + } + + @Override + public void push(Value value) { + qScope.push(value); + } + + @Override + public Parameters pop(int number) { + return qScope.pop(number); + } + + @Override + public Value pop() { + return qScope.pop(); + } + + @Override + public Value peek() { + return qScope.peek(); + } + + @Override + public QScope getParent() { + return qScope.getParent(); + } + + @Override + public QScope getCurrentScope() { + return qScope; + } + + @Override + public QScope newScope() { + return qScope = qScope.newScope(); + } + + @Override + public void closeScope() { + qScope = qScope.getParent(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/ExceptionTable.java b/src/main/java/com/alibaba/qlexpress4/runtime/ExceptionTable.java new file mode 100644 index 0000000..5124818 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/ExceptionTable.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Author: DQinYuan + */ +public class ExceptionTable { + + public static final ExceptionTable EMPTY = new ExceptionTable(Collections.emptyList(), null); + + private final List, Integer>> handlerPosMap; + + /** + * nullable + */ + private final Integer finalPos; + + public ExceptionTable(List, Integer>> handlerPosMap, Integer finalPos) { + this.handlerPosMap = handlerPosMap; + this.finalPos = finalPos; + } + + public Integer getRelativePos(Object throwObj) { + for (Map.Entry, Integer> classHandlerMap : handlerPosMap) { + if (throwObj == null) { + return classHandlerMap.getValue(); + } + else if (classHandlerMap.getKey().isAssignableFrom(throwObj.getClass())) { + return classHandlerMap.getValue(); + } + } + return null; + } + + public Integer getFinalPos() { + return finalPos; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/FixedSizeStack.java b/src/main/java/com/alibaba/qlexpress4/runtime/FixedSizeStack.java new file mode 100644 index 0000000..77cc1be --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/FixedSizeStack.java @@ -0,0 +1,61 @@ +package com.alibaba.qlexpress4.runtime; + +/** + * Author: DQinYuan + */ +public class FixedSizeStack { + + private final Value[] elements; + + /** + * next element to push + */ + private int cursor = 0; + + public FixedSizeStack(int size) { + this.elements = new Value[size]; + } + + public void push(Value ele) { + elements[cursor++] = ele; + } + + public Value pop() { + return elements[--cursor]; + } + + public Value peak() { + return elements[cursor - 1]; + } + + public Parameters pop(int n) { + cursor -= n; + return new StackSwapParameters(elements, cursor, n); + } + + private static class StackSwapParameters implements Parameters { + + private final Value[] elements; + + private final int start; + + private final int length; + + private StackSwapParameters(Value[] elements, int start, int length) { + this.elements = elements; + this.start = start; + this.length = length; + } + + @Override + public Value get(int i) { + return i >= length ? null : elements[start + i]; + } + + @Override + public int size() { + return length; + } + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/IMethod.java b/src/main/java/com/alibaba/qlexpress4/runtime/IMethod.java new file mode 100644 index 0000000..b27effc --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/IMethod.java @@ -0,0 +1,24 @@ +package com.alibaba.qlexpress4.runtime; + +import java.lang.reflect.InvocationTargetException; + +/** + * Author: DQinYuan + */ +public interface IMethod { + + Class[] getParameterTypes(); + + boolean isVarArgs(); + + boolean isAccess(); + + void setAccessible(boolean flag); + + String getName(); + + Class getDeclaringClass(); + + Object invoke(Object obj, Object[] args) + throws InvocationTargetException, IllegalAccessException; +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/JvmIMethod.java b/src/main/java/com/alibaba/qlexpress4/runtime/JvmIMethod.java new file mode 100644 index 0000000..050f0e6 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/JvmIMethod.java @@ -0,0 +1,56 @@ +package com.alibaba.qlexpress4.runtime; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * Author: DQinYuan + */ +public class JvmIMethod implements IMethod { + private final Method method; + + public JvmIMethod(Method method) { + this.method = method; + } + + public Method getMethod() { + return method; + } + + @Override + public Class[] getParameterTypes() { + return method.getParameterTypes(); + } + + @Override + public boolean isVarArgs() { + return method.isVarArgs(); + } + + @Override + public boolean isAccess() { + return Modifier.isPublic(method.getDeclaringClass().getModifiers()) && Modifier.isPublic(method.getModifiers()); + } + + @Override + public void setAccessible(boolean flag) { + method.setAccessible(flag); + } + + @Override + public String getName() { + return method.getName(); + } + + @Override + public Class getDeclaringClass() { + return method.getDeclaringClass(); + } + + @Override + public Object invoke(Object obj, Object[] args) + throws InvocationTargetException, IllegalAccessException { + return method.invoke(obj, args); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/LeftValue.java b/src/main/java/com/alibaba/qlexpress4/runtime/LeftValue.java new file mode 100644 index 0000000..96e7f05 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/LeftValue.java @@ -0,0 +1,40 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.data.convert.ObjTypeConvertor; + +/** + * assignable value + *

+ * Author: DQinYuan + */ +public interface LeftValue extends Value { + + Class getDefinedType(); + + @Override + default Class getType() { + Class definedType = getDefinedType(); + return definedType == null ? Value.super.getType() : definedType; + } + + default void set(Object newValue, ErrorReporter errorReporter) { + Class defineType = getDefinedType(); + ObjTypeConvertor.QConverted result = ObjTypeConvertor.cast(newValue, defineType); + if (!result.isConvertible()) { + throw errorReporter.reportFormat(QLErrorCodes.INCOMPATIBLE_ASSIGNMENT_TYPE.name(), + QLErrorCodes.INCOMPATIBLE_ASSIGNMENT_TYPE.getErrorMsg(), + newValue == null ? "null" : newValue.getClass().getName(), + defineType.getName()); + } + setInner(result.getConverted()); + } + + void setInner(Object newValue); + + /** + * @return Nullable + */ + String getSymbolName(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/MemberResolver.java b/src/main/java/com/alibaba/qlexpress4/runtime/MemberResolver.java new file mode 100644 index 0000000..d89d684 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/MemberResolver.java @@ -0,0 +1,242 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.annotation.QLAlias; +import com.alibaba.qlexpress4.utils.BasicUtil; +import com.alibaba.qlexpress4.utils.CacheUtil; +import com.alibaba.qlexpress4.utils.QLAliasUtils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Author: DQinYuan + */ +public class MemberResolver { + + public enum MatchPriority { + MISMATCH(-1), + // e.g. Integer -> Number + EXTEND(0), + // e.g. BigDecimal -> int + NUMBER_DEMOTION(9), + // e.g. int -> long + // 1 -> 8 + NUMBER_PROMOTION(8), + // e.g. Integer -> int + UNBOX(9), + // e.g. QLambda -> Function, Runnable, ... + LAMBDA(10), + // e.g. Integer -> Integer + EQUAL(11); + + public final int priority; + + MatchPriority(int priority) { + this.priority = priority; + } + } + + public static Constructor resolveConstructor(Class cls, Class[] argTypes) { + Constructor[] constructors = cls.getConstructors(); + + // simple match + Class[][] candidates = new Class[constructors.length][]; + for (int i = 0; i < constructors.length; i++) { + candidates[i] = constructors[i].getParameterTypes(); + } + Integer bestIndex = resolveBestMatch(candidates, argTypes); + if (bestIndex != null) { + return constructors[bestIndex]; + } + + // var args match + List[]> varArgsCandidates = new ArrayList<>(constructors.length); + List varArgsConstructorI = new ArrayList<>(constructors.length); + for (int i = 0; i < constructors.length; i++) { + Constructor constructor = constructors[i]; + if (!constructor.isVarArgs()) { + continue; + } + varArgsCandidates.add(adapt2VarArgTypes(constructor.getParameterTypes(), argTypes.length)); + varArgsConstructorI.add(i); + } + Integer varArgBestIndex = resolveBestMatch(varArgsCandidates.toArray(new Class[0][]), argTypes); + if (varArgBestIndex == null) { + return null; + } + return constructors[varArgsConstructorI.get(varArgBestIndex)]; + } + + public static IMethod resolveMethod(Class cls, String methodName, Class[] argTypes, boolean isStatic, + boolean allowPrivate) { + Class curCls = cls; + List> inters = new ArrayList<>(); + while (curCls != null) { + IMethod method = resolveDeclaredMethod(curCls, methodName, argTypes, isStatic, allowPrivate); + if (method != null) { + return method; + } + // collect interfaces implemented by current class to search default methods later + Class[] curInters = curCls.getInterfaces(); + inters.addAll(Arrays.asList(curInters)); + + curCls = curCls.getSuperclass(); + } + // interface method + return resolveIntersMethod(inters.toArray(new Class[0]), methodName, argTypes, isStatic); + } + + private static IMethod resolveIntersMethod(Class[] inters, String methodName, Class[] argTypes, + boolean isStatic) { + for (Class inter : inters) { + IMethod method = resolveInterMethod(inter, methodName, argTypes, isStatic); + if (method != null) { + return method; + } + } + return null; + } + + private static IMethod resolveInterMethod(Class inter, String methodName, Class[] argTypes, + boolean isStatic) { + // no private method in interface, so pass false to 'allowPrivate' + IMethod method = resolveDeclaredMethod(inter, methodName, argTypes, isStatic, false); + if (method != null) { + return method; + } + return resolveIntersMethod(inter.getInterfaces(), methodName, argTypes, isStatic); + } + + public static int resolvePriority(Class[] paramTypes, Class[] argTypes) { + if (paramTypes.length != argTypes.length) { + return MatchPriority.MISMATCH.priority; + } + int methodPriority = MatchPriority.EQUAL.priority; + for (int i = 0; i < paramTypes.length; i++) { + Class paramType = paramTypes[i]; + Class argType = argTypes[i]; + int paramPriority = resolveArgPriority(paramType, argType); + if (paramPriority == MatchPriority.MISMATCH.priority) { + return paramPriority; + } + if (paramPriority < methodPriority) { + // methodPriority is min(paramPriority) + methodPriority = paramPriority; + } + } + return methodPriority; + } + + private static IMethod resolveDeclaredMethod(Class cls, String methodName, Class[] argTypes, boolean isStatic, + boolean allowPrivate) { + return resolveMethod(getDeclaredMethod(cls, methodName, isStatic, allowPrivate), argTypes); + } + + public static IMethod resolveMethod(List methods, Class[] argTypes) { + // simple match + Class[][] candidates = new Class[methods.size()][]; + for (int i = 0; i < methods.size(); i++) { + IMethod declaredMethod = methods.get(i); + candidates[i] = declaredMethod.getParameterTypes(); + } + Integer bestIndex = resolveBestMatch(candidates, argTypes); + if (bestIndex != null) { + return methods.get(bestIndex); + } + + // var args match + List[]> varArgsCandidates = new ArrayList<>(methods.size()); + List varArgsMethodI = new ArrayList<>(methods.size()); + for (int i = 0; i < methods.size(); i++) { + IMethod declaredMethod = methods.get(i); + if (!declaredMethod.isVarArgs()) { + continue; + } + varArgsCandidates.add(adapt2VarArgTypes(declaredMethod.getParameterTypes(), argTypes.length)); + varArgsMethodI.add(i); + } + Integer varArgBestIndex = resolveBestMatch(varArgsCandidates.toArray(new Class[0][]), argTypes); + if (varArgBestIndex == null) { + return null; + } + return methods.get(varArgsMethodI.get(varArgBestIndex)); + } + + private static List getDeclaredMethod(Class cls, String methodName, boolean isStatic, + boolean allowPrivate) { + Method[] declaredMethods = cls.getDeclaredMethods(); + List result = new ArrayList<>(declaredMethods.length); + for (Method declaredMethod : declaredMethods) { + if (!methodName.equals(declaredMethod.getName()) + && !QLAliasUtils.matchQLAlias(methodName, declaredMethod.getAnnotationsByType(QLAlias.class))) { + continue; + } + if ((!isStatic || BasicUtil.isStatic(declaredMethod)) + && (allowPrivate || BasicUtil.isPublic(declaredMethod))) { + result.add(new JvmIMethod(declaredMethod)); + } + } + return result; + } + + private static Class[] adapt2VarArgTypes(Class[] parameterTypes, int argLength) { + Class varItemType = parameterTypes[parameterTypes.length - 1].getComponentType(); + + Class[] varParamTypes = new Class[argLength]; + System.arraycopy(parameterTypes, 0, varParamTypes, 0, parameterTypes.length - 1); + for (int i = parameterTypes.length - 1; i < argLength; i++) { + varParamTypes[i] = varItemType; + } + return varParamTypes; + } + + public static Integer resolveBestMatch(Class[][] candidates, Class[] argTypes) { + Integer bestMatchIndex = null; + int bestPriority = MatchPriority.MISMATCH.priority; + for (int i = 0; i < candidates.length; i++) { + Class[] candidate = candidates[i]; + int priority = resolvePriority(candidate, argTypes); + if (priority > bestPriority) { + bestPriority = priority; + bestMatchIndex = i; + } + } + return bestMatchIndex; + } + + private static int resolveArgPriority(Class paramType, Class argType) { + if (paramType == argType) { + return MatchPriority.EQUAL.priority; + } + if (CacheUtil.isFunctionInterface(paramType) && QLambda.class.isAssignableFrom(argType)) { + return MatchPriority.LAMBDA.priority; + } + + Class primitiveArgCls = argType.isPrimitive() ? argType : BasicUtil.transToPrimitive(argType); + Class primitiveParamCls = paramType.isPrimitive() ? paramType : BasicUtil.transToPrimitive(paramType); + if (primitiveArgCls != null && primitiveArgCls == primitiveParamCls) { + return MatchPriority.UNBOX.priority; + } + + Integer paramNumLevel = BasicUtil.numberPromoteLevel(paramType); + Integer argNumLevel = BasicUtil.numberPromoteLevel(argType); + if (paramNumLevel != null && argNumLevel != null) { + return paramNumLevel >= argNumLevel ? MatchPriority.NUMBER_PROMOTION.priority + argNumLevel - paramNumLevel + : MatchPriority.NUMBER_DEMOTION.priority; + } + + // Handle primitive to Object boxing conversion + if (argType.isPrimitive() && paramType == Object.class) { + return MatchPriority.EXTEND.priority; + } + + if (argType == Nothing.class || paramType.isAssignableFrom(argType)) { + return MatchPriority.EXTEND.priority; + } + return MatchPriority.MISMATCH.priority; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/MetaClass.java b/src/main/java/com/alibaba/qlexpress4/runtime/MetaClass.java new file mode 100644 index 0000000..9adab77 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/MetaClass.java @@ -0,0 +1,34 @@ +package com.alibaba.qlexpress4.runtime; + +import java.util.Objects; + +/** + * Author: DQinYuan + */ +public class MetaClass { + + private final Class clz; + + public MetaClass(Class clz) { + this.clz = clz; + } + + public Class getClz() { + return clz; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + MetaClass metaClass = (MetaClass)o; + return clz.equals(metaClass.clz); + } + + @Override + public int hashCode() { + return Objects.hash(clz); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/Nothing.java b/src/main/java/com/alibaba/qlexpress4/runtime/Nothing.java new file mode 100644 index 0000000..dd7e6cf --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/Nothing.java @@ -0,0 +1,12 @@ +package com.alibaba.qlexpress4.runtime; + +/** + * subclass of any object + */ +public class Nothing { + + private Nothing() { + + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/Parameters.java b/src/main/java/com/alibaba/qlexpress4/runtime/Parameters.java new file mode 100644 index 0000000..a52ff0b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/Parameters.java @@ -0,0 +1,26 @@ +package com.alibaba.qlexpress4.runtime; + +/** + * Author: DQinYuan + */ +public interface Parameters { + + default Object getValue(int i) { + Value boxedValue = get(i); + return boxedValue == null ? null : boxedValue.get(); + } + + /** + * get parameters in i position + * + * @param i index + * @return value in index, null if exceed parameters' length + */ + Value get(int i); + + /** + * parameters size + * @return size + */ + int size(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QContext.java b/src/main/java/com/alibaba/qlexpress4/runtime/QContext.java new file mode 100644 index 0000000..3091659 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QContext.java @@ -0,0 +1,13 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.scope.QScope; + +/** + * Author: DQinYuan + */ +public interface QContext extends QScope, QRuntime { + + QScope getCurrentScope(); + + void closeScope(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QLambda.java b/src/main/java/com/alibaba/qlexpress4/runtime/QLambda.java new file mode 100644 index 0000000..f410a83 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QLambda.java @@ -0,0 +1,84 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public interface QLambda + extends Runnable, Supplier, Consumer, Predicate, Function { + + /** + * @param params params of lambda + * @return result of lambda + * @throws Throwable {@link com.alibaba.qlexpress4.exception.UserDefineException} for custom error message + */ + QResult call(Object... params) + throws Throwable; + + /** + * Get functions defined inside this lambda when invoked with parameters. + * + * @param params parameters passed to the lambda + * @return function table defined by the lambda (empty by default) + * @throws Throwable if user code throws an exception while collecting functions + */ + default Map getFunctionDefined(Object... params) + throws Throwable { + return Collections.emptyMap(); + } + + @Override + default Object get() { + try { + return call().getResult().get(); + } + catch (Throwable t) { + throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t); + } + } + + @Override + default void accept(Object o) { + try { + call(o); + } + catch (Throwable t) { + throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t); + } + } + + @Override + default void run() { + try { + call(); + } + catch (Throwable t) { + throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t); + } + } + + @Override + default boolean test(Object o) { + try { + return (boolean)call(o).getResult().get(); + } + catch (Throwable t) { + throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t); + } + } + + @Override + default Object apply(Object o) { + try { + return call(o).getResult().get(); + } + catch (Throwable t) { + throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t); + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaDefinition.java b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaDefinition.java new file mode 100644 index 0000000..cded31b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaDefinition.java @@ -0,0 +1,17 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.QLOptions; + +import java.util.function.Consumer; + +/** + * Author: DQinYuan + */ +public interface QLambdaDefinition { + + QLambda toLambda(QContext qContext, QLOptions qlOptions, boolean newEnv); + + void println(int depth, Consumer debug); + + String getName(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaDefinitionEmpty.java b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaDefinitionEmpty.java new file mode 100644 index 0000000..9c985c9 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaDefinitionEmpty.java @@ -0,0 +1,29 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Author: DQinYuan + */ +public class QLambdaDefinitionEmpty implements QLambdaDefinition { + + public static QLambdaDefinition INSTANCE = new QLambdaDefinitionEmpty(); + + @Override + public QLambda toLambda(QContext qContext, QLOptions qlOptions, boolean newEnv) { + return QLambdaEmpty.INSTANCE; + } + + @Override + public void println(int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, getName(), debug); + } + + @Override + public String getName() { + return "EmptyLambdaDefinition"; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaDefinitionInner.java b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaDefinitionInner.java new file mode 100644 index 0000000..425beba --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaDefinitionInner.java @@ -0,0 +1,89 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.runtime.instruction.CloseScopeInstruction; +import com.alibaba.qlexpress4.runtime.instruction.NewScopeInstruction; +import com.alibaba.qlexpress4.runtime.instruction.QLInstruction; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Author: DQinYuan + */ +public class QLambdaDefinitionInner implements QLambdaDefinition { + + /** + * function name + */ + private final String name; + + private final QLInstruction[] instructions; + + private final List paramsType; + + private final int maxStackSize; + + public QLambdaDefinitionInner(String name, List instructions, List paramsType, + int maxStackSize) { + this.name = name; + this.instructions = instructions.toArray(new QLInstruction[0]); + this.paramsType = paramsType; + this.maxStackSize = maxStackSize; + } + + public QLambdaDefinitionInner(String name, QLInstruction[] instructions, List paramsType, int maxStackSize) { + this.name = name; + this.instructions = instructions; + this.paramsType = paramsType; + this.maxStackSize = maxStackSize; + } + + @Override + public String getName() { + return name; + } + + public QLInstruction[] getInstructions() { + return instructions; + } + + public List getParamsType() { + return paramsType; + } + + public int getMaxStackSize() { + return maxStackSize; + } + + @Override + public QLambda toLambda(QContext qContext, QLOptions qlOptions, boolean newEnv) { + return new QLambdaInner(this, new DelegateQContext(qContext, qContext.getCurrentScope()), qlOptions, newEnv); + } + + @Override + public void println(int depth, Consumer debug) { + for (int i = 0; i < instructions.length; i++) { + instructions[i].println(i, depth, debug); + } + } + + public static class Param { + private final String name; + + private final Class clazz; + + public Param(String name, Class clazz) { + this.name = name; + this.clazz = clazz; + } + + public String getName() { + return name; + } + + public Class getClazz() { + return clazz; + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaEmpty.java b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaEmpty.java new file mode 100644 index 0000000..0af9291 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaEmpty.java @@ -0,0 +1,21 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +import java.util.Collections; +import java.util.Map; + +/** + * Author: DQinYuan + */ +public class QLambdaEmpty implements QLambda { + + public static QLambda INSTANCE = new QLambdaEmpty(); + + @Override + public QResult call(Object... params) + throws Throwable { + return new QResult(Value.NULL_VALUE, QResult.ResultType.RETURN); + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaInner.java b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaInner.java new file mode 100644 index 0000000..b2caabd --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaInner.java @@ -0,0 +1,99 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.runtime.data.AssignableDataValue; +import com.alibaba.qlexpress4.runtime.data.convert.ObjTypeConvertor; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; +import com.alibaba.qlexpress4.runtime.instruction.QLInstruction; +import com.alibaba.qlexpress4.runtime.scope.QvmBlockScope; + +import java.text.MessageFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Author: DQinYuan + */ +public class QLambdaInner implements QLambda { + + private final QLambdaDefinitionInner lambdaDefinition; + + private final QContext qContext; + + private final QLOptions qlOptions; + + private final boolean newEnv; + + public QLambdaInner(QLambdaDefinitionInner lambdaDefinition, QContext qContext, QLOptions qlOptions, + boolean newEnv) { + this.lambdaDefinition = lambdaDefinition; + this.qContext = qContext; + this.qlOptions = qlOptions; + this.newEnv = newEnv; + } + + public QResult call(Object... params) + throws Throwable { + return callInner(newEnv ? inheritScope(params) : qContext); + } + + @Override + public Map getFunctionDefined(Object... params) + throws Throwable { + QContext newRuntime = newEnv ? inheritScope(params) : qContext; + callInner(newRuntime); + return newRuntime.getFunctionTable(); + } + + private QResult callInner(QContext runtime) { + QLInstruction[] instructions = lambdaDefinition.getInstructions(); + for (int i = 0; i < instructions.length; i++) { + QResult qResult = instructions[i].execute(runtime, qlOptions); + switch (qResult.getResultType()) { + case JUMP: + i += (int)qResult.getResult().get(); + continue; + case RETURN: + case BREAK: + case CONTINUE: + return qResult; + } + } + + return QResult.NEXT_INSTRUCTION; + } + + private QContext inheritScope(Object[] params) + throws UserDefineException { + Map initSymbolTable = new HashMap<>(params.length); + List paramsDefinition = lambdaDefinition.getParamsType(); + for (int i = 0; i < Math.min(params.length, paramsDefinition.size()); i++) { + QLambdaDefinitionInner.Param paramDefinition = paramsDefinition.get(i); + Object originParamI = params[i]; + Class targetCls = paramDefinition.getClazz(); + ObjTypeConvertor.QConverted qlConvertResult = ObjTypeConvertor.cast(originParamI, targetCls); + if (!qlConvertResult.isConvertible()) { + throw new UserDefineException(UserDefineException.ExceptionType.INVALID_ARGUMENT, + MessageFormat.format( + "invalid argument at index {0} (start from 0), required type {1}, but {2} provided", + i, + targetCls.getName(), + originParamI == null ? "null" : originParamI.getClass().getName())); + } + initSymbolTable.put(paramDefinition.getName(), + new AssignableDataValue(paramDefinition.getName(), qlConvertResult.getConverted(), targetCls)); + } + // null for rest params + for (int i = params.length; i < paramsDefinition.size(); i++) { + QLambdaDefinitionInner.Param paramDefinition = paramsDefinition.get(i); + initSymbolTable.put(paramDefinition.getName(), + new AssignableDataValue(paramDefinition.getName(), null, paramDefinition.getClazz())); + } + QvmBlockScope newScope = + new QvmBlockScope(qContext, initSymbolTable, lambdaDefinition.getMaxStackSize(), ExceptionTable.EMPTY); + return new DelegateQContext(qContext, newScope); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaTrace.java b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaTrace.java new file mode 100644 index 0000000..75339e2 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QLambdaTrace.java @@ -0,0 +1,23 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.trace.QTraces; + +public class QLambdaTrace { + + private final QLambda qLambda; + + private final QTraces traces; + + public QLambdaTrace(QLambda qLambda, QTraces traces) { + this.qLambda = qLambda; + this.traces = traces; + } + + public QLambda getqLambda() { + return qLambda; + } + + public QTraces getTraces() { + return traces; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QResult.java b/src/main/java/com/alibaba/qlexpress4/runtime/QResult.java new file mode 100644 index 0000000..8e1c453 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QResult.java @@ -0,0 +1,44 @@ +package com.alibaba.qlexpress4.runtime; + +/** + * Author: DQinYuan + */ +public class QResult { + + public static final QResult LOOP_BREAK_RESULT = new QResult(Value.NULL_VALUE, ResultType.BREAK); + + public static final QResult LOOP_CONTINUE_RESULT = new QResult(Value.NULL_VALUE, ResultType.CONTINUE); + + public static final QResult NEXT_INSTRUCTION = new QResult(Value.NULL_VALUE, ResultType.NEXT_INSTRUCTION); + + public enum ResultType { + // break + BREAK, + // without return, different with return null + CONTINUE, + // jump to other instruction position. + // in this case, result is Value of int, which is the position to be jumped + JUMP, + // return from function/lambda/script + RETURN, + // execute next instruction + NEXT_INSTRUCTION + } + + public QResult(Value result, ResultType rType) { + this.result = result; + this.resultType = rType; + } + + private final Value result; + + private final ResultType resultType; + + public Value getResult() { + return result; + } + + public ResultType getResultType() { + return resultType; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QRuntime.java b/src/main/java/com/alibaba/qlexpress4/runtime/QRuntime.java new file mode 100644 index 0000000..c5a304b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QRuntime.java @@ -0,0 +1,23 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.trace.QTraces; + +import java.util.Map; + +/** + * Author: DQinYuan + */ +public interface QRuntime { + + /** + * get script start time + * @return start time + */ + long scriptStartTimeStamp(); + + Map attachment(); + + ReflectLoader getReflectLoader(); + + QTraces getTraces(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QvmGlobalScope.java b/src/main/java/com/alibaba/qlexpress4/runtime/QvmGlobalScope.java new file mode 100644 index 0000000..ec1caef --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QvmGlobalScope.java @@ -0,0 +1,105 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.runtime.context.ExpressContext; +import com.alibaba.qlexpress4.runtime.data.AssignableDataValue; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; +import com.alibaba.qlexpress4.runtime.scope.QScope; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * global scope + * + * Author: DQinYuan + */ +public class QvmGlobalScope implements QScope { + + private final ExpressContext externalVariable; + + private final Map newVariables; + + private final Map externalFunction; + + private final QLOptions qlOptions; + + public QvmGlobalScope(ExpressContext externalVariable, Map externalFunction, + QLOptions qlOptions) { + this.externalVariable = externalVariable; + this.newVariables = new HashMap<>(); + this.externalFunction = externalFunction; + this.qlOptions = qlOptions; + } + + @Override + public Value getSymbol(String varName) { + LeftValue newVariable = newVariables.get(varName); + if (newVariable != null) { + return newVariable; + } + Value externalValue = externalVariable.get(qlOptions.getAttachments(), varName); + if (externalValue != null && qlOptions.isPolluteUserContext()) { + return externalValue; + } + newVariable = new AssignableDataValue(varName, externalValue == null ? null : externalValue.get()); + newVariables.put(varName, newVariable); + return newVariable; + } + + @Override + public Object getSymbolValue(String varName) { + return QScope.super.getSymbolValue(varName); + } + + @Override + public void defineLocalSymbol(String varName, Class varClz, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public void defineFunction(String functionName, CustomFunction function) { + throw new UnsupportedOperationException(); + } + + @Override + public CustomFunction getFunction(String functionName) { + return externalFunction.get(functionName); + } + + @Override + public Map getFunctionTable() { + return externalFunction; + } + + @Override + public void push(Value value) { + throw new UnsupportedOperationException(); + } + + @Override + public Parameters pop(int number) { + throw new UnsupportedOperationException(); + } + + @Override + public Value pop() { + throw new UnsupportedOperationException(); + } + + @Override + public Value peek() { + throw new UnsupportedOperationException(); + } + + @Override + public QScope getParent() { + throw new UnsupportedOperationException(); + } + + @Override + public QScope newScope() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/QvmRuntime.java b/src/main/java/com/alibaba/qlexpress4/runtime/QvmRuntime.java new file mode 100644 index 0000000..65cbfc1 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/QvmRuntime.java @@ -0,0 +1,46 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.trace.QTraces; + +import java.util.Map; + +/** + * root runtime with external variable and function + * Author: DQinYuan + */ +public class QvmRuntime implements QRuntime { + + private final QTraces traces; + + private final Map attachments; + + private final ReflectLoader reflectLoader; + + private final long startTime; + + public QvmRuntime(QTraces traces, Map attachments, ReflectLoader reflectLoader, long startTime) { + this.traces = traces; + this.attachments = attachments; + this.reflectLoader = reflectLoader; + this.startTime = startTime; + } + + @Override + public long scriptStartTimeStamp() { + return startTime; + } + + @Override + public Map attachment() { + return attachments; + } + + public ReflectLoader getReflectLoader() { + return reflectLoader; + } + + @Override + public QTraces getTraces() { + return traces; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/ReflectLoader.java b/src/main/java/com/alibaba/qlexpress4/runtime/ReflectLoader.java new file mode 100644 index 0000000..d32012b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/ReflectLoader.java @@ -0,0 +1,461 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.member.FieldHandler; +import com.alibaba.qlexpress4.member.MethodHandler; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.data.FieldValue; +import com.alibaba.qlexpress4.runtime.data.MapItemValue; +import com.alibaba.qlexpress4.runtime.function.ExtensionFunction; +import com.alibaba.qlexpress4.runtime.function.FilterExtensionFunction; +import com.alibaba.qlexpress4.runtime.function.MapExtensionFunction; +import com.alibaba.qlexpress4.security.QLSecurityStrategy; +import com.alibaba.qlexpress4.security.StrategyIsolation; +import com.alibaba.qlexpress4.utils.BasicUtil; + +import java.io.Serializable; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * java reflect util with cache + * Author: DQinYuan + */ +public class ReflectLoader { + + private final QLSecurityStrategy securityStrategy; + + private final boolean allowPrivateAccess; + + private final Map>, Constructor> constructorCache = new ConcurrentHashMap<>(); + + private final Map, FieldReflectCache> fieldCache = new ConcurrentHashMap<>(); + + private final Map staticMethodCache = new ConcurrentHashMap<>(); + + private final Map memberMethodCache = new ConcurrentHashMap<>(); + + /** + * default extension functions + */ + private final List extensionFunctions = + new CopyOnWriteArrayList<>(Arrays.asList(FilterExtensionFunction.INSTANCE, MapExtensionFunction.INSTANCE)); + + public ReflectLoader(QLSecurityStrategy securityStrategy, boolean allowPrivateAccess) { + this.securityStrategy = securityStrategy; + this.allowPrivateAccess = allowPrivateAccess; + } + + public void addExtendFunction(ExtensionFunction extensionFunction) { + extensionFunctions.add(extensionFunction); + } + + public Constructor loadConstructor(Class cls, Class[] paramTypes) { + if (securityStrategy instanceof StrategyIsolation) { + return null; + } + + List> cacheKey = new ArrayList<>(paramTypes.length + 1); + cacheKey.add(cls); + cacheKey.addAll(Arrays.asList(paramTypes)); + + Constructor cachedConstructor = constructorCache.get(cacheKey); + if (cachedConstructor != null) { + return cachedConstructor; + } + + Constructor constructor = securityFilter(MemberResolver.resolveConstructor(cls, paramTypes)); + if (constructor == null) { + return null; + } + constructorCache.put(cacheKey, constructor); + return constructor; + } + + public Value loadField(Object bean, String fieldName, boolean skipSecurity, ErrorReporter errorReporter) { + if (bean.getClass().isArray() && BasicUtil.LENGTH.equals(fieldName)) { + return new DataValue(((Object[])bean).length); + } + else if (bean instanceof List && BasicUtil.LENGTH.equals(fieldName)) { + return new DataValue(((List)bean).size()); + } + else if (bean instanceof Map) { + return new MapItemValue((Map)bean, fieldName); + } + else if (!skipSecurity && securityStrategy instanceof StrategyIsolation) { + return null; + } + else if (bean instanceof MetaClass) { + MetaClass metaClass = (MetaClass)bean; + if (BasicUtil.CLASS.equals(fieldName)) { + return new DataValue(metaClass.getClz()); + } + return loadJavaField(metaClass.getClz(), null, fieldName, skipSecurity, errorReporter); + } + else { + return loadJavaField(bean.getClass(), bean, fieldName, skipSecurity, errorReporter); + } + } + + public IMethod loadMethod(Object bean, String methodName, Class[] argTypes) { + boolean isStaticMethod = bean instanceof MetaClass; + Class clz = isStaticMethod ? ((MetaClass)bean).getClz() : bean.getClass(); + // only support member extension method + if (!isStaticMethod) { + IMethod extendFunction = loadExtendFunction(clz, methodName, argTypes); + if (extendFunction != null) { + return extendFunction; + } + } + + if (securityStrategy instanceof StrategyIsolation) { + return null; + } + + MethodCacheKey cacheKey = new MethodCacheKey(clz, methodName, argTypes); + Map methodCache = isStaticMethod ? staticMethodCache : memberMethodCache; + IMethod cachedMethod = methodCache.get(cacheKey); + if (cachedMethod != null) { + return cachedMethod; + } + + IMethod method = securityFilterIMethod( + MemberResolver.resolveMethod(clz, methodName, argTypes, isStaticMethod, allowPrivateAccess)); + if (method == null) { + return null; + } + + methodCache.put(cacheKey, method); + return method; + } + + private IMethod securityFilterIMethod(IMethod iMethod) { + if (iMethod instanceof JvmIMethod) { + Method filterResult = securityFilter(((JvmIMethod)iMethod).getMethod()); + return filterResult == null ? null : iMethod; + } + else { + return iMethod; + } + } + + private IMethod loadExtendFunction(Class clz, String methodName, Class[] argTypes) { + List assignableExtensionFunctions = extensionFunctions.stream() + .filter(extensionFunction -> extensionFunction.getDeclaringClass().isAssignableFrom(clz) + && methodName.equals(extensionFunction.getName())) + .collect(Collectors.toList()); + if (assignableExtensionFunctions.isEmpty()) { + return null; + } + return MemberResolver.resolveMethod(assignableExtensionFunctions, argTypes); + } + + private Value loadJavaField(Class cls, Object bean, String fieldName, boolean skipSecurity, + ErrorReporter errorReporter) { + FieldReflectCache fieldReflectCache = loadFieldReflectCache(cls, fieldName, skipSecurity); + if (fieldReflectCache == null) { + return null; + } + Supplier getterOp = fieldReflectCache.getterSupplier.apply(errorReporter, bean); + if (fieldReflectCache.setterSupplier == null) { + return new DataValue(getterOp.get()); + } + Consumer setterOp = fieldReflectCache.setterSupplier.apply(errorReporter, bean); + return new FieldValue(getterOp, setterOp, fieldReflectCache.defType); + } + + private FieldReflectCache loadFieldReflectCache(Class cls, String fieldName, boolean skipSecurity) { + List cacheKey = Arrays.asList(cls, fieldName); + FieldReflectCache cachedField = fieldCache.get(cacheKey); + if (cachedField != null) { + return cachedField; + } + + FieldReflectCache fieldReflect = loadJavaFieldInner(cls, fieldName, skipSecurity); + if (fieldReflect != null) { + fieldCache.put(cacheKey, fieldReflect); + } + return fieldReflect; + } + + private FieldReflectCache loadJavaFieldInner(Class cls, String fieldName, boolean skipSecurity) { + String preHandledName = FieldHandler.Preferred.preHandleAlias(cls, fieldName); + Method getMethod = skipSecurity ? MethodHandler.getGetter(cls, preHandledName) + : securityFilter(MethodHandler.getGetter(cls, preHandledName)); + Field field = skipSecurity ? FieldHandler.Preferred.gatherFieldRecursive(cls, preHandledName) + : securityFilter(FieldHandler.Preferred.gatherFieldRecursive(cls, preHandledName)); + BiFunction> getterSupplier = fieldGetter(getMethod, field); + if (getterSupplier == null) { + return null; + } + Method setMethod = securityFilter(MethodHandler.getSetter(cls, preHandledName)); + BiFunction> setterSupplier = fieldSetter(setMethod, field); + return new FieldReflectCache(getterSupplier, setterSupplier, fieldDefCls(setMethod, field)); + } + + private T securityFilter(T member) { + return member == null ? null : (securityStrategy.check(member) ? member : null); + } + + private Class fieldDefCls(Method setMethod, Field field) { + return setMethod != null ? setMethod.getParameterTypes()[0] : field != null ? field.getType() : Object.class; + } + + private BiFunction> fieldSetter(Method setMethod, Field field) { + if (setMethod != null) { + if (BasicUtil.isPublic(setMethod)) { + return setMethodAccessible(setMethod); + } + if (allowPrivateAccess) { + return setMethodUnAccessible(setMethod); + } + } + if (field != null) { + if (BasicUtil.isPublic(field)) { + return setFieldAccessible(field); + } + if (allowPrivateAccess) { + return setFieldUnAccessible(field); + } + } + return null; + } + + private BiFunction> fieldGetter(Method getMethod, Field field) { + if (getMethod != null) { + if (BasicUtil.isPublic(getMethod)) { + return getMethodAccessible(getMethod); + } + if (allowPrivateAccess) { + return getMethodUnAccessible(getMethod); + } + } + if (field != null) { + if (BasicUtil.isPublic(field)) { + return getFieldAccessible(field); + } + if (allowPrivateAccess) { + return getFieldUnAccessible(field); + } + } + return null; + } + + private BiFunction> setMethodAccessible(Method setMethod) { + return (errorReporter, bean) -> newValue -> { + try { + setMethod.invoke(bean, newValue); + } + catch (Exception e) { + throw unwrapMethodInvokeEx(errorReporter, setMethod.getName(), e); + } + }; + } + + private BiFunction> setMethodUnAccessible(Method setMethod) { + return (errorReporter, bean) -> newValue -> { + try { + setMethod.setAccessible(true); + setMethod.invoke(bean, newValue); + } + catch (Exception e) { + throw unwrapMethodInvokeEx(errorReporter, setMethod.getName(), e); + } + }; + } + + private BiFunction> setFieldAccessible(Field field) { + return (errorReporter, bean) -> newValue -> { + try { + field.set(bean, newValue); + } + catch (Exception e) { + throw errorReporter.report(e, + QLErrorCodes.SET_FIELD_UNKNOWN_ERROR.name(), + String.format(QLErrorCodes.SET_FIELD_UNKNOWN_ERROR.getErrorMsg(), field.getName())); + } + }; + } + + private BiFunction> setFieldUnAccessible(Field field) { + return (errorReporter, bean) -> newValue -> { + try { + field.setAccessible(true); + field.set(bean, newValue); + } + catch (Exception e) { + throw errorReporter.report(e, + QLErrorCodes.SET_FIELD_UNKNOWN_ERROR.name(), + String.format(QLErrorCodes.SET_FIELD_UNKNOWN_ERROR.getErrorMsg(), field.getName())); + } + }; + } + + private BiFunction> getMethodAccessible(Method getMethod) { + return (errorReporter, bean) -> () -> { + try { + return getMethod.invoke(bean); + } + catch (Exception e) { + throw unwrapMethodInvokeEx(errorReporter, getMethod.getName(), e); + } + }; + } + + private BiFunction> getMethodUnAccessible(Method getMethod) { + return (errorReporter, bean) -> () -> { + try { + getMethod.setAccessible(true); + return getMethod.invoke(bean); + } + catch (Exception e) { + throw unwrapMethodInvokeEx(errorReporter, getMethod.getName(), e); + } + }; + } + + private BiFunction> getFieldAccessible(Field field) { + return (errorReporter, bean) -> () -> { + try { + return field.get(bean); + } + catch (Exception e) { + throw errorReporter.report(e, + QLErrorCodes.GET_FIELD_UNKNOWN_ERROR.name(), + String.format(QLErrorCodes.GET_FIELD_UNKNOWN_ERROR.getErrorMsg(), field.getName())); + } + }; + } + + private BiFunction> getFieldUnAccessible(Field field) { + return (errorReporter, bean) -> () -> { + try { + field.setAccessible(true); + return field.get(bean); + } + catch (Exception e) { + throw errorReporter.report(e, + QLErrorCodes.GET_FIELD_UNKNOWN_ERROR.name(), + String.format(QLErrorCodes.GET_FIELD_UNKNOWN_ERROR.getErrorMsg(), field.getName())); + } + }; + } + + public static QLRuntimeException unwrapMethodInvokeEx(ErrorReporter errorReporter, String methodName, + Exception ex) { + if (ex instanceof IllegalArgumentException) { + return errorReporter.reportFormat(QLErrorCodes.INVOKE_METHOD_WITH_WRONG_ARGUMENTS.name(), + String.format(QLErrorCodes.INVOKE_METHOD_WITH_WRONG_ARGUMENTS.getErrorMsg(), methodName)); + } + else if (ex instanceof InvocationTargetException) { + return errorReporter.report(((InvocationTargetException)ex).getTargetException(), + QLErrorCodes.INVOKE_METHOD_INNER_ERROR.name(), + String.format(QLErrorCodes.INVOKE_METHOD_INNER_ERROR.getErrorMsg(), methodName)); + } + else { + return errorReporter.report(ex, + QLErrorCodes.INVOKE_METHOD_UNKNOWN_ERROR.name(), + String.format(QLErrorCodes.INVOKE_METHOD_UNKNOWN_ERROR.getErrorMsg(), methodName)); + } + } + + private static class FieldReflectCache { + private final BiFunction> getterSupplier; + + private final BiFunction> setterSupplier; + + private final Class defType; + + private FieldReflectCache(BiFunction> getterSupplier, + BiFunction> setterSupplier, Class defType) { + this.getterSupplier = getterSupplier; + this.setterSupplier = setterSupplier; + this.defType = defType; + } + } + + private static class ExtensionMapKey { + private final Class cls; + + private final String methodName; + + public ExtensionMapKey(Class cls, String methodName) { + this.cls = cls; + this.methodName = methodName; + } + + public Class getCls() { + return cls; + } + + public String getMethodName() { + return methodName; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ExtensionMapKey that = (ExtensionMapKey)o; + return Objects.equals(cls, that.cls) && Objects.equals(methodName, that.methodName); + } + + @Override + public int hashCode() { + return Objects.hash(cls, methodName); + } + } + + private static class MethodCacheKey { + private final Class cls; + + private final String methodName; + + private final Class[] argTypes; + + public MethodCacheKey(Class cls, String methodName, Class[] argTypes) { + this.cls = cls; + this.methodName = methodName; + this.argTypes = argTypes; + } + + public Class getCls() { + return cls; + } + + public String getMethodName() { + return methodName; + } + + public Class[] getArgTypes() { + return argTypes; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + MethodCacheKey that = (MethodCacheKey)o; + return cls.equals(that.cls) && methodName.equals(that.methodName) && Arrays.equals(argTypes, that.argTypes); + } + + @Override + public int hashCode() { + int result = Objects.hash(cls, methodName); + result = 31 * result + Arrays.hashCode(argTypes); + return result; + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/Value.java b/src/main/java/com/alibaba/qlexpress4/runtime/Value.java new file mode 100644 index 0000000..1b299f8 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/Value.java @@ -0,0 +1,29 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.data.DataValue; + +/** + * Author: DQinYuan + */ +public interface Value { + Value NULL_VALUE = new DataValue((Object)null); + + Object get(); + + default Class getType() { + Object value = get(); + if (value == null) { + return Nothing.class; + } + + return value.getClass(); + } + + default String getTypeName() { + Class type = getType(); + if (type == null) { + return null; + } + return type.getName(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/context/DynamicVariableContext.java b/src/main/java/com/alibaba/qlexpress4/runtime/context/DynamicVariableContext.java new file mode 100644 index 0000000..999caae --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/context/DynamicVariableContext.java @@ -0,0 +1,51 @@ +package com.alibaba.qlexpress4.runtime.context; + +import com.alibaba.qlexpress4.Express4Runner; +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.data.MapItemValue; + +import java.util.HashMap; +import java.util.Map; + +public class DynamicVariableContext implements ExpressContext { + + private final Express4Runner runner; + + private final Map staticContext; + + private final QLOptions qlOptions; + + private final Map dynamicContext; + + public DynamicVariableContext(Express4Runner runner, Map staticContext, QLOptions qlOptions, + Map dynamicContext) { + this.runner = runner; + this.staticContext = staticContext; + this.qlOptions = qlOptions; + this.dynamicContext = dynamicContext; + } + + public DynamicVariableContext(Express4Runner runner, Map staticContext, QLOptions qlOptions) { + this.runner = runner; + this.staticContext = staticContext; + this.qlOptions = qlOptions; + this.dynamicContext = new HashMap<>(); + } + + public void put(String name, String valueExpression) { + dynamicContext.put(name, valueExpression); + } + + @Override + public Value get(Map attachments, String variableName) { + String dynamicScript = dynamicContext.get(variableName); + if (dynamicScript != null) { + QLResult result = runner.execute(dynamicScript, this, qlOptions); + return new DataValue(result.getResult()); + } + return new MapItemValue(staticContext, variableName); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/context/EmptyContext.java b/src/main/java/com/alibaba/qlexpress4/runtime/context/EmptyContext.java new file mode 100644 index 0000000..08cc24c --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/context/EmptyContext.java @@ -0,0 +1,12 @@ +package com.alibaba.qlexpress4.runtime.context; + +import com.alibaba.qlexpress4.runtime.Value; + +import java.util.Map; + +public class EmptyContext implements ExpressContext { + @Override + public Value get(Map attachments, String variableName) { + return Value.NULL_VALUE; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/context/ExpressContext.java b/src/main/java/com/alibaba/qlexpress4/runtime/context/ExpressContext.java new file mode 100644 index 0000000..af77f2d --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/context/ExpressContext.java @@ -0,0 +1,16 @@ +package com.alibaba.qlexpress4.runtime.context; + +import com.alibaba.qlexpress4.runtime.Value; + +import java.util.Map; + +/** + * Author: DQinYuan + */ +public interface ExpressContext { + + ExpressContext EMPTY_CONTEXT = new EmptyContext(); + + Value get(Map attachments, String variableName); + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/context/MapExpressContext.java b/src/main/java/com/alibaba/qlexpress4/runtime/context/MapExpressContext.java new file mode 100644 index 0000000..2a6b022 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/context/MapExpressContext.java @@ -0,0 +1,23 @@ +package com.alibaba.qlexpress4.runtime.context; + +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.MapItemValue; + +import java.util.Map; + +/** + * Author: DQinYuan + */ +public class MapExpressContext implements ExpressContext { + + private final Map source; + + public MapExpressContext(Map source) { + this.source = source; + } + + @Override + public Value get(Map attachments, String variableName) { + return new MapItemValue(source, variableName); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/context/ObjectFieldExpressContext.java b/src/main/java/com/alibaba/qlexpress4/runtime/context/ObjectFieldExpressContext.java new file mode 100644 index 0000000..e9d131c --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/context/ObjectFieldExpressContext.java @@ -0,0 +1,25 @@ +package com.alibaba.qlexpress4.runtime.context; + +import com.alibaba.qlexpress4.Express4Runner; +import com.alibaba.qlexpress4.exception.PureErrReporter; +import com.alibaba.qlexpress4.runtime.ReflectLoader; +import com.alibaba.qlexpress4.runtime.Value; + +import java.util.Map; + +public class ObjectFieldExpressContext implements ExpressContext { + + private final Object object; + + private final Express4Runner express4Runner; + + public ObjectFieldExpressContext(Object object, Express4Runner express4Runner) { + this.object = object; + this.express4Runner = express4Runner; + } + + @Override + public Value get(Map attachments, String variableName) { + return express4Runner.loadField(object, variableName); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/context/QLAliasContext.java b/src/main/java/com/alibaba/qlexpress4/runtime/context/QLAliasContext.java new file mode 100644 index 0000000..fc61a28 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/context/QLAliasContext.java @@ -0,0 +1,32 @@ +package com.alibaba.qlexpress4.runtime.context; + +import com.alibaba.qlexpress4.annotation.QLAlias; +import com.alibaba.qlexpress4.exception.QLException; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.MapItemValue; + +import java.util.HashMap; +import java.util.Map; + +public class QLAliasContext implements ExpressContext { + + private final Map context; + + public QLAliasContext(Object... os) { + Map context = new HashMap<>(); + for (Object o : os) { + QLAlias[] qlAliases = o.getClass().getAnnotationsByType(QLAlias.class); + for (QLAlias qlAlias : qlAliases) { + for (String alias : qlAlias.value()) { + context.put(alias, o); + } + } + } + this.context = context; + } + + @Override + public Value get(Map attachments, String variableName) { + return new MapItemValue(context, variableName); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/data/ArrayItemValue.java b/src/main/java/com/alibaba/qlexpress4/runtime/data/ArrayItemValue.java new file mode 100644 index 0000000..9a6b636 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/data/ArrayItemValue.java @@ -0,0 +1,40 @@ +package com.alibaba.qlexpress4.runtime.data; + +import com.alibaba.qlexpress4.runtime.LeftValue; + +import java.lang.reflect.Array; + +/** + * Author: DQinYuan + */ +public class ArrayItemValue implements LeftValue { + + private final Object array; + + private final int index; + + public ArrayItemValue(Object array, int index) { + this.array = array; + this.index = index; + } + + @Override + public void setInner(Object newValue) { + Array.set(array, index, newValue); + } + + @Override + public String getSymbolName() { + return null; + } + + @Override + public Object get() { + return Array.get(array, index); + } + + @Override + public Class getDefinedType() { + return array.getClass().getComponentType(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/data/AssignableDataValue.java b/src/main/java/com/alibaba/qlexpress4/runtime/data/AssignableDataValue.java new file mode 100644 index 0000000..e9e0cd7 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/data/AssignableDataValue.java @@ -0,0 +1,47 @@ +package com.alibaba.qlexpress4.runtime.data; + +import com.alibaba.qlexpress4.runtime.LeftValue; + +/** + * Author: DQinYuan + */ +public class AssignableDataValue implements LeftValue { + + private String symbolName; + + private Object value; + + private final Class defineType; + + public AssignableDataValue(String symbolName, Object value) { + this.symbolName = symbolName; + this.value = value; + this.defineType = null; + } + + public AssignableDataValue(String symbolName, Object value, Class defineType) { + this.symbolName = symbolName; + this.value = value; + this.defineType = defineType; + } + + @Override + public void setInner(Object newValue) { + this.value = newValue; + } + + @Override + public Object get() { + return value; + } + + @Override + public Class getDefinedType() { + return defineType; + } + + @Override + public String getSymbolName() { + return symbolName; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/data/DataValue.java b/src/main/java/com/alibaba/qlexpress4/runtime/data/DataValue.java new file mode 100644 index 0000000..60c8a1d --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/data/DataValue.java @@ -0,0 +1,24 @@ +package com.alibaba.qlexpress4.runtime.data; + +import com.alibaba.qlexpress4.runtime.Value; + +/** + * Author: TaoKan + */ +public class DataValue implements Value { + + private final Object value; + + public DataValue(Object value) { + this.value = value; + } + + public DataValue(Value value) { + this.value = value.get(); + } + + @Override + public Object get() { + return this.value; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/data/FieldValue.java b/src/main/java/com/alibaba/qlexpress4/runtime/data/FieldValue.java new file mode 100644 index 0000000..8f623da --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/data/FieldValue.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.data; + +import com.alibaba.qlexpress4.runtime.LeftValue; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Author: TaoKan + */ +public class FieldValue implements LeftValue { + private final Supplier getOp; + + private final Consumer setOp; + + private final Class defineType; + + public FieldValue(Supplier getOp, Consumer setOp, Class defineType) { + this.getOp = getOp; + this.setOp = setOp; + this.defineType = defineType; + } + + @Override + public void setInner(Object newValue) { + setOp.accept(newValue); + } + + @Override + public String getSymbolName() { + return null; + } + + @Override + public Object get() { + return getOp.get(); + } + + @Override + public Class getDefinedType() { + return this.defineType; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/data/ListItemValue.java b/src/main/java/com/alibaba/qlexpress4/runtime/data/ListItemValue.java new file mode 100644 index 0000000..6780f94 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/data/ListItemValue.java @@ -0,0 +1,40 @@ +package com.alibaba.qlexpress4.runtime.data; + +import com.alibaba.qlexpress4.runtime.LeftValue; + +import java.util.List; + +/** + * Author: DQinYuan + */ +public class ListItemValue implements LeftValue { + + private final List list; + + private final int index; + + public ListItemValue(List list, int index) { + this.list = list; + this.index = index; + } + + @Override + public void setInner(Object newValue) { + list.set(index, newValue); + } + + @Override + public String getSymbolName() { + return null; + } + + @Override + public Object get() { + return list.get(index); + } + + @Override + public Class getDefinedType() { + return Object.class; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/data/MapItemValue.java b/src/main/java/com/alibaba/qlexpress4/runtime/data/MapItemValue.java new file mode 100644 index 0000000..946316b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/data/MapItemValue.java @@ -0,0 +1,50 @@ +package com.alibaba.qlexpress4.runtime.data; + +import com.alibaba.qlexpress4.runtime.LeftValue; + +import java.util.Map; + +/** + * Author: DQinYuan + */ +@SuppressWarnings("all") +public class MapItemValue implements LeftValue { + + private final String symbolName; + + private final Map map; + + private final Object key; + + public MapItemValue(Map map, Object key) { + this.symbolName = null; + this.map = map; + this.key = key; + } + + public MapItemValue(String symbolName, Map map, Object key) { + this.symbolName = symbolName; + this.map = map; + this.key = key; + } + + @Override + public void setInner(Object newValue) { + map.put(key, newValue); + } + + @Override + public String getSymbolName() { + return symbolName; + } + + @Override + public Object get() { + return map.get(key); + } + + @Override + public Class getDefinedType() { + return null; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/data/convert/ObjTypeConvertor.java b/src/main/java/com/alibaba/qlexpress4/runtime/data/convert/ObjTypeConvertor.java new file mode 100644 index 0000000..eecb83c --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/data/convert/ObjTypeConvertor.java @@ -0,0 +1,163 @@ +package com.alibaba.qlexpress4.runtime.data.convert; + +import com.alibaba.qlexpress4.proxy.QLambdaInvocationHandler; +import com.alibaba.qlexpress4.runtime.Nothing; +import com.alibaba.qlexpress4.runtime.QLambda; +import com.alibaba.qlexpress4.runtime.operator.number.NumberMath; +import com.alibaba.qlexpress4.utils.CacheUtil; + +import java.lang.reflect.Proxy; +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Author: TaoKan + */ +public class ObjTypeConvertor { + + public static QConverted cast(Object value, Class type) { + // no need to convert + if (noNeedConvert(value, type)) { + return converted(value); + } + if (type == Character.class || type == char.class) { + return castChar(value); + } + if (CacheUtil.isFunctionInterface(type)) { + return castFunctionInter(value, type); + } + // unboxed boxed + if (type == boolean.class || type == Boolean.class) { + if (value instanceof Boolean) { + return converted(value); + } + return unConvertible(); + } + if (type == byte.class || type == Byte.class) { + if (value instanceof Byte) { + return converted(value); + } + if (value instanceof Number) { + return converted(((Number)value).byteValue()); + } + return unConvertible(); + } + if (type == short.class || type == Short.class) { + if (value instanceof Short) { + return converted(value); + } + if (value instanceof Number) { + return converted(((Number)value).shortValue()); + } + return unConvertible(); + } + if (type == int.class || type == Integer.class) { + if (value instanceof Integer) { + return converted(value); + } + if (value instanceof Number) { + return converted(((Number)value).intValue()); + } + return unConvertible(); + } + if (type == long.class || type == Long.class) { + if (value instanceof Long) { + return converted(value); + } + if (value instanceof Number) { + return converted(((Number)value).longValue()); + } + return unConvertible(); + } + if (type == float.class || type == Float.class) { + if (value instanceof Float) { + return converted(value); + } + if (value instanceof Number) { + return converted(((Number)value).floatValue()); + } + return unConvertible(); + } + if (type == double.class || type == Double.class) { + if (value instanceof Double) { + return converted(value); + } + if (value instanceof Number) { + return converted(((Number)value).doubleValue()); + } + return unConvertible(); + } + if (type == BigInteger.class) { + if (value instanceof Number) { + return converted(NumberMath.toBigInteger((Number)value)); + } + return unConvertible(); + } + if (type == BigDecimal.class) { + if (value instanceof Number) { + return converted(NumberMath.toBigDecimal((Number)value)); + } + return unConvertible(); + } + return unConvertible(); + } + + private static QConverted castFunctionInter(Object value, Class functionInter) { + if (value instanceof QLambda) { + return converted(Proxy.newProxyInstance(functionInter.getClassLoader(), + new Class[] {functionInter}, + new QLambdaInvocationHandler((QLambda)value))); + } + return unConvertible(); + } + + private static QConverted castChar(Object value) { + if (value instanceof Character) { + return converted(value); + } + if (value instanceof Number) { + return converted((char)((Number)value).intValue()); + } + if (value instanceof String) { + String strValue = (String)value; + if (strValue.length() == 1) { + return converted(strValue.charAt(0)); + } + return unConvertible(); + } + return unConvertible(); + } + + private static boolean noNeedConvert(Object value, Class type) { + return type == null || value == null || type.isInstance(value); + } + + private static QConverted converted(Object converted) { + return new QConverted(true, converted); + } + + private static QConverted unConvertible() { + return new QConverted(false, null); + } + + public static class QConverted { + + private final boolean convertible; + + private final Object converted; + + public QConverted(boolean convertible, Object converted) { + this.convertible = convertible; + this.converted = converted; + } + + public boolean isConvertible() { + return convertible; + } + + public Object getConverted() { + return converted; + } + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/data/convert/ParametersTypeConvertor.java b/src/main/java/com/alibaba/qlexpress4/runtime/data/convert/ParametersTypeConvertor.java new file mode 100644 index 0000000..7b153ca --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/data/convert/ParametersTypeConvertor.java @@ -0,0 +1,35 @@ +package com.alibaba.qlexpress4.runtime.data.convert; + +import java.lang.reflect.Array; + +/** + * Author: TaoKan + */ +public class ParametersTypeConvertor { + public static Object[] cast(Object[] arguments, Class[] paramTypes, boolean isVarArg) { + if (!isVarArg) { + Object[] result = new Object[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + Object argument = arguments[i]; + result[i] = ObjTypeConvertor.cast(argument, paramTypes[i]).getConverted(); + } + return result; + } + + Class itemType = paramTypes[paramTypes.length - 1].getComponentType(); + Object varArgs = Array.newInstance(itemType, arguments.length - paramTypes.length + 1); + int varArgStart = paramTypes.length - 1; + for (int i = varArgStart; i < arguments.length; i++) { + Object argument = arguments[i]; + Object castValue = ObjTypeConvertor.cast(argument, itemType).getConverted(); + Array.set(varArgs, i - varArgStart, castValue); + } + + Object[] result = new Object[paramTypes.length]; + for (int i = 0; i < paramTypes.length - 1; i++) { + result[i] = ObjTypeConvertor.cast(arguments[i], paramTypes[i]).getConverted(); + } + result[result.length - 1] = varArgs; + return result; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/data/lambda/QLambdaMethod.java b/src/main/java/com/alibaba/qlexpress4/runtime/data/lambda/QLambdaMethod.java new file mode 100644 index 0000000..4f228e2 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/data/lambda/QLambdaMethod.java @@ -0,0 +1,89 @@ +package com.alibaba.qlexpress4.runtime.data.lambda; + +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.member.MethodHandler; +import com.alibaba.qlexpress4.runtime.IMethod; +import com.alibaba.qlexpress4.runtime.MetaClass; +import com.alibaba.qlexpress4.runtime.QLambda; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.ReflectLoader; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.data.convert.ParametersTypeConvertor; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; +import com.alibaba.qlexpress4.utils.BasicUtil; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +/** + * Author: TaoKan + */ +public class QLambdaMethod implements QLambda { + + private final String methodName; + + private final ReflectLoader reflectLoader; + + private final Object bean; + + public QLambdaMethod(String methodName, ReflectLoader reflectLoader, Object obj) { + this.methodName = methodName; + this.reflectLoader = reflectLoader; + this.bean = obj; + } + + @Override + public QResult call(Object... params) + throws Exception { + if (bean instanceof MetaClass) { + Class[] type = BasicUtil.getTypeOfObject(params); + IMethod method = reflectLoader.loadMethod(bean, methodName, type); + if (method != null) { + // static method + Object[] convertResult = + ParametersTypeConvertor.cast(params, method.getParameterTypes(), method.isVarArgs()); + Object value = MethodHandler.Access.accessMethodValue(method, bean, convertResult); + return new QResult(new DataValue(value), QResult.ResultType.RETURN); + } + if (params.length < 1) { + throw createMethodNotFoundException(type); + } + if (((MetaClass)bean).getClz() != params[0].getClass()) { + throw createMethodNotFoundException(type); + } + Class[] restParamsType = Arrays.copyOfRange(type, 1, type.length); + method = reflectLoader.loadMethod(params[0], methodName, restParamsType); + if (method == null) { + throw createMethodNotFoundException(restParamsType); + } + Object paramBean = params[0]; + Object[] restParams = Arrays.copyOfRange(params, 1, params.length); + Object[] convertResult = + ParametersTypeConvertor.cast(restParams, method.getParameterTypes(), method.isVarArgs()); + Object value = MethodHandler.Access.accessMethodValue(method, paramBean, convertResult); + return new QResult(new DataValue(value), QResult.ResultType.RETURN); + } + else { + Class[] type = BasicUtil.getTypeOfObject(params); + IMethod method = reflectLoader.loadMethod(bean, methodName, type); + if (method == null) { + throw createMethodNotFoundException(type); + } + Object[] convertResult = + ParametersTypeConvertor.cast(params, method.getParameterTypes(), method.isVarArgs()); + Object value = MethodHandler.Access.accessMethodValue(method, bean, convertResult); + return new QResult(new DataValue(value), QResult.ResultType.RETURN); + } + } + + /** + * Create method not found exception + * @param type parameter type array + * @return UserDefineException exception object + */ + private UserDefineException createMethodNotFoundException(Class[] type) { + return new UserDefineException(UserDefineException.ExceptionType.INVALID_ARGUMENT, + "method reference '" + methodName + "' not found for argument types " + Arrays.toString(type)); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/function/CustomFunction.java b/src/main/java/com/alibaba/qlexpress4/runtime/function/CustomFunction.java new file mode 100644 index 0000000..0f854d3 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/function/CustomFunction.java @@ -0,0 +1,21 @@ +package com.alibaba.qlexpress4.runtime.function; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; + +/** + * Author: DQinYuan + */ +public interface CustomFunction { + + /** + * @param qContext context of current script run + * @param parameters parameters + * @return result of function + * @throws Throwable + * {@link com.alibaba.qlexpress4.exception.UserDefineException} for custom error message + */ + Object call(QContext qContext, Parameters parameters) + throws Throwable; + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/function/ExtensionFunction.java b/src/main/java/com/alibaba/qlexpress4/runtime/function/ExtensionFunction.java new file mode 100644 index 0000000..5a4f143 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/function/ExtensionFunction.java @@ -0,0 +1,27 @@ +package com.alibaba.qlexpress4.runtime.function; + +import com.alibaba.qlexpress4.runtime.IMethod; + +import java.lang.reflect.InvocationTargetException; + +/** + * Author: DQinYuan + */ +public abstract class ExtensionFunction implements IMethod { + + @Override + public boolean isVarArgs() { + return false; + } + + @Override + public boolean isAccess() { + return true; + } + + @Override + public void setAccessible(boolean flag) { + + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/function/FilterExtensionFunction.java b/src/main/java/com/alibaba/qlexpress4/runtime/function/FilterExtensionFunction.java new file mode 100644 index 0000000..2694064 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/function/FilterExtensionFunction.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime.function; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Author: DQinYuan + */ +public class FilterExtensionFunction extends ExtensionFunction { + + public static FilterExtensionFunction INSTANCE = new FilterExtensionFunction(); + + private FilterExtensionFunction() { + } + + @Override + public Class[] getParameterTypes() { + return new Class[] {Predicate.class}; + } + + @Override + public String getName() { + return "filter"; + } + + @Override + public Class getDeclaringClass() { + return List.class; + } + + @Override + public Object invoke(Object obj, Object[] args) + throws InvocationTargetException, IllegalAccessException { + if (!(obj instanceof List)) { + return null; + } + return ((List)obj).stream().filter((Predicate)args[0]).collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/function/MapExtensionFunction.java b/src/main/java/com/alibaba/qlexpress4/runtime/function/MapExtensionFunction.java new file mode 100644 index 0000000..83a621e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/function/MapExtensionFunction.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime.function; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Author: DQinYuan + */ +public class MapExtensionFunction extends ExtensionFunction { + + public static final MapExtensionFunction INSTANCE = new MapExtensionFunction(); + + private MapExtensionFunction() { + } + + @Override + public Class[] getParameterTypes() { + return new Class[] {Function.class}; + } + + @Override + public String getName() { + return "map"; + } + + @Override + public Class getDeclaringClass() { + return List.class; + } + + @Override + public Object invoke(Object obj, Object[] args) + throws InvocationTargetException, IllegalAccessException { + if (!(obj instanceof List)) { + return null; + } + return ((List)obj).stream().map((Function)args[0]).collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/function/QLambdaFunction.java b/src/main/java/com/alibaba/qlexpress4/runtime/function/QLambdaFunction.java new file mode 100644 index 0000000..7c93b01 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/function/QLambdaFunction.java @@ -0,0 +1,28 @@ +package com.alibaba.qlexpress4.runtime.function; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QLambda; +import com.alibaba.qlexpress4.runtime.QRuntime; + +/** + * Author: DQinYuan + */ +public class QLambdaFunction implements CustomFunction { + + private final QLambda qLambda; + + public QLambdaFunction(QLambda qLambda) { + this.qLambda = qLambda; + } + + @Override + public Object call(QContext qContext, Parameters parameters) + throws Throwable { + Object[] paramsArr = new Object[parameters.size()]; + for (int i = 0; i < paramsArr.length; i++) { + paramsArr[i] = parameters.get(i).get(); + } + return qLambda.call(paramsArr).getResult().get(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/function/QMethodFunction.java b/src/main/java/com/alibaba/qlexpress4/runtime/function/QMethodFunction.java new file mode 100644 index 0000000..28ddb39 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/function/QMethodFunction.java @@ -0,0 +1,51 @@ +package com.alibaba.qlexpress4.runtime.function; + +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.member.MethodHandler; +import com.alibaba.qlexpress4.runtime.IMethod; +import com.alibaba.qlexpress4.runtime.JvmIMethod; +import com.alibaba.qlexpress4.runtime.MemberResolver; +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.convert.ParametersTypeConvertor; +import com.alibaba.qlexpress4.utils.BasicUtil; + +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * Author: DQinYuan + */ +public class QMethodFunction implements CustomFunction { + + private final Object object; + + private final IMethod method; + + public QMethodFunction(Object object, Method method) { + this.object = object; + this.method = new JvmIMethod(method); + } + + @Override + public Object call(QContext qContext, Parameters parameters) + throws Throwable { + Class[] type = new Class[parameters.size()]; + Object[] params = new Object[parameters.size()]; + for (int i = 0; i < params.length; i++) { + Value v = parameters.get(i); + params[i] = v.get(); + type[i] = v.getType(); + } + + int priority = MemberResolver.resolvePriority(method.getParameterTypes(), type); + if (priority == MemberResolver.MatchPriority.MISMATCH.priority) { + throw new UserDefineException(UserDefineException.ExceptionType.INVALID_ARGUMENT, + "invalid argument types " + Arrays.toString(type) + " for java method '" + method.getName() + "'" + + " in declaring java class '" + method.getDeclaringClass().getName() + "'"); + } + Object[] convertResult = ParametersTypeConvertor.cast(params, method.getParameterTypes(), method.isVarArgs()); + return MethodHandler.Access.accessMethodValue(method, object, convertResult); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/BreakContinueInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/BreakContinueInstruction.java new file mode 100644 index 0000000..7fdd77a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/BreakContinueInstruction.java @@ -0,0 +1,47 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: return break object and exit lambda + * Input: 0 + * Output: 0 + *

+ * Author: DQinYuan + */ +public class BreakContinueInstruction extends QLInstruction { + + private final QResult result; + + public BreakContinueInstruction(ErrorReporter errorReporter, QResult result) { + super(errorReporter); + this.result = result; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + return result; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + String breakContinue = result == QResult.LOOP_BREAK_RESULT ? "Break" : "Continue"; + PrintlnUtils.printlnByCurDepth(depth, index + ": " + breakContinue, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CallConstInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CallConstInstruction.java new file mode 100644 index 0000000..dc23ba8 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CallConstInstruction.java @@ -0,0 +1,74 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.util.ThrowUtils; +import com.alibaba.qlexpress4.runtime.util.ValueUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: call const lambda + * Input: ${argNum} + * Output: 1 const lambda result + *

+ * Author: DQinYuan + */ +public class CallConstInstruction extends QLInstruction { + + private final QLambda constLambda; + + private final int argNum; + + private final String lambdaName; + + public CallConstInstruction(ErrorReporter errorReporter, QLambda constLambda, int argNum, String lambdaName) { + super(errorReporter); + this.constLambda = constLambda; + this.argNum = argNum; + this.lambdaName = lambdaName; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Parameters args = qContext.pop(argNum); + Object[] argArr = new Object[argNum]; + for (int i = 0; i < argNum; i++) { + argArr[i] = args.getValue(i); + } + + try { + QResult result = constLambda.call(argArr); + qContext.push(ValueUtils.toImmutable(result.getResult())); + return QResult.NEXT_INSTRUCTION; + } + catch (UserDefineException e) { + throw ThrowUtils.reportUserDefinedException(errorReporter, e); + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.EXECUTE_BLOCK_ERROR.name(), + QLErrorCodes.EXECUTE_BLOCK_ERROR.getErrorMsg()); + } + } + + @Override + public int stackInput() { + return argNum; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": CallConstLambda " + lambdaName, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CallFunctionInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CallFunctionInstruction.java new file mode 100644 index 0000000..efd1d80 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CallFunctionInstruction.java @@ -0,0 +1,121 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.runtime.util.ThrowUtils; +import com.alibaba.qlexpress4.runtime.util.ValueUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: call ql function from function table + * Input: ${argNum} + * Output: 1 function result + * Author: DQinYuan + */ +public class CallFunctionInstruction extends QLInstruction { + + private final String functionName; + + private final int argNum; + + private final Integer traceKey; + + public CallFunctionInstruction(ErrorReporter errorReporter, String functionName, int argNum, Integer traceKey) { + super(errorReporter); + this.functionName = functionName; + this.argNum = argNum; + this.traceKey = traceKey; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + CustomFunction function = qContext.getFunction(functionName); + if (function == null) { + callLambda(qContext, qlOptions); + return QResult.NEXT_INSTRUCTION; + } + Parameters parameters = qContext.pop(argNum); + try { + Object functionResultObj = function.call(qContext, parameters); + qContext.push(new DataValue(functionResultObj)); + + // trace + ExpressionTrace expressionTrace = qContext.getTraces().getExpressionTraceByKey(traceKey); + if (expressionTrace != null) { + expressionTrace.valueEvaluated(functionResultObj); + } + + return QResult.NEXT_INSTRUCTION; + } + catch (UserDefineException e) { + throw ThrowUtils.reportUserDefinedException(errorReporter, e); + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.INVOKE_FUNCTION_INNER_ERROR.name(), + String.format(QLErrorCodes.INVOKE_FUNCTION_INNER_ERROR.getErrorMsg(), functionName, t.getMessage())); + } + } + + private void callLambda(QContext qContext, QLOptions qlOptions) { + Object lambdaSymbol = qContext.getSymbolValue(functionName); + if (lambdaSymbol == null) { + if (qlOptions.isAvoidNullPointer()) { + qContext.pop(argNum); + qContext.push(DataValue.NULL_VALUE); + } + else { + throw errorReporter.report(new NullPointerException(), + QLErrorCodes.FUNCTION_NOT_FOUND.name(), + String.format(QLErrorCodes.FUNCTION_NOT_FOUND.getErrorMsg(), functionName)); + } + return; + } + if (!(lambdaSymbol instanceof QLambda)) { + throw errorReporter.report(QLErrorCodes.FUNCTION_TYPE_MISMATCH.name(), + String.format(QLErrorCodes.FUNCTION_TYPE_MISMATCH.getErrorMsg(), functionName)); + } + Parameters parameters = qContext.pop(argNum); + Object[] parametersArr = new Object[parameters.size()]; + for (int i = 0; i < parametersArr.length; i++) { + parametersArr[i] = parameters.get(i).get(); + } + try { + Value resultValue = ((QLambda)lambdaSymbol).call(parametersArr).getResult(); + qContext.push(ValueUtils.toImmutable(resultValue)); + } + catch (UserDefineException e) { + throw ThrowUtils.reportUserDefinedException(errorReporter, e); + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.INVOKE_LAMBDA_ERROR.name(), + QLErrorCodes.INVOKE_LAMBDA_ERROR.getErrorMsg()); + } + } + + @Override + public int stackInput() { + return argNum; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": CallFunction " + functionName + " " + argNum, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CallInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CallInstruction.java new file mode 100644 index 0000000..23e8b58 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CallInstruction.java @@ -0,0 +1,84 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.util.ThrowUtils; +import com.alibaba.qlexpress4.runtime.util.ValueUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: call a lambda with fixed number of arguments + * Input: ${argNum} + 1 + * Output: 1, lambda return result + *

+ * Author: DQinYuan + */ +public class CallInstruction extends QLInstruction { + + private final int argNum; + + public CallInstruction(ErrorReporter errorReporter, int argNum) { + super(errorReporter); + this.argNum = argNum; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Parameters parameters = qContext.pop(this.argNum + 1); + Object bean = parameters.get(0).get(); + if (bean == null) { + if (qlOptions.isAvoidNullPointer()) { + qContext.push(DataValue.NULL_VALUE); + return QResult.NEXT_INSTRUCTION; + } + else { + throw this.errorReporter.report(new NullPointerException(), + QLErrorCodes.NULL_CALL.name(), + QLErrorCodes.NULL_CALL.getErrorMsg()); + } + } + if (!(bean instanceof QLambda)) { + throw this.errorReporter.report(QLErrorCodes.OBJECT_NOT_CALLABLE.name(), + String.format(QLErrorCodes.OBJECT_NOT_CALLABLE.getErrorMsg(), bean.getClass().getName())); + } + Object[] params = new Object[this.argNum]; + for (int i = 0; i < this.argNum; i++) { + params[i] = parameters.get(i + 1).get(); + } + try { + QLambda qLambda = (QLambda)bean; + qContext.push(ValueUtils.toImmutable(qLambda.call(params).getResult())); + return QResult.NEXT_INSTRUCTION; + } + catch (UserDefineException e) { + throw ThrowUtils.reportUserDefinedException(errorReporter, e); + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.INVOKE_LAMBDA_ERROR.name(), + QLErrorCodes.INVOKE_LAMBDA_ERROR.getErrorMsg()); + } + } + + @Override + public int stackInput() { + return argNum + 1; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Call with argNum " + argNum, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CastInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CastInstruction.java new file mode 100644 index 0000000..fcdf723 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CastInstruction.java @@ -0,0 +1,75 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.MetaClass; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.data.convert.ObjTypeConvertor; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: force cast value to specified type + * Input: 2 targetCls and value + * Output: 1 casted value + *

+ * Author: DQinYuan + */ +public class CastInstruction extends QLInstruction { + + public CastInstruction(ErrorReporter errorReporter) { + super(errorReporter); + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Value value = qContext.pop(); + Class targetClz = popTargetClz(qContext.pop().get()); + if (value == null) { + qContext.push(Value.NULL_VALUE); + return QResult.NEXT_INSTRUCTION; + } + ObjTypeConvertor.QConverted result = ObjTypeConvertor.cast(value.get(), targetClz); + if (!result.isConvertible()) { + throw errorReporter.reportFormat(QLErrorCodes.INCOMPATIBLE_TYPE_CAST.name(), + QLErrorCodes.INCOMPATIBLE_TYPE_CAST.getErrorMsg(), + value.getTypeName(), + targetClz.getName()); + } + Value dataCast = new DataValue(result.getConverted()); + qContext.push(dataCast); + return QResult.NEXT_INSTRUCTION; + } + + private Class popTargetClz(Object target) { + if (target instanceof MetaClass) { + return ((MetaClass)target).getClz(); + } + else if (target instanceof Class) { + return (Class)target; + } + throw errorReporter.reportFormat(QLErrorCodes.INVALID_CAST_TARGET.name(), + QLErrorCodes.INVALID_CAST_TARGET.getErrorMsg(), + target == null ? "null" : target.getClass().getName()); + } + + @Override + public int stackInput() { + return 2; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Cast", debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CheckTimeOutInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CheckTimeOutInstruction.java new file mode 100644 index 0000000..94ff7d1 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CheckTimeOutInstruction.java @@ -0,0 +1,52 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: check if program timeout + * Input: 0 + * Output: 0 + *

+ * Author: DQinYuan + */ +public class CheckTimeOutInstruction extends QLInstruction { + public CheckTimeOutInstruction(ErrorReporter errorReporter) { + super(errorReporter); + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + if (qlOptions.getTimeoutMillis() <= 0) { + return QResult.NEXT_INSTRUCTION; + } + if (System.currentTimeMillis() - qContext.scriptStartTimeStamp() > qlOptions.getTimeoutMillis()) { + // timeout + throw errorReporter.reportFormat(QLErrorCodes.SCRIPT_TIME_OUT.name(), + QLErrorCodes.SCRIPT_TIME_OUT.getErrorMsg(), + qlOptions.getTimeoutMillis()); + } + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": CheckTimeout", debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CloseScopeInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CloseScopeInstruction.java new file mode 100644 index 0000000..1097f5e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/CloseScopeInstruction.java @@ -0,0 +1,47 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: close scope + * Input: 0 + * Output: 0 + * + * Author: DQinYuan + */ +public class CloseScopeInstruction extends QLInstruction { + + private final String scopeName; + + public CloseScopeInstruction(ErrorReporter errorReporter, String scopeName) { + super(errorReporter); + this.scopeName = scopeName; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + qContext.closeScope(); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": CloseScope " + scopeName, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ConstInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ConstInstruction.java new file mode 100644 index 0000000..f895d4f --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ConstInstruction.java @@ -0,0 +1,65 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: push constObj to stack + * Input: 0 + * Output: 1 + *

+ * Author: DQinYuan + */ +public class ConstInstruction extends QLInstruction { + + private final Object constObj; + + private final Integer traceKey; + + public ConstInstruction(ErrorReporter errorReporter, Object constObj, Integer traceKey) { + super(errorReporter); + this.constObj = constObj; + this.traceKey = traceKey; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + qContext.push(new DataValue(constObj)); + + // trace + ExpressionTrace expressionTrace = qContext.getTraces().getExpressionTraceByKey(traceKey); + if (expressionTrace != null) { + expressionTrace.valueEvaluated(constObj); + } + + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 1; + } + + public Object getConstObj() { + return constObj; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, + index + ": LoadConst " + (constObj == null ? "null" : constObj.toString()), + debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/DefineFunctionInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/DefineFunctionInstruction.java new file mode 100644 index 0000000..cf7be6f --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/DefineFunctionInstruction.java @@ -0,0 +1,52 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.function.QLambdaFunction; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: define function + * Input: 0 + * Output: 0 + *

+ * Author: DQinYuan + */ +public class DefineFunctionInstruction extends QLInstruction { + + private final String name; + + private final QLambdaDefinition lambdaDefinition; + + public DefineFunctionInstruction(ErrorReporter errorReporter, String name, QLambdaDefinition lambdaDefinition) { + super(errorReporter); + this.name = name; + this.lambdaDefinition = lambdaDefinition; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + QLambda lambda = lambdaDefinition.toLambda(qContext, qlOptions, true); + qContext.defineFunction(name, new QLambdaFunction(lambda)); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": DefineFunction " + name, debug); + lambdaDefinition.println(depth + 1, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/DefineLocalInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/DefineLocalInstruction.java new file mode 100644 index 0000000..b012b49 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/DefineLocalInstruction.java @@ -0,0 +1,60 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.convert.ObjTypeConvertor; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: define a symbol in local scope + * Input: 1 symbol init value + * Output: 0 + * + * Author: DQinYuan + */ +public class DefineLocalInstruction extends QLInstruction { + + private final String variableName; + + private final Class defineClz; + + public DefineLocalInstruction(ErrorReporter errorReporter, String variableName, Class defineClz) { + super(errorReporter); + this.variableName = variableName; + this.defineClz = defineClz; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Object initValue = qContext.pop().get(); + ObjTypeConvertor.QConverted qlConvertResult = ObjTypeConvertor.cast(initValue, defineClz); + if (!qlConvertResult.isConvertible()) { + throw errorReporter.reportFormat(QLErrorCodes.INCOMPATIBLE_ASSIGNMENT_TYPE.name(), + QLErrorCodes.INCOMPATIBLE_ASSIGNMENT_TYPE.getErrorMsg(), + defineClz.getName(), + initValue == null ? "null" : initValue.getClass().getName()); + } + qContext.defineLocalSymbol(variableName, defineClz, qlConvertResult.getConverted()); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": DefineLocal " + variableName, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ForEachInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ForEachInstruction.java new file mode 100644 index 0000000..d7c842e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ForEachInstruction.java @@ -0,0 +1,124 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.lang.reflect.Array; +import java.text.MessageFormat; +import java.util.Iterator; +import java.util.function.Consumer; + +/** + * Operation: process each element in iterable object on top of stack, + * Input: 1 + * Output: 0 + * + * Author: DQinYuan + */ +public class ForEachInstruction extends QLInstruction { + + private final QLambdaDefinition body; + + private final ErrorReporter targetErrorReporter; + + private final Class itCls; + + public ForEachInstruction(ErrorReporter errorReporter, QLambdaDefinition body, Class itCls, + ErrorReporter targetErrorReporter) { + super(errorReporter); + this.body = body; + this.itCls = itCls; + this.targetErrorReporter = targetErrorReporter; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Object mayBeIterable = qContext.pop().get(); + if (mayBeIterable != null && mayBeIterable.getClass().isArray()) { + mayBeIterable = new ReflectArrayIterable(mayBeIterable); + } + else if (!(mayBeIterable instanceof Iterable)) { + throw targetErrorReporter.report(QLErrorCodes.FOR_EACH_ITERABLE_REQUIRED.name(), + QLErrorCodes.FOR_EACH_ITERABLE_REQUIRED.getErrorMsg()); + } + Iterable iterable = (Iterable)mayBeIterable; + QLambda bodyLambda = body.toLambda(qContext, qlOptions, true); + forEachBody: for (Object item : iterable) { + try { + QResult bodyResult = bodyLambda.call(item); + switch (bodyResult.getResultType()) { + case RETURN: + return bodyResult; + case BREAK: + break forEachBody; + } + } + catch (UserDefineException e) { + throw errorReporter.reportFormat(QLErrorCodes.FOR_EACH_TYPE_MISMATCH.name(), + QLErrorCodes.FOR_EACH_TYPE_MISMATCH.getErrorMsg(), + itCls.getName(), + item == null ? "null" : item.getClass().getName()); + } + catch (Throwable t) { + if (t instanceof QLRuntimeException) { + throw (QLRuntimeException)t; + } + // should not run there + throw errorReporter.report(QLErrorCodes.FOR_EACH_UNKNOWN_ERROR.name(), + QLErrorCodes.FOR_EACH_UNKNOWN_ERROR.getErrorMsg()); + } + } + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": ForEach", debug); + body.println(depth + 1, debug); + } + + private static class ReflectArrayIterable implements Iterable { + + private final Object arrObj; + + private ReflectArrayIterable(Object arrObj) { + this.arrObj = arrObj; + } + + @Override + public Iterator iterator() { + return new ReflectArrayIterator(); + } + + private class ReflectArrayIterator implements Iterator { + + private int cursor; + + @Override + public boolean hasNext() { + return cursor < Array.getLength(arrObj); + } + + @Override + public Object next() { + return Array.get(arrObj, cursor++); + } + } + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ForInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ForInstruction.java new file mode 100644 index 0000000..46618ed --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ForInstruction.java @@ -0,0 +1,163 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.scope.QvmBlockScope; +import com.alibaba.qlexpress4.runtime.util.ThrowUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.HashMap; +import java.util.function.Consumer; + +/** + * Operation: traditional for loop + * Input: 0 + * Output: 0 + * Author: DQinYuan + */ +public class ForInstruction extends QLInstruction { + + /** + * nullable + */ + private final QLambdaDefinition forInit; + + /** + * nullable + */ + private final QLambdaDefinition condition; + + private final ErrorReporter conditionErrorReporter; + + /** + * nullable + */ + private final QLambdaDefinition forUpdate; + + private final int forScopeMaxStackSize; + + private final QLambdaDefinition forBody; + + public ForInstruction(ErrorReporter errorReporter, QLambdaDefinition forInit, QLambdaDefinition condition, + ErrorReporter conditionErrorReporter, QLambdaDefinition forUpdate, int forScopeMaxStackSize, + QLambdaDefinition forBody) { + super(errorReporter); + this.forInit = forInit; + this.condition = condition; + this.conditionErrorReporter = conditionErrorReporter; + this.forUpdate = forUpdate; + this.forScopeMaxStackSize = forScopeMaxStackSize; + this.forBody = forBody; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + QContext forScopeContext = + needForScope() + ? new DelegateQContext(qContext, + new QvmBlockScope(qContext, new HashMap<>(1), forScopeMaxStackSize, ExceptionTable.EMPTY)) + : qContext; + if (forInit != null) { + QLambda initLambda = forInit.toLambda(forScopeContext, qlOptions, false); + try { + initLambda.call(); + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.FOR_INIT_ERROR.name(), + QLErrorCodes.FOR_INIT_ERROR.getErrorMsg()); + } + } + + QLambda conditionLambda = condition != null ? condition.toLambda(forScopeContext, qlOptions, false) : null; + QLambda updateLambda = forUpdate != null ? forUpdate.toLambda(forScopeContext, qlOptions, false) : null; + QLambda bodyLambda = forBody.toLambda(forScopeContext, qlOptions, true); + + forBody: while (conditionLambda == null || evalCondition(conditionLambda)) { + try { + QResult bodyResult = bodyLambda.call(); + switch (bodyResult.getResultType()) { + case RETURN: + return bodyResult; + case BREAK: + break forBody; + } + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.FOR_BODY_ERROR.name(), + QLErrorCodes.FOR_BODY_ERROR.getErrorMsg()); + } + if (updateLambda != null) { + runUpdate(updateLambda); + } + } + return QResult.NEXT_INSTRUCTION; + } + + private boolean needForScope() { + return forInit != null || condition != null || forUpdate != null; + } + + private void runUpdate(QLambda updateLambda) { + try { + updateLambda.call(); + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.FOR_UPDATE_ERROR.name(), + QLErrorCodes.FOR_UPDATE_ERROR.getErrorMsg()); + } + } + + private boolean evalCondition(QLambda conditionLambda) { + try { + Object conditionResult = conditionLambda.call().getResult().get(); + if (!(conditionResult instanceof Boolean)) { + throw conditionErrorReporter.report(QLErrorCodes.FOR_CONDITION_BOOL_REQUIRED.name(), + QLErrorCodes.FOR_CONDITION_BOOL_REQUIRED.getErrorMsg()); + } + return (boolean)conditionResult; + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + conditionErrorReporter, + QLErrorCodes.FOR_CONDITION_ERROR.name(), + QLErrorCodes.FOR_CONDITION_ERROR.getErrorMsg()); + } + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": For", debug); + PrintlnUtils.printlnByCurDepth(depth + 1, "Init", debug); + if (forInit != null) { + forInit.println(depth + 2, debug); + } + PrintlnUtils.printlnByCurDepth(depth + 1, "Condition", debug); + if (condition != null) { + condition.println(depth + 2, debug); + } + PrintlnUtils.printlnByCurDepth(depth + 1, "Update", debug); + if (forUpdate != null) { + forUpdate.println(depth + 2, debug); + } + PrintlnUtils.printlnByCurDepth(depth + 1, "Body", debug); + forBody.println(depth + 2, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/GetFieldInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/GetFieldInstruction.java new file mode 100644 index 0000000..acaee78 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/GetFieldInstruction.java @@ -0,0 +1,72 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: get specified field of object on the top of stack + * Input: 1 + * Output: 1 + *

+ * Author: DQinYuan + */ +public class GetFieldInstruction extends QLInstruction { + + private final String fieldName; + + private final boolean optional; + + public GetFieldInstruction(ErrorReporter errorReporter, String fieldName, boolean optional) { + super(errorReporter); + this.fieldName = fieldName; + this.optional = optional; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Object bean = qContext.pop().get(); + if (bean == null) { + if (qlOptions.isAvoidNullPointer() || optional) { + qContext.push(DataValue.NULL_VALUE); + return QResult.NEXT_INSTRUCTION; + } + throw errorReporter.report(new NullPointerException(), + QLErrorCodes.NULL_FIELD_ACCESS.name(), + QLErrorCodes.NULL_FIELD_ACCESS.getErrorMsg()); + } + Value fieldValue = qContext.getReflectLoader().loadField(bean, fieldName, false, errorReporter); + if (fieldValue == null) { + throw errorReporter.reportFormat(QLErrorCodes.FIELD_NOT_FOUND.name(), + QLErrorCodes.FIELD_NOT_FOUND.getErrorMsg(), + fieldName); + } + qContext.push(fieldValue); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": GetField " + fieldName, debug); + } + + public String getFieldName() { + return fieldName; + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/GetMethodInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/GetMethodInstruction.java new file mode 100644 index 0000000..05c1dfe --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/GetMethodInstruction.java @@ -0,0 +1,61 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.data.lambda.QLambdaMethod; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: get specified method of object on the top of stack + * Input: 1 + * Output: 1 + *

+ * Author: DQinYuan + */ +public class GetMethodInstruction extends QLInstruction { + + private final String methodName; + + public GetMethodInstruction(ErrorReporter errorReporter, String methodName) { + super(errorReporter); + this.methodName = methodName; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Object bean = qContext.pop().get(); + if (bean == null) { + if (qlOptions.isAvoidNullPointer()) { + qContext.push(DataValue.NULL_VALUE); + return QResult.NEXT_INSTRUCTION; + } + throw this.errorReporter.report(new NullPointerException(), + QLErrorCodes.NULL_METHOD_ACCESS.name(), + QLErrorCodes.NULL_METHOD_ACCESS.getErrorMsg()); + } + ReflectLoader reflectLoader = qContext.getReflectLoader(); + qContext.push(new DataValue(new QLambdaMethod(methodName, reflectLoader, bean))); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": GetMethod " + methodName, debug); + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/IndexInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/IndexInstruction.java new file mode 100644 index 0000000..d03a4c1 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/IndexInstruction.java @@ -0,0 +1,94 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.ArrayItemValue; +import com.alibaba.qlexpress4.runtime.data.ListItemValue; +import com.alibaba.qlexpress4.runtime.data.MapItemValue; +import com.alibaba.qlexpress4.runtime.util.ValueUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.lang.reflect.Array; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Operation: extract value with index, like a[0], m['a'] + * Input: 2, indexable object and index + * Output: 1 + *

+ * Author: DQinYuan + */ +public class IndexInstruction extends QLInstruction { + + public IndexInstruction(ErrorReporter errorReporter) { + super(errorReporter); + } + + @SuppressWarnings("unchecked") + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Object index = qContext.pop().get(); + Object indexAble = qContext.pop().get(); + if (indexAble instanceof List) { + Number indexNumber = ValueUtils.assertType(index, + Number.class, + QLErrorCodes.INVALID_INDEX.name(), + QLErrorCodes.INVALID_INDEX.getErrorMsg(), + errorReporter); + List list = (List)indexAble; + int intIndex = ValueUtils.javaIndex(list.size(), indexNumber.intValue()); + if (intIndex < 0 || intIndex >= list.size()) { + throw errorReporter.report(QLErrorCodes.INDEX_OUT_BOUND.name(), + QLErrorCodes.INDEX_OUT_BOUND.getErrorMsg()); + } + qContext.push(new ListItemValue(list, intIndex)); + } + else if (indexAble instanceof Map) { + qContext.push(new MapItemValue((Map)indexAble, index)); + } + else if (indexAble != null && indexAble.getClass().isArray()) { + Number indexNumber = ValueUtils.assertType(index, + Number.class, + QLErrorCodes.INVALID_INDEX.name(), + QLErrorCodes.INVALID_INDEX.getErrorMsg(), + errorReporter); + int arrLen = Array.getLength(indexAble); + int intIndex = ValueUtils.javaIndex(arrLen, indexNumber.intValue()); + if (intIndex < 0 || intIndex >= arrLen) { + throw errorReporter.report(QLErrorCodes.INDEX_OUT_BOUND.name(), + QLErrorCodes.INDEX_OUT_BOUND.getErrorMsg()); + } + qContext.push(new ArrayItemValue(indexAble, intIndex)); + } + else if (indexAble == null && qlOptions.isAvoidNullPointer()) { + qContext.push(Value.NULL_VALUE); + } + else { + throw errorReporter.reportFormat(QLErrorCodes.NONINDEXABLE_OBJECT.name(), + QLErrorCodes.NONINDEXABLE_OBJECT.getErrorMsg(), + indexAble == null ? "null" : indexAble.getClass().getName()); + } + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 2; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Index", debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/JumpIfInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/JumpIfInstruction.java new file mode 100644 index 0000000..9f1ad83 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/JumpIfInstruction.java @@ -0,0 +1,83 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: if the element is ${expect}, jump to position, else execute next instruction as normal. not jump if null + * Input: 0 + * Output: 0 + * + * Author: DQinYuan + */ +public class JumpIfInstruction extends QLInstruction { + + private final boolean expect; + + private int position; + + private final Integer traceKey; + + public JumpIfInstruction(ErrorReporter errorReporter, boolean expect, int position, Integer traceKey) { + super(errorReporter); + this.expect = expect; + this.position = position; + this.traceKey = traceKey; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + boolean conditionBool = conditionToBool(qContext.peek().get()); + if (conditionBool == expect && !qlOptions.isShortCircuitDisable()) { + // short circuit + // trace + ExpressionTrace expressionTrace = qContext.getTraces().getExpressionTraceByKey(traceKey); + if (expressionTrace != null) { + expressionTrace.valueEvaluated(conditionBool); + expressionTrace.getChildren().get(0).valueEvaluated(conditionBool); + } + return new QResult(new DataValue(position), QResult.ResultType.JUMP); + } + else { + return QResult.NEXT_INSTRUCTION; + } + } + + private boolean conditionToBool(Object condition) { + if (condition == null) { + return !expect; + } + if (!(condition instanceof Boolean)) { + throw errorReporter.report(QLErrorCodes.CONDITION_BOOL_REQUIRED.name(), + QLErrorCodes.CONDITION_BOOL_REQUIRED.getErrorMsg()); + } + return (boolean)condition; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + public void setPosition(int position) { + this.position = position; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": JumpIf " + expect + " " + position, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/JumpIfPopInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/JumpIfPopInstruction.java new file mode 100644 index 0000000..b162e0d --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/JumpIfPopInstruction.java @@ -0,0 +1,68 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: pop the top of stack, if the element is ${expect}, jump to position, else execute next instruction as normal. jump if null + * Input: 1 + * Output: 0 + * + * Author: DQinYuan + */ +public class JumpIfPopInstruction extends QLInstruction { + + private final boolean expect; + + private int position; + + public JumpIfPopInstruction(ErrorReporter errorReporter, boolean expect, int position) { + super(errorReporter); + this.expect = expect; + this.position = position; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + boolean conditionBool = conditionToBool(qContext.pop().get()); + return conditionBool == expect ? new QResult(new DataValue(position), QResult.ResultType.JUMP) + : QResult.NEXT_INSTRUCTION; + } + + private boolean conditionToBool(Object condition) { + if (condition == null) { + return expect; + } + if (!(condition instanceof Boolean)) { + throw errorReporter.report(QLErrorCodes.CONDITION_BOOL_REQUIRED.name(), + QLErrorCodes.CONDITION_BOOL_REQUIRED.getErrorMsg()); + } + return (boolean)condition; + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 0; + } + + public void setPosition(int position) { + this.position = position; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": JumpIfPop " + expect + " " + position, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/JumpInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/JumpInstruction.java new file mode 100644 index 0000000..a9597c0 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/JumpInstruction.java @@ -0,0 +1,53 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: jump to a position + * Input: 0 + * Output: 0 + * + * Author: DQinYuan + */ +public class JumpInstruction extends QLInstruction { + + public static final JumpInstruction INSTANCE = new JumpInstruction(null, -1); + + private int position; + + public JumpInstruction(ErrorReporter errorReporter, int position) { + super(errorReporter); + this.position = position; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + return new QResult(new DataValue(position), QResult.ResultType.JUMP); + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + public void setPosition(int position) { + this.position = position; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Jump " + position, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/LoadInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/LoadInstruction.java new file mode 100644 index 0000000..60b942a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/LoadInstruction.java @@ -0,0 +1,62 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: load variable from local to global scope, create when not exist + * Input: 0 + * Output: 1 left value of local variable + * + * Author: DQinYuan + */ +public class LoadInstruction extends QLInstruction { + + private final String name; + + private final Integer traceKey; + + public LoadInstruction(ErrorReporter errorReporter, String name, Integer traceKey) { + super(errorReporter); + this.name = name; + this.traceKey = traceKey; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Value symbolValue = qContext.getSymbol(name); + qContext.push(symbolValue); + + // trace + ExpressionTrace expressionTrace = qContext.getTraces().getExpressionTraceByKey(traceKey); + if (expressionTrace != null) { + expressionTrace.valueEvaluated(symbolValue.get()); + } + + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 1; + } + + public String getName() { + return name; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Load " + name, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/LoadLambdaInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/LoadLambdaInstruction.java new file mode 100644 index 0000000..671c687 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/LoadLambdaInstruction.java @@ -0,0 +1,53 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: instantiate lambda definition on stack + * Input: 0 + * Output: 1 lambda instance + * + * Author: DQinYuan + */ +public class LoadLambdaInstruction extends QLInstruction { + + private final QLambdaDefinition lambdaDefinition; + + public LoadLambdaInstruction(ErrorReporter errorReporter, QLambdaDefinition lambdaDefinition) { + super(errorReporter); + this.lambdaDefinition = lambdaDefinition; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + QLambda lambdaInstance = lambdaDefinition.toLambda(qContext, qlOptions, true); + qContext.push(new DataValue(lambdaInstance)); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": LoadLambda", debug); + lambdaDefinition.println(depth + 1, debug); + } + + public QLambdaDefinition getLambdaDefinition() { + return lambdaDefinition; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/MethodInvokeInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/MethodInvokeInstruction.java new file mode 100644 index 0000000..eec1f6b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/MethodInvokeInstruction.java @@ -0,0 +1,82 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.util.MethodInvokeUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: invoke specified method of object on the top of stack + * Input: ${argNum} + 1 + * Output: 1, method return value, null for void method + *

+ * equivalent to GetMethodInstruction + CallInstruction + *

+ * Author: DQinYuan + */ +public class MethodInvokeInstruction extends QLInstruction { + + private final String methodName; + + private final int argNum; + + private final boolean optional; + + public MethodInvokeInstruction(ErrorReporter errorReporter, String methodName, int argNum, boolean optional) { + super(errorReporter); + this.methodName = methodName; + this.argNum = argNum; + this.optional = optional; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Parameters parameters = qContext.pop(this.argNum + 1); + Object bean = parameters.get(0).get(); + Class[] type = new Class[this.argNum]; + Object[] params = new Object[this.argNum]; + for (int i = 0; i < this.argNum; i++) { + Value v = parameters.get(i + 1); + params[i] = v.get(); + type[i] = v.getType(); + } + if (bean == null) { + if (qlOptions.isAvoidNullPointer() || optional) { + qContext.push(DataValue.NULL_VALUE); + return QResult.NEXT_INSTRUCTION; + } + throw errorReporter.report(new NullPointerException(), + QLErrorCodes.NULL_METHOD_ACCESS.name(), + QLErrorCodes.NULL_METHOD_ACCESS.getErrorMsg()); + } + Value invokeRes = MethodInvokeUtils + .findMethodAndInvoke(bean, methodName, params, type, qContext.getReflectLoader(), errorReporter); + qContext.push(invokeRes); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return argNum + 1; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": MethodInvoke " + methodName + " with argNum " + argNum, debug); + } + + public String getMethodName() { + return methodName; + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/MultiNewArrayInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/MultiNewArrayInstruction.java new file mode 100644 index 0000000..9bd226a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/MultiNewArrayInstruction.java @@ -0,0 +1,71 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.lang.reflect.Array; +import java.util.function.Consumer; + +/** + * new int[1][2][][] + * Operation: new array with multi dims + * Input: ${dims} + * Output: 1 + * Author: DQinYuan + */ +public class MultiNewArrayInstruction extends QLInstruction { + + private final Class clz; + + private final int dims; + + public MultiNewArrayInstruction(ErrorReporter errorReporter, Class clz, int dims) { + super(errorReporter); + this.clz = clz; + this.dims = dims; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Parameters dimValues = qContext.pop(dims); + int[] dimArray = new int[dims]; + for (int i = 0; i < dims; i++) { + Object dimValue = dimValues.get(i).get(); + if (!(dimValue instanceof Number)) { + throw errorReporter.reportFormat(QLErrorCodes.ARRAY_SIZE_NUM_REQUIRED.name(), + QLErrorCodes.ARRAY_SIZE_NUM_REQUIRED.getErrorMsg()); + } + int dimLen = ((Number)dimValue).intValue(); + if (!qlOptions.checkArrLen(dimLen)) { + throw errorReporter.reportFormat(QLErrorCodes.EXCEED_MAX_ARR_LENGTH.name(), + QLErrorCodes.EXCEED_MAX_ARR_LENGTH.getErrorMsg(), + dimLen, + qlOptions.getMaxArrLength()); + } + dimArray[i] = dimLen; + } + qContext.push(new DataValue(Array.newInstance(clz, dimArray))); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return dims; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": MultiNewArray with dims " + dims, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewArrayInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewArrayInstruction.java new file mode 100644 index 0000000..17dd7ea --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewArrayInstruction.java @@ -0,0 +1,79 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.data.convert.ObjTypeConvertor; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.lang.reflect.Array; +import java.util.function.Consumer; + +/** + * new int[] {1,2,3} + * Operation: new array with init items + * Input: ${length} + * Output: 1 + * Author: DQinYuan + */ +public class NewArrayInstruction extends QLInstruction { + + private final Class clz; + + private final int length; + + public NewArrayInstruction(ErrorReporter errorReporter, Class clz, int length) { + super(errorReporter); + this.clz = clz; + this.length = length; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + if (!qlOptions.checkArrLen(length)) { + throw errorReporter.reportFormat(QLErrorCodes.EXCEED_MAX_ARR_LENGTH.name(), + QLErrorCodes.EXCEED_MAX_ARR_LENGTH.getErrorMsg(), + length, + qlOptions.getMaxArrLength()); + } + Object array = Array.newInstance(clz, length); + Parameters initItems = qContext.pop(length); + for (int i = 0; i < initItems.size(); i++) { + Object initItemObj = initItems.get(i).get(); + ObjTypeConvertor.QConverted qlConvertResult = ObjTypeConvertor.cast(initItemObj, clz); + if (!qlConvertResult.isConvertible()) { + throw errorReporter.reportFormat(QLErrorCodes.INCOMPATIBLE_ARRAY_ITEM_TYPE.name(), + QLErrorCodes.INCOMPATIBLE_ARRAY_ITEM_TYPE.getErrorMsg(), + i, + initItemObj == null ? "null" : initItemObj.getClass().getName(), + clz.getName()); + } + Array.set(array, i, qlConvertResult.getConverted()); + } + qContext.push(new DataValue(array)); + return QResult.NEXT_INSTRUCTION; + } + + public Class getClz() { + return clz; + } + + @Override + public int stackInput() { + return length; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": NewArray with length " + length, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewFilledInstanceInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewFilledInstanceInstruction.java new file mode 100644 index 0000000..7ac6602 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewFilledInstanceInstruction.java @@ -0,0 +1,93 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.function.Consumer; + +/** + * Operation: new a Map with top ${keys.length} stack element + * Input: ${keys.length} + * Output: 1 + *

+ * Author: DQinYuan + */ +public class NewFilledInstanceInstruction extends QLInstruction { + + private final Class newCls; + + private final List keys; + + public NewFilledInstanceInstruction(ErrorReporter errorReporter, Class newCls, List keys) { + super(errorReporter); + this.newCls = newCls; + this.keys = keys; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Object instance = newInstance(qContext); + Parameters initItems = qContext.pop(keys.size()); + for (int i = 0; i < keys.size(); i++) { + Object initValue = initItems.get(i).get(); + String fieldName = keys.get(i); + Value fieldValue = qContext.getReflectLoader().loadField(instance, fieldName, false, errorReporter); + if (fieldValue == null) { + // ignore field that don't exist + continue; + } + if (!(fieldValue instanceof LeftValue)) { + throw errorReporter.reportFormat(QLErrorCodes.INVALID_ASSIGNMENT.name(), + QLErrorCodes.INVALID_ASSIGNMENT.getErrorMsg(), + "of field '" + fieldName + "'"); + } + ((LeftValue)fieldValue).set(initValue, errorReporter); + } + qContext.push(new DataValue(instance)); + return QResult.NEXT_INSTRUCTION; + } + + private Object newInstance(QContext qContext) { + Constructor constructor = qContext.getReflectLoader().loadConstructor(newCls, new Class[0]); + try { + return constructor.newInstance(); + } + catch (InvocationTargetException e) { + throw errorReporter.report(e.getTargetException(), + QLErrorCodes.INVOKE_CONSTRUCTOR_INNER_ERROR.name(), + QLErrorCodes.INVOKE_CONSTRUCTOR_INNER_ERROR.getErrorMsg()); + } + catch (Exception e) { + throw errorReporter.report(QLErrorCodes.INVOKE_CONSTRUCTOR_UNKNOWN_ERROR.name(), + QLErrorCodes.INVOKE_CONSTRUCTOR_UNKNOWN_ERROR.getErrorMsg()); + } + } + + @Override + public int stackInput() { + return keys.size(); + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, + index + ": New instace of cls " + newCls.getSimpleName() + " with fields " + keys, + debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewInstanceInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewInstanceInstruction.java new file mode 100644 index 0000000..9e26aab --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewInstanceInstruction.java @@ -0,0 +1,92 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.data.convert.ParametersTypeConvertor; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.function.Consumer; + +/** + * Operation: new an object of specified class + * Input: ${argNum} + 1 + * Output: 1 + *

+ * Author: DQinYuan + */ +public class NewInstanceInstruction extends QLInstruction { + + private final Class newClz; + + private final int argNum; + + public NewInstanceInstruction(ErrorReporter errorReporter, Class newClz, int argNum) { + super(errorReporter); + this.newClz = newClz; + this.argNum = argNum; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Parameters parameters = this.argNum == 0 ? null : qContext.pop(this.argNum); + Class[] paramTypes = new Class[this.argNum]; + Object[] objs = new Object[this.argNum]; + + for (int i = 0; i < this.argNum; i++) { + Value v = parameters.get(i); + objs[i] = v.get(); + paramTypes[i] = v.getType(); + } + ReflectLoader reflectLoader = qContext.getReflectLoader(); + Constructor constructor = reflectLoader.loadConstructor(newClz, paramTypes); + if (constructor == null) { + throw errorReporter.reportFormat(QLErrorCodes.NO_SUITABLE_CONSTRUCTOR.name(), + QLErrorCodes.NO_SUITABLE_CONSTRUCTOR.getErrorMsg(), + Arrays.toString(paramTypes)); + } + Object[] convertResult = + ParametersTypeConvertor.cast(objs, constructor.getParameterTypes(), constructor.isVarArgs()); + Object newObject = newObject(constructor, convertResult); + Value dataInstruction = new DataValue(newObject); + qContext.push(dataInstruction); + return QResult.NEXT_INSTRUCTION; + } + + private Object newObject(Constructor constructor, Object[] params) { + try { + return constructor.newInstance(params); + } + catch (InvocationTargetException e) { + throw errorReporter.report(e.getTargetException(), + QLErrorCodes.INVOKE_CONSTRUCTOR_INNER_ERROR.name(), + QLErrorCodes.INVOKE_CONSTRUCTOR_INNER_ERROR.getErrorMsg()); + } + catch (Exception e) { + throw errorReporter.report(QLErrorCodes.INVOKE_CONSTRUCTOR_UNKNOWN_ERROR.name(), + QLErrorCodes.INVOKE_CONSTRUCTOR_UNKNOWN_ERROR.getErrorMsg()); + } + } + + @Override + public int stackInput() { + return argNum; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, + index + ": New instance of cls " + newClz.getSimpleName() + " with argNum " + argNum, + debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewListInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewListInstruction.java new file mode 100644 index 0000000..274841a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewListInstruction.java @@ -0,0 +1,56 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Operation: new a List with top ${initLength} stack element + * Input: ${initLength} + * Output: 1 + *

+ * Author: DQinYuan + */ +public class NewListInstruction extends QLInstruction { + + private final int initLength; + + public NewListInstruction(ErrorReporter errorReporter, int initLength) { + super(errorReporter); + this.initLength = initLength; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Parameters initItems = qContext.pop(initLength); + List l = new ArrayList<>(initLength); + for (int i = 0; i < initLength; i++) { + l.add(initItems.getValue(i)); + } + qContext.push(new DataValue(l)); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return initLength; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": NewList " + initLength, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewMapInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewMapInstruction.java new file mode 100644 index 0000000..7aad124 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewMapInstruction.java @@ -0,0 +1,57 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Operation: new a Map with top ${keys.length} stack element + * Input: ${keys.length} + * Output: 1 + *

+ * Author: DQinYuan + */ +public class NewMapInstruction extends QLInstruction { + + private final List keys; + + public NewMapInstruction(ErrorReporter errorReporter, List keys) { + super(errorReporter); + this.keys = keys; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Parameters initItems = qContext.pop(keys.size()); + Map m = new LinkedHashMap<>(); + for (int i = 0; i < keys.size(); i++) { + m.put(keys.get(i), initItems.get(i).get()); + } + qContext.push(new DataValue(m)); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return keys.size(); + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": NewMap by keys " + keys, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewScopeInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewScopeInstruction.java new file mode 100644 index 0000000..27f9592 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/NewScopeInstruction.java @@ -0,0 +1,47 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: new scope + * Input: 0 + * Output: 0 + * + * Author: DQinYuan + */ +public class NewScopeInstruction extends QLInstruction { + + private final String scopeName; + + public NewScopeInstruction(ErrorReporter errorReporter, String scopeName) { + super(errorReporter); + this.scopeName = scopeName; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + qContext.newScope(); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": NewScope " + scopeName, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/OperatorInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/OperatorInstruction.java new file mode 100644 index 0000000..17c9c02 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/OperatorInstruction.java @@ -0,0 +1,87 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import java.util.function.Consumer; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.operator.BinaryOperator; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.runtime.util.ThrowUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +/** + * Operation: do middle operator +=,>>,>>>,<<,. + * Input: 2 + * Output: 1, operator result + *

+ * Author: DQinYuan + */ +public class OperatorInstruction extends QLInstruction { + + private final BinaryOperator operator; + + private final Integer traceKey; + + public OperatorInstruction(ErrorReporter errorReporter, BinaryOperator operator, Integer traceKey) { + super(errorReporter); + this.operator = operator; + this.traceKey = traceKey; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Value rightValue = qContext.pop(); + Value leftValue = qContext.pop(); + try { + // trace children + ExpressionTrace expressionTrace = qContext.getTraces().getExpressionTraceByKey(traceKey); + if (expressionTrace != null) { + expressionTrace.getChildren().get(0).valueEvaluated(leftValue.get()); + expressionTrace.getChildren().get(1).valueEvaluated(rightValue.get()); + } + + Object result = operator.execute(leftValue, rightValue, qContext, qlOptions, errorReporter); + qContext.push(new DataValue(result)); + + // trace result + if (expressionTrace != null) { + expressionTrace.valueEvaluated(result); + } + + return QResult.NEXT_INSTRUCTION; + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.EXECUTE_OPERATOR_EXCEPTION.name(), + QLErrorCodes.EXECUTE_OPERATOR_EXCEPTION.getErrorMsg(), + String.valueOf(leftValue.get()), + operator.getOperator(), + String.valueOf(rightValue.get())); + } + } + + @Override + public int stackInput() { + return 2; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Operator " + operator.getOperator(), debug); + } + + public BinaryOperator getOperator() { + return operator; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/PopInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/PopInstruction.java new file mode 100644 index 0000000..5439c41 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/PopInstruction.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: pop top element + * Input: 1 + * Output: 0 + *

+ * Author: DQinYuan + */ +public class PopInstruction extends QLInstruction { + public PopInstruction(ErrorReporter errorReporter) { + super(errorReporter); + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + qContext.pop(); + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Pop", debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/QLInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/QLInstruction.java new file mode 100644 index 0000000..e5807f5 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/QLInstruction.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import java.util.function.Consumer; + +/** + * Instruction Specification: + * + * Operation: What does it do? + * Input: How many stack element it consumes? and their means + * Output: How many stack element it push back? and their means + *

+ * Author: DQinYuan + */ +public abstract class QLInstruction { + + protected final ErrorReporter errorReporter; + + public QLInstruction(ErrorReporter errorReporter) { + this.errorReporter = errorReporter; + } + + public abstract QResult execute(QContext qContext, QLOptions qlOptions); + + /** + * @return input size + */ + public abstract int stackInput(); + + /** + * @return output size + */ + public abstract int stackOutput(); + + public abstract void println(int index, int depth, Consumer debug); + + public ErrorReporter getErrorReporter() { + return errorReporter; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ReturnInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ReturnInstruction.java new file mode 100644 index 0000000..3831b39 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ReturnInstruction.java @@ -0,0 +1,59 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import java.util.function.Consumer; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +/** + * Operation: return top element and exit lambda + * Input: 1 + * Output: 0 + *

+ * Author: DQinYuan + */ +public class ReturnInstruction extends QLInstruction { + + private final QResult.ResultType resultType; + + private final Integer traceKey; + + public ReturnInstruction(ErrorReporter errorReporter, QResult.ResultType resultType, Integer traceKey) { + super(errorReporter); + this.resultType = resultType; + this.traceKey = traceKey; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Value returnValue = qContext.pop(); + if (traceKey != null) { + ExpressionTrace expressionTrace = qContext.getTraces().getExpressionTraceByKey(traceKey); + if (expressionTrace != null) { + expressionTrace.valueEvaluated(returnValue.get()); + } + } + return new QResult(new DataValue(returnValue.get()), resultType); + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Return", debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/SliceInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/SliceInstruction.java new file mode 100644 index 0000000..b7858a8 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/SliceInstruction.java @@ -0,0 +1,176 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.util.ValueUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Operation: slice array or list, like a[2:4], a[4:-1], a[:4], a[5:], a[:] + * Input: 0-2 + * Output: 1 + *

+ * Author: DQinYuan + */ +public class SliceInstruction extends QLInstruction { + + public enum Mode { + LEFT, RIGHT, BOTH, COPY + } + + private final Mode mode; + + public SliceInstruction(ErrorReporter errorReporter, Mode mode) { + super(errorReporter); + this.mode = mode; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + int startInt = 0; + int endInt = 0; + Object indexAble = null; + if (mode == Mode.BOTH) { + Object end = qContext.pop().get(); + Object start = qContext.pop().get(); + indexAble = qContext.pop().get(); + startInt = + ValueUtils + .assertType(start, + Number.class, + QLErrorCodes.INVALID_INDEX.name(), + QLErrorCodes.INVALID_INDEX.getErrorMsg(), + errorReporter) + .intValue(); + endInt = + ValueUtils + .assertType(end, + Number.class, + QLErrorCodes.INVALID_INDEX.name(), + QLErrorCodes.INVALID_INDEX.getErrorMsg(), + errorReporter) + .intValue(); + } + else if (mode == Mode.LEFT) { + Object end = qContext.pop().get(); + indexAble = qContext.pop().get(); + endInt = + ValueUtils + .assertType(end, + Number.class, + QLErrorCodes.INVALID_INDEX.name(), + QLErrorCodes.INVALID_INDEX.getErrorMsg(), + errorReporter) + .intValue(); + } + else if (mode == Mode.RIGHT) { + Object start = qContext.pop().get(); + indexAble = qContext.pop().get(); + startInt = + ValueUtils + .assertType(start, + Number.class, + QLErrorCodes.INVALID_INDEX.name(), + QLErrorCodes.INVALID_INDEX.getErrorMsg(), + errorReporter) + .intValue(); + endInt = indexAbleLen(indexAble); + } + else if (mode == Mode.COPY) { + indexAble = qContext.pop().get(); + endInt = indexAbleLen(indexAble); + } + + if (indexAble instanceof List) { + List result = listSlice((List)indexAble, startInt, endInt); + qContext.push(new DataValue(result)); + return QResult.NEXT_INSTRUCTION; + } + else if (indexAble != null && indexAble.getClass().isArray()) { + Object result = arraySlice(indexAble, startInt, endInt); + qContext.push(new DataValue(result)); + return QResult.NEXT_INSTRUCTION; + } + else if (indexAble == null && qlOptions.isAvoidNullPointer()) { + qContext.push(Value.NULL_VALUE); + return QResult.NEXT_INSTRUCTION; + } + else { + throw errorReporter.reportFormat(QLErrorCodes.NONINDEXABLE_OBJECT.name(), + QLErrorCodes.NONINDEXABLE_OBJECT.getErrorMsg(), + indexAble == null ? "null" : indexAble.getClass().getName()); + } + } + + private int indexAbleLen(Object indexAble) { + if (indexAble instanceof List) { + return ((List)indexAble).size(); + } + else if (indexAble != null && indexAble.getClass().isArray()) { + return Array.getLength(indexAble); + } + else { + throw errorReporter.reportFormat(QLErrorCodes.NONINDEXABLE_OBJECT.name(), + QLErrorCodes.NONINDEXABLE_OBJECT.getErrorMsg(), + indexAble == null ? "null" : indexAble.getClass().getName()); + } + } + + private List listSlice(List listObj, int originStart, int originEnd) { + int start = Math.max(ValueUtils.javaIndex(listObj.size(), originStart), 0); + int end = Math.min(ValueUtils.javaIndex(listObj.size(), originEnd), listObj.size()); + if (start >= end) { + return new ArrayList<>(0); + } + + return listObj.subList(start, end); + } + + private Object arraySlice(Object arrObj, int originStart, int originEnd) { + int arrLen = Array.getLength(arrObj); + int start = Math.max(ValueUtils.javaIndex(arrLen, originStart), 0); + int end = Math.min(ValueUtils.javaIndex(arrLen, originEnd), arrLen); + if (start >= end) { + return Array.newInstance(arrObj.getClass().getComponentType(), 0); + } + + Object newArr = Array.newInstance(arrObj.getClass().getComponentType(), end - start); + for (int i = start; i < end; i++) { + Array.set(newArr, i - start, Array.get(arrObj, i)); + } + return newArr; + } + + @Override + public int stackInput() { + if (mode == Mode.BOTH) { + return 2; + } + else if (mode == Mode.COPY) { + return 0; + } + else { + return 1; + } + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Slice", debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/SpreadGetFieldInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/SpreadGetFieldInstruction.java new file mode 100644 index 0000000..07c6a10 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/SpreadGetFieldInstruction.java @@ -0,0 +1,136 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Operation: get field of each object in the list + * Input: 1 + * Output: 1, a list composed of field values + * Author: DQinYuan + */ +public class SpreadGetFieldInstruction extends QLInstruction { + + private static final String KEY = "key"; + + private static final String VALUE = "value"; + + private final String fieldName; + + public SpreadGetFieldInstruction(ErrorReporter errorReporter, String fieldName) { + super(errorReporter); + this.fieldName = fieldName; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Object traversable = qContext.pop().get(); + if (traversable == null) { + if (qlOptions.isAvoidNullPointer()) { + qContext.push(DataValue.NULL_VALUE); + return QResult.NEXT_INSTRUCTION; + } + throw errorReporter.reportFormat(QLErrorCodes.NONTRAVERSABLE_OBJECT.name(), + QLErrorCodes.NONTRAVERSABLE_OBJECT.getErrorMsg(), + "null"); + } + if (traversable instanceof Iterable) { + Iterable iterable = (Iterable)traversable; + List result = new ArrayList<>(); + for (Object item : iterable) { + if (item == null) { + if (qlOptions.isAvoidNullPointer()) { + result.add(null); + continue; + } + throw errorReporter.report(new NullPointerException(), + QLErrorCodes.NULL_FIELD_ACCESS.name(), + QLErrorCodes.NULL_FIELD_ACCESS.getErrorMsg()); + } + Value fieldValue = qContext.getReflectLoader().loadField(item, fieldName, false, errorReporter); + if (fieldValue == null) { + throw errorReporter.reportFormat(QLErrorCodes.FIELD_NOT_FOUND.name(), + QLErrorCodes.FIELD_NOT_FOUND.getErrorMsg(), + fieldName); + } + result.add(fieldValue.get()); + } + qContext.push(new DataValue(result)); + } + else if (traversable instanceof Map) { + Map lhm = (Map)traversable; + List result = new ArrayList<>(); + for (Map.Entry entry : lhm.entrySet()) { + if (KEY.equals(fieldName)) { + result.add(entry.getKey()); + } + else if (VALUE.equals(fieldName)) { + result.add(entry.getValue()); + } + else { + throw errorReporter.reportFormat(QLErrorCodes.FIELD_NOT_FOUND.name(), + QLErrorCodes.FIELD_NOT_FOUND.getErrorMsg(), + fieldName); + } + } + qContext.push(new DataValue(result)); + } + else if (traversable.getClass().isArray()) { + int arrLen = Array.getLength(traversable); + List result = new ArrayList<>(); + for (int i = 0; i < arrLen; i++) { + Object item = Array.get(traversable, i); + if (item == null) { + if (qlOptions.isAvoidNullPointer()) { + result.add(null); + continue; + } + throw errorReporter.report(new NullPointerException(), + QLErrorCodes.NULL_FIELD_ACCESS.name(), + QLErrorCodes.NULL_FIELD_ACCESS.getErrorMsg()); + } + Value fieldValue = qContext.getReflectLoader().loadField(item, fieldName, false, errorReporter); + if (fieldValue == null) { + throw errorReporter.reportFormat(QLErrorCodes.FIELD_NOT_FOUND.name(), + QLErrorCodes.FIELD_NOT_FOUND.getErrorMsg(), + fieldName); + } + result.add(fieldValue.get()); + } + qContext.push(new DataValue(result)); + } + else { + throw errorReporter.reportFormat(QLErrorCodes.NONTRAVERSABLE_OBJECT.name(), + QLErrorCodes.NONTRAVERSABLE_OBJECT.getErrorMsg(), + traversable.getClass().getName()); + } + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": SpreadGetField " + fieldName, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/SpreadMethodInvokeInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/SpreadMethodInvokeInstruction.java new file mode 100644 index 0000000..5bb73ce --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/SpreadMethodInvokeInstruction.java @@ -0,0 +1,119 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.util.MethodInvokeUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Operation: Invoke specified method of each object in the list + * Input: ${argNum} + 1 + * Output: 1, a list composed of return values from methods. + * Author: DQinYuan + */ +public class SpreadMethodInvokeInstruction extends QLInstruction { + + private final String methodName; + + private final int argNum; + + public SpreadMethodInvokeInstruction(ErrorReporter errorReporter, String methodName, int argNum) { + super(errorReporter); + this.methodName = methodName; + this.argNum = argNum; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Parameters parameters = qContext.pop(this.argNum + 1); + Object traversable = parameters.get(0).get(); + if (traversable == null) { + if (qlOptions.isAvoidNullPointer()) { + qContext.push(DataValue.NULL_VALUE); + return QResult.NEXT_INSTRUCTION; + } + throw errorReporter.reportFormat(QLErrorCodes.NONTRAVERSABLE_OBJECT.name(), + QLErrorCodes.NONTRAVERSABLE_OBJECT.getErrorMsg(), + "null"); + } + Class[] type = new Class[this.argNum]; + Object[] params = new Object[this.argNum]; + for (int i = 0; i < this.argNum; i++) { + Value v = parameters.get(i + 1); + params[i] = v.get(); + type[i] = v.getType(); + } + + if (traversable instanceof Iterable) { + Iterable iterable = (Iterable)traversable; + List result = new ArrayList<>(); + for (Object item : iterable) { + if (item == null) { + if (qlOptions.isAvoidNullPointer()) { + result.add(null); + continue; + } + throw errorReporter.report(new NullPointerException(), + QLErrorCodes.NULL_METHOD_ACCESS.name(), + QLErrorCodes.NULL_METHOD_ACCESS.getErrorMsg()); + } + Value invokeRes = MethodInvokeUtils + .findMethodAndInvoke(item, methodName, params, type, qContext.getReflectLoader(), errorReporter); + result.add(invokeRes.get()); + } + qContext.push(new DataValue(result)); + } + else if (traversable.getClass().isArray()) { + int arrLen = Array.getLength(traversable); + List result = new ArrayList<>(); + for (int i = 0; i < arrLen; i++) { + Object item = Array.get(traversable, i); + if (item == null) { + if (qlOptions.isAvoidNullPointer()) { + result.add(null); + continue; + } + throw errorReporter.report(new NullPointerException(), + QLErrorCodes.NULL_METHOD_ACCESS.name(), + QLErrorCodes.NULL_METHOD_ACCESS.getErrorMsg()); + } + Value invokeRes = MethodInvokeUtils + .findMethodAndInvoke(item, methodName, params, type, qContext.getReflectLoader(), errorReporter); + result.add(invokeRes.get()); + } + qContext.push(new DataValue(result)); + } + else { + throw errorReporter.reportFormat(QLErrorCodes.NONTRAVERSABLE_OBJECT.name(), + QLErrorCodes.NONTRAVERSABLE_OBJECT.getErrorMsg(), + traversable.getClass().getName()); + } + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return argNum + 1; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": SpreadMethodInvoke " + methodName, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/StringJoinInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/StringJoinInstruction.java new file mode 100644 index 0000000..66a56b1 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/StringJoinInstruction.java @@ -0,0 +1,58 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: concat n string on the top of stack + * Input: ${n} + * Output: concat result + * + * Author: DQinYuan + */ +public class StringJoinInstruction extends QLInstruction { + + private final int n; + + public StringJoinInstruction(ErrorReporter errorReporter, int n) { + super(errorReporter); + this.n = n; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Parameters arguments = qContext.pop(n); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + sb.append(arguments.get(i).get()); + } + qContext.push(new DataValue(sb.toString())); + return QResult.NEXT_INSTRUCTION; + } + + public int getN() { + return n; + } + + @Override + public int stackInput() { + return n; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": StringJoin " + n, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ThrowInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ThrowInstruction.java new file mode 100644 index 0000000..157598e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/ThrowInstruction.java @@ -0,0 +1,44 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: throw top element on the stack + * Input: 1 + * Output: 0 + *

+ * Author: DQinYuan + */ +public class ThrowInstruction extends QLInstruction { + public ThrowInstruction(ErrorReporter errorReporter) { + super(errorReporter); + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Object throwObj = qContext.pop().get(); + throw errorReporter.report(throwObj, QLErrorCodes.QL_THROW.name(), QLErrorCodes.QL_THROW.getErrorMsg()); + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": Throw", debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/TraceEvaludatedInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/TraceEvaludatedInstruction.java new file mode 100644 index 0000000..36f7b4e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/TraceEvaludatedInstruction.java @@ -0,0 +1,51 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import java.util.function.Consumer; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +/** + * Operation: no op, only for marking evaludated as true + * Input: 0 + * Output: 0 + * + * Author: DQinYuan + */ +public class TraceEvaludatedInstruction extends QLInstruction { + + private final Integer traceKey; + + public TraceEvaludatedInstruction(ErrorReporter errorReporter, Integer traceKey) { + super(errorReporter); + this.traceKey = traceKey; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + ExpressionTrace expressionTrace = qContext.getTraces().getExpressionTraceByKey(traceKey); + if (expressionTrace != null) { + expressionTrace.valueEvaluated(null); + } + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": TraceEvaludated " + traceKey, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/TracePeekInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/TracePeekInstruction.java new file mode 100644 index 0000000..273022d --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/TracePeekInstruction.java @@ -0,0 +1,51 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.function.Consumer; + +/** + * Operation: no op, only for tracing peek value of stack + * Input: 0 + * Output: 0 + * + * Author: DQinYuan + */ +public class TracePeekInstruction extends QLInstruction { + + private final Integer traceKey; + + public TracePeekInstruction(ErrorReporter errorReporter, Integer traceKey) { + super(errorReporter); + this.traceKey = traceKey; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + ExpressionTrace expressionTrace = qContext.getTraces().getExpressionTraceByKey(traceKey); + if (expressionTrace != null) { + expressionTrace.valueEvaluated(qContext.peek().get()); + } + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": TracePeek " + traceKey, debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/TryCatchInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/TryCatchInstruction.java new file mode 100644 index 0000000..0c88d74 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/TryCatchInstruction.java @@ -0,0 +1,148 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.util.ThrowUtils; +import com.alibaba.qlexpress4.runtime.util.ValueUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Operation: try and catch throw element + * Input: 0 + * Output: 1 + * + * Author: DQinYuan + */ +public class TryCatchInstruction extends QLInstruction { + + private final QLambdaDefinition body; + + private final List, QLambdaDefinition>> exceptionTable; + + private final QLambdaDefinition finalBody; + + public TryCatchInstruction(ErrorReporter errorReporter, QLambdaDefinition body, + List, QLambdaDefinition>> exceptionTable, QLambdaDefinition finalBody) { + super(errorReporter); + this.body = body; + this.exceptionTable = exceptionTable; + this.finalBody = finalBody; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + QResult tryCatchResult = tryCatchResult(qContext, qlOptions); + Value resultValue = tryCatchResult.getResult(); + qContext.push(ValueUtils.toImmutable(resultValue)); + + if (finalBody != null) { + callFinal(qContext, qlOptions); + } + if (tryCatchResult.getResultType() == QResult.ResultType.RETURN) { + return tryCatchResult; + } + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": TryCatch", debug); + PrintlnUtils.printlnByCurDepth(depth + 1, "Body", debug); + body.println(depth + 2, debug); + for (Map.Entry, QLambdaDefinition> clsLambdaEn : exceptionTable) { + PrintlnUtils.printlnByCurDepth(depth + 1, clsLambdaEn.getKey().getSimpleName(), debug); + clsLambdaEn.getValue().println(depth + 2, debug); + } + if (finalBody != null) { + PrintlnUtils.printlnByCurDepth(depth + 1, "Finally", debug); + finalBody.println(depth + 2, debug); + } + } + + private void callFinal(QContext qContext, QLOptions qlOptions) { + QLambda finalLambda = finalBody.toLambda(qContext, qlOptions, true); + try { + finalLambda.call(); + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.EXECUTE_FINAL_BLOCK_ERROR.name(), + QLErrorCodes.EXECUTE_FINAL_BLOCK_ERROR.getErrorMsg()); + } + } + + private QResult tryCatchResult(QContext qContext, QLOptions qlOptions) { + try { + QLambda bodyLambda = body.toLambda(qContext, qlOptions, true); + return bodyLambda.call(); + } + catch (QLRuntimeException e) { + Object catchObj = e.getCatchObj(); + if (catchObj == null) { + // ensure catch object can catch all exception + catchObj = new Object(); + } + QResult result = callExceptionHandler(catchObj, qContext, qlOptions); + if (result == null) { + throw e; + } + return result; + } + catch (Throwable t) { + QResult result = callExceptionHandler(t, qContext, qlOptions); + if (result == null) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.EXECUTE_TRY_BLOCK_ERROR.name(), + QLErrorCodes.EXECUTE_TRY_BLOCK_ERROR.getErrorMsg()); + } + return result; + } + } + + private QResult callExceptionHandler(Object catchObj, QContext qContext, QLOptions qlOptions) { + QLambdaDefinition exceptionHandler = getExceptionHandler(catchObj.getClass()); + if (exceptionHandler == null) { + return null; + } + QLambda catchHandlerLambda = exceptionHandler.toLambda(qContext, qlOptions, true); + + try { + return catchHandlerLambda.call(catchObj); + } + catch (Throwable th) { + throw ThrowUtils.wrapThrowable(th, + errorReporter, + QLErrorCodes.EXECUTE_CATCH_HANDLER_ERROR.name(), + QLErrorCodes.EXECUTE_CATCH_HANDLER_ERROR.getErrorMsg(), + catchObj.getClass().getName()); + } + } + + private QLambdaDefinition getExceptionHandler(Class catchObjClass) { + for (Map.Entry, QLambdaDefinition> clsLambdaEn : exceptionTable) { + if (clsLambdaEn.getKey().isAssignableFrom(catchObjClass)) { + return clsLambdaEn.getValue(); + } + } + return null; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/UnaryInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/UnaryInstruction.java new file mode 100644 index 0000000..ca038c0 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/UnaryInstruction.java @@ -0,0 +1,64 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import java.util.function.Consumer; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.operator.unary.UnaryOperator; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +/** + * Operation: do unary operator like, ++,--,!,~ + * Input: 1 + * Output: 1, unary result + * + * Author: DQinYuan + */ +public class UnaryInstruction extends QLInstruction { + + private final UnaryOperator unaryOperator; + + private final Integer traceKey; + + public UnaryInstruction(ErrorReporter errorReporter, UnaryOperator unaryOperator, Integer traceKey) { + super(errorReporter); + this.unaryOperator = unaryOperator; + this.traceKey = traceKey; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + Value value = qContext.pop(); + Object result = unaryOperator.execute(value, errorReporter); + qContext.push(new DataValue(result)); + + // trace + ExpressionTrace expressionTrace = qContext.getTraces().getExpressionTraceByKey(traceKey); + if (expressionTrace != null) { + expressionTrace.valueEvaluated(result); + expressionTrace.getChildren().get(0).valueEvaluated(value.get()); + } + + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 1; + } + + @Override + public int stackOutput() { + return 1; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": UnaryOp " + unaryOperator.getOperator(), debug); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/instruction/WhileInstruction.java b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/WhileInstruction.java new file mode 100644 index 0000000..b990eb9 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/instruction/WhileInstruction.java @@ -0,0 +1,97 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.scope.QvmBlockScope; +import com.alibaba.qlexpress4.runtime.util.ThrowUtils; +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.function.Consumer; + +/** + * Operation: while (condition) do body + * Input: 0 + * Output: 0 + *

+ * Author: DQinYuan + */ +public class WhileInstruction extends QLInstruction { + + private final QLambdaDefinition condition; + + private final QLambdaDefinition body; + + private final int whileScopeMaxStackSize; + + public WhileInstruction(ErrorReporter errorReporter, QLambdaDefinition condition, QLambdaDefinition body, + int whileScopeMaxStackSize) { + super(errorReporter); + this.condition = condition; + this.body = body; + this.whileScopeMaxStackSize = whileScopeMaxStackSize; + } + + @Override + public QResult execute(QContext qContext, QLOptions qlOptions) { + DelegateQContext whileScopeContext = new DelegateQContext(qContext, + new QvmBlockScope(qContext, Collections.emptyMap(), whileScopeMaxStackSize, ExceptionTable.EMPTY)); + QLambda conditionLambda = condition.toLambda(whileScopeContext, qlOptions, false); + QLambda bodyLambda = body.toLambda(whileScopeContext, qlOptions, true); + whileBody: while (evalCondition(conditionLambda)) { + try { + QResult bodyResult = bodyLambda.call(); + switch (bodyResult.getResultType()) { + case RETURN: + return bodyResult; + case BREAK: + break whileBody; + } + } + catch (Throwable t) { + throw ThrowUtils + .wrapThrowable(t, errorReporter, "WHILE_BODY_EXECUTE_ERROR", "while body execute error"); + } + } + return QResult.NEXT_INSTRUCTION; + } + + @Override + public int stackInput() { + return 0; + } + + @Override + public int stackOutput() { + return 0; + } + + @Override + public void println(int index, int depth, Consumer debug) { + PrintlnUtils.printlnByCurDepth(depth, index + ": While", debug); + PrintlnUtils.printlnByCurDepth(depth + 1, "Condition", debug); + condition.println(depth + 2, debug); + PrintlnUtils.printlnByCurDepth(depth + 1, "Body", debug); + body.println(depth + 2, debug); + } + + private boolean evalCondition(QLambda conditionLambda) { + try { + Object conditionResult = conditionLambda.call().getResult().get(); + if (!(conditionResult instanceof Boolean)) { + throw errorReporter.report(QLErrorCodes.WHILE_CONDITION_BOOL_REQUIRED.name(), + QLErrorCodes.WHILE_CONDITION_BOOL_REQUIRED.getErrorMsg()); + } + return (boolean)conditionResult; + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.WHILE_CONDITION_ERROR.name(), + QLErrorCodes.WHILE_CONDITION_ERROR.getErrorMsg()); + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/BinaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/BinaryOperator.java new file mode 100644 index 0000000..4e8e892 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/BinaryOperator.java @@ -0,0 +1,25 @@ +package com.alibaba.qlexpress4.runtime.operator; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; + +/** + * 二元操作符 + * Author: DQinYuan + * date 2022/1/12 2:34 下午 + */ +public interface BinaryOperator extends Operator { + /** + * 执行操作符计算 + * + * @param left left operand + * @param right right operand + * @param qRuntime runtime of qlexpress + * @param qlOptions options of current script run + * @param errorReporter error reporter + * @return result of operator + */ + Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, ErrorReporter errorReporter); +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/CustomBinaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/CustomBinaryOperator.java new file mode 100644 index 0000000..9911a78 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/CustomBinaryOperator.java @@ -0,0 +1,17 @@ +package com.alibaba.qlexpress4.runtime.operator; + +import com.alibaba.qlexpress4.runtime.Value; + +/** + * @author bingo + */ +public interface CustomBinaryOperator { + /** + * @param left left operand + * @param right right operand + * @return result + * @throws Throwable {@link com.alibaba.qlexpress4.exception.UserDefineException} for custom error message + */ + Object execute(Value left, Value right) + throws Throwable; +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/InstanceOfOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/InstanceOfOperator.java new file mode 100644 index 0000000..9bd558b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/InstanceOfOperator.java @@ -0,0 +1,53 @@ +package com.alibaba.qlexpress4.runtime.operator; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.MetaClass; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class InstanceOfOperator extends BaseBinaryOperator { + private static final InstanceOfOperator INSTANCE = new InstanceOfOperator(); + + private InstanceOfOperator() { + } + + public static InstanceOfOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + Object sourceObject = left.get(); + Object targetClass = right.get(); + if (targetClass == null) { + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + if (targetClass instanceof MetaClass) { + targetClass = ((MetaClass)targetClass).getClz(); + } + else if (!(targetClass instanceof Class)) { + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + if (sourceObject == null) { + return false; + } + return ((Class)targetClass).isAssignableFrom(sourceObject.getClass()); + } + + @Override + public String getOperator() { + return "instanceof"; + } + + @Override + public int getPriority() { + return QLPrecedences.COMPARE; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/Operator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/Operator.java new file mode 100644 index 0000000..8a6ef54 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/Operator.java @@ -0,0 +1,18 @@ +package com.alibaba.qlexpress4.runtime.operator; + +/** + * 操作符接口 + * + * @author bingo + */ +public interface Operator { + /** + * @return 操作符 + */ + String getOperator(); + + /** + * @return 操作符优先级 + */ + int getPriority(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/OperatorManager.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/OperatorManager.java new file mode 100644 index 0000000..1cfe985 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/OperatorManager.java @@ -0,0 +1,321 @@ +package com.alibaba.qlexpress4.runtime.operator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.aparser.OperatorFactory; +import com.alibaba.qlexpress4.aparser.ParserOperatorManager; +import com.alibaba.qlexpress4.aparser.QLexer; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.DivideAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.DivideOperator; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.MinusAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.MinusOperator; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.MultiplyAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.MultiplyOperator; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.PlusAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.PlusOperator; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.RemainderAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.RemainderOperator; +import com.alibaba.qlexpress4.runtime.operator.assign.AssignOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseAndAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseAndOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseInvertOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseLeftShiftAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseLeftShiftOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseOrAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseOrOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseRightShiftAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseRightShiftOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseRightShiftUnsignedAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseRightShiftUnsignedOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseXorAssignOperator; +import com.alibaba.qlexpress4.runtime.operator.bit.BitwiseXorOperator; +import com.alibaba.qlexpress4.runtime.operator.collection.InOperator; +import com.alibaba.qlexpress4.runtime.operator.collection.NotInOperator; +import com.alibaba.qlexpress4.runtime.operator.compare.EqualOperator; +import com.alibaba.qlexpress4.runtime.operator.compare.GreaterEqualOperator; +import com.alibaba.qlexpress4.runtime.operator.compare.GreaterOperator; +import com.alibaba.qlexpress4.runtime.operator.compare.LessEqualOperator; +import com.alibaba.qlexpress4.runtime.operator.compare.LessOperator; +import com.alibaba.qlexpress4.runtime.operator.compare.UnequalOperator; +import com.alibaba.qlexpress4.runtime.operator.logic.LogicAndOperator; +import com.alibaba.qlexpress4.runtime.operator.logic.LogicNotOperator; +import com.alibaba.qlexpress4.runtime.operator.logic.LogicOrOperator; +import com.alibaba.qlexpress4.runtime.operator.string.LikeOperator; +import com.alibaba.qlexpress4.runtime.operator.string.NotLikeOperator; +import com.alibaba.qlexpress4.runtime.operator.unary.MinusMinusPrefixUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.unary.MinusMinusSuffixUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.unary.MinusUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.unary.PlusPlusPrefixUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.unary.PlusPlusSuffixUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.unary.PlusUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.unary.UnaryOperator; +import com.alibaba.qlexpress4.runtime.util.ThrowUtils; + +/** + * @author bingo + */ +public class OperatorManager implements OperatorFactory, ParserOperatorManager { + private static final Map DEFAULT_BINARY_OPERATOR_MAP = new ConcurrentHashMap<>(64); + + private static final Map DEFAULT_PREFIX_UNARY_OPERATOR_MAP = new ConcurrentHashMap<>(8); + + private static final Map DEFAULT_SUFFIX_UNARY_OPERATOR_MAP = new ConcurrentHashMap<>(8); + + static { + List binaryOperatorList = new ArrayList<>(64); + binaryOperatorList.add(AssignOperator.getInstance()); + binaryOperatorList.add(PlusOperator.getInstance()); + binaryOperatorList.add(PlusAssignOperator.getInstance()); + binaryOperatorList.add(MinusOperator.getInstance()); + binaryOperatorList.add(MinusAssignOperator.getInstance()); + binaryOperatorList.add(MultiplyOperator.getInstance()); + binaryOperatorList.add(MultiplyAssignOperator.getInstance()); + binaryOperatorList.add(DivideOperator.getInstance()); + binaryOperatorList.add(DivideAssignOperator.getInstance()); + binaryOperatorList.add(RemainderOperator.getInstance("%")); + binaryOperatorList.add(RemainderAssignOperator.getInstance()); + //binaryOperatorList.add(RemainderOperator.getInstance("mod")); + binaryOperatorList.add(BitwiseAndOperator.getInstance()); + binaryOperatorList.add(BitwiseAndAssignOperator.getInstance()); + binaryOperatorList.add(BitwiseOrOperator.getInstance()); + binaryOperatorList.add(BitwiseOrAssignOperator.getInstance()); + binaryOperatorList.add(BitwiseXorOperator.getInstance()); + binaryOperatorList.add(BitwiseXorAssignOperator.getInstance()); + binaryOperatorList.add(BitwiseLeftShiftOperator.getInstance()); + binaryOperatorList.add(BitwiseLeftShiftAssignOperator.getInstance()); + binaryOperatorList.add(BitwiseRightShiftOperator.getInstance()); + binaryOperatorList.add(BitwiseRightShiftAssignOperator.getInstance()); + binaryOperatorList.add(BitwiseRightShiftUnsignedOperator.getInstance()); + binaryOperatorList.add(BitwiseRightShiftUnsignedAssignOperator.getInstance()); + binaryOperatorList.add(LogicAndOperator.getInstance("&&")); + binaryOperatorList.add(LogicAndOperator.getInstance("and")); + binaryOperatorList.add(LogicOrOperator.getInstance("||")); + binaryOperatorList.add(LogicOrOperator.getInstance("or")); + binaryOperatorList.add(EqualOperator.getInstance()); + binaryOperatorList.add(UnequalOperator.getInstance("!=")); + binaryOperatorList.add(UnequalOperator.getInstance("<>")); + //binaryOperatorList.add(PrismaticUnequalOperator.getInstance()); + binaryOperatorList.add(GreaterOperator.getInstance()); + binaryOperatorList.add(GreaterEqualOperator.getInstance()); + binaryOperatorList.add(LessOperator.getInstance()); + binaryOperatorList.add(LessEqualOperator.getInstance()); + binaryOperatorList.add(InOperator.getInstance()); + binaryOperatorList.add(NotInOperator.getInstance()); + binaryOperatorList.add(LikeOperator.getInstance()); + binaryOperatorList.add(NotLikeOperator.getInstance()); + binaryOperatorList.add(InstanceOfOperator.getInstance()); + for (BinaryOperator binaryOperator : binaryOperatorList) { + DEFAULT_BINARY_OPERATOR_MAP.put(binaryOperator.getOperator(), binaryOperator); + } + + List prefixUnaryOperatorList = new ArrayList<>(8); + prefixUnaryOperatorList.add(PlusUnaryOperator.getInstance()); + prefixUnaryOperatorList.add(MinusUnaryOperator.getInstance()); + prefixUnaryOperatorList.add(PlusPlusPrefixUnaryOperator.getInstance()); + prefixUnaryOperatorList.add(MinusMinusPrefixUnaryOperator.getInstance()); + prefixUnaryOperatorList.add(BitwiseInvertOperator.getInstance()); + prefixUnaryOperatorList.add(LogicNotOperator.getInstance()); + for (UnaryOperator unaryOperator : prefixUnaryOperatorList) { + DEFAULT_PREFIX_UNARY_OPERATOR_MAP.put(unaryOperator.getOperator(), unaryOperator); + } + + List suffixUnaryOperatorList = new ArrayList<>(8); + suffixUnaryOperatorList.add(PlusPlusSuffixUnaryOperator.getInstance()); + suffixUnaryOperatorList.add(MinusMinusSuffixUnaryOperator.getInstance()); + for (UnaryOperator unaryOperator : suffixUnaryOperatorList) { + DEFAULT_SUFFIX_UNARY_OPERATOR_MAP.put(unaryOperator.getOperator(), unaryOperator); + } + } + + private static final Map ALIASABLE_KEYWORDS = new HashMap<>(); + + static { + ALIASABLE_KEYWORDS.put("if", QLexer.IF); + ALIASABLE_KEYWORDS.put("then", QLexer.THEN); + ALIASABLE_KEYWORDS.put("else", QLexer.ELSE); + ALIASABLE_KEYWORDS.put("for", QLexer.FOR); + ALIASABLE_KEYWORDS.put("while", QLexer.WHILE); + ALIASABLE_KEYWORDS.put("break", QLexer.BREAK); + ALIASABLE_KEYWORDS.put("continue", QLexer.CONTINUE); + ALIASABLE_KEYWORDS.put("return", QLexer.RETURN); + ALIASABLE_KEYWORDS.put("function", QLexer.FUNCTION); + ALIASABLE_KEYWORDS.put("macro", QLexer.MACRO); + ALIASABLE_KEYWORDS.put("new", QLexer.NEW); + ALIASABLE_KEYWORDS.put("null", QLexer.NULL); + ALIASABLE_KEYWORDS.put("true", QLexer.TRUE); + ALIASABLE_KEYWORDS.put("false", QLexer.FALSE); + } + + private final Map customBinaryOperatorMap = new ConcurrentHashMap<>(); + + private final Map keyWordAliases = new ConcurrentHashMap<>(); + + /** + * Register a custom binary operator if it does not clash with a built-in operator. + * + * @param operatorName operator lexeme, for example {@code "**"} + * @param customBinaryOperator implementation of the custom operator + * @param priority operator precedence, see {@link QLPrecedences} + * @return {@code true} if registered successfully; {@code false} if the name is already used + */ + public boolean addBinaryOperator(String operatorName, CustomBinaryOperator customBinaryOperator, int priority) { + if (DEFAULT_BINARY_OPERATOR_MAP.containsKey(operatorName)) { + return false; + } + BinaryOperator preBinaryOperator = customBinaryOperatorMap.putIfAbsent(operatorName, + adapt2BinOp(operatorName, customBinaryOperator, priority)); + return preBinaryOperator == null; + } + + public boolean replaceDefaultOperator(String operatorName, CustomBinaryOperator customBinaryOperator) { + BinaryOperator defaultOperator = DEFAULT_BINARY_OPERATOR_MAP.get(operatorName); + if (defaultOperator == null) { + return false; + } + BinaryOperator preBinaryOperator = customBinaryOperatorMap.putIfAbsent(operatorName, + adapt2BinOp(operatorName, customBinaryOperator, defaultOperator.getPriority())); + return preBinaryOperator == null; + } + + private BinaryOperator adapt2BinOp(String operatorName, CustomBinaryOperator customBinaryOperator, int priority) { + return new BinaryOperator() { + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + try { + return customBinaryOperator.execute(left, right); + } + catch (UserDefineException e) { + throw ThrowUtils.reportUserDefinedException(errorReporter, e); + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + "OPERATOR_INNER_EXCEPTION", + "custom operator '" + operatorName + "' inner exception"); + } + } + + @Override + public String getOperator() { + return operatorName; + } + + @Override + public int getPriority() { + return priority; + } + }; + } + + /** + * @param operatorLexeme like +, =, *, / + * @return binary operator + */ + public BinaryOperator getBinaryOperator(String operatorLexeme) { + BinaryOperator customBinaryOperator = customBinaryOperatorMap.get(operatorLexeme); + if (customBinaryOperator != null) { + return customBinaryOperator; + } + + return DEFAULT_BINARY_OPERATOR_MAP.get(operatorLexeme); + } + + /** + * like --1 ++1 !true ~1 ^1 + * + * @param operatorLexeme ++, -- + * @return prefix unary operator + */ + public UnaryOperator getPrefixUnaryOperator(String operatorLexeme) { + return DEFAULT_PREFIX_UNARY_OPERATOR_MAP.get(operatorLexeme); + } + + /** + * like 1-- 1++ + * + * @param operatorLexeme ++, -- + * @return suffix unary operator + */ + public UnaryOperator getSuffixUnaryOperator(String operatorLexeme) { + return DEFAULT_SUFFIX_UNARY_OPERATOR_MAP.get(operatorLexeme); + } + + @Override + public boolean isOpType(String lexeme, OpType opType) { + switch (opType) { + case MIDDLE: + return getBinaryOperator(lexeme) != null; + case PREFIX: + return DEFAULT_PREFIX_UNARY_OPERATOR_MAP.containsKey(lexeme); + case SUFFIX: + return DEFAULT_SUFFIX_UNARY_OPERATOR_MAP.containsKey(lexeme); + } + return false; + } + + @Override + public Integer precedence(String lexeme) { + return getBinaryOperator(lexeme).getPriority(); + } + + @Override + public Integer getAlias(String lexeme) { + return keyWordAliases.get(lexeme); + } + + public boolean addKeyWordAlias(String lexeme, String keyWord) { + Integer keyWordId = ALIASABLE_KEYWORDS.get(keyWord); + if (keyWordId == null) { + return false; + } + keyWordAliases.put(lexeme, keyWordId); + return true; + } + + public boolean addOperatorAlias(String lexeme, String operator) { + BinaryOperator originDefaultOp = DEFAULT_BINARY_OPERATOR_MAP.get(operator); + if (originDefaultOp != null) { + BinaryOperator newOperator = adaptOriginOperator(originDefaultOp, lexeme); + BinaryOperator prev = customBinaryOperatorMap.putIfAbsent(lexeme, newOperator); + return prev == null; + } + BinaryOperator originCusOp = customBinaryOperatorMap.get(operator); + if (originCusOp != null) { + BinaryOperator newOperator = adaptOriginOperator(originCusOp, lexeme); + BinaryOperator prev = customBinaryOperatorMap.putIfAbsent(lexeme, newOperator); + return prev == null; + } + return false; + } + + private BinaryOperator adaptOriginOperator(BinaryOperator originOperator, String lexeme) { + return new BinaryOperator() { + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return originOperator.execute(left, right, qRuntime, qlOptions, errorReporter); + } + + @Override + public String getOperator() { + return lexeme; + } + + @Override + public int getPriority() { + return originOperator.getPriority(); + } + }; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/DivideAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/DivideAssignOperator.java new file mode 100644 index 0000000..7b587aa --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/DivideAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class DivideAssignOperator extends BaseBinaryOperator { + private static final DivideAssignOperator INSTANCE = new DivideAssignOperator(); + + private DivideAssignOperator() { + } + + public static DivideAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = divide(left, right, qlOptions, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return "/="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} \ No newline at end of file diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/DivideOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/DivideOperator.java new file mode 100644 index 0000000..0e64f23 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/DivideOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class DivideOperator extends BaseBinaryOperator { + private static final DivideOperator INSTANCE = new DivideOperator(); + + private DivideOperator() { + } + + public static DivideOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return divide(left, right, qlOptions, errorReporter); + } + + @Override + public String getOperator() { + return "/"; + } + + @Override + public int getPriority() { + return QLPrecedences.MULTI; + } +} \ No newline at end of file diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MinusAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MinusAssignOperator.java new file mode 100644 index 0000000..c060f9b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MinusAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class MinusAssignOperator extends BaseBinaryOperator { + private static final MinusAssignOperator INSTANCE = new MinusAssignOperator(); + + private MinusAssignOperator() { + } + + public static MinusAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = minus(left, right, qlOptions, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return "-="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MinusOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MinusOperator.java new file mode 100644 index 0000000..d1fa92e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MinusOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class MinusOperator extends BaseBinaryOperator { + private static final MinusOperator INSTANCE = new MinusOperator(); + + private MinusOperator() { + } + + public static MinusOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return minus(left, right, qlOptions, errorReporter); + } + + @Override + public String getOperator() { + return "-"; + } + + @Override + public int getPriority() { + return QLPrecedences.ADD; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MultiplyAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MultiplyAssignOperator.java new file mode 100644 index 0000000..6cb9bec --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MultiplyAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class MultiplyAssignOperator extends BaseBinaryOperator { + private static final MultiplyAssignOperator INSTANCE = new MultiplyAssignOperator(); + + private MultiplyAssignOperator() { + } + + public static MultiplyAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = multiply(left, right, qlOptions, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return "*="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MultiplyOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MultiplyOperator.java new file mode 100644 index 0000000..8f8c542 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/MultiplyOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class MultiplyOperator extends BaseBinaryOperator { + private static final MultiplyOperator INSTANCE = new MultiplyOperator(); + + private MultiplyOperator() { + } + + public static MultiplyOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return multiply(left, right, qlOptions, errorReporter); + } + + @Override + public String getOperator() { + return "*"; + } + + @Override + public int getPriority() { + return QLPrecedences.MULTI; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/PlusAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/PlusAssignOperator.java new file mode 100644 index 0000000..98e5dee --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/PlusAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class PlusAssignOperator extends BaseBinaryOperator { + private static final PlusAssignOperator INSTANCE = new PlusAssignOperator(); + + private PlusAssignOperator() { + } + + public static PlusAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = plus(left, right, qlOptions, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return "+="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/PlusOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/PlusOperator.java new file mode 100644 index 0000000..c3c7b23 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/PlusOperator.java @@ -0,0 +1,40 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * QLExpress只支持String和Number类型的+ + * + * @author bingo + */ +public class PlusOperator extends BaseBinaryOperator { + private static final PlusOperator INSTANCE = new PlusOperator(); + + private PlusOperator() { + } + + public static PlusOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return plus(left, right, qlOptions, errorReporter); + } + + @Override + public String getOperator() { + return "+"; + } + + @Override + public int getPriority() { + return QLPrecedences.ADD; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/RemainderAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/RemainderAssignOperator.java new file mode 100644 index 0000000..413bb1b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/RemainderAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class RemainderAssignOperator extends BaseBinaryOperator { + private static final RemainderAssignOperator INSTANCE = new RemainderAssignOperator(); + + private RemainderAssignOperator() { + } + + public static RemainderAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = remainder(left, right, qlOptions, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return "%="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} \ No newline at end of file diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/RemainderOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/RemainderOperator.java new file mode 100644 index 0000000..247e831 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/arithmetic/RemainderOperator.java @@ -0,0 +1,49 @@ +package com.alibaba.qlexpress4.runtime.operator.arithmetic; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class RemainderOperator extends BaseBinaryOperator { + private static final Map INSTANCE_CACHE = new ConcurrentHashMap<>(2); + + static { + INSTANCE_CACHE.put("%", new RemainderOperator("%")); + //INSTANCE_CACHE.put("mod", new RemainderOperator("mod")); + } + + private final String operator; + + private RemainderOperator(String operator) { + this.operator = operator; + } + + public static RemainderOperator getInstance(String operator) { + return INSTANCE_CACHE.get(operator); + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return remainder(left, right, qlOptions, errorReporter); + } + + @Override + public String getOperator() { + return operator; + } + + @Override + public int getPriority() { + return QLPrecedences.MULTI; + } +} \ No newline at end of file diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/assign/AssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/assign/AssignOperator.java new file mode 100644 index 0000000..696be77 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/assign/AssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.assign; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class AssignOperator extends BaseBinaryOperator { + private static final AssignOperator INSTANCE = new AssignOperator(); + + private AssignOperator() { + } + + public static AssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object newValue = right.get(); + leftValue.set(newValue, errorReporter); + return newValue; + } + + @Override + public String getOperator() { + return "="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/base/BaseBinaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/base/BaseBinaryOperator.java new file mode 100644 index 0000000..48bd867 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/base/BaseBinaryOperator.java @@ -0,0 +1,386 @@ +package com.alibaba.qlexpress4.runtime.operator.base; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.operator.BinaryOperator; +import com.alibaba.qlexpress4.runtime.operator.number.NumberMath; + +/** + * @author bingo + */ +public abstract class BaseBinaryOperator implements BinaryOperator { + protected boolean isSameType(Value left, Value right) { + return left.getTypeName() != null && right.getTypeName() != null + && Objects.equals(left.getTypeName(), right.getTypeName()); + } + + protected boolean isInstanceofComparable(Value value) { + return value.get() instanceof Comparable; + } + + protected boolean isBothBoolean(Value left, Value right) { + return left.get() instanceof Boolean && right.get() instanceof Boolean; + } + + protected boolean isBooleanAndNull(Value left, Value right) { + return (left.get() == null && right.get() instanceof Boolean) + || (left.get() instanceof Boolean && right.get() == null); + } + + protected boolean isBothNumber(Value left, Value right) { + return left.get() instanceof Number && right.get() instanceof Number; + } + + protected boolean isBothNumberOrChar(Object leftValue, Object rightValue) { + return (leftValue instanceof Character || leftValue instanceof Number) + && (rightValue instanceof Character || rightValue instanceof Number); + } + + protected Number char2Number(Object charOrNumber) { + return charOrNumber instanceof Character ? (int)(Character)charOrNumber : (Number)charOrNumber; + } + + protected boolean isNumberCharacter(Value left, Value right) { + return (left.get() instanceof Character && right.get() instanceof Number) + || (left.get() instanceof Number && right.get() instanceof Character); + } + + protected boolean isNumber(Value value) { + return value.get() instanceof Number; + } + + protected void assertLeftValue(Value left, ErrorReporter errorReporter) { + if (!(left instanceof LeftValue)) { + throw errorReporter.reportFormat(QLErrorCodes.INVALID_ASSIGNMENT.name(), + QLErrorCodes.INVALID_ASSIGNMENT.getErrorMsg(), + "on the left side"); + } + } + + protected Object plus(Value left, Value right, QLOptions qlOptions, ErrorReporter errorReporter) { + Object leftValue = left.get(); + Object rightValue = right.get(); + + if (leftValue instanceof String) { + return (String)leftValue + rightValue; + } + + if (rightValue instanceof String) { + return leftValue + (String)rightValue; + } + + if (isBothNumber(left, right)) { + return add(qlOptions, (Number)leftValue, (Number)rightValue); + } + + if (isBothNumberOrChar(leftValue, rightValue)) { + return add(qlOptions, char2Number(leftValue), char2Number(rightValue)); + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + private Number add(QLOptions qlOptions, Number leftValue, Number rightValue) { + if (qlOptions.isPrecise()) { + return NumberMath.add(NumberMath.toBigDecimal(leftValue), NumberMath.toBigDecimal(rightValue)); + } + else { + return NumberMath.add(leftValue, rightValue); + } + } + + protected Object minus(Value left, Value right, QLOptions qlOptions, ErrorReporter errorReporter) { + Object leftValue = left.get(); + Object rightValue = right.get(); + + if (isBothNumber(left, right)) { + return subtract(qlOptions, (Number)leftValue, (Number)rightValue); + } + + if (isBothNumberOrChar(leftValue, rightValue)) { + return subtract(qlOptions, char2Number(leftValue), char2Number(rightValue)); + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + private Number subtract(QLOptions qlOptions, Number leftValue, Number rightValue) { + if (qlOptions.isPrecise()) { + return NumberMath.subtract(NumberMath.toBigDecimal(leftValue), NumberMath.toBigDecimal(rightValue)); + } + else { + return NumberMath.subtract(leftValue, rightValue); + } + } + + protected Object multiply(Value left, Value right, QLOptions qlOptions, ErrorReporter errorReporter) { + Object leftValue = left.get(); + Object rightValue = right.get(); + + if (isBothNumber(left, right)) { + if (qlOptions.isPrecise()) { + return NumberMath.multiply(NumberMath.toBigDecimal((Number)leftValue), + NumberMath.toBigDecimal((Number)rightValue)); + } + else { + return NumberMath.multiply((Number)leftValue, (Number)rightValue); + } + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + protected Object divide(Value left, Value right, QLOptions qlOptions, ErrorReporter errorReporter) { + Object leftValue = left.get(); + Object rightValue = right.get(); + + if (isBothNumber(left, right)) { + try { + if (qlOptions.isPrecise()) { + return NumberMath.divide(NumberMath.toBigDecimal((Number)leftValue), + NumberMath.toBigDecimal((Number)rightValue)); + } + else { + return NumberMath.divide((Number)leftValue, (Number)rightValue); + } + } + catch (ArithmeticException arithmeticException) { + throw errorReporter.report(arithmeticException, + QLErrorCodes.INVALID_ARITHMETIC.name(), + arithmeticException.getMessage()); + } + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + protected Object remainder(Value left, Value right, QLOptions qlOptions, ErrorReporter errorReporter) { + Object leftValue = left.get(); + Object rightValue = right.get(); + + if (isBothNumber(left, right)) { + if (qlOptions.isPrecise()) { + return NumberMath.remainder(NumberMath.toBigDecimal((Number)leftValue), + NumberMath.toBigDecimal((Number)rightValue)); + } + else { + return NumberMath.remainder((Number)leftValue, (Number)rightValue); + } + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + protected Object bitwiseAnd(Value left, Value right, ErrorReporter errorReporter) { + if (isBothBoolean(left, right) || isBooleanAndNull(left, right)) { + return (Boolean)Optional.ofNullable(left.get()).orElse(Boolean.FALSE) + & (Boolean)Optional.ofNullable(right.get()).orElse(Boolean.FALSE); + } + + if (isBothNumber(left, right)) { + Number leftValue = (Number)left.get(); + Number rightValue = (Number)right.get(); + return NumberMath.and(leftValue, rightValue); + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + protected Object bitwiseOr(Value left, Value right, ErrorReporter errorReporter) { + if (isBothBoolean(left, right) || isBooleanAndNull(left, right)) { + return (Boolean)Optional.ofNullable(left.get()).orElse(Boolean.FALSE) + | (Boolean)Optional.ofNullable(right.get()).orElse(Boolean.FALSE); + } + + if (isBothNumber(left, right)) { + Number leftValue = (Number)left.get(); + Number rightValue = (Number)right.get(); + return NumberMath.or(leftValue, rightValue); + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + protected Object bitwiseXor(Value left, Value right, ErrorReporter errorReporter) { + if (isBothBoolean(left, right) || isBooleanAndNull(left, right)) { + return (Boolean)Optional.ofNullable(left.get()).orElse(Boolean.FALSE) + ^ (Boolean)Optional.ofNullable(right.get()).orElse(Boolean.FALSE); + } + + if (isBothNumber(left, right)) { + Number leftValue = (Number)left.get(); + Number rightValue = (Number)right.get(); + return NumberMath.xor(leftValue, rightValue); + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + protected Object leftShift(Value left, Value right, ErrorReporter errorReporter) { + if (isBothNumber(left, right)) { + Number leftValue = (Number)left.get(); + Number rightValue = (Number)right.get(); + return NumberMath.leftShift(leftValue, rightValue); + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + protected Object rightShift(Value left, Value right, ErrorReporter errorReporter) { + if (isBothNumber(left, right)) { + Number leftValue = (Number)left.get(); + Number rightValue = (Number)right.get(); + return NumberMath.rightShift(leftValue, rightValue); + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + protected Object rightShiftUnsigned(Value left, Value right, ErrorReporter errorReporter) { + if (isBothNumber(left, right)) { + Number leftValue = (Number)left.get(); + Number rightValue = (Number)right.get(); + return NumberMath.rightShiftUnsigned(leftValue, rightValue); + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected int compare(Value left, Value right, ErrorReporter errorReporter) { + if (Objects.equals(left.get(), right.get())) { + return 0; + } + + if (isBothNumber(left, right)) { + return NumberMath.compareTo((Number)left.get(), (Number)right.get()); + } + + if (isNumberCharacter(left, right)) { + if (isNumber(left)) { + return NumberMath.compareTo((Number)left.get(), (int)(Character)right.get()); + } + else { + return NumberMath.compareTo((int)(Character)left.get(), (Number)right.get()); + } + } + + if (isSameType(left, right) && isInstanceofComparable(left)) { + return ((Comparable)(left.get())).compareTo(right.get()); + } + + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + protected boolean equals(Value left, Value right, ErrorReporter errorReporter) { + Object leftValue = left.get(); + Object rightValue = right.get(); + if (isBothNumber(left, right) || isNumberCharacter(left, right) + || (isSameType(left, right) && isInstanceofComparable(left))) { + return compare(left, right, errorReporter) == 0; + } + else { + return Objects.equals(leftValue, rightValue); + } + } + + protected boolean in(Value left, Value right, ErrorReporter errorReporter) { + Object rightOperand = right.get(); + Object leftOperand = left.get(); + if (leftOperand == null && rightOperand == null) { + return true; + } + else if (leftOperand == null || rightOperand == null) { + return false; + } + + if (rightOperand instanceof Collection) { + Collection rightCollection = (Collection)rightOperand; + for (Object rightElement : rightCollection) { + if (equals(left, new DataValue(rightElement), errorReporter)) { + return true; + } + } + return false; + } + else if (rightOperand.getClass().isArray()) { + Object[] rightArray = (Object[])rightOperand; + for (Object rightElement : rightArray) { + if (equals(left, new DataValue(rightElement), errorReporter)) { + return true; + } + } + return false; + } + else if (rightOperand instanceof String) { + return ((String)rightOperand).contains(String.valueOf(leftOperand)); + } + else { + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + } + + protected boolean like(Value left, Value right, ErrorReporter errorReporter) { + Object target = left.get(); + Object pattern = right.get(); + if (target == null && pattern == null) { + return true; + } + else if (target == null || pattern == null) { + return false; + } + + if (!(target instanceof String) || !(pattern instanceof String)) { + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + return matchPattern((String)target, (String)pattern); + } + + private static boolean matchPattern(String s, String pattern) { + int sPointer = 0, pPointer = 0; + int sLen = s.length(), pLen = pattern.length(); + int sRecall = -1, pRecall = -1; + while (sPointer < sLen) { + if (pPointer < pLen && (s.charAt(sPointer) == pattern.charAt(pPointer))) { + sPointer++; + pPointer++; + } + else if (pPointer < pLen && pattern.charAt(pPointer) == '%') { + sRecall = sPointer; + pRecall = pPointer; + pPointer++; + } + else if (sRecall >= 0) { + sPointer = ++sRecall; + pPointer = pRecall + 1; + } + else { + return false; + } + } + while (pPointer < pLen && pattern.charAt(pPointer) == '%') { + pPointer++; + } + return pPointer == pLen; + } + + protected QLRuntimeException buildInvalidOperandTypeException(Value left, Value right, + ErrorReporter errorReporter) { + return errorReporter.reportFormat(QLErrorCodes.INVALID_BINARY_OPERAND.name(), + QLErrorCodes.INVALID_BINARY_OPERAND.getErrorMsg(), + getOperator(), + left.getTypeName(), + left.get(), + right.getTypeName(), + right.get()); + } +} \ No newline at end of file diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/base/BaseUnaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/base/BaseUnaryOperator.java new file mode 100644 index 0000000..6b2cc5f --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/base/BaseUnaryOperator.java @@ -0,0 +1,17 @@ +package com.alibaba.qlexpress4.runtime.operator.base; + +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.unary.UnaryOperator; + +/** + * @author bingo + */ +public abstract class BaseUnaryOperator implements UnaryOperator { + protected QLRuntimeException buildInvalidOperandTypeException(Value value, ErrorReporter errorReporter) { + return errorReporter.reportFormat(QLErrorCodes.INVALID_UNARY_OPERAND + .name(), QLErrorCodes.INVALID_UNARY_OPERAND.getErrorMsg(), getOperator(), value.getTypeName(), value.get()); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseAndAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseAndAssignOperator.java new file mode 100644 index 0000000..a6f3116 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseAndAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseAndAssignOperator extends BaseBinaryOperator { + private static final BitwiseAndAssignOperator INSTANCE = new BitwiseAndAssignOperator(); + + private BitwiseAndAssignOperator() { + } + + public static BitwiseAndAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = bitwiseAnd(left, right, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return "&="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseAndOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseAndOperator.java new file mode 100644 index 0000000..4d211a2 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseAndOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseAndOperator extends BaseBinaryOperator { + private static final BitwiseAndOperator INSTANCE = new BitwiseAndOperator(); + + private BitwiseAndOperator() { + } + + public static BitwiseAndOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "&"; + } + + @Override + public int getPriority() { + return QLPrecedences.BIT_AND; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return bitwiseAnd(left, right, errorReporter); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseInvertOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseInvertOperator.java new file mode 100644 index 0000000..590ee44 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseInvertOperator.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.number.NumberMath; + +/** + * @author bingo + */ +public class BitwiseInvertOperator extends BaseUnaryOperator { + private static final BitwiseInvertOperator INSTANCE = new BitwiseInvertOperator(); + + private BitwiseInvertOperator() { + } + + public static BitwiseInvertOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value value, ErrorReporter errorReporter) { + Object operand = value.get(); + if (!(operand instanceof Number)) { + throw buildInvalidOperandTypeException(value, errorReporter); + } + + return NumberMath.bitwiseNegate((Number)operand); + } + + @Override + public String getOperator() { + return "~"; + } + + @Override + public int getPriority() { + return QLPrecedences.UNARY; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseLeftShiftAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseLeftShiftAssignOperator.java new file mode 100644 index 0000000..85e53fb --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseLeftShiftAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseLeftShiftAssignOperator extends BaseBinaryOperator { + private static final BitwiseLeftShiftAssignOperator INSTANCE = new BitwiseLeftShiftAssignOperator(); + + private BitwiseLeftShiftAssignOperator() { + } + + public static BitwiseLeftShiftAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = leftShift(left, right, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return "<<="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseLeftShiftOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseLeftShiftOperator.java new file mode 100644 index 0000000..23d02d5 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseLeftShiftOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseLeftShiftOperator extends BaseBinaryOperator { + private static final BitwiseLeftShiftOperator INSTANCE = new BitwiseLeftShiftOperator(); + + private BitwiseLeftShiftOperator() { + } + + public static BitwiseLeftShiftOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "<<"; + } + + @Override + public int getPriority() { + return QLPrecedences.BIT_MOVE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return leftShift(left, right, errorReporter); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseOrAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseOrAssignOperator.java new file mode 100644 index 0000000..2e40723 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseOrAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseOrAssignOperator extends BaseBinaryOperator { + private static final BitwiseOrAssignOperator INSTANCE = new BitwiseOrAssignOperator(); + + private BitwiseOrAssignOperator() { + } + + public static BitwiseOrAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = bitwiseOr(left, right, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return "|="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseOrOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseOrOperator.java new file mode 100644 index 0000000..95e5ad6 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseOrOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseOrOperator extends BaseBinaryOperator { + private static final BitwiseOrOperator INSTANCE = new BitwiseOrOperator(); + + private BitwiseOrOperator() { + } + + public static BitwiseOrOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "|"; + } + + @Override + public int getPriority() { + return QLPrecedences.BIT_OR; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return bitwiseOr(left, right, errorReporter); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftAssignOperator.java new file mode 100644 index 0000000..146b63e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseRightShiftAssignOperator extends BaseBinaryOperator { + private static final BitwiseRightShiftAssignOperator INSTANCE = new BitwiseRightShiftAssignOperator(); + + private BitwiseRightShiftAssignOperator() { + } + + public static BitwiseRightShiftAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = rightShift(left, right, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return ">>="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftOperator.java new file mode 100644 index 0000000..5b27058 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseRightShiftOperator extends BaseBinaryOperator { + private static final BitwiseRightShiftOperator INSTANCE = new BitwiseRightShiftOperator(); + + private BitwiseRightShiftOperator() { + } + + public static BitwiseRightShiftOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return ">>"; + } + + @Override + public int getPriority() { + return QLPrecedences.BIT_MOVE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return rightShift(left, right, errorReporter); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftUnsignedAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftUnsignedAssignOperator.java new file mode 100644 index 0000000..603f6ab --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftUnsignedAssignOperator.java @@ -0,0 +1,44 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseRightShiftUnsignedAssignOperator extends BaseBinaryOperator { + private static final BitwiseRightShiftUnsignedAssignOperator INSTANCE = + new BitwiseRightShiftUnsignedAssignOperator(); + + private BitwiseRightShiftUnsignedAssignOperator() { + } + + public static BitwiseRightShiftUnsignedAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = rightShiftUnsigned(left, right, errorReporter); + leftValue.set(result, errorReporter); + return result; + } + + @Override + public String getOperator() { + return ">>>="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftUnsignedOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftUnsignedOperator.java new file mode 100644 index 0000000..b532148 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseRightShiftUnsignedOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseRightShiftUnsignedOperator extends BaseBinaryOperator { + private static final BitwiseRightShiftUnsignedOperator INSTANCE = new BitwiseRightShiftUnsignedOperator(); + + private BitwiseRightShiftUnsignedOperator() { + } + + public static BitwiseRightShiftUnsignedOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return ">>>"; + } + + @Override + public int getPriority() { + return QLPrecedences.BIT_MOVE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return rightShiftUnsigned(left, right, errorReporter); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseXorAssignOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseXorAssignOperator.java new file mode 100644 index 0000000..9addb70 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseXorAssignOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseXorAssignOperator extends BaseBinaryOperator { + private static final BitwiseXorAssignOperator INSTANCE = new BitwiseXorAssignOperator(); + + private BitwiseXorAssignOperator() { + } + + public static BitwiseXorAssignOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "^="; + } + + @Override + public int getPriority() { + return QLPrecedences.ASSIGN; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + assertLeftValue(left, errorReporter); + LeftValue leftValue = (LeftValue)left; + Object result = bitwiseXor(left, right, errorReporter); + leftValue.set(result, errorReporter); + return result; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseXorOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseXorOperator.java new file mode 100644 index 0000000..efadcc7 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/bit/BitwiseXorOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.bit; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class BitwiseXorOperator extends BaseBinaryOperator { + private static final BitwiseXorOperator INSTANCE = new BitwiseXorOperator(); + + private BitwiseXorOperator() { + } + + public static BitwiseXorOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "^"; + } + + @Override + public int getPriority() { + return QLPrecedences.XOR; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return bitwiseXor(left, right, errorReporter); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/collection/InOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/collection/InOperator.java new file mode 100644 index 0000000..4d0d613 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/collection/InOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.collection; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class InOperator extends BaseBinaryOperator { + private static final InOperator INSTANCE = new InOperator(); + + private InOperator() { + } + + public static InOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return in(left, right, errorReporter); + } + + @Override + public String getOperator() { + return "in"; + } + + @Override + public int getPriority() { + return QLPrecedences.IN_LIKE; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/collection/NotInOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/collection/NotInOperator.java new file mode 100644 index 0000000..08b165a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/collection/NotInOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.collection; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class NotInOperator extends BaseBinaryOperator { + private static final NotInOperator INSTANCE = new NotInOperator(); + + private NotInOperator() { + } + + public static NotInOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return !in(left, right, errorReporter); + } + + @Override + public String getOperator() { + return "not_in"; + } + + @Override + public int getPriority() { + return QLPrecedences.IN_LIKE; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/EqualOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/EqualOperator.java new file mode 100644 index 0000000..bcf470a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/EqualOperator.java @@ -0,0 +1,38 @@ +package com.alibaba.qlexpress4.runtime.operator.compare; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class EqualOperator extends BaseBinaryOperator { + private static final EqualOperator INSTANCE = new EqualOperator(); + + private EqualOperator() { + } + + public static EqualOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "=="; + } + + @Override + public int getPriority() { + return QLPrecedences.EQUAL; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return equals(left, right, errorReporter); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/GreaterEqualOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/GreaterEqualOperator.java new file mode 100644 index 0000000..178c6f2 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/GreaterEqualOperator.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime.operator.compare; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class GreaterEqualOperator extends BaseBinaryOperator { + private static final GreaterEqualOperator INSTANCE = new GreaterEqualOperator(); + + private GreaterEqualOperator() { + } + + public static GreaterEqualOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return ">="; + } + + @Override + public int getPriority() { + return QLPrecedences.COMPARE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + if (qlOptions.isAvoidNullPointer() && (left.get() == null || right.get() == null)) { + return false; + } + return compare(left, right, errorReporter) >= 0; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/GreaterOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/GreaterOperator.java new file mode 100644 index 0000000..1b004f5 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/GreaterOperator.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime.operator.compare; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class GreaterOperator extends BaseBinaryOperator { + private static final GreaterOperator INSTANCE = new GreaterOperator(); + + private GreaterOperator() { + } + + public static GreaterOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return ">"; + } + + @Override + public int getPriority() { + return QLPrecedences.COMPARE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + if (qlOptions.isAvoidNullPointer() && (left.get() == null || right.get() == null)) { + return false; + } + return compare(left, right, errorReporter) > 0; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/LessEqualOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/LessEqualOperator.java new file mode 100644 index 0000000..9234310 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/LessEqualOperator.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime.operator.compare; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class LessEqualOperator extends BaseBinaryOperator { + private static final LessEqualOperator INSTANCE = new LessEqualOperator(); + + private LessEqualOperator() { + } + + public static LessEqualOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "<="; + } + + @Override + public int getPriority() { + return QLPrecedences.COMPARE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + if (qlOptions.isAvoidNullPointer() && (left.get() == null || right.get() == null)) { + return false; + } + return compare(left, right, errorReporter) <= 0; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/LessOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/LessOperator.java new file mode 100644 index 0000000..4879748 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/LessOperator.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime.operator.compare; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class LessOperator extends BaseBinaryOperator { + private static final LessOperator INSTANCE = new LessOperator(); + + private LessOperator() { + } + + public static LessOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "<"; + } + + @Override + public int getPriority() { + return QLPrecedences.COMPARE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + if (qlOptions.isAvoidNullPointer() && (left.get() == null || right.get() == null)) { + return false; + } + return compare(left, right, errorReporter) < 0; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/UnequalOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/UnequalOperator.java new file mode 100644 index 0000000..a60553a --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/compare/UnequalOperator.java @@ -0,0 +1,49 @@ +package com.alibaba.qlexpress4.runtime.operator.compare; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class UnequalOperator extends BaseBinaryOperator { + private static final Map INSTANCE_CACHE = new ConcurrentHashMap<>(2); + + static { + INSTANCE_CACHE.put("!=", new UnequalOperator("!=")); + INSTANCE_CACHE.put("<>", new UnequalOperator("<>")); + } + + private final String operator; + + private UnequalOperator(String operator) { + this.operator = operator; + } + + public static UnequalOperator getInstance(String operator) { + return INSTANCE_CACHE.get(operator); + } + + @Override + public String getOperator() { + return operator; + } + + @Override + public int getPriority() { + return QLPrecedences.EQUAL; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return !equals(left, right, errorReporter); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/logic/LogicAndOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/logic/LogicAndOperator.java new file mode 100644 index 0000000..69df6e4 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/logic/LogicAndOperator.java @@ -0,0 +1,62 @@ +package com.alibaba.qlexpress4.runtime.operator.logic; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class LogicAndOperator extends BaseBinaryOperator { + private static final Map INSTANCE_CACHE = new ConcurrentHashMap<>(2); + + static { + INSTANCE_CACHE.put("&&", new LogicAndOperator("&&")); + INSTANCE_CACHE.put("and", new LogicAndOperator("and")); + } + + private final String operator; + + private LogicAndOperator(String operator) { + this.operator = operator; + } + + public static LogicAndOperator getInstance(String operator) { + return INSTANCE_CACHE.get(operator); + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + Object leftValue = left.get(); + Object rightValue = right.get(); + if (leftValue == null) { + leftValue = false; + } + if (rightValue == null) { + rightValue = false; + } + + if (!(leftValue instanceof Boolean) || !(rightValue instanceof Boolean)) { + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + return (Boolean)leftValue && (Boolean)rightValue; + } + + @Override + public String getOperator() { + return operator; + } + + @Override + public int getPriority() { + return QLPrecedences.AND; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/logic/LogicNotOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/logic/LogicNotOperator.java new file mode 100644 index 0000000..f90350c --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/logic/LogicNotOperator.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.runtime.operator.logic; + +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseUnaryOperator; + +/** + * @author bingo + */ +public class LogicNotOperator extends BaseUnaryOperator { + private static final LogicNotOperator INSTANCE = new LogicNotOperator(); + + private LogicNotOperator() { + } + + public static LogicNotOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value value, ErrorReporter errorReporter) { + Object operand = value.get(); + if (operand == null) { + operand = false; + } + if (!(operand instanceof Boolean)) { + throw buildInvalidOperandTypeException(value, errorReporter); + } + + return !(Boolean)operand; + } + + @Override + public String getOperator() { + return "!"; + } + + @Override + public int getPriority() { + return QLPrecedences.UNARY; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/logic/LogicOrOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/logic/LogicOrOperator.java new file mode 100644 index 0000000..d64a520 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/logic/LogicOrOperator.java @@ -0,0 +1,55 @@ +package com.alibaba.qlexpress4.runtime.operator.logic; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class LogicOrOperator extends BaseBinaryOperator { + private static final Map INSTANCE_CACHE = new ConcurrentHashMap<>(2); + + static { + INSTANCE_CACHE.put("||", new LogicOrOperator("||")); + INSTANCE_CACHE.put("or", new LogicOrOperator("or")); + } + + private final String operator; + + private LogicOrOperator(String operator) { + this.operator = operator; + } + + public static LogicOrOperator getInstance(String operator) { + return INSTANCE_CACHE.get(operator); + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + Object leftValue = left.get(); + Object rightValue = right.get(); + if (!(leftValue instanceof Boolean) || !(rightValue instanceof Boolean)) { + throw buildInvalidOperandTypeException(left, right, errorReporter); + } + + return (Boolean)leftValue || (Boolean)rightValue; + } + + @Override + public String getOperator() { + return operator; + } + + @Override + public int getPriority() { + return QLPrecedences.OR; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/BigDecimalMath.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/BigDecimalMath.java new file mode 100644 index 0000000..3c16b58 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/BigDecimalMath.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.qlexpress4.runtime.operator.number; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +/** + * BigDecimal NumberMath operations + */ +public final class BigDecimalMath extends NumberMath { + + // This is an arbitrary value, picked as a reasonable choice for a precision + // for typical user math when a non-terminating result would otherwise occur. + public static final int DIVISION_EXTRA_PRECISION = Integer.getInteger("qlexpress4.division.extra.precision", 10); + + //This is an arbitrary value, picked as a reasonable choice for a rounding point + //for typical user math. + public static final int DIVISION_MIN_SCALE = Integer.getInteger("qlexpress4.division.min.scale", 10); + + public static final BigDecimalMath INSTANCE = new BigDecimalMath(); + + private BigDecimalMath() { + } + + @Override + public Number absImpl(Number number) { + return toBigDecimal(number).abs(); + } + + @Override + public Number addImpl(Number left, Number right) { + return toBigDecimal(left).add(toBigDecimal(right)); + } + + @Override + public Number subtractImpl(Number left, Number right) { + return toBigDecimal(left).subtract(toBigDecimal(right)); + } + + @Override + public Number multiplyImpl(Number left, Number right) { + return toBigDecimal(left).multiply(toBigDecimal(right)); + } + + @Override + public Number divideImpl(Number left, Number right) { + BigDecimal bigLeft = toBigDecimal(left); + BigDecimal bigRight = toBigDecimal(right); + try { + return bigLeft.divide(bigRight); + } + catch (ArithmeticException e) { + // set a DEFAULT precision if otherwise non-terminating + int precision = Math.max(bigLeft.precision(), bigRight.precision()) + DIVISION_EXTRA_PRECISION; + BigDecimal result = bigLeft.divide(bigRight, new MathContext(precision)); + int scale = Math.max(Math.max(bigLeft.scale(), bigRight.scale()), DIVISION_MIN_SCALE); + if (result.scale() > scale) { + result = result.setScale(scale, RoundingMode.HALF_UP); + } + return result; + } + } + + @Override + public int compareToImpl(Number left, Number right) { + return toBigDecimal(left).compareTo(toBigDecimal(right)); + } + + @Override + public Number unaryMinusImpl(Number left) { + return toBigDecimal(left).negate(); + } + + @Override + public Number unaryPlusImpl(Number left) { + return toBigDecimal(left); + } + + @Override + public Number remainderImpl(Number left, Number right) { + return toBigDecimal(left).remainder(toBigDecimal(right)); + } + + @Override + public Number modImpl(Number self, Number divisor) { + BigDecimal selfDecimal = toBigDecimal(self); + BigDecimal divDecimal = toBigDecimal(divisor); + BigDecimal remainder = selfDecimal.remainder(divDecimal); + return remainder.signum() < 0 ? remainder.add(divDecimal) : remainder; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/BigIntegerMath.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/BigIntegerMath.java new file mode 100644 index 0000000..07af321 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/BigIntegerMath.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.qlexpress4.runtime.operator.number; + +/** + * BigInteger NumberMath operations + * reference groovy source code + */ +public final class BigIntegerMath extends NumberMath { + + public static final BigIntegerMath INSTANCE = new BigIntegerMath(); + + private BigIntegerMath() { + } + + @Override + public Number absImpl(Number number) { + return toBigInteger(number).abs(); + } + + @Override + public Number addImpl(Number left, Number right) { + return toBigInteger(left).add(toBigInteger(right)); + } + + @Override + public Number subtractImpl(Number left, Number right) { + return toBigInteger(left).subtract(toBigInteger(right)); + } + + @Override + public Number multiplyImpl(Number left, Number right) { + return toBigInteger(left).multiply(toBigInteger(right)); + } + + @Override + public Number divideImpl(Number left, Number right) { + return BigDecimalMath.INSTANCE.divideImpl(left, right); + } + + @Override + public int compareToImpl(Number left, Number right) { + return toBigInteger(left).compareTo(toBigInteger(right)); + } + + @Override + public Number intDivImpl(Number left, Number right) { + return toBigInteger(left).divide(toBigInteger(right)); + } + + @Override + public Number modImpl(Number left, Number right) { + return toBigInteger(left).mod(toBigInteger(right)); + } + + @Override + public Number remainderImpl(Number left, Number right) { + return toBigInteger(left).remainder(toBigInteger(right)); + } + + @Override + public Number unaryMinusImpl(Number left) { + return toBigInteger(left).negate(); + } + + @Override + public Number unaryPlusImpl(Number left) { + return toBigInteger(left); + } + + @Override + public Number bitwiseNegateImpl(Number left) { + return toBigInteger(left).not(); + } + + @Override + public Number orImpl(Number left, Number right) { + return toBigInteger(left).or(toBigInteger(right)); + } + + @Override + public Number andImpl(Number left, Number right) { + return toBigInteger(left).and(toBigInteger(right)); + } + + @Override + public Number xorImpl(Number left, Number right) { + return toBigInteger(left).xor(toBigInteger(right)); + } + + @Override + public Number leftShiftImpl(Number left, Number right) { + return toBigInteger(left).shiftLeft(right.intValue()); + } + + @Override + public Number rightShiftImpl(Number left, Number right) { + return toBigInteger(left).shiftRight(right.intValue()); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/FloatingPointMath.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/FloatingPointMath.java new file mode 100644 index 0000000..3306cca --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/FloatingPointMath.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.qlexpress4.runtime.operator.number; + +/** + * FloatingPoint (Double and Float) NumberMath operations + * reference groovy source code + */ +public final class FloatingPointMath extends NumberMath { + + public static final FloatingPointMath INSTANCE = new FloatingPointMath(); + + private FloatingPointMath() { + } + + @Override + public Number absImpl(Number number) { + return Math.abs(number.doubleValue()); + } + + @Override + public Number addImpl(Number left, Number right) { + return left.doubleValue() + right.doubleValue(); + } + + @Override + public Number subtractImpl(Number left, Number right) { + return left.doubleValue() - right.doubleValue(); + } + + @Override + public Number multiplyImpl(Number left, Number right) { + return left.doubleValue() * right.doubleValue(); + } + + @Override + public Number divideImpl(Number left, Number right) { + return left.doubleValue() / right.doubleValue(); + } + + @Override + public int compareToImpl(Number left, Number right) { + return Double.compare(left.doubleValue(), right.doubleValue()); + } + + @Override + public Number remainderImpl(Number left, Number right) { + return left.doubleValue() % right.doubleValue(); + } + + @Override + public Number modImpl(Number left, Number right) { + return toBigInteger(left).mod(toBigInteger(right)).doubleValue(); + } + + @Override + public Number unaryMinusImpl(Number left) { + return -left.doubleValue(); + } + + @Override + public Number unaryPlusImpl(Number left) { + return left.doubleValue(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/IntegerMath.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/IntegerMath.java new file mode 100644 index 0000000..412bd3b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/IntegerMath.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.qlexpress4.runtime.operator.number; + +/** + * Integer NumberMath operations + * reference groovy source code + */ +public final class IntegerMath extends NumberMath { + + public static final IntegerMath INSTANCE = new IntegerMath(); + + private IntegerMath() { + } + + @Override + public Number absImpl(Number number) { + return Math.abs(number.intValue()); + } + + @Override + public Number addImpl(Number left, Number right) { + return left.intValue() + right.intValue(); + } + + @Override + public Number subtractImpl(Number left, Number right) { + return left.intValue() - right.intValue(); + } + + @Override + public Number multiplyImpl(Number left, Number right) { + return left.intValue() * right.intValue(); + } + + @Override + public Number divideImpl(Number left, Number right) { + return BigDecimalMath.INSTANCE.divideImpl(left, right); + } + + @Override + public int compareToImpl(Number left, Number right) { + int leftVal = left.intValue(); + int rightVal = right.intValue(); + return Integer.compare(leftVal, rightVal); + } + + @Override + public Number orImpl(Number left, Number right) { + return left.intValue() | right.intValue(); + } + + @Override + public Number andImpl(Number left, Number right) { + return left.intValue() & right.intValue(); + } + + @Override + public Number xorImpl(Number left, Number right) { + return left.intValue() ^ right.intValue(); + } + + @Override + public Number intDivImpl(Number left, Number right) { + return left.intValue() / right.intValue(); + } + + @Override + public Number modImpl(Number left, Number right) { + return toBigInteger(left).mod(toBigInteger(right)).intValue(); + } + + @Override + public Number remainderImpl(Number left, Number right) { + return left.intValue() % right.intValue(); + } + + @Override + public Number unaryMinusImpl(Number left) { + return -left.intValue(); + } + + @Override + public Number unaryPlusImpl(Number left) { + return left.intValue(); + } + + @Override + public Number bitwiseNegateImpl(Number left) { + return ~left.intValue(); + } + + @Override + public Number leftShiftImpl(Number left, Number right) { + return left.intValue() << right.intValue(); + } + + @Override + public Number rightShiftImpl(Number left, Number right) { + return left.intValue() >> right.intValue(); + } + + @Override + public Number rightShiftUnsignedImpl(Number left, Number right) { + return left.intValue() >>> right.intValue(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/LongMath.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/LongMath.java new file mode 100644 index 0000000..6a78454 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/LongMath.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.qlexpress4.runtime.operator.number; + +/** + * Long NumberMath operations + * reference groovy source code + */ +public final class LongMath extends NumberMath { + + public static final LongMath INSTANCE = new LongMath(); + + private LongMath() { + } + + @Override + public Number absImpl(Number number) { + return Math.abs(number.longValue()); + } + + @Override + public Number addImpl(Number left, Number right) { + return left.longValue() + right.longValue(); + } + + @Override + public Number subtractImpl(Number left, Number right) { + return left.longValue() - right.longValue(); + } + + @Override + public Number multiplyImpl(Number left, Number right) { + return left.longValue() * right.longValue(); + } + + @Override + public Number divideImpl(Number left, Number right) { + return BigDecimalMath.INSTANCE.divideImpl(left, right); + } + + @Override + public int compareToImpl(Number left, Number right) { + long leftVal = left.longValue(); + long rightVal = right.longValue(); + return Long.compare(leftVal, rightVal); + } + + @Override + public Number intDivImpl(Number left, Number right) { + return left.longValue() / right.longValue(); + } + + @Override + public Number remainderImpl(Number left, Number right) { + return left.longValue() % right.longValue(); + } + + @Override + public Number modImpl(Number left, Number right) { + return toBigInteger(left).mod(toBigInteger(right)).longValue(); + } + + @Override + public Number unaryMinusImpl(Number left) { + return -left.longValue(); + } + + @Override + public Number unaryPlusImpl(Number left) { + return left.longValue(); + } + + @Override + public Number bitwiseNegateImpl(Number left) { + return ~left.longValue(); + } + + @Override + public Number orImpl(Number left, Number right) { + return left.longValue() | right.longValue(); + } + + @Override + public Number andImpl(Number left, Number right) { + return left.longValue() & right.longValue(); + } + + @Override + public Number xorImpl(Number left, Number right) { + return left.longValue() ^ right.longValue(); + } + + @Override + public Number leftShiftImpl(Number left, Number right) { + return left.longValue() << right.longValue(); + } + + @Override + public Number rightShiftImpl(Number left, Number right) { + return left.longValue() >> right.longValue(); + } + + @Override + public Number rightShiftUnsignedImpl(Number left, Number right) { + return left.longValue() >>> right.longValue(); + } + + public Number bitAndImpl(Number left, Number right) { + return left.longValue() & right.longValue(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/NumberMath.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/NumberMath.java new file mode 100644 index 0000000..cc04d31 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/number/NumberMath.java @@ -0,0 +1,338 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.qlexpress4.runtime.operator.number; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Stateless objects used to perform math on the various Number subclasses. + * Instances are required so that polymorphic calls work properly, but each + * subclass creates a singleton instance to minimize garbage. All methods + * must be thread-safe. + * + * The design goals of this class are as follows: + *

    + *
  1. Support a 'least surprising' math model to scripting language users. This + * means that exact, or decimal math should be used for default calculations. This + * scheme assumes that by default, groovy literals with decimal points are instantiated + * as BigDecimal objects rather than binary floating points (Float, Double). + *
  2. Do not force the appearance of exactness on a number that is by definition not + * guaranteed to be exact. In particular this means that if an operand in a NumberMath + * operation is a binary floating point number, ensure that the result remains a binary floating point + * number (i.e. never automatically promote a binary floating point number to a BigDecimal). + * This has the effect of preserving the expectations of binary floating point users and helps performance. + *
  3. Provide an implementation that is as close as practical to the Java 1.5 BigDecimal math model which implements + * precision based floating point decimal math (ANSI X3.274-1996 and ANSI X3.274-1996/AM 1-2000 (section 7.4). + *
+ * reference groovy source code + */ +public abstract class NumberMath { + + public static Number abs(Number number) { + return getMath(number).absImpl(number); + } + + public static Number add(Number left, Number right) { + return getMath(left, right).addImpl(left, right); + } + + public static Number subtract(Number left, Number right) { + return getMath(left, right).subtractImpl(left, right); + } + + public static Number multiply(Number left, Number right) { + return getMath(left, right).multiplyImpl(left, right); + } + + public static Number divide(Number left, Number right) { + return getMath(left, right).divideImpl(left, right); + } + + public static int compareTo(Number left, Number right) { + return getMath(left, right).compareToImpl(left, right); + } + + public static Number or(Number left, Number right) { + return getMath(left, right).orImpl(left, right); + } + + public static Number and(Number left, Number right) { + return getMath(left, right).andImpl(left, right); + } + + public static Number xor(Number left, Number right) { + return getMath(left, right).xorImpl(left, right); + } + + public static Number intDiv(Number left, Number right) { + return getMath(left, right).intDivImpl(left, right); + } + + // retain for backwards compatibility + public static Number mod(Number left, Number right) { + return getMath(left, right).modImpl(left, right); + } + + public static Number remainder(Number left, Number right) { + return getMath(left, right).remainderImpl(left, right); + } + + /* + * For this operation, consider the operands independently. Throw an exception if the right operand + * (shift distance) is not an integral type. For the left operand (shift value) also require an integral + * type, but do NOT promote from Integer to Long. This is consistent with Java, and makes sense for the + * shift operators. + */ + public static Number leftShift(Number left, Number right) { + if (isFloatingPoint(right) || isBigDecimal(right)) { + throw new UnsupportedOperationException("Shift distance must be an integral type, but " + right + " (" + + right.getClass().getName() + ") was supplied"); + } + return getMath(left).leftShiftImpl(left, right); + } + + /* + * For this operation, consider the operands independently. Throw an exception if the right operand + * (shift distance) is not an integral type. For the left operand (shift value) also require an integral + * type, but do NOT promote from Integer to Long. This is consistent with Java, and makes sense for the + * shift operators. + */ + public static Number rightShift(Number left, Number right) { + if (isFloatingPoint(right) || isBigDecimal(right)) { + throw new UnsupportedOperationException("Shift distance must be an integral type, but " + right + " (" + + right.getClass().getName() + ") was supplied"); + } + return getMath(left).rightShiftImpl(left, right); + } + + /* + * For this operation, consider the operands independently. Throw an exception if the right operand + * (shift distance) is not an integral type. For the left operand (shift value) also require an integral + * type, but do NOT promote from Integer to Long. This is consistent with Java, and makes sense for the + * shift operators. + */ + public static Number rightShiftUnsigned(Number left, Number right) { + if (isFloatingPoint(right) || isBigDecimal(right)) { + throw new UnsupportedOperationException("Shift distance must be an integral type, but " + right + " (" + + right.getClass().getName() + ") was supplied"); + } + return getMath(left).rightShiftUnsignedImpl(left, right); + } + + public static Number bitwiseNegate(Number left) { + return getMath(left).bitwiseNegateImpl(left); + } + + public static Number unaryMinus(Number left) { + return getMath(left).unaryMinusImpl(left); + } + + public static Number unaryPlus(Number left) { + return getMath(left).unaryPlusImpl(left); + } + + public static boolean isFloatingPoint(Number number) { + return number instanceof Double || number instanceof Float; + } + + public static boolean isInteger(Number number) { + return number instanceof Integer; + } + + public static boolean isShort(Number number) { + return number instanceof Short; + } + + public static boolean isByte(Number number) { + return number instanceof Byte; + } + + public static boolean isLong(Number number) { + return number instanceof Long; + } + + public static boolean isBigDecimal(Number number) { + return number instanceof BigDecimal; + } + + public static boolean isBigInteger(Number number) { + return number instanceof BigInteger; + } + + public static BigDecimal toBigDecimal(Number n) { + if (n instanceof BigDecimal) { + return (BigDecimal)n; + } + if (n instanceof BigInteger) { + return new BigDecimal((BigInteger)n); + } + if (n instanceof Integer || n instanceof Long || n instanceof Byte || n instanceof Short) { + return BigDecimal.valueOf(n.longValue()); + } + try { + return new BigDecimal(n.toString()); + } + catch (NumberFormatException nfe) { + return BigDecimal.valueOf(n.doubleValue()); + } + } + + public static BigInteger toBigInteger(Number n) { + if (n instanceof BigInteger) { + return (BigInteger)n; + } + if (n instanceof Integer || n instanceof Long || n instanceof Byte || n instanceof Short) { + return BigInteger.valueOf(n.longValue()); + } + + if (n instanceof Float || n instanceof Double) { + BigDecimal bd = new BigDecimal(n.toString()); + return bd.toBigInteger(); + } + if (n instanceof BigDecimal) { + return ((BigDecimal)n).toBigInteger(); + } + + return new BigInteger(n.toString()); + } + + /* + * Determine which NumberMath instance to use, given the supplied operands. This method implements + * the type promotion rules discussed in the documentation. Note that by the time this method is + * called, any Byte, Character or Short operands will have been promoted to Integer. For reference, + * here is the promotion matrix: + * bD bI D F L I + * bD bD bD D D bD bD + * bI bD bI D D bI bI + * D D D D D D D + * F D D D D D D + * L bD bI D D L L + * I bD bI D D L I + * + * Note that for division, if either operand isFloatingPoint, the result will be floating. Otherwise, + * the result is BigDecimal + */ + public static NumberMath getMath(Number left, Number right) { + // FloatingPointMath wins according to promotion Matrix + if (isFloatingPoint(left) || isFloatingPoint(right)) { + return FloatingPointMath.INSTANCE; + } + NumberMath leftMath = getMath(left); + NumberMath rightMath = getMath(right); + + if (leftMath == BigDecimalMath.INSTANCE || rightMath == BigDecimalMath.INSTANCE) { + return BigDecimalMath.INSTANCE; + } + if (leftMath == BigIntegerMath.INSTANCE || rightMath == BigIntegerMath.INSTANCE) { + return BigIntegerMath.INSTANCE; + } + if (leftMath == LongMath.INSTANCE || rightMath == LongMath.INSTANCE) { + return LongMath.INSTANCE; + } + if (leftMath == IntegerMath.INSTANCE || rightMath == IntegerMath.INSTANCE) { + return IntegerMath.INSTANCE; + } + // also for custom Number implementations + return BigDecimalMath.INSTANCE; + } + + /* package private */ + static NumberMath getMath(Number number) { + if (isLong(number)) { + return LongMath.INSTANCE; + } + if (isFloatingPoint(number)) { + return FloatingPointMath.INSTANCE; + } + if (isBigDecimal(number)) { + return BigDecimalMath.INSTANCE; + } + if (isBigInteger(number)) { + return BigIntegerMath.INSTANCE; + } + if (isInteger(number) || isShort(number) || isByte(number)) { + return IntegerMath.INSTANCE; + } + // also for custom Number implementations + return BigDecimalMath.INSTANCE; + } + + //Subclasses implement according to the type promotion hierarchy rules + protected abstract Number absImpl(Number number); + + public abstract Number addImpl(Number left, Number right); + + public abstract Number subtractImpl(Number left, Number right); + + public abstract Number multiplyImpl(Number left, Number right); + + public abstract Number divideImpl(Number left, Number right); + + public abstract int compareToImpl(Number left, Number right); + + protected abstract Number unaryMinusImpl(Number left); + + protected abstract Number unaryPlusImpl(Number left); + + protected Number bitwiseNegateImpl(Number left) { + throw createUnsupportedException("bitwiseNegate()", left); + } + + protected Number orImpl(Number left, Number right) { + throw createUnsupportedException("or()", left); + } + + protected Number andImpl(Number left, Number right) { + throw createUnsupportedException("and()", left); + } + + protected Number xorImpl(Number left, Number right) { + throw createUnsupportedException("xor()", left); + } + + protected Number remainderImpl(Number left, Number right) { + throw createUnsupportedException("remainder()", left); + } + + protected Number modImpl(Number left, Number right) { + throw createUnsupportedException("mod()", left); + } + + protected Number intDivImpl(Number left, Number right) { + throw createUnsupportedException("intDiv()", left); + } + + protected Number leftShiftImpl(Number left, Number right) { + throw createUnsupportedException("leftShift()", left); + } + + protected Number rightShiftImpl(Number left, Number right) { + throw createUnsupportedException("rightShift()", left); + } + + protected Number rightShiftUnsignedImpl(Number left, Number right) { + throw createUnsupportedException("rightShiftUnsigned()", left); + } + + protected UnsupportedOperationException createUnsupportedException(String operation, Number left) { + return new UnsupportedOperationException( + "Cannot use " + operation + " on this number type: " + left.getClass().getName() + " with value: " + left); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/string/LikeOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/string/LikeOperator.java new file mode 100644 index 0000000..d04ca0d --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/string/LikeOperator.java @@ -0,0 +1,36 @@ +package com.alibaba.qlexpress4.runtime.operator.string; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.BinaryOperator; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * Author: DQinYuan + */ +public class LikeOperator extends BaseBinaryOperator { + private static final LikeOperator INSTANCE = new LikeOperator(); + + public static BinaryOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return like(left, right, errorReporter); + } + + @Override + public String getOperator() { + return "like"; + } + + @Override + public int getPriority() { + return QLPrecedences.IN_LIKE; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/string/NotLikeOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/string/NotLikeOperator.java new file mode 100644 index 0000000..1a6f9fd --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/string/NotLikeOperator.java @@ -0,0 +1,36 @@ +package com.alibaba.qlexpress4.runtime.operator.string; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.QRuntime; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.BinaryOperator; +import com.alibaba.qlexpress4.runtime.operator.base.BaseBinaryOperator; + +/** + * @author bingo + */ +public class NotLikeOperator extends BaseBinaryOperator { + private static final NotLikeOperator INSTANCE = new NotLikeOperator(); + + public static BinaryOperator getInstance() { + return INSTANCE; + } + + @Override + public Object execute(Value left, Value right, QRuntime qRuntime, QLOptions qlOptions, + ErrorReporter errorReporter) { + return !like(left, right, errorReporter); + } + + @Override + public String getOperator() { + return "not_like"; + } + + @Override + public int getPriority() { + return QLPrecedences.IN_LIKE; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusPrefixUnaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusPrefixUnaryOperator.java new file mode 100644 index 0000000..5339862 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusPrefixUnaryOperator.java @@ -0,0 +1,45 @@ +package com.alibaba.qlexpress4.runtime.operator.unary; + +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.number.NumberMath; + +/** + * @author bingo + */ +public class MinusMinusPrefixUnaryOperator extends BaseUnaryOperator { + private static final MinusMinusPrefixUnaryOperator INSTANCE = new MinusMinusPrefixUnaryOperator(); + + private MinusMinusPrefixUnaryOperator() { + } + + public static MinusMinusPrefixUnaryOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "--"; + } + + @Override + public int getPriority() { + return QLPrecedences.UNARY; + } + + @Override + public Object execute(Value value, ErrorReporter errorReporter) { + Object operand = value.get(); + if (!(operand instanceof Number)) { + throw buildInvalidOperandTypeException(value, errorReporter); + } + + if (value instanceof LeftValue) { + ((LeftValue)value).set(NumberMath.subtract((Number)operand, 1), errorReporter); + } + return operand; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusSuffixUnaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusSuffixUnaryOperator.java new file mode 100644 index 0000000..0ae9b8d --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusSuffixUnaryOperator.java @@ -0,0 +1,46 @@ +package com.alibaba.qlexpress4.runtime.operator.unary; + +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.number.NumberMath; + +/** + * @author bingo + */ +public class MinusMinusSuffixUnaryOperator extends BaseUnaryOperator { + private static final MinusMinusSuffixUnaryOperator INSTANCE = new MinusMinusSuffixUnaryOperator(); + + private MinusMinusSuffixUnaryOperator() { + } + + public static MinusMinusSuffixUnaryOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "--"; + } + + @Override + public int getPriority() { + return QLPrecedences.UNARY_SUFFIX; + } + + @Override + public Object execute(Value value, ErrorReporter errorReporter) { + Object operand = value.get(); + if (!(operand instanceof Number)) { + throw buildInvalidOperandTypeException(value, errorReporter); + } + + Number result = NumberMath.subtract((Number)operand, 1); + if (value instanceof LeftValue) { + ((LeftValue)value).set(result, errorReporter); + } + return result; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusUnaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusUnaryOperator.java new file mode 100644 index 0000000..3b185b0 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusUnaryOperator.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime.operator.unary; + +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.number.NumberMath; + +/** + * @author bingo + */ +public class MinusUnaryOperator extends BaseUnaryOperator { + private static final MinusUnaryOperator INSTANCE = new MinusUnaryOperator(); + + private MinusUnaryOperator() { + } + + public static MinusUnaryOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "-"; + } + + @Override + public int getPriority() { + return QLPrecedences.UNARY; + } + + @Override + public Object execute(Value value, ErrorReporter errorReporter) { + Object operand = value.get(); + if (!(operand instanceof Number)) { + throw buildInvalidOperandTypeException(value, errorReporter); + } + + return NumberMath.unaryMinus((Number)operand); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/PlusPlusPrefixUnaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/PlusPlusPrefixUnaryOperator.java new file mode 100644 index 0000000..38dd7f2 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/PlusPlusPrefixUnaryOperator.java @@ -0,0 +1,46 @@ +package com.alibaba.qlexpress4.runtime.operator.unary; + +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.number.NumberMath; + +/** + * @author bingo + */ +public class PlusPlusPrefixUnaryOperator extends BaseUnaryOperator { + private static final PlusPlusPrefixUnaryOperator INSTANCE = new PlusPlusPrefixUnaryOperator(); + + private PlusPlusPrefixUnaryOperator() { + } + + public static PlusPlusPrefixUnaryOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "++"; + } + + @Override + public int getPriority() { + return QLPrecedences.UNARY; + } + + @Override + public Object execute(Value value, ErrorReporter errorReporter) { + Object operand = value.get(); + if (!(operand instanceof Number)) { + throw buildInvalidOperandTypeException(value, errorReporter); + } + + Number result = NumberMath.add((Number)operand, 1); + if (value instanceof LeftValue) { + ((LeftValue)value).set(result, errorReporter); + } + return result; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/PlusPlusSuffixUnaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/PlusPlusSuffixUnaryOperator.java new file mode 100644 index 0000000..110a8a5 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/PlusPlusSuffixUnaryOperator.java @@ -0,0 +1,45 @@ +package com.alibaba.qlexpress4.runtime.operator.unary; + +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.number.NumberMath; + +/** + * @author bingo + */ +public class PlusPlusSuffixUnaryOperator extends BaseUnaryOperator { + private static final PlusPlusSuffixUnaryOperator INSTANCE = new PlusPlusSuffixUnaryOperator(); + + private PlusPlusSuffixUnaryOperator() { + } + + public static PlusPlusSuffixUnaryOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "++"; + } + + @Override + public int getPriority() { + return QLPrecedences.UNARY_SUFFIX; + } + + @Override + public Object execute(Value value, ErrorReporter errorReporter) { + Object operand = value.get(); + if (!(operand instanceof Number)) { + throw buildInvalidOperandTypeException(value, errorReporter); + } + + if (value instanceof LeftValue) { + ((LeftValue)value).set(NumberMath.add((Number)operand, 1), errorReporter); + } + return operand; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/PlusUnaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/PlusUnaryOperator.java new file mode 100644 index 0000000..4125157 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/PlusUnaryOperator.java @@ -0,0 +1,41 @@ +package com.alibaba.qlexpress4.runtime.operator.unary; + +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.base.BaseUnaryOperator; +import com.alibaba.qlexpress4.runtime.operator.number.NumberMath; + +/** + * @author bingo + */ +public class PlusUnaryOperator extends BaseUnaryOperator { + private static final PlusUnaryOperator INSTANCE = new PlusUnaryOperator(); + + private PlusUnaryOperator() { + } + + public static PlusUnaryOperator getInstance() { + return INSTANCE; + } + + @Override + public String getOperator() { + return "+"; + } + + @Override + public int getPriority() { + return QLPrecedences.UNARY; + } + + @Override + public Object execute(Value value, ErrorReporter errorReporter) { + Object operand = value.get(); + if (!(operand instanceof Number)) { + throw buildInvalidOperandTypeException(value, errorReporter); + } + + return NumberMath.unaryPlus((Number)operand); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/UnaryOperator.java b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/UnaryOperator.java new file mode 100644 index 0000000..1cb3590 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/operator/unary/UnaryOperator.java @@ -0,0 +1,20 @@ +package com.alibaba.qlexpress4.runtime.operator.unary; + +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.Operator; + +/** + * unary operator, include: + * prefix operator + * suffix operator + * Author: DQinYuan + */ +public interface UnaryOperator extends Operator { + /** + * @param value operand + * @param errorReporter operator + * @return result of operator + */ + Object execute(Value value, ErrorReporter errorReporter); +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/scope/QScope.java b/src/main/java/com/alibaba/qlexpress4/runtime/scope/QScope.java new file mode 100644 index 0000000..575768b --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/scope/QScope.java @@ -0,0 +1,91 @@ +package com.alibaba.qlexpress4.runtime.scope; + +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +import java.util.Map; + +/** + * Author: DQinYuan + */ +public interface QScope { + + /** + * get assignable symbol variable by name + * @param varName variable name + * @return value, null if not exist + */ + Value getSymbol(String varName); + + /** + * get symbol variable value by name + * @param varName variable name + * @return inner value, null if not exist + */ + default Object getSymbolValue(String varName) { + Value symbolVal = getSymbol(varName); + return symbolVal == null ? null : symbolVal.get(); + } + + /** + * define a symbol in local scope + * @param varName variable name + * @param varClz class of variable + * @param value init value + */ + void defineLocalSymbol(String varName, Class varClz, Object value); + + /** + * define local function in scope + * @param functionName name of function + * @param function implement of function + */ + void defineFunction(String functionName, CustomFunction function); + + /** + * get function or lambda define + * @param functionName name of function + * @return null if not exist + */ + CustomFunction getFunction(String functionName); + + /** + * get function table in this scope + * @return function table + */ + Map getFunctionTable(); + + /** + * push value on the top of stack + * @param value pushed element + */ + void push(Value value); + + /** + * pop number elements on top of stack + * @param number pop elements' number + * @return popped elements + */ + Parameters pop(int number); + + /** + * pop one element on top of stack + * @return popped element + */ + Value pop(); + + /** + * @return top element of stack without pop + */ + Value peek(); + + /** + * @return parent scope + */ + QScope getParent(); + + /** + * @return new scope base origin stack + */ + QScope newScope(); +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/scope/QvmBlockScope.java b/src/main/java/com/alibaba/qlexpress4/runtime/scope/QvmBlockScope.java new file mode 100644 index 0000000..de3bedd --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/scope/QvmBlockScope.java @@ -0,0 +1,96 @@ +package com.alibaba.qlexpress4.runtime.scope; + +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.data.AssignableDataValue; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +import java.util.HashMap; +import java.util.Map; + +/** + * Author: DQinYuan + */ +public class QvmBlockScope implements QScope { + + private final QScope parent; + + private final Map symbolTable; + + private final Map functionTable; + + private final FixedSizeStack opStack; + + private final ExceptionTable exceptionTable; + + public QvmBlockScope(QScope parent, Map symbolTable, int maxStackSize, + ExceptionTable exceptionTable) { + this(parent, symbolTable, new FixedSizeStack(maxStackSize), exceptionTable); + } + + public QvmBlockScope(QScope parent, Map symbolTable, FixedSizeStack reuseStack, + ExceptionTable exceptionTable) { + this.parent = parent; + this.symbolTable = symbolTable; + this.functionTable = new HashMap<>(); + this.opStack = reuseStack; + this.exceptionTable = exceptionTable; + } + + @Override + public Value getSymbol(String varName) { + Value localSymbol = symbolTable.get(varName); + return localSymbol != null ? localSymbol : parent.getSymbol(varName); + } + + @Override + public void defineLocalSymbol(String varName, Class varClz, Object value) { + symbolTable.put(varName, new AssignableDataValue(varName, value, varClz)); + } + + @Override + public void defineFunction(String functionName, CustomFunction function) { + functionTable.put(functionName, function); + } + + @Override + public CustomFunction getFunction(String functionName) { + CustomFunction function = functionTable.get(functionName); + return function == null ? parent.getFunction(functionName) : function; + } + + @Override + public Map getFunctionTable() { + return functionTable; + } + + @Override + public void push(Value value) { + opStack.push(value); + } + + @Override + public Parameters pop(int number) { + return opStack.pop(number); + } + + @Override + public Value pop() { + return opStack.pop(); + } + + @Override + public Value peek() { + return opStack.peak(); + } + + @Override + public QScope getParent() { + return parent; + } + + @Override + public QScope newScope() { + return new QvmBlockScope(this, new HashMap<>(), opStack, exceptionTable); + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/trace/ExpressionTrace.java b/src/main/java/com/alibaba/qlexpress4/runtime/trace/ExpressionTrace.java new file mode 100644 index 0000000..d7cbce5 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/trace/ExpressionTrace.java @@ -0,0 +1,96 @@ +package com.alibaba.qlexpress4.runtime.trace; + +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.List; + +public class ExpressionTrace { + + public ExpressionTrace(TraceType type, String token, List children, Integer line, Integer col, + Integer position) { + this.type = type; + this.token = token; + this.children = children; + this.line = line; + this.col = col; + this.position = position; + } + + private final TraceType type; + + private final String token; + + /** + * Intermediate calculation result of this trace point + */ + private Object value; + + /** + * true if this point is evaluated in this execution + * false if short-circuited + */ + private boolean evaluated; + + private final List children; + + /** + * The corresponding line number in the source code + */ + private final int line; + + /** + * The corresponding col number in the source code + */ + private final int col; + + /** + * The corresponding position of character in the source code string + */ + private final int position; + + public String toPrettyString(int indent) { + StringBuilder nodeStringBuilder = new StringBuilder( + PrintlnUtils.buildIndentString(indent, type + " " + token + " " + (evaluated ? value : ""))).append('\n'); + for (ExpressionTrace child : children) { + nodeStringBuilder.append(child.toPrettyString(indent + 2)); + } + return nodeStringBuilder.toString(); + } + + public void valueEvaluated(Object value) { + this.value = value; + this.evaluated = true; + } + + public TraceType getType() { + return type; + } + + public String getToken() { + return token; + } + + public Object getValue() { + return value; + } + + public boolean isEvaluated() { + return evaluated; + } + + public List getChildren() { + return children; + } + + public int getLine() { + return line; + } + + public int getCol() { + return col; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/trace/QTraces.java b/src/main/java/com/alibaba/qlexpress4/runtime/trace/QTraces.java new file mode 100644 index 0000000..90e3dbf --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/trace/QTraces.java @@ -0,0 +1,30 @@ +package com.alibaba.qlexpress4.runtime.trace; + +import java.util.List; +import java.util.Map; + +public class QTraces { + + private final List expressionTraces; + + private final Map expressionTraceMap; + + public QTraces(List expressionTraces, Map expressionTraceMap) { + this.expressionTraces = expressionTraces; + this.expressionTraceMap = expressionTraceMap; + } + + public ExpressionTrace getExpressionTraceByKey(Integer traceKey) { + if (traceKey == null) { + return null; + } + if (expressionTraceMap == null) { + return null; + } + return expressionTraceMap.get(traceKey); + } + + public List getExpressionTraces() { + return expressionTraces; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/trace/TracePointTree.java b/src/main/java/com/alibaba/qlexpress4/runtime/trace/TracePointTree.java new file mode 100644 index 0000000..f9c2ec9 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/trace/TracePointTree.java @@ -0,0 +1,62 @@ +package com.alibaba.qlexpress4.runtime.trace; + +import com.alibaba.qlexpress4.utils.PrintlnUtils; + +import java.util.List; + +public class TracePointTree { + private final TraceType type; + + private final String token; + + private final List children; + + private final int line; + + private final int col; + + private final int position; + + public TracePointTree(TraceType type, String token, List children, int line, int col, + int position) { + this.type = type; + this.token = token; + this.children = children; + this.line = line; + this.col = col; + this.position = position; + } + + public String toPrettyString(int indent) { + StringBuilder nodeStringBuilder = + new StringBuilder(PrintlnUtils.buildIndentString(indent, type + " " + token)).append('\n'); + for (TracePointTree child : children) { + nodeStringBuilder.append(child.toPrettyString(indent + 2)); + } + return nodeStringBuilder.toString(); + } + + public TraceType getType() { + return type; + } + + public String getToken() { + return token; + } + + public List getChildren() { + return children; + } + + public int getLine() { + return line; + } + + public int getCol() { + return col; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/trace/TraceType.java b/src/main/java/com/alibaba/qlexpress4/runtime/trace/TraceType.java new file mode 100644 index 0000000..4400ca1 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/trace/TraceType.java @@ -0,0 +1,10 @@ +package com.alibaba.qlexpress4.runtime.trace; + +public enum TraceType { + // parent + OPERATOR, FUNCTION, METHOD, FIELD, LIST, MAP, IF, RETURN, BLOCK, + // children + VARIABLE, VALUE, DEFINE_FUNCTION, DEFINE_MACRO, + // other composite children + PRIMARY, STATEMENT +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/util/MethodInvokeUtils.java b/src/main/java/com/alibaba/qlexpress4/runtime/util/MethodInvokeUtils.java new file mode 100644 index 0000000..0552766 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/util/MethodInvokeUtils.java @@ -0,0 +1,73 @@ +package com.alibaba.qlexpress4.runtime.util; + +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.member.MethodHandler; +import com.alibaba.qlexpress4.runtime.IMethod; +import com.alibaba.qlexpress4.runtime.QLambda; +import com.alibaba.qlexpress4.runtime.QResult; +import com.alibaba.qlexpress4.runtime.ReflectLoader; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.data.convert.ParametersTypeConvertor; + +import java.util.Arrays; +import java.util.Map; + +/** + * Author: DQinYuan + */ +public class MethodInvokeUtils { + + public static Value findMethodAndInvoke(Object bean, String methodName, Object[] params, Class[] type, + ReflectLoader reflectLoader, ErrorReporter errorReporter) { + IMethod method = reflectLoader.loadMethod(bean, methodName, type); + if (method == null) { + QLambda qLambdaInnerMethod = findQLambdaInstance(bean, methodName); + if (qLambdaInnerMethod != null) { + try { + QResult qResult = qLambdaInnerMethod.call(params); + return ValueUtils.toImmutable(qResult.getResult()); + } + catch (UserDefineException e) { + throw ThrowUtils.reportUserDefinedException(errorReporter, e); + } + catch (Throwable t) { + throw ThrowUtils.wrapThrowable(t, + errorReporter, + QLErrorCodes.INVOKE_LAMBDA_ERROR.name(), + QLErrorCodes.INVOKE_LAMBDA_ERROR.getErrorMsg()); + } + } + else { + throw errorReporter.report(QLErrorCodes.METHOD_NOT_FOUND.name(), + String.format(QLErrorCodes.METHOD_NOT_FOUND.getErrorMsg(), methodName, Arrays.toString(params))); + } + } + else { + // method invoke + Object[] convertResult = + ParametersTypeConvertor.cast(params, method.getParameterTypes(), method.isVarArgs()); + try { + Object value = MethodHandler.Access.accessMethodValue(method, bean, convertResult); + return new DataValue(value); + } + catch (Exception e) { + throw ReflectLoader.unwrapMethodInvokeEx(errorReporter, methodName, e); + } + } + } + + private static QLambda findQLambdaInstance(Object bean, String methodName) { + if (bean instanceof Map) { + Map map = (Map)bean; + Object mapValue = map.get(methodName); + if (mapValue instanceof QLambda) { + return (QLambda)mapValue; + } + } + return null; + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/util/ThrowUtils.java b/src/main/java/com/alibaba/qlexpress4/runtime/util/ThrowUtils.java new file mode 100644 index 0000000..b44f193 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/util/ThrowUtils.java @@ -0,0 +1,29 @@ +package com.alibaba.qlexpress4.runtime.util; + +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.exception.UserDefineException; + +import java.util.Objects; + +/** + * Author: DQinYuan + */ +public class ThrowUtils { + + public static QLRuntimeException wrapThrowable(Throwable t, ErrorReporter errorReporter, String errCode, + String errMsg, String... args) { + return t instanceof QLRuntimeException ? (QLRuntimeException)t + : errorReporter.reportFormatWithCatch(t, errCode, errMsg, (Object[])args); + } + + public static QLRuntimeException reportUserDefinedException(ErrorReporter errorReporter, UserDefineException e) { + if (Objects.equals(e.getType(), UserDefineException.ExceptionType.INVALID_ARGUMENT)) { + throw errorReporter.report(QLErrorCodes.INVALID_ARGUMENT.name(), e.getMessage()); + } + else { + throw errorReporter.report(QLErrorCodes.BIZ_EXCEPTION.name(), e.getMessage()); + } + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/runtime/util/ValueUtils.java b/src/main/java/com/alibaba/qlexpress4/runtime/util/ValueUtils.java new file mode 100644 index 0000000..f3e12df --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/runtime/util/ValueUtils.java @@ -0,0 +1,28 @@ +package com.alibaba.qlexpress4.runtime.util; + +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; + +/** + * Author: DQinYuan + */ +public class ValueUtils { + + public static Value toImmutable(Value origin) { + return origin instanceof LeftValue ? new DataValue(origin) : origin; + } + + public static T assertType(Object obj, Class assertType, String errCode, String errMsg, + ErrorReporter errorReporter) { + if (obj != null && assertType.isAssignableFrom(obj.getClass())) { + return assertType.cast(obj); + } + throw errorReporter.report(errCode, errMsg); + } + + public static int javaIndex(int length, int qlIndex) { + return qlIndex < 0 ? length + qlIndex : qlIndex; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/security/QLSecurityStrategy.java b/src/main/java/com/alibaba/qlexpress4/security/QLSecurityStrategy.java new file mode 100644 index 0000000..bdb734e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/security/QLSecurityStrategy.java @@ -0,0 +1,34 @@ +package com.alibaba.qlexpress4.security; + +import java.lang.reflect.Member; +import java.util.Set; + +/** + * Author: DQinYuan + */ +public interface QLSecurityStrategy { + + static QLSecurityStrategy open() { + return StrategyOpen.getInstance(); + } + + static QLSecurityStrategy isolation() { + return StrategyIsolation.getInstance(); + } + + static QLSecurityStrategy blackList(Set blackList) { + return new StrategyBlackList(blackList); + } + + static QLSecurityStrategy whiteList(Set whiteList) { + return new StrategyWhiteList(whiteList); + } + + /** + * check if member secure + * @param member member of object + * @return true if secure + */ + boolean check(Member member); + +} diff --git a/src/main/java/com/alibaba/qlexpress4/security/StrategyBlackList.java b/src/main/java/com/alibaba/qlexpress4/security/StrategyBlackList.java new file mode 100644 index 0000000..9081b05 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/security/StrategyBlackList.java @@ -0,0 +1,23 @@ +package com.alibaba.qlexpress4.security; + +import java.lang.reflect.Member; +import java.util.Set; + +/** + * + * A security policy that prohibits access to Java members in the blacklist. + * Author: DQinYuan + */ +public class StrategyBlackList implements QLSecurityStrategy { + + private final Set blackList; + + public StrategyBlackList(Set blackList) { + this.blackList = blackList; + } + + @Override + public boolean check(Member member) { + return !blackList.contains(member); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/security/StrategyIsolation.java b/src/main/java/com/alibaba/qlexpress4/security/StrategyIsolation.java new file mode 100644 index 0000000..78ff867 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/security/StrategyIsolation.java @@ -0,0 +1,21 @@ +package com.alibaba.qlexpress4.security; + +import java.lang.reflect.Member; + +/** + * A security policy that isolates qlexpress script with jvm + * Author: DQinYuan + */ +public class StrategyIsolation implements QLSecurityStrategy { + + private static final StrategyIsolation INSTANCE = new StrategyIsolation(); + + public static StrategyIsolation getInstance() { + return INSTANCE; + } + + @Override + public boolean check(Member member) { + throw new IllegalStateException(); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/security/StrategyOpen.java b/src/main/java/com/alibaba/qlexpress4/security/StrategyOpen.java new file mode 100644 index 0000000..18086f6 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/security/StrategyOpen.java @@ -0,0 +1,21 @@ +package com.alibaba.qlexpress4.security; + +import java.lang.reflect.Member; + +/** + * A security policy that allows access to all Java classes within the application. + * Author: DQinYuan + */ +public class StrategyOpen implements QLSecurityStrategy { + + private static final StrategyOpen INSTANCE = new StrategyOpen(); + + public static StrategyOpen getInstance() { + return INSTANCE; + } + + @Override + public boolean check(Member member) { + return true; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/security/StrategyWhiteList.java b/src/main/java/com/alibaba/qlexpress4/security/StrategyWhiteList.java new file mode 100644 index 0000000..892728e --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/security/StrategyWhiteList.java @@ -0,0 +1,22 @@ +package com.alibaba.qlexpress4.security; + +import java.lang.reflect.Member; +import java.util.Set; + +/** + * A security policy that only permits access to Java members in the whitelist. + * Author: DQinYuan + */ +public class StrategyWhiteList implements QLSecurityStrategy { + + private final Set whiteList; + + public StrategyWhiteList(Set whiteList) { + this.whiteList = whiteList; + } + + @Override + public boolean check(Member member) { + return whiteList.contains(member); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/utils/BasicUtil.java b/src/main/java/com/alibaba/qlexpress4/utils/BasicUtil.java new file mode 100644 index 0000000..56bbf24 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/utils/BasicUtil.java @@ -0,0 +1,100 @@ +package com.alibaba.qlexpress4.utils; + +import com.alibaba.qlexpress4.runtime.Nothing; +import com.alibaba.qlexpress4.runtime.Parameters; + +import java.lang.reflect.*; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +import static java.lang.Character.toUpperCase; + +/** + * Author: TaoKan + */ +public class BasicUtil { + public static final String LENGTH = "length"; + + public static final String CLASS = "class"; + + private static final Map, Class> primitiveMap; + + static { + primitiveMap = new HashMap<>(8); + primitiveMap.put(Boolean.class, boolean.class); + primitiveMap.put(Character.class, char.class); + primitiveMap.put(Double.class, double.class); + primitiveMap.put(Float.class, float.class); + primitiveMap.put(Integer.class, int.class); + primitiveMap.put(Long.class, long.class); + primitiveMap.put(Byte.class, byte.class); + primitiveMap.put(Short.class, short.class); + } + + public static Class transToPrimitive(Class clazz) { + return primitiveMap.get(clazz); + } + + public static Integer numberPromoteLevel(Class numCls) { + if (numCls == byte.class || numCls == Byte.class) { + return 0; + } + if (numCls == short.class || numCls == Short.class) { + return 1; + } + if (numCls == int.class || numCls == Integer.class) { + return 2; + } + if (numCls == long.class || numCls == Long.class) { + return 3; + } + if (numCls == BigInteger.class) { + return 4; + } + if (numCls == float.class || numCls == Float.class) { + return 5; + } + if (numCls == double.class || numCls == Double.class) { + return 6; + } + if (numCls == BigDecimal.class) { + return 7; + } + return null; + } + + public static boolean isPublic(Member member) { + return Modifier.isPublic(member.getModifiers()); + } + + public static boolean isStatic(Member member) { + return Modifier.isStatic(member.getModifiers()); + } + + public static String getGetter(String s) { + return "get" + toUpperCase(s.charAt(0)) + s.substring(1); + } + + public static String getSetter(String s) { + return "set" + toUpperCase(s.charAt(0)) + s.substring(1); + } + + public static String getIsGetter(String s) { + return "is" + toUpperCase(s.charAt(0)) + s.substring(1); + } + + public static Class[] getTypeOfObject(Object[] objects) { + Class[] classes = new Class[objects.length]; + for (int i = 0; i < objects.length; i++) { + if (objects[i] == null) { + classes[i] = Nothing.class; + } + else { + classes[i] = objects[i].getClass(); + } + } + return classes; + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/utils/CacheUtil.java b/src/main/java/com/alibaba/qlexpress4/utils/CacheUtil.java new file mode 100644 index 0000000..8cc1d69 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/utils/CacheUtil.java @@ -0,0 +1,17 @@ +package com.alibaba.qlexpress4.utils; + +import com.alibaba.qlexpress4.member.MethodHandler; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Author: TaoKan + */ +public class CacheUtil { + private static final Map FUNCTION_INTERFACE_CACHE = new ConcurrentHashMap<>(); + + public static boolean isFunctionInterface(Class clazz) { + return FUNCTION_INTERFACE_CACHE.computeIfAbsent(clazz, + ignore -> clazz.isInterface() && MethodHandler.hasOnlyOneAbstractMethod(clazz.getMethods())); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/utils/PrintlnUtils.java b/src/main/java/com/alibaba/qlexpress4/utils/PrintlnUtils.java new file mode 100644 index 0000000..e6b8367 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/utils/PrintlnUtils.java @@ -0,0 +1,28 @@ +package com.alibaba.qlexpress4.utils; + +import java.util.function.Consumer; + +/** + * Author: DQinYuan + */ +public class PrintlnUtils { + + public static void printlnByCurDepth(int depth, String str, Consumer debug) { + debug.accept(buildIndentString(depth, str)); + } + + public static String buildIndentString(int indent, String originStr) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < indent; i++) { + if (i == indent - 1) { + builder.append("| "); + } + else { + builder.append(" "); + } + } + builder.append(originStr); + return builder.toString(); + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/utils/QLAliasUtils.java b/src/main/java/com/alibaba/qlexpress4/utils/QLAliasUtils.java new file mode 100644 index 0000000..7e772ce --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/utils/QLAliasUtils.java @@ -0,0 +1,18 @@ +package com.alibaba.qlexpress4.utils; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +public class QLAliasUtils { + + public static boolean matchQLAlias(String matchName, QLAlias[] qlAliases) { + for (QLAlias alias : qlAliases) { + for (int i = 0; i < alias.value().length; i++) { + if (matchName.equals(alias.value()[i])) { + return true; + } + } + } + return false; + } + +} diff --git a/src/main/java/com/alibaba/qlexpress4/utils/QLFunctionUtil.java b/src/main/java/com/alibaba/qlexpress4/utils/QLFunctionUtil.java new file mode 100644 index 0000000..ed34545 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/utils/QLFunctionUtil.java @@ -0,0 +1,19 @@ +package com.alibaba.qlexpress4.utils; + +import com.alibaba.qlexpress4.annotation.QLFunction; + +import java.lang.reflect.Method; + +/** + * Author: TaoKan + */ +public class QLFunctionUtil { + + public static String[] getQLFunctionValue(Method method) { + return method.getAnnotation(QLFunction.class).value(); + } + + public static boolean containsQLFunctionForMethod(Method method) { + return method.isAnnotationPresent(QLFunction.class); + } +} diff --git a/src/main/java/com/alibaba/qlexpress4/utils/QLStringUtils.java b/src/main/java/com/alibaba/qlexpress4/utils/QLStringUtils.java new file mode 100644 index 0000000..71e4cf4 --- /dev/null +++ b/src/main/java/com/alibaba/qlexpress4/utils/QLStringUtils.java @@ -0,0 +1,64 @@ +package com.alibaba.qlexpress4.utils; + +public class QLStringUtils { + + public static String parseStringEscape(String originStr) { + return parseStringEscapeStartEnd(originStr, 1, originStr.length() - 1); + } + + public static String parseStringEscapeStartEnd(String originStr, int start, int end) { + StringBuilder result = new StringBuilder(); + final byte init = 0; + final byte escape = 1; + byte state = 0; + + int i = start; + while (i < end) { + char cur = originStr.charAt(i++); + switch (state) { + case init: + if (cur == '\\') { + state = escape; + } + else { + result.append(cur); + } + break; + case escape: + state = init; + switch (cur) { + case 'b': + result.append('\b'); + break; + case 't': + result.append('\t'); + break; + case 'n': + result.append('\n'); + break; + case 'f': + result.append('\f'); + break; + case 'r': + result.append('\r'); + break; + case '"': + result.append('"'); + break; + case '\'': + result.append('\''); + break; + case '\\': + result.append('\\'); + break; + case '$': + result.append('$'); + break; + } + break; + } + } + return result.toString(); + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/ClearDfaCacheTest.java b/src/test/java/com/alibaba/qlexpress4/ClearDfaCacheTest.java new file mode 100644 index 0000000..bc87b2a --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/ClearDfaCacheTest.java @@ -0,0 +1,69 @@ +package com.alibaba.qlexpress4; + +import com.alibaba.qlexpress4.runtime.context.ExpressContext; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.DecimalFormat; +import java.util.Objects; + +public class ClearDfaCacheTest { + + private static final DecimalFormat MEM_FORMAT = new DecimalFormat("#,###.##"); + + @Test + public void clearDFACacheTest() + throws URISyntaxException, IOException { + String complexDataProcessingExpress = + new String(Files.readAllBytes(getPerfRoot().resolve("complexDataProcessing.ql"))); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + double beforeMemoryUsed = getMemoryUsedMB(); + runner.parseToSyntaxTree(complexDataProcessingExpress); + runner.clearDFACache(); + Assert.assertTrue(getMemoryUsedMB() <= beforeMemoryUsed + 1); + } + + @Test + public void bestPractice() + throws URISyntaxException, IOException { + String exampleExpress = "1+1"; + // tag::clearDFACacheBestPractice[] + /* + * When the expression changes, parse it and add it to the expression cache; + * after parsing is complete, call clearDFACache. + */ + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.parseToDefinitionWithCache(exampleExpress); + runner.clearDFACache(); + + /* + * All subsequent runs of this script must enable the cache option to ensure that re-compilation does not occur. + */ + for (int i = 0; i < 3; i++) { + runner.execute(exampleExpress, ExpressContext.EMPTY_CONTEXT, QLOptions.builder().cache(true).build()); + } + // end::clearDFACacheBestPractice[] + } + + private double getMemoryUsedMB() { + System.gc(); + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage heap = memoryMXBean.getHeapMemoryUsage(); + long memory = heap.getUsed(); + return memory / (1024.0 * 1024.0); + } + + private Path getPerfRoot() + throws URISyntaxException { + return Paths.get(Objects.requireNonNull(getClass().getClassLoader().getResource("perf")).toURI()); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java b/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java new file mode 100644 index 0000000..88dbd4e --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java @@ -0,0 +1,1609 @@ +package com.alibaba.qlexpress4; + +import com.alibaba.qlexpress4.annotation.QLFunction; +import com.alibaba.qlexpress4.aparser.ImportManager; +import com.alibaba.qlexpress4.aparser.InterpolationMode; +import com.alibaba.qlexpress4.api.BatchAddFunctionResult; +import com.alibaba.qlexpress4.exception.QLErrorCodes; +import com.alibaba.qlexpress4.exception.QLException; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.exception.QLSyntaxException; +import com.alibaba.qlexpress4.exception.QLTimeoutException; +import com.alibaba.qlexpress4.inport.MyDesk; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.context.DynamicVariableContext; +import com.alibaba.qlexpress4.runtime.context.ExpressContext; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.function.ExtensionFunction; +import com.alibaba.qlexpress4.runtime.trace.ExpressionTrace; +import com.alibaba.qlexpress4.runtime.trace.TracePointTree; +import com.alibaba.qlexpress4.security.QLSecurityStrategy; +import com.alibaba.qlexpress4.test.function.HelloFunction; +import com.alibaba.qlexpress4.test.qlalias.Order; +import com.alibaba.qlexpress4.test.qlalias.Patient; +import com.alibaba.qlexpress4.test.qlalias.Person; +import com.alibaba.qlexpress4.test.qlalias.User; +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Member; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +/** + * Author: DQinYuan + */ +public class Express4RunnerTest { + + @Test + public void parseToCacheTest() { + // tag::parseToCache[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.parseToDefinitionWithCache("a+b"); + // end::parseToCache[] + } + + @Test + public void addFunctionsDefinedInScriptTest() + throws InterruptedException { + // tag::addFunctionsDefinedInScript[] + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + BatchAddFunctionResult addResult = express4Runner.addFunctionsDefinedInScript( + "function myAdd(a,b) {\n" + " return a+b;" + "}\n" + "\n" + "function getCurrentTime() {\n" + + " return System.currentTimeMillis();\n" + "}" + "\n" + "defineTime=System.currentTimeMillis();\n" + + "function defineTime() {\n" + " return defineTime;" + "}\n", + ExpressContext.EMPTY_CONTEXT, + QLOptions.DEFAULT_OPTIONS); + assertEquals(3, addResult.getSucc().size()); + QLResult result = express4Runner.execute("myAdd(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertEquals(3, result.getResult()); + + QLResult resultCurTime1 = + express4Runner.execute("getCurrentTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + Thread.sleep(1000); + QLResult resultCurTime2 = + express4Runner.execute("getCurrentTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertNotSame(resultCurTime1.getResult(), resultCurTime2.getResult()); + + /* + * The defineTime variable is defined outside the function and is initialized when the function is defined; + * it is not recalculated afterward, so the value returned is always the time at which the function was defined. + */ + QLResult resultDefineTime1 = + express4Runner.execute("defineTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + Thread.sleep(1000); + QLResult resultDefineTime2 = + express4Runner.execute("defineTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertSame(resultDefineTime1.getResult(), resultDefineTime2.getResult()); + // end::addFunctionsDefinedInScript[] + } + + @Test + public void expressionTraceTest() { + // tag::expressionTrace[] + Express4Runner express4Runner = new Express4Runner(InitOptions.builder().traceExpression(true).build()); + express4Runner.addFunction("myTest", (Predicate)i -> i > 10); + + Map context = new HashMap<>(); + context.put("a", true); + QLResult result = express4Runner + .execute("a && (!myTest(11) || false)", context, QLOptions.builder().traceExpression(true).build()); + Assert.assertFalse((Boolean)result.getResult()); + + List expressionTraces = result.getExpressionTraces(); + Assert.assertEquals(1, expressionTraces.size()); + ExpressionTrace expressionTrace = expressionTraces.get(0); + Assert.assertEquals("OPERATOR && false\n" + " | VARIABLE a true\n" + " | OPERATOR || false\n" + + " | OPERATOR ! false\n" + " | FUNCTION myTest true\n" + " | VALUE 11 11\n" + + " | VALUE false false\n", expressionTrace.toPrettyString(0)); + + // short circuit + context.put("a", false); + QLResult resultShortCircuit = express4Runner.execute("(a && true) && (!myTest(11) || false)", + context, + QLOptions.builder().traceExpression(true).build()); + Assert.assertFalse((Boolean)resultShortCircuit.getResult()); + ExpressionTrace expressionTraceShortCircuit = resultShortCircuit.getExpressionTraces().get(0); + Assert.assertEquals( + "OPERATOR && false\n" + " | OPERATOR && false\n" + " | VARIABLE a false\n" + " | VALUE true \n" + + " | OPERATOR || \n" + " | OPERATOR ! \n" + " | FUNCTION myTest \n" + + " | VALUE 11 \n" + " | VALUE false \n", + expressionTraceShortCircuit.toPrettyString(0)); + Assert.assertTrue(expressionTraceShortCircuit.getChildren().get(0).isEvaluated()); + Assert.assertFalse(expressionTraceShortCircuit.getChildren().get(1).isEvaluated()); + + // in + QLResult resultIn = express4Runner + .execute("'ab' in ['cc', 'dd', 'ff']", context, QLOptions.builder().traceExpression(true).build()); + Assert.assertFalse((Boolean)resultIn.getResult()); + ExpressionTrace expressionTraceIn = resultIn.getExpressionTraces().get(0); + Assert + .assertEquals( + "OPERATOR in false\n" + " | VALUE 'ab' ab\n" + " | LIST [ [cc, dd, ff]\n" + " | VALUE 'cc' cc\n" + + " | VALUE 'dd' dd\n" + " | VALUE 'ff' ff\n", + expressionTraceIn.toPrettyString(0)); + // end::expressionTrace[] + + QLResult resultTernary = express4Runner + .execute("true? 2: 1;false? 2: 1", context, QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("OPERATOR ? 2\n" + " | VALUE true true\n" + " | VALUE 2 2\n" + " | VALUE 1 \n", + resultTernary.getExpressionTraces().get(0).toPrettyString(0)); + Assert.assertEquals("OPERATOR ? 1\n" + " | VALUE false false\n" + " | VALUE 2 \n" + " | VALUE 1 1\n", + resultTernary.getExpressionTraces().get(1).toPrettyString(0)); + + QLResult resultIf = express4Runner + .execute("if(true) {11} else {13}", context, QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("IF if 11\n" + " | VALUE true true\n" + " | BLOCK { 11\n" + " | VALUE 11 11\n" + + " | BLOCK { \n" + " | VALUE 13 \n", resultIf.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultAssign = + express4Runner.execute("aab = 11", context, QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("OPERATOR = 11\n" + " | VARIABLE aab null\n" + " | VALUE 11 11\n", + resultAssign.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultAssignChange = express4Runner.execute("aab = 111", + Collections.singletonMap("aab", 100), + QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("OPERATOR = 111\n" + " | VARIABLE aab 100\n" + " | VALUE 111 111\n", + resultAssignChange.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultAssignFunctionCall = express4Runner.execute("m = {bbb:6};aaa = () -> m;aaa().bbb=10;m.bbb", + new HashMap<>(), + QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals(10, resultAssignFunctionCall.getResult()); + Assert.assertEquals("OPERATOR = {bbb=10}\n" + " | VARIABLE m null\n" + " | MAP { {bbb=10}\n", + resultAssignFunctionCall.getExpressionTraces().get(0).toPrettyString(0)); + Assert.assertEquals("OPERATOR = 10\n" + " | FIELD bbb 6\n" + " | FUNCTION aaa \n" + " | VALUE 10 10\n", + resultAssignFunctionCall.getExpressionTraces().get(2).toPrettyString(0)); + + QLResult resultBlock = express4Runner + .execute("a = {m=10;m+11}", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals( + "OPERATOR = 21\n" + " | VARIABLE a null\n" + " | BLOCK { 21\n" + " | OPERATOR = 10\n" + + " | VARIABLE m null\n" + " | VALUE 10 10\n" + " | OPERATOR + 21\n" + + " | VARIABLE m 10\n" + " | VALUE 11 11\n", + resultBlock.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultNestedIf = express4Runner.execute("if(false) {11} else if (1>10) {15} else {}", + new HashMap<>(), + QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals( + "IF if null\n" + " | VALUE false false\n" + " | BLOCK { \n" + " | VALUE 11 \n" + " | IF if null\n" + + " | OPERATOR > false\n" + " | VALUE 1 1\n" + " | VALUE 10 10\n" + + " | BLOCK { \n" + " | VALUE 15 \n" + " | BLOCK { null\n", + resultNestedIf.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultLocalVeriableStmt = + express4Runner.execute("int a = 1;", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("STATEMENT int null\n", + resultLocalVeriableStmt.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultThrow = express4Runner + .execute("if (true) 10 else throw 1", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("IF if 10\n" + " | VALUE true true\n" + " | VALUE 10 10\n" + " | STATEMENT throw \n", + resultThrow.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultIfWithFunctionCall = express4Runner + .execute("if (myTest(11)) 10 else 1", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("IF if 10\n" + " | FUNCTION myTest true\n" + " | VALUE 11 11\n" + " | VALUE 10 10\n" + + " | VALUE 1 \n", resultIfWithFunctionCall.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultWhile = express4Runner + .execute("while(false) {m=10}", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("STATEMENT while null\n", resultWhile.getExpressionTraces().get(0).toPrettyString(0)); + + // traditional for statement test + QLResult resultTraditionalFor = express4Runner + .execute("for(int i=0; i<3; i++) {i}", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("STATEMENT for null\n", + resultTraditionalFor.getExpressionTraces().get(0).toPrettyString(0)); + + // for each statement test + QLResult resultForEach = express4Runner.execute("for(int item : [1,2,3]) {item}", + new HashMap<>(), + QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("STATEMENT for null\n", resultForEach.getExpressionTraces().get(0).toPrettyString(0)); + + // function statement test + QLResult resultFunction = express4Runner.execute("function testFunc() {return 10}", + new HashMap<>(), + QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("DEFINE_FUNCTION testFunc null\n", + resultFunction.getExpressionTraces().get(0).toPrettyString(0)); + + // macro statement test + QLResult resultMacro = express4Runner + .execute("macro testMacro {return 20}", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("DEFINE_MACRO testMacro null\n", + resultMacro.getExpressionTraces().get(0).toPrettyString(0)); + + // break statement test + QLResult resultBreak = + express4Runner.execute("break", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("STATEMENT break null\n", resultBreak.getExpressionTraces().get(0).toPrettyString(0)); + + // continue statement test + QLResult resultContinue = + express4Runner.execute("continue", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("STATEMENT continue null\n", resultContinue.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultReturn = + express4Runner.execute("return 1+1", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals("RETURN return 2\n" + " | OPERATOR + 2\n" + " | VALUE 1 1\n" + " | VALUE 1 1\n", + resultReturn.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultEmptyStmt = + express4Runner.execute(";;;;", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals(1, resultEmptyStmt.getExpressionTraces().size()); + Assert.assertEquals("STATEMENT ; \n", resultEmptyStmt.getExpressionTraces().get(0).toPrettyString(0)); + + QLResult resultEmptyStmtFilter = + express4Runner.execute("a=1;;;;", new HashMap<>(), QLOptions.builder().traceExpression(true).build()); + Assert.assertEquals(1, resultEmptyStmtFilter.getExpressionTraces().size()); + } + + @Test + public void getExpressionTracePointsTest() { + // tag::getExpressionTracePoints[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + TracePointTree tracePointTree = express4Runner.getExpressionTracePoints("1+3+5*ab+9").get(0); + Assert.assertEquals("OPERATOR +\n" + " | OPERATOR +\n" + " | OPERATOR +\n" + " | VALUE 1\n" + + " | VALUE 3\n" + " | OPERATOR *\n" + " | VALUE 5\n" + " | VARIABLE ab\n" + + " | VALUE 9\n", tracePointTree.toPrettyString(0)); + // end::getExpressionTracePoints[] + + TracePointTree tracePointTreeFunction = + express4Runner.getExpressionTracePoints("ab && (myTest(1,2) || false)").get(0); + Assert.assertEquals( + "OPERATOR &&\n" + " | VARIABLE ab\n" + " | OPERATOR ||\n" + " | FUNCTION myTest\n" + + " | VALUE 1\n" + " | VALUE 2\n" + " | VALUE false\n", + tracePointTreeFunction.toPrettyString(0)); + + TracePointTree tracePointIn = express4Runner.getExpressionTracePoints("'ab' in ['cc', 'dd', 'ff']").get(0); + Assert.assertEquals("OPERATOR in\n" + " | VALUE 'ab'\n" + " | LIST [\n" + " | VALUE 'cc'\n" + + " | VALUE 'dd'\n" + " | VALUE 'ff'\n", tracePointIn.toPrettyString(0)); + } + + @Test + public void checkSyntaxTest() { + // tag::checkSyntax[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + express4Runner.check("a+b;\n(a+b"); + fail(); + } + catch (QLSyntaxException e) { + assertEquals(2, e.getLineNo()); + assertEquals(4, e.getColNo()); + assertEquals("SYNTAX_ERROR", e.getErrorCode()); + // represents the end of script + assertEquals( + "[Error SYNTAX_ERROR: mismatched input '' expecting ')']\n" + "[Near: a+b; (a+b]\n" + + " ^^^^^\n" + "[Line: 2, Column: 4]", + e.getMessage()); + } + // end::checkSyntax[] + + try { + express4Runner.check("sellerId in [1001] || (sellerId not in [1001])"); + fail(); + } + catch (QLSyntaxException e) { + assertEquals( + "[Error SYNTAX_ERROR: mismatched input 'not' expecting ')']\n" + + "[Near: ...[1001] || (sellerId not in [1001])]\n" + " ^^^\n" + + "[Line: 1, Column: 32]", + e.getMessage()); + } + } + + @Test + public void addAliasTest() { + // tag::addAlias[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // add custom function zero + express4Runner.addFunction("zero", (String ignore) -> 0); + + // keyword alias + assertTrue(express4Runner.addAlias("如果", "if")); + assertTrue(express4Runner.addAlias("则", "then")); + assertTrue(express4Runner.addAlias("否则", "else")); + assertTrue(express4Runner.addAlias("返回", "return")); + // operator alias + assertTrue(express4Runner.addAlias("大于", ">")); + // function alias + assertTrue(express4Runner.addAlias("零", "zero")); + + Map context = new HashMap<>(); + context.put("语文", 90); + context.put("数学", 90); + context.put("英语", 90); + + Object result = express4Runner + .execute("如果 (语文 + 数学 + 英语 大于 270) 则 返回 1 否则 返回 零()", context, QLOptions.DEFAULT_OPTIONS) + .getResult(); + assertEquals(0, result); + // end::addAlias[] + } + + @Test + public void inTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addAlias("属于", "in"); + assertTrue( + (Boolean)express4Runner.execute("1 属于 [1,2]", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult()); + assertFalse( + (Boolean)express4Runner.execute("1 属于 [3,2]", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult()); + } + + @Test + public void cacheDocTest() { + // tag::cacheSwitch[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // open cache switch + express4Runner.execute("1+2", new HashMap<>(), QLOptions.builder().cache(true).build()); + // end::cacheSwitch[] + } + + @Test + public void docQuickStartTest() { + // tag::firstQl[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map context = new HashMap<>(); + context.put("a", 1); + context.put("b", 2); + context.put("c", 3); + Object result = express4Runner.execute("a + b * c", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(7, result); + // end::firstQl[] + } + + @Test + public void docAddFunctionAndOperatorTest() { + // tag::addFunctionAndOperator[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // custom function + express4Runner.addVarArgsFunction("join", + params -> Arrays.stream(params).map(Object::toString).collect(Collectors.joining(","))); + Object resultFunction = + express4Runner.execute("join(1,2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultFunction); + + // custom operator + express4Runner.addOperatorBiFunction("join", (left, right) -> left + "," + right); + Object resultOperator = + express4Runner.execute("1 join 2 join 3", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultOperator); + // end::addFunctionAndOperator[] + } + + @Test + public void docImportJavaTest() { + // tag::importJavaCls[] + Express4Runner express4Runner = new Express4Runner(InitOptions.builder() + // open security strategy, which allows access to all Java classes within the application. + .securityStrategy(QLSecurityStrategy.open()) + .build()); + // Import Java classes using the import statement. + Map params = new HashMap<>(); + params.put("a", 1); + params.put("b", 2); + Object result = + express4Runner + .execute("import com.alibaba.qlexpress4.QLImportTester;" + "QLImportTester.add(a,b)", + params, + QLOptions.DEFAULT_OPTIONS) + .getResult(); + Assert.assertEquals(3, result); + // end::importJavaCls[] + } + + @Test + public void docDefaultImportJavaTest() { + // tag::defaultImport[] + Express4Runner express4Runner = new Express4Runner(InitOptions.builder() + .addDefaultImport( + Collections.singletonList(ImportManager.importCls("com.alibaba.qlexpress4.QLImportTester"))) + .securityStrategy(QLSecurityStrategy.open()) + .build()); + Object result = + express4Runner.execute("QLImportTester.add(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult(); + Assert.assertEquals(3, result); + // end::defaultImport[] + } + + @Test + public void notStrictNewLinesTest() { + // tag::notStrictNewLinesTest[] + Express4Runner express4Runner = new Express4Runner(InitOptions.builder().strictNewLines(false).build()); + String script = "商家应收=\n 价格\n - 饭卡商家承担\n + 平台补贴"; + Map context = new HashMap<>(); + context.put("价格", 10); + context.put("饭卡商家承担", 3); + context.put("平台补贴", 5); + QLResult result = express4Runner.execute(script, context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(12, ((Number)result.getResult()).intValue()); + // end::notStrictNewLinesTest[] + } + + @Test + public void invokeDefaultMethodTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.builder() + .addDefaultImport(Collections.singletonList(ImportManager.importPack("com.alibaba.qlexpress4.inport"))) + .securityStrategy(QLSecurityStrategy.open()) + .build()); + Object result = express4Runner + .execute("a = new InterWithDefaultImplChild();a.haha()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult(); + Assert.assertEquals("haha", result); + + Object result1 = + express4Runner + .execute("a = new InterWithDefaultImplGrandPaChild();a.haha()", + Collections.emptyMap(), + QLOptions.DEFAULT_OPTIONS) + .getResult(); + Assert.assertEquals("grandPa", result1); + + Map map = new HashMap<>(); + map.put("a", "123"); + map.put("b", "456"); + map.put("c", "789"); + + Map context = new HashMap<>(); + context.put("map", map); + map.entrySet().parallelStream().map(en -> en.getKey() + ":" + en.getValue()).collect(Collectors.toList()); + Object result2 = express4Runner.execute( + "map.entrySet()" + + ".parallelStream().map(en -> en.getKey() + \":\" + en.getValue()).collect(Collectors.toList())", + context, + QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertEquals(Arrays.asList("a:123", "b:456", "c:789"), result2); + } + + @Test + public void concurrentCacheTest() + throws InterruptedException { + int threadCount = 5; + ExecutorService pool = Executors.newFixedThreadPool(threadCount); + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + String expression = "a+b*c"; + + for (int i = 0; i < threadCount; i++) { + pool.submit(() -> { + Map context = new HashMap<>(); + context.put("a", 1); + context.put("b", 2); + context.put("c", 3); + + express4Runner.execute(expression, context, QLOptions.builder().cache(true).build()).getResult(); + }); + } + + pool.shutdown(); + long start = System.currentTimeMillis(); + pool.awaitTermination(10, TimeUnit.SECONDS); + + long cost = System.currentTimeMillis() - start; + Assert.assertTrue(cost < 5000); + } + + @Test + public void dollarVariableTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = + express4Runner.execute("$a = 10; $a", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertEquals(10, result); + } + + @Test + public void docTryCatchTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = express4Runner + .execute("1 + try {\n" + " 100 + 1/0\n" + "} catch(e) {\n" + " // Throw a zero-division exception\n" + + " 11\n" + "}", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult(); + Assert.assertEquals(12, result); + } + + @Test + public void docPreciseTest() { + // tag::bigDecimalForPrecise[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = express4Runner.execute("0.1", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertTrue(result instanceof BigDecimal); + // end::bigDecimalForPrecise[] + + // tag::preciseComparisonWithJava[] + assertNotEquals(0.3, 0.1 + 0.2, 0.0); + assertTrue((Boolean)express4Runner.execute("0.3==0.1+0.2", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + // end::preciseComparisonWithJava[] + + // tag::preciseSwitch[] + Map context = new HashMap<>(); + context.put("a", 0.1); + context.put("b", 0.2); + assertFalse((Boolean)express4Runner.execute("0.3==a+b", context, QLOptions.DEFAULT_OPTIONS).getResult()); + // open precise switch + assertTrue((Boolean)express4Runner.execute("0.3==a+b", context, QLOptions.builder().precise(true).build()) + .getResult()); + // end::preciseSwitch[] + } + + @Test + public void securityStrategyTest() + throws NoSuchMethodException, SecurityException, NoSuchFieldException { + // tag::securityStrategyContextSetup[] + MyDesk desk = new MyDesk(); + desk.setBook1("Thinking in Java"); + desk.setBook2("Effective Java"); + Map context = Collections.singletonMap("desk", desk); + // end::securityStrategyContextSetup[] + + // tag::securityStrategyIsolation[] + // default isolation strategy, no field or method can be found + Express4Runner express4RunnerIsolation = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + assertErrorCode(express4RunnerIsolation, context, "desk.book1", "FIELD_NOT_FOUND"); + assertErrorCode(express4RunnerIsolation, context, "desk.getBook2()", "METHOD_NOT_FOUND"); + // end::securityStrategyIsolation[] + + // tag::securityStrategyBlackList[] + // black list security strategy + Set memberList = new HashSet<>(); + memberList.add(MyDesk.class.getMethod("getBook2")); + Express4Runner express4RunnerBlackList = new Express4Runner( + InitOptions.builder().securityStrategy(QLSecurityStrategy.blackList(memberList)).build()); + assertErrorCode(express4RunnerBlackList, context, "desk.book2", "FIELD_NOT_FOUND"); + Object resultBlack = + express4RunnerBlackList.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertEquals("Thinking in Java", resultBlack); + // end::securityStrategyBlackList[] + + // tag::securityStrategyWhiteList[] + // white list security strategy + Express4Runner express4RunnerWhiteList = new Express4Runner( + InitOptions.builder().securityStrategy(QLSecurityStrategy.whiteList(memberList)).build()); + Object resultWhite = + express4RunnerWhiteList.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertEquals("Effective Java", resultWhite); + assertErrorCode(express4RunnerWhiteList, context, "desk.getBook1()", "METHOD_NOT_FOUND"); + // end::securityStrategyWhiteList[] + + // tag::securityStrategyOpen[] + // open security strategy + Express4Runner express4RunnerOpen = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Assert.assertEquals("Thinking in Java", + express4RunnerOpen.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Effective Java", + express4RunnerOpen.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult()); + // end::securityStrategyOpen[] + } + + @Test + public void mapSetGetTest() { + String script = "a = new HashMap<>();" + "a['aaa'] = 'bbb';" + "a"; + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Object result = express4Runner.execute(script, Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertTrue(result instanceof HashMap); + assertEquals("bbb", ((HashMap)result).get("aaa")); + } + + @Test + public void shortCircuitTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + assertTrue( + (Boolean)express4Runner.execute("true && true && true", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + assertFalse( + (Boolean)express4Runner.execute("true && false && (1/0)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + assertTrue( + (Boolean)express4Runner + .execute("a = 1+1+1+1+1+1+1+1+1;" + "true && true && true", + Collections.emptyMap(), + QLOptions.DEFAULT_OPTIONS) + .getResult()); + + assertFalse((Boolean)express4Runner + .execute("false || false || false", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + assertTrue( + (Boolean)express4Runner.execute("false || true || (1/0)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + assertTrue((Boolean)express4Runner + .execute("(false && (1/0)) || true || (1/0)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + + assertErrorCode(express4Runner, "true && (1/0)", "INVALID_ARITHMETIC"); + + // disable short circuit test + QLOptions disableShortCircuitOp = QLOptions.builder().shortCircuitDisable(true).build(); + assertTrue( + (Boolean)express4Runner.execute("false || false || true", Collections.emptyMap(), disableShortCircuitOp) + .getResult()); + assertFalse( + (Boolean)express4Runner.execute("(true && false) || false", Collections.emptyMap(), disableShortCircuitOp) + .getResult()); + } + + @Test + public void disableShortCircuitTest() { + // tag::disableShortCircuit[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // execute when enable short circuit (default) + // `1/0` is short-circuited by the preceding `false`, so it won't throw an error. + assertFalse((Boolean)express4Runner.execute("false && (1/0)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + try { + // execute when disable short circuit + express4Runner.execute("false && (1/0)", + Collections.emptyMap(), + QLOptions.builder().shortCircuitDisable(true).build()); + fail(); + } + catch (QLException e) { + Assert.assertEquals("INVALID_ARITHMETIC", e.getErrorCode()); + Assert.assertEquals("Division by zero", e.getReason()); + } + // end::disableShortCircuit[] + } + + @Test + public void assignTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + assertErrorCode(express4Runner, "1 = 0", "SYNTAX_ERROR"); + } + + @Test + public void ifTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + QLOptions debugOptions = QLOptions.builder().build(); + Object result = + express4Runner.execute("if (2==3) {if (2==2) 10} else 4", Collections.emptyMap(), debugOptions).getResult(); + assertEquals(4, result); + } + + @Test + public void debugExample() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + QLOptions debugOptions = QLOptions.builder().build(); + Object result = express4Runner.execute("1+1", Collections.emptyMap(), debugOptions).getResult(); + assertEquals(2, result); + + Object result1 = + express4Runner.execute("false || true || (1/0)", Collections.emptyMap(), debugOptions).getResult(); + assertTrue((Boolean)result1); + } + + @Test + public void populateTest() { + // tag::polluteUserContext[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + QLOptions populateOption = QLOptions.builder().polluteUserContext(true).build(); + Map populatedMap = new HashMap<>(); + populatedMap.put("b", 10); + express4Runner.execute("a = 11;b = a", populatedMap, populateOption); + assertEquals(11, populatedMap.get("a")); + assertEquals(11, populatedMap.get("b")); + + // no population + Map noPopulatedMap1 = new HashMap<>(); + express4Runner.execute("a = 11", noPopulatedMap1, QLOptions.DEFAULT_OPTIONS); + assertFalse(noPopulatedMap1.containsKey("a")); + + Map noPopulatedMap2 = new HashMap<>(); + noPopulatedMap2.put("a", 10); + assertEquals(19, express4Runner.execute("a = 19;a", noPopulatedMap2, QLOptions.DEFAULT_OPTIONS).getResult()); + assertEquals(10, noPopulatedMap2.get("a")); + // end::polluteUserContext[] + } + + @SuppressWarnings("unchecked") + @Test + public void mapLiteralTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map result = (Map)express4Runner + .execute("{a:123,'b':'test'}", new HashMap<>(), QLOptions.DEFAULT_OPTIONS) + .getResult(); + assertEquals(123, result.get("a")); + assertEquals("test", result.get("b")); + } + + @Test + public void classFieldTest() { + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + assertEquals(List.class, + express4Runner.execute("List.class", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult()); + assertEquals(List.class, + express4Runner.execute("java.util.List.class", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult()); + } + + @Test + public void invalidOperatorTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + assertErrorCode(express4Runner, "a abcd bb", "SYNTAX_ERROR"); + assertErrorCode(express4Runner, "import a.b v = 1", "SYNTAX_ERROR"); + assertErrorCode(express4Runner, "a.*bbb", "SYNTAX_ERROR"); + } + + @Test + public void importNotAtBeginningTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + express4Runner + .execute("a = 10;\n" + "import a.b.c;", new HashMap<>(), QLOptions.builder().cache(false).build()); + fail(); + } + catch (QLSyntaxException e) { + assertEquals("SYNTAX_ERROR", e.getErrorCode()); + assertEquals("Import statement is not at the beginning of the file.", e.getReason()); + } + } + + @Test + public void extensionFunctionTest() { + // tag::extensionFunction[] + // tag::defineExtensionFunctionByExtensionFunction[] + ExtensionFunction helloFunction = new ExtensionFunction() { + @Override + public Class[] getParameterTypes() { + return new Class[0]; + } + + @Override + public String getName() { + return "hello"; + } + + @Override + public Class getDeclaringClass() { + return String.class; + } + + @Override + public Object invoke(Object obj, Object[] args) { + String originStr = (String)obj; + return "Hello," + originStr; + } + }; + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addExtendFunction(helloFunction); + Object result = + express4Runner.execute("'jack'.hello()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("Hello,jack", result); + // end::defineExtensionFunctionByExtensionFunction[] + + // tag::defineExtensionFunctionByQLFunctionalVarargs[] + // simpler way to define extension function + express4Runner.addExtendFunction("add", + Number.class, + params -> ((Number)params[0]).intValue() + ((Number)params[1]).intValue()); + QLResult resultAdd = express4Runner.execute("1.add(2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertEquals(3, resultAdd.getResult()); + // end::defineExtensionFunctionByQLFunctionalVarargs[] + + // end::extensionFunction[] + + express4Runner.addExtendFunction("add2", + Number.class, + params -> ((Number)params[0]).intValue() + ((Number)params[1]).intValue() + ((Number)params[2]).intValue()); + QLResult resultAdd2 = express4Runner.execute("1.add2(2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + assertEquals(6, resultAdd2.getResult()); + } + + @Test + public void scripTimeoutTest() { + // tag::scripTimeout[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + express4Runner.execute("while (true) {\n" + " 1+1\n" + "}", + Collections.emptyMap(), + QLOptions.builder().timeoutMillis(10L).build()); + fail("should timeout"); + } + catch (QLTimeoutException e) { + assertEquals(QLErrorCodes.SCRIPT_TIME_OUT.name(), e.getErrorCode()); + } + // end::scripTimeout[] + + try { + express4Runner.execute("while (2) {\n" + " 1+1\n" + "}", + Collections.emptyMap(), + QLOptions.builder().timeoutMillis(10L).build()); + fail("should exception"); + } + catch (QLTimeoutException e) { + fail(); + } + catch (QLRuntimeException e) { + assertEquals(QLErrorCodes.WHILE_CONDITION_BOOL_REQUIRED.name(), e.getErrorCode()); + } + } + + @Test + public void interpolationTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map context = new HashMap<>(); + context.put("a", 1); + QLResult result = express4Runner.execute("\"Hello,${a+1}\"", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("Hello,2", result.getResult()); + + // tag::disableInterpolation[] + Express4Runner express4RunnerDisable = new Express4Runner( + // disable string interpolation + InitOptions.builder().interpolationMode(InterpolationMode.DISABLE).build()); + Assert.assertEquals("Hello,${ a + 1 }", + express4RunnerDisable.execute("\"Hello,${ a + 1 }\"", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Hello,${lll", + express4RunnerDisable.execute("\"Hello,${lll\"", context, QLOptions.DEFAULT_OPTIONS).getResult()); + Assert.assertEquals("Hello,aaa $ lll\"\n\b", + express4RunnerDisable.execute("\"Hello,aaa $ lll\\\"\n\b\"", context, QLOptions.DEFAULT_OPTIONS) + .getResult()); + // end::disableInterpolation[] + } + + @Test + public void templateEngineTest() { + // tag::templateEngine[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Map context = new HashMap<>(); + context.put("a", 1); + context.put("b", 2); + context.put("c", "test"); + QLResult simpleTemplate = express4Runner.executeTemplate("a ${a};b ${b+2}", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("a 1;b 4", simpleTemplate.getResult()); + QLResult conditionTemplate = + express4Runner.executeTemplate("m xx ${\n" + " if (c like 't%') {\n" + " 'YYY'\n" + " }\n" + "}", + context, + QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("m xx YYY", conditionTemplate.getResult()); + QLResult multiLineTemplate = express4Runner.executeTemplate("m\n ${a}\n c", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("m\n 1\n c", multiLineTemplate.getResult()); + QLResult escapeStringTemplate = + express4Runner.executeTemplate("m \n\"haha\" d\"", context, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("m \n\"haha\" d\"", escapeStringTemplate.getResult()); + // end::templateEngine[] + } + + @Test + public void logicAndTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = + express4Runner.execute("null && true", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertFalse((Boolean)result); + } + + @Test + public void numberTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object[][] scriptAndExpects = + new Object[][] {{"12323", 12323}, {"2147483647", 2147483647}, {"9223372036854775807", 9223372036854775807L}, + {"18446744073709552000", new BigInteger("18446744073709552000")}, {"1.1", new BigDecimal("1.1")}, + {"1.25", 1.25d}, {"1.", 1.0}, {".1", new BigDecimal("0.1")}, {"0xfff", 4095}, {"0b11", 3}, {"072", 58}, + {"12e1", 120.0}, {"12.1E2", 1210.0}, {"10l", 10L}, {"10L", 10L}, {"10d", 10.0}, {"10.313D", 10.313d}, + {"10.2f", 10.2f}, {"10.2F", 10.2f}}; + + for (Object[] scriptAndExpect : scriptAndExpects) { + assertResultEquals(express4Runner, (String)scriptAndExpect[0], scriptAndExpect[1]); + } + } + + @Test + public void numberAmbiguousValueTest() { + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Object result = + express4Runner.execute("1.doubleValue()", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(1d, result); + } + + @Test + public void errorReportColNumTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + express4Runner.execute("1+1;\n2+2;\n1+cc()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + } + catch (QLRuntimeException e) { + assertEquals(2, e.getColNo()); + assertEquals(3, e.getLineNo()); + } + + try { + express4Runner.execute("1/0", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + } + catch (QLRuntimeException e) { + assertEquals(1, e.getColNo()); + assertEquals(1, e.getLineNo()); + } + + try { + express4Runner.execute("a[]", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + } + catch (QLSyntaxException e) { + assertEquals(1, e.getLineNo()); + assertEquals(2, e.getColNo()); + } + } + + // tag::annoObj[] + public static class MyFunctionUtil { + @QLFunction({"myAdd", "iAdd"}) + public int add(int a, int b) { + return a + b; + } + + @QLFunction("arr3") + public static int[] array3(int a, int b, int c) { + return new int[] {a, b, c}; + } + + @QLFunction("concat") + public String concat(String a, String b) { + return a + b; + } + } + // end::annoObj[] + + @Test + public void addFunctionByAnnotationTest() { + // tag::addFunctionByAnnotationObject[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + BatchAddFunctionResult addResult = express4Runner.addObjFunction(new MyFunctionUtil()); + assertEquals(4, addResult.getSucc().size()); + Object result = + express4Runner.execute("myAdd(1,2) + iAdd(5,6)", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(14, result); + express4Runner.addStaticFunction(MyFunctionUtil.class); + Object result1 = + express4Runner.execute("arr3(5,9,10)[2]", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(10, result1); + + Object result2 = + express4Runner.execute("concat('aa', null)", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("aanull", result2); + // end::addFunctionByAnnotationObject[] + } + + public static class OverloadService { + public String format(String value) { + return "S:" + value; + } + + public String format(Integer left, int right) { + return "I:" + left + "," + right; + } + } + + @Test + public void addFunctionOfServiceMethodBasicTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + MyFunctionUtil service = new MyFunctionUtil(); + + boolean added = + express4Runner.addFunctionOfServiceMethod("svcAdd", service, "add", new Class[] {int.class, int.class}); + assertTrue(added); + + Object result = express4Runner.execute("svcAdd(1,2)", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(3, result); + } + + @Test + public void addFunctionOfServiceMethodOverloadTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + OverloadService service = new OverloadService(); + + boolean addedFormatString = + express4Runner.addFunctionOfServiceMethod("fmtStr", service, "format", new Class[] {String.class}); + assertTrue(addedFormatString); + Object resultStr = + express4Runner.execute("fmtStr('x')", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("S:x", resultStr); + + boolean addedFormatInt = express4Runner + .addFunctionOfServiceMethod("fmtInt", service, "format", new Class[] {Integer.class, int.class}); + assertTrue(addedFormatInt); + Object resultInt = + express4Runner.execute("fmtInt(null,2)", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("I:null,2", resultInt); + } + + @Test + public void variableStartsWithWellNumber() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + HashMap context = new HashMap<>(); + context.put("#cost", 10); + Object result = express4Runner.execute("#cost + 1", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(11, result); + } + + @Test + public void customExpressKeyValue() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + Map attachments = new HashMap<>(); + + Map subAttachA = new HashMap<>(); + subAttachA.put("aa", 123); + + Map subAttachB = new HashMap<>(); + subAttachB.put("bb", 12); + + attachments.put("a", subAttachA); + attachments.put("b", subAttachB); + + QLOptions qlOptions = QLOptions.builder().attachments(attachments).build(); + Object result = express4Runner.execute("${/a/aa} + ${/b/bb}", new ExpressContext() { + @Override + public Value get(Map attachments, String variableName) { + String[] split = variableName.split("/"); + Map subMap = (Map)attachments.get(split[1]); + return new DataValue(subMap.get(split[2])); + } + }, qlOptions).getResult(); + assertEquals(135, result); + } + + @Test + public void getOutVarNamesTest() { + // tag::getOutVarNames[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Set outVarNames = + express4Runner.getOutVarNames("int a = 1, b = 10;\n" + "c = 11\n" + "e = a + b + c + d\n" + "f+e"); + Set expectSet = new HashSet<>(); + expectSet.add("d"); + expectSet.add("f"); + assertEquals(expectSet, outVarNames); + // end::getOutVarNames[] + + Set outVarNames2 = express4Runner.getOutVarNames("if (true) {a = 10} else {a}"); + Set expectSet2 = new HashSet<>(); + expectSet2.add("a"); + assertEquals(expectSet2, outVarNames2); + + Set outVarNames3 = express4Runner.getOutVarNames("while (a>2) {a++;b=100} a+b"); + Set expectSet3 = new HashSet<>(); + expectSet3.add("a"); + assertEquals(expectSet3, outVarNames3); + + express4Runner.addFunction("dd", () -> { + }); + Set outVarNames4 = express4Runner.getOutVarNames("cc(a,bc(2,m,1))\ndd(c)"); + Set expectSet4 = new HashSet<>(); + expectSet4.add("a"); + expectSet4.add("m"); + expectSet4.add("c"); + Assert.assertEquals(expectSet4, outVarNames4); + + Set outVarNames5 = express4Runner.getOutVarNames("resultSet = ''; " + "if (a == 11)" + "true"); + Set expectSet5 = new HashSet<>(); + expectSet5.add("a"); + Assert.assertEquals(expectSet5, outVarNames5); + + Set outVarNamesWithSelector = express4Runner.getOutVarNames("${0} + ${1}"); + Set expectSetWithSelector = new HashSet<>(); + expectSetWithSelector.add("0"); + expectSetWithSelector.add("1"); + Assert.assertEquals(expectSetWithSelector, outVarNamesWithSelector); + + Set outVarNamesWithDyString = express4Runner.getOutVarNames("\"Hello ${a+b}\""); + Set expectSetWithDyString = new HashSet<>(); + expectSetWithDyString.add("a"); + expectSetWithDyString.add("b"); + Assert.assertEquals(expectSetWithDyString, outVarNamesWithDyString); + + Set outVarNamesWithClsName = express4Runner.getOutVarNames("Math.abs(1)"); + Assert.assertEquals(Collections.emptySet(), outVarNamesWithClsName); + + Set outVarNamesWithQualified = express4Runner.getOutVarNames("java.lang.Math.abs(1)"); + Assert.assertEquals(Collections.emptySet(), outVarNamesWithQualified); + + Set outVarNamesWithClsMethodCall = express4Runner.getOutVarNames("Arrays.asList(a,b)"); + Set expectOutVarNamesWithClsMethodCall = new HashSet<>(); + expectOutVarNamesWithClsMethodCall.add("a"); + expectOutVarNamesWithClsMethodCall.add("b"); + Assert.assertEquals(expectOutVarNamesWithClsMethodCall, outVarNamesWithClsMethodCall); + } + + @Test + public void getOutVarAttrs() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Assert.assertEquals(Arrays.asList("a.b.c", "a.b.d", "c.m"), + flatOutVarAttrs(express4Runner.getOutVarAttrs("a.b.c+a.b.c-a.b.d*c.m"))); + Assert.assertEquals(Collections.singletonList("c.m"), + flatOutVarAttrs(express4Runner.getOutVarAttrs("a=2;test(a.b.c,c.m)"))); + Assert.assertEquals(Arrays.asList("a.b", "c.m"), + flatOutVarAttrs(express4Runner.getOutVarAttrs("a.b=2;test(c.m)"))); + Assert.assertEquals(Collections.singletonList("c"), + flatOutVarAttrs(express4Runner.getOutVarAttrs("java.lang.Math.abs(c)"))); + } + + private List flatOutVarAttrs(Set> outVarAttrs) { + return outVarAttrs.stream().map(l -> String.join(".", l)).sorted().collect(Collectors.toList()); + } + + @Test + public void getOutFunctions() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Set outFuncNames = express4Runner.getOutFunctions("time('2025-09-8')+sum(1,sub(3,2))"); + Set expectOutFuncNames = new HashSet<>(); + expectOutFuncNames.add("time"); + expectOutFuncNames.add("sum"); + expectOutFuncNames.add("sub"); + Assert.assertEquals(expectOutFuncNames, outFuncNames); + + Set outFuncNamesExcludeDefined = + express4Runner.getOutFunctions("function add(a,b) {a+b}\n add(1,2)+sub(3,1)"); + Assert.assertEquals(Collections.singleton("sub"), outFuncNamesExcludeDefined); + + Set outFuncNamesMultiScope = + express4Runner.getOutFunctions("function add(a,b) {\n function sub(a,b) { a-b }\n add(1,2)+sub(3,1) \n}\n"); + Assert.assertEquals(Collections.emptySet(), outFuncNamesMultiScope); + + Set outFuncNamesMultiScope1 = express4Runner + .getOutFunctions("function add(a,b) {\n function sub(a,b) { a-b }\n add(1,2)+sub(3,1) \n}\nsub(3,1)"); + Assert.assertEquals(Collections.singleton("sub"), outFuncNamesMultiScope1); + + Set outFuncNamesRecur = + express4Runner.getOutFunctions("function recur(a,b) {\n recur(1,2) \n}\nrecur(3,1)"); + Assert.assertEquals(Collections.emptySet(), outFuncNamesRecur); + + Set outFuncNamesReorder = express4Runner.getOutFunctions("add(1,2); function add(a,b) {\n a+b \n}"); + Assert.assertEquals(Collections.emptySet(), outFuncNamesReorder); + } + + @Test + public void addOperatorTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = express4Runner.execute("'1.2'+'2.3'", new HashMap<>(), QLOptions.builder().cache(false).build()) + .getResult(); + assertEquals("1.22.3", result); + boolean replaceResult = express4Runner.replaceDefaultOperator("+", + (left, right) -> Double.parseDouble(left.get().toString()) + Double.parseDouble(right.get().toString())); + assertTrue(replaceResult); + Object result1 = + express4Runner.execute("'1.2'+'2.3'", new HashMap<>(), QLOptions.builder().cache(false).build()) + .getResult(); + assertEquals(3.5d, result1); + express4Runner.addOperator("join", (left, right) -> left.get().toString() + right.get().toString()); + Object result2 = + express4Runner.execute("1.2 join 2", new HashMap<>(), QLOptions.builder().cache(false).build()).getResult(); + assertEquals("1.22", result2); + + express4Runner.addOperator(".*", (left, right) -> { + String fieldName = (String)right.get(); + return ((List>)left.get()).stream().map(m -> m.get(fieldName)).collect(Collectors.toList()); + }, QLPrecedences.GROUP); + Object result3 = + express4Runner.execute("[{a:1}, {a:5}].*a", new HashMap<>(), QLOptions.builder().cache(false).build()) + .getResult(); + List expect = new ArrayList<>(); + expect.add(1); + expect.add(5); + assertEquals(expect, result3); + + Object result4 = express4Runner + .execute("[{a:1}, {a:5}, {a:10}, {a:20}].*a[1:-1]", new HashMap<>(), QLOptions.DEFAULT_OPTIONS) + .getResult(); + assertEquals(Arrays.asList(5, 10), result4); + + try { + express4Runner.execute("[{a:1}, {a:5}].*'abc'", new HashMap<>(), QLOptions.builder().cache(false).build()); + } + catch (QLRuntimeException e) { + assertEquals("INVALID_ARGUMENT", e.getErrorCode()); + assertEquals("custom e test", e.getReason()); + } + } + + @Test + public void methodInvokeCauseTest() { + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + try { + express4Runner.execute("l = [];l.get(3)", new HashMap<>(), QLOptions.builder().cache(false).build()); + } + catch (QLRuntimeException e) { + assertTrue(e.getCause() instanceof IndexOutOfBoundsException); + } + } + + @Test + public void emptyListCacheTest() { + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + String content = "arr = []; arr.add(1); return arr;"; + for (int i = 0; i < 10; i++) { + Object result = + express4Runner.execute(content, new HashMap<>(), QLOptions.builder().cache(true).build()).getResult(); + assertEquals(1, ((List)result).size()); + } + } + + @Test + public void emptyMapCacheTest() { + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + for (int i = 0; i < 10; i++) { + String content = "m = {:}; m.put(k,'b'); return m;"; + Object result = express4Runner + .execute(content, Collections.singletonMap("k", "k" + i), QLOptions.builder().cache(true).build()) + .getResult(); + assertEquals(1, ((Map)result).size()); + } + } + + @Test + public void innerFunctionExceptionTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addFunction("testExp", () -> { + throw new RuntimeException("inner test"); + }); + assertNotNull(express4Runner.getFunction("testExp")); + try { + express4Runner.execute("1+testExp()+10", new HashMap<>(), QLOptions.DEFAULT_OPTIONS); + } + catch (QLException e) { + assertEquals("inner test", e.getCause().getMessage()); + assertEquals( + "[Error INVOKE_FUNCTION_INNER_ERROR: exception from inner when invoking function 'testExp', error message: inner test]\n" + + "[Near: 1+testExp()+10]\n" + " ^^^^^^^\n" + "[Line: 1, Column: 2]", + e.getMessage()); + assertEquals(2, e.getPos()); + } + } + + @Test + public void avoidNullPointerTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = express4Runner + .execute("'a '+${a}+aa('xxx')", new HashMap<>(), QLOptions.builder().avoidNullPointer(true).build()) + .getResult(); + assertEquals("a nullnull", result); + } + + @Test + public void atFunctionTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addFunction("@", (String s) -> s + "," + s); + Object result = express4Runner.execute("@('a')", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("a,a", result); + } + + @Test + public void multilineStrNotCloseTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + express4Runner.execute("a=1;'aaa \n \n cccc", new HashMap<>(), QLOptions.DEFAULT_OPTIONS); + fail("should throw"); + } + catch (QLException e) { + assertEquals("unterminated string literal", e.getReason()); + } + + try { + express4Runner.execute("\"aaa \n cccc", new HashMap<>(), QLOptions.DEFAULT_OPTIONS); + fail("should throw"); + } + catch (QLException e) { + assertEquals("unterminated string literal", e.getReason()); + } + } + + @Test + public void chineseCommaPropertyTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = + express4Runner.execute("{'销售方地址、电话':'test'}.销售方地址、电话", new HashMap<>(), QLOptions.DEFAULT_OPTIONS) + .getResult(); + assertEquals("test", result); + } + + @Test + public void stringEscapeTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // "'\na + Object result = + express4Runner.execute("\"\\\"\"+'\\'\na'", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("\"'\na", result); + } + + @Test + public void addMacroTest() { + // tag::addMacro[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addMacro("rename", "name='haha-'+name"); + Map context = Collections.singletonMap("name", "wuli"); + Object result = express4Runner.execute("rename", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("haha-wuli", result); + + // replace macro + express4Runner.addOrReplaceMacro("rename", "name='huhu-'+name"); + Object result1 = express4Runner.execute("rename", context, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("huhu-wuli", result1); + // end::addMacro[] + } + + @Test + public void dynamicVariableComplexTest() { + // tag::dynamicVar[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + Map staticContext = new HashMap<>(); + staticContext.put("语文", 88); + staticContext.put("数学", 99); + staticContext.put("英语", 95); + + QLOptions defaultOptions = QLOptions.DEFAULT_OPTIONS; + DynamicVariableContext dynamicContext = + new DynamicVariableContext(express4Runner, staticContext, defaultOptions); + dynamicContext.put("平均成绩", "(语文+数学+英语)/3.0"); + dynamicContext.put("是否优秀", "平均成绩>90"); + + // dynamic var + assertTrue((Boolean)express4Runner.execute("是否优秀", dynamicContext, defaultOptions).getResult()); + assertEquals(94, + ((Number)express4Runner.execute("平均成绩", dynamicContext, defaultOptions).getResult()).intValue()); + // static var + assertEquals(187, + ((Number)express4Runner.execute("语文+数学", dynamicContext, defaultOptions).getResult()).intValue()); + // end::dynamicVar[] + } + + public static class MyObj { + public int a; + + public String b; + } + + @Test + public void executeWithObjContextTest() { + // tag::executeWithObject[] + MyObj myObj = new MyObj(); + myObj.a = 1; + myObj.b = "test"; + + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + Object result = express4Runner.execute("a+b", myObj, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1test", result); + // end::executeWithObject[] + } + + @Test + public void qlAliasTest() { + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + String[] exps = new String[] {"患者.birth", "1987-02-23", "患者.生日()", "1987-02-23", "患者.患者姓名", "老王", "患者.姓名", "老王", + "患者.getBirth()==患者.出生年月()", "true", //方法注解 + "患者.生日()==患者.生日", "true", //get方法和字段名字一样是不冲突的 + "患者.患者姓名 + ' 今年 '+ 患者.获取年龄() +' 岁'", "老王 今年 34 岁", //任意方法的注解 + "患者.级别='低风险';return 患者.级别;", "低风险",}; + Person person = new Patient(); + person.setName("老王"); + person.setSex("男"); + person.setBirth("1987-02-23"); + for (int i = 0; i < exps.length; i += 2) { + Object result = + express4Runner.executeWithAliasObjects(exps[i], QLOptions.DEFAULT_OPTIONS, person).getResult(); + assertEquals(result.toString(), exps[i + 1]); + } + } + + @Test + public void qlAliasDocTest() { + // tag::qlAlias[] + Order order = new Order(); + order.setOrderNum("OR123455"); + order.setAmount(100); + + User user = new User(); + user.setName("jack"); + user.setVip(true); + + // Calculate the Final Order Amount + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Number result = (Number)express4Runner + .executeWithAliasObjects("用户.是vip? 订单.金额 * 0.8 : 订单.金额", QLOptions.DEFAULT_OPTIONS, order, user) + .getResult(); + assertEquals(80, result.intValue()); + // end::qlAlias[] + } + + @Test + public void customComplexFunctionDocTest() { + // tag::customComplexFunction[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addFunction("hello", new HelloFunction()); + String resultJack = (String)express4Runner.execute("hello()", + Collections.emptyMap(), + // Additional information(tenant for example) can be brought into the custom function from outside via attachments + QLOptions.builder().attachments(Collections.singletonMap("tenant", "jack")).build()).getResult(); + assertEquals("hello,jack", resultJack); + String resultLucy = + (String)express4Runner + .execute("hello()", + Collections.emptyMap(), + QLOptions.builder().attachments(Collections.singletonMap("tenant", "lucy")).build()) + .getResult(); + assertEquals("hello,lucy", resultLucy); + // end::customComplexFunction[] + } + + @Test + public void customSelectorTest() { + // tag::customSelector[] + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().selectorStart("#[").selectorEnd("]").build()); + + Map context = new HashMap<>(); + context.put("0", "World"); + + QLResult result = express4Runner.execute("'Hello ' + #[0]", context, QLOptions.DEFAULT_OPTIONS); + assertEquals("Hello World", result.getResult()); + // end::customSelector[] + } + + @Test + public void customSelectorWhenNoCloseTest() { + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().selectorStart("#[").selectorEnd("]").build()); + + assertErrorCode(express4Runner, "'Hello ' + #[0grg", QLErrorCodes.SYNTAX_ERROR.name()); + assertErrorCode(express4Runner, "'Hello ' + ${pl}", QLErrorCodes.SYNTAX_ERROR.name()); + } + + @Test + public void listGetWhenPreciseTest() { + InitOptions initOptions = InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build(); + Express4Runner runner = new Express4Runner(initOptions); + QLOptions qlOptions = QLOptions.builder().precise(true).cache(true).build(); + Map context = new HashMap<>(); + ArrayList list = new ArrayList<>(); + list.add("a"); + list.add("b"); + context.put("list", list); + QLResult result = runner.execute("list.get(list.size()-1);", context, qlOptions); + assertEquals("b", result.getResult()); + } + + private void assertResultEquals(Express4Runner express4Runner, String script, Object expect) { + assertResultPredicate(express4Runner, script, result -> Objects.equals(expect, result)); + } + + private void assertResultPredicate(Express4Runner express4Runner, String script, Predicate predicate) { + Object result = express4Runner.execute(script, Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertTrue(script, predicate.test(result)); + } + + private void assertErrorCode(Express4Runner express4Runner, String script, String errCode) { + try { + express4Runner.execute(script, Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + } + catch (QLException e) { + assertEquals(errCode, e.getErrorCode()); + } + } + + private void assertErrorCode(Express4Runner express4Runner, Map existMap, String script, + String errCode) { + try { + express4Runner.execute(script, existMap, QLOptions.DEFAULT_OPTIONS); + fail("no errCode:" + errCode); + } + catch (QLException e) { + assertEquals(errCode, e.getErrorCode()); + } + } + + /** + * Tests for function call disable feature + */ + @Test + public void testDefaultAllowFunctionCall() + throws Exception { + // Create a runner + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Use default options (disableFunctionCalls = false) + String scriptWithFunctionCall = "Math.max(1, 2)"; + + // Should pass validation + runner.check(scriptWithFunctionCall, CheckOptions.DEFAULT_OPTIONS); + } + + @Test + public void testDisableFunctionCalls() { + // tag::disableFunctionCallsExample[] + // Create a runner + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Create options with function calls disabled + CheckOptions options = CheckOptions.builder().disableFunctionCalls(true).build(); + + // Script with function call + String scriptWithFunctionCall = "Math.max(1, 2)"; + + // Use custom options to check script + try { + runner.check(scriptWithFunctionCall, options); + } + catch (QLSyntaxException e) { + // Will throw exception as function calls are disabled + } + // end::disableFunctionCallsExample[] + + // Should throw QLSyntaxException + QLSyntaxException exception = Assert.assertThrows(QLSyntaxException.class, () -> { + runner.check(scriptWithFunctionCall, options); + }); + + // Verify exception message contains relevant information + Assert.assertTrue(exception.getMessage().contains("Function calls are not allowed")); + } + + @Test + public void testDisableDifferentFunctionCallStyles() { + // Create a runner + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Create options with function calls disabled + CheckOptions options = CheckOptions.builder().disableFunctionCalls(true).build(); + + // Test basic function call styles first + String[] basicFunctionCallScripts = {"func()", // Basic function call + "obj.method()" // Method call + }; + + // All should throw exception (don't check exact message for now) + for (String script : basicFunctionCallScripts) { + Assert.assertThrows(Exception.class, () -> { + runner.check(script, options); + }); + } + } + + @Test + public void testDisableFunctionCallsAllowOtherSyntax() + throws Exception { + // Create a runner + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Create options with function calls disabled + CheckOptions options = CheckOptions.builder().disableFunctionCalls(true).build(); + + // Scripts without function calls should still be allowed + String[] validScripts = {"1 + 2", // Arithmetic expression + "x = 5", // Assignment + "x > 3 ? 'yes' : 'no'", // Ternary operator + "{a: 1, b: 2}" // Map literal + }; + + // All should pass validation + for (String script : validScripts) { + runner.check(script, options); + } + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/OperatorLimitTest.java b/src/test/java/com/alibaba/qlexpress4/OperatorLimitTest.java new file mode 100644 index 0000000..7f365bc --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/OperatorLimitTest.java @@ -0,0 +1,325 @@ +package com.alibaba.qlexpress4; + +import com.alibaba.qlexpress4.exception.QLSyntaxException; +import com.alibaba.qlexpress4.operator.OperatorCheckStrategy; +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class OperatorLimitTest { + + @Test + public void testCheckWithAllowedOperators() + throws QLSyntaxException { + // tag::operatorCheckStrategyExample[] + // Create a whitelist of allowed operators + Set allowedOps = new HashSet<>(Arrays.asList("+", "*")); + + // Configure check options with operator whitelist + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build(); + + // Create runner and check script with custom options + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.check("a + b * c", checkOptions); // This will pass as + and * are allowed + // end::operatorCheckStrategyExample[] + } + + @Test + public void testCheckWithDisallowedOperators() { + Set allowedOps = new HashSet<>(Arrays.asList("+", "*")); + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + runner.check("a = b + c", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + // Verify error code + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + + // Verify position information - should be precise now + assertEquals(2, e.getPos()); // Position of '=' in "a = b + c" + assertEquals(1, e.getLineNo()); // First line + assertEquals(2, e.getColNo()); // Column of '=' + + // Verify error lexeme + assertEquals("=", e.getErrLexeme()); + + // Verify error reason + assertTrue(e.getReason().contains("Script uses disallowed operator")); + assertTrue(e.getReason().contains("=")); + + // Verify complete message format + assertTrue("Message should contain error code", e.getMessage().contains("OPERATOR_NOT_ALLOWED")); + assertTrue("Message should contain line info", e.getMessage().contains("Line: 1")); + assertTrue("Message should contain column info", e.getMessage().contains("Column: 2")); + } + } + + @Test + public void testCheckWithForbiddenOperators() + throws QLSyntaxException { + Set forbiddenOps = new HashSet<>(Arrays.asList("=")); + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.blacklist(forbiddenOps)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.check("a + b * c - d / e", checkOptions); + } + + @Test + public void testCheckWithForbiddenOperatorUsed() { + Set forbiddenOps = new HashSet<>(Arrays.asList("=")); + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.blacklist(forbiddenOps)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + try { + runner.check("a = b + c", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + // Verify error code + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + + // Verify position information - should be precise now + assertEquals(2, e.getPos()); // Position of '=' in "a = b + c" + assertEquals(1, e.getLineNo()); // First line + assertEquals(2, e.getColNo()); // Column of '=' + + // Verify error lexeme + assertEquals("=", e.getErrLexeme()); + + // Verify error reason + assertTrue(e.getReason().contains("Script uses disallowed operator")); + assertTrue(e.getReason().contains("=")); + + // Verify complete message format + assertTrue("Message should contain error code", e.getMessage().contains("OPERATOR_NOT_ALLOWED")); + assertTrue("Message should contain line info", e.getMessage().contains("Line: 1")); + assertTrue("Message should contain column info", e.getMessage().contains("Column: 2")); + } + } + + @Test + public void testWhitelistWithPrefixOperator() { + Set allowedOps = new HashSet<>(Arrays.asList("+", "++")); + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Should allow prefix increment + runner.check("++a + b", checkOptions); + + // Should disallow prefix decrement + try { + runner.check("--a + b", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + assertEquals("--", e.getErrLexeme()); + assertEquals(1, e.getLineNo()); + assertEquals(0, e.getColNo()); // Position of '--' + } + } + + @Test + public void testWhitelistWithSuffixOperator() { + Set allowedOps = new HashSet<>(Arrays.asList("+", "++")); + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Should allow suffix increment + runner.check("a++ + b", checkOptions); + + // Should disallow suffix decrement + try { + runner.check("a-- + b", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + assertEquals("--", e.getErrLexeme()); + assertEquals(1, e.getLineNo()); + assertEquals(1, e.getColNo()); // Position of '--' in "a--" + } + } + + @Test + public void testBlacklistWithMultipleOperators() { + Set forbiddenOps = new HashSet<>(Arrays.asList("=", "*")); + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.blacklist(forbiddenOps)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Should allow addition and subtraction + runner.check("a + b - c", checkOptions); + + // Should disallow assignment + try { + runner.check("a = b", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + assertEquals("=", e.getErrLexeme()); + } + + // Should disallow multiplication + try { + runner.check("a * b", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + assertEquals("*", e.getErrLexeme()); + } + } + + @Test + public void testPreciseErrorPositionInMultiLineScript() { + Set allowedOps = new HashSet<>(Arrays.asList("+")); + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + String multiLineScript = "a + b\n" + "c = d\n" + "e + f"; + + try { + runner.check(multiLineScript, checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + assertEquals("=", e.getErrLexeme()); + assertEquals(2, e.getLineNo()); // Second line where '=' appears + assertEquals(2, e.getColNo()); // Column of '=' in second line + } + } + + @Test + public void testEmptyWhitelistValidation() + throws QLSyntaxException { + // Empty whitelist means no operators are allowed + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(new HashSet<>())).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Any operator should be disallowed + try { + runner.check("a + b", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + assertEquals("+", e.getErrLexeme()); + } + } + + @Test + public void testEmptyBlacklistValidation() + throws QLSyntaxException { + // Empty blacklist means all operators are allowed + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.blacklist(new HashSet<>())).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // All operators should be allowed + runner.check("a = b + c * d / e % f", checkOptions); + runner.check("++a--", checkOptions); + } + + @Test + public void testNullWhitelistValidation() + throws QLSyntaxException { + // Null whitelist is treated as empty set, meaning no operators are allowed + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(null)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Any operator should be disallowed + try { + runner.check("a + b", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + assertEquals("+", e.getErrLexeme()); + } + } + + @Test + public void testNullBlacklistValidation() + throws QLSyntaxException { + // Null blacklist is treated as empty set, meaning all operators are allowed + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.blacklist(null)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // All operators should be allowed + runner.check("a = b + c * d / e % f", checkOptions); + runner.check("++a--", checkOptions); + } + + @Test + public void testComplexExpressionWithWhitelist() + throws QLSyntaxException { + Set allowedOps = new HashSet<>(Arrays.asList("+", "*")); + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + // Should allow complex arithmetic with allowed operators + runner.check("(a + b) * (c + d)", checkOptions); + + // Should disallow division operator (not in whitelist) + try { + runner.check("(a + b) * (c + d) / e", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + assertEquals("/", e.getErrLexeme()); + } + + // Should disallow assignment in complex expression + try { + runner.check("(a + b) * (c = d)", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + assertEquals("OPERATOR_NOT_ALLOWED", e.getErrorCode()); + assertEquals("=", e.getErrLexeme()); + // Position should be accurate within the complex expression + assertTrue(e.getPos() > 10); // Should be after "(a + b) * (c " + } + } + + @Test + public void testErrorMessageContainsOperatorSet() { + Set allowedOps = new HashSet<>(Arrays.asList("+")); + CheckOptions checkOptions = + CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build(); + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + + try { + runner.check("a * b", checkOptions); + fail("Should throw QLSyntaxException"); + } + catch (QLSyntaxException e) { + String reason = e.getReason(); + assertTrue(reason.contains("Script uses disallowed operator")); + assertTrue(reason.contains("*")); + // Verify that the error message contains information about the operator + assertTrue(reason.contains("+") || reason.contains("allowed")); + } + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/QLImportTester.java b/src/test/java/com/alibaba/qlexpress4/QLImportTester.java new file mode 100644 index 0000000..73e3290 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/QLImportTester.java @@ -0,0 +1,9 @@ +package com.alibaba.qlexpress4; + +public class QLImportTester { + + public static int add(int a, int b) { + return a + b; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/TestSuiteRunner.java b/src/test/java/com/alibaba/qlexpress4/TestSuiteRunner.java new file mode 100644 index 0000000..cba99c3 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/TestSuiteRunner.java @@ -0,0 +1,295 @@ +package com.alibaba.qlexpress4; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +import com.alibaba.fastjson2.JSONException; +import com.alibaba.qlexpress4.aparser.ImportManager; +import com.alibaba.qlexpress4.exception.QLException; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.exception.UserDefineException; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +import com.alibaba.qlexpress4.security.QLSecurityStrategy; + +import org.junit.Assert; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Author: DQinYuan + */ +public class TestSuiteRunner { + private static final String ASSERT_FUNCTION_NAME = "assert"; + + private static final String ASSERT_FALSE_FUNCTION_NAME = "assertFalse"; + + private static final String ASSERT_ERROR_CODE_FUNCTION_NAME = "assertErrorCode"; + + private static final String PRINT_FUNCTION_NAME = "println"; + + private static final String TEST_PATH_ATT = "TEST_PATH"; + + private static final Express4Runner CONFIG_RUNNER = new Express4Runner(InitOptions.builder() + .securityStrategy(QLSecurityStrategy.open()) + .addDefaultImport(Arrays.asList(ImportManager.importCls("com.alibaba.qlexpress4.QLOptions"), + ImportManager.importCls("com.alibaba.qlexpress4.InitOptions"))) + .build()); + + private Express4Runner prepareRunner(InitOptions initOptions) { + Express4Runner testRunner = new Express4Runner(initOptions); + testRunner.addFunction(ASSERT_FUNCTION_NAME, new AssertFunction()); + testRunner.addFunction(ASSERT_FALSE_FUNCTION_NAME, new AssertFalseFunction()); + testRunner.addFunction(ASSERT_ERROR_CODE_FUNCTION_NAME, new AssertErrorCodeFunction()); + testRunner.addFunction(PRINT_FUNCTION_NAME, new PrintFunction()); + return testRunner; + } + + @Test + public void suiteTest() + throws URISyntaxException, IOException { + Path testSuiteRoot = getTestSuiteRoot(); + handleDirectory(testSuiteRoot, ""); + } + + @Test + public void featureDebug() + throws URISyntaxException, IOException { + Path filePath = getTestSuiteRoot().resolve("independent/function/nested_function_calls.ql"); + handleFile(filePath, filePath.toString(), true); + } + + private void handleDirectory(Path dir, String pathPrefix) + throws IOException { + Files.list(dir).forEach(path -> { + try { + String newPrefix = pathPrefix + "/" + path.getFileName(); + if (Files.isDirectory(path)) { + handleDirectory(path, newPrefix); + } + else { + handleFile(path, newPrefix, false); + } + } + catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public void handleFile(Path qlFile, String path, boolean debug) + throws IOException { + printRunning(path); + Map attachments = new HashMap<>(); + attachments.put(TEST_PATH_ATT, path); + + String qlScript = new String(Files.readAllBytes(qlFile)); + // parse testsuite option first + Optional> scriptOptionOp = parseOption(qlScript); + Optional errCodeOp = scriptOptionOp.map(scriptOption -> (String)scriptOption.get("errCode")); + Optional initOptionsBuilder = + scriptOptionOp.map(scriptOption -> (InitOptions.Builder)scriptOption.get("initOptions")); + InitOptions initOptions = initOptionsBuilder.isPresent() + ? initOptionsBuilder.get().securityStrategy(QLSecurityStrategy.open()).debug(debug).build() + : InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).debug(debug).build(); + Express4Runner express4Runner = prepareRunner(initOptions); + Optional optionsBuilder = + scriptOptionOp.map(scriptOption -> (QLOptions.Builder)scriptOption.get("qlOptions")); + QLOptions qlOptions = optionsBuilder.isPresent() ? optionsBuilder.get().attachments(attachments).build() + : QLOptions.builder().attachments(attachments).build(); + if (errCodeOp.isPresent()) { + long start = System.currentTimeMillis(); + assertErrCode(express4Runner, path, qlScript, qlOptions, errCodeOp.get(), debug); + printOk(path, System.currentTimeMillis() - start); + return; + } + + try { + long start = System.currentTimeMillis(); + express4Runner.execute(qlScript, Collections.emptyMap(), qlOptions); + printOk(path, System.currentTimeMillis() - start); + } + catch (Exception e) { + System.out.printf("%1$-95s %2$s\n", path, "error"); + throw e; + } + } + + private void printRunning(String path) { + System.out.printf("%1$-98s %2$s\n", path, "running"); + } + + private void printOk(String path, long consumeTime) { + System.out.printf("%1$-98s %2$s consume %3$dms\n", path, "ok", consumeTime); + } + + private void assertErrCode(Express4Runner runner, String path, String qlScript, QLOptions qlOptions, + String expectErrCode, boolean printE) { + try { + runner.execute(qlScript, Collections.emptyMap(), qlOptions); + Assert.fail("expect error codes:" + expectErrCode + ", but end normally"); + } + catch (QLException qlException) { + if (printE) { + qlException.printStackTrace(); + } + assertEquals(path + " error code assert fail", expectErrCode, qlException.getErrorCode()); + } + catch (Exception e) { + throw new RuntimeException(path + " unknown error", e); + } + } + + private Optional> parseOption(String qlScript) { + if (!qlScript.startsWith("/*")) { + return Optional.empty(); + } + int endIndex = qlScript.indexOf("*/"); + if (endIndex == -1) { + return Optional.empty(); + } + String configJson = qlScript.substring(2, endIndex); + try { + Map scriptOptions = (Map)CONFIG_RUNNER + .execute(configJson, Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS) + .getResult(); + return Optional.of(scriptOptions); + } + catch (JSONException e) { + return Optional.empty(); + } + } + + @Test + public void assertTest() { + Map attachment = new HashMap<>(); + attachment.put(TEST_PATH_ATT, "a/b.ql"); + + QLOptions attachOptions = QLOptions.builder().attachments(attachment).build(); + Express4Runner express4Runner = prepareRunner(InitOptions.DEFAULT_OPTIONS); + express4Runner.execute("assert(true)", Collections.emptyMap(), attachOptions); + assertErrCodeAndReason(express4Runner, "assert(false)", attachOptions, "BIZ_EXCEPTION", "a/b.ql: assert fail"); + assertErrCodeAndReason(express4Runner, + "assert(false, 'my test')", + attachOptions, + "BIZ_EXCEPTION", + "a/b.ql: my test"); + // variable can be the same name with function + express4Runner.execute("assert = 4;assert(assert == 4)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); + } + + private Path getTestSuiteRoot() + throws URISyntaxException { + return Paths.get(getClass().getClassLoader().getResource("testsuite").toURI()); + } + + private void assertErrCodeAndReason(Express4Runner express4Runner, String script, QLOptions qlOptions, + String errCode, String reason) { + try { + express4Runner.execute(script, Collections.emptyMap(), qlOptions); + } + catch (QLException e) { + assertEquals(errCode, e.getErrorCode()); + assertEquals(reason, e.getReason()); + } + } + + private static class PrintFunction implements CustomFunction { + @Override + public Object call(QContext qContext, Parameters parameters) + throws Exception { + for (int i = 0; i < parameters.size(); i++) { + System.out.println(parameters.get(i).get()); + } + return null; + } + } + + private static class AssertFunction implements CustomFunction { + @Override + public Object call(QContext qContext, Parameters parameters) + throws Exception { + int pSize = parameters.size(); + switch (pSize) { + case 1: + Boolean b = (Boolean)parameters.getValue(0); + if (b == null || !b) { + throw new UserDefineException(wrap(qContext.attachment(), "assert fail")); + } + return null; + case 2: + Boolean b0 = (Boolean)parameters.getValue(0); + if (b0 == null || !b0) { + throw new UserDefineException(wrap(qContext.attachment(), (String)parameters.getValue(1))); + } + return null; + default: + throw new UserDefineException("invalid parameter size"); + } + } + + private String wrap(Map attachments, String originErrInfo) { + return attachments.get(TEST_PATH_ATT) + ": " + originErrInfo; + } + } + + private static class AssertFalseFunction implements CustomFunction { + @Override + public Object call(QContext qContext, Parameters parameters) + throws Exception { + int pSize = parameters.size(); + switch (pSize) { + case 1: + Boolean b = (Boolean)parameters.getValue(0); + if (b == null || b) { + throw new UserDefineException(wrap(qContext.attachment(), "assert fail")); + } + return null; + case 2: + Boolean b0 = (Boolean)parameters.getValue(0); + if (b0 == null || b0) { + throw new UserDefineException(wrap(qContext.attachment(), (String)parameters.getValue(1))); + } + return null; + default: + throw new UserDefineException("invalid parameter size"); + } + } + + private String wrap(Map attachments, String originErrInfo) { + return attachments.get(TEST_PATH_ATT) + ": " + originErrInfo; + } + } + + private static class AssertErrorCodeFunction implements CustomFunction { + @Override + public Object call(QContext qContext, Parameters parameters) + throws Throwable { + int pSize = parameters.size(); + if (pSize != 2) { + throw new UserDefineException(String.format("invalid pSize:%s, expected 2 parameters", pSize)); + } + + Value value = parameters.get(0); + Value value1 = parameters.get(1); + QLambda qLambda = (QLambda)value.get(); + try { + qLambda.run(); + throw new UserDefineException(String.format("expectedErrorCode:%s, but no error", value1.get())); + } + catch (QLRuntimeException e) { + if (!Objects.equals(value1.get(), e.getErrorCode())) { + throw new UserDefineException( + String.format("expectedErrorCode:%s, actualErrorCode:%s", value1.get(), e.getErrorCode())); + } + return null; + } + } + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/aparser/CompileTimeFunctionTest.java b/src/test/java/com/alibaba/qlexpress4/aparser/CompileTimeFunctionTest.java new file mode 100644 index 0000000..bd7c6b1 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/aparser/CompileTimeFunctionTest.java @@ -0,0 +1,99 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.Express4Runner; +import com.alibaba.qlexpress4.InitOptions; +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.aparser.compiletimefunction.CodeGenerator; +import com.alibaba.qlexpress4.aparser.compiletimefunction.CompileTimeFunction; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.context.MapExpressContext; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.instruction.*; +import com.alibaba.qlexpress4.security.QLSecurityStrategy; +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; + +/** + * Author: DQinYuan + */ +public class CompileTimeFunctionTest { + + public static class ForEachFunction implements CompileTimeFunction { + + private static final String SCOPE_NAME = "FOR_EACH_FUNCTION"; + + // load iterator + // load lambda 第二个参数编译而来 + // load lambda 自定义, 循环 iterator, 调用 lambda 返回结果 list + // call + @Override + public void createFunctionInstruction(String functionName, List arguments, + OperatorFactory operatorFactory, CodeGenerator codeGenerator) { + if (arguments.size() != 2) { + throw codeGenerator.reportParseErr("INVALID_ARGUMENTS", + "FOREACH must hava 2 params, but accept " + arguments.size()); + } + + ErrorReporter functionErrReporter = codeGenerator.getErrorReporter(); + codeGenerator.addInstruction(new NewScopeInstruction(functionErrReporter, SCOPE_NAME)); + + // load iterator + QLParser.ExpressionContext arg0 = arguments.get(0); + codeGenerator.addInstructionsByTree(arg0); + codeGenerator.addInstruction( + new MethodInvokeInstruction(codeGenerator.newReporterWithToken(arg0.getStart()), "iterator", 0, false)); + + // load lambda + QLParser.ExpressionContext arg1 = arguments.get(1); + QLambdaDefinition bodyDefinition = codeGenerator.generateLambdaDefinition(arg1, + Collections.singletonList(new QLambdaDefinitionInner.Param("_", Object.class))); + codeGenerator.addInstruction(new LoadLambdaInstruction(functionErrReporter, bodyDefinition)); + + // custom lambda + codeGenerator.addInstruction(new CallConstInstruction(functionErrReporter, params -> { + Iterator iterator = (Iterator)params[0]; + QLambda body = (QLambda)params[1]; + List result = new ArrayList<>(); + while (iterator.hasNext()) { + Object next = iterator.next(); + result.add(body.call(next).getResult().get()); + } + return new QResult(new DataValue(result), QResult.ResultType.RETURN); + }, 2, "FOR_EACH")); + } + } + + @Test + public void forEachCompileFunctionTest() { + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + express4Runner.addCompileTimeFunction("FOR_EACH", new ForEachFunction()); + assertNotNull(express4Runner.getCompileTimeFunction("FOR_EACH")); + Object result = + express4Runner.execute("FOR_EACH([1,2,3,4], _+1)", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(Arrays.asList(2, 3, 4, 5), result); + } + + public static class GenInstructionNumFunction implements CompileTimeFunction { + @Override + public void createFunctionInstruction(String functionName, List arguments, + OperatorFactory operatorFactory, CodeGenerator codeGenerator) { + QLambdaDefinition lambdaDefinition = + codeGenerator.generateLambdaDefinition(arguments.get(0), Collections.emptyList()); + assertEquals(2, ((QLambdaDefinitionInner)lambdaDefinition).getInstructions().length); + } + } + + @Test + public void genInstructionNumTest() { + Express4Runner express4Runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + express4Runner.addCompileTimeFunction("GEN_INST_NUM", new GenInstructionNumFunction()); + express4Runner + .parseToLambda("GEN_INST_NUM(1)", new MapExpressContext(new HashMap<>()), QLOptions.DEFAULT_OPTIONS); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/aparser/ImportManagerTest.java b/src/test/java/com/alibaba/qlexpress4/aparser/ImportManagerTest.java new file mode 100644 index 0000000..4ffff61 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/aparser/ImportManagerTest.java @@ -0,0 +1,74 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.DefaultClassSupplier; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.function.Function; + +import static org.junit.Assert.*; + +/** + * Author: DQinYuan + */ +public class ImportManagerTest { + + public static class TestImportInner { + public static class TestImportInner2 { + + } + } + + @Test + public void loadTest() { + ImportManager importManager = + new ImportManager(DefaultClassSupplier.getInstance(), new ArrayList<>(), new HashMap<>()); + ImportManager.LoadPartQualifiedResult result0 = importManager.loadPartQualified(Arrays.asList("Function")); + assertNull(result0.getCls()); + importManager.addImport(ImportManager.importPack("java.util.function")); + + ImportManager.LoadPartQualifiedResult result1 = importManager.loadPartQualified(Arrays.asList("Function")); + assertEquals(Function.class, result1.getCls()); + assertEquals(1, result1.getRestIndex()); + + ImportManager.LoadPartQualifiedResult result2 = + importManager.loadPartQualified(Arrays.asList("java", "util", "function", "Function", "a", "b")); + assertEquals(Function.class, result2.getCls()); + assertEquals(4, result2.getRestIndex()); + + ImportManager.LoadPartQualifiedResult result3 = importManager.loadPartQualified(Arrays.asList("com", + "alibaba", + "qlexpress4", + "aparser", + "ImportManagerTest", + "TestImportInner", + "TestImportInner2")); + assertEquals(TestImportInner.TestImportInner2.class, result3.getCls()); + + ImportManager.LoadPartQualifiedResult result4 = + importManager.loadPartQualified(Arrays.asList("Function", "value")); + assertEquals(Function.class, result4.getCls()); + assertEquals(1, result4.getRestIndex()); + + ImportManager.LoadPartQualifiedResult result5 = + importManager.loadPartQualified(Arrays.asList("Function", "TT", "v")); + assertEquals(1, result5.getRestIndex()); + } + + @Test + public void loadInnerTest() { + ImportManager importManager = + new ImportManager(DefaultClassSupplier.getInstance(), new ArrayList<>(), new HashMap<>()); + importManager.addImport(ImportManager.importInnerCls("com.alibaba.qlexpress4.aparser.ImportManagerTest")); + ImportManager.LoadPartQualifiedResult result = + importManager.loadPartQualified(Arrays.asList("TestImportInner", "TestImportInner2")); + assertEquals(TestImportInner.TestImportInner2.class, result.getCls()); + + ImportManager.LoadPartQualifiedResult result2 = + importManager.loadPartQualified(Arrays.asList("TestImportInner", "testImportInner2")); + assertEquals(TestImportInner.class, result2.getCls()); + assertEquals(1, result2.getRestIndex()); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/aparser/MockOpM.java b/src/test/java/com/alibaba/qlexpress4/aparser/MockOpM.java new file mode 100644 index 0000000..4e88461 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/aparser/MockOpM.java @@ -0,0 +1,96 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.QLPrecedences; + +public class MockOpM implements ParserOperatorManager { + @Override + public boolean isOpType(String lexeme, OpType opType) { + switch (opType) { + case MIDDLE: + switch (lexeme) { + case "+": + case "-": + case "/": + case ".*": + case ">>": + case ">>>": + case "==": + case "=": + case "instanceof": + case "&": + case "|": + case "%": + case ">": + case "<": + case "*": + case "?.": + case "<<": + case "&&": + case "||": + return true; + } + return false; + case PREFIX: + switch (lexeme) { + case "++": + case "-": + case "+": + case "~": + return true; + + } + return false; + case SUFFIX: + switch (lexeme) { + case "++": + return true; + } + return false; + default: + return false; + } + } + + @Override + public Integer precedence(String lexeme) { + switch (lexeme) { + case "+": + case "-": + return QLPrecedences.ADD; + case "/": + case "%": + case "*": + return QLPrecedences.MULTI; + case ".*": + case "?.": + return QLPrecedences.GROUP; + case "==": + case "instanceof": + case ">": + case "<": + return QLPrecedences.COMPARE; + case ">>": + case ">>>": + case "<<": + return QLPrecedences.BIT_MOVE; + case "=": + return QLPrecedences.ASSIGN; + case "||": + return QLPrecedences.OR; + case "&&": + return QLPrecedences.AND; + case "&": + return QLPrecedences.BIT_AND; + case "|": + return QLPrecedences.BIT_OR; + default: + throw new IllegalStateException("unknown op"); + } + } + + @Override + public Integer getAlias(String lexeme) { + return null; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactoryTest.java b/src/test/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactoryTest.java new file mode 100644 index 0000000..33ef669 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactoryTest.java @@ -0,0 +1,311 @@ +package com.alibaba.qlexpress4.aparser; + +import com.alibaba.qlexpress4.runtime.MetaClass; +import com.alibaba.qlexpress4.runtime.QLambdaDefinitionInner; +import com.alibaba.qlexpress4.runtime.instruction.*; +import com.alibaba.qlexpress4.runtime.operator.arithmetic.MultiplyOperator; +import com.alibaba.qlexpress4.runtime.operator.assign.AssignOperator; +import org.junit.Test; + +import java.util.List; +import java.util.function.Function; + +import static org.junit.Assert.*; + +/** + * Author: DQinYuan + */ +public class SyntaxTreeFactoryTest { + + @Test + public void visitPathExprTestWhenMix() { + String script = "java.util.function.Function.a.b.cc()"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + assertEquals(6, instructions.size()); + ConstInstruction constInstruction = (ConstInstruction)instructions.get(0); + MetaClass metaClass = (MetaClass)constInstruction.getConstObj(); + assertEquals(Function.class, metaClass.getClz()); + GetFieldInstruction getFieldInstruction1 = (GetFieldInstruction)instructions.get(1); + assertEquals("a", getFieldInstruction1.getFieldName()); + GetFieldInstruction getFieldInstruction2 = (GetFieldInstruction)instructions.get(2); + assertEquals("b", getFieldInstruction2.getFieldName()); + MethodInvokeInstruction methodInvokeInstruction = (MethodInvokeInstruction)instructions.get(3); + assertEquals("cc", methodInvokeInstruction.getMethodName()); + } + + @Test + public void visitPathExprTestWhenInnerCls() { + String script = "com.alibaba.qlexpress4.aparser.ImportManagerTest.TestImportInner.TestImportInner2.pp[m]"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + assertEquals(5, instructions.size()); + ConstInstruction constInstruction = (ConstInstruction)instructions.get(0); + MetaClass metaClass = (MetaClass)constInstruction.getConstObj(); + assertEquals(ImportManagerTest.TestImportInner.TestImportInner2.class, metaClass.getClz()); + GetFieldInstruction getFieldInstruction = (GetFieldInstruction)instructions.get(1); + assertEquals("pp", getFieldInstruction.getFieldName()); + LoadInstruction loadInstruction = (LoadInstruction)instructions.get(2); + assertEquals("m", loadInstruction.getName()); + assertTrue(instructions.get(3) instanceof IndexInstruction); + } + + @Test + public void visitCallTest() { + String script = "call(mm)"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + assertEquals(4, instructions.size()); + LoadInstruction loadInstruction = (LoadInstruction)instructions.get(0); + assertEquals("mm", loadInstruction.getName()); + } + + @Test + public void numberTest() { + String script = "10_0_0l"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + assertEquals(2, instructions.size()); + ConstInstruction constInstruction = (ConstInstruction)instructions.get(0); + assertEquals(1000L, constInstruction.getConstObj()); + } + + @Test + public void macroDefineTest() { + String script = "macro add {a+b} add;int c = 10;"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + assertEquals(7, instructions.size()); + } + + @Test + public void stringEscapeTest() { + // invalid escape \p will be ignored + String script = "\"\\r\\n\\p\""; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + ConstInstruction qlInstruction = (ConstInstruction)instructions.get(0); + String constObj = (String)qlInstruction.getConstObj(); + assertEquals("\r\n", constObj); + } + + @Test + public void castTest() { + String script = "1+(int)3L"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + assertTrue(instructions.get(3) instanceof CastInstruction); + } + + @Test + public void cusOpTest() { + String script = "c.*d"; + getScriptInstructions(script, InterpolationMode.VARIABLE); + + String script1 = "c>>>d"; + getScriptInstructions(script1, InterpolationMode.VARIABLE); + } + + @Test + public void pathPartTest() { + String script = "assert((java.lang.Object) a == 1)"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + ConstInstruction constInstruction = (ConstInstruction)instructions.get(0); + assertEquals(Object.class, constInstruction.getConstObj()); + LoadInstruction loadInstruction = (LoadInstruction)instructions.get(1); + assertEquals("a", loadInstruction.getName()); + assertTrue(instructions.get(2) instanceof CastInstruction); + } + + @Test + public void fieldExpressionTest() { + String script = "\"null\".equals(b)"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + assertTrue(instructions.get(0) instanceof ConstInstruction); + } + + @Test + public void lambdaExprBodyTest() { + String script = "f = e -> try {\n" + " throw e;\n" + "} catch (java.lang.NullPointerException n) {\n" + + " 100\n" + "} catch (java.lang.Exception e) {\n" + " 10\n" + "};"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + LoadLambdaInstruction loadLambdaInstruction = (LoadLambdaInstruction)instructions.get(1); + QLambdaDefinitionInner lambdaDefinition = (QLambdaDefinitionInner)loadLambdaInstruction.getLambdaDefinition(); + assertEquals(2, lambdaDefinition.getInstructions().length); + } + + @Test + public void lambdaBlockBodyTest() { + String script = "f = e -> {10};"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + LoadLambdaInstruction loadLambdaInstruction = (LoadLambdaInstruction)instructions.get(1); + QLambdaDefinitionInner lambdaDefinition = (QLambdaDefinitionInner)loadLambdaInstruction.getLambdaDefinition(); + assertEquals(2, lambdaDefinition.getInstructions().length); + assertTrue(lambdaDefinition.getInstructions()[0] instanceof ConstInstruction); + } + + @Test + public void lambdaMapBodyTest() { + String script = "f = e -> {'test': 1234};"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + LoadLambdaInstruction loadLambdaInstruction = (LoadLambdaInstruction)instructions.get(1); + QLambdaDefinitionInner lambdaDefinition = (QLambdaDefinitionInner)loadLambdaInstruction.getLambdaDefinition(); + assertEquals(3, lambdaDefinition.getInstructions().length); + } + + @Test + public void newArrayTest() { + String script = "new int[][] {new int[] {1,2}, new int[] {3,4}}"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + NewArrayInstruction newArrayInstruction = (NewArrayInstruction)instructions.get(instructions.size() - 2); + assertEquals(Integer[].class, newArrayInstruction.getClz()); + } + + @Test + public void instanceOfTest() { + String script = "1 instanceof int"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + ConstInstruction constInstruction = (ConstInstruction)instructions.get(1); + MetaClass constMeta = (MetaClass)constInstruction.getConstObj(); + assertEquals(Integer.class, constMeta.getClz()); + } + + @Test + public void instanceOfStrArrTest() { + String script = "1 instanceof java.lang.String[][][]"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + ConstInstruction constInstruction = (ConstInstruction)instructions.get(1); + MetaClass constMeta = (MetaClass)constInstruction.getConstObj(); + assertEquals(String[][][].class, constMeta.getClz()); + } + + @Test + public void instanceOfIntArrTest() { + String script = "1 instanceof int[][][]"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + ConstInstruction constInstruction = (ConstInstruction)instructions.get(1); + MetaClass constMeta = (MetaClass)constInstruction.getConstObj(); + assertEquals(Integer[][][].class, constMeta.getClz()); + } + + @Test + public void bitOperatorTest() { + assertOperator("true & true", "&"); + assertOperator("true | true", "|"); + assertOperator("2 % 3", "%"); + } + + private void assertOperator(String script, String expectOp) { + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + OperatorInstruction operatorInstruction = (OperatorInstruction)instructions.get(2); + assertEquals(expectOp, operatorInstruction.getOperator().getOperator()); + } + + @Test + public void opPrecedencesTest() { + String script = "a = 1+2*3+10"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + OperatorInstruction operatorMulti = (OperatorInstruction)instructions.get(4); + assertTrue(operatorMulti.getOperator() instanceof MultiplyOperator); + OperatorInstruction operatorAssign = (OperatorInstruction)instructions.get(9); + assertTrue(operatorAssign.getOperator() instanceof AssignOperator); + } + + @Test + public void ternaryTest() { + String script = "l = (x) -> x > 10 ? 11 : 100"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + LoadInstruction loadInstruction = (LoadInstruction)instructions.get(0); + assertEquals("l", loadInstruction.getName()); + assertTrue(instructions.get(1) instanceof LoadLambdaInstruction); + OperatorInstruction operatorInstruction = (OperatorInstruction)instructions.get(2); + assertTrue(operatorInstruction.getOperator() instanceof AssignOperator); + } + + @Test + public void functionInterfaceTest() { + String script = "java.lang.Runnable r = () -> a = 8;"; + getScriptInstructions(script, InterpolationMode.VARIABLE); + } + + @Test + public void groupPriorityTest() { + String script = "a.*b.*c[2]+d.*e[1:2]"; + getScriptInstructions(script, InterpolationMode.VARIABLE); + } + + @Test + public void numberAmbiguousValueTest() { + String script = "1.doubleValue()"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + assertEquals("doubleValue", ((MethodInvokeInstruction)instructions.get(1)).getMethodName()); + } + + @Test + public void classifiedJsonTest() { + String script = "{'@class':'java.lang.Object', 'a': 'cccc'}"; + List instructions = getScriptInstructions(script, InterpolationMode.VARIABLE); + assertTrue(instructions.get(1) instanceof NewFilledInstanceInstruction); + } + + @Test + public void selectorTest() { + List instructions = + getScriptInstructions("${ TextField-AXXE } + ${v231}", InterpolationMode.SCRIPT); + LoadInstruction loadInstruction0 = (LoadInstruction)instructions.get(0); + assertEquals("TextField-AXXE", loadInstruction0.getName()); + LoadInstruction loadInstruction1 = (LoadInstruction)instructions.get(1); + assertEquals("v231", loadInstruction1.getName()); + + List instructions1 = + getScriptInstructions("${ TextField-A} + ${v2}", InterpolationMode.VARIABLE); + LoadInstruction loadInstruction10 = (LoadInstruction)instructions1.get(0); + assertEquals("TextField-A", loadInstruction10.getName()); + LoadInstruction loadInstruction11 = (LoadInstruction)instructions1.get(1); + assertEquals("v2", loadInstruction11.getName()); + } + + @Test + public void doubleQuoteStringScriptTest() { + List instructs = getScriptInstructions("\"a ${v-1}\"", InterpolationMode.SCRIPT); + ConstInstruction i0 = (ConstInstruction)instructs.get(0); + assertEquals("a ", i0.getConstObj()); + LoadInstruction i1 = (LoadInstruction)instructs.get(1); + assertEquals("v", i1.getName()); + ConstInstruction i2 = (ConstInstruction)instructs.get(2); + assertEquals(1, i2.getConstObj()); + OperatorInstruction i3 = (OperatorInstruction)instructs.get(3); + assertEquals("-", i3.getOperator().getOperator()); + StringJoinInstruction i4 = (StringJoinInstruction)instructs.get(4); + assertEquals(2, i4.getN()); + } + + @Test + public void doubleQuoteStringScriptTest2() { + getScriptInstructions("\"Hello ${a} ccc\"", InterpolationMode.SCRIPT); + } + + @Test + public void doubleQuoteStringVariableTest() { + List instructions = getScriptInstructions("\"a ${ v-1 } b\"", InterpolationMode.VARIABLE); + ConstInstruction i0 = (ConstInstruction)instructions.get(0); + assertEquals("a ", i0.getConstObj()); + LoadInstruction i1 = (LoadInstruction)instructions.get(1); + assertEquals("v-1", i1.getName()); + ConstInstruction i2 = (ConstInstruction)instructions.get(2); + assertEquals(" b", i2.getConstObj()); + StringJoinInstruction i3 = (StringJoinInstruction)instructions.get(3); + assertEquals(3, i3.getN()); + } + + @Test + public void ifTest() { + List instructions = getScriptInstructions( + "if (a > 0 && a < 5) {\n" + " true\n" + "} else if (a > 5 && a < 10) {\n" + " false\n" + + "} else if (a > 10 && a < 15) {\n" + " true\n" + "} == true", + InterpolationMode.VARIABLE); + OperatorInstruction equalInstruction = (OperatorInstruction)instructions.get(instructions.size() - 2); + assertEquals("==", equalInstruction.getOperator().getOperator()); + } + + private List getScriptInstructions(String script, InterpolationMode interpolationMode) { + QLParser.ProgramContext programContext = SyntaxTreeFactory + .buildTree(script, new MockOpM(), true, false, System.out::println, interpolationMode, "${", "}", true); + QvmInstructionVisitor visitor = new QvmInstructionVisitor(script); + programContext.accept(visitor); + return visitor.getInstructions(); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java b/src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java new file mode 100644 index 0000000..25e01dc --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/docs/CustomItemsDocTest.java @@ -0,0 +1,143 @@ +package com.alibaba.qlexpress4.docs; + +import com.alibaba.qlexpress4.Express4Runner; +import com.alibaba.qlexpress4.InitOptions; +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLPrecedences; +import com.alibaba.qlexpress4.api.QLFunctionalVarargs; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Examples used by docs/custom-item-source.adoc + */ +public class CustomItemsDocTest { + + @Test + public void addFunctionWithJavaFunctionalTest() { + // tag::addFunctionWithJavaFunctional[] + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // Function + runner.addFunction("inc", (Function)x -> x + 1); + // Predicate + runner.addFunction("isPos", (Predicate)x -> x > 0); + // Runnable + runner.addFunction("notify", () -> { + }); + // Consumer + runner.addFunction("print", (Consumer)System.out::println); + + Object r1 = runner.execute("inc(1)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + Object r2 = runner.execute("isPos(1)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(2, r1); + assertEquals(true, r2); + // end::addFunctionWithJavaFunctional[] + } + + // tag::joinFunctionVarargsObj[] + public static class JoinFunction implements QLFunctionalVarargs { + @Override + public Object call(Object... params) { + return Arrays.stream(params).map(Object::toString).collect(Collectors.joining(",")); + } + } + // end::joinFunctionVarargsObj[] + + @Test + public void addFunctionByVarargsTest() { + // tag::addFunctionByVarargs[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addVarArgsFunction("join", new JoinFunction()); + Object resultFunction = + express4Runner.execute("join(1,2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultFunction); + // end::addFunctionByVarargs[] + } + + @Test + public void addOperatorWithPrecedenceTest() { + // tag::addOperatorWithPrecedence[] + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.addOperator("?><", (left, right) -> left.get().toString() + right.get().toString(), QLPrecedences.ADD); + Object r = runner.execute("1 ?>< 2 * 3", new HashMap<>(), QLOptions.DEFAULT_OPTIONS).getResult(); + // precedence set to ADD, so multiply first, then custom operator: "1" + "6" => "16" + assertEquals("16", r); + // end::addOperatorWithPrecedence[] + } + + @Test + public void replaceDefaultOperatorTest() { + // tag::replaceDefaultOperator[] + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + boolean ok = runner.replaceDefaultOperator("+", + (left, right) -> Double.parseDouble(left.get().toString()) + Double.parseDouble(right.get().toString())); + assertTrue(ok); + Object r = runner.execute("'1.2' + '2.3'", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(3.5d, r); + // end::replaceDefaultOperator[] + } + + @Test + public void addOperatorBiFunctionTest() { + // tag::addOperatorBiFunction[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + express4Runner.addOperatorBiFunction("join", (left, right) -> left + "," + right); + Object resultOperator = + express4Runner.execute("1 join 2 join 3", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2,3", resultOperator); + // end::addOperatorBiFunction[] + } + + @Test + public void addOperatorByVarargsTest() { + // tag::addOperatorByVarargs[] + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + runner.addOperator("join", params -> params[0] + "," + params[1]); + Object r = runner.execute("1 join 2", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals("1,2", r); + // end::addOperatorByVarargs[] + } + + @Test + public void qlfunctionalvarargsAllInOneTest() { + // tag::qlfunctionalvarargsAllInOne[] + Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + QLFunctionalVarargs allInOne = params -> { + // sum numbers no matter how many args + double sum = 0d; + for (Object p : params) { + if (p instanceof Number) { + sum += ((Number)p).doubleValue(); + } + } + return sum; + }; + + // as function + runner.addVarArgsFunction("sumAll", allInOne); + // as operator + runner.addOperator("+&", allInOne); + // as extension function: first arg is the receiver, followed by call arguments + runner.addExtendFunction("plusAll", Number.class, allInOne); + + Map ctx = new HashMap<>(); + Object rf = runner.execute("sumAll(1,2,3)", ctx, QLOptions.DEFAULT_OPTIONS).getResult(); + Object ro = runner.execute("1 +& 4", ctx, QLOptions.DEFAULT_OPTIONS).getResult(); + Object re = runner.execute("1.plusAll(5)", ctx, QLOptions.DEFAULT_OPTIONS).getResult(); + assertEquals(6d, rf); + assertEquals(5d, ro); + assertEquals(6d, re); + // end::qlfunctionalvarargsAllInOne[] + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/exception/MockErrorReporter.java b/src/test/java/com/alibaba/qlexpress4/exception/MockErrorReporter.java new file mode 100644 index 0000000..b48298b --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/exception/MockErrorReporter.java @@ -0,0 +1,8 @@ +package com.alibaba.qlexpress4.exception; + +public class MockErrorReporter implements ErrorReporter { + @Override + public QLRuntimeException reportFormatWithCatch(Object catchObj, String errorCode, String format, Object... args) { + return new QLRuntimeException(null, String.format(format, args), errorCode); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/exception/QLExceptionTest.java b/src/test/java/com/alibaba/qlexpress4/exception/QLExceptionTest.java new file mode 100644 index 0000000..140f4fc --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/exception/QLExceptionTest.java @@ -0,0 +1,24 @@ +package com.alibaba.qlexpress4.exception; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Author: DQinYuan + */ +public class QLExceptionTest { + + @Test + public void reportTest() { + String script = "if (3>1) {\n" + " break 9;\n" + "} else {\n" + " return 11;\n" + "}"; + QLSyntaxException qlSyntaxException = QLException + .reportScannerErr(script, 13, 2, 3, "break", "BREAK_MUST_IN_FOR_OR_WHILE", "break must in for/while"); + assertEquals( + "[Error BREAK_MUST_IN_FOR_OR_WHILE: break must in for/while]\n" + + "[Near: if (3>1) { break 9; } else { retur...]\n" + " ^^^^^\n" + + "[Line: 2, Column: 3]", + qlSyntaxException.getMessage()); + } + +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/generic/GenericTypeTest.java b/src/test/java/com/alibaba/qlexpress4/generic/GenericTypeTest.java new file mode 100644 index 0000000..3518a9e --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/generic/GenericTypeTest.java @@ -0,0 +1,48 @@ +package com.alibaba.qlexpress4.generic; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class GenericTypeTest { + @Test + public void test() + throws NoSuchFieldException, NoSuchMethodException { + // 获取属性的泛型类型 + Field field1 = Class4GenericType.class.getDeclaredField("field1"); + Type type1 = field1.getGenericType(); + Type[] actualTypeArguments = ((ParameterizedType)type1).getActualTypeArguments(); + Assert.assertEquals(String.class, actualTypeArguments[0]); + + Field field2 = Class4GenericType.class.getDeclaredField("field2"); + Type type2 = field2.getGenericType(); + Assert.assertEquals(String.class, type2); + + // 获取方法出入参的泛型类型 + Method method1 = Class4GenericType.class.getMethod("method1", List.class); + Parameter firstParameter = method1.getParameters()[0]; + ParameterizedType firstParameterizedType = (ParameterizedType)(firstParameter.getParameterizedType()); + Assert.assertEquals(Long.class, firstParameterizedType.getActualTypeArguments()[0]); + + Type returnType = method1.getGenericReturnType(); + ParameterizedType returnParameterizedType = (ParameterizedType)returnType; + Assert.assertEquals(String.class, returnParameterizedType.getActualTypeArguments()[0]); + } + + public static class Class4GenericType { + private List field1; + + private String field2; + + public List method1(List longList) { + return Collections.emptyList(); + } + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/InterGrandPa.java b/src/test/java/com/alibaba/qlexpress4/inport/InterGrandPa.java new file mode 100644 index 0000000..f403e10 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/InterGrandPa.java @@ -0,0 +1,9 @@ +package com.alibaba.qlexpress4.inport; + +public class InterGrandPa { + + public String haha() { + return "grandPa"; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImpl.java b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImpl.java new file mode 100644 index 0000000..2828e03 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImpl.java @@ -0,0 +1,4 @@ +package com.alibaba.qlexpress4.inport; + +public class InterWithDefaultImpl implements InterWithDefaultMethod { +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImplChild.java b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImplChild.java new file mode 100644 index 0000000..a7200bc --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImplChild.java @@ -0,0 +1,4 @@ +package com.alibaba.qlexpress4.inport; + +public class InterWithDefaultImplChild extends InterWithDefaultImpl { +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImplExtGrandPa.java b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImplExtGrandPa.java new file mode 100644 index 0000000..3106a03 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImplExtGrandPa.java @@ -0,0 +1,4 @@ +package com.alibaba.qlexpress4.inport; + +public class InterWithDefaultImplExtGrandPa extends InterGrandPa implements InterWithDefaultMethod { +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImplGrandPaChild.java b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImplGrandPaChild.java new file mode 100644 index 0000000..b58505c --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultImplGrandPaChild.java @@ -0,0 +1,4 @@ +package com.alibaba.qlexpress4.inport; + +public class InterWithDefaultImplGrandPaChild extends InterWithDefaultImplExtGrandPa { +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultMethod.java b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultMethod.java new file mode 100644 index 0000000..03448cc --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/InterWithDefaultMethod.java @@ -0,0 +1,9 @@ +package com.alibaba.qlexpress4.inport; + +public interface InterWithDefaultMethod { + + default String haha() { + return "haha"; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/MyDesk.java b/src/test/java/com/alibaba/qlexpress4/inport/MyDesk.java new file mode 100644 index 0000000..200c3cc --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/MyDesk.java @@ -0,0 +1,27 @@ +package com.alibaba.qlexpress4.inport; + +/** + * Author: DQinYuan + */ +public class MyDesk { + + private String book1; + + private String book2; + + public String getBook1() { + return book1; + } + + public void setBook1(String book1) { + this.book1 = book1; + } + + public String getBook2() { + return book2; + } + + public void setBook2(String book2) { + this.book2 = book2; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/MyHome.java b/src/test/java/com/alibaba/qlexpress4/inport/MyHome.java new file mode 100644 index 0000000..f7d4ee6 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/MyHome.java @@ -0,0 +1,43 @@ +package com.alibaba.qlexpress4.inport; + +/** + * Author: DQinYuan + */ +public class MyHome { + + private String sofa; + + private String chair; + + private MyDesk myDesk; + + private String bed; + + public String getSofa() { + return sofa; + } + + public void setSofa(String sofa) { + this.sofa = sofa; + } + + public String getChair() { + return chair; + } + + public MyDesk getMyDesk() { + return myDesk; + } + + public void setMyDesk(MyDesk myDesk) { + this.myDesk = myDesk; + } + + public void setChair(String chair) { + this.chair = chair; + } + + public String getBed() { + return bed; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/Person.java b/src/test/java/com/alibaba/qlexpress4/inport/Person.java new file mode 100644 index 0000000..6a2cef7 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/Person.java @@ -0,0 +1,17 @@ +package com.alibaba.qlexpress4.inport; + +/** + * @author 冰够 + */ +public class Person implements Comparable { + private final int age; + + public Person(int age) { + this.age = age; + } + + @Override + public int compareTo(Person o) { + return Integer.compare(age, o.age); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/Sample.java b/src/test/java/com/alibaba/qlexpress4/inport/Sample.java new file mode 100644 index 0000000..280e63d --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/Sample.java @@ -0,0 +1,10 @@ +package com.alibaba.qlexpress4.inport; + +/** + * Author: DQinYuan + */ +public class Sample { + + public static int value = 1; + +} diff --git a/src/test/java/com/alibaba/qlexpress4/inport/Sample1.java b/src/test/java/com/alibaba/qlexpress4/inport/Sample1.java new file mode 100644 index 0000000..1b57ef2 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/inport/Sample1.java @@ -0,0 +1,10 @@ +package com.alibaba.qlexpress4.inport; + +/** + * Author: DQinYuan + */ +public class Sample1 { + + public static int value = 10; + +} diff --git a/src/test/java/com/alibaba/qlexpress4/pf4j/Pf4jClassSupplierTest.java b/src/test/java/com/alibaba/qlexpress4/pf4j/Pf4jClassSupplierTest.java new file mode 100644 index 0000000..1a9b852 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/pf4j/Pf4jClassSupplierTest.java @@ -0,0 +1,56 @@ +package com.alibaba.qlexpress4.pf4j; + +import com.alibaba.qlexpress4.ClassSupplier; +import com.alibaba.qlexpress4.Express4Runner; +import com.alibaba.qlexpress4.InitOptions; +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.security.QLSecurityStrategy; + +import org.junit.Assert; +import org.junit.Test; +import org.pf4j.DefaultPluginManager; +import org.pf4j.PluginManager; +import org.pf4j.PluginWrapper; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collections; + +public class Pf4jClassSupplierTest { + @Test + public void testPluginClassSupplier() + throws Exception { + // tag::pluginClassSupplier[] + // Specify plugin directory (test-plugins directory under test resources) + Path pluginsDir = new File("src/test/resources/test-plugins").toPath(); + PluginManager pluginManager = new DefaultPluginManager(pluginsDir); + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + + // Get the PluginClassLoader of the first plugin + PluginWrapper plugin = pluginManager.getPlugins().get(0); + ClassLoader pluginClassLoader = plugin.getPluginClassLoader(); + + // Custom class supplier using plugin ClassLoader + ClassSupplier pluginClassSupplier = clsName -> { + try { + return Class.forName(clsName, true, pluginClassLoader); + } + catch (ClassNotFoundException | NoClassDefFoundError e) { + return null; + } + }; + + InitOptions options = InitOptions.builder() + .securityStrategy(QLSecurityStrategy.open()) + .classSupplier(pluginClassSupplier) + .build(); + Express4Runner runner = new Express4Runner(options); + + String script = "import com.alibaba.qlexpress4.pf4j.TestPluginInterface; TestPluginInterface.TEST_CONSTANT"; + Object result = runner.execute(script, Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); + + Assert.assertEquals("Hello from PF4J Plugin!", result.toString()); + // end::pluginClassSupplier[] + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/pf4j/TestPluginImpl.java b/src/test/java/com/alibaba/qlexpress4/pf4j/TestPluginImpl.java new file mode 100644 index 0000000..2a8e308 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/pf4j/TestPluginImpl.java @@ -0,0 +1,24 @@ +package com.alibaba.qlexpress4.pf4j; + +import org.pf4j.Plugin; +import org.pf4j.PluginWrapper; + +/** + * Test plugin implementation class + */ +public class TestPluginImpl extends Plugin { + + public TestPluginImpl(PluginWrapper wrapper) { + super(wrapper); + } + + @Override + public void start() { + System.out.println("TestPlugin started"); + } + + @Override + public void stop() { + System.out.println("TestPlugin stopped"); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/pf4j/TestPluginInterface.java b/src/test/java/com/alibaba/qlexpress4/pf4j/TestPluginInterface.java new file mode 100644 index 0000000..770a3fd --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/pf4j/TestPluginInterface.java @@ -0,0 +1,13 @@ +package com.alibaba.qlexpress4.pf4j; + +/** + * Test plugin interface containing a string constant + */ +public interface TestPluginInterface { + + /** + * Test string constant + */ + String TEST_CONSTANT = "Hello from PF4J Plugin!"; + +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/FixedSizeStackTest.java b/src/test/java/com/alibaba/qlexpress4/runtime/FixedSizeStackTest.java new file mode 100644 index 0000000..829d9bf --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/FixedSizeStackTest.java @@ -0,0 +1,32 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.data.DataValue; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Author: DQinYuan + */ +public class FixedSizeStackTest { + + @Test + public void pushPopTest() { + FixedSizeStack fixedSizeStack = new FixedSizeStack(4); + fixedSizeStack.push(new DataValue(1)); + fixedSizeStack.push(new DataValue(2)); + fixedSizeStack.push(new DataValue(3)); + fixedSizeStack.push(new DataValue(4)); + assertEquals(4, fixedSizeStack.pop().get()); + assertEquals(3, fixedSizeStack.pop().get()); + fixedSizeStack.push(new DataValue(5)); + fixedSizeStack.push(new DataValue(6)); + Parameters parameters = fixedSizeStack.pop(3); + assertEquals(2, parameters.get(0).get()); + assertEquals(5, parameters.get(1).get()); + assertEquals(6, parameters.get(2).get()); + // exceed size + assertNull(parameters.get(3)); + } + +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/MemberResolverTest.java b/src/test/java/com/alibaba/qlexpress4/runtime/MemberResolverTest.java new file mode 100644 index 0000000..f661dc3 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/MemberResolverTest.java @@ -0,0 +1,62 @@ +package com.alibaba.qlexpress4.runtime; + +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.function.Predicate; + +import static org.junit.Assert.*; + +/** + * Author: DQinYuan + */ +public class MemberResolverTest { + + public static class HelloMemberResolver { + + public HelloMemberResolver(Number i) { + + } + + public HelloMemberResolver(long i) { + + } + + public HelloMemberResolver(Long i, Runnable runnable) { + + } + + public HelloMemberResolver(long i, Runnable runnable) { + + } + } + + @Test + public void resolveConstructorTest() { + Constructor constructor = + MemberResolver.resolveConstructor(HelloMemberResolver.class, new Class[] {Integer.class}); + assertEquals(long.class, constructor.getParameterTypes()[0]); + + Constructor constructor1 = + MemberResolver.resolveConstructor(HelloMemberResolver.class, new Class[] {Long.class, QLambda.class}); + assertEquals(Runnable.class, constructor1.getParameterTypes()[1]); + } + + @Test + public void resolvePriorityTest() { + int result = MemberResolver.resolvePriority(new Class[] {boolean.class}, new Class[] {Boolean.class}); + assertEquals(MemberResolver.MatchPriority.UNBOX.priority, result); + + int result1 = MemberResolver.resolvePriority(new Class[] {}, new Class[] {}); + assertEquals(MemberResolver.MatchPriority.EQUAL.priority, result1); + } + + @Test + public void resolveStreamTest() { + IMethod result = MemberResolver + .resolveMethod(new ArrayList().stream().getClass(), "filter", new Class[] {Predicate.class}, false, false); + assertNotNull(result); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/MockLeftValue.java b/src/test/java/com/alibaba/qlexpress4/runtime/MockLeftValue.java new file mode 100644 index 0000000..b5f5b18 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/MockLeftValue.java @@ -0,0 +1,24 @@ +package com.alibaba.qlexpress4.runtime; + +import com.alibaba.qlexpress4.runtime.operator.MockValue; + +public class MockLeftValue extends MockValue implements LeftValue { + public MockLeftValue(Object value, Class declaredClass) { + super(value, declaredClass); + } + + @Override + public Class getDefinedType() { + return declaredClass; + } + + @Override + public void setInner(Object newValue) { + this.value = newValue; + } + + @Override + public String getSymbolName() { + return null; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/instruction/CallInstructionTest.java b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/CallInstructionTest.java new file mode 100644 index 0000000..65865fd --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/CallInstructionTest.java @@ -0,0 +1,39 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.MockErrorReporter; +import com.alibaba.qlexpress4.runtime.QLambda; +import com.alibaba.qlexpress4.test.property.Child; +import com.alibaba.qlexpress4.test.property.ParentParameters; +import org.junit.Assert; +import org.junit.Test; + +/** + * Author: TaoKan + */ +public class CallInstructionTest { + /** + * child.getMethod1 (methodInstruction+callInstruction) + * getMethod1 + */ + @Test + public void case1() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetMethodInstruction getMethodInstruction = new GetMethodInstruction(errorReporter, "getMethod1"); + MockQContextParent mockQContextParent = new MockQContextParent(true); + mockQContextParent.push(new Child()); + getMethodInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertTrue(mockQContextParent.getValue().get() instanceof QLambda); + QLambda qLambda = (QLambda)mockQContextParent.getValue().get(); + + CallInstruction callInstruction = new CallInstruction(errorReporter, 2); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(qLambda); + parentParameters.push(1); + parentParameters.push(2); + mockQContextParent.setParameters(parentParameters); + callInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 3); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/instruction/GetFieldInstructionTest.java b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/GetFieldInstructionTest.java new file mode 100644 index 0000000..e48d738 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/GetFieldInstructionTest.java @@ -0,0 +1,208 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.MockErrorReporter; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.runtime.LeftValue; +import com.alibaba.qlexpress4.runtime.MetaClass; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.test.property.Child; +import com.alibaba.qlexpress4.test.property.Parent; +import com.alibaba.qlexpress4.test.property.TestEnum; +import org.junit.Assert; +import org.junit.Test; + +/** + * Author: TaoKan + */ +public class GetFieldInstructionTest { + /** + * Parent::getStaticGet() setMethod is null and fieldNotAccess + */ + @Test + public void case1() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "staticGet", false); + MockQContextParent mockQContextParent = new MockQContextParent(false); + mockQContextParent.push(new DataValue(new MetaClass(Parent.class))); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals((mockQContextParent.getValue()).get(), "staticGet1"); + } + + /** + * public static case(Static field of Parent) + * Parent.staticSet + */ + @Test + public void case2() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "staticSet", false); + MockQContextParent mockQContextParent = new MockQContextParent(false); + mockQContextParent.push(new DataValue(new MetaClass(Parent.class))); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + ((LeftValue)mockQContextParent.getValue()).set("staticSet1", errorReporter); + Assert.assertEquals((mockQContextParent.getValue()).get(), "staticSet1"); + } + + /** + * private static case(Static field of Parent) + * Parent.staticSetPrivate notSetAble + */ + @Test + public void case3() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "staticSetPrivate", false); + MockQContextParent mockQContextParent = new MockQContextParent(false); + mockQContextParent.push(new DataValue(new MetaClass(Parent.class))); + try { + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + } + catch (QLRuntimeException e) { + Assert.assertEquals("FIELD_NOT_FOUND", e.getErrorCode()); + return; + } + Assert.fail(); + } + + /** + * private static case(Static field of Parent) + * Parent.staticSetPrivate allowPrivateAccess + */ + @Test + public void case4() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "staticSetPrivate", false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + mockQContextParent.push(new DataValue(new MetaClass(Parent.class))); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("staticSetPrivate", (mockQContextParent.getValue()).get()); + } + + /** + * static case(Static method of Parent instance) + * parent.getStaticGet() allowPrivateAccess + */ + @Test + public void case5() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "staticGet", false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + mockQContextParent.push(new Parent()); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals("staticGet1", (mockQContextParent.getValue()).get()); + } + + /** + * normal case(member method of Parent instance) + * parent.getAge() + */ + @Test + public void case6() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "age", false); + MockQContextParent mockQContextParent = new MockQContextParent(false); + mockQContextParent.push(new Parent()); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + ((LeftValue)mockQContextParent.getValue()).set(35, errorReporter); + Assert.assertEquals((mockQContextParent.getValue()).get(), 35); + } + + /** + * normal case(member field of Parent instance) + * parent.name allowPrivateAccess + */ + @Test + public void case7() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "name", false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + mockQContextParent.push(new Parent()); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + ((LeftValue)mockQContextParent.getValue()).set("name1", errorReporter); + Assert.assertEquals((mockQContextParent.getValue()).get(), "name1"); + } + + /** + * error case(member field of Parent instance) + * parent.name not accessible + */ + @Test + public void case8() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "name", false); + MockQContextParent mockQContextParent = new MockQContextParent(false); + mockQContextParent.push(new Parent()); + try { + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.fail(); + } + catch (QLRuntimeException e) { + Assert.assertEquals("FIELD_NOT_FOUND", e.getErrorCode()); + } + } + + /** + * normal case(normal method of Parent,Child instance) + * child.getAge() instead of parent.getAge() + */ + @Test + public void case9() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "age", false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + mockQContextParent.push(new Child()); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals((mockQContextParent.getValue()).get(), 11); + } + + /** + * normal case(member method of Parent,Child instance) + * parent.getBirth() public instead of child.birth(not allow) + */ + @Test + public void case10() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "birth", false); + MockQContextParent mockQContextParent = new MockQContextParent(false); + mockQContextParent.push(new Child()); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals((mockQContextParent.getValue()).get(), "2022-01-01"); + } + + /** + * error case(member method of Parent instance) + * parent.getMethod1() not found + */ + @Test + public void case11() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "method1", false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + mockQContextParent.push(new Child()); + try { + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.fail(); + } + catch (QLRuntimeException e) { + Assert.assertEquals("FIELD_NOT_FOUND", e.getErrorCode()); + } + } + + /** + * enum attr get + * TestEnum.skt.getValue() + */ + @Test + public void case12() { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "SKT", false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + mockQContextParent.push(new DataValue(new MetaClass(TestEnum.class))); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + GetFieldInstruction getFieldInstruction1 = new GetFieldInstruction(errorReporter, "value", false); + getFieldInstruction1.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals((mockQContextParent.getValue()).get(), -1); + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/instruction/InterWithDefaultMethod.java b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/InterWithDefaultMethod.java new file mode 100644 index 0000000..7e0d121 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/InterWithDefaultMethod.java @@ -0,0 +1,13 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +/** + * Author: DQinYuan + */ +public interface InterWithDefaultMethod { + + int getI(); + + default int returnI() { + return getI(); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MethodInvokeInstructionTest.java b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MethodInvokeInstructionTest.java new file mode 100644 index 0000000..7b2cc49 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MethodInvokeInstructionTest.java @@ -0,0 +1,265 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.MockErrorReporter; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.test.property.*; +import org.junit.Assert; +import org.junit.Test; + +import java.math.BigDecimal; + +import static org.junit.Assert.*; + +/** + * Author: TaoKan + */ +public class MethodInvokeInstructionTest { + /** + * child.getMethod1 (methodInstruction+callInstruction) + * getMethod1 + */ + @Test + public void equalTypeTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "getMethod1", 2, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child()); + parentParameters.push(1); + parentParameters.push(2); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + assertEquals(mockQContextParent.getValue().get(), 3); + } + + @Test + public void runnableTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = new MethodInvokeInstruction(errorReporter, "run", 0, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push((Runnable)() -> { + }); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + } + + @Test + public void defaultMethodTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "returnI", 0, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push((InterWithDefaultMethod)() -> 9); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + assertEquals(9, mockQContextParent.getValue().get()); + } + + @Test + public void childMethodMatchTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "getMethod11", 2, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child()); + parentParameters.push(1); + parentParameters.push(2); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 3L); + } + + @Test + public void parentMethodMatch() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "getMethod12", 2, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child()); + parentParameters.push(1); + parentParameters.push(2); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 3L); + } + + @Test + public void convertTypeAssignedMatch() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "getMethod5", 1, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child3()); + parentParameters.push(new Child3()); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 0); + } + + @Test + public void arrayParamTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "getMethod6", 1, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child3()); + parentParameters.push(new Integer[] {5, 6}); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 10); + } + + @Test + public void primitiveParamTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "getMethod7", 1, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child4()); + parentParameters.push(new Integer(5)); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 5); + } + + @Test + public void primitiveImplicitTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "getMethod8", 1, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child5()); + parentParameters.push(5); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 5.0); + } + + @Test + public void bigIntegerImplicitTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "getMethod9", 1, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child6()); + parentParameters.push(5); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 5); + } + + @Test + public void doubleMatchTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "getMethod10", 1, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child6()); + parentParameters.push(5.0f); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), new BigDecimal("5.0")); + } + + @Test + public void varArgTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "addField", 3, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child9()); + parentParameters.push(5); + parentParameters.push("5.0"); + parentParameters.push("5.0"); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), "1"); + } + + @Test + public void varArgTest2() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "addField1", 3, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child9()); + parentParameters.push(5); + parentParameters.push("5.0"); + parentParameters.push("5.0"); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), "1"); + } + + @Test + public void varArgTest3() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "addField2", 3, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child9()); + parentParameters.push(5); + parentParameters.push("5.0"); + parentParameters.push("5.0"); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), "1"); + } + + @Test + public void varArgNotMatchTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "addField3", 3, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child9()); + parentParameters.push(5); + parentParameters.push("asd"); + parentParameters.push("sss"); + mockQContextParent.setParameters(parentParameters); + try { + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.fail(); + } + catch (QLRuntimeException e) { + Assert.assertEquals("METHOD_NOT_FOUND", e.getErrorCode()); + } + } + + @Test + public void varArgNotMatchTest2() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "addField", 3, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child9()); + parentParameters.push(5); + parentParameters.push(1); + parentParameters.push(1); + mockQContextParent.setParameters(parentParameters); + try { + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + } + catch (QLRuntimeException e) { + Assert.assertEquals("METHOD_NOT_FOUND", e.getErrorCode()); + } + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MockCastParameters.java b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MockCastParameters.java new file mode 100644 index 0000000..ed84ad0 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MockCastParameters.java @@ -0,0 +1,35 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; + +/** + * Author: TaoKan + */ +public class MockCastParameters implements Parameters { + private Class aClass; + + private Object obj; + + public MockCastParameters(Class aClass, Object obj) { + this.aClass = aClass; + this.obj = obj; + } + + @Override + public Value get(int i) { + if (i == 0) { + return new DataValue(aClass); + } + if (i == 1) { + return new DataValue(obj); + } + return null; + } + + @Override + public int size() { + return 0; + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MockParametersParentClass.java b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MockParametersParentClass.java new file mode 100644 index 0000000..9f85276 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MockParametersParentClass.java @@ -0,0 +1,21 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.test.property.ParentClass; + +/** + * Author: TaoKan + */ +public class MockParametersParentClass implements Parameters { + @Override + public Value get(int i) { + ParentClass parent = new ParentClass(); + return parent; + } + + @Override + public int size() { + return 0; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MockQContextParent.java b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MockQContextParent.java new file mode 100644 index 0000000..3914662 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/MockQContextParent.java @@ -0,0 +1,129 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.runtime.*; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; +import com.alibaba.qlexpress4.runtime.scope.QScope; +import com.alibaba.qlexpress4.runtime.trace.QTraces; +import com.alibaba.qlexpress4.security.QLSecurityStrategy; + +import java.util.Collections; +import java.util.Map; + +/** + * Author: TaoKan + */ +public class MockQContextParent implements QContext { + private Value value; + + private Parameters parameters; + + private final boolean allowPrivateAccess; + + private final ReflectLoader reflectLoader; + + public MockQContextParent(boolean allowPrivateAccess) { + this.allowPrivateAccess = allowPrivateAccess; + this.reflectLoader = new ReflectLoader(QLSecurityStrategy.open(), allowPrivateAccess); + } + + public Value getValue() { + return value; + } + + public void setParameters(Parameters parameters) { + this.parameters = parameters; + } + + @Override + public LeftValue getSymbol(String varName) { + return null; + } + + @Override + public Value getSymbolValue(String varName) { + return null; + } + + @Override + public void defineLocalSymbol(String varName, Class varClz, Object value) { + + } + + @Override + public void defineFunction(String functionName, CustomFunction function) { + + } + + @Override + public CustomFunction getFunction(String functionName) { + return null; + } + + @Override + public Map getFunctionTable() { + return Collections.emptyMap(); + } + + @Override + public void push(Value value) { + this.value = value; + } + + public void pushParameter(Parameters parameters) { + this.parameters = parameters; + } + + @Override + public Parameters pop(int number) { + return this.parameters; + } + + @Override + public Value pop() { + return this.value; + } + + @Override + public Value peek() { + return this.value; + } + + @Override + public QScope getParent() { + return null; + } + + @Override + public QScope newScope() { + return null; + } + + @Override + public long scriptStartTimeStamp() { + return 0; + } + + @Override + public Map attachment() { + return null; + } + + @Override + public ReflectLoader getReflectLoader() { + return reflectLoader; + } + + @Override + public QTraces getTraces() { + return null; + } + + @Override + public QScope getCurrentScope() { + return this; + } + + @Override + public void closeScope() { + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/instruction/NewInstanceInstructionTest.java b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/NewInstanceInstructionTest.java new file mode 100644 index 0000000..d2c4b01 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/instruction/NewInstanceInstructionTest.java @@ -0,0 +1,151 @@ +package com.alibaba.qlexpress4.runtime.instruction; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.MockErrorReporter; +import com.alibaba.qlexpress4.exception.QLRuntimeException; +import com.alibaba.qlexpress4.test.property.*; +import org.junit.Assert; +import org.junit.Test; + +/** + * Author: TaoKan + */ +public class NewInstanceInstructionTest { + @Test + public void newInstructionTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstructionForParent0 = new NewInstanceInstruction(errorReporter, Parent.class, 0); + MockQContextParent mockQContextParent = new MockQContextParent(false); + newInstructionForParent0.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Object s = mockQContextParent.getValue().get(); + Assert.assertTrue(s instanceof Parent); + + NewInstanceInstruction newInstructionForParentWithAge = + new NewInstanceInstruction(errorReporter, Parent.class, 1); + MockQContextParent mockQContextParent1 = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(6); + mockQContextParent1.pushParameter(parentParameters); + newInstructionForParentWithAge.execute(mockQContextParent1, QLOptions.DEFAULT_OPTIONS); + Object result = mockQContextParent1.getValue().get(); + Assert.assertTrue(result instanceof Parent && ((Parent)result).getAge() == 6); + } + + @Test + public void constructorNotFoundTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstruction = new NewInstanceInstruction(errorReporter, Child.class, 2); + MockQContextParent mockQContextParent = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(2); + parentParameters.push(3); + mockQContextParent.pushParameter(parentParameters); + try { + newInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.fail(); + } + catch (QLRuntimeException e) { + Assert.assertEquals("NO_SUITABLE_CONSTRUCTOR", e.getErrorCode()); + } + } + + @Test + public void constructorConvertMatchTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstruction = new NewInstanceInstruction(errorReporter, Child1.class, 2); + MockQContextParent mockQContextParent = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(2); + parentParameters.push(3); + mockQContextParent.pushParameter(parentParameters); + newInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertTrue(mockQContextParent.getValue().get() instanceof Child1); + } + + @Test + public void constructorConvertAssignedMatch() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstruction = new NewInstanceInstruction(errorReporter, Child3.class, 1); + MockQContextParent mockQContextParent = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child3()); + mockQContextParent.pushParameter(parentParameters); + newInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertTrue(mockQContextParent.getValue().get() instanceof Child3); + } + + @Test + public void arrayParamTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstruction = new NewInstanceInstruction(errorReporter, Child3.class, 1); + MockQContextParent mockQContextParent = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Integer[] {5, 6}); + mockQContextParent.pushParameter(parentParameters); + newInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertTrue(mockQContextParent.getValue().get() instanceof Child3); + } + + @Test + public void primitiveParamTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstruction = new NewInstanceInstruction(errorReporter, Child4.class, 1); + MockQContextParent mockQContextParent = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(5); + mockQContextParent.pushParameter(parentParameters); + newInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertTrue(mockQContextParent.getValue().get() instanceof Child4); + } + + @Test + public void primitiveImplicitTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstruction = new NewInstanceInstruction(errorReporter, Child5.class, 1); + MockQContextParent mockQContextParent = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(5); + mockQContextParent.pushParameter(parentParameters); + newInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertTrue(mockQContextParent.getValue().get() instanceof Child5); + } + + @Test + public void bigIntegerImplicitTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstruction = new NewInstanceInstruction(errorReporter, Child6.class, 1); + MockQContextParent mockQContextParent = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(5); + mockQContextParent.pushParameter(parentParameters); + newInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertTrue(mockQContextParent.getValue().get() instanceof Child6); + } + + @Test + public void numberConstructorMatchTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstruction = new NewInstanceInstruction(errorReporter, NumberConstructor.class, 1); + MockQContextParent mockQContextParent = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(5.0); + mockQContextParent.pushParameter(parentParameters); + newInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + NumberConstructor numberConstructor = (NumberConstructor)mockQContextParent.getValue().get(); + Assert.assertEquals(0, numberConstructor.getFlag()); + } + + @Test + public void varArgTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + NewInstanceInstruction newInstruction = new NewInstanceInstruction(errorReporter, Child9.class, 3); + MockQContextParent mockQContextParent = new MockQContextParent(false); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(5); + parentParameters.push("5.0"); + parentParameters.push("5.0"); + mockQContextParent.pushParameter(parentParameters); + newInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/operator/MockValue.java b/src/test/java/com/alibaba/qlexpress4/runtime/operator/MockValue.java new file mode 100644 index 0000000..13c2a02 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/operator/MockValue.java @@ -0,0 +1,19 @@ +package com.alibaba.qlexpress4.runtime.operator; + +import com.alibaba.qlexpress4.runtime.Value; + +public class MockValue implements Value { + protected Object value; + + protected Class declaredClass; + + public MockValue(Object value, Class declaredClass) { + this.value = value; + this.declaredClass = declaredClass; + } + + @Override + public Object get() { + return value; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusPrefixUnaryOperatorTest.java b/src/test/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusPrefixUnaryOperatorTest.java new file mode 100644 index 0000000..f090430 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusPrefixUnaryOperatorTest.java @@ -0,0 +1,26 @@ +package com.alibaba.qlexpress4.runtime.operator.unary; + +import java.math.BigInteger; + +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.MockErrorReporter; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.MockValue; + +import junit.framework.TestCase; +import org.junit.Assert; + +public class MinusMinusPrefixUnaryOperatorTest extends TestCase { + public void testExecute() { + ErrorReporter errorReporter = new MockErrorReporter(); + MinusMinusPrefixUnaryOperator minusMinusPrefixUnaryOperator = MinusMinusPrefixUnaryOperator.getInstance(); + + Value value = new MockValue(10L, null); + Object result = minusMinusPrefixUnaryOperator.execute(value, errorReporter); + Assert.assertEquals(10L, result); + + value = new MockValue(new BigInteger("10"), BigInteger.class); + BigInteger bigIntegerResult = (BigInteger)minusMinusPrefixUnaryOperator.execute(value, errorReporter); + Assert.assertEquals(10L, bigIntegerResult.intValue()); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusSuffixUnaryOperatorTest.java b/src/test/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusSuffixUnaryOperatorTest.java new file mode 100644 index 0000000..fd5e31e --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/runtime/operator/unary/MinusMinusSuffixUnaryOperatorTest.java @@ -0,0 +1,27 @@ +package com.alibaba.qlexpress4.runtime.operator.unary; + +import java.math.BigInteger; + +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.MockErrorReporter; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.operator.MockValue; + +import junit.framework.TestCase; +import org.junit.Assert; + +public class MinusMinusSuffixUnaryOperatorTest extends TestCase { + public void testExecute() { + ErrorReporter errorReporter = new MockErrorReporter(); + MinusMinusSuffixUnaryOperator minusMinusSuffixUnaryOperator = MinusMinusSuffixUnaryOperator.getInstance(); + + Value value = new MockValue(10L, null); + Object result = minusMinusSuffixUnaryOperator.execute(value, errorReporter); + Assert.assertEquals(9L, result); + + BigInteger bigInteger = BigInteger.valueOf(10L); + value = new MockValue(bigInteger, BigInteger.class); + BigInteger bigIntegerResult = (BigInteger)minusMinusSuffixUnaryOperator.execute(value, errorReporter); + Assert.assertEquals(9L, bigIntegerResult.longValue()); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/spring/HelloService.java b/src/test/java/com/alibaba/qlexpress4/spring/HelloService.java new file mode 100644 index 0000000..dcc0252 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/spring/HelloService.java @@ -0,0 +1,18 @@ +package com.alibaba.qlexpress4.spring; + +import org.springframework.stereotype.Service; + +/** + * Spring Bean example service class + */ +@Service +public class HelloService { + + /** + * Hello method that returns a greeting string + * @return greeting string + */ + public String hello(String name) { + return "Hello, " + name + "!"; + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/spring/QLExecuteService.java b/src/test/java/com/alibaba/qlexpress4/spring/QLExecuteService.java new file mode 100644 index 0000000..0338727 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/spring/QLExecuteService.java @@ -0,0 +1,26 @@ +package com.alibaba.qlexpress4.spring; + +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import com.alibaba.qlexpress4.Express4Runner; +import com.alibaba.qlexpress4.InitOptions; +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.security.QLSecurityStrategy; + +@Service +public class QLExecuteService { + + private final Express4Runner runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + + @Autowired + private ApplicationContext applicationContext; + + public Object execute(String script, Map context) { + QLSpringContext springContext = new QLSpringContext(context, applicationContext); + return runner.execute(script, springContext, QLOptions.DEFAULT_OPTIONS).getResult(); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/spring/QLSpringContext.java b/src/test/java/com/alibaba/qlexpress4/spring/QLSpringContext.java new file mode 100644 index 0000000..f1b41f0 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/spring/QLSpringContext.java @@ -0,0 +1,35 @@ +package com.alibaba.qlexpress4.spring; + +import java.util.Map; + +import org.springframework.context.ApplicationContext; + +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.context.ExpressContext; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.data.MapItemValue; + +public class QLSpringContext implements ExpressContext { + + private final Map context; + + private final ApplicationContext applicationContext; + + public QLSpringContext(Map context, ApplicationContext applicationContext) { + this.context = context; + this.applicationContext = applicationContext; + } + + @Override + public Value get(Map attachments, String variableName) { + Object value = context.get(variableName); + if (value != null) { + return new MapItemValue(context, variableName); + } + Object bean = applicationContext.getBean(variableName); + if (bean != null) { + return new DataValue(bean); + } + return new MapItemValue(context, value); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/spring/SpringDemoTest.java b/src/test/java/com/alibaba/qlexpress4/spring/SpringDemoTest.java new file mode 100644 index 0000000..6102dd3 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/spring/SpringDemoTest.java @@ -0,0 +1,30 @@ +package com.alibaba.qlexpress4.spring; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.HashMap; +import java.util.Map; + +/** + * HelloService unit test class + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = SpringTestConfig.class) +public class SpringDemoTest { + + @Autowired + private QLExecuteService qlExecuteService; + + @Test + public void qlExecuteWithSpringContextTest() { + Map context = new HashMap<>(); + context.put("name", "Wang"); + String result = (String)qlExecuteService.execute("helloService.hello(name)", context); + Assert.assertEquals("Hello, Wang!", result); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/spring/SpringTestConfig.java b/src/test/java/com/alibaba/qlexpress4/spring/SpringTestConfig.java new file mode 100644 index 0000000..9637b68 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/spring/SpringTestConfig.java @@ -0,0 +1,13 @@ +package com.alibaba.qlexpress4.spring; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Spring test configuration class + */ +@Configuration +@ComponentScan(basePackages = "com.alibaba.qlexpress4.spring") +public class SpringTestConfig { + // Configuration class, enables component scanning +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/test/annotation/QL4AliasTest.java b/src/test/java/com/alibaba/qlexpress4/test/annotation/QL4AliasTest.java new file mode 100644 index 0000000..9c2bccc --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/annotation/QL4AliasTest.java @@ -0,0 +1,56 @@ +package com.alibaba.qlexpress4.test.annotation; + +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.exception.ErrorReporter; +import com.alibaba.qlexpress4.exception.MockErrorReporter; +import com.alibaba.qlexpress4.runtime.MetaClass; +import com.alibaba.qlexpress4.runtime.data.DataValue; +import com.alibaba.qlexpress4.runtime.instruction.GetFieldInstruction; +import com.alibaba.qlexpress4.runtime.instruction.MethodInvokeInstruction; +import com.alibaba.qlexpress4.runtime.instruction.MockQContextParent; +import com.alibaba.qlexpress4.test.property.Child7; +import com.alibaba.qlexpress4.test.property.ParentParameters; +import org.junit.Assert; +import org.junit.Test; + +/** + * Author: TaoKan + */ +public class QL4AliasTest { + + @Test + public void classFieldTest() + throws Exception { + ErrorReporter errorReporter = new MockErrorReporter(); + GetFieldInstruction getFieldInstruction = new GetFieldInstruction(errorReporter, "测试静态字段", false); + MockQContextParent mockQContextParent = new MockQContextParent(false); + mockQContextParent.push(new DataValue(new MetaClass(Child7.class))); + getFieldInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals((mockQContextParent.getValue()).get(), 8); + } + + @Test + public void staticMethodTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = + new MethodInvokeInstruction(errorReporter, "测试静态方法", 0, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new MetaClass(Child7.class)); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 11); + } + + @Test + public void memberMethodTest() { + ErrorReporter errorReporter = new MockErrorReporter(); + MethodInvokeInstruction methodInvokeInstruction = new MethodInvokeInstruction(errorReporter, "测试方法", 0, false); + MockQContextParent mockQContextParent = new MockQContextParent(true); + ParentParameters parentParameters = new ParentParameters(); + parentParameters.push(new Child7()); + mockQContextParent.setParameters(parentParameters); + methodInvokeInstruction.execute(mockQContextParent, QLOptions.DEFAULT_OPTIONS); + Assert.assertEquals(mockQContextParent.getValue().get(), 10); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/constructor/HelloChild.java b/src/test/java/com/alibaba/qlexpress4/test/constructor/HelloChild.java new file mode 100644 index 0000000..0e91e51 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/constructor/HelloChild.java @@ -0,0 +1,7 @@ +package com.alibaba.qlexpress4.test.constructor; + +/** + * Author: DQinYuan + */ +public class HelloChild extends HelloParent { +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/constructor/HelloConstructor.java b/src/test/java/com/alibaba/qlexpress4/test/constructor/HelloConstructor.java new file mode 100644 index 0000000..e254321 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/constructor/HelloConstructor.java @@ -0,0 +1,35 @@ +package com.alibaba.qlexpress4.test.constructor; + +import com.alibaba.qlexpress4.runtime.QLambda; + +/** + * Author: DQinYuan + */ +public class HelloConstructor { + + public int flag; + + public HelloConstructor(HelloParent helloParent) { + this.flag = 0; + } + + public HelloConstructor(HelloChild child) { + this.flag = 1; + } + + public HelloConstructor(String... s) { + this.flag = 2; + } + + public HelloConstructor(String s) { + this.flag = 3; + } + + public HelloConstructor(HelloChild child, Runnable r) { + this.flag = 4; + } + + public HelloConstructor(HelloParent helloParent, QLambda q) { + this.flag = 5; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/constructor/HelloParent.java b/src/test/java/com/alibaba/qlexpress4/test/constructor/HelloParent.java new file mode 100644 index 0000000..30d4b63 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/constructor/HelloParent.java @@ -0,0 +1,7 @@ +package com.alibaba.qlexpress4.test.constructor; + +/** + * Author: DQinYuan + */ +public class HelloParent { +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/function/HelloFunction.java b/src/test/java/com/alibaba/qlexpress4/test/function/HelloFunction.java new file mode 100644 index 0000000..df123aa --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/function/HelloFunction.java @@ -0,0 +1,14 @@ +package com.alibaba.qlexpress4.test.function; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.QContext; +import com.alibaba.qlexpress4.runtime.function.CustomFunction; + +public class HelloFunction implements CustomFunction { + @Override + public Object call(QContext qContext, Parameters parameters) + throws Throwable { + String tenant = (String)qContext.attachment().get("tenant"); + return "hello," + tenant; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/issue/Issue318Test.java b/src/test/java/com/alibaba/qlexpress4/test/issue/Issue318Test.java new file mode 100644 index 0000000..ca13de7 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/issue/Issue318Test.java @@ -0,0 +1,54 @@ +package com.alibaba.qlexpress4.test.issue; + +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.qlexpress4.Express4Runner; +import com.alibaba.qlexpress4.InitOptions; +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.security.QLSecurityStrategy; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author 冰够 + */ +public class Issue318Test { + @Test + public void test() + throws Exception { + Express4Runner runner = + new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); + Map context = new HashMap<>(); + + Student student = new Student(); + student.name = "张三"; + student.alias = "zhangsan"; + context.put("student", student); + Object result = runner.execute("student.name == \"张三\"", context, QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertTrue((Boolean)result); + + result = runner.execute("student.alias == \"zhangsan\"", context, QLOptions.DEFAULT_OPTIONS).getResult(); + Assert.assertTrue((Boolean)result); + } + + public static class Student { + /** + * 默认不支持属性直接访问,必须要有getter方法 + * + * @return + */ + public String name; + + public String alias; + + public String getName() { + return name; + } + + public String getAlias() { + return alias; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/qlexpress4/test/lambda/UserFunctionalInterface.java b/src/test/java/com/alibaba/qlexpress4/test/lambda/UserFunctionalInterface.java new file mode 100644 index 0000000..52b221c --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/lambda/UserFunctionalInterface.java @@ -0,0 +1,10 @@ +package com.alibaba.qlexpress4.test.lambda; + +/** + * Author: DQinYuan + */ +public interface UserFunctionalInterface { + + int lala(int a, int b); + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/method/InterWithDefault.java b/src/test/java/com/alibaba/qlexpress4/test/method/InterWithDefault.java new file mode 100644 index 0000000..9f12d93 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/method/InterWithDefault.java @@ -0,0 +1,12 @@ +package com.alibaba.qlexpress4.test.method; + +/** + * Author: DQinYuan + */ +public interface InterWithDefault { + + default int get100() { + return 100; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/method/TestChild.java b/src/test/java/com/alibaba/qlexpress4/test/method/TestChild.java new file mode 100644 index 0000000..ec9e443 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/method/TestChild.java @@ -0,0 +1,15 @@ +package com.alibaba.qlexpress4.test.method; + +/** + * Author: DQinYuan + */ +public class TestChild extends TestParent implements InterWithDefault { + + public int get10() { + return 10; + } + + public int get10(String... s) { + return 11; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/method/TestParent.java b/src/test/java/com/alibaba/qlexpress4/test/method/TestParent.java new file mode 100644 index 0000000..15ddb46 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/method/TestParent.java @@ -0,0 +1,12 @@ +package com.alibaba.qlexpress4.test.method; + +/** + * Author: DQinYuan + */ +public class TestParent { + + public int get1() { + return 1; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child.java new file mode 100644 index 0000000..dd51e87 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child.java @@ -0,0 +1,73 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class Child extends Parent { + public String work = "childWork"; + + public static String staticWork = "childStaticWork"; + + private Boolean booValue = false; + + private int age = 11; + + private String birth = "2021-02-02"; + + private final long result; + + public Child() { + this.result = 0L; + } + + public Child(boolean a, int b) { + this.result = 1; + } + + public static String getStaticGet() { + return "5"; + } + + public static String getStaticGetParam(Integer a, Integer b) { + return "5"; + } + + public String getWork() { + return "child"; + } + + public Boolean getBooValue() { + return true; + } + + public void setBooValue(Boolean booValue) { + this.booValue = booValue; + } + + private int getMethod1(int a, int b) { + return a + b; + } + + private long getMethod11(long a, int b) { + return a + b; + } + + private long getMethod12(Boolean a, int b) { + return b; + } + + @Override + public int getAge() { + return age; + } + + @Override + public void setAge(int age) { + this.age = age; + } + + @Override + public Child getParentOwn() { + return new Child(); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child1.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child1.java new file mode 100644 index 0000000..2953a9e --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child1.java @@ -0,0 +1,28 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class Child1 extends Parent { + private final long result; + + public Child1() { + this.result = 0L; + } + + public Child1(boolean t) { + this.result = 0L; + } + + public Child1(long a, int b) { + this.result = a + b + 1; + } + + public boolean getMethod3(boolean t) { + return t; + } + + public int getMethod4(Object s, boolean t) { + return 2; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child10.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child10.java new file mode 100644 index 0000000..d62fa74 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child10.java @@ -0,0 +1,30 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class Child10 { + public void setAA(int a) { + System.out.println("setAA"); + } + + public boolean setA(int a) { + return true; + } + + public Integer setAAA(Integer... a) { + int s = 0; + for (int i : a) { + s += i; + } + return s; + } + + public Integer setAAAA(String a, Integer... b) { + int s = 0; + for (int i : b) { + s += i; + } + return s; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child2.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child2.java new file mode 100644 index 0000000..49c102b --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child2.java @@ -0,0 +1,28 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class Child2 extends Parent { + private final long result; + + public Child2() { + this.result = 0L; + } + + public Child2(boolean t) { + this.result = 0L; + } + + public Child2(Object a, boolean b) { + this.result = 1; + } + + public boolean getMethod3(boolean t) { + return t; + } + + public int getMethod4(Object s, Boolean t) { + return 2; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child3.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child3.java new file mode 100644 index 0000000..289edd8 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child3.java @@ -0,0 +1,29 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class Child3 extends Parent { + private final long result; + + public Child3() { + this.result = 0L; + } + + public Child3(Parent t) { + this.result = 0L; + } + + public Child3(Object[] t) { + this.result = 0L; + } + + public int getMethod5(Parent t) { + return t.getAge(); + } + + public int getMethod6(Object[] obj) { + return 10; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child4.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child4.java new file mode 100644 index 0000000..fba08c4 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child4.java @@ -0,0 +1,21 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class Child4 extends Parent { + private final long result; + + public Child4() { + this.result = 0L; + } + + public Child4(int t) { + this.result = 0L; + } + + public int getMethod7(int t) { + return t; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child5.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child5.java new file mode 100644 index 0000000..dec2df9 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child5.java @@ -0,0 +1,21 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class Child5 extends Parent { + private final long result; + + public Child5() { + this.result = 0L; + } + + public Child5(double t) { + this.result = 0L; + } + + public double getMethod8(double t) { + return t; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child6.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child6.java new file mode 100644 index 0000000..1583510 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child6.java @@ -0,0 +1,32 @@ +package com.alibaba.qlexpress4.test.property; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Author: TaoKan + */ +public class Child6 extends Parent { + private final long result; + + public Child6() { + this.result = 0L; + } + + public Child6(double t) { + this.result = 0L; + } + + public Child6(BigInteger bigInteger) { + this.result = 0L; + } + + public int getMethod9(BigInteger t) { + return t.intValue(); + } + + public BigDecimal getMethod10(double t) { + return new BigDecimal(String.valueOf(t)); + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child7.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child7.java new file mode 100644 index 0000000..11180cf --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child7.java @@ -0,0 +1,24 @@ +package com.alibaba.qlexpress4.test.property; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +/** + * Author: TaoKan + */ +public class Child7 { + @QLAlias("测试静态字段") + public static final int t = 8; + + @QLAlias("测试字段") + public int ts = 9; + + @QLAlias("测试方法") + public int getSide() { + return 10; + } + + @QLAlias("测试静态方法") + public static int getSii() { + return 11; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Child9.java b/src/test/java/com/alibaba/qlexpress4/test/property/Child9.java new file mode 100644 index 0000000..a3ea764 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Child9.java @@ -0,0 +1,29 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class Child9 { + public Child9() { + + } + + public Child9(int a, String... args) { + } + + public String addField(int a, String... args) { + return "1"; + }; + + public String addField1(Object... args) { + return "1"; + }; + + public String addField2(Object s, Object... args) { + return "1"; + }; + + public String addField3(Object s, Integer... args) { + return "1"; + }; +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/NumberConstructor.java b/src/test/java/com/alibaba/qlexpress4/test/property/NumberConstructor.java new file mode 100644 index 0000000..9345945 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/NumberConstructor.java @@ -0,0 +1,31 @@ +package com.alibaba.qlexpress4.test.property; + +import java.math.BigDecimal; + +/** + * Author: TaoKan + */ +public class NumberConstructor { + + private int flag; + + public NumberConstructor(double a) { + this.flag = 0; + } + + public NumberConstructor(Number a) { + this.flag = 1; + } + + public NumberConstructor(BigDecimal a) { + this.flag = 2; + } + + public NumberConstructor(String a) { + this.flag = 3; + } + + public int getFlag() { + return flag; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Parent.java b/src/test/java/com/alibaba/qlexpress4/test/property/Parent.java new file mode 100644 index 0000000..91388fc --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Parent.java @@ -0,0 +1,113 @@ +package com.alibaba.qlexpress4.test.property; + +import com.alibaba.qlexpress4.annotation.QLAlias; +import com.alibaba.qlexpress4.runtime.Value; + +/** + * Author: TaoKan + */ +public class Parent implements Value { + private final long result; + + public static final String staticPublic = "staticPublic"; + + private static final String staticPrivate = "staticPrivate"; + + private static final String staticGet = "staticGet"; + + public static String staticSet = "staticSet"; + + private static String staticSetPrivate = "staticSetPrivate"; + + private static final String staticFinal = "staticFinal"; + + public String sex = "man"; + + public int lockStatus; + + public Integer lockStatus2; + + public Parent() { + this.result = 0L; + } + + public Parent(int age) { + this.age = age; + this.result = 0L; + } + + public Parent(int a, int b) { + this.result = a + b; + } + + @QLAlias("生日") + public String birth = "2022-01-01"; + + private int age; + + private String name = "example"; + + public static String getStaticGet() { + return "staticGet1"; + } + + public static String getStaticGetParam(Integer a) { + return staticGet; + } + + public static String findStatic() { + return "static"; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getWork() { + return "parent"; + } + + public Parent getParentOwn() { + return new Parent(); + } + + private String getMethod1(int a, int b) { + return "a"; + } + + public String getBirth() { + return birth; + } + + public void setBirth(String birth) { + this.birth = birth; + } + + @Override + public Object get() { + return this; + } + + // public String getMethod2(long a,long b){return "b";} + + public String getMethod2(long a) { + return "b"; + } + + public String getMethod2(int b) { + return "b"; + } + + private long getMethod11(int a, int b) { + return a + b + 1; + } + + private long getMethod12(int a, int b) { + return a + b; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/ParentClass.java b/src/test/java/com/alibaba/qlexpress4/test/property/ParentClass.java new file mode 100644 index 0000000..4a01376 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/ParentClass.java @@ -0,0 +1,15 @@ +package com.alibaba.qlexpress4.test.property; + +import com.alibaba.qlexpress4.runtime.Value; + +/** + * Author: TaoKan + */ +public class ParentClass implements Value { + @Override + public Object get() { + Parent parent = new Parent(); + parent.setAge(35); + return parent.getClass(); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/ParentParameters.java b/src/test/java/com/alibaba/qlexpress4/test/property/ParentParameters.java new file mode 100644 index 0000000..311b370 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/ParentParameters.java @@ -0,0 +1,29 @@ +package com.alibaba.qlexpress4.test.property; + +import com.alibaba.qlexpress4.runtime.Parameters; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.data.DataValue; + +import java.util.ArrayList; +import java.util.List; + +/** + * Author: TaoKan + */ +public class ParentParameters implements Parameters { + public List values = new ArrayList<>(); + + @Override + public Value get(int i) { + return values.get(i); + } + + @Override + public int size() { + return values.size(); + } + + public void push(Object s) { + values.add(new DataValue(s)); + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/Sample.java b/src/test/java/com/alibaba/qlexpress4/test/property/Sample.java new file mode 100644 index 0000000..892d7be --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/Sample.java @@ -0,0 +1,22 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class Sample { + + private int count; + + public Sample(int count) { + this.count = count; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/SampleEnum.java b/src/test/java/com/alibaba/qlexpress4/test/property/SampleEnum.java new file mode 100644 index 0000000..d9bd289 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/SampleEnum.java @@ -0,0 +1,12 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public enum SampleEnum { + NORMAL, UN_SUPPORT; + + public int testField = 10; + + public static int testStaticField = 1000; +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/SampleForPrivate.java b/src/test/java/com/alibaba/qlexpress4/test/property/SampleForPrivate.java new file mode 100644 index 0000000..905f9db --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/SampleForPrivate.java @@ -0,0 +1,14 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class SampleForPrivate { + + private int count; + + public SampleForPrivate(int count) { + this.count = count; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/SampleSet.java b/src/test/java/com/alibaba/qlexpress4/test/property/SampleSet.java new file mode 100644 index 0000000..9627010 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/SampleSet.java @@ -0,0 +1,8 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public class SampleSet { + public int count; +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/SomeInter.java b/src/test/java/com/alibaba/qlexpress4/test/property/SomeInter.java new file mode 100644 index 0000000..022a186 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/SomeInter.java @@ -0,0 +1,7 @@ +package com.alibaba.qlexpress4.test.property; + +public interface SomeInter { + + String INTER_CONST_1 = "test1"; + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/property/TestEnum.java b/src/test/java/com/alibaba/qlexpress4/test/property/TestEnum.java new file mode 100644 index 0000000..64b91b4 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/property/TestEnum.java @@ -0,0 +1,19 @@ +package com.alibaba.qlexpress4.test.property; + +/** + * Author: TaoKan + */ +public enum TestEnum { + SKT(-1), KSY(1); + + private final int value; + + TestEnum(int i) { + this.value = i; + } + + public int getValue() { + return value; + } + +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/qlalias/Order.java b/src/test/java/com/alibaba/qlexpress4/test/qlalias/Order.java new file mode 100644 index 0000000..062667e --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/qlalias/Order.java @@ -0,0 +1,29 @@ +package com.alibaba.qlexpress4.test.qlalias; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +@QLAlias("订单") +public class Order { + + @QLAlias("订单号") + private String orderNum; + + @QLAlias("金额") + private int amount; + + public String getOrderNum() { + return orderNum; + } + + public void setOrderNum(String orderNum) { + this.orderNum = orderNum; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/qlalias/Patient.java b/src/test/java/com/alibaba/qlexpress4/test/qlalias/Patient.java new file mode 100644 index 0000000..f15ee4b --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/qlalias/Patient.java @@ -0,0 +1,36 @@ +package com.alibaba.qlexpress4.test.qlalias; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +@QLAlias("患者") +public class Patient extends Person { + + @QLAlias("级别") + private String level = "高危"; + + @QLAlias("患者姓名") + private String name; + + @QLAlias("获取患者年龄") + public int getAge() { + return 2021 - Integer.parseInt(this.getBirth().substring(0, 4)); + } + + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/qlalias/Person.java b/src/test/java/com/alibaba/qlexpress4/test/qlalias/Person.java new file mode 100644 index 0000000..3724b46 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/qlalias/Person.java @@ -0,0 +1,44 @@ +package com.alibaba.qlexpress4.test.qlalias; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +public class Person { + @QLAlias({"出生年月", "生日"}) + private String birth; + + @QLAlias("姓名") + private String name; + + @QLAlias("性别") + private String sex; + + @QLAlias("获取年龄") + public int getAge() { + return 2021 - Integer.parseInt(this.birth.substring(0, 4)); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + } + + @QLAlias({"出生年月", "生日"}) + public String getBirth() { + return birth; + } + + public void setBirth(String birth) { + this.birth = birth; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/qlalias/User.java b/src/test/java/com/alibaba/qlexpress4/test/qlalias/User.java new file mode 100644 index 0000000..f4979d2 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/qlalias/User.java @@ -0,0 +1,29 @@ +package com.alibaba.qlexpress4.test.qlalias; + +import com.alibaba.qlexpress4.annotation.QLAlias; + +@QLAlias("用户") +public class User { + + @QLAlias("是vip") + private boolean vip; + + @QLAlias("用户名") + private String name; + + public boolean isVip() { + return vip; + } + + public void setVip(boolean vip) { + this.vip = vip; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/stream/STObject.java b/src/test/java/com/alibaba/qlexpress4/test/stream/STObject.java new file mode 100644 index 0000000..e94e326 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/stream/STObject.java @@ -0,0 +1,14 @@ +package com.alibaba.qlexpress4.test.stream; + +public class STObject { + + private final String payload; + + public STObject(String payload) { + this.payload = payload; + } + + public String getPayload() { + return payload; + } +} diff --git a/src/test/java/com/alibaba/qlexpress4/test/trycatch/SampleException.java b/src/test/java/com/alibaba/qlexpress4/test/trycatch/SampleException.java new file mode 100644 index 0000000..dca16cc --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/trycatch/SampleException.java @@ -0,0 +1,12 @@ +package com.alibaba.qlexpress4.test.trycatch; + +/** + * Author: DQinYuan + */ +public class SampleException { + + public void willThrow() { + throw new IllegalStateException(); + } + +} diff --git a/src/test/resources/perf/complexDataProcessing.ql b/src/test/resources/perf/complexDataProcessing.ql new file mode 100644 index 0000000..b9bb928 --- /dev/null +++ b/src/test/resources/perf/complexDataProcessing.ql @@ -0,0 +1,514 @@ +import java.lang.String; +import java.lang.Integer; +import java.lang.Long; +import java.lang.Double; +import java.lang.Boolean; +import java.lang.Math; +import java.lang.System; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.HashSet; +import java.util.TreeSet; +import java.util.Collections; +import java.util.Comparator; +import java.util.Random; +import java.util.Date; +import java.util.UUID; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Complex Data Processing Engine + * This script demonstrates complex business logic with multiple functions, + * nested conditions, loops, and extensive data transformations. + */ + +// Configuration constants +Integer MAX_RETRY_COUNT = 5; +Integer BATCH_SIZE = 100; +Double THRESHOLD_RATIO = 0.85; +Long TIMEOUT_MILLIS = 30000L; +Boolean ENABLE_CACHE = true; +Boolean STRICT_MODE = false; + +/** + * Calculate weighted score based on multiple factors + */ +function calculateWeightedScore(Map dataPoint, Map weights) { + Double totalScore = 0.0; + Double totalWeight = 0.0; + + if (dataPoint == null || weights == null) { + return 0.0; + } + + for (String key : weights.keySet()) { + if (dataPoint.containsKey(key)) { + Double value = (Double) dataPoint.get(key); + Double weight = (Double) weights.get(key); + + if (value != null && weight != null && weight > 0) { + totalScore = totalScore + (value * weight); + totalWeight = totalWeight + weight; + } + } + } + + if (totalWeight > 0) { + return totalScore / totalWeight; + } else { + return 0.0; + } +} + +/** + * Validate data quality and completeness + */ +function validateDataQuality(List records, Map schema) { + Integer validCount = 0; + Integer invalidCount = 0; + List errorMessages = new ArrayList(); + + for (Integer i = 0; i < records.size(); i++) { + Map record = (Map) records.get(i); + Boolean isValid = true; + + // Check required fields + for (String fieldName : schema.keySet()) { + Map fieldSpec = (Map) schema.get(fieldName); + Boolean required = (Boolean) fieldSpec.get("required"); + + if (required != null && required) { + if (!record.containsKey(fieldName) || record.get(fieldName) == null) { + isValid = false; + errorMessages.add("Record " + i + " missing required field: " + fieldName); + } + } + + // Type validation + if (record.containsKey(fieldName) && record.get(fieldName) != null) { + String expectedType = (String) fieldSpec.get("type"); + Object value = record.get(fieldName); + + if ("number".equals(expectedType) && !(value instanceof Number)) { + isValid = false; + errorMessages.add("Record " + i + " field " + fieldName + " has wrong type"); + } else if ("string".equals(expectedType) && !(value instanceof String)) { + isValid = false; + errorMessages.add("Record " + i + " field " + fieldName + " has wrong type"); + } + } + } + + if (isValid) { + validCount = validCount + 1; + } else { + invalidCount = invalidCount + 1; + } + } + + Map result = new HashMap(); + result.put("validCount", validCount); + result.put("invalidCount", invalidCount); + result.put("totalCount", records.size()); + result.put("qualityRatio", (Double) validCount / records.size()); + result.put("errors", errorMessages); + + return result; +} + +/** + * Transform and normalize data + */ +function transformData(List rawData, Map transformRules) { + List transformedData = new ArrayList(); + + for (Integer idx = 0; idx < rawData.size(); idx++) { + Map rawRecord = (Map) rawData.get(idx); + Map transformedRecord = new HashMap(); + + for (String sourceField : transformRules.keySet()) { + Map rule = (Map) transformRules.get(sourceField); + String targetField = (String) rule.get("target"); + String operation = (String) rule.get("operation"); + + if (rawRecord.containsKey(sourceField)) { + Object rawValue = rawRecord.get(sourceField); + Object transformedValue = rawValue; + + if ("uppercase".equals(operation) && rawValue instanceof String) { + transformedValue = ((String) rawValue).toUpperCase(); + } else if ("lowercase".equals(operation) && rawValue instanceof String) { + transformedValue = ((String) rawValue).toLowerCase(); + } else if ("multiply".equals(operation) && rawValue instanceof Number) { + Double multiplier = (Double) rule.get("factor"); + transformedValue = ((Number) rawValue).doubleValue() * multiplier; + } else if ("round".equals(operation) && rawValue instanceof Number) { + transformedValue = Math.round(((Number) rawValue).doubleValue()); + } else if ("abs".equals(operation) && rawValue instanceof Number) { + transformedValue = Math.abs(((Number) rawValue).doubleValue()); + } + + transformedRecord.put(targetField, transformedValue); + } + } + + // Copy unmapped fields + for (String field : rawRecord.keySet()) { + if (!transformedRecord.containsKey(field)) { + transformedRecord.put(field, rawRecord.get(field)); + } + } + + transformedData.add(transformedRecord); + } + + return transformedData; +} + +/** + * Aggregate data with grouping and statistics + */ +function aggregateData(List data, List groupByFields, Map aggregations) { + Map groups = new HashMap(); + + // Group data + for (Integer i = 0; i < data.size(); i++) { + Map record = (Map) data.get(i); + String groupKey = ""; + + for (Integer j = 0; j < groupByFields.size(); j++) { + String field = (String) groupByFields.get(j); + Object value = record.get(field); + groupKey = groupKey + field + ":" + value + "|"; + } + + if (!groups.containsKey(groupKey)) { + groups.put(groupKey, new ArrayList()); + } + + List groupRecords = (List) groups.get(groupKey); + groupRecords.add(record); + } + + // Calculate aggregations + List results = new ArrayList(); + + for (String groupKey : groups.keySet()) { + List groupRecords = (List) groups.get(groupKey); + Map aggResult = new HashMap(); + + // Add group key fields + String[] keyParts = groupKey.split("\\|"); + for (Integer k = 0; k < keyParts.length - 1; k++) { + String[] fieldValue = keyParts[k].split(":"); + if (fieldValue.length == 2) { + aggResult.put(fieldValue[0], fieldValue[1]); + } + } + + // Calculate aggregations + for (String aggField : aggregations.keySet()) { + Map aggSpec = (Map) aggregations.get(aggField); + String operation = (String) aggSpec.get("operation"); + + if ("count".equals(operation)) { + aggResult.put(aggField + "_count", groupRecords.size()); + } else if ("sum".equals(operation)) { + Double sum = 0.0; + for (Integer m = 0; m < groupRecords.size(); m++) { + Map rec = (Map) groupRecords.get(m); + Object val = rec.get(aggField); + if (val instanceof Number) { + sum = sum + ((Number) val).doubleValue(); + } + } + aggResult.put(aggField + "_sum", sum); + } else if ("avg".equals(operation)) { + Double sum = 0.0; + Integer count = 0; + for (Integer m = 0; m < groupRecords.size(); m++) { + Map rec = (Map) groupRecords.get(m); + Object val = rec.get(aggField); + if (val instanceof Number) { + sum = sum + ((Number) val).doubleValue(); + count = count + 1; + } + } + aggResult.put(aggField + "_avg", count > 0 ? sum / count : 0.0); + } else if ("max".equals(operation)) { + Double max = null; + for (Integer m = 0; m < groupRecords.size(); m++) { + Map rec = (Map) groupRecords.get(m); + Object val = rec.get(aggField); + if (val instanceof Number) { + Double numVal = ((Number) val).doubleValue(); + if (max == null || numVal > max) { + max = numVal; + } + } + } + aggResult.put(aggField + "_max", max); + } else if ("min".equals(operation)) { + Double min = null; + for (Integer m = 0; m < groupRecords.size(); m++) { + Map rec = (Map) groupRecords.get(m); + Object val = rec.get(aggField); + if (val instanceof Number) { + Double numVal = ((Number) val).doubleValue(); + if (min == null || numVal < min) { + min = numVal; + } + } + } + aggResult.put(aggField + "_min", min); + } + } + + results.add(aggResult); + } + + return results; +} + +/** + * Filter data based on complex conditions + */ +function filterData(List data, Map filterConditions) { + List filteredData = new ArrayList(); + + for (Integer i = 0; i < data.size(); i++) { + Map record = (Map) data.get(i); + Boolean matchesAllConditions = true; + + for (String field : filterConditions.keySet()) { + Map condition = (Map) filterConditions.get(field); + String operator = (String) condition.get("operator"); + Object expectedValue = condition.get("value"); + Object actualValue = record.get(field); + + Boolean matches = false; + + if ("equals".equals(operator)) { + matches = (actualValue != null && actualValue.equals(expectedValue)); + } else if ("notEquals".equals(operator)) { + matches = (actualValue == null || !actualValue.equals(expectedValue)); + } else if ("greaterThan".equals(operator) && actualValue instanceof Number && expectedValue instanceof Number) { + matches = ((Number) actualValue).doubleValue() > ((Number) expectedValue).doubleValue(); + } else if ("lessThan".equals(operator) && actualValue instanceof Number && expectedValue instanceof Number) { + matches = ((Number) actualValue).doubleValue() < ((Number) expectedValue).doubleValue(); + } else if ("greaterOrEqual".equals(operator) && actualValue instanceof Number && expectedValue instanceof Number) { + matches = ((Number) actualValue).doubleValue() >= ((Number) expectedValue).doubleValue(); + } else if ("lessOrEqual".equals(operator) && actualValue instanceof Number && expectedValue instanceof Number) { + matches = ((Number) actualValue).doubleValue() <= ((Number) expectedValue).doubleValue(); + } else if ("contains".equals(operator) && actualValue instanceof String && expectedValue instanceof String) { + matches = ((String) actualValue).contains((String) expectedValue); + } else if ("startsWith".equals(operator) && actualValue instanceof String && expectedValue instanceof String) { + matches = ((String) actualValue).startsWith((String) expectedValue); + } else if ("endsWith".equals(operator) && actualValue instanceof String && expectedValue instanceof String) { + matches = ((String) actualValue).endsWith((String) expectedValue); + } else if ("in".equals(operator) && expectedValue instanceof List) { + List valueList = (List) expectedValue; + matches = valueList.contains(actualValue); + } + + if (!matches) { + matchesAllConditions = false; + break; + } + } + + if (matchesAllConditions) { + filteredData.add(record); + } + } + + return filteredData; +} + +/** + * Join two datasets + */ +function joinDatasets(List leftData, List rightData, String leftKey, String rightKey, String joinType) { + List joinedData = new ArrayList(); + + // Build index for right dataset + Map rightIndex = new HashMap(); + for (Integer i = 0; i < rightData.size(); i++) { + Map rightRecord = (Map) rightData.get(i); + Object keyValue = rightRecord.get(rightKey); + + if (!rightIndex.containsKey(keyValue)) { + rightIndex.put(keyValue, new ArrayList()); + } + + List rightRecords = (List) rightIndex.get(keyValue); + rightRecords.add(rightRecord); + } + + // Perform join + for (Integer i = 0; i < leftData.size(); i++) { + Map leftRecord = (Map) leftData.get(i); + Object keyValue = leftRecord.get(leftKey); + + if (rightIndex.containsKey(keyValue)) { + List matchingRightRecords = (List) rightIndex.get(keyValue); + + for (Integer j = 0; j < matchingRightRecords.size(); j++) { + Map rightRecord = (Map) matchingRightRecords.get(j); + Map joinedRecord = new HashMap(); + + // Add left record fields + for (String field : leftRecord.keySet()) { + joinedRecord.put("left_" + field, leftRecord.get(field)); + } + + // Add right record fields + for (String field : rightRecord.keySet()) { + joinedRecord.put("right_" + field, rightRecord.get(field)); + } + + joinedData.add(joinedRecord); + } + } else if ("left".equals(joinType) || "outer".equals(joinType)) { + Map joinedRecord = new HashMap(); + + // Add left record fields + for (String field : leftRecord.keySet()) { + joinedRecord.put("left_" + field, leftRecord.get(field)); + } + + joinedData.add(joinedRecord); + } + } + + return joinedData; +} + +/** + * Sort data by multiple fields + */ +function sortData(List data, List sortFields, List sortOrders) { + // Simple bubble sort implementation for demonstration + List sortedData = new ArrayList(); + for (Integer i = 0; i < data.size(); i++) { + sortedData.add(data.get(i)); + } + + for (Integer i = 0; i < sortedData.size() - 1; i++) { + for (Integer j = 0; j < sortedData.size() - i - 1; j++) { + Map record1 = (Map) sortedData.get(j); + Map record2 = (Map) sortedData.get(j + 1); + + Boolean shouldSwap = false; + + for (Integer k = 0; k < sortFields.size(); k++) { + String field = (String) sortFields.get(k); + String order = (String) sortOrders.get(k); + + Object value1 = record1.get(field); + Object value2 = record2.get(field); + + if (value1 == null && value2 != null) { + shouldSwap = "asc".equals(order); + break; + } else if (value1 != null && value2 == null) { + shouldSwap = "desc".equals(order); + break; + } else if (value1 != null && value2 != null) { + if (value1 instanceof Number && value2 instanceof Number) { + Double num1 = ((Number) value1).doubleValue(); + Double num2 = ((Number) value2).doubleValue(); + + if (num1 < num2) { + shouldSwap = "desc".equals(order); + break; + } else if (num1 > num2) { + shouldSwap = "asc".equals(order); + break; + } + } else if (value1 instanceof String && value2 instanceof String) { + Integer comparison = ((String) value1).compareTo((String) value2); + + if (comparison < 0) { + shouldSwap = "desc".equals(order); + break; + } else if (comparison > 0) { + shouldSwap = "asc".equals(order); + break; + } + } + } + } + + if (shouldSwap) { + Object temp = sortedData.get(j); + sortedData.set(j, sortedData.get(j + 1)); + sortedData.set(j + 1, temp); + } + } + } + + return sortedData; +} + +// Main processing logic +Map processingConfig = new HashMap(); +processingConfig.put("enableValidation", true); +processingConfig.put("enableTransformation", true); +processingConfig.put("enableAggregation", true); +processingConfig.put("enableFiltering", true); + +List sampleData = new ArrayList(); +Map weights = new HashMap(); +weights.put("quality", 0.3); +weights.put("speed", 0.5); +weights.put("cost", 0.2); + +// Create sample validation schema +Map schema = new HashMap(); +Map field1 = new HashMap(); +field1.put("required", true); +field1.put("type", "string"); +schema.put("id", field1); + +Map field2 = new HashMap(); +field2.put("required", true); +field2.put("type", "number"); +schema.put("value", field2); + +// Process data in batches +Integer totalProcessed = 0; +Integer totalErrors = 0; +Double avgScore = 0.0; + +String status = "initialized"; +Long startTime = System.currentTimeMillis(); + +if (processingConfig.get("enableValidation") != null && (Boolean) processingConfig.get("enableValidation")) { + status = "validating"; +} + +if (processingConfig.get("enableTransformation") != null && (Boolean) processingConfig.get("enableTransformation")) { + status = "transforming"; +} + +Long endTime = System.currentTimeMillis(); +Long duration = endTime - startTime; + +Map finalResult = new HashMap(); +finalResult.put("status", status); +finalResult.put("totalProcessed", totalProcessed); +finalResult.put("totalErrors", totalErrors); +finalResult.put("avgScore", avgScore); +finalResult.put("duration", duration); +finalResult.put("timestamp", endTime); + +return finalResult; diff --git a/src/test/resources/test-plugins/build-plugin.sh b/src/test/resources/test-plugins/build-plugin.sh new file mode 100644 index 0000000..b70ef07 --- /dev/null +++ b/src/test/resources/test-plugins/build-plugin.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Create temporary directory +mkdir -p target/test-plugin-classes + +# Compile test plugin classes +javac -cp "$(mvn dependency:build-classpath -Dmdep.outputFile=/dev/stdout -q):target/classes" \ + -d target/test-plugin-classes \ + src/test/java/com/alibaba/qlexpress4/pf4j/TestPluginInterface.java \ + src/test/java/com/alibaba/qlexpress4/pf4j/TestPluginImpl.java + +# Copy plugin configuration file +cp src/test/resources/test-plugins/plugin.properties target/test-plugin-classes/ + +# Package into jar +jar cf src/test/resources/test-plugins/test-plugin.jar -C target/test-plugin-classes . + +echo "Plugin jar created: src/test/resources/test-plugins/test-plugin.jar" \ No newline at end of file diff --git a/src/test/resources/test-plugins/plugin.properties b/src/test/resources/test-plugins/plugin.properties new file mode 100644 index 0000000..3ffa0aa --- /dev/null +++ b/src/test/resources/test-plugins/plugin.properties @@ -0,0 +1,7 @@ +plugin.id=test-plugin +plugin.version=1.0.0 +plugin.class=com.alibaba.qlexpress4.pf4j.TestPluginImpl +plugin.description=Test plugin for QLExpress PF4J integration +plugin.provider=QLExpress Test +plugin.dependencies= +plugin.requires=* \ No newline at end of file diff --git a/src/test/resources/test-plugins/test-plugin.jar b/src/test/resources/test-plugins/test-plugin.jar new file mode 100644 index 0000000..6929465 Binary files /dev/null and b/src/test/resources/test-plugins/test-plugin.jar differ diff --git a/src/test/resources/testsuite/independent/array/array_index_out_of_bound.ql b/src/test/resources/testsuite/independent/array/array_index_out_of_bound.ql new file mode 100644 index 0000000..fe30c50 --- /dev/null +++ b/src/test/resources/testsuite/independent/array/array_index_out_of_bound.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "INDEX_OUT_BOUND" +} +*/ +a = []; +a[1] \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/array/array_literal.ql b/src/test/resources/testsuite/independent/array/array_literal.ql new file mode 100644 index 0000000..9c3463f --- /dev/null +++ b/src/test/resources/testsuite/independent/array/array_literal.ql @@ -0,0 +1,6 @@ +a = [1,2,3, "123"]; +assert(a == [1,2,3, "123"]); +assert(a != [1,2,3, "125"]); +assert(a[0] == 1 && a[3] == "123"); +assert(a.length == 4); +assert(a[-1] == a[3]); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/array/float_index.ql b/src/test/resources/testsuite/independent/array/float_index.ql new file mode 100644 index 0000000..cea9dae --- /dev/null +++ b/src/test/resources/testsuite/independent/array/float_index.ql @@ -0,0 +1,2 @@ +a = [1,2,3,4]; +assert(a[2.8] == 3); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/array/invalid_index.ql b/src/test/resources/testsuite/independent/array/invalid_index.ql new file mode 100644 index 0000000..bf5f94d --- /dev/null +++ b/src/test/resources/testsuite/independent/array/invalid_index.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "INVALID_INDEX" +} +*/ +a = [1]; +a["aaa"] = 2; \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/array/max_arr_len.ql b/src/test/resources/testsuite/independent/array/max_arr_len.ql new file mode 100644 index 0000000..3230505 --- /dev/null +++ b/src/test/resources/testsuite/independent/array/max_arr_len.ql @@ -0,0 +1,15 @@ +/* +{ + "qlOptions": QLOptions.builder().maxArrLength(10) +} +*/ + +try { + a = new int[10] + a = new int[1][2][10][10][9] +} catch(o) { + assert(false); +} + +assertErrorCode(() -> new int[11], "EXCEED_MAX_ARR_LENGTH") +assertErrorCode(() -> new int[1][13][3], "EXCEED_MAX_ARR_LENGTH") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/array/miss_comma_between_elements.ql b/src/test/resources/testsuite/independent/array/miss_comma_between_elements.ql new file mode 100644 index 0000000..a50297c --- /dev/null +++ b/src/test/resources/testsuite/independent/array/miss_comma_between_elements.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +[123 334] \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/array/no_rbrack_to_match.ql b/src/test/resources/testsuite/independent/array/no_rbrack_to_match.ql new file mode 100644 index 0000000..81517a2 --- /dev/null +++ b/src/test/resources/testsuite/independent/array/no_rbrack_to_match.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +a = [1223,34,34 \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/array/slice.ql b/src/test/resources/testsuite/independent/array/slice.ql new file mode 100644 index 0000000..3173344 --- /dev/null +++ b/src/test/resources/testsuite/independent/array/slice.ql @@ -0,0 +1,7 @@ +a = [1,2,3,4,5,6]; +assert(a[3:] == [4,5,6]); +assert(a[:2] == [1,2]); +assert(a[2:4] == [3,4]); +assert(a[4:10] == [5, 6]); +assert(a[-88:100] == [1,2,3,4,5,6]) +assert(a[3:-1] == [4,5]) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/array/unindexable.ql b/src/test/resources/testsuite/independent/array/unindexable.ql new file mode 100644 index 0000000..152a4c1 --- /dev/null +++ b/src/test/resources/testsuite/independent/array/unindexable.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "NONINDEXABLE_OBJECT" +} +*/ +a = new HashSet(); +a[1] \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/avoidnullpointer/avoid_null_pointer.ql b/src/test/resources/testsuite/independent/avoidnullpointer/avoid_null_pointer.ql new file mode 100644 index 0000000..bc6ffd9 --- /dev/null +++ b/src/test/resources/testsuite/independent/avoidnullpointer/avoid_null_pointer.ql @@ -0,0 +1,15 @@ +/* +{ + "qlOptions": QLOptions.builder().avoidNullPointer(true) +} +*/ +assert(a.b == null); +assert(a.b.c == null); +assert(a.b() == null); +assert(a.b().c.d() == null); +assert(a::b == null); +assert(a.b.c.mm() == null); +assert(a.b.c.mm()() == null); +assert(mmm() == null) +assert(a.n.c[2]==null) +assert(a.n.c[1:4]==null) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/avoidnullpointer/can_not_find_function.ql b/src/test/resources/testsuite/independent/avoidnullpointer/can_not_find_function.ql new file mode 100644 index 0000000..69feaa8 --- /dev/null +++ b/src/test/resources/testsuite/independent/avoidnullpointer/can_not_find_function.ql @@ -0,0 +1,4 @@ +/*{ + "errCode": "FUNCTION_NOT_FOUND" +}*/ +mmm() \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/avoidnullpointer/get_from_null.ql b/src/test/resources/testsuite/independent/avoidnullpointer/get_from_null.ql new file mode 100644 index 0000000..537d6f8 --- /dev/null +++ b/src/test/resources/testsuite/independent/avoidnullpointer/get_from_null.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "NULL_FIELD_ACCESS" +} +*/ +a.b \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/avoidnullpointer/get_method_from_null.ql b/src/test/resources/testsuite/independent/avoidnullpointer/get_method_from_null.ql new file mode 100644 index 0000000..4cf51c6 --- /dev/null +++ b/src/test/resources/testsuite/independent/avoidnullpointer/get_method_from_null.ql @@ -0,0 +1,4 @@ +/*{ + "errCode": "NULL_METHOD_ACCESS" +}*/ +a::b \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/block/block_as_expr.ql b/src/test/resources/testsuite/independent/block/block_as_expr.ql new file mode 100644 index 0000000..679fb87 --- /dev/null +++ b/src/test/resources/testsuite/independent/block/block_as_expr.ql @@ -0,0 +1,19 @@ +a = { + 1 + 1 +} + 1; +assert(a == 3); +b = { + String c = 'ccc'; + String d = 'ddd'; + c + '-' + d +}; +assert(b == 'ccc-ddd'); +f = { + int e = 10; + if (a > 5) { + a + e + } else { + a * 2 + } +}; +assert(f == 6); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/block/block_at_if.ql b/src/test/resources/testsuite/independent/block/block_at_if.ql new file mode 100644 index 0000000..a6cf646 --- /dev/null +++ b/src/test/resources/testsuite/independent/block/block_at_if.ql @@ -0,0 +1,12 @@ +a = 1; +b = if (a < 5) { + { + a + 10 + } +} else { + { + a * 10 + } +}; +assert(b == 11); + diff --git a/src/test/resources/testsuite/independent/block/lambda_with_block.ql b/src/test/resources/testsuite/independent/block/lambda_with_block.ql new file mode 100644 index 0000000..04bbd47 --- /dev/null +++ b/src/test/resources/testsuite/independent/block/lambda_with_block.ql @@ -0,0 +1,10 @@ +f = (x) -> { + int e = 10; + if (x > 5) { + x + e + } else { + x * 2 + } +}; +assert(f(6) == 16); +assert(f(3) == 6); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/block/missing_rbrace.ql b/src/test/resources/testsuite/independent/block/missing_rbrace.ql new file mode 100644 index 0000000..d1f9391 --- /dev/null +++ b/src/test/resources/testsuite/independent/block/missing_rbrace.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +{ + 1+1 diff --git a/src/test/resources/testsuite/independent/block/return_at_block.ql b/src/test/resources/testsuite/independent/block/return_at_block.ql new file mode 100644 index 0000000..18fd722 --- /dev/null +++ b/src/test/resources/testsuite/independent/block/return_at_block.ql @@ -0,0 +1,17 @@ +function returnAtBlock(a) { + int i = if (a > 10) { + { + return 100; + } + } else { + if (a < 5) { + {return 1000;} + } + {return 101;} + }; + 10000 +} + +assert(returnAtBlock(11) == 100); +assert(returnAtBlock(5) == 101); +assert(returnAtBlock(-5) == 1000) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/bool/bool_literal.ql b/src/test/resources/testsuite/independent/bool/bool_literal.ql new file mode 100644 index 0000000..594db4a --- /dev/null +++ b/src/test/resources/testsuite/independent/bool/bool_literal.ql @@ -0,0 +1,4 @@ +assert(true == true); +assert(false == false); +assert(true != false); +assert(true != "true"); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/bool/bool_literal_is_keyword.ql b/src/test/resources/testsuite/independent/bool/bool_literal_is_keyword.ql new file mode 100644 index 0000000..56ee7b6 --- /dev/null +++ b/src/test/resources/testsuite/independent/bool/bool_literal_is_keyword.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +true = 1; \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/bool/short_circuit.ql b/src/test/resources/testsuite/independent/bool/short_circuit.ql new file mode 100644 index 0000000..3e63651 --- /dev/null +++ b/src/test/resources/testsuite/independent/bool/short_circuit.ql @@ -0,0 +1,24 @@ +function a(int value, boolean b) { + a = value; + return b; +} + +c = a(1, false) && a(10, true); +assert(c == false); +assert(a == 1); + +c = a(100, true) || a(1, false); +assert(c == true); +assert(a == 100); + +d = a(1000, true) && a(10000, false); +assert(d == false); +assert(a == 10000); + +e = a(11, false) or a(111, true); +assert(e == true); +assert(a == 111); + +f = a(2, true) || a(3, false) && a(5, false); +assert(f == true); +assert(a == 2); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/cast/cast_express.ql b/src/test/resources/testsuite/independent/cast/cast_express.ql new file mode 100644 index 0000000..4aa00ca --- /dev/null +++ b/src/test/resources/testsuite/independent/cast/cast_express.ql @@ -0,0 +1,7 @@ +a = int; +assert(a == int); +b = 12L; +c = (int) b; +assert(c.class == a.class); +d = (int) 100.12d; +assert(d == 100); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/cast/null_cast.ql b/src/test/resources/testsuite/independent/cast/null_cast.ql new file mode 100644 index 0000000..8231e6e --- /dev/null +++ b/src/test/resources/testsuite/independent/cast/null_cast.ql @@ -0,0 +1,3 @@ +Integer a = null; +assert(!(boolean) a); +assert((int) a == null); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/comment/comment.ql b/src/test/resources/testsuite/independent/comment/comment.ql new file mode 100644 index 0000000..50d4bb9 --- /dev/null +++ b/src/test/resources/testsuite/independent/comment/comment.ql @@ -0,0 +1,6 @@ +// in-line comment +/* +multiline comment +*/ + +// \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql b/src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql new file mode 100644 index 0000000..0ddc0e6 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql @@ -0,0 +1,21 @@ +// list +l = [1,2,3] +assert(l[0]==1) +assert(l[-1]==3) +// Underlying data type of list is ArrayList in Java +assert(l instanceof ArrayList) +// map +m = { + "aa": 10, + "bb": { + "cc": "cc1", + "dd": "dd1" + } +} +assert(m['aa']==10) +// Underlying data type of map is ArrayList in Java +assert(m instanceof LinkedHashMap) +// empty map +emMap = {:} +emMap['haha']='huhu' +assert(emMap['haha']=='huhu') \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/dynamic_string.ql b/src/test/resources/testsuite/independent/doc/dynamic_string.ql new file mode 100644 index 0000000..63acbb1 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/dynamic_string.ql @@ -0,0 +1,12 @@ +a = 123 +assert("hello,${a-1}" == "hello,122") + +// escape $ with \$ +assert("hello,\${a-1}" == "hello,\${a-1}") + +b = "test" +assert("m xx ${ + if (b like 't%') { + 'YYY' + } +}" == "m xx YYY") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/dynamic_typing.ql b/src/test/resources/testsuite/independent/doc/dynamic_typing.ql new file mode 100644 index 0000000..7458a82 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/dynamic_typing.ql @@ -0,0 +1,8 @@ +// Dynamic Typeing +a = 1; +a = "1"; +// Static Typing +int b = 2; +// throw QLException with error code INCOMPATIBLE_ASSIGNMENT_TYPE when assign with incompatible type String +assertErrorCode(() -> b = "1", "INCOMPATIBLE_ASSIGNMENT_TYPE") + diff --git a/src/test/resources/testsuite/independent/doc/for.ql b/src/test/resources/testsuite/independent/doc/for.ql new file mode 100644 index 0000000..9240489 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/for.ql @@ -0,0 +1,5 @@ +l = []; +for (int i = 3; i < 6; i++) { + l.add(i); +} +assert(l==[3,4,5]) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/for_each.ql b/src/test/resources/testsuite/independent/doc/for_each.ql new file mode 100644 index 0000000..9065e5e --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/for_each.ql @@ -0,0 +1,8 @@ +sum = 0; +for (i: [0,1,2,3,4]) { + if (i == 2) { + continue; + } + sum += i; +} +assert(sum==8) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/function.ql b/src/test/resources/testsuite/independent/doc/function.ql new file mode 100644 index 0000000..6744785 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/function.ql @@ -0,0 +1,4 @@ +function sub(a, b) { + return a-b; +} +assert(sub(3,1)==2) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/if.ql b/src/test/resources/testsuite/independent/doc/if.ql new file mode 100644 index 0000000..d231a89 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/if.ql @@ -0,0 +1,13 @@ +a = 11; +// if ... else ... +assert(if (a >= 0 && a < 5) { + true +} else if (a >= 5 && a < 10) { + false +} else if (a >= 10 && a < 15) { + true +} == true) + +// if ... then ... else ... +r = if (a == 11) then true else false +assert(r == true) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/if_as_expr.ql b/src/test/resources/testsuite/independent/doc/if_as_expr.ql new file mode 100644 index 0000000..424dbcc --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/if_as_expr.ql @@ -0,0 +1,5 @@ +assert(if (11 == 11) { + 10 +} else { + 20 + 2 +} + 1 == 11) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/if_then.ql b/src/test/resources/testsuite/independent/doc/if_then.ql new file mode 100644 index 0000000..82a1928 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/if_then.ql @@ -0,0 +1,3 @@ +a = 11; + +assert(if (a >= 0 && a < 5) then true else false == false) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/lambda.ql b/src/test/resources/testsuite/independent/doc/lambda.ql new file mode 100644 index 0000000..07c162a --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/lambda.ql @@ -0,0 +1,4 @@ +add = (a, b) -> { + return a + b; +} +assert(add(1,2)==3) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/list_map_filter.ql b/src/test/resources/testsuite/independent/doc/list_map_filter.ql new file mode 100644 index 0000000..c8d86a4 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/list_map_filter.ql @@ -0,0 +1,3 @@ +l = ["a-111", "a-222", "b-333", "c-888"] +assert(l.filter(i -> i.startsWith("a-")) + .map(i -> i.split("-")[1]) == ["111", "222"]) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/try_catch.ql b/src/test/resources/testsuite/independent/doc/try_catch.ql new file mode 100644 index 0000000..cb836db --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/try_catch.ql @@ -0,0 +1,6 @@ +assert(try { + 100 + 1/0 +} catch(e) { + // Throw a zero-division exception + 11 +} == 11) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql b/src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql new file mode 100644 index 0000000..4a9ccfa --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql @@ -0,0 +1,6 @@ +assert(1 + try { + 100 + 1/0 +} catch(e) { + // Throw a zero-division exception + 11 +} == 12) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/while.ql b/src/test/resources/testsuite/independent/doc/while.ql new file mode 100644 index 0000000..8b2f732 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/while.ql @@ -0,0 +1,7 @@ +i = 0; +while (i < 5) { + if (++i == 2) { + break; + } +} +assert(i==2) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/extensionfunction/extension_function.ql b/src/test/resources/testsuite/independent/extensionfunction/extension_function.ql new file mode 100644 index 0000000..bdf6db2 --- /dev/null +++ b/src/test/resources/testsuite/independent/extensionfunction/extension_function.ql @@ -0,0 +1,7 @@ +a = [1,2,3,4].filter(i -> i > 2) +assert(a == [3,4]) +assert(a instanceof List) + +assert([1,2].map(i -> i+2) == a) + +assertErrorCode(() -> {'a':1}.filter(en -> en.value > 10), "METHOD_NOT_FOUND") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/break_continue.ql b/src/test/resources/testsuite/independent/for/break_continue.ql new file mode 100644 index 0000000..6197323 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/break_continue.ql @@ -0,0 +1,27 @@ +for (i = 0; i < 5; i++) { + if (i == 2) { + break; + } +} +assert(i == 2); + +sum = 0; +for (i = 0; i < 5; i++) { + if (i == 2) { + continue; + } + sum += i; +} +assert(sum == 8); + +sum = 0; +for (i = 0; i < 5; i++) { + if (i == 2) { + if (i == 2) { + continue; + } + } + sum += i; +} +assert(sum == 8); + diff --git a/src/test/resources/testsuite/independent/for/c_for.ql b/src/test/resources/testsuite/independent/for/c_for.ql new file mode 100644 index 0000000..df8e0e6 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/c_for.ql @@ -0,0 +1,19 @@ +l = []; +for (int i = 3; i < 6; i++) { + l.add(i); +} +assert(l == [3,4,5]); +assert(i == null); + +l1 = []; +for (j = 10; j > 8; j--) { + l1.add(j); +} +assert(l1 == [10, 9]); +assert(j == 8); + +// scope test; h not in for condition scope +for (m = 0; m < 5 && h == null; m++) { + int h = 5; +} +assert(m == 5); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/condition_not_bool.ql b/src/test/resources/testsuite/independent/for/condition_not_bool.ql new file mode 100644 index 0000000..a5e7eb1 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/condition_not_bool.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "FOR_CONDITION_BOOL_REQUIRED" +} +*/ +for (i = 0; 1+1; false) {} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/for_each.ql b/src/test/resources/testsuite/independent/for/for_each.ql new file mode 100644 index 0000000..f65949f --- /dev/null +++ b/src/test/resources/testsuite/independent/for/for_each.ql @@ -0,0 +1,10 @@ +i = 0; +l = [1,2,3]; +for (ele : l) { + assert(l[i++] == ele); +} + +j = 0; +for (int ele : l) { + assert(l[j++] == ele); +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/for_each_break_continue.ql b/src/test/resources/testsuite/independent/for/for_each_break_continue.ql new file mode 100644 index 0000000..515a50b --- /dev/null +++ b/src/test/resources/testsuite/independent/for/for_each_break_continue.ql @@ -0,0 +1,29 @@ +globalI = 0; +for (int i: [0,1,2,3,4]) { + globalI = i; + if (i == 2) { + break; + } +} +assert(globalI == 2); + +sum = 0; +for (i: [0,1,2,3,4]) { + if (i == 2) { + continue; + } + sum += i; +} +assert(sum == 8); + +sum = 0; +for (i: [0,1,2,3,4]) { + if (i == 2) { + if (i == 2) { + continue; + } + } + sum += i; +} +assert(sum == 8); + diff --git a/src/test/resources/testsuite/independent/for/for_each_invalid_type.ql b/src/test/resources/testsuite/independent/for/for_each_invalid_type.ql new file mode 100644 index 0000000..1e2fcb2 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/for_each_invalid_type.ql @@ -0,0 +1,10 @@ +/* +{ + "errCode": "FOR_EACH_TYPE_MISMATCH" +} +*/ +a = [1,2,"abc"]; + +for (int b : a) { + c = 2 +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/for_each_not_iterable.ql b/src/test/resources/testsuite/independent/for/for_each_not_iterable.ql new file mode 100644 index 0000000..2f4003c --- /dev/null +++ b/src/test/resources/testsuite/independent/for/for_each_not_iterable.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "FOR_EACH_ITERABLE_REQUIRED" +} +*/ +for (c : 100) { +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/infinite_loop.ql b/src/test/resources/testsuite/independent/for/infinite_loop.ql new file mode 100644 index 0000000..2a6a995 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/infinite_loop.ql @@ -0,0 +1,15 @@ +i = 0; +for (;;) { + if (i > 3) { + break; + } + i++; +} +assert(i == 4); + +for (j = 0; ; j++) { + if (j > 3) { + break; + } +} +assert(j == 4); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/missing_lparen_at_for.ql b/src/test/resources/testsuite/independent/for/missing_lparen_at_for.ql new file mode 100644 index 0000000..6a665f0 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/missing_lparen_at_for.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +for int i;;;) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/missing_rparen_after_for_update.ql b/src/test/resources/testsuite/independent/for/missing_rparen_after_for_update.ql new file mode 100644 index 0000000..da2f162 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/missing_rparen_after_for_update.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +for (int i = 0; i < 10; i++ {} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/missing_rparen_at_for_each.ql b/src/test/resources/testsuite/independent/for/missing_rparen_at_for_each.ql new file mode 100644 index 0000000..90be511 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/missing_rparen_at_for_each.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +for (a :[1,2,3] { +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/missing_semi_after_for_init.ql b/src/test/resources/testsuite/independent/for/missing_semi_after_for_init.ql new file mode 100644 index 0000000..d8eeaa1 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/missing_semi_after_for_init.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +for (i = 0 i < 10; i++) {} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/for/return_from_for.ql b/src/test/resources/testsuite/independent/for/return_from_for.ql new file mode 100644 index 0000000..37b5fa3 --- /dev/null +++ b/src/test/resources/testsuite/independent/for/return_from_for.ql @@ -0,0 +1,13 @@ +function test(l) { + for (o:l) { + if (o == 10) { + return "find" + o; + } + } +} + +r1 = test([3,4,10]); +assert(r1 == 'find10'); + +r2 = test([3,4,11]); +assert(r2 == null); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/function/complex_parameters.ql b/src/test/resources/testsuite/independent/function/complex_parameters.ql new file mode 100644 index 0000000..4696f9b --- /dev/null +++ b/src/test/resources/testsuite/independent/function/complex_parameters.ql @@ -0,0 +1,67 @@ +// Test functions with various parameter types and complexity +assert(processData(5, true) == 15) +assert(calculateComplex(1.5, 2.5, 10) == 160.0) + +// Test function calls with complex expressions as parameters +assert(mathOperations(add(2, 3), multiply(2, 2)) == 625) // 5^4 = 625 + +function processData(int count, boolean flag) { + int result = count * 2; + if (flag) { + result = result + 5; + } + return result; +} + +function calculateComplex(double x, double y, int multiplier) { + double base = x + y; + return base * base * multiplier; +} + +function mathOperations(int a, int b) { + return power(a, b); +} + +function add(int x, int y) { + return x + y; +} + +function multiply(int x, int y) { + return x * y; +} + +function power(int base, int exp) { + if (exp == 0) { + return 1; + } + int result = 1; + for (int i = 0; i < exp; i++) { + result *= base; + } + return result; +} + +// Test functions with no parameters +assert(getConstant() == 42) +assert(generateRandom() > 0) + +function getConstant() { + return 42; +} + +function generateRandom() { + // Simple pseudo-random using current execution context + return 123; // For deterministic testing +} + +// Test function overloading-like behavior with different parameter counts +assert(calculate(5) == 25) +assert(calculate2(5, 3) == 39) // 5*5 + 3*3 + 5 = 25 + 9 + 5 = 39 + +function calculate(int x) { + return x * x; +} + +function calculate2(int x, int y) { + return x * x + y * y + 5; +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/function/edge_cases.ql b/src/test/resources/testsuite/independent/function/edge_cases.ql new file mode 100644 index 0000000..1462523 --- /dev/null +++ b/src/test/resources/testsuite/independent/function/edge_cases.ql @@ -0,0 +1,100 @@ +// Test edge cases for function hoisting +// Test function called immediately at global level before definition +int immediateResult = callImmediately(); +assert(immediateResult == 42) + +// Test function in conditional blocks called before definition +if (true) { + assert(conditionalFunction(5) == 10) +} + +// Test function in loop called before definition +for (int i = 0; i < 2; i++) { + assert(loopFunction(i) == i * 3) +} + +// Test function with same name as built-in (should work) +assert(toString(123) == "custom_123") + +// Test function that returns function call result +assert(chainReturn() == 999) + +// Function definitions (after all calls) +function callImmediately() { + return 42; +} + +function conditionalFunction(int x) { + return x * 2; +} + +function loopFunction(int x) { + return x * 3; +} + +function toString(int value) { + return "custom_" + value; +} + +function chainReturn() { + return getSpecialValue(); +} + +function getSpecialValue() { + return 999; +} + +// Test empty function +assert(emptyFunction() == null) + +function emptyFunction() { + // Empty body +} + +// Test function that just returns constant +assert(constantFunction() == "CONSTANT") + +function constantFunction() { + return "CONSTANT"; +} + +// Test functions with early returns +assert(earlyReturnFunction(true) == 1) +assert(earlyReturnFunction(false) == 2) + +function earlyReturnFunction(boolean condition) { + if (condition) { + return 1; + } + return 2; +} + +// Test function calling itself indirectly (through another function) +assert(indirectSelfCall(3) == 6) + +function indirectSelfCall(int n) { + if (n <= 0) { + return 0; + } + return n + helperForIndirect(n - 1); +} + +function helperForIndirect(int n) { + return indirectSelfCall(n); +} + +// Test function with multiple return types based on conditions +function dynamicReturn(int choice) { + if (choice == 1) { + return 100; + } else if (choice == 2) { + return "text"; + } else { + return true; + } +} + +// Test the dynamic returns +assert(dynamicReturn(1) == 100) +assert(dynamicReturn(2) == "text") +assert(dynamicReturn(3) == true) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/function/function_call.ql b/src/test/resources/testsuite/independent/function/function_call.ql new file mode 100644 index 0000000..daa1310 --- /dev/null +++ b/src/test/resources/testsuite/independent/function/function_call.ql @@ -0,0 +1,19 @@ +function add(int a, int b) { + return a+b; +} + +assert(add(1,1)==2) + +function sub(a, b) { + return a-b; +} + +assert(sub(3,1)==2) + +assertErrorCode(() -> {add(1, "2")}, "INVALID_ARGUMENT") + +assert(check(3,1)==false) + +function check(a, b) { + return a < b; +} diff --git a/src/test/resources/testsuite/independent/function/function_scoping.ql b/src/test/resources/testsuite/independent/function/function_scoping.ql new file mode 100644 index 0000000..125a735 --- /dev/null +++ b/src/test/resources/testsuite/independent/function/function_scoping.ql @@ -0,0 +1,84 @@ +// Test function scoping and parameter shadowing +int outerVariable = 10; + +// Test function with same parameter name as global variable +assert(testScoping(5) == 15) + +// Test that global variable is not affected +assert(outerVariable == 10) + +function testScoping(int outerVariable) { + // Parameter shadows global variable + return outerVariable + 10; +} + +// Test nested scoping with local variables +assert(nestedScoping(3) == 18) + +function nestedScoping(int x) { + int localVar = x * 2; // 6 + if (localVar > 5) { + int innerVar = localVar * 2; // 12 + return innerVar + 6; // 18 + } + return localVar; +} + +// Test functions that call other functions with same parameter names +assert(chainedScoping(2) == 14) + +function chainedScoping(int value) { + return helperFunction(value + 1); +} + +function helperFunction(int value) { + // Different 'value' parameter + return value * 4 + 2; // (2+1) * 4 + 2 = 14 +} + +// Test function with multiple parameters having local scope +assert(multipleParams(1, 2, 3) == 12) + +function multipleParams(int a, int b, int c) { + int sum = a + b + c; + int doubled = sum * 2; + return doubled; +} + +// Test function that modifies parameters (local copies) +int originalValue = 5; +assert(modifyParameter(originalValue) == 25) +assert(originalValue == 5) // Original should remain unchanged + +function modifyParameter(int param) { + param = param * 5; // Modifying local copy + return param; +} + +// Test function with conditional blocks and local variables +assert(conditionalScoping(true, 10) == 30) +assert(conditionalScoping(false, 10) == 0) // 10 - 10 = 0 + +function conditionalScoping(boolean condition, int base) { + int result = base; + if (condition) { + int bonus = 20; + result = result + bonus; + } else { + int penalty = 10; + result = result - penalty; + } + return result; +} + +// Test loops with local scope +assert(loopScoping(3) == 6) + +function loopScoping(int count) { + int total = 0; + for (int i = 1; i <= count; i++) { + int squared = i; // Local to loop iteration + total += squared; + } + return total; // 1 + 2 + 3 = 6 +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/function/invalid_argument.ql b/src/test/resources/testsuite/independent/function/invalid_argument.ql new file mode 100644 index 0000000..78de2ac --- /dev/null +++ b/src/test/resources/testsuite/independent/function/invalid_argument.ql @@ -0,0 +1,10 @@ +/* +{ + "errCode": "INVALID_ARGUMENT" +} +*/ +function add(a, int b) { + return a + b; +} + +add("aaa", "ffff"); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/function/mixed_declarations.ql b/src/test/resources/testsuite/independent/function/mixed_declarations.ql new file mode 100644 index 0000000..2e7da8e --- /dev/null +++ b/src/test/resources/testsuite/independent/function/mixed_declarations.ql @@ -0,0 +1,54 @@ +// Test mixed function and variable declarations with forward references +int globalVar = computeInitialValue(); +int message = formatMessage(42, getValue()); + +assert(globalVar == 100) +assert(message == 92) // 42 + 50 = 92 + +// Variables referencing functions before they're defined +int result1 = doubleValue(25); +assert(result1 == 50) + +function computeInitialValue() { + return 100; +} + +function formatMessage(int prefix, int value) { + return prefix + value; +} + +function getValue() { + return 50; +} + +function doubleValue(int x) { + return x * 2; +} + +// Test functions that modify and return based on global state +int counter = 0; + +function increment() { + counter = counter + 1; + return counter; +} + +function getCounterValue() { + return counter; +} + +// Test the counter functions +int first = increment(); // Should be 1 +int second = increment(); // Should be 2 +int current = getCounterValue(); // Should be 2 + +assert(first == 1) +assert(second == 2) +assert(current == 2) + +// Test simple calculation with constants +function simpleCalculation() { + return 10 * 30; // 300 +} + +assert(simpleCalculation() == 300) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/function/multi_call.ql b/src/test/resources/testsuite/independent/function/multi_call.ql new file mode 100644 index 0000000..f80f3fd --- /dev/null +++ b/src/test/resources/testsuite/independent/function/multi_call.ql @@ -0,0 +1,15 @@ +function mc() { + return () -> 10; +} + +assert(mc()()==10); + +a = {:}; + +function a() { + return a; +} + +a().b = 10; + +assert(a.b==10); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/function/nested_function_calls.ql b/src/test/resources/testsuite/independent/function/nested_function_calls.ql new file mode 100644 index 0000000..761a17a --- /dev/null +++ b/src/test/resources/testsuite/independent/function/nested_function_calls.ql @@ -0,0 +1,40 @@ +// Test nested function calls with forward declarations +assert(outerFunc(5) == 25) + +assert(calculateArea(3, 4) == 26) // calls getPerimeter inside - 2*(3+4) + 3*4 = 14 + 12 = 26 + +function outerFunc(x) { + return innerFunc(x) * 5; +} + +function innerFunc(x) { + return x; +} + +function calculateArea(width, height) { + int perimeter = getPerimeter(width, height); + return perimeter + (width * height); +} + +function getPerimeter(w, h) { + return 2 * (w + h); +} + +// Test deeply nested calls +assert(level1(2) == 14) // level4(2)*3-1+2*2 = 6-1+2*2 = 5+2*2 = 7*2 = 14 + +function level1(x) { + return level2(x) * 2; +} + +function level2(x) { + return level3(x) + 2; +} + +function level3(x) { + return level4(x) - 1; +} + +function level4(x) { + return x * 3; +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/function/recursive_functions.ql b/src/test/resources/testsuite/independent/function/recursive_functions.ql new file mode 100644 index 0000000..c445d1c --- /dev/null +++ b/src/test/resources/testsuite/independent/function/recursive_functions.ql @@ -0,0 +1,52 @@ +// Test recursive functions with forward declarations +assert(factorial(5) == 120) +assert(fibonacci(6) == 8) +assert(gcd(48, 18) == 6) + +// Forward references to recursive functions +int result1 = factorial(4); +assert(result1 == 24) + +int result2 = fibonacci(7); +assert(result2 == 13) + +function factorial(n) { + if (n <= 1) { + return 1; + } + return n * factorial(n - 1); +} + +function fibonacci(n) { + if (n <= 1) { + return n; + } + return fibonacci(n - 1) + fibonacci(n - 2); +} + +function gcd(a, b) { + if (b == 0) { + return a; + } + return gcd(b, a % b); +} + +// Test mutually recursive functions called before definition +assert(isEven(10) == true) +assert(isOdd(10) == false) +assert(isEven(7) == false) +assert(isOdd(7) == true) + +function isEven(n) { + if (n == 0) { + return true; + } + return isOdd(n - 1); +} + +function isOdd(n) { + if (n == 0) { + return false; + } + return isEven(n - 1); +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/function/return_left_value.ql b/src/test/resources/testsuite/independent/function/return_left_value.ql new file mode 100644 index 0000000..25ef14d --- /dev/null +++ b/src/test/resources/testsuite/independent/function/return_left_value.ql @@ -0,0 +1,20 @@ +map = {a:1, b:123} + +function returnLeftValue() { + return map.b; +} + +c = returnLeftValue(); +assert(c == 123); +// c's modification will not effect m.b +c = 190; +assert(map.b == 123); + +function returnEmbedLeftValue() { + return () -> map.b; +} + +c = returnEmbedLeftValue()(); +assert(c == 123); +c = 190; +assert(map.b == 123); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/if_as_expr.ql b/src/test/resources/testsuite/independent/if/if_as_expr.ql new file mode 100644 index 0000000..02d2173 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/if_as_expr.ql @@ -0,0 +1,16 @@ +a = if (11 == 11) { + 10 +} else { + 20 + 2 +} + 1; +b = if (a == 11) 20 else 9; +c = if (a != 11) 11 else 12; +println(b); +assert(b == 20); +assert(c == 12); + +assert(if (20==20) { + 11 == 11 +}); + +assert(if (20==20) 11 == 11); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/if_condition_not_bool.ql b/src/test/resources/testsuite/independent/if/if_condition_not_bool.ql new file mode 100644 index 0000000..07d77ec --- /dev/null +++ b/src/test/resources/testsuite/independent/if/if_condition_not_bool.ql @@ -0,0 +1,8 @@ +/* +{ + "errCode": "CONDITION_BOOL_REQUIRED" +} +*/ +if (1) { + return 2; +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/if_else_if.ql b/src/test/resources/testsuite/independent/if/if_else_if.ql new file mode 100644 index 0000000..8f21e10 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/if_else_if.ql @@ -0,0 +1,8 @@ +a = 11; +if (a >= 0 && a < 5) { + assert(false); +} else if (a >= 5 && a < 10) { + assert(false); +} else if (a >= 10 && a < 15) { + assert(true); +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/if_else_miss_body.ql b/src/test/resources/testsuite/independent/if/if_else_miss_body.ql new file mode 100644 index 0000000..782aa79 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/if_else_miss_body.ql @@ -0,0 +1,8 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +if (1>2) { + return 10; +} else \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/if_followed_by_cast.ql b/src/test/resources/testsuite/independent/if/if_followed_by_cast.ql new file mode 100644 index 0000000..5e8de38 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/if_followed_by_cast.ql @@ -0,0 +1,3 @@ +if(true) { +} +((long)2); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/if_miss_body.ql b/src/test/resources/testsuite/independent/if/if_miss_body.ql new file mode 100644 index 0000000..d06d3de --- /dev/null +++ b/src/test/resources/testsuite/independent/if/if_miss_body.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +if (1>2) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/if_with_one_statement_body.ql b/src/test/resources/testsuite/independent/if/if_with_one_statement_body.ql new file mode 100644 index 0000000..2efdfa6 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/if_with_one_statement_body.ql @@ -0,0 +1,6 @@ +b = () -> if (a != 100) + return 11; +else + return 12; +; +assert(b() == 11); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/if_without_condition.ql b/src/test/resources/testsuite/independent/if/if_without_condition.ql new file mode 100644 index 0000000..3ae8e9c --- /dev/null +++ b/src/test/resources/testsuite/independent/if/if_without_condition.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +if () { +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/if_without_condition_2.ql b/src/test/resources/testsuite/independent/if/if_without_condition_2.ql new file mode 100644 index 0000000..db4d526 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/if_without_condition_2.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +if( \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/miss_if_lparen.ql b/src/test/resources/testsuite/independent/if/miss_if_lparen.ql new file mode 100644 index 0000000..d2c06f0 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/miss_if_lparen.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +if a( \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/miss_if_rparen.ql b/src/test/resources/testsuite/independent/if/miss_if_rparen.ql new file mode 100644 index 0000000..6cd63b6 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/miss_if_rparen.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +if (a>10; \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/return_at_if.ql b/src/test/resources/testsuite/independent/if/return_at_if.ql new file mode 100644 index 0000000..3e8c2c4 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/return_at_if.ql @@ -0,0 +1,14 @@ +function returnFromIf(a) { + int i = if (a > 10) { + return 100; + } else { + if (a < 5) { + return 1000; + } + return 101; + }; +} + +assert(returnFromIf(11) == 100); +assert(returnFromIf(5) == 101); +assert(returnFromIf(-5) == 1000) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/if/simple_if.ql b/src/test/resources/testsuite/independent/if/simple_if.ql new file mode 100644 index 0000000..0676cb7 --- /dev/null +++ b/src/test/resources/testsuite/independent/if/simple_if.ql @@ -0,0 +1,27 @@ +int a = 10; +if (a > 9) { + a = 11; +} else { + a = 5; +} + +assert(a == 11); + +b = 5; +if (a > 20) { + b = 90; +} +assert(b == 5); + +if (b==5) a = 90 else a = 900; + +assert(a == 90); + +if (b==5) { + int m = 100; +} + +if (mmm != null) { +} else { + int mmm = 201; +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/lambda/invalid_argument.ql b/src/test/resources/testsuite/independent/lambda/invalid_argument.ql new file mode 100644 index 0000000..aecb6cf --- /dev/null +++ b/src/test/resources/testsuite/independent/lambda/invalid_argument.ql @@ -0,0 +1,9 @@ +/* +{ + "errCode": "INVALID_ARGUMENT" +} +*/ +add = (a, int b) -> { + return a + b; +}; +add('aa', 'bbb'); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/lambda/invalid_argument_call.ql b/src/test/resources/testsuite/independent/lambda/invalid_argument_call.ql new file mode 100644 index 0000000..038f3b1 --- /dev/null +++ b/src/test/resources/testsuite/independent/lambda/invalid_argument_call.ql @@ -0,0 +1,10 @@ +/* +{ + "errCode": "INVALID_ARGUMENT" +} +*/ +a = () -> { + return (int c) -> c + 1; +}; + +a()("abc"); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/lambda/lambda_doc.ql b/src/test/resources/testsuite/independent/lambda/lambda_doc.ql new file mode 100644 index 0000000..30be04c --- /dev/null +++ b/src/test/resources/testsuite/independent/lambda/lambda_doc.ql @@ -0,0 +1,5 @@ +add = (a, b) -> { + return a + b; +}; +i = add(1,2); +assert(i == 3); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/lambda/lambda_return.ql b/src/test/resources/testsuite/independent/lambda/lambda_return.ql new file mode 100644 index 0000000..7a1aa7f --- /dev/null +++ b/src/test/resources/testsuite/independent/lambda/lambda_return.ql @@ -0,0 +1,7 @@ +add = (a, int b) -> { + return a + b; +}; +i = add(1,2); +assert(i == 3); +j = add(4,5); +assert(j == 9); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/lambda/simple_lambda.ql b/src/test/resources/testsuite/independent/lambda/simple_lambda.ql new file mode 100644 index 0000000..b178be5 --- /dev/null +++ b/src/test/resources/testsuite/independent/lambda/simple_lambda.ql @@ -0,0 +1,10 @@ +exprLambda = () -> 12; +assert(exprLambda() == 12); + +blockLambda = () -> { + return 6 + 6; +}; +assert(blockLambda() == 12); + +emptyLambda = () -> {}; +assert(emptyLambda() == null); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/lambda/unmatch_param_num.ql b/src/test/resources/testsuite/independent/lambda/unmatch_param_num.ql new file mode 100644 index 0000000..fb34718 --- /dev/null +++ b/src/test/resources/testsuite/independent/lambda/unmatch_param_num.ql @@ -0,0 +1,3 @@ +l = (a,b) -> a + b; + +assert(l("abc-") == "abc-null"); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/macro/empty_macro.ql b/src/test/resources/testsuite/independent/macro/empty_macro.ql new file mode 100644 index 0000000..1d88a92 --- /dev/null +++ b/src/test/resources/testsuite/independent/macro/empty_macro.ql @@ -0,0 +1,9 @@ +macro empty { +} + +function func() { + 1+1; + empty; +} + +assert(func() == null); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/macro/invalid_macro_name.ql b/src/test/resources/testsuite/independent/macro/invalid_macro_name.ql new file mode 100644 index 0000000..34d38e9 --- /dev/null +++ b/src/test/resources/testsuite/independent/macro/invalid_macro_name.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +macro if { +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/macro/macro.ql b/src/test/resources/testsuite/independent/macro/macro.ql new file mode 100644 index 0000000..a4ac872 --- /dev/null +++ b/src/test/resources/testsuite/independent/macro/macro.ql @@ -0,0 +1,26 @@ +// tag::addMacroInScript[] +macro add { + c = a + b; +} + +a = 1; +b = 2; +add; +assert(c == 3); +// end::addMacroInScript[] +b = 10; +add; +assert(c == 11); +// variable has the same name with macro +add = 100; +a = 3; +add; +assert(c == 13); +assert(add == 100); + +// expression auto return +function macroReturn(a, b) { + add +} + +assert(macroReturn(6,7)==13) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/macro/macro_break_continue.ql b/src/test/resources/testsuite/independent/macro/macro_break_continue.ql new file mode 100644 index 0000000..7318b86 --- /dev/null +++ b/src/test/resources/testsuite/independent/macro/macro_break_continue.ql @@ -0,0 +1,12 @@ +macro bc { + if (i < 5) { + continue; + } +} + +s = 0; +for (int i = 0; i < 10; i++) { + bc; + s++; +} +assert(s == 5) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/macro/macro_control_flow.ql b/src/test/resources/testsuite/independent/macro/macro_control_flow.ql new file mode 100644 index 0000000..15070f1 --- /dev/null +++ b/src/test/resources/testsuite/independent/macro/macro_control_flow.ql @@ -0,0 +1,16 @@ +macro control { + if (i > 3) { + return; + } +}; + +t = -1; +forBody = (i) -> { + control; + t = i; +}; + +forBody(10); +assert(t == -1); +forBody(2); +assert(t == 2); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/macro/macro_define_in_sub_scope.ql b/src/test/resources/testsuite/independent/macro/macro_define_in_sub_scope.ql new file mode 100644 index 0000000..669887f --- /dev/null +++ b/src/test/resources/testsuite/independent/macro/macro_define_in_sub_scope.ql @@ -0,0 +1,17 @@ +function testMacroInSubScope() { + macro add { + int c = a + b; + } + int a = 1; + int b = 10; + add; + return c; +} + +c = testMacroInSubScope() +assert(c==11) +a = 11 +b = 100 +// add is not a macro in this scope +add +assert(c==11) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/macro/macro_return.ql b/src/test/resources/testsuite/independent/macro/macro_return.ql new file mode 100644 index 0000000..3dcad3a --- /dev/null +++ b/src/test/resources/testsuite/independent/macro/macro_return.ql @@ -0,0 +1,9 @@ +macro test { + 1+1 +} + +l = () -> { + test +}; + +assert(l() == 2) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/macro/missing_lbrace.ql b/src/test/resources/testsuite/independent/macro/missing_lbrace.ql new file mode 100644 index 0000000..8cd4a35 --- /dev/null +++ b/src/test/resources/testsuite/independent/macro/missing_lbrace.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +macro m a=1 \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/map/colon_absent_between_map_entry.ql b/src/test/resources/testsuite/independent/map/colon_absent_between_map_entry.ql new file mode 100644 index 0000000..8fab17a --- /dev/null +++ b/src/test/resources/testsuite/independent/map/colon_absent_between_map_entry.ql @@ -0,0 +1,9 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +map = { + aa: 111 + bb: 222 +}; \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/map/colon_absent_in_entry.ql b/src/test/resources/testsuite/independent/map/colon_absent_in_entry.ql new file mode 100644 index 0000000..2c5d203 --- /dev/null +++ b/src/test/resources/testsuite/independent/map/colon_absent_in_entry.ql @@ -0,0 +1,9 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +{ + aa: 123, + bb 444 +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/map/invalid_map_key.ql b/src/test/resources/testsuite/independent/map/invalid_map_key.ql new file mode 100644 index 0000000..58a017c --- /dev/null +++ b/src/test/resources/testsuite/independent/map/invalid_map_key.ql @@ -0,0 +1,8 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +{ + 12 : 'aa' +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/map/key_word_can_not_get_from_field.ql b/src/test/resources/testsuite/independent/map/key_word_can_not_get_from_field.ql new file mode 100644 index 0000000..d546c6b --- /dev/null +++ b/src/test/resources/testsuite/independent/map/key_word_can_not_get_from_field.ql @@ -0,0 +1,9 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +map = { + if: 1 +}; +assert(map.if == 1); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/map/keyword_key_map.ql b/src/test/resources/testsuite/independent/map/keyword_key_map.ql new file mode 100644 index 0000000..4b2f1d9 --- /dev/null +++ b/src/test/resources/testsuite/independent/map/keyword_key_map.ql @@ -0,0 +1,8 @@ +map = { + if: 1, + else: 2, + int: 3 +}; +assert(map['if'] == 1); +assert(map['else'] == 2); +assert(map['int'] == 3); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/map/map_at_block.ql b/src/test/resources/testsuite/independent/map/map_at_block.ql new file mode 100644 index 0000000..c681d13 --- /dev/null +++ b/src/test/resources/testsuite/independent/map/map_at_block.ql @@ -0,0 +1,7 @@ +m = { + { + mmm: 111, + ccc: 222 + } +}; +assert(m.mmm == 111); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/map/map_define.ql b/src/test/resources/testsuite/independent/map/map_define.ql new file mode 100644 index 0000000..e5fff18 --- /dev/null +++ b/src/test/resources/testsuite/independent/map/map_define.ql @@ -0,0 +1,31 @@ +address = { + 'owner': 'cole', + age: 30, + contacts: [ + { + name: 'cassandra', + phoneNumber: '0000000' + }, + { + name: 'cole', + phoneNumber: '1111111' + } + ] +}; +assert(address['owner'] == 'cole'); +assert(address['age'] == 30); +assert(address.contacts[0].phoneNumber == '0000000'); + +List addressBook = [address, {owner: 'john'}]; + +assert(addressBook[0].owner == 'cole'); +assert(addressBook[1].owner == 'john'); + +empty = {:}; +assert(empty.a == null); + +extra_comma_map = { + "test_id" : "acd", + "cc_id" : "ttt", +} +assert(extra_comma_map.test_id == "acd") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/map/string_literal_as_field_access.ql b/src/test/resources/testsuite/independent/map/string_literal_as_field_access.ql new file mode 100644 index 0000000..89da0c5 --- /dev/null +++ b/src/test/resources/testsuite/independent/map/string_literal_as_field_access.ql @@ -0,0 +1,6 @@ +assert({"门店 test": 1234}.'门店 test' == 1234) + +a = {"门店 test": 1234, "a b c d": 'oopp'} +assert(a.'门店 test' == 1234) +assert(a.'a b c d' == 'oopp') + diff --git a/src/test/resources/testsuite/independent/newlines/newlines.ql b/src/test/resources/testsuite/independent/newlines/newlines.ql new file mode 100644 index 0000000..4bd0051 --- /dev/null +++ b/src/test/resources/testsuite/independent/newlines/newlines.ql @@ -0,0 +1,88 @@ +function testAdd( + int a, int b + , int c, int d, + int e +) { +} + +a = (int a + , int b, int c, + int d, int e) -> + { + a + } + +try { +} catch (int a) { +} catch (Object b) { +} finally { +} + +assert( + a(1,2 + ,3, + 4,5,6) == + 1 +) + +m = +[ + 1,2 + ,3 + ,4,5, +] + +assert(m[ + 1 + : + 3 +]==[2,3]) + +assert(m[ + 2 + + 1 +] == 4) + +new ArrayList( + 10 +) + +a = 3 +b = new int[ + a+1 +][ + 9 +] + +c = new int[] { + 1,2 + ,3,4, + 5 +} +assert(c[2]==3) + +int ddd = +c[3], +eee = 10 +, mmm = 90; + +Map< + String + ,Map + > +> map = +{ + 'aaa': + {'bbb':['ccc']} +}; + +Map< +> map2 = { + : +}; + +f = true ? + 10: + 11 +assert(f==10) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/number/number.ql b/src/test/resources/testsuite/independent/number/number.ql new file mode 100644 index 0000000..2629404 --- /dev/null +++ b/src/test/resources/testsuite/independent/number/number.ql @@ -0,0 +1,14 @@ +assert(-1==-1) +assert(17 == 0x11); +assert(0x11 == 021); +assert(021 == 0b10001); +assert(-17 == -0x11); +assert(-0x11 == -021); +assert(-021 == -0b10001); +assert(.0 == 0.); +assert(1l == 1L); +assert(1f == 1F); +assert(13.45d == 1.345e1); +assert(13.45d == 1.345e+1); +assert(13.45d == 134.5e-1); +assert(0 == 0); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/number/precise.ql b/src/test/resources/testsuite/independent/number/precise.ql new file mode 100644 index 0000000..f7e6daa --- /dev/null +++ b/src/test/resources/testsuite/independent/number/precise.ql @@ -0,0 +1,4 @@ +assert(123456789.123456789+987654321.987654321==1111111111.11111111) +assert(123456789012345678901234567890*987654321098765432109876543210==121932631137021795226185032733622923332237463801111263526900) +assert(123456789.123456789/0.000000001==123456789123456789) +assert((123456789.123456789 + 987654321.987654321) * (1 - 0.000000001) / 2==555555554.999999999444444445) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/big_decimal.ql b/src/test/resources/testsuite/independent/operator/big_decimal.ql new file mode 100644 index 0000000..2c9e171 --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/big_decimal.ql @@ -0,0 +1,87 @@ +/* +{ + "qlOptions": QLOptions.builder().precise(true) +} +*/ + +// plus +x = 0.1 + 1.1; +assert(x instanceof BigDecimal) +assert(x == 1.2); + +x = 3 + 2.2 +assert(x == 5.2) +assert(x instanceof BigDecimal) + +x = 2.2 + 4 +assert(x instanceof BigDecimal) +assert(x == 6.2) + +y = x + 1 +assert(y instanceof BigDecimal) +assert(y == 7.2) + +z = y + x + 1 + 2 +assert(z instanceof BigDecimal) +assert(z == 16.4) + +// minus +x = 1.1 - 0.01 +assert(x == 1.09) + +x = 6 - 2.2 +assert(x == 3.8) + +x = 5.8 - 2 +assert(x == 3.8) + +y = x - 1 +assert(y == 2.8) + +// multiply +x = 3 * 2.0 +assert(x == 6.0) + +x = 3.0 * 2 +assert(x == 6.0) + +x = 3.0 * 2.0 +assert(x == 6.0) + +y = x * 2 +assert(y == 12.0) + +y = 11 * 3.333 +assert(y == 36.663) + +y = 3.333 * 11 +assert(y == 36.663) + +// divide +x = 80.0 / 4 +assert(x == 20.0 , "x = " + x) + +x = 80 / 4.0 +assert(x == 20.0 , "x = " + x) + +y = x / 2 +assert(y == 10.0 , "y = " + y) +assert(y == 10 , "y = " + y) + +y = 34 / 3.000; +assert(y == 11.3333333333); + +y = 34.00000000000 / 3; +assert(y == 11.3333333333); + +// remainder +x = 100.0 % 3 +assert(x == 1) + +y = 5.5 +y %= 2.0 +assert(y == 1.5) + +y = -5.5 +y %= 2.0 +assert(y == -1.5) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/big_integer.ql b/src/test/resources/testsuite/independent/operator/big_integer.ql new file mode 100644 index 0000000..dbac005 --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/big_integer.ql @@ -0,0 +1,58 @@ +// assign +BigInteger bi; +bi = (byte) 20; +assert(bi instanceof BigInteger); +assert(bi == 20); + +bi = (short) 20 +assert(bi instanceof BigInteger) +assert(bi == 20) + +bi = (int) 20 +assert(bi instanceof BigInteger) +assert(bi == 20) + +bi = (long) 20 +assert(bi instanceof BigInteger) +assert(bi == 20) + +bi = (float) 0.5f +assert(bi instanceof BigInteger) +assert(bi == 0) + +bi = (double) 0.5d +assert(bi instanceof BigInteger) +assert(bi == 0) + +bi = 10.5 +assert(bi instanceof BigInteger) +assert(bi == 10) + +double d; +d = 1000; +d *= d +d *= d +d *= d +assert((long)d != d) +assert((BigInteger) d == d) + +// plus +x = BigInteger.valueOf(2) + BigInteger.valueOf(3) +assert(x instanceof BigInteger) +assert(x == 5) + +// multiply +x = BigInteger.valueOf(2) * BigInteger.valueOf(3) +assert(x instanceof BigInteger) +assert(x == 6) + +// remainder +x = BigInteger.valueOf(100) % 3 +assert(x == 1) + +y = BigInteger.valueOf(11) +y %= 3 +assert(y == 2) +y = BigInteger.valueOf(-11) +y %= 3 +assert(y == -2) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/bitwise.ql b/src/test/resources/testsuite/independent/operator/bitwise.ql new file mode 100644 index 0000000..b210c46 --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/bitwise.ql @@ -0,0 +1,145 @@ +assert(true & true); +assertFalse(true & false); +assertFalse(true & null); + +assertFalse(false & true); +assertFalse(false & false); +assertFalse(false & null); + +assert(true | true); +assert(true | false); +assert(true | null); + +assert(false | true); +assertFalse(false | false); +assertFalse(false | null); + +assertFalse(true ^ true); +assert(true ^ false); +assert(true ^ null); + +assert(false ^ true); +assertFalse(false ^ false); +assertFalse(false ^ null); + +// bitwise shift +a = 4; +b = -4; +assert(a << 1 == 8); +assert(a << 2 == 16); +assert(a >> 1 == 2); +assert(a >> 2 == 1); +assert(a >>> 1 == 2); +assert(a >>> 2 == 1); +assert(b << 1 == -8); +assert(b << 2 == -16); +assert(b >> 1 == -2); +assert(b >> 2 == -1); +assert(b >>> 1 == 0x7FFFFFFE); +assert(b >>> 2 == 0x3FFFFFFF); + +assertErrorCode(()-> {8.0F >> 2}, "EXECUTE_OPERATOR_EXCEPTION") +assertErrorCode(()-> {8 >> 2.0}, "EXECUTE_OPERATOR_EXCEPTION") + +// bitwise shift equal +a = 4; +a <<= 1; +assert(a == 8); +a <<= 2; +assert(a == 32); +a >>= 1; +assert(a == 16); +a >>= 2; +assert(a == 4); + +b = -4; +b <<= 1; +assert(b == -8); +b <<= 2; +assert(b == -32); +b >>= 1; +assert(b == -16); +b >>= 2; +assert(b == -4); + +b = -4; +b >>>= 1; +assert(b == 0x7FFFFFFE); +b = -8; +b >>>= 2; +assert(b == 0x3FFFFFFE); + +// bitwise and +a = 13; +assert((a & 3) == 1); // 0x0000000D & 0x00000003 +assert((a & 7) == 5); // 0x0000000D & 0x00000007 +b = -13; +assert((b & 3) == 3); // 0xFFFFFFF3 & 0x00000003 +assert((b & 7) == 3); // 0xFFFFFFF3 & 0x00000007 + +// bitwise and equals +a = 13; +a &= 3; +assert(a == 1); // 0x0000000D & 0x00000003 + +a &= 4; +assert(a == 0); // 0x00000001 & 0x00000004 + +b = -13; +b &= 3; +assert(b == 3); // 0xFFFFFFF3 & 0x00000003 + +b &= 7; +assert(b == 3); // 0x00000003 & 0x00000007 + +// bitwise or +a = 13; +assert((a | 8) == 13); // 0x0000000D | 0x00000008 +assert((a | 16) == 29); // 0x0000000D | 0x00000010 +b = -13; +assert((b | 8) == -5); // 0xFFFFFFF3 | 0x00000008 +assert((b | 16) == -13); // 0xFFFFFFF3 | 0x00000010 + +// bitwise or equal +a = 13; +a |= 2; +assert(a == 15); // 0x0000000D | 0x00000002 +a |= 16; +assert(a == 31); // 0x0000000F | 0x0000001F +b = -13; +b |= 8; +assert(b == -5); // 0xFFFFFFF3 | 0x00000008 +b |= 1; +assert(b == -5); // 0xFFFFFFFB | 0x00000001 + +// bitwise xor +a = 13; +assert((a ^ 10) == 7); // 0x0000000D ^ 0x0000000A = 0x000000007 +assert((a ^ 15) == 2); // 0x0000000D ^ 0x0000000F = 0x000000002 +b = -13; +assert((b ^ 10) == -7); // 0xFFFFFFF3 ^ 0x0000000A = 0xFFFFFFF9 +assert((b ^ 15) == -4); // 0xFFFFFFF3 ^ 0x0000000F = 0xFFFFFFFC + +// bitwise xor equal +a = 13; +a ^= 8; +assert(a == 5); // 0x0000000D ^ 0x00000008 = 0x000000005 +a ^= 16 +assert(a == 21); // 0x00000005 ^ 0x00000010 = 0x000000015 +b = -13; +b ^= 8; +assert(b == -5); // 0xFFFFFFF3 ^ 0x00000008 = 0xFFFFFFFB +b ^= 16; +assert(b == -21); // 0xFFFFFFFB ^ 0x00000010 = 0xFFFFFFEB + +// bitwise negation +assert(~1 == -2); // ~0x00000001 = 0xFFFFFFFE +assert(~(-1) == 0); // ~0xFFFFFFFF = 0x00000000 +assert(~(~5) == 5); // ~~0x00000005 = ~0xFFFFFFFA = 0xFFFFFFF5 +a = 13; +assert(~a == -14); // ~0x0000000D = 0xFFFFFFF2 +assert(~(~a) == 13); // ~~0x0000000D = ~0xFFFFFFF2 = 0x0000000D +assert(-(~a) == 14); // -~0x0000000D = -0xFFFFFFF2 = 0x0000000E + +// bitwise exception +assertErrorCode(()-> {~"8"}, "INVALID_UNARY_OPERAND") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/boolean.ql b/src/test/resources/testsuite/independent/operator/boolean.ql new file mode 100644 index 0000000..d7a72a3 --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/boolean.ql @@ -0,0 +1,107 @@ +// comparison +assert(true) +assert(true != false) + +x = true +assert(x) +assert(x == true) +assert(x != false) + +x = false +assert(x == false) +assert(x != true) +assert(!x) + +y = false +assert(x == y) + +y = true +assert(x != y) + +// if branch +x = false +r = false +if (x) { + // ignore +} +else { + r = true +} +assert(r) + +x = true +r = false +if (x) { + r = true +} +else { + // ignore +} +assert(r) + +if (!x) { + r = false +} +else { + r = true +} +assert(r) + +// expression +x = 5 +value = x > 2 +assert(value) + +value = x < 2 +assert(value == false) + +// ops +boolean x = true; +boolean y = false; +assert((x & x) == true) +assert((x & y) == false) +assert((y & x) == false) +assert((y & y) == false) + +assert((x | x) == true) +assert((x | y) == true) +assert((y | x) == true) +assert((y | y) == false) + +assert((x ^ x) == false) +assert((x ^ y) == true) +assert((y ^ x) == true) +assert((y ^ y) == false) + +assert((!x) == false) +assert((!y) == true) + +// assign ops +boolean z = true; +z &= true +assert(z == true) +z &= false +assert(z == false) + +z = true +z |= true +assert(z == true) +z |= false +assert(z == true) +z = false +z |= false +assert(z == false) +z |= true +assert(z == true) + +z = true +z ^= true +assert(z == false) +z ^= true +assert(z == true) +z ^= false +assert(z == true) +z ^= true +assert(z == false) +z ^= false +assert(z == false) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/character.ql b/src/test/resources/testsuite/independent/operator/character.ql new file mode 100644 index 0000000..b2c9bd2 --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/character.ql @@ -0,0 +1,18 @@ +assert(98 > (char)'a'); +assert(98 == (char)'b'); +assert(98 < (char)'c'); + +assert((char)'a' < 98); +assert((char)'b' == 98); +assert((char)'c' > 98); + +assert(98 != (char)'a'); +assert(98 <> (char)'a'); +assert((char)'a' != 98); +assert((char)'a' <> 98); + +assert((char)'a' < (char)'b'); +assert((char)'b' == (char)'b'); +assert((char)'c' > (char)'b'); + +assertErrorCode(() -> {'测试一下' > 1}, "INVALID_BINARY_OPERAND") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/comparable.ql b/src/test/resources/testsuite/independent/operator/comparable.ql new file mode 100644 index 0000000..53743d3 --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/comparable.ql @@ -0,0 +1,13 @@ +import com.alibaba.qlexpress4.inport.Person; + +p1 = new Person(10) +p2 = new Person(20) +assert(p1 < p2) +assert(p1 <= p2) +assert(p1 != p2) +assert(p1 <> p2) + +assert(p2 > p1) +assert(p2 >= p1) +assert(p2 != p1) +assert(p2 <> p1) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/double.ql b/src/test/resources/testsuite/independent/operator/double.ql new file mode 100644 index 0000000..c430fdd --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/double.ql @@ -0,0 +1,58 @@ +// plus +x = 2.1d + 2.1d +assert(x == 4.2d) + +x = 3d + 2.2d +assert(x == 5.2d) + +x = 2.2d + 4d +assert(x == 6.2d) + +y = x + 1d +assert(y == 7.2d) + +z = y + x + 1d + 2d +assert(z == 16.4d) + +// minus +x = 6d - 2.2d +assert(x == 3.8d) + +x = 5.8d - 2d +assert(x == 3.8d) + +y = x - 1d +assert(y == 2.8d) + +// multiply +x = 3d * 2.0d +assert(x == 6.0d) + +x = 3.0d * 2d +assert(x == 6.0d) + +x = 3.0d * 2.0d +assert(x == 6.0d) +y = x * 2d +assert(y == 12.0d) + +// divide +x = 80.0d / 4d +assert(x == 20.0d, "x = " + x) + +x = 80d / 4.0d +assert(x == 20.0d, "x = " + x) + +y = x / 2d +assert(y == 10.0d, "y = " + y) + +// remainder +x = 100d % 3 +assert(x == 1d) + +y = 11d +y %= 3d +assert(y == 2d) +y = -11d +y %= 3d +assert(y == -2d) diff --git a/src/test/resources/testsuite/independent/operator/equals.ql b/src/test/resources/testsuite/independent/operator/equals.ql new file mode 100644 index 0000000..6bcdd52 --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/equals.ql @@ -0,0 +1,34 @@ +assert(0 == 0); +assert(512 == 512); +assert(512 == 512L); +assert(512 == 512F); +assert(512 == 512D); + +assert(512L == 512L); +assert(512L == 512F); +assert(512L == 512D); + +assert(512F == 512F); +assert(512F == 512D); + +assert(512D == 512D); + +assertFalse(512 == 513); + +assert((char)'a' == 97); +assert(97 == (char)'a'); +assertFalse((char)'b' == 97); +assertFalse(97 == (char)'b'); + +assert((char)'b' != 97); +assert(97 != (char)'b'); +assertFalse((char)'a' != 97); +assertFalse(97 != (char)'a'); + +assert((char)'b' <> 97); +assert(97 <> (char)'b'); +assertFalse((char)'a' <> 97); +assertFalse(97 <> (char)'a'); + +assert(null == null); +assertFalse(null != null); diff --git a/src/test/resources/testsuite/independent/operator/in_not_in.ql b/src/test/resources/testsuite/independent/operator/in_not_in.ql new file mode 100644 index 0000000..206931d --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/in_not_in.ql @@ -0,0 +1,59 @@ +assert(null in null); +assertFalse(null in "abc"); +assertFalse(null in 123); + +assertFalse(null not_in null); +assert(null not_in "abc"); +assert(null not_in 123); + +assertFalse(null in [1, 2, 3]); +assertFalse(null in new int[]{1, 2, 3}); + +assert(null not_in [1, 2, 3]); +assert(null not_in new int[]{1, 2, 3}); + +assertFalse(null in ["abc", "def", "ghi"]); +assertFalse(null in new String[]{"abc", "def", "ghi"}); + +assert(null not_in ["abc", "def", "ghi"]); +assert(null not_in new String[]{"abc", "def", "ghi"}); + +assert(1 in [1, 2, 3]); +assert(1 in new int[]{1, 2, 3}); + +assertFalse(1 not_in [1, 2, 3]); +assertFalse(1 not_in new int[]{1, 2, 3}); + +assertFalse(1 in ["1", "2", "3"]); +assertFalse(1 in new String[]{"1", "2", "3"}); + +assert(1 not_in ["1", "2", "3"]); +assert(1 not_in new String[]{"1", "2", "3"}); + +assert("abc" in ["abc", "def", "ghi"]); +assert("abc" in new String[]{"abc", "def", "ghi"}); + +assertFalse("abc" not_in ["abc", "def", "gcpghi"]); +assertFalse("abc" not_in new String[]{"abc", "def", "ghi"}); + +assertFalse("bcd" in ["abc", "def", "ghi"]); +assertFalse("bcd" in new String[]{"abc", "def", "ghi"}); + +assert("bcd" not_in ["abc", "def", "ghi"]); +assert("bcd" not_in new String[]{"abc", "def", "ghi"}); + +assert("bc" in "abc"); +assert("bc" in "bcd"); +assert("bc" in "abcd"); +assertFalse("bc" in "ab"); +assertFalse("bc" in "cd"); +assertFalse("bc" in "abd"); +assertFalse("bc" in "acd"); + +assertFalse("bc" not_in "abc"); +assertFalse("bc" not_in "bcd"); +assertFalse("bc" not_in "abcd"); +assert("bc" not_in "ab"); +assert("bc" not_in "cd"); +assert("bc" not_in "abd"); +assert("bc" not_in "acd"); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/integer.ql b/src/test/resources/testsuite/independent/operator/integer.ql new file mode 100644 index 0000000..e61fa7e --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/integer.ql @@ -0,0 +1,147 @@ +// plus +x = 2 + 2 +assert(x == 4) + +y = x + 1 +assert(y == 5) + +z = y + x + 1 + 2 +assert(z == 12) + +// unary plus +x = 3 +y = +x +assert(y == 3) + +// character plus +char c1 = 1; +char c2 = 2; + +x = c2 + 2 +assert(x == 4) + +x = 2 + c2 +assert(x == 4) + +x = c2 + c2 +assert(x == 4) + +y = x + c1 +assert(y == 5) + +y = c1 + x +assert(y == 5) + +z = y + x + c1 + 2 +assert(z == 12) + +z = y + x + 1 + c2 +assert(z == 12) + +z = y + x + c1 + c2 +assert(z == 12) + +// minus +x = 6 - 2 +assert(x == 4) + +x = 6 +x -= 2 +assert(x == 4) + +y = x - 1 +assert(y == 3) + +// unary minus +x = 3 +y = -x +assert(y == -3) + +// bitwise negate +x = 3 +y = ~x +assert(y == -4) + +// character minus +Character c1 = 1; +Character c2 = 2; +Character c6 = 6; + +x = c6 - 2 +assert(x == 4) + +x = 6 - c2 +assert(x == 4) + +x = c6 - c2 +assert(x == 4) + +y = x - c1 +assert(y == 3) + +// multiply +x = 3 * 2 +assert(x == 6) + +y = x * 2 +assert(y == 12) + +// divide +x = 80 / 4 +assert(x == 20.0) + +x = 80 +x /= 4 +assert(x == 20.0) + +y = x / 2 +assert(y == 10.0) + +// remainder +x = 100 % 3 +assert(x == 1) + +y = 11 +y %= 3 +assert(y == 2) + +y = -11 +y %= 3 +assert(y == -2) + +// and +x = 1 & 3 +assert(x == 1) + +// or +x = 1 | 3 +assert(x == 3) + +// shift operator +x = 8 >> 1 +assert(x == 4) +assert(x instanceof Integer) + +x = 8 << 2 +assert(x == 32) +assert(x instanceof Integer) + +x = 8L << 2 +assert(x == 32) +assert(x instanceof Long) + +x = -16 >> 4 +assert(x == -1) + +x = -16 >>> 4 +assert(x == 0xFFFFFFF) + +//Ensure that the type of the right operand (shift distance) is ignored when calculating the +//result. This is how java works, and for these operators, it makes sense to keep that behavior. +x = Integer.MAX_VALUE << 1L +assert(x == -2) +assert(x instanceof Integer) + +x = new Long(Integer.MAX_VALUE).longValue() << 1 +assert(x == 0xfffffffe) +assert(x instanceof Long) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/like.ql b/src/test/resources/testsuite/independent/operator/like.ql new file mode 100644 index 0000000..f7e9f4b --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/like.ql @@ -0,0 +1,20 @@ +assert(null like null); +assertFalse(null not_like null); + +assertFalse("a" like null); +assert("a" not_like null); + +assert("1006" like "%6"); +assert("1006" like "1%"); +assert("ABCD" like "A%B%D"); + +assertFalse("1006" not_like "%6"); +assertFalse("1006" not_like "1%"); +assertFalse("ABCD" not_like "A%B%D"); + +// error code +assertErrorCode(() -> {"ABCD" like 200}, "INVALID_BINARY_OPERAND"); +assertErrorCode(() -> {200 like "200"}, "INVALID_BINARY_OPERAND"); + +assertErrorCode(() -> {"ABCD" not_like 200}, "INVALID_BINARY_OPERAND"); +assertErrorCode(() -> {200 not_like "200"}, "INVALID_BINARY_OPERAND"); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/logic.ql b/src/test/resources/testsuite/independent/operator/logic.ql new file mode 100644 index 0000000..c2c18bd --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/logic.ql @@ -0,0 +1,14 @@ +assert(true && true); +assertFalse(true && false); +assertFalse(true && null); +assert(true and true); +assertFalse(true and false); +assertFalse(true and null); + +assertFalse(false && true); +assertFalse(false && false); +assertFalse(false && null); + +assertFalse(null && true); +assertFalse(null && false); +assertFalse(null && null); diff --git a/src/test/resources/testsuite/independent/operator/optional_chaining.ql b/src/test/resources/testsuite/independent/operator/optional_chaining.ql new file mode 100644 index 0000000..73c1f59 --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/optional_chaining.ql @@ -0,0 +1,14 @@ +assert(a?.b?.c?.d == null); + +try { + assert(a?.b.c == null); + throw new RuntimeException(); +} catch (e) { + assert(e instanceof NullPointerException); +} + +mm = {cc: 123} + +assert(mm?.cc == 123); +assert(mm.dd?.ee == null); +assert(mm.dd?.test() == null); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/operator/string.ql b/src/test/resources/testsuite/independent/operator/string.ql new file mode 100644 index 0000000..066f9c7 --- /dev/null +++ b/src/test/resources/testsuite/independent/operator/string.ql @@ -0,0 +1,19 @@ +x = "hello " + "there" +assert(x == "hello there") + +x = "hello " + 2 +assert(x == "hello 2") + +x = "hello " + 1.2 +assert(x == "hello 1.2") + +y = x + 1 +assert(y == "hello 1.21") + +x = "hello" + " " + "there" + " nice" + " day" +assert(x == "hello there nice day") + +assert("bc" > "ab") +assert("bc" > "ab") +assert("bcd" > "ab") +assert("bcd" > "abc") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/scope/block_scope.ql b/src/test/resources/testsuite/independent/scope/block_scope.ql new file mode 100644 index 0000000..0be15cf --- /dev/null +++ b/src/test/resources/testsuite/independent/scope/block_scope.ql @@ -0,0 +1,6 @@ +{ + int b = 12; + a = 100 +} +assert(a + "-" + b == '100-null'); + diff --git a/src/test/resources/testsuite/independent/scope/global_variable.ql b/src/test/resources/testsuite/independent/scope/global_variable.ql new file mode 100644 index 0000000..f6de498 --- /dev/null +++ b/src/test/resources/testsuite/independent/scope/global_variable.ql @@ -0,0 +1,8 @@ +function setA(value) { + a = value; +} + +a = 10; +assert(a == 10); +setA(10000); +assert(a == 10000); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/scope/lexical_scope.ql b/src/test/resources/testsuite/independent/scope/lexical_scope.ql new file mode 100644 index 0000000..5a82558 --- /dev/null +++ b/src/test/resources/testsuite/independent/scope/lexical_scope.ql @@ -0,0 +1,14 @@ +String a = "lexical first"; + +aFactory = () -> a; + +{ + String a = "runtime first-block"; + assert(aFactory() == "lexical first"); +} + +function testScope(a) { + assert(aFactory() == "lexical first"); +} + +testScope("runtime first-function"); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/scope/scope_cover.ql b/src/test/resources/testsuite/independent/scope/scope_cover.ql new file mode 100644 index 0000000..fa62a6e --- /dev/null +++ b/src/test/resources/testsuite/independent/scope/scope_cover.ql @@ -0,0 +1,10 @@ +a = 10; + +assert(a == 10); + +{ + int a = 100; + assert(a == 100); +} + +assert(a == 10); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/spread/arr_spread.ql b/src/test/resources/testsuite/independent/spread/arr_spread.ql new file mode 100644 index 0000000..4757bda --- /dev/null +++ b/src/test/resources/testsuite/independent/spread/arr_spread.ql @@ -0,0 +1,8 @@ +Map[] arr = new Map[]{{"a":1},{"a":2}}; +assert(arr*.a==[1,2]) + +Map[] arr1 = new Map[]{{"a":1},{"a":2}, null}; +assertErrorCode(() -> arr1*.a, "NULL_FIELD_ACCESS") + +Map[] b = new Map[]{{"get100": () -> 100}, null}; +assertErrorCode(() -> b*.get100(), "NULL_METHOD_ACCESS") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/spread/list_spread.ql b/src/test/resources/testsuite/independent/spread/list_spread.ql new file mode 100644 index 0000000..5e1d8b3 --- /dev/null +++ b/src/test/resources/testsuite/independent/spread/list_spread.ql @@ -0,0 +1,46 @@ +// tag::spreadExample[] +list = [ + { + "name": "Li", + "age": 10 + }, + { + "name": "Wang", + "age": 15 + } +] + +// get field from list +assert(list*.age==[10,15]) + +mm = { + "aaa": 1, + "bbb": 2 +} + +// get map key value list +assert(mm*.key==["aaa", "bbb"]) +assert(mm*.value==[1, 2]) +// end::spreadExample[] + +methodMaps = [ + { + "getNum": () -> 100, + }, + { + "getNum": () -> 101, + }, + { + "getNum": () -> 102, + } +] + +assert(methodMaps*.getNum()==[100,101,102]) + +a = [{"c":2}, null] +assertErrorCode(() -> a*.c, "NULL_FIELD_ACCESS") +assertErrorCode(() -> notExist*.c, "NONTRAVERSABLE_OBJECT") + +b = [{"get100": () -> 100}, null] +assertErrorCode(() -> b*.get100(), "NULL_METHOD_ACCESS") +assertErrorCode(() -> notExist*.get100(), "NONTRAVERSABLE_OBJECT") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/spread/spread_avoid_null.ql b/src/test/resources/testsuite/independent/spread/spread_avoid_null.ql new file mode 100644 index 0000000..a84c23b --- /dev/null +++ b/src/test/resources/testsuite/independent/spread/spread_avoid_null.ql @@ -0,0 +1,18 @@ +/* +{ + "qlOptions": QLOptions.builder().avoidNullPointer(true) +} +*/ +a = [{"c":2}, null] +assert(a*.c==[2, null]) +assert(notExist*.c==null) +assert(notExist*.c()==null) + +b = [{"get100": () -> 100}, null] +assert(b*.get100()==[100, null]) + +Map[] arr1 = new Map[]{{"a":1},{"a":2}, null}; +assert(arr1*.a==[1,2,null]) + +Map[] brr = new Map[]{{"get100": () -> 100}, null}; +assert(brr*.get100()==[100,null]) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/string/char.ql b/src/test/resources/testsuite/independent/string/char.ql new file mode 100644 index 0000000..184e92f --- /dev/null +++ b/src/test/resources/testsuite/independent/string/char.ql @@ -0,0 +1,5 @@ +char a = 'a'; +char b = "a"; +assert(a instanceof Character); +assert(b instanceof Character); +assert(a == b); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/string/interpolation.ql b/src/test/resources/testsuite/independent/string/interpolation.ql new file mode 100644 index 0000000..acd4e12 --- /dev/null +++ b/src/test/resources/testsuite/independent/string/interpolation.ql @@ -0,0 +1,37 @@ +a = 123; +b = "test" + +assert("Hello ${a} ${b } ccc" == "Hello 123 test ccc"); +// $ escape +assert("Hello \${a bb cc" == 'Hello ${a bb cc') +// selector variable +assert(${a} == 123) + +assert("${a-1}" == "122") + +assert("m xx ${ + if (b like 't%') { + 'YYY' + } +}" == "m xx YYY") + +assert("m xx ${ + if (b like 't%') { + "YYY" + } +}" == "m xx YYY") + +// nest interpolation +assert("m xx ${ + if (b like 't%') { + "YY${b}Y" + } +}" == "m xx YYtestY") + +assert("m xx ${ + if (b like 'mm%') { + 'YYY' + } +}" == "m xx null") + + diff --git a/src/test/resources/testsuite/independent/string/invalid_char.ql b/src/test/resources/testsuite/independent/string/invalid_char.ql new file mode 100644 index 0000000..4dd9a79 --- /dev/null +++ b/src/test/resources/testsuite/independent/string/invalid_char.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "INCOMPATIBLE_ASSIGNMENT_TYPE" +} +*/ +char a = 'aa'; +println(a); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/string/literal.ql b/src/test/resources/testsuite/independent/string/literal.ql new file mode 100644 index 0000000..b8db3aa --- /dev/null +++ b/src/test/resources/testsuite/independent/string/literal.ql @@ -0,0 +1 @@ +assert('Hello World' == "Hello World") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/string/string_escape.ql b/src/test/resources/testsuite/independent/string/string_escape.ql new file mode 100644 index 0000000..f324fa6 --- /dev/null +++ b/src/test/resources/testsuite/independent/string/string_escape.ql @@ -0,0 +1,13 @@ +assert('\' \\r \'' == "' \\r '") + +assert('hello +world' == "hello\nworld") + +a = "hello +qlexpress" +assert(a == "hello\nqlexpress") + +assert("hello + +qlexpress" == "hello +\nqlexpress") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/string/string_not_close.ql b/src/test/resources/testsuite/independent/string/string_not_close.ql new file mode 100644 index 0000000..fc95ca6 --- /dev/null +++ b/src/test/resources/testsuite/independent/string/string_not_close.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +a = "abc \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/ternary/missing_colon.ql b/src/test/resources/testsuite/independent/ternary/missing_colon.ql new file mode 100644 index 0000000..3a3a4b6 --- /dev/null +++ b/src/test/resources/testsuite/independent/ternary/missing_colon.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +a = x > 10? 10; \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/ternary/ternary.ql b/src/test/resources/testsuite/independent/ternary/ternary.ql new file mode 100644 index 0000000..f5061e4 --- /dev/null +++ b/src/test/resources/testsuite/independent/ternary/ternary.ql @@ -0,0 +1,17 @@ +l = (x) -> x > 10? 11: 100; +assert(l(11) == 11); +assert(l(5) == 100); + +l1 = (x) -> x > 100? 101: x > 50? 51: 11; +assert(l1(120) == 101); +assert(l1(99) == 51); +assert(l1(15) == 11); + +l2 = x -> x <= 10? -9: x < 20? 19: 11; +assert(l2(1) == -9); +assert(l2(17) == 19); +assert(l2(29) == 11); + +l3 = true? a = 100: b = 200; +assert(a == 100); +assert(b == null); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/timeout/timeout.ql b/src/test/resources/testsuite/independent/timeout/timeout.ql new file mode 100644 index 0000000..183b94e --- /dev/null +++ b/src/test/resources/testsuite/independent/timeout/timeout.ql @@ -0,0 +1,9 @@ +/* +{ + "qlOptions": QLOptions.builder().timeoutMillis(10), + "errCode": "SCRIPT_TIME_OUT" +} +*/ +while (true) { + 1+1 +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/trycatch/catch_order.ql b/src/test/resources/testsuite/independent/trycatch/catch_order.ql new file mode 100644 index 0000000..6462109 --- /dev/null +++ b/src/test/resources/testsuite/independent/trycatch/catch_order.ql @@ -0,0 +1,9 @@ +a = try { + throw 10; +} catch (int a) { + 100 +} catch (int b) { + 1000 +} + +assert(a == 100) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/trycatch/missing_lbrace_at_try.ql b/src/test/resources/testsuite/independent/trycatch/missing_lbrace_at_try.ql new file mode 100644 index 0000000..b8907da --- /dev/null +++ b/src/test/resources/testsuite/independent/trycatch/missing_lbrace_at_try.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +try 1+1 \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/trycatch/missing_lbrace_at_try_finally.ql b/src/test/resources/testsuite/independent/trycatch/missing_lbrace_at_try_finally.ql new file mode 100644 index 0000000..21b1d2a --- /dev/null +++ b/src/test/resources/testsuite/independent/trycatch/missing_lbrace_at_try_finally.ql @@ -0,0 +1,10 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +try { + 2+1 +} catch(Object o) { + +} finally } \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/trycatch/multi_exception_catch.ql b/src/test/resources/testsuite/independent/trycatch/multi_exception_catch.ql new file mode 100644 index 0000000..7ec3d3e --- /dev/null +++ b/src/test/resources/testsuite/independent/trycatch/multi_exception_catch.ql @@ -0,0 +1,18 @@ +function f(x) { + try { + throw x; + } catch (int | long i) { + assert(i == x); + } +} + +f(1); + +f(100L); + +try { + f(1.1d); + assert(false); +} catch (double d) { + assert(d == 1.1d); +} diff --git a/src/test/resources/testsuite/independent/trycatch/return_from_try.ql b/src/test/resources/testsuite/independent/trycatch/return_from_try.ql new file mode 100644 index 0000000..ebe0527 --- /dev/null +++ b/src/test/resources/testsuite/independent/trycatch/return_from_try.ql @@ -0,0 +1,31 @@ +function tryTest() { + try { + return 10; + } catch (ignore) { + } + return 1000; +} + +assert(tryTest() == 10) + +function catchTest() { + try { + throw 10; + } catch (ignore) { + return 1000; + } + return 10000; +} + +assert(catchTest()==1000) + +function returnInsideFinally() { + try { + return 30; + } catch (ignore) { + } finally { + return 9000; + } +} + +assert(returnInsideFinally() == 30) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/trycatch/throw_number.ql b/src/test/resources/testsuite/independent/trycatch/throw_number.ql new file mode 100644 index 0000000..65b0785 --- /dev/null +++ b/src/test/resources/testsuite/independent/trycatch/throw_number.ql @@ -0,0 +1,6 @@ +try { + throw 11; + assert(false); +} catch(int i) { + assert(i == 11); +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/trycatch/try_catch_expr.ql b/src/test/resources/testsuite/independent/trycatch/try_catch_expr.ql new file mode 100644 index 0000000..92fcfaf --- /dev/null +++ b/src/test/resources/testsuite/independent/trycatch/try_catch_expr.ql @@ -0,0 +1,16 @@ +a = 1 + try { + 100 + 1/0 +} catch(Object e) { + 11 +}; + +assert(a == 12); + +b = 1 + try { + 100 + 1/0 +} catch(Object e) { + 11 +} finally { + 1000 +}; +assert(b == 12); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/trycatch/try_catch_final_scope.ql b/src/test/resources/testsuite/independent/trycatch/try_catch_final_scope.ql new file mode 100644 index 0000000..66514c9 --- /dev/null +++ b/src/test/resources/testsuite/independent/trycatch/try_catch_final_scope.ql @@ -0,0 +1,11 @@ +int a = 10; + +try { + int a = 1000; + throw new NullPointerException(); + assert(false); +} catch (Object o) { + assert(a == 10); +} finally { + assert(a == 10); +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/while/break_continue.ql b/src/test/resources/testsuite/independent/while/break_continue.ql new file mode 100644 index 0000000..cb81ad6 --- /dev/null +++ b/src/test/resources/testsuite/independent/while/break_continue.ql @@ -0,0 +1,18 @@ +i = 0; +while (i < 5) { + if (++i == 2) { + break; + } +} +assert(i == 2); + +sum = 0; +i = 0; +while (i < 5) { + if (i == 2) { + i += 1; + continue; + } + sum += i++; +} +assert(sum == 8); \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/while/condition_not_bool.ql b/src/test/resources/testsuite/independent/while/condition_not_bool.ql new file mode 100644 index 0000000..d8b74d4 --- /dev/null +++ b/src/test/resources/testsuite/independent/while/condition_not_bool.ql @@ -0,0 +1,8 @@ +/* +{ + "errCode": "WHILE_CONDITION_BOOL_REQUIRED" +} +*/ +while (1) { + true; +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/while/missing_lparen.ql b/src/test/resources/testsuite/independent/while/missing_lparen.ql new file mode 100644 index 0000000..90b5b4d --- /dev/null +++ b/src/test/resources/testsuite/independent/while/missing_lparen.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +while i < 5 { +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/while/missing_rparen.ql b/src/test/resources/testsuite/independent/while/missing_rparen.ql new file mode 100644 index 0000000..fed14c7 --- /dev/null +++ b/src/test/resources/testsuite/independent/while/missing_rparen.ql @@ -0,0 +1,9 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +i = 0; +while (i < 5 { + i++; +} \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/while/while.ql b/src/test/resources/testsuite/independent/while/while.ql new file mode 100644 index 0000000..68d13e2 --- /dev/null +++ b/src/test/resources/testsuite/independent/while/while.ql @@ -0,0 +1,8 @@ +i = 0; +sum = 0; +// m not in scope +while (i < 4 && m == null) { + int m = 10; + sum += (i++); +} +assert(sum==6); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/arr_index_out_of_bound.ql b/src/test/resources/testsuite/java/array/arr_index_out_of_bound.ql new file mode 100644 index 0000000..2603964 --- /dev/null +++ b/src/test/resources/testsuite/java/array/arr_index_out_of_bound.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "INDEX_OUT_BOUND" +} +*/ +int[] is = new int[0]; +is[1] \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/arr_literal.ql b/src/test/resources/testsuite/java/array/arr_literal.ql new file mode 100644 index 0000000..4cfb356 --- /dev/null +++ b/src/test/resources/testsuite/java/array/arr_literal.ql @@ -0,0 +1,3 @@ +int[] ii = new int[]{1,2,3}; +assert(ii[2] == 3); +assert(ii[-1] == ii[2]); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/arr_with_init_item.ql b/src/test/resources/testsuite/java/array/arr_with_init_item.ql new file mode 100644 index 0000000..3fe35b2 --- /dev/null +++ b/src/test/resources/testsuite/java/array/arr_with_init_item.ql @@ -0,0 +1,6 @@ +int[] os = new int[] {1,2,3,4}; +assert(os[1] == 2); + +int[][] os = new int[][] {new int[] {1,2}, new int[] {3,4}}; +assert(os[0][1] == 2); +assert(os[1][0] == 3); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/array_item_type_convert.ql b/src/test/resources/testsuite/java/array/array_item_type_convert.ql new file mode 100644 index 0000000..349741f --- /dev/null +++ b/src/test/resources/testsuite/java/array/array_item_type_convert.ql @@ -0,0 +1,4 @@ +int[] is = new int[] {1L, 5.67}; +assert(is[1] == 5); +is[1] = 4.56d; +assert(is[1] == 4); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/array_slice.ql b/src/test/resources/testsuite/java/array/array_slice.ql new file mode 100644 index 0000000..895bf4f --- /dev/null +++ b/src/test/resources/testsuite/java/array/array_slice.ql @@ -0,0 +1,11 @@ +a = new int[] {1, 2, 3, 4}; +assert(a[1:2][0] == 2); +b = a[-88:100]; +assert(b.length == 4); +assert(b[0] == 1); +assert(b[1] == 2); +assert(b[2] == 3); +assert(b[3] == 4); +assert(a[:1].length == 1); +assert(a[:1][0] == 1); +assert(a[3:][0] == 4); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/comma_absent.ql b/src/test/resources/testsuite/java/array/comma_absent.ql new file mode 100644 index 0000000..b8c926e --- /dev/null +++ b/src/test/resources/testsuite/java/array/comma_absent.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +new int[] {1 2} \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/invalid_arr_define.ql b/src/test/resources/testsuite/java/array/invalid_arr_define.ql new file mode 100644 index 0000000..4ab654b --- /dev/null +++ b/src/test/resources/testsuite/java/array/invalid_arr_define.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +int[][] a = new int[][]; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/invalid_arr_item.ql b/src/test/resources/testsuite/java/array/invalid_arr_item.ql new file mode 100644 index 0000000..97a3792 --- /dev/null +++ b/src/test/resources/testsuite/java/array/invalid_arr_item.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "INCOMPATIBLE_ARRAY_ITEM_TYPE" +} +*/ +List[] s = new List[] {true, false}; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/invalid_arr_size_define.ql b/src/test/resources/testsuite/java/array/invalid_arr_size_define.ql new file mode 100644 index 0000000..6432676 --- /dev/null +++ b/src/test/resources/testsuite/java/array/invalid_arr_size_define.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +new int[][10][] \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/invalid_arr_size_type.ql b/src/test/resources/testsuite/java/array/invalid_arr_size_type.ql new file mode 100644 index 0000000..2684cbe --- /dev/null +++ b/src/test/resources/testsuite/java/array/invalid_arr_size_type.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "ARRAY_SIZE_NUM_REQUIRED" +} +*/ +a = 'aaa'; +new int[a]; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/multi_dim_array.ql b/src/test/resources/testsuite/java/array/multi_dim_array.ql new file mode 100644 index 0000000..270e2f1 --- /dev/null +++ b/src/test/resources/testsuite/java/array/multi_dim_array.ql @@ -0,0 +1,4 @@ +long[][] ls = new long[2][3]; +assert(ls[0][0] == null); +assert(ls.length == 2); +assert(ls[1].length == 3); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/array/type_arr.ql b/src/test/resources/testsuite/java/array/type_arr.ql new file mode 100644 index 0000000..634754d --- /dev/null +++ b/src/test/resources/testsuite/java/array/type_arr.ql @@ -0,0 +1,2 @@ +Object[] os = new Object[10]; +os.length == 10; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/cast/assignable_cast.ql b/src/test/resources/testsuite/java/cast/assignable_cast.ql new file mode 100644 index 0000000..b2e60d2 --- /dev/null +++ b/src/test/resources/testsuite/java/cast/assignable_cast.ql @@ -0,0 +1,5 @@ +Integer a = 1; +assert((Number) a == 1); + +Number b = 1; +assert((Integer) b == 1); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/cast/define_local_cast.ql b/src/test/resources/testsuite/java/cast/define_local_cast.ql new file mode 100644 index 0000000..ccbc318 --- /dev/null +++ b/src/test/resources/testsuite/java/cast/define_local_cast.ql @@ -0,0 +1,2 @@ +double b = 12; +assert(b instanceof double) \ No newline at end of file diff --git a/src/test/resources/testsuite/java/cast/object_cast.ql b/src/test/resources/testsuite/java/cast/object_cast.ql new file mode 100644 index 0000000..ddb4e6c --- /dev/null +++ b/src/test/resources/testsuite/java/cast/object_cast.ql @@ -0,0 +1,2 @@ +Integer a = 1; +assert((Object) a == 1); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/cast/string_cast.ql b/src/test/resources/testsuite/java/cast/string_cast.ql new file mode 100644 index 0000000..1053520 --- /dev/null +++ b/src/test/resources/testsuite/java/cast/string_cast.ql @@ -0,0 +1,3 @@ +Object a = null; +String b = (String) a; +assertFalse("null".equals(b)); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/for/for_each_array.ql b/src/test/resources/testsuite/java/for/for_each_array.ql new file mode 100644 index 0000000..cfe72f3 --- /dev/null +++ b/src/test/resources/testsuite/java/for/for_each_array.ql @@ -0,0 +1,12 @@ +i = 0; +for (ele: new int[] {1,2,3}) { + assert(ele == (++i)); +} + +nestedArr = new int[][] {new int[] {1,2}, new int[] {3,4}}; +j = 0; +for (arr: nestedArr) { + for (int ele: arr) { + assert(ele == (++j)); + } +} diff --git a/src/test/resources/testsuite/java/generics/generics.ql b/src/test/resources/testsuite/java/generics/generics.ql new file mode 100644 index 0000000..7af7db5 --- /dev/null +++ b/src/test/resources/testsuite/java/generics/generics.ql @@ -0,0 +1,4 @@ +Map m; +Map> m2; +Map> m3; +Map<> m4; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/generics/invalid_type_bound.ql b/src/test/resources/testsuite/java/generics/invalid_type_bound.ql new file mode 100644 index 0000000..ff7fb30 --- /dev/null +++ b/src/test/resources/testsuite/java/generics/invalid_type_bound.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +List l; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/implicit/arithmetic.ql b/src/test/resources/testsuite/java/implicit/arithmetic.ql new file mode 100644 index 0000000..8c3f141 --- /dev/null +++ b/src/test/resources/testsuite/java/implicit/arithmetic.ql @@ -0,0 +1,16 @@ +//trans after arithmetic +byte b1 = 1; +byte b2 = 2; +int i = b1 + b2; +assert(i == 3); + +//trans in arithmetic +int a = 1; +String s = a + "q"; +assert(s.equals("1q")); + +//trans in arithmetic mixed +int imax = Integer.MAX_VALUE; +int imin = Integer.MIN_VALUE; +long s = imax - (long)imin; +assert(s == 4294967295l); diff --git a/src/test/resources/testsuite/java/implicit/assignment_basic.ql b/src/test/resources/testsuite/java/implicit/assignment_basic.ql new file mode 100644 index 0000000..e473433 --- /dev/null +++ b/src/test/resources/testsuite/java/implicit/assignment_basic.ql @@ -0,0 +1,32 @@ +byte a = 0; +assert(a == 0); + +//byte→short +short s = a; +assert(s == 0); + +//byte→char +char r = a; +assert(r == 0); + +//byte→int +int i = a; +assert(i == 0); + +//byte→float +float f = a; +assert(f == 0.0); + +//byte→double +double d = a; +assert(d == 0); + +//byte→long +long l = a; +assert(l == 0); + +//long→double +double d1 = l; +assert(d1 == 0); + + diff --git a/src/test/resources/testsuite/java/implicit/assignment_extend.ql b/src/test/resources/testsuite/java/implicit/assignment_extend.ql new file mode 100644 index 0000000..71cd786 --- /dev/null +++ b/src/test/resources/testsuite/java/implicit/assignment_extend.ql @@ -0,0 +1,43 @@ +//double→(Double)→long(lost precise) +double d2 = 0.1d; +long l2 = d2; +assert(l2 == 0l); + +//double→(Double)→int(lost precise && use max bound) +d2 = 11111111111.1d; +int i1 = d2; +assert(i1 == 2147483647); + +//double→(Double)→int(lost precise && use min bound) +d2 = -11111111111.1d; +int i2 = d2; +assert(i2 == -2147483648); + +//double->BigInteger +BigInteger big = d2; +assert(big == -11111111111); + +//long→BigInteger +BigInteger big = 11111111111l; +assert(big == 11111111111l); + +//long→BigDecimal +BigDecimal big = 11111111111l; +assert(big == 11111111111l); + +//long→BigDecimal +BigDecimal big = 11111111111l; +assert(big == 11111111111l); + + +//long→BigDecimal +BigDecimal big = 11111111111l; +long l1 = big; +assert(l1 == 11111111111l); + +//String→char +String s1 = "a"; +char c1 = s1; +assert(c1 == 97); + + diff --git a/src/test/resources/testsuite/java/implicit/function_param.ql b/src/test/resources/testsuite/java/implicit/function_param.ql new file mode 100644 index 0000000..37bcc1f --- /dev/null +++ b/src/test/resources/testsuite/java/implicit/function_param.ql @@ -0,0 +1,19 @@ +function add(int a, long b){ + return a + b; +}; + +//param trans +assert(add(1L,444L) == 445L); + +//return object +assert(add(Integer.MAX_VALUE,444L) == 2147484091L); + +//result trans +assert(add(1L,444L) == 445d); + +function convertChar(char a) { + return a; +} + +assert(convertChar('a')==(char)'a'); +assert(convertChar((String)'a')==(char)'a'); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/implicit/incompatible_assignment_type.ql b/src/test/resources/testsuite/java/implicit/incompatible_assignment_type.ql new file mode 100644 index 0000000..a5d285f --- /dev/null +++ b/src/test/resources/testsuite/java/implicit/incompatible_assignment_type.ql @@ -0,0 +1,9 @@ +assertErrorCode(() -> { + String s = "as"; + char a = s; +}, "INCOMPATIBLE_ASSIGNMENT_TYPE"); + +assertErrorCode(() -> { + String s = "as"; + s = 1; +}, "INCOMPATIBLE_ASSIGNMENT_TYPE"); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/implicit/packing.ql b/src/test/resources/testsuite/java/implicit/packing.ql new file mode 100644 index 0000000..2836b2e --- /dev/null +++ b/src/test/resources/testsuite/java/implicit/packing.ql @@ -0,0 +1,16 @@ +int i = 1; +Integer ig = i; +assert(ig == 1); +assert(ig instanceof Integer); + +int i1 = ig; +assert(i1 == 1); + +double d = 0.1d; +Double ds = d; +assert(ds == 0.1d); + +float f = 0.22f; +Float f1 = f; +assert(f1 == 0.22f); + diff --git a/src/test/resources/testsuite/java/implicit/pointer.ql b/src/test/resources/testsuite/java/implicit/pointer.ql new file mode 100644 index 0000000..a9acbfc --- /dev/null +++ b/src/test/resources/testsuite/java/implicit/pointer.ql @@ -0,0 +1,6 @@ +//pointer in array +int[] array = new int[]{1,2,3}; +Long[] newArray = new Long[]{array[0], array[1], array[2]}; +assert(newArray[0] == 1); +assert(newArray[1] == 2); +assert(newArray[2] == 3); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/import/import_class.ql b/src/test/resources/testsuite/java/import/import_class.ql new file mode 100644 index 0000000..4a47225 --- /dev/null +++ b/src/test/resources/testsuite/java/import/import_class.ql @@ -0,0 +1,2 @@ +import com.alibaba.qlexpress4.inport.Sample; +assert(Sample.value == 1); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/import/import_not_at_beginning.ql b/src/test/resources/testsuite/java/import/import_not_at_beginning.ql new file mode 100644 index 0000000..b758f98 --- /dev/null +++ b/src/test/resources/testsuite/java/import/import_not_at_beginning.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +a = 10; +import a.b.c; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/import/import_not_end_with_semi.ql b/src/test/resources/testsuite/java/import/import_not_end_with_semi.ql new file mode 100644 index 0000000..21fda6b --- /dev/null +++ b/src/test/resources/testsuite/java/import/import_not_end_with_semi.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +import a.b +v = 1 \ No newline at end of file diff --git a/src/test/resources/testsuite/java/import/import_pack_not_end_with_semi.ql b/src/test/resources/testsuite/java/import/import_pack_not_end_with_semi.ql new file mode 100644 index 0000000..400570a --- /dev/null +++ b/src/test/resources/testsuite/java/import/import_pack_not_end_with_semi.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +import a.b.c.*- \ No newline at end of file diff --git a/src/test/resources/testsuite/java/import/import_package.ql b/src/test/resources/testsuite/java/import/import_package.ql new file mode 100644 index 0000000..9abeb2e --- /dev/null +++ b/src/test/resources/testsuite/java/import/import_package.ql @@ -0,0 +1,3 @@ +import com.alibaba.qlexpress4.inport.*; +assert(Sample.value == 1); +assert(Sample1.value == 10); diff --git a/src/test/resources/testsuite/java/import/import_star.ql b/src/test/resources/testsuite/java/import/import_star.ql new file mode 100644 index 0000000..d26e0fd --- /dev/null +++ b/src/test/resources/testsuite/java/import/import_star.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +import *; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/import/incomplete_import.ql b/src/test/resources/testsuite/java/import/incomplete_import.ql new file mode 100644 index 0000000..c5f6b40 --- /dev/null +++ b/src/test/resources/testsuite/java/import/incomplete_import.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +import a.b. \ No newline at end of file diff --git a/src/test/resources/testsuite/java/import/invalid_package.ql b/src/test/resources/testsuite/java/import/invalid_package.ql new file mode 100644 index 0000000..ad66b99 --- /dev/null +++ b/src/test/resources/testsuite/java/import/invalid_package.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +import a.b.- \ No newline at end of file diff --git a/src/test/resources/testsuite/java/import/multi_import.ql b/src/test/resources/testsuite/java/import/multi_import.ql new file mode 100644 index 0000000..cfc0c12 --- /dev/null +++ b/src/test/resources/testsuite/java/import/multi_import.ql @@ -0,0 +1,4 @@ +import com.alibaba.qlexpress4.inport.Sample; +import com.alibaba.qlexpress4.inport.Sample1; +assert(Sample.value == 1); +assert(Sample1.value == 10); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/import/not_support_import_static.ql b/src/test/resources/testsuite/java/import/not_support_import_static.ql new file mode 100644 index 0000000..2ccd74a --- /dev/null +++ b/src/test/resources/testsuite/java/import/not_support_import_static.ql @@ -0,0 +1,6 @@ +/* +{ + "errCode": "SYNTAX_ERROR" +} +*/ +import static ab.Assert.*; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/lambda/java_functional_interface.ql b/src/test/resources/testsuite/java/lambda/java_functional_interface.ql new file mode 100644 index 0000000..aaac84e --- /dev/null +++ b/src/test/resources/testsuite/java/lambda/java_functional_interface.ql @@ -0,0 +1,16 @@ +Runnable r = () -> a = 8; +r.run(); +assert(a == 8); + +Supplier s = () -> "test"; +assert(s.get() == 'test'); + +Consumer c = (a) -> b = a + "-te"; +c.accept("ccc"); +assert(b == 'ccc-te'); + +Function f = a -> a + 3; +assert(f.apply(1) == 4); + +Function f1 = (a, b) -> a + b; +assert(f1.apply("test-") == "test-null"); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/lambda/lambda_implicit.ql b/src/test/resources/testsuite/java/lambda/lambda_implicit.ql new file mode 100644 index 0000000..3872170 --- /dev/null +++ b/src/test/resources/testsuite/java/lambda/lambda_implicit.ql @@ -0,0 +1,5 @@ +a = { + func: (long c) -> c + 1 +}; + +assert(a.func(1) == 2L); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/lambda/lambda_method.ql b/src/test/resources/testsuite/java/lambda/lambda_method.ql new file mode 100644 index 0000000..047f87d --- /dev/null +++ b/src/test/resources/testsuite/java/lambda/lambda_method.ql @@ -0,0 +1,5 @@ +a = { + func: (int c) -> c + 1 +}; + +assert(a.func(1) == 2); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/lambda/user_functional_interface.ql b/src/test/resources/testsuite/java/lambda/user_functional_interface.ql new file mode 100644 index 0000000..013d43c --- /dev/null +++ b/src/test/resources/testsuite/java/lambda/user_functional_interface.ql @@ -0,0 +1,4 @@ +import com.alibaba.qlexpress4.test.lambda.UserFunctionalInterface; + +UserFunctionalInterface ufi = (a, b) -> a + b; +assert(ufi.lala(1,2) == 3); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/map/classified_json.ql b/src/test/resources/testsuite/java/map/classified_json.ql new file mode 100644 index 0000000..fe190c4 --- /dev/null +++ b/src/test/resources/testsuite/java/map/classified_json.ql @@ -0,0 +1,42 @@ +// tag::classifiedJson[] +myHome = { + '@class': 'com.alibaba.qlexpress4.inport.MyHome', + 'sofa': 'a-sofa', + 'chair': 'b-chair', + 'myDesk': { + 'book1': 'Then Moon and Sixpence', + '@class': 'com.alibaba.qlexpress4.inport.MyDesk' + }, + // ignore field that don't exist + 'notexist': 1234 +} +assert(myHome.getSofa()=='a-sofa') +assert(myHome instanceof com.alibaba.qlexpress4.inport.MyHome) +assert(myHome.getMyDesk().getBook1()=='Then Moon and Sixpence') +assert(myHome.getMyDesk() instanceof com.alibaba.qlexpress4.inport.MyDesk) +// end::classifiedJson[] + +// @class override +myDesk = { + '@class': 'com.alibaba.qlexpress4.inport.MyHome', + 'book1': 'Then Moon and Sixpence', + '@class': 'com.alibaba.qlexpress4.inport.MyDesk' +} +assert(myDesk instanceof com.alibaba.qlexpress4.inport.MyDesk) + +// cls not exist +m = { + '@class': 'notexist', + 'book1': 'Then Moon and Sixpence' +} +assert(m["@class"]=='notexist') +assert(m instanceof Map) + +// invalid assignment +assertErrorCode(() -> { + '@class': 'com.alibaba.qlexpress4.inport.MyHome', + 'sofa': 'a-sofa', + 'chair': 'b-chair', + 'bed': 'c-bed' +}, "INVALID_ASSIGNMENT") + diff --git a/src/test/resources/testsuite/java/map/equal_to_hash_map.ql b/src/test/resources/testsuite/java/map/equal_to_hash_map.ql new file mode 100644 index 0000000..e5161e9 --- /dev/null +++ b/src/test/resources/testsuite/java/map/equal_to_hash_map.ql @@ -0,0 +1,13 @@ +dict = {a: 123, b: 234, c:[1,2,3]}; +map = new HashMap(); +map.put('a', 123); +map.put('b', 234); +map.put('c', [1,2,3]); +assert(dict == map); +assert(dict.get('a') == 123); +assert(dict.a == 123); +assert(dict['a'] == 123); +dict.d = 100; +dict['e'] = 200; +assert(dict.d == 100); +assert(dict.e == 200); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/method/method_invoke.ql b/src/test/resources/testsuite/java/method/method_invoke.ql new file mode 100644 index 0000000..d605a62 --- /dev/null +++ b/src/test/resources/testsuite/java/method/method_invoke.ql @@ -0,0 +1,7 @@ +import com.alibaba.qlexpress4.test.method.TestChild; + +tc = new TestChild(); +assert(tc.get10() == 10); +assert(tc.get10('') == 11); +assert(tc.get1() == 1); +assert(tc.get100() == 100); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/method_reference/class_method.ql b/src/test/resources/testsuite/java/method_reference/class_method.ql new file mode 100644 index 0000000..5fb6de9 --- /dev/null +++ b/src/test/resources/testsuite/java/method_reference/class_method.ql @@ -0,0 +1,8 @@ +// class static method + +a = Integer::max; +assert(a(1,2) == 2); + +// class member method +c = String::concat +assert(c("aa", "-bb") == "aa-bb") \ No newline at end of file diff --git a/src/test/resources/testsuite/java/method_reference/class_obj_method.ql b/src/test/resources/testsuite/java/method_reference/class_obj_method.ql new file mode 100644 index 0000000..84ae6b8 --- /dev/null +++ b/src/test/resources/testsuite/java/method_reference/class_obj_method.ql @@ -0,0 +1,2 @@ +refer = Integer.class::isArray; +assert(!refer()); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/method_reference/method_not_found.ql b/src/test/resources/testsuite/java/method_reference/method_not_found.ql new file mode 100644 index 0000000..d8936d2 --- /dev/null +++ b/src/test/resources/testsuite/java/method_reference/method_not_found.ql @@ -0,0 +1,7 @@ +/* +{ + "errCode": "INVALID_ARGUMENT" +} +*/ +referNotExist = Integer.class::notExist; +referNotExist() \ No newline at end of file diff --git a/src/test/resources/testsuite/java/method_reference/object_method.ql b/src/test/resources/testsuite/java/method_reference/object_method.ql new file mode 100644 index 0000000..26673de --- /dev/null +++ b/src/test/resources/testsuite/java/method_reference/object_method.ql @@ -0,0 +1,5 @@ +import com.alibaba.qlexpress4.test.property.Sample; + +refer = new Sample(3)::getCount; +assert(refer() == 3); +assert(new Sample(3).getCount() == 3); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/newexpr/new_resolver.ql b/src/test/resources/testsuite/java/newexpr/new_resolver.ql new file mode 100644 index 0000000..061e5f2 --- /dev/null +++ b/src/test/resources/testsuite/java/newexpr/new_resolver.ql @@ -0,0 +1,25 @@ +import com.alibaba.qlexpress4.test.constructor.HelloConstructor; +import com.alibaba.qlexpress4.test.constructor.HelloParent; +import com.alibaba.qlexpress4.test.constructor.HelloChild; + +h = new HelloConstructor(new HelloParent()); +assert(h.flag == 0); + +h = new HelloConstructor(new HelloChild()); +assert(h.flag == 1); + +h = new HelloConstructor("s", "s1"); +assert(h.flag == 2); + +h = new HelloConstructor("s"); +assert(h.flag == 3); + +h = new HelloConstructor(new HelloChild(), () -> 12); +assert(h.flag == 4); + +h = new HelloConstructor(new HelloParent(), () -> 12); +assert(h.flag == 5); + +// var args +h = new HelloConstructor(); +assert(h.flag == 2); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/newexpr/noArgument.ql b/src/test/resources/testsuite/java/newexpr/noArgument.ql new file mode 100644 index 0000000..7fda9a6 --- /dev/null +++ b/src/test/resources/testsuite/java/newexpr/noArgument.ql @@ -0,0 +1,3 @@ +a = new HashMap(); +a['m'] = 12; +assert(a['m'] == 12) \ No newline at end of file diff --git a/src/test/resources/testsuite/java/newexpr/no_match_constructor.ql b/src/test/resources/testsuite/java/newexpr/no_match_constructor.ql new file mode 100644 index 0000000..c1f3997 --- /dev/null +++ b/src/test/resources/testsuite/java/newexpr/no_match_constructor.ql @@ -0,0 +1,8 @@ +/* +{ + "errCode": "NO_SUITABLE_CONSTRUCTOR" +} +*/ +import com.alibaba.qlexpress4.test.constructor.HelloConstructor; + +HelloConstructor h = new HelloConstructor(12, 13, "sss"); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/number/long_max_value.ql b/src/test/resources/testsuite/java/number/long_max_value.ql new file mode 100644 index 0000000..ac5ce5f --- /dev/null +++ b/src/test/resources/testsuite/java/number/long_max_value.ql @@ -0,0 +1,2 @@ +a = Long.MIN_VALUE; +assert(a == -9223372036854775808L); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/number/min_value_not_equal_to_hex.ql b/src/test/resources/testsuite/java/number/min_value_not_equal_to_hex.ql new file mode 100644 index 0000000..ace8503 --- /dev/null +++ b/src/test/resources/testsuite/java/number/min_value_not_equal_to_hex.ql @@ -0,0 +1,3 @@ +assert(Integer.MIN_VALUE != 0x80000000); +assert(Integer.MIN_VALUE == -0x80000000); +assert(Integer.MIN_VALUE == (int)0x80000000); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/number/number_auto_type.ql b/src/test/resources/testsuite/java/number/number_auto_type.ql new file mode 100644 index 0000000..97b9b8c --- /dev/null +++ b/src/test/resources/testsuite/java/number/number_auto_type.ql @@ -0,0 +1,6 @@ +assert(2147483647 instanceof Integer); +assert(9223372036854775807 instanceof Long); +assert(18446744073709552000 instanceof BigInteger); +// 0.25 can be precisely presented with double +assert(0.25 instanceof Double); +assert(2.7976931348623157E308 instanceof BigDecimal); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/number/number_invoke.ql b/src/test/resources/testsuite/java/number/number_invoke.ql new file mode 100644 index 0000000..75388e7 --- /dev/null +++ b/src/test/resources/testsuite/java/number/number_invoke.ql @@ -0,0 +1,2 @@ +assert(1.doubleValue() == 1.d) +assert(2.floatValue() == 2f) \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/array_length_get.ql b/src/test/resources/testsuite/java/property/array_length_get.ql new file mode 100644 index 0000000..10602f7 --- /dev/null +++ b/src/test/resources/testsuite/java/property/array_length_get.ql @@ -0,0 +1,2 @@ +Object[] a = new Object[3]; +assert(a.length == 3); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/class_get.ql b/src/test/resources/testsuite/java/property/class_get.ql new file mode 100644 index 0000000..52cfb4a --- /dev/null +++ b/src/test/resources/testsuite/java/property/class_get.ql @@ -0,0 +1,2 @@ +a = Integer.class; +assert(a == Integer.class); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/enum_get.ql b/src/test/resources/testsuite/java/property/enum_get.ql new file mode 100644 index 0000000..4942d2d --- /dev/null +++ b/src/test/resources/testsuite/java/property/enum_get.ql @@ -0,0 +1,4 @@ +import com.alibaba.qlexpress4.test.property.SampleEnum; + +SampleEnum s = SampleEnum.NORMAL; +assert(s.equals(SampleEnum.NORMAL)) \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/enum_get_not_exist.ql b/src/test/resources/testsuite/java/property/enum_get_not_exist.ql new file mode 100644 index 0000000..1256b43 --- /dev/null +++ b/src/test/resources/testsuite/java/property/enum_get_not_exist.ql @@ -0,0 +1,8 @@ +/* +{ + "errCode": "FIELD_NOT_FOUND" +} +*/ +import com.alibaba.qlexpress4.test.property.SampleEnum; + +SampleEnum s = SampleEnum.NOT_EXIST; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/enum_member_field.ql b/src/test/resources/testsuite/java/property/enum_member_field.ql new file mode 100644 index 0000000..1753a45 --- /dev/null +++ b/src/test/resources/testsuite/java/property/enum_member_field.ql @@ -0,0 +1,4 @@ +s = com.alibaba.qlexpress4.test.property.SampleEnum.NORMAL; +assert(s.testField == 10); +assert(s.testStaticField == 1000); +assert(com.alibaba.qlexpress4.test.property.SampleEnum.testStaticField == 1000); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/interface_const_field.ql b/src/test/resources/testsuite/java/property/interface_const_field.ql new file mode 100644 index 0000000..f552aad --- /dev/null +++ b/src/test/resources/testsuite/java/property/interface_const_field.ql @@ -0,0 +1,3 @@ +import com.alibaba.qlexpress4.test.property.SomeInter; + +assert(SomeInter.INTER_CONST_1=='test1') \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/jsonobject_vs_map_put.ql b/src/test/resources/testsuite/java/property/jsonobject_vs_map_put.ql new file mode 100644 index 0000000..b9958c8 --- /dev/null +++ b/src/test/resources/testsuite/java/property/jsonobject_vs_map_put.ql @@ -0,0 +1,27 @@ +// Test for JSONObject put method support +import java.util.HashMap; +import java.util.Map; +import com.alibaba.fastjson2.JSON; +import com.alibaba.qlexpress4.test.property.Parent; +import com.alibaba.fastjson2.JSONObject; + +// Test JSONObject put method (now expected to work) +p = new Parent(); +p.lockStatus = 1; +p.lockStatus2 = 1; +JSONObject reportData = new JSONObject(); +reportData.put("doorStatus", p.lockStatus); +reportData.put("doorStatus2", p.lockStatus2); +reportData.put("operateType", "0"); +reportData.put("openType", 1); + +String jsonString = JSON.toJSONString(reportData); + +assert(reportData.get("doorStatus") == 1); +assert(reportData.get("doorStatus2") == 1); + +assert(reportData.get("operateType").equals("0")); +assert(reportData.get("openType") == 1); + + +return reportData; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/null_set_invoke.ql b/src/test/resources/testsuite/java/property/null_set_invoke.ql new file mode 100644 index 0000000..94beeb0 --- /dev/null +++ b/src/test/resources/testsuite/java/property/null_set_invoke.ql @@ -0,0 +1,11 @@ +import com.alibaba.qlexpress4.test.property.Parent; + +p = new Parent(); +p.setBirth(null); +assert(p.birth==null); + +p.birth = "2025-01-01"; +assert(p.birth=="2025-01-01"); + +p.birth = null; +assert(p.birth==null); diff --git a/src/test/resources/testsuite/java/property/private_member_attr_access_get.ql b/src/test/resources/testsuite/java/property/private_member_attr_access_get.ql new file mode 100644 index 0000000..63fc6e9 --- /dev/null +++ b/src/test/resources/testsuite/java/property/private_member_attr_access_get.ql @@ -0,0 +1,9 @@ +/* +{ + "initOptions": InitOptions.builder().allowPrivateAccess(true) +} +*/ +import com.alibaba.qlexpress4.test.property.SampleForPrivate; + +SampleForPrivate a = new SampleForPrivate(5); +assert(a.count == 5); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/private_member_attr_access_set.ql b/src/test/resources/testsuite/java/property/private_member_attr_access_set.ql new file mode 100644 index 0000000..a388b5e --- /dev/null +++ b/src/test/resources/testsuite/java/property/private_member_attr_access_set.ql @@ -0,0 +1,10 @@ +/* +{ + "initOptions": InitOptions.builder().allowPrivateAccess(true) +} +*/ +import com.alibaba.qlexpress4.test.property.SampleForPrivate; + +SampleForPrivate a = new SampleForPrivate(4); +a.count = 5; +assert(a.count == 5); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/private_member_attr_getter.ql b/src/test/resources/testsuite/java/property/private_member_attr_getter.ql new file mode 100644 index 0000000..07979e1 --- /dev/null +++ b/src/test/resources/testsuite/java/property/private_member_attr_getter.ql @@ -0,0 +1,4 @@ +import com.alibaba.qlexpress4.test.property.Sample; + +Sample a = new Sample(5); +assert(a.count == 5); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/private_member_attr_not_access_get.ql b/src/test/resources/testsuite/java/property/private_member_attr_not_access_get.ql new file mode 100644 index 0000000..c821897 --- /dev/null +++ b/src/test/resources/testsuite/java/property/private_member_attr_not_access_get.ql @@ -0,0 +1,9 @@ +/* +{ + "errCode": "FIELD_NOT_FOUND" +} +*/ +import com.alibaba.qlexpress4.test.property.SampleForPrivate; + +SampleForPrivate a = new SampleForPrivate(5); +assert(a.count == 5); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/private_member_attr_not_access_set.ql b/src/test/resources/testsuite/java/property/private_member_attr_not_access_set.ql new file mode 100644 index 0000000..2125328 --- /dev/null +++ b/src/test/resources/testsuite/java/property/private_member_attr_not_access_set.ql @@ -0,0 +1,10 @@ +/* +{ + "errCode": "FIELD_NOT_FOUND" +} +*/ +import com.alibaba.qlexpress4.test.property.SampleForPrivate; + +SampleForPrivate a = new SampleForPrivate(4); +a.count = 5; +assert(a.count == 5); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/private_member_attr_setter.ql b/src/test/resources/testsuite/java/property/private_member_attr_setter.ql new file mode 100644 index 0000000..7d67017 --- /dev/null +++ b/src/test/resources/testsuite/java/property/private_member_attr_setter.ql @@ -0,0 +1,5 @@ +import com.alibaba.qlexpress4.test.property.Sample; + +Sample a = new Sample(4); +a.count = 5; +assert(a.count == 5); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/private_member_set_not_accessible.ql b/src/test/resources/testsuite/java/property/private_member_set_not_accessible.ql new file mode 100644 index 0000000..41dbf21 --- /dev/null +++ b/src/test/resources/testsuite/java/property/private_member_set_not_accessible.ql @@ -0,0 +1,12 @@ +/* +{ + "errCode": "INVALID_ASSIGNMENT" +} +*/ +import com.alibaba.qlexpress4.test.property.TestEnum; + +v = TestEnum.SKT.value; +assert(v == -1); +v = 10; +assert(v == 10); +TestEnum.SKT.value = 100; \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/public_member_set.ql b/src/test/resources/testsuite/java/property/public_member_set.ql new file mode 100644 index 0000000..9a32c09 --- /dev/null +++ b/src/test/resources/testsuite/java/property/public_member_set.ql @@ -0,0 +1,5 @@ +import com.alibaba.qlexpress4.test.property.SampleSet; + +SampleSet a = new SampleSet(); +a.count = 5; +assert(a.count == 5); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/property/public_static.ql b/src/test/resources/testsuite/java/property/public_static.ql new file mode 100644 index 0000000..132807d --- /dev/null +++ b/src/test/resources/testsuite/java/property/public_static.ql @@ -0,0 +1,2 @@ +a = Integer.MAX_VALUE; +assert(a == 0x7fffffff); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/stream/java_stream.ql b/src/test/resources/testsuite/java/stream/java_stream.ql new file mode 100644 index 0000000..279926b --- /dev/null +++ b/src/test/resources/testsuite/java/stream/java_stream.ql @@ -0,0 +1,7 @@ +l = ["a-111", "a-222", "b-333", "c-888"] + +l2 = l.stream() + .filter(i -> i.startsWith("a-")) + .map(i -> i.split("-")[1]) + .collect(Collectors.toList()); +assert(l2 == ["111", "222"]); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/stream/java_stream_method_ref.ql b/src/test/resources/testsuite/java/stream/java_stream_method_ref.ql new file mode 100644 index 0000000..24498f8 --- /dev/null +++ b/src/test/resources/testsuite/java/stream/java_stream_method_ref.ql @@ -0,0 +1,6 @@ +import com.alibaba.qlexpress4.test.stream.STObject; + +l = [new STObject("aa"), new STObject("bb")].stream().map(STObject::getPayload).collect(Collectors.toList()) + +println(l) +assert(l == ["aa", "bb"]) \ No newline at end of file diff --git a/src/test/resources/testsuite/java/trycatch/catch_java_exception.ql b/src/test/resources/testsuite/java/trycatch/catch_java_exception.ql new file mode 100644 index 0000000..92c4189 --- /dev/null +++ b/src/test/resources/testsuite/java/trycatch/catch_java_exception.ql @@ -0,0 +1,8 @@ +try { + BigDecimal divisor = new BigDecimal(3); + BigDecimal dividend = new BigDecimal(1); + dividend.divide(divisor); + assert(false); +} catch (ArithmeticException e) { + assert(e != null); +} diff --git a/src/test/resources/testsuite/java/trycatch/catch_operator_exception.ql b/src/test/resources/testsuite/java/trycatch/catch_operator_exception.ql new file mode 100644 index 0000000..b27bc53 --- /dev/null +++ b/src/test/resources/testsuite/java/trycatch/catch_operator_exception.ql @@ -0,0 +1,6 @@ +a = try { + 1/0 +} catch(ArithmeticException e) { + 2+2 +}; +assert(a == 4); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/trycatch/catch_order.ql b/src/test/resources/testsuite/java/trycatch/catch_order.ql new file mode 100644 index 0000000..9161cbb --- /dev/null +++ b/src/test/resources/testsuite/java/trycatch/catch_order.ql @@ -0,0 +1,10 @@ +f = e -> try { + throw e; +} catch (NullPointerException n) { + 100 +} catch (Exception e) { + 10 +}; + +assert(f(new NullPointerException()) == 100); +assert(f(new Exception()) == 10); \ No newline at end of file diff --git a/src/test/resources/testsuite/java/trycatch/ql_npe.ql b/src/test/resources/testsuite/java/trycatch/ql_npe.ql new file mode 100644 index 0000000..ded4c4c --- /dev/null +++ b/src/test/resources/testsuite/java/trycatch/ql_npe.ql @@ -0,0 +1,22 @@ +f = npe -> try { + npe(); + return 10; +} catch(NullPointerException n) { + return 100; +}; + +assert(f(() -> {a=null;a.b;}) == 100); + +assert(f(() -> {a=null;a::b;}) == 100); + +assert(f(null) == 100); + +assert(f(() -> { + a= () -> null; + a()(); +}) == 100); + +assert(f(() -> { + a= null; + a.b(); +}) == 100); \ No newline at end of file