├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── config └── config.exs ├── lib ├── erl2ex.ex ├── erl2ex │ ├── cli.ex │ ├── convert │ │ ├── context.ex │ │ ├── erl_expressions.ex │ │ ├── erl_forms.ex │ │ ├── ext_forms.ex │ │ └── headers.ex │ ├── pipeline │ │ ├── analyze.ex │ │ ├── codegen.ex │ │ ├── convert.ex │ │ ├── erl_syntax.ex │ │ ├── ex_data.ex │ │ ├── inline_includes.ex │ │ ├── module_data.ex │ │ ├── names.ex │ │ ├── parse.ex │ │ └── utils.ex │ ├── results.ex │ ├── results_collector.ex │ ├── sink.ex │ └── source.ex └── mix │ └── tasks │ └── erl2ex.ex ├── mix.exs ├── mix.lock └── test ├── e2e_test.exs ├── error_test.exs ├── expression_test.exs ├── files ├── files2 │ └── include2.hrl ├── include1.hrl └── include3.hrl ├── function_test.exs ├── preprocessor_test.exs ├── scope_test.exs ├── structure_test.exs ├── test_helper.exs └── type_test.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | /erl2ex 6 | /test/dummy_test.exs 7 | /tmp 8 | erl_crash.dump 9 | *.ez 10 | SCRATCH.txt 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | elixir: 4 | - 1.4.5 5 | otp_release: 6 | - 18.3 7 | - 19.0 8 | 9 | script: 10 | - mix test --include e2e 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Erl2ex is currently pre-alpha software. Expect significant backwards-incompatible changes for the time being. 4 | 5 | ## v0.0.10 (not yet released) 6 | 7 | * Compatibility with Elixir 1.3 and Erlang 19. 8 | * Support for variables in fun M:F/A. 9 | * A macro argument that collides with a reserved word or function name wasn't properly renamed. Fixed. 10 | * The "inline" compile option didn't map function names properly. Fixed. 11 | * The "nowarn_unused_function" compile option is broken under Elixir. Disabled it during transpilation. 12 | * Further simplified conversion of public functions with normally un-deffable names. 13 | * Documented the code better. 14 | 15 | ## v0.0.9 (2016-04-11) 16 | 17 | * Created a structure representing the results and status of a conversion, and reworked the main entry point functions to use it. 18 | * Allow specification of the directory for application dependencies (i.e. where to look for include_lib files.) 19 | * Simple macros can now appear in typespecs and record fields (but not yet the record name). The transpiler substitutes the replacement value because Elixir syntax can't handle a macro call at those locations. 20 | * Rename specs when the transpiler renames the associated function. 21 | * Reworked conversion of public functions whose names can't be deffed directly, to use a simpler technique that now also works for "unquote". 22 | * Calls to remote functions with names that can't be referenced directly (like "1func" or "unquote") failed. Fixed using Kernel.apply. 23 | * Reworked conversion of "if" statements so it supports expressions that cause errors. 24 | * Add "when" to the list of reserved names. 25 | * The Erlang is_record BIF is now mapped to the Elixir Record.is_record macro so it's compatible with Elixir-defined records. 26 | 27 | ## v0.0.8 (2016-02-25) 28 | 29 | * Factored out source and sink processes, to make testing easier and secondary jobs cleaner. 30 | * Perform `epp_dodger` parsing in addition to `erl_parse`. We're not taking full advantage of this yet, but it should eventually help preserve comments better, as well as handle some more preprocessor edge cases. 31 | * REGRESSION: preservation of module and form comments is currently broken as a result of the above. This will be fixed in a later version. 32 | * Refactored and rearranged the internal pipeline modules. 33 | * Support full bitstring modifier syntax. 34 | * Support comprehensions whose first qualifier isn't a generator. (Thanks to eproxus for the tip.) 35 | * If an operator is translated to an Elixir BIF with the same name as an exported function, it is now properly qualified. 36 | * Better analysis to determine when variables in arguments passed to macros should be exported. 37 | * Unhygenize variables created in macro body, to match Erlang macro behavior. 38 | * Ensure all variables in specs are called out in constraints as required by Elixir. 39 | * Recognize type info in record declarations, and emit them with record types. 40 | * Variables passed to size() in a binary pattern match incorrectly had a caret. Fixed. 41 | * A -file attribute no longer converts directly to Elixir (which would break compilation). 42 | * Better error reporting when an included file could not be found. 43 | 44 | ## v0.0.7 (2016-01-25) 45 | 46 | * Overhauled the logic that reconciled imported vs defined functions, and fixed some related issues. Calls of functions with conflicting names are now properly qualified. 47 | * Allow definition and calling of exported functions with names that the parser won't normally accept; e.g. Elixir keywords such as "do", or names with strange characters such as "E=mc^2". 48 | * Support invoking a constant macro as a function name. 49 | * If a function's argument pattern looks like a keyword block, it tried to codegen as such. Fixed. 50 | * If an Erlang variable name was a capitalized version of an Elixir keyword (such as "End"), it would generate uncompilable Elixir code. Fixed. 51 | * If the input did not end with a newline, the final form was dropped. Fixed. 52 | * The "??" stringification preprocessor syntax generated binaries rather than char lists. Fixed. 53 | * Created a mechanism to compile and run the generated code in unit tests, and started modifying a subset of the tests to use it. 54 | * Started some optional end-to-end tests that convert and run against common Erlang libraries. 55 | * Refactor: Break analysis out into a separate stage instead of combining with conversion context. 56 | 57 | ## v0.0.6 (2016-01-19) 58 | 59 | * Unicode characters greater than 127 were incorrectly encoded in output files, and codepoints greater than 255 caused codegen to crash. Fixed. 60 | * Convert string() and nonempty_string() Erlang types to the preferred Elixir equivalents, to avoid an Elixir warning. 61 | * Support for environment variable interpolation in include paths. 62 | * Support for redefining macros. 63 | * Support for macro definitions that include comma and semicolon delimited expressions. 64 | * Generate macros for record_info calls and record index expressions. 65 | * Erlang characters (e.g. $A) are translated into the Elixir equivalent syntax (e.g. ?A) rather than integers. 66 | 67 | ## v0.0.5 (2016-01-11) 68 | 69 | * All Erlang macros now translate to Elixir macros, since it seems to be possible for parameterless macros not to be simple values. 70 | * Variable names could clash with a BIF referenced in another function. Fixed. 71 | 72 | ## v0.0.4 (2016-01-05) 73 | 74 | * Generate file comments by default. 75 | * When a comment begins with multiple percent signs, convert all of them to hashes. 76 | * Separate clauses within a case, receive, or catch leaked variable scopes to each other. Fixed. 77 | * Support remote function calls with an expression as the function name. 78 | * Evaluate record_info calls directly since creating a function doesn't seem to work. 79 | * Ensure "defined_*" attributes are initialized if the erlang source doesn't define them explicitly. 80 | * Allow definition of constant macros from environment variables or application configs. 81 | 82 | ## v0.0.3 (2016-01-03) 83 | 84 | * Requires Elixir 1.2. Updated the source for 1.2 warnings and deprecations. 85 | * Updated and cleaned up ExDoc documentation. 86 | * Support include_lib directive. 87 | * Generate comments around each inline included file. 88 | * Repeated matches on the same variable weren't properly annotated with a caret. Fixed. 89 | * Funs with no parameters incorrectly translated to a single nil parameter. Fixed. 90 | * Variable name mangling did not preserve leading underscores. Fixed. 91 | 92 | ## v0.0.2 (2015-12-31) 93 | 94 | * Better reporting of parse and conversion errors. 95 | * Support for custom and remote types. 96 | * Support for the after-clause of receive. 97 | * Ifdef/ifndef/undef didn't handle capitalized macro names. Fixed. 98 | * Catch assumed the kind was an atom and didn't accept an expression. Fixed. 99 | * Recognize bitstring elements with an explicit value, explicit size, and binary type. 100 | 101 | ## v0.0.1 (2015-12-28) 102 | 103 | * Initial release to hex. 104 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Copyright 2015 Daniel Azuma 4 | 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | * Neither the name of the copyright holder, nor the names of any other 16 | contributors to this software, may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erl2ex 2 | 3 | [![Build Status](https://travis-ci.org/dazuma/erl2ex.svg?branch=master)](https://travis-ci.org/dazuma/erl2ex) 4 | 5 | Erl2ex is an Erlang to Elixir transpiler, converting well-formed Erlang source to Elixir source with equivalent functionality. 6 | 7 | The goal is to produce correct, functioning, but not necessarily perfectly idiomatic, Elixir code. This tool may be used as a starting point to port code from Erlang to Elixir, but manual cleanup will likely be desired. 8 | 9 | This software is currently highly experimental and should be considered "pre-alpha". Some capabilities are not yet complete, and there are significant known issues, particularly in the Erlang preprocessor support. See the Caveats section for more information. 10 | 11 | ## Installing 12 | 13 | Erl2ex may be run as a mix task or as a standalone escript. 14 | 15 | ### Requirements 16 | 17 | Erl2ex recognizes Erlang source that is compatible with Erlang 18.x. The generated Elixir source requires Elixir 1.4 or later. The Erl2ex tool itself also requires Elixir 1.4 or later. 18 | 19 | ### Installing the mix task 20 | 21 | To run the mix task, first add Erl2ex as a dependency to your existing Elixir project: 22 | 23 | ```elixir 24 | def deps do 25 | [ {:erl2ex, ">= 0.0.9", only: :dev} ] 26 | end 27 | ``` 28 | 29 | After adding Erl2ex as a dependency, run `mix deps.get` followed by `mix deps.compile` to install it. An `erl2ex` task will now be available. Run `mix help erl2ex` for help. 30 | 31 | ### Building the escript 32 | 33 | To build a standalone command line application (escript), clone this repository using `git clone https://github.com/dazuma/erl2ex.git`, and then run `mix escript.build` within the cloned project. 34 | 35 | ## Usage 36 | 37 | Erl2ex may be run in three modes: 38 | 39 | * It can read an Erlang source file on stdin, and write the generated Elixir source to stdout. 40 | * It can read a specified Erlang source file (.erl) from the file system, and write the generated Elixir source file (.ex) to the same or a different specified directory. 41 | * It can search a directory for all Erlang source files (.erl), and write corresponding Elixir source files (.ex) to the same or a different specified directory. 42 | 43 | Various switches may also be provided on the command line, including the include path for searching for Erlang include files (.hrl), and other conversion settings. 44 | 45 | For detailed help, use `mix help erl2ex` for the mix task, or `erl2ex --help` for the standalone tool. 46 | 47 | ## Caveats 48 | 49 | This software is still under heavy development, and many capabilities are not yet complete. The following is a partial list of known issues and planned features. 50 | 51 | ### Known issues 52 | 53 | * Erlang exports match strings in case statements, whereas Elixir does not. Examples: the `ER` match in `elixir_bitstring:expand_bitstr/4` and the `Final` match in `elixir_interpolation:finish_extraction/5`. 54 | * Returning a remote function reference from a macro is not supported: e.g. `-define(A, m:f).` generates illegal Elixir syntax. 55 | * Function macros cannot return function names; Erlang's parser rejects the syntax `?A()()`. In Erlang, the preprocessor fixes this, but we're not running the Erlang preprocessor directly. 56 | * Record names cannot be macro results; Erlang's parser rejects the syntax `-record(?MODULE, {...}).` and `#?MODULE{...}`. (Examples in https://github.com/soranoba/bbmustache/blob/master/src/bbmustache.erl) 57 | * Fully-qualified macros cannot appear in in typespecs; Erlang's parser won't handle it. (Example in the spec for abstract_module_/2 in https://github.com/DeadZen/goldrush/blob/master/src/glc_code.erl) 58 | * Macro invocations cannot appear in comprehensions; Erlang's parser rejects it. (Example in the definition of the LOWER macro in https://github.com/ninenines/cowlib/blob/master/include/cow_inline.hrl) 59 | * Elixir reserves the function name `__info__` and won't allow its definition. (Failure example in https://github.com/elixir-lang/elixir/blob/master/lib/elixir/src/elixir_bootstrap.erl). 60 | 61 | ### Incomplete features / wishlist 62 | 63 | * Preserve comments from the Erlang source. 64 | * Generate exdoc comments, probably based on heuristics on the funtion comments. 65 | * Provide an option to elixirize module names (e.g. instead of generating `defmodule :my_erlang_module`, generate `defmodule MyErlangModule`) 66 | * Provide an option to convert variable names from camelCase to snake_case. 67 | * Provide (possibly optional) translation of include files (.hrl) to separate modules rather than copying into the including module, so the declarations can be shared after translation to Elixir. 68 | * Correct the usage of leading underscores in variable names. 69 | * Closer integration with EUnit. 70 | * Dead macro elimination, especially when inlining hrl files. 71 | * Do better at determining when a macro contains content that is allowed in guard clauses. We may be able to do away with the generated `Macro.Env.in_guard?` check. 72 | * Convert yrl files. 73 | * Fix expressions that cause scope "export" warnings in newer versions of Elixir. 74 | 75 | ## Contributing 76 | 77 | While we appreciate contributions, please note that this software is currently highly experimental, and the code is evolving very rapidly. It is recommended to contact the author before embarking on a major pull request. More detailed contribution guidelines will be provided when the software stabilizes further. 78 | 79 | The source can be found on Github at [https://github.com/dazuma/erl2ex](https://github.com/dazuma/erl2ex) 80 | 81 | ## License 82 | 83 | Copyright 2015 Daniel Azuma 84 | 85 | This software is licensed under the 3-clause BSD license. 86 | 87 | See the LICENSE.md file for more information. 88 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :erl2ex, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:erl2ex, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/erl2ex.ex: -------------------------------------------------------------------------------- 1 | # Main entry points for erl2ex. 2 | 3 | defmodule Erl2ex do 4 | 5 | @moduledoc """ 6 | Erl2ex is an Erlang to Elixir transpiler, converting well-formed Erlang 7 | source to Elixir source with equivalent functionality. 8 | 9 | The goal is to produce correct, functioning Elixir code, but not necessarily 10 | perfectly idiomatic. This tool may be used as a starting point when porting 11 | code from Erlang to Elixir, but manual cleanup will likely be desired. 12 | 13 | This module provides the main entry points into Erl2ex. 14 | """ 15 | 16 | alias Erl2ex.Results 17 | alias Erl2ex.Sink 18 | alias Erl2ex.Source 19 | 20 | alias Erl2ex.Pipeline.Analyze 21 | alias Erl2ex.Pipeline.Codegen 22 | alias Erl2ex.Pipeline.Convert 23 | alias Erl2ex.Pipeline.InlineIncludes 24 | alias Erl2ex.Pipeline.Parse 25 | 26 | 27 | @typedoc """ 28 | Options that may be provided to a conversion run. 29 | 30 | Recognized options are: 31 | * `:include_dir` Add a directory to the include path. 32 | * `:lib_dir` Specifies the directory for a named application. 33 | * `:define_prefix` Prefix added to the environment variable or config key 34 | names that are read to initialize macro definitions. Default: "DEFINE_". 35 | * `:defines_from_config` An application whose config should be used to 36 | initialize macro definitions. If not specified or set to nil, system 37 | environment variables will be used. 38 | * `:emit_file_headers` Add a header comment to each file. Default is true. 39 | * `:verbosity` Set the output verbosity level. (Default is 0, which 40 | outputs only error messages. 1 outputs basic status information, and 41 | 2 outputs debug information.) 42 | """ 43 | @type options :: [ 44 | include_dir: Path.t, 45 | lib_dir: {atom, Path.t} | %{atom => Path.t}, 46 | define_prefix: String.t, 47 | defines_from_config: atom, 48 | emit_file_headers: boolean, 49 | verbosity: integer 50 | ] 51 | 52 | 53 | @typedoc """ 54 | A file identifier, which may be a filesystem path or a symbolic id. 55 | """ 56 | 57 | @type file_id :: Path.t | atom 58 | 59 | 60 | @doc """ 61 | Converts the source for an Erlang module, represented as a string. 62 | 63 | If the conversion is successful, returns a tuple of {:ok, result}. 64 | If an error occurs, returns a tuple of {:error, error_details}. 65 | """ 66 | 67 | @spec convert_str(String.t, options) :: 68 | {:ok, String.t} | {:error, %CompileError{}} 69 | 70 | def convert_str(source_str, opts \\ []) do 71 | internal_convert_str(source_str, opts, 72 | fn(results, sink) -> 73 | case Results.get_error(results) do 74 | nil -> Sink.get_string(sink, nil) 75 | err -> {:error, err} 76 | end 77 | end) 78 | end 79 | 80 | 81 | @doc """ 82 | Converts the source for an Erlang module, represented as a string, and 83 | returns the Elixir source as a string. 84 | 85 | Raises a CompileError if an error occurs. 86 | """ 87 | 88 | @spec convert_str!(String.t, options) :: String.t 89 | 90 | def convert_str!(source_str, opts \\ []) do 91 | internal_convert_str(source_str, opts, 92 | fn(results, sink) -> 93 | Results.throw_error(results) 94 | {:ok, str} = Sink.get_string(sink, nil) 95 | str 96 | end) 97 | end 98 | 99 | 100 | defp internal_convert_str(source_str, opts, result_handler) do 101 | opts = Keyword.merge(opts, source_data: source_str) 102 | source = Source.start_link(opts) 103 | sink = Sink.start_link(allow_get: true) 104 | results_collector = Results.Collector.start_link() 105 | try do 106 | convert(source, sink, results_collector, nil, nil, opts) 107 | results = Results.Collector.get(results_collector) 108 | result_handler.(results, sink) 109 | after 110 | Source.stop(source) 111 | Sink.stop(sink) 112 | Results.Collector.stop(results_collector) 113 | end 114 | end 115 | 116 | 117 | @doc """ 118 | Converts a single Erlang source file, and writes the generated Elixir code 119 | to a new file. 120 | 121 | You must provide the relative or absolute path to the Erlang source. You may 122 | optionally provide a path to the Elixir destination. If the destination is 123 | not specified, the result will be written in the same directory as the source. 124 | 125 | Returns a results object. 126 | """ 127 | 128 | @spec convert_file(Path.t, Path.t | nil, options) :: Results.t 129 | 130 | def convert_file(source_path, dest_path \\ nil, opts \\ []) do 131 | dest_path = 132 | if dest_path == nil do 133 | "#{Path.rootname(source_path)}.ex" 134 | else 135 | dest_path 136 | end 137 | cur_dir = File.cwd! 138 | include_dirs = Keyword.get_values(opts, :include_dir) 139 | source = Source.start_link(source_dir: cur_dir, include_dirs: include_dirs) 140 | sink = Sink.start_link(dest_dir: cur_dir) 141 | results_collector = Results.Collector.start_link() 142 | try do 143 | convert(source, sink, source_path, dest_path, opts) 144 | if Keyword.get(opts, :verbosity, 0) > 0 do 145 | IO.puts(:stderr, "Converted #{source_path} -> #{dest_path}") 146 | end 147 | Results.Collector.get(results_collector) 148 | after 149 | Source.stop(source) 150 | Sink.stop(sink) 151 | Results.Collector.stop(results_collector) 152 | end 153 | end 154 | 155 | 156 | @doc """ 157 | Searches a directory for Erlang source files, and writes corresponding 158 | Elixir files for each module. 159 | 160 | By default, the Elixir files will be written in the same directories as the 161 | Erlang source files. You may optionally provide a different base directory 162 | for the destination files. 163 | 164 | Returns a results object. 165 | """ 166 | 167 | @spec convert_dir(Path.t, Path.t | nil, options) :: Results.t 168 | 169 | def convert_dir(source_dir, dest_dir \\ nil, opts \\ []) do 170 | dest_dir = if dest_dir == nil, do: source_dir, else: dest_dir 171 | source = opts 172 | |> Keyword.put(:source_dir, source_dir) 173 | |> Source.start_link 174 | sink = Sink.start_link(dest_dir: dest_dir) 175 | results_collector = Results.Collector.start_link() 176 | try do 177 | "#{source_dir}/**/*.erl" 178 | |> Path.wildcard 179 | |> Enum.each(fn source_full_path -> 180 | source_rel_path = Path.relative_to(source_full_path, source_dir) 181 | dest_rel_path = "#{Path.rootname(source_rel_path)}.ex" 182 | dest_full_path = Path.join(dest_dir, dest_rel_path) 183 | convert(source, sink, results_collector, source_rel_path, dest_rel_path, opts) 184 | if Keyword.get(opts, :verbosity, 0) > 0 do 185 | IO.puts(:stderr, "Converted #{source_full_path} -> #{dest_full_path}") 186 | end 187 | end) 188 | Results.Collector.get(results_collector) 189 | after 190 | Source.stop(source) 191 | Sink.stop(sink) 192 | Results.Collector.stop(results_collector) 193 | end 194 | end 195 | 196 | 197 | @doc """ 198 | Given a source and a sink, and the source path for one Erlang source file, 199 | converts to Elixir and writes the result to the sink at the given destination 200 | path. Writes the result to the given results collector. Returns :ok. 201 | """ 202 | 203 | @spec convert(Source.t, Sink.t, Results.Collector.t, Erl2ex.file_id, Erl2ex.file_id, options) :: :ok 204 | 205 | def convert(source, sink, results_collector, source_path, dest_path, opts \\ []) do 206 | {source_str, actual_source_path} = Source.read_source(source, source_path) 207 | opts = 208 | if actual_source_path == nil do 209 | opts 210 | else 211 | [{:cur_file_path, actual_source_path} | opts] 212 | end 213 | try do 214 | str = source_str 215 | |> Parse.string(opts) 216 | |> InlineIncludes.process(source, actual_source_path) 217 | |> Analyze.forms(opts) 218 | |> Convert.module(opts) 219 | |> Codegen.to_str(opts) 220 | :ok = Sink.write(sink, dest_path, str) 221 | :ok = Results.Collector.put_success(results_collector, source_path, dest_path) 222 | rescue 223 | error in CompileError -> 224 | :ok = Results.Collector.put_error(results_collector, source_path, error) 225 | end 226 | :ok 227 | end 228 | 229 | end 230 | -------------------------------------------------------------------------------- /lib/erl2ex/cli.ex: -------------------------------------------------------------------------------- 1 | # CLI implementation for erl2ex 2 | 3 | defmodule Erl2ex.Cli do 4 | 5 | @moduledoc """ 6 | This module provides the command line interface for the erl2ex binary and 7 | the mix erl2ex task. 8 | """ 9 | 10 | 11 | alias Erl2ex.Results 12 | 13 | 14 | @doc """ 15 | Runs the erl2ex binary, given a set of command line arguments. 16 | Returns the OS result code, which is 0 for success or nonzero for failure. 17 | """ 18 | 19 | @spec run([String.t]) :: non_neg_integer 20 | 21 | def run(argv) do 22 | {options, args, errors} = 23 | OptionParser.parse(argv, 24 | strict: [ 25 | output: :string, 26 | include_dir: [:string, :keep], 27 | lib_dir: [:string, :keep], 28 | emit_file_headers: :boolean, 29 | define_prefix: :string, 30 | defines_from_config: :string, 31 | verbose: [:boolean, :keep], 32 | help: :boolean 33 | ], 34 | aliases: [ 35 | v: :verbose, 36 | "?": :help, 37 | o: :output, 38 | "I": :include_dir 39 | ] 40 | ) 41 | 42 | {options, errors2} = decode_options(options) 43 | errors = errors ++ errors2 44 | 45 | cond do 46 | not Enum.empty?(errors) -> 47 | display_errors(errors) 48 | Keyword.get(options, :help) -> 49 | display_help() 50 | true -> 51 | run_conversion(args, options) 52 | end 53 | end 54 | 55 | 56 | @doc """ 57 | Runs the erl2ex binary, given a set of command line arguments. 58 | Does not return. Instead, halts the VM on completion with the appropriate 59 | OS result code. 60 | """ 61 | 62 | @spec main([String.t]) :: no_return 63 | 64 | def main(argv) do 65 | argv 66 | |> run 67 | |> System.halt 68 | end 69 | 70 | 71 | @doc """ 72 | Returns the usage documentation for the command line. 73 | 74 | The argument specifies how to invoke the binary (e.g. "erl2ex" or 75 | "mix erl2ex"). The returned text is in markdown form, but may be rendered 76 | as plain text as well. 77 | """ 78 | 79 | @spec usage_text(String.t) :: String.t 80 | 81 | def usage_text(invocation \\ "erl2ex") do 82 | """ 83 | Usage: `#{invocation} [options] [input path]` 84 | 85 | Command line options: 86 | --output, -o "path" (Set the output file or directory path) 87 | --include-dir, -I "dir" (Add a directory to the include path) 88 | --[no-]emit-file-headers (Emit a header comment in each file) 89 | --define-prefix "prefix" (Prefix for variables used to define macros) 90 | --defines-from-config "app" (Define macros from this application's config) 91 | --verbose, -v (Display verbose status) 92 | --help, -? (Display help text) 93 | 94 | erl2ex is a Erlang to Elixir transpiler. 95 | 96 | When no input path is provided, erl2ex reads from stdin and writes to 97 | stdout. Any output path is ignored. 98 | 99 | When the input path is a file, erl2ex reads from the file and writes to 100 | the specified output path. If no output path is present, erl2ex creates 101 | an output file in the same directory as the input file. 102 | 103 | When the input path is a directory, erl2ex recursively searches the 104 | directory and reads from every Erlang (*.erl) file it finds. It writes 105 | the results in the same directory structure under the given output path, 106 | which must also be a directory. If no output path is provided, the 107 | results are written in the same directories as the input files. 108 | """ 109 | end 110 | 111 | 112 | defp decode_options(options) do 113 | verbose_count = options 114 | |> Keyword.get_values(:verbose) 115 | |> Enum.count 116 | {lib_dirs, errors} = options 117 | |> Keyword.get_values(:lib_dir) 118 | |> Enum.reduce({%{}, []}, fn (str, {map, errs}) -> 119 | case Regex.run(~r{^([^=]+)=([^=]+)$}, str) do 120 | nil -> 121 | {map, [{:lib_dir, str} | errs]} 122 | [_, key, val] -> 123 | {Map.put(map, String.to_atom(key), val), errs} 124 | end 125 | end) 126 | 127 | options = options 128 | |> Keyword.put(:verbosity, verbose_count) 129 | |> Keyword.put(:lib_dir, lib_dirs) 130 | {options, errors} 131 | end 132 | 133 | 134 | defp run_conversion([], options) do 135 | :all 136 | |> IO.read 137 | |> Erl2ex.convert_str!(options) 138 | |> IO.write 139 | 0 140 | end 141 | 142 | defp run_conversion([path], options) do 143 | output = Keyword.get(options, :output) 144 | cond do 145 | File.dir?(path) -> 146 | result = Erl2ex.convert_dir(path, output, options) 147 | handle_result(result) 148 | File.regular?(path) -> 149 | result = Erl2ex.convert_file(path, output, options) 150 | handle_result(result) 151 | true -> 152 | IO.puts(:stderr, "Could not find input: #{path}") 153 | 1 154 | end 155 | end 156 | 157 | defp run_conversion(paths, _) do 158 | IO.puts(:stderr, "Got too many input paths: #{inspect(paths)}\n") 159 | display_help() 160 | 1 161 | end 162 | 163 | 164 | defp handle_result(results) do 165 | error = Results.get_error(results) 166 | if error == nil do 167 | 0 168 | else 169 | IO.puts(:stderr, "Error converting #{error.file}, line #{error.line}: #{error.description}") 170 | 1 171 | end 172 | end 173 | 174 | 175 | defp display_errors(errors) do 176 | Enum.each(errors, fn 177 | {switch, val} -> 178 | IO.puts(:stderr, "Unrecognized or malformed switch: #{switch}=#{val}") 179 | end) 180 | IO.puts(:stderr, "") 181 | display_help() 182 | 1 183 | end 184 | 185 | 186 | defp display_help do 187 | IO.write(:stderr, usage_text("erl2ex")) 188 | end 189 | 190 | end 191 | -------------------------------------------------------------------------------- /lib/erl2ex/convert/erl_forms.ex: -------------------------------------------------------------------------------- 1 | # Conversion logic for standard (erlparse) AST forms. 2 | 3 | defmodule Erl2ex.Convert.ErlForms do 4 | 5 | @moduledoc false 6 | 7 | 8 | alias Erl2ex.Pipeline.ExAttr 9 | alias Erl2ex.Pipeline.ExClause 10 | alias Erl2ex.Pipeline.ExComment 11 | alias Erl2ex.Pipeline.ExDirective 12 | alias Erl2ex.Pipeline.ExFunc 13 | alias Erl2ex.Pipeline.ExImport 14 | alias Erl2ex.Pipeline.ExMacro 15 | alias Erl2ex.Pipeline.ExRecord 16 | alias Erl2ex.Pipeline.ExSpec 17 | alias Erl2ex.Pipeline.ExType 18 | 19 | alias Erl2ex.Convert.Context 20 | alias Erl2ex.Convert.ErlExpressions 21 | 22 | alias Erl2ex.Pipeline.ModuleData 23 | alias Erl2ex.Pipeline.Names 24 | 25 | 26 | # A list of attributes that are automatically registered in Elixir and 27 | # do not need to be registered explicitly. 28 | @auto_registered_attrs [:vsn, :compile, :on_load, :behaviour, :behavior] 29 | 30 | 31 | # A dispatching function that converts a form with a context. Returns a 32 | # tuple of a list (possibly empty) of ex_data forms, and an updated context. 33 | 34 | # Handler for function definitions. 35 | def conv_form({:function, _line, name, arity, clauses}, context) do 36 | conv_function_form(name, arity, clauses, context) 37 | end 38 | 39 | # Handler for attribute definitions that are ignored by the converter because 40 | # they are fully handled by earlier phases. 41 | def conv_form({:attribute, _line, attr_name, _}, context) 42 | when attr_name == :export or attr_name == :export_type or attr_name == :module or 43 | attr_name == :include or attr_name == :include_lib do 44 | {[], context} 45 | end 46 | 47 | # Handler for import directives. 48 | def conv_form({:attribute, _line, :import, {modname, funcs}}, context) do 49 | conv_import_form(modname, funcs, context) 50 | end 51 | 52 | # Handler for type definition directives. 53 | def conv_form({:attribute, _line, attr_name, {name, defn, params}}, context) 54 | when attr_name == :type or attr_name == :opaque do 55 | conv_type_form(attr_name, name, defn, params, context) 56 | end 57 | 58 | # Handler for local function specification directives. 59 | def conv_form({:attribute, _line, attr_name, {{name, _}, clauses}}, context) 60 | when attr_name == :spec or attr_name == :callback do 61 | conv_spec_form(attr_name, {}, name, clauses, context) 62 | end 63 | 64 | # Handler for remote function specification directives. 65 | def conv_form({:attribute, _line, :spec, {{spec_mod, name, _}, clauses}}, context) do 66 | conv_spec_form(:spec, spec_mod, name, clauses, context) 67 | end 68 | 69 | # Handler for record definition directives. 70 | def conv_form({:attribute, _line, :record, {recname, fields}}, context) do 71 | conv_record_form(recname, fields, context) 72 | end 73 | 74 | # Handler for file/line state directives. 75 | def conv_form({:attribute, _line, :file, {file, fline}}, context) do 76 | conv_file_form(file, fline, context) 77 | end 78 | 79 | # Handler for file/line state directives. 80 | def conv_form({:attribute, _line, :compile, arg}, context) do 81 | conv_compile_directive_form(arg, context) 82 | end 83 | 84 | # Handler for "else" and "endif" directives (i.e. with no arguments) 85 | def conv_form({:attribute, _line, attr_name}, context) 86 | when attr_name == :else or attr_name == :endif do 87 | conv_directive_form(attr_name, {}, context) 88 | end 89 | 90 | # Handler for "ifdef", "ifndef", and "undef" directives (i.e. with one argument) 91 | def conv_form({:attribute, _line, attr_name, arg}, context) 92 | when attr_name == :ifdef or attr_name == :ifndef or attr_name == :undef do 93 | conv_directive_form(attr_name, arg, context) 94 | end 95 | 96 | # Handler for attributes not otherwise recognized as special. 97 | def conv_form({:attribute, _line, attr_name, arg}, context) do 98 | conv_attr_form(attr_name, arg, context) 99 | end 100 | 101 | # Handler for Erlang macro definitions. 102 | def conv_form({:define, _line, macro, replacement}, context) do 103 | conv_define_form(macro, replacement, context) 104 | end 105 | 106 | # Fall-through handler that throws an error for unrecognized form type. 107 | def conv_form(erl_form, context) do 108 | line = if is_tuple(erl_form) and tuple_size(erl_form) >= 3, do: elem(erl_form, 1), else: :unknown 109 | raise CompileError, 110 | file: Context.cur_file_path_for_display(context), 111 | line: line, 112 | description: "Unrecognized Erlang form ast: #{inspect(erl_form)}" 113 | end 114 | 115 | 116 | #### Converts the given function. 117 | 118 | defp conv_function_form(name, arity, clauses, context) do 119 | module_data = context.module_data 120 | mapped_name = ModuleData.local_function_name(module_data, name) 121 | is_exported = ModuleData.is_exported?(module_data, name, arity) 122 | ex_clauses = Enum.map(clauses, &(conv_clause(context, &1, mapped_name))) 123 | 124 | ex_func = %ExFunc{ 125 | name: mapped_name, 126 | arity: arity, 127 | public: is_exported, 128 | clauses: ex_clauses 129 | } 130 | {[ex_func], context} 131 | end 132 | 133 | 134 | # Converts a single clause in a function definition 135 | 136 | defp conv_clause(context, {:clause, _line, args, guards, exprs} = clause, name) do 137 | context = context 138 | |> Context.set_variable_maps(clause) 139 | |> Context.push_scope() 140 | {ex_signature, context} = clause_signature(name, args, guards, context) 141 | {ex_exprs, _} = ErlExpressions.conv_list(exprs, context) 142 | 143 | %ExClause{ 144 | signature: ex_signature, 145 | exprs: ex_exprs 146 | } 147 | end 148 | 149 | 150 | # Converts the signature in a function clause. 151 | 152 | # This function handle the case without guards 153 | defp clause_signature(name, params, [], context) do 154 | context = Context.push_match_level(context, true) 155 | {ex_params, context} = ErlExpressions.conv_list(params, context) 156 | context = Context.pop_match_level(context) 157 | name = 158 | if Names.deffable_function_name?(name) do 159 | name 160 | else 161 | {:unquote, [], [name]} 162 | end 163 | {{name, [], ex_params}, context} 164 | end 165 | 166 | # This function handle the case with guards 167 | defp clause_signature(name, params, guards, context) do 168 | {ex_guards, context} = ErlExpressions.guard_seq(guards, context) 169 | {sig_without_guards, context} = clause_signature(name, params, [], context) 170 | {{:when, [], [sig_without_guards | ex_guards]}, context} 171 | end 172 | 173 | 174 | #### Converts the given import directive. 175 | 176 | defp conv_import_form(modname, funcs, context) do 177 | ex_import = %ExImport{ 178 | module: modname, 179 | funcs: funcs 180 | } 181 | {[ex_import], context} 182 | end 183 | 184 | 185 | #### Converts the given type definition directive. 186 | 187 | defp conv_type_form(attr_name, name, defn, params, context) do 188 | ex_kind = cond do 189 | attr_name == :opaque -> 190 | :opaque 191 | ModuleData.is_type_exported?(context.module_data, name, Enum.count(params)) -> 192 | :type 193 | true -> 194 | :typep 195 | end 196 | 197 | type_context = context 198 | |> Context.set_variable_maps(params) 199 | |> Context.set_type_expr_mode() 200 | {ex_params, _} = ErlExpressions.conv_list(params, type_context) 201 | {ex_defn, _} = ErlExpressions.conv_expr(defn, type_context) 202 | 203 | ex_type = %ExType{ 204 | kind: ex_kind, 205 | signature: {name, [], ex_params}, 206 | defn: ex_defn 207 | } 208 | {[ex_type], context} 209 | end 210 | 211 | 212 | #### Converts the given function specification directive. 213 | # The mod_name argument is nil if local, or the module if remote. 214 | # For a remote function, emits something only if the module matches the 215 | # current module being defined. 216 | # Breaks the spec into clauses and calls conv_spec_clause on each. 217 | 218 | defp conv_spec_form(attr_name, mod_name, name, clauses, context) do 219 | if mod_name == {} or mod_name == context.module_data.name do 220 | name = 221 | if ModuleData.has_local_function_name?(context.module_data, name) do 222 | ModuleData.local_function_name(context.module_data, name) 223 | else 224 | name 225 | end 226 | specs = clauses |> Enum.map(fn spec_clause -> 227 | {ex_spec_expr, _} = conv_spec_clause(name, spec_clause, context) 228 | ex_spec_expr 229 | end) 230 | ex_spec = %ExSpec{ 231 | kind: attr_name, 232 | name: name, 233 | specs: specs 234 | } 235 | {[ex_spec], context} 236 | else 237 | {[], context} 238 | end 239 | end 240 | 241 | 242 | # Converts a function specification clause without guards 243 | defp conv_spec_clause(name, {:type, _, :fun, [args, result]}, context) do 244 | conv_spec_clause_impl(name, args, result, [], context) 245 | end 246 | 247 | # Converts a function specification clause with guards 248 | defp conv_spec_clause(name, {:type, _, :bounded_fun, [{:type, _, :fun, [args, result]}, constraints]}, context) do 249 | conv_spec_clause_impl(name, args, result, constraints, context) 250 | end 251 | 252 | defp conv_spec_clause(name, expr, context), do: 253 | Context.handle_error(context, expr, "in spec for #{name}") 254 | 255 | 256 | # Convert a single function specification clause. 257 | 258 | defp conv_spec_clause_impl(name, args, result, constraints, context) do 259 | context = context 260 | |> Context.set_type_expr_mode() 261 | |> Context.set_variable_maps([args, result, constraints]) 262 | 263 | {ex_args, context} = ErlExpressions.conv_expr(args, context) 264 | {ex_result, context} = ErlExpressions.conv_expr(result, context) 265 | ex_expr = {:::, [], [{name, [], ex_args}, ex_result]} 266 | 267 | ex_constraints = Enum.map(constraints, &(conv_spec_constraint(context, name, &1))) 268 | ex_constraints = context 269 | |> Context.get_variable_map() 270 | |> Map.values() 271 | |> Enum.sort() 272 | |> Enum.reduce(ex_constraints, fn mapped_var, cur_constraints -> 273 | if Keyword.has_key?(cur_constraints, mapped_var) do 274 | cur_constraints 275 | else 276 | cur_constraints ++ [{mapped_var, {:any, [], []}}] 277 | end 278 | end) 279 | 280 | ex_expr = 281 | if Enum.empty?(ex_constraints) do 282 | ex_expr 283 | else 284 | {:when, [], [ex_expr, ex_constraints]} 285 | end 286 | {ex_expr, context} 287 | end 288 | 289 | 290 | # Convert a single constraint in a function specification 291 | 292 | defp conv_spec_constraint(context, _name, {:type, _, :constraint, [{:atom, _, :is_subtype}, [{:var, _, var}, type]]}) do 293 | {ex_type, _} = ErlExpressions.conv_expr(type, context) 294 | {:normal_var, mapped_name, _, _} = Context.map_variable_name(context, var) 295 | {mapped_name, ex_type} 296 | end 297 | 298 | defp conv_spec_constraint(context, name, expr), do: 299 | Context.handle_error(context, expr, "in spec constraint for #{name}") 300 | 301 | 302 | #### Converts the given record definition directive. 303 | 304 | defp conv_record_form(recname, fields, context) do 305 | context = Context.start_record_types(context) 306 | {ex_fields, context} = ErlExpressions.conv_record_def_list(fields, context) 307 | context = Context.finish_record_types(context, recname) 308 | 309 | ex_record = %ExRecord{ 310 | tag: recname, 311 | macro: ModuleData.record_function_name(context.module_data, recname), 312 | data_attr: ModuleData.record_data_attr_name(context.module_data, recname), 313 | fields: ex_fields 314 | } 315 | {[ex_record], context} 316 | end 317 | 318 | 319 | #### Converts the given file/line state directive. 320 | 321 | defp conv_file_form(file, fline, context) do 322 | comment = convert_comments(["% File #{file |> List.to_string |> inspect} Line #{fline}"]) 323 | ex_comment = %ExComment{comments: comment} 324 | {[ex_comment], context} 325 | end 326 | 327 | 328 | # Given a list of comment data, returns a list of Elixir comment strings. 329 | 330 | defp convert_comments(comments) do 331 | comments |> Enum.map(fn 332 | {:comment, _, str} -> str |> List.to_string |> convert_comment_str 333 | str when is_binary(str) -> convert_comment_str(str) 334 | end) 335 | end 336 | 337 | 338 | # Coverts an Erlang comment string to an Elixir comment string. i.e. 339 | # it changes the % delimiter to #. 340 | 341 | defp convert_comment_str(str) do 342 | Regex.replace(~r{^%+}, str, fn prefix -> String.replace(prefix, "%", "#") end) 343 | end 344 | 345 | 346 | #### Converts the given preprocessor control directive. 347 | 348 | defp conv_directive_form(directive, name, context) do 349 | tracking_name = if name == {} do 350 | nil 351 | else 352 | ModuleData.tracking_attr_name(context.module_data, interpret_macro_name(name)) 353 | end 354 | 355 | ex_directive = %ExDirective{ 356 | directive: directive, 357 | name: tracking_name 358 | } 359 | {[ex_directive], context} 360 | end 361 | 362 | 363 | #### Converts the given compile options directive. 364 | 365 | defp conv_compile_directive_form(args, context) do 366 | case conv_compile_option(args, context) do 367 | [] -> {[], context} 368 | [arg] -> conv_attr_form(:compile, arg, context) 369 | arg_list -> conv_attr_form(:compile, arg_list, context) 370 | end 371 | end 372 | 373 | 374 | # Does some transforms on the compile options. 375 | # For inline options, changes function names to their Elixir counterparts. 376 | # Removes nowarn_unused_function because it doesn't work in Elixir. 377 | 378 | defp conv_compile_option(options, context) when is_list(options) do 379 | Enum.flat_map(options, &(conv_compile_option(&1, context))) 380 | end 381 | 382 | defp conv_compile_option({:inline, {name, arity}}, context) do 383 | mapped_name = ModuleData.local_function_name(context.module_data, name) 384 | [{:inline, {mapped_name, arity}}] 385 | end 386 | 387 | defp conv_compile_option({:inline, funcs}, context) when is_list(funcs) do 388 | module_data = context.module_data 389 | mapped_funcs = funcs 390 | |> Enum.map(fn {name, arity} -> 391 | {ModuleData.local_function_name(module_data, name), arity} 392 | end) 393 | [{:inline, mapped_funcs}] 394 | end 395 | 396 | defp conv_compile_option({:nowarn_unused_function, _funcs}, _context) do 397 | [] 398 | end 399 | 400 | defp conv_compile_option(option, _context) do 401 | [option] 402 | end 403 | 404 | 405 | #### Converts the given attribute definition directive. 406 | 407 | defp conv_attr_form(name, arg, context) do 408 | {name, arg} = conv_attr(name, arg) 409 | register = not(name in @auto_registered_attrs) 410 | 411 | ex_attr = %ExAttr{ 412 | name: name, 413 | register: register, 414 | arg: arg 415 | } 416 | {[ex_attr], context} 417 | end 418 | 419 | 420 | # Maps a few well-known attributes to Elixir equivalents. 421 | 422 | defp conv_attr(:on_load, {name, 0}), do: {:on_load, name} 423 | defp conv_attr(:behavior, behaviour), do: {:behaviour, behaviour} 424 | defp conv_attr(attr, val), do: {attr, val} 425 | 426 | 427 | #### Converts the given macro definition directive. 428 | 429 | defp conv_define_form(macro, replacement, context) do 430 | {name, args} = interpret_macro_expr(macro) 431 | arity = if args == nil, do: nil, else: Enum.count(args) 432 | args = if args == nil, do: [], else: args 433 | module_data = context.module_data 434 | needs_dispatch = ModuleData.macro_needs_dispatch?(module_data, name) 435 | macro_name = ModuleData.macro_function_name(module_data, name, arity) 436 | mapped_name = macro_name 437 | dispatch_name = nil 438 | {{mapped_name, context}, dispatch_name} = 439 | if needs_dispatch do 440 | {Context.generate_macro_name(context, name, arity), macro_name} 441 | else 442 | {{mapped_name, context}, dispatch_name} 443 | end 444 | tracking_name = ModuleData.tracking_attr_name(module_data, name) 445 | 446 | context = context 447 | |> Context.set_variable_maps(replacement, args) 448 | |> Context.push_scope 449 | |> Context.start_macro_export_collection(args) 450 | 451 | variable_map = Context.get_variable_map(context) 452 | ex_args = args 453 | |> Enum.map(fn arg -> 454 | {Map.fetch!(variable_map, arg), [], Elixir} 455 | end) 456 | 457 | {normal_expr, guard_expr, context} = ErlExpressions.conv_macro_expr(replacement, context) 458 | ex_macro = %ExMacro{ 459 | macro_name: mapped_name, 460 | signature: {mapped_name, [], ex_args}, 461 | tracking_name: tracking_name, 462 | dispatch_name: dispatch_name, 463 | stringifications: context.stringification_map, 464 | expr: normal_expr, 465 | guard_expr: guard_expr 466 | } 467 | context = context 468 | |> Context.finish_macro_export_collection(name, arity) 469 | |> Context.pop_scope 470 | |> Context.clear_variable_maps 471 | 472 | {[ex_macro], context} 473 | end 474 | 475 | 476 | # Interprets the macro call sequence. 477 | 478 | defp interpret_macro_expr({:call, _, name_expr, arg_exprs}) do 479 | name = interpret_macro_name(name_expr) 480 | args = arg_exprs |> Enum.map(fn {:var, _, n} -> n end) 481 | {name, args} 482 | end 483 | 484 | defp interpret_macro_expr(macro_expr) do 485 | name = interpret_macro_name(macro_expr) 486 | {name, nil} 487 | end 488 | 489 | 490 | # Interprets a macro name. It may be a var or atom in the parse tree because 491 | # it may be capitalized or not. 492 | 493 | defp interpret_macro_name({:var, _, name}), do: name 494 | defp interpret_macro_name({:atom, _, name}), do: name 495 | defp interpret_macro_name(name) when is_atom(name), do: name 496 | 497 | end 498 | -------------------------------------------------------------------------------- /lib/erl2ex/convert/ext_forms.ex: -------------------------------------------------------------------------------- 1 | # Conversion logic for extended (epp_dodger) AST forms. 2 | 3 | defmodule Erl2ex.Convert.ExtForms do 4 | 5 | @moduledoc false 6 | 7 | 8 | alias Erl2ex.Pipeline.ExComment 9 | 10 | 11 | # A dispatching function that converts a form with a context. Returns a 12 | # tuple of a list (possibly empty) of ex_data forms, and an updated context. 13 | 14 | # This clause converts a comment form to an ExComment 15 | def conv_form(:comment, ext_form, context) do 16 | comments = ext_form 17 | |> :erl_syntax.comment_text 18 | |> Enum.map(&List.to_string/1) 19 | |> convert_comments 20 | ex_comment = %ExComment{comments: comments} 21 | {[ex_comment], context} 22 | end 23 | 24 | # This clause handles all other form types, and does not emit anything. 25 | def conv_form(_, _, context) do 26 | {[], context} 27 | end 28 | 29 | 30 | # Given a list of comment data, returns a list of Elixir comment strings. 31 | 32 | defp convert_comments(comments) do 33 | comments |> Enum.map(fn 34 | {:comment, _, str} -> str |> List.to_string |> convert_comment_str 35 | str when is_binary(str) -> convert_comment_str(str) 36 | end) 37 | end 38 | 39 | 40 | # Coverts an Erlang comment string to an Elixir comment string. i.e. 41 | # it changes the % delimiter to #. 42 | 43 | defp convert_comment_str(str) do 44 | Regex.replace(~r{^%+}, str, fn prefix -> String.replace(prefix, "%", "#") end) 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /lib/erl2ex/convert/headers.ex: -------------------------------------------------------------------------------- 1 | # Logic to determine what should appear in the "header" of the Elixir module. 2 | 3 | defmodule Erl2ex.Convert.Headers do 4 | 5 | @moduledoc false 6 | 7 | 8 | alias Erl2ex.Pipeline.ExAttr 9 | alias Erl2ex.Pipeline.ExClause 10 | alias Erl2ex.Pipeline.ExFunc 11 | alias Erl2ex.Pipeline.ExHeader 12 | alias Erl2ex.Pipeline.ExMacro 13 | 14 | alias Erl2ex.Pipeline.ModuleData 15 | 16 | 17 | # Builds an ExHeader structure specifying what should go in the Elixir module 18 | # header. 19 | 20 | def build_header(module_data, forms) do 21 | header = forms 22 | |> Enum.reduce(%ExHeader{}, &header_check_form/2) 23 | %ExHeader{header | 24 | records: ModuleData.map_records(module_data, fn(name, fields) -> {name, fields} end), 25 | has_is_record: module_data.has_is_record, 26 | init_macros: ModuleData.macros_that_need_init(module_data), 27 | macro_dispatcher: ModuleData.macro_dispatcher_name(module_data), 28 | record_size_macro: ModuleData.record_size_macro(module_data), 29 | record_index_macro: ModuleData.record_index_macro(module_data), 30 | } 31 | end 32 | 33 | 34 | # Dispatcher called during reduction over forms. 35 | 36 | defp header_check_form(%ExFunc{clauses: clauses}, header), do: 37 | clauses |> Enum.reduce(header, &header_check_clause/2) 38 | defp header_check_form(%ExMacro{expr: expr}, header), do: 39 | header_check_expr(expr, header) 40 | defp header_check_form(%ExAttr{arg: arg}, header), do: 41 | header_check_expr(arg, header) 42 | defp header_check_form(_form, header), do: header 43 | 44 | 45 | defp header_check_clause(%ExClause{exprs: exprs}, header), do: 46 | exprs |> Enum.reduce(header, &header_check_expr/2) 47 | 48 | 49 | # Searches expressions looking for uses of Bitwise operator, to determine 50 | # whether we need to use Bitwise. 51 | 52 | defp header_check_expr(expr, header) when is_tuple(expr) and tuple_size(expr) == 2, do: 53 | header_check_expr(elem(expr, 1), header) 54 | 55 | defp header_check_expr(expr, header) when is_tuple(expr) and tuple_size(expr) >= 3 do 56 | imported = expr |> elem(1) |> Keyword.get(:import, nil) 57 | header = 58 | if imported == Bitwise do 59 | %ExHeader{header | use_bitwise: true} 60 | else 61 | header 62 | end 63 | expr 64 | |> Tuple.to_list 65 | |> Enum.reduce(header, &header_check_expr/2) 66 | end 67 | 68 | defp header_check_expr(expr, header) when is_list(expr), do: 69 | expr |> Enum.reduce(header, &header_check_expr/2) 70 | 71 | defp header_check_expr(_expr, header), do: header 72 | 73 | end 74 | -------------------------------------------------------------------------------- /lib/erl2ex/pipeline/codegen.ex: -------------------------------------------------------------------------------- 1 | # This is the final stage of the pipeline, after conversion. 2 | # It takes as input a data structure as defined in ex_data.ex that includes 3 | # Elixir AST and some accompanying metadata, and generates Elixir source code. 4 | 5 | defmodule Erl2ex.Pipeline.Codegen do 6 | 7 | @moduledoc false 8 | 9 | alias Erl2ex.Pipeline.ExAttr 10 | alias Erl2ex.Pipeline.ExComment 11 | alias Erl2ex.Pipeline.ExDirective 12 | alias Erl2ex.Pipeline.ExFunc 13 | alias Erl2ex.Pipeline.ExHeader 14 | alias Erl2ex.Pipeline.ExImport 15 | alias Erl2ex.Pipeline.ExMacro 16 | alias Erl2ex.Pipeline.ExModule 17 | alias Erl2ex.Pipeline.ExRecord 18 | alias Erl2ex.Pipeline.ExSpec 19 | alias Erl2ex.Pipeline.ExType 20 | 21 | 22 | # Generate code and write to the given IO. 23 | 24 | def to_io(ex_module, io, opts \\ []) do 25 | opts 26 | |> build_context 27 | |> write_module(ex_module, io) 28 | :ok 29 | end 30 | 31 | 32 | # Generate code and return a generated string. 33 | 34 | def to_str(ex_module, opts \\ []) do 35 | {:ok, io} = StringIO.open("") 36 | to_io(ex_module, io, opts) 37 | {:ok, {_, str}} = StringIO.close(io) 38 | str 39 | end 40 | 41 | 42 | # An internal codegen context structure used to decide indentation and 43 | # vertical whitespace. 44 | 45 | defmodule Context do 46 | @moduledoc false 47 | defstruct( 48 | # The current indentation level (1 level = 2 spaces) 49 | indent: 0, 50 | # The kind of text last generated. 51 | last_form: :start, 52 | # A prefix string for environment variables used to define macros. 53 | define_prefix: "", 54 | # A string to pass to Application.get_env, or nil to use System.get_env. 55 | defines_from_config: nil 56 | ) 57 | end 58 | 59 | 60 | # Initializes the context struct given options. 61 | 62 | defp build_context(opts) do 63 | defines_from_config = Keyword.get(opts, :defines_from_config, nil) 64 | defines_from_config = 65 | if is_binary(defines_from_config) do 66 | String.to_atom(defines_from_config) 67 | else 68 | defines_from_config 69 | end 70 | %Context{ 71 | define_prefix: Keyword.get(opts, :define_prefix, "DEFINE_"), 72 | defines_from_config: defines_from_config 73 | } 74 | end 75 | 76 | 77 | # Update the indent level. 78 | 79 | def increment_indent(context) do 80 | %Context{context | indent: context.indent + 1} 81 | end 82 | 83 | def decrement_indent(context) do 84 | %Context{context | indent: context.indent - 1} 85 | end 86 | 87 | 88 | # Writes an Elixir module to the given IO, in the given context. 89 | 90 | # This version is used when there is no module name. We write the forms but 91 | # no defmodule. 92 | defp write_module(context, %ExModule{name: nil, forms: forms, file_comments: file_comments}, io) do 93 | context 94 | |> write_comment_list(file_comments, :structure_comments, io) 95 | |> foreach(forms, io, &write_form/3) 96 | end 97 | 98 | # This version is used when there is a module name. We write the entire 99 | # module structure including defmodule. 100 | defp write_module(context, %ExModule{name: name, forms: forms, file_comments: file_comments, comments: comments}, io) do 101 | context 102 | |> write_comment_list(file_comments, :structure_comments, io) 103 | |> write_comment_list(comments, :module_comments, io) 104 | |> skip_lines(:module_begin, io) 105 | |> write_string("defmodule :#{to_string(name)} do", io) 106 | |> increment_indent 107 | |> foreach(forms, io, &write_form/3) 108 | |> decrement_indent 109 | |> skip_lines(:module_end, io) 110 | |> write_string("end", io) 111 | end 112 | 113 | 114 | # Dispatcher for writing the Elixir equivalent of an Erlang form. 115 | 116 | # This version writes the module header pseudo-form, which includes a 117 | # bunch of macros and other prelude code that may be auto-generated by 118 | # the transpiler. 119 | defp write_form(context, header = %ExHeader{}, io) do 120 | context = 121 | if header.use_bitwise do 122 | context 123 | |> skip_lines(:attr, io) 124 | |> write_string("use Bitwise, only_operators: true", io) 125 | else 126 | context 127 | end 128 | context = context 129 | |> foreach(header.init_macros, fn(ctx, {name, defined_name}) -> 130 | ctx = ctx |> skip_lines(:attr, io) 131 | env_name = ctx.define_prefix <> to_string(name) 132 | get_env_syntax = if ctx.defines_from_config do 133 | "Application.get_env(#{inspect(ctx.defines_from_config)}, #{env_name |> String.to_atom |> inspect})" 134 | else 135 | "System.get_env(#{inspect(env_name)})" 136 | end 137 | ctx |> write_string("@#{defined_name} #{get_env_syntax} != nil", io) 138 | end) 139 | context = 140 | if not Enum.empty?(header.records) or header.has_is_record do 141 | context2 = context 142 | |> skip_lines(:attr, io) 143 | |> write_string("require Record", io) 144 | context2 = 145 | if header.record_size_macro != nil do 146 | context2 147 | |> skip_lines(:attr, io) 148 | |> write_string("defmacrop #{header.record_size_macro}(data_attr) do", io) 149 | |> increment_indent 150 | |> write_string("__MODULE__ |> Module.get_attribute(data_attr) |> Enum.count |> +(1)", io) 151 | |> decrement_indent 152 | |> write_string("end", io) 153 | else 154 | context2 155 | end 156 | if header.record_index_macro != nil do 157 | context2 158 | |> skip_lines(:attr, io) 159 | |> write_string("defmacrop #{header.record_index_macro}(data_attr, field) do", io) 160 | |> increment_indent 161 | |> write_string("index = __MODULE__ |> Module.get_attribute(data_attr) |> Enum.find_index(&(&1 ==field))", io) 162 | |> write_string("if index == nil, do: 0, else: index + 1", io) 163 | |> decrement_indent 164 | |> write_string("end", io) 165 | else 166 | context2 167 | end 168 | else 169 | context 170 | end 171 | if header.macro_dispatcher != nil do 172 | context 173 | |> skip_lines(:attr, io) 174 | |> write_string("defmacrop #{header.macro_dispatcher}(name, args) when is_atom(name), do:", io) 175 | |> increment_indent 176 | |> write_string("{Module.get_attribute(__MODULE__, name), [], args}", io) 177 | |> decrement_indent 178 | |> write_string("defmacrop #{header.macro_dispatcher}(macro, args), do:", io) 179 | |> increment_indent 180 | |> write_string("{Macro.expand(macro, __CALLER__), [], args}", io) 181 | |> decrement_indent 182 | else 183 | context 184 | end 185 | end 186 | 187 | # This version writes a comment. 188 | defp write_form(context, %ExComment{comments: comments}, io) do 189 | context 190 | |> write_comment_list(comments, :structure_comments, io) 191 | end 192 | 193 | # This version writes a function definition. 194 | defp write_form( 195 | context, 196 | %ExFunc{ 197 | comments: comments, 198 | clauses: [first_clause | remaining_clauses], 199 | public: public, 200 | specs: specs 201 | }, 202 | io) 203 | do 204 | context 205 | |> write_comment_list(comments, :func_header, io) 206 | |> write_func_specs(specs, io) 207 | |> write_func_clause(public, first_clause, :func_clause_first, io) 208 | |> foreach(remaining_clauses, fn (ctx, clause) -> 209 | write_func_clause(ctx, public, clause, :func_clause, io) 210 | end) 211 | end 212 | 213 | # This version writes an attribute definition 214 | defp write_form(context, %ExAttr{name: name, register: register, arg: arg, comments: comments}, io) do 215 | context 216 | |> skip_lines(:attr, io) 217 | |> foreach(comments, io, &write_string/3) 218 | |> write_raw_attr(name, register, arg, io) 219 | end 220 | 221 | # This version writes most directives 222 | defp write_form(context, %ExDirective{directive: directive, name: name, comments: comments}, io) do 223 | context 224 | |> skip_lines(:directive, io) 225 | |> foreach(comments, io, &write_string/3) 226 | |> write_raw_directive(directive, name, io) 227 | end 228 | 229 | # This version writes an import directive 230 | defp write_form(context, %ExImport{module: module, funcs: funcs, comments: comments}, io) do 231 | context 232 | |> skip_lines(:attr, io) 233 | |> foreach(comments, io, &write_string/3) 234 | |> write_string("import #{expr_to_string(module)}, only: #{expr_to_string(funcs)}", io) 235 | end 236 | 237 | # This version writes an attribute definition 238 | defp write_form(context, %ExRecord{tag: tag, macro: macro, data_attr: data_attr, fields: fields, comments: comments}, io) do 239 | field_names = fields |> Enum.map(&(elem(&1, 0))) 240 | context 241 | |> skip_lines(:attr, io) 242 | |> foreach(comments, io, &write_string/3) 243 | |> write_string("@#{data_attr} #{expr_to_string(field_names)}", io) 244 | |> write_string("Record.defrecordp #{expr_to_string(macro)}, #{expr_to_string(tag)}, #{expr_to_string(fields)}", io) 245 | end 246 | 247 | # This version writes a type definition 248 | defp write_form(context, %ExType{kind: kind, signature: signature, defn: defn, comments: comments}, io) do 249 | context 250 | |> skip_lines(:attr, io) 251 | |> foreach(comments, io, &write_string/3) 252 | |> write_string("@#{kind} #{expr_to_string(signature)} :: #{expr_to_string(defn)}", io) 253 | end 254 | 255 | # This version writes a function spec 256 | defp write_form(context, %ExSpec{kind: kind, specs: specs, comments: comments}, io) do 257 | context 258 | |> skip_lines(:attr, io) 259 | |> foreach(comments, io, &write_string/3) 260 | |> foreach(specs, fn(ctx, spec) -> 261 | write_string(ctx, "@#{kind} #{expr_to_string(spec)}", io) 262 | end) 263 | end 264 | 265 | # This version writes a macro 266 | defp write_form( 267 | context, 268 | %ExMacro{ 269 | macro_name: macro_name, 270 | signature: signature, 271 | tracking_name: tracking_name, 272 | dispatch_name: dispatch_name, 273 | stringifications: stringifications, 274 | expr: expr, 275 | guard_expr: guard_expr, 276 | comments: comments 277 | }, 278 | io) 279 | do 280 | context = context 281 | |> write_comment_list(comments, :func_header, io) 282 | |> skip_lines(:func_clause_first, io) 283 | |> write_string("defmacrop #{signature_to_string(signature)} do", io) 284 | |> increment_indent 285 | |> foreach(stringifications, fn(ctx, {var, str}) -> 286 | write_string(ctx, "#{str} = Macro.to_string(quote do: unquote(#{var})) |> String.to_charlist", io) 287 | end) 288 | context = 289 | if guard_expr != nil do 290 | context 291 | |> write_string("if Macro.Env.in_guard?(__CALLER__) do", io) 292 | |> increment_indent 293 | |> write_string("quote do", io) 294 | |> increment_indent 295 | |> write_string(expr_to_string(guard_expr), io) 296 | |> decrement_indent 297 | |> write_string("end", io) 298 | |> decrement_indent 299 | |> write_string("else", io) 300 | |> increment_indent 301 | else 302 | context 303 | end 304 | context = context 305 | |> write_string("quote do", io) 306 | |> increment_indent 307 | |> write_string(expr_to_string(expr), io) 308 | |> decrement_indent 309 | |> write_string("end", io) 310 | context = 311 | if guard_expr != nil do 312 | context 313 | |> decrement_indent 314 | |> write_string("end", io) 315 | else 316 | context 317 | end 318 | context = context 319 | |> decrement_indent 320 | |> write_string("end", io) 321 | context = 322 | if tracking_name != nil do 323 | context 324 | |> write_string("@#{tracking_name} true", io) 325 | else 326 | context 327 | end 328 | context = 329 | if dispatch_name != nil do 330 | context 331 | |> write_string("@#{dispatch_name} :#{macro_name}", io) 332 | else 333 | context 334 | end 335 | context 336 | end 337 | 338 | 339 | # Write an attribute definition to the given IO. 340 | 341 | defp write_raw_attr(context, name, register, arg, io) do 342 | context = 343 | if register do 344 | context 345 | |> write_string("Module.register_attribute(__MODULE__, #{expr_to_string(name)}, persist: true, accumulate: true)", io) 346 | else 347 | context 348 | end 349 | context 350 | |> write_string("@#{name} #{expr_to_string(arg)}", io) 351 | end 352 | 353 | 354 | # Write the Elixir equivalent of a preprocessor control structure directive. 355 | 356 | # The undef directive. 357 | defp write_raw_directive(context, :undef, tracking_name, io) do 358 | context 359 | |> write_string("@#{tracking_name} false", io) 360 | end 361 | 362 | # The ifdef directive. 363 | defp write_raw_directive(context, :ifdef, tracking_name, io) do 364 | context 365 | |> write_string("if @#{tracking_name} do", io) 366 | end 367 | 368 | # The ifndef directive. 369 | defp write_raw_directive(context, :ifndef, tracking_name, io) do 370 | context 371 | |> write_string("if not @#{tracking_name} do", io) 372 | end 373 | 374 | # The else directive. 375 | defp write_raw_directive(context, :else, nil, io) do 376 | context 377 | |> write_string("else", io) 378 | end 379 | 380 | # The endif directive. 381 | defp write_raw_directive(context, :endif, nil, io) do 382 | context 383 | |> write_string("end", io) 384 | end 385 | 386 | 387 | # Write a list of full-line comments. 388 | 389 | defp write_comment_list(context, [], _form_type, _io), do: context 390 | defp write_comment_list(context, comments, form_type, io) do 391 | context 392 | |> skip_lines(form_type, io) 393 | |> foreach(comments, io, &write_string/3) 394 | end 395 | 396 | 397 | # Write a list of function specification clauses. 398 | 399 | defp write_func_specs(context, [], _io), do: context 400 | defp write_func_specs(context, specs, io) do 401 | context 402 | |> skip_lines(:func_specs, io) 403 | |> foreach(specs, fn(ctx, spec) -> 404 | write_string(ctx, "@spec #{expr_to_string(spec)}", io) 405 | end) 406 | end 407 | 408 | 409 | # Write a single function clause (i.e. a def or defp) 410 | 411 | defp write_func_clause(context, public, clause, form_type, io) do 412 | decl = if public, do: "def", else: "defp" 413 | sig = clause.signature 414 | context = context 415 | |> skip_lines(form_type, io) 416 | |> foreach(clause.comments, io, &write_string/3) 417 | context = context 418 | |> write_string("#{decl} #{signature_to_string(sig)} do", io) 419 | |> increment_indent 420 | |> foreach(clause.exprs, fn (ctx, expr) -> 421 | write_string(ctx, expr_to_string(expr), io) 422 | end) 423 | |> decrement_indent 424 | |> write_string("end", io) 425 | context 426 | end 427 | 428 | 429 | # Low-level function that writes a string, handling indent. 430 | 431 | defp write_string(context, str, io) do 432 | indent = String.duplicate(" ", context.indent) 433 | str 434 | |> String.split("\n") 435 | |> Enum.each(fn line -> 436 | IO.write(io, "#{indent}#{line}\n") 437 | end) 438 | context 439 | end 440 | 441 | 442 | # Performs the given operation on elements of the given list, updating the 443 | # context and returning the final context value. 444 | 445 | defp foreach(context, list, io, func) do 446 | Enum.reduce(list, context, fn (e, ctx) -> func.(ctx, e, io) end) 447 | end 448 | 449 | defp foreach(context, list, func) do 450 | Enum.reduce(list, context, fn (e, ctx) -> func.(ctx, e) end) 451 | end 452 | 453 | 454 | # Inserts appropriate vertical whitespace, given a form type. 455 | 456 | defp skip_lines(context, cur_form, io) do 457 | lines = calc_skip_lines(context.last_form, cur_form) 458 | if lines > 0 do 459 | IO.puts(io, String.duplicate("\n", lines - 1)) 460 | end 461 | %Context{context | last_form: cur_form} 462 | end 463 | 464 | 465 | # Computes the vertical whitespace between two form types. 466 | 467 | defp calc_skip_lines(:start, _), do: 0 468 | defp calc_skip_lines(:module_comments, :module_begin), do: 1 469 | defp calc_skip_lines(:module_begin, _), do: 1 470 | defp calc_skip_lines(_, :module_end), do: 1 471 | defp calc_skip_lines(:func_header, :func_specs), do: 1 472 | defp calc_skip_lines(:func_header, :func_clause_first), do: 1 473 | defp calc_skip_lines(:func_specs, :func_clause_first), do: 1 474 | defp calc_skip_lines(:func_clause_first, :func_clause), do: 1 475 | defp calc_skip_lines(:func_clause, :func_clause), do: 1 476 | defp calc_skip_lines(:attr, :attr), do: 1 477 | defp calc_skip_lines(_, _), do: 2 478 | 479 | 480 | # Generates code for a function signature. 481 | # Handles a special case where the signature ends with a keyword argument 482 | # block that includes "do:" as a keyword. Macro.to_string erroneously 483 | # generates a do block for that case, so we detect that case and modify the 484 | # generated string. 485 | 486 | defp signature_to_string({target, ctx, args} = expr) do 487 | str = expr_to_string(expr) 488 | if String.ends_with?(str, "\nend") do 489 | args = args |> List.update_at(-1, fn kwargs -> 490 | kwargs ++ [a: :a] 491 | end) 492 | {target, ctx, args} 493 | |> expr_to_string 494 | |> String.replace_suffix(", a: :a)", ")") 495 | else 496 | str 497 | end 498 | end 499 | 500 | 501 | # Given an Elixir AST, generate Elixir source code. 502 | # This wraps Macro.to_string but does a bit of customization. 503 | 504 | defp expr_to_string(expr) do 505 | Macro.to_string(expr, &modify_codegen/2) 506 | end 507 | 508 | 509 | # Codegen customization. 510 | 511 | # Custom codegen for character literals. There's a hack in conversion that 512 | # converts character literals to the illegal "?" variable name, with the 513 | # actual character in the metadata. 514 | defp modify_codegen({:"?", metadata, Elixir}, str) do 515 | case Keyword.get(metadata, :char) do 516 | nil -> str 517 | val -> << "?"::utf8, escape_char(val)::binary >> 518 | end 519 | end 520 | 521 | # Fallthrough for codegen 522 | defp modify_codegen(_ast, str) do 523 | str 524 | end 525 | 526 | 527 | # Escaped character literals. 528 | defp escape_char(?\\), do: "\\\\" 529 | defp escape_char(?\a), do: "\\a" 530 | defp escape_char(?\b), do: "\\b" 531 | defp escape_char(?\d), do: "\\d" 532 | defp escape_char(?\e), do: "\\e" 533 | defp escape_char(?\f), do: "\\f" 534 | defp escape_char(?\n), do: "\\n" 535 | defp escape_char(?\r), do: "\\r" 536 | defp escape_char(?\s), do: "\\s" 537 | defp escape_char(?\t), do: "\\t" 538 | defp escape_char(?\v), do: "\\v" 539 | defp escape_char(?\0), do: "\\0" 540 | defp escape_char(val), do: <> 541 | 542 | 543 | end 544 | -------------------------------------------------------------------------------- /lib/erl2ex/pipeline/convert.ex: -------------------------------------------------------------------------------- 1 | # This is the fourth stage in the pipeline, after analyze. 2 | # It takes the output of the analysis phase (which inclues both the parsed 3 | # Erlang AST forms and the module-wide analysis results) and generates Elixir 4 | # AST along with metadata for codegen. The output is in the form of structures 5 | # defined in ex_data.ex. 6 | # 7 | # Much of the code for the convert phase lives in modules in the convert 8 | # directory. 9 | 10 | defmodule Erl2ex.Pipeline.Convert do 11 | 12 | @moduledoc false 13 | 14 | 15 | alias Erl2ex.Pipeline.ExModule 16 | 17 | alias Erl2ex.Convert.Context 18 | alias Erl2ex.Convert.ErlForms 19 | alias Erl2ex.Convert.ExtForms 20 | alias Erl2ex.Convert.Headers 21 | 22 | 23 | # The entry point of the convert phase. Takes a ModuleData as input and 24 | # returns an ExModule. 25 | 26 | def module(module_data, opts \\ []) do 27 | context = Context.build(module_data, opts) 28 | {forms, context} = module_data.forms 29 | |> Enum.flat_map_reduce(context, &conv_form/2) 30 | forms = [Headers.build_header(context.module_data, forms) | forms] 31 | %ExModule{ 32 | name: module_data.name, 33 | file_comments: file_comments(context, opts), 34 | forms: forms 35 | } 36 | end 37 | 38 | 39 | # Generates comment header for a generate Elixir source file. 40 | 41 | defp file_comments(context, opts) do 42 | if Keyword.get(opts, :emit_file_headers, true) do 43 | {{year, month, day}, {hour, minute, second}} = 44 | :os.timestamp |> :calendar.now_to_local_time 45 | timestamp = "~4..0B-~2..0B-~2..0B ~2..0B:~2..0B:~2..0B" 46 | |> :io_lib.format([year, month, day, hour, minute, second]) 47 | |> List.to_string 48 | [ 49 | "# Generated by erl2ex (http://github.com/dazuma/erl2ex)", 50 | "# From Erlang source: #{Context.cur_file_path_for_display(context)}", 51 | "# At: #{timestamp}", 52 | ] 53 | else 54 | [] 55 | end 56 | end 57 | 58 | 59 | defp conv_form({nil, ext_form}, context) do 60 | ExtForms.conv_form(:erl_syntax.type(ext_form), ext_form, context) 61 | end 62 | 63 | defp conv_form({erl_form, _ext_form}, context) do 64 | ErlForms.conv_form(erl_form, context) 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /lib/erl2ex/pipeline/erl_syntax.ex: -------------------------------------------------------------------------------- 1 | # This is a collection of convenience functions for parsing erl_syntax trees. 2 | # 3 | # Most of these functions take a node, a default, and a function. 4 | # If the given node matches what the function is expecting, it calls the given 5 | # function and returns its result; otherwise it returns the default. 6 | 7 | defmodule Erl2ex.Pipeline.ErlSyntax do 8 | 9 | @moduledoc false 10 | 11 | 12 | # Calls the given function if the argument is a list of one tree. 13 | # The tree is passed as the function argument. 14 | 15 | def on_trees1([node1], _default, func), do: func.(node1) 16 | def on_trees1(_nodes, default, _func), do: handle_default(default) 17 | 18 | 19 | # Calls the given function if the argument is a list of two trees. 20 | # The trees are passed as the function's two arguments. 21 | 22 | def on_trees2([node1, node2], _default, func), do: func.(node1, node2) 23 | def on_trees2(_nodes, default, _func), do: handle_default(default) 24 | 25 | 26 | # Calls the given function if the argument is a list skeleton node. 27 | # The list elements are passed to the function as a list. 28 | 29 | def on_list_skeleton(list_node, default, func) do 30 | if :erl_syntax.is_list_skeleton(list_node) do 31 | func.(:erl_syntax.list_elements(list_node)) 32 | else 33 | handle_default(default) 34 | end 35 | end 36 | 37 | 38 | # Calls the given function if the argument is a single tree node of the given type. 39 | # No argument is passed to the function. 40 | 41 | def on_type(tree_node, expected_type, default, func) do 42 | if :erl_syntax.type(tree_node) == expected_type do 43 | func.() 44 | else 45 | handle_default(default) 46 | end 47 | end 48 | 49 | 50 | # Calls the given function if the argument is an atom node. 51 | # The atom value is passed as the function argument. 52 | 53 | def on_atom(atom_node, default, func) do 54 | on_type(atom_node, :atom, default, fn -> 55 | func.(:erl_syntax.atom_value(atom_node)) 56 | end) 57 | end 58 | 59 | 60 | # Calls the given function if the argument is an atom node with the given value. 61 | # No argument is passed to the function. 62 | 63 | def on_atom_value(atom_node, expected_value, default, func) do 64 | on_atom(atom_node, default, fn value -> 65 | if value == expected_value do 66 | func.() 67 | else 68 | handle_default(default) 69 | end 70 | end) 71 | end 72 | 73 | 74 | # Calls the given function if the argument is an integer node. 75 | # The integer value is passed as the function argument. 76 | 77 | def on_integer(integer_node, default, func) do 78 | on_type(integer_node, :integer, default, fn -> 79 | func.(:erl_syntax.integer_value(integer_node)) 80 | end) 81 | end 82 | 83 | 84 | # Calls the given function if the argument is a string node. 85 | # The string value (as a binary) is passed as the function argument. 86 | 87 | def on_string(string_node, default, func) do 88 | on_type(string_node, :string, default, fn -> 89 | func.(string_node |> :erl_syntax.string_value |> List.to_string) 90 | end) 91 | end 92 | 93 | 94 | # Calls the given function if the argument is a tuple node. 95 | # The tuple size as an integer, and the tuple elements as a list, are passed 96 | # as the function arguments. 97 | 98 | def on_tuple(tuple_node, default, func) do 99 | on_type(tuple_node, :tuple, default, fn -> 100 | func.(:erl_syntax.tuple_size(tuple_node), :erl_syntax.tuple_elements(tuple_node)) 101 | end) 102 | end 103 | 104 | 105 | # Uses the given default as an initial accumulator, and reduces on the given 106 | # arity qualifier list. For each arity qualifier found, the accumulator, the 107 | # function name as an atom, and arity as an integer, are passed to the given 108 | # function. 109 | 110 | def on_arity_qualifier_list(list_node, default, func) do 111 | on_list_skeleton(list_node, default, fn elem_nodes -> 112 | elem_nodes |> Enum.reduce(default, fn elem_node, cur_obj -> 113 | on_type(elem_node, :arity_qualifier, cur_obj, fn -> 114 | body_node = :erl_syntax.arity_qualifier_body(elem_node) 115 | arity_node = :erl_syntax.arity_qualifier_argument(elem_node) 116 | on_atom(body_node, cur_obj, fn name -> 117 | on_integer(arity_node, cur_obj, fn arity -> 118 | func.(cur_obj, name, arity) 119 | end) 120 | end) 121 | end) 122 | end) 123 | end) 124 | end 125 | 126 | 127 | # Uses the given default as an initial accumulator, and reduces on the given 128 | # list of type-with-arity tuples. For each tuple found, the accumulator, the 129 | # type name as an atom, and the arity as an integer, are passed to the given 130 | # function. 131 | 132 | def on_type_with_arity_list(list_node, default, func) do 133 | on_list_skeleton(list_node, default, fn elem_nodes -> 134 | elem_nodes |> Enum.reduce(default, fn elem_node, cur_obj -> 135 | on_tuple(elem_node, cur_obj, fn 136 | 2, tuple_elem_nodes -> 137 | [body_node, arity_node] = tuple_elem_nodes 138 | on_atom(body_node, cur_obj, fn name -> 139 | on_integer(arity_node, cur_obj, fn arity -> 140 | func.(cur_obj, name, arity) 141 | end) 142 | end) 143 | _, _ -> 144 | cur_obj 145 | end) 146 | end) 147 | end) 148 | end 149 | 150 | 151 | # Calls the given function if the argument is an attribute node. 152 | # The attribute name node and list of argument nodes, are passed as the 153 | # function arguments. 154 | 155 | def on_attribute(form_node, default, func) do 156 | if :erl_syntax.type(form_node) == :attribute do 157 | name_node = :erl_syntax.attribute_name(form_node) 158 | arg_nodes = :erl_syntax.attribute_arguments(form_node) 159 | func.(name_node, arg_nodes) 160 | else 161 | handle_default(default) 162 | end 163 | end 164 | 165 | 166 | # Calls the given function if the argument is an attribute with a static 167 | # name (i.e. the name is just an atom.) The attribute name as an atom, and 168 | # the list of argument nodes, are passed as the function arguments. 169 | 170 | def on_static_attribute(form_node, default, func) do 171 | on_attribute(form_node, default, fn name_node, arg_nodes -> 172 | on_atom(name_node, default, fn name -> 173 | func.(name, arg_nodes) 174 | end) 175 | end) 176 | end 177 | 178 | 179 | # Calls the given function if the argument is an attribute with the given 180 | # static name. The list of argument nodes is passed as the function argument. 181 | 182 | def on_attribute_name(form_node, expected_name, default, func) do 183 | on_attribute(form_node, default, fn name_node, arg_nodes -> 184 | on_atom_value(name_node, expected_name, default, fn -> 185 | func.(arg_nodes) 186 | end) 187 | end) 188 | end 189 | 190 | 191 | defp handle_default(default) when is_function(default), do: default.() 192 | defp handle_default(default), do: default 193 | 194 | 195 | end 196 | -------------------------------------------------------------------------------- /lib/erl2ex/pipeline/ex_data.ex: -------------------------------------------------------------------------------- 1 | # These internal data structures are the output of Pipeline.Convert, and 2 | # represent Elixir source trees for codegen. 3 | 4 | 5 | # A toplevel comment. 6 | 7 | defmodule Erl2ex.Pipeline.ExComment do 8 | @moduledoc false 9 | 10 | defstruct( 11 | # List of comments, one per line. Each comment must begin with a hash "#". 12 | comments: [] 13 | ) 14 | 15 | end 16 | 17 | 18 | # A module attribute. 19 | 20 | defmodule Erl2ex.Pipeline.ExAttr do 21 | @moduledoc false 22 | 23 | defstruct( 24 | # Name of the attribute as an atom. 25 | name: nil, 26 | # Whether to register the attribute. 27 | register: false, 28 | # List of arguments. Most attributes have a single argument (the value). 29 | arg: nil, 30 | # List of pre-form comments, one per line. Each must begin with a hash "#". 31 | comments: [] 32 | ) 33 | 34 | end 35 | 36 | 37 | # The Elixir form of an Erlang compiler directive (such as ifdef). 38 | # This is represented as an abstract directive here, and codegen takes care 39 | # of generating Elixir compile-time code. 40 | 41 | defmodule Erl2ex.Pipeline.ExDirective do 42 | @moduledoc false 43 | 44 | defstruct( 45 | # The directive as an atom. 46 | directive: nil, 47 | # The name of the referenced name (e.g. the macro name for ifdef) as an atom. 48 | name: nil, 49 | # List of pre-form comments, one per line. Each must begin with a hash "#". 50 | comments: [] 51 | ) 52 | 53 | end 54 | 55 | 56 | # A directive to import a module. 57 | 58 | defmodule Erl2ex.Pipeline.ExImport do 59 | @moduledoc false 60 | 61 | defstruct( 62 | # The name of the module, as an atom. 63 | module: nil, 64 | # List of functions to import, each as {name_as_atom, arity_as_integer}. 65 | funcs: [], 66 | # List of pre-form comments, one per line. Each must begin with a hash "#". 67 | comments: [] 68 | ) 69 | 70 | end 71 | 72 | 73 | # An Elixir macro 74 | 75 | defmodule Erl2ex.Pipeline.ExMacro do 76 | @moduledoc false 77 | 78 | defstruct( 79 | # Elixir AST for the signature of the macro 80 | signature: nil, 81 | # The macro name as an atom. 82 | macro_name: nil, 83 | # The name of an attribute that tracks whether the macro is defined, as an atom. 84 | tracking_name: nil, 85 | # The name of an attribute that tracks the current macro name, as an atom. 86 | # Used when a macro is redefined in a module, which Elixir doesn't allow. So we 87 | # define macros with different names, and use this attribute to specify which name 88 | # we are using. 89 | dispatch_name: nil, 90 | # A map from argument name (as atom) to a variable name used for the stringified 91 | # form of the argument (i.e. the Erlang "??" preprocessor operator). 92 | stringifications: nil, 93 | # Elixir AST for the macro replacement when expanded in normal context. 94 | expr: nil, 95 | # Elixir AST for the macro replacement when expanded in a guard context, or 96 | # nil if the expansion should not be different from normal context. 97 | guard_expr: nil, 98 | # List of pre-form comments, one per line. Each must begin with a hash "#". 99 | comments: [] 100 | ) 101 | 102 | end 103 | 104 | 105 | # An Elixir record. 106 | 107 | defmodule Erl2ex.Pipeline.ExRecord do 108 | @moduledoc false 109 | 110 | defstruct( 111 | # The tag atom used in the record 112 | tag: nil, 113 | # The name of the record macro, as an atom. 114 | macro: nil, 115 | # The name of an attribute that stores the record definition. 116 | data_attr: nil, 117 | # The record fields, as Elixir AST. 118 | fields: [], 119 | # List of pre-form comments, one per line. Each must begin with a hash "#". 120 | comments: [] 121 | ) 122 | 123 | end 124 | 125 | 126 | # An Elixir type definition. 127 | 128 | defmodule Erl2ex.Pipeline.ExType do 129 | @moduledoc false 130 | 131 | defstruct( 132 | # One of the following: :opaque, :type, :typep 133 | kind: nil, 134 | # An Elixir AST describing the type and its parameters (which may be empty). 135 | signature: nil, 136 | # Elixir AST describing the type's definition 137 | defn: nil, 138 | # List of pre-form comments, one per line. Each must begin with a hash "#". 139 | comments: [] 140 | ) 141 | 142 | end 143 | 144 | 145 | # An Elixir function spec. 146 | 147 | defmodule Erl2ex.Pipeline.ExSpec do 148 | @moduledoc false 149 | 150 | defstruct( 151 | # Either :spec or :callback. 152 | kind: nil, 153 | # Name of the function specified. 154 | name: nil, 155 | # List of Elixir ASTs describing the specs. 156 | specs: [], 157 | # List of pre-form comments, one per line. Each must begin with a hash "#". 158 | comments: [] 159 | ) 160 | 161 | end 162 | 163 | 164 | # The header for an Elixir module. Includes auto-generated pieces such as 165 | # require statements for Bitwise and Record, if needed, as well as various 166 | # macros, attributes, etc. needed to implement Erlang semantics. 167 | 168 | defmodule Erl2ex.Pipeline.ExHeader do 169 | @moduledoc false 170 | 171 | defstruct( 172 | # True if Bitwise operators are used in this module. 173 | use_bitwise: false, 174 | # True if the Erlang is_record BIF is used (so Elixir needs to require Record) 175 | has_is_record: false, 176 | # List of {record_name, [record_fields]} so codegen can define the records. 177 | records: [], 178 | # List of macro names that are not initialized explicitly and probably should be 179 | # initialized from environment variables. 180 | init_macros: [], 181 | # The name of the macro dispatcher macro (if needed) as an atom, or nil if the 182 | # dispatcher is not needed. 183 | macro_dispatcher: nil, 184 | # The name of the macro that returns record size, or nil if not needed. 185 | record_size_macro: nil, 186 | # The name of the macro that computes record index, or nil if not needed. 187 | record_index_macro: nil 188 | ) 189 | 190 | end 191 | 192 | 193 | # An Elixir function. 194 | 195 | defmodule Erl2ex.Pipeline.ExFunc do 196 | @moduledoc false 197 | 198 | defstruct( 199 | # The name of the function. 200 | name: nil, 201 | # Arity of the function, as an integer 202 | arity: nil, 203 | # Whether the function should be public. 204 | public: false, 205 | # Not currently used. Later we expect we'll consolidate specs for the function 206 | # here instead of emitting them separately. 207 | specs: [], 208 | # List of ExClause structures. 209 | clauses: [], 210 | # List of pre-form comments, one per line. Each must begin with a hash "#". 211 | comments: [] 212 | ) 213 | 214 | end 215 | 216 | 217 | # A single clause of an Elixir function. 218 | 219 | defmodule Erl2ex.Pipeline.ExClause do 220 | @moduledoc false 221 | 222 | defstruct( 223 | # Elixir AST of the function signature. 224 | signature: nil, 225 | # List of Elixir ASTs representing the list of expressions in the function. 226 | exprs: [], 227 | # List of pre-form comments, one per line. Each must begin with a hash "#". 228 | comments: [] 229 | ) 230 | 231 | end 232 | 233 | 234 | # The full Elixir module representation. 235 | 236 | defmodule Erl2ex.Pipeline.ExModule do 237 | @moduledoc false 238 | 239 | defstruct( 240 | # Name of the module, as an atom. 241 | name: nil, 242 | # List of top-of-file comments, one per line. Each must begin with a hash "#". 243 | file_comments: [], 244 | # List of top-of-module comments, one per line. These are indented within the 245 | # module definition. Each must begin with a hash "#". 246 | comments: [], 247 | # List of forms (other structures from this file). 248 | forms: [] 249 | ) 250 | 251 | end 252 | -------------------------------------------------------------------------------- /lib/erl2ex/pipeline/inline_includes.ex: -------------------------------------------------------------------------------- 1 | # This is the second phase in the pipeline, after parse. It goes through 2 | # the parsed forms, looking for include and include_lib calls. Any included 3 | # files are parsed and its forms inlined into the form list. 4 | 5 | defmodule Erl2ex.Pipeline.InlineIncludes do 6 | 7 | @moduledoc false 8 | 9 | alias Erl2ex.Source 10 | 11 | alias Erl2ex.Pipeline.ErlSyntax 12 | alias Erl2ex.Pipeline.Parse 13 | 14 | 15 | # Entry point into InlineIncludes. Takes a list of forms, a Source 16 | # process, and the path to the main file. 17 | 18 | def process(forms, source, main_source_path) do 19 | forms |> Enum.flat_map(&(handle_form(&1, source, main_source_path))) 20 | end 21 | 22 | 23 | # Handles a single form, using the extended (epp_dodger) AST. If the 24 | # form is an include or include_lib directive, replaces it with the 25 | # contents of the referenced file. 26 | 27 | defp handle_form({_erl_ast, form_node} = form, source, main_source_path) do 28 | ErlSyntax.on_static_attribute(form_node, [form], fn name, arg_nodes -> 29 | ErlSyntax.on_trees1(arg_nodes, [form], fn arg_node -> 30 | ErlSyntax.on_string(arg_node, [form], fn path -> 31 | path = Regex.replace(~r/^\$(\w+)/, path, fn (match, env) -> 32 | case System.get_env(env) do 33 | nil -> match 34 | val -> val 35 | end 36 | end) 37 | case name do 38 | :include -> 39 | source_dir = if main_source_path == nil, do: nil, else: Path.dirname(main_source_path) 40 | {include_str, include_path} = Source.read_include(source, path, source_dir) 41 | do_include(include_str, include_path, path, source, main_source_path) 42 | :include_lib -> 43 | [lib_name | path_elems] = path |> Path.relative |> Path.split 44 | rel_path = Path.join(path_elems) 45 | lib_atom = String.to_atom(lib_name) 46 | {include_str, include_path} = Source.read_lib_include(source, lib_atom, rel_path) 47 | display_path = "#{rel_path} from library #{lib_name}" 48 | do_include(include_str, include_path, display_path, source, main_source_path) 49 | _ -> 50 | [form] 51 | end 52 | end) 53 | end) 54 | end) 55 | end 56 | 57 | 58 | # Loads a file to be included. Runs the Parse and (recursively) the 59 | # InlineIncludes phases on the file contents to obtain parsed forms. Also 60 | # generates comments delineating the file inclusion. 61 | 62 | defp do_include(include_str, include_path, display_path, source, main_source_path) do 63 | include_forms = include_str 64 | |> Parse.string(cur_file_path: include_path) 65 | |> process(source, main_source_path) 66 | pre_comment = :erl_syntax.comment(['% Begin included file: #{display_path}']) 67 | post_comment = :erl_syntax.comment(['% End included file: #{display_path}']) 68 | [{nil, pre_comment} | include_forms] ++ [{nil, post_comment}] 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /lib/erl2ex/pipeline/module_data.ex: -------------------------------------------------------------------------------- 1 | # This data structure is the output of the analyze phase. It includes a bunch 2 | # of information about the module as a whole. 3 | 4 | defmodule Erl2ex.Pipeline.ModuleData do 5 | 6 | 7 | @moduledoc false 8 | 9 | alias Erl2ex.Pipeline.ModuleData 10 | alias Erl2ex.Pipeline.Names 11 | 12 | 13 | defstruct( 14 | # Name of the module, as an atom 15 | name: nil, 16 | # List of forms, as {erl_ast, erl_syntax_node} 17 | forms: [], 18 | # List of function name suffixes that should automatically be exported, 19 | # as a list of strings. 20 | auto_export_suffixes: [], 21 | # Set of Erlang function names to be exported. Each function is 22 | # represented both as a {name atom, arity integer} tuple and as a 23 | # bare name atom. 24 | exports: MapSet.new, 25 | # Set of Erlang types to be exported. Each is represented as a 26 | # {name atom, arity integer} tuple. 27 | type_exports: MapSet.new, 28 | # A map of imported functions, specifying what module each is imported 29 | # from. Structure is (name atom => (arity integer => module name atom)) 30 | imported_funcs: %{}, 31 | # A set of the original (Erlang) names of functions defined in this module. 32 | # The structure is {name atom, arity integer}. 33 | local_funcs: MapSet.new, 34 | # Map of Erlang record name atoms to RecordData. 35 | records: %{}, 36 | # Set of attribute names (as atoms) that are in use and can no longer 37 | # be assigned. 38 | used_attr_names: MapSet.new, 39 | # Set of function names (as atoms) that are in use and can no longer 40 | # be assigned. 41 | used_func_names: MapSet.new, 42 | # Mapping from Erlang to Elixir function names (atom => atom) 43 | func_rename_map: %{}, 44 | # Map of Erlang macro name atoms to MacroData 45 | macros: %{}, 46 | # Name of the macro dispatcher as an atom, if needed, or nil if not. 47 | macro_dispatcher: nil, 48 | # Name (as an atom) of the Elixir macro that returns record size, or nil 49 | # if not needed. 50 | record_size_macro: nil, 51 | # Name (as an atom) of the Elixir macro that returns record field index, 52 | # or nil if not needed. 53 | record_index_macro: nil, 54 | # True if the is_record BIF is called in this module. 55 | has_is_record: false 56 | ) 57 | 58 | 59 | # A structure of data about a macro. 60 | 61 | defmodule MacroData do 62 | @moduledoc false 63 | defstruct( 64 | # The name of the single no-argument macro in Elixir, if this macro 65 | # has exactly one no-argument definition. Or nil if this macro has 66 | # zero or multiple no-argument definitions. 67 | const_name: nil, 68 | # The name of the single macro with arguments in Elixir, if this macro 69 | # has exactly one definition with arguments. Or nil if this macro has 70 | # zero or multiple such definitions. 71 | func_name: nil, 72 | # The name of the Elixir attribute that tracks whether this macro has 73 | # been defined, or nil if such an attribute is not needed. 74 | define_tracker: nil, 75 | # True if this macro is tested for existence (i.e. ifdef) prior to its 76 | # first definition (which means we need to initialize the define_tracker 77 | # at the top of the module). False if this macro is defined prior to its 78 | # first existence test. Or nil if we have no information (which should 79 | # be treated the same as false). 80 | requires_init: nil, 81 | # True if this macro is invoked with arguments. 82 | has_func_style_call: false, 83 | # Whether this macro is redefined during the module. If true, we've 84 | # determined it has been redefined. Otherwise, it will be a set of 85 | # integers representing the arities of definitions we've seen so far. 86 | is_redefined: MapSet.new, 87 | # If this macro has a single constant defintion, stores the Erlang AST 88 | # for that definition. Used for cases where we need to inline it. 89 | const_expr: nil 90 | ) 91 | end 92 | 93 | 94 | # A structure of data about a record. 95 | 96 | defmodule RecordData do 97 | @moduledoc false 98 | defstruct( 99 | # The name of the Elixir macro for this record, as an atom 100 | func_name: nil, 101 | # The name of the attribute storing this record's fields 102 | data_attr_name: nil, 103 | # The field data, as a list of {name, type} tuples. The name is an atom, 104 | # and the type is an Erlang type AST. 105 | fields: [] 106 | ) 107 | end 108 | 109 | 110 | # Returns true if the given Erlang function name and arity are exported. 111 | 112 | def is_exported?(%ModuleData{exports: exports, auto_export_suffixes: auto_export_suffixes}, name, arity) do 113 | MapSet.member?(exports, {name, arity}) or 114 | String.ends_with?(Atom.to_string(name), auto_export_suffixes) 115 | end 116 | 117 | 118 | # Returns true if the given Erlang type name and arity are exported. 119 | 120 | def is_type_exported?(%ModuleData{type_exports: type_exports}, name, arity) do 121 | MapSet.member?(type_exports, {name, arity}) 122 | end 123 | 124 | 125 | # Returns true if the given Erlang function name and arity is defined in 126 | # this module. 127 | 128 | def is_local_func?(%ModuleData{local_funcs: local_funcs}, name, arity) do 129 | MapSet.member?(local_funcs, {name, arity}) 130 | end 131 | 132 | 133 | # Returns true if the given function name which is a BIF in Erlang needs 134 | # qualification in Elixir. 135 | 136 | def binary_bif_requires_qualification?( 137 | %ModuleData{local_funcs: local_funcs, imported_funcs: imported_funcs}, 138 | func_name) 139 | do 140 | is_atom(func_name) and Regex.match?(~r/^[a-z]+$/, Atom.to_string(func_name)) and 141 | (MapSet.member?(local_funcs, {func_name, 2}) or 142 | Map.has_key?(Map.get(imported_funcs, func_name, %{}), 2)) and 143 | not MapSet.member?(Names.elixir_reserved_words, func_name) 144 | end 145 | 146 | 147 | # Given an Erlang name for a function in this module, returns the name that 148 | # should be used in the Elixir module. 149 | 150 | def local_function_name(%ModuleData{func_rename_map: func_rename_map}, name) do 151 | Map.fetch!(func_rename_map, name) 152 | end 153 | 154 | 155 | # Given a name atom, returns true if it is an Erlang function defined in 156 | # this module. 157 | 158 | def has_local_function_name?(%ModuleData{func_rename_map: func_rename_map}, name) do 159 | Map.has_key?(func_rename_map, name) 160 | end 161 | 162 | 163 | # Given a function name/arity that was called without qualification in 164 | # Erlang, returns some information on how to call it in Elixir. Possible 165 | # return values are: 166 | # * {:apply, local_name_atom} 167 | # * {:apply, module_atom, local_name_atom} 168 | # * {:qualify, local_name_atom} 169 | # * {:qualify, module_atom, local_name_atom} 170 | # * {:bare, local_name_atom} 171 | # * {:bare, module_atom, local_name_atom} 172 | # Apply means Kernel.apply must be used. 173 | # Qualify means Elixir must call Module.func. 174 | # Bare means Elixir may call the function without qualification. 175 | 176 | def local_call_strategy( 177 | %ModuleData{ 178 | local_funcs: local_funcs, 179 | imported_funcs: imported_funcs, 180 | func_rename_map: func_rename_map 181 | }, 182 | name, arity) 183 | do 184 | if MapSet.member?(local_funcs, {name, arity}) do 185 | mapped_name = Map.fetch!(func_rename_map, name) 186 | cond do 187 | not Names.callable_function_name?(mapped_name) -> 188 | {:apply, mapped_name} 189 | not Names.local_callable_function_name?(mapped_name) 190 | or Map.has_key?(imported_funcs, mapped_name) -> 191 | {:qualify, mapped_name} 192 | true -> 193 | {:bare, mapped_name} 194 | end 195 | else 196 | import_info = Map.get(imported_funcs, name, %{}) 197 | {imported, which_module, mapped_name} = case Map.fetch(import_info, arity) do 198 | {:ok, mod} -> {true, mod, name} 199 | :error -> 200 | case Names.map_bif(name) do 201 | {:ok, Kernel, mapped_func} -> 202 | {true, Kernel, mapped_func} 203 | {:ok, mapped_mod, mapped_func} -> 204 | {false, mapped_mod, mapped_func} 205 | :error -> 206 | {false, :erlang, name} 207 | end 208 | end 209 | cond do 210 | not Names.callable_function_name?(mapped_name) -> 211 | {:apply, which_module, mapped_name} 212 | not Names.local_callable_function_name?(mapped_name) or not imported -> 213 | {:qualify, which_module, mapped_name} 214 | true -> 215 | {:bare, which_module, mapped_name} 216 | end 217 | end 218 | end 219 | 220 | 221 | # Returns true if the given Erlang macro requires the macro dispatcher. 222 | 223 | def macro_needs_dispatch?(%ModuleData{macros: macros}, name) do 224 | macro_info = Map.get(macros, name, nil) 225 | if macro_info == nil do 226 | false 227 | else 228 | macro_info.is_redefined == true or 229 | macro_info.has_func_style_call and macro_info.func_name == nil 230 | end 231 | end 232 | 233 | 234 | # Given an Erlang macro and arity, returns the Elixir macro name. 235 | 236 | def macro_function_name(%ModuleData{macros: macros}, name, arity) do 237 | macro_info = Map.get(macros, name, nil) 238 | cond do 239 | macro_info == nil -> nil 240 | arity == nil -> macro_info.const_name 241 | true -> macro_info.func_name 242 | end 243 | end 244 | 245 | 246 | # Returns the name of the macro dispatcher. 247 | 248 | def macro_dispatcher_name(%ModuleData{macro_dispatcher: macro_name}) do 249 | macro_name 250 | end 251 | 252 | 253 | # Given a macro with a single constant replacement, returns that replacement 254 | # as an Erlang AST, or nil if no such replacement exists. 255 | 256 | def macro_eager_replacement(%ModuleData{macros: macros}, name) do 257 | macro_info = Map.fetch!(macros, name) 258 | macro_info.const_expr 259 | end 260 | 261 | 262 | # Returns the name of the Elixir macro to call to get a record's size. 263 | 264 | def record_size_macro(%ModuleData{record_size_macro: macro_name}) do 265 | macro_name 266 | end 267 | 268 | 269 | # Returns the name of the Elixir macro to call to get the index of 270 | # a record field. 271 | 272 | def record_index_macro(%ModuleData{record_index_macro: macro_name}) do 273 | macro_name 274 | end 275 | 276 | 277 | # Returns the name of the Elixir record macro for the given Erlang record 278 | # name. 279 | 280 | def record_function_name(%ModuleData{records: records}, name) do 281 | record_info = Map.fetch!(records, name) 282 | record_info.func_name 283 | end 284 | 285 | 286 | # Returns the name of the attribute storing the fields for the given Erlang 287 | # record name. 288 | 289 | def record_data_attr_name(%ModuleData{records: records}, name) do 290 | record_info = Map.fetch!(records, name) 291 | record_info.data_attr_name 292 | end 293 | 294 | 295 | # Returns a list of field names (as atoms) for the given Erlang record name. 296 | 297 | def record_field_names(%ModuleData{records: records}, name) do 298 | record_info = Map.fetch!(records, name) 299 | Enum.map(record_info.fields, fn {name, _type} -> name end) 300 | end 301 | 302 | 303 | # Passes the given function to Enum.map over the records. Each function 304 | # call is passed the Erlang name of the record, and the list of fields 305 | # as a list of {name, type} tuples, where name is an atom and type is the 306 | # Erlang type AST. 307 | 308 | def map_records(%ModuleData{records: records}, func) do 309 | Enum.map(records, fn {name, info} -> func.(name, info.fields) end) 310 | end 311 | 312 | 313 | # Returns the name of the attribute tracking definition of the given 314 | # Erlang record name. 315 | 316 | def tracking_attr_name(%ModuleData{macros: macros}, name) do 317 | Map.fetch!(macros, name).define_tracker 318 | end 319 | 320 | 321 | # Returns a list of {name, define_tracker_attribute} for all macros that 322 | # require init. 323 | 324 | def macros_that_need_init(%ModuleData{macros: macros}) do 325 | macros 326 | |> Enum.filter(fn 327 | {_, %MacroData{requires_init: true}} -> true 328 | _ -> false 329 | end) 330 | |> Enum.map(fn {name, %MacroData{define_tracker: define_tracker}} -> 331 | {name, define_tracker} 332 | end) 333 | end 334 | 335 | 336 | end 337 | -------------------------------------------------------------------------------- /lib/erl2ex/pipeline/names.ex: -------------------------------------------------------------------------------- 1 | # This module knows about things like reserved words and other special names, 2 | # and what names are allowed in what contexts. 3 | 4 | defmodule Erl2ex.Pipeline.Names do 5 | 6 | @moduledoc false 7 | 8 | 9 | # These are not allowed as names of functions or variables. 10 | # The converter will attempt to rename things that use one of these names. 11 | # If an exported function uses one of these names, it will require special 12 | # handling in both definition and calling. 13 | 14 | @elixir_reserved_words [ 15 | :after, 16 | :and, 17 | :catch, 18 | :do, 19 | :else, 20 | :end, 21 | :false, 22 | :fn, 23 | :nil, 24 | :not, 25 | :or, 26 | :rescue, 27 | :true, 28 | :unquote, 29 | :unquote_splicing, 30 | :when, 31 | :__CALLER__, 32 | :__DIR__, 33 | :__ENV__, 34 | :__MODULE__, 35 | :__aliases__, 36 | :__block__ 37 | ] |> Enum.into(MapSet.new) 38 | 39 | 40 | # These are not allowed as qualified function names because Elixir's parser 41 | # treats them specially. Calling functions with these names requires using 42 | # Kernel.apply. 43 | 44 | @elixir_uncallable_functions [ 45 | :unquote, 46 | :unquote_splicing 47 | ] |> Enum.into(MapSet.new) 48 | 49 | 50 | # This is a map of Erlang BIFs to equivalent Elixir functions. 51 | 52 | @bif_map %{ 53 | abs: :abs, 54 | apply: :apply, 55 | bit_size: :bit_size, 56 | byte_size: :byte_size, 57 | hd: :hd, 58 | is_atom: :is_atom, 59 | is_binary: :is_binary, 60 | is_bitstring: :is_bitstring, 61 | is_boolean: :is_boolean, 62 | is_float: :is_float, 63 | is_function: :is_function, 64 | is_integer: :is_integer, 65 | is_list: :is_list, 66 | is_map: :is_map, 67 | is_number: :is_number, 68 | is_pid: :is_pid, 69 | is_port: :is_port, 70 | is_record: {Record, :is_record}, 71 | is_reference: :is_reference, 72 | is_tuple: :is_tuple, 73 | length: :length, 74 | make_ref: :make_ref, 75 | map_size: :map_size, 76 | max: :max, 77 | min: :min, 78 | node: :node, 79 | round: :round, 80 | self: :self, 81 | throw: :throw, 82 | tl: :tl, 83 | trunc: :trunc, 84 | tuple_size: :tuple_size 85 | } 86 | 87 | 88 | # These names are allowed as names of functions or variables, but clash with 89 | # Elixir special forms. 90 | # The converter will allow variables with these names, but will attempt to 91 | # rename private functions. Exported functions will not be renamed, and will 92 | # be defined normally, but calling them will require full qualification. 93 | 94 | @elixir_special_forms [ 95 | :alias, 96 | :case, 97 | :cond, 98 | :for, 99 | :import, 100 | :quote, 101 | :receive, 102 | :require, 103 | :super, 104 | :try, 105 | :with 106 | ] |> Enum.into(MapSet.new) 107 | 108 | 109 | # These names are allowed as names of functions or variables, but clash with 110 | # auto-imported functions/macros from Kernel. 111 | # The converter will allow variables with these names, but will attempt to 112 | # rename private functions. Exported functions will not be renamed, and will 113 | # be defined normally, but calling them (and calling the Kernel functions of 114 | # the same name) will require full qualification. 115 | 116 | @elixir_auto_imports [ 117 | # Kernel functions 118 | abs: 1, 119 | apply: 2, 120 | apply: 3, 121 | binary_part: 3, 122 | bit_size: 1, 123 | byte_size: 1, 124 | div: 2, 125 | elem: 2, 126 | exit: 1, 127 | function_exported?: 3, 128 | get_and_update_in: 3, 129 | get_in: 2, 130 | hd: 1, 131 | inspect: 2, 132 | is_atom: 1, 133 | is_binary: 1, 134 | is_bitstring: 1, 135 | is_boolean: 1, 136 | is_float: 1, 137 | is_function: 1, 138 | is_function: 2, 139 | is_integer: 1, 140 | is_list: 1, 141 | is_map: 1, 142 | is_number: 1, 143 | is_pid: 1, 144 | is_port: 1, 145 | is_reference: 1, 146 | is_tuple: 1, 147 | length: 1, 148 | macro_exported?: 3, 149 | make_ref: 0, 150 | map_size: 1, 151 | max: 2, 152 | min: 2, 153 | node: 0, 154 | node: 1, 155 | not: 1, 156 | put_elem: 3, 157 | put_in: 3, 158 | rem: 2, 159 | round: 1, 160 | self: 0, 161 | send: 2, 162 | spawn: 1, 163 | spawn: 3, 164 | spawn_link: 1, 165 | spawn_link: 3, 166 | spawn_monitor: 1, 167 | spawn_monitor: 3, 168 | struct: 2, 169 | struct!: 2, 170 | throw: 1, 171 | tl: 1, 172 | trunc: 1, 173 | tuple_size: 1, 174 | update_in: 3, 175 | 176 | # Kernel macros 177 | alias!: 1, 178 | and: 2, 179 | binding: 1, 180 | def: 2, 181 | defdelegate: 2, 182 | defexception: 1, 183 | defimpl: 3, 184 | defmacro: 2, 185 | defmacrop: 2, 186 | defmodule: 2, 187 | defoverridable: 1, 188 | defp: 2, 189 | defprotocol: 2, 190 | defstruct: 1, 191 | destructure: 2, 192 | get_and_update_in: 2, 193 | if: 2, 194 | in: 2, 195 | is_nil: 1, 196 | match?: 2, 197 | or: 2, 198 | put_in: 2, 199 | raise: 1, 200 | raise: 2, 201 | reraise: 2, 202 | reraise: 3, 203 | sigil_C: 2, 204 | sigil_R: 2, 205 | sigil_S: 2, 206 | sigil_W: 2, 207 | sigil_c: 2, 208 | sigil_r: 2, 209 | sigil_s: 2, 210 | sigil_w: 2, 211 | to_char_list: 1, 212 | to_string: 1, 213 | unless: 2, 214 | update_in: 2, 215 | use: 2, 216 | var!: 2, 217 | ] |> Enum.reduce(%{}, fn({k, v}, m) -> 218 | Map.update(m, k, [], fn list -> [v | list] end) 219 | end) 220 | 221 | 222 | # Attributes that have a semantic meaning to Erlang. 223 | 224 | @special_attribute_names [ 225 | :callback, 226 | :else, 227 | :endif, 228 | :export, 229 | :export_type, 230 | :file, 231 | :ifdef, 232 | :ifndef, 233 | :include, 234 | :include_lib, 235 | :module, 236 | :opaque, 237 | :record, 238 | :spec, 239 | :type, 240 | :undef, 241 | ] |> Enum.into(MapSet.new) 242 | 243 | 244 | def elixir_reserved_words, do: @elixir_reserved_words 245 | 246 | def elixir_auto_imports, do: @elixir_auto_imports 247 | 248 | 249 | # Returns true if the given name is an attribute with a semantic meaning 250 | # to Erlang. 251 | 252 | def special_attr_name?(name), do: 253 | MapSet.member?(@special_attribute_names, name) 254 | 255 | 256 | # Returns true if the given name can be a function called by name using 257 | # normal qualified syntax. e.g. :foo, :def, and :nil are callable because you 258 | # can say Kernel.nil(). However, :"9foo" is not callable. If a function name 259 | # is not callable, you have to use Kernel.apply() to call it. 260 | 261 | def callable_function_name?(name), do: 262 | Regex.match?(~r/^[_a-z]\w*$/, Atom.to_string(name)) and 263 | not MapSet.member?(@elixir_uncallable_functions, name) 264 | 265 | 266 | # Returns true if the given name can be a function defined using "def" 267 | # syntax. If a function name is not deffable, you have to use a macro to 268 | # muck with the AST in order to define it. 269 | 270 | def deffable_function_name?(name), do: 271 | callable_function_name?(name) and not MapSet.member?(@elixir_reserved_words, name) 272 | 273 | 274 | # Returns true if the given function name can be called without module 275 | # qualification. 276 | 277 | def local_callable_function_name?(name), do: 278 | deffable_function_name?(name) and not MapSet.member?(@elixir_special_forms, name) 279 | 280 | 281 | # Returns {:ok, elixir_module, elixir_func} or :error depending on whether 282 | # the given Erlang BIF corresponds to an Elixir autoimport. 283 | 284 | def map_bif(name) do 285 | case Map.fetch(@bif_map, name) do 286 | {:ok, {mod, func}} -> {:ok, mod, func} 287 | {:ok, kernel_func} -> {:ok, Kernel, kernel_func} 288 | :error -> :error 289 | end 290 | end 291 | 292 | end 293 | -------------------------------------------------------------------------------- /lib/erl2ex/pipeline/parse.ex: -------------------------------------------------------------------------------- 1 | # This is the first phase of the pipeline. It parses the input file string 2 | # using both the standard Erlang parser (erlparse) and the alternate 3 | # generalized parser (epp_dodger). Both parsers have their strengths and 4 | # weaknesses, and we expect to use data from both sources. 5 | 6 | defmodule Erl2ex.Pipeline.Parse do 7 | 8 | @moduledoc false 9 | 10 | 11 | # Takes a string as input, and returns the forms in the file as a list 12 | # of {erlparse_form, epp_dodger_form} tuples. 13 | # 14 | # Currently, an error from either parser will cause the entire parse to fail; 15 | # however, later we expect to relax that behavior because some valid Erlang 16 | # code is expected to fail erlparse (due to preprocessor syntax). At that 17 | # time, we will return nil for failed erlparse forms. 18 | 19 | def string(str, opts \\ []) do 20 | charlist = generate_charlist(str) 21 | erl_forms = parse_erl_forms(charlist, opts) 22 | ext_forms = parse_ext_forms(charlist, opts) 23 | Enum.zip(erl_forms, ext_forms) 24 | end 25 | 26 | 27 | # Converts the given string to charlist (expected by Erlang's parsers) and 28 | # make sure it ends with a newline (also expected by Erlang's parsers). 29 | 30 | defp generate_charlist(str) do 31 | str = if String.ends_with?(str, "\n"), do: str, else: str <> "\n" 32 | String.to_charlist(str) 33 | end 34 | 35 | 36 | #### The erl_parse parser #### 37 | 38 | 39 | # Runs erl_parse on the given charlist, returning a list of forms. 40 | 41 | defp parse_erl_forms(charlist, opts) do 42 | charlist 43 | |> generate_token_group_stream 44 | |> Stream.map(&preprocess_tokens_for_erl/1) 45 | |> Stream.map(&(parse_erl_form(&1, opts))) 46 | |> Enum.to_list 47 | end 48 | 49 | 50 | # Given a charlist, runs erl_scan, grouping tokens into forms, and returns a 51 | # stream of those token lists (one list per form). 52 | 53 | defp generate_token_group_stream(charlist) do 54 | {charlist, 1} 55 | |> Stream.unfold(fn {ch, pos} -> 56 | case :erl_scan.tokens([], ch, pos, [:return_comments]) do 57 | {:done, {:ok, tokens, npos}, nch} -> {tokens, {nch, npos}} 58 | _ -> nil 59 | end 60 | end) 61 | end 62 | 63 | 64 | # Preprocesses tokens for erl_parse. Does the following mapping to simulate 65 | # the preprocessor: 66 | # 67 | # * Two "?"s followed by an atom or variable is interpreted as a stringify 68 | # operator and turned into the variable "??#{name}". This is otherwise 69 | # an illegal variable name, so we can detect it reliably during conversion. 70 | # * A single "?" followed by an atom or variable is interpreted as a macro 71 | # invocation and turned into the variable "?#{name}". This is otherwise 72 | # an illegal variable name, so we can detect it reliably during conversion. 73 | # * Comments are stripped because erlparse doesn't like them. 74 | 75 | defp preprocess_tokens_for_erl(form_tokens), do: 76 | preprocess_tokens_for_erl(form_tokens, []) 77 | 78 | defp preprocess_tokens_for_erl([], result), do: Enum.reverse(result) 79 | defp preprocess_tokens_for_erl([{:"?", _}, {:"?", _}, {:atom, line, name} | tail], result), do: 80 | preprocess_tokens_for_erl(tail, [{:var, line, :"??#{name}"} | result]) 81 | defp preprocess_tokens_for_erl([{:"?", _}, {:"?", _}, {:var, line, name} | tail], result), do: 82 | preprocess_tokens_for_erl(tail, [{:var, line, :"??#{name}"} | result]) 83 | defp preprocess_tokens_for_erl([{:"?", _}, {:atom, line, name} | tail], result), do: 84 | preprocess_tokens_for_erl(tail, [{:var, line, :"?#{name}"} | result]) 85 | defp preprocess_tokens_for_erl([{:"?", _}, {:var, line, name} | tail], result), do: 86 | preprocess_tokens_for_erl(tail, [{:var, line, :"?#{name}"} | result]) 87 | defp preprocess_tokens_for_erl([{:comment, _, _} | tail], result), do: 88 | preprocess_tokens_for_erl(tail, result) 89 | defp preprocess_tokens_for_erl([tok | tail], result), do: 90 | preprocess_tokens_for_erl(tail, [tok | result]) 91 | 92 | 93 | # Parses a single form for erl_parse. We detect certain preprocessor cases up 94 | # front because erl_parse itself doesn't like the format. 95 | 96 | # This clause handles define directives. 97 | defp parse_erl_form([{:-, line} | defn_tokens = [{:atom, _, :define} | _]], opts) do 98 | parse_erl_define(line, defn_tokens, opts) 99 | end 100 | 101 | # This clause handles other directives that take an argument (like ifdef) and 102 | # creates a pseudo attribute node. 103 | defp parse_erl_form([{:-, line} | defn_tokens = [{:atom, _, directive} | _]], opts) 104 | when directive == :ifdef or directive == :ifndef or directive == :undef do 105 | [{:call, _, {:atom, _, ^directive}, [value]}] = 106 | defn_tokens |> :erl_parse.parse_exprs |> handle_erl_parse_result(opts) 107 | {:attribute, line, directive, value} 108 | end 109 | 110 | # This clause handles other directives that take no argument (like endif) and 111 | # creates a pseudo attribute node. 112 | defp parse_erl_form([{:-, line}, {:atom, _, directive}, {:dot, _}], _opts) do 113 | {:attribute, line, directive} 114 | end 115 | 116 | # This clause handles any other form by passing it to erl_parse. 117 | defp parse_erl_form(form_tokens, opts) do 118 | form_tokens |> :erl_parse.parse_form |> handle_erl_parse_result(opts) 119 | end 120 | 121 | 122 | # Parser for define directives. We have to handle some of the parsing 123 | # manually because a define may define a guard or a multi-expression 124 | # replacement that has separate clauses separated by commas or semicolons, 125 | # which erl_parser doesn't like in an attribute. 126 | 127 | # Cases with no arguments. 128 | defp parse_erl_define(line, [{:atom, _, :define}, {:"(", _}, name_token, {:",", dot_line} | replacement_tokens], opts) do 129 | parse_erl_define(line, [name_token, {:dot, dot_line}], replacement_tokens, opts) 130 | end 131 | 132 | # Cases with arguments. 133 | defp parse_erl_define(line, [{:atom, _, :define}, {:"(", _} | remaining_tokens = [_, {:"(", _} | _]], opts) do 134 | close_paren_index = remaining_tokens 135 | |> Enum.find_index(fn 136 | {:")", _} -> true 137 | _ -> false 138 | end) 139 | {macro_tokens, [{:",", dot_line} | replacement_tokens]} = Enum.split(remaining_tokens, close_paren_index + 1) 140 | parse_erl_define(line, macro_tokens ++ [{:dot, dot_line}], replacement_tokens, opts) 141 | end 142 | 143 | 144 | # The core of define parsing. Parse the macro name and replacement separately. 145 | # If parsing of the replacement fails, it's probably because there are 146 | # commas or semicolons and it should be treated as a guard. To parse that 147 | # case, we pretend it is a guard, and embed it in a fake function AST. 148 | # Pass that AST to erl_parse, and then extract the parsed guards. 149 | 150 | defp parse_erl_define(line, macro_tokens, replacement_tokens, opts) do 151 | macro_expr = macro_tokens 152 | |> :erl_parse.parse_exprs 153 | |> handle_erl_parse_result(opts) 154 | |> hd 155 | replacement_tokens = replacement_tokens |> List.delete_at(-2) 156 | replacement_exprs = case :erl_parse.parse_exprs(replacement_tokens) do 157 | {:ok, asts} -> [asts] 158 | {:error, _} -> 159 | {guard_tokens, [{:dot, dot_line}]} = Enum.split(replacement_tokens, -1) 160 | temp_form_tokens = [{:atom, 1, :foo}, {:"(", 1}, {:")", 1}, {:when, 1}] ++ 161 | guard_tokens ++ 162 | [{:"->", dot_line}, {:atom, dot_line, :ok}, {:dot, dot_line}] 163 | {:ok, {:function, _, :foo, 0, [{:clause, _, [], guards, _}]}} = :erl_parse.parse_form(temp_form_tokens) 164 | guards 165 | end 166 | {:define, line, macro_expr, replacement_exprs} 167 | end 168 | 169 | 170 | # Handle results of erl_parse, raising an appropriate CompileError on error. 171 | 172 | defp handle_erl_parse_result({:ok, ast}, _opts), do: ast 173 | 174 | defp handle_erl_parse_result({:error, {line, :erl_parse, messages = [h | _]}}, opts) when is_list(h) do 175 | raise CompileError, 176 | file: Keyword.get(opts, :cur_file_path, "(unknown source file)"), 177 | line: line, 178 | description: Enum.join(messages) 179 | end 180 | 181 | defp handle_erl_parse_result({:error, {line, :erl_parse, messages}}, opts) do 182 | raise CompileError, 183 | file: Keyword.get(opts, :cur_file_path, "(unknown source file)"), 184 | line: line, 185 | description: inspect(messages) 186 | end 187 | 188 | defp handle_erl_parse_result(info, opts) do 189 | raise CompileError, 190 | file: Keyword.get(opts, :cur_file_path, "(unknown source file)"), 191 | line: :unknown, 192 | description: "Unknown error: #{inspect(info)}" 193 | end 194 | 195 | 196 | #### The epp_dodger parser #### 197 | 198 | 199 | # Entry point for this parser. Takes a charlist and returns a list of trees. 200 | 201 | defp parse_ext_forms(charlist, opts) do 202 | comments = :erl_comment_scan.string(charlist) 203 | {:ok, io} = charlist 204 | |> preprocess_charlist_for_ext 205 | |> List.to_string 206 | |> StringIO.open 207 | case :epp_dodger.parse(io) do 208 | {:ok, forms} -> 209 | reconcile_comments(forms, comments) 210 | {:error, {line, _, desc}} -> 211 | raise CompileError, 212 | file: Keyword.get(opts, :cur_file_path, "(unknown source file)"), 213 | line: line, 214 | description: desc 215 | end 216 | end 217 | 218 | 219 | # The epp_dodger parser doesn't like the "??" stringification operator. 220 | # We preprocess the inputs by converting those to a single "?" operator 221 | # followed by an atom beginning with "??". This form is the recognized 222 | # specially by the converter as a pseudo-macro call. 223 | 224 | defp preprocess_charlist_for_ext(charlist) do 225 | {charlist, 1} 226 | |> Stream.unfold(fn {ch, pos} -> 227 | case :erl_scan.tokens([], ch, pos) do 228 | {:done, {:ok, tokens, npos}, nch} -> {tokens, {nch, npos}} 229 | _ -> nil 230 | end 231 | end) 232 | |> Enum.to_list 233 | |> List.flatten 234 | |> preprocess_tokens_for_ext([]) 235 | |> unscan 236 | end 237 | 238 | defp preprocess_tokens_for_ext([], result), do: Enum.reverse(result) 239 | defp preprocess_tokens_for_ext([{:"?", _}, {:"?", _}, {type, line, name} | tail], result) 240 | when type == :atom or type == :var do 241 | preprocess_tokens_for_ext(tail, [{:atom, line, :"??#{name}"}, {:"?", line} | result]) 242 | end 243 | defp preprocess_tokens_for_ext([tok | tail], result), do: 244 | preprocess_tokens_for_ext(tail, [tok | result]) 245 | 246 | 247 | # In order to do the above preprocessing, we have to scan the input, 248 | # preprocess, and then unscan back into a charlist. It turns out that while 249 | # epp_dodger.tokens_to_string could do this, it doesn't preserve newlines 250 | # correctly, which is necessary to reconstruct the correct line numbers for 251 | # error messages. So I lifted the below implementation from epp_dodger and 252 | # modified it to preserve newlines. 253 | 254 | defp unscan(list) do 255 | list 256 | |> Enum.flat_map_reduce(1, fn tok, last_line -> 257 | line = elem(tok, 1) 258 | {generate_newlines(line - last_line) ++ unscan_token(tok), line} 259 | end) 260 | |> elem(0) 261 | end 262 | 263 | defp unscan_token({:atom, _, a}), do: :io_lib.write_atom(a) ++ ' ' 264 | defp unscan_token({:string, _, s}), do: :io_lib.write_string(s) ++ ' ' 265 | defp unscan_token({:char, _, c}), do: :io_lib.write_char(c) ++ ' ' 266 | defp unscan_token({:float, _, f}), do: :erlang.float_to_list(f) ++ ' ' 267 | defp unscan_token({:integer, _, n}), do: :erlang.integer_to_list(n) ++ ' ' 268 | defp unscan_token({:var, _, a}), do: :erlang.atom_to_list(a) ++ ' ' 269 | defp unscan_token({:dot, _}), do: '. ' 270 | defp unscan_token({:., _}), do: '.' 271 | defp unscan_token({a, _}), do: :erlang.atom_to_list(a) ++ ' ' 272 | 273 | defp generate_newlines(0), do: [] 274 | defp generate_newlines(i), do: [?\n | generate_newlines(i - 1)] 275 | 276 | 277 | # Combine comments back into the given forms and return the modified forms. 278 | # Ideally, this would invoke erl_recomment, but that doesn't seem to be 279 | # working correctly. Need to investigate why. 280 | 281 | defp reconcile_comments(forms, _comments) do 282 | # TODO 283 | forms 284 | end 285 | 286 | end 287 | -------------------------------------------------------------------------------- /lib/erl2ex/pipeline/utils.ex: -------------------------------------------------------------------------------- 1 | # A set of internal utility functions. 2 | 3 | defmodule Erl2ex.Pipeline.Utils do 4 | 5 | @moduledoc false 6 | 7 | 8 | # Generates a name given a suggested "base" name, and a set of blacklisted names. 9 | # If the suggested name is blacklisted, appends a number to get a unique name. 10 | 11 | def find_available_name(basename, used_names), do: 12 | find_available_name(to_string(basename), used_names, "", 0) 13 | 14 | 15 | # Generates a name given a suggested "base" name, and a set of blacklisted names. 16 | # If the suggested name is blacklisted, prepends the given prefix. If that name 17 | # is also blacklisted, appends a number to the prefix. 18 | 19 | def find_available_name(basename, used_names, prefix), do: 20 | find_available_name(to_string(basename), used_names, prefix, 1) 21 | 22 | 23 | # Generates a name given a suggested "base" name, and a set of blacklisted names. 24 | # If the suggested name is blacklisted, prepends the given prefix. If that name 25 | # is also blacklisted, appends a number to the prefix. You can specify the first 26 | # number to use. 0 indicates no number (i.e. it will try the prefix by itself). 27 | 28 | def find_available_name(basename, used_names, prefix, val) do 29 | suggestion = suggest_name(basename, prefix, val) 30 | set = MapSet.new(used_names) 31 | if MapSet.member?(set, suggestion) do 32 | find_available_name(basename, used_names, prefix, val + 1) 33 | else 34 | suggestion 35 | end 36 | end 37 | 38 | 39 | # Given a string, returns it with the first letter lowercased. Generally used 40 | # to convert variable names to Elixir form. 41 | 42 | def lower_str("_"), do: "_" 43 | def lower_str(<< "_" :: utf8, rest :: binary >>), do: 44 | << "_" :: utf8, lower_str(rest) :: binary >> 45 | def lower_str(<< first :: utf8, rest :: binary >>), do: 46 | << String.downcase(<< first >>) :: binary, rest :: binary >> 47 | 48 | 49 | # Same as lower_str/1 but takes an atom and returns an atom. 50 | 51 | def lower_atom(atom), do: 52 | atom |> Atom.to_string |> lower_str |> String.to_atom 53 | 54 | 55 | # Internal function used by find_available_name, to suggest a name. 56 | 57 | defp suggest_name(basename, _, 0), do: 58 | String.to_atom(basename) 59 | defp suggest_name(basename, "", val), do: 60 | String.to_atom("#{basename}#{val + 1}") 61 | defp suggest_name(<< "_" :: utf8, basename :: binary >>, prefix, 1), do: 62 | String.to_atom("_#{prefix}_#{basename}") 63 | defp suggest_name(basename, prefix, 1), do: 64 | String.to_atom("#{prefix}_#{basename}") 65 | defp suggest_name(<< "_" :: utf8, basename :: binary >>, prefix, val), do: 66 | String.to_atom("_#{prefix}#{val}_#{basename}") 67 | defp suggest_name(basename, prefix, val), do: 68 | String.to_atom("#{prefix}#{val}_#{basename}") 69 | 70 | end 71 | -------------------------------------------------------------------------------- /lib/erl2ex/results.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule Erl2ex.Results do 3 | 4 | @moduledoc """ 5 | Erl2ex.Results defines the structure of result data returned from most 6 | functions in the Erl2ex module. 7 | """ 8 | 9 | 10 | alias Erl2ex.Results 11 | 12 | 13 | defmodule File do 14 | 15 | @moduledoc """ 16 | Erl2ex.Results.File defines the result data structure for a particular file. 17 | """ 18 | 19 | defstruct( 20 | input_path: nil, 21 | output_path: nil, 22 | error: nil 23 | ) 24 | 25 | 26 | @typedoc """ 27 | The conversion results of a single file. 28 | 29 | * `input_path` is the path to the input Erlang file, or nil if the input 30 | is a string 31 | * `output_path` is the path to the output Elixir file, or nil if the 32 | output is a string. 33 | * `error` is the CompileError if a fatal error happened, or nil if the 34 | conversion was successful. 35 | """ 36 | 37 | @type t :: %__MODULE__{ 38 | input_path: Path.t | nil, 39 | output_path: Path.t | nil, 40 | error: %CompileError{} | nil 41 | } 42 | end 43 | 44 | 45 | defstruct( 46 | files: [] 47 | ) 48 | 49 | 50 | @typedoc """ 51 | Overall results for an entire conversion job of one or more files. 52 | """ 53 | 54 | @type t :: %__MODULE__{ 55 | files: [Results.File.t] 56 | } 57 | 58 | 59 | @doc """ 60 | Returns true if the entire conversion was successful, meaning no file 61 | resulted in an error. 62 | """ 63 | 64 | @spec success?(Results.t | Results.File.t) :: boolean 65 | 66 | def success?(%Results{files: files}), do: 67 | not Enum.any?(files, &get_error/1) 68 | def success?(%Results.File{error: nil}), do: true 69 | def success?(%Results.File{}), do: false 70 | 71 | 72 | @doc """ 73 | Returns the error that caused a conversion to fail, or nil if the conversion 74 | was successful. If more than one fatal error was detected, one error is 75 | returned but it is undefined which one is chosen. 76 | """ 77 | 78 | @spec get_error(Results.t | Results.File.t) :: %CompileError{} | nil 79 | 80 | def get_error(%Results{files: files}), do: 81 | Enum.find_value(files, &get_error/1) 82 | def get_error(%Results.File{error: err}), do: err 83 | 84 | 85 | @doc """ 86 | If the conversion failed, throw the error that caused the failure. Otherwise 87 | return the results. 88 | """ 89 | 90 | @spec throw_error(a) :: a when a: Results.t 91 | 92 | def throw_error(results) do 93 | case get_error(results) do 94 | nil -> results 95 | err -> throw(err) 96 | end 97 | end 98 | 99 | 100 | end 101 | -------------------------------------------------------------------------------- /lib/erl2ex/results_collector.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule Erl2ex.Results.Collector do 3 | 4 | @moduledoc """ 5 | Erl2ex.Results.Collector is a process that accumulates results of a 6 | conversion run. 7 | """ 8 | 9 | 10 | alias Erl2ex.Results 11 | 12 | 13 | @typedoc """ 14 | The ProcessID of a results collector process. 15 | """ 16 | 17 | @type t :: pid() 18 | 19 | 20 | @typedoc """ 21 | A file identifier, which may be a filesystem path or a symbolic id. 22 | """ 23 | 24 | @type file_id :: Path.t | atom 25 | 26 | 27 | @doc """ 28 | Starts a result collector and returns its PID. 29 | """ 30 | 31 | @spec start_link(list) :: t 32 | 33 | def start_link(opts \\ []) do 34 | {:ok, pid} = GenServer.start_link(__MODULE__, opts) 35 | pid 36 | end 37 | 38 | 39 | @doc """ 40 | Record that a conversion was successful for the given input and output paths. 41 | """ 42 | 43 | @spec put_success(t, file_id, file_id) :: :ok | {:error, term} 44 | 45 | def put_success(results, input_path, output_path) do 46 | GenServer.call(results, {:success, input_path, output_path}) 47 | end 48 | 49 | 50 | @doc """ 51 | Record that a conversion was unsuccessful for the given input path. 52 | """ 53 | 54 | @spec put_error(t, file_id, %CompileError{}) :: :ok | {:error, term} 55 | 56 | def put_error(results, input_path, error) do 57 | GenServer.call(results, {:error, input_path, error}) 58 | end 59 | 60 | 61 | @doc """ 62 | Returns the results for the given input path. 63 | """ 64 | 65 | @spec get_file(t, file_id) :: {:ok, Results.File.t} | {:error, term} 66 | 67 | def get_file(results, path) do 68 | GenServer.call(results, {:get_file, path}) 69 | end 70 | 71 | 72 | @doc """ 73 | Returns the results for the entire conversion so far. 74 | """ 75 | 76 | @spec get(t) :: Results.t 77 | 78 | def get(results) do 79 | GenServer.call(results, {:get}) 80 | end 81 | 82 | 83 | @doc """ 84 | Stops the collector process. 85 | """ 86 | 87 | @spec stop(t) :: :ok 88 | 89 | def stop(results) do 90 | GenServer.cast(results, {:stop}) 91 | end 92 | 93 | 94 | use GenServer 95 | 96 | defmodule State do 97 | @moduledoc false 98 | defstruct( 99 | data: %{}, 100 | allow_overwrite: false 101 | ) 102 | end 103 | 104 | 105 | def init(opts) do 106 | state = %State{ 107 | allow_overwrite: Keyword.get(opts, :allow_overwrite, false) 108 | } 109 | {:ok, state} 110 | end 111 | 112 | 113 | def handle_call({:success, input_path, output_path}, _from, state) do 114 | if not state.allow_overwrite and Map.has_key?(state.data, input_path) do 115 | {:reply, {:error, :file_exists}, state} 116 | else 117 | file = %Results.File{ 118 | input_path: input_path, 119 | output_path: output_path 120 | } 121 | state = %State{state | data: Map.put(state.data, input_path, file)} 122 | {:reply, :ok, state} 123 | end 124 | end 125 | 126 | def handle_call({:error, input_path, error}, _from, state) do 127 | if not state.allow_overwrite and Map.has_key?(state.data, input_path) do 128 | {:reply, {:error, :file_exists}, state} 129 | else 130 | file = %Results.File{ 131 | input_path: input_path, 132 | error: error 133 | } 134 | state = %State{state | data: Map.put(state.data, input_path, file)} 135 | {:reply, :ok, state} 136 | end 137 | end 138 | 139 | def handle_call({:get_file, input_path}, _from, %State{data: data} = state) do 140 | reply = case Map.fetch(data, input_path) do 141 | {:ok, file} -> {:ok, file} 142 | :error -> {:error, :not_found} 143 | end 144 | {:reply, reply, state} 145 | end 146 | 147 | def handle_call({:get}, _from, %State{data: data} = state) do 148 | {:reply, %Results{files: Map.values(data)}, state} 149 | end 150 | 151 | 152 | def handle_cast({:stop}, state) do 153 | {:stop, :normal, state} 154 | end 155 | 156 | end 157 | -------------------------------------------------------------------------------- /lib/erl2ex/sink.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule Erl2ex.Sink do 3 | 4 | @moduledoc """ 5 | Erl2ex.Sink is a process that consumes generated Elixir source, normally 6 | writing files to the file system. 7 | """ 8 | 9 | 10 | @typedoc """ 11 | The ProcessID of a sink process. 12 | """ 13 | 14 | @type t :: pid() 15 | 16 | 17 | @doc """ 18 | Starts a sink and returns its PID. 19 | """ 20 | 21 | @spec start_link(list) :: t 22 | 23 | def start_link(opts \\ []) do 24 | {:ok, pid} = GenServer.start_link(__MODULE__, opts) 25 | pid 26 | end 27 | 28 | 29 | @doc """ 30 | Writes data to a sink, at the given path. 31 | """ 32 | 33 | @spec write(t, Erl2ex.file_id, String.t) :: :ok | {:error, term} 34 | 35 | def write(sink, path, str) do 36 | GenServer.call(sink, {:write, path, str}) 37 | end 38 | 39 | 40 | @doc """ 41 | Gets the file contents written to the given ID. 42 | 43 | Available only if the `allow_get` configuration is in effect. 44 | """ 45 | 46 | @spec get_string(t, Erl2ex.file_id) :: {:ok, String.t} | {:error, term} 47 | 48 | def get_string(sink, path) do 49 | GenServer.call(sink, {:get_string, path}) 50 | end 51 | 52 | 53 | @doc """ 54 | Returns whether the given file identifier has been written to. 55 | """ 56 | 57 | @spec path_written?(t, Erl2ex.file_id) :: boolean 58 | 59 | def path_written?(sink, path) do 60 | GenServer.call(sink, {:path_written, path}) 61 | end 62 | 63 | 64 | @doc """ 65 | Stops the sink process. 66 | """ 67 | 68 | @spec stop(t) :: :ok 69 | 70 | def stop(sink) do 71 | GenServer.cast(sink, {:stop}) 72 | end 73 | 74 | 75 | use GenServer 76 | 77 | defmodule State do 78 | @moduledoc false 79 | defstruct( 80 | dest_dir: nil, 81 | data: %{}, 82 | allow_get: false, 83 | allow_overwrite: false 84 | ) 85 | end 86 | 87 | 88 | def init(opts) do 89 | state = %State{ 90 | dest_dir: Keyword.get(opts, :dest_dir, nil), 91 | allow_get: Keyword.get(opts, :allow_get, false), 92 | allow_overwrite: Keyword.get(opts, :allow_overwrite, false) 93 | } 94 | {:ok, state} 95 | end 96 | 97 | 98 | def handle_call({:write, path, str}, _from, state) do 99 | if not state.allow_overwrite and Map.has_key?(state.data, path) do 100 | {:reply, {:error, :file_exists}, state} 101 | else 102 | if state.dest_dir != nil do 103 | File.open(Path.expand(path, state.dest_dir), [:write], fn io -> 104 | IO.binwrite(io, str) 105 | end) 106 | end 107 | str = if state.allow_get, do: str, else: nil 108 | state = %State{state | data: Map.put(state.data, path, str)} 109 | {:reply, :ok, state} 110 | end 111 | end 112 | 113 | def handle_call({:get_string, _path}, _from, %State{allow_get: nil} = state) do 114 | {:reply, {:error, :not_supported}, state} 115 | end 116 | 117 | def handle_call({:get_string, path}, _from, %State{data: data} = state) do 118 | result = case Map.fetch(data, path) do 119 | {:ok, str} -> {:ok, str} 120 | :error -> {:error, :not_found} 121 | end 122 | {:reply, result, state} 123 | end 124 | 125 | def handle_call({:path_written, path}, _from, %State{data: data} = state) do 126 | {:reply, Map.has_key?(data, path), state} 127 | end 128 | 129 | 130 | def handle_cast({:stop}, state) do 131 | {:stop, :normal, state} 132 | end 133 | 134 | end 135 | -------------------------------------------------------------------------------- /lib/erl2ex/source.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule Erl2ex.Source do 3 | 4 | @moduledoc """ 5 | Erl2ex.Source is a process that produces Erlang source, normally reading 6 | files from the file system. 7 | """ 8 | 9 | 10 | @typedoc """ 11 | The ProcessID of a source process. 12 | """ 13 | 14 | @type t :: pid() 15 | 16 | 17 | @doc """ 18 | Starts a source and returns its PID. 19 | """ 20 | 21 | @spec start_link(list) :: t 22 | 23 | def start_link(opts) do 24 | {:ok, pid} = GenServer.start_link(__MODULE__, opts) 25 | pid 26 | end 27 | 28 | 29 | @doc """ 30 | Reads the source file at the given path or symbolic location, and returns a 31 | tuple comprising the data in the file and the full path to it. 32 | """ 33 | 34 | @spec read_source(t, Erl2ex.file_id) :: {String.t, Erl2ex.file_id} 35 | 36 | def read_source(source, path) do 37 | source 38 | |> GenServer.call({:read_source, path}) 39 | |> handle_result 40 | end 41 | 42 | 43 | @doc """ 44 | Reads the include file at the given path, given a context directory, and 45 | returns a tuple comprising the data in the file and the full path to it. 46 | """ 47 | 48 | @spec read_include(t, Path.t, Path.t | nil) :: {String.t, Path.t} 49 | 50 | def read_include(source, path, cur_dir) do 51 | source 52 | |> GenServer.call({:read_include, path, cur_dir}) 53 | |> handle_result 54 | end 55 | 56 | 57 | @doc """ 58 | Reads the include file at the given path, given a context library, and 59 | returns a tuple comprising the data in the file and the full path to it. 60 | """ 61 | 62 | @spec read_lib_include(t, atom, Path.t) :: {String.t, Path.t} 63 | 64 | def read_lib_include(source, lib, path) do 65 | source 66 | |> GenServer.call({:read_lib_include, lib, path}) 67 | |> handle_result 68 | end 69 | 70 | 71 | @doc """ 72 | Stops the source process. 73 | """ 74 | 75 | @spec stop(t) :: :ok 76 | 77 | def stop(source) do 78 | GenServer.cast(source, {:stop}) 79 | end 80 | 81 | 82 | defp handle_result({:ok, data, path}), do: {data, path} 83 | defp handle_result({:error, code, path}) do 84 | raise CompileError, 85 | file: path, 86 | line: :unknown, 87 | description: "Error #{code} while reading source file" 88 | end 89 | 90 | 91 | use GenServer 92 | 93 | defmodule State do 94 | @moduledoc false 95 | defstruct( 96 | source_dir: nil, 97 | source_data: %{}, 98 | include_dirs: [], 99 | include_data: %{}, 100 | lib_dirs: %{}, 101 | lib_data: %{} 102 | ) 103 | end 104 | 105 | 106 | def init(opts) do 107 | source_dir = Keyword.get(opts, :source_dir, nil) 108 | source_data = opts 109 | |> Keyword.get_values(:source_data) 110 | |> Enum.reduce(%{}, &(add_to_map(&2, &1))) 111 | include_dirs = opts 112 | |> Keyword.get_values(:include_dir) 113 | |> Enum.reduce([], &([&1 | &2])) 114 | include_data = opts 115 | |> Keyword.get_values(:include_data) 116 | |> Enum.reduce(%{}, &(add_to_map(&2, &1))) 117 | lib_dirs = opts 118 | |> Keyword.get_values(:lib_dir) 119 | |> Enum.reduce(%{}, &(add_to_map(&2, &1))) 120 | lib_data = opts 121 | |> Keyword.get_values(:lib_data) 122 | |> Enum.reduce(%{}, &(add_to_map(&2, &1))) 123 | 124 | {:ok, 125 | %State{ 126 | source_dir: source_dir, 127 | source_data: source_data, 128 | include_dirs: include_dirs, 129 | include_data: include_data, 130 | lib_dirs: lib_dirs, 131 | lib_data: lib_data, 132 | } 133 | } 134 | end 135 | 136 | 137 | def handle_call( 138 | {:read_source, path}, 139 | _from, 140 | %State{source_dir: source_dir, source_data: source_data} = state) 141 | do 142 | dirs = if source_dir == nil, do: [], else: [source_dir] 143 | result = read_impl(path, source_data, dirs) 144 | {:reply, result, state} 145 | end 146 | 147 | def handle_call( 148 | {:read_include, path, cur_dir}, 149 | _from, 150 | %State{include_dirs: include_dirs, include_data: include_data} = state) 151 | do 152 | dirs = 153 | if cur_dir == nil do 154 | include_dirs 155 | else 156 | [cur_dir | include_dirs] 157 | end 158 | dirs = [File.cwd! | dirs] 159 | result = read_impl(path, include_data, dirs) 160 | {:reply, result, state} 161 | end 162 | 163 | def handle_call( 164 | {:read_lib_include, lib, path}, 165 | _from, 166 | %State{lib_data: lib_data, lib_dirs: lib_dirs} = state) 167 | do 168 | case get_lib_dir(lib_dirs, lib) do 169 | {:error, code} -> 170 | {:reply, {:error, code, path}, state} 171 | {:ok, lib_dir} -> 172 | result = read_impl(path, lib_data, [lib_dir]) 173 | {:reply, result, state} 174 | end 175 | end 176 | 177 | 178 | def handle_cast({:stop}, state) do 179 | {:stop, :normal, state} 180 | end 181 | 182 | 183 | defp read_impl(path, data_map, search_dirs) do 184 | case Map.fetch(data_map, path) do 185 | {:ok, data} when is_binary(data) -> 186 | {:ok, data, path} 187 | {:ok, io} when is_pid(io) -> 188 | data = io |> IO.read(:all) |> IO.chardata_to_string 189 | {:ok, data, path} 190 | :error -> 191 | Enum.find_value(search_dirs, {:error, :not_found, path}, fn dir -> 192 | actual_path = Path.expand(path, dir) 193 | if File.exists?(actual_path) do 194 | case File.read(actual_path) do 195 | {:ok, data} -> {:ok, data, actual_path} 196 | {:error, code} -> {:error, code, path} 197 | end 198 | else 199 | false 200 | end 201 | end) 202 | end 203 | end 204 | 205 | 206 | defp get_lib_dir(lib_dirs, lib) do 207 | case Map.fetch(lib_dirs, lib) do 208 | {:ok, dir} -> 209 | {:ok, dir} 210 | :error -> 211 | case :code.lib_dir(lib) do 212 | {:error, code} -> {:error, code} 213 | dir -> {:ok, dir} 214 | end 215 | end 216 | end 217 | 218 | 219 | defp add_to_map(map, value) when is_map(value), do: 220 | Map.merge(map, value) 221 | defp add_to_map(map, {key, value}), do: 222 | Map.put(map, key, value) 223 | defp add_to_map(map, value), do: 224 | Map.put(map, nil, value) 225 | 226 | end 227 | -------------------------------------------------------------------------------- /lib/mix/tasks/erl2ex.ex: -------------------------------------------------------------------------------- 1 | # Mix task for erl2ex 2 | 3 | defmodule Mix.Tasks.Erl2ex do 4 | 5 | @moduledoc Erl2ex.Cli.usage_text("mix erl2ex") 6 | 7 | @shortdoc "Transpiles Erlang source to Elixir" 8 | 9 | use Mix.Task 10 | 11 | 12 | def run(args) do 13 | Erl2ex.Cli.main(args) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Erl2ex.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :erl2ex, 7 | version: "0.0.10", 8 | elixir: "~> 1.4", 9 | name: "Erl2ex", 10 | source_url: "https://github.com/dazuma/erl2ex", 11 | build_embedded: Mix.env() == :prod, 12 | start_permanent: Mix.env() == :prod, 13 | escript: [main_module: Erl2ex.Cli], 14 | deps: deps(), 15 | docs: docs(), 16 | description: description(), 17 | package: package() 18 | ] 19 | end 20 | 21 | def application do 22 | [applications: [:logger]] 23 | end 24 | 25 | defp deps do 26 | [ 27 | {:earmark, "~> 1.0", only: :dev}, 28 | {:ex_doc, "~> 0.13", only: :dev}, 29 | {:credo, "~> 0.4", only: :dev} 30 | ] 31 | end 32 | 33 | defp docs do 34 | [ 35 | extras: ["README.md", "LICENSE.md", "CHANGELOG.md"] 36 | ] 37 | end 38 | 39 | defp description do 40 | """ 41 | Erl2ex is an Erlang to Elixir transpiler, converting well-formed Erlang 42 | source to Elixir source with equivalent functionality. 43 | """ 44 | end 45 | 46 | defp package do 47 | [ 48 | files: ["lib", "mix.exs", "README.md", "LICENSE.md", "CHANGELOG.md"], 49 | maintainers: ["Daniel Azuma"], 50 | licenses: ["BSD"], 51 | links: %{"GitHub" => "https://github.com/dazuma/erl2ex"} 52 | ] 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, 2 | "credo": {:hex, :credo, "0.4.7", "1516ebd3c6099eff74ed0ef50637f0a43113c3f65338a9e1792efc03dff34855", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, 3 | "earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []}, 4 | "ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}} 5 | -------------------------------------------------------------------------------- /test/e2e_test.exs: -------------------------------------------------------------------------------- 1 | 2 | defmodule E2ETest do 3 | use ExUnit.Case 4 | 5 | import Erl2ex.TestHelper 6 | 7 | 8 | # Libraries that are working 9 | 10 | 11 | @tag :e2e 12 | test "erlware_commons" do 13 | download_project("erlware_commons", "https://github.com/erlware/erlware_commons.git") 14 | clean_dir("erlware_commons", "src_ex") 15 | convert_dir("erlware_commons", "src", "src_ex", 16 | include_dir: project_path("erlware_commons", "include"), 17 | auto_export_suffix: "_test_", 18 | auto_export_suffix: "_test") 19 | copy_dir("erlware_commons", "test", "src_ex") 20 | compile_dir("erlware_commons", "src_ex", 21 | display_output: true, 22 | DEFINE_namespaced_types: "true") 23 | run_eunit_tests( 24 | [:ec_plists], 25 | "erlware_commons", "src_ex", display_output: true) 26 | end 27 | 28 | 29 | @tag :e2e 30 | test "getopt" do 31 | download_project("getopt", "https://github.com/jcomellas/getopt.git") 32 | clean_dir("getopt", "src_ex") 33 | convert_dir("getopt", "src", "src_ex") 34 | copy_dir("getopt", "test", "src_ex") 35 | compile_dir("getopt", "src_ex", display_output: true) 36 | run_eunit_tests( 37 | [:getopt_test], 38 | "getopt", "src_ex", display_output: true) 39 | end 40 | 41 | 42 | @tag :e2e 43 | test "gproc" do 44 | download_project("gproc", "https://github.com/uwiger/gproc.git") 45 | clean_dir("gproc", "src_ex") 46 | convert_dir("gproc", "src", "src_ex", 47 | include_dir: project_path("gproc", "include"), 48 | auto_export_suffix: "_test_", 49 | auto_export_suffix: "_test") 50 | copy_dir("gproc", "test", "src_ex", ["gproc_tests.erl", "gproc_test_lib.erl"]) 51 | copy_file("gproc", "src/gproc.app.src", "src_ex/gproc.app") 52 | compile_dir("gproc", "src_ex", display_output: true) 53 | run_eunit_tests( 54 | [:gproc_tests], 55 | "gproc", "src_ex", display_output: true) 56 | end 57 | 58 | 59 | @tag :e2e 60 | test "idna" do 61 | download_project("idna", "https://github.com/benoitc/erlang-idna.git") 62 | clean_dir("idna", "src_ex") 63 | convert_dir("idna", "src", "src_ex") 64 | copy_dir("idna", "test", "src_ex") 65 | compile_dir("idna", "src_ex", display_output: true) 66 | run_eunit_tests( 67 | [:idna_test], 68 | "idna", "src_ex", display_output: true) 69 | end 70 | 71 | 72 | @tag :e2e 73 | test "jsx" do 74 | download_project("jsx", "https://github.com/talentdeficit/jsx.git") 75 | clean_dir("jsx", "src_ex") 76 | convert_dir("jsx", "src", "src_ex", auto_export_suffix: "_test_") 77 | compile_dir("jsx", "src_ex", display_output: true) 78 | run_eunit_tests( 79 | [ 80 | :jsx, 81 | :jsx_config, 82 | :jsx_decoder, 83 | :jsx_encoder, 84 | :jsx_parser, 85 | :jsx_to_json, 86 | :jsx_to_term, 87 | :jsx_verify 88 | ], 89 | "jsx", "src_ex", display_output: true) 90 | end 91 | 92 | 93 | @tag :e2e 94 | test "mochiweb" do 95 | download_project("mochiweb", "https://github.com/mochi/mochiweb.git") 96 | clean_dir("mochiweb", "src_ex") 97 | convert_dir("mochiweb", "src", "src_ex", 98 | include_dir: project_path("mochiweb", "include")) 99 | copy_dir("mochiweb", "test", "src_ex") 100 | compile_dir("mochiweb", "src_ex", display_output: true) 101 | run_eunit_tests( 102 | [ 103 | :mochiweb_base64url_tests, 104 | :mochiweb_html_tests, 105 | :mochiweb_http_tests, 106 | :mochiweb_request_tests, 107 | :mochiweb_socket_server_tests, 108 | :mochiweb_tests, 109 | :mochiweb_websocket_tests 110 | ], 111 | "mochiweb", "src_ex", display_output: true) 112 | end 113 | 114 | 115 | @tag :e2e 116 | test "poolboy" do 117 | download_project("poolboy", "https://github.com/devinus/poolboy.git") 118 | clean_dir("poolboy", "src_ex") 119 | convert_dir("poolboy", "src", "src_ex") 120 | copy_dir("poolboy", "test", "src_ex") 121 | compile_dir("poolboy", "src_ex", display_output: true) 122 | run_eunit_tests( 123 | [:poolboy], 124 | "poolboy", "src_ex", display_output: true) 125 | end 126 | 127 | 128 | @tag :e2e 129 | test "ranch" do 130 | download_project("ranch", "https://github.com/ninenines/ranch.git") 131 | clean_dir("ranch", "src_ex") 132 | convert_dir("ranch", "src", "src_ex") 133 | compile_dir("ranch", "src_ex", display_output: true) 134 | # Not sure how to run tests 135 | end 136 | 137 | 138 | # Libraries that are not yet working 139 | 140 | 141 | # Fails because a macro appears as a record name. 142 | @tag :skip 143 | test "bbmustache" do 144 | download_project("bbmustache", "https://github.com/soranoba/bbmustache.git") 145 | clean_dir("bbmustache", "src_ex") 146 | convert_dir("bbmustache", "src", "src_ex") 147 | copy_dir("bbmustache", "test", "src_ex") 148 | compile_dir("bbmustache", "src_ex", display_output: true) 149 | run_eunit_tests( 150 | [:bbmustache_tests], 151 | "bbmustache", "src_ex", display_output: true) 152 | end 153 | 154 | 155 | # Fails because cowlib fails 156 | @tag :skip 157 | test "cowboy" do 158 | download_project("cowboy", "https://github.com/ninenines/cowboy.git") 159 | download_project("cowlib", "https://github.com/ninenines/cowlib.git") 160 | download_project("ranch", "https://github.com/ninenines/ranch.git") 161 | clean_dir("cowboy", "src_ex") 162 | convert_dir("cowboy", "src", "src_ex", 163 | lib_dir: %{cowlib: project_path("cowlib"), ranch: project_path("ranch")}) 164 | compile_dir("cowboy", "src_ex", display_output: true) 165 | # Not sure how to run tests 166 | end 167 | 168 | 169 | # Fails because a comprehension contains a macro invocation 170 | @tag :skip 171 | test "cowlib" do 172 | download_project("ranch", "https://github.com/ninenines/cowlib.git") 173 | clean_dir("cowlib", "src_ex") 174 | convert_dir("cowlib", "src", "src_ex", 175 | include_dir: project_path("cowlib", "include")) 176 | compile_dir("cowlib", "src_ex", display_output: true) 177 | # Not sure how to run tests 178 | end 179 | 180 | 181 | # In progress. 182 | @tag :skip 183 | @tag timeout: 300000 184 | test "elixir" do 185 | download_project("elixir", "https://github.com/elixir-lang/elixir.git") 186 | 187 | File.rm_rf!(project_path("elixir", "lib/elixir/ebin")) 188 | clean_dir("elixir", "lib/elixir/src2") 189 | clean_dir("elixir", "lib/elixir/src_ex") 190 | 191 | # Run yecc on the elixir_parser source. 192 | copy_file("elixir", "lib/elixir/src/elixir_parser.yrl", "lib/elixir/src2/elixir_parser.yrl") 193 | run_cmd("erl", ["-run", "yecc", "file", "elixir_parser", "-run", "init", "stop", "-noshell"], 194 | name: "elixir", path: "lib/elixir/src2") 195 | 196 | # Convert 197 | convert_dir("elixir", "lib/elixir/src", "lib/elixir/src_ex") 198 | convert_dir("elixir", "lib/elixir/src2", "lib/elixir/src_ex") 199 | 200 | # elixir_bootstrap.erl generates __info__ functions so can't be converted for now 201 | File.rm!(project_path("elixir", "lib/elixir/src_ex/elixir_bootstrap.ex")) 202 | File.cp!(project_path("elixir", "lib/elixir/src/elixir_bootstrap.erl"), 203 | project_path("elixir", "lib/elixir/src_ex/elixir_bootstrap.erl")) 204 | 205 | # Create a dummy elixir_parser.erl so rebar doesn't generate it. 206 | create_file("elixir", "lib/elixir/src/elixir_parser.erl", 207 | """ 208 | -module(elixir_parser). 209 | """) 210 | 211 | # Converted code needs Enum.reduce 212 | create_file("elixir", "lib/elixir/src_ex/Elixir.Enum.erl", 213 | """ 214 | -module('Elixir.Enum'). 215 | -export([reduce/3]). 216 | reduce(List, Acc, Fn) -> lists:foldl(Fn, Acc, List). 217 | """) 218 | 219 | # Compile each elixir file separately; otherwise newly compiled modules will 220 | # be added to the VM, causing compatibility issues between old and new. 221 | compile_dir_individually("elixir", "lib/elixir/src_ex", 222 | display_output: true, display_cmd: true, output: "../ebin") 223 | 224 | run_cmd("make", ["lib/elixir/src/elixir.app.src"], name: "elixir") 225 | run_cmd(project_path("elixir", "rebar"), ["compile"], name: "elixir", path: "lib/elixir") 226 | 227 | run_cmd("make", ["test"], name: "elixir") 228 | end 229 | 230 | 231 | # Fails because a binary literal contains a macro invocation returning a list. 232 | @tag :skip 233 | test "eredis" do 234 | download_project("eredis", "https://github.com/wooga/eredis.git") 235 | clean_dir("eredis", "src_ex") 236 | convert_dir("eredis", "src", "src_ex", 237 | include_dir: project_path("eredis", "include")) 238 | copy_dir("eredis", "test", "src_ex") 239 | compile_dir("eredis", "src_ex", display_output: true) 240 | run_eunit_tests( 241 | [:eredis_parser_tests, :eredis_sub_tests, :eredis_tests], 242 | "eredis", "src_ex", display_output: true) 243 | end 244 | 245 | 246 | # Fails because a fully qualified macro appears in a typespec 247 | @tag :skip 248 | test "goldrush" do 249 | download_project("goldrush", "https://github.com/DeadZen/goldrush.git") 250 | clean_dir("goldrush", "src_ex") 251 | convert_dir("goldrush", "src", "src_ex", 252 | auto_export_suffix: "_test_", 253 | auto_export_suffix: "_test") 254 | compile_dir("goldrush", "src_ex", display_output: true) 255 | run_eunit_tests( 256 | [:glc], 257 | "goldrush", "src_ex", display_output: true) 258 | end 259 | 260 | 261 | # Fails because of a strange missing file in the public_key application. 262 | @tag :skip 263 | test "ssl_verify_fun" do 264 | download_project("ssl_verify_fun", "https://github.com/deadtrickster/ssl_verify_fun.erl.git") 265 | clean_dir("ssl_verify_fun", "src_ex") 266 | convert_dir("ssl_verify_fun", "src", "src_ex") 267 | copy_dir("ssl_verify_fun", "test", "src_ex") 268 | compile_dir("ssl_verify_fun", "src_ex", display_output: true) 269 | run_eunit_tests( 270 | [:ssl_verify_fingerprint_tests, :ssl_verify_hostname_tests, :ssl_verify_pk_tests], 271 | "ssl_verify_fun", "src_ex", display_output: true) 272 | end 273 | 274 | end 275 | -------------------------------------------------------------------------------- /test/error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ErrorTest do 2 | use ExUnit.Case 3 | 4 | @opts [emit_file_headers: false] 5 | 6 | 7 | test "Erlang parse error" do 8 | input = """ 9 | foo() -> 10 | bar(. 11 | """ 12 | 13 | expected = %CompileError{ 14 | file: "(unknown source file)", 15 | line: 2, 16 | description: "syntax error before: '.'" 17 | } 18 | 19 | assert Erl2ex.convert_str(input, @opts) == {:error, expected} 20 | end 21 | 22 | 23 | end 24 | -------------------------------------------------------------------------------- /test/expression_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExpressionTest do 2 | use ExUnit.Case 3 | 4 | import Erl2ex.TestHelper 5 | 6 | 7 | @opts [emit_file_headers: false] 8 | 9 | 10 | test "Various value types" do 11 | input = """ 12 | -export([foo/1]). 13 | foo(A) -> [atom, 123, 3.14, {A, {}, {hello, "world"}}, [1, [], 2]]. 14 | """ 15 | 16 | expected = """ 17 | def foo(a) do 18 | [:atom, 123, 3.14, {a, {}, {:hello, 'world'}}, [1, [], 2]] 19 | end 20 | """ 21 | 22 | result = test_conversion(input, @opts) 23 | assert result.output == expected 24 | assert apply(result.module, :foo, [:x]) == [:atom, 123, 3.14, {:x, {}, {:hello, 'world'}}, [1, [], 2]] 25 | end 26 | 27 | 28 | test "Character values" do 29 | input = """ 30 | -export([foo/0]). 31 | foo() -> $A + $🐱 + $\\n + $". 32 | """ 33 | 34 | expected = """ 35 | def foo() do 36 | ?A + ?🐱 + ?\\n + ?" 37 | end 38 | """ 39 | 40 | result = test_conversion(input, @opts) 41 | assert result.output == expected 42 | assert apply(result.module, :foo, []) == 128158 43 | end 44 | 45 | 46 | test "String escaping" do 47 | input = """ 48 | foo() -> 49 | "hi\#{1}", 50 | "\\n\\s". 51 | """ 52 | 53 | expected = """ 54 | defp foo() do 55 | 'hi\\\#{1}' 56 | '\\n ' 57 | end 58 | """ 59 | 60 | assert Erl2ex.convert_str!(input, @opts) == expected 61 | end 62 | 63 | 64 | test "Variable case conversion" do 65 | input = """ 66 | foo(_A, __B, _) -> 1. 67 | """ 68 | 69 | expected = """ 70 | defp foo(_a, __b, _) do 71 | 1 72 | end 73 | """ 74 | 75 | assert Erl2ex.convert_str!(input, @opts) == expected 76 | end 77 | 78 | 79 | test "Matches" do 80 | input = """ 81 | foo() -> {A, [B]} = {1, [2]}. 82 | """ 83 | 84 | expected = """ 85 | defp foo() do 86 | {a, [b]} = {1, [2]} 87 | end 88 | """ 89 | 90 | assert Erl2ex.convert_str!(input, @opts) == expected 91 | end 92 | 93 | 94 | test "List destructuring" do 95 | input = """ 96 | foo() -> [A, B | C] = X. 97 | """ 98 | 99 | expected = """ 100 | defp foo() do 101 | [a, b | c] = x 102 | end 103 | """ 104 | 105 | assert Erl2ex.convert_str!(input, @opts) == expected 106 | end 107 | 108 | 109 | test "Math operations" do 110 | input = """ 111 | foo() -> A + (B - C) / D * E, 112 | +A, 113 | -A, 114 | A div 1, 115 | A rem 1. 116 | """ 117 | 118 | expected = """ 119 | defp foo() do 120 | a + (b - c) / d * e 121 | +a 122 | -a 123 | div(a, 1) 124 | rem(a, 1) 125 | end 126 | """ 127 | 128 | assert Erl2ex.convert_str!(input, @opts) == expected 129 | end 130 | 131 | 132 | test "Comparison operations" do 133 | input = """ 134 | foo() -> 135 | A == 1, 136 | A /= 1, 137 | A =< 1, 138 | A >= 1, 139 | A < 1, 140 | A > 1, 141 | A =:= 1, 142 | A =/= 1. 143 | """ 144 | 145 | expected = """ 146 | defp foo() do 147 | a == 1 148 | a != 1 149 | a <= 1 150 | a >= 1 151 | a < 1 152 | a > 1 153 | a === 1 154 | a !== 1 155 | end 156 | """ 157 | 158 | assert Erl2ex.convert_str!(input, @opts) == expected 159 | end 160 | 161 | 162 | test "Logic and misc operations" do 163 | input = """ 164 | foo() -> 165 | not A, 166 | A orelse B, 167 | A andalso B, 168 | A and B, 169 | A or B, 170 | A xor B, 171 | A ++ B, 172 | A -- B, 173 | A ! B. 174 | """ 175 | 176 | expected = """ 177 | defp foo() do 178 | not(a) 179 | a or b 180 | a and b 181 | :erlang.and(a, b) 182 | :erlang.or(a, b) 183 | :erlang.xor(a, b) 184 | a ++ b 185 | a -- b 186 | send(a, b) 187 | end 188 | """ 189 | 190 | assert Erl2ex.convert_str!(input, @opts) == expected 191 | end 192 | 193 | 194 | test "Bitwise operations" do 195 | input = """ 196 | foo() -> 197 | A band B, 198 | A bor B, 199 | A bxor B, 200 | bnot A, 201 | A bsl 1, 202 | A bsr 1. 203 | """ 204 | 205 | expected = """ 206 | use Bitwise, only_operators: true 207 | 208 | 209 | defp foo() do 210 | a &&& b 211 | a ||| b 212 | a ^^^ b 213 | ~~~a 214 | a <<< 1 215 | a >>> 1 216 | end 217 | """ 218 | 219 | assert Erl2ex.convert_str!(input, @opts) == expected 220 | end 221 | 222 | 223 | test "Case statement" do 224 | input = """ 225 | foo() -> 226 | case A of 227 | {B, 1} when B, C; D == 2 -> 228 | E = 3, 229 | E; 230 | _ -> 2 231 | end. 232 | """ 233 | 234 | expected = """ 235 | defp foo() do 236 | case(a) do 237 | {b, 1} when b and c or d == 2 -> 238 | e = 3 239 | e 240 | _ -> 241 | 2 242 | end 243 | end 244 | """ 245 | 246 | assert Erl2ex.convert_str!(input, @opts) == expected 247 | end 248 | 249 | 250 | test "If statement" do 251 | input = """ 252 | foo() -> 253 | if 254 | B, C; D == 2 -> 255 | E = 3, 256 | E; 257 | true -> 2 258 | end. 259 | """ 260 | 261 | expected = """ 262 | defp foo() do 263 | case(:if) do 264 | :if when b and c or d == 2 -> 265 | e = 3 266 | e 267 | :if when true -> 268 | 2 269 | end 270 | end 271 | """ 272 | 273 | assert Erl2ex.convert_str!(input, @opts) == expected 274 | end 275 | 276 | 277 | test "If statement that generates errors" do 278 | input = """ 279 | foo() -> 280 | if 281 | hd([]) -> 1; 282 | true -> 2 283 | end. 284 | """ 285 | 286 | expected = """ 287 | defp foo() do 288 | case(:if) do 289 | :if when hd([]) -> 290 | 1 291 | :if when true -> 292 | 2 293 | end 294 | end 295 | """ 296 | 297 | assert Erl2ex.convert_str!(input, @opts) == expected 298 | end 299 | 300 | 301 | test "Receive statement" do 302 | input = """ 303 | foo() -> 304 | receive 305 | A when B, C; D == 2 -> 306 | E = 3, 307 | E; 308 | _ -> 2 309 | end. 310 | """ 311 | 312 | expected = """ 313 | defp foo() do 314 | receive() do 315 | a when b and c or d == 2 -> 316 | e = 3 317 | e 318 | _ -> 319 | 2 320 | end 321 | end 322 | """ 323 | 324 | assert Erl2ex.convert_str!(input, @opts) == expected 325 | end 326 | 327 | 328 | test "Receive with timeout" do 329 | input = """ 330 | foo() -> 331 | receive 332 | A -> ok 333 | after 334 | 100 -> err 335 | end. 336 | """ 337 | 338 | expected = """ 339 | defp foo() do 340 | receive() do 341 | a -> 342 | :ok 343 | after 344 | 100 -> 345 | :err 346 | end 347 | end 348 | """ 349 | 350 | assert Erl2ex.convert_str!(input, @opts) == expected 351 | end 352 | 353 | 354 | test "Simple fun" do 355 | input = """ 356 | foo() -> 357 | fun 358 | (X) when B, C; D == 2 -> 359 | E = 3, 360 | E; 361 | (_) -> 2 362 | end. 363 | """ 364 | 365 | expected = """ 366 | defp foo() do 367 | fn 368 | x when b and c or d == 2 -> 369 | e = 3 370 | e 371 | _ -> 372 | 2 373 | end 374 | end 375 | """ 376 | 377 | assert Erl2ex.convert_str!(input, @opts) == expected 378 | end 379 | 380 | 381 | test "Local fun capture" do 382 | input = """ 383 | -export([foo/0]). 384 | foo() -> fun double/1. 385 | double(A) -> A * 2. 386 | """ 387 | 388 | expected = """ 389 | def foo() do 390 | &double/1 391 | end 392 | 393 | 394 | defp double(a) do 395 | a * 2 396 | end 397 | """ 398 | 399 | result = test_conversion(input, @opts) 400 | assert result.output == expected 401 | f = apply(result.module, :foo, []) 402 | assert f.(4) == 8 403 | end 404 | 405 | 406 | test "Remote fun capture" do 407 | input = """ 408 | -export([foo/1]). 409 | foo(A) -> fun A:sqrt/1. 410 | """ 411 | 412 | expected = """ 413 | def foo(a) do 414 | &a.sqrt/1 415 | end 416 | """ 417 | 418 | result = test_conversion(input, @opts) 419 | assert result.output == expected 420 | f = apply(result.module, :foo, [:math]) 421 | assert f.(4) == 2.0 422 | end 423 | 424 | 425 | test "Remote fun capture with variables" do 426 | input = """ 427 | -export([foo/3]). 428 | foo(A, B, C) -> fun A:B/C. 429 | """ 430 | 431 | expected = """ 432 | def foo(a, b, c) do 433 | :erlang.make_fun(a, b, c) 434 | end 435 | """ 436 | 437 | result = test_conversion(input, @opts) 438 | assert result.output == expected 439 | f = apply(result.module, :foo, [:math, :sqrt, 1]) 440 | assert f.(4) == 2.0 441 | end 442 | 443 | 444 | test "Block" do 445 | input = """ 446 | foo() -> 447 | begin 448 | E = 3, 449 | E 450 | end. 451 | """ 452 | 453 | expected = """ 454 | defp foo() do 455 | ( 456 | e = 3 457 | e 458 | ) 459 | end 460 | """ 461 | 462 | assert Erl2ex.convert_str!(input, @opts) == expected 463 | end 464 | 465 | 466 | test "List comprehension" do 467 | input = """ 468 | foo(X) -> [A + B || A <- [1,2,3], B <- X, A /= B]. 469 | """ 470 | 471 | expected = """ 472 | defp foo(x) do 473 | for(a <- [1, 2, 3], b <- x, a != b, into: [], do: a + b) 474 | end 475 | """ 476 | 477 | assert Erl2ex.convert_str!(input, @opts) == expected 478 | end 479 | 480 | 481 | test "List comprehension with binary generator" do 482 | input = """ 483 | foo() -> [A + B || <> <= <<1, 2, 3, 4>>]. 484 | """ 485 | 486 | expected = """ 487 | defp foo() do 488 | for(<>)>>, into: [], do: a + b) 489 | end 490 | """ 491 | 492 | assert Erl2ex.convert_str!(input, @opts) == expected 493 | end 494 | 495 | 496 | test "List comprehension with no generator" do 497 | input = """ 498 | foo(X) -> [x || X]. 499 | """ 500 | 501 | expected = """ 502 | defp foo(x) do 503 | if(x) do 504 | for(into: [], do: :x) 505 | else 506 | [] 507 | end 508 | end 509 | """ 510 | 511 | assert Erl2ex.convert_str!(input, @opts) == expected 512 | end 513 | 514 | 515 | test "List comprehension starting with static qualifiers" do 516 | input = """ 517 | foo(X, Y, Z) -> [A || X, Y, Z, A <- [1,2], A > 1]. 518 | """ 519 | 520 | expected = """ 521 | defp foo(x, y, z) do 522 | if(x and y and z) do 523 | for(a <- [1, 2], a > 1, into: [], do: a) 524 | else 525 | [] 526 | end 527 | end 528 | """ 529 | 530 | assert Erl2ex.convert_str!(input, @opts) == expected 531 | end 532 | 533 | 534 | test "Binary comprehension with binary generator" do 535 | input = """ 536 | foo() -> << <> || <> <= <<1, 2, 3, 4>> >>. 537 | """ 538 | 539 | expected = """ 540 | defp foo() do 541 | for(<>)>>, into: <<>>, do: <>) 542 | end 543 | """ 544 | 545 | assert Erl2ex.convert_str!(input, @opts) == expected 546 | end 547 | 548 | 549 | test "Binary comprehension with no generator" do 550 | input = """ 551 | foo(X) -> << <<1>> || X>>. 552 | """ 553 | 554 | expected = """ 555 | defp foo(x) do 556 | if(x) do 557 | for(into: <<>>, do: <<1>>) 558 | else 559 | <<>> 560 | end 561 | end 562 | """ 563 | 564 | assert Erl2ex.convert_str!(input, @opts) == expected 565 | end 566 | 567 | 568 | test "Binary comprehension starting with static qualifiers" do 569 | input = """ 570 | foo(X, Y, Z) -> << <> || X, Y, Z, <> <= <<1,2>>, A > 1>>. 571 | """ 572 | 573 | expected = """ 574 | defp foo(x, y, z) do 575 | if(x and y and z) do 576 | for(<<(a <- <<1, 2>>)>>, a > 1, into: <<>>, do: <>) 577 | else 578 | <<>> 579 | end 580 | end 581 | """ 582 | 583 | assert Erl2ex.convert_str!(input, @opts) == expected 584 | end 585 | 586 | 587 | test "Map literal" do 588 | input = """ 589 | foo(X) -> \#{}, \#{a => X + 1, b => X - 1}. 590 | """ 591 | 592 | expected = """ 593 | defp foo(x) do 594 | %{} 595 | %{a: x + 1, b: x - 1} 596 | end 597 | """ 598 | 599 | assert Erl2ex.convert_str!(input, @opts) == expected 600 | end 601 | 602 | 603 | test "Map update with exact followed by inexact" do 604 | input = """ 605 | foo() -> M\#{a := 1, b := 2, c => 3}. 606 | """ 607 | 608 | expected = """ 609 | defp foo() do 610 | Map.merge(%{m | a: 1, b: 2}, %{c: 3}) 611 | end 612 | """ 613 | 614 | assert Erl2ex.convert_str!(input, @opts) == expected 615 | end 616 | 617 | 618 | test "Map update with inexact followed by exact" do 619 | input = """ 620 | foo() -> M\#{a => 1, b => 2, c := 3}. 621 | """ 622 | 623 | expected = """ 624 | defp foo() do 625 | %{Map.merge(m, %{a: 1, b: 2}) | c: 3} 626 | end 627 | """ 628 | 629 | assert Erl2ex.convert_str!(input, @opts) == expected 630 | end 631 | 632 | 633 | test "Empty map update" do 634 | input = """ 635 | foo() -> M\#{}. 636 | """ 637 | 638 | expected = """ 639 | defp foo() do 640 | m 641 | end 642 | """ 643 | 644 | assert Erl2ex.convert_str!(input, @opts) == expected 645 | end 646 | 647 | 648 | test "Map pattern match" do 649 | input = """ 650 | foo(M) -> \#{a := X, b := {1, Y}} = M. 651 | """ 652 | 653 | expected = """ 654 | defp foo(m) do 655 | %{a: x, b: {1, y}} = m 656 | end 657 | """ 658 | 659 | assert Erl2ex.convert_str!(input, @opts) == expected 660 | end 661 | 662 | 663 | test "Bitstring literal with no qualifiers" do 664 | input = """ 665 | foo() -> <<1, 2, "hello">>. 666 | """ 667 | 668 | expected = """ 669 | defp foo() do 670 | <<1, 2, "hello">> 671 | end 672 | """ 673 | 674 | assert Erl2ex.convert_str!(input, @opts) == expected 675 | end 676 | 677 | 678 | test "Bitstring literal with size expressions" do 679 | input = """ 680 | foo(A, B) -> <<1:10, 2:A>> = B. 681 | """ 682 | 683 | expected = """ 684 | defp foo(a, b) do 685 | <<1::10, 2::size(a)>> = b 686 | end 687 | """ 688 | 689 | assert Erl2ex.convert_str!(input, @opts) == expected 690 | end 691 | 692 | 693 | test "Bitstring literal with size expressions and explicit binary type" do 694 | input = """ 695 | -export([foo/1]). 696 | foo(A) -> 697 | <> = <<1, 2, 3, 4, 5>>, 698 | {B, C, D}. 699 | """ 700 | 701 | expected = """ 702 | def foo(a) do 703 | <> = <<1, 2, 3, 4, 5>> 704 | {b, c, d} 705 | end 706 | """ 707 | 708 | result = test_conversion(input, @opts) 709 | assert result.output == expected 710 | assert apply(result.module, :foo, [2]) == {<<1, 2>>, <<3, 4>>, <<5>>} 711 | 712 | assert Erl2ex.convert_str!(input, @opts) == expected 713 | end 714 | 715 | 716 | test "Bitstring literal with simple type specifiers" do 717 | input = """ 718 | foo() -> <<1/integer, 2/float, "hello"/utf16>>. 719 | """ 720 | 721 | expected = """ 722 | defp foo() do 723 | <<1::integer, 2::float, "hello"::utf16>> 724 | end 725 | """ 726 | 727 | assert Erl2ex.convert_str!(input, @opts) == expected 728 | end 729 | 730 | 731 | test "Bitstring literal with complex type specifiers" do 732 | input = """ 733 | foo() -> <<1:4/integer-signed-unit:4-native>>. 734 | """ 735 | 736 | expected = """ 737 | defp foo() do 738 | <<1::size(4)-integer-signed-unit(4)-native>> 739 | end 740 | """ 741 | 742 | assert Erl2ex.convert_str!(input, @opts) == expected 743 | end 744 | 745 | 746 | test "Bitstring pattern match" do 747 | input = """ 748 | foo(S) -> <> = S. 749 | """ 750 | 751 | expected = """ 752 | defp foo(s) do 753 | <> = s 754 | end 755 | """ 756 | 757 | assert Erl2ex.convert_str!(input, @opts) == expected 758 | end 759 | 760 | 761 | test "Try with all features" do 762 | input = """ 763 | foo() -> 764 | try 765 | X, Y 766 | of 767 | A -> A + 2 768 | catch 769 | throw:B when is_integer(B) -> B; 770 | C -> C; 771 | exit:D when D == 0 -> D; 772 | error:badarith -> E; 773 | Kind:H -> {Kind, H} 774 | after 775 | F, G 776 | end. 777 | """ 778 | 779 | expected = """ 780 | defp foo() do 781 | try() do 782 | x 783 | y 784 | catch 785 | (:throw, b) when is_integer(b) -> 786 | b 787 | :throw, c -> 788 | c 789 | (:exit, d) when d == 0 -> 790 | d 791 | :error, :badarith -> 792 | e 793 | kind, h -> 794 | {kind, h} 795 | else 796 | a -> 797 | a + 2 798 | after 799 | f 800 | g 801 | end 802 | end 803 | """ 804 | 805 | assert Erl2ex.convert_str!(input, @opts) == expected 806 | end 807 | 808 | 809 | test "Catch expression" do 810 | input = """ 811 | foo() -> 812 | catch A. 813 | """ 814 | 815 | expected = """ 816 | defp foo() do 817 | try() do 818 | a 819 | catch 820 | :throw, term -> 821 | term 822 | :exit, reason -> 823 | {:EXIT, reason} 824 | :error, reason -> 825 | {:EXIT, {reason, :erlang.get_stacktrace()}} 826 | end 827 | end 828 | """ 829 | 830 | assert Erl2ex.convert_str!(input, @opts) == expected 831 | end 832 | 833 | end 834 | -------------------------------------------------------------------------------- /test/files/files2/include2.hrl: -------------------------------------------------------------------------------- 1 | -define(INCLUDE2_CONST, 2). 2 | -------------------------------------------------------------------------------- /test/files/include1.hrl: -------------------------------------------------------------------------------- 1 | -define(INCLUDE1_CONST, 1). 2 | -include("files2/include2.hrl"). 3 | -------------------------------------------------------------------------------- /test/files/include3.hrl: -------------------------------------------------------------------------------- 1 | -define(INCLUDE3_CONST, 3). 2 | -------------------------------------------------------------------------------- /test/function_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FunctionTest do 2 | use ExUnit.Case 3 | 4 | import Erl2ex.TestHelper 5 | 6 | 7 | @opts [emit_file_headers: false] 8 | 9 | 10 | test "Single clause, single expr with no arguments or guards" do 11 | input = """ 12 | foo() -> hello. 13 | """ 14 | 15 | expected = """ 16 | defp foo() do 17 | :hello 18 | end 19 | """ 20 | 21 | assert Erl2ex.convert_str!(input, @opts) == expected 22 | end 23 | 24 | 25 | test "Single clause, single expr with arguments but no guards" do 26 | input = """ 27 | foo(A, B) -> A + B. 28 | """ 29 | 30 | expected = """ 31 | defp foo(a, b) do 32 | a + b 33 | end 34 | """ 35 | 36 | assert Erl2ex.convert_str!(input, @opts) == expected 37 | end 38 | 39 | 40 | test "Single clause, single expr with arguments and guards" do 41 | input = """ 42 | foo(A, B) when A, is_atom(B); A + B > 0 -> hello. 43 | """ 44 | 45 | expected = """ 46 | defp foo(a, b) when a and is_atom(b) or a + b > 0 do 47 | :hello 48 | end 49 | """ 50 | 51 | assert Erl2ex.convert_str!(input, @opts) == expected 52 | end 53 | 54 | 55 | test "Multi clause, multi expr" do 56 | input = """ 57 | foo(A) -> B = A + 1, B; 58 | foo({A}) -> B = A - 1, B. 59 | """ 60 | 61 | expected = """ 62 | defp foo(a) do 63 | b = a + 1 64 | b 65 | end 66 | 67 | defp foo({a}) do 68 | b = a - 1 69 | b 70 | end 71 | """ 72 | 73 | assert Erl2ex.convert_str!(input, @opts) == expected 74 | end 75 | 76 | 77 | test "Remote function calls" do 78 | input = """ 79 | foo() -> baz:bar(A). 80 | """ 81 | 82 | expected = """ 83 | defp foo() do 84 | :baz.bar(a) 85 | end 86 | """ 87 | 88 | assert Erl2ex.convert_str!(input, @opts) == expected 89 | end 90 | 91 | 92 | test "Remote function calls with an expression as the function name" do 93 | input = """ 94 | foo(A, B) -> baz:A(B). 95 | """ 96 | 97 | expected = """ 98 | defp foo(a, b) do 99 | :erlang.apply(:baz, a, [b]) 100 | end 101 | """ 102 | 103 | assert Erl2ex.convert_str!(input, @opts) == expected 104 | end 105 | 106 | 107 | test "Remote function calls with an expression as the module name" do 108 | input = """ 109 | foo(A, B) -> A:baz(B). 110 | """ 111 | 112 | expected = """ 113 | defp foo(a, b) do 114 | a.baz(b) 115 | end 116 | """ 117 | 118 | assert Erl2ex.convert_str!(input, @opts) == expected 119 | end 120 | 121 | 122 | test "Anonymous function calls" do 123 | input = """ 124 | foo(A, B) -> B(A). 125 | """ 126 | 127 | expected = """ 128 | defp foo(a, b) do 129 | b.(a) 130 | end 131 | """ 132 | 133 | assert Erl2ex.convert_str!(input, @opts) == expected 134 | end 135 | 136 | 137 | test "Unqualified function calls" do 138 | input = """ 139 | foo(A) -> bar(A). 140 | bar(A) -> statistics(A). 141 | baz(A) -> abs(A). 142 | """ 143 | 144 | expected = """ 145 | defp foo(a) do 146 | bar(a) 147 | end 148 | 149 | 150 | defp bar(a) do 151 | :erlang.statistics(a) 152 | end 153 | 154 | 155 | defp baz(a) do 156 | abs(a) 157 | end 158 | """ 159 | 160 | assert Erl2ex.convert_str!(input, @opts) == expected 161 | end 162 | 163 | 164 | test "Imported function calls" do 165 | input = """ 166 | -import(math, [pi/0, sin/1]). 167 | foo() -> pi(). 168 | bar(A) -> cos(A). 169 | baz(A) -> sin(A). 170 | """ 171 | 172 | expected = """ 173 | import :math, only: [pi: 0, sin: 1] 174 | 175 | 176 | defp foo() do 177 | pi() 178 | end 179 | 180 | 181 | defp bar(a) do 182 | :erlang.cos(a) 183 | end 184 | 185 | 186 | defp baz(a) do 187 | sin(a) 188 | end 189 | """ 190 | 191 | assert Erl2ex.convert_str!(input, @opts) == expected 192 | end 193 | 194 | 195 | test "Local private function name is reserved or has strange characters" do 196 | input = """ 197 | do() -> hello. 198 | 'E=mc^2'() -> bye. 199 | foo() -> do(), 'E=mc^2'(). 200 | """ 201 | 202 | expected = """ 203 | defp func_do() do 204 | :hello 205 | end 206 | 207 | 208 | defp func_E_mc_2() do 209 | :bye 210 | end 211 | 212 | 213 | defp foo() do 214 | func_do() 215 | func_E_mc_2() 216 | end 217 | """ 218 | 219 | assert Erl2ex.convert_str!(input, @opts) == expected 220 | end 221 | 222 | 223 | test "Local exported function name is reserved or has strange characters" do 224 | input = """ 225 | -export([do/0, unquote/0, 'E=mc^2'/0, foo/0]). 226 | do() -> hello. 227 | unquote() -> world. 228 | 'E=mc^2'() -> bye. 229 | foo() -> {do(), unquote(), 'E=mc^2'()}. 230 | """ 231 | 232 | expected = """ 233 | def unquote(:do)() do 234 | :hello 235 | end 236 | 237 | 238 | def unquote(:unquote)() do 239 | :world 240 | end 241 | 242 | 243 | def unquote(:"E=mc^2")() do 244 | :bye 245 | end 246 | 247 | 248 | def foo() do 249 | {__MODULE__.do(), Kernel.apply(__MODULE__, :unquote, []), Kernel.apply(__MODULE__, :"E=mc^2", [])} 250 | end 251 | """ 252 | 253 | result = test_conversion(input, @opts) 254 | assert result.output == expected 255 | assert apply(result.module, :do, []) == :hello 256 | assert apply(result.module, :unquote, []) == :world 257 | assert apply(result.module, :"E=mc^2", []) == :bye 258 | assert apply(result.module, :foo, []) == {:hello, :world, :bye} 259 | end 260 | 261 | 262 | test "Call to remote functions whose name is reserved or has strange characters" do 263 | input = """ 264 | foo() -> {blah:do(), blah:unquote(), blah:'E=mc^2'()}. 265 | """ 266 | 267 | expected = """ 268 | defp foo() do 269 | {:blah.do(), Kernel.apply(:blah, :unquote, []), Kernel.apply(:blah, :"E=mc^2", [])} 270 | end 271 | """ 272 | 273 | assert Erl2ex.convert_str!(input, @opts) == expected 274 | end 275 | 276 | 277 | test "Local private function name is a special form" do 278 | input = """ 279 | 'cond'(X) -> X. 280 | foo() -> 'cond'(a). 281 | """ 282 | 283 | expected = """ 284 | defp func_cond(x) do 285 | x 286 | end 287 | 288 | 289 | defp foo() do 290 | func_cond(:a) 291 | end 292 | """ 293 | 294 | assert Erl2ex.convert_str!(input, @opts) == expected 295 | end 296 | 297 | 298 | test "Local exported function name is a special form" do 299 | input = """ 300 | -export(['cond'/1, foo/0]). 301 | 'cond'(X) -> X. 302 | foo() -> 'cond'(a). 303 | """ 304 | 305 | expected = """ 306 | def cond(x) do 307 | x 308 | end 309 | 310 | 311 | def foo() do 312 | __MODULE__.cond(:a) 313 | end 314 | """ 315 | 316 | result = test_conversion(input, @opts) 317 | assert result.output == expected 318 | assert apply(result.module, :foo, []) == :a 319 | assert apply(result.module, :cond, [:b]) == :b 320 | end 321 | 322 | 323 | test "Local private function name conflicts with auto-imported function" do 324 | input = """ 325 | self() -> 1. 326 | foo() -> self(). 327 | """ 328 | 329 | expected = """ 330 | defp func_self() do 331 | 1 332 | end 333 | 334 | 335 | defp foo() do 336 | func_self() 337 | end 338 | """ 339 | 340 | assert Erl2ex.convert_str!(input, @opts) == expected 341 | end 342 | 343 | 344 | test "Local exported function name conflicts with auto-imported function" do 345 | input = """ 346 | -export([self/0, foo/0]). 347 | self() -> 1. 348 | foo() -> self(). 349 | """ 350 | 351 | expected = """ 352 | def self() do 353 | 1 354 | end 355 | 356 | 357 | def foo() do 358 | __MODULE__.self() 359 | end 360 | """ 361 | 362 | result = test_conversion(input, @opts) 363 | assert result.output == expected 364 | assert apply(result.module, :foo, []) == 1 365 | assert apply(result.module, :self, []) == 1 366 | end 367 | 368 | 369 | test "Imported function name is an elixir reserved word or special form" do 370 | input = """ 371 | -import(mymod, [do/0, 'cond'/1]). 372 | foo() -> do(), 'cond'(x). 373 | """ 374 | 375 | expected = """ 376 | import :mymod, only: [do: 0, cond: 1] 377 | 378 | 379 | defp foo() do 380 | :mymod.do() 381 | :mymod.cond(:x) 382 | end 383 | """ 384 | 385 | assert Erl2ex.convert_str!(input, @opts) == expected 386 | end 387 | 388 | 389 | test "Imported function name has illegal characters" do 390 | input = """ 391 | -import(mymod, ['minus-one'/1]). 392 | foo() -> 'minus-one'(x). 393 | """ 394 | 395 | expected = """ 396 | import :mymod, only: ["minus-one": 1] 397 | 398 | 399 | defp foo() do 400 | Kernel.apply(:mymod, :"minus-one", [:x]) 401 | end 402 | """ 403 | 404 | assert Erl2ex.convert_str!(input, @opts) == expected 405 | end 406 | 407 | 408 | test "Variable and function name clash" do 409 | input = """ 410 | foo() -> 1. 411 | bar(Foo) -> foo() + Foo. 412 | """ 413 | 414 | expected = """ 415 | defp foo() do 416 | 1 417 | end 418 | 419 | 420 | defp bar(var_foo) do 421 | foo() + var_foo 422 | end 423 | """ 424 | 425 | assert Erl2ex.convert_str!(input, @opts) == expected 426 | end 427 | 428 | 429 | test "Variable and function name clash beginning with underscore" do 430 | input = """ 431 | '_foo'() -> 1. 432 | bar(_Foo) -> '_foo'(). 433 | """ 434 | 435 | expected = """ 436 | defp _foo() do 437 | 1 438 | end 439 | 440 | 441 | defp bar(_var_foo) do 442 | _foo() 443 | end 444 | """ 445 | 446 | assert Erl2ex.convert_str!(input, @opts) == expected 447 | end 448 | 449 | 450 | test "Variable name clash with a BIF name" do 451 | input = """ 452 | foo() -> self(). 453 | bar(Self) -> Self. 454 | """ 455 | 456 | expected = """ 457 | defp foo() do 458 | self() 459 | end 460 | 461 | 462 | defp bar(var_self) do 463 | var_self 464 | end 465 | """ 466 | 467 | assert Erl2ex.convert_str!(input, @opts) == expected 468 | end 469 | 470 | 471 | test "Local exported function named 'send'" do 472 | input = """ 473 | -export([send/2]). 474 | send(X, Y) -> {X, Y}. 475 | foo(X, Y) -> X ! Y, send(X, Y). 476 | """ 477 | 478 | expected = """ 479 | def send(x, y) do 480 | {x, y} 481 | end 482 | 483 | 484 | defp foo(x, y) do 485 | Kernel.send(x, y) 486 | __MODULE__.send(x, y) 487 | end 488 | """ 489 | 490 | assert Erl2ex.convert_str!(input, @opts) == expected 491 | end 492 | 493 | 494 | test "Function pattern looks like keyword block" do 495 | input = """ 496 | foo([{do, a}, {else, b}]) -> ok. 497 | """ 498 | 499 | expected = """ 500 | defp foo(do: :a, else: :b) do 501 | :ok 502 | end 503 | """ 504 | 505 | assert Erl2ex.convert_str!(input, @opts) == expected 506 | end 507 | 508 | 509 | end 510 | -------------------------------------------------------------------------------- /test/scope_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ScopeTest do 2 | use ExUnit.Case 3 | 4 | @opts [emit_file_headers: false] 5 | 6 | 7 | test "Illegal variable names" do 8 | input = """ 9 | foo() -> 10 | Do = 1, 11 | Else = 2, 12 | End = 3, 13 | False = 4, 14 | Fn = 5, 15 | Nil = 6, 16 | True = 7. 17 | """ 18 | 19 | expected = """ 20 | defp foo() do 21 | var_do = 1 22 | var_else = 2 23 | var_end = 3 24 | var_false = 4 25 | var_fn = 5 26 | var_nil = 6 27 | var_true = 7 28 | end 29 | """ 30 | 31 | assert Erl2ex.convert_str!(input, @opts) == expected 32 | end 33 | 34 | 35 | test "Reference param var in toplevel function" do 36 | input = """ 37 | foo(A) -> 38 | A = 3. 39 | """ 40 | 41 | expected = """ 42 | defp foo(a) do 43 | ^a = 3 44 | end 45 | """ 46 | 47 | assert Erl2ex.convert_str!(input, @opts) == expected 48 | end 49 | 50 | 51 | test "Reference previously matched var in toplevel function" do 52 | input = """ 53 | foo(P, Q) -> 54 | [A] = P, 55 | {A} = Q. 56 | """ 57 | 58 | expected = """ 59 | defp foo(p, q) do 60 | [a] = p 61 | {^a} = q 62 | end 63 | """ 64 | 65 | assert Erl2ex.convert_str!(input, @opts) == expected 66 | end 67 | 68 | 69 | test "Reference vars previously matched in a conditional" do 70 | input = """ 71 | foo(P) -> 72 | case P of 73 | 1 -> A = 1, B = 1; 74 | B -> A = 2 75 | end, 76 | A = 0, 77 | B = 0. 78 | """ 79 | 80 | expected = """ 81 | defp foo(p) do 82 | case(p) do 83 | 1 -> 84 | a = 1 85 | b = 1 86 | b -> 87 | a = 2 88 | end 89 | ^a = 0 90 | ^b = 0 91 | end 92 | """ 93 | 94 | assert Erl2ex.convert_str!(input, @opts) == expected 95 | end 96 | 97 | 98 | test "Reference in a conditional a var previously matched outside" do 99 | input = """ 100 | foo(P) -> 101 | A = 2, 102 | case P of 103 | 1 -> A = 3 104 | end. 105 | """ 106 | 107 | expected = """ 108 | defp foo(p) do 109 | a = 2 110 | case(p) do 111 | 1 -> 112 | ^a = 3 113 | end 114 | end 115 | """ 116 | 117 | assert Erl2ex.convert_str!(input, @opts) == expected 118 | end 119 | 120 | 121 | test "Inner fun match does not export unless already declared in the surrounding scope" do 122 | input = """ 123 | foo() -> 124 | fun () -> A = 1 end, 125 | A = 2, 126 | fun () -> A = 3 end. 127 | """ 128 | 129 | expected = """ 130 | defp foo() do 131 | fn -> a = 1 end 132 | a = 2 133 | fn -> ^a = 3 end 134 | end 135 | """ 136 | 137 | assert Erl2ex.convert_str!(input, @opts) == expected 138 | end 139 | 140 | 141 | test "Inner fun param shadows external variable of the same name" do 142 | input = """ 143 | foo(A) -> fun (A) -> ok end. 144 | """ 145 | 146 | expected = """ 147 | defp foo(a) do 148 | fn a -> :ok end 149 | end 150 | """ 151 | 152 | assert Erl2ex.convert_str!(input, @opts) == expected 153 | end 154 | 155 | 156 | test "List comprehension param shadows external variable of the same name" do 157 | input = """ 158 | foo() -> 159 | [X || X <- [2,3]], 160 | X = 1, 161 | [X || X <- [2,3]]. 162 | """ 163 | 164 | expected = """ 165 | defp foo() do 166 | for(x <- [2, 3], into: [], do: x) 167 | x = 1 168 | for(x <- [2, 3], into: [], do: x) 169 | end 170 | """ 171 | 172 | assert Erl2ex.convert_str!(input, @opts) == expected 173 | end 174 | 175 | 176 | test "String comprehension param shadows external variable of the same name" do 177 | input = """ 178 | foo() -> 179 | << <> || <> <= <<2,3>> >>, 180 | X = 1, 181 | << <> || <> <= <<2,3>> >>. 182 | """ 183 | 184 | expected = """ 185 | defp foo() do 186 | for(<<(x <- <<2, 3>>)>>, into: <<>>, do: <>) 187 | x = 1 188 | for(<<(x <- <<2, 3>>)>>, into: <<>>, do: <>) 189 | end 190 | """ 191 | 192 | assert Erl2ex.convert_str!(input, @opts) == expected 193 | end 194 | 195 | 196 | test "Matches in case statement reference already declared variables" do 197 | input = """ 198 | foo(A) -> 199 | case 1 of 200 | A -> ok; 201 | B -> B 202 | end. 203 | """ 204 | 205 | expected = """ 206 | defp foo(a) do 207 | case(1) do 208 | ^a -> 209 | :ok 210 | b -> 211 | b 212 | end 213 | end 214 | """ 215 | 216 | assert Erl2ex.convert_str!(input, @opts) == expected 217 | end 218 | 219 | 220 | test "Case statement clauses do not clash, but variables are exported" do 221 | input = """ 222 | foo(P) -> 223 | case P of 224 | A -> 1; 225 | {A} -> 2 226 | end, 227 | A = 3. 228 | """ 229 | 230 | expected = """ 231 | defp foo(p) do 232 | case(p) do 233 | a -> 234 | 1 235 | {a} -> 236 | 2 237 | end 238 | ^a = 3 239 | end 240 | """ 241 | 242 | assert Erl2ex.convert_str!(input, @opts) == expected 243 | end 244 | 245 | 246 | test "Variables can occur multiple times within a match" do 247 | input = """ 248 | foo(A, A) -> 249 | A = 2, 250 | {B, B, C = {B}} = A, 251 | fun(C, {C}, D = C) -> ok end, 252 | B = 3. 253 | """ 254 | 255 | expected = """ 256 | defp foo(a, a) do 257 | ^a = 2 258 | {b, b, c = {b}} = a 259 | fn c, {c}, d = c -> :ok end 260 | ^b = 3 261 | end 262 | """ 263 | 264 | assert Erl2ex.convert_str!(input, @opts) == expected 265 | end 266 | 267 | 268 | test "Underscore should never have a caret" do 269 | input = """ 270 | foo(_, _) -> 271 | _ = 3, 272 | case 1 of 273 | _ -> ok 274 | end. 275 | """ 276 | 277 | expected = """ 278 | defp foo(_, _) do 279 | _ = 3 280 | case(1) do 281 | _ -> 282 | :ok 283 | end 284 | end 285 | """ 286 | 287 | assert Erl2ex.convert_str!(input, @opts) == expected 288 | end 289 | 290 | 291 | end 292 | -------------------------------------------------------------------------------- /test/structure_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StructureTest do 2 | use ExUnit.Case 3 | 4 | @opts [emit_file_headers: false] 5 | 6 | 7 | # Comments are not working. 8 | @tag :skip 9 | test "Module comments" do 10 | input = """ 11 | %% This is an empty module. 12 | -module(foo). 13 | """ 14 | 15 | expected = """ 16 | ## This is an empty module. 17 | 18 | defmodule :foo do 19 | 20 | end 21 | """ 22 | 23 | assert Erl2ex.convert_str!(input, @opts) == expected 24 | end 25 | 26 | 27 | test "Input does not end with a newline" do 28 | input = "-module(foo)." 29 | 30 | expected = """ 31 | defmodule :foo do 32 | 33 | end 34 | """ 35 | 36 | assert Erl2ex.convert_str!(input, @opts) == expected 37 | end 38 | 39 | 40 | test "Record operations" do 41 | input = """ 42 | -record(foo, {field1, field2=123}). 43 | foo() -> 44 | A = #foo{field1="Ada"}, 45 | B = A#foo{field2=234}, 46 | C = #foo{field1="Lovelace", _=345}, 47 | #foo{field1=D} = B, 48 | B#foo.field2. 49 | """ 50 | 51 | expected = """ 52 | require Record 53 | 54 | @erlrecordfields_foo [:field1, :field2] 55 | Record.defrecordp :erlrecord_foo, :foo, [field1: :undefined, field2: 123] 56 | 57 | 58 | defp foo() do 59 | a = erlrecord_foo(field1: 'Ada') 60 | b = erlrecord_foo(a, field2: 234) 61 | c = erlrecord_foo(field1: 'Lovelace', field2: 345) 62 | erlrecord_foo(field1: d) = b 63 | erlrecord_foo(b, :field2) 64 | end 65 | """ 66 | 67 | assert Erl2ex.convert_str!(input, @opts) == expected 68 | end 69 | 70 | 71 | test "Record queries" do 72 | input = """ 73 | -record(foo, {field1, field2=123}). 74 | foo() -> 75 | #foo.field2, 76 | record_info(size, foo), 77 | record_info(fields, foo). 78 | """ 79 | 80 | expected = """ 81 | require Record 82 | 83 | defmacrop erlrecordsize(data_attr) do 84 | __MODULE__ |> Module.get_attribute(data_attr) |> Enum.count |> +(1) 85 | end 86 | 87 | defmacrop erlrecordindex(data_attr, field) do 88 | index = __MODULE__ |> Module.get_attribute(data_attr) |> Enum.find_index(&(&1 ==field)) 89 | if index == nil, do: 0, else: index + 1 90 | end 91 | 92 | @erlrecordfields_foo [:field1, :field2] 93 | Record.defrecordp :erlrecord_foo, :foo, [field1: :undefined, field2: 123] 94 | 95 | 96 | defp foo() do 97 | erlrecordindex(:erlrecordfields_foo, :field2) 98 | erlrecordsize(:erlrecordfields_foo) 99 | @erlrecordfields_foo 100 | end 101 | """ 102 | 103 | assert Erl2ex.convert_str!(input, @opts) == expected 104 | end 105 | 106 | 107 | test "is_record BIF" do 108 | input = """ 109 | foo() -> is_record(foo, bar). 110 | """ 111 | 112 | expected = """ 113 | require Record 114 | 115 | 116 | defp foo() do 117 | Record.is_record(:foo, :bar) 118 | end 119 | """ 120 | 121 | assert Erl2ex.convert_str!(input, @opts) == expected 122 | end 123 | 124 | 125 | test "on_load attribute" do 126 | input = """ 127 | -on_load(foo/0). 128 | """ 129 | 130 | expected = """ 131 | @on_load :foo 132 | """ 133 | 134 | assert Erl2ex.convert_str!(input, @opts) == expected 135 | end 136 | 137 | 138 | test "vsn attribute" do 139 | input = """ 140 | -vsn(123). 141 | """ 142 | 143 | expected = """ 144 | @vsn 123 145 | """ 146 | 147 | assert Erl2ex.convert_str!(input, @opts) == expected 148 | end 149 | 150 | 151 | test "behaviour attribute (british spelling)" do 152 | input = """ 153 | -behaviour(gen_server). 154 | """ 155 | 156 | expected = """ 157 | @behaviour :gen_server 158 | """ 159 | 160 | assert Erl2ex.convert_str!(input, @opts) == expected 161 | end 162 | 163 | 164 | test "behavior attribute (american spelling)" do 165 | input = """ 166 | -behavior(gen_server). 167 | """ 168 | 169 | expected = """ 170 | @behaviour :gen_server 171 | """ 172 | 173 | assert Erl2ex.convert_str!(input, @opts) == expected 174 | end 175 | 176 | 177 | test "callback attributes" do 178 | input = """ 179 | -callback foo(A :: atom(), integer()) -> boolean() 180 | ; (A :: integer(), B :: atom()) -> 'hello' | boolean(). 181 | -callback bar(A, B) -> A | B when A :: tuple(), B :: atom(). 182 | """ 183 | 184 | expected = """ 185 | @callback foo(atom(), integer()) :: boolean() 186 | @callback foo(integer(), atom()) :: :hello | boolean() 187 | 188 | @callback bar(a, b) :: a | b when a: tuple(), b: atom() 189 | """ 190 | 191 | assert Erl2ex.convert_str!(input, @opts) == expected 192 | end 193 | 194 | 195 | test "file attribute" do 196 | input = """ 197 | -file("myfile.erl", 10). 198 | """ 199 | 200 | expected = """ 201 | # File "myfile.erl" Line 10 202 | """ 203 | 204 | assert Erl2ex.convert_str!(input, @opts) == expected 205 | end 206 | 207 | 208 | test "inline compile attribute" do 209 | input = """ 210 | -compile({inline, [pi/0, def/1]}). 211 | pi() -> 3.14. 212 | def(A) -> A. 213 | """ 214 | 215 | expected = """ 216 | @compile {:inline, [pi: 0, func_def: 1]} 217 | 218 | 219 | defp pi() do 220 | 3.14 221 | end 222 | 223 | 224 | defp func_def(a) do 225 | a 226 | end 227 | """ 228 | 229 | assert Erl2ex.convert_str!(input, @opts) == expected 230 | end 231 | 232 | 233 | test "inline nowarn_unused_function attribute" do 234 | input = """ 235 | -compile([nowarn_unused_function, {nowarn_unused_function, [pi/0]}]). 236 | pi() -> 3.14. 237 | """ 238 | 239 | expected = """ 240 | @compile :nowarn_unused_function 241 | 242 | 243 | defp pi() do 244 | 3.14 245 | end 246 | """ 247 | 248 | assert Erl2ex.convert_str!(input, @opts) == expected 249 | end 250 | 251 | end 252 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | defmodule Erl2ex.TestHelper do 2 | 3 | @e2e_files_dir "tmp/e2e" 4 | 5 | 6 | def download_project(name, url) do 7 | File.mkdir_p!(@e2e_files_dir) 8 | if File.dir?(project_path(name, ".git")) do 9 | run_cmd("git", ["pull"], name: name) 10 | else 11 | run_cmd("git", ["clone", url, name]) 12 | end 13 | end 14 | 15 | 16 | def clean_dir(name, path) do 17 | File.rm_rf!(project_path(name, path)) 18 | File.mkdir_p!(project_path(name, path)) 19 | end 20 | 21 | 22 | def convert_dir(name, src_path, dest_path, opts \\ []) do 23 | File.mkdir_p!(project_path(name, dest_path)) 24 | results = Erl2ex.convert_dir(project_path(name, src_path), project_path(name, dest_path), opts) 25 | Erl2ex.Results.throw_error(results) 26 | end 27 | 28 | 29 | def copy_dir(name, src_path, dest_path) do 30 | File.mkdir_p!(project_path(name, dest_path)) 31 | File.cp_r!(project_path(name, src_path), project_path(name, dest_path)) 32 | end 33 | 34 | 35 | def copy_dir(name, src_path, dest_path, files) do 36 | File.mkdir_p!(project_path(name, dest_path)) 37 | Enum.each(files, fn file -> 38 | File.cp(project_path(name, "#{src_path}/#{file}"), project_path(name, "#{dest_path}/#{file}")) 39 | end) 40 | end 41 | 42 | 43 | def copy_file(name, src_path, dest_path) do 44 | File.mkdir_p!(Path.dirname(project_path(name, dest_path))) 45 | File.cp(project_path(name, src_path), project_path(name, dest_path)) 46 | end 47 | 48 | 49 | def create_file(name, path, content) do 50 | full_path = project_path(name, path) 51 | File.write(full_path, content); 52 | end 53 | 54 | 55 | def compile_dir_individually(name, path, opts \\ []) do 56 | output_dir = Keyword.get(opts, :output, ".") 57 | "#{project_path(name, path)}/*.ex" 58 | |> Path.wildcard 59 | |> Enum.each(fn file_path -> 60 | run_cmd("elixirc", ["-o", output_dir, Path.basename(file_path)], 61 | Keyword.merge(opts, name: name, path: path, DEFINE_TEST: "true")) 62 | end) 63 | "#{project_path(name, path)}/*.erl" 64 | |> Path.wildcard 65 | |> Enum.each(fn file_path -> 66 | run_cmd("erlc", ["-o", output_dir, "-DTEST", Path.basename(file_path)], 67 | Keyword.merge(opts, name: name, path: path)) 68 | end) 69 | end 70 | 71 | 72 | def compile_dir(name, path, opts \\ []) do 73 | output_dir = Keyword.get(opts, :output, ".") 74 | if Path.wildcard("#{project_path(name, path)}/*.ex") != [] do 75 | run_cmd("elixirc", ["-o", output_dir, {"*.ex"}], 76 | Keyword.merge(opts, name: name, path: path, DEFINE_TEST: "true")) 77 | end 78 | if Path.wildcard("#{project_path(name, path)}/*.erl") != [] do 79 | run_cmd("erlc", ["-o", output_dir, "-DTEST", {"*.erl"}], 80 | Keyword.merge(opts, name: name, path: path)) 81 | end 82 | end 83 | 84 | 85 | def run_eunit_tests(tests, name, path, opts \\ []) do 86 | tests |> Enum.each(fn test -> 87 | run_elixir(":ok = :eunit.test(:#{test})", 88 | Keyword.merge(opts, name: name, path: path)) 89 | end) 90 | end 91 | 92 | 93 | def run_elixir(cmd, opts \\ []) do 94 | run_cmd("elixir", ["-e", cmd], opts) 95 | end 96 | 97 | 98 | def run_cmd(cmd, args, opts \\ []) do 99 | name = Keyword.get(opts, :name) 100 | path = Keyword.get(opts, :path) 101 | cd = Keyword.get(opts, :cd, project_path(name, path)) 102 | display_output = Keyword.get(opts, :display_output) 103 | display_cmd = Keyword.get(opts, :display_cmd) 104 | 105 | env = 106 | opts 107 | |> Enum.filter(fn {k, _} -> Regex.match?(~r/^[A-Z]/, Atom.to_string(k)) end) 108 | |> Enum.map(fn {k, v} -> {Atom.to_string(k), v} end) 109 | 110 | args = args |> Enum.flat_map(fn 111 | {wildcard} -> 112 | "#{cd}/#{wildcard}" 113 | |> Path.wildcard 114 | |> Enum.map(&(String.replace_prefix(&1, "#{cd}/", ""))) 115 | str -> [str] 116 | end) 117 | if display_cmd do 118 | IO.puts("cd #{cd} && #{cmd} #{Enum.join(args, " ")}") 119 | end 120 | output = case System.cmd(cmd, args, cd: cd, env: env, stderr_to_stdout: true) do 121 | {str, 0} -> str 122 | {str, code} -> 123 | raise "Error #{code} when running command #{cmd} #{inspect(args)} :: #{str}" 124 | end 125 | if display_output do 126 | IO.puts(output) 127 | end 128 | output 129 | end 130 | 131 | 132 | def project_path(name, path \\ nil) 133 | 134 | def project_path(nil, nil) do 135 | @e2e_files_dir 136 | end 137 | def project_path(name, nil) do 138 | "#{@e2e_files_dir}/#{name}" 139 | end 140 | def project_path(name, path) do 141 | "#{@e2e_files_dir}/#{name}/#{path}" 142 | end 143 | 144 | 145 | defmodule Result do 146 | defstruct( 147 | output: nil, 148 | module: nil 149 | ) 150 | end 151 | 152 | 153 | def test_conversion(input, opts) do 154 | output = Erl2ex.convert_str!(input, opts) 155 | test_num = :erlang.unique_integer([:positive]) 156 | module = :"Elixir.Erl2ex.TestModule#{test_num}" 157 | module_name = module |> Module.split |> Enum.join(".") 158 | Code.eval_string "defmodule #{module_name} do\n#{output}\nend" 159 | %Result{output: output, module: module} 160 | end 161 | 162 | 163 | end 164 | 165 | 166 | ExUnit.configure exclude: [:e2e, :dummy] 167 | ExUnit.start() 168 | -------------------------------------------------------------------------------- /test/type_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TypeTest do 2 | use ExUnit.Case 3 | 4 | @opts [emit_file_headers: false] 5 | 6 | 7 | test "Visibility" do 8 | input = """ 9 | -type public_type() :: any(). 10 | -opaque opaque_type(A) :: list(A). 11 | -type private_type() :: integer(). 12 | -export_type([public_type/0, opaque_type/1]). 13 | """ 14 | 15 | expected = """ 16 | @type public_type() :: any() 17 | 18 | @opaque opaque_type(a) :: list(a) 19 | 20 | @typep private_type() :: integer() 21 | """ 22 | 23 | assert Erl2ex.convert_str!(input, @opts) == expected 24 | end 25 | 26 | 27 | test "Misc base types" do 28 | input = """ 29 | -type type1() :: any(). 30 | -type type2() :: none(). 31 | -type type3() :: pid(). 32 | -type type4() :: port(). 33 | -type type5() :: reference(). 34 | -type type6() :: float(). 35 | """ 36 | 37 | expected = """ 38 | @typep type1() :: any() 39 | 40 | @typep type2() :: none() 41 | 42 | @typep type3() :: pid() 43 | 44 | @typep type4() :: port() 45 | 46 | @typep type5() :: reference() 47 | 48 | @typep type6() :: float() 49 | """ 50 | 51 | assert Erl2ex.convert_str!(input, @opts) == expected 52 | end 53 | 54 | 55 | test "Atom types" do 56 | input = """ 57 | -type type1() :: atom(). 58 | -type type2() :: hello. 59 | -type type3() :: '123'. 60 | """ 61 | 62 | expected = """ 63 | @typep type1() :: atom() 64 | 65 | @typep type2() :: :hello 66 | 67 | @typep type3() :: :"123" 68 | """ 69 | 70 | assert Erl2ex.convert_str!(input, @opts) == expected 71 | end 72 | 73 | 74 | test "Integer types" do 75 | input = """ 76 | -type type1() :: integer(). 77 | -type type2() :: 42. 78 | -type type3() :: -42. 79 | -type type4() :: -1..10. 80 | """ 81 | 82 | expected = """ 83 | @typep type1() :: integer() 84 | 85 | @typep type2() :: 42 86 | 87 | @typep type3() :: -42 88 | 89 | @typep type4() :: -1..10 90 | """ 91 | 92 | assert Erl2ex.convert_str!(input, @opts) == expected 93 | end 94 | 95 | 96 | test "Const types" do 97 | input = """ 98 | -type type1() :: hello. 99 | -type type2() :: 42. 100 | """ 101 | 102 | expected = """ 103 | @typep type1() :: :hello 104 | 105 | @typep type2() :: 42 106 | """ 107 | 108 | assert Erl2ex.convert_str!(input, @opts) == expected 109 | end 110 | 111 | 112 | test "List types" do 113 | input = """ 114 | -type type1() :: list(). 115 | -type type2() :: [integer()]. 116 | -type type3() :: list(integer()). 117 | -type type4() :: []. 118 | -type type5() :: [atom(),...]. 119 | -type type6() :: nil(). 120 | """ 121 | 122 | expected = """ 123 | @typep type1() :: list() 124 | 125 | @typep type2() :: list(integer()) 126 | 127 | @typep type3() :: list(integer()) 128 | 129 | @typep type4() :: [] 130 | 131 | @typep type5() :: nonempty_list(atom()) 132 | 133 | @typep type6() :: [] 134 | """ 135 | 136 | assert Erl2ex.convert_str!(input, @opts) == expected 137 | end 138 | 139 | 140 | test "Tuple types" do 141 | input = """ 142 | -type type1() :: tuple(). 143 | -type type2() :: {}. 144 | -type type3() :: {any()}. 145 | -type type4() :: {integer(), atom(), hello}. 146 | """ 147 | 148 | expected = """ 149 | @typep type1() :: tuple() 150 | 151 | @typep type2() :: {} 152 | 153 | @typep type3() :: {any()} 154 | 155 | @typep type4() :: {integer(), atom(), :hello} 156 | """ 157 | 158 | assert Erl2ex.convert_str!(input, @opts) == expected 159 | end 160 | 161 | 162 | test "Bitstring types" do 163 | input = """ 164 | -type type1() :: binary(). 165 | -type type2() :: bitstring(). 166 | -type type3() :: <<>>. 167 | -type type4() :: <<_:10>>. 168 | -type type5() :: <<_:_*8>>. 169 | -type type6() :: <<_:10,_:_*8>>. 170 | """ 171 | 172 | expected = """ 173 | @typep type1() :: binary() 174 | 175 | @typep type2() :: bitstring() 176 | 177 | @typep type3() :: <<>> 178 | 179 | @typep type4() :: <<_::10>> 180 | 181 | @typep type5() :: <<_::_*8>> 182 | 183 | @typep type6() :: <<_::10, _::_*8>> 184 | """ 185 | 186 | assert Erl2ex.convert_str!(input, @opts) == expected 187 | end 188 | 189 | 190 | test "Function types" do 191 | input = """ 192 | -type type1() :: fun(). 193 | -type type2() :: fun((...) -> any()). 194 | -type type3() :: fun(() -> integer()). 195 | -type type4() :: fun((atom(), atom()) -> integer()). 196 | """ 197 | 198 | expected = """ 199 | @typep type1() :: fun() 200 | 201 | @typep type2() :: (... -> any()) 202 | 203 | @typep type3() :: (() -> integer()) 204 | 205 | @typep type4() :: (atom(), atom() -> integer()) 206 | """ 207 | 208 | assert Erl2ex.convert_str!(input, @opts) == expected 209 | end 210 | 211 | 212 | test "Map types" do 213 | input = """ 214 | -type type1() :: map(). 215 | -type type2() :: \#{}. 216 | -type type3() :: \#{atom() => integer()}. 217 | """ 218 | 219 | expected = """ 220 | @typep type1() :: map() 221 | 222 | @typep type2() :: %{} 223 | 224 | @typep type3() :: %{atom() => integer()} 225 | """ 226 | 227 | assert Erl2ex.convert_str!(input, @opts) == expected 228 | end 229 | 230 | 231 | test "Unions" do 232 | input = """ 233 | -type type1() :: atom() | integer(). 234 | -type type2() :: true | false | nil. 235 | """ 236 | 237 | expected = """ 238 | @typep type1() :: atom() | integer() 239 | 240 | @typep type2() :: true | false | nil 241 | """ 242 | 243 | assert Erl2ex.convert_str!(input, @opts) == expected 244 | end 245 | 246 | 247 | test "Records" do 248 | input = """ 249 | -record(myrecord, {field1=hello :: any(), field2 :: tuple() | integer(), field3}). 250 | -type type1() :: #myrecord{}. 251 | -type type2() :: #myrecord{field1 :: string()}. 252 | """ 253 | 254 | expected = """ 255 | require Record 256 | 257 | @erlrecordfields_myrecord [:field1, :field2, :field3] 258 | Record.defrecordp :erlrecord_myrecord, :myrecord, [field1: :hello, field2: :undefined, field3: :undefined] 259 | 260 | @typep type1() :: record(:erlrecord_myrecord, field1: any(), field2: :undefined | tuple() | integer(), field3: term()) 261 | 262 | @typep type2() :: record(:erlrecord_myrecord, field1: char_list(), field2: :undefined | tuple() | integer(), field3: term()) 263 | """ 264 | 265 | assert Erl2ex.convert_str!(input, @opts) == expected 266 | end 267 | 268 | 269 | test "Unknown parameters" do 270 | input = """ 271 | -type type1(T) :: list(T) | {_}. 272 | """ 273 | 274 | expected = """ 275 | @typep type1(t) :: list(t) | {any()} 276 | """ 277 | 278 | assert Erl2ex.convert_str!(input, @opts) == expected 279 | end 280 | 281 | 282 | test "Custom type" do 283 | input = """ 284 | -type type1() :: atom(). 285 | -type type2() :: type1() | integer(). 286 | """ 287 | 288 | expected = """ 289 | @typep type1() :: atom() 290 | 291 | @typep type2() :: type1() | integer() 292 | """ 293 | 294 | assert Erl2ex.convert_str!(input, @opts) == expected 295 | end 296 | 297 | 298 | test "Remote type" do 299 | input = """ 300 | -type type1() :: supervisor:startchild_ret(). 301 | """ 302 | 303 | expected = """ 304 | @typep type1() :: :supervisor.startchild_ret() 305 | """ 306 | 307 | assert Erl2ex.convert_str!(input, @opts) == expected 308 | end 309 | 310 | 311 | test "Simple specs" do 312 | input = """ 313 | -spec foo(A :: atom(), integer()) -> boolean() 314 | ; (A :: integer(), B :: atom()) -> 'hello' | boolean(). 315 | foo(A, B) -> true. 316 | """ 317 | 318 | expected = """ 319 | @spec foo(atom(), integer()) :: boolean() 320 | @spec foo(integer(), atom()) :: :hello | boolean() 321 | 322 | 323 | defp foo(a, b) do 324 | true 325 | end 326 | """ 327 | 328 | assert Erl2ex.convert_str!(input, @opts) == expected 329 | end 330 | 331 | 332 | test "Specs with variables" do 333 | input = """ 334 | -spec foo(A, B) -> A | B | list(T). 335 | foo(A, B) -> A. 336 | """ 337 | 338 | expected = """ 339 | @spec foo(a, b) :: a | b | list(t) when a: any(), b: any(), t: any() 340 | 341 | 342 | defp foo(a, b) do 343 | a 344 | end 345 | """ 346 | 347 | assert Erl2ex.convert_str!(input, @opts) == expected 348 | end 349 | 350 | 351 | test "Specs with guards" do 352 | input = """ 353 | -spec foo(A, B) -> A | B when A :: tuple(), B :: atom(). 354 | foo(A, B) -> A. 355 | """ 356 | 357 | expected = """ 358 | @spec foo(a, b) :: a | b when a: tuple(), b: atom() 359 | 360 | 361 | defp foo(a, b) do 362 | a 363 | end 364 | """ 365 | 366 | assert Erl2ex.convert_str!(input, @opts) == expected 367 | end 368 | 369 | 370 | test "Specs with guards constraining other guards" do 371 | input = """ 372 | -spec foo() -> A when A :: fun(() -> B), B :: atom(). 373 | foo() -> fun () -> ok end. 374 | """ 375 | 376 | expected = """ 377 | @spec foo() :: a when a: (() -> b), b: atom() 378 | 379 | 380 | defp foo() do 381 | fn -> :ok end 382 | end 383 | """ 384 | 385 | assert Erl2ex.convert_str!(input, @opts) == expected 386 | end 387 | 388 | 389 | test "Specs with module qualifiers" do 390 | input = """ 391 | -module(mod). 392 | -spec mod:foo(atom()) -> boolean(). 393 | -spec mod2:foo(integer()) -> boolean(). 394 | foo(A) -> true. 395 | """ 396 | 397 | expected = """ 398 | defmodule :mod do 399 | 400 | @spec foo(atom()) :: boolean() 401 | 402 | 403 | defp foo(a) do 404 | true 405 | end 406 | 407 | end 408 | """ 409 | 410 | assert Erl2ex.convert_str!(input, @opts) == expected 411 | end 412 | 413 | 414 | test "Specs for function that gets renamed" do 415 | input = """ 416 | -spec to_string(any()) -> any(). 417 | to_string(A) -> A. 418 | """ 419 | 420 | expected = """ 421 | @spec func_to_string(any()) :: any() 422 | 423 | 424 | defp func_to_string(a) do 425 | a 426 | end 427 | """ 428 | 429 | assert Erl2ex.convert_str!(input, @opts) == expected 430 | end 431 | 432 | 433 | end 434 | --------------------------------------------------------------------------------