From dfb24274b0565dc491c9cc55b8ffbc3ee696f121 Mon Sep 17 00:00:00 2001 From: Robert Prehn <3952444+prehnRA@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:45:35 -0600 Subject: [PATCH] Commit initially --- .formatter.exs | 4 ++ .gitignore | 24 ++++++++ README.md | 21 +++++++ lib/beam_js.ex | 133 ++++++++++++++++++++++++++++++++++++++++++ mix.exs | 29 +++++++++ mix.lock | 3 + test/beam_js_test.exs | 8 +++ test/test_helper.exs | 1 + 8 files changed, 223 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/beam_js.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/beam_js_test.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b79f5a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Temporary files, for example, from tests. +/tmp/ + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +beam_js-*.tar + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2db58f --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# BeamJs + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `beam_js` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:beam_js, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/lib/beam_js.ex b/lib/beam_js.ex new file mode 100644 index 0000000..bc3d615 --- /dev/null +++ b/lib/beam_js.ex @@ -0,0 +1,133 @@ +defmodule BeamJs do + @moduledoc """ + Documentation for `BeamJs`. + """ + require Logger + + def module_code(module) do + module + |> BeamFile.byte_code() + |> ok(fn {_, _, _, _, _, sections} -> + {:ok, Enum.filter(sections, fn + {:function, _fun_name, _arity, _, _code} -> + true + _other -> + false + end)} + end) + |> ok(fn functions -> + Enum.map(functions, fn {:function, name, arity, _, code} -> + {"#{name}/#{arity}", transform_function_code(code)} + end) + |> Map.new() + end) + end + + def transform_function_code(ops) do + Enum.reduce(ops, [], fn + {:label, number}, [{line_no, head_ops} | tail] -> + [{number, []} | [{line_no, Enum.reverse(head_ops)} | tail]] + + {:label, number}, acc -> + [{number, []} | acc] + + {:line, _line_number}, [] -> + [] + + other_op, [{number, head} | tail] -> + [{number, [other_op|head]} | tail] + end) + |> Enum.reverse() + end + + def encode_fun(label_sections) do + "{\n" <> Enum.map_join(label_sections, ",\n", fn {label, ops} -> + body = Enum.map_join(ops, "", &encode_op/1) + " #{label}: (p) => {\n#{body}}" + end) <> "}" + end + + def encode_op(tuple) when is_tuple(tuple) do + op_name = elem(tuple, 0) + args = + try do + encode_args(tuple) + rescue + e -> + Logger.error("Error encoding op: #{inspect(tuple)}") + reraise e, __STACKTRACE__ + end + + " BeamJs.Ops.#{op_name}(p, " <> Enum.join(args, ", ") <> ");\n" + end + + def encode_op(atom) when is_atom(atom), do: " BeamJs.Ops.#{atom}(p);\n" + + def encode_args({:allocate, frame_size, live}), do: [number(frame_size), number(live)] + def encode_args({:line, number}), do: [number(number)] + def encode_args({:func_info, {:atom, m}, {:atom, f}, arity}), do: [quoted_string(m), quoted_string(f), number(arity)] + def encode_args({:select_val, source, {:f, fail_label}, comparison}), do: [encode_source(source), number(fail_label), encode_value(comparison)] + def encode_args({:move, source, {register_bank, number}}) when register_bank in [:x, :y], do: [encode_source(source), quoted_string(register_bank), number(number)] + def encode_args({:call, _live, {_m, f, a}}), do: [quoted_string(f), number(a)] + def encode_args({:call_ext_only, _live, {:extfunc, m, f, a}}), do: [quoted_string(m), quoted_string(f), number(a)] + def encode_args({:call_only, _live, {_m, f, a}}), do: [quoted_string(f), number(a)] + def encode_args({:deallocate, live}), do: [number(live)] + def encode_args({:gc_bif, bif, {:f, fail_to}, _unknown, args, {register_bank, number}}) when register_bank in [:x, :y] do + [ + quoted_string(bif), + number(fail_to), + "[" <> (args |> Enum.map(&encode_source/1) |> Enum.join(", ")) <> "]", + register_bank, + number + ] + end + + def encode_source({:tr, source, _}), do: encode_source(source) + def encode_source({:x, number}), do: "p.registers.x[#{number}]" + def encode_source({:y, number}), do: "p.registers.y[#{number}]" + def encode_source({:integer, number}), do: number(number) + def encode_source(value), do: encode_value(value) + + + def encode_value({:literal, value}), do: encode_value(value) + + def encode_value({type, value}) when type in [:atom, :x, :y, :list] do + ~s|BeamJs.boxed("#{type}", #{encode_value(value)})| + end + + def encode_value(tuple) when is_tuple(tuple) do + ~s|BeamJs.boxed("tuple", #{encode_value(Tuple.to_list(tuple))})| + end + + def encode_value(value) when is_binary(value) do + if String.valid?(value) do + quoted_string(value) + else + # Likely a raw bytes array + base64 = value |> Base.encode64() |> quoted_string() + ~s|atob(#{base64})| + end + end + + def encode_value(value) when is_list(value) do + ~s|[#{Enum.map_join(value, ", ", &encode_value/1)}]| + end + + def encode_value(value) when is_atom(value) do + quoted_string(value) + end + + def encode_value(value) when is_number(value) do + "#{value}" + end + + defp quoted_string(v), do: ~s|"#{v}"| + defp number(n), do: "#{n}" + + def fact(0), do: 1 + def fact(1), do: 1 + def fact(n), do: fact(n - 1) + fact(n - 2) + + defp ok({:ok, value}, fun), do: fun.(value) + defp ok(other, _fun), do: other +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..d6ec3df --- /dev/null +++ b/mix.exs @@ -0,0 +1,29 @@ +defmodule BeamJs.MixProject do + use Mix.Project + + def project do + [ + app: :beam_js, + version: "0.1.0", + elixir: "~> 1.19", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + {:beam_file, "~> 0.6.4"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..ddb8226 --- /dev/null +++ b/mix.lock @@ -0,0 +1,3 @@ +%{ + "beam_file": {:hex, :beam_file, "0.6.4", "73660f76330e6b29be2b8ae3779b30cb67a7803cde88a21e3e74371130eed4d4", [:mix], [], "hexpm", "3f295dba08a68360903e86be4f183d7fb70f762ee37ee176438dde23ea494431"}, +} diff --git a/test/beam_js_test.exs b/test/beam_js_test.exs new file mode 100644 index 0000000..e326d3b --- /dev/null +++ b/test/beam_js_test.exs @@ -0,0 +1,8 @@ +defmodule BeamJsTest do + use ExUnit.Case + doctest BeamJs + + test "greets the world" do + assert BeamJs.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()