Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 23 additions & 4 deletions lib/elixir_script/beam.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long (max is 80, was 114).

{:ok, {^module, attribute_info}} = :beam_lib.chunks(beam, [:attributes]) do

if Keyword.get(attribute_info[:attributes], :protocol) do
Expand All @@ -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} ->
Expand Down
76 changes: 60 additions & 16 deletions lib/elixir_script/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
2 changes: 1 addition & 1 deletion lib/elixir_script/passes/find_used_functions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function is too complex (ABC size is 32, max is 30).

Enum.each(generators, fn
{:<<>>, _, body} ->
walk(body, state)
Expand Down
19 changes: 13 additions & 6 deletions lib/elixir_script/passes/find_used_modules.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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} ->
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function is too complex (ABC size is 33, max is 30).

walk(Collectable, state)

Enum.each(generators, fn
Expand Down
11 changes: 10 additions & 1 deletion lib/elixir_script/passes/translate/form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions should have a @SPEC type specification.

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
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions should have a @SPEC type specification.

For.compile(ast, state)
end

Expand Down
7 changes: 7 additions & 0 deletions lib/elixir_script/passes/translate/forms/remote.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
4 changes: 4 additions & 0 deletions lib/elixir_script/passes/translate/function.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 ->
Expand All @@ -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)
Expand Down
23 changes: 22 additions & 1 deletion lib/elixir_script/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ defmodule ElixirScript.State do
Agent.start_link(fn ->
%{
modules: Keyword.new,
js_modules: []
js_modules: [],
in_memory_modules: []
}
end)
end
Expand Down Expand Up @@ -88,4 +89,24 @@ defmodule ElixirScript.State do
state.modules
end)
end

def get_in_memory_module(pid, module) do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions should have a @SPEC type specification.

Agent.get(pid, fn(state) ->
Keyword.get(state.in_memory_modules, module)
end)
end

def get_in_memory_modules(pid) do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions should have a @SPEC type specification.

Agent.get(pid, fn(state) ->
state.in_memory_modules
end)
end

def put_in_memory_module(pid, module, beam) do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions should have a @SPEC type specification.

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
22 changes: 22 additions & 0 deletions test/compiler_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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