├── rebar.lock ├── .gitignore ├── rebar.config ├── src ├── power_shell.app.src ├── power_shell_sup.erl ├── power_shell_app.erl ├── power_shell_default.erl ├── power_shell.erl ├── power_shell_cache.erl └── power_shell_eval.erl ├── test ├── power_shell_export.erl ├── power_shell_default_SUITE.erl ├── power_shell_cache_SUITE.erl └── power_shell_SUITE.erl ├── CHANGELOG.md ├── LICENSE.md ├── CONTRIBUTING.md ├── .github └── workflows │ └── erlang.yml ├── CODE_OF_CONDUCT.md └── README.md /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | doc 13 | erl_crash.dump 14 | .rebar 15 | logs 16 | _build 17 | .idea 18 | *.iml 19 | rebar3.crashdump 20 | *~ 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | {deps, []}. 3 | 4 | {cover_enabled, true}. 5 | {cover_excl_mods, [power_shell_eval]}. 6 | {cover_opts, [verbose, {keep_logs, 1}]}. 7 | 8 | {shell, [ 9 | {apps, [power_shell]} 10 | ]}. 11 | 12 | {hex, [ 13 | {doc, #{provider => ex_doc}} 14 | ]}. 15 | 16 | {ex_doc, [ 17 | {extras, [ 18 | {"CHANGELOG.md", #{title => "Changelog"}}, 19 | {"CODE_OF_CONDUCT.md", #{title => "Code of Conduct"}}, 20 | {"CONTRIBUTING.md", #{title => "Contributing"}}, 21 | {"LICENSE.md", #{title => "License"}}, 22 | {"README.md", #{title => "Overview"}} 23 | ]}, 24 | {main, "README.md"}, 25 | {source_url, "https://github.com/WhatsApp/power_shell"} 26 | ]}. 27 | -------------------------------------------------------------------------------- /src/power_shell.app.src: -------------------------------------------------------------------------------- 1 | {application, power_shell, 2 | [{description, "Erlang shell extension allowing to evaluate non-exported functions"}, 3 | {vsn, "1.3.0"}, 4 | {registered, [power_shell_cache]}, 5 | {mod, {power_shell_app, []}}, 6 | {applications, 7 | [kernel, 8 | stdlib, 9 | compiler 10 | ]}, 11 | {env, [ 12 | % {cache_code, false}, % do not use cache, decompile every call 13 | % {shell_integration, user_default} % load helpers into user_default 14 | % {skip_on_load, false} % run -onload() when decompiling module 15 | ]}, 16 | {modules, [power_shell, power_shell_cache, power_shell_app, power_shell_sup]}, 17 | 18 | {licenses, ["BSD-3-Clause-Clear"]}, 19 | {links, [{"GitHub", "https://github.com/WhatsApp/power_shell"}]} 20 | ]}. 21 | -------------------------------------------------------------------------------- /test/power_shell_export.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Maxim Fedorov 3 | %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. 4 | %%% @doc 5 | %%% Used for testing power_shell:export/2,3 functions. It is not possible 6 | %%% to safely load and purge the power_shell_SUITE module itself when 7 | %%% running tests over the module, hence the need in this one. 8 | %%% @end 9 | %%%------------------------------------------------------------------- 10 | -module(power_shell_export). 11 | -author("maximfca@gmail.com"). 12 | 13 | %% API 14 | -export([export_all/1]). 15 | 16 | %%-------------------------------------------------------------------- 17 | %% IMPORTANT: THESE MUST NOT BE EXPORTED !!! 18 | 19 | local_unexported(Echo) -> 20 | Echo. 21 | 22 | local_never(Echo) -> 23 | Echo. 24 | 25 | %% Kitchen sink to silence compiler in a good way (without suppressing warnings) 26 | export_all(Arg) -> 27 | local_never(local_unexported(Arg)). -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | Every release has a corresponding tag. Use `main` branch for fresh-from-the-oven code. 3 | 4 | ## 1.3.0 5 | - Add support for map comprehensions from OTP 26 6 | 7 | ## 1.2.2 8 | - Bugfix enabling recursive power_shell invocations 9 | 10 | ## 1.2.1 11 | - Bugfix for configurable -on_load() support 12 | 13 | ## 1.2.0 14 | - Added support for hot-code loaded exports 15 | 16 | ## 1.1.7 17 | - Dropped support for OTP 20, added support for future OTP 25 release 18 | 19 | ## 1.1.6 20 | - Configurable support for -on_load() in evaluated files 21 | 22 | ## 1.1.5 23 | - Support for OTP 23 24 | 25 | ## 1.1.4 26 | - Fixed incorrect anonymous function name when raising exception 27 | 28 | ## 1.1.3 29 | - Fixed bindings leaking into function defined locally, but called externally 30 | 31 | ## 1.1.2 32 | - Added cover (Coverage Analysis Tool for Erlang) support, evaluated functions are now 33 | displayed as 'covered' 34 | 35 | ## 1.1.1: 36 | - Fixed handling of try ... catch clauses 37 | 38 | ## 1.1.0: 39 | - Added support for cover-compiled files 40 | - Added support for loading *.erl files when *.beam files are missing 41 | - Added support for preloaded *.beam files (prim_file, erlang, ...) 42 | - Added support for built-in functions called directly (e.g. erlang:self()). 43 | 44 | ## 1.0.0: 45 | - Initial release 46 | - Implemented calling non-exported function 47 | - Fixed erl_eval deficiency (existing in escript as well, inability to make local calls for `fun local_name/Arity`) 48 | - Added shell integration 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) WhatsApp Inc. and its affiliates. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY 19 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 20 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 22 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 23 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 24 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 25 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 26 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 27 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/power_shell_sup.erl: -------------------------------------------------------------------------------- 1 | %% @private 2 | %%%------------------------------------------------------------------- 3 | %%%------------------------------------------------------------------- 4 | %%% @author Maxim Fedorov 5 | %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. 6 | %%% @doc 7 | %%% power_shell top level supervisor. 8 | %%% When started, supervisor checks cache_code application configuration 9 | %%% variable, and in case it's set to true, power_shell_cache server 10 | %%% is started to allow abstract code caching. 11 | %%% @end 12 | %%%------------------------------------------------------------------- 13 | 14 | -module(power_shell_sup). 15 | 16 | -behaviour(supervisor). 17 | 18 | %% API 19 | -export([start_link/0]). 20 | 21 | %% Supervisor callbacks 22 | -export([init/1]). 23 | 24 | -define(SERVER, ?MODULE). 25 | 26 | %%==================================================================== 27 | %% API functions 28 | 29 | -spec start_link() -> supervisor:startlink_ret(). 30 | start_link() -> 31 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 32 | 33 | %%==================================================================== 34 | %% Supervisor callbacks 35 | 36 | init([]) -> 37 | {ok, {#{}, 38 | case application:get_env(cache_code) of 39 | {ok, true} -> 40 | [power_shell_cache_child_spec()]; 41 | undefined -> 42 | [] 43 | end 44 | }}. 45 | 46 | %%==================================================================== 47 | %% Internal functions 48 | 49 | power_shell_cache_child_spec() -> 50 | #{id => power_shell_cache, 51 | start => {power_shell_cache, start_link, []}, modules => [power_shell_cache]}. 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to power_shell 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Our Development Process 6 | We require 100% test coverage. 7 | All exported functions must include -spec(). specification. 8 | 9 | ## Pull Requests 10 | We actively welcome your pull requests. 11 | 12 | 1. Fork the repo and create your branch from `master`. 13 | 2. Ensure that tests suite run with `rebar3 ct` passes and cover 100% source code. 14 | Use `rebar3 cover` and examine coverage. 15 | 3. If you've changed APIs, update the documentation. Use `rebar3 edoc` to 16 | auto-generate documentation from source code. 17 | 4. Make sure your code compiles with no warnings. 18 | 5. Ensure Dialyzer does not report any problems. 19 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 20 | 21 | ## Contributor License Agreement ("CLA") 22 | In order to accept your pull request, we need you to submit a CLA. You only need 23 | to do this once to work on any of Facebook's open source projects. 24 | 25 | Complete your CLA here: 26 | 27 | ## Issues 28 | We use GitHub issues to track public bugs. Please ensure your description is 29 | clear and has sufficient instructions to be able to reproduce the issue. 30 | 31 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 32 | disclosure of security bugs. In those cases, please go through the process 33 | outlined on that page and do not file a public issue. 34 | 35 | ## Coding Style 36 | * Use 4 spaces for indentation 37 | * Avoid lines longer that 100 characters 38 | * Do not commit commented-out code or files that are no longer needed 39 | 40 | ## License 41 | By contributing to power_shell, you agree that your contributions will be licensed 42 | under the LICENSE file in the root directory of this source tree. 43 | -------------------------------------------------------------------------------- /src/power_shell_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Maxim Fedorov 3 | %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. 4 | %%% @doc Application bootstrap. If shell_default configuration 5 | %% variable is set to true during start, power_shell is 6 | %% automatically injected into current shell_default. 7 | %%% @private 8 | %%%------------------------------------------------------------------- 9 | 10 | -module(power_shell_app). 11 | 12 | -behaviour(application). 13 | 14 | %% Application callbacks 15 | -export([start/2, stop/1, integrate/1]). 16 | 17 | %%==================================================================== 18 | %% API 19 | 20 | start(_StartType, _StartArgs) -> 21 | % fail if shell_default is already loaded 22 | % register as default shell, if asked for 23 | case application:get_env(shell_integration) of 24 | {ok, shell_default} -> 25 | integrate(shell_default); 26 | {ok, user_default} -> 27 | integrate(user_default); 28 | undefined -> 29 | integrate(user_default) 30 | end, 31 | % 32 | % we have to run a no-op supervisor to report 33 | % application health. 34 | % otherwise, just use it as a library, do not 35 | % start the app, it is good enough! 36 | power_shell_sup:start_link(). 37 | 38 | %%-------------------------------------------------------------------- 39 | stop(_State) -> 40 | % unload shell_default when stopping - via supervisor 41 | power_shell_default:eject(shell_default, power_shell), 42 | power_shell_default:eject(user_default, power_shell), 43 | ok. 44 | 45 | %%==================================================================== 46 | %% API 47 | 48 | %% @doc Implements user_default or shell_default integration. 49 | %% For shell_default, proxy injection technique is used. 50 | %% For user_default, if there is no other user_default module 51 | %% loaded, just rename power_shell to user_default and load as 52 | %% binary. 53 | -spec integrate(user_default | shell_default) -> ok | {error, power_shell_default:inject_error()}. 54 | 55 | integrate(shell_default) -> 56 | ok = power_shell_default:inject(shell_default, power_shell); 57 | integrate(user_default) -> 58 | ok = power_shell_default:inject(user_default, power_shell). 59 | -------------------------------------------------------------------------------- /.github/workflows/erlang.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - os: ubuntu-20.04 16 | otp-version: 21.3 17 | rebar3-version: 3.15 18 | - os: ubuntu-20.04 19 | otp-version: 22.3 20 | rebar3-version: 3.16 21 | - os: ubuntu-20.04 22 | otp-version: 23.3 23 | rebar3-version: 3.19 24 | - os: ubuntu-20.04 25 | otp-version: 24.3 26 | rebar3-version: 3.22 27 | - os: ubuntu-20.04 28 | otp-version: 25.3 29 | rebar3-version: 3.22 30 | - os: ubuntu-22.04 31 | otp-version: 26 32 | rebar3-version: 3.22 33 | runs-on: ${{ matrix.os }} 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v2 37 | - uses: erlef/setup-beam@v1 38 | with: 39 | otp-version: ${{matrix.otp-version}} 40 | rebar3-version: ${{matrix.rebar3-version}} 41 | - name: Cache Hex packages 42 | uses: actions/cache@v1 43 | with: 44 | path: ~/.cache/rebar3/hex/hexpm/packages 45 | key: ${{ runner.os }}-hex-${{ hashFiles(format('{0}{1}', github.workspace, '/rebar.lock')) }} 46 | restore-keys: | 47 | ${{ runner.os }}-hex- 48 | - name: Compile 49 | run: rebar3 compile 50 | - name: Edoc 51 | run: rebar3 edoc 52 | - name: Test 53 | run: rebar3 ct 54 | 55 | dialyzer: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v2 59 | - uses: erlef/setup-beam@v1 60 | with: 61 | otp-version: 26 62 | rebar3-version: 3.22 63 | - name: Cache Dialyzer PLTs 64 | uses: actions/cache@v1 65 | with: 66 | path: ~/.cache/rebar3/rebar3_*.plt 67 | key: ${{ runner.os }}-dialyzer-${{ hashFiles(format('{0}{1}', github.workspace, '/rebar.config')) }} 68 | restore-keys: | 69 | ${{ runner.os }}-dialyzer- 70 | - name: Cache Dialyzer PLTs 71 | uses: actions/cache@v1 72 | with: 73 | path: ~/.cache/rebar3/rebar3_*.plt 74 | key: ${{ runner.os }}-dialyzer-${{ hashFiles(format('{0}{1}', github.workspace, '/rebar.config')) }} 75 | restore-keys: | 76 | ${{ runner.os }}-dialyzer- 77 | - name: Dialyzer 78 | run: rebar3 dialyzer 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when there is a 56 | reasonable belief that an individual's behavior may have a negative impact on 57 | the project or its community. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the project team at . All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # power_shell 2 | 3 | [![Build Status](https://github.com/WhatsApp/power_shell/actions/workflows/erlang.yml/badge.svg?branch=main)](https://github.com/WhatsApp/power_shell/actions) [![Hex.pm](https://img.shields.io/hexpm/v/power_shell.svg)](https://hex.pm/packages/power_shell) [![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/power_shell) 4 | 5 | Advanced system-wide Erlang shell capabilities. Evaluates Erlang code loaded from the module debug 6 | information, or source file. 7 | 8 | Erlang/OTP versions supported: tested with 21, 22, 23, 24, and 25. 9 | 10 | ## License 11 | 12 | `power_shell` license is available in the [LICENSE.md](https://github.com/WhatsApp/power_shell/blob/main/LICENSE.md) file in the root directory of this source tree. 13 | 14 | ## Installation 15 | 16 | For Elixir, the package can be installed by adding `power_shell` to your list of dependencies in `mix.exs`: 17 | 18 | ```elixir 19 | def deps do 20 | [ 21 | {:power_shell, "~> 1.2.2"} 22 | ] 23 | end 24 | ``` 25 | 26 | For Erlang, the package can be installed by adding `power_shell` to your list of dependencies in `rebar.config`: 27 | 28 | ```erlang 29 | {deps, [ 30 | {power_shell, "1.2.2"} 31 | ]}. 32 | ``` 33 | 34 | Documentation can be found at [hexdocs.pm/power_shell](https://hexdocs.pm/power_shell). 35 | 36 | ## Usage 37 | 38 | `power_shell` can be used as an application, and in form of a library. 39 | 40 | When started as application, `power_shell` examines `shell_integration` configuration parameter. It can be set to `shell_default`, or `user_default`. 41 | 42 | Depending on the parameter, `power_shell` can load itself as `user_default` module (if none was loaded before), or add proxy calls to already loaded `user_default` module. 43 | 44 | ### Evaluating non-exported functions 45 | 46 | Enter the following in the shell to evaluate function `Fun` in module `Mod`. The function does not need to be exported. 47 | 48 | ```erlang 49 | power_shell:eval(Mod, Fun, [Arg1, Arg2, Arg3]). 50 | ``` 51 | 52 | If module `Mod` is currently loaded, `power_shell` will try to locate corresponding `*.beam` file, load debug info chunk, and evaluate function `Fun` with supplied arguments. 53 | 54 | If module is not loaded, but `*.beam` file can be found, `power_shell` does not attempt to load it. 55 | 56 | However, if there is a remote (fully qualified) call, Erlang VM (BEAM) will load the file automatically. 57 | 58 | When shell integration (injection) is enabled, evaluating non-exported functions is simplified to just: 59 | 60 | ```erlang 61 | eval(Mod, Fun, [Args]). 62 | ``` 63 | 64 | ### Debugging shortcuts 65 | 66 | Available with shell integration. 67 | 68 | ### Coverage Analysis Tool for Erlang 69 | 70 | Modules that were patched by [`cover`](https://www.erlang.org/doc/man/cover.html) application are also supported. 71 | 72 | If a module was `cover_compiled`, and it is being evaluated with `power_shell:eval`, coverage analysis works as expected. 73 | 74 | ### `-on_load()` attribute support 75 | 76 | To preserve compatibility with earlier versions, `power_shell` does not execute `-on_load()` by default. 77 | 78 | To change this behaviour, set `skip_on_load` application variable to `false`: 79 | 80 | ```erlang 81 | ok = application:load(power_shell), 82 | ok = application:set_env(power_shell, skip_on_load, false). 83 | ``` 84 | 85 | ### Recompiling and hot-loading modules with extra functions exported 86 | 87 | Starting with version 1.2.0, `power_shell` can recompile an existing module exporting all or selected functions. 88 | 89 | Use `power_shell:export(Mod)` to export all functions. 90 | 91 | This may be used in conjunction with Common Test suites, allowing for white-box testing of functions that should not be exported in production code. Example: 92 | 93 | ```erlang 94 | my_case(Config) when is_list(Config) -> 95 | Old = power_shell:export(prod_module), 96 | WhiteBoxRet = prod_module:not_exported_fun(123), 97 | power_shell:revert(Old). 98 | ``` 99 | 100 | For reliable Common Test execution `export/1,2,3` start a linked process that will reload the original code upon termination. 101 | 102 | This allows to avoid failing other test cases that do not expect extra exports available. 103 | 104 | ## Configuration 105 | 106 | During application startup, `power_shell` examines following application environment variables: 107 | 108 | - `cache_code :: boolean()` (default `false`) - start code cache process. This process keeps decompiled beam files in memory, speeding up execution if code does not change. 109 | - `shell_integration :: shell_default | user_default` (default `user_default`) - apply shell integration, making `power_shell:eval(Mod, Fun, Args)` available in shell, via `eval(Mod, Fun, Args)`. 110 | - `skip_on_load :: boolean()` (default `true`) - skip `-on_load()` execution when decompiling beam (or loading source). 111 | 112 | It is possible to supply values using `sys.config` file, or via command line when starting BEAM: 113 | 114 | ```bash 115 | erl -power_shell cache_code true 116 | ``` 117 | -------------------------------------------------------------------------------- /src/power_shell_default.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Maxim Fedorov 3 | %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. 4 | %%% @doc 5 | %%% Allows to inject proxy functions into existing modules, compiled 6 | %%% with debug into. Added proxy functions are just redirecting 7 | %%% calls to another module. 8 | %%% 9 | %%% This could be useful for environments where both shell_default 10 | %%% and user_default are already used, and it's not possible to 11 | %%% replace any of these. 12 | %%% @end 13 | %%%------------------------------------------------------------------- 14 | -module(power_shell_default). 15 | -author("maximfca@gmail.com"). 16 | 17 | %% API 18 | -export([inject/2, 19 | eject/2]). 20 | 21 | -export_type([inject_error/0]). 22 | 23 | -type inject_error() :: 24 | already_loaded | 25 | no_abstract_code | 26 | {badmatch, module()} | 27 | {beam_lib, beam_lib:chnk_rsn()}. 28 | 29 | %%==================================================================== 30 | %% API 31 | 32 | %% @doc Extracts abstract code from WhereTo module, adds all 33 | %% exports from the module Mod into WhereTo, compiles 34 | %% result and loads it, replacing current WhereTo. 35 | %% This sequence injects proxies for all functions exported 36 | %% from Mod into WhereTo (ignoring compiler-generated 37 | %% module_info/0 and module_info/1). 38 | %% For example, if Mod exports eval/3, after calling 39 | %% power_shell_default:inject(Mod) resulting WhereTo 40 | %% contains following code: 41 | %% ``` 42 | %% -export([eval/3]). 43 | %% eval(Arg1, Arg2, Arg3) -> 44 | %% Mod:eval(Arg1, Arg2, Arg3).''' 45 | %% 46 | %% If there is no WhereTo module loaded, proxy just 47 | %% creates one from scratch. 48 | %% @param WhereTo Module name to add proxy methods to 49 | %% @param Mod Module name to proxy calls to 50 | 51 | -spec inject(WhereTo :: module(), Mod :: module()) -> ok | {error, inject_error()}. 52 | inject(WhereTo, Mod) when is_atom(WhereTo), is_atom(Mod) -> 53 | Filename = atom_to_list(Mod), 54 | case code:which(WhereTo) of 55 | Filename -> 56 | {error, already_loaded}; 57 | NewFile when is_list(NewFile) -> 58 | case beam_lib:chunks(NewFile, [abstract_code]) of 59 | {ok, {WhereTo, [{abstract_code, {_, Forms}}]}} -> 60 | inject_impl(Forms, Filename, WhereTo, Mod); 61 | {ok, {WhereTo, [{abstract_code,no_abstract_code}]}} -> 62 | {error, no_abstract_code}; 63 | {ok, {ActualMod, _}} -> 64 | {error, {badmatch, ActualMod}}; 65 | Error -> 66 | {error, {beam_lib, Error}} 67 | end; 68 | non_existing -> 69 | inject_impl([{attribute,20,module,WhereTo}], Filename, WhereTo, Mod) 70 | end. 71 | 72 | %% @doc Verifies that current WhereFrom has injected functions from 73 | %% Mod, and purges/deletes WhereFrom code from memory. Next 74 | %% time shell does ensure_loaded() for WhereFrom, an unmodified 75 | %% version is loaded from disk. 76 | %% @param WhereFrom original module name, e.g. shell_default 77 | %% @param Mod module that has been injected 78 | 79 | -spec eject(WhereFrom :: module(), Mod :: module()) -> ok | {error, not_loaded}. 80 | eject(WhereFrom, Mod) when is_atom(WhereFrom), is_atom(Mod) -> 81 | ModFile = atom_to_list(Mod), 82 | case code:is_loaded(WhereFrom) of 83 | {file, ModFile} -> 84 | % just purge/delete our version, so OTP loads 85 | % previous edition from dist 86 | code:purge(WhereFrom), 87 | true = code:delete(WhereFrom), 88 | ok; 89 | _ -> 90 | {error, not_loaded} 91 | end. 92 | 93 | %%==================================================================== 94 | %% Internal functions 95 | 96 | % Inserts an already reversed sequence of attributes and 97 | % functions in the middle of parsed AST. 98 | % This essentially adds a few more exported functions to the 99 | % file, pretty much like compiler adds module_info(). 100 | insert_funs(Exports, Funs, Forms) -> 101 | {Attrs, Functions} = lists:splitwith( 102 | fun ({function, _, _, _, _}) -> 103 | false; 104 | (_) -> 105 | true 106 | end, 107 | Forms), 108 | Attrs ++ [Exports | Funs] ++ Functions. 109 | 110 | % Generates abstract syntax tree for proxy function Mod:Fun/Arity. 111 | proxy_fun(Mod, Fun, Arity) -> 112 | ProxyArgs = [{var, 1, list_to_atom("Arg" ++ integer_to_list(N))} || 113 | N <- lists:seq(1, Arity)], % [{var,1,'T'}] 114 | {function, 1, Fun, Arity, 115 | [{clause, 1, 116 | ProxyArgs, 117 | [], 118 | [{call,1, 119 | {remote, 1, {atom, 1, Mod}, {atom, 1, Fun}}, 120 | ProxyArgs}]}]}. 121 | 122 | inject_impl(Forms, Filename, WhereTo, Mod) -> 123 | % pick exports from Mod (power_shell expected), removing OTP-added functions 124 | Forwards = Mod:module_info(exports) -- [{module_info, 0}, {module_info, 1}], 125 | % make AST out of exports 126 | Exports = {attribute, 1, export, Forwards}, 127 | Funs = [proxy_fun(Mod, Fun, Arity) || {Fun, Arity} <- Forwards], 128 | Forms1 = insert_funs(Exports, Funs, Forms), 129 | % compile to beam 130 | {ok, App, Bin} = compile:forms(Forms1), 131 | % unstick if necessary 132 | StickBack = case code:is_sticky(WhereTo) of 133 | true -> 134 | code:unstick_mod(WhereTo); 135 | false -> 136 | false 137 | end, 138 | % load augmented code 139 | {module, WhereTo} = code:load_binary(App, Filename, Bin), 140 | % stick back if was unstuck 141 | if StickBack -> code:stick_mod(WhereTo); true -> ok end, 142 | % and immediately purge old code (race conditions are unlikely) 143 | code:purge(WhereTo), 144 | ok. 145 | -------------------------------------------------------------------------------- /test/power_shell_default_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Maxim Fedorov 3 | %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. 4 | %%% @private 5 | %%%------------------------------------------------------------------- 6 | -module(power_shell_default_SUITE). 7 | -author("maximfca@gmail.com"). 8 | 9 | %%-------------------------------------------------------------------- 10 | 11 | -export([all/0, 12 | suite/0, 13 | init_per_suite/1, 14 | end_per_suite/1, 15 | init_per_testcase/2, 16 | end_per_testcase/2]). 17 | 18 | -export([inject/0, inject/1, 19 | already/0, already/1, 20 | shell_default_inject/0, shell_default_inject/1, 21 | user_default/0,user_default/1, 22 | user_default_inject/0, user_default_inject/1, 23 | failed_inject/0, failed_inject/1]). 24 | 25 | %% Common Test headers 26 | -include_lib("common_test/include/ct.hrl"). 27 | 28 | %% Include stdlib header to enable ?assert() for readable output 29 | -include_lib("stdlib/include/assert.hrl"). 30 | 31 | suite() -> 32 | [{timetrap,{seconds,30}}]. 33 | 34 | all() -> 35 | [inject, already, shell_default_inject, user_default, user_default_inject, failed_inject]. 36 | 37 | init_per_suite(Config) -> 38 | Config. 39 | 40 | end_per_suite(Config) -> 41 | Config. 42 | 43 | init_per_testcase(TC, Config) when TC =:= shell_default_inject; TC =:= user_default; 44 | TC =:= user_default_inject; TC =:= failed_inject -> 45 | Filename = filename:join(proplists:get_value(priv_dir, Config), "user_default"), 46 | [{user_default, Filename} | Config]; 47 | init_per_testcase(_, Config) -> 48 | Config. 49 | 50 | end_per_testcase(TC, Config) when TC =:= shell_default_inject; TC =:= user_default; 51 | TC =:= user_default_inject; TC =:= failed_inject -> 52 | application:stop(power_shell), 53 | Filename = proplists:get_value(user_default, Config), 54 | file:delete(Filename), 55 | proplists:delete(user_default, Config); 56 | end_per_testcase(_, Config) -> 57 | Config. 58 | 59 | inject() -> 60 | [{doc, "Tests power_shell injection"}]. 61 | 62 | inject(_Config) -> 63 | ?assertNot(erlang:function_exported(shell_default, eval, 3)), 64 | ok = power_shell_default:inject(shell_default, power_shell), 65 | % test injected: 66 | ?assert(erlang:function_exported(shell_default, eval, 3)), 67 | [1, 2] = shell_default:eval(lists, seq, [1,2]), 68 | % 69 | ok = power_shell_default:eject(shell_default, power_shell), 70 | ok. 71 | 72 | already() -> 73 | [{doc, "Test proxy injection on already augmented code"}]. 74 | 75 | already(_Config) -> 76 | ?assertNot(erlang:function_exported(shell_default, eval, 3)), 77 | ok = power_shell_default:inject(shell_default, power_shell), 78 | {error, already_loaded} = power_shell_default:inject(shell_default, power_shell), 79 | ok = power_shell_default:eject(shell_default, power_shell), 80 | {error, not_loaded} = power_shell_default:eject(shell_default, power_shell), 81 | ok. 82 | 83 | shell_default_inject() -> 84 | [{doc, "Test shell_default integration with injected proxy"}]. 85 | 86 | shell_default_inject(_Config) -> 87 | ok = application:set_env(power_shell, shell_integration, shell_default), 88 | {ok, _} = application:ensure_all_started(power_shell), 89 | % so shell_default must be working now, right? 90 | [1, 2] = shell_default:eval(lists, seq, [1,2]), 91 | ok. 92 | 93 | user_default() -> 94 | [{doc, "Test user_default integration with no user-defined user_default"}]. 95 | 96 | user_default(_Config) -> 97 | ok = application:set_env(power_shell, shell_integration, user_default), 98 | {ok, _} = application:ensure_all_started(power_shell), 99 | % so shell_default must be working now, right? 100 | [1, 2] = user_default:eval(lists, seq, [1,2]), 101 | ok. 102 | 103 | user_default_inject() -> 104 | [{doc, "Test user_default integration with injected proxy"}]. 105 | 106 | user_default_inject(Config) -> 107 | make_user_default(Config, user_default, [debug_info]), 108 | % 109 | ok = application:set_env(power_shell, shell_integration, user_default), 110 | ok = application:start(power_shell), 111 | % ensure 'old' user_default is still there 112 | true = user_default:exist(), 113 | % so shell_default must be working now, right? 114 | [1, 2] = user_default:eval(lists, seq, [1,2]), 115 | ok. 116 | 117 | failed_inject() -> 118 | [{doc, "Test that failed injection does not crash everything"}]. 119 | 120 | failed_inject(Config) -> 121 | % current user_default is broken 122 | make_user_default(Config, user_default, [debug_info]), 123 | {module, user_default} = code:ensure_loaded(user_default), 124 | % break it now 125 | write_user_default(Config, no_default, []), 126 | ?assertMatch({error, {bad_return, {{power_shell_app,start,[normal,[]]}, 127 | {'EXIT', {{badmatch,{error,{badmatch,_}}}, _}}}}}, 128 | application:start(power_shell)), 129 | % 130 | % 131 | Beamname = make_user_default(Config, user_default, []), 132 | % no debug info in the current user_default 133 | ok = application:set_env(power_shell, shell_integration, user_default), 134 | ?assertMatch({error, {bad_return, {{power_shell_app,start,[normal,[]]}, 135 | {'EXIT', {{badmatch,{error,no_abstract_code}}, _}}}}}, 136 | application:start(power_shell)), 137 | 138 | % beam file broken 139 | ok = file:write_file(Beamname, <<"FOR1 broken non-beam file">>), 140 | ?assertMatch({error, {bad_return, {{power_shell_app,start,[normal,[]]}, 141 | {'EXIT', {{badmatch,{error,{beam_lib, {error,beam_lib, 142 | {not_a_beam_file, _}}}}}, _}}}}}, 143 | application:start(power_shell)), 144 | code:purge(user_default), 145 | code:delete(user_default), 146 | ok. 147 | 148 | make_user_default(Config, Mod, Options) -> 149 | Filename = write_user_default(Config, Mod, Options), 150 | % load module from binary 151 | {module, Mod} = code:load_abs(Filename), 152 | code:purge(Mod), 153 | % ensure it was success 154 | true = Mod:exist(), 155 | Filename ++ ".beam". 156 | 157 | write_user_default(Config, Mod, Options) -> 158 | Filename = proplists:get_value(user_default, Config), 159 | % make and load fake user_default module, that has a single function 160 | Scans = lists:map(fun (Line) -> 161 | {ok, Scan, _} = erl_scan:string(Line), 162 | {ok, Form} = erl_parse:parse_form(Scan), 163 | Form end, ["-module(" ++ atom_to_list(Mod) ++ ").","-export([exist/0]).","exist() -> true."]), 164 | % compile forms to binary 165 | {ok, Mod, Bin} = compile:forms(Scans, Options), 166 | ok = file:write_file(Filename ++ ".beam", Bin), 167 | Filename. 168 | -------------------------------------------------------------------------------- /src/power_shell.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Maxim Fedorov 3 | %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. 4 | %%% @doc 5 | %%% Implements Erlang interpreter via eval/3 function. 6 | %%% 7 | %%% Allows to `call' functions that are not exported by interpreting 8 | %%% function code. 9 | %%% 10 | %%% For cases when evaluation is too slow, it's possible to recompile 11 | %%% a module and hot-code-load it using `export/1,2,3', and later 12 | %%% revert the change with `revert/1'. 13 | %%% @end 14 | %%%------------------------------------------------------------------- 15 | -module(power_shell). 16 | -author("maximfca@gmail.com"). 17 | 18 | %% API 19 | -export([ 20 | eval/3, 21 | eval/4, 22 | export/1, 23 | export/2, 24 | export/3, 25 | revert/1 26 | ]). 27 | 28 | %% Internal exports 29 | -export([sentinel/3]). 30 | 31 | %%-------------------------------------------------------------------- 32 | %% API 33 | 34 | %% @doc Performs erlang:apply(Module, Fun, Args) by evaluating AST 35 | %% of Module:Fun. 36 | %% @param Module Module name, must be either loaded or discoverable with code:which() or filelib:find_source() 37 | %% @param Fun function name, may not be exported 38 | %% @param Args List of arguments 39 | -spec eval( Module :: module(), Fun :: atom(), Args :: [term()]) -> 40 | term(). 41 | 42 | eval(Mod, Fun, Args) when is_atom(Mod), is_atom(Fun), is_list(Args) -> 43 | eval_apply(erlang:is_builtin(Mod, Fun, length(Args)), Mod, Fun, Args, undefined). 44 | 45 | %% @doc Performs erlang:apply(Module, Fun, Args) by evaluating AST 46 | %% of Module:Fun. 47 | %% @param Module Module name, must be either loaded or discoverable with code:which() or filelib:find_source() 48 | %% @param Fun function name, may not be exported 49 | %% @param Args List of arguments 50 | %% @param FunMap AST of all functions defined in Mod, as returned by power_shell_cache:get_module(Mod) 51 | %% Should be used if starting power_shell_cache gen_server is undesirable. 52 | -spec eval(Module :: module(), Fun :: atom(), Args :: [term()], power_shell_cache:function_map()) -> 53 | term(). 54 | eval(Mod, Fun, Args, FunMap) -> 55 | eval_apply(erlang:is_builtin(Mod, Fun, length(Args)), Mod, Fun, Args, FunMap). 56 | 57 | %% @equiv export(Module, all, #{}) 58 | -spec export(Module :: module()) -> pid(). 59 | export(Mod) -> 60 | export(Mod, all). 61 | 62 | %% @equiv export(Module, Export, #{}) 63 | -spec export(Module :: module(), Export :: all | [{Fun :: atom(), Arity :: non_neg_integer()}]) -> pid(). 64 | export(Mod, Export) -> 65 | export(Mod, Export, #{}). 66 | 67 | -type export_options() :: #{ 68 | link => boolean() 69 | }. 70 | 71 | %% @doc Retrieves code (AST) of the `Module' from the debug information chunk. Exports all or selected 72 | %% functions and reloads the module. 73 | %% A sentinel process is created for every `export' call, linked to the caller process. When 74 | %% the calling process terminates, linked sentinel loads the original module back and 75 | %% terminates. Sentinel process can be stopped gracefully by calling `revert(Sentinel)'. 76 | %% There is no protection against multiple `export' calls interacting, and caller is 77 | %% responsible for proper synchronisation. Use `eval' when no safe sequence can be found. 78 | %% @param Module Module name, must be either loaded or discoverable with code:which() 79 | %% @param Export list of tuples `{Fun, Arity}' to be exported, or `all' to export all functions. 80 | %% @param Options use `#{link => false}' to start the sentinel process unlinked, assuming manual 81 | %% `revert' call. 82 | -spec export(Module :: module(), all | [{Fun :: atom(), Arity :: non_neg_integer()}], 83 | export_options()) -> pid(). 84 | export(Mod, Export, #{link := false}) -> 85 | proc_lib:start(?MODULE, sentinel, [self(), Mod, Export]); 86 | export(Mod, Export, _Options) -> 87 | proc_lib:start_link(?MODULE, sentinel, [self(), Mod, Export]). 88 | 89 | %% @doc Gracefully stops the sentinel process, causing the original module to be loaded back. 90 | %% @param Sentinel process to stop. 91 | -spec revert(Sentinel :: pid()) -> ok. 92 | revert(Sentinel) -> 93 | proc_lib:stop(Sentinel). 94 | 95 | %%-------------------------------------------------------------------- 96 | %% Internal functions 97 | %% Sentinel process 98 | sentinel(Parent, Mod, Export) -> 99 | Options = proplists:get_value(options, Mod:module_info(compile), []), 100 | {Mod, Binary, File} = code:get_object_code(Mod), 101 | {ok, {Mod, [{abstract_code, {_, Forms}}]}} = beam_lib:chunks(Binary, [abstract_code]), 102 | Expanded = erl_expand_records:module(Forms, [strict_record_tests]), 103 | {ok, Mod, Bin} = make_export(Expanded, Options, Export), 104 | %% trap exits before loading the code 105 | erlang:process_flag(trap_exit, true), 106 | {module, Mod} = code:load_binary(Mod, File, Bin), 107 | proc_lib:init_ack(Parent, self()), 108 | receive 109 | {'EXIT', Parent, _Reason} -> 110 | {module, Mod} = code:load_binary(Mod, File, Binary); 111 | {system, Reply, {terminate, _Reason}} -> 112 | {module, Mod} = code:load_binary(Mod, File, Binary), 113 | gen:reply(Reply, ok) 114 | end. 115 | 116 | make_export(Forms, Options, all) -> 117 | compile:forms(Forms, [export_all, binary, debug_info] ++ Options); 118 | make_export(Forms, Options, Exports) -> 119 | Exported = insert_export(Forms, [], Exports), 120 | compile:forms(Exported, [binary, debug_info] ++ Options). 121 | 122 | %% don't handle modules that export nothing, crash instead, for these modules 123 | %% aren't useful anyway. 124 | insert_export([{attribute, Anno, export, _} = First | Tail], Passed, Exports) -> 125 | ExAnno = erl_anno:new(erl_anno:line(Anno)), 126 | lists:reverse(Passed) ++ [First, {attribute, ExAnno, export, Exports} | Tail]; 127 | insert_export([Form | Tail], Passed, Exports) -> 128 | insert_export(Tail, [Form | Passed], Exports). 129 | 130 | %%-------------------------------------------------------------------- 131 | %% Evaluator 132 | 133 | -define (STACK_TOKEN, '$power_shell_stack_trace'). 134 | 135 | eval_apply(true, Mod, Fun, Args, _FunMap) -> 136 | erlang:apply(Mod, Fun, Args); 137 | eval_apply(false, Mod, Fun, Args, FunMap0) -> 138 | PreservedStack = put(?STACK_TOKEN, []), 139 | try 140 | FunMap = if FunMap0 =:= undefined -> power_shell_cache:get_module(Mod); true -> FunMap0 end, 141 | eval_impl(Mod, Fun, Args, FunMap) 142 | catch 143 | error:enoent -> 144 | {current_stacktrace, Trace} = process_info(self(), current_stacktrace), 145 | erlang:raise(error, undef, [{Mod, Fun, Args, []}] ++ tl(Trace)); 146 | throw:Reason -> 147 | {current_stacktrace, Trace} = process_info(self(), current_stacktrace), 148 | % recover stack from process dictionary 149 | pop_stack(), 150 | erlang:raise(throw, Reason, get_stack() ++ tl(Trace)); 151 | Class:Reason -> 152 | {current_stacktrace, Trace} = process_info(self(), current_stacktrace), 153 | % recover stack from process dictionary 154 | erlang:raise(Class, Reason, get_stack() ++ tl(Trace)) 155 | after 156 | case PreservedStack of 157 | undefined -> erase(?STACK_TOKEN); 158 | _ when is_list(PreservedStack) -> put(?STACK_TOKEN, PreservedStack) 159 | end 160 | end. 161 | 162 | push_stack(MFA) -> 163 | put(?STACK_TOKEN, [MFA | get(?STACK_TOKEN)]). 164 | 165 | pop_stack() -> 166 | put(?STACK_TOKEN, tl(get(?STACK_TOKEN))). 167 | 168 | get_stack() -> 169 | get(?STACK_TOKEN). 170 | %[{M, F, length(Args), Dbg} || {M, F, Args, Dbg} <- Stack]. 171 | %[hd(Stack) | [{M, F, length(Args)} || {M, F, Args} <- tl(Stack)]]. 172 | 173 | eval_impl(Mod, Fun, Args, FunMap) -> 174 | Arity = length(Args), 175 | case maps:get({Fun, Arity}, FunMap, undefined) of 176 | undefined -> 177 | push_stack({Mod, Fun, Args, []}), 178 | erlang:raise(error, undef, [{Mod, Fun, Args, []}]); 179 | {function, _, Fun, Arity, Clauses} -> 180 | case power_shell_eval:match_clause(Clauses, Args, power_shell_eval:new_bindings(), local_fun_handler(Mod, FunMap)) of 181 | {Body, Bindings} -> 182 | % find line number by reverse scan of clauses, in {clause,_,H,G,B} 183 | %%% UGLY %%% - See TODO for eval_exprs 184 | {clause, _Line, _, _, Body} = lists:keyfind(Body, 5, Clauses), 185 | %NewLocalStack = [{Mod, Fun, Arity, [{line, Line}]}], 186 | push_stack({Mod, Fun, length(Args), []}), 187 | % power_shell_eval:exprs() does not allow to get the value of the last expr only 188 | % power_shell_eval:exprs(Body, Bindings, local_fun_handler(Mod, FunMap)), 189 | R = eval_exprs(Body, Bindings, 190 | local_fun_handler(Mod, FunMap), 191 | non_local_fun_handler()), 192 | pop_stack(), 193 | R; 194 | nomatch -> 195 | push_stack({Mod, Fun, Args, []}), 196 | erlang:raise(error, function_clause, [{Mod, Fun, Args, []}]) 197 | end 198 | end. 199 | 200 | %% TODO: find a way to supply Line Number (every Expr has it) for Stack purposes 201 | eval_exprs([Expr], Bindings, LocalFun, NonLocalFun) -> 202 | power_shell_eval:expr(Expr, Bindings, LocalFun, NonLocalFun, value); 203 | eval_exprs([Expr | Exprs], Bindings, LocalFun, NonLocalFun) -> 204 | {value, _Value, NewBindings} = power_shell_eval:expr(Expr, Bindings, LocalFun, NonLocalFun), 205 | eval_exprs(Exprs, NewBindings, LocalFun, NonLocalFun). 206 | 207 | local_fun_handler(Mod, FunMap) -> 208 | { 209 | value, 210 | fun ({function, Fun, Arity}, []) -> 211 | % extract body for this locally-defined function 212 | % requires patched erl_eval, thus a need for power_shell_eval. 213 | % we rely on the fact that AST has been compiled without errors, 214 | % which means local functions are surely defined 215 | FunArity = {Fun, Arity}, 216 | #{FunArity := {function, _, Fun, Arity, Clauses}} = FunMap, 217 | Clauses; 218 | (Fun, Args) -> 219 | eval_impl(Mod, Fun, Args, FunMap) 220 | end 221 | }. 222 | 223 | %% Non-local handler serves following purposes: 224 | % * catch exception thrown and fixup stack 225 | non_local_fun_handler() -> 226 | { 227 | value, fun do_apply/2 228 | }. 229 | 230 | do_apply({M, F}, A) -> 231 | push_stack({M, F, length(A), []}), 232 | Ret = try 233 | erlang:apply(M, F, A) 234 | catch 235 | error:undef:Stack -> 236 | pop_stack(), 237 | push_stack({M, F, A, []}), 238 | erlang:raise(error, undef, Stack) 239 | end, 240 | pop_stack(), 241 | Ret; 242 | do_apply(Fun, A) -> 243 | [{Mod, Name, _, _} | _] = get_stack(), % assume that anonymous fun comes from the same module & function 244 | Lambda = list_to_atom(atom_to_list(Name) ++ "-fun-$"), 245 | push_stack({Mod, Lambda, length(A), []}), 246 | Ret = erlang:apply(Fun, A), 247 | pop_stack(), 248 | Ret. 249 | -------------------------------------------------------------------------------- /src/power_shell_cache.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Maxim Fedorov 3 | %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. 4 | %%% @doc 5 | %%% Loads abstract module code, either from debug_info *.beam chunk, 6 | %%% or from *.erl source file. 7 | %%% 8 | %%% Caching server may not be started, in this case get_module() does 9 | %%% not use caching logic and creates AST every call. 10 | %%% If loaded module md5 hash is changed, or *.erl file modification 11 | %%% date is different from the last call, module AST gets reloaded. 12 | %%% @end 13 | %%%------------------------------------------------------------------- 14 | -module(power_shell_cache). 15 | -author("maximfca@gmail.com"). 16 | 17 | -behaviour(gen_server). 18 | 19 | -export([get_module/1]). 20 | 21 | %% Decompiled/loaded code cache server 22 | -export([start_link/0]). 23 | 24 | %% gen_server callbacks 25 | -export([init/1, 26 | handle_call/3, 27 | handle_cast/2]). 28 | 29 | -export_type([function_map/0]). 30 | 31 | -define(SERVER, ?MODULE). 32 | 33 | -include_lib("kernel/include/file.hrl"). 34 | -include_lib("kernel/include/logger.hrl"). 35 | 36 | -type function_clause() :: {function, integer(), atom(), arity(), {clauses, [erl_parse:abstract_clause()]}}. 37 | 38 | -type function_map() :: #{{atom(), arity()} => function_clause()}. 39 | 40 | % For every module, store: 41 | % * loaded code md5 hash (undefined when *.beam is not loaded) 42 | % * file name used to access the file, *.beam or *.erl (could also be 'preloaded') 43 | % * modification date of the file (*.beam or source) 44 | % For file-based interpreter, it could be either BEAM or *.erl. 45 | -record(module_data, { 46 | hash = undefined :: binary() | undefined, 47 | filename = undefined :: string() | undefined, 48 | mtime = undefined :: file:date_time() | undefined, 49 | fun_map = #{} :: function_map() 50 | }). 51 | 52 | -type module_data() :: #module_data{}. 53 | 54 | % Code cache state 55 | -record(state, { 56 | modules = #{} :: #{module() => module_data()} 57 | }). 58 | 59 | %%%=================================================================== 60 | %%% API 61 | %%%=================================================================== 62 | 63 | %% @doc Extracts AST of all functions defined in Mod. Caches this 64 | %% if power_shell_cache server is running. 65 | -spec get_module(Mod :: module()) -> function_map(). 66 | get_module(Mod) -> 67 | try gen_server:call(?MODULE, {get, Mod}) of 68 | {ok, #module_data{fun_map = FunMap}} -> 69 | FunMap; 70 | {error, Reason, Stack} -> 71 | erlang:raise(error, Reason, Stack) 72 | catch 73 | exit:{noproc, _} -> 74 | #module_data{fun_map = FunMap} = 75 | case decompile(Mod, is_loaded(Mod), undefined) of 76 | #module_data{} = ModData -> 77 | ModData; 78 | need_cover -> 79 | % requested module was cover-compiled 80 | decompile(Mod, is_loaded(Mod), decompile(cover, true, undefined)) 81 | end, 82 | FunMap 83 | end. 84 | 85 | %% @doc 86 | %% Starts power_shell_cache server that keeps track of already 87 | %% decompiled modules. 88 | -spec(start_link() -> 89 | {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). 90 | start_link() -> 91 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). 92 | 93 | %%%=================================================================== 94 | %%% gen_server callbacks 95 | %%%=================================================================== 96 | 97 | %%-------------------------------------------------------------------- 98 | %% @private 99 | %% @doc 100 | %% Initializes the server 101 | %% @end 102 | %%-------------------------------------------------------------------- 103 | -spec(init(Args :: term()) -> 104 | {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | 105 | {stop, Reason :: term()} | ignore). 106 | init([]) -> 107 | {ok, #state{}}. 108 | 109 | %%-------------------------------------------------------------------- 110 | %% @private 111 | %% @doc 112 | %% Handling call messages 113 | %% @end 114 | %%-------------------------------------------------------------------- 115 | -spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, 116 | State :: #state{}) -> 117 | {reply, Reply :: term(), NewState :: #state{}} | 118 | {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | 119 | {noreply, NewState :: #state{}} | 120 | {noreply, NewState :: #state{}, timeout() | hibernate} | 121 | {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | 122 | {stop, Reason :: term(), NewState :: #state{}}). 123 | 124 | % Can't match Mod and #modules{} map for the module, 125 | % even though Joe Armstrong writes this example 126 | % in the book. So use maps:get()... 127 | handle_call({get, Mod}, _From, #state{modules = Modules} = State) -> 128 | % 129 | Loaded = is_loaded(Mod), 130 | case maps:get(Mod, Modules, error) of 131 | 132 | error -> 133 | reply_recompile(Mod, Loaded, State); 134 | 135 | #module_data{hash = Hash} when 136 | (Hash =:= undefined andalso Loaded =:= true); 137 | (Hash =/= undefined andalso Loaded =:= false) -> 138 | % BEAM has been either loaded or unloaded since last call, 139 | % need to rescan 140 | reply_recompile(Mod, Loaded, State); 141 | 142 | #module_data{hash = undefined, filename = Filename, mtime = MTime} = ModData -> 143 | % loaded from file, need to check modification time 144 | case filelib:last_modified(Filename) of 145 | MTime -> 146 | {reply, {ok, ModData}, State}; 147 | _AnotherTime -> 148 | reply_recompile(Mod, Loaded, State) 149 | end; 150 | 151 | #module_data{hash = Hash} = ModData -> 152 | % check if loaded BEAM has a different md5 153 | case Mod:module_info(md5) of 154 | Hash -> 155 | {reply, {ok, ModData}, State}; 156 | AnotherHash when is_binary(AnotherHash) -> 157 | reply_recompile(Mod, Loaded, State) 158 | end 159 | end; 160 | 161 | handle_call(Request, _From, State) -> 162 | {reply, {error, {undef, Request}}, State}. 163 | 164 | %%-------------------------------------------------------------------- 165 | %% @private 166 | %% @doc 167 | %% Handling cast messages 168 | %% @end 169 | %%-------------------------------------------------------------------- 170 | -spec(handle_cast(Request :: term(), State :: #state{}) -> 171 | {noreply, NewState :: #state{}} | 172 | {noreply, NewState :: #state{}, timeout() | hibernate} | 173 | {stop, Reason :: term(), NewState :: #state{}}). 174 | handle_cast(Request, State) -> 175 | % next line cannot be split into several lines, otherwise coverage will 176 | % report missing lines (and those actually are missing, but we don't care now) 177 | ?LOG_WARNING("** Undefined handle_cast in ~p~n** Unhandled message: ~tp~n", [?MODULE, Request], #{domain=>[otp], error_logger => #{tag=>warning_msg}}), 178 | {noreply, State}. 179 | 180 | %%%=================================================================== 181 | %%% Internal functions 182 | 183 | is_loaded(Mod) -> 184 | code:is_loaded(Mod) =/= false. 185 | 186 | reply_recompile(Mod, Loaded, #state{modules = Modules} = State) -> 187 | try decompile(Mod, Loaded, maps:get(cover, Modules, undefined)) of 188 | need_cover -> 189 | % requested module was cover-compiled, and now we need to decompile 'cover' 190 | % to allow further progress 191 | ModInfo = decompile(cover, true, undefined), 192 | reply_recompile(Mod, Loaded, State#state{modules = maps:put(cover, ModInfo, Modules)}); 193 | #module_data{} = ModInfo -> 194 | {reply, {ok, ModInfo}, State#state{modules = maps:put(Mod, ModInfo, Modules)}} 195 | catch 196 | error:Reason:Stack -> 197 | {reply, {error, Reason, Stack}, State} 198 | end. 199 | 200 | %%%=================================================================== 201 | 202 | extract_hash(Mod, true) -> 203 | Mod:module_info(md5); 204 | extract_hash(_, false) -> 205 | undefined. 206 | 207 | select_funs(Mod, Forms) -> 208 | lists:foldl( 209 | fun ({function, _, FunName, Arity, _} = Body, FunMap) -> 210 | maps:put({FunName, Arity}, Body, FunMap); 211 | % following can only happen if we use source *.erl code. 212 | % When it's from debug_info, beam_lib:chunks() does the 213 | % detection routine. 214 | %({attribute, _, module, ActualMod}, _) when ActualMod =/= Mod -> 215 | % erlang:error({badmatch, ActualMod}); 216 | ({attribute, _, on_load, {OnLoad, 0}}, FunMap) -> 217 | maps:put(on_load, OnLoad, FunMap); 218 | (_, FunMap) -> 219 | FunMap 220 | end, #{module => Mod}, Forms). 221 | 222 | % may error enoent for missing beam & source files 223 | % {enoent, abstract_code} for missing debug_info and source file 224 | % {badmatch, ActualName} for wrong module name 225 | % {beam_lib, Reason} for beam_lib error 226 | -spec decompile(Module :: module(), Loaded :: boolean(), Cover :: function_map() | undefined | need_cover) -> 227 | module_data() | need_cover. 228 | decompile(Mod, Loaded, Cover) when is_atom(Mod) -> 229 | case code:get_object_code(Mod) of 230 | {Mod, Binary, Filename} -> 231 | load_binary(Mod, Binary, Filename, Loaded, Cover); 232 | error -> 233 | % look for source file, we might be lucky to find one 234 | % and parse it instead of BEAM debug_info chunks 235 | load_erl(Mod, code:where_is_file(atom_to_list(Mod) ++ ".erl"), Cover) 236 | end. 237 | 238 | load_binary(Mod, Binary, Filename, Loaded, Cover) -> 239 | case beam_lib:chunks(Binary, [abstract_code]) of 240 | {ok, {Mod, [{abstract_code, {_, Forms}}]}} -> 241 | Expanded = erl_expand_records:module(Forms, [strict_record_tests]), 242 | maybe_cover(Expanded, code:which(Mod), Mod, Filename, Loaded, Cover); 243 | {ok,{Mod, [{abstract_code,no_abstract_code}]}} -> 244 | % no debug_info, but maybe there is a source file 245 | % lying around? Yes, there is a chance this file 246 | % is different from compiled BEAM version. 247 | % This could be detected by comparing md5 of 248 | % the compiled version, TODO: actually compare 249 | {ok, Source} = filelib:find_source(code:which(Mod)), 250 | load_erl(Mod, Source, Cover); 251 | {ok, {ActualMod, _}} -> 252 | erlang:error({badmatch, ActualMod}); 253 | Error -> 254 | erlang:error({beam_lib, Error}) 255 | end. 256 | 257 | load_erl(Mod, Filename, Cover) when is_list(Filename) -> 258 | {ok, Mod, Binary} = compile:file(Filename, [binary, debug_info]), 259 | load_binary(Mod, Binary, Filename, false, Cover); 260 | load_erl(_, _, _) -> 261 | error(enoent). 262 | 263 | maybe_cover(_Expanded, cover_compiled, _Mod, _Filename, _Loaded, undefined) -> 264 | need_cover; 265 | maybe_cover(Expanded, cover_compiled, Mod, Filename, Loaded, #module_data{fun_map = FunMap}) -> 266 | {attribute, _, file, {MainFile, _}} = lists:keyfind(file, 3, Expanded), 267 | Covered = cover_unsafe_internal_compile(Mod, is_modern_cover(), Expanded, MainFile, FunMap), 268 | maybe_cover(Covered, undefined, Mod, Filename, Loaded, undefined); 269 | maybe_cover(Expanded, _File, Mod, Filename, Loaded, _Cover) -> 270 | FunMap = select_funs(Mod, Expanded), 271 | #module_data{ 272 | hash = extract_hash(Mod, Loaded), 273 | fun_map = maybe_onload( 274 | %% compatibility behaviour: power_shell was skipping on_load 275 | application:get_env(power_shell, skip_on_load, true), 276 | Mod, 277 | maps:get(on_load, FunMap, false), 278 | FunMap), 279 | filename = Filename, 280 | mtime = filelib:last_modified(Filename) 281 | }. 282 | 283 | maybe_onload(false, Mod, FunName, FunMap) when is_map_key(on_load, FunMap) -> 284 | ok = power_shell:eval(Mod, FunName, [], FunMap), 285 | FunMap; 286 | maybe_onload(_Skip, _Mod, _, FunMap) -> 287 | FunMap. 288 | 289 | is_modern_cover() -> 290 | Vsn = case application:get_key(tools, vsn) of 291 | {ok, Vsn0} -> 292 | Vsn0; 293 | undefined -> 294 | ok = application:load(tools), 295 | {ok, Vsn0} = application:get_key(tools, vsn), 296 | Vsn0 297 | end, 298 | [Major, Minor | _] = [list_to_integer(V) || V <- string:lexemes(Vsn, ".")], 299 | Major >= 3 andalso Minor >= 2. 300 | 301 | %% 'cover', OTP21 or below 302 | cover_unsafe_internal_compile(Mod, false, Expanded, MainFile, FunMap) -> 303 | {Covered, _Vars} = power_shell:eval(cover, transform, [Expanded, Mod, MainFile], FunMap), 304 | Covered; 305 | 306 | %% 'cover' from tools-3.2 and above: uses 'counters' 307 | cover_unsafe_internal_compile(Mod, true, Expanded, MainFile, FunMap) -> 308 | Vars0 = {vars, Mod, [], undefined, undefined, undefined, undefined, undefined, undefined, false}, 309 | {ok, MungedForms0, _Vars} = power_shell:eval(cover, transform_2, [Expanded, [], Vars0, MainFile, on], FunMap), 310 | %{Covered1, _Vars} = power_shell:eval(cover, transform, [Expanded, power_shell, MainFile, true], FunMap), 311 | Cref = ets:lookup_element(cover_internal_mapping_table, {counters, Mod}, 2), 312 | AbstrCref = power_shell:eval(cover, cid_to_abstract, [Cref], FunMap), 313 | power_shell:eval(cover, patch_code1, [MungedForms0, {local_only,AbstrCref}], FunMap). 314 | -------------------------------------------------------------------------------- /test/power_shell_cache_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Maxim Fedorov 3 | %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. 4 | %%% @private 5 | %%%------------------------------------------------------------------- 6 | -module(power_shell_cache_SUITE). 7 | -author("maximfca@gmail.com"). 8 | 9 | %%-------------------------------------------------------------------- 10 | 11 | -export([all/0, 12 | suite/0, 13 | init_per_suite/1, end_per_suite/1, 14 | init_per_testcase/2, end_per_testcase/2 15 | ]). 16 | 17 | -export([get_module/0, get_module/1, 18 | bad_calls/0, bad_calls/1, 19 | start_stop/0, start_stop/1, 20 | wrong_module/0, wrong_module/1, 21 | cache_md5_check/0, cache_md5_check/1, 22 | not_loaded/0, not_loaded/1, 23 | no_debug_info/0, no_debug_info/1, 24 | cover_compiled/0, cover_compiled/1, 25 | cover_compiled_direct/0, cover_compiled_direct/1, 26 | parse_transform/0, parse_transform/1, 27 | source_beam_select/0, source_beam_select/1, 28 | source_reload/0, source_reload/1, 29 | no_beam/0, no_beam/1, 30 | on_load/0, on_load/1, 31 | no_on_load/0, no_on_load/1, 32 | broken_beam/0, broken_beam/1]). 33 | 34 | %% Common Test headers 35 | -include_lib("common_test/include/ct.hrl"). 36 | 37 | %% Include stdlib header to enable ?assert() for readable output 38 | -include_lib("stdlib/include/assert.hrl"). 39 | -include_lib("kernel/include/file.hrl"). 40 | 41 | -define(SERVER, power_shell_cache). 42 | -define(VICTIM, power_shell_SUITE). 43 | 44 | suite() -> 45 | [{timetrap, {seconds, 30}}]. 46 | 47 | all() -> 48 | [get_module, bad_calls, start_stop, cache_md5_check, not_loaded, cover_compiled_direct, 49 | no_debug_info, cover_compiled, parse_transform, source_beam_select, 50 | source_reload, no_beam, on_load, no_on_load, broken_beam, wrong_module]. 51 | 52 | init_per_suite(Config) -> 53 | Loaded = application:load(power_shell), 54 | ?assert(Loaded =:= ok orelse Loaded =:= {error,{already_loaded,power_shell}}), 55 | ok = application:set_env(power_shell, cache_code, true), 56 | {ok, _} = application:ensure_all_started(power_shell), 57 | % make a backup of power_shell_SUITE.beam 58 | Path = code:which(?VICTIM), 59 | % into private data dir 60 | PrivPath = ?config(priv_dir, Config), 61 | BackupPath = [PrivPath, filename:basename(Path)], 62 | {ok, _} = file:copy(Path, BackupPath), 63 | [{beam_backup, {Path, BackupPath}} | Config]. 64 | 65 | end_per_suite(Config) -> 66 | case proplists:get_value(beam_backup, Config, undefined) of 67 | {Path, BackupPath} -> 68 | file:copy(BackupPath, Path); 69 | _ -> 70 | ok 71 | end, 72 | ok = application:unset_env(power_shell, cache_code), 73 | ok = application:stop(power_shell), 74 | Config. 75 | 76 | init_per_testcase(cover_compiled_direct, Config) -> 77 | ok = application:stop(power_shell), 78 | Config; 79 | init_per_testcase(_TestCase, Config) -> 80 | Config. 81 | 82 | end_per_testcase(cover_compiled_direct, Config) -> 83 | ok = application:start(power_shell), 84 | Config; 85 | end_per_testcase(_TestCase, Config) -> 86 | Config. 87 | 88 | touch_file(Filename) -> 89 | {ok, FileInfo} = file:read_file_info(Filename, [{time, posix}]), 90 | ok = file:write_file_info(Filename, 91 | FileInfo#file_info{mtime = FileInfo#file_info.mtime + 2}, [{time, posix}]). 92 | 93 | get_counter(false, Fun, Arity) -> 94 | Pattern = {{bump, power_shell_default, Fun, Arity, '_', '$1'}, '$2'}, 95 | Lines = ets:match(cover_internal_data_table, Pattern), 96 | [_ExpectedLine, Counter] = hd(Lines), 97 | Counter; 98 | get_counter(true, Fun, Arity) -> 99 | Pattern = {{bump, power_shell_default, Fun, Arity, '_', '$1'}, '$2'}, 100 | Lines = ets:match(cover_internal_mapping_table, Pattern), 101 | [_ExpectedLine, CounterIndex] = hd(Lines), 102 | counters:get(persistent_term:get({cover, power_shell_default}), CounterIndex). 103 | 104 | cover_compile_test(TestFun) -> 105 | application:load(tools), 106 | {ok, Vsn} = application:get_key(tools, vsn), 107 | [Major, Minor | _] = [list_to_integer(V) || V <- string:lexemes(Vsn, ".")], 108 | Modern = Major >= 3 andalso Minor >= 2, 109 | % power_shell itself should be cover-compiled, ensure it is so 110 | ?assertEqual(cover_compiled, code:which(power_shell_default)), 111 | % ensure decompilation works on cover-compiled files 112 | FunMap = power_shell_cache:get_module(power_shell_default), 113 | ?assertNotEqual(error, maps:find({proxy_fun, 3}, FunMap)), 114 | % now verify that cover-compiled modules are actually bumping their counters 115 | %% new code for OTP 22 116 | PrevCounter = get_counter(Modern, proxy_fun, 3), 117 | % "we need to go deeper" - now within 4 apply levels 118 | Reps = 10, 119 | % do this "Reps" times 120 | [ 121 | ?assertMatch({function,1,node,0, _}, TestFun()) 122 | || _ <- lists:seq(1, Reps)], 123 | % Get the counter again 124 | NextCounter = get_counter(Modern, proxy_fun, 3), 125 | ?assertEqual(Reps, NextCounter - PrevCounter). 126 | 127 | get_module() -> 128 | [{doc, "Tests decompilation from debug_info, an check it is cached"}]. 129 | 130 | get_module(_Config) -> 131 | code:delete(?VICTIM), 132 | ?assert(is_map(power_shell_cache:get_module(?VICTIM))), 133 | {state, #{?VICTIM := ModInfo}} = sys:get_state(power_shell_cache), 134 | ?assertEqual(module_data, element(1, ModInfo)), 135 | % ugly check via 'coverage' info 136 | % does not really verify that cache is used, TODO: implement the check 137 | ?assert(is_map(power_shell_cache:get_module(?VICTIM))). 138 | 139 | bad_calls() -> 140 | [{doc, "Tests invalid casts and calls to cache server"}]. 141 | 142 | bad_calls(_Config) -> 143 | ?assertEqual({error, {undef, bad_calls}}, 144 | gen_server:call(?SERVER, bad_calls)), 145 | % no report from cast or info, but at least test it does not crash 146 | % that's mostly for coverage 147 | gen_server:cast(?SERVER, cast), 148 | ok. 149 | 150 | start_stop() -> 151 | [{doc, "Tests cache start/stop procedures"}]. 152 | 153 | start_stop(_Config) -> 154 | ok = supervisor:terminate_child(power_shell_sup, ?SERVER), 155 | {ok, _Pid} = supervisor:restart_child(power_shell_sup, ?SERVER). 156 | 157 | not_loaded() -> 158 | [{doc, "Tests interpretation of not yet loaded *.beam"}]. 159 | 160 | not_loaded(_Config) -> 161 | code:delete(?VICTIM), 162 | ?assertNot(code:is_loaded(?VICTIM)), 163 | % 164 | ?assertEqual(echo, power_shell:eval(?VICTIM, local_unexported, [echo])), 165 | % 166 | ?assertNot(code:is_loaded(?VICTIM)), 167 | ok. 168 | 169 | cache_md5_check() -> 170 | [{doc, "Test cache reloading BEAM when in-memory md5 changes"}]. 171 | 172 | cache_md5_check(_Config) -> 173 | % ensure current version is cached 174 | code:ensure_loaded(?VICTIM), 175 | power_shell_cache:get_module(?VICTIM), 176 | {state, #{?VICTIM := ModInfo}} = sys:get_state(power_shell_cache), 177 | ?assertEqual(?VICTIM:module_info(md5), element(2, ModInfo)), 178 | % replace module so its md5 changes 179 | Filename = code:which(?VICTIM), 180 | {ok, ?VICTIM, Chunks} = beam_lib:all_chunks(Filename), 181 | % add atom 'none' to the tail of the atom list 182 | {value, {"AtU8", <>}, AllButAtoms} = 183 | lists:keytake("AtU8", 1, Chunks), 184 | Count1 = Count + 1, 185 | NewAtomChunk = <>/binary>>, 186 | {ok, Bin} = beam_lib:build_module([{"AtU8", NewAtomChunk} | AllButAtoms]), 187 | % reload module that we've just build 188 | {module, ?VICTIM} = code:load_binary(?VICTIM, Filename, Bin), 189 | code:purge(?VICTIM), 190 | % ensure it still works 191 | ?assertEqual(echo, power_shell:eval(?VICTIM, local_unexported, [echo])), 192 | % ensure new md5 is cached 193 | {state, #{?VICTIM := ModInfo1}} = sys:get_state(power_shell_cache), 194 | ?assertEqual(?VICTIM:module_info(md5), element(2, ModInfo1)), 195 | ok. 196 | 197 | no_debug_info() -> 198 | [{doc, "Tests loading from source code (*.erl) due to missing debug_info chunk"}]. 199 | 200 | no_debug_info(_Config) -> 201 | % remove debug_info from power_shell_SUITE 202 | Filename = code:which(?VICTIM), 203 | {ok, ?VICTIM, Chunks} = beam_lib:all_chunks(Filename), 204 | % 205 | WithoutDbgInfo = lists:keydelete("Dbgi", 1, Chunks), 206 | {ok, Bin} = beam_lib:build_module(WithoutDbgInfo), 207 | ok = file:write_file(Filename, Bin), 208 | touch_file(Filename), 209 | % rewrite the *.beam file 210 | {module, ?VICTIM} = code:load_binary(?VICTIM, Filename, Bin), 211 | code:purge(?VICTIM), 212 | % keep it loaded, or unload - result should be the same 213 | ?assertEqual(echo, power_shell:eval(?VICTIM, local_unexported, [echo])), 214 | ok. 215 | 216 | cover_compiled() -> 217 | [{doc, "Tests *.beam lookup for cover-compiled modules"}]. 218 | 219 | cover_compiled(_Config) -> 220 | cover_compile_test(fun () -> power_shell:eval(power_shell_default, proxy_fun, [erlang, node, 0]) end). 221 | 222 | cover_compiled_direct() -> 223 | [{doc, "Tests *.beam lookup for cover-compiled modules, but without using gen_server cache"}]. 224 | 225 | cover_compiled_direct(_Config) -> 226 | FunMap = power_shell_cache:get_module(power_shell_default), 227 | cover_compile_test(fun () -> power_shell:eval(power_shell_default, proxy_fun, [erlang, node, 0], FunMap) end). 228 | 229 | parse_transform() -> 230 | [{doc, "Tests that parse_transforms are applied when reading source code from *.erl file"}]. 231 | 232 | parse_transform(Config) -> 233 | Source = 234 | "-module(pt). -export([bar/0]). -include_lib(\"syntax_tools/include/merl.hrl\"). " 235 | "bar() -> inner(). " 236 | "inner() ->" 237 | "Tuple = ?Q(\"{foo, 42}\")," 238 | "?Q(\"{foo, _@Number}\") = Tuple," 239 | "Call = ?Q(\"foo:bar(_@Number)\")," 240 | "erl_prettypr:format(merl:tree(Call)).", 241 | PrivPath = ?config(priv_dir, Config), 242 | true = code:add_path(PrivPath), 243 | Filename = filename:join(PrivPath, "pt.erl"), 244 | ok = file:write_file(Filename, Source), 245 | ?assertEqual("foo:bar(42)", power_shell:eval(pt, inner, [])), 246 | true = code:del_path(PrivPath), 247 | ok. 248 | 249 | source_beam_select() -> 250 | [{doc, "Tests reloading from the latest timestamped file (*.beam or *.erl)"}]. 251 | 252 | source_beam_select(Config) -> 253 | Common = "-module(m1).\n-export([bar/0]).\nbar() -> inner().\ninner() ->", 254 | PrivPath = ?config(priv_dir, Config), 255 | true = code:add_path(PrivPath), 256 | SrcName = filename:join(PrivPath, "m1.erl"), 257 | ok = file:write_file(SrcName, Common ++ "bar."), 258 | touch_file(SrcName), 259 | %% write *.beam file that's younger 260 | Forms = [ 261 | begin 262 | {ok, Line, _} = erl_scan:string(L), 263 | {ok, F} = erl_parse:parse_form(Line), 264 | F 265 | end || L <- string:lexemes(Common ++ "baz.", "\n")], 266 | {ok, m1, Bin} = compile:forms(Forms, [no_spawn_compiler_process, debug_info]), 267 | BeamName = filename:join(PrivPath, "m1.beam"), 268 | ok = file:write_file(BeamName, Bin), 269 | touch_file(BeamName), 270 | %% ensure new beam was selected 271 | ?assertEqual(baz, power_shell:eval(m1, inner, [])), 272 | %% overwrite source file, ensure old beam is still used 273 | ok = file:write_file(SrcName, Common ++ "ding."), 274 | touch_file(SrcName), 275 | ?assertEqual(baz, power_shell:eval(m1, inner, [])), 276 | %% now recompile and ensure new one is... 277 | {ok, m1} = compile:file(SrcName, [no_spawn_compiler_process, debug_info, {outdir, ?config(priv_dir, Config)}]), 278 | ?assertEqual(ding, power_shell:eval(m1, inner, [])), 279 | true = code:del_path(PrivPath), 280 | ok. 281 | 282 | source_reload() -> 283 | [{doc, "Tests reloading source (*.erl) file"}]. 284 | 285 | source_reload(Config) -> 286 | Source = "-module(foo). -export([bar/0]). bar() -> inner(). inner() -> blah.", 287 | Source2 = "-module(foo). -export([bar/0]). bar() -> inner(). inner() -> boo.", 288 | PrivPath = ?config(priv_dir, Config), 289 | true = code:add_path(PrivPath), 290 | Filename = filename:join(PrivPath, "foo.erl"), 291 | ok = file:write_file(Filename, Source), 292 | touch_file(Filename), 293 | ?assertEqual(blah, power_shell:eval(foo, inner, [])), 294 | ok = file:write_file(Filename, Source2), 295 | ?assertEqual(boo, power_shell:eval(foo, inner, [])), 296 | true = code:del_path(PrivPath), 297 | ok. 298 | 299 | no_beam() -> 300 | [{doc, "Tests loading from source code (*.erl) due to missing *.beam file"}]. 301 | 302 | no_beam(Config) -> 303 | {Filename, _} = ?config(beam_backup, Config), 304 | ?assertNotEqual(non_existing, Filename), 305 | file:delete(Filename), 306 | code:purge(?VICTIM), 307 | code:delete(?VICTIM), 308 | ?assertNot(code:is_loaded(?VICTIM)), 309 | ?assertEqual(echo, power_shell:eval(?VICTIM, local_unexported, [echo])), 310 | ?assertNot(code:is_loaded(?VICTIM)), 311 | ok. 312 | 313 | on_load() -> 314 | [{doc, "Tests on_load executed for AST loaded from BEAM/source file"}]. 315 | 316 | on_load(Config) -> 317 | ok = application:set_env(power_shell, skip_on_load, false), 318 | Source = 319 | "-module(onl). -export([bar/0]). -on_load(side_effect/0). " 320 | "side_effect() -> ets:new(side, [named_table, public]), ok. bar() -> inner(). inner() -> ets:insert(side, {1, 2}).", 321 | PrivPath = ?config(priv_dir, Config), 322 | true = code:add_path(PrivPath), 323 | Filename = filename:join(PrivPath, "onl.erl"), 324 | ok = file:write_file(Filename, Source), 325 | power_shell_cache:get_module(onl), 326 | ?assertEqual(true, power_shell:eval(onl, inner, [])), 327 | true = code:del_path(PrivPath), 328 | %% cleanup side effect - remove ETS table 329 | true = ets:delete(side), 330 | ok = application:unset_env(power_shell, skip_on_load), 331 | ok. 332 | 333 | no_on_load() -> 334 | [{doc, "Tests on_load skipped for modules without on_load function"}]. 335 | 336 | no_on_load(Config) -> 337 | ok = application:set_env(power_shell, skip_on_load, false), 338 | Source = 339 | "-module(no_onl). -export([bar/0]). " 340 | "bar() -> inner(). inner() -> true.", 341 | PrivPath = ?config(priv_dir, Config), 342 | true = code:add_path(PrivPath), 343 | Filename = filename:join(PrivPath, "no_onl.erl"), 344 | ok = file:write_file(Filename, Source), 345 | power_shell_cache:get_module(no_onl), 346 | ?assert(power_shell:eval(no_onl, inner, [])), 347 | true = code:del_path(PrivPath), 348 | ok = application:unset_env(power_shell, skip_on_load), 349 | ok. 350 | 351 | broken_beam() -> 352 | [{doc, "Tests attempt to load a non-beam file"}]. 353 | 354 | broken_beam(Config) -> 355 | % make a fresh server 356 | ok = supervisor:terminate_child(power_shell_sup, ?SERVER), 357 | {ok, _Pid} = supervisor:restart_child(power_shell_sup, ?SERVER), 358 | % 359 | Filename = element(1, ?config(beam_backup, Config)), 360 | ?assertNotEqual(non_existing, Filename), 361 | ok = file:write_file(Filename, <<"FOR1 broken non-beam file">>), 362 | touch_file(Filename), 363 | ?assertMatch( 364 | {'EXIT', {{beam_lib, {error,beam_lib, {not_a_beam_file, _}}}, _}}, 365 | catch power_shell:eval(?VICTIM, local_unexported, [echo])), 366 | ok. 367 | 368 | wrong_module() -> 369 | [{doc, "Attempt to load module with wrong name"}]. 370 | 371 | wrong_module(Config) -> 372 | % make a fresh server 373 | ok = supervisor:terminate_child(power_shell_sup, ?SERVER), 374 | {ok, _Pid} = supervisor:restart_child(power_shell_sup, ?SERVER), 375 | % 376 | {Filename, _} = ?config(beam_backup, Config), 377 | ?assertNotEqual(non_existing, Filename), 378 | {ok, _} = file:copy(code:which(?MODULE), Filename), 379 | touch_file(Filename), 380 | ?assertMatch({'EXIT', {{badmatch, ?MODULE}, _}}, 381 | catch power_shell_cache:get_module(?VICTIM)), 382 | ok. 383 | -------------------------------------------------------------------------------- /test/power_shell_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Maxim Fedorov 3 | %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. 4 | %%% @private 5 | %%%------------------------------------------------------------------- 6 | -module(power_shell_SUITE). 7 | -author("maximfca@gmail.com"). 8 | 9 | %%-------------------------------------------------------------------- 10 | %% IMPORTANT: DO NOT USE EXPORT_ALL! 11 | %% This test relies on functions _not_ being exported from the module. 12 | %% It is the whole point of test. 13 | 14 | -export([all/0, 15 | groups/0, 16 | init_per_group/2, 17 | end_per_group/2, 18 | suite/0]). 19 | 20 | -export([echo/0, echo/1, 21 | self_echo/0, self_echo/1, 22 | calling_local/0, calling_local/1, 23 | second_clause/0, second_clause/1, 24 | undef/0, undef/1, 25 | undef_local/0, undef_local/1, 26 | recursive/0, recursive/1, 27 | undef_nested/0, undef_nested/1, 28 | bad_match/0, bad_match/1, 29 | function_clause/0, function_clause/1, 30 | preloaded/0, preloaded/1, 31 | throwing/0, throwing/1, 32 | callback_local/0, callback_local/1, 33 | callback_local_fun_obj/0, callback_local_fun_obj/1, 34 | remote_callback/0, remote_callback/1, 35 | callback_local_make_fun/0, callback_local_make_fun/1, 36 | remote_callback_exported/0, remote_callback_exported/1, 37 | record/0, record/1, 38 | try_side_effect/0, try_side_effect/1, 39 | rebind_var/0, rebind_var/1, 40 | external_fun/0, external_fun/1, 41 | map_comp_from_list/0, map_comp_from_list/1, 42 | map_comp_from_bin/0, map_comp_from_bin/1, 43 | map_gen_to_map/0, map_gen_to_map/1, 44 | map_gen_to_list/0, map_gen_to_list/1, 45 | map_gen_to_bin/0, map_gen_to_bin/1, 46 | catch_apply/0, catch_apply/1, 47 | export/0, export/1, 48 | export_partial/0, export_partial/1, 49 | export_auto_revert/0, export_auto_revert/1, 50 | export_no_link/0, export_no_link/1, 51 | recursive_eval/0, recursive_eval/1 52 | ]). 53 | 54 | -export([export_all/0, remote_cb_exported/1]). 55 | 56 | %% Common Test headers 57 | -include_lib("common_test/include/ct.hrl"). 58 | 59 | %% Include stdlib header to enable ?assert() for readable output 60 | -include_lib("stdlib/include/assert.hrl"). 61 | 62 | %%-------------------------------------------------------------------- 63 | %% Function: suite() -> Info 64 | %% Info = [tuple()] 65 | %%-------------------------------------------------------------------- 66 | suite() -> 67 | [{timetrap, {seconds, 30}}]. 68 | 69 | test_cases() -> 70 | [echo, self_echo, preloaded, second_clause, undef, undef_local, undef_nested, recursive, 71 | calling_local, throwing, bad_match, function_clause, remote_callback, 72 | callback_local, callback_local_fun_obj, callback_local_make_fun, recursive_eval, 73 | remote_callback_exported, record, try_side_effect, rebind_var, external_fun, catch_apply, 74 | map_comp_from_list, map_comp_from_bin, map_gen_to_map, map_gen_to_list, map_gen_to_bin]. 75 | 76 | %%-------------------------------------------------------------------- 77 | %% Function: groups() -> [Group] 78 | %% Group = {GroupName,Properties,GroupsAndTestCases} 79 | %% GroupName = atom() 80 | %% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] 81 | %% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] 82 | %% TestCase = atom() 83 | %% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}} 84 | %% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | 85 | %% repeat_until_any_ok | repeat_until_any_fail 86 | %% N = integer() | forever 87 | %%-------------------------------------------------------------------- 88 | groups() -> 89 | [ 90 | {direct, [parallel], test_cases()}, 91 | {cached, [parallel], test_cases()}, 92 | {export, [], [export, export_partial, export_auto_revert, export_no_link]} 93 | ]. 94 | 95 | %%-------------------------------------------------------------------- 96 | %% Function: all() -> GroupsAndTestCases | {skip,Reason} 97 | %% GroupsAndTestCases = [{group,GroupName} | TestCase] 98 | %% GroupName = atom() 99 | %% TestCase = atom() 100 | %% Reason = term() 101 | %%-------------------------------------------------------------------- 102 | all() -> 103 | [{group, direct}, {group, cached}, {group, export}]. 104 | 105 | %%-------------------------------------------------------------------- 106 | %% Function: init_per_group(GroupName, Config0) -> 107 | %% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} 108 | %% GroupName = atom() 109 | %% Config0 = Config1 = [tuple()] 110 | %% Reason = term() 111 | %%-------------------------------------------------------------------- 112 | init_per_group(cached, Config) -> 113 | % ensure power_shell cache started 114 | Loaded = application:load(power_shell), 115 | ?assert(Loaded =:= ok orelse Loaded =:= {error,{already_loaded,power_shell}}), 116 | ok = application:set_env(power_shell, cache_code, true), 117 | {ok, _} = application:ensure_all_started(power_shell), 118 | Config; 119 | init_per_group(_GroupName, Config) -> 120 | Config. 121 | 122 | %%-------------------------------------------------------------------- 123 | %% Function: end_per_group(GroupName, Config0) -> 124 | %% term() | {save_config,Config1} 125 | %% GroupName = atom() 126 | %% Config0 = Config1 = [tuple()] 127 | %%-------------------------------------------------------------------- 128 | end_per_group(cached, _Config) -> 129 | ok = application:unset_env(power_shell, cache_code), 130 | ok = application:stop(power_shell), 131 | ok; 132 | end_per_group(_GroupName, _Config) -> 133 | ok. 134 | 135 | %%-------------------------------------------------------------------- 136 | %% IMPORTANT: THESE MUST NOT BE EXPORTED !!! 137 | 138 | local_unexported(Echo) -> 139 | Echo. 140 | 141 | local_self() -> 142 | erlang:self(). 143 | 144 | local_unexported_recursive(N, Acc) when N =< 0 -> 145 | Acc; 146 | local_unexported_recursive(N, Acc) -> 147 | local_unexported_recursive(N - 1, [N | Acc]). 148 | 149 | local_unexported_nested(N) -> 150 | local_unexported_recursive(N, []). 151 | 152 | local_undef_nested(Atom) -> 153 | local_undef_nested_impl(Atom), 154 | true = rand:uniform(100). % to avoid tail recursion 155 | 156 | local_undef_nested_impl(Atom) -> 157 | not_a_module:not_a_function(1, Atom, 3), 158 | true = rand:uniform(100). % to avoid tail recursion 159 | 160 | local_second_clause(Arg, Selector) when Selector =:= one -> 161 | Arg + 1; 162 | local_second_clause(Arg, Selector) when Selector =:= two -> 163 | Arg + 2; 164 | local_second_clause(Arg, Selector) when Selector =:= three -> 165 | Arg + 3; 166 | local_second_clause(Arg, Selector) when is_atom(Selector) -> 167 | Arg + 10. 168 | 169 | local_throw(What) -> 170 | rand:uniform(100) < 200 andalso throw(What). 171 | 172 | local_throwing() -> 173 | local_throw(ball), 174 | % to make it non-tail-recursive and save call stack: 175 | ok. 176 | 177 | local_bad_match() -> 178 | local_do_bad_match(one), 179 | true = rand:uniform(100). 180 | 181 | local_do_bad_match(What) -> 182 | true = What =:= rand:uniform(100), 183 | ok. 184 | 185 | local_function_clause() -> 186 | local_second_clause(0, get(test_server_logopts)), 187 | true = rand:uniform(100). 188 | 189 | local_cb_fun(Arg) -> 190 | {cb, Arg}. 191 | 192 | local_cb_caller(Fun, Args) -> 193 | Fun(Args). 194 | 195 | local_cb_init() -> 196 | local_cb_caller(fun local_cb_fun/1, [1]). 197 | 198 | local_cb_fun_obj() -> 199 | local_cb_caller(fun ([N]) -> N * 2 end, [2]). 200 | 201 | local_cb_make_fun() -> 202 | F = erlang:make_fun(?MODULE, module_info, 1), 203 | local_cb_caller(F, md5). 204 | 205 | remote_cb(N) when N <0 -> 206 | N rem 3 =:= 0; 207 | remote_cb(N) -> 208 | N rem 2 =:= 0. 209 | 210 | remote_cb_init(List) -> 211 | Cb = fun remote_cb/1, 212 | case List of 213 | [] -> 214 | ok; 215 | [E] -> 216 | Cb(E); 217 | [_ | _] -> 218 | lists:filter(fun remote_cb/1, List) 219 | end, 220 | %lists:filter(fun (K) -> K + 1, F = fun remote_cb/1, F(K) end, List). 221 | lists:filter(Cb, List). 222 | 223 | remote_cb_exported(N) -> 224 | N rem 3 =:= 0. 225 | 226 | remote_cb_exported_init(List) -> 227 | lists:filter(fun ?MODULE:remote_cb_exported/1, List). 228 | 229 | %% Kitchen sink to silence compiler in a good way (without suppressing warnings) 230 | export_all() -> 231 | local_undef_nested(atom), 232 | local_cb_fun(1), 233 | local_throwing(), 234 | local_bad_match(), 235 | rebind([]), 236 | external_filter([]), 237 | otp26_maps(), 238 | throw_applied(), 239 | try_side({1, 1}). 240 | 241 | -if(?OTP_RELEASE > 25). 242 | otp26_maps() -> 243 | map_comprehension_from_list(), 244 | map_comprehension_from_binary(), 245 | map_generator_to_map(), 246 | map_generator_to_list(), 247 | map_generator_to_binary(). 248 | -else. 249 | otp26_maps() -> 250 | ok. 251 | -endif. 252 | 253 | -record(rec, {first= "1", second, third = initial}). 254 | create_record() -> 255 | #rec{third = application:get_env(kernel, missing, 3), first = "1"}. 256 | 257 | modify_record(#rec{} = Rec) -> 258 | Rec#rec{third = 10, second = "2"}. 259 | 260 | try_side(MaybeBinaryInteger) -> 261 | try 262 | _ = binary_to_integer(MaybeBinaryInteger), 263 | true 264 | catch 265 | error:badarg -> 266 | false 267 | end. 268 | 269 | internal_lambda(Atom) -> 270 | RebindVar = atom_to_list(Atom), 271 | is_list(RebindVar). 272 | 273 | rebind(Original) -> 274 | RebindVar = Original, 275 | lists:filtermap(fun internal_lambda/1, RebindVar). 276 | 277 | external_filter(List) -> 278 | lists:filter(fun erlang:is_number/1, List). 279 | 280 | throw_applied() -> 281 | Args = [[fun() -> exit(some_error) end, []], []], % this generates: {'EXIT',{{badfun,[#Fun,[]]}, 282 | case catch apply(erlang, apply, Args) of 283 | {'EXIT', _Reason} -> 284 | throw(expected) 285 | end. 286 | 287 | -if(?OTP_RELEASE > 25). 288 | map_comprehension_from_list() -> 289 | #{N => N*N || N <- lists:seq(1, 10), N < 4}. 290 | 291 | map_comprehension_from_binary() -> 292 | #{X => Y || <> <= <<"abcdefgh">>}. 293 | 294 | map_generator_to_map() -> 295 | #{V => K 296 | || K := V <- #{1 => 1, 2 => 4, 3 => 9, one => one, two => four}, 297 | is_integer(K)}. 298 | 299 | map_generator_to_list() -> 300 | lists:sort([{V, K} || K := V <- #{1 => 1, 2 => 4, 3 => 9}]). 301 | 302 | map_generator_to_binary() -> 303 | << <> || K := V <- #{$a => $b, $c => $d} >>. 304 | -endif. 305 | 306 | %%-------------------------------------------------------------------- 307 | %% Test Cases 308 | 309 | echo() -> 310 | [{doc}, "Evaluate non-exported function"]. 311 | 312 | echo(_Config) -> 313 | ?assertEqual(local_unexported(echo), power_shell:eval(?MODULE, local_unexported, [echo])), 314 | ok. 315 | 316 | self_echo() -> 317 | [{doc}, "Evaluates function returning self() - to see if NIFs are working"]. 318 | 319 | self_echo(_Config) -> 320 | ?assertEqual(local_self(), power_shell:eval(?MODULE, local_self, [])), 321 | ok. 322 | 323 | undef() -> 324 | [{doc, "Ensure undefined function throws undef"}]. 325 | 326 | undef(_Config) -> 327 | % next 2 statements must be on the same line, otherwise stack trace info is broken 328 | {current_stacktrace, Trace} = process_info(self(), current_stacktrace), Actual = (catch power_shell:eval(not_a_module, not_a_function, [1, 2, 3])), 329 | Expected = {'EXIT', {undef, [{not_a_module, not_a_function,[1,2,3], []}] ++ Trace}}, 330 | ?assertEqual(Expected, Actual), 331 | ok. 332 | 333 | undef_local() -> 334 | [{doc, "Ensure undefined function in this very module throws undef"}]. 335 | 336 | undef_local(_Config) -> 337 | % next 2 statments must be on the same line, otherwise stack trace info is broken 338 | {current_stacktrace, Trace} = process_info(self(), current_stacktrace), Actual = (catch power_shell:eval(?MODULE, not_a_function, [1, 2, 3])), 339 | Expected = {'EXIT', {undef, [{?MODULE,not_a_function,[1,2,3], []}] ++ Trace}}, 340 | ?assertEqual(Expected, Actual), 341 | ok. 342 | 343 | undef_nested() -> 344 | [{doc, "Ensure undefined function throws undef even when it's nested"}]. 345 | 346 | undef_nested(_Config) -> 347 | exception_check(fun local_undef_nested/1, local_undef_nested, [atom]). 348 | 349 | preloaded() -> 350 | [{doc, "Ensure that functions from preloaded modules are just applied"}]. 351 | 352 | preloaded(_Config) -> 353 | ?assertEqual([2, 1], power_shell:eval(prim_zip, reverse, [[1, 2]])), 354 | ?assertEqual(self(), power_shell:eval(erlang, self, [])). 355 | 356 | throwing() -> 357 | [{doc, "Unexported function throwing"}]. 358 | 359 | throwing(_Config) -> 360 | exception_check(fun local_throwing/0, local_throwing, []). 361 | 362 | bad_match() -> 363 | [{doc, "Unexported function throwing badmatch"}]. 364 | 365 | bad_match(_Config) -> 366 | exception_check(fun local_bad_match/0, local_bad_match, []). 367 | 368 | function_clause() -> 369 | [{doc, "Unexported function throwing function_clause"}]. 370 | 371 | function_clause(_Config) -> 372 | exception_check(fun local_function_clause/0, local_function_clause, []). 373 | 374 | recursive() -> 375 | [{doc, "Evaluate recursive function"}]. 376 | 377 | recursive(_Config) -> 378 | ?assertEqual(local_unexported_recursive(1, []), power_shell:eval(?MODULE, local_unexported_recursive, [1, []])), 379 | ok. 380 | 381 | calling_local() -> 382 | [{doc, "Evaluate non-exported function calling another non-exported function"}]. 383 | 384 | calling_local(_Config) -> 385 | ?assertEqual(local_unexported_nested(0), power_shell:eval(?MODULE, local_unexported_nested, [0])), 386 | ok. 387 | 388 | second_clause() -> 389 | [{doc, "Evaluate non-exported function with multiple clauses, ensure right one is selected"}]. 390 | 391 | second_clause(_Config) -> 392 | ?assertEqual(local_second_clause(10, two), power_shell:eval(?MODULE, local_second_clause, [10, two])), 393 | ok. 394 | 395 | callback_local() -> 396 | [{doc, "Non-exported function calls some function that calls a callback fun"}]. 397 | 398 | callback_local(_Config) -> 399 | ?assertEqual(local_cb_init(), power_shell:eval(?MODULE, local_cb_init, [])), 400 | ok. 401 | 402 | callback_local_fun_obj() -> 403 | [{doc, "Non-exported function calls some function that calls a callback function object"}]. 404 | 405 | callback_local_fun_obj(_Config) -> 406 | ?assertEqual(local_cb_fun_obj(), power_shell:eval(?MODULE, local_cb_fun_obj, [])), 407 | ok. 408 | 409 | callback_local_make_fun() -> 410 | [{doc, "Non-exported function and make_fun/3 fun"}]. 411 | 412 | callback_local_make_fun(_Config) -> 413 | ?assertEqual(local_cb_make_fun(), power_shell:eval(?MODULE, local_cb_make_fun, [])), 414 | ok. 415 | 416 | remote_callback_exported() -> 417 | [{doc, "Non-exported function calls remote function that calls exported callback"}]. 418 | 419 | remote_callback_exported(_Config) -> 420 | L = lists:seq(1, 20), 421 | ?assertEqual(remote_cb_exported_init(L), power_shell:eval(?MODULE, remote_cb_exported_init, [L])), 422 | ok. 423 | 424 | remote_callback() -> 425 | [{doc, "Non-exported function calls some function that calls a callback fun"}]. 426 | 427 | remote_callback(_Config) -> 428 | L = lists:seq(1, 20), 429 | ?assertEqual(remote_cb_init(L), power_shell:eval(?MODULE, remote_cb_init, [L])), 430 | ok. 431 | 432 | record() -> 433 | [{doc, "Tests records - creation & modification"}]. 434 | 435 | record(_Config) -> 436 | Rec = create_record(), 437 | ?assertEqual(Rec, power_shell:eval(?MODULE, create_record, [])), 438 | ?assertEqual(modify_record(Rec), power_shell:eval(?MODULE, modify_record, [Rec])). 439 | 440 | try_side_effect() -> 441 | [{doc, "Tests try ... catch returning value from both flows"}]. 442 | 443 | try_side_effect(_Config) -> 444 | ?assertEqual(false, power_shell:eval(?MODULE, try_side, [atom])). 445 | 446 | rebind_var() -> 447 | [{doc, "Tests that variable is unbound when returned from function"}]. 448 | 449 | rebind_var(Config) when is_list(Config) -> 450 | ?assertEqual([atom, atom], power_shell:eval(?MODULE, rebind, [[atom, atom]])). 451 | 452 | external_fun() -> 453 | [{doc, "Tests external function passed to lists:filter"}]. 454 | 455 | external_fun(Config) when is_list(Config) -> 456 | ?assertEqual([1, 2], power_shell:eval(?MODULE, external_filter, [[1, atom, 2, atom]])). 457 | 458 | -if(?OTP_RELEASE > 25). 459 | map_comp_from_list() -> 460 | [{doc, "Tests handling of map comprehension introduced in OTP26"}]. 461 | 462 | map_comp_from_list(Config) when is_list(Config) -> 463 | ?assertEqual(#{1 => 1, 2 => 4, 3 => 9}, 464 | power_shell:eval(?MODULE, map_comprehension_from_list, [])). 465 | 466 | map_comp_from_bin() -> 467 | [{doc, "Tests handling of map comprehension introduced in OTP26"}]. 468 | 469 | map_comp_from_bin(Config) when is_list(Config) -> 470 | ?assertEqual(#{$a => $b, $c => $d, $e => $f, $g => $h}, 471 | power_shell:eval(?MODULE, map_comprehension_from_binary, [])). 472 | 473 | map_gen_to_map() -> 474 | [{doc, "Tests handling of map generator introduced in OTP26"}]. 475 | 476 | map_gen_to_map(Config) when is_list(Config) -> 477 | ?assertEqual(#{1 => 1, 4 => 2, 9 => 3}, 478 | power_shell:eval(?MODULE, map_generator_to_map, [])). 479 | 480 | map_gen_to_list() -> 481 | [{doc, "Tests handling of map generator introduced in OTP26"}]. 482 | 483 | map_gen_to_list(Config) when is_list(Config) -> 484 | ?assertEqual([{1, 1}, {4, 2}, {9, 3}], 485 | power_shell:eval(?MODULE, map_generator_to_list, [])). 486 | 487 | map_gen_to_bin() -> 488 | [{doc, "Tests handling of map generator introduced in OTP26"}]. 489 | 490 | map_gen_to_bin(Config) when is_list(Config) -> 491 | ?assertEqual("abcd", 492 | lists:sort(binary_to_list(power_shell:eval(?MODULE, map_generator_to_binary, [])))). 493 | -else. 494 | map_comp_from_list() -> 495 | [{doc, "Tests handling of map comprehension introduced in OTP26"}]. 496 | 497 | map_comp_from_list(Config) when is_list(Config) -> 498 | ok. 499 | 500 | map_comp_from_bin() -> 501 | [{doc, "Tests handling of map comprehension introduced in OTP26"}]. 502 | 503 | map_comp_from_bin(Config) when is_list(Config) -> 504 | ok. 505 | 506 | map_gen_to_map() -> 507 | [{doc, "Tests handling of map generator introduced in OTP26"}]. 508 | 509 | map_gen_to_map(Config) when is_list(Config) -> 510 | ok. 511 | 512 | map_gen_to_list() -> 513 | [{doc, "Tests handling of map generator introduced in OTP26"}]. 514 | 515 | map_gen_to_list(Config) when is_list(Config) -> 516 | ok. 517 | 518 | map_gen_to_bin() -> 519 | [{doc, "Tests handling of map generator introduced in OTP26"}]. 520 | 521 | map_gen_to_bin(Config) when is_list(Config) -> 522 | ok. 523 | -endif. 524 | 525 | catch_apply() -> 526 | [{doc, "Tests that cast catch erlang:apply works and throws as expected, not converting it to badarg"}]. 527 | 528 | catch_apply(Config) when is_list(Config) -> 529 | ?assertThrow(expected, power_shell:eval(?MODULE, throw_applied, [])). 530 | 531 | export() -> 532 | [{doc, "Tests export_all (hot code load and revert)"}]. 533 | 534 | export(Config) when is_list(Config) -> 535 | ?assertException(error, undef, power_shell_export:local_unexported(success)), 536 | %% find some compiler options to keep 537 | CompileFlags = proplists:get_value(options, power_shell_export:module_info(compile)), 538 | Sentinel = power_shell:export(power_shell_export), 539 | ?assertEqual(success, power_shell_export:local_unexported(success)), 540 | %% verify compile flags - original flags should be honoured! 541 | NewFlags = proplists:get_value(options, power_shell_export:module_info(compile)), 542 | ?assertEqual([], CompileFlags -- NewFlags), 543 | %% unload now 544 | power_shell:revert(Sentinel), 545 | ?assertException(error, undef, power_shell_export:local_unexported(success)). 546 | 547 | export_partial() -> 548 | [{doc, "Tests selective export"}]. 549 | 550 | export_partial(Config) when is_list(Config) -> 551 | ?assertEqual(echo, power_shell_export:export_all(echo)), %% verify echo 552 | ?assertException(error, undef, power_shell_export:local_unexported(success)), 553 | ?assertException(error, undef, power_shell_export:local_never(success)), 554 | Sentinel = power_shell:export(power_shell_export, [{local_unexported, 1}]), 555 | ?assertEqual(success, power_shell_export:local_unexported(success)), 556 | %% local_never should not be expected, as it's not in the fun/arity list 557 | ?assertException(error, undef, power_shell_export:local_never(success)), 558 | %% unload now 559 | ok = power_shell:revert(Sentinel), 560 | ?assertException(error, undef, power_shell_export:local_unexported(success)). 561 | 562 | export_auto_revert() -> 563 | [{doc, "Tests auto-revert when sentinel process goes down"}]. 564 | 565 | export_auto_revert(Config) when is_list(Config) -> 566 | ?assertException(error, undef, power_shell_export:local_unexported(success)), 567 | {Pid, MRef} = spawn_monitor(fun () -> 568 | power_shell:export(power_shell_export), 569 | ?assertEqual(success, power_shell_export:local_unexported(success)) 570 | end), 571 | receive 572 | {'DOWN', MRef, process, Pid, normal} -> 573 | ct:sleep(1000), %% can't think of any better way to wait for code load event to complete 574 | ?assertException(error, undef, power_shell_export:local_unexported(success)) 575 | end. 576 | 577 | export_no_link() -> 578 | [{doc, "Tests no auto-revert when sentinel starts unlinked"}]. 579 | 580 | export_no_link(Config) when is_list(Config) -> 581 | ?assertException(error, undef, power_shell_export:local_unexported(success)), 582 | Self = self(), 583 | spawn_link(fun () -> 584 | Sentinel = power_shell:export(power_shell_export, all, #{link => false}), 585 | Self ! {go, Sentinel} 586 | end), 587 | receive 588 | {go, Sentinel} -> 589 | ?assertEqual(success, power_shell_export:local_unexported(success)), 590 | ok = power_shell:revert(Sentinel), 591 | ?assertException(error, undef, power_shell_export:local_unexported(success)) 592 | end. 593 | 594 | recursive_eval() -> 595 | [{doc, "Tests on_load preserves stack"}]. 596 | 597 | recursive_eval(Config) -> 598 | Source = 599 | "-module(recursive). -export([foo/0, bar/0]). " 600 | "foo() -> ok. bar() -> power_shell:eval(recursive, foo, []), true.", 601 | PrivPath = ?config(priv_dir, Config), 602 | true = code:add_path(PrivPath), 603 | Filename = filename:join(PrivPath, "recursive.erl"), 604 | ok = file:write_file(Filename, Source), 605 | PrevDict = get(), 606 | ?assert(power_shell:eval(recursive, bar, [])), 607 | ?assertEqual(PrevDict, get()), 608 | true = code:del_path(PrivPath), 609 | ok. 610 | 611 | %%-------------------------------------------------------------------- 612 | %% Exception testing helper 613 | %%-------------------------------------------------------------------- 614 | %% Compatibility: stacktrace 615 | 616 | strip_dbg(Trace) -> 617 | [{M, F, A} || {M, F, A, _Dbg} <- Trace]. 618 | 619 | exception_check(Fun, FunAtomName, Args) -> 620 | % again, next line cannot be split, otherwise line information would be broken 621 | Expected = try erlang:apply(Fun, Args) of Val -> throw({test_broken, Val}) catch C:R:S -> {C, R, strip_dbg(S)} end, Actual = try power_shell:eval(?MODULE, FunAtomName, Args) of 622 | Val1 -> 623 | throw({test_broken, Val1}) 624 | catch 625 | Class:Reason:Stack -> 626 | {Class, Reason, strip_dbg(Stack)} 627 | end, 628 | % allow line numbers and file names to slip through 629 | ?assertEqual(Expected, Actual). 630 | -------------------------------------------------------------------------------- /src/power_shell_eval.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% %CopyrightBegin% 3 | %% 4 | %% Copyright Ericsson AB 1996-2021. All Rights Reserved. 5 | %% 6 | %% Licensed under the Apache License, Version 2.0 (the "License"); 7 | %% you may not use this file except in compliance with the License. 8 | %% You may obtain a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, software 13 | %% distributed under the License is distributed on an "AS IS" BASIS, 14 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | %% See the License for the specific language governing permissions and 16 | %% limitations under the License. 17 | %% 18 | %% %CopyrightEnd% 19 | %% 20 | 21 | %% This file is imported from 22 | %% https://github.com/erlang/otp/blob/master/lib/stdlib/src/erl_eval.erl 23 | %% to patch against http://erlang.org/pipermail/erlang-bugs/2012-October/003134.html 24 | %% and address a few more comments dated 2009. 25 | %% Support for erl_val and local functions was promised in 2008, but, 26 | %% apparently, it takes time to implement. 27 | 28 | %% Actual change is mostly around this: 29 | %% expr({'fun',_Line,{function,Name,Arity}}, 30 | 31 | %% The other important thing is line number information 32 | %% kept while evaluating, when module has not been 33 | %% compiled. 34 | 35 | -module(power_shell_eval). 36 | 37 | %% An evaluator for Erlang abstract syntax. 38 | 39 | -export([exprs/2,exprs/3,exprs/4,expr/2,expr/3,expr/4,expr/5, 40 | expr_list/2,expr_list/3,expr_list/4]). 41 | -export([new_bindings/0,bindings/1,binding/2,add_binding/3,del_binding/2]). 42 | -export([extended_parse_exprs/1, extended_parse_term/1]). 43 | -export([is_constant_expr/1, partial_eval/1, eval_str/1]). 44 | 45 | %% Is used by standalone Erlang (escript). 46 | %% Also used by shell.erl. 47 | -export([match_clause/4]). 48 | 49 | -export([check_command/2, fun_data/1]). 50 | 51 | -import(lists, [reverse/1,foldl/3,member/2]). 52 | 53 | -export_type([binding_struct/0]). 54 | 55 | -type(expression() :: erl_parse:abstract_expr()). 56 | -type(expressions() :: [erl_parse:abstract_expr()]). 57 | -type(expression_list() :: [expression()]). 58 | -type(clauses() :: [erl_parse:abstract_clause()]). 59 | -type(name() :: term()). 60 | -type(value() :: term()). 61 | -type(bindings() :: [{name(), value()}]). 62 | -type(binding_struct() :: orddict:orddict() | map()). 63 | 64 | -type(lfun_value_handler() :: fun((Name :: atom(), 65 | Arguments :: [term()]) -> 66 | Value :: value())). 67 | -type(lfun_eval_handler() :: fun((Name :: atom(), 68 | Arguments :: expression_list(), 69 | Bindings :: binding_struct()) -> 70 | {value, 71 | Value :: value(), 72 | NewBindings :: binding_struct()})). 73 | -type(local_function_handler() :: {value, lfun_value_handler()} 74 | | {eval, lfun_eval_handler()} 75 | | none). 76 | 77 | -type(func_spec() :: {Module :: module(), Function :: atom()} | function()). 78 | -type(nlfun_handler() :: fun((FuncSpec :: func_spec(), 79 | Arguments :: [term()]) -> 80 | term())). 81 | -type(non_local_function_handler() :: {value, nlfun_handler()} 82 | | none). 83 | 84 | -define(STACKTRACE, 85 | element(2, erlang:process_info(self(), current_stacktrace))). 86 | 87 | empty_fun_used_vars() -> #{}. 88 | 89 | %% exprs(ExpressionSeq, Bindings) 90 | %% exprs(ExpressionSeq, Bindings, LocalFuncHandler) 91 | %% exprs(ExpressionSeq, Bindings, LocalFuncHandler, ExternalFuncHandler) 92 | %% Returns: 93 | %% {value,Value,NewBindings} 94 | %% or {'EXIT', Reason} 95 | %% Only exprs/2 checks the command by calling erl_lint. The reason is 96 | %% that if there is a function handler present, then it is possible 97 | %% that there are valid constructs in Expression to be taken care of 98 | %% by a function handler but considered errors by erl_lint. 99 | 100 | -spec(exprs(Expressions, Bindings) -> {value, Value, NewBindings} when 101 | Expressions :: expressions(), 102 | Bindings :: binding_struct(), 103 | Value :: value(), 104 | NewBindings :: binding_struct()). 105 | exprs(Exprs, Bs) -> 106 | case check_command(Exprs, Bs) of 107 | ok -> 108 | exprs(Exprs, Bs, none, none, none, empty_fun_used_vars()); 109 | {error,{_Location,_Mod,Error}} -> 110 | erlang:raise(error, Error, ?STACKTRACE) 111 | end. 112 | 113 | -spec(exprs(Expressions, Bindings, LocalFunctionHandler) -> 114 | {value, Value, NewBindings} when 115 | Expressions :: expressions(), 116 | Bindings :: binding_struct(), 117 | LocalFunctionHandler :: local_function_handler(), 118 | Value :: value(), 119 | NewBindings :: binding_struct()). 120 | exprs(Exprs, Bs, Lf) -> 121 | exprs(Exprs, Bs, Lf, none, none, empty_fun_used_vars()). 122 | 123 | -spec(exprs(Expressions, Bindings, LocalFunctionHandler, 124 | NonLocalFunctionHandler) -> 125 | {value, Value, NewBindings} when 126 | Expressions :: expressions(), 127 | Bindings :: binding_struct(), 128 | LocalFunctionHandler :: local_function_handler(), 129 | NonLocalFunctionHandler :: non_local_function_handler(), 130 | Value :: value(), 131 | NewBindings :: binding_struct()). 132 | exprs(Exprs, Bs, Lf, Ef) -> 133 | exprs(Exprs, Bs, Lf, Ef, none, empty_fun_used_vars()). 134 | 135 | -spec(exprs(Expressions, Bindings, LocalFunctionHandler, 136 | NonLocalFunctionHandler, ReturnFormat, FunUsedVars) -> 137 | {value, Value, NewBindings} when 138 | Expressions :: expressions(), 139 | Bindings :: binding_struct(), 140 | LocalFunctionHandler :: local_function_handler(), 141 | NonLocalFunctionHandler :: non_local_function_handler(), 142 | ReturnFormat :: none | value, 143 | FunUsedVars :: erl_lint:fun_used_vars(), 144 | Value :: value(), 145 | NewBindings :: binding_struct()). 146 | exprs([E], Bs0, Lf, Ef, RBs, FUVs) -> 147 | expr(E, Bs0, Lf, Ef, RBs, FUVs); 148 | exprs([E|Es], Bs0, Lf, Ef, RBs, FUVs) -> 149 | RBs1 = none, 150 | {value,_V,Bs} = expr(E, Bs0, Lf, Ef, RBs1, FUVs), 151 | exprs(Es, Bs, Lf, Ef, RBs, FUVs). 152 | 153 | %% expr(Expression, Bindings) 154 | %% expr(Expression, Bindings, LocalFuncHandler) 155 | %% expr(Expression, Bindings, LocalFuncHandler, ExternalFuncHandler) 156 | %% Returns: 157 | %% {value,Value,NewBindings} 158 | %% or {'EXIT', Reason} 159 | %% 160 | %% Only expr/2 checks the command by calling erl_lint. See exprs/2. 161 | 162 | -spec(expr(Expression, Bindings) -> {value, Value, NewBindings} when 163 | Expression :: expression(), 164 | Bindings :: binding_struct(), 165 | Value :: value(), 166 | NewBindings :: binding_struct()). 167 | expr(E, Bs) -> 168 | case check_command([E], Bs) of 169 | ok -> 170 | expr(E, Bs, none, none, none); 171 | {error,{_Location,_Mod,Error}} -> 172 | erlang:raise(error, Error, ?STACKTRACE) 173 | end. 174 | 175 | -spec(expr(Expression, Bindings, LocalFunctionHandler) -> 176 | {value, Value, NewBindings} when 177 | Expression :: expression(), 178 | Bindings :: binding_struct(), 179 | LocalFunctionHandler :: local_function_handler(), 180 | Value :: value(), 181 | NewBindings :: binding_struct()). 182 | expr(E, Bs, Lf) -> 183 | expr(E, Bs, Lf, none, none). 184 | 185 | -spec(expr(Expression, Bindings, LocalFunctionHandler, 186 | NonLocalFunctionHandler) -> 187 | {value, Value, NewBindings} when 188 | Expression :: expression(), 189 | Bindings :: binding_struct(), 190 | LocalFunctionHandler :: local_function_handler(), 191 | NonLocalFunctionHandler :: non_local_function_handler(), 192 | Value :: value(), 193 | NewBindings :: binding_struct()). 194 | expr(E, Bs, Lf, Ef) -> 195 | expr(E, Bs, Lf, Ef, none). 196 | 197 | %% Check a command (a list of expressions) by calling erl_lint. 198 | 199 | check_command(Es, Bs) -> 200 | Opts = [bitlevel_binaries,binary_comprehension], 201 | case erl_lint:exprs_opt(Es, bindings(Bs), Opts) of 202 | {ok,_Ws} -> 203 | ok; 204 | {error,[{_File,[Error|_]}],_Ws} -> 205 | {error,Error} 206 | end. 207 | 208 | %% Check whether a term F is a function created by this module. 209 | %% Returns 'false' if not, otherwise {fun_data,Imports,Clauses}. 210 | 211 | fun_data(F) when is_function(F) -> 212 | case erlang:fun_info(F, module) of 213 | {module,?MODULE} -> 214 | case erlang:fun_info(F, env) of 215 | {env,[{FBs,_FLf,_FEf,_FUVs,FCs}]} -> 216 | {fun_data,FBs,FCs}; 217 | {env,[{FBs,_FLf,_FEf,_FUVs,FCs,FName}]} -> 218 | {named_fun_data,FBs,FName,FCs} 219 | end; 220 | _ -> 221 | false 222 | end; 223 | fun_data(_T) -> 224 | false. 225 | 226 | -spec(expr(Expression, Bindings, LocalFunctionHandler, 227 | NonLocalFunctionHandler, ReturnFormat) -> 228 | {value, Value, NewBindings} | Value when 229 | Expression :: expression(), 230 | Bindings :: binding_struct(), 231 | LocalFunctionHandler :: local_function_handler(), 232 | NonLocalFunctionHandler :: non_local_function_handler(), 233 | ReturnFormat :: none | value, 234 | Value :: value(), 235 | NewBindings :: binding_struct()). 236 | expr(Expr, Bs, Lf, Ef, Rbs) -> 237 | expr(Expr, Bs, Lf, Ef, Rbs, empty_fun_used_vars()). 238 | 239 | -spec(expr(Expression, Bindings, LocalFunctionHandler, 240 | NonLocalFunctionHandler, ReturnFormat, FunUsedVars) -> 241 | {value, Value, NewBindings} | Value when 242 | Expression :: expression(), 243 | Bindings :: binding_struct(), 244 | LocalFunctionHandler :: local_function_handler(), 245 | NonLocalFunctionHandler :: non_local_function_handler(), 246 | ReturnFormat :: none | value, 247 | FunUsedVars :: erl_lint:fun_used_vars(), 248 | Value :: value(), 249 | NewBindings :: binding_struct()). 250 | expr({var,_,V}, Bs, _Lf, _Ef, RBs, _FUVs) -> 251 | case binding(V, Bs) of 252 | {value,Val} -> 253 | ret_expr(Val, Bs, RBs); 254 | unbound -> % Cannot not happen if checked by erl_lint 255 | erlang:raise(error, {unbound,V}, ?STACKTRACE) 256 | end; 257 | expr({char,_,C}, Bs, _Lf, _Ef, RBs, _FUVs) -> 258 | ret_expr(C, Bs, RBs); 259 | expr({integer,_,I}, Bs, _Lf, _Ef, RBs, _FUVs) -> 260 | ret_expr(I, Bs, RBs); 261 | expr({float,_,F}, Bs, _Lf, _Ef, RBs, _FUVs) -> 262 | ret_expr(F, Bs, RBs); 263 | expr({atom,_,A}, Bs, _Lf, _Ef, RBs, _FUVs) -> 264 | ret_expr(A, Bs, RBs); 265 | expr({string,_,S}, Bs, _Lf, _Ef, RBs, _FUVs) -> 266 | ret_expr(S, Bs, RBs); 267 | expr({nil, _}, Bs, _Lf, _Ef, RBs, _FUVs) -> 268 | ret_expr([], Bs, RBs); 269 | expr({cons,_,H0,T0}, Bs0, Lf, Ef, RBs, FUVs) -> 270 | {value,H,Bs1} = expr(H0, Bs0, Lf, Ef, none, FUVs), 271 | {value,T,Bs2} = expr(T0, Bs0, Lf, Ef, none, FUVs), 272 | ret_expr([H|T], merge_bindings(Bs1, Bs2), RBs); 273 | expr({lc,_,E,Qs}, Bs, Lf, Ef, RBs, FUVs) -> 274 | eval_lc(E, Qs, Bs, Lf, Ef, RBs, FUVs); 275 | expr({bc,_,E,Qs}, Bs, Lf, Ef, RBs, FUVs) -> 276 | eval_bc(E, Qs, Bs, Lf, Ef, RBs, FUVs); 277 | expr({mc,_,E,Qs}, Bs, Lf, Ef, RBs, FUVs) -> 278 | eval_mc(E, Qs, Bs, Lf, Ef, RBs, FUVs); 279 | expr({tuple,_,Es}, Bs0, Lf, Ef, RBs, FUVs) -> 280 | {Vs,Bs} = expr_list(Es, Bs0, Lf, Ef, FUVs), 281 | ret_expr(list_to_tuple(Vs), Bs, RBs); 282 | expr({record_field,_,_,Name,_}, _Bs, _Lf, _Ef, _RBs, _FUVs) -> 283 | erlang:raise(error, {undef_record,Name}, ?STACKTRACE); 284 | expr({record_index,_,Name,_}, _Bs, _Lf, _Ef, _RBs, _FUVs) -> 285 | erlang:raise(error, {undef_record,Name}, ?STACKTRACE); 286 | expr({record,_,Name,_}, _Bs, _Lf, _Ef, _RBs, _FUVs) -> 287 | erlang:raise(error, {undef_record,Name}, ?STACKTRACE); 288 | expr({record,_,_,Name,_}, _Bs, _Lf, _Ef, _RBs, _FUVs) -> 289 | erlang:raise(error, {undef_record,Name}, ?STACKTRACE); 290 | 291 | %% map 292 | expr({map,_,Binding,Es}, Bs0, Lf, Ef, RBs, FUVs) -> 293 | {value, Map0, Bs1} = expr(Binding, Bs0, Lf, Ef, none, FUVs), 294 | {Vs,Bs2} = eval_map_fields(Es, Bs0, Lf, Ef, FUVs), 295 | _ = maps:put(k, v, Map0), %Validate map. 296 | Map1 = lists:foldl(fun ({map_assoc,K,V}, Mi) -> 297 | maps:put(K, V, Mi); 298 | ({map_exact,K,V}, Mi) -> 299 | maps:update(K, V, Mi) 300 | end, Map0, Vs), 301 | ret_expr(Map1, merge_bindings(Bs2, Bs1), RBs); 302 | expr({map,_,Es}, Bs0, Lf, Ef, RBs, FUVs) -> 303 | {Vs,Bs} = eval_map_fields(Es, Bs0, Lf, Ef, FUVs), 304 | ret_expr(lists:foldl(fun 305 | ({map_assoc,K,V}, Mi) -> maps:put(K,V,Mi) 306 | end, maps:new(), Vs), Bs, RBs); 307 | 308 | expr({block,_,Es}, Bs, Lf, Ef, RBs, FUVs) -> 309 | exprs(Es, Bs, Lf, Ef, RBs, FUVs); 310 | expr({'if',_,Cs}, Bs, Lf, Ef, RBs, FUVs) -> 311 | if_clauses(Cs, Bs, Lf, Ef, RBs, FUVs); 312 | expr({'case',_,E,Cs}, Bs0, Lf, Ef, RBs, FUVs) -> 313 | {value,Val,Bs} = expr(E, Bs0, Lf, Ef, none, FUVs), 314 | case_clauses(Val, Cs, Bs, Lf, Ef, RBs, FUVs); 315 | expr({'try',_,B,Cases,Catches,AB}, Bs, Lf, Ef, RBs, FUVs) -> 316 | try_clauses(B, Cases, Catches, AB, Bs, Lf, Ef, RBs, FUVs); 317 | expr({'receive',_,Cs}, Bs, Lf, Ef, RBs, FUVs) -> 318 | receive_clauses(Cs, Bs, Lf, Ef, RBs, FUVs); 319 | expr({'receive',_, Cs, E, TB}, Bs0, Lf, Ef, RBs, FUVs) -> 320 | {value,T,Bs} = expr(E, Bs0, Lf, Ef, none, FUVs), 321 | receive_clauses(T, Cs, {TB,Bs}, Bs0, Lf, Ef, RBs, FUVs); 322 | expr({'fun',_Anno,{function,Mod0,Name0,Arity0}}, Bs0, Lf, Ef, RBs, FUVs) -> 323 | {[Mod,Name,Arity],Bs} = expr_list([Mod0,Name0,Arity0], Bs0, Lf, Ef, FUVs), 324 | F = erlang:make_fun(Mod, Name, Arity), 325 | ret_expr(F, Bs, RBs); 326 | 327 | 328 | expr({'fun',Anno,{function,Name,Arity}}, Bs0, Lf, Ef, RBs, FUVs) -> % R8 329 | %% Do know what to do. Use LocalFunHandler to extract clauses. 330 | {value, Clauses, _} = local_func({function, Name, Arity}, [], Bs0, Lf, Ef, RBs, FUVs), 331 | {value, Fun, _} = expr({named_fun,Anno,Name,Clauses}, new_bindings(), Lf, Ef, RBs), 332 | {value, Fun, Bs0}; 333 | 334 | expr({'fun',Anno,{clauses,Cs}} = Ex, Bs, Lf, Ef, RBs, FUVs) -> 335 | {En,NewFUVs} = fun_used_bindings(Ex, Cs, Bs, FUVs), 336 | Info = {En,Lf,Ef,NewFUVs,Cs}, 337 | 338 | %% This is a really ugly hack! 339 | F = 340 | case length(element(3,hd(Cs))) of 341 | 0 -> fun () -> eval_fun([], Info) end; 342 | 1 -> fun (A) -> eval_fun([A], Info) end; 343 | 2 -> fun (A,B) -> eval_fun([A,B], Info) end; 344 | 3 -> fun (A,B,C) -> eval_fun([A,B,C], Info) end; 345 | 4 -> fun (A,B,C,D) -> eval_fun([A,B,C,D], Info) end; 346 | 5 -> fun (A,B,C,D,E) -> eval_fun([A,B,C,D,E], Info) end; 347 | 6 -> fun (A,B,C,D,E,F) -> eval_fun([A,B,C,D,E,F], Info) end; 348 | 7 -> fun (A,B,C,D,E,F,G) -> eval_fun([A,B,C,D,E,F,G], Info) end; 349 | 8 -> fun (A,B,C,D,E,F,G,H) -> eval_fun([A,B,C,D,E,F,G,H], Info) end; 350 | 9 -> fun (A,B,C,D,E,F,G,H,I) -> eval_fun([A,B,C,D,E,F,G,H,I], Info) end; 351 | 10 -> fun (A,B,C,D,E,F,G,H,I,J) -> 352 | eval_fun([A,B,C,D,E,F,G,H,I,J], Info) end; 353 | 11 -> fun (A,B,C,D,E,F,G,H,I,J,K) -> 354 | eval_fun([A,B,C,D,E,F,G,H,I,J,K], Info) end; 355 | 12 -> fun (A,B,C,D,E,F,G,H,I,J,K,L) -> 356 | eval_fun([A,B,C,D,E,F,G,H,I,J,K,L], Info) end; 357 | 13 -> fun (A,B,C,D,E,F,G,H,I,J,K,L,M) -> 358 | eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M], Info) end; 359 | 14 -> fun (A,B,C,D,E,F,G,H,I,J,K,L,M,N) -> 360 | eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N], Info) end; 361 | 15 -> fun (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O) -> 362 | eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O], Info) end; 363 | 16 -> fun (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P) -> 364 | eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P], Info) end; 365 | 17 -> fun (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q) -> 366 | eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q], Info) end; 367 | 18 -> fun (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R) -> 368 | eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R], Info) end; 369 | 19 -> fun (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S) -> 370 | eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S], Info) end; 371 | 20 -> fun (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T) -> 372 | eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T], Info) end; 373 | _Other -> 374 | L = erl_anno:location(Anno), 375 | erlang:raise(error, {'argument_limit',{'fun',L,to_terms(Cs)}}, 376 | ?STACKTRACE) 377 | end, 378 | ret_expr(F, Bs, RBs); 379 | expr({named_fun,Anno,Name,Cs} = Ex, Bs, Lf, Ef, RBs, FUVs) -> 380 | {En,NewFUVs} = fun_used_bindings(Ex, Cs, Bs, FUVs), 381 | Info = {En,Lf,Ef,NewFUVs,Cs,Name}, 382 | 383 | %% This is a really ugly hack! 384 | F = 385 | case length(element(3,hd(Cs))) of 386 | 0 -> fun RF() -> eval_named_fun([], RF, Info) end; 387 | 1 -> fun RF(A) -> eval_named_fun([A], RF, Info) end; 388 | 2 -> fun RF(A,B) -> eval_named_fun([A,B], RF, Info) end; 389 | 3 -> fun RF(A,B,C) -> eval_named_fun([A,B,C], RF, Info) end; 390 | 4 -> fun RF(A,B,C,D) -> eval_named_fun([A,B,C,D], RF, Info) end; 391 | 5 -> fun RF(A,B,C,D,E) -> eval_named_fun([A,B,C,D,E], RF, Info) end; 392 | 6 -> fun RF(A,B,C,D,E,F) -> eval_named_fun([A,B,C,D,E,F], RF, Info) end; 393 | 7 -> fun RF(A,B,C,D,E,F,G) -> 394 | eval_named_fun([A,B,C,D,E,F,G], RF, Info) end; 395 | 8 -> fun RF(A,B,C,D,E,F,G,H) -> 396 | eval_named_fun([A,B,C,D,E,F,G,H], RF, Info) end; 397 | 9 -> fun RF(A,B,C,D,E,F,G,H,I) -> 398 | eval_named_fun([A,B,C,D,E,F,G,H,I], RF, Info) end; 399 | 10 -> fun RF(A,B,C,D,E,F,G,H,I,J) -> 400 | eval_named_fun([A,B,C,D,E,F,G,H,I,J], RF, Info) end; 401 | 11 -> fun RF(A,B,C,D,E,F,G,H,I,J,K) -> 402 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K], RF, Info) end; 403 | 12 -> fun RF(A,B,C,D,E,F,G,H,I,J,K,L) -> 404 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K,L], RF, Info) end; 405 | 13 -> fun RF(A,B,C,D,E,F,G,H,I,J,K,L,M) -> 406 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K,L,M], RF, Info) end; 407 | 14 -> fun RF(A,B,C,D,E,F,G,H,I,J,K,L,M,N) -> 408 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N], RF, Info) end; 409 | 15 -> fun RF(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O) -> 410 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O], RF, Info) end; 411 | 16 -> fun RF(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P) -> 412 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P], RF, Info) end; 413 | 17 -> fun RF(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q) -> 414 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q], RF, Info) end; 415 | 18 -> fun RF(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R) -> 416 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R], RF, Info) end; 417 | 19 -> fun RF(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S) -> 418 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S], 419 | RF, Info) end; 420 | 20 -> fun RF(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T) -> 421 | eval_named_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T], 422 | RF, Info) end; 423 | _Other -> 424 | L = erl_anno:location(Anno), 425 | erlang:raise(error, {'argument_limit', 426 | {named_fun,L,Name,to_terms(Cs)}}, 427 | ?STACKTRACE) 428 | end, 429 | ret_expr(F, Bs, RBs); 430 | expr({call,_,{remote,_,{atom,_,qlc},{atom,_,q}},[{lc,_,_E,_Qs}=LC | As0]}, 431 | Bs0, Lf, Ef, RBs, FUVs) when length(As0) =< 1 -> 432 | %% No expansion or evaluation of module name or function name. 433 | MaxLine = find_maxline(LC), 434 | {LC1, D} = hide_calls(LC, MaxLine), 435 | case qlc:transform_from_evaluator(LC1, Bs0) of 436 | {ok,{call,A,Remote,[QLC]}} -> 437 | QLC1 = unhide_calls(QLC, MaxLine, D), 438 | expr({call,A,Remote,[QLC1 | As0]}, Bs0, Lf, Ef, RBs, FUVs); 439 | {not_ok,Error} -> 440 | ret_expr(Error, Bs0, RBs) 441 | end; 442 | expr({call,A1,{remote,A2,{record_field,_,{atom,_,''},{atom,_,qlc}=Mod}, 443 | {atom,_,q}=Func}, 444 | [{lc,_,_E,_Qs} | As0]=As}, 445 | Bs, Lf, Ef, RBs, FUVs) when length(As0) =< 1 -> 446 | expr({call,A1,{remote,A2,Mod,Func},As}, Bs, Lf, Ef, RBs, FUVs); 447 | expr({call,_,{remote,_,Mod,Func},As0}, Bs0, Lf, Ef, RBs, FUVs) -> 448 | {value,M,Bs1} = expr(Mod, Bs0, Lf, Ef, none, FUVs), 449 | {value,F,Bs2} = expr(Func, Bs0, Lf, Ef, none, FUVs), 450 | {As,Bs3} = expr_list(As0, merge_bindings(Bs1, Bs2), Lf, Ef, FUVs), 451 | %% M could be a parameterized module (not an atom). 452 | case is_atom(M) andalso erl_internal:bif(M, F, length(As)) of 453 | true -> 454 | bif(F, As, Bs3, Ef, RBs); 455 | false -> 456 | do_apply(M, F, As, Bs3, Ef, RBs) 457 | end; 458 | expr({call,_,{atom,_,Func},As0}, Bs0, Lf, Ef, RBs, FUVs) -> 459 | case erl_internal:bif(Func, length(As0)) of 460 | true -> 461 | {As,Bs} = expr_list(As0, Bs0, Lf, Ef), 462 | bif(Func, As, Bs, Ef, RBs); 463 | false -> 464 | local_func(Func, As0, Bs0, Lf, Ef, RBs, FUVs) 465 | end; 466 | expr({call,_,Func0,As0}, Bs0, Lf, Ef, RBs, FUVs) -> % function or {Mod,Fun} 467 | {value,Func,Bs1} = expr(Func0, Bs0, Lf, Ef, none, FUVs), 468 | {As,Bs2} = expr_list(As0, Bs1, Lf, Ef, FUVs), 469 | case Func of 470 | {M,F} when is_atom(M), is_atom(F) -> 471 | erlang:raise(error, {badfun,Func}, ?STACKTRACE); 472 | _ -> 473 | do_apply(Func, As, Bs2, Ef, RBs) 474 | end; 475 | expr({'catch',_,Expr}, Bs0, Lf, Ef, RBs, FUVs) -> 476 | try expr(Expr, Bs0, Lf, Ef, none, FUVs) of 477 | {value,V,Bs} -> 478 | ret_expr(V, Bs, RBs) 479 | catch 480 | throw:Term -> 481 | ret_expr(Term, Bs0, RBs); 482 | exit:Reason -> 483 | ret_expr({'EXIT',Reason}, Bs0, RBs); 484 | error:Reason:Stacktrace -> 485 | ret_expr({'EXIT',{Reason,Stacktrace}}, Bs0, RBs) 486 | end; 487 | expr({match,_,Lhs,Rhs0}, Bs0, Lf, Ef, RBs, FUVs) -> 488 | {value,Rhs,Bs1} = expr(Rhs0, Bs0, Lf, Ef, none, FUVs), 489 | case match(Lhs, Rhs, Bs1) of 490 | {match,Bs} -> 491 | ret_expr(Rhs, Bs, RBs); 492 | nomatch -> erlang:raise(error, {badmatch,Rhs}, ?STACKTRACE) 493 | end; 494 | expr({op,_,Op,A0}, Bs0, Lf, Ef, RBs, FUVs) -> 495 | {value,A,Bs} = expr(A0, Bs0, Lf, Ef, none, FUVs), 496 | eval_op(Op, A, Bs, Ef, RBs); 497 | expr({op,_,'andalso',L0,R0}, Bs0, Lf, Ef, RBs, FUVs) -> 498 | {value,L,Bs1} = expr(L0, Bs0, Lf, Ef, none, FUVs), 499 | V = case L of 500 | true -> 501 | {value,R,_} = expr(R0, Bs1, Lf, Ef, none, FUVs), 502 | R; 503 | false -> false; 504 | _ -> erlang:raise(error, {badarg,L}, ?STACKTRACE) 505 | end, 506 | ret_expr(V, Bs1, RBs); 507 | expr({op,_,'orelse',L0,R0}, Bs0, Lf, Ef, RBs, FUVs) -> 508 | {value,L,Bs1} = expr(L0, Bs0, Lf, Ef, none, FUVs), 509 | V = case L of 510 | true -> true; 511 | false -> 512 | {value,R,_} = expr(R0, Bs1, Lf, Ef, none, FUVs), 513 | R; 514 | _ -> erlang:raise(error, {badarg,L}, ?STACKTRACE) 515 | end, 516 | ret_expr(V, Bs1, RBs); 517 | expr({op,_,Op,L0,R0}, Bs0, Lf, Ef, RBs, FUVs) -> 518 | {value,L,Bs1} = expr(L0, Bs0, Lf, Ef, none, FUVs), 519 | {value,R,Bs2} = expr(R0, Bs0, Lf, Ef, none, FUVs), 520 | eval_op(Op, L, R, merge_bindings(Bs1, Bs2), Ef, RBs); 521 | expr({bin,_,Fs}, Bs0, Lf, Ef, RBs, FUVs) -> 522 | EvalFun = fun(E, B) -> expr(E, B, Lf, Ef, none, FUVs) end, 523 | {value,V,Bs} = eval_bits:expr_grp(Fs, Bs0, EvalFun), 524 | ret_expr(V, Bs, RBs); 525 | expr({remote,_,_,_}, _Bs, _Lf, _Ef, _RBs, _FUVs) -> 526 | erlang:raise(error, {badexpr,':'}, ?STACKTRACE). 527 | 528 | apply_error(Reason, Stack, _Anno, Bs0, Ef, RBs) -> 529 | do_apply(erlang, raise, [error, Reason, Stack], Bs0, Ef, RBs). 530 | 531 | find_maxline(LC) -> 532 | put('$erl_eval_max_line', 0), 533 | F = fun(A) -> 534 | case erl_anno:is_anno(A) of 535 | true -> 536 | L = erl_anno:line(A), 537 | case 538 | is_integer(L) and (L > get('$erl_eval_max_line')) 539 | of 540 | true -> put('$erl_eval_max_line', L); 541 | false -> ok 542 | end; 543 | false -> ok 544 | end 545 | end, 546 | _ = erl_parse:map_anno(F, LC), 547 | erase('$erl_eval_max_line'). 548 | 549 | %% OTP 24 Dialyzer understands that erl_lint:used_vars/2 always 550 | %% returns {ok, List} and complains that second case statement 551 | %% can never match. 552 | -dialyzer({no_match, fun_used_bindings/4}). 553 | fun_used_bindings(Fun, Cs, Bs, FUVs) -> 554 | {Used,InnerFUVs} = 555 | case FUVs of 556 | %% If this clause is in our fun used vars tree, 557 | %% then we do not need to compute to traverse it again. 558 | #{Cs := UsedAndFUVs} -> 559 | UsedAndFUVs; 560 | 561 | #{} -> 562 | %% Save only used variables in the function environment. 563 | case erl_lint:used_vars([Fun], bindings(Bs)) of 564 | {ok, AllUsedVars} when is_list(AllUsedVars) -> 565 | {AllUsedVars, #{}}; 566 | AllUsedVars when is_map(AllUsedVars) -> 567 | %% At the root we should see only a single function, 568 | %% so we extract its used vars and its tree out. 569 | [{_,UsedAndFUVs}] = maps:to_list(AllUsedVars), 570 | UsedAndFUVs 571 | end 572 | end, 573 | 574 | {filter_bindings(fun(K,_V) -> member(K,Used) end, Bs),InnerFUVs}. 575 | 576 | hide_calls(LC, MaxLine) -> 577 | LineId0 = MaxLine + 1, 578 | {NLC, _, D} = hide(LC, LineId0, maps:new()), 579 | {NLC, D}. 580 | 581 | %% Local calls are hidden from qlc so they are not expanded. 582 | hide({call,A,{atom,_,N}=Atom,Args}, Id0, D0) -> 583 | {NArgs, Id, D} = hide(Args, Id0, D0), 584 | C = case erl_internal:bif(N, length(Args)) of 585 | true -> 586 | {call,A,Atom,NArgs}; 587 | false -> 588 | Anno = erl_anno:new(Id), 589 | {call,Anno,{remote,A,{atom,A,m},{atom,A,f}},NArgs} 590 | end, 591 | {C, Id+1, maps:put(Id, {call,Atom}, D)}; 592 | hide(T0, Id0, D0) when is_tuple(T0) -> 593 | {L, Id, D} = hide(tuple_to_list(T0), Id0, D0), 594 | {list_to_tuple(L), Id, D}; 595 | hide([E0 | Es0], Id0, D0) -> 596 | {E, Id1, D1} = hide(E0, Id0, D0), 597 | {Es, Id, D} = hide(Es0, Id1, D1), 598 | {[E | Es], Id, D}; 599 | hide(E, Id, D) -> 600 | {E, Id, D}. 601 | 602 | unhide_calls({call,Anno,{remote,A,{atom,A,m},{atom,A,f}}=F,Args}, 603 | MaxLine, D) -> 604 | Line = erl_anno:line(Anno), 605 | if 606 | Line > MaxLine -> 607 | {call,Atom} = map_get(Line, D), 608 | {call,A,Atom,unhide_calls(Args, MaxLine, D)}; 609 | true -> 610 | {call,Anno,F,unhide_calls(Args, MaxLine, D)} 611 | end; 612 | unhide_calls(T, MaxLine, D) when is_tuple(T) -> 613 | list_to_tuple(unhide_calls(tuple_to_list(T), MaxLine, D)); 614 | unhide_calls([E | Es], MaxLine, D) -> 615 | [unhide_calls(E, MaxLine, D) | unhide_calls(Es, MaxLine, D)]; 616 | unhide_calls(E, _MaxLine, _D) -> 617 | E. 618 | 619 | %% local_func(Function, Arguments, Bindings, LocalFuncHandler, 620 | %% ExternalFuncHandler, RBs, FunUsedVars) -> 621 | %% {value,Value,Bindings} | Value when 622 | %% LocalFuncHandler = {value,F} | {value,F,Eas} | 623 | %% {eval,F} | {eval,F,Eas} | none. 624 | 625 | local_func(Func, As0, Bs0, {value,F}, Ef, value, FUVs) -> 626 | {As1,_Bs1} = expr_list(As0, Bs0, {value,F}, Ef, FUVs), 627 | %% Make tail recursive calls when possible. 628 | F(Func, As1); 629 | local_func(Func, As0, Bs0, {value,F}, Ef, RBs, FUVs) -> 630 | {As1,Bs1} = expr_list(As0, Bs0, {value,F}, Ef, FUVs), 631 | ret_expr(F(Func, As1), Bs1, RBs); 632 | local_func(Func, As0, Bs0, {value,F,Eas}, Ef, RBs, FUVs) -> 633 | Fun = fun(Name, Args) -> apply(F, [Name,Args|Eas]) end, 634 | local_func(Func, As0, Bs0, {value, Fun}, Ef, RBs, FUVs); 635 | local_func(Func, As, Bs, {eval,F}, _Ef, RBs, _FUVs) -> 636 | local_func2(F(Func, As, Bs), RBs); 637 | local_func(Func, As, Bs, {eval,F,Eas}, _Ef, RBs, _FUVs) -> 638 | local_func2(apply(F, [Func,As,Bs|Eas]), RBs); 639 | %% These two clauses are for backwards compatibility. 640 | local_func(Func, As0, Bs0, {M,F}, Ef, RBs, FUVs) -> 641 | {As1,Bs1} = expr_list(As0, Bs0, {M,F}, Ef, FUVs), 642 | ret_expr(M:F(Func,As1), Bs1, RBs); 643 | local_func(Func, As, _Bs, {M,F,Eas}, _Ef, RBs, _FUVs) -> 644 | local_func2(apply(M, F, [Func,As|Eas]), RBs); 645 | %% Default unknown function handler to undefined function. 646 | local_func(Func, As0, _Bs0, none, _Ef, _RBs, _FUVs) -> 647 | erlang:raise(error, undef, [{?MODULE,Func,length(As0)}|?STACKTRACE]). 648 | 649 | local_func2({value,V,Bs}, RBs) -> 650 | ret_expr(V, Bs, RBs); 651 | local_func2({eval,F,As,Bs}, RBs) -> % This reply is not documented. 652 | %% The shell found F. erl_eval tries to do a tail recursive call, 653 | %% something the shell cannot do. Do not use Ef here. 654 | do_apply(F, As, Bs, none, RBs). 655 | 656 | %% bif(Name, Arguments, RBs) 657 | %% Evaluate the Erlang auto-imported function Name. erlang:apply/2,3 658 | %% are "hidden" from the external function handler. 659 | 660 | bif(apply, [erlang,apply,As], Bs, Ef, RBs) -> 661 | bif(apply, As, Bs, Ef, RBs); 662 | bif(apply, [M,F,As], Bs, Ef, RBs) -> 663 | do_apply(M, F, As, Bs, Ef, RBs); 664 | bif(apply, [F,As], Bs, Ef, RBs) -> 665 | do_apply(F, As, Bs, Ef, RBs); 666 | bif(Name, As, Bs, Ef, RBs) -> 667 | do_apply(erlang, Name, As, Bs, Ef, RBs). 668 | 669 | %% do_apply(Func, Arguments, Bindings, ExternalFuncHandler, RBs) -> 670 | %% {value,Value,Bindings} | Value when 671 | %% ExternalFuncHandler = {value,F} | none, 672 | %% Func = fun() 673 | 674 | do_apply(Func, As, Bs0, Ef, RBs) -> 675 | Env = if 676 | is_function(Func) -> 677 | case {erlang:fun_info(Func, module), 678 | erlang:fun_info(Func, env)} of 679 | {{module,?MODULE},{env,Env1}} when Env1 =/= [] -> 680 | {env,Env1}; 681 | _ -> 682 | no_env 683 | end; 684 | true -> 685 | no_env 686 | end, 687 | case {Env,Ef} of 688 | {{env,[{FBs,FLf,FEf,FFUVs,FCs}]},_} -> 689 | %% If we are evaluting within another function body 690 | %% (RBs =/= none), we return RBs when this function body 691 | %% has been evalutated, otherwise we return Bs0, the 692 | %% bindings when evalution of this function body started. 693 | NRBs = if 694 | RBs =:= none -> Bs0; 695 | true -> RBs 696 | end, 697 | case {erlang:fun_info(Func, arity), length(As)} of 698 | {{arity, Arity}, Arity} -> 699 | eval_fun(FCs, As, FBs, FLf, FEf, NRBs, FFUVs); 700 | _ -> 701 | erlang:raise(error, {badarity,{Func,As}},?STACKTRACE) 702 | end; 703 | {{env,[{FBs,FLf,FEf,FFUVs,FCs,FName}]},_} -> 704 | NRBs = if 705 | RBs =:= none -> Bs0; 706 | true -> RBs 707 | end, 708 | case {erlang:fun_info(Func, arity), length(As)} of 709 | {{arity, Arity}, Arity} -> 710 | eval_named_fun(FCs, As, FBs, FLf, FEf, FName, Func, NRBs, FFUVs); 711 | _ -> 712 | erlang:raise(error, {badarity,{Func,As}},?STACKTRACE) 713 | end; 714 | {no_env,none} when RBs =:= value -> 715 | %% Make tail recursive calls when possible. 716 | apply(Func, As); 717 | {no_env,none} -> 718 | ret_expr(apply(Func, As), Bs0, RBs); 719 | {no_env,{value,F}} when RBs =:= value -> 720 | F(Func,As); 721 | {no_env,{value,F}} -> 722 | ret_expr(F(Func, As), Bs0, RBs) 723 | end. 724 | 725 | do_apply(Mod, Func, As, Bs0, Ef, RBs) -> 726 | case Ef of 727 | none when RBs =:= value -> 728 | %% Make tail recursive calls when possible. 729 | apply(Mod, Func, As); 730 | none -> 731 | ret_expr(apply(Mod, Func, As), Bs0, RBs); 732 | {value,F} when RBs =:= value -> 733 | F({Mod,Func}, As); 734 | {value,F} -> 735 | ret_expr(F({Mod,Func}, As), Bs0, RBs) 736 | end. 737 | 738 | %% eval_lc(Expr, [Qualifier], Bindings, LocalFunctionHandler, 739 | %% ExternalFuncHandler, RetBindings) -> 740 | %% {value,Value,Bindings} | Value 741 | 742 | eval_lc(E, Qs, Bs, Lf, Ef, RBs, FUVs) -> 743 | ret_expr(lists:reverse(eval_lc1(E, Qs, Bs, Lf, Ef, FUVs, [])), Bs, RBs). 744 | 745 | eval_lc1(E, [Q|Qs], Bs0, Lf, Ef, FUVs, Acc0) -> 746 | case is_generator(Q) of 747 | true -> 748 | CF = fun(Bs, Acc) -> eval_lc1(E, Qs, Bs, Lf, Ef, FUVs, Acc) end, 749 | eval_generator(Q, Bs0, Lf, Ef, FUVs, Acc0, CF); 750 | false -> 751 | CF = fun(Bs) -> eval_lc1(E, Qs, Bs, Lf, Ef, FUVs, Acc0) end, 752 | eval_filter(Q, Bs0, Lf, Ef, CF, FUVs, Acc0) 753 | end; 754 | eval_lc1(E, [], Bs, Lf, Ef, FUVs, Acc) -> 755 | {value,V,_} = expr(E, Bs, Lf, Ef, none, FUVs), 756 | [V|Acc]. 757 | 758 | %% eval_bc(Expr, [Qualifier], Bindings, LocalFunctionHandler, 759 | %% ExternalFuncHandler, RetBindings) -> 760 | %% {value,Value,Bindings} | Value 761 | 762 | eval_bc(E, Qs, Bs, Lf, Ef, RBs, FUVs) -> 763 | ret_expr(eval_bc1(E, Qs, Bs, Lf, Ef, FUVs, <<>>), Bs, RBs). 764 | 765 | eval_bc1(E, [Q|Qs], Bs0, Lf, Ef, FUVs, Acc0) -> 766 | case is_generator(Q) of 767 | true -> 768 | CF = fun(Bs, Acc) -> eval_bc1(E, Qs, Bs, Lf, Ef, FUVs, Acc) end, 769 | eval_generator(Q, Bs0, Lf, Ef, FUVs, Acc0, CF); 770 | false -> 771 | CF = fun(Bs) -> eval_bc1(E, Qs, Bs, Lf, Ef, FUVs, Acc0) end, 772 | eval_filter(Q, Bs0, Lf, Ef, CF, FUVs, Acc0) 773 | end; 774 | eval_bc1(E, [], Bs, Lf, Ef, FUVs, Acc) -> 775 | {value,V,_} = expr(E, Bs, Lf, Ef, none, FUVs), 776 | <>. 777 | 778 | 779 | %% eval_mc(Expr, [Qualifier], Bindings, LocalFunctionHandler, 780 | %% ExternalFuncHandler, RetBindings) -> 781 | %% {value,Value,Bindings} | Value 782 | 783 | eval_mc(E, Qs, Bs, Lf, Ef, RBs, FUVs) -> 784 | L = eval_mc1(E, Qs, Bs, Lf, Ef, FUVs, []), 785 | Map = maps:from_list(L), 786 | ret_expr(Map, Bs, RBs). 787 | 788 | eval_mc1(E, [Q|Qs], Bs0, Lf, Ef, FUVs, Acc0) -> 789 | case is_generator(Q) of 790 | true -> 791 | CF = fun(Bs, Acc) -> eval_mc1(E, Qs, Bs, Lf, Ef, FUVs, Acc) end, 792 | eval_generator(Q, Bs0, Lf, Ef, FUVs, Acc0, CF); 793 | false -> 794 | CF = fun(Bs) -> eval_mc1(E, Qs, Bs, Lf, Ef, FUVs, Acc0) end, 795 | eval_filter(Q, Bs0, Lf, Ef, CF, FUVs, Acc0) 796 | end; 797 | eval_mc1({map_field_assoc,Lfa,K0,V0}, [], Bs, Lf, Ef, FUVs, Acc) -> 798 | {value,KV,_} = expr({tuple,Lfa,[K0,V0]}, Bs, Lf, Ef, none, FUVs), 799 | [KV|Acc]. 800 | 801 | eval_generator({generate,_Anno,P,L0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) -> 802 | {value,L1,_Bs1} = expr(L0, Bs0, Lf, Ef, none, FUVs), 803 | eval_generate(L1, P, Bs0, Lf, Ef, CompFun, Acc0); 804 | eval_generator({b_generate,_Anno,P,Bin0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) -> 805 | {value,Bin,_Bs1} = expr(Bin0, Bs0, Lf, Ef, none, FUVs), 806 | eval_b_generate(Bin, P, Bs0, Lf, Ef, CompFun, Acc0); 807 | eval_generator({m_generate,Anno,P,Map0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) -> 808 | {map_field_exact,_,K,V} = P, 809 | {value,Map,_Bs1} = expr(Map0, Bs0, Lf, Ef, none, FUVs), 810 | Iter = case is_map(Map) of 811 | true -> 812 | maps:iterator(Map); 813 | false -> 814 | %% Validate iterator. 815 | try maps:foreach(fun(_, _) -> ok end, Map) of 816 | _ -> 817 | Map 818 | catch 819 | _:_ -> 820 | apply_error({bad_generator,Map}, ?STACKTRACE, 821 | Anno, Bs0, Ef, none) 822 | end 823 | end, 824 | eval_m_generate(Iter, {tuple,Anno,[K,V]}, Anno, Bs0, Lf, Ef, CompFun, Acc0). 825 | 826 | eval_generate([V|Rest], P, Bs0, Lf, Ef, CompFun, Acc) -> 827 | case match(P, V, new_bindings(Bs0), Bs0) of 828 | {match,Bsn} -> 829 | Bs2 = add_bindings(Bsn, Bs0), 830 | NewAcc = CompFun(Bs2, Acc), 831 | eval_generate(Rest, P, Bs0, Lf, Ef, CompFun, NewAcc); 832 | nomatch -> 833 | eval_generate(Rest, P, Bs0, Lf, Ef, CompFun, Acc) 834 | end; 835 | eval_generate([], _P, _Bs0, _Lf, _Ef, _CompFun, Acc) -> 836 | Acc; 837 | eval_generate(Term, _P, _Bs0, _Lf, _Ef, _CompFun, _Acc) -> 838 | erlang:raise(error, {bad_generator,Term}, ?STACKTRACE). 839 | 840 | eval_b_generate(<<_/bitstring>>=Bin, P, Bs0, Lf, Ef, CompFun, Acc) -> 841 | Mfun = match_fun(Bs0), 842 | Efun = fun(Exp, Bs) -> expr(Exp, Bs, Lf, Ef, none) end, 843 | case eval_bits:bin_gen(P, Bin, new_bindings(Bs0), Bs0, Mfun, Efun) of 844 | {match, Rest, Bs1} -> 845 | Bs2 = add_bindings(Bs1, Bs0), 846 | NewAcc = CompFun(Bs2, Acc), 847 | eval_b_generate(Rest, P, Bs0, Lf, Ef, CompFun, NewAcc); 848 | {nomatch, Rest} -> 849 | eval_b_generate(Rest, P, Bs0, Lf, Ef, CompFun, Acc); 850 | done -> 851 | Acc 852 | end; 853 | eval_b_generate(Term, _P, _Bs0, _Lf, _Ef, _CompFun, _Acc) -> 854 | erlang:raise(error, {bad_generator,Term}, ?STACKTRACE). 855 | 856 | 857 | eval_m_generate(Iter0, P, Anno, Bs0, Lf, Ef, CompFun, Acc0) -> 858 | case maps:next(Iter0) of 859 | {K,V,Iter} -> 860 | case match(P, {K,V}, new_bindings(Bs0), Bs0) of 861 | {match,Bsn} -> 862 | Bs2 = add_bindings(Bsn, Bs0), 863 | Acc = CompFun(Bs2, Acc0), 864 | eval_m_generate(Iter, P, Anno, Bs0, Lf, Ef, CompFun, Acc); 865 | nomatch -> 866 | eval_m_generate(Iter, P, Anno, Bs0, Lf, Ef, CompFun, Acc0) 867 | end; 868 | none -> 869 | Acc0 870 | end. 871 | 872 | eval_filter(F, Bs0, Lf, Ef, CompFun, FUVs, Acc) -> 873 | case erl_lint:is_guard_test(F) of 874 | true -> 875 | case guard_test(F, Bs0, Lf, Ef) of 876 | {value,true,Bs1} -> CompFun(Bs1); 877 | {value,false,_} -> Acc 878 | end; 879 | false -> 880 | case expr(F, Bs0, Lf, Ef, none, FUVs) of 881 | {value,true,Bs1} -> CompFun(Bs1); 882 | {value,false,_} -> Acc; 883 | {value,V,_} -> 884 | erlang:raise(error, {bad_filter,V}, ?STACKTRACE) 885 | end 886 | end. 887 | 888 | is_generator({generate,_,_,_}) -> true; 889 | is_generator({b_generate,_,_,_}) -> true; 890 | is_generator({m_generate,_,_,_}) -> true; 891 | is_generator(_) -> false. 892 | 893 | %% eval_map_fields([Field], Bindings, LocalFunctionHandler, 894 | %% ExternalFuncHandler) -> 895 | %% {[{map_assoc | map_exact,Key,Value}],Bindings} 896 | 897 | eval_map_fields(Fs, Bs, Lf, Ef, FUVs) -> 898 | eval_map_fields(Fs, Bs, Lf, Ef, FUVs, []). 899 | 900 | eval_map_fields([{map_field_assoc,_,K0,V0}|Fs], Bs0, Lf, Ef, FUVs, Acc) -> 901 | {value,K1,Bs1} = expr(K0, Bs0, Lf, Ef, none, FUVs), 902 | {value,V1,Bs2} = expr(V0, Bs1, Lf, Ef, none, FUVs), 903 | eval_map_fields(Fs, Bs2, Lf, Ef, FUVs, [{map_assoc,K1,V1}|Acc]); 904 | eval_map_fields([{map_field_exact,_,K0,V0}|Fs], Bs0, Lf, Ef, FUVs, Acc) -> 905 | {value,K1,Bs1} = expr(K0, Bs0, Lf, Ef, none, FUVs), 906 | {value,V1,Bs2} = expr(V0, Bs1, Lf, Ef, none, FUVs), 907 | eval_map_fields(Fs, Bs2, Lf, Ef, FUVs, [{map_exact,K1,V1}|Acc]); 908 | eval_map_fields([], Bs, _Lf, _Ef, _FUVs, Acc) -> 909 | {lists:reverse(Acc),Bs}. 910 | 911 | 912 | %% RBs is the bindings to return when the evalution of a function 913 | %% (fun) has finished. If RBs =:= none, then the evalution took place 914 | %% outside a function. If RBs =:= value, only the value (not the bindings) 915 | %% is to be returned (to a compiled function). 916 | 917 | ret_expr(V, _Bs, value) -> 918 | V; 919 | ret_expr(V, Bs, none) -> 920 | {value,V,Bs}; 921 | ret_expr(V, _Bs, RBs) when is_list(RBs); is_map(RBs) -> 922 | {value,V,RBs}. 923 | 924 | %% eval_fun(Arguments, {Bindings,LocalFunctionHandler, 925 | %% ExternalFunctionHandler,FunUsedVars,Clauses}) -> Value 926 | %% This function is called when the fun is called from compiled code 927 | %% or from apply. 928 | 929 | eval_fun(As, {Bs0,Lf,Ef,FUVs,Cs}) -> 930 | eval_fun(Cs, As, Bs0, Lf, Ef, value, FUVs). 931 | 932 | eval_fun([{clause,_,H,G,B}|Cs], As, Bs0, Lf, Ef, RBs, FUVs) -> 933 | case match_list(H, As, new_bindings(Bs0), Bs0) of 934 | {match,Bsn} -> % The new bindings for the head 935 | Bs1 = add_bindings(Bsn, Bs0), % which then shadow! 936 | case guard(G, Bs1, Lf, Ef) of 937 | true -> exprs(B, Bs1, Lf, Ef, RBs, FUVs); 938 | false -> eval_fun(Cs, As, Bs0, Lf, Ef, RBs, FUVs) 939 | end; 940 | nomatch -> 941 | eval_fun(Cs, As, Bs0, Lf, Ef, RBs, FUVs) 942 | end; 943 | eval_fun([], As, _Bs, _Lf, _Ef, _RBs, _FUVs) -> 944 | erlang:raise(error, function_clause, 945 | [{?MODULE,'-inside-an-interpreted-fun-',As}|?STACKTRACE]). 946 | 947 | 948 | eval_named_fun(As, Fun, {Bs0,Lf,Ef,FUVs,Cs,Name}) -> 949 | eval_named_fun(Cs, As, Bs0, Lf, Ef, Name, Fun, value, FUVs). 950 | 951 | eval_named_fun([{clause,_,H,G,B}|Cs], As, Bs0, Lf, Ef, Name, Fun, RBs, FUVs) -> 952 | Bs1 = add_binding(Name, Fun, Bs0), 953 | case match_list(H, As, new_bindings(Bs0), Bs1) of 954 | {match,Bsn} -> % The new bindings for the head 955 | Bs2 = add_bindings(Bsn, Bs1), % which then shadow! 956 | case guard(G, Bs2, Lf, Ef) of 957 | true -> exprs(B, Bs2, Lf, Ef, RBs, FUVs); 958 | false -> eval_named_fun(Cs, As, Bs0, Lf, Ef, Name, Fun, RBs, FUVs) 959 | end; 960 | nomatch -> 961 | eval_named_fun(Cs, As, Bs0, Lf, Ef, Name, Fun, RBs, FUVs) 962 | end; 963 | eval_named_fun([], As, _Bs, _Lf, _Ef, _Name, _Fun, _RBs, _FUVs) -> 964 | erlang:raise(error, function_clause, 965 | [{?MODULE,'-inside-an-interpreted-fun-',As}|?STACKTRACE]). 966 | 967 | 968 | %% expr_list(ExpressionList, Bindings) 969 | %% expr_list(ExpressionList, Bindings, LocalFuncHandler) 970 | %% expr_list(ExpressionList, Bindings, LocalFuncHandler, ExternalFuncHandler) 971 | %% Evaluate a list of expressions "in parallel" at the same level. 972 | 973 | -spec(expr_list(ExpressionList, Bindings) -> {ValueList, NewBindings} when 974 | ExpressionList :: expression_list(), 975 | Bindings :: binding_struct(), 976 | ValueList :: [value()], 977 | NewBindings :: binding_struct()). 978 | expr_list(Es, Bs) -> 979 | expr_list(Es, Bs, none, none, empty_fun_used_vars()). 980 | 981 | -spec(expr_list(ExpressionList, Bindings, LocalFunctionHandler) -> 982 | {ValueList, NewBindings} when 983 | ExpressionList :: expression_list(), 984 | Bindings :: binding_struct(), 985 | LocalFunctionHandler :: local_function_handler(), 986 | ValueList :: [value()], 987 | NewBindings :: binding_struct()). 988 | expr_list(Es, Bs, Lf) -> 989 | expr_list(Es, Bs, Lf, none, empty_fun_used_vars()). 990 | 991 | -spec(expr_list(ExpressionList, Bindings, LocalFunctionHandler, 992 | NonLocalFunctionHandler) -> 993 | {ValueList, NewBindings} when 994 | ExpressionList :: expression_list(), 995 | Bindings :: binding_struct(), 996 | LocalFunctionHandler :: local_function_handler(), 997 | NonLocalFunctionHandler :: non_local_function_handler(), 998 | ValueList :: [value()], 999 | NewBindings :: binding_struct()). 1000 | expr_list(Es, Bs, Lf, Ef) -> 1001 | expr_list(Es, Bs, Lf, Ef, empty_fun_used_vars()). 1002 | 1003 | expr_list(Es, Bs, Lf, Ef, FUVs) -> 1004 | expr_list(Es, [], Bs, Bs, Lf, Ef, FUVs). 1005 | 1006 | expr_list([E|Es], Vs, BsOrig, Bs0, Lf, Ef, FUVs) -> 1007 | {value,V,Bs1} = expr(E, BsOrig, Lf, Ef, none, FUVs), 1008 | expr_list(Es, [V|Vs], BsOrig, merge_bindings(Bs1, Bs0), Lf, Ef, FUVs); 1009 | expr_list([], Vs, _, Bs, _Lf, _Ef, _FUVs) -> 1010 | {reverse(Vs),Bs}. 1011 | 1012 | eval_op(Op, Arg1, Arg2, Bs, Ef, RBs) -> 1013 | do_apply(erlang, Op, [Arg1,Arg2], Bs, Ef, RBs). 1014 | 1015 | eval_op(Op, Arg, Bs, Ef, RBs) -> 1016 | do_apply(erlang, Op, [Arg], Bs, Ef, RBs). 1017 | 1018 | %% if_clauses(Clauses, Bindings, LocalFuncHandler, ExtFuncHandler, RBs) 1019 | 1020 | if_clauses([{clause,_,[],G,B}|Cs], Bs, Lf, Ef, RBs, FUVs) -> 1021 | case guard(G, Bs, Lf, Ef) of 1022 | true -> exprs(B, Bs, Lf, Ef, RBs, FUVs); 1023 | false -> if_clauses(Cs, Bs, Lf, Ef, RBs, FUVs) 1024 | end; 1025 | if_clauses([], _Bs, _Lf, _Ef, _RBs, _FUVs) -> 1026 | erlang:raise(error, if_clause, ?STACKTRACE). 1027 | 1028 | %% try_clauses(Body, CaseClauses, CatchClauses, AfterBody, Bindings, 1029 | %% LocalFuncHandler, ExtFuncHandler, RBs) 1030 | 1031 | try_clauses(B, Cases, Catches, AB, Bs, Lf, Ef, RBs, FUVs) -> 1032 | check_stacktrace_vars(Catches, Bs), 1033 | try exprs(B, Bs, Lf, Ef, none, FUVs) of 1034 | {value,V,Bs1} when Cases =:= [] -> 1035 | ret_expr(V, Bs1, RBs); 1036 | {value,V,Bs1} -> 1037 | case match_clause(Cases, [V], Bs1, Lf, Ef) of 1038 | {B2,Bs2} -> 1039 | exprs(B2, Bs2, Lf, Ef, RBs, FUVs); 1040 | nomatch -> 1041 | erlang:raise(error, {try_clause,V}, ?STACKTRACE) 1042 | end 1043 | catch 1044 | Class:Reason:Stacktrace when Catches =:= [] -> 1045 | erlang:raise(Class, Reason, Stacktrace); 1046 | Class:Reason:Stacktrace -> 1047 | V = {Class,Reason,Stacktrace}, 1048 | case match_clause(Catches, [V], Bs, Lf, Ef) of 1049 | {B2,Bs2} -> 1050 | exprs(B2, Bs2, Lf, Ef, RBs, FUVs); 1051 | nomatch -> 1052 | erlang:raise(Class, Reason, Stacktrace) 1053 | end 1054 | after 1055 | if AB =:= [] -> 1056 | Bs; % any 1057 | true -> 1058 | exprs(AB, Bs, Lf, Ef, none, FUVs) 1059 | end 1060 | end. 1061 | 1062 | check_stacktrace_vars([{clause,_,[{tuple,_,[_,_,STV]}],_,_}|Cs], Bs) -> 1063 | case STV of 1064 | {var,_,V} -> 1065 | case binding(V, Bs) of 1066 | {value, _} -> 1067 | erlang:raise(error, stacktrace_bound, ?STACKTRACE); 1068 | unbound -> 1069 | check_stacktrace_vars(Cs, Bs) 1070 | end; 1071 | _ -> 1072 | erlang:raise(error, 1073 | {illegal_stacktrace_variable,STV}, 1074 | ?STACKTRACE) 1075 | end; 1076 | check_stacktrace_vars([], _Bs) -> 1077 | ok. 1078 | 1079 | %% case_clauses(Value, Clauses, Bindings, LocalFuncHandler, ExtFuncHandler, 1080 | %% RBs) 1081 | 1082 | case_clauses(Val, Cs, Bs, Lf, Ef, RBs, FUVs) -> 1083 | case match_clause(Cs, [Val], Bs, Lf, Ef) of 1084 | {B, Bs1} -> 1085 | exprs(B, Bs1, Lf, Ef, RBs, FUVs); 1086 | nomatch -> 1087 | erlang:raise(error, {case_clause,Val}, ?STACKTRACE) 1088 | end. 1089 | 1090 | %% 1091 | %% receive_clauses(Clauses, Bindings, LocalFuncHnd,ExtFuncHnd, RBs) 1092 | %% 1093 | receive_clauses(Cs, Bs, Lf, Ef, RBs, FUVs) -> 1094 | receive_clauses(infinity, Cs, unused, Bs, Lf, Ef, RBs, FUVs). 1095 | %% 1096 | %% receive_clauses(TimeOut, Clauses, TimeoutBody, Bindings, 1097 | %% ExternalFuncHandler, LocalFuncHandler, RBs) 1098 | %% 1099 | receive_clauses(T, Cs, TB, Bs, Lf, Ef, RBs, FUVs) -> 1100 | F = fun (M) -> match_clause(Cs, [M], Bs, Lf, Ef) end, 1101 | case prim_eval:'receive'(F, T) of 1102 | {B, Bs1} -> 1103 | exprs(B, Bs1, Lf, Ef, RBs, FUVs); 1104 | timeout -> 1105 | {B, Bs1} = TB, 1106 | exprs(B, Bs1, Lf, Ef, RBs, FUVs) 1107 | end. 1108 | 1109 | %% match_clause -> {Body, Bindings} or nomatch 1110 | 1111 | -spec(match_clause(Clauses, ValueList, Bindings, LocalFunctionHandler) -> 1112 | {Body, NewBindings} | nomatch when 1113 | Clauses :: clauses(), 1114 | ValueList :: [value()], 1115 | Bindings :: binding_struct(), 1116 | LocalFunctionHandler :: local_function_handler(), 1117 | Body :: expression_list(), 1118 | NewBindings :: binding_struct()). 1119 | 1120 | match_clause(Cs, Vs, Bs, Lf) -> 1121 | match_clause(Cs, Vs, Bs, Lf, none). 1122 | 1123 | match_clause([{clause,_,H,G,B}|Cs], Vals, Bs, Lf, Ef) -> 1124 | case match_list(H, Vals, Bs) of 1125 | {match, Bs1} -> 1126 | case guard(G, Bs1, Lf, Ef) of 1127 | true -> {B, Bs1}; 1128 | false -> match_clause(Cs, Vals, Bs, Lf, Ef) 1129 | end; 1130 | nomatch -> match_clause(Cs, Vals, Bs, Lf, Ef) 1131 | end; 1132 | match_clause([], _Vals, _Bs, _Lf, _Ef) -> 1133 | nomatch. 1134 | 1135 | %% guard(GuardTests, Bindings, LocalFuncHandler, ExtFuncHandler) -> bool() 1136 | %% Evaluate a guard. We test if the guard is a true guard. 1137 | 1138 | guard(L=[G|_], Bs0, Lf, Ef) when is_list(G) -> 1139 | guard1(L, Bs0, Lf, Ef); 1140 | guard(L, Bs0, Lf, Ef) -> 1141 | guard0(L, Bs0, Lf, Ef). 1142 | 1143 | %% disjunction of guard conjunctions 1144 | guard1([G|Gs], Bs0, Lf, Ef) when is_list(G) -> 1145 | case guard0(G, Bs0, Lf, Ef) of 1146 | true -> 1147 | true; 1148 | false -> 1149 | guard1(Gs, Bs0, Lf, Ef) 1150 | end; 1151 | guard1([], _Bs, _Lf, _Ef) -> false. 1152 | 1153 | %% guard conjunction 1154 | guard0([G|Gs], Bs0, Lf, Ef) -> 1155 | case erl_lint:is_guard_test(G) of 1156 | true -> 1157 | case guard_test(G, Bs0, Lf, Ef) of 1158 | {value,true,Bs} -> guard0(Gs, Bs, Lf, Ef); 1159 | {value,false,_} -> false 1160 | end; 1161 | false -> 1162 | erlang:raise(error, guard_expr, ?STACKTRACE) 1163 | end; 1164 | guard0([], _Bs, _Lf, _Ef) -> true. 1165 | 1166 | %% guard_test(GuardTest, Bindings, LocalFuncHandler, ExtFuncHandler) -> 1167 | %% {value,bool(),NewBindings}. 1168 | %% Evaluate one guard test. Never fails, returns bool(). 1169 | 1170 | guard_test({call,A,{atom,Ln,F},As0}, Bs0, Lf, Ef) -> 1171 | TT = type_test(F), 1172 | G = {call,A,{atom,Ln,TT},As0}, 1173 | expr_guard_test(G, Bs0, Lf, Ef); 1174 | guard_test({call,A,{remote,Ar,{atom,Am,erlang},{atom,Af,F}},As0}, 1175 | Bs0, Lf, Ef) -> 1176 | TT = type_test(F), 1177 | G = {call,A,{remote,Ar,{atom,Am,erlang},{atom,Af,TT}},As0}, 1178 | expr_guard_test(G, Bs0, Lf, Ef); 1179 | guard_test(G, Bs0, Lf, Ef) -> 1180 | expr_guard_test(G, Bs0, Lf, Ef). 1181 | 1182 | expr_guard_test(G, Bs0, Lf, Ef) -> 1183 | try {value,true,_} = expr(G, Bs0, Lf, Ef, none) 1184 | catch error:_ -> {value,false,Bs0} end. 1185 | 1186 | type_test(integer) -> is_integer; 1187 | type_test(float) -> is_float; 1188 | type_test(number) -> is_number; 1189 | type_test(atom) -> is_atom; 1190 | type_test(list) -> is_list; 1191 | type_test(tuple) -> is_tuple; 1192 | type_test(pid) -> is_pid; 1193 | type_test(reference) -> is_reference; 1194 | type_test(port) -> is_port; 1195 | type_test(function) -> is_function; 1196 | type_test(binary) -> is_binary; 1197 | type_test(record) -> is_record; 1198 | type_test(map) -> is_map; 1199 | type_test(Test) -> Test. 1200 | 1201 | 1202 | %% match(Pattern, Term, Bindings) -> 1203 | %% {match,NewBindings} | nomatch 1204 | %% or erlang:error({illegal_pattern, Pattern}). 1205 | %% Try to match Pattern against Term with the current bindings. 1206 | 1207 | match(Pat, Term, Bs) -> 1208 | match(Pat, Term, Bs, Bs). 1209 | 1210 | %% Bs are the bindings that are augmented with new bindings. BBs are 1211 | %% the bindings used for "binsize" variables (in <>, Y is a 1212 | %% binsize variable). 1213 | 1214 | match(Pat, Term, Bs, BBs) -> 1215 | case catch match1(Pat, Term, Bs, BBs) of 1216 | invalid -> 1217 | erlang:raise(error, {illegal_pattern,to_term(Pat)}, ?STACKTRACE); 1218 | Other -> 1219 | Other 1220 | end. 1221 | 1222 | string_to_conses([], _, Tail) -> Tail; 1223 | string_to_conses([E|Rest], Anno, Tail) -> 1224 | {cons, Anno, {integer, Anno, E}, string_to_conses(Rest, Anno, Tail)}. 1225 | 1226 | match1({atom,_,A0}, A, Bs, _BBs) -> 1227 | case A of 1228 | A0 -> {match,Bs}; 1229 | _ -> throw(nomatch) 1230 | end; 1231 | match1({integer,_,I0}, I, Bs, _BBs) -> 1232 | case I of 1233 | I0 -> {match,Bs}; 1234 | _ -> throw(nomatch) 1235 | end; 1236 | match1({float,_,F0}, F, Bs, _BBs) -> 1237 | case F of 1238 | F0 -> {match,Bs}; 1239 | _ -> throw(nomatch) 1240 | end; 1241 | match1({char,_,C0}, C, Bs, _BBs) -> 1242 | case C of 1243 | C0 -> {match,Bs}; 1244 | _ -> throw(nomatch) 1245 | end; 1246 | match1({var,_,'_'}, _, Bs, _BBs) -> %Anonymous variable matches 1247 | {match,Bs}; % everything, no new bindings 1248 | match1({var,_,Name}, Term, Bs, _BBs) -> 1249 | case binding(Name, Bs) of 1250 | {value,Term} -> 1251 | {match,Bs}; 1252 | {value,_} -> 1253 | throw(nomatch); 1254 | unbound -> 1255 | {match,add_binding(Name, Term, Bs)} 1256 | end; 1257 | match1({match,_,Pat1,Pat2}, Term, Bs0, BBs) -> 1258 | {match, Bs1} = match1(Pat1, Term, Bs0, BBs), 1259 | match1(Pat2, Term, Bs1, BBs); 1260 | match1({string,_,S0}, S, Bs, _BBs) -> 1261 | case S of 1262 | S0 -> {match,Bs}; 1263 | _ -> throw(nomatch) 1264 | end; 1265 | match1({nil,_}, Nil, Bs, _BBs) -> 1266 | case Nil of 1267 | [] -> {match,Bs}; 1268 | _ -> throw(nomatch) 1269 | end; 1270 | match1({cons,_,H,T}, [H1|T1], Bs0, BBs) -> 1271 | {match,Bs} = match1(H, H1, Bs0, BBs), 1272 | match1(T, T1, Bs, BBs); 1273 | match1({cons,_,_,_}, _, _Bs, _BBs) -> 1274 | throw(nomatch); 1275 | match1({tuple,_,Elts}, Tuple, Bs, BBs) 1276 | when length(Elts) =:= tuple_size(Tuple) -> 1277 | match_tuple(Elts, Tuple, 1, Bs, BBs); 1278 | match1({tuple,_,_}, _, _Bs, _BBs) -> 1279 | throw(nomatch); 1280 | match1({map,_,Fs}, #{}=Map, Bs, BBs) -> 1281 | match_map(Fs, Map, Bs, BBs); 1282 | match1({map,_,_}, _, _Bs, _BBs) -> 1283 | throw(nomatch); 1284 | match1({bin, _, Fs}, <<_/bitstring>>=B, Bs0, BBs) -> 1285 | EvalFun = fun(E, Bs) -> 1286 | case erl_lint:is_guard_expr(E) of 1287 | true -> ok; 1288 | false -> throw(invalid) 1289 | end, 1290 | try 1291 | expr(E, Bs, none, none, none) 1292 | catch 1293 | error:{unbound, _} -> 1294 | throw(invalid) 1295 | end 1296 | end, 1297 | eval_bits:match_bits(Fs, B, Bs0, BBs, match_fun(BBs), EvalFun); 1298 | match1({bin,_,_}, _, _Bs, _BBs) -> 1299 | throw(nomatch); 1300 | match1({op,_,'++',{nil,_},R}, Term, Bs, BBs) -> 1301 | match1(R, Term, Bs, BBs); 1302 | match1({op,_,'++',{cons,Ai,{integer,A2,I},T},R}, Term, Bs, BBs) -> 1303 | match1({cons,Ai,{integer,A2,I},{op,Ai,'++',T,R}}, Term, Bs, BBs); 1304 | match1({op,_,'++',{cons,Ai,{char,A2,C},T},R}, Term, Bs, BBs) -> 1305 | match1({cons,Ai,{char,A2,C},{op,Ai,'++',T,R}}, Term, Bs, BBs); 1306 | match1({op,_,'++',{string,Ai,L},R}, Term, Bs, BBs) -> 1307 | match1(string_to_conses(L, Ai, R), Term, Bs, BBs); 1308 | match1({op,Anno,Op,A}, Term, Bs, BBs) -> 1309 | case partial_eval({op,Anno,Op,A}) of 1310 | {op,Anno,Op,A} -> 1311 | throw(invalid); 1312 | X -> 1313 | match1(X, Term, Bs, BBs) 1314 | end; 1315 | match1({op,Anno,Op,L,R}, Term, Bs, BBs) -> 1316 | case partial_eval({op,Anno,Op,L,R}) of 1317 | {op,Anno,Op,L,R} -> 1318 | throw(invalid); 1319 | X -> 1320 | match1(X, Term, Bs, BBs) 1321 | end; 1322 | match1(_, _, _Bs, _BBs) -> 1323 | throw(invalid). 1324 | 1325 | match_fun(BBs) -> 1326 | fun(match, {L,R,Bs}) -> match1(L, R, Bs, BBs); 1327 | (binding, {Name,Bs}) -> binding(Name, Bs); 1328 | (add_binding, {Name,Val,Bs}) -> add_binding(Name, Val, Bs) 1329 | end. 1330 | 1331 | match_tuple([E|Es], Tuple, I, Bs0, BBs) -> 1332 | {match,Bs} = match1(E, element(I, Tuple), Bs0, BBs), 1333 | match_tuple(Es, Tuple, I+1, Bs, BBs); 1334 | match_tuple([], _, _, Bs, _BBs) -> 1335 | {match,Bs}. 1336 | 1337 | match_map([{map_field_exact, _, K, V}|Fs], Map, Bs0, BBs) -> 1338 | Vm = try 1339 | {value, Ke, _} = expr(K, BBs), 1340 | maps:get(Ke,Map) 1341 | catch error:_ -> 1342 | throw(nomatch) 1343 | end, 1344 | {match, Bs} = match1(V, Vm, Bs0, BBs), 1345 | match_map(Fs, Map, Bs, BBs); 1346 | match_map([], _, Bs, _) -> 1347 | {match, Bs}. 1348 | 1349 | %% match_list(PatternList, TermList, Bindings) -> 1350 | %% {match,NewBindings} | nomatch 1351 | %% Try to match a list of patterns against a list of terms with the 1352 | %% current bindings. 1353 | 1354 | match_list(Ps, Ts, Bs) -> 1355 | match_list(Ps, Ts, Bs, Bs). 1356 | 1357 | match_list([P|Ps], [T|Ts], Bs0, BBs) -> 1358 | case match(P, T, Bs0, BBs) of 1359 | {match,Bs1} -> match_list(Ps, Ts, Bs1, BBs); 1360 | nomatch -> nomatch 1361 | end; 1362 | match_list([], [], Bs, _BBs) -> 1363 | {match,Bs}; 1364 | match_list(_, _, _Bs, _BBs) -> 1365 | nomatch. 1366 | 1367 | %% new_bindings() 1368 | %% bindings(Bindings) 1369 | %% binding(Name, Bindings) 1370 | %% add_binding(Name, Value, Bindings) 1371 | %% del_binding(Name, Bindings) 1372 | 1373 | -spec(new_bindings() -> binding_struct()). 1374 | new_bindings() -> orddict:new(). 1375 | 1376 | -spec(bindings(BindingStruct :: binding_struct()) -> bindings()). 1377 | bindings(Bs) when is_map(Bs) -> maps:to_list(Bs); 1378 | bindings(Bs) when is_list(Bs) -> orddict:to_list(Bs). 1379 | 1380 | -spec(binding(Name, BindingStruct) -> {value, value()} | unbound when 1381 | Name :: name(), 1382 | BindingStruct :: binding_struct()). 1383 | binding(Name, Bs) when is_map(Bs) -> 1384 | case maps:find(Name, Bs) of 1385 | {ok,Val} -> {value,Val}; 1386 | error -> unbound 1387 | end; 1388 | binding(Name, Bs) when is_list(Bs) -> 1389 | case orddict:find(Name, Bs) of 1390 | {ok,Val} -> {value,Val}; 1391 | error -> unbound 1392 | end. 1393 | 1394 | -spec(add_binding(Name, Value, BindingStruct) -> binding_struct() when 1395 | Name :: name(), 1396 | Value :: value(), 1397 | BindingStruct :: binding_struct()). 1398 | add_binding(Name, Val, Bs) when is_map(Bs) -> maps:put(Name, Val, Bs); 1399 | add_binding(Name, Val, Bs) when is_list(Bs) -> orddict:store(Name, Val, Bs). 1400 | 1401 | -spec(del_binding(Name, BindingStruct) -> binding_struct() when 1402 | Name :: name(), 1403 | BindingStruct :: binding_struct()). 1404 | del_binding(Name, Bs) when is_map(Bs) -> maps:remove(Name, Bs); 1405 | del_binding(Name, Bs) when is_list(Bs) -> orddict:erase(Name, Bs). 1406 | 1407 | add_bindings(Bs1, Bs2) when is_map(Bs1), is_map(Bs2) -> 1408 | maps:merge(Bs2, Bs1); 1409 | add_bindings(Bs1, Bs2) -> 1410 | foldl(fun ({Name,Val}, Bs) -> orddict:store(Name, Val, Bs) end, 1411 | Bs2, orddict:to_list(Bs1)). 1412 | 1413 | merge_bindings(Bs1, Bs2) when is_map(Bs1), is_map(Bs2) -> 1414 | merge_with(fun 1415 | (_K, V, V) -> V; 1416 | (_K, _, V) -> erlang:raise(error, {badmatch,V}, ?STACKTRACE) 1417 | end, Bs2, Bs1); 1418 | merge_bindings(Bs1, Bs2) -> 1419 | foldl(fun ({Name,Val}, Bs) -> 1420 | case orddict:find(Name, Bs) of 1421 | {ok,Val} -> Bs; %Already with SAME value 1422 | {ok,V1} -> 1423 | erlang:raise(error, {badmatch,V1}, ?STACKTRACE); 1424 | error -> orddict:store(Name, Val, Bs) 1425 | end end, 1426 | Bs2, orddict:to_list(Bs1)). 1427 | 1428 | new_bindings(Bs) when is_map(Bs) -> maps:new(); 1429 | new_bindings(Bs) when is_list(Bs) -> orddict:new(). 1430 | 1431 | filter_bindings(Fun, Bs) when is_map(Bs) -> maps:filter(Fun, Bs); 1432 | filter_bindings(Fun, Bs) when is_list(Bs) -> orddict:filter(Fun, Bs). 1433 | 1434 | to_terms(Abstrs) -> 1435 | [to_term(Abstr) || Abstr <- Abstrs]. 1436 | 1437 | to_term(Abstr) -> 1438 | erl_parse:anno_to_term(Abstr). 1439 | 1440 | %% `Tokens' is assumed to have been scanned with the 'text' option. 1441 | %% The annotations of the returned expressions are locations. 1442 | %% 1443 | %% Can handle pids, ports, references, and external funs ("items"). 1444 | %% Known items are represented by variables in the erl_parse tree, and 1445 | %% the items themselves are stored in the returned bindings. 1446 | 1447 | -spec extended_parse_exprs(Tokens) -> 1448 | {'ok', ExprList} | {'error', ErrorInfo} when 1449 | Tokens :: [erl_scan:token()], 1450 | ExprList :: [erl_parse:abstract_expr()], 1451 | ErrorInfo :: erl_parse:error_info(). 1452 | 1453 | extended_parse_exprs(Tokens) -> 1454 | Ts = tokens_fixup(Tokens), 1455 | case erl_parse:parse_exprs(Ts) of 1456 | {ok, Exprs0} -> 1457 | Exprs = expr_fixup(Exprs0), 1458 | {ok, reset_expr_anno(Exprs)}; 1459 | _ErrorInfo -> 1460 | erl_parse:parse_exprs(reset_token_anno(Ts)) 1461 | end. 1462 | 1463 | tokens_fixup([]) -> []; 1464 | tokens_fixup([T|Ts]=Ts0) -> 1465 | try token_fixup(Ts0) of 1466 | {NewT, NewTs} -> 1467 | [NewT|tokens_fixup(NewTs)] 1468 | catch 1469 | _:_ -> 1470 | [T|tokens_fixup(Ts)] 1471 | end. 1472 | 1473 | token_fixup(Ts) -> 1474 | {AnnoL, NewTs, FixupTag} = unscannable(Ts), 1475 | String = lists:append([erl_anno:text(A) || A <- AnnoL]), 1476 | _ = validate_tag(FixupTag, String), 1477 | NewAnno = erl_anno:set_text(fixup_text(FixupTag), hd(AnnoL)), 1478 | {{string, NewAnno, String}, NewTs}. 1479 | 1480 | unscannable([{'#', A1}, {var, A2, 'Fun'}, {'<', A3}, {atom, A4, _}, 1481 | {'.', A5}, {float, A6, _}, {'>', A7}|Ts]) -> 1482 | {[A1, A2, A3, A4, A5, A6, A7], Ts, function}; 1483 | unscannable([{'#', A1}, {var, A2, 'Fun'}, {'<', A3}, {atom, A4, _}, 1484 | {'.', A5}, {atom, A6, _}, {'.', A7}, {integer, A8, _}, 1485 | {'>', A9}|Ts]) -> 1486 | {[A1, A2, A3, A4, A5, A6, A7, A8, A9], Ts, function}; 1487 | unscannable([{'<', A1}, {float, A2, _}, {'.', A3}, {integer, A4, _}, 1488 | {'>', A5}|Ts]) -> 1489 | {[A1, A2, A3, A4, A5], Ts, pid}; 1490 | unscannable([{'#', A1}, {var, A2, 'Port'}, {'<', A3}, {float, A4, _}, 1491 | {'>', A5}|Ts]) -> 1492 | {[A1, A2, A3, A4, A5], Ts, port}; 1493 | unscannable([{'#', A1}, {var, A2, 'Ref'}, {'<', A3}, {float, A4, _}, 1494 | {'.', A5}, {float, A6, _}, {'>', A7}|Ts]) -> 1495 | {[A1, A2, A3, A4, A5, A6, A7], Ts, reference}. 1496 | 1497 | expr_fixup({string,A,S}=T) -> 1498 | try string_fixup(A, S, T) of 1499 | Expr -> Expr 1500 | catch 1501 | _:_ -> T 1502 | end; 1503 | expr_fixup(Tuple) when is_tuple(Tuple) -> 1504 | L = expr_fixup(tuple_to_list(Tuple)), 1505 | list_to_tuple(L); 1506 | expr_fixup([E0|Es0]) -> 1507 | E = expr_fixup(E0), 1508 | Es = expr_fixup(Es0), 1509 | [E|Es]; 1510 | expr_fixup(T) -> 1511 | T. 1512 | 1513 | string_fixup(Ann, String, Token) -> 1514 | Text = erl_anno:text(Ann), 1515 | FixupTag = fixup_tag(Text, String), 1516 | fixup_ast(FixupTag, Ann, String, Token). 1517 | 1518 | reset_token_anno(Tokens) -> 1519 | [setelement(2, T, (reset_anno())(element(2, T))) || T <- Tokens]. 1520 | 1521 | reset_expr_anno(Exprs) -> 1522 | [erl_parse:map_anno(reset_anno(), E) || E <- Exprs]. 1523 | 1524 | reset_anno() -> 1525 | fun(A) -> erl_anno:new(erl_anno:location(A)) end. 1526 | 1527 | fixup_ast(pid, A, _S, T) -> 1528 | {call,A,{remote,A,{atom,A,erlang},{atom,A,list_to_pid}},[T]}; 1529 | fixup_ast(port, A, _S, T) -> 1530 | {call,A,{remote,A,{atom,A,erlang},{atom,A,list_to_port}},[T]}; 1531 | fixup_ast(reference, A, _S, T) -> 1532 | {call,A,{remote,A,{atom,A,erlang},{atom,A,list_to_ref}},[T]}; 1533 | fixup_ast(function, A, S, _T) -> 1534 | {Module, Function, Arity} = fixup_mfa(S), 1535 | {'fun',A,{function,{atom,A,Module},{atom,A,Function},{integer,A,Arity}}}. 1536 | 1537 | fixup_text(function) -> "function"; 1538 | fixup_text(pid) -> "pid"; 1539 | fixup_text(port) -> "port"; 1540 | fixup_text(reference) -> "reference". 1541 | 1542 | fixup_tag("function", "#"++_) -> function; 1543 | fixup_tag("pid", "<"++_) -> pid; 1544 | fixup_tag("port", "#"++_) -> port; 1545 | fixup_tag("reference", "#"++_) -> reference. 1546 | 1547 | fixup_mfa(S) -> 1548 | {ok, [_, _, _, 1549 | {atom, _, Module}, _, 1550 | {atom, _, Function}, _, 1551 | {integer, _, Arity}|_], _} = erl_scan:string(S), 1552 | {Module, Function, Arity}. 1553 | 1554 | validate_tag(pid, String) -> erlang:list_to_pid(String); 1555 | validate_tag(port, String) -> erlang:list_to_port(String); 1556 | validate_tag(reference, String) -> erlang:list_to_ref(String); 1557 | validate_tag(function, String) -> 1558 | {Module, Function, Arity} = fixup_mfa(String), 1559 | erlang:make_fun(Module, Function, Arity). 1560 | 1561 | %%% End of extended_parse_exprs. 1562 | 1563 | %% `Tokens' is assumed to have been scanned with the 'text' option. 1564 | %% 1565 | %% Can handle pids, ports, references, and external funs. 1566 | 1567 | -spec extended_parse_term(Tokens) -> 1568 | {'ok', Term} | {'error', ErrorInfo} when 1569 | Tokens :: [erl_scan:token()], 1570 | Term :: term(), 1571 | ErrorInfo :: erl_parse:error_info(). 1572 | 1573 | extended_parse_term(Tokens) -> 1574 | case extended_parse_exprs(Tokens) of 1575 | {ok, [Expr]} -> 1576 | try normalise(Expr) of 1577 | Term -> 1578 | {ok, Term} 1579 | catch 1580 | _:_ -> 1581 | Loc = erl_anno:location(element(2, Expr)), 1582 | {error,{Loc,?MODULE,"bad term"}} 1583 | end; 1584 | {ok, [_,Expr|_]} -> 1585 | Loc = erl_anno:location(element(2, Expr)), 1586 | {error,{Loc,?MODULE,"bad term"}}; 1587 | {error, _} = Error -> 1588 | Error 1589 | end. 1590 | 1591 | %% From erl_parse. 1592 | normalise({char,_,C}) -> C; 1593 | normalise({integer,_,I}) -> I; 1594 | normalise({float,_,F}) -> F; 1595 | normalise({atom,_,A}) -> A; 1596 | normalise({string,_,S}) -> S; 1597 | normalise({nil,_}) -> []; 1598 | normalise({bin,_,Fs}) -> 1599 | {value, B, _} = 1600 | eval_bits:expr_grp(Fs, [], 1601 | fun(E, _) -> 1602 | {value, normalise(E), []} 1603 | end), 1604 | B; 1605 | normalise({cons,_,Head,Tail}) -> 1606 | [normalise(Head)|normalise(Tail)]; 1607 | normalise({tuple,_,Args}) -> 1608 | list_to_tuple(normalise_list(Args)); 1609 | normalise({map,_,Pairs}) -> 1610 | maps:from_list(lists:map(fun 1611 | %% only allow '=>' 1612 | ({map_field_assoc,_,K,V}) -> 1613 | {normalise(K),normalise(V)} 1614 | end, Pairs)); 1615 | %% Special case for unary +/-. 1616 | normalise({op,_,'+',{char,_,I}}) -> I; 1617 | normalise({op,_,'+',{integer,_,I}}) -> I; 1618 | normalise({op,_,'+',{float,_,F}}) -> F; 1619 | normalise({op,_,'-',{char,_,I}}) -> -I; %Weird, but compatible! 1620 | normalise({op,_,'-',{integer,_,I}}) -> -I; 1621 | normalise({op,_,'-',{float,_,F}}) -> -F; 1622 | %% Special case for #...<> 1623 | normalise({call,_,{remote,_,{atom,_,erlang},{atom,_,Fun}},[{string,_,S}]}) when 1624 | Fun =:= list_to_ref; Fun =:= list_to_port; Fun =:= list_to_pid -> 1625 | erlang:Fun(S); 1626 | normalise({'fun',_,{function,{atom,_,M},{atom,_,F},{integer,_,A}}}) -> 1627 | %% Since "#Fun" is recognized, "fun M:F/A" should be too. 1628 | fun M:F/A. 1629 | 1630 | normalise_list([H|T]) -> 1631 | [normalise(H)|normalise_list(T)]; 1632 | normalise_list([]) -> 1633 | []. 1634 | 1635 | %%---------------------------------------------------------------------------- 1636 | %% 1637 | %% Evaluate expressions: 1638 | %% constants and 1639 | %% op A 1640 | %% L op R 1641 | %% Things that evaluate to constants are accepted 1642 | %% and guard_bifs are allowed in constant expressions 1643 | %%---------------------------------------------------------------------------- 1644 | 1645 | is_constant_expr(Expr) -> 1646 | case eval_expr(Expr) of 1647 | {ok, X} when is_number(X) -> true; 1648 | _ -> false 1649 | end. 1650 | 1651 | eval_expr(Expr) -> 1652 | case catch ev_expr(Expr) of 1653 | X when is_integer(X) -> {ok, X}; 1654 | X when is_float(X) -> {ok, X}; 1655 | X when is_atom(X) -> {ok,X}; 1656 | {'EXIT',Reason} -> {error, Reason}; 1657 | _ -> {error, badarg} 1658 | end. 1659 | 1660 | partial_eval(Expr) -> 1661 | Anno = anno(Expr), 1662 | case catch ev_expr(Expr) of 1663 | X when is_integer(X) -> ret_expr(Expr,{integer,Anno,X}); 1664 | X when is_float(X) -> ret_expr(Expr,{float,Anno,X}); 1665 | X when is_atom(X) -> ret_expr(Expr,{atom,Anno,X}); 1666 | _ -> 1667 | Expr 1668 | end. 1669 | 1670 | ev_expr({op,_,Op,L,R}) -> erlang:Op(ev_expr(L), ev_expr(R)); 1671 | ev_expr({op,_,Op,A}) -> erlang:Op(ev_expr(A)); 1672 | ev_expr({integer,_,X}) -> X; 1673 | ev_expr({char,_,X}) -> X; 1674 | ev_expr({float,_,X}) -> X; 1675 | ev_expr({atom,_,X}) -> X; 1676 | ev_expr({tuple,_,Es}) -> 1677 | list_to_tuple([ev_expr(X) || X <- Es]); 1678 | ev_expr({nil,_}) -> []; 1679 | ev_expr({cons,_,H,T}) -> [ev_expr(H) | ev_expr(T)]. 1680 | %%ev_expr({call,Anno,{atom,_,F},As}) -> 1681 | %% true = erl_internal:guard_bif(F, length(As)), 1682 | %% apply(erlang, F, [ev_expr(X) || X <- As]); 1683 | %%ev_expr({call,Anno,{remote,_,{atom,_,erlang},{atom,_,F}},As}) -> 1684 | %% true = erl_internal:guard_bif(F, length(As)), 1685 | %% apply(erlang, F, [ev_expr(X) || X <- As]); 1686 | 1687 | %% eval_str(InStr) -> {ok, OutStr} | {error, ErrStr'} 1688 | %% InStr must represent a body 1689 | %% Note: If InStr is a binary it has to be a Latin-1 string. 1690 | %% If you have a UTF-8 encoded binary you have to call 1691 | %% unicode:characters_to_list/1 before the call to eval_str(). 1692 | 1693 | -define(result(F,D), lists:flatten(io_lib:format(F, D))). 1694 | 1695 | -spec eval_str(string() | unicode:latin1_binary()) -> 1696 | {'ok', string()} | {'error', string()}. 1697 | 1698 | eval_str(Str) when is_list(Str) -> 1699 | case erl_scan:tokens([], Str, 0) of 1700 | {more, _} -> 1701 | {error, "Incomplete form (missing .)??"}; 1702 | {done, {ok, Toks, _}, Rest} -> 1703 | case all_white(Rest) of 1704 | true -> 1705 | case erl_parse:parse_exprs(Toks) of 1706 | {ok, Exprs} -> 1707 | case catch erl_eval:exprs(Exprs, erl_eval:new_bindings()) of 1708 | {value, Val, _} -> 1709 | {ok, Val}; 1710 | Other -> 1711 | {error, ?result("*** eval: ~p", [Other])} 1712 | end; 1713 | {error, {_Location, Mod, Args}} -> 1714 | Msg = ?result("*** ~ts",[Mod:format_error(Args)]), 1715 | {error, Msg} 1716 | end; 1717 | false -> 1718 | {error, ?result("Non-white space found after " 1719 | "end-of-form :~ts", [Rest])} 1720 | end 1721 | end; 1722 | eval_str(Bin) when is_binary(Bin) -> 1723 | eval_str(binary_to_list(Bin)). 1724 | 1725 | all_white([$\s|T]) -> all_white(T); 1726 | all_white([$\n|T]) -> all_white(T); 1727 | all_white([$\t|T]) -> all_white(T); 1728 | all_white([]) -> true; 1729 | all_white(_) -> false. 1730 | 1731 | ret_expr(_Old, New) -> 1732 | %% io:format("~w: reduced ~s => ~s~n", 1733 | %% [line(Old), erl_pp:expr(Old), erl_pp:expr(New)]), 1734 | New. 1735 | 1736 | anno(Expr) -> element(2, Expr). 1737 | 1738 | merge_with(Combiner, Map1, Map2) when is_map(Map1), 1739 | is_map(Map2), 1740 | is_function(Combiner, 3) -> 1741 | case map_size(Map1) > map_size(Map2) of 1742 | true -> 1743 | Iterator = maps:iterator(Map2), 1744 | merge_with_1(maps:next(Iterator), 1745 | Map1, 1746 | Map2, 1747 | Combiner); 1748 | false -> 1749 | Iterator = maps:iterator(Map1), 1750 | merge_with_1(maps:next(Iterator), 1751 | Map2, 1752 | Map1, 1753 | fun(K, V1, V2) -> Combiner(K, V2, V1) end) 1754 | end. 1755 | 1756 | merge_with_1({K, V2, Iterator}, Map1, Map2, Combiner) -> 1757 | case Map1 of 1758 | #{ K := V1 } -> 1759 | NewMap1 = Map1#{ K := Combiner(K, V1, V2) }, 1760 | merge_with_1(maps:next(Iterator), NewMap1, Map2, Combiner); 1761 | #{ } -> 1762 | merge_with_1(maps:next(Iterator), maps:put(K, V2, Map1), Map2, Combiner) 1763 | end; 1764 | merge_with_1(none, Result, _, _) -> 1765 | Result. 1766 | --------------------------------------------------------------------------------