diff --git a/CHANGELOG.md b/CHANGELOG.md index 58225567..1b80c3bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] ### Added +- [Compiler will now accept a path to Elixir Files to compile](https://github.com/elixirscript/elixirscript/issues/420) - [Added `ElixirScript.JS.map_to_object/2` with options [keys: :string, symbols: false]](https://github.com/elixirscript/elixirscript/issues/362) - [Added `ElixirScript.JS.object_to_map/1|2` with options [keys: :atom, recurse_array: true]](https://github.com/elixirscript/elixirscript/issues/381) - [Fully implement `__info__` on modules](https://github.com/elixirscript/elixirscript/pull/378) diff --git a/lib/elixir_script/beam.ex b/lib/elixir_script/beam.ex index f97e224c..254cb803 100644 --- a/lib/elixir_script/beam.ex +++ b/lib/elixir_script/beam.ex @@ -7,7 +7,7 @@ defmodule ElixirScript.Beam do For protocols, this will return a list of all the protocol implementations """ - @spec debug_info(atom) :: {:ok | :error, map | binary} + @spec debug_info(atom | bitstring) :: {:ok | :error, map | binary} def debug_info(module) # We get debug info from String and then replace @@ -39,9 +39,23 @@ defmodule ElixirScript.Beam do do_debug_info(module) end - defp do_debug_info(module) when is_atom(module) do - with {_, beam, beam_path} <- :code.get_object_code(module), - {:ok, {^module, [debug_info: {:debug_info_v1, backend, data}]}} <- :beam_lib.chunks(beam, [:debug_info]), + def debug_info(beam) when is_bitstring(beam) do + do_debug_info(beam) + end + + defp do_debug_info(module, path \\ nil) + + defp do_debug_info(module, _) when is_atom(module) do + case :code.get_object_code(module) do + {_, beam, beam_path} -> + do_debug_info(beam, beam_path) + :error -> + {:error, "Unknown module"} + end + end + + defp do_debug_info(beam, beam_path) do + with {:ok, {module, [debug_info: {:debug_info_v1, backend, data}]}} <- :beam_lib.chunks(beam, [:debug_info]), {:ok, {^module, attribute_info}} = :beam_lib.chunks(beam, [:attributes]) do if Keyword.get(attribute_info[:attributes], :protocol) do @@ -62,6 +76,11 @@ defmodule ElixirScript.Beam do end end + defp process_debug_info({:ok, info}, nil) do + info = Map.put(info, :last_modified, nil) + {:ok, info} + end + defp process_debug_info({:ok, info}, beam_path) do info = case File.stat(beam_path, time: :posix) do {:ok, file_info} -> diff --git a/lib/elixir_script/compiler.ex b/lib/elixir_script/compiler.ex index ee2de7a9..f1cc9a32 100644 --- a/lib/elixir_script/compiler.ex +++ b/lib/elixir_script/compiler.ex @@ -2,11 +2,13 @@ defmodule ElixirScript.Compiler do @moduledoc """ The entry point for the ElixirScript compilation process. Takes the given module(s) and compiles them and all modules - and functions they use into JavaScript + and functions they use into JavaScript. + + Will also take a path to Elixir files """ @doc """ - Takes either a module name or a list of module names as + Takes either a module name, list of module names, or a path as the entry point(s) of an application/library. From there it will determine which modules and functions are needed to be compiled. @@ -22,36 +24,78 @@ defmodule ElixirScript.Compiler do * `root`: Optional root for imports of FFI JavaScript modules. Defaults to `.`. """ - @spec compile(atom | [atom], []) :: nil - def compile(entry_modules, opts \\ []) do - opts = build_compiler_options(opts, entry_modules) - {:ok, pid} = ElixirScript.State.start_link() + alias ElixirScript.{ + State, + Translate, + FindUsedModules, + FindUsedFunctions, + Output + } + alias ElixirScript.ModuleSystems.ES + alias Kernel.ParallelCompiler + + @spec compile(atom | [atom] | binary, []) :: nil + def compile(path, opts \\ []) + + def compile(path, opts) when is_binary(path) do + opts = build_compiler_options(opts) + {:ok, pid} = State.start_link() + + path = if String.ends_with?(path, [".ex", ".exs"]) do + path + else + Path.join([path, "**", "*.{ex,exs}"]) + end + + files = Path.wildcard(path) + + ParallelCompiler.files(files, [ + each_module: &on_module_compile(pid, &1, &2, &3) + ]) + + entry_modules = pid + |> State.get_in_memory_modules + |> Keyword.keys + + do_compile(entry_modules, pid, opts) + end + + def compile(entry_modules, opts) do + opts = build_compiler_options(opts) + {:ok, pid} = State.start_link() entry_modules = List.wrap(entry_modules) - ElixirScript.FindUsedModules.execute(entry_modules, pid) + do_compile(entry_modules, pid, opts) + end - ElixirScript.FindUsedFunctions.execute(entry_modules, pid) + defp do_compile(entry_modules, pid, opts) do + FindUsedModules.execute(entry_modules, pid) - modules = ElixirScript.State.list_modules(pid) - ElixirScript.Translate.execute(modules, pid) + FindUsedFunctions.execute(entry_modules, pid) - modules = ElixirScript.State.list_modules(pid) - result = ElixirScript.Output.execute(modules, pid, opts) + modules = State.list_modules(pid) + Translate.execute(modules, pid) - ElixirScript.State.stop(pid) + modules = State.list_modules(pid) + result = Output.execute(modules, pid, opts) + + State.stop(pid) result end - defp build_compiler_options(opts, entry_modules) do + defp build_compiler_options(opts) do default_options = Map.new |> Map.put(:output, Keyword.get(opts, :output)) |> Map.put(:format, :es) - |> Map.put(:entry_modules, entry_modules) |> Map.put(:root, Keyword.get(opts, :root, ".")) options = default_options - Map.put(options, :module_formatter, ElixirScript.ModuleSystems.ES) + Map.put(options, :module_formatter, ES) + end + + defp on_module_compile(pid, _file, module, beam) do + State.put_in_memory_module(pid, module, beam) end end diff --git a/lib/elixir_script/passes/find_used_functions.ex b/lib/elixir_script/passes/find_used_functions.ex index 7aeb7eed..90d00435 100644 --- a/lib/elixir_script/passes/find_used_functions.ex +++ b/lib/elixir_script/passes/find_used_functions.ex @@ -137,7 +137,7 @@ defmodule ElixirScript.FindUsedFunctions do walk(params, state) end - defp walk({:for, _, generators}, state) do + defp walk({:for, _, generators}, state) when is_list(generators) do Enum.each(generators, fn {:<<>>, _, body} -> walk(body, state) diff --git a/lib/elixir_script/passes/find_used_modules.ex b/lib/elixir_script/passes/find_used_modules.ex index 4b902bbe..b983fefc 100644 --- a/lib/elixir_script/passes/find_used_modules.ex +++ b/lib/elixir_script/passes/find_used_modules.ex @@ -11,14 +11,19 @@ defmodule ElixirScript.FindUsedModules do modules |> List.wrap |> Enum.each(fn(module) -> - if ElixirScript.State.get_module(pid, module) == nil do - do_execute(module, pid) - end + do_execute(module, pid) end) end defp do_execute(module, pid) do - case ElixirScript.Beam.debug_info(module) do + result = case ModuleState.get_in_memory_module(pid, module) do + nil -> + ElixirScript.Beam.debug_info(module) + beam -> + ElixirScript.Beam.debug_info(beam) + end + + case result do {:ok, info} -> walk_module(module, info, pid) {:ok, module, implementations} -> @@ -73,7 +78,9 @@ defmodule ElixirScript.FindUsedModules do module: module } - Enum.each(reachable_defs, &walk(&1, state)) + Enum.each(reachable_defs, fn(x) -> + walk(x, state) + end) end defp walk_protocol(module, implementations, pid) do @@ -165,7 +172,7 @@ defmodule ElixirScript.FindUsedModules do walk(params, state) end - defp walk({:for, _, generators}, state) do + defp walk({:for, _, generators}, state) when is_list(generators) do walk(Collectable, state) Enum.each(generators, fn diff --git a/lib/elixir_script/passes/translate/form.ex b/lib/elixir_script/passes/translate/form.ex index 3d410a4d..3f580783 100644 --- a/lib/elixir_script/passes/translate/form.ex +++ b/lib/elixir_script/passes/translate/form.ex @@ -9,16 +9,25 @@ defmodule ElixirScript.Translate.Form do alias ElixirScript.Translate.Clause require Logger + @spec compile!(any, map) :: ESTree.Node.t def compile!(ast, state) do {js_ast, _} = compile(ast, state) js_ast end + @spec compile(any, map) :: {ESTree.Node.t, map} + def compile(ast, state) + def compile(nil, state) do { J.identifier("null"), state } end + def compile(map, state) when is_map(map) do + quoted = Code.string_to_quoted!("#{inspect map}") + compile(quoted, state) + end + def compile(form, state) when is_boolean(form) or is_integer(form) or is_float(form) or is_binary(form) do { J.literal(form), state } end @@ -122,7 +131,7 @@ defmodule ElixirScript.Translate.Form do { ast, state } end - def compile({:for, _, _} = ast, state) do + def compile({:for, _, generators} = ast, state) when is_list(generators) do For.compile(ast, state) end diff --git a/lib/elixir_script/passes/translate/forms/remote.ex b/lib/elixir_script/passes/translate/forms/remote.ex index 8052e041..918f986d 100644 --- a/lib/elixir_script/passes/translate/forms/remote.ex +++ b/lib/elixir_script/passes/translate/forms/remote.ex @@ -85,6 +85,13 @@ defmodule ElixirScript.Translate.Forms.Remote do module === Elixir -> members = ["Elixir", "__load"] + Helpers.call( + Identifier.make_namespace_members(members), + [J.identifier("Elixir")] + ) + module === :ElixirScript -> + members = ["Elixir", "ElixirScript", "__load"] + Helpers.call( Identifier.make_namespace_members(members), [J.identifier("Elixir")] diff --git a/lib/elixir_script/passes/translate/function.ex b/lib/elixir_script/passes/translate/function.ex index ab9d50f8..23888453 100644 --- a/lib/elixir_script/passes/translate/function.ex +++ b/lib/elixir_script/passes/translate/function.ex @@ -8,6 +8,7 @@ defmodule ElixirScript.Translate.Function do alias ElixirScript.Translate.{Clause, Form, Helpers} alias ElixirScript.Translate.Forms.Pattern + @spec compile(any, map) :: {ESTree.Node.t, map} def compile({:fn, _, clauses}, state) do anonymous? = Map.get(state, :anonymous_fn, false) @@ -51,6 +52,7 @@ defmodule ElixirScript.Translate.Function do end def compile({{name, arity}, _type, _, clauses}, state) do + state = Map.put(state, :function, {name, arity}) |> Map.put(:anonymous_fn, false) |> Map.put(:in_guard, false) @@ -156,6 +158,7 @@ defmodule ElixirScript.Translate.Function do compile_clause({[], params, [], body}, state) end + @spec compile_block(any, map) :: {ESTree.Node.t, map} def compile_block(block, state) do ast = case block do nil -> @@ -170,6 +173,7 @@ defmodule ElixirScript.Translate.Function do {ast, state} end + @spec update_last_call([ESTree.Node.t], map) :: ESTree.Node.t def update_last_call(clause_body, %{function: {name, _}, anonymous_fn: anonymous?}) do last_item = List.last(clause_body) function_name = ElixirScript.Translate.Identifier.make_function_name(name) diff --git a/lib/elixir_script/state.ex b/lib/elixir_script/state.ex index 78c0d418..daad8b76 100644 --- a/lib/elixir_script/state.ex +++ b/lib/elixir_script/state.ex @@ -7,7 +7,8 @@ defmodule ElixirScript.State do Agent.start_link(fn -> %{ modules: Keyword.new, - js_modules: [] + js_modules: [], + in_memory_modules: [] } end) end @@ -88,4 +89,24 @@ defmodule ElixirScript.State do state.modules end) end + + def get_in_memory_module(pid, module) do + Agent.get(pid, fn(state) -> + Keyword.get(state.in_memory_modules, module) + end) + end + + def get_in_memory_modules(pid) do + Agent.get(pid, fn(state) -> + state.in_memory_modules + end) + end + + def put_in_memory_module(pid, module, beam) do + Agent.update(pid, fn(state) -> + in_memory_modules = Map.get(state, :in_memory_modules, []) + in_memory_modules = Keyword.put(in_memory_modules, module, beam) + %{ state | in_memory_modules: in_memory_modules } + end) + end end diff --git a/test/compiler_test.exs b/test/compiler_test.exs index 63b4ba63..508d22ae 100644 --- a/test/compiler_test.exs +++ b/test/compiler_test.exs @@ -30,4 +30,26 @@ defmodule ElixirScript.Compiler.Test do ElixirScript.Compiler.compile(Atom, [output: path]) assert File.exists?(path) end + + test "compile file" do + path = System.tmp_dir() + path = Path.join([path, "myfile.js"]) + + input_path = Path.join([File.cwd!(), "test", "beam_test.exs"]) + + ElixirScript.Compiler.compile(input_path, [output: path]) + assert File.exists?(path) + assert String.contains?(File.read!(path), "Elixir.ElixirScript.Beam.Test") + end + + test "compile wildcard" do + path = System.tmp_dir() + path = Path.join([path, "myfile.js"]) + + input_path = Path.join([File.cwd!(), "test", "*fi_test.exs"]) + + ElixirScript.Compiler.compile(input_path, [output: path]) + assert File.exists?(path) + assert String.contains?(File.read!(path), "Elixir.ElixirScript.FFI.Test") + end end