defmodule Mix.Tasks.Firebird.Target.New do @moduledoc """ Generate a new WASM-ready Elixir module with `@wasm true` annotations. ## Usage # Generate a basic WASM module mix firebird.target.new MyApp.Math # Generate with specific functions mix firebird.target.new MyApp.Math --functions add,subtract,multiply # Generate in a specific directory mix firebird.target.new MyApp.Math --dir lib/wasm # Generate with test file mix firebird.target.new MyApp.Math --with-test ## Generated Module The generated module will include: - Module with `@wasm true` annotations - Example functions showing compilable patterns - Comments explaining the compilable subset """ use Mix.Task @shortdoc "Generate a new WASM-ready Elixir module" @switches [ dir: :string, functions: :string, with_test: :boolean ] @aliases [ d: :dir, f: :functions, t: :with_test ] @impl Mix.Task def run(args) do {opts, positional, _} = OptionParser.parse(args, switches: @switches, aliases: @aliases) if Enum.empty?(positional) do Mix.raise("Usage: mix firebird.target.new ModuleName [options]") end module_name = hd(positional) dir = Keyword.get(opts, :dir, "lib/wasm_modules") functions = parse_functions(Keyword.get(opts, :functions)) with_test = Keyword.get(opts, :with_test, false) # Generate module path filename = module_name |> String.split(".") |> List.last() |> Macro.underscore() module_path = Path.join(dir, "#{filename}.ex") File.mkdir_p!(dir) # Generate module content content = generate_module(module_name, functions) File.write!(module_path, content) Mix.shell().info("✅ Created #{module_path}") # Generate test if requested if with_test do # Determine test directory - if dir is absolute, create test alongside it # Otherwise use project's test directory structure test_dir = if Path.type(dir) == :absolute do # For absolute paths (e.g., temp dirs in /var/...), find the project root # by looking for a directory that looks like a project root (contains lib/) # or just use sibling to the first "lib" in the path parts = Path.split(dir) lib_index = Enum.find_index(parts, &(&1 == "lib")) if lib_index do # Found lib/ in path - test goes at same level as lib/ project_root = parts |> Enum.take(lib_index) |> Path.join() Path.join(project_root, "test") else # No lib/ found - put test next to the dir Path.join(Path.dirname(dir), "test") end else Path.join("test", dir |> String.replace_prefix("lib/", "")) end File.mkdir_p!(test_dir) test_path = Path.join(test_dir, "#{filename}_test.exs") test_content = generate_test(module_name, functions, dir) File.write!(test_path, test_content) Mix.shell().info("✅ Created #{test_path}") end Mix.shell().info("") Mix.shell().info("Next steps:") Mix.shell().info(" 1. Edit #{module_path} to add your functions") Mix.shell().info(" 2. Compile: mix firebird.target --files #{module_path}") Mix.shell().info(" 3. Verify: mix firebird.target --files #{module_path} --verify") :ok end defp parse_functions(nil), do: nil defp parse_functions(str), do: String.split(str, ",", trim: true) defp generate_module(module_name, nil) do """ defmodule #{module_name} do @moduledoc \"\"\" WASM-compiled module. All functions marked with `@wasm true` will be compiled to WebAssembly. ## Supported operations - Integer arithmetic: +, -, *, div, rem - Comparison: ==, !=, <, >, <=, >= - Boolean: and, or, not - Control flow: if/else, case, cond - Pattern matching on integer literals - Recursive functions \"\"\" @wasm true def add(a, b), do: a + b @wasm true def subtract(a, b), do: a - b @wasm true def multiply(a, b), do: a * b end """ end defp generate_module(module_name, functions) do func_defs = Enum.map(functions, fn name -> name = String.trim(name) """ @wasm true def #{name}(a, b), do: a + b """ end) |> Enum.join("\n") """ defmodule #{module_name} do @moduledoc \"\"\" WASM-compiled module. \"\"\" #{func_defs}end """ end defp generate_test(module_name, nil, source_dir) do filename = module_name |> String.split(".") |> List.last() |> Macro.underscore() source_path = Path.join(source_dir, "#{filename}.ex") """ defmodule #{module_name}Test do use ExUnit.Case @moduletag :wasm setup do source = File.read!("#{source_path}") {:ok, result} = Firebird.Compiler.compile_source(source) {:ok, instance} = Firebird.load(result.wasm) on_exit(fn -> Firebird.stop(instance) end) {:ok, instance: instance} end test "add", %{instance: inst} do assert {:ok, [3]} = Firebird.call(inst, "add", [1, 2]) end test "subtract", %{instance: inst} do assert {:ok, [3]} = Firebird.call(inst, "subtract", [5, 2]) end test "multiply", %{instance: inst} do assert {:ok, [6]} = Firebird.call(inst, "multiply", [2, 3]) end end """ end defp generate_test(module_name, functions, source_dir) do filename = module_name |> String.split(".") |> List.last() |> Macro.underscore() source_path = Path.join(source_dir, "#{filename}.ex") test_cases = Enum.map(functions, fn name -> name = String.trim(name) """ test "#{name}", %{instance: inst} do assert {:ok, [_result]} = Firebird.call(inst, "#{name}", [1, 2]) end """ end) |> Enum.join("\n") """ defmodule #{module_name}Test do use ExUnit.Case @moduletag :wasm setup do source = File.read!("#{source_path}") {:ok, result} = Firebird.Compiler.compile_source(source) {:ok, instance} = Firebird.load(result.wasm) on_exit(fn -> Firebird.stop(instance) end) {:ok, instance: instance} end #{test_cases}end """ end end