├── .formatter.exs ├── .gitignore ├── README.md ├── config └── config.exs ├── lib └── mix │ ├── compilers │ └── lfe.ex │ └── tasks │ ├── compile.lfe.ex │ └── lfe.test.ex ├── mix.exs ├── mix.lock └── test ├── fixtures └── sample │ ├── mix.exs │ └── src │ ├── tut4.lfe │ └── tut5.lfe ├── mix └── tasks │ └── compile.lfe_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | elixir_lfe-*.tar 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mix LFE Compiler 2 | 3 | A very simple Mix task that compiles LFE (lisp (flavoured (erlang))). 4 | 5 | It is a Mix compiler which uses the Erlang Mix compiler. 6 | 7 | Lisp is a great language to get into the functional way of thinking and LFE is Lisp 2+ which runs on the greatest platform (personal opinion). 8 | Elixir's Mix is a great (or the greatest) configuration/package/build manager, so why not use it to compile LFE? 9 | Also Elixir developers should try LFE! This little project has the purpose to make that easier. 10 | 11 | ## Installation and setup 12 | 13 | Install the Mix plugin for LFE if it isn't installed already: 14 | 15 | ``` 16 | mix archive.install https://github.com/meddle0x53/mix_lfe_new/releases/download/v0.2.0/mix_lfe_new-0.2.0.ez 17 | ``` 18 | 19 | You can create a LFE project with: 20 | 21 | ``` 22 | mix lfe.new 23 | ``` 24 | 25 | The project uses the rebar3 `lfe` package to compile LFE source files, so the following will be necessary: 26 | 27 | 1. Navigate to the root of the new project with `cd `. 28 | 2. Run `mix lfe.setup`. 29 | 30 | This will install the LFE compiler and set it up, so it can be used to compile `*.lfe` files located in the `src` folder. 31 | 32 | The project can be created and set up in one command like this: 33 | 34 | ``` 35 | mix lfe.new --setup 36 | ``` 37 | 38 | ## Compilation 39 | 40 | Compiling `*.lfe` sources, located in the `src` folder can be done with: 41 | 42 | ``` 43 | mix compile 44 | ``` 45 | 46 | To use the compiled modules with the LFE REPL, you can run: 47 | 48 | ``` 49 | ./deps/lfe/bin/lfe -pa _build/dev/lib/*/ebin 50 | ``` 51 | 52 | The compiled LFE modules can be used from Elixir too: 53 | 54 | ``` 55 | iex -S mix 56 | ``` 57 | 58 | Now the modules will be accessible just like Erlang modules are accessible in Elixir. 59 | 60 | ## Running tests 61 | 62 | The compiler can compile and run [ltest](https://github.com/lfex/ltest) tests. 63 | Just put all the tests in the `test` folder of the project and run: 64 | 65 | ``` 66 | mix lfe.test 67 | ``` 68 | 69 | Works with umbrella applications, meaning that some of applications in the umbrella can be LFE ones. 70 | 71 | ## Example projects 72 | 73 | A list of projects created with `mix_lfe`. More to come. 74 | 75 | * [Echo](https://github.com/meddle0x53/echo) is an example LFE OTP application, created with mix_lfe. 76 | 77 | ## TODO 78 | 79 | The tests of this project mirror the ones for the Erlang Mix compiler. 80 | For now the source is very simple and uses an [idea](https://github.com/elixir-lang/elixir/blob/e1c903a5956e4cb9075f0aac00638145788b0da4/lib/mix/lib/mix/compilers/erlang.ex#L20) from the Erlang Mix compiler. 81 | All works well, but requires some manual work and doesn't support LFE compiler fine tuning, so that's what we'll be after next. 82 | 83 | 1. Pass more options to the LFE compiler, using mix configuration. 84 | 2. More and more examples. 85 | 3. A mix task or binary running the LFE REPL in the context of the compiled artifacts. 86 | 4. Add CI to this project. 87 | 5. Make it possible to add options when running `mix lfe.test`. Work on making stable versions by using another test runner/library or contacting the `ltest` maintainers. 88 | 89 | ## License 90 | 91 | Same as Elixir. 92 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # This configuration is loaded before any dependency and is restricted 4 | # to this project. If another project depends on this project, this 5 | # file won't be loaded nor affect the parent project. For this reason, 6 | # if you want to provide default values for your application for 7 | # 3rd-party users, it should be done in your "mix.exs" file. 8 | 9 | # You can configure your application as: 10 | # 11 | # config :elixir_lfe, key: :value 12 | # 13 | # and access this configuration in your application as: 14 | # 15 | # Application.get_env(:elixir_lfe, :key) 16 | # 17 | # You can also configure a 3rd-party app: 18 | # 19 | # config :logger, level: :info 20 | # 21 | 22 | # It is also possible to import configuration files, relative to this 23 | # directory. For example, you can emulate configuration per environment 24 | # by uncommenting the line below and defining dev.exs, test.exs and such. 25 | # Configuration from the imported file will override the ones defined 26 | # here (which is why it is important to import them last). 27 | # 28 | # import_config "#{Mix.env}.exs" 29 | -------------------------------------------------------------------------------- /lib/mix/compilers/lfe.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Compilers.Lfe do 2 | alias Mix.Compilers.Erlang, as: ErlangCompiler 3 | 4 | @moduledoc false 5 | 6 | @doc """ 7 | Compiles the files in `mappings` with '.lfe' extensions into 8 | the destinations. 9 | Does this for each stale input and output pair (or for all if `force` is `true`) and 10 | removes files that no longer have a source, while keeping the `manifest` up to date. 11 | 12 | `mappings` should be a list of tuples in the form of `{src, dest}` paths. 13 | 14 | Uses an [idea](https://github.com/elixir-lang/elixir/blob/e1c903a5956e4cb9075f0aac00638145788b0da4/lib/mix/lib/mix/compilers/erlang.ex#L20) from the Erlang Mix compiler to do so. 15 | 16 | It supports the options of the Erlang Mix compiler under the covers as it is used. 17 | """ 18 | def compile(manifest, [{_, _} | _] = mappings, opts) do 19 | callback = fn input, output -> 20 | module = input |> Path.basename(".lfe") |> String.to_atom() 21 | :code.purge(module) 22 | :code.delete(module) 23 | 24 | outdir = output |> Path.dirname() |> ErlangCompiler.to_erl_file() 25 | 26 | compile_result(:lfe_comp.file(ErlangCompiler.to_erl_file(input), [{:outdir, outdir}, :return, :report])) 27 | end 28 | 29 | ErlangCompiler.compile(manifest, mappings, :lfe, :beam, opts, callback) 30 | end 31 | 32 | @doc """ 33 | Removes compiled files for the given `manifest`. 34 | """ 35 | def clean(manifest), do: ErlangCompiler.clean(manifest) 36 | 37 | defp compile_result({:error, [{:error, [{file, [error | _]}], []}], [], []}) do 38 | {:error, [{file, [error]}], []} 39 | end 40 | 41 | defp compile_result(result), do: result 42 | end 43 | -------------------------------------------------------------------------------- /lib/mix/tasks/compile.lfe.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Compile.Lfe do 2 | use Mix.Task.Compiler 3 | import Mix.Compilers.Lfe 4 | 5 | @recursive true 6 | @manifest "compile.lfe" 7 | @switches [force: :boolean, all_warnings: :boolean] 8 | 9 | @moduledoc """ 10 | Compiles LFE source files. 11 | 12 | Uses an [idea](https://github.com/elixir-lang/elixir/blob/e1c903a5956e4cb9075f0aac00638145788b0da4/lib/mix/lib/mix/compilers/erlang.ex#L20) from the Erlang Mix compiler to do so. 13 | 14 | These options are supported: 15 | 16 | ## Command line options 17 | * `--force` - forces compilation regardless of modification times 18 | * `--all-warnings` - prints warnings even from files that do not need to be recompiled 19 | 20 | ## Configuration 21 | 22 | The [Erlang compiler configuration](https://github.com/elixir-lang/elixir/blob/master/lib/mix/lib/mix/tasks/compile.erlang.ex#L31) is supported. 23 | Specific configuration options for the LFE compiler will be supported in future. 24 | """ 25 | 26 | @doc """ 27 | Runs this task. 28 | """ 29 | def run(args) do 30 | {opts, _, _} = OptionParser.parse(args, switches: @switches) 31 | do_run(opts) 32 | end 33 | 34 | defp do_run(opts) do 35 | dest = Mix.Project.compile_path() 36 | 37 | compile(manifest(), [{"src", dest}], opts) 38 | end 39 | 40 | @doc """ 41 | Returns LFE manifests. 42 | """ 43 | def manifests, do: [manifest()] 44 | 45 | @doc """ 46 | Cleans up compilation artifacts. 47 | """ 48 | def clean, do: clean(manifest()) 49 | 50 | defp manifest, do: Path.join(Mix.Project.manifest_path(), @manifest) 51 | end 52 | -------------------------------------------------------------------------------- /lib/mix/tasks/lfe.test.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Lfe.Test do 2 | use Mix.Task.Compiler 3 | import Mix.Compilers.Lfe 4 | 5 | @recursive true 6 | @manifest "test.lfe" 7 | @switches [force: :boolean, all_warnings: :boolean] 8 | 9 | @shortdoc "Runs a LFE project's tests" 10 | 11 | @moduledoc """ 12 | Compiles the source LFE source files of the project, using `Mix.Compilers.Lfe` 13 | and also compiles and runs all the test LFE files with the '-tests.lfe' extension in the 14 | 'test' folder of the project. 15 | 16 | Uses the [ltest](https://github.com/lfex/ltest) library to do so. 17 | 18 | For the compilation it supports the command line options and the configuration of the `Mix.Tasks.Compile.Lfe` task. 19 | """ 20 | 21 | @doc """ 22 | Runs this task. 23 | """ 24 | def run(args) do 25 | {opts, _, _} = OptionParser.parse(args, switches: @switches) 26 | Mix.env(:test) 27 | 28 | do_run(opts) 29 | end 30 | 31 | @doc """ 32 | Returns LFE test manifests. 33 | """ 34 | def manifests, do: [manifest()] 35 | 36 | defp do_run(opts) do 37 | dest = Mix.Project.compile_path() |> String.replace("_build/dev", "_build/test") 38 | location = String.split(dest, "_build") |> List.last() 39 | {:ok, root} = File.cwd() 40 | dest = root |> Path.join("_build") |> Path.join(location) 41 | 42 | File.rmdir(dest) 43 | File.mkdir_p!(dest) 44 | 45 | dest_test = dest |> Path.join("_build") |> Path.join(location) 46 | 47 | File.rmdir(dest_test) 48 | File.mkdir_p!(dest_test) 49 | 50 | compile(manifest(), [{"src", dest}, {"test", dest_test}], opts) 51 | 52 | File.cd(dest) 53 | 54 | :"ltest-runner".unit() 55 | end 56 | 57 | defp manifest, do: Path.join(Mix.Project.manifest_path(), @manifest) 58 | end 59 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MixLfe.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.2.0-rc3" 5 | 6 | def project do 7 | [ 8 | app: :mix_lfe, 9 | version: @version, 10 | elixir: "~> 1.6", 11 | start_permanent: Mix.env() == :prod, 12 | description: "A LFE compiler for Mix", 13 | compilers: Mix.compilers() ++ [:lfe], 14 | docs: [ 15 | extras: ["README.md"], 16 | main: "readme", 17 | source_ref: "v#{@version}", 18 | source_url: "https://github.com/meddle0x53/mix_lfe" 19 | ], 20 | package: package(), 21 | deps: deps() 22 | ] 23 | end 24 | 25 | def application do 26 | [extra_applications: [:logger]] 27 | end 28 | 29 | def package do 30 | %{ 31 | licenses: ["Apache 2"], 32 | links: %{"GitHub" => "https://github.com/meddle0x53/mix_lfe"}, 33 | maintainers: ["Nikolay Tsvetinov (Meddle)"] 34 | } 35 | end 36 | 37 | def deps do 38 | [ 39 | {:lfe, "~> 1.2"}, 40 | {:ltest, "0.10.0-rc6"}, 41 | {:color, "~> 1.0", hex: :erlang_color}, 42 | {:lutil, "~> 0.10.0-rc6"}, 43 | {:ex_doc, ">= 0.0.0", only: :dev} 44 | ] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "color": {:hex, :erlang_color, "1.0.0", "145fe1d2e65c4516e4f03fefca6c2f47ebad5899d978d70382a5cfe643e4ac9e", [:rebar3], [], "hexpm"}, 3 | "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, 4 | "erlang_color": {:hex, :erlang_color, "1.0.0", "145fe1d2e65c4516e4f03fefca6c2f47ebad5899d978d70382a5cfe643e4ac9e", [:rebar3], [], "hexpm"}, 5 | "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "lfe": {:hex, :lfe, "1.2.0", "257708859c0a6949f174cecee6f08bb5d6f08c0f97ec7b75c07072014723ecbc", [:rebar3], [], "hexpm"}, 7 | "ltest": {:hex, :ltest, "0.10.0-rc6", "a605158832d4dc2704cbb572423ec13d1a18dd4d48dec7aff7a3011d0261f9db", [:rebar3], [], "hexpm"}, 8 | "lutil": {:hex, :lutil, "0.10.0-rc6", "c3a560c9ed8cdc34b689591cf60336981ad4ea6ce299624ba87191d3fffb8da2", [:rebar3], [], "hexpm"}, 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/sample/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Sample.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :sample, version: "1.0.0"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/sample/src/tut4.lfe: -------------------------------------------------------------------------------- 1 | (defmodule tut4 2 | (export (convert 2))) 3 | 4 | (defun convert 5 | ((m 'inch) (/ m 2.54)) 6 | ((n 'centimeter) (* n 2.54))) 7 | -------------------------------------------------------------------------------- /test/fixtures/sample/src/tut5.lfe: -------------------------------------------------------------------------------- 1 | (defmodule tut5 2 | (export (convert-length 1))) 3 | 4 | (defun convert-length 5 | (((tuple 'centimeter x)) (tuple 'inch (/ x 2.54))) 6 | (((tuple 'inch y)) (tuple 'centimeter (* y 2.54)))) 7 | -------------------------------------------------------------------------------- /test/mix/tasks/compile.lfe_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Compile.LfeTest do 2 | use ExUnit.Case 3 | 4 | import Mix.Tasks.Compile.Lfe 5 | import ExUnit.CaptureIO 6 | 7 | @fixture_project Path.expand("../../fixtures/sample", __DIR__) 8 | 9 | defmodule Sample do 10 | def project do 11 | [app: :sample, version: "0.1.0", aliases: [sample: "compile"]] 12 | end 13 | end 14 | 15 | setup do 16 | Mix.start() 17 | Mix.shell(Mix.Shell.Process) 18 | Mix.Project.push(Sample) 19 | Mix.env(:dev) 20 | 21 | on_exit(fn -> 22 | Mix.env(:dev) 23 | Mix.Task.clear() 24 | Mix.Shell.Process.flush() 25 | Mix.ProjectStack.clear_cache() 26 | Mix.ProjectStack.clear_stack() 27 | 28 | delete_tmp_paths() 29 | end) 30 | 31 | :ok 32 | end 33 | 34 | test "compiles and cleans src/tut4.lfe and src/tut5.lfe" do 35 | in_fixture(fn -> 36 | assert run(["--verbose"]) == {:ok, []} 37 | assert_received {:mix_shell, :info, ["Compiled src/tut4.lfe"]} 38 | assert_received {:mix_shell, :info, ["Compiled src/tut5.lfe"]} 39 | 40 | assert File.regular?("_build/dev/lib/sample/ebin/tut4.beam") 41 | assert File.regular?("_build/dev/lib/sample/ebin/tut5.beam") 42 | 43 | assert run(["--verbose"]) == {:noop, []} 44 | refute_received {:mix_shell, :info, ["Compiled src/tut4.lfe"]} 45 | 46 | assert run(["--force", "--verbose"]) == {:ok, []} 47 | assert_received {:mix_shell, :info, ["Compiled src/tut4.lfe"]} 48 | assert_received {:mix_shell, :info, ["Compiled src/tut5.lfe"]} 49 | 50 | assert clean() 51 | refute File.regular?("_build/dev/lib/sample/ebin/tut4.beam") 52 | refute File.regular?("_build/dev/lib/sample/ebin/tut5.beam") 53 | end) 54 | end 55 | 56 | test "removes old artifact files" do 57 | in_fixture(fn -> 58 | assert run([]) == {:ok, []} 59 | assert File.regular?("_build/dev/lib/sample/ebin/tut4.beam") 60 | 61 | File.rm!("src/tut4.lfe") 62 | assert run([]) == {:ok, []} 63 | refute File.regular?("_build/dev/lib/sample/ebin/tut4.beam") 64 | end) 65 | end 66 | 67 | test "compilation purges the module" do 68 | in_fixture(fn -> 69 | # Create the first version of the module. 70 | defmodule :purge_test do 71 | def version, do: :v1 72 | end 73 | 74 | assert :v1 == :purge_test.version() 75 | 76 | # Create the second version of the module (this time as LFE source). 77 | File.write!("src/purge_test.lfe", """ 78 | (defmodule purge_test 79 | (export (version 0))) 80 | 81 | (defun version 82 | (() 'v2)) 83 | """) 84 | 85 | assert run([]) == {:ok, []} 86 | 87 | # If the module was not purged on recompilation, this would fail. 88 | assert :v2 == :purge_test.version() 89 | end) 90 | end 91 | 92 | test "continues even if one file fails to compile" do 93 | in_fixture(fn -> 94 | file = Path.absname("src/foo.lfe") 95 | 96 | File.write!(file, """ 97 | (defmodule foo 98 | (export (bar 0))) 99 | 100 | bar() -> bar. 101 | """) 102 | 103 | capture_io(fn -> 104 | assert {:error, [diagnostic]} = run([]) 105 | 106 | assert %Mix.Task.Compiler.Diagnostic{ 107 | compiler_name: "lfe_lint", 108 | file: ^file, 109 | message: "unknown form", 110 | position: 4, 111 | severity: :error 112 | } = diagnostic 113 | end) 114 | 115 | assert File.regular?("_build/dev/lib/sample/ebin/tut4.beam") 116 | assert File.regular?("_build/dev/lib/sample/ebin/tut5.beam") 117 | end) 118 | end 119 | 120 | defp in_fixture(function) do 121 | dest = Path.expand("../tmp", __DIR__) 122 | flag = String.to_charlist(dest) 123 | 124 | File.rm_rf!(dest) 125 | File.mkdir_p!(dest) 126 | File.cp_r!(@fixture_project, dest) 127 | 128 | get_path = :code.get_path() 129 | previous = :code.all_loaded() 130 | 131 | try do 132 | File.cd!(dest, function) 133 | after 134 | :code.set_path(get_path) 135 | 136 | for {mod, file} <- :code.all_loaded() -- previous, 137 | file == [] or (is_list(file) and :lists.prefix(flag, file)) do 138 | purge([mod]) 139 | end 140 | end 141 | end 142 | 143 | defp purge(modules) do 144 | Enum.each(modules, fn m -> 145 | :code.purge(m) 146 | :code.delete(m) 147 | end) 148 | end 149 | 150 | defp delete_tmp_paths do 151 | "../tmp" |> Path.expand(__DIR__) |> File.rm_rf!() 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------