defmodule Hologram.Compiler.Encoder do @moduledoc false if Application.compile_env(:hologram, :debug_encoder) do use Interceptor.Annotated, config: %{ {Hologram.Compiler.Encoder, :encode_ir, 2} => [ on_success: {Hologram.Compiler.Encoder, :debug, 3}, on_error: {Hologram.Compiler.Encoder, :debug, 3} ] } end alias Hologram.Commons.IntegerUtils alias Hologram.Commons.StringUtils alias Hologram.Compiler.Context alias Hologram.Compiler.IR alias Hologram.Reflection @doc """ Encodes Elixir or Erlang alias as JavaScript class name. ## Examples iex> encode_as_class_name(Aaa.Bbb.Ccc) "Elixir_Aaa_Bbb_Ccc" iex> encode_as_class_name(:erlang) "Erlang" iex> encode_as_class_name(:aaa_bbb) "Erlang_Aaa_Bbb" """ @spec encode_as_class_name(module | atom) :: String.t() def encode_as_class_name(alias_atom) def encode_as_class_name(:erlang), do: "Erlang" def encode_as_class_name(alias_atom) do module_segments = alias_atom |> to_string() |> String.split([".", "_"]) class_segments = if hd(module_segments) == "Elixir" do module_segments else ["Erlang" | module_segments] end Enum.map_join(class_segments, "_", &:string.titlecase/1) end @doc """ Encodes an Elixir function into a JavaScript statement. """ @spec encode_elixir_function( String.t(), atom, non_neg_integer, :public | :private, list(IR.FunctionClause.t()), Context.t() ) :: String.t() def encode_elixir_function(module_name, function, arity, visibility, clauses, context) do clauses_js = encode_as_array(clauses, context) ~s/Interpreter.defineElixirFunction("#{module_name}", "#{function}", #{arity}, "#{visibility}", #{clauses_js});/ end @doc """ Extracts JavaScript source code for the given ported Erlang function and generates interpreter function definition JavaScript statetement. """ @spec encode_erlang_function(module, atom, integer, String.t()) :: String.t() def encode_erlang_function(module, function, arity, erlang_source_dir) do file_path = if module == :erlang do "#{erlang_source_dir}/erlang.mjs" else "#{erlang_source_dir}/#{module}.mjs" end source_code = if File.exists?(file_path) do extract_erlang_function_source_code(file_path, function, arity) else nil end if source_code do normalized_source_code = StringUtils.normalize_newlines(source_code) ~s/Interpreter.defineErlangFunction("#{module}", "#{function}", #{arity}, #{normalized_source_code});/ else ~s/Interpreter.defineNotImplementedErlangFunction("#{module}", "#{function}", #{arity});/ end end @doc """ Encodes Elixir IR to JavaScript source code. ## Examples iex> ir = %IR.ListType{ ...> data: [ ...> %IR.IntegerType{value: 1}, ...> %IR.AtomType{value: :abc} ...> ] ...> } iex> encode_ir(ir, %Context{}) "Type.list([Type.integer(1), Type.atom(\"abc\")])" """ @intercept true @spec encode_ir(IR.t(), Context.t()) :: String.t() def encode_ir(ir, context) def encode_ir(%IR.AnonymousFunctionCall{function: function, args: args}, context) do function_js = encode_ir(function, context) args_js = encode_as_array(args, context) "Interpreter.callAnonymousFunction(#{function_js}, #{args_js})" end def encode_ir( %IR.AnonymousFunctionType{ arity: arity, captured_function: nil, captured_module: nil, clauses: clauses }, context ) do clauses_js = encode_as_array(clauses, context) "Type.anonymousFunction(#{arity}, #{clauses_js}, context)" end def encode_ir( %IR.AnonymousFunctionType{ arity: arity, captured_function: captured_function, captured_module: captured_module, clauses: clauses }, context ) do captured_function_js = encode_as_string(captured_function, true) captured_module_str = if Reflection.elixir_module?(captured_module) do Reflection.module_name(captured_module) else ":#{captured_module}" end captured_module_js = encode_as_string(captured_module_str, true) clauses_js = encode_as_array(clauses, context) "Type.functionCapture(#{captured_module_js}, #{captured_function_js}, #{arity}, #{clauses_js}, context)" end def encode_ir(%IR.AtomType{value: value}, _context) do encode_primitive_type(:atom, value, true) end # See: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1 def encode_ir( %IR.BitstringSegment{value: %IR.StringType{value: value}, modifiers: modifiers}, context ) do value_str = encode_primitive_type(:string, value, true) encode_bitstring_segment(value_str, modifiers, context) end # See: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1 def encode_ir( %IR.BitstringSegment{value: value, modifiers: modifiers}, context ) do value_str = encode_ir(value, context) encode_bitstring_segment(value_str, modifiers, context) end def encode_ir(%IR.BitstringType{segments: segments}, %{pattern?: true} = context) do segments |> encode_bitstring_segments(context) |> StringUtils.wrap("Type.bitstringPattern([", "])") end def encode_ir(%IR.BitstringType{segments: segments}, %{pattern?: false} = context) do segments |> encode_bitstring_segments(context) |> StringUtils.wrap("Type.bitstring([", "])") end def encode_ir(%IR.Block{} = block, context) do "(#{encode_closure(block, context)})(context)" end def encode_ir(%IR.Case{condition: condition, clauses: clauses}, context) do condition_js = case condition do %IR.Block{} = block -> encode_closure(block, context) expr -> encode_ir(expr, context) end clauses_js = encode_as_array(clauses, context) "Interpreter.case(#{condition_js}, #{clauses_js}, context)" end def encode_ir(%IR.Clause{} = clause, context) do match = encode_ir(clause.match, %{context | pattern?: true}) guards = encode_as_array(clause.guards, context, &encode_closure/2) body = encode_closure(clause.body, context) "{match: #{match}, guards: #{guards}, body: #{body}}" end def encode_ir(%IR.Comprehension{} = comprehension, context) do generators = encode_as_array(comprehension.generators, context) filters = encode_as_array(comprehension.filters, context) collectable = encode_ir(comprehension.collectable, context) unique = comprehension.unique.value mapper = encode_closure(comprehension.mapper, context) "Interpreter.comprehension(#{generators}, #{filters}, #{collectable}, #{unique}, #{mapper}, context)" end def encode_ir(%IR.ComprehensionFilter{expression: expr}, context) do encode_closure(expr, context) end def encode_ir(%IR.Cond{clauses: clauses_ir}, context) do clauses_js = encode_as_array(clauses_ir, context) "Interpreter.cond(#{clauses_js}, context)" end def encode_ir(%IR.CondClause{condition: condition_ir, body: body_ir}, context) do condition_js = encode_closure(condition_ir, context) body_js = encode_closure(body_ir, context) "{condition: #{condition_js}, body: #{body_js}}" end def encode_ir(%IR.ConsOperator{head: head, tail: tail}, %{pattern?: true} = context) do "Type.consPattern(#{encode_ir(head, context)}, #{encode_ir(tail, context)})" end def encode_ir(%IR.ConsOperator{head: head, tail: tail}, %{pattern?: false} = context) do "Interpreter.consOperator(#{encode_ir(head, context)}, #{encode_ir(tail, context)})" end def encode_ir(%IR.DotOperator{left: left, right: right}, context) do left_js = encode_ir(left, context) right_js = encode_ir(right, context) "Interpreter.dotOperator(#{left_js}, #{right_js})" end def encode_ir(%IR.FloatType{value: value}, _context) do encode_primitive_type(:float, value, false) end def encode_ir(%IR.FunctionClause{} = clause, context) do params_array = encode_as_array(clause.params, %{context | pattern?: true}) params_closure = "(context) => #{params_array}" guards = encode_as_array(clause.guards, context, &encode_closure/2) body = encode_closure(clause.body, context) "{params: #{params_closure}, guards: #{guards}, body: #{body}}" end def encode_ir(%IR.IntegerType{value: value}, _context) do encode_primitive_type(:integer, "#{value}n", false) end def encode_ir(%IR.ListType{data: data}, context) do data_str = encode_as_array(data, context) "Type.list(#{data_str})" end def encode_ir( %IR.LocalFunctionCall{function: function, args: args}, %{module: module} = context ) do module_ir = %IR.AtomType{value: module} encode_named_function_call(module_ir, function, args, context) end def encode_ir(%IR.MapType{data: data}, context) do data |> Enum.sort() |> Enum.map_join(", ", fn {key, value} -> "[" <> encode_ir(key, context) <> ", " <> encode_ir(value, context) <> "]" end) |> StringUtils.wrap("Type.map([", "])") end def encode_ir(%IR.MatchOperator{left: left, right: right}, %{match_operator?: true} = context) do left = encode_ir(left, %{context | pattern?: true}) right = encode_ir(right, context) "Interpreter.matchOperator(#{right}, #{left}, context)" end def encode_ir(%IR.MatchOperator{left: left, right: right}, context) do left = encode_ir(left, %{context | match_operator?: true, pattern?: true}) right = encode_ir(right, %{context | match_operator?: true}) "Interpreter.matchOperator(#{right}, #{left}, context)" end def encode_ir(%IR.MatchPlaceholder{}, _context) do "Type.matchPlaceholder()" end def encode_ir(%IR.ModuleAttributeOperator{name: name}, _context) do encode_var("@#{name}", nil) end def encode_ir(%IR.ModuleDefinition{module: module} = module_def, context) do module_name = Reflection.module_name(module.value) module_def |> IR.aggregate_module_funs() |> Enum.reduce([], fn {{function, arity}, {visibility, clauses}}, acc -> [encode_elixir_function(module_name, function, arity, visibility, clauses, context) | acc] end) |> Enum.reverse() |> Enum.join("\n\n") rescue error -> message = StringUtils.normalize_newlines(""" can't encode #{Reflection.module_name(module.value)} module definition #{Exception.message(error)}\ """) reraise RuntimeError, [message: message], __STACKTRACE__ end # See info about the internal structure of PIDs: https://stackoverflow.com/a/262179/13040586 def encode_ir(%IR.PIDType{value: value}, context) do segments = value |> :erlang.pid_to_list() |> List.delete_at(0) |> List.delete_at(-1) |> to_string() |> String.split(".") |> Enum.map(&IntegerUtils.parse!/1) encode_identifier(:pid, value, segments, context) end def encode_ir(%IR.PinOperator{variable: variable}, context) do encode_ir(variable, %{context | pattern?: false}) end def encode_ir(%IR.PortType{value: value}, context) do segments = value |> :erlang.port_to_list() |> Enum.drop(6) |> List.delete_at(-1) |> to_string() |> String.split(".") |> Enum.map(&IntegerUtils.parse!/1) encode_identifier(:port, value, segments, context) end def encode_ir(%IR.ReferenceType{value: value}, context) do segments = value |> :erlang.ref_to_list() |> Enum.drop(5) |> List.delete_at(-1) |> to_string() |> String.split(".") |> Enum.map(&IntegerUtils.parse!/1) encode_identifier(:reference, value, segments, context) end def encode_ir( %IR.RemoteFunctionCall{ module: module, function: function, args: args }, context ) do encode_named_function_call(module, function, args, context) end def encode_ir(%IR.StringType{value: value}, _context) do encode_primitive_type(:bitstring, value, true) end # TODO: catch_clauses, else_clauses, after_block def encode_ir(%IR.Try{} = ir, context) do body_js = encode_closure(ir.body, context) rescue_clauses_js = encode_as_array(ir.rescue_clauses, context) "Interpreter.try(#{body_js}, #{rescue_clauses_js}, [], [], null, context)" end def encode_ir(%IR.TryRescueClause{} = ir, context) do variable_js = if ir.variable do encode_ir(ir.variable, %{context | pattern?: true}) else "null" end modules_js = encode_as_array(ir.modules, context) body_js = encode_closure(ir.body, context) "{variable: #{variable_js}, modules: #{modules_js}, body: #{body_js}}" end def encode_ir(%IR.TupleType{data: data}, context) do data_js = encode_as_array(data, context) "Type.tuple(#{data_js})" end def encode_ir(%IR.Variable{name: name, version: version}, %{pattern?: true}) do var_name = encode_var_name(name, version) ~s/Type.variablePattern("#{var_name}")/ end def encode_ir(%IR.Variable{name: name, version: version}, %{pattern?: false}) do encode_var(name, version) end # TODO: finish implementing def encode_ir(%IR.With{}, _context) do "Interpreter.with()" end @doc """ Encodes Elixir term into JavaScript. If the term can be encoded into JavaScript then the result is in the shape of {:ok, js}. If the term can't be encoded into JavaScript then an error message is returned in the shape of {:error, message}. """ @spec encode_term(any) :: {:ok, String.t()} | {:error, String.t()} def encode_term(term) do {:ok, encode_term!(term)} rescue e in ArgumentError -> {:error, e.message} end @doc """ Encodes Elixir term into JavaScript, erroring out if the term can't be encoded into JavaScript. """ @spec encode_term!(any) :: String.t() def encode_term!(term) do term |> IR.for_term!() |> encode_ir(%Context{}) end @doc """ Prints debug info for intercepted encode_ir/2 calls. """ @spec debug( {module, atom, list(IR.t() | Context.t())}, String.t() | %{__struct__: FunctionClauseError}, integer ) :: :ok def debug({_module, _function, [ir, context] = _args}, result, _start_timestamp) do # credo:disable-for-lines:10 /Credo.Check.Refactor.IoPuts|Credo.Check.Warning.IoInspect/ IO.puts("\nENCODE IR...............................\n") IO.puts("ir") IO.inspect(ir) IO.puts("") IO.puts("context") IO.inspect(context) IO.puts("") IO.puts("result") IO.inspect(result) IO.puts("\n........................................\n") end defp encode_as_array(data, context, encoder \\ &encode_ir/2) do data |> Enum.map_join(", ", &encoder.(&1, context)) |> StringUtils.wrap("[", "]") end defp encode_as_string(value, wrap) defp encode_as_string(nil, false) do "nil" end defp encode_as_string(value, false) do value |> to_string() |> escape_non_printable_and_special_chars() end defp encode_as_string(value, true) do value |> encode_as_string(false) |> StringUtils.wrap("\"", "\"") end defp encode_bitstring_modifier({:size, size}, context) do "size: #{encode_ir(size, context)}" end defp encode_bitstring_modifier({:unit, unit}, _context) do "unit: #{unit}n" end defp encode_bitstring_modifier({name, value}, _context) do ~s(#{name}: "#{value}") end defp encode_bitstring_segment(value_str, modifiers, context) do modifiers_str = modifiers |> Enum.map_join(", ", &encode_bitstring_modifier(&1, context)) |> StringUtils.wrap("{", "}") "Type.bitstringSegment(#{value_str}, #{modifiers_str})" end defp encode_bitstring_segments(segments, context) do Enum.map_join(segments, ", ", &encode_ir(&1, context)) end defp encode_block_body(%IR.Block{expressions: exprs}, context) do exprs = if exprs == [] do [%IR.AtomType{value: nil}] else exprs end expr_count = Enum.count(exprs) body = exprs |> Enum.with_index() |> Enum.map_join("", fn {expr, idx} -> expr_js = encode_ir(expr, context) last_expr? = idx == expr_count - 1 has_match_operator? = has_match_operator?(expr) encode_block_expr(expr_js, last_expr?, has_match_operator?) end) "{#{body}\n}" end defp encode_block_expr(expr_js, last_expr?, has_match_operator?) defp encode_block_expr(expr_js, true, true) do StringUtils.normalize_newlines(""" globalThis.hologram.return = #{expr_js}; Interpreter.updateVarsToMatchedValues(context); return globalThis.hologram.return;\ """) end defp encode_block_expr(expr_js, true, false) do "\nreturn #{expr_js};" end defp encode_block_expr(expr_js, false, true) do StringUtils.normalize_newlines(""" #{expr_js}; Interpreter.updateVarsToMatchedValues(context);\ """) end defp encode_block_expr(expr_js, false, false) do "\n#{expr_js};" end defp encode_closure(ir, context) defp encode_closure(nil, _context), do: "null" defp encode_closure(%IR.Block{} = ir, context) do "(context) => #{encode_block_body(ir, context)}" end defp encode_closure(ir, context) do "(context) => #{encode_ir(ir, context)}" end defp encode_dynamic_named_function_call(module, function, args, context) do module_js = encode_ir(module, context) function_js = encode_ir(function, context) args_js = encode_ir(args, context) "Interpreter.callNamedFunction(#{module_js}, #{function_js}, #{args_js}, context)" end defp encode_identifier(type, value, segments, context) do encoded_node = encode_as_string(node(value), true) integer_encoder = fn integer, _context -> to_string(integer) end encoded_segments = encode_as_array(segments, context, integer_encoder) "Type.#{type}(#{encoded_node}, #{encoded_segments})" end defp encode_named_function_call(%IR.AtomType{value: :erlang}, :andalso, [left, right], context) do left_js = encode_closure(left, context) right_js = encode_closure(right, context) "Erlang[\"andalso/2\"](#{left_js}, #{right_js}, context)" end defp encode_named_function_call( %IR.AtomType{value: :erlang}, :apply, [module, function, args], context ) do encode_dynamic_named_function_call(module, function, args, context) end defp encode_named_function_call(%IR.AtomType{value: :erlang}, :orelse, [left, right], context) do left_js = encode_closure(left, context) right_js = encode_closure(right, context) "Erlang[\"orelse/2\"](#{left_js}, #{right_js}, context)" end defp encode_named_function_call(%IR.AtomType{} = module, function, args, context) do class = encode_as_class_name(module.value) arity = Enum.count(args) args_js = Enum.map_join(args, ", ", &encode_ir(&1, context)) "#{class}[\"#{function}/#{arity}\"](#{args_js})" end defp encode_named_function_call(module_ir, function, args, context) do function_ir = if is_atom(function) do %IR.AtomType{value: function} else function end args_ir = if is_list(args) do %IR.ListType{data: args} else args end encode_dynamic_named_function_call(module_ir, function_ir, args_ir, context) end defp encode_primitive_type(type, value, as_string) defp encode_primitive_type(type, value, true) do value_str = encode_as_string(value, true) encode_primitive_type(type, value_str, false) end defp encode_primitive_type(type, value, false) do "Type.#{type}(#{value})" end defp encode_var(name, version) do var_name = encode_var_name(name, version) if String.match?(var_name, ~r/[^a-zA-Z0-9_]+/) do ~s'context.vars["#{var_name}"]' else "context.vars.#{var_name}" end end defp encode_var_name(name, nil) do encode_as_string(name, false) end defp encode_var_name(name, version) do encode_as_string(name, false) <> "_#{version}" end defp escape_non_printable_and_special_chars(str) defp escape_non_printable_and_special_chars("\\" <> rest) do "\\\\" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars("\"" <> rest) do "\\\"" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars("\a" <> rest) do "\\x07" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars("\b" <> rest) do "\\b" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars("\f" <> rest) do "\\f" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars("\n" <> rest) do "\\n" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars("\r" <> rest) do "\\r" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars("\t" <> rest) do "\\t" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars("\v" <> rest) do "\\v" <> escape_non_printable_and_special_chars(rest) end # Line separator character (LS) # (JavaScript editors have problems with this char) defp escape_non_printable_and_special_chars(<<8_232::utf8>> <> rest) do "\\u{2028}" <> escape_non_printable_and_special_chars(rest) end # Paragraph separator character (PS) # (JavaScript editors have problems with this char) defp escape_non_printable_and_special_chars(<<8_233::utf8>> <> rest) do "\\u{2029}" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars(<>) do char = <> escaped_char = if String.printable?(char) do char else "\\u{#{Integer.to_string(code, 16)}}" end escaped_char <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars(<>) do # No need to pad with 0, because chars smaller that 16 will be encoded differently "\\x#{Integer.to_string(char, 16)}" <> escape_non_printable_and_special_chars(rest) end defp escape_non_printable_and_special_chars(""), do: "" defp extract_erlang_function_source_code(file_path, function, arity) do key = "#{function}/#{arity}" start_marker = "// Start #{key}" end_marker = "// End #{key}" regex = ~r/#{Regex.escape(start_marker)}[[:space:]]+"#{Regex.escape(key)}":[[:space:]]+(.+),[[:space:]]+#{Regex.escape(end_marker)}/s file_contents = File.read!(file_path) case Regex.run(regex, file_contents) do [_full_capture, source_code] -> source_code nil -> nil end end defp has_match_operator?(ir) defp has_match_operator?(%IR.MatchOperator{}), do: true defp has_match_operator?(ir) when is_list(ir) do Enum.any?(ir, &has_match_operator?/1) end defp has_match_operator?(ir) when is_tuple(ir) do ir |> Tuple.to_list() |> has_match_operator?() end defp has_match_operator?(%_struct{} = ir) do ir |> Map.from_struct() |> has_match_operator?() end defp has_match_operator?(ir) when is_map(ir) do Enum.any?(ir, fn {key, value} -> has_match_operator?(key) || has_match_operator?(value) end) end defp has_match_operator?(_ast), do: false end