Skip to content

Commit 7b5d021

Browse files
authored
Replace wrapping with meck with a custom solution. (#1)
A meck-based solution was a good PoC, but ultimately not production-grade due to slowdowns introduced with piping all of the compilation through a single process and various overheads related to keeping track of mock expectations.
1 parent dfd3f39 commit 7b5d021

File tree

6 files changed

+87
-51
lines changed

6 files changed

+87
-51
lines changed

README.md

+23-23
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@ This often results in repetitive code patterns such as `ctx = %{variable: variab
1515
I believe that introducing a shorthand form of object creation to Elixir enhances the language's ergonomics and is a natural extension of its existing map literals syntax.
1616
This feature will be immediately familiar to JavaScript and Rust developers, and similar shorthands are present in other languages such as Go.
1717

18+
## Installation
19+
20+
The package can be installed by adding `es6_maps` to your list of dependencies and compilers in `mix.exs`:
21+
22+
```elixir
23+
def project do
24+
[
25+
app: :testme,
26+
version: "0.1.0",
27+
compilers: [:es6_maps | Mix.compilers()],
28+
deps: deps()
29+
]
30+
end
31+
32+
def deps do
33+
[
34+
{:es6_maps, "~> 0.1.0", runtime: false}
35+
]
36+
end
37+
```
38+
1839
## Usage
1940

2041
### Creating maps
@@ -69,31 +90,10 @@ iex> hello
6990

7091
## How does it work?
7192

72-
`es6_maps` uses [`meck`](https://github.com/eproxus/meck) to replace the implementation of Elixir compiler's `elixir_map` module.
73-
The module's `expand_map/4` function is then replaced to expand map keys `%{k}` as if they were `%{k: k}`.
93+
`es6_maps` replaces in runtime the Elixir compiler's `elixir_map` module.
94+
The module's `expand_map/4` function is wrapped with a function that replaces map keys `%{k}` as if they were `%{k: k}`.
7495
After `es6_maps` runs as one of the Mix compilers, the Elixir compiler will use the replaced functions to compile the rest of the code.
7596

7697
> [!IMPORTANT]
7798
> By the nature of this solution it's tightly coupled to the internal Elixir implementation.
7899
> The current version of `es6_maps` should work for Elixir 1.15, 1.16 and the upcoming 1.17 version, but may break in the future.
79-
80-
## Installation
81-
82-
The package can be installed by adding `es6_maps` to your list of dependencies and compilers in `mix.exs`:
83-
84-
```elixir
85-
def project do
86-
[
87-
app: :testme,
88-
version: "0.1.0",
89-
compilers: [:es6_maps | Mix.compilers()],
90-
deps: deps()
91-
]
92-
end
93-
94-
def deps do
95-
[
96-
{:es6_maps, "~> 0.1.0", runtime: false}
97-
]
98-
end
99-
```

lib/mix/tasks/compile.es6_map.ex

-23
This file was deleted.

lib/mix/tasks/compile/es6_map.ex

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
defmodule Mix.Tasks.Compile.Es6Maps do
2+
@moduledoc false
3+
4+
use Mix.Task.Compiler
5+
6+
def run(_args) do
7+
{:elixir_map, elixir_map_bytecode, elixir_map_filename} = :code.get_object_code(:elixir_map)
8+
elixir_map_forms = abstract_code(elixir_map_bytecode)
9+
compile_opts = compile_opts(elixir_map_bytecode)
10+
injected_forms = injected_forms()
11+
12+
{forms_start, [expand_map_forms | forms_end]} =
13+
Enum.split_while(elixir_map_forms, &(not function?(&1, expand_map: 4)))
14+
15+
expand_map_orig_forms = rename_function(expand_map_forms, :es6_maps_expand_map_orig)
16+
forms = Enum.concat([forms_start, injected_forms, [expand_map_orig_forms], forms_end])
17+
18+
{:ok, :elixir_map, binary, _warnings} =
19+
:compile.forms(forms, [:return_errors, :return_warnings | compile_opts])
20+
21+
{:module, :elixir_map} = :code.load_binary(:elixir_map, elixir_map_filename, binary)
22+
23+
:ok
24+
end
25+
26+
defp function?({:function, _, name, arity, _}, names), do: {name, arity} in names
27+
defp function?(_form, _names), do: false
28+
29+
defp rename_function({:function, meta, _name, arity, clauses}, new_name),
30+
do: {:function, meta, new_name, arity, clauses}
31+
32+
defp abstract_code(bytecode) do
33+
{:ok, {_, abstract_code: abstract_code}} = :beam_lib.chunks(bytecode, [:abstract_code])
34+
{:raw_abstract_v1, abstract} = abstract_code
35+
abstract
36+
end
37+
38+
defp compile_opts(bytecode) do
39+
{:ok, {_, compile_info: info}} = :beam_lib.chunks(bytecode, [:compile_info])
40+
Keyword.fetch!(info, :options)
41+
end
42+
43+
defp injected_forms do
44+
{:module, _, bytecode, _} =
45+
defmodule Es6Map.InjectedCode do
46+
def expand_map(meta, args, s, e),
47+
do: es6_maps_expand_map_orig(meta, expand_atom_keys(args), s, e)
48+
49+
defp expand_atom_keys(args) do
50+
Enum.map(args, fn
51+
{:|, meta, [map, inner_args]} -> {:|, meta, [map, expand_atom_keys(inner_args)]}
52+
{k, _meta, ctx} = v when is_atom(ctx) -> {k, v}
53+
other -> other
54+
end)
55+
end
56+
57+
defp es6_maps_expand_map_orig(_meta, _args, _s, _e), do: nil
58+
end
59+
60+
bytecode
61+
|> abstract_code()
62+
|> Enum.filter(&function?(&1, expand_map: 4, expand_atom_keys: 1))
63+
end
64+
end

mix.exs

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ defmodule Es6Maps.MixProject do
2525

2626
defp deps do
2727
[
28-
{:meck, "~> 0.9"},
2928
{:ex_doc, "~> 0.31", only: :dev, runtime: false}
3029
]
3130
end

mix.lock

-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@
44
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
55
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
66
"makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
7-
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
87
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
98
}

test/es6_maps_test/mix.lock

-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +0,0 @@
1-
%{
2-
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
3-
}

0 commit comments

Comments
 (0)