if Code.ensure_loaded?(QuickBEAM) do defmodule Reach.Plugins.QuickBEAM.JavaScriptFrontend do @moduledoc """ QuickBEAM JavaScript source frontend — parses `.js` files into Reach IR. Uses QuickBEAM to compile JavaScript to QuickJS bytecode, then translates the decoded bytecode into Reach IR nodes. Only available when the `:quickbeam` package is installed. """ alias Reach.IR.{Counter, Node} import Reach.IR.Helpers, only: [mark_as_definitions: 1] def extensions, do: [".js", ".ts", ".tsx", ".jsx", ".mjs"] @spec parse(String.t(), keyword()) :: {:ok, [Node.t()]} | {:error, term()} def parse(source, opts \\ []) do file = Keyword.get(opts, :file, "nofile") counter = Keyword.get(opts, :counter, Counter.new()) source = prepare_source(source, file) with {:ok, rt} <- QuickBEAM.start(apis: false) do try do case QuickBEAM.disasm(rt, source) do {:ok, bc} -> {:ok, translate_bytecode(bc, counter, file)} {:error, reason} -> {:error, reason} end rescue e in [ArgumentError, ErlangError, MatchError, RuntimeError] -> {:error, Exception.message(e)} after QuickBEAM.stop(rt) end end end defp prepare_source(source, file) do source |> maybe_strip_types(file) |> strip_module_syntax() end defp maybe_strip_types(source, file) do if String.ends_with?(to_string(file), [".ts", ".tsx"]) and Code.ensure_loaded?(OXC) do case OXC.transform(source, Path.basename(to_string(file)), sourcemap: false) do {:ok, js} when is_binary(js) -> js {:ok, %{code: js}} -> js _ -> source end else source end end defp strip_module_syntax(source) do if Code.ensure_loaded?(OXC) do patch_module_syntax(source) else source end end defp patch_module_syntax(source) do case OXC.parse(source, "module.js") do {:ok, ast} -> patches = Enum.flat_map(ast.body, &module_syntax_patch/1) if patches == [], do: source, else: OXC.patch_string(source, patches) {:error, _} -> source end end defp module_syntax_patch(%{type: t} = node) when t in [:export_named_declaration, :export_default_declaration] do case Map.get(node, :declaration) do nil -> [%{start: node.start, end: node[:end], change: ""}] decl -> [%{start: node.start, end: decl.start, change: ""}] end end defp module_syntax_patch(%{type: :import_declaration} = node) do [%{start: node.start, end: node[:end], change: ""}] end defp module_syntax_patch(_), do: [] @spec parse!(String.t(), keyword()) :: [Node.t()] def parse!(source, opts \\ []) do case parse(source, opts) do {:ok, nodes} -> nodes {:error, reason} -> raise ArgumentError, "JS parse error: #{inspect(reason)}" end end @spec parse_file(Path.t(), keyword()) :: {:ok, [Node.t()]} | {:error, term()} def parse_file(path, opts \\ []) do path = Path.expand(path) case File.read(path) do {:ok, source} -> parse(source, Keyword.put_new(opts, :file, path)) {:error, reason} -> {:error, {:file, reason}} end end # --- Bytecode → IR translation --- defp translate_bytecode(bc, counter, file) do nested = Enum.map(bc.cpool, fn %{opcodes: _} = func -> translate_function(func, counter, file) _ -> nil end) |> Enum.reject(&is_nil/1) if has_module_level_code?(bc) do top = translate_function(%{bc | name: ""}, counter, file) [top | nested] else nested end end defp has_module_level_code?(bc) do bc.name in ["", nil, ""] and Enum.any?(bc.opcodes, fn op -> elem(op, 1) not in [ :check_define_var, :define_func, :fclosure, :fclosure8, :get_loc0, :get_loc, :return, :set_loc_uninitialized, :put_loc, :put_loc0, :put_loc1 ] end) end defp translate_function(func, counter, file) do start_line = func.line || 1 end_line = start_line + source_line_count(func.source) - 1 fn_span = %{ file: file, start_line: start_line, start_col: func.column, end_line: end_line, end_col: nil } arg_nodes = func.args |> Enum.map(fn name -> %Node{ id: Counter.next(counter), type: :var, meta: %{name: String.to_atom(name), binding_role: :definition}, children: [], source_span: fn_span } end) |> Enum.map(&mark_as_definitions/1) body_nodes = translate_opcodes(func, counter, file) clause = %Node{ id: Counter.next(counter), type: :clause, meta: %{kind: :function_clause}, children: arg_nodes ++ body_nodes, source_span: fn_span } %Node{ id: Counter.next(counter), type: :function_def, meta: %{ name: String.to_atom(func.name || ""), arity: length(func.args), kind: :def, language: :javascript, source: func.source }, children: [clause], source_span: fn_span } end defp source_line_count(nil), do: 1 defp source_line_count(source), do: source |> String.split("\n") |> length() # --- Opcode translation via abstract stack interpretation --- defp translate_opcodes(func, counter, file) do local_names = build_local_names(func) closure_names = build_closure_names(func) arg_names = build_arg_names(func) nested_fns = build_nested_fns(func, counter, file) ctx = %{locals: local_names, closures: closure_names, args: arg_names, fns: nested_fns} env = %{ctx: ctx, counter: counter, file: file} branch_targets = collect_branch_targets(func.opcodes) blocks = split_into_blocks(func.opcodes, branch_targets) fall_through_targets = collect_fall_through_targets(blocks) block_map = blocks |> Enum.map(fn ops -> {elem(hd(ops), 0), ops} end) |> Map.new() processed = MapSet.new() base_line = func.line || 1 max_lines = source_line_count(func.source) {all_nodes, _, _} = blocks |> Stream.with_index() |> Enum.reduce({[], [], processed}, fn {block_ops, block_idx}, acc -> block_line = base_line + min(block_idx + 1, max_lines - 1) reduce_block( block_ops, block_map, acc, branch_targets, fall_through_targets, env, block_line ) end) all_nodes end defp reduce_block( block_ops, block_map, {acc_nodes, prev_stack, visited}, targets, fall_through, env, line ) do first_offset = elem(hd(block_ops), 0) if MapSet.member?(visited, first_offset) do {acc_nodes, prev_stack, visited} else stack = if MapSet.member?(targets, first_offset) and not MapSet.member?(fall_through, first_offset), do: [], else: prev_stack process_block(block_ops, block_map, stack, acc_nodes, visited, env, line) end end defp process_block(block_ops, block_map, stack, acc_nodes, visited, env, line) do first_offset = elem(hd(block_ops), 0) case detect_if_else(block_ops, block_map) do {:if_else, condition_ops, true_ops, false_ops} -> {new_nodes, body_stack, new_visited} = build_if_else(condition_ops, true_ops, false_ops, stack, visited, env, line) {acc_nodes ++ new_nodes, body_stack, MapSet.put(new_visited, first_offset)} nil -> {nodes, stack} = run_block(block_ops, stack, env, line) {acc_nodes ++ nodes, stack, MapSet.put(visited, first_offset)} end end defp build_if_else(condition_ops, true_ops, false_ops, stack, visited, env, line) do %{counter: counter, file: file} = env {cond_nodes, cond_stack} = run_block(condition_ops, stack, env, line) {condition, body_stack} = pop_condition(cond_stack) {true_nodes, _} = run_block(true_ops, body_stack, env, line) {false_nodes, _} = run_block(false_ops, body_stack, env, line) case_span = span(file, line, nil) case_node = %Node{ id: Counter.next(counter), type: :case, meta: %{desugared_from: :if}, children: [ condition, %Node{ id: Counter.next(counter), type: :clause, meta: %{kind: :true_branch}, children: true_nodes, source_span: case_span }, %Node{ id: Counter.next(counter), type: :clause, meta: %{kind: :false_branch}, children: false_nodes, source_span: case_span } ], source_span: case_span } visited = visited |> mark_visited(true_ops) |> mark_visited(false_ops) {cond_nodes ++ [case_node], body_stack, visited} end defp run_block(ops, stack, env, line) do %{ctx: ctx, counter: counter, file: file} = env {nodes, stack} = Enum.reduce(ops, {[], stack}, fn op, {nodes, stack} -> translate_op(op, nodes, stack, ctx, counter, file, line) end) {Enum.reverse(nodes, Enum.reverse(stack)), stack} end defp pop_condition([top | rest]), do: {top, rest} defp pop_condition([]), do: {literal(Counter.new(), nil), []} defp mark_visited(visited, ops) do Enum.reduce(ops, visited, fn op, v -> MapSet.put(v, elem(op, 0)) end) end defp detect_if_else(block_ops, block_map) do last = List.last(block_ops) with {_, kind, target} when kind in [:if_false, :if_false8] <- last, condition_ops = Enum.drop(block_ops, -1), true_offset = find_next_block_offset(block_map, elem(last, 0)), true_block when true_block != nil <- Map.get(block_map, true_offset), true_last = List.last(true_block), true when true <- terminal?(true_last), false_block when false_block != nil <- Map.get(block_map, target) do {:if_else, condition_ops, true_block, false_block} else _ -> nil end end defp find_next_block_offset(block_map, branch_offset) do for({k, _v} <- block_map, k > branch_offset, do: k) |> Enum.min(fn -> nil end) end @js_terminal_ops [ :return, :return_undef, :return_async, :throw, :tail_call, :tail_call_method ] @js_branch_ops [:if_false, :if_true, :if_false8, :if_true8, :goto, :goto8, :goto16] defp terminal?({_, op}) when op in @js_terminal_ops, do: true defp terminal?({_, op, _}) when op in @js_branch_ops, do: true defp terminal?({_, op, _}) when op in @js_terminal_ops, do: true defp terminal?(_), do: false @branch_ops [:if_false, :if_true, :if_false8, :if_true8, :goto, :goto8, :goto16, :catch] @terminal_ops [:return, :return_undef, :return_async, :throw, :tail_call, :tail_call_method] defp collect_branch_targets(opcodes) do indexed = Enum.with_index(opcodes) indexed |> Enum.flat_map(fn {op, idx} -> next_offset = next_op_offset(indexed, idx) case op do {_, kind, target} when kind in @branch_ops -> [target | List.wrap(next_offset)] {_, kind} when kind in @terminal_ops -> List.wrap(next_offset) _ -> [] end end) |> MapSet.new() end defp next_op_offset(indexed, idx) do case Enum.at(indexed, idx + 1) do {next_op, _} -> elem(next_op, 0) nil -> nil end end defp collect_fall_through_targets(blocks) do blocks |> Enum.chunk_every(2, 1, []) |> Enum.flat_map(fn [prev_block, [next_op | _]] -> last_op = List.last(prev_block) falls_through = elem(last_op, 1) not in [ :goto, :goto8, :goto16, :return, :return_undef, :return_async, :throw, :tail_call, :tail_call_method ] if falls_through, do: [elem(next_op, 0)], else: [] _ -> [] end) |> MapSet.new() end defp split_into_blocks(opcodes, targets) do {blocks, current} = Enum.reduce(opcodes, {[], []}, fn op, {blocks, current} -> offset = elem(op, 0) if MapSet.member?(targets, offset) and current != [] do {[Enum.reverse(current) | blocks], [op]} else {blocks, [op | current]} end end) blocks = if current == [], do: blocks, else: [Enum.reverse(current) | blocks] Enum.reverse(blocks) end defp build_local_names(func) do func.locals |> Enum.with_index() |> Map.new(fn {local, idx} -> {idx, String.to_atom(local["name"] || "_local#{idx}")} end) end defp build_arg_names(func) do func.args |> Enum.with_index() |> Map.new(fn {name, idx} -> {idx, String.to_atom(name)} end) end defp build_closure_names(func) do func.closure_vars |> Enum.with_index() |> Map.new(fn {cv, idx} -> {idx, String.to_atom(cv["name"] || "_closure#{idx}")} end) end defp build_nested_fns(func, counter, file) do func.cpool |> Enum.with_index() |> Enum.filter(fn {item, _} -> match?(%{opcodes: _}, item) end) |> Map.new(fn {f, idx} -> {idx, translate_function(f, counter, file)} end) end # --- Individual opcode handlers --- # Push constants defp translate_op({_, :push_0, _}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, 0) | stack]} defp translate_op({_, :push_1, _}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, 1) | stack]} defp translate_op({_, :push_2, _}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, 2) | stack]} defp translate_op({_, :push_3, _}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, 3) | stack]} defp translate_op({_, :push_4, _}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, 4) | stack]} defp translate_op({_, :push_5, _}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, 5) | stack]} defp translate_op({_, :push_6, _}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, 6) | stack]} defp translate_op({_, :push_minus1, _}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, -1) | stack]} defp translate_op({_, :push_i8, val}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, val) | stack]} defp translate_op({_, :push_i32, val}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, val) | stack]} defp translate_op({_, :push_const, val}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, val) | stack]} defp translate_op({_, :push_atom_value, val}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, val) | stack]} defp translate_op({_, :push_null}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, nil) | stack]} defp translate_op({_, :push_undefined}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, :undefined) | stack]} defp translate_op({_, :push_true}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, true) | stack]} defp translate_op({_, :push_false}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, false) | stack]} # Get/put arguments defp translate_op({_, op, idx}, nodes, stack, %{args: args} = _ctx, counter, file, line) when op in [:get_arg, :get_arg0, :get_arg1, :get_arg2, :get_arg3] do idx = arg_index(op, idx) name = Map.get(args, idx, :"arg#{idx}") {nodes, [var_ref(counter, name, file, line) | stack]} end # Get/put locals defp translate_op({_, op, idx}, nodes, stack, %{locals: locals} = _ctx, counter, file, line) when op in [ :get_loc, :get_loc0, :get_loc1, :get_loc2, :get_loc3, :get_loc8, :get_loc_check ] do idx = loc_index(op, idx) name = Map.get(locals, idx, :"_local#{idx}") {nodes, [var_ref(counter, name, file, line) | stack]} end defp translate_op({_, op, idx}, nodes, stack, %{locals: locals} = _ctx, counter, file, line) when op in [ :put_loc, :put_loc0, :put_loc1, :put_loc2, :put_loc3, :put_loc8, :put_loc_check, :put_loc_check_init, :set_loc_uninitialized ] do case op do :set_loc_uninitialized -> {nodes, stack} _ -> idx = loc_index(op, idx) name = Map.get(locals, idx, :"_local#{idx}") emit_assignment(nodes, stack, name, counter, file, line) end end # Global variable access defp translate_op({_, :get_var, name}, nodes, stack, _ctx, counter, file, line) do {nodes, [var_ref(counter, String.to_atom(name), file, line) | stack]} end defp translate_op({_, :push_empty_string}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, "") | stack]} defp translate_op({_, :push_this}, nodes, stack, _ctx, counter, file, line), do: {nodes, [var_ref(counter, :this, file, line) | stack]} defp translate_op({_, :is_undefined_or_null}, nodes, stack, _ctx, _counter, _file, _line), do: {nodes, stack} # More push variants defp translate_op({_, :undefined}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, :undefined) | stack]} defp translate_op({_, :null}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, nil) | stack]} defp translate_op({_, :push_i16, val}, nodes, stack, _ctx, counter, _, _), do: {nodes, [literal(counter, val) | stack]} # Logical NOT defp translate_op({_, :lnot}, nodes, stack, _ctx, counter, file, line) do case stack do [operand | rest] -> node = %Node{ id: Counter.next(counter), type: :unary_op, meta: %{operator: :!}, children: [operand], source_span: span(file, line, nil) } {nodes, [node | rest]} _ -> {nodes, stack} end end # Property write: obj.field = value defp translate_op({_, :put_field, name}, nodes, stack, _ctx, counter, file, line) do case stack do [value, obj | rest] -> node = %Node{ id: Counter.next(counter), type: :call, meta: %{function: String.to_atom(name), kind: :field_write, module: extract_name(obj)}, children: [obj, value], source_span: span(file, line, nil) } {[node | nodes], rest} _ -> {nodes, stack} end end # Method definition in object literal defp translate_op({_, :define_method, name, _flags}, nodes, stack, _ctx, counter, file, line) do case stack do [func, obj | rest] -> field_node = %Node{ id: Counter.next(counter), type: :map_field, meta: %{key: String.to_atom(name)}, children: [func], source_span: span(file, line, nil) } updated = %{obj | children: obj.children ++ [field_node]} {nodes, [updated | rest]} _ -> {nodes, stack} end end # Global variable write defp translate_op({_, :put_var, name}, nodes, stack, _ctx, counter, file, line) do emit_assignment(nodes, stack, String.to_atom(name), counter, file, line) end # Argument mutation defp translate_op({_, op, idx}, nodes, stack, _ctx, counter, file, line) when op in [:put_arg, :put_arg0, :put_arg1, :put_arg2, :put_arg3] do idx = arg_index(op, idx) emit_assignment(nodes, stack, :"arg#{idx}", counter, file, line) end # Variable access variants defp translate_op({_, :get_var_undef, name}, nodes, stack, _ctx, counter, file, line), do: {nodes, [var_ref(counter, String.to_atom(name), file, line) | stack]} defp translate_op( {_, :get_var_ref1, idx}, nodes, stack, %{closures: closures} = _ctx, counter, file, line ) do name = Map.get(closures, idx, :"_closure#{idx}") {nodes, [var_ref(counter, name, file, line) | stack]} end # Control flow variants defp translate_op({_, :goto16, _}, nodes, stack, _ctx, _counter, _file, _line), do: {nodes, stack} # Try/catch — for IR purposes, catch pushes exception on stack defp translate_op({_, :catch, _}, nodes, stack, _ctx, counter, file, line), do: {nodes, [var_ref(counter, :"", file, line) | stack]} defp translate_op({_, :nip_catch}, nodes, stack, _ctx, _counter, _file, _line) do case stack do [top, _ | rest] -> {nodes, [top | rest]} _ -> {nodes, stack} end end # For-of loops defp translate_op({_, :for_of_start}, nodes, stack, _ctx, _counter, _file, _line) do case stack do [iterable | rest] -> {nodes, [iterable, iterable, iterable | rest]} _ -> {nodes, stack} end end defp translate_op({_, :for_of_next, _}, nodes, stack, _ctx, counter, file, line) do case stack do [_iter, _obj, _method | rest] -> {nodes, [literal(counter, false), var_ref(counter, :"", file, line) | rest]} _ -> {nodes, stack} end end defp translate_op({_, :iterator_close}, nodes, stack, _ctx, _counter, _file, _line) do case stack do [_, _, _ | rest] -> {nodes, rest} _ -> {nodes, stack} end end # Array/object operations defp translate_op({_, :get_length}, nodes, stack, _ctx, counter, file, line) do case stack do [obj | rest] -> node = %Node{ id: Counter.next(counter), type: :call, meta: %{function: :length, kind: :field_access}, children: [obj], source_span: span(file, line, nil) } {nodes, [node | rest]} _ -> {nodes, stack} end end defp translate_op({_, :get_array_el}, nodes, stack, _ctx, counter, file, line) do case stack do [index, arr | rest] -> {nodes, [make_array_access(counter, arr, index, file, line) | rest]} _ -> {nodes, stack} end end defp translate_op({_, :get_array_el2}, nodes, stack, _ctx, counter, file, line) do case stack do [index, arr | rest] -> {nodes, [make_array_access(counter, arr, index, file, line), arr | rest]} _ -> {nodes, stack} end end defp translate_op( {_, :get_loc0_loc1}, nodes, stack, %{locals: locals} = _ctx, counter, file, line ) do name0 = Map.get(locals, 0, :_local0) name1 = Map.get(locals, 1, :_local1) {nodes, [var_ref(counter, name1, file, line), var_ref(counter, name0, file, line) | stack]} end defp translate_op({_, :check_define_var, _, _}, nodes, stack, _ctx, _, _, _), do: {nodes, stack} defp translate_op({_, :define_func, _, _}, nodes, stack, _ctx, _, _, _) do case stack do [_ | rest] -> {nodes, rest} _ -> {nodes, stack} end end # Type checks — leave value on stack, result replaces it defp translate_op({_, op}, nodes, stack, _ctx, _counter, _file, _line) when op in [:typeof_is_function, :typeof_is_undefined, :is_undefined, :to_object], do: {nodes, stack} # Dynamic import — pops specifier, pushes promise defp translate_op({_, :import}, nodes, stack, _ctx, counter, file, line) do case stack do [_specifier, _obj | rest] -> node = %Node{ id: Counter.next(counter), type: :call, meta: %{function: :import, kind: :local, arity: 1}, children: [], source_span: span(file, line, nil) } {nodes, [node | rest]} _ -> {nodes, stack} end end # Stack permutations defp translate_op({_, :perm3}, nodes, [a, b, c | rest], _ctx, _, _, _), do: {nodes, [b, c, a | rest]} defp translate_op({_, :rest, _}, nodes, stack, _ctx, counter, file, line) do case stack do [arr | rest] -> node = %Node{ id: Counter.next(counter), type: :call, meta: %{function: :..., kind: :local}, children: [arr], source_span: span(file, line, nil) } {nodes, [node | rest]} _ -> {nodes, stack} end end # Closure variable access defp translate_op( {_, op, idx}, nodes, stack, %{closures: closures} = _ctx, counter, file, line ) when op in [:get_var_ref, :get_var_ref_check] do name = Map.get(closures, idx, :"_closure#{idx}") {nodes, [var_ref(counter, name, file, line) | stack]} end defp translate_op( {_, :put_var_ref, idx}, nodes, stack, %{closures: closures} = _ctx, counter, file, line ) do name = Map.get(closures, idx, :"_closure#{idx}") emit_assignment(nodes, stack, name, counter, file, line) end # Binary operators @binary_ops %{ add: :+, mul: :*, sub: :-, div: :/, mod: :%, pow: :**, eq: :==, neq: :!=, strict_eq: :===, strict_neq: :!==, lt: :<, lte: :<=, gt: :>, gte: :>=, shl: :"<<", sar: :">>", shr: :>>>, and: :&, or: :|, xor: :^, in: :in, instanceof: :instanceof } for {opname, operator} <- @binary_ops do defp translate_op({_, unquote(opname)}, nodes, stack, _ctx, counter, file, line) do case stack do [right, left | rest] -> node = %Node{ id: Counter.next(counter), type: :binary_op, meta: %{operator: unquote(operator)}, children: [left, right], source_span: span(file, line, nil) } {nodes, [node | rest]} _ -> {nodes, stack} end end end # Unary operators @unary_ops %{ neg: :-, plus: :+, not: :!, bnot: :"~", typeof: :typeof, void: :void } for {opname, operator} <- @unary_ops do defp translate_op({_, unquote(opname)}, nodes, stack, _ctx, counter, file, line) do case stack do [operand | rest] -> node = %Node{ id: Counter.next(counter), type: :unary_op, meta: %{operator: unquote(operator)}, children: [operand], source_span: span(file, line, nil) } {nodes, [node | rest]} _ -> {nodes, stack} end end end # Increment/decrement defp translate_op({_, op}, nodes, stack, _ctx, counter, file, line) when op in [:inc, :dec, :post_inc, :post_dec] do case stack do [operand | rest] -> operator = if op in [:inc, :post_inc], do: :++, else: :-- node = %Node{ id: Counter.next(counter), type: :unary_op, meta: %{operator: operator}, children: [operand], source_span: span(file, line, nil) } {nodes, [node | rest]} _ -> {nodes, stack} end end # Function calls defp translate_op({_, op, argc}, nodes, stack, _ctx, counter, file, line) when op in [:call, :call0, :call1, :call2, :call3, :tail_call] do argc = call_argc(op, argc) case safe_pop(stack, argc + 1) do {popped, rest} -> [func | args] = Enum.reverse(popped) call_node = %Node{ id: Counter.next(counter), type: :call, meta: %{ function: extract_name(func), arity: argc, kind: :local }, children: args, source_span: span(file, line, nil) } {nodes, [call_node | rest]} :error -> {nodes, stack} end end # Method calls — stack order (top→bottom): argN..arg0, method, obj defp translate_op({_, op, argc}, nodes, stack, _ctx, counter, file, line) when op in [:call_method, :tail_call_method] do case safe_pop(stack, argc + 2) do {popped, rest} -> [obj, method | args] = Enum.reverse(popped) {nodes, [build_method_call(obj, method, args, counter, file, line) | rest]} :error -> {nodes, stack} end end # Return defp translate_op({_, :return}, nodes, stack, _ctx, _counter, _file, _line) do case stack do [value | rest] -> {[value | nodes], rest} [] -> {nodes, stack} end end defp translate_op({_, :return_undef}, nodes, stack, _ctx, counter, _, _) do {[literal(counter, :undefined) | nodes], stack} end defp translate_op({_, :return_async}, nodes, stack, _ctx, _counter, _file, _line) do case stack do [value | rest] -> {[value | nodes], rest} [] -> {nodes, stack} end end # Await — for IR purposes, same value passes through defp translate_op({_, :await}, nodes, stack, _ctx, _counter, _file, _line), do: {nodes, stack} # Control flow — if_false / goto defp translate_op({_, op, _target}, nodes, stack, _ctx, _counter, _file, _line) when op in [:if_false, :if_true, :if_false8, :if_true8] do case stack do [_condition | rest] -> {nodes, rest} [] -> {nodes, stack} end end defp translate_op({_, :goto, _target}, nodes, stack, _ctx, _, _, _), do: {nodes, stack} defp translate_op({_, :goto8, _target}, nodes, stack, _ctx, _, _, _), do: {nodes, stack} # Object construction defp translate_op({_, :object}, nodes, stack, _ctx, counter, file, line) do node = %Node{ id: Counter.next(counter), type: :map, meta: %{kind: :object}, children: [], source_span: span(file, line, nil) } {nodes, [node | stack]} end defp translate_op( {_, :define_field, name}, nodes, [value, obj | rest], _ctx, counter, file, line ) do field_node = %Node{ id: Counter.next(counter), type: :map_field, meta: %{key: String.to_atom(name)}, children: [value], source_span: span(file, line, nil) } updated = %{obj | children: obj.children ++ [field_node]} {nodes, [updated | rest]} end # Property access (get_field pushes value, get_field2 pushes obj + value for method calls) defp translate_op({_, :get_field, name}, nodes, stack, _ctx, counter, file, line) do case stack do [obj | rest] -> node = %Node{ id: Counter.next(counter), type: :call, meta: %{function: String.to_atom(name), kind: :field_access}, children: [obj], source_span: span(file, line, nil) } {nodes, [node | rest]} [] -> {nodes, stack} end end defp translate_op({_, :get_field2, name}, nodes, stack, _ctx, counter, file, line) do case stack do [obj | rest] -> method = %Node{ id: Counter.next(counter), type: :literal, meta: %{value: name}, children: [], source_span: span(file, line, nil) } {nodes, [method, obj | rest]} [] -> {nodes, stack} end end # Closures defp translate_op( {_, op, idx}, nodes, stack, %{fns: nested_fns} = _ctx, _counter, _file, _line ) when op in [:fclosure, :fclosure8] do case Map.get(nested_fns, idx) do nil -> {nodes, stack} fn_node -> {nodes, [fn_node | stack]} end end # Stack manipulation defp translate_op({_, :dup}, nodes, [top | rest], _ctx, _, _, _), do: {nodes, [top, top | rest]} defp translate_op({_, :drop}, nodes, [top | rest], _ctx, _, _, _) do case top.type do :call -> {[top | nodes], rest} :match -> {[top | nodes], rest} _ -> {nodes, rest} end end defp translate_op({_, :nip}, nodes, [top, _ | rest], _ctx, _, _, _), do: {nodes, [top | rest]} defp translate_op({_, :swap}, nodes, [a, b | rest], _ctx, _, _, _), do: {nodes, [b, a | rest]} # Set name (used for function/property naming, skip) defp translate_op({_, :set_name, _}, nodes, stack, _ctx, _, _, _), do: {nodes, stack} # Throw defp translate_op({_, :throw}, nodes, stack, _ctx, counter, file, line) do case stack do [value | rest] -> node = %Node{ id: Counter.next(counter), type: :call, meta: %{function: :throw, kind: :local}, children: [value], source_span: span(file, line, nil) } {[node | nodes], rest} [] -> {nodes, stack} end end # Array literal defp translate_op({_, :array_from, count}, nodes, stack, _ctx, counter, file, line) do case safe_pop(stack, count) do {elements, rest} -> node = %Node{ id: Counter.next(counter), type: :list, meta: %{kind: :array}, children: elements, source_span: span(file, line, nil) } {nodes, [node | rest]} :error -> {nodes, stack} end end # Constructor call defp translate_op({_, :call_constructor, argc}, nodes, stack, _ctx, counter, file, line) do case safe_pop(stack, argc + 2) do {[_new_target, constructor | args], rest} -> node = %Node{ id: Counter.next(counter), type: :call, meta: %{function: extract_name(constructor), arity: argc, kind: :constructor}, children: args, source_span: span(file, line, nil) } {nodes, [node | rest]} :error -> {nodes, stack} end end # Catch-all for unhandled opcodes defp translate_op(_op, nodes, stack, _ctx, _counter, _file, _line) do {nodes, stack} end # --- Method call helpers --- defp build_method_call( %Node{type: :literal, meta: %{value: str}}, %Node{type: :literal, meta: %{value: "concat"}}, args, counter, file, line ) when is_binary(str) do parts = [literal(counter, str) | args] Enum.reduce(tl(parts), hd(parts), fn right, left -> %Node{ id: Counter.next(counter), type: :binary_op, meta: %{operator: :+}, children: [left, right], source_span: span(file, line, nil) } end) end defp build_method_call(obj, method, args, counter, file, line) do %Node{ id: Counter.next(counter), type: :call, meta: %{ module: extract_name(obj), function: extract_name(method), arity: length(args), kind: :remote }, children: args, source_span: span(file, line, nil) } end # --- Helpers --- defp emit_assignment(nodes, stack, name, counter, file, line) do case stack do [value | rest] -> node = %Node{ id: Counter.next(counter), type: :match, meta: %{}, children: [var_def(counter, name, file, line), value], source_span: span(file, line, nil) } {[node | nodes], rest} [] -> {nodes, stack} end end defp make_array_access(counter, arr, index, file, line) do %Node{ id: Counter.next(counter), type: :call, meta: %{function: :"[]", kind: :field_access}, children: [arr, index], source_span: span(file, line, nil) } end defp literal(counter, value) do %Node{id: Counter.next(counter), type: :literal, meta: %{value: value}, children: []} end defp var_ref(counter, name, file, line) do %Node{ id: Counter.next(counter), type: :var, meta: %{name: name}, children: [], source_span: span(file, line, nil) } end defp var_def(counter, name, file, line) do %Node{ id: Counter.next(counter), type: :var, meta: %{name: name, binding_role: :definition}, children: [], source_span: span(file, line, nil) } end defp span(file, line, col) do %{file: file, start_line: line, start_col: col, end_line: nil, end_col: nil} end defp extract_name(%Node{type: :var, meta: %{name: name}}), do: name defp extract_name(%Node{type: :literal, meta: %{value: v}}) when is_binary(v), do: String.to_atom(v) defp extract_name(%Node{type: :literal, meta: %{value: v}}) when is_atom(v), do: v defp extract_name(%Node{type: :call, meta: %{function: f}}), do: f defp extract_name(%Node{type: :function_def, meta: %{name: n}}), do: n defp extract_name(_), do: :unknown defp arg_index(:get_arg0, _), do: 0 defp arg_index(:get_arg1, _), do: 1 defp arg_index(:get_arg2, _), do: 2 defp arg_index(:get_arg3, _), do: 3 defp arg_index(:put_arg0, _), do: 0 defp arg_index(:put_arg1, _), do: 1 defp arg_index(:put_arg2, _), do: 2 defp arg_index(:put_arg3, _), do: 3 defp arg_index(:get_arg, idx), do: idx defp arg_index(:put_arg, idx), do: idx defp loc_index(:get_loc0, _), do: 0 defp loc_index(:put_loc0, _), do: 0 defp loc_index(:get_loc1, _), do: 1 defp loc_index(:put_loc1, _), do: 1 defp loc_index(:get_loc2, _), do: 2 defp loc_index(:put_loc2, _), do: 2 defp loc_index(:get_loc3, _), do: 3 defp loc_index(:put_loc3, _), do: 3 defp loc_index(_, idx), do: idx defp call_argc(:call0, _), do: 0 defp call_argc(:call1, _), do: 1 defp call_argc(:call2, _), do: 2 defp call_argc(:call3, _), do: 3 defp call_argc(_, argc), do: argc defp safe_pop(stack, n) do if length(stack) >= n do {Enum.take(stack, n), Enum.drop(stack, n)} else :error end end end end