diff --git a/.gitignore b/.gitignore index b409e41..116da1f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ erl_crash.dump *.beam /tmp/ .elixir_ls/ +.clj-kondo/ +.lsp/ .nrepl-port diff --git a/bb.edn b/bb.edn index 5b34a14..53656f6 100644 --- a/bb.edn +++ b/bb.edn @@ -1,12 +1,9 @@ {:tasks {compile {:doc "Compile all .clje and .ex files" - :task (shell "mix compile")} + :task (apply shell "mix compile" *command-line-args*)} - test {:doc "Run all tests" - :task (shell "mix test")} - - test:trace {:doc "Run all tests with trace output" - :task (shell "mix test --trace")} + test {:doc "Run tests (accepts mix test args, e.g. bb test --only phase5)" + :task (apply shell "mix test" *command-line-args*)} repl {:doc "Start interactive CljElixir REPL" :task (shell "rlwrap mix clje.repl")} @@ -14,11 +11,17 @@ repl:basic {:doc "Start REPL without rlwrap" :task (shell "mix clje.repl")} - nrepl {:doc "Start nREPL server (random port)" - :task (shell "mix clje.nrepl")} + nrepl {:doc "Start nREPL server (bb nrepl --port 7888)" + :task (apply shell "mix clje.nrepl" *command-line-args*)} - nrepl:port {:doc "Start nREPL on port 7888" - :task (shell "mix clje.nrepl --port 7888")} + eval {:doc "Evaluate expression (bb eval '(+ 1 2)')" + :task (apply shell "mix clje.eval" *command-line-args*)} + + run {:doc "Run a .clje file (bb run examples/chat_room.clje)" + :task (apply shell "mix clje.run" *command-line-args*)} + + build {:doc "Compile .clje to BEAM (bb build src/foo.clje [-o dir])" + :task (apply shell "mix clje.build" *command-line-args*)} clean {:doc "Clean build artifacts" :task (shell "mix clean")} diff --git a/bench/persistent_vector_bench.exs b/bench/persistent_vector_bench.exs new file mode 100644 index 0000000..8f72475 --- /dev/null +++ b/bench/persistent_vector_bench.exs @@ -0,0 +1,138 @@ +# PersistentVector vs Erlang Tuple vs List benchmarks +# Run: mix run bench/persistent_vector_bench.exs + +defmodule PVBench do + def run do + sizes = [10, 100, 1_000, 10_000] + + IO.puts("=" |> String.duplicate(70)) + IO.puts("PersistentVector vs Tuple vs List — Benchmarks") + IO.puts("=" |> String.duplicate(70)) + + for size <- sizes do + IO.puts("\n--- Size: #{size} ---\n") + list = Enum.to_list(1..size) + tuple = List.to_tuple(list) + pv = CljElixir.PersistentVector.from_list(list) + + bench_construction(size, list) + bench_indexed_access(size, list, tuple, pv) + bench_append(size, list, tuple, pv) + bench_update(size, list, tuple, pv) + bench_iteration(size, list, tuple, pv) + end + end + + defp bench_construction(size, list) do + {pv_us, _} = :timer.tc(fn -> + CljElixir.PersistentVector.from_list(list) + end) + + {tuple_us, _} = :timer.tc(fn -> + List.to_tuple(list) + end) + + IO.puts(" Construction:") + IO.puts(" PersistentVector.from_list: #{format_us(pv_us)}") + IO.puts(" List.to_tuple: #{format_us(tuple_us)}") + IO.puts(" List (identity): ~0 µs") + end + + defp bench_indexed_access(size, list, tuple, pv) do + indices = for _ <- 1..1000, do: :rand.uniform(size) - 1 + + {pv_us, _} = :timer.tc(fn -> + Enum.each(indices, fn i -> CljElixir.PersistentVector.pv_nth(pv, i) end) + end) + + {tuple_us, _} = :timer.tc(fn -> + Enum.each(indices, fn i -> elem(tuple, i) end) + end) + + {list_us, _} = :timer.tc(fn -> + Enum.each(indices, fn i -> Enum.at(list, i) end) + end) + + IO.puts(" Indexed access (1000 random):") + IO.puts(" PersistentVector.pv_nth: #{format_us(pv_us)}") + IO.puts(" elem(tuple, i): #{format_us(tuple_us)}") + IO.puts(" Enum.at(list, i): #{format_us(list_us)}") + end + + defp bench_append(size, _list, tuple, pv) do + iters = min(1000, size) + + {pv_us, _} = :timer.tc(fn -> + Enum.reduce(1..iters, pv, fn x, acc -> + CljElixir.PersistentVector.pv_conj(acc, x) + end) + end) + + {tuple_us, _} = :timer.tc(fn -> + Enum.reduce(1..iters, tuple, fn x, acc -> + :erlang.append_element(acc, x) + end) + end) + + {list_us, _} = :timer.tc(fn -> + # Prepend (O(1)) — append would be O(n) per op + Enum.reduce(1..iters, [], fn x, acc -> [x | acc] end) + end) + + IO.puts(" Append #{iters} elements:") + IO.puts(" PersistentVector.pv_conj: #{format_us(pv_us)}") + IO.puts(" :erlang.append_element: #{format_us(tuple_us)}") + IO.puts(" [x | list] (prepend): #{format_us(list_us)}") + end + + defp bench_update(size, _list, tuple, pv) do + indices = for _ <- 1..1000, do: :rand.uniform(size) - 1 + + {pv_us, _} = :timer.tc(fn -> + Enum.each(indices, fn i -> + CljElixir.PersistentVector.pv_assoc(pv, i, :updated) + end) + end) + + {tuple_us, _} = :timer.tc(fn -> + Enum.each(indices, fn i -> + put_elem(tuple, i, :updated) + end) + end) + + IO.puts(" Update (1000 random):") + IO.puts(" PersistentVector.pv_assoc: #{format_us(pv_us)}") + IO.puts(" put_elem(tuple, i, v): #{format_us(tuple_us)}") + IO.puts(" List: N/A (O(n) per update)") + end + + defp bench_iteration(size, list, tuple, pv) do + {pv_us, _} = :timer.tc(fn -> + cnt = CljElixir.PersistentVector.pv_count(pv) + Enum.reduce(0..(cnt - 1), 0, fn i, acc -> + acc + CljElixir.PersistentVector.pv_nth(pv, i) + end) + end) + + {tuple_us, _} = :timer.tc(fn -> + Enum.reduce(0..(size - 1), 0, fn i, acc -> + acc + elem(tuple, i) + end) + end) + + {list_us, _} = :timer.tc(fn -> + Enum.reduce(list, 0, fn x, acc -> acc + x end) + end) + + IO.puts(" Iteration (sum all):") + IO.puts(" PersistentVector by index: #{format_us(pv_us)}") + IO.puts(" Tuple by index: #{format_us(tuple_us)}") + IO.puts(" List reduce: #{format_us(list_us)}") + end + + defp format_us(us) when us < 1_000, do: "#{us} µs" + defp format_us(us) when us < 1_000_000, do: "#{Float.round(us / 1_000, 1)} ms" + defp format_us(us), do: "#{Float.round(us / 1_000_000, 2)} s" +end + +PVBench.run() diff --git a/examples/chat_room.clje b/examples/chat_room.clje new file mode 100644 index 0000000..bcb3d39 --- /dev/null +++ b/examples/chat_room.clje @@ -0,0 +1,142 @@ +;;; ChatRoom — process-based chat room using spawn/send/receive +;;; +;;; This is a single-VM demo showing BEAM concurrency primitives. +;;; Run with: mix clje.run examples/chat_room.clje +;;; +;;; For a multi-terminal chat experience, see: +;;; examples/tcp_chat_server.clje and examples/tcp_chat_client.clje + +(ns ChatRoom + (:require [clje.core :refer :all] + [Enum] [IO] [Map] [Process] [String])) + +;; ── Room loop ───────────────────────────────────────────────────── +;; Manages members map of {username → pid} and broadcasts messages. + +(defn run-loop [state] + (receive + [:join username pid] + (let [members (assoc (:members state) username pid)] + (send pid #el[:welcome username (count members)]) + ;; Notify existing members + (doseq [[_name member-pid] (:members state)] + (send member-pid #el[:system (str username " joined the room")])) + (recur (assoc state :members members))) + + [:message from body] :guard [(is-binary body)] + (do + (doseq [[_name pid] (:members state)] + (send pid #el[:chat from body])) + (recur state)) + + [:leave username] + (let [new-members (dissoc (:members state) username)] + (doseq [[_name pid] new-members] + (send pid #el[:system (str username " left the room")])) + (recur (assoc state :members new-members))) + + [:who reply-pid] + (do + (send reply-pid #el[:members (Map/keys (:members state))]) + (recur state)) + + :shutdown + (do + (doseq [[_name pid] (:members state)] + (send pid :room-closed)) + :ok) + + :after 5000 + (if (== (count (:members state)) 0) + :empty-timeout + (recur state)))) + +;; ── User listener ───────────────────────────────────────────────── +;; Collects messages received by a user and prints them. + +(defn listen [username messages] + (receive + [:welcome _name member-count] + (do + (println (str " [" username " sees: Welcome! " member-count " user(s) here]")) + (ChatRoom/listen username messages)) + + [:chat from body] + (do + (println (str " [" username " sees: " from "> " body "]")) + (ChatRoom/listen username (cons #el[from body] messages))) + + [:system text] + (do + (println (str " [" username " sees: * " text "]")) + (ChatRoom/listen username messages)) + + :room-closed + (println (str " [" username " sees: room closed]")) + + :dump + messages + + :after 2000 + (do + (println (str " [" username " done listening]")) + messages))) + +;; ── Run the demo ──────────────────────────────────────────────────── + +(println "=== ChatRoom Demo ===\n") + +;; Start the room +(let [room (spawn (fn [] (ChatRoom/run-loop {:owner "system" :members {}}))) + alice-listener (spawn (fn [] (ChatRoom/listen "alice" (list)))) + bob-listener (spawn (fn [] (ChatRoom/listen "bob" (list)))) + carol-listener (spawn (fn [] (ChatRoom/listen "carol" (list))))] + + ;; Alice joins + (println "Alice joins...") + (send room #el[:join "alice" alice-listener]) + (Process/sleep 100) + + ;; Bob joins + (println "\nBob joins...") + (send room #el[:join "bob" bob-listener]) + (Process/sleep 100) + + ;; Alice sends a message + (println "\nAlice sends a message...") + (send room #el[:message "alice" "Hello everyone!"]) + (Process/sleep 100) + + ;; Carol joins + (println "\nCarol joins...") + (send room #el[:join "carol" carol-listener]) + (Process/sleep 100) + + ;; Bob sends a message + (println "\nBob sends a message...") + (send room #el[:message "bob" "Hey Alice! Welcome Carol!"]) + (Process/sleep 100) + + ;; Carol sends a message + (println "\nCarol sends a message...") + (send room #el[:message "carol" "Thanks Bob!"]) + (Process/sleep 100) + + ;; Check who's online + (println "\nWho's online?") + (send room #el[:who *self*]) + (receive + [:members names] + (println (str " Online: " (Enum/join names ", ")))) + + ;; Bob leaves + (println "\nBob leaves...") + (send room #el[:leave "bob"]) + (Process/sleep 100) + + ;; Shutdown the room + (println "\nShutting down room...") + (send room :shutdown) + (Process/sleep 200) + + (println "\n=== Demo complete ===")) diff --git a/examples/tcp_chat_client.clje b/examples/tcp_chat_client.clje new file mode 100644 index 0000000..23e53b1 --- /dev/null +++ b/examples/tcp_chat_client.clje @@ -0,0 +1,130 @@ +;;; TCP Chat Client — connects to the TCP chat server +;;; +;;; Usage: +;;; mix clje.run --no-halt examples/tcp_chat_client.clje -- +;;; +;;; The server must be running first: +;;; mix clje.run --no-halt examples/tcp_chat_server.clje +;;; +;;; Commands: +;;; Type a message and press Enter to send +;;; /who — list online users +;;; /quit — disconnect and exit + +(ns TcpChatClient + (:require [clje.core :refer :all] + [IO] [String] [System] [erlang] [gen_tcp])) + +;; ── Receiver process ────────────────────────────────────────────── +;; Listens for TCP messages from the server and prints them. + +(defn receiver [socket parent] + (receive + [:tcp _sock data] + (let [line (String/trim data)] + (cond + (String/starts-with? line "MSG:") + (let [rest (String/slice line 4 (String/length line)) + parts (String/split rest ":" (list #el[:parts 2])) + from (hd parts) + text (hd (tl parts))] + (IO/puts (str from "> " text)) + (TcpChatClient/receiver socket parent)) + + (String/starts-with? line "SYS:") + (do + (IO/puts (str "* " (String/slice line 4 (String/length line)))) + (TcpChatClient/receiver socket parent)) + + (String/starts-with? line "JOIN:") + (do + (IO/puts (str "* " (String/slice line 5 (String/length line)) " joined")) + (TcpChatClient/receiver socket parent)) + + (String/starts-with? line "QUIT:") + (do + (IO/puts (str "* " (String/slice line 5 (String/length line)) " left")) + (TcpChatClient/receiver socket parent)) + + :else + (do + (IO/puts line) + (TcpChatClient/receiver socket parent)))) + + [:tcp_closed _sock] + (do + (IO/puts "* Connection closed by server.") + (System/halt 0)) + + [:tcp_error _sock _reason] + (do + (IO/puts "* Connection error.") + (System/halt 1)))) + +;; ── Input loop ──────────────────────────────────────────────────── +;; Reads from stdin and sends to the server. + +(defn send-line [socket line] + (gen_tcp/send socket line)) + +(defn input-loop [socket] + (let [line (IO/gets "")] + (cond + (== line :eof) + (do + (TcpChatClient/send-line socket "QUIT\n") + (gen_tcp/close socket) + (System/halt 0)) + + :else + (let [trimmed (String/trim line)] + (cond + (== trimmed "/quit") + (do + (TcpChatClient/send-line socket "QUIT\n") + (gen_tcp/close socket) + (IO/puts "Goodbye!") + (System/halt 0)) + + (== trimmed "") + (TcpChatClient/input-loop socket) + + :else + (do + (TcpChatClient/send-line socket (str "MSG:" trimmed "\n")) + (TcpChatClient/input-loop socket))))))) + +;; ── Connect ─────────────────────────────────────────────────────── + +(defn start [username] + (case (gen_tcp/connect (erlang/binary-to-list "127.0.0.1") 4040 + (list :binary #el[:active true] #el[:packet :line])) + [:ok socket] + (do + ;; Send JOIN + (TcpChatClient/send-line socket (str "JOIN:" username "\n")) + + ;; Spawn receiver and hand it the socket + (let [me *self* + recv-pid (spawn (fn [] (TcpChatClient/receiver socket me)))] + (gen_tcp/controlling-process socket recv-pid) + + ;; Run input loop in the main process + (IO/puts (str "Connected as " username ". Type a message or /quit to exit.")) + (TcpChatClient/input-loop socket))) + + [:error reason] + (do + (IO/puts (str "Could not connect: " reason)) + (IO/puts "Is the server running? Start it with:") + (IO/puts " mix clje.run --no-halt examples/tcp_chat_server.clje") + (System/halt 1)))) + +;; ── Entry point ───────────────────────────────────────────────────── + +(let [args (System/argv)] + (if (== (count args) 0) + (do + (IO/puts "Usage: mix clje.run --no-halt examples/tcp_chat_client.clje -- ") + (System/halt 1)) + (TcpChatClient/start (hd args)))) diff --git a/examples/tcp_chat_server.clje b/examples/tcp_chat_server.clje new file mode 100644 index 0000000..e889e15 --- /dev/null +++ b/examples/tcp_chat_server.clje @@ -0,0 +1,131 @@ +;;; TCP Chat Server — multi-terminal chat using gen_tcp +;;; +;;; Start the server: +;;; mix clje.run --no-halt examples/tcp_chat_server.clje +;;; +;;; Then connect clients in separate terminals: +;;; mix clje.run --no-halt examples/tcp_chat_client.clje -- alice +;;; mix clje.run --no-halt examples/tcp_chat_client.clje -- bob +;;; +;;; Protocol (line-based): +;;; Client → Server: JOIN: | MSG: | QUIT +;;; Server → Client: SYS: | MSG:: | JOIN: | QUIT: + +(ns TcpChatServer + (:require [clje.core :refer :all] + [Map] [String] [gen_tcp] [inet])) + +;; ── Room manager ────────────────────────────────────────────────── +;; Holds {username → socket} map, broadcasts messages to all members. + +(defn room-loop [members] + (receive + [:join username socket handler-pid] + (do + (TcpChatServer/broadcast members (str "JOIN:" username "\n")) + (TcpChatServer/send-line socket (str "SYS:Welcome " username "! " (count members) " user(s) online.\n")) + (TcpChatServer/room-loop (assoc members username #el[socket handler-pid]))) + + [:msg username text] + (do + (TcpChatServer/broadcast members (str "MSG:" username ":" text "\n")) + (TcpChatServer/room-loop members)) + + [:quit username] + (let [new-members (dissoc members username)] + (TcpChatServer/broadcast new-members (str "QUIT:" username "\n")) + (TcpChatServer/room-loop new-members)) + + [:list reply-pid] + (do + (send reply-pid #el[:members (Map/keys members)]) + (TcpChatServer/room-loop members)))) + +(defn broadcast [members line] + (doseq [[_name [socket _pid]] members] + (gen_tcp/send socket line))) + +(defn send-line [socket line] + (gen_tcp/send socket line)) + +;; ── Per-client handler ──────────────────────────────────────────── +;; Receives TCP data via active mode and forwards to room manager. + +(defn client-handler [socket room username] + (receive + [:tcp _sock data] + (let [line (String/trim data)] + (cond + (String/starts-with? line "MSG:") + (do + (send room #el[:msg username (String/slice line 4 (String/length line))]) + (TcpChatServer/client-handler socket room username)) + + (== line "QUIT") + (do + (send room #el[:quit username]) + (gen_tcp/close socket) + (println (str " [" username " quit]"))) + + :else + (do + (TcpChatServer/send-line socket (str "SYS:Unknown command. Send MSG: or QUIT\n")) + (TcpChatServer/client-handler socket room username)))) + + [:tcp_closed _sock] + (do + (send room #el[:quit username]) + (println (str " [" username " disconnected]"))) + + [:tcp_error _sock _reason] + (do + (send room #el[:quit username]) + (println (str " [" username " error]"))))) + +;; ── Accept loop ─────────────────────────────────────────────────── + +(defn accept-loop [listen-socket room] + (case (gen_tcp/accept listen-socket) + [:ok client-socket] + (do + (TcpChatServer/handle-new-client client-socket room) + (TcpChatServer/accept-loop listen-socket room)) + [:error reason] + (println (str "Accept error: " reason)))) + +(defn handle-new-client [socket room] + ;; First message must be JOIN: + ;; Socket starts in passive mode so we can recv synchronously + (case (gen_tcp/recv socket 0 5000) + [:ok data] + (let [line (String/trim data)] + (if (String/starts-with? line "JOIN:") + (let [username (String/slice line 5 (String/length line)) + ;; Switch to active mode for the handler + _ (inet/setopts socket (list #el[:active true])) + handler (spawn (fn [] + (TcpChatServer/client-handler socket room username)))] + (gen_tcp/controlling-process socket handler) + (send room #el[:join username socket handler]) + (println (str " [" username " connected]"))) + (do + (TcpChatServer/send-line socket "SYS:First message must be JOIN:\n") + (gen_tcp/close socket)))) + [:error _reason] + (gen_tcp/close socket))) + +;; ── Start ───────────────────────────────────────────────────────── + +(defn start [port] + (case (gen_tcp/listen port (list :binary #el[:active false] #el[:packet :line] #el[:reuseaddr true])) + [:ok listen-socket] + (do + (println (str "TCP Chat Server listening on port " port)) + (println "Connect with: mix clje.run --no-halt examples/tcp_chat_client.clje -- ") + (let [room (spawn (fn [] (TcpChatServer/room-loop {})))] + (TcpChatServer/accept-loop listen-socket room))) + [:error reason] + (println (str "Failed to listen on port " port ": " reason)))) + +;; Start the server on port 4040 +(TcpChatServer/start 4040) diff --git a/lib/clj_elixir/analyzer.ex b/lib/clj_elixir/analyzer.ex index 35ebcd0..e885213 100644 --- a/lib/clj_elixir/analyzer.ex +++ b/lib/clj_elixir/analyzer.ex @@ -100,6 +100,10 @@ defmodule CljElixir.Analyzer do validate_loop(args, meta, ctx) end + defp validate_form({:list, _meta, [{:symbol, _, "receive"} | args]}, ctx) do + validate_receive(args, ctx) + end + defp validate_form({:list, meta, [{:symbol, _, "recur"} | _args]}, ctx) do validate_recur(meta, ctx) end @@ -529,6 +533,27 @@ defmodule CljElixir.Analyzer do end end + # receive propagates tail position into clause bodies + defp validate_receive(clauses, ctx) do + validate_receive_clauses(clauses, ctx) + end + + defp validate_receive_clauses([], _ctx), do: [] + + defp validate_receive_clauses([:after, _timeout, body | rest], ctx) do + validate_form(body, ctx) ++ validate_receive_clauses(rest, ctx) + end + + defp validate_receive_clauses([_pattern, :guard, _guard, body | rest], ctx) do + validate_form(body, ctx) ++ validate_receive_clauses(rest, ctx) + end + + defp validate_receive_clauses([_pattern, body | rest], ctx) do + validate_form(body, ctx) ++ validate_receive_clauses(rest, ctx) + end + + defp validate_receive_clauses([_], _ctx), do: [] + defp validate_recur(meta, ctx) do line = meta_line(meta) col = meta_col(meta) diff --git a/lib/clj_elixir/compiler.ex b/lib/clj_elixir/compiler.ex index a3cd45a..311b7fe 100644 --- a/lib/clj_elixir/compiler.ex +++ b/lib/clj_elixir/compiler.ex @@ -83,26 +83,46 @@ defmodule CljElixir.Compiler do @spec eval_string(String.t(), keyword()) :: {:ok, term(), keyword()} | {:error, list()} def eval_string(source, opts \\ []) do with {:ok, ast} <- compile_string(source, opts) do - try do - bindings = opts[:bindings] || [] - env_opts = build_eval_opts(opts) - {result, new_bindings} = Code.eval_quoted(ast, bindings, env_opts) - {:ok, result, new_bindings} - rescue - e -> - file = opts[:file] || "nofile" + eval_ast(ast, opts) + end + end - {:error, - [ - %{ - severity: :error, - message: format_eval_error(e), - file: file, - line: extract_line(e), - col: 0 - } - ]} - end + @doc """ + Evaluate pre-parsed CljElixir AST forms. + + Runs analyze → transform → eval, skipping the read step. + Used by the REPL for incremental re-evaluation of accumulated definitions. + + Returns `{:ok, result, bindings}` on success, or `{:error, diagnostics}` on failure. + """ + @spec eval_forms(list(), keyword()) :: {:ok, term(), keyword()} | {:error, list()} + def eval_forms(forms, opts \\ []) do + with {:ok, forms} <- analyze(forms, opts), + {:ok, ast} <- transform(forms, opts) do + eval_ast(ast, opts) + end + end + + defp eval_ast(ast, opts) do + try do + bindings = opts[:bindings] || [] + env_opts = build_eval_opts(opts) + {result, new_bindings} = Code.eval_quoted(ast, bindings, env_opts) + {:ok, result, new_bindings} + rescue + e -> + file = opts[:file] || "nofile" + + {:error, + [ + %{ + severity: :error, + message: format_eval_error(e), + file: file, + line: extract_line(e), + col: 0 + } + ]} end end diff --git a/lib/clj_elixir/nrepl/handler.ex b/lib/clj_elixir/nrepl/handler.ex index a488b14..d13852f 100644 --- a/lib/clj_elixir/nrepl/handler.ex +++ b/lib/clj_elixir/nrepl/handler.ex @@ -35,7 +35,7 @@ defmodule CljElixir.NRepl.Handler do code = Map.get(msg, "code", "") # Capture stdout inside the Agent process where eval actually runs - {output, result} = SessionManager.eval_with_capture(manager, session, code) + {output, result, ns} = SessionManager.eval_with_capture(manager, session, code) responses = [] @@ -51,7 +51,7 @@ defmodule CljElixir.NRepl.Handler do responses = case result do {:ok, value} -> - responses ++ [%{"id" => id, "session" => session, "value" => value, "ns" => "user"}] + responses ++ [%{"id" => id, "session" => session, "value" => value, "ns" => ns}] {:error, error} -> responses ++ diff --git a/lib/clj_elixir/nrepl/session.ex b/lib/clj_elixir/nrepl/session.ex index 92ba5ef..1cd9c54 100644 --- a/lib/clj_elixir/nrepl/session.ex +++ b/lib/clj_elixir/nrepl/session.ex @@ -87,21 +87,22 @@ defmodule CljElixir.NRepl.SessionManager do def handle_call({:eval_with_capture, id, code}, _from, state) do case Map.get(state.sessions, id) do nil -> - {:reply, {"", {:error, "unknown session"}}, state} + {:reply, {"", {:error, "unknown session"}, "user"}, state} pid -> - {output, result} = + {output, result, ns} = Agent.get_and_update( pid, fn repl_state -> {output, eval_result, new_state} = eval_capturing_output(code, repl_state) + ns = CljElixir.REPL.current_ns(new_state) - {{output, eval_result}, new_state} + {{output, eval_result, ns}, new_state} end, :infinity ) - {:reply, {output, result}, state} + {:reply, {output, result, ns}, state} end end diff --git a/lib/clj_elixir/printer.ex b/lib/clj_elixir/printer.ex index bb416ca..75e7599 100644 --- a/lib/clj_elixir/printer.ex +++ b/lib/clj_elixir/printer.ex @@ -36,6 +36,10 @@ defmodule CljElixir.Printer do do_print_str(value) end + @doc "Clojure-compatible str: nil→\"\", strings pass through, else print representation" + def str_value(nil), do: "" + def str_value(value), do: print_str(value) + # Check if IPrintWithWriter is compiled and implemented for this value defp protocol_implemented?(value) do case Code.ensure_loaded(CljElixir.IPrintWithWriter) do diff --git a/lib/clj_elixir/repl.ex b/lib/clj_elixir/repl.ex index 7c4f669..71e5bd3 100644 --- a/lib/clj_elixir/repl.ex +++ b/lib/clj_elixir/repl.ex @@ -4,44 +4,52 @@ defmodule CljElixir.REPL do Maintains state across evaluations: bindings persist, modules defined in one evaluation are available in the next. + + Tracks the current namespace (`ns`) so that bare `defn`/`def` forms + are merged into the active module and the module is recompiled + incrementally. """ defstruct bindings: [], history: [], counter: 1, - env: nil + env: nil, + current_ns: nil, + module_defs: %{} @doc "Create a new REPL state" def new do + Code.compiler_options(ignore_module_conflict: true) %__MODULE__{} end + @doc "Return the current namespace name (defaults to \"user\")" + def current_ns(%__MODULE__{current_ns: ns}), do: ns || "user" + @doc """ Evaluate a CljElixir source string in the given REPL state. Returns {:ok, result_string, new_state} or {:error, error_string, new_state}. """ def eval(source, state) do - opts = [ - bindings: state.bindings, - file: "repl" - ] + case CljElixir.Reader.read_string(source) do + {:ok, forms} -> + has_ns = Enum.any?(forms, &ns_form?/1) + has_defs = Enum.any?(forms, &def_form?/1) - case CljElixir.Compiler.eval_string(source, opts) do - {:ok, result, new_bindings} -> - result_str = CljElixir.Printer.pr_str(result) + cond do + has_ns -> + eval_with_ns(forms, source, state) - new_state = %{state | - bindings: new_bindings, - history: [source | state.history], - counter: state.counter + 1 - } + has_defs and state.current_ns != nil -> + eval_in_ns(forms, source, state) - {:ok, result_str, new_state} + true -> + eval_plain(source, state) + end - {:error, errors} -> - error_str = format_errors(errors) - new_state = %{state | counter: state.counter + 1} - {:error, error_str, new_state} + {:error, reason} -> + error_msg = if is_binary(reason), do: reason, else: inspect(reason) + {:error, "Read error: #{error_msg}", %{state | counter: state.counter + 1}} end end @@ -52,15 +60,150 @@ defmodule CljElixir.REPL do |> count_delimiters(0, 0, 0, false, false) end - # Count open/close delimiters, respecting strings and comments + # --------------------------------------------------------------------------- + # Eval strategies + # --------------------------------------------------------------------------- + + # Full ns block: set namespace, capture defs, compile normally + defp eval_with_ns(forms, source, state) do + ns_name = extract_ns_name(forms) + new_defs = collect_defs(forms) + + opts = [bindings: state.bindings, file: "repl"] + + case CljElixir.Compiler.eval_string(source, opts) do + {:ok, result, new_bindings} -> + new_state = %{state | + bindings: new_bindings, + current_ns: ns_name, + module_defs: new_defs, + history: [source | state.history], + counter: state.counter + 1 + } + + {:ok, CljElixir.Printer.pr_str(result), new_state} + + {:error, errors} -> + {:error, format_errors(errors), %{state | counter: state.counter + 1}} + end + end + + # Bare defs in active namespace: merge into module_defs and recompile module + defp eval_in_ns(forms, source, state) do + {new_def_forms, exprs} = Enum.split_with(forms, &def_form?/1) + + # Merge new defs into accumulated module_defs (keyed by name) + merged_defs = + Enum.reduce(new_def_forms, state.module_defs, fn form, acc -> + name = extract_def_name(form) + Map.put(acc, name, form) + end) + + # Reconstruct: ns + all accumulated defs + current expressions + ns_form = make_ns_form(state.current_ns) + all_forms = [ns_form | Map.values(merged_defs)] ++ exprs + + opts = [bindings: state.bindings, file: "repl"] + + case CljElixir.Compiler.eval_forms(all_forms, opts) do + {:ok, result, new_bindings} -> + result_str = + if exprs == [] do + # Def-only: show var-like representation + new_def_forms + |> Enum.map(&extract_def_name/1) + |> Enum.map_join(" ", &"#'#{state.current_ns}/#{&1}") + else + CljElixir.Printer.pr_str(result) + end + + new_state = %{state | + bindings: new_bindings, + module_defs: merged_defs, + history: [source | state.history], + counter: state.counter + 1 + } + + {:ok, result_str, new_state} + + {:error, errors} -> + {:error, format_errors(errors), %{state | counter: state.counter + 1}} + end + end + + # No ns context: eval as-is (legacy / ad-hoc expressions) + defp eval_plain(source, state) do + opts = [bindings: state.bindings, file: "repl"] + + case CljElixir.Compiler.eval_string(source, opts) do + {:ok, result, new_bindings} -> + new_state = %{state | + bindings: new_bindings, + history: [source | state.history], + counter: state.counter + 1 + } + + {:ok, CljElixir.Printer.pr_str(result), new_state} + + {:error, errors} -> + {:error, format_errors(errors), %{state | counter: state.counter + 1}} + end + end + + # --------------------------------------------------------------------------- + # Form classification helpers + # --------------------------------------------------------------------------- + + defp ns_form?({:list, _, [{:symbol, _, "ns"} | _]}), do: true + defp ns_form?(_), do: false + + defp def_form?({:list, _, [{:symbol, _, name} | _]}) + when name in ~w(defn defn- def defprotocol defrecord extend-type + extend-protocol reify defmacro use), + do: true + + defp def_form?({:list, _, [{:symbol, _, "m/=>"} | _]}), do: true + defp def_form?(_), do: false + + defp extract_ns_name(forms) do + Enum.find_value(forms, fn + {:list, _, [{:symbol, _, "ns"}, {:symbol, _, name} | _]} -> name + _ -> nil + end) + end + + defp collect_defs(forms) do + forms + |> Enum.filter(&def_form?/1) + |> Enum.reduce(%{}, fn form, acc -> + name = extract_def_name(form) + Map.put(acc, name, form) + end) + end + + defp extract_def_name({:list, _, [{:symbol, _, _}, {:symbol, _, name} | _]}), do: name + defp extract_def_name(form), do: "anon_#{:erlang.phash2(form)}" + + defp make_ns_form(ns_name) do + {:list, %{line: 0, col: 0}, [ + {:symbol, %{line: 0, col: 0}, "ns"}, + {:symbol, %{line: 0, col: 0}, ns_name} + ]} + end + + # --------------------------------------------------------------------------- + # Delimiter balancing + # --------------------------------------------------------------------------- + defp count_delimiters([], parens, brackets, braces, _in_string, _escape) do - parens == 0 and brackets == 0 and braces == 0 + # Negative counts mean excess closing delimiters — let the reader report the error + parens < 0 or brackets < 0 or braces < 0 or + (parens == 0 and brackets == 0 and braces == 0) end defp count_delimiters([char | rest], p, b, br, in_string, escape) do cond do escape -> - # Previous char was \, skip this one count_delimiters(rest, p, b, br, in_string, false) char == "\\" and in_string -> @@ -76,7 +219,6 @@ defmodule CljElixir.REPL do count_delimiters(rest, p, b, br, true, false) char == ";" -> - # Comment - skip rest of line rest_after_newline = Enum.drop_while(rest, &(&1 != "\n")) count_delimiters(rest_after_newline, p, b, br, false, false) @@ -91,6 +233,10 @@ defmodule CljElixir.REPL do end end + # --------------------------------------------------------------------------- + # Error formatting + # --------------------------------------------------------------------------- + defp format_errors(errors) when is_list(errors) do Enum.map_join(errors, "\n", fn %{message: msg, line: line} when is_integer(line) and line > 0 -> diff --git a/lib/clj_elixir/transformer.ex b/lib/clj_elixir/transformer.ex index 5f46f1e..66bd559 100644 --- a/lib/clj_elixir/transformer.ex +++ b/lib/clj_elixir/transformer.ex @@ -53,14 +53,51 @@ defmodule CljElixir.Transformer do def transform(forms, ctx \\ %Context{}) def transform(forms, ctx) when is_list(forms) do - {elixir_forms, _ctx} = - Enum.map_reduce(forms, ctx, fn form, acc -> - {ast, new_ctx} = transform_form(form, acc) - {ast, new_ctx} + # Check if file has explicit defmodule forms (ns won't auto-wrap if so) + has_defmodule = + Enum.any?(forms, fn + {:list, _, [{:symbol, _, "defmodule"} | _]} -> true + _ -> false end) - # Filter out nil (from defmacro which produces no runtime code) - elixir_forms = Enum.filter(elixir_forms, &(&1 != nil)) + {elixir_forms, final_ctx} = + Enum.map_reduce(forms, ctx, fn form, acc -> + {ast, new_ctx} = transform_form(form, acc) + # Tag each transformed form with whether the source was a def-like form + {{ast, def_form?(form)}, new_ctx} + end) + + # Filter out nil (from ns, defmacro which produce no runtime code) + elixir_forms = Enum.filter(elixir_forms, fn {ast, _} -> ast != nil end) + + # If ns declared a module and there are no explicit defmodule forms, + # separate def-forms (inside module) from expressions (after module) + elixir_forms = + if final_ctx.module_name != nil and ctx.module_name == nil and not has_defmodule do + {defs, exprs} = + Enum.split_with(elixir_forms, fn {_ast, is_def} -> is_def end) + + def_asts = Enum.map(defs, fn {ast, _} -> ast end) + expr_asts = Enum.map(exprs, fn {ast, _} -> ast end) + + block = + case def_asts do + [] -> nil + [single] -> single + multiple -> {:__block__, [], multiple} + end + + module_ast = + if block do + [{:defmodule, [context: Elixir], [final_ctx.module_name, [do: block]]}] + else + [] + end + + module_ast ++ expr_asts + else + Enum.map(elixir_forms, fn {ast, _} -> ast end) + end case elixir_forms do [] -> nil @@ -74,6 +111,30 @@ defmodule CljElixir.Transformer do ast end + # Transform a list of guard forms into a single ANDed Elixir guard AST. + # [:guard [(> x 0) (< x 10)]] → {:and, [], [guard1, guard2]} + defp transform_guards(guard_forms, ctx) do + guard_asts = Enum.map(guard_forms, &transform(&1, ctx)) + + case guard_asts do + [single] -> single + [first | rest] -> Enum.reduce(rest, first, fn g, acc -> + {:and, [context: Elixir], [acc, g]} + end) + end + end + + # Is this CljElixir AST form a definition (goes inside defmodule)? + defp def_form?({:list, _, [{:symbol, _, name} | _]}) + when name in ~w(defn defn- def defprotocol defrecord extend-type + extend-protocol reify defmacro use), + do: true + + # m/=> schema annotations + defp def_form?({:list, _, [{:symbol, _, "m/=>"} | _]}), do: true + + defp def_form?(_), do: false + # --------------------------------------------------------------------------- # Main dispatch # --------------------------------------------------------------------------- @@ -258,6 +319,7 @@ defmodule CljElixir.Transformer do defp transform_list([head | args], meta, ctx) do case head do # --- Special forms (symbols) --- + {:symbol, _, "ns"} -> transform_ns(args, meta, ctx) {:symbol, _, "defmodule"} -> transform_defmodule(args, meta, ctx) {:symbol, _, "defn"} -> transform_defn(args, meta, ctx, :def) {:symbol, _, "defn-"} -> transform_defn(args, meta, ctx, :defp) @@ -419,6 +481,17 @@ defmodule CljElixir.Transformer do end end + # --------------------------------------------------------------------------- + # 0. ns — module declaration (sets ctx.module_name for auto-wrapping) + # --------------------------------------------------------------------------- + + defp transform_ns([name_form | _rest], _meta, ctx) do + mod_alias = module_name_ast(name_form) + {nil, %{ctx | module_name: mod_alias}} + end + + defp transform_ns([], _meta, ctx), do: {nil, ctx} + # --------------------------------------------------------------------------- # 1. defmodule # --------------------------------------------------------------------------- @@ -527,8 +600,8 @@ defmodule CljElixir.Transformer do nil -> {def_kind, em, [call_with_args(fun_name, param_asts), [do: body_ast]]} - guard_form -> - guard_ast = transform(guard_form, fn_ctx) + guard_forms -> + guard_ast = transform_guards(guard_forms, fn_ctx) {def_kind, em, [ @@ -568,18 +641,18 @@ defmodule CljElixir.Transformer do {required, rest_param, nil, body} {:list, _, clause_elements} -> - # Might have guard: ([params] :when guard body) - parse_clause_with_guard(clause_elements) + # Might have guard: ([params] :guard guard body) + parse_clause_with_guards(clause_elements) end) end end - defp parse_clause_with_guard([{:vector, _, params}, :when, guard | body]) do + defp parse_clause_with_guards([{:vector, _, params}, :guard, {:vector, _, guards} | body]) do {required, rest_param} = split_rest_params(params) - {required, rest_param, guard, body} + {required, rest_param, guards, body} end - defp parse_clause_with_guard([{:vector, _, params} | body]) do + defp parse_clause_with_guards([{:vector, _, params} | body]) do {required, rest_param} = split_rest_params(params) {required, rest_param, nil, body} end @@ -630,8 +703,8 @@ defmodule CljElixir.Transformer do nil -> {:->, [], [all_param_asts, body_ast]} - guard_form -> - guard_ast = transform(guard_form, ctx) + guard_forms -> + guard_ast = transform_guards(guard_forms, ctx) guard_params = [{:when, [], all_param_asts ++ [guard_ast]}] {:->, [], [guard_params, body_ast]} end @@ -657,7 +730,7 @@ defmodule CljElixir.Transformer do {required, rest_param, nil, body} {:list, _, clause_elements} -> - parse_clause_with_guard(clause_elements) + parse_clause_with_guards(clause_elements) end) end end @@ -823,8 +896,8 @@ defmodule CljElixir.Transformer do clauses |> Enum.chunk_every(2) |> Enum.map(fn - [pattern, :when | rest] -> - # pattern :when guard body — need to re-chunk + [pattern, :guard | rest] -> + # pattern :guard guard body — need to re-chunk # This won't happen with chunk_every(2), handle differently pat_ast = transform(pattern, pattern_ctx) body_ast = transform(List.last(rest), ctx) @@ -1550,8 +1623,8 @@ defmodule CljElixir.Transformer do nil -> {:->, [], [[pat_ast], body_ast]} - guard_form -> - guard_ast = transform(guard_form, ctx) + guard_forms -> + guard_ast = transform_guards(guard_forms, ctx) {:->, [], [[{:when, [], [pat_ast, guard_ast]}], body_ast]} end end) @@ -1585,8 +1658,8 @@ defmodule CljElixir.Transformer do parse_receive_clauses(rest, acc, {timeout, body}) end - defp parse_receive_clauses([pattern, :when, guard, body | rest], acc, after_clause) do - parse_receive_clauses(rest, [{pattern, guard, body} | acc], after_clause) + defp parse_receive_clauses([pattern, :guard, {:vector, _, guards}, body | rest], acc, after_clause) do + parse_receive_clauses(rest, [{pattern, guards, body} | acc], after_clause) end defp parse_receive_clauses([pattern, body | rest], acc, after_clause) do @@ -1959,23 +2032,24 @@ defmodule CljElixir.Transformer do {{:-, [], [a_ast, 1]}, ctx} end - # str — concatenate with <> using to_string + # str — Clojure-compatible: nil→"", strings pass through, collections use print repr defp transform_str(args, ctx) do t_args = Enum.map(args, fn a -> transform(a, ctx) end) + str_call = fn arg -> + {{:., [], [{:__aliases__, [alias: false], [:CljElixir, :Printer]}, :str_value]}, [], [arg]} + end + ast = case t_args do [] -> "" [single] -> - {{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :to_string]}, [], [single]} + str_call.(single) _ -> - stringified = - Enum.map(t_args, fn a -> - {{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :to_string]}, [], [a]} - end) + stringified = Enum.map(t_args, str_call) Enum.reduce(tl(stringified), hd(stringified), fn arg, acc -> {:<>, [], [acc, arg]} @@ -1985,12 +2059,12 @@ defmodule CljElixir.Transformer do {ast, ctx} end - # println → IO.puts + # println → IO.puts, returns nil (Clojure convention) defp transform_println(args, ctx) do t_args = Enum.map(args, fn a -> transform(a, ctx) end) # If multiple args, join with str first - ast = + io_call = case t_args do [single] -> {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], [single]} @@ -2001,6 +2075,7 @@ defmodule CljElixir.Transformer do {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], [str_ast]} end + ast = {:__block__, [], [io_call, nil]} {ast, ctx} end @@ -2052,11 +2127,11 @@ defmodule CljElixir.Transformer do end end) - ast = {:__block__, [], writes} + ast = {:__block__, [], writes ++ [nil]} {ast, ctx} end - # (prn val) -> IO.puts(CljElixir.Printer.pr_str(val)) + # (prn val) -> IO.puts(CljElixir.Printer.pr_str(val)), returns nil # Multiple args joined with spaces, then newline defp transform_prn(args, ctx) do t_args = Enum.map(args, fn a -> transform(a, ctx) end) @@ -2079,7 +2154,8 @@ defmodule CljElixir.Transformer do end) end - ast = {{:., [], [io_mod, :puts]}, [], [joined]} + io_call = {{:., [], [io_mod, :puts]}, [], [joined]} + ast = {:__block__, [], [io_call, nil]} {ast, ctx} end @@ -2535,6 +2611,16 @@ defmodule CljElixir.Transformer do {ast, ctx} end + # update - (update m k f x y ...) => rewrite to (assoc m k (f (get m k) x y ...)) + # Rewritten at AST level so f goes through builtin dispatch (e.g. dissoc) + defp transform_update([m, k, f | extra_args], ctx) do + meta = %{line: 0, col: 0} + get_call = {:list, meta, [{:symbol, meta, "get"}, m, k]} + f_call = {:list, meta, [f, get_call | extra_args]} + assoc_call = {:list, meta, [{:symbol, meta, "assoc"}, m, k, f_call]} + do_transform(assoc_call, ctx) + end + # conj defp transform_conj([c, x], ctx) do c_ast = transform(c, ctx) diff --git a/lib/mix/tasks/clje.build.ex b/lib/mix/tasks/clje.build.ex new file mode 100644 index 0000000..0fb1aff --- /dev/null +++ b/lib/mix/tasks/clje.build.ex @@ -0,0 +1,72 @@ +defmodule Mix.Tasks.Clje.Build do + @moduledoc """ + Compile CljElixir files to BEAM bytecode. + + ## Usage + + mix clje.build src/my_module.clje + mix clje.build src/foo.clje src/bar.clje + mix clje.build src/foo.clje -o _build/dev/lib/clj_elixir/ebin + + Like `elixirc`. Compiles `.clje` files to `.beam` files without running them. + + ## Options + + * `-o` / `--output` - output directory for .beam files (default: `_build/dev/lib//ebin`) + """ + + use Mix.Task + + @shortdoc "Compile .clje files to BEAM bytecode" + + @impl Mix.Task + def run(args) do + {opts, files, _} = + OptionParser.parse(args, + switches: [output: :string], + aliases: [o: :output] + ) + + if files == [] do + Mix.shell().error("Usage: mix clje.build [...] [-o output_dir]") + System.halt(1) + end + + Mix.Task.run("compile") + Mix.Task.run("app.start") + + output_dir = opts[:output] || Mix.Project.compile_path() + + results = + Enum.map(files, fn file -> + Mix.shell().info("Compiling #{file}") + + case CljElixir.Compiler.compile_file_to_beam(file, output_dir: output_dir) do + {:ok, modules} -> + Enum.each(modules, fn {mod, _binary} -> + Mix.shell().info(" -> #{mod}") + end) + + :ok + + {:error, diagnostics} -> + Enum.each(diagnostics, fn diag -> + loc = + case {Map.get(diag, :file), Map.get(diag, :line, 0)} do + {nil, _} -> "" + {_f, 0} -> "#{diag.file}: " + {_f, l} -> "#{diag.file}:#{l}: " + end + + Mix.shell().error("#{loc}#{diag.severity}: #{diag.message}") + end) + + :error + end + end) + + if Enum.any?(results, &(&1 == :error)) do + System.halt(1) + end + end +end diff --git a/lib/mix/tasks/clje.eval.ex b/lib/mix/tasks/clje.eval.ex new file mode 100644 index 0000000..70e3886 --- /dev/null +++ b/lib/mix/tasks/clje.eval.ex @@ -0,0 +1,45 @@ +defmodule Mix.Tasks.Clje.Eval do + @moduledoc """ + Evaluate a CljElixir expression from the command line. + + ## Usage + + mix clje.eval '(+ 1 2)' + mix clje.eval '(defn greet [name] (str "hello " name))' '(greet "world")' + + Multiple expressions are evaluated in sequence, with bindings persisting. + The result of the last expression is printed. + """ + + use Mix.Task + + @shortdoc "Evaluate CljElixir expressions" + + @impl Mix.Task + def run([]) do + Mix.shell().error("Usage: mix clje.eval '' [...]") + System.halt(1) + end + + def run(exprs) do + Mix.Task.run("compile") + Mix.Task.run("app.start") + + {result, _bindings} = + Enum.reduce(exprs, {nil, []}, fn expr, {_prev, bindings} -> + case CljElixir.Compiler.eval_string(expr, bindings: bindings) do + {:ok, result, new_bindings} -> + {result, new_bindings} + + {:error, diagnostics} -> + Enum.each(diagnostics, fn diag -> + Mix.shell().error("#{diag.severity}: #{diag.message}") + end) + + System.halt(1) + end + end) + + IO.puts(CljElixir.Printer.pr_str(result)) + end +end diff --git a/lib/mix/tasks/clje.repl.ex b/lib/mix/tasks/clje.repl.ex index 484424b..1f38572 100644 --- a/lib/mix/tasks/clje.repl.ex +++ b/lib/mix/tasks/clje.repl.ex @@ -38,7 +38,8 @@ defmodule Mix.Tasks.Clje.Repl do end defp loop(state) do - prompt = "clje:#{state.counter}> " + ns = CljElixir.REPL.current_ns(state) + prompt = "#{ns}:#{state.counter}> " case read_input(prompt) do :eof -> @@ -80,10 +81,28 @@ defmodule Mix.Tasks.Clje.Repl do end defp read_input(prompt) do - case IO.gets(prompt) do - :eof -> :eof - {:error, _} -> :eof - data -> data + IO.write(prompt) + read_line() + end + + # Read a line character-by-character, treating both \r and \n as line terminators. + # This avoids IO.gets hanging when the terminal sends \r without \n. + defp read_line, do: read_line([]) + + defp read_line(acc) do + case IO.getn("", 1) do + :eof -> + if acc == [], do: :eof, else: acc |> Enum.reverse() |> IO.iodata_to_binary() + + {:error, _} -> + if acc == [], do: :eof, else: acc |> Enum.reverse() |> IO.iodata_to_binary() + + <> when c in [?\r, ?\n] -> + IO.write("\n") + acc |> Enum.reverse() |> IO.iodata_to_binary() + + char -> + read_line([char | acc]) end end @@ -96,16 +115,24 @@ defmodule Mix.Tasks.Clje.Repl do end defp read_continuation(acc) do - case IO.gets(" ") do - :eof -> acc - {:error, _} -> acc - line -> - new_acc = acc <> "\n" <> String.trim_trailing(line, "\n") + IO.write(" ") - if CljElixir.REPL.balanced?(new_acc) do - new_acc + case read_line() do + :eof -> acc + line -> + trimmed = String.trim(line) + + if trimmed == "" do + # Empty Enter in continuation mode: submit what we have + acc else - read_continuation(new_acc) + new_acc = acc <> "\n" <> trimmed + + if CljElixir.REPL.balanced?(new_acc) do + new_acc + else + read_continuation(new_acc) + end end end end diff --git a/lib/mix/tasks/clje.run.ex b/lib/mix/tasks/clje.run.ex new file mode 100644 index 0000000..9368c20 --- /dev/null +++ b/lib/mix/tasks/clje.run.ex @@ -0,0 +1,103 @@ +defmodule Mix.Tasks.Clje.Run do + @moduledoc """ + Compile and run a CljElixir file. + + ## Usage + + mix clje.run examples/chat_room.clje + mix clje.run -e '(println "hello")' script.clje + + Like `elixir script.exs` or `bb script.clj`. The file is compiled and + evaluated. Modules defined in the file become available. + + ## Options + + * `-e` / `--eval` - evaluate expression before running the file + * `--no-halt` - keep the system running after execution (useful for spawned processes) + + Arguments after `--` are available via `System.argv()`. + """ + + use Mix.Task + + @shortdoc "Run a CljElixir file" + + @impl Mix.Task + def run(args) do + # Split on "--" to separate mix opts from script args + {before_dashdash, script_args} = split_on_dashdash(args) + + {opts, positional, _} = + OptionParser.parse(before_dashdash, + switches: [eval: :keep, no_halt: :boolean], + aliases: [e: :eval] + ) + + Mix.Task.run("compile") + Mix.Task.run("app.start") + + # Make script args available via System.argv() + System.argv(script_args) + + # Evaluate any -e expressions first + bindings = + opts + |> Keyword.get_values(:eval) + |> Enum.reduce([], fn expr, bindings -> + case CljElixir.Compiler.eval_string(expr, bindings: bindings) do + {:ok, result, new_bindings} -> + IO.puts(CljElixir.Printer.pr_str(result)) + new_bindings + + {:error, diagnostics} -> + print_diagnostics(diagnostics) + System.halt(1) + end + end) + + # Run file(s) + case positional do + [] -> + unless Keyword.has_key?(opts, :eval) do + Mix.shell().error("Usage: mix clje.run [options] ") + System.halt(1) + end + + files -> + Enum.reduce(files, bindings, fn file, bindings -> + case CljElixir.Compiler.eval_file(file, bindings: bindings) do + {:ok, _result, new_bindings} -> + new_bindings + + {:error, diagnostics} -> + print_diagnostics(diagnostics) + System.halt(1) + end + end) + end + + if opts[:no_halt] do + Process.sleep(:infinity) + end + end + + defp split_on_dashdash(args) do + case Enum.split_while(args, &(&1 != "--")) do + {before, ["--" | rest]} -> {before, rest} + {before, []} -> {before, []} + end + end + + defp print_diagnostics(diagnostics) do + Enum.each(diagnostics, fn diag -> + loc = + case {Map.get(diag, :file), Map.get(diag, :line, 0)} do + {nil, _} -> "" + {_f, 0} -> "#{diag.file}: " + {_f, l} -> "#{diag.file}:#{l}: " + end + + Mix.shell().error("#{loc}#{diag.severity}: #{diag.message}") + end) + end +end diff --git a/spec.md b/spec.md new file mode 100644 index 0000000..0f4d2b5 --- /dev/null +++ b/spec.md @@ -0,0 +1,1572 @@ +# CljElixir: A Clojure-Syntax Language for the BEAM + +## Overview + +CljElixir is Clojure for the BEAM. It combines Clojure's syntax, data-driven philosophy, and core vocabulary with the BEAM runtime — OTP, supervision trees, hot code swapping, distributed computing, and lightweight processes. + +CljElixir compiles to Elixir AST and delegates to the Elixir compiler for macro expansion, protocol consolidation, and BEAM bytecode generation. This means full interop with Elixir and Erlang libraries — any BEAM module is one `module/function` call away. + +BEAM's native data structures — maps (HAMT for >32 keys), lists (cons cells with tail sharing), and MapSets — are already persistent and immutable with structural sharing. CljElixir uses them directly. The one addition is PersistentVector (bit-partitioned trie for O(log32 n) indexed access), ported from ClojureScript. A core vocabulary (`get`, `assoc`, `dissoc`, `reduce`, `first`, `rest`, etc.) built on extensible protocols provides a uniform interface across all types. + +--- + +## Architecture + +``` +.clje source files + │ + ▼ +┌─────────────┐ +│ Reader │ S-expressions → CljElixir AST (Clojure-shaped) +└─────┬───────┘ + │ + ▼ +┌──────────────┐ +│ Analyzer │ AST validation (arity, recur position, map literals) +└─────┬────────┘ + │ + ▼ +┌──────────────┐ +│ Transformer │ CljElixir AST → Elixir AST ({atom, meta, args} tuples) +└─────┬────────┘ + │ + ▼ +┌──────────────┐ +│ Elixir │ Macro expansion, protocol consolidation, +│ Compiler │ BEAM bytecode generation +└──────────────┘ +``` + +Key insight: Elixir's AST is already a tagged s-expression — every node is `{operation, metadata, arguments}`. CljElixir's transformer maps from one s-expression format to another. The Elixir compiler handles everything hard. + +Implementation: A Mix compiler that reads `.clje` files, transforms them to Elixir AST, and feeds them into the standard compilation pipeline. + +### Analyzer + +The analyzer validates the CljElixir AST before transformation. It catches structural errors early with source-mapped diagnostics: + +- **Special form arity** — `defmodule`, `let`, `if`, `case`, `cond`, `loop`, `fn`, `defn` checked for correct argument count +- **Map literals** — must have even number of forms (key-value pairs) +- **Recur position** — `recur` must be in tail position within `loop` or `defn` +- **Binding vectors** — `let`/`loop` must have even-length binding pairs +- **Receive clauses** — validates pattern/body or pattern/guard/body structure + +### Protocol Compilation + +Both `extend-type` and `extend-protocol` compile to Elixir `defimpl` calls. The mapping is mechanical — `defimpl` is always a `(protocol, type, fns)` triple, and both Clojure forms decompose into a list of those triples: + +``` +extend-type: type fixed, iterate over protocols → N defimpls +extend-protocol: protocol fixed, iterate over types → N defimpls +``` + +Example: + +```clojure +;; CljElixir source: +(extend-type Map + ILookup + (-lookup + ([m k] (Map/get m k)) + ([m k not-found] (Map/get m k not-found))) + ICounted + (-count [m] (map-size m))) +``` + +```elixir +# Emitted Elixir AST (two defimpl nodes): +defimpl ILookup, for: Map do + def lookup(m, k), do: Map.get(m, k) + def lookup(m, k, not_found), do: Map.get(m, k, not_found) +end + +defimpl ICounted, for: Map do + def count(m), do: map_size(m) +end +``` + +`defprotocol` compiles directly to Elixir's `defprotocol`. `reify` compiles to an anonymous struct + inline `defimpl` calls. The transformer hoists `defimpl` nodes to module scope when they appear inside function bodies. + +--- + +## Syntax Reference + +### Modules and Functions + +```clojure +;; ns declares the module — all top-level forms become module members +(ns Greeter (:require [clje.core :refer :all])) + +(defn hello [name] + (str "hello " name)) + +;; defmodule is available for scripts with multiple modules +(defmodule Greeter + (defn hello [name] + (str "hello " name))) + +;; Multi-clause pattern matching (same shape as multi-arity) +(defn process + ([[:ok data]] (handle data)) + ([[:error reason]] (log reason))) + +;; Multi-arity +(defn greet + ([name] (greet name "hello")) + ([name greeting] (str greeting " " name))) + +;; Both together: multi-clause + multi-arity +(defn handle + ([[:ok data]] (process data)) + ([[:error reason]] (log reason)) + ([[:error reason] opts] (log reason opts))) + +;; Private functions +(defn- internal-helper [x] + (* x 2)) + +;; Anonymous functions +#(* % 2) ;; single arg +#(+ %1 %2) ;; multiple args +(fn [x y] (+ x y)) ;; explicit form +``` + +### Data Literals + +```clojure +;; === BEAM-native data (maps, lists, sets — already persistent) === +{:name "Ada" :age 30} ;; Erlang map (HAMT for >32 keys) +'(1 2 3) ;; Erlang list (cons cells, tail sharing) +#{:a :b :c} ;; Erlang MapSet + +;; === CljElixir-provided === +[1 2 3] ;; PersistentVector (bit-partitioned trie) + +;; === Raw BEAM tuple === +#el[:ok value] ;; Erlang tuple + +;; Keywords +:ok :error :shutdown + +;; Strings +"hello world" + +;; Regex (Clojure style) +#"pattern" +#"^\d{3}-\d{4}$" +``` + +Maps, lists, and sets are BEAM-native types. They flow freely between CljElixir and Elixir/Erlang with zero conversion. PersistentVector is the one CljElixir-specific type. `#el[]` creates BEAM tuples directly. + +Vectors in pattern position match BEAM tuples (since incoming messages and Elixir interop always produce tuples): + +```clojure +[:ok data] ;; in case/receive: matches tuple {:ok, data} +[x y] ;; in defn: parameter list +[x 1 y 2] ;; in let: binding pairs +``` + +### Module Calls (FFI) + +All BEAM module calls use the same `module/function` syntax. The compiler distinguishes Elixir from Erlang modules by case — this isn't an invented rule, it's how the BEAM works. Erlang modules are lowercase atoms (`:crypto`, `:ets`). Elixir modules are uppercase-prefixed atoms (`:"Elixir.Enum"`, `:"Elixir.Map"`). + +```clojure +;; Elixir modules (uppercase → :"Elixir.Enum".map(...)) +(Enum/map list func) +(String/split s ",") +(Map/merge m1 m2) +(GenServer/start-link MyServer args) + +;; Erlang modules (lowercase → :crypto.strong_rand_bytes(...)) +(erlang/system-time :millisecond) +(crypto/strong-rand-bytes 16) +(ets/new :my-table [:set :public]) +(io/format "hello ~s~n" ["world"]) + +;; No special syntax — it's all just module/function +``` + +### Dynamic Vars + +```clojure +*self* ;; current process (replaces self()) +*node* ;; current BEAM node (e.g. :"myapp@192.168.1.1") +``` + +### Metadata + +```clojure +;; Module metadata via ^{} (defmodule form) +(defmodule ^{:author "Ada"} Greeter + ...) +``` + +### Docstrings + +Docstrings are metadata. The string between name and body is sugar for `^{:doc "..."}`: + +```clojure +;; These are equivalent: +(defn hello + "Greets someone by name" + [name] + (str "hello " name)) + +(defn ^{:doc "Greets someone by name"} hello + [name] + (str "hello " name)) +``` + +Works everywhere metadata works — `defn`, `defmodule`, `defprotocol`, `defrecord`: + +```clojure +(defmodule Greeter + "A module for greeting people" + + (defn hello + "Greets someone by name" + [name] + (str "hello " name))) + +(defprotocol Describable + "Protocol for human-readable descriptions" + (describe [value] "Returns a description string")) + +(defrecord User + "A user in the system" + [name age email]) +``` + +--- + +## Records + +CljElixir has `defrecord`. It does not have `deftype`. + +Clojure's `deftype` exists for mutable fields — `set!` on fields inside method bodies, used to build data structure internals. BEAM has no in-place mutation of heap terms, so `deftype`'s core capability cannot be supported. + +There is no `{:bare true}` mode either. On BEAM, a `defrecord` compiles to an Elixir struct, which is a map. Stripping map interfaces from something that *is* a map just hides functionality for no reason. If it's immutable and it's a map, let it be a map. + +### `defrecord` + +`defrecord` compiles to an Elixir `defstruct` with auto-generated protocol implementations for `ILookup`, `IAssociative`, `IMap`, `ICounted`, `ISeqable`, `IEquiv`, `IHash`, `IMeta`, `IWithMeta`. You get keyword access, `assoc`, `dissoc`, equality, hashing, and destructuring for free. + +```clojure +(defrecord User [name age email]) + +;; Positional constructor +(->User "Ada" 30 "ada@example.com") + +;; Map constructor +(map->User {:name "Ada" :age 30 :email "ada@example.com"}) + +;; Keyword access (auto ILookup) +(:name user) ;; => "Ada" + +;; Update (auto IAssociative) +(assoc user :age 31) + +;; Equality (auto IEquiv) +(= (->User "Ada" 30 "ada@example.com") + (->User "Ada" 30 "ada@example.com")) ;; => true + +;; Destructuring +(let [{:keys [name email]} user] + (str name " <" email ">")) +``` + +Records can implement additional protocols inline: + +```clojure +(defrecord PriorityQueue [items comparator] + ICounted + (-count [_] (count items)) + + ICollection + (-conj [_ item] + (let [new-items (sort-by comparator (cons item items))] + (->PriorityQueue new-items comparator))) + + ISeqable + (-seq [_] items)) +``` + +Internal data structures use the same `defrecord` — there's no reason to strip map interfaces from something that is a map: + +```clojure +(defrecord VectorNode [edit arr]) + +(defrecord PersistentVector [meta cnt shift root tail] + ICounted + (-count [_] cnt) + + IIndexed + (-nth [this n] + (if (and (>= n 0) (< n cnt)) + (let [node (unchecked-array-for this n)] + (nth node (bit-and n 0x01f))) + (throw (str "Index " n " out of bounds")))) + + ICollection + (-conj [this val] + ;; ... append to tail or create new level + ) + + IMeta + (-meta [_] meta) + + IWithMeta + (-with-meta [_ new-meta] + (->PersistentVector new-meta cnt shift root tail))) + +;; VectorNode fields are accessible like any record +(:arr node) ;; works +(:edit node) ;; works +``` + +### Impact of No `deftype` + +**PersistentVector is buildable.** The persistent operations — `nth`, `assoc`, `conj`, `pop` — are path-copying by design. You create new nodes along the changed path, sharing everything else. This doesn't require mutable fields: + +```clojure +;; Path-copy: clone node, replace one slot +(defn clone-and-set [node i val] + (let [arr (Map/get node :arr) + new-arr (put-elem arr i val)] + (->VectorNode (Map/get node :edit) new-arr))) +``` + +**Hash caching is eager.** ClojureScript caches hash codes lazily in a `^:mutable __hash` field. On BEAM, hashes must be computed at construction time or recomputed on every call. + +--- + +## Schemas (Malli) + +CljElixir uses Malli-style data-driven schemas instead of Elixir's `@spec` annotations. Schemas are plain data — maps, vectors, keywords — not macros or special syntax. + +### Compile-Time Requirement + +Schemas are "just data," but that data must be available at compile time to be useful. Top-level `def` forms are evaluated at compile time (same as Clojure), so named schemas are compile-time constants: + +```clojure +;; These are compile-time values — the compiler can see them +(def User + [:map + [:name :string] + [:age :int] + [:email :string]]) + +(def PositiveInt + [:and :int [:> 0]]) + +(def Status + [:enum :active :inactive :pending]) +``` + +`m/=>` is a macro that runs at compile time. It reads the schema data, emits an Elixir `@spec` attribute for Dialyzer, and optionally registers the schema for runtime validation. Because the schema is a compile-time constant, the macro can walk it immediately: + +```clojure +(defn hello [name] + (str "hello " name)) + +(m/=> hello [:=> [:cat :string] :string]) +;; At compile time: +;; 1. Emits @spec hello(String.t()) :: String.t() for Dialyzer +;; 2. Registers schema for optional runtime validation +``` + +If a schema references another schema by name (`PositiveInt` inside `Config`), that name must resolve to a compile-time value — it must be `def`'d before use: + +```clojure +(def PositiveInt [:and :int [:> 0]]) + +(def Config + [:map + [:host :string] + [:port PositiveInt] ;; resolved at compile time + [:ssl? :boolean]]) +``` + +### Function Schemas + +```clojure +;; Single arity +(defn hello [name] + (str "hello " name)) +(m/=> hello [:=> [:cat :string] :string]) + +;; Multi-arity +(defn greet + ([name] (greet name "hello")) + ([name greeting] (str greeting " " name))) +(m/=> greet [:function + [:=> [:cat :string] :string] + [:=> [:cat :string :string] :string]]) +``` + +### Validation and Coercion (Runtime) + +These are runtime operations — the adapter phase. Not in the initial implementation: + +```clojure +;; Validate +(m/validate User {:name "Ada" :age 30 :email "ada@example.com"}) +;; => true + +;; Explain failures +(m/explain User {:name "Ada" :age "thirty"}) +;; => {:errors [{:path [:age] :value "thirty" :schema :int}]} + +;; Coerce +(m/coerce PositiveInt "42") +;; => 42 +``` + +### Recursive Types + +Malli handles recursion via `:ref` within a local `:registry`. Without `:ref`, schemas expand eagerly and stack overflow: + +```clojure +;; Recursive: linked list of ints +(def IntList + [:schema {:registry {::cons [:maybe [:tuple :int [:ref ::cons]]]}} + [:ref ::cons]]) + +;; Mutual recursion +(def PingPong + [:schema {:registry {::ping [:maybe [:tuple [:= "ping"] [:ref ::pong]]] + ::pong [:maybe [:tuple [:= "pong"] [:ref ::ping]]]}} + [:ref ::ping]]) + +;; Tree +(def Tree + [:schema {:registry {::tree [:or :int [:tuple [:ref ::tree] [:ref ::tree]]]}} + [:ref ::tree]]) +``` + +The adapter maps these to Elixir recursive typespecs at compile time. Registry entries become `@type` definitions, `:ref` becomes a named type reference: + +```clojure +(def Tree + [:schema {:registry {::tree [:or :int [:tuple [:ref ::tree] [:ref ::tree]]]}} + [:ref ::tree]]) + +;; emits: +;; @type tree :: integer | {tree, tree} +``` + +This works because Elixir/Dialyzer resolves named types lazily — the self-reference is fine as long as it's a named `@type`. + +The initial adapter generates Elixir typespecs from Malli schemas at compile time: + +```clojure +(m/=> hello [:=> [:cat :string] :string]) +;; emits: @spec hello(String.t()) :: String.t() + +(def User [:map [:name :string] [:age :int] [:email :string]]) +;; emits: @type user :: %{name: String.t(), age: integer()} +``` + +This gives you Dialyzer static analysis from day one. Full Malli (validation, coercion, generation) and clojure.spec support come later. + +--- + +## Data Structures + +### BEAM's Native Persistent Data Structures + +Elixir/Erlang data structures are already persistent and immutable with structural sharing. This is not a Clojure-specific feature — it's how BEAM works: + +**Maps (≤32 keys):** Flat sorted tuple of keys + contiguous values. Updates copy the entire structure, but it's small enough to be fast. + +**Maps (>32 keys):** Hash Array Mapped Trie (HAMT) — the same data structure Clojure uses for PersistentHashMap. Updates share common parts of the trie. Implemented in C inside the BEAM VM since OTP 18. + +**Lists:** Singly-linked cons cells. Prepend is O(1) with tail sharing — `(cons x xs)` reuses the entire existing list. Same as Clojure's lists. + +**Tuples:** Flat contiguous arrays. No structural sharing — any update copies the entire tuple. Used for small fixed-size groups (like `{:ok, value}`), not for collections. + +This means CljElixir does **not** need to port Clojure's PersistentHashMap. BEAM already has one, implemented in C, battle-tested, and almost certainly faster than anything we'd write in CljElixir. The core vocabulary (`get`, `assoc`, `dissoc`, etc.) dispatches through protocols directly to native BEAM operations. + +### What BEAM Doesn't Have + +**Persistent Vector.** BEAM has no equivalent of Clojure's PersistentVector — a bit-partitioned trie giving O(log32 n) indexed access, O(1) append, and structural sharing. Erlang tuples give O(1) indexed access but O(n) update. Erlang lists give O(1) prepend but O(n) indexed access. PersistentVector fills the gap for collections that need both. This is the one data structure worth porting from ClojureScript. + +For batch-building maps, Erlang's `maps:from_list/1` (build a list of pairs, then convert in one shot) is already efficient. For batch-building vectors, `(into [] some-list)` is the idiomatic approach. + +### CljElixir Data Literal Summary + +```clojure +;; BEAM-native persistent data +{:name "Ada" :age 30} ;; Erlang map (HAMT for >32 keys) +'(1 2 3) ;; Erlang list (cons cells, tail sharing) +#{:a :b :c} ;; Erlang MapSet (backed by map) + +;; CljElixir-provided +[1 2 3] ;; PersistentVector (bit-partitioned trie) + +;; Tuple literal +#el[:ok value] ;; Erlang tuple + +;; Keywords +:ok :error :shutdown + +;; Strings +"hello world" + +;; Regex (Clojure style) +#"pattern" +#"^\d{3}-\d{4}$" +``` + +Vectors in pattern position match BEAM tuples (since incoming messages and Elixir interop always produce tuples): + +```clojure +[:ok data] ;; in case/receive: matches tuple {:ok, data} +[x y] ;; in defn: parameter list +[x 1 y 2] ;; in let: binding pairs +``` + +Core vocabulary (`get`, `assoc`, `dissoc`, `first`, `rest`, `count`, etc.) works on all types through protocol dispatch. + +All persistent types implement the core protocols (`ILookup`, `IAssociative`, `ISeq`, `ICounted`, etc.), so the entire core vocabulary works on them. + +### BEAM Interop Boundary + +Since CljElixir maps, lists, and sets ARE BEAM-native types, there's no conversion boundary for them. Data flows freely between CljElixir and Elixir/Erlang code without any conversion. `(assoc m :k v)` calls `Map.put` — it's the same map. + +The only CljElixir-specific data structure is PersistentVector (`[]`). For tuples, CljElixir provides both a reader macro and a function: + +```clojure +;; Reader macro (literal) +#el[:ok "data"] + +;; Function (n-ary, apply-compatible) +(tuple :ok "data") ;; => {:ok, "data"} +(tuple) ;; => {} (empty tuple) +(apply tuple args) ;; works +(map (fn [x] (tuple :ok x)) items) ;; works +``` + +Conversions use `into` with an empty target: + +```clojure +;; Vector → tuple +(into (tuple) [1 2 3]) ;; => {1, 2, 3} + +;; Tuple → vector +(into [] some-tuple) ;; => [elem0, elem1, ...] + +;; Tuple → list +(into '() some-tuple) ;; => (elemN, ... elem1, elem0) + +;; List → vector +(into [] some-list) + +;; Vector → list +(into '() some-vector) +``` + +`tuple` is a regular function in `clje.core`. Tuples implement `ISeqable` and `ICounted`, so `into`, `count`, `seq`, `first`, `rest`, `nth` all work on them. + +### Runtime Conversion: `clojurify` and `elixirify` + +Since maps and lists are already BEAM-native, conversion is only needed for vectors (CljElixir-specific) and tuples (BEAM-specific): + +```clojure +(defprotocol IClojurify + "Convert BEAM tuples/lists to CljElixir vectors where appropriate." + (-clojurify [o])) + +(defprotocol IElixirify + "Convert CljElixir vectors to BEAM lists/tuples." + (-elixirify [o])) +``` + +```clojure +;; Tuple → vector +(clojurify #el[:ok "data"]) ;; => [:ok "data"] + +;; List → vector +(clojurify '(1 2 3)) ;; => [1 2 3] + +;; Vector → list +(elixirify [1 2 3]) ;; => '(1 2 3) + +;; Deep conversion walks nested structures +(clojurify #el[:ok #el[:nested "data"]]) +;; => [:ok [:nested "data"]] +``` + +Since they're protocols, you can extend them to your own types: + +```clojure +(defrecord User [name age] + IElixirify + (-elixirify [u] {:name name :age age :type "user"})) +``` + +### Implementation Note + +BEAM maps >32 keys already use the same HAMT data structure as Clojure's PersistentHashMap, implemented in C inside the VM. There is no need to reimplement this. Erlang lists are already cons-cell linked lists with tail sharing. The only data structure that needs porting from ClojureScript is PersistentVector (~600 lines in ClojureScript), which provides O(log32 n) indexed access and O(1) append — something BEAM has no native equivalent for. + +--- + +## Core Protocols + +CljElixir uses the ClojureScript protocol naming convention. These are real Elixir protocols under the hood — users can extend them to custom types — but the compiler optimizes known cases to direct function calls. + +### Protocol Hierarchy + +``` +;; Persistent (structural sharing, immutable) +ILookup — -lookup +IAssociative — -assoc, -contains-key? + extends ILookup +IMap — -dissoc + extends IAssociative +ICollection — -conj +ICounted — -count +ISeqable — -seq +ISeq — -first, -rest +IIndexed — -nth +IFn — -invoke +IMeta — -meta +IWithMeta — -with-meta +IStack — -peek, -pop +IMapEntry — -key, -val +IKVReduce — -kv-reduce + +``` + +Two tiers of data: + +**BEAM-native (maps, lists, sets)** — the default for maps, lists, and sets. Already persistent with structural sharing (maps >32 keys use HAMT, lists share tails). `assoc`, `dissoc`, `conj` return new values via BEAM operations. + +**PersistentVector** — CljElixir-provided. Bit-partitioned trie for O(log32 n) indexed access and O(1) append with structural sharing. The one data structure BEAM doesn't have natively. + +```clojure +;; Maps are BEAM-native, already persistent +(assoc {:a 1} :b 2) ;; => BEAM map, structural sharing for >32 keys + +;; Vectors are CljElixir PersistentVector +(assoc [1 2 3] 1 :x) ;; => [1 :x 3], structural sharing +(conj [1 2 3] 4) ;; => [1 2 3 4], O(1) append + +;; Batch building uses into +(into [] (map (fn [x] (* x x)) items)) +(into {} (map (fn [x] [x (* x x)]) items)) +``` + +### Protocol Definitions + +```clojure +(defprotocol ILookup + "Protocol for looking up a value in a data structure." + (-lookup [o k] [o k not-found])) + +(defprotocol IAssociative + "Protocol for adding associativity to collections." + (-contains-key? [coll k]) + (-assoc [coll k v])) + +(defprotocol IMap + "Protocol for full map operations." + (-dissoc [coll k])) + +(defprotocol ICounted + "Calculates the count of a collection in constant time." + (-count [coll])) + +(defprotocol ISeqable + "Protocol for producing a sequence from a collection." + (-seq [o])) + +(defprotocol ISeq + "Protocol for sequential access." + (-first [coll]) + (-rest [coll])) + +(defprotocol ICollection + "Protocol for generic collection operations." + (-conj [coll o])) + +(defprotocol IIndexed + "Protocol for numeric index access." + (-nth [coll n] [coll n not-found])) + +(defprotocol IFn + "Protocol for invocable things." + (-invoke [o] [o a] [o a b] [o a b c])) + +(defprotocol IMeta + "Protocol for accessing metadata." + (-meta [o])) + +(defprotocol IWithMeta + "Protocol for adding metadata." + (-with-meta [o meta])) + +(defprotocol IStack + "Protocol for stack operations." + (-peek [coll]) + (-pop [coll])) + +(defprotocol IMapEntry + "Protocol for examining a map entry." + (-key [coll]) + (-val [coll])) + +(defprotocol IKVReduce + "Protocol for key-value reduce." + (-kv-reduce [coll f init])) +``` + +### Extending Types + +Use `extend-type` (one type, many protocols) and `extend-protocol` (one protocol, many types), exactly as in Clojure: + +```clojure +;; extend-type: extend one BEAM type with many protocols +(extend-type Map + ILookup + (-lookup + ([m k] (Map/get m k)) + ([m k not-found] (Map/get m k not-found))) + + IAssociative + (-contains-key? [m k] (Map/has-key? m k)) + (-assoc [m k v] (Map/put m k v)) + + IMap + (-dissoc [m k] (Map/delete m k)) + + ICounted + (-count [m] (map-size m)) + + ISeqable + (-seq [m] (Map/to-list m)) + + ICollection + (-conj [m entry] (Map/merge m entry)) + + IFn + (-invoke [m k] (Map/get m k)) + + IKVReduce + (-kv-reduce [m f init] + (Enum/reduce m init (fn [acc [k v]] (f acc k v))))) + +;; extend-type for lists +(extend-type List + ISeq + (-first [l] (hd l)) + (-rest [l] (tl l)) + + ICounted + (-count [l] (length l)) + + ISeqable + (-seq [l] l) + + ICollection + (-conj [l o] (cons o l))) + +;; extend-protocol: one protocol across many BEAM types +(extend-protocol ICounted + Map (-count [m] (map-size m)) + List (-count [l] (length l)) + Tuple (-count [t] (tuple-size t)) + BitString (-count [s] (byte-size s))) +``` + +### Performance + +Core functions dispatch through protocols. The protocols are real Elixir protocols — the BEAM handles dispatch optimization natively (consolidated protocols use a lookup table, not dynamic dispatch). + +--- + +## Core Vocabulary + +Core functions dispatch through protocols. Interop calls (`Module/function`) are always available as an escape hatch. + +### Data Access + +| CljElixir | Dispatches through | Notes | +|----------------------|--------------------------|--------------------------------| +| `(get m k)` | `ILookup` | Maps, vectors, records | +| `(get m k nf)` | `ILookup` | With not-found default | +| `(get-in m ks)` | `ILookup` (nested) | Deep access | +| `(assoc m k v)` | `IAssociative` | Maps and vectors | +| `(assoc-in m ks v)` | `IAssociative` (nested) | Deep update | +| `(dissoc m k)` | `IMap` | Maps only | +| `(update m k f)` | `IAssociative` | Apply f to value at k | +| `(update-in m ks f)` | `IAssociative` (nested) | Deep apply | +| `(merge m1 m2)` | `IMap` | Maps only | +| `(conj c x)` | `ICollection` | Type-dependent append | +| `(into c1 c2)` | `ICollection` | Reduce c2 into c1 | +| `(count c)` | `ICounted` | All collections | +| `(contains? m k)` | `IAssociative` | Key presence | +| `(keys m)` | `IMap` | Maps only | +| `(vals m)` | `IMap` | Maps only | +| `(select-keys m ks)` | `IMap` | Maps only | +| `(empty? c)` | `ICounted`/`ISeqable` | All collections | +| `(nth v n)` | `IIndexed` | Vectors (O(log32 n)) | + +### Keyword-as-Function + +```clojure +(:name user) ;; => ILookup.-lookup(user, :name) +(:name user "default") ;; => ILookup.-lookup(user, :name, "default") +``` + +### Sequences + +| CljElixir | Compiles to | +|-------------------|--------------------------| +| `map` | `Enum.map` | +| `filter` | `Enum.filter` | +| `reduce` | `Enum.reduce` | +| `reduce-kv` | `IKVReduce.-kv-reduce` | +| `first` | `ISeq.-first` | +| `rest` | `ISeq.-rest` | +| `seq` | `ISeqable.-seq` | +| `cons` | `[h \| t]` construction | +| `concat` | `Enum.concat` | +| `take` | `Enum.take` | +| `drop` | `Enum.drop` | +| `partition` | `Enum.chunk_every` | +| `sort` | `Enum.sort` | +| `sort-by` | `Enum.sort_by` | +| `group-by` | `Enum.group_by` | +| `frequencies` | `Enum.frequencies` | +| `distinct` | `Enum.uniq` | +| `mapcat` | `Enum.flat_map` | + +### Arithmetic, Logic, and Type Checks + +| CljElixir | Notes | +|-------------------|--------------------------| +| `+`, `-`, `*`, `/`| Variadic arithmetic | +| `>`, `<`, `>=`, `<=` | Comparisons | +| `=` | Value equality (IEquiv) | +| `==` | Numeric equality | +| `not=`, `!=` | Inequality | +| `inc`, `dec` | Increment/decrement | +| `rem` | Remainder | +| `not` | Logical negation | +| `and`, `or` | Variadic logical ops | +| `nil?` | Nil check | +| `is-pid` | PID type check | +| `is-binary` | Binary type check | +| `is-list` | List type check | +| `is-integer` | Integer type check | + +### Vectors + +| CljElixir | Notes | +|-------------------|--------------------------| +| `vec` | Collection → PersistentVector | +| `vector` | Args → PersistentVector | +| `vector?` | PersistentVector check | +| `subvec` | SubVector view (start, optional end) | +| `peek` | Last element (IStack) | +| `pop` | Remove last (IStack) | + +### Tuples + +| CljElixir | Notes | +|-------------------|--------------------------| +| `tuple` | Args → BEAM tuple | +| `tuple-size` | Tuple element count | +| `elem` | Indexed access | +| `put-elem` | Immutable tuple update | + +### Lists + +| CljElixir | Notes | +|-------------------|--------------------------| +| `list` | Args → list | +| `hd` | Head of list | +| `tl` | Tail of list | + +### Other Builtins + +| CljElixir | Notes | +|-------------------|--------------------------| +| `str` | String concatenation | +| `println` | Print with newline | +| `pr-str` | EDN-like string repr | +| `pr` | Print EDN to stdout | +| `prn` | Print EDN + newline | +| `print-str` | Human-readable string | +| `throw` | Raise exception | +| `apply` | Dynamic function call | + +--- + +## Equality + +CljElixir's `=` implements Clojure's value equality semantics, inspired by Baker's EGAL. On BEAM this is simpler than on JVM because everything is immutable — there are no mutable objects to special-case, and there is no pointer identity. + +```clojure +;; = is deep value equality via IEquiv +(= {:a 1} {:a 1}) ;; => true +(= [1 2 3] [1 2 3]) ;; => true +(= (->User "Ada" 30 "a@b") + (->User "Ada" 30 "a@b")) ;; => true + +;; Cross-type sequential equality +(= [1 2 3] '(1 2 3)) ;; => true (both sequential, same elements) + +;; == is numeric equality across types +(== 1 1.0) ;; => true +(== 1 2) ;; => false +``` + +Two operators: + +**`=`** — value equality. Dispatches through `IEquiv`/`-equiv`. For BEAM-native maps and lists, delegates to Erlang's structural comparison. For PersistentVector, element-by-element. For `defrecord`, auto-generated `IEquiv` compares type + all fields. Cross-type sequential equality: `(= [1 2] '(1 2))` is `true`. + +**`==`** — numeric equality across types. `(== 1 1.0)` is `true`. Throws on non-numbers. + +There is no `identical?`. BEAM has no pointer identity — all equality is value-based. This is a natural fit for EGAL: since nothing is mutable, value equality is the only equality that matters. + +`hash` is consistent with `=` — equal values produce the same hash. Dispatches through `IHash`/`-hash`. + +--- + +## Printing + +CljElixir follows ClojureScript's print model: an `IPrintWithWriter` protocol that controls how values are represented as text. + +### Two Print Families + +**Machine-readable (EDN):** `pr`, `prn`, `pr-str` — round-trippable. `(read-string (pr-str x))` gives back `x`. This is what the REPL uses. + +**Human-readable:** `print`, `println` — for display. Strings print without quotes, no escaping. + +```clojure +(pr-str "hello") ;; => "\"hello\"" +(print-str "hello") ;; => "hello" + +(pr-str {:name "Ada"}) ;; => "{:name \"Ada\"}" +(pr-str [1 2 3]) ;; => "[1 2 3]" +(pr-str '(1 2 3)) ;; => "(1 2 3)" +(pr-str #el[:ok "data"]) ;; => "#el[:ok \"data\"]" +(pr-str (->User "Ada" 30 "a@b")) +;; => "#User{:name \"Ada\", :age 30, :email \"a@b\"}" +``` + +### `IPrintWithWriter` + +All printing dispatches through the `IPrintWithWriter` protocol: + +```clojure +(defprotocol IPrintWithWriter + (-pr-writer [o writer opts])) +``` + +Extend it to control how your types print: + +```clojure +(defrecord Money [amount currency] + IPrintWithWriter + (-pr-writer [this writer opts] + (write writer (str "#Money[" amount " " currency "]")))) + +(pr-str (->Money 42.50 :USD)) +;; => "#Money[42.5 :USD]" +``` + +BEAM-native types have default implementations: maps as `{:k v}`, lists as `(1 2 3)`, tuples as `#el[:ok val]`, keywords as `:keyword`, strings as `"string"`. + +### REPL + +The Read-Eval-Print Loop uses Elixir's `Code.eval_quoted/3` to compile and execute CljElixir AST at runtime: + +1. **Read** — parse CljElixir source text into forms (s-expressions) +2. **Eval** — transform to Elixir AST, pass to `Code.eval_quoted/3` +3. **Print** — call `pr-str` on the result +4. **Loop** — carry the environment forward so `def`s persist across evaluations + +``` +clje> (assoc {:a 1} :b 2) +{:a 1, :b 2} + +clje> (defrecord Point [x y]) +Point + +clje> (->Point 3 4) +#Point{:x 3, :y 4} + +clje> (+ 1 2) +3 +``` + +No special eval infrastructure — the compiler already produces Elixir AST, and Elixir can evaluate AST at runtime natively. + +--- + +## Control Flow + +### Binding Forms (use vectors) + +```clojure +(let [x 1 + y 2] + (+ x y)) + +(for [x (list 1 2 3 4 5) + :when (> x 2)] + (* x x)) + +(doseq [[name pid] members] + (send pid :shutdown)) + +(if-let [val (get m :key)] + (process val) + :not-found) + +(when-let [val (get m :key)] + (process val)) +``` + +### `loop`/`recur` — Tail Recursion + +`loop` establishes bindings and a recursion point. `recur` jumps back to the nearest `loop` (or `defn`) with new binding values. Compiles to a tail-recursive function call — BEAM does tail call optimization natively, so no stack growth. + +```clojure +;; loop with recur +(loop [i 0 + acc []] + (if (< i 10) + (recur (inc i) (conj acc (* i i))) + acc)) + +;; recur in defn +(defn factorial [n] + (loop [i n acc 1] + (if (<= i 1) + acc + (recur (dec i) (* acc i))))) + +;; recur to defn head (no loop needed) +(defn count-down [n] + (when (> n 0) + (println n) + (recur (dec n)))) +``` + +### `with` — Sequential Pattern Matching + +`with` chains multiple pattern match bindings. Each binding matches the result of its expression. If any match fails, the non-matching value is returned immediately (short-circuit). This is not threading — each binding is independent, but later bindings can reference earlier ones. + +```clojure +;; Basic: chain fallible operations +(with [[:ok config] (load-config path) + [:ok conn] (connect config) + [:ok user] (authenticate conn creds)] + (start-session user)) +;; if (load-config path) returns [:error :not-found], +;; the whole expression returns [:error :not-found] + +;; With :else — handle the failure explicitly +(with [[:ok config] (load-config path) + [:ok conn] (connect config) + [:ok user] (authenticate conn creds)] + (start-session user) + :else + [:error :not-found] (log "config file missing") + [:error :timeout] (retry) + [:error reason] (log (str "failed: " reason))) +``` + +Without `:else`, the non-matching value passes through as the return value. With `:else`, you can pattern match on the failure to handle it explicitly. + +`with` is distinct from `if-let`: `if-let` binds a single value and branches on nil/truthiness. `with` chains N bindings and short-circuits on structural pattern mismatch. + +### Branch Forms (bare clauses) + +```clojure +(case value + [:ok x] x + [:error _] nil) + +(cond + (> x 0) :positive + (< x 0) :negative + :else :zero) + +(if (> x 0) + :positive + :non-positive) + +(when (> x 0) + (do-something) + :positive) +``` + +### Threading Macros + +`->` inserts the threaded value as the first argument. `->>` inserts as the last argument. Both rewrite CljElixir AST at compile time (not runtime macros): + +```clojure +;; Thread-first +(-> "hello" + (String/upcase) + (str " WORLD")) +;; => "HELLO WORLD" + +;; Thread-last +(->> (list 1 2 3 4 5) + (filter (fn [x] (> x 2))) + (map (fn [x] (* x x)))) +;; => (9 16 25) +``` + +### Exception Handling + +```clojure +;; Basic try/catch +(try + (risky-operation) + (catch e (str "error: " e))) + +;; Typed catch (Elixir rescue) +(try + (risky-operation) + (catch RuntimeError e (str "runtime: " e)) + (catch ArgumentError e (str "argument: " e))) + +;; Erlang-style catch (throw/exit/error) +(try + (Kernel/throw :boom) + (catch :throw val val) + (catch :exit reason reason) + (catch :error e e)) + +;; Finally (cleanup, doesn't affect return value) +(try + (open-resource) + (catch e (log e)) + (finally (close-resource))) +``` + +### Variadic Parameters + +Functions can accept variable arguments with `& rest`: + +```clojure +(defn greet [greeting & names] + (str greeting " " (Enum/join names ", "))) + +(greet "hello" "alice" "bob") +;; => "hello alice, bob" + +;; Works in fn too +(fn [x & rest] (cons x rest)) +``` + +### Destructuring + +Works in `let`, `fn`, `defn`, `for`, and `doseq`: + +```clojure +;; Map destructuring with :keys +(let [{:keys [name age]} {:name "Ada" :age 30}] + (str name " is " age)) + +;; With :as to bind the whole map +(let [{:keys [name] :as person} {:name "Ada" :age 30}] + person) + +;; String keys with :strs +(let [{:strs [name]} {"name" "Ada"}] + name) + +;; Literal key binding +(let [{x :x y :y} {:x 1 :y 2}] + (+ x y)) + +;; Sequential destructuring with & rest +(let [[a b & rest] (list 1 2 3 4 5)] + rest) ;; => (3 4 5) + +;; Nested destructuring +(let [{:keys [name] {:keys [city]} :address} + {:name "Ada" :address {:city "London"}}] + (str name " in " city)) + +;; In defn +(defn process [{:keys [name age]}] + (str name " is " age)) +``` + +### Guards + +Guards work in `receive`, `case`, and multi-clause `defn`/`fn`: + +```clojure +;; Guard in receive +(receive + [:message from body] :guard [(is-binary body)] + (handle body)) + +;; Guard in case +(case value + x :guard [(> x 0)] :positive + x :guard [(< x 0)] :negative + _ :zero) +``` + +--- + +## Concurrency + +### Process Primitives + +```clojure +(spawn (fn [] (loop-fn initial-state))) +(spawn-link (fn [] (loop-fn initial-state))) +(send pid [:message data]) + +(receive + [:join username pid] + (handle-join state username pid) + + [:message from body] :guard [(is-binary body)] + (handle-message state from body) + + :shutdown + :ok + + :after 60000 + :timeout) + +(monitor :process pid) +(link pid) +``` + +### Dynamic Vars + +```clojure +*self* ;; current process +*node* ;; current BEAM node +``` + +--- + +## User-Defined Protocols + +Protocols are defined with `defprotocol`, same as Clojure. They compile to Elixir protocols. + +```clojure +(defprotocol Describable + "Protocol for human-readable descriptions." + (describe [value])) +``` + +Extend to built-in types with `extend-type`: + +```clojure +(extend-type Integer + Describable + (describe [n] (str "the integer " n))) + +(extend-type List + Describable + (describe [l] (str "a list with " (count l) " elements"))) +``` + +Or extend across multiple types at once with `extend-protocol`: + +```clojure +(extend-protocol Describable + Integer + (describe [n] (str "the integer " n)) + + List + (describe [l] (str "a list with " (count l) " elements")) + + Any + (describe [x] (str "something: " x))) +``` + +### `reify` — Anonymous Protocol Implementations + +`reify` creates a one-off instance implementing one or more protocols, with lexical closure over the surrounding scope. It's the anonymous version of `defrecord`: + +```clojure +;; Adapter wrapping an Elixir resource +(defn wrap-ets-table [table-id] + (reify + ILookup + (-lookup + ([_ k] (ets/lookup table-id k)) + ([_ k not-found] (or (ets/lookup table-id k) not-found))) + + ICounted + (-count [_] (ets/info table-id :size)))) +``` + +--- + +## GenServer (OTP) + +```clojure +(ns MyServer (:require [clje.core :refer :all])) + +(use GenServer) + +(defn init [args] + [:ok {:count 0}]) + +(defn handle-call + ([:get _from state] + [:reply (:count state) state]) + + ([:increment _from state] + (let [new-state (update state :count inc)] + [:reply :ok new-state]))) + +(defn handle-cast + ([:reset state] + [:noreply (assoc state :count 0)])) +``` + +--- + +## Reference Example: ChatRoom + +```clojure +(ns ChatRoom (:require [clje.core :refer :all])) + +(defn loop [state] + (receive + [:join username pid] + (let [members (assoc (:members state) username pid)] + (send pid [:welcome username (count members)]) + (loop (assoc state :members members))) + + [:message from body] :guard [(is-binary body)] + (do + (doseq [[_name pid] (:members state)] + (send pid [:chat from body])) + (loop state)) + + [:leave username] + (loop (update state :members dissoc username)) + + [:kick username reason] :guard [(!= username (:owner state))] + (if-let [pid (get-in state [:members username])] + (do + (send pid [:kicked reason]) + (loop (update state :members dissoc username))) + (loop state)) + + :shutdown + (do + (doseq [[_name pid] (:members state)] + (send pid :room-closed)) + :ok) + + :after 60000 + (if (== (count (:members state)) 0) + :empty-timeout + (loop state)))) +``` + +### Usage + +```clojure +(def room (spawn (fn [] (ChatRoom/loop {:owner "alice" :members {}})))) + +;; alice joins +(send room [:join "alice" *self*]) + +;; bob joins +(send room [:join "bob" *self*]) + +;; carol joins +(send room [:join "carol" *self*]) + +;; bob says hi +(send room [:message "bob" "hey everyone"]) + +;; alice kicks bob +(send room [:kick "bob" "being rude"]) + +;; carol leaves +(send room [:leave "carol"]) +``` + +### ChatRoom with Schemas + +```clojure +(def ChatState + [:map + [:owner :string] + [:members [:map-of :string :pid]]]) + +(def ChatMessage + [:or + [:tuple [:= :join] :string :pid] + [:tuple [:= :message] :string :string] + [:tuple [:= :leave] :string] + [:tuple [:= :kick] :string :string] + [:= :shutdown]]) +``` + +--- + +## Project Structure + +CljElixir follows the ClojureScript model: the compiler is written in the host language (Elixir), the runtime is written in CljElixir itself. + +``` +clj_elixir/ +├── mix.exs +├── bb.edn # Babashka runner scripts +│ +├── lib/ # Bootstrap compiler (Elixir) +│ ├── clj_elixir/ +│ │ ├── reader.ex # S-expression reader/parser +│ │ ├── analyzer.ex # AST validation +│ │ ├── transformer.ex # CljElixir AST → Elixir AST +│ │ ├── compiler.ex # Compilation pipeline orchestrator +│ │ ├── printer.ex # EDN-like printer for all BEAM types +│ │ ├── repl.ex # REPL engine (eval, bindings, history) +│ │ ├── malli.ex # Malli schema → typespec adapter +│ │ └── nrepl/ # nREPL server +│ │ ├── server.ex # TCP server +│ │ ├── handler.ex # Message handler (ops dispatch) +│ │ ├── session.ex # Session manager (GenServer + Agents) +│ │ └── bencode.ex # Bencode codec +│ └── mix/ +│ └── tasks/ +│ ├── compile.clj_elixir.ex # Mix compiler plugin +│ ├── clje.repl.ex # mix clje.repl +│ ├── clje.nrepl.ex # mix clje.nrepl [--port PORT] +│ ├── clje.eval.ex # mix clje.eval EXPR +│ ├── clje.run.ex # mix clje.run FILE +│ └── clje.build.ex # mix clje.build (compile to BEAM) +│ +├── src/ # Runtime library (CljElixir) +│ └── clje/ +│ ├── core.clje # Core functions (get-in, assoc-in, etc.) +│ └── core/ +│ ├── protocols.clje # 16 protocols + type extensions +│ └── persistent_vector.clje # VectorNode, PersistentVector, SubVector +│ +├── stubs/ # Editor completion stubs (.clj) +│ ├── Enum.clj, Map.clj, ... # Elixir module stubs +│ ├── erlang.clj, gen_tcp.clj, ... # Erlang module stubs +│ └── clje/core.clj # Core vocabulary stubs +│ +├── test/ # All ExUnit tests (.exs) +│ └── clj_elixir/ +│ ├── reader_test.exs +│ ├── transformer_test.exs +│ ├── compiler_test.exs +│ ├── phase2_test.exs # Protocols & core data ops +│ ├── phase3_test.exs # PersistentVector +│ ├── phase4_test.exs # clojurify/elixirify +│ ├── phase5_test.exs # BEAM concurrency & GenServer +│ ├── phase6_test.exs # Control flow, macros, destructuring +│ ├── phase7_test.exs # Malli schemas & type specs +│ ├── malli_test.exs # Malli unit tests +│ ├── phase8_test.exs # Printing & source maps +│ ├── repl_test.exs # REPL engine +│ └── nrepl_test.exs # nREPL server +│ +└── examples/ + ├── chat_room.clje # Single-VM actor-based chat room + ├── tcp_chat_server.clje # TCP chat server (gen_tcp) + └── tcp_chat_client.clje # TCP chat client +``` + +### Bootstrap Sequence + +The compiler builds in two passes: + +1. **Compile the compiler** — `mix compile` builds the Elixir-based reader, analyzer, transformer, and mix compiler plugin. + +2. **Compile the runtime** — The mix compiler plugin reads `.clje` files from `src/`, transforms them to Elixir AST, and feeds them to the Elixir compiler. This produces BEAM modules for `CljElixir.Core`, `CljElixir.Core.Protocols`, `CljElixir.Core.PersistentVector`, etc. + +User projects depend on the `clj_elixir` package, which provides both the compiler plugin and the precompiled runtime modules. + +### Implementation Phases + +**Phase 1: Bootstrap Compiler (Elixir)** +The compiler must support the three primitives that everything else is built on: +- Reader: parse s-expressions into CljElixir AST +- Transformer: core forms → Elixir AST + - **Primitives:** `defrecord`, `defprotocol`, `extend-type`, `extend-protocol`, `reify` + - **Core forms:** `ns`, `defmodule`, `defn`, `defn-`, `fn`, `let`, `if`, `case`, `cond`, `do`, `loop`/`recur` + - **Data:** maps `{}`, vectors `[]`, sets `#{}`, lists, tuples `#el[]` + - **Interop:** `Module/function` calls (uppercase = Elixir, lowercase = Erlang) + - **Naming:** hyphen-to-underscore conversion +- Mix compiler plugin: `.clje` → Elixir AST → BEAM bytecode +- Verify: compile and call `.clje` modules from Elixir + +**Phase 2: Core Protocols (CljElixir)** +Written in CljElixir using the Phase 1 compiler: +- All core protocols: `ILookup`, `IAssociative`, `IMap`, `ICounted`, `ISeqable`, `ISeq`, `ICollection`, `IIndexed`, `IFn`, `IMeta`, `IWithMeta`, `IStack`, `IMapEntry`, `IKVReduce`, `IHash`, `IEquiv` +- Extend protocols to BEAM built-in types (Map, List, Tuple, etc.) +- Core functions: `get`, `assoc`, `dissoc`, `update`, `count`, `first`, `rest`, `seq`, `conj`, `into`, `keys`, `vals`, `merge`, `select-keys`, `reduce`, `map`, `filter` +- Keyword-as-function dispatch + +**Phase 3: PersistentVector (CljElixir)** +Built with `defrecord` + protocols, ported from ClojureScript: +- `PersistentVector` (bit-partitioned trie) — the one data structure BEAM doesn't have +- `SubVector` for efficient subvec +- Benchmark against Erlang tuples and lists for indexed-access workloads + +**Phase 4: Domain Tools (CljElixir)** +- `clojurify` / `elixirify` protocols and functions +- `tuple` function (n-ary, apply-compatible) + +**Phase 5: BEAM Concurrency (CljElixir)** +- `receive` with pattern matching, guards (`:guard`), `:after` +- `spawn`, `spawn-link`, `send` +- `*self*`, `*node*` +- GenServer integration via `(use GenServer)` + +**Phase 6: Control Flow and Macros (CljElixir)** +- `with` (sequential pattern matching with `:else`) +- `for`, `doseq`, `if-let`, `when-let`, `if-some`, `when-some` +- `->` and `->>` threading macros +- `#()` anonymous function shorthand +- Destructuring in `let`, `fn`, `defn`, `for`, `doseq` +- `defmacro` with quasiquote, unquote, splice-unquote, auto-gensym + +**Phase 7: Malli Schema Adapter (CljElixir)** +- Malli schema definitions as data +- `m/=>` function schema annotations +- Schema → Elixir typespec generation for Dialyzer + +**Phase 8: REPL and Ecosystem Integration** +- REPL engine (`CljElixir.REPL`): eval with binding persistence, history, namespace tracking, balanced-input detection + - `mix clje.repl` — interactive REPL with multi-line input, `:help`/`:quit`/`:history` commands +- Printer (`CljElixir.Printer`): EDN-like repr for all BEAM types (maps, lists, tuples, sets, PIDs, etc.) + - Transformer builtins: `pr-str`, `pr`, `prn`, `print-str` + - `IPrintWithWriter` protocol for user-extensible printing +- nREPL server — TCP-based, Bencode protocol, `.nrepl-port` file + - `mix clje.nrepl [--port PORT]` to launch + - Ops: `clone`, `close`, `eval`, `describe`, `ls-sessions`, `load-file`, `interrupt`, `completions` + - Session isolation via GenServer + per-session Agents + - Stdout capture via `StringIO` + `Process.group_leader` swap +- Source-mapped metadata: `elixir_meta/1` propagates `%{line: L, col: C}` through transformer to Elixir AST +- Mix tasks: `mix clje.eval EXPR`, `mix clje.run FILE`, `mix clje.build` + +--- + +## Open Design Questions + +1. **Lazy sequences.** Elixir has `Stream` for laziness. How much of Clojure's lazy-seq model do we port? BEAM's process model often replaces what laziness does in Clojure. + +2. **PersistentVector performance on BEAM.** The bit-partitioned trie is implemented but not yet benchmarked. BEAM's memory model and per-process GC may affect trie node allocation patterns differently than JVM/JS. Needs benchmarking against Erlang tuples and lists for indexed-access workloads to confirm the tradeoff is worth it. + +3. **Full Malli port.** The current adapter generates Elixir typespecs from Malli schemas for Dialyzer. Full Malli (validation, coercion, generation) and clojure.spec support may come later. + +4. **Vector-as-function.** `([1 2 3] 0)` doesn't work yet — needs the transformer to dispatch non-function call heads through IFn. diff --git a/stubs/Access.clj b/stubs/Access.clj new file mode 100644 index 0000000..0f8feea --- /dev/null +++ b/stubs/Access.clj @@ -0,0 +1,75 @@ +(ns Access + "Elixir Access module — nested data access and update. + + In CljElixir: Provides access functions used with get-in, update-in, etc. + Access functions create composable paths for nested data manipulation.") + +(defn key + "Accesses a key in a map. Returns `default` if key is missing. + (get-in data [(Access/key :user) (Access/key :name)])" + ([key]) + ([key default])) + +(defn key! + "Accesses a key in a map. Raises if key is missing." + [key]) + +(defn elem + "Accesses an element in a tuple by index. + (get-in data [(Access/elem 0)])" + [index]) + +(defn at + "Accesses an element in a list by index. + (get-in data [(Access/at 0)])" + ([index]) + ([index default])) + +(defn at! + "Accesses a list element by index. Raises on out of bounds." + [index]) + +(defn all + "Accesses all elements in a list. + (get-in data [(Access/all)])" + []) + +(defn filter + "Filters elements in a list. + (update-in data [(Access/filter (fn [x] (> x 2)))] inc)" + [fun]) + +(defn find + "Finds the first element matching `fun`. + (get-in data [(Access/find (fn [x] (> x 2)))])" + [fun]) + +(defn slice + "Accesses a slice of a list. + (get-in data [(Access/slice 1 3)])" + [range-or-index]) + +(defn pop + "Pops a key from a map or index from a list." + [key]) + +(defn fetch + "Fetches a value with {:ok value} or :error semantics." + [container key]) + +(defn fetch! + "Fetches a value. Raises on missing key." + [container key]) + +(defn get + "Gets a value from a container with optional default." + ([container key]) + ([container key default])) + +(defn get-and-update + "Gets and updates a key in one operation." + [container key fun]) + +(defn get-and-update! + "Gets and updates. Raises if container doesn't implement Access." + [container key fun]) diff --git a/stubs/Agent.clj b/stubs/Agent.clj new file mode 100644 index 0000000..4786a97 --- /dev/null +++ b/stubs/Agent.clj @@ -0,0 +1,60 @@ +(ns Agent + "Elixir Agent module — simple state wrapper around a process. + + In CljElixir: (Agent/start-link (fn [] initial-state)), etc. + Agents are a simpler alternative to GenServer for pure state management.") + +(defn start + "Starts an Agent without linking. Returns {:ok pid}. + (Agent/start (fn [] {:count 0}))" + ([fun]) + ([fun opts]) + ([module fun args]) + ([module fun args opts])) + +(defn start-link + "Starts an Agent linked to the current process. Returns {:ok pid}. + (Agent/start-link (fn [] {:count 0})) + (Agent/start-link (fn [] {:count 0}) :name :my-agent)" + ([fun]) + ([fun opts]) + ([module fun args]) + ([module fun args opts])) + +(defn get + "Gets the agent state by applying `fun`. Blocks until result. + (Agent/get pid (fn [state] (:count state))) ;=> 0 + (Agent/get pid (fn [state] state) 5000) ;=> with 5s timeout" + ([agent fun]) + ([agent fun timeout]) + ([agent module fun args]) + ([agent module fun args timeout])) + +(defn update + "Updates the agent state by applying `fun`. Returns :ok. + (Agent/update pid (fn [state] (update state :count inc))) ;=> :ok" + ([agent fun]) + ([agent fun timeout]) + ([agent module fun args]) + ([agent module fun args timeout])) + +(defn get-and-update + "Gets and updates in one operation. `fun` returns {get_value, new_state}. + (Agent/get-and-update pid (fn [state] {(:count state) (update state :count inc)}))" + ([agent fun]) + ([agent fun timeout]) + ([agent module fun args]) + ([agent module fun args timeout])) + +(defn cast + "Async update. Returns :ok immediately. + (Agent/cast pid (fn [state] (assoc state :key \"val\"))) ;=> :ok" + ([agent fun]) + ([agent module fun args])) + +(defn stop + "Stops the agent. + (Agent/stop pid) ;=> :ok" + ([agent]) + ([agent reason]) + ([agent reason timeout])) diff --git a/stubs/Application.clj b/stubs/Application.clj new file mode 100644 index 0000000..a565914 --- /dev/null +++ b/stubs/Application.clj @@ -0,0 +1,104 @@ +(ns Application + "Elixir Application module — OTP application management. + + In CljElixir: (Application/get-env :my-app :key), etc. + Applications are the unit of deployment on the BEAM.") + +(defn get-env + "Gets application environment value. + (Application/get-env :my-app :key) ;=> value or nil + (Application/get-env :my-app :key :default) ;=> value or :default" + ([app key]) + ([app key default])) + +(defn fetch-env + "Gets app env. Returns {:ok value} or :error. + (Application/fetch-env :my-app :key) ;=> {:ok value}" + [app key]) + +(defn fetch-env! + "Gets app env. Raises if missing." + [app key]) + +(defn get-all-env + "Returns all environment for an application. + (Application/get-all-env :my-app) ;=> [[:key1 val1] [:key2 val2]]" + [app]) + +(defn put-env + "Sets an application environment value. + (Application/put-env :my-app :key \"value\")" + ([app key value]) + ([app key value opts])) + +(defn put-all-env + "Sets multiple app env values." + ([app config]) + ([app config opts])) + +(defn delete-env + "Deletes an application environment value." + [app key]) + +(defn start + "Starts an application and its dependencies. + (Application/start :my-app) ;=> :ok + (Application/start :my-app :permanent) ;=> with restart type" + ([app]) + ([app type])) + +(defn stop + "Stops an application. + (Application/stop :my-app) ;=> :ok" + [app]) + +(defn ensure-started + "Ensures an application is started. No-op if already running. + (Application/ensure-started :logger) ;=> :ok" + ([app]) + ([app type])) + +(defn ensure-all-started + "Starts an application and all dependencies. + (Application/ensure-all-started :my-app) ;=> {:ok [:dep1 :dep2 :my-app]}" + ([app]) + ([app type])) + +(defn started-applications + "Returns a list of all started applications. + (Application/started-applications) ;=> [[:kernel \"...\" '1.0'] ...]" + ([]) + ([timeout])) + +(defn loaded-applications + "Returns a list of all loaded applications." + ([]) + ([timeout])) + +(defn which-applications + "Returns running applications. + (Application/which-applications) ;=> [...]" + ([]) + ([timeout])) + +(defn spec + "Returns the application specification. + (Application/spec :my-app) ;=> full spec + (Application/spec :my-app :vsn) ;=> version" + ([app]) + ([app key])) + +(defn app-dir + "Returns the application directory. + (Application/app-dir :my-app) ;=> \"/path/to/my_app\"" + ([app]) + ([app path])) + +(defn compile-env + "Reads compile-time environment values." + ([app key]) + ([app key default])) + +(defn compile-env! + "Reads compile-time environment. Raises if missing." + [app key]) diff --git a/stubs/Atom.clj b/stubs/Atom.clj new file mode 100644 index 0000000..b63e46e --- /dev/null +++ b/stubs/Atom.clj @@ -0,0 +1,16 @@ +(ns Atom + "Elixir Atom module — atom operations. + + In CljElixir: (Atom/to-string :hello), etc. + Atoms are constants whose value is their name.") + +(defn to-string + "Converts an atom to a string. + (Atom/to-string :hello) ;=> \"hello\" + (Atom/to-string :Elixir.MyModule) ;=> \"Elixir.MyModule\"" + [atom]) + +(defn to-charlist + "Converts an atom to a charlist. + (Atom/to-charlist :hello) ;=> 'hello'" + [atom]) diff --git a/stubs/Base.clj b/stubs/Base.clj new file mode 100644 index 0000000..7b8f68c --- /dev/null +++ b/stubs/Base.clj @@ -0,0 +1,88 @@ +(ns Base + "Elixir Base module — encoding/decoding (base16, base32, base64). + + In CljElixir: (Base/encode64 data), (Base/decode16 hex-string), etc.") + +(defn encode16 + "Encodes binary to base-16 (hex) string. + (Base/encode16 \"hello\") ;=> \"68656C6C6F\" + (Base/encode16 \"hello\" :case :lower) ;=> \"68656c6c6f\"" + ([data]) + ([data opts])) + +(defn decode16 + "Decodes a base-16 string. Returns {:ok binary} or :error. + (Base/decode16 \"68656C6C6F\") ;=> {:ok \"hello\"}" + ([string]) + ([string opts])) + +(defn decode16! + "Decodes base-16. Raises on error. + (Base/decode16! \"68656C6C6F\") ;=> \"hello\"" + ([string]) + ([string opts])) + +(defn encode32 + "Encodes binary to base-32 string. + (Base/encode32 \"hello\") ;=> \"NBSWY3DP\"" + ([data]) + ([data opts])) + +(defn decode32 + "Decodes a base-32 string. Returns {:ok binary} or :error." + ([string]) + ([string opts])) + +(defn decode32! + "Decodes base-32. Raises on error." + ([string]) + ([string opts])) + +(defn encode64 + "Encodes binary to base-64 string. + (Base/encode64 \"hello\") ;=> \"aGVsbG8=\"" + ([data]) + ([data opts])) + +(defn decode64 + "Decodes a base-64 string. Returns {:ok binary} or :error. + (Base/decode64 \"aGVsbG8=\") ;=> {:ok \"hello\"}" + ([string]) + ([string opts])) + +(defn decode64! + "Decodes base-64. Raises on error. + (Base/decode64! \"aGVsbG8=\") ;=> \"hello\"" + ([string]) + ([string opts])) + +(defn url-encode64 + "Encodes binary to URL-safe base-64. + (Base/url-encode64 data) ;=> URL-safe base64 string" + ([data]) + ([data opts])) + +(defn url-decode64 + "Decodes URL-safe base-64. Returns {:ok binary} or :error." + ([string]) + ([string opts])) + +(defn url-decode64! + "Decodes URL-safe base-64. Raises on error." + ([string]) + ([string opts])) + +(defn hex-encode32 + "Encodes binary to hex-base-32 (Extended Hex)." + ([data]) + ([data opts])) + +(defn hex-decode32 + "Decodes hex-base-32." + ([string]) + ([string opts])) + +(defn hex-decode32! + "Decodes hex-base-32. Raises on error." + ([string]) + ([string opts])) diff --git a/stubs/Code.clj b/stubs/Code.clj new file mode 100644 index 0000000..bb446d9 --- /dev/null +++ b/stubs/Code.clj @@ -0,0 +1,133 @@ +(ns Code + "Elixir Code module — code loading, compilation, and evaluation. + + In CljElixir: (Code/eval-string \"1 + 2\"), (Code/compile-file \"mod.ex\"), etc.") + +(defn eval-string + "Evaluates Elixir code from a string. Returns {result, binding}. + (Code/eval-string \"1 + 2\") ;=> {3 []}" + ([string]) + ([string binding]) + ([string binding opts])) + +(defn eval-quoted + "Evaluates a quoted Elixir AST. + (Code/eval-quoted ast binding)" + ([quoted]) + ([quoted binding]) + ([quoted binding opts])) + +(defn compile-string + "Compiles Elixir code from a string." + ([string]) + ([string file])) + +(defn compile-file + "Compiles an Elixir source file." + ([file]) + ([file relative-to])) + +(defn compile-quoted + "Compiles a quoted AST." + ([quoted]) + ([quoted file])) + +(defn require-file + "Requires a file, compiling it if needed. No-op if already loaded." + ([file]) + ([file relative-to])) + +(defn ensure-loaded + "Ensures the module is loaded. Returns {:module module} or {:error reason}. + (Code/ensure-loaded Enum) ;=> {:module Enum}" + [module]) + +(defn ensure-loaded! + "Ensures the module is loaded. Raises on error." + [module]) + +(defn ensure-compiled + "Ensures the module is compiled. Returns {:module module} or {:error reason}." + [module]) + +(defn ensure-compiled! + "Ensures the module is compiled. Raises on error." + [module]) + +(defn loaded? + "Returns true if the module is loaded. + (Code/loaded? Enum) ;=> true" + [module]) + +(defn available? + "Returns true if the module is available." + [module]) + +(defn fetch-docs + "Returns documentation for a module." + [module]) + +(defn get-docs + "Returns docs for a module, function, or callback." + ([module spec])) + +(defn string-to-quoted + "Parses an Elixir string to AST. Returns {:ok ast} or {:error reason}. + (Code/string-to-quoted \"1 + 2\") ;=> {:ok {:+ [line: 1] [1 2]}}" + ([string]) + ([string opts])) + +(defn string-to-quoted! + "Parses to AST. Raises on error." + ([string]) + ([string opts])) + +(defn quoted-to-algebra + "Converts quoted AST to formatted algebra document." + ([quoted]) + ([quoted opts])) + +(defn format-string! + "Formats Elixir code string. + (Code/format-string! \"1+2\") ;=> \"1 + 2\"" + ([string]) + ([string opts])) + +(defn format-file! + "Formats an Elixir source file." + ([file]) + ([file opts])) + +(defn purge + "Purges a module (removes old code)." + [module]) + +(defn delete + "Deletes a module from the VM." + [module]) + +(defn compiler-options + "Gets or sets compiler options. + (Code/compiler-options) ;=> current options" + ([]) + ([opts])) + +(defn put-compiler-option + "Sets a single compiler option." + [key value]) + +(defn unrequire-files + "Un-requires files so they can be required again." + [files]) + +(defn required-files + "Returns list of required files." + []) + +(defn append-path + "Appends a path to the code path." + [path]) + +(defn prepend-path + "Prepends a path to the code path." + [path]) diff --git a/stubs/Date.clj b/stubs/Date.clj new file mode 100644 index 0000000..2355c72 --- /dev/null +++ b/stubs/Date.clj @@ -0,0 +1,161 @@ +(ns Date + "Elixir Date module — calendar date operations. + + In CljElixir: (Date/utc-today), (Date/new 2024 3 9), etc.") + +(defn utc-today + "Returns today's date in UTC. + (Date/utc-today) ;=> ~D[2024-03-09]" + ([]) + ([calendar])) + +(defn new + "Creates a new date. + (Date/new 2024 3 9) ;=> {:ok ~D[2024-03-09]}" + ([year month day]) + ([year month day calendar])) + +(defn new! + "Creates a new date. Raises on error. + (Date/new! 2024 3 9) ;=> ~D[2024-03-09]" + ([year month day]) + ([year month day calendar])) + +(defn from-iso8601 + "Parses ISO 8601 date. Returns {:ok date} or {:error reason}. + (Date/from-iso8601 \"2024-03-09\") ;=> {:ok ~D[2024-03-09]}" + ([string]) + ([string calendar])) + +(defn from-iso8601! + "Parses ISO 8601. Raises on error." + ([string]) + ([string calendar])) + +(defn to-iso8601 + "Converts to ISO 8601 string. + (Date/to-iso8601 date) ;=> \"2024-03-09\"" + ([date]) + ([date format])) + +(defn to-string + "Converts to string. + (Date/to-string date) ;=> \"2024-03-09\"" + [date]) + +(defn add + "Adds days to a date. + (Date/add date 7) ;=> one week later + (Date/add date 1 :month)" + ([date days]) + ([date amount unit])) + +(defn diff + "Returns the difference in days between two dates. + (Date/diff date1 date2) ;=> 7" + [date1 date2]) + +(defn day-of-week + "Returns the day of the week (1=Monday, 7=Sunday). + (Date/day-of-week date) ;=> 6 (Saturday)" + ([date]) + ([date starting-on])) + +(defn day-of-year + "Returns the day of the year (1-366). + (Date/day-of-year date) ;=> 69" + [date]) + +(defn day-of-era + "Returns the day of the era and era number." + [date]) + +(defn days-in-month + "Returns the number of days in the month. + (Date/days-in-month date) ;=> 31" + [date]) + +(defn months-in-year + "Returns the number of months in the year." + [date]) + +(defn quarter-of-year + "Returns the quarter (1-4). + (Date/quarter-of-year date) ;=> 1" + [date]) + +(defn year-of-era + "Returns the year of the era." + [date]) + +(defn leap-year? + "Returns true if the date's year is a leap year. + (Date/leap-year? date) ;=> true" + [date]) + +(defn beginning-of-month + "Returns the first day of the month. + (Date/beginning-of-month date)" + [date]) + +(defn end-of-month + "Returns the last day of the month. + (Date/end-of-month date)" + [date]) + +(defn beginning-of-week + "Returns the first day of the week." + ([date]) + ([date starting-on])) + +(defn end-of-week + "Returns the last day of the week." + ([date]) + ([date starting-on])) + +(defn range + "Creates a date range. + (Date/range date1 date2) ;=> %Date.Range{}" + ([first last]) + ([first last step])) + +(defn compare + "Compares two dates. Returns :lt, :eq, or :gt." + [date1 date2]) + +(defn before? + "Returns true if `date1` is before `date2`." + [date1 date2]) + +(defn after? + "Returns true if `date1` is after `date2`." + [date1 date2]) + +(defn shift + "Shifts date by a duration. + (Date/shift date :month 1 :day -3)" + [date duration]) + +(defn convert + "Converts a date to a different calendar." + ([date calendar])) + +(defn convert! + "Converts to a different calendar. Raises on error." + ([date calendar])) + +(defn from-erl + "Converts Erlang date tuple to Date. + (Date/from-erl {2024 3 9}) ;=> {:ok ~D[2024-03-09]}" + ([tuple]) + ([tuple calendar])) + +(defn from-erl! + "Converts Erlang date tuple. Raises on error." + ([tuple]) + ([tuple calendar])) + +(defn to-erl + "Converts Date to Erlang date tuple. + (Date/to-erl date) ;=> {2024 3 9}" + [date]) diff --git a/stubs/DateTime.clj b/stubs/DateTime.clj new file mode 100644 index 0000000..18bec67 --- /dev/null +++ b/stubs/DateTime.clj @@ -0,0 +1,155 @@ +(ns DateTime + "Elixir DateTime module — date and time with timezone. + + In CljElixir: (DateTime/utc-now), (DateTime/to-iso8601 dt), etc.") + +(defn utc-now + "Returns the current UTC datetime. + (DateTime/utc-now) ;=> ~U[2024-03-09 12:00:00Z] + (DateTime/utc-now Calendar.ISO)" + ([]) + ([calendar])) + +(defn now + "Returns the current datetime for a timezone. + (DateTime/now \"Etc/UTC\") ;=> {:ok datetime}" + ([timezone]) + ([timezone calendar])) + +(defn now! + "Returns the current datetime. Raises on error. + (DateTime/now! \"Etc/UTC\")" + ([timezone]) + ([timezone calendar])) + +(defn new + "Creates a new DateTime. + (DateTime/new 2024 3 9 12 0 0)" + ([date time]) + ([date time timezone]) + ([date time timezone database])) + +(defn new! + "Creates a new DateTime. Raises on error." + ([date time]) + ([date time timezone]) + ([date time timezone database])) + +(defn from-unix + "Converts Unix timestamp to DateTime. + (DateTime/from-unix 1709985600) ;=> {:ok datetime} + (DateTime/from-unix 1709985600000 :millisecond)" + ([integer]) + ([integer unit]) + ([integer unit calendar])) + +(defn from-unix! + "Converts Unix timestamp. Raises on error. + (DateTime/from-unix! 1709985600)" + ([integer]) + ([integer unit]) + ([integer unit calendar])) + +(defn to-unix + "Converts DateTime to Unix timestamp. + (DateTime/to-unix datetime) ;=> 1709985600 + (DateTime/to-unix datetime :millisecond)" + ([datetime]) + ([datetime unit])) + +(defn from-iso8601 + "Parses ISO 8601 string. Returns {:ok datetime utc-offset}. + (DateTime/from-iso8601 \"2024-03-09T12:00:00Z\") ;=> {:ok datetime 0}" + ([string]) + ([string calendar-or-format])) + +(defn from-iso8601! + "Parses ISO 8601. Raises on error." + ([string]) + ([string calendar-or-format])) + +(defn to-iso8601 + "Converts to ISO 8601 string. + (DateTime/to-iso8601 datetime) ;=> \"2024-03-09T12:00:00Z\"" + ([datetime]) + ([datetime format]) + ([datetime format offset])) + +(defn to-string + "Converts to human-readable string. + (DateTime/to-string datetime) ;=> \"2024-03-09 12:00:00Z\"" + [datetime]) + +(defn to-date + "Extracts the Date part. + (DateTime/to-date datetime) ;=> ~D[2024-03-09]" + [datetime]) + +(defn to-time + "Extracts the Time part. + (DateTime/to-time datetime) ;=> ~T[12:00:00]" + [datetime]) + +(defn to-naive + "Converts to NaiveDateTime (drops timezone info). + (DateTime/to-naive datetime) ;=> ~N[2024-03-09 12:00:00]" + [datetime]) + +(defn from-naive + "Converts NaiveDateTime to DateTime with timezone. + (DateTime/from-naive naive \"Etc/UTC\") ;=> {:ok datetime}" + ([naive-datetime timezone]) + ([naive-datetime timezone database])) + +(defn from-naive! + "Converts NaiveDateTime. Raises on error." + ([naive-datetime timezone]) + ([naive-datetime timezone database])) + +(defn add + "Adds `amount` of time to a datetime. + (DateTime/add datetime 3600) ;=> +1 hour + (DateTime/add datetime 1 :hour)" + ([datetime amount]) + ([datetime amount unit])) + +(defn diff + "Returns the difference between two datetimes. + (DateTime/diff dt1 dt2) ;=> seconds + (DateTime/diff dt1 dt2 :hour) ;=> hours" + ([datetime1 datetime2]) + ([datetime1 datetime2 unit])) + +(defn shift-zone + "Shifts datetime to a different timezone. + (DateTime/shift-zone datetime \"America/New_York\")" + ([datetime timezone]) + ([datetime timezone database])) + +(defn shift-zone! + "Shifts timezone. Raises on error." + ([datetime timezone]) + ([datetime timezone database])) + +(defn truncate + "Truncates datetime to given precision. + (DateTime/truncate datetime :second)" + [datetime precision]) + +(defn compare + "Compares two datetimes. Returns :lt, :eq, or :gt. + (DateTime/compare dt1 dt2) ;=> :lt" + [datetime1 datetime2]) + +(defn before? + "Returns true if `datetime1` is before `datetime2`." + [datetime1 datetime2]) + +(defn after? + "Returns true if `datetime1` is after `datetime2`." + [datetime1 datetime2]) + +(defn shift + "Shifts datetime by a duration. + (DateTime/shift datetime :hour 1 :minute 30)" + [datetime duration]) diff --git a/stubs/Enum.clj b/stubs/Enum.clj new file mode 100644 index 0000000..418fc4d --- /dev/null +++ b/stubs/Enum.clj @@ -0,0 +1,378 @@ +(ns Enum + "Elixir Enum module — eager operations on enumerables. + + In CljElixir: (Enum/map coll f), (Enum/reduce coll acc f), etc. + Works on lists, maps, ranges, and any Enumerable.") + +;; --- Mapping & Transformation --- + +(defn map + "Returns a list with `f` applied to each element. + (Enum/map [1 2 3] (fn [x] (* x 2))) ;=> [2 4 6]" + [enumerable f]) + +(defn map-every + "Applies `f` to every `nth` element, starting with the first. + (Enum/map-every [1 2 3 4 5] 2 (fn [x] (* x 2))) ;=> [2 2 6 4 10]" + [enumerable nth f]) + +(defn flat-map + "Maps `f` over `enumerable` and flattens the result. + (Enum/flat-map [[1 2] [3 4]] (fn [x] x)) ;=> [1 2 3 4]" + [enumerable f]) + +(defn map-reduce + "Maps and reduces in one pass. Returns {mapped_list, acc}. + (Enum/map-reduce [1 2 3] 0 (fn [x acc] {(* x 2) (+ acc x)})) ;=> {[2 4 6] 6}" + [enumerable acc f]) + +(defn scan + "Returns a list of successive reduced values from the left. + (Enum/scan [1 2 3 4] (fn [x acc] (+ x acc))) ;=> [1 3 6 10]" + ([enumerable f]) + ([enumerable acc f])) + +;; --- Filtering --- + +(defn filter + "Returns elements for which `f` returns a truthy value. + (Enum/filter [1 2 3 4] (fn [x] (> x 2))) ;=> [3 4]" + [enumerable f]) + +(defn reject + "Returns elements for which `f` returns a falsy value. + (Enum/reject [1 2 3 4] (fn [x] (> x 2))) ;=> [1 2]" + [enumerable f]) + +(defn uniq + "Returns unique elements, preserving order. + (Enum/uniq [1 2 1 3 2]) ;=> [1 2 3]" + [enumerable]) + +(defn uniq-by + "Returns elements unique by the result of `f`. + (Enum/uniq-by [{:x 1 :y 2} {:x 1 :y 3}] (fn [m] (:x m))) ;=> [{:x 1 :y 2}]" + [enumerable f]) + +(defn dedup + "Removes consecutive duplicate elements. + (Enum/dedup [1 1 2 2 3 1]) ;=> [1 2 3 1]" + [enumerable]) + +(defn dedup-by + "Removes consecutive elements where `f` returns the same value." + [enumerable f]) + +;; --- Reducing --- + +(defn reduce + "Invokes `f` for each element with the accumulator. + (Enum/reduce [1 2 3] 0 (fn [x acc] (+ acc x))) ;=> 6 + Note: Elixir callback order is (element, acc) not (acc, element)." + ([enumerable f]) + ([enumerable acc f])) + +(defn reduce-while + "Reduces while `f` returns {:cont, acc}. Halts on {:halt, acc}. + (Enum/reduce-while [1 2 3 4] 0 (fn [x acc] (if (< acc 5) {:cont (+ acc x)} {:halt acc})))" + [enumerable acc f]) + +(defn map-join + "Maps `f` over `enumerable` then joins results with `joiner`. + (Enum/map-join [1 2 3] \", \" (fn [x] (str x))) ;=> \"1, 2, 3\"" + ([enumerable f]) + ([enumerable joiner f])) + +;; --- Sorting --- + +(defn sort + "Sorts the enumerable. Uses Erlang term ordering or a custom comparator. + (Enum/sort [3 1 2]) ;=> [1 2 3] + (Enum/sort [3 1 2] (fn [a b] (> a b))) ;=> [3 2 1]" + ([enumerable]) + ([enumerable sorter])) + +(defn sort-by + "Sorts by the result of applying `mapper` to each element. + (Enum/sort-by [{:name \"b\"} {:name \"a\"}] (fn [x] (:name x))) ;=> [{:name \"a\"} {:name \"b\"}]" + ([enumerable mapper]) + ([enumerable mapper sorter])) + +;; --- Grouping & Partitioning --- + +(defn group-by + "Groups elements by the result of `f`. + (Enum/group-by [\"ant\" \"bee\" \"ape\"] (fn [s] (String/at s 0))) + ;=> %{\"a\" => [\"ant\" \"ape\"], \"b\" => [\"bee\"]}" + [enumerable f]) + +(defn chunk-by + "Splits into chunks where consecutive elements return the same value for `f`. + (Enum/chunk-by [1 1 2 2 3] (fn [x] x)) ;=> [[1 1] [2 2] [3]]" + [enumerable f]) + +(defn chunk-every + "Splits into chunks of `count` elements. + (Enum/chunk-every [1 2 3 4 5] 2) ;=> [[1 2] [3 4] [5]]" + ([enumerable count]) + ([enumerable count step]) + ([enumerable count step leftover])) + +(defn frequencies + "Returns a map with keys as unique elements and values as counts. + (Enum/frequencies [\"a\" \"b\" \"a\" \"c\" \"b\" \"a\"]) ;=> %{\"a\" => 3, \"b\" => 2, \"c\" => 1}" + [enumerable]) + +(defn split-with + "Splits into two lists: elements satisfying `f` and the rest. + (Enum/split-with [1 2 3 4] (fn [x] (< x 3))) ;=> {[1 2] [3 4]}" + [enumerable f]) + +(defn partition-by + "Splits when `f` returns a new value. + (Enum/partition-by [1 1 2 2 3] (fn [x] x)) ;=> [[1 1] [2 2] [3]]" + [enumerable f]) + +;; --- Lookup & Search --- + +(defn find + "Returns the first element for which `f` returns truthy. + (Enum/find [1 2 3 4] (fn [x] (> x 2))) ;=> 3" + ([enumerable f]) + ([enumerable default f])) + +(defn find-index + "Returns the index of the first element for which `f` returns truthy. + (Enum/find-index [1 2 3] (fn [x] (= x 2))) ;=> 1" + [enumerable f]) + +(defn find-value + "Returns the first truthy return value of `f`. + (Enum/find-value [1 2 3] (fn [x] (if (> x 2) (* x 10) nil))) ;=> 30" + ([enumerable f]) + ([enumerable default f])) + +(defn member? + "Returns true if `element` exists in the enumerable. + (Enum/member? [1 2 3] 2) ;=> true" + [enumerable element]) + +(defn any? + "Returns true if any element satisfies `f` (or if any element is truthy). + (Enum/any? [false nil true]) ;=> true + (Enum/any? [1 2 3] (fn [x] (> x 2))) ;=> true" + ([enumerable]) + ([enumerable f])) + +(defn all? + "Returns true if all elements satisfy `f` (or all are truthy). + (Enum/all? [1 2 3] (fn [x] (> x 0))) ;=> true" + ([enumerable]) + ([enumerable f])) + +(defn count + "Returns the count of elements, optionally only those satisfying `f`. + (Enum/count [1 2 3]) ;=> 3 + (Enum/count [1 2 3] (fn [x] (> x 1))) ;=> 2" + ([enumerable]) + ([enumerable f])) + +(defn empty? + "Returns true if the enumerable is empty. + (Enum/empty? []) ;=> true" + [enumerable]) + +;; --- Subsequences --- + +(defn take + "Takes the first `amount` elements. + (Enum/take [1 2 3 4 5] 3) ;=> [1 2 3]" + [enumerable amount]) + +(defn take-while + "Takes elements while `f` returns truthy. + (Enum/take-while [1 2 3 4] (fn [x] (< x 3))) ;=> [1 2]" + [enumerable f]) + +(defn take-every + "Takes every `nth` element (0-indexed). + (Enum/take-every [1 2 3 4 5 6] 2) ;=> [1 3 5]" + [enumerable nth]) + +(defn drop + "Drops the first `amount` elements. + (Enum/drop [1 2 3 4 5] 2) ;=> [3 4 5]" + [enumerable amount]) + +(defn drop-while + "Drops elements while `f` returns truthy. + (Enum/drop-while [1 2 3 4] (fn [x] (< x 3))) ;=> [3 4]" + [enumerable f]) + +(defn slice + "Returns a subset of the enumerable. + (Enum/slice [1 2 3 4 5] 1 3) ;=> [2 3 4] + (Enum/slice [1 2 3 4 5] 1..3) ;=> [2 3 4]" + ([enumerable index-range]) + ([enumerable start amount])) + +(defn reverse + "Reverses the enumerable. + (Enum/reverse [1 2 3]) ;=> [3 2 1]" + ([enumerable]) + ([enumerable tail])) + +(defn shuffle + "Returns a list with elements in random order. + (Enum/shuffle [1 2 3 4 5]) ;=> [3 1 5 2 4]" + [enumerable]) + +;; --- Aggregation --- + +(defn sum + "Returns the sum of all elements. + (Enum/sum [1 2 3]) ;=> 6" + [enumerable]) + +(defn product + "Returns the product of all elements. + (Enum/product [1 2 3 4]) ;=> 24" + [enumerable]) + +(defn min + "Returns the minimum element. + (Enum/min [3 1 2]) ;=> 1" + ([enumerable]) + ([enumerable empty-fallback])) + +(defn max + "Returns the maximum element. + (Enum/max [3 1 2]) ;=> 3" + ([enumerable]) + ([enumerable empty-fallback])) + +(defn min-by + "Returns the element for which `f` returns the smallest value. + (Enum/min-by [\"aaa\" \"b\" \"cc\"] (fn [s] (String/length s))) ;=> \"b\"" + ([enumerable f]) + ([enumerable f sorter])) + +(defn max-by + "Returns the element for which `f` returns the largest value. + (Enum/max-by [\"aaa\" \"b\" \"cc\"] (fn [s] (String/length s))) ;=> \"aaa\"" + ([enumerable f]) + ([enumerable f sorter])) + +(defn min-max + "Returns a tuple with the minimum and maximum elements. + (Enum/min-max [3 1 2]) ;=> {1 3}" + ([enumerable]) + ([enumerable empty-fallback])) + +(defn min-max-by + "Returns a tuple with the min/max elements by `f`." + ([enumerable f]) + ([enumerable f sorter-or-empty])) + +;; --- Joining & Conversion --- + +(defn join + "Joins elements into a string with an optional separator. + (Enum/join [1 2 3] \", \") ;=> \"1, 2, 3\" + (Enum/join [1 2 3]) ;=> \"123\"" + ([enumerable]) + ([enumerable joiner])) + +(defn into + "Inserts each element into a collectable. + (Enum/into [1 2 3] []) ;=> [1 2 3] + (Enum/into %{a: 1} %{b: 2}) ;=> %{a: 1, b: 2} + (Enum/into [1 2 3] [] (fn [x] (* x 2))) ;=> [2 4 6]" + ([enumerable collectable]) + ([enumerable collectable transform])) + +(defn to-list + "Converts an enumerable to a list. + (Enum/to-list (1..5)) ;=> [1 2 3 4 5]" + [enumerable]) + +(defn zip + "Zips corresponding elements from a finite collection of enumerables. + (Enum/zip [1 2 3] [:a :b :c]) ;=> [{1 :a} {2 :b} {3 :c}]" + ([enumerables]) + ([enum1 enum2])) + +(defn zip-with + "Zips with a merge function. + (Enum/zip-with [1 2 3] [4 5 6] (fn [a b] (+ a b))) ;=> [5 7 9]" + ([enumerables zip-fun]) + ([enum1 enum2 zip-fun])) + +(defn unzip + "Opposite of zip. Takes a list of two-element tuples and returns two lists. + (Enum/unzip [{1 :a} {2 :b}]) ;=> {[1 2] [:a :b]}" + [list]) + +(defn with-index + "Wraps each element in a tuple with its index. + (Enum/with-index [:a :b :c]) ;=> [{:a 0} {:b 1} {:c 2}]" + ([enumerable]) + ([enumerable fun-or-offset])) + +(defn flat-map-reduce + "Maps and reduces, emitting lists that get flattened. Returns {[flat_mapped], acc}." + [enumerable acc f]) + +;; --- Element Access --- + +(defn at + "Returns the element at `index`. Returns `default` if out of bounds. + (Enum/at [1 2 3] 1) ;=> 2 + (Enum/at [1 2 3] 5 :none) ;=> :none" + ([enumerable index]) + ([enumerable index default])) + +(defn fetch + "Returns {:ok element} or :error for index lookup. + (Enum/fetch [1 2 3] 1) ;=> {:ok 2} + (Enum/fetch [1 2 3] 5) ;=> :error" + [enumerable index]) + +(defn fetch! + "Returns the element at `index`. Raises if out of bounds. + (Enum/fetch! [1 2 3] 1) ;=> 2" + [enumerable index]) + +(defn random + "Returns a random element. + (Enum/random [1 2 3 4 5]) ;=> 3" + ([enumerable]) + ([enumerable count])) + +;; --- List Operations --- + +(defn concat + "Concatenates enumerables. + (Enum/concat [1 2] [3 4]) ;=> [1 2 3 4] + (Enum/concat [[1 2] [3 4]]) ;=> [1 2 3 4]" + ([enumerables]) + ([left right])) + +(defn intersperse + "Inserts `separator` between each element. + (Enum/intersperse [1 2 3] 0) ;=> [1 0 2 0 3]" + [enumerable separator]) + +(defn each + "Invokes `f` for each element (side effects). Returns :ok. + (Enum/each [1 2 3] (fn [x] (IO/puts x)))" + [enumerable f]) + +(defn map-intersperse + "Maps and intersperses in one pass." + [enumerable separator mapper]) + +(defn split + "Splits into two lists at `count`. + (Enum/split [1 2 3 4 5] 3) ;=> {[1 2 3] [4 5]}" + [enumerable count]) diff --git a/stubs/File.clj b/stubs/File.clj new file mode 100644 index 0000000..3658dd6 --- /dev/null +++ b/stubs/File.clj @@ -0,0 +1,223 @@ +(ns File + "Elixir File module — file system operations. + + In CljElixir: (File/read \"path\"), (File/write \"path\" content), etc. + Returns {:ok result} or {:error reason} for most operations.") + +(defn read + "Reads the contents of `path`. Returns {:ok binary} or {:error reason}. + (File/read \"myfile.txt\") ;=> {:ok \"contents\"}" + [path]) + +(defn read! + "Reads the contents of `path`. Raises on error. + (File/read! \"myfile.txt\") ;=> \"contents\"" + [path]) + +(defn write + "Writes `content` to `path`. Returns :ok or {:error reason}. + (File/write \"myfile.txt\" \"hello\") + (File/write \"myfile.txt\" \"hello\" [:append])" + ([path content]) + ([path content modes])) + +(defn write! + "Writes `content` to `path`. Raises on error. + (File/write! \"myfile.txt\" \"hello\")" + ([path content]) + ([path content modes])) + +(defn exists? + "Returns true if `path` exists. + (File/exists? \"myfile.txt\") ;=> true" + [path]) + +(defn dir? + "Returns true if `path` is a directory. + (File/dir? \"/tmp\") ;=> true" + [path]) + +(defn regular? + "Returns true if `path` is a regular file. + (File/regular? \"myfile.txt\") ;=> true" + [path]) + +(defn mkdir + "Creates a directory at `path`. + (File/mkdir \"mydir\") ;=> :ok" + [path]) + +(defn mkdir-p + "Creates a directory and all parent directories. + (File/mkdir-p \"a/b/c\") ;=> :ok" + [path]) + +(defn rm + "Removes a file at `path`. + (File/rm \"myfile.txt\") ;=> :ok" + [path]) + +(defn rm! + "Removes a file at `path`. Raises on error." + [path]) + +(defn rm-rf + "Removes files and directories recursively. + (File/rm-rf \"mydir\") ;=> {:ok [\"mydir/a\" \"mydir\"]}" + [path]) + +(defn rmdir + "Removes an empty directory. + (File/rmdir \"mydir\") ;=> :ok" + [path]) + +(defn cp + "Copies `source` to `destination`. + (File/cp \"src.txt\" \"dst.txt\") ;=> :ok" + ([source destination]) + ([source destination callback])) + +(defn cp! + "Copies. Raises on error." + ([source destination]) + ([source destination callback])) + +(defn cp-r + "Copies recursively. + (File/cp-r \"src_dir\" \"dst_dir\")" + ([source destination]) + ([source destination callback])) + +(defn cp-r! + "Copies recursively. Raises on error." + ([source destination]) + ([source destination callback])) + +(defn rename + "Renames/moves `source` to `destination`. + (File/rename \"old.txt\" \"new.txt\") ;=> :ok" + [source destination]) + +(defn rename! + "Renames/moves. Raises on error." + [source destination]) + +(defn ln-s + "Creates a symbolic link. + (File/ln-s \"target\" \"link_name\") ;=> :ok" + [existing new-link]) + +(defn ls + "Lists files in a directory. Returns {:ok [filenames]} or {:error reason}. + (File/ls \".\") ;=> {:ok [\"mix.exs\" \"lib\"]}" + ([path]) + ([])) + +(defn ls! + "Lists files. Raises on error. + (File/ls! \".\") ;=> [\"mix.exs\" \"lib\"]" + ([path]) + ([])) + +(defn stat + "Returns file info. Returns {:ok stat} or {:error reason}. + (File/stat \"myfile.txt\") ;=> {:ok %File.Stat{...}}" + ([path]) + ([path opts])) + +(defn stat! + "Returns file info. Raises on error." + ([path]) + ([path opts])) + +(defn lstat + "Like stat but doesn't follow symlinks." + ([path]) + ([path opts])) + +(defn lstat! + "Like stat! but doesn't follow symlinks." + ([path]) + ([path opts])) + +(defn cwd + "Returns the current working directory. + (File/cwd) ;=> {:ok \"/Users/ajet/repos/clje\"}" + []) + +(defn cwd! + "Returns the current working directory. Raises on error." + []) + +(defn cd + "Changes the current working directory. + (File/cd \"/tmp\") ;=> :ok" + [path]) + +(defn cd! + "Changes directory. Raises on error." + [path]) + +(defn open + "Opens a file. Returns {:ok io-device} or {:error reason}. + (File/open \"myfile.txt\" [:read :utf8]) + (File/open \"myfile.txt\" [:write :append])" + ([path]) + ([path modes])) + +(defn open! + "Opens a file. Raises on error." + ([path]) + ([path modes])) + +(defn close + "Closes a file IO device. + (File/close io-device) ;=> :ok" + [io-device]) + +(defn stream + "Returns a File.Stream for lazy reading. + (File/stream \"bigfile.txt\") ;=> %File.Stream{...} + (File/stream \"bigfile.txt\" [:read] :line) ;=> line-by-line" + ([path]) + ([path modes]) + ([path modes line-or-bytes])) + +(defn stream! + "Returns a File.Stream. Raises on error." + ([path]) + ([path modes]) + ([path modes line-or-bytes])) + +(defn touch + "Updates file timestamps, creating the file if it doesn't exist. + (File/touch \"myfile.txt\") ;=> :ok" + ([path]) + ([path time])) + +(defn touch! + "Touch. Raises on error." + ([path]) + ([path time])) + +(defn chmod + "Changes file permissions. + (File/chmod \"script.sh\" 0o755) ;=> :ok" + [path mode]) + +(defn chown + "Changes file ownership." + [path uid]) + +(defn chgrp + "Changes file group." + [path gid]) + +(defn read-link + "Reads the target of a symbolic link. + (File/read-link \"my-link\") ;=> {:ok \"target\"}" + [path]) + +(defn read-link! + "Reads symlink target. Raises on error." + [path]) diff --git a/stubs/Float.clj b/stubs/Float.clj new file mode 100644 index 0000000..f741d47 --- /dev/null +++ b/stubs/Float.clj @@ -0,0 +1,63 @@ +(ns Float + "Elixir Float module — float operations. + + In CljElixir: (Float/round 3.14159 2), (Float/parse \"3.14\"), etc.") + +(defn parse + "Parses a string into a float. Returns {float rest} or :error. + (Float/parse \"3.14\") ;=> {3.14 \"\"} + (Float/parse \"3.14abc\") ;=> {3.14 \"abc\"} + (Float/parse \"nope\") ;=> :error" + [string]) + +(defn round + "Rounds to the given number of decimal places. + (Float/round 3.14159 2) ;=> 3.14 + (Float/round 3.5) ;=> 4.0" + ([float]) + ([float precision])) + +(defn ceil + "Rounds up to given decimal precision. + (Float/ceil 3.14 1) ;=> 3.2 + (Float/ceil 3.14) ;=> 4.0" + ([float]) + ([float precision])) + +(defn floor + "Rounds down to given decimal precision. + (Float/floor 3.14 1) ;=> 3.1 + (Float/floor 3.14) ;=> 3.0" + ([float]) + ([float precision])) + +(defn to-string + "Converts float to string. + (Float/to-string 3.14) ;=> \"3.14\" + (Float/to-string 3.14 :compact) ;=> compact decimal + (Float/to-string 3.14 [:decimals 2]) ;=> \"3.14\"" + ([float]) + ([float opts])) + +(defn to-charlist + "Converts float to charlist." + ([float]) + ([float opts])) + +(defn ratio + "Returns {numerator denominator} for the given float. + (Float/ratio 0.75) ;=> {3 4}" + [float]) + +(defn min-finite + "Returns the minimum finite float value." + []) + +(defn max-finite + "Returns the maximum finite float value." + []) + +(defn pow + "Returns `base` raised to `exponent` as a float. + (Float/pow 2.0 10) ;=> 1024.0" + [base exponent]) diff --git a/stubs/GenServer.clj b/stubs/GenServer.clj new file mode 100644 index 0000000..010d1b7 --- /dev/null +++ b/stubs/GenServer.clj @@ -0,0 +1,64 @@ +(ns GenServer + "Elixir GenServer module — generic server (OTP behaviour). + + In CljElixir: (GenServer/start-link MyModule init-arg opts), etc. + GenServer is the core abstraction for stateful processes on the BEAM. + + Callbacks to implement in your module: + init/1, handle-call/3, handle-cast/2, handle-info/2, terminate/2") + +(defn start + "Starts a GenServer without linking. Returns {:ok pid} or {:error reason}. + (GenServer/start MyModule init-arg []) + (GenServer/start MyModule init-arg :name :my-server)" + ([module init-arg]) + ([module init-arg opts])) + +(defn start-link + "Starts a GenServer linked to the current process. Returns {:ok pid}. + (GenServer/start-link MyModule init-arg []) + (GenServer/start-link MyModule init-arg :name :my-server)" + ([module init-arg]) + ([module init-arg opts])) + +(defn call + "Makes a synchronous call to the server. Blocks until reply. Default timeout 5000ms. + (GenServer/call pid :get-state) ;=> server's reply + (GenServer/call pid {:set \"value\"} 10000) ;=> with 10s timeout" + ([server request]) + ([server request timeout])) + +(defn cast + "Sends an asynchronous request to the server. Returns :ok immediately. + (GenServer/cast pid {:update \"value\"}) ;=> :ok" + [server request]) + +(defn reply + "Replies to a client from within handle-call (for delayed replies). + (GenServer/reply from {:ok result}) ;=> :ok" + [client reply]) + +(defn stop + "Stops the GenServer. + (GenServer/stop pid) ;=> :ok + (GenServer/stop pid :normal) ;=> with reason + (GenServer/stop pid :normal :infinity) ;=> with timeout" + ([server]) + ([server reason]) + ([server reason timeout])) + +(defn whereis + "Returns the PID of a named GenServer, or nil. + (GenServer/whereis :my-server) ;=> #PID<0.123.0>" + [name]) + +(defn multi-call + "Calls all locally registered servers on all connected nodes." + ([name request]) + ([nodes name request]) + ([nodes name request timeout])) + +(defn abcast + "Casts to all locally registered servers on all connected nodes." + ([name request]) + ([nodes name request])) diff --git a/stubs/IO.clj b/stubs/IO.clj new file mode 100644 index 0000000..7706cfe --- /dev/null +++ b/stubs/IO.clj @@ -0,0 +1,83 @@ +(ns IO + "Elixir IO module — input/output operations. + + In CljElixir: (IO/puts msg), (IO/inspect val), etc. + Handles reading/writing to stdio, files, and IO devices.") + +(defn puts + "Writes `item` to the device followed by a newline. Returns :ok. + (IO/puts \"hello\") ;=> prints 'hello\\n', returns :ok + (IO/puts :stderr \"error!\") ;=> prints to stderr" + ([item]) + ([device item])) + +(defn write + "Writes `item` to the device without a trailing newline. + (IO/write \"hello\") ;=> prints 'hello', returns :ok" + ([item]) + ([device item])) + +(defn inspect + "Inspects the given value and prints it. Returns the value (pass-through). + (IO/inspect {:a 1}) ;=> prints '%{a: 1}', returns {:a 1} + (IO/inspect val :label \"debug\") ;=> prints 'debug: ...' + Useful for debugging — can be inserted anywhere in a pipeline." + ([item]) + ([item opts]) + ([device item opts])) + +(defn gets + "Reads a line from the IO device. Shows `prompt` and returns user input. + (IO/gets \"Enter name: \") ;=> \"Alice\\n\"" + ([prompt]) + ([device prompt])) + +(defn read + "Reads from the IO device. + (IO/read :stdio :line) ;=> reads one line + (IO/read :stdio 10) ;=> reads 10 characters" + ([device count-or-line]) + ([device count-or-line opts])) + +(defn warn + "Writes `message` to stderr followed by a newline. + (IO/warn \"deprecation warning\")" + [message]) + +(defn iodata-to-binary + "Converts iodata (a list of binaries/integers/iolists) to a single binary. + (IO/iodata-to-binary [\"hello\" \" \" \"world\"]) ;=> \"hello world\"" + [iodata]) + +(defn iodata-length + "Returns the length of iodata without converting to binary. + (IO/iodata-length [\"hello\" \" \" \"world\"]) ;=> 11" + [iodata]) + +(defn chardata-to-string + "Converts chardata to a string." + [chardata]) + +(defn getn + "Gets a number of bytes from IO device. + (IO/getn \"prompt> \" 3)" + ([prompt]) + ([prompt count]) + ([device prompt count])) + +(defn binread + "Reads `count` bytes from IO device as binary. + (IO/binread :stdio 10)" + ([device count]) + ([count])) + +(defn binwrite + "Writes binary data to IO device. + (IO/binwrite :stdio <<1 2 3>>)" + ([device iodata]) + ([iodata])) + +(defn stream + "Converts an IO device into a Stream. Useful for lazy line-by-line reading. + (IO/stream :stdio :line)" + ([device mode])) diff --git a/stubs/Integer.clj b/stubs/Integer.clj new file mode 100644 index 0000000..83be19d --- /dev/null +++ b/stubs/Integer.clj @@ -0,0 +1,80 @@ +(ns Integer + "Elixir Integer module — integer operations. + + In CljElixir: (Integer/to-string 255 16), (Integer/digits 123), etc.") + +(defn to-string + "Converts integer to string, optionally in a given base. + (Integer/to-string 123) ;=> \"123\" + (Integer/to-string 255 16) ;=> \"FF\"" + ([integer]) + ([integer base])) + +(defn to-charlist + "Converts integer to charlist. + (Integer/to-charlist 123) ;=> '123'" + ([integer]) + ([integer base])) + +(defn parse + "Parses a string into an integer. Returns {integer rest} or :error. + (Integer/parse \"123abc\") ;=> {123 \"abc\"} + (Integer/parse \"FF\" 16) ;=> {255 \"\"} + (Integer/parse \"nope\") ;=> :error" + ([string]) + ([string base])) + +(defn digits + "Returns the digits of `integer` as a list. + (Integer/digits 123) ;=> [1 2 3] + (Integer/digits 255 16) ;=> [15 15]" + ([integer]) + ([integer base])) + +(defn undigits + "Converts a list of digits back to an integer. + (Integer/undigits [1 2 3]) ;=> 123 + (Integer/undigits [15 15] 16) ;=> 255" + ([digits]) + ([digits base])) + +(defn pow + "Returns `base` raised to `exponent` (integer exponentiation). + (Integer/pow 2 10) ;=> 1024" + [base exponent]) + +(defn gcd + "Returns the greatest common divisor. + (Integer/gcd 12 8) ;=> 4" + [integer1 integer2]) + +(defn mod + "Computes modulo (always non-negative for positive divisor). + (Integer/mod 10 3) ;=> 1 + (Integer/mod -5 3) ;=> 1 (differs from rem)" + [dividend divisor]) + +(defn floor-div + "Integer division rounded towards negative infinity. + (Integer/floor-div 10 3) ;=> 3 + (Integer/floor-div -5 3) ;=> -2" + [dividend divisor]) + +(defn is-odd + "Returns true if `integer` is odd. Allowed in guards. + (Integer/is-odd 3) ;=> true" + [integer]) + +(defn is-even + "Returns true if `integer` is even. Allowed in guards. + (Integer/is-even 4) ;=> true" + [integer]) + +(defn extended-gcd + "Returns {gcd, s, t} such that gcd = s*a + t*b (Bezout's identity). + (Integer/extended-gcd 12 8) ;=> {4 1 -1}" + [a b]) + +(defn to-string-padded + "Converts integer to string with zero-padding." + [integer width]) diff --git a/stubs/Kernel.clj b/stubs/Kernel.clj new file mode 100644 index 0000000..786ec2a --- /dev/null +++ b/stubs/Kernel.clj @@ -0,0 +1,336 @@ +(ns Kernel + "Elixir Kernel module — core functions auto-imported into every module. + + In CljElixir: most Kernel functions are available as builtins (e.g., +, -, if, etc.). + Use (Kernel/function ...) for less common ones. + Note: many of these are already available as CljElixir builtins without the Kernel/ prefix.") + +;; --- Type Checking --- + +(defn is-atom + "Returns true if `term` is an atom. + (Kernel/is-atom :hello) ;=> true" + [term]) + +(defn is-binary + "Returns true if `term` is a binary (string). + (Kernel/is-binary \"hello\") ;=> true" + [term]) + +(defn is-bitstring + "Returns true if `term` is a bitstring." + [term]) + +(defn is-boolean + "Returns true if `term` is a boolean. + (Kernel/is-boolean true) ;=> true" + [term]) + +(defn is-float + "Returns true if `term` is a float. + (Kernel/is-float 1.0) ;=> true" + [term]) + +(defn is-function + "Returns true if `term` is a function, optionally with given `arity`. + (Kernel/is-function f) ;=> true + (Kernel/is-function f 2) ;=> true if f accepts 2 args" + ([term]) + ([term arity])) + +(defn is-integer + "Returns true if `term` is an integer. + (Kernel/is-integer 42) ;=> true" + [term]) + +(defn is-list + "Returns true if `term` is a list. + (Kernel/is-list [1 2 3]) ;=> true" + [term]) + +(defn is-map + "Returns true if `term` is a map. + (Kernel/is-map {:a 1}) ;=> true" + [term]) + +(defn is-map-key + "Returns true if `key` exists in `map`. Allowed in guard expressions." + [map key]) + +(defn is-nil + "Returns true if `term` is nil. + (Kernel/is-nil nil) ;=> true" + [term]) + +(defn is-number + "Returns true if `term` is a number (integer or float). + (Kernel/is-number 42) ;=> true" + [term]) + +(defn is-pid + "Returns true if `term` is a PID. + (Kernel/is-pid (Process/self)) ;=> true" + [term]) + +(defn is-port + "Returns true if `term` is a port." + [term]) + +(defn is-reference + "Returns true if `term` is a reference." + [term]) + +(defn is-tuple + "Returns true if `term` is a tuple. + (Kernel/is-tuple #el[1 2 3]) ;=> true" + [term]) + +(defn is-struct + "Returns true if `term` is a struct. + (Kernel/is-struct term) + (Kernel/is-struct term MyStruct) ;=> checks specific struct type" + ([term]) + ([term name])) + +(defn is-exception + "Returns true if `term` is an exception." + ([term]) + ([term name])) + +;; --- Arithmetic (also available as +, -, *, / builtins) --- + +(defn div + "Integer division (truncated). + (Kernel/div 10 3) ;=> 3" + [dividend divisor]) + +(defn rem + "Integer remainder (same sign as dividend). + (Kernel/rem 10 3) ;=> 1" + [dividend divisor]) + +(defn abs + "Returns the absolute value. + (Kernel/abs -5) ;=> 5" + [number]) + +(defn max + "Returns the maximum of two terms. + (Kernel/max 1 2) ;=> 2" + [first second]) + +(defn min + "Returns the minimum of two terms. + (Kernel/min 1 2) ;=> 1" + [first second]) + +(defn ceil + "Returns the smallest integer >= number. + (Kernel/ceil 1.2) ;=> 2" + [number]) + +(defn floor + "Returns the largest integer <= number. + (Kernel/floor 1.8) ;=> 1" + [number]) + +(defn round + "Rounds to the nearest integer. + (Kernel/round 1.5) ;=> 2" + [number]) + +(defn trunc + "Truncates the float to an integer. + (Kernel/trunc 1.9) ;=> 1" + [number]) + +;; --- Comparison --- + +(defn == + "Structural equality. 1 == 1.0 is true. + (Kernel/== 1 1.0) ;=> true" + [left right]) + +(defn === + "Strict equality. 1 === 1.0 is false. + (Kernel/=== 1 1.0) ;=> false" + [left right]) + +(defn != + "Not equal (structural). + (Kernel/!= 1 2) ;=> true" + [left right]) + +(defn !== + "Strict not equal. + (Kernel/!== 1 1.0) ;=> true" + [left right]) + +;; --- String & Binary --- + +(defn to-string + "Converts `term` to a string via the String.Chars protocol. + (Kernel/to-string 123) ;=> \"123\" + (Kernel/to-string :hello) ;=> \"hello\"" + [term]) + +(defn inspect + "Returns a string representation of `term` (via Inspect protocol). + (Kernel/inspect {:a 1}) ;=> \"%{a: 1}\"" + ([term]) + ([term opts])) + +(defn byte-size + "Returns the number of bytes in a binary. + (Kernel/byte-size \"hello\") ;=> 5" + [binary]) + +(defn bit-size + "Returns the number of bits in a bitstring." + [bitstring]) + +(defn binary-part + "Extracts a binary part. + (Kernel/binary-part \"hello\" 1 3) ;=> \"ell\"" + ([binary start length])) + +;; --- Process & Node --- + +(defn self + "Returns the PID of the calling process. + (Kernel/self) ;=> #PID<0.123.0>" + []) + +(defn node + "Returns the current node name, or the node for a given PID/ref. + (Kernel/node) ;=> :nonode@nohost" + ([]) + ([pid-or-ref])) + +(defn spawn + "Spawns a new process. Returns PID. + (Kernel/spawn (fn [] (IO/puts \"hello\")))" + ([fun]) + ([module fun args])) + +(defn spawn-link + "Spawns a linked process. + (Kernel/spawn-link (fn [] (IO/puts \"linked\")))" + ([fun]) + ([module fun args])) + +(defn spawn-monitor + "Spawns a monitored process. Returns {pid ref}." + ([fun]) + ([module fun args])) + +(defn send + "Sends a message. Returns the message. + (Kernel/send pid :hello)" + [dest msg]) + +(defn exit + "Sends an exit signal. + (Kernel/exit :normal)" + [reason]) + +;; --- Tuple Operations --- + +(defn elem + "Gets element at `index` from a tuple (0-based). + (Kernel/elem #el[:a :b :c] 1) ;=> :b" + [tuple index]) + +(defn put-elem + "Puts `value` at `index` in a tuple. + (Kernel/put-elem #el[:a :b :c] 1 :x) ;=> #el[:a :x :c]" + [tuple index value]) + +(defn tuple-size + "Returns the number of elements in a tuple. + (Kernel/tuple-size #el[1 2 3]) ;=> 3" + [tuple]) + +(defn make-ref + "Creates a unique reference. + (Kernel/make-ref) ;=> #Reference<0.0.0.1>" + []) + +;; --- List Operations --- + +(defn hd + "Returns the head of a list. + (Kernel/hd [1 2 3]) ;=> 1" + [list]) + +(defn tl + "Returns the tail of a list. + (Kernel/tl [1 2 3]) ;=> [2 3]" + [list]) + +(defn length + "Returns the length of a list. + (Kernel/length [1 2 3]) ;=> 3" + [list]) + +(defn in + "Membership test (for use in guards). Checks if `elem` is in `list`. + (Kernel/in x [1 2 3])" + [elem list]) + +;; --- Misc --- + +(defn apply + "Applies `fun` with `args`. + (Kernel/apply Enum :map [[1 2 3] inc])" + ([fun args]) + ([module fun args])) + +(defn function-exported? + "Returns true if `module` exports `function` with given `arity`. + (Kernel/function-exported? Enum :map 2) ;=> true" + [module function arity]) + +(defn struct + "Creates a struct from a module. + (Kernel/struct MyStruct {:field \"value\"})" + ([module]) + ([module fields])) + +(defn struct! + "Like struct/2 but raises on invalid keys." + ([module]) + ([module fields])) + +(defn raise + "Raises a RuntimeError or specific exception. + (Kernel/raise \"something went wrong\") + (Kernel/raise ArgumentError :message \"bad arg\")" + ([message]) + ([exception attrs])) + +(defn reraise + "Re-raises an exception preserving the original stacktrace." + ([message stacktrace]) + ([exception attrs stacktrace])) + +(defn throw + "Throws a value to be caught with try/catch. + (Kernel/throw :some-value)" + [value]) + +(defn tap + "Pipes `value` into `fun` for side effects, returns `value`. + (Kernel/tap {:a 1} (fn [x] (IO/inspect x))) ;=> {:a 1}" + [value fun]) + +(defn then + "Pipes `value` into `fun`, returns the result of `fun`. + (Kernel/then 5 (fn [x] (* x 2))) ;=> 10" + [value fun]) + +(defn dbg + "Debug macro. Prints the code and its result. Returns the result. + (Kernel/dbg (+ 1 2)) ;=> prints '(+ 1 2) #=> 3', returns 3" + ([code]) + ([code opts])) diff --git a/stubs/Keyword.clj b/stubs/Keyword.clj new file mode 100644 index 0000000..b390363 --- /dev/null +++ b/stubs/Keyword.clj @@ -0,0 +1,165 @@ +(ns Keyword + "Elixir Keyword module — operations on keyword lists (list of {atom, value} tuples). + + In CljElixir: (Keyword/get kw :key), (Keyword/put kw :key val), etc. + Keyword lists allow duplicate keys and preserve insertion order.") + +(defn get + "Gets the value for `key`. Returns `default` if not found. + (Keyword/get [[:a 1] [:b 2]] :a) ;=> 1" + ([keywords key]) + ([keywords key default])) + +(defn get-lazy + "Gets value for `key`, calling `fun` for default if missing." + [keywords key fun]) + +(defn fetch + "Returns {:ok value} or :error. + (Keyword/fetch [[:a 1]] :a) ;=> {:ok 1}" + [keywords key]) + +(defn fetch! + "Gets value for `key`. Raises if missing." + [keywords key]) + +(defn get-values + "Gets all values for `key` (keyword lists allow duplicates). + (Keyword/get-values [[:a 1] [:a 2] [:b 3]] :a) ;=> [1 2]" + [keywords key]) + +(defn has-key? + "Returns true if `key` exists. + (Keyword/has-key? [[:a 1]] :a) ;=> true" + [keywords key]) + +(defn keys + "Returns all keys. + (Keyword/keys [[:a 1] [:b 2]]) ;=> [:a :b]" + [keywords]) + +(defn values + "Returns all values. + (Keyword/values [[:a 1] [:b 2]]) ;=> [1 2]" + [keywords]) + +(defn put + "Puts `value` under `key`, replacing any existing. + (Keyword/put [[:a 1]] :b 2) ;=> [[:a 1] [:b 2]]" + [keywords key value]) + +(defn put-new + "Puts `value` under `key` only if `key` doesn't exist." + [keywords key value]) + +(defn put-new-lazy + "Like put-new but calls `fun` only if key is absent." + [keywords key fun]) + +(defn delete + "Deletes all entries for `key`. + (Keyword/delete [[:a 1] [:b 2] [:a 3]] :a) ;=> [[:b 2]]" + [keywords key]) + +(defn delete-first + "Deletes only the first entry for `key`." + [keywords key]) + +(defn pop + "Returns {value, rest} for `key`. + (Keyword/pop [[:a 1] [:b 2]] :a) ;=> {1 [[:b 2]]}" + ([keywords key]) + ([keywords key default])) + +(defn pop-first + "Pops only the first entry for `key`." + ([keywords key]) + ([keywords key default])) + +(defn pop-lazy + "Like pop but calls `fun` for default." + [keywords key fun]) + +(defn pop-values + "Pops all values for `key`. Returns {values, rest}." + [keywords key]) + +(defn update + "Updates `key` by applying `fun` to current value. + (Keyword/update [[:a 1]] :a (fn [v] (+ v 1))) ;=> [[:a 2]]" + ([keywords key fun]) + ([keywords key default fun])) + +(defn update! + "Updates `key`. Raises if missing." + [keywords key fun]) + +(defn replace + "Replaces value at `key` only if it exists." + [keywords key value]) + +(defn replace! + "Replaces value at `key`. Raises if missing." + [keywords key value]) + +(defn merge + "Merges two keyword lists. + (Keyword/merge [[:a 1]] [[:b 2]]) ;=> [[:a 1] [:b 2]] + (Keyword/merge kw1 kw2 (fn [k v1 v2] v2)) ;=> with resolver" + ([keywords1 keywords2]) + ([keywords1 keywords2 fun])) + +(defn split + "Splits keyword list into two based on `keys`. + (Keyword/split [[:a 1] [:b 2] [:c 3]] [:a :c]) ;=> {[[:a 1] [:c 3]] [[:b 2]]}" + [keywords keys]) + +(defn take + "Takes only the given `keys`. + (Keyword/take [[:a 1] [:b 2] [:c 3]] [:a :c]) ;=> [[:a 1] [:c 3]]" + [keywords keys]) + +(defn drop + "Drops the given `keys`." + [keywords keys]) + +(defn filter + "Filters entries where `fun` returns truthy." + [keywords fun]) + +(defn reject + "Rejects entries where `fun` returns truthy." + [keywords fun]) + +(defn map + "Maps over entries. `fun` receives {key, value}." + [keywords fun]) + +(defn new + "Creates a new keyword list. + (Keyword/new) ;=> [] + (Keyword/new [[:a 1] [:b 2]]) ;=> [[:a 1] [:b 2]]" + ([]) + ([pairs]) + ([pairs transform])) + +(defn keyword? + "Returns true if `term` is a keyword list." + [term]) + +(defn equal? + "Returns true if two keyword lists are equal." + [keywords1 keywords2]) + +(defn to-list + "Converts keyword list to a list of tuples (identity for keyword lists)." + [keywords]) + +(defn validate + "Validates keyword list against a set of allowed keys. + (Keyword/validate [[:a 1] [:b 2]] [:a :b :c]) ;=> {:ok [[:a 1] [:b 2]]}" + [keywords values]) + +(defn validate! + "Validates. Raises on invalid keys." + [keywords values]) diff --git a/stubs/List.clj b/stubs/List.clj new file mode 100644 index 0000000..0f5868b --- /dev/null +++ b/stubs/List.clj @@ -0,0 +1,171 @@ +(ns List + "Elixir List module — operations on linked lists. + + In CljElixir: (List/flatten nested), (List/to-tuple lst), etc. + Lists are the fundamental sequence type on the BEAM (singly-linked).") + +(defn first + "Returns the first element, or `default` if empty. + (List/first [1 2 3]) ;=> 1 + (List/first [] :none) ;=> :none" + ([list]) + ([list default])) + +(defn last + "Returns the last element, or `default` if empty. + (List/last [1 2 3]) ;=> 3 + (List/last [] :none) ;=> :none" + ([list]) + ([list default])) + +(defn flatten + "Flattens nested lists. + (List/flatten [[1 [2]] [3 4]]) ;=> [1 2 3 4] + (List/flatten [1 [2 [3]]] 1) ;=> [1 2 [3]] (one level only)" + ([list]) + ([list tail])) + +(defn foldl + "Left fold. Invokes `fun` for each element with accumulator. + (List/foldl [1 2 3] 0 (fn [elem acc] (+ acc elem))) ;=> 6" + [list acc fun]) + +(defn foldr + "Right fold. + (List/foldr [1 2 3] 0 (fn [elem acc] (+ acc elem))) ;=> 6" + [list acc fun]) + +(defn wrap + "Wraps a non-list value in a list. nil becomes []. Lists pass through. + (List/wrap 1) ;=> [1] + (List/wrap nil) ;=> [] + (List/wrap [1 2]) ;=> [1 2]" + [term]) + +(defn duplicate + "Creates a list with `elem` repeated `n` times. + (List/duplicate :ok 3) ;=> [:ok :ok :ok]" + [elem n]) + +(defn zip + "Zips corresponding elements from multiple lists into tuples. + (List/zip [[1 2 3] [:a :b :c]]) ;=> [{1 :a} {2 :b} {3 :c}]" + [list-of-lists]) + +(defn insert-at + "Inserts `value` at `index`. + (List/insert-at [1 2 3] 1 :a) ;=> [1 :a 2 3]" + [list index value]) + +(defn replace-at + "Replaces element at `index` with `value`. + (List/replace-at [1 2 3] 1 :a) ;=> [1 :a 3]" + [list index value]) + +(defn update-at + "Updates element at `index` by applying `fun`. + (List/update-at [1 2 3] 1 (fn [x] (* x 10))) ;=> [1 20 3]" + [list index fun]) + +(defn delete-at + "Deletes element at `index`. + (List/delete-at [1 2 3] 1) ;=> [1 3]" + [list index]) + +(defn delete + "Deletes the first occurrence of `element`. + (List/delete [1 2 1 3] 1) ;=> [2 1 3]" + [list element]) + +(defn pop-at + "Returns the element at `index` and the list without it. + (List/pop-at [1 2 3] 1) ;=> {2 [1 3]}" + ([list index]) + ([list index default])) + +(defn starts-with? + "Returns true if `list` starts with `prefix`. + (List/starts-with? [1 2 3] [1 2]) ;=> true" + [list prefix]) + +(defn myers-difference + "Returns the edit steps to transform `list1` into `list2`. + (List/myers-difference [1 2 3] [1 3 4]) ;=> [eq: [1], del: [2], eq: [3], ins: [4]]" + ([list1 list2]) + ([list1 list2 diff-script])) + +(defn to-tuple + "Converts a list to a tuple. + (List/to-tuple [1 2 3]) ;=> #el[1 2 3]" + [list]) + +(defn to-string + "Converts a charlist to a string. + (List/to-string [104 101 108 108 111]) ;=> \"hello\"" + [charlist]) + +(defn to-charlist + "Converts a list of codepoints to a charlist." + [list]) + +(defn to-integer + "Converts a charlist to integer. + (List/to-integer '123') ;=> 123" + ([charlist]) + ([charlist base])) + +(defn to-float + "Converts a charlist to float." + [charlist]) + +(defn to-atom + "Converts a charlist to atom." + [charlist]) + +(defn to-existing-atom + "Converts a charlist to an existing atom." + [charlist]) + +(defn ascii-printable? + "Returns true if all chars are ASCII printable. + (List/ascii-printable? 'hello') ;=> true" + ([list]) + ([list limit])) + +(defn improper? + "Returns true if the list is improper (doesn't end in []). + (List/improper? [1 | 2]) ;=> true" + [list]) + +(defn keyfind + "Finds a tuple in a list of tuples where element at `position` matches `key`. + (List/keyfind [{:a 1} {:b 2}] :b 0) ;=> {:b 2} + (List/keyfind [{:a 1}] :c 0 :default) ;=> :default" + ([list key position]) + ([list key position default])) + +(defn keystore + "Replaces or inserts tuple in list based on key at position." + [list position key new-tuple]) + +(defn keydelete + "Deletes tuple from list where element at position matches key." + [list key position]) + +(defn keymember? + "Returns true if any tuple has `key` at `position`. + (List/keymember? [{:a 1} {:b 2}] :a 0) ;=> true" + [list key position]) + +(defn keyreplace + "Replaces the first tuple with matching key at position." + [list key position new-tuple]) + +(defn keysort + "Sorts list of tuples by element at `position`. + (List/keysort [{:b 2} {:a 1}] 0) ;=> [{:a 1} {:b 2}]" + [list position]) + +(defn keytake + "Takes all tuples from list where element at position matches key." + [list key position]) diff --git a/stubs/Logger.clj b/stubs/Logger.clj new file mode 100644 index 0000000..5ab639d --- /dev/null +++ b/stubs/Logger.clj @@ -0,0 +1,127 @@ +(ns Logger + "Elixir Logger module — structured logging. + + In CljElixir: (Logger/info \"message\"), (Logger/debug (fn [] \"lazy\")), etc. + Log levels: :emergency :alert :critical :error :warning :notice :info :debug") + +(defn debug + "Logs a debug message. Accepts string or zero-arity fn (lazy evaluation). + (Logger/debug \"detailed info\") + (Logger/debug (fn [] (str \"user=\" user-id)))" + ([message-or-fun]) + ([message-or-fun metadata])) + +(defn info + "Logs an info message. + (Logger/info \"server started on port 4000\")" + ([message-or-fun]) + ([message-or-fun metadata])) + +(defn notice + "Logs a notice message. + (Logger/notice \"configuration changed\")" + ([message-or-fun]) + ([message-or-fun metadata])) + +(defn warning + "Logs a warning message. + (Logger/warning \"disk usage above 80%\")" + ([message-or-fun]) + ([message-or-fun metadata])) + +(defn error + "Logs an error message. + (Logger/error \"failed to connect to database\")" + ([message-or-fun]) + ([message-or-fun metadata])) + +(defn critical + "Logs a critical message. + (Logger/critical \"system overload detected\")" + ([message-or-fun]) + ([message-or-fun metadata])) + +(defn alert + "Logs an alert message. + (Logger/alert \"database corruption detected\")" + ([message-or-fun]) + ([message-or-fun metadata])) + +(defn emergency + "Logs an emergency message. + (Logger/emergency \"system is going down\")" + ([message-or-fun]) + ([message-or-fun metadata])) + +(defn log + "Logs at the specified level. + (Logger/log :info \"dynamic level logging\")" + ([level message-or-fun]) + ([level message-or-fun metadata])) + +(defn configure + "Configures the logger at runtime. + (Logger/configure :level :debug)" + [options]) + +(defn level + "Returns the current log level. + (Logger/level) ;=> :info" + []) + +(defn put-module-level + "Sets log level for specific modules. + (Logger/put-module-level MyModule :debug)" + [module level]) + +(defn delete-module-level + "Resets module-level logging to global. + (Logger/delete-module-level MyModule)" + [module]) + +(defn put-process-level + "Sets log level for the current process. + (Logger/put-process-level (Process/self) :debug)" + [pid level]) + +(defn delete-process-level + "Resets process-level logging. + (Logger/delete-process-level pid)" + [pid]) + +(defn enable + "Enables logging for the current process." + [pid]) + +(defn disable + "Disables logging for the current process." + [pid]) + +(defn metadata + "Gets or sets logger metadata for the current process. + (Logger/metadata) ;=> [] + (Logger/metadata :request-id \"abc123\")" + ([]) + ([keyword-list])) + +(defn reset-metadata + "Resets all logger metadata for the current process. + (Logger/reset-metadata) ;=> :ok" + ([]) + ([keys])) + +(defn flush + "Flushes the logger, ensuring all messages are processed. + (Logger/flush) ;=> :ok" + []) + +(defn add-backend + "Adds a logging backend. + (Logger/add-backend :console)" + ([backend]) + ([backend opts])) + +(defn remove-backend + "Removes a logging backend." + ([backend]) + ([backend opts])) diff --git a/stubs/Map.clj b/stubs/Map.clj new file mode 100644 index 0000000..7b43aa6 --- /dev/null +++ b/stubs/Map.clj @@ -0,0 +1,176 @@ +(ns Map + "Elixir Map module — operations on maps. + + In CljElixir: (Map/get m :key), (Map/put m :key val), etc. + Maps are the fundamental key-value data structure on the BEAM.") + +;; --- Access --- + +(defn get + "Gets the value for `key` in `map`. Returns `default` if key is missing. + (Map/get {:a 1 :b 2} :a) ;=> 1 + (Map/get {:a 1} :c :not-found) ;=> :not-found" + ([map key]) + ([map key default])) + +(defn get-lazy + "Gets `key` from `map`, calling `fun` for default if missing. + (Map/get-lazy m :key (fn [] (expensive-computation)))" + [map key fun]) + +(defn fetch + "Returns {:ok value} if `key` exists, :error otherwise. + (Map/fetch {:a 1} :a) ;=> {:ok 1} + (Map/fetch {:a 1} :b) ;=> :error" + [map key]) + +(defn fetch! + "Gets the value for `key`. Raises if key is missing. + (Map/fetch! {:a 1} :a) ;=> 1" + [map key]) + +(defn has-key? + "Returns true if `map` contains `key`. + (Map/has-key? {:a 1 :b 2} :a) ;=> true" + [map key]) + +(defn keys + "Returns all keys in the map. + (Map/keys {:a 1 :b 2}) ;=> [:a :b]" + [map]) + +(defn values + "Returns all values in the map. + (Map/values {:a 1 :b 2}) ;=> [1 2]" + [map]) + +;; --- Modification --- + +(defn put + "Puts the given `value` under `key` in `map`. + (Map/put {:a 1} :b 2) ;=> {:a 1 :b 2}" + [map key value]) + +(defn put-new + "Puts `value` under `key` only if `key` doesn't exist yet. + (Map/put-new {:a 1} :a 99) ;=> {:a 1} + (Map/put-new {:a 1} :b 2) ;=> {:a 1 :b 2}" + [map key value]) + +(defn put-new-lazy + "Like put-new but calls `fun` only if key is absent. + (Map/put-new-lazy m :key (fn [] (expensive-computation)))" + [map key fun]) + +(defn delete + "Deletes `key` from `map`. No-op if key doesn't exist. + (Map/delete {:a 1 :b 2} :a) ;=> {:b 2}" + [map key]) + +(defn drop + "Drops the given `keys` from `map`. + (Map/drop {:a 1 :b 2 :c 3} [:a :c]) ;=> {:b 2}" + [map keys]) + +(defn take + "Takes only the given `keys` from `map`. + (Map/take {:a 1 :b 2 :c 3} [:a :c]) ;=> {:a 1 :c 3}" + [map keys]) + +(defn pop + "Returns the value for `key` and the map without `key`. + (Map/pop {:a 1 :b 2} :a) ;=> {1 {:b 2}} + (Map/pop {:a 1} :c :default) ;=> {:default {:a 1}}" + ([map key]) + ([map key default])) + +(defn pop-lazy + "Like pop but calls `fun` for default if key is absent." + [map key fun]) + +(defn update + "Updates the value at `key` by applying `fun` to the current value. + (Map/update {:a 1} :a (fn [v] (+ v 1))) ;=> {:a 2}" + ([map key fun]) + ([map key default fun])) + +(defn update! + "Updates `key` by applying `fun`. Raises if `key` doesn't exist. + (Map/update! {:a 1} :a (fn [v] (* v 2))) ;=> {:a 2}" + [map key fun]) + +(defn replace + "Replaces value at `key` only if it already exists. No-op otherwise. + (Map/replace {:a 1} :a 99) ;=> {:a 99} + (Map/replace {:a 1} :b 99) ;=> {:a 1}" + [map key value]) + +(defn replace! + "Replaces value at `key`. Raises if `key` doesn't exist." + [map key value]) + +;; --- Merging --- + +(defn merge + "Merges two maps. Values from `map2` take precedence. + (Map/merge {:a 1 :b 2} {:b 3 :c 4}) ;=> {:a 1 :b 3 :c 4} + With resolver: (Map/merge m1 m2 (fn [k v1 v2] (+ v1 v2)))" + ([map1 map2]) + ([map1 map2 resolver])) + +(defn split + "Splits `map` into two maps based on the given `keys`. + (Map/split {:a 1 :b 2 :c 3} [:a :c]) ;=> {%{a: 1, c: 3} %{b: 2}}" + [map keys]) + +;; --- Conversion --- + +(defn new + "Creates a new empty map or from an enumerable. + (Map/new) ;=> {} + (Map/new [[:a 1] [:b 2]]) ;=> {:a 1 :b 2} + (Map/new [1 2 3] (fn [x] {x (* x x)})) ;=> {1 1, 2 4, 3 9}" + ([]) + ([enumerable]) + ([enumerable transform])) + +(defn from-struct + "Converts a struct to a plain map (removes __struct__ key). + (Map/from-struct my-struct) ;=> {...}" + [struct]) + +(defn to-list + "Converts a map to a keyword list (list of {key, value} tuples). + (Map/to-list {:a 1 :b 2}) ;=> [[:a 1] [:b 2]]" + [map]) + +(defn equal? + "Returns true if two maps are equal. + (Map/equal? {:a 1} {:a 1}) ;=> true" + [map1 map2]) + +;; --- Filtering --- + +(defn filter + "Filters map entries where `f` returns truthy. `f` receives {key, value}. + (Map/filter {:a 1 :b 2 :c 3} (fn [{k v}] (> v 1))) ;=> {:b 2 :c 3}" + [map f]) + +(defn reject + "Rejects map entries where `f` returns truthy. + (Map/reject {:a 1 :b 2 :c 3} (fn [{k v}] (> v 1))) ;=> {:a 1}" + [map f]) + +(defn map + "Maps over entries, `f` receives {key, value} and must return {key, value}. + (Map/map {:a 1 :b 2} (fn [{k v}] {k (* v 10)})) ;=> {:a 10 :b 20}" + [map f]) + +(defn intersect + "Returns entries common to both maps (using `map2` values)." + ([map1 map2]) + ([map1 map2 resolver])) + +(defn diff + "Returns {entries_only_in_map1, entries_only_in_map2, entries_in_both}." + [map1 map2]) diff --git a/stubs/MapSet.clj b/stubs/MapSet.clj new file mode 100644 index 0000000..ca9f47d --- /dev/null +++ b/stubs/MapSet.clj @@ -0,0 +1,87 @@ +(ns MapSet + "Elixir MapSet module — set operations backed by maps. + + In CljElixir: (MapSet/new [1 2 3]), (MapSet/member? s 2), etc. + Use #{1 2 3} literal syntax for set creation in CljElixir.") + +(defn new + "Creates a new set, optionally from an enumerable. + (MapSet/new) ;=> #{} + (MapSet/new [1 2 3]) ;=> #{1 2 3} + (MapSet/new [1 2 3] (fn [x] (* x 2))) ;=> #{2 4 6}" + ([]) + ([enumerable]) + ([enumerable transform])) + +(defn put + "Inserts `value` into the set. + (MapSet/put #{1 2} 3) ;=> #{1 2 3}" + [set value]) + +(defn delete + "Deletes `value` from the set. + (MapSet/delete #{1 2 3} 2) ;=> #{1 3}" + [set value]) + +(defn member? + "Returns true if `value` is in `set`. + (MapSet/member? #{1 2 3} 2) ;=> true" + [set value]) + +(defn size + "Returns the number of elements. + (MapSet/size #{1 2 3}) ;=> 3" + [set]) + +(defn to-list + "Converts the set to a list. + (MapSet/to-list #{1 2 3}) ;=> [1 2 3]" + [set]) + +(defn equal? + "Returns true if two sets are equal. + (MapSet/equal? #{1 2} #{2 1}) ;=> true" + [set1 set2]) + +(defn union + "Returns the union of two sets. + (MapSet/union #{1 2} #{2 3}) ;=> #{1 2 3}" + [set1 set2]) + +(defn intersection + "Returns the intersection of two sets. + (MapSet/intersection #{1 2 3} #{2 3 4}) ;=> #{2 3}" + [set1 set2]) + +(defn difference + "Returns elements in `set1` not in `set2`. + (MapSet/difference #{1 2 3} #{2 3}) ;=> #{1}" + [set1 set2]) + +(defn symmetric-difference + "Returns elements in either set but not both. + (MapSet/symmetric-difference #{1 2 3} #{2 3 4}) ;=> #{1 4}" + [set1 set2]) + +(defn subset? + "Returns true if `set1` is a subset of `set2`. + (MapSet/subset? #{1 2} #{1 2 3}) ;=> true" + [set1 set2]) + +(defn disjoint? + "Returns true if `set1` and `set2` have no elements in common. + (MapSet/disjoint? #{1 2} #{3 4}) ;=> true" + [set1 set2]) + +(defn filter + "Returns elements for which `fun` returns truthy. + (MapSet/filter #{1 2 3 4} (fn [x] (> x 2))) ;=> #{3 4}" + [set fun]) + +(defn reject + "Returns elements for which `fun` returns falsy." + [set fun]) + +(defn map + "Maps `fun` over the set, returning a new set." + [set fun]) diff --git a/stubs/Module.clj b/stubs/Module.clj new file mode 100644 index 0000000..5bd33bd --- /dev/null +++ b/stubs/Module.clj @@ -0,0 +1,76 @@ +(ns Module + "Elixir Module module — module introspection and manipulation. + + In CljElixir: (Module/defines? MyModule :my-func 2), etc.") + +(defn concat + "Concatenates atoms/strings into a module name. + (Module/concat [\"Elixir\" \"MyApp\" \"Router\"]) ;=> MyApp.Router + (Module/concat Elixir.MyApp :Router) ;=> MyApp.Router" + ([list]) + ([left right])) + +(defn split + "Splits a module name into parts. + (Module/split MyApp.Router) ;=> [\"MyApp\" \"Router\"]" + [module]) + +(defn defines? + "Returns true if `module` defines `function` with given `arity`. + (Module/defines? Enum :map 2) ;=> true + (Module/defines? Enum :map) ;=> true" + ([module function-name]) + ([module function-name arity])) + +(defn definitions-in + "Returns all functions/macros defined in `module`. + (Module/definitions-in Enum) ;=> [{:map 2} {:filter 2} ...]" + ([module]) + ([module kind])) + +(defn has-attribute? + "Returns true if `module` has `attribute`. + (Module/has-attribute? MyModule :behaviour) ;=> true" + [module attribute]) + +(defn get-attribute + "Gets a module attribute. + (Module/get-attribute MyModule :moduledoc)" + ([module attribute]) + ([module attribute default])) + +(defn put-attribute + "Sets a module attribute during compilation." + [module attribute value]) + +(defn delete-attribute + "Deletes a module attribute during compilation." + [module attribute]) + +(defn register-attribute + "Registers a module attribute during compilation." + [module attribute opts]) + +(defn spec-to-callback + "Converts a spec to a callback." + [module spec]) + +(defn open? + "Returns true if the module is open (being compiled)." + [module]) + +(defn overridable? + "Returns true if function is overridable in module." + [module function-arity]) + +(defn make-overridable + "Makes functions overridable." + [module function-arities]) + +(defn safe-concat + "Safely concatenates module atoms without creating new atoms." + [list]) + +(defn create + "Creates a module at runtime." + [module quoted opts]) diff --git a/stubs/NaiveDateTime.clj b/stubs/NaiveDateTime.clj new file mode 100644 index 0000000..1692adc --- /dev/null +++ b/stubs/NaiveDateTime.clj @@ -0,0 +1,126 @@ +(ns NaiveDateTime + "Elixir NaiveDateTime module — datetime without timezone. + + In CljElixir: (NaiveDateTime/utc-now), (NaiveDateTime/new 2024 3 9 12 0 0), etc. + 'Naive' because it has no timezone information.") + +(defn utc-now + "Returns the current UTC naive datetime. + (NaiveDateTime/utc-now) ;=> ~N[2024-03-09 12:00:00]" + ([]) + ([calendar])) + +(defn local-now + "Returns the current local naive datetime." + ([]) + ([calendar])) + +(defn new + "Creates a new NaiveDateTime. + (NaiveDateTime/new 2024 3 9 12 0 0) ;=> {:ok ~N[2024-03-09 12:00:00]}" + ([year month day hour minute second]) + ([year month day hour minute second microsecond]) + ([date time])) + +(defn new! + "Creates a new NaiveDateTime. Raises on error." + ([year month day hour minute second]) + ([year month day hour minute second microsecond]) + ([date time])) + +(defn from-iso8601 + "Parses ISO 8601 string. + (NaiveDateTime/from-iso8601 \"2024-03-09T12:00:00\") ;=> {:ok naive}" + ([string]) + ([string calendar])) + +(defn from-iso8601! + "Parses ISO 8601. Raises on error." + ([string]) + ([string calendar])) + +(defn to-iso8601 + "Converts to ISO 8601 string. + (NaiveDateTime/to-iso8601 ndt) ;=> \"2024-03-09T12:00:00\"" + ([naive-datetime]) + ([naive-datetime format])) + +(defn to-string + "Converts to string." + [naive-datetime]) + +(defn to-date + "Extracts the Date part." + [naive-datetime]) + +(defn to-time + "Extracts the Time part." + [naive-datetime]) + +(defn add + "Adds time. + (NaiveDateTime/add ndt 3600) ;=> +1 hour" + ([naive-datetime amount]) + ([naive-datetime amount unit])) + +(defn diff + "Returns difference. + (NaiveDateTime/diff ndt1 ndt2) ;=> seconds" + ([naive-datetime1 naive-datetime2]) + ([naive-datetime1 naive-datetime2 unit])) + +(defn truncate + "Truncates to precision." + [naive-datetime precision]) + +(defn compare + "Compares two NaiveDateTimes. Returns :lt, :eq, or :gt." + [naive-datetime1 naive-datetime2]) + +(defn before? + "Returns true if first is before second." + [naive-datetime1 naive-datetime2]) + +(defn after? + "Returns true if first is after second." + [naive-datetime1 naive-datetime2]) + +(defn shift + "Shifts by a duration." + [naive-datetime duration]) + +(defn beginning-of-day + "Returns midnight of the same day." + [naive-datetime]) + +(defn end-of-day + "Returns 23:59:59.999999 of the same day." + [naive-datetime]) + +(defn from-erl + "Converts from Erlang datetime tuple. + (NaiveDateTime/from-erl {{2024 3 9} {12 0 0}}) ;=> {:ok naive}" + ([tuple]) + ([tuple microsecond]) + ([tuple microsecond calendar])) + +(defn from-erl! + "Converts from Erlang tuple. Raises on error." + ([tuple]) + ([tuple microsecond]) + ([tuple microsecond calendar])) + +(defn to-erl + "Converts to Erlang datetime tuple. + (NaiveDateTime/to-erl ndt) ;=> {{2024 3 9} {12 0 0}}" + [naive-datetime]) + +(defn from-gregorian-seconds + "Creates from seconds since year 0." + ([seconds]) + ([seconds microsecond]) + ([seconds microsecond calendar])) + +(defn to-gregorian-seconds + "Returns seconds since year 0." + [naive-datetime]) diff --git a/stubs/Node.clj b/stubs/Node.clj new file mode 100644 index 0000000..c0740d1 --- /dev/null +++ b/stubs/Node.clj @@ -0,0 +1,88 @@ +(ns Node + "Elixir Node module — distributed Erlang node operations. + + In CljElixir: (Node/self), (Node/connect :other@host), etc. + For building distributed BEAM applications.") + +(defn self + "Returns the current node name. + (Node/self) ;=> :mynode@myhost" + []) + +(defn alive? + "Returns true if the local node is alive (part of a distributed system). + (Node/alive?) ;=> true" + []) + +(defn list + "Returns a list of all connected nodes. + (Node/list) ;=> [:node1@host :node2@host] + (Node/list :known) ;=> includes known but disconnected nodes" + ([]) + ([type])) + +(defn connect + "Connects to a remote node. + (Node/connect :other@hostname) ;=> true" + [node]) + +(defn disconnect + "Disconnects from a remote node. + (Node/disconnect :other@hostname) ;=> true" + [node]) + +(defn monitor + "Monitors a remote node. Sends {:nodedown node} on disconnect. + (Node/monitor :other@hostname true)" + ([node flag]) + ([node flag opts])) + +(defn ping + "Pings a remote node. + (Node/ping :other@hostname) ;=> :pong or :pang" + [node]) + +(defn spawn + "Spawns a process on a remote node. + (Node/spawn :other@hostname (fn [] (IO/puts \"remote!\")))" + ([node fun]) + ([node fun opts]) + ([node module fun args]) + ([node module fun args opts])) + +(defn spawn-link + "Spawns a linked process on a remote node." + ([node fun]) + ([node fun opts]) + ([node module fun args]) + ([node module fun args opts])) + +(defn spawn-monitor + "Spawns a monitored process on a remote node." + ([node fun]) + ([node fun opts]) + ([node module fun args]) + ([node module fun args opts])) + +(defn get-cookie + "Returns the magic cookie for the node. + (Node/get-cookie) ;=> :nocookie or :somecookie" + []) + +(defn set-cookie + "Sets the magic cookie. + (Node/set-cookie :my-secret-cookie)" + ([cookie]) + ([node cookie])) + +(defn start + "Starts distribution. Returns {:ok pid} or {:error reason}. + (Node/start :mynode :shortnames)" + ([name]) + ([name type]) + ([name type tick-time])) + +(defn stop + "Stops distribution. + (Node/stop) ;=> :ok" + []) diff --git a/stubs/Path.clj b/stubs/Path.clj new file mode 100644 index 0000000..e3c932f --- /dev/null +++ b/stubs/Path.clj @@ -0,0 +1,90 @@ +(ns Path + "Elixir Path module — file path manipulation. + + In CljElixir: (Path/join \"a\" \"b\"), (Path/expand \"~/file\"), etc. + All functions work with forward slashes on all platforms.") + +(defn join + "Joins path segments. + (Path/join \"a\" \"b\") ;=> \"a/b\" + (Path/join [\"a\" \"b\" \"c\"]) ;=> \"a/b/c\"" + ([paths]) + ([left right])) + +(defn expand + "Expands a path to an absolute path, resolving ~ and relative components. + (Path/expand \"~/file.txt\") ;=> \"/Users/ajet/file.txt\" + (Path/expand \"../other\" \"/base\") ;=> \"/other\"" + ([path]) + ([path relative-to])) + +(defn absname + "Converts to absolute path. + (Path/absname \"file.txt\") ;=> \"/Users/ajet/repos/clje/file.txt\"" + ([path]) + ([path relative-to])) + +(defn relative-to + "Returns the relative path from `path` to `from`. + (Path/relative-to \"/a/b/c\" \"/a\") ;=> \"b/c\"" + ([path from]) + ([path from opts])) + +(defn relative + "Forces path to be relative. + (Path/relative \"/a/b\") ;=> \"a/b\"" + [path]) + +(defn basename + "Returns the last component of the path. + (Path/basename \"/a/b/c.txt\") ;=> \"c.txt\" + (Path/basename \"/a/b/c.txt\" \".txt\") ;=> \"c\"" + ([path]) + ([path extension])) + +(defn dirname + "Returns the directory component. + (Path/dirname \"/a/b/c.txt\") ;=> \"/a/b\"" + [path]) + +(defn extname + "Returns the file extension. + (Path/extname \"file.ex\") ;=> \".ex\" + (Path/extname \"file\") ;=> \"\"" + [path]) + +(defn rootname + "Returns the path without extension. + (Path/rootname \"file.ex\") ;=> \"file\" + (Path/rootname \"file.tar.gz\" \".tar.gz\") ;=> \"file\"" + ([path]) + ([path extension])) + +(defn split + "Splits a path into its components. + (Path/split \"/a/b/c\") ;=> [\"/\" \"a\" \"b\" \"c\"]" + [path]) + +(defn type + "Returns the path type: :absolute, :relative, or :volumerelative. + (Path/type \"/a/b\") ;=> :absolute + (Path/type \"a/b\") ;=> :relative" + [path]) + +(defn wildcard + "Expands a glob pattern. Returns matching paths. + (Path/wildcard \"lib/**/*.ex\") ;=> [\"lib/my_app.ex\" ...] + (Path/wildcard \"*.{ex,exs}\") ;=> matches .ex and .exs files" + ([glob]) + ([glob opts])) + +(defn safe-relative + "Returns a safe relative path (no ..). Returns {:ok path} or :error. + (Path/safe-relative \"a/b/c\") ;=> {:ok \"a/b/c\"} + (Path/safe-relative \"../etc/passwd\") ;=> :error" + ([path]) + ([path cwd])) + +(defn safe-relative-to + "Returns a safe relative path from `path` to `cwd`. Returns {:ok path} or :error." + [path cwd]) diff --git a/stubs/Port.clj b/stubs/Port.clj new file mode 100644 index 0000000..607e2e1 --- /dev/null +++ b/stubs/Port.clj @@ -0,0 +1,49 @@ +(ns Port + "Elixir Port module — external program interaction. + + In CljElixir: (Port/open {:spawn \"cmd\"} opts), etc. + Ports allow communication with external OS processes via stdin/stdout.") + +(defn open + "Opens a port to an external program. Returns a port. + (Port/open {:spawn \"cat\"} [:binary]) + (Port/open {:spawn-executable \"/usr/bin/python3\"} [:binary {:args [\"-c\" \"print(1)\"]}]) + (Port/open {:fd 0 1} [:binary]) ;=> stdin/stdout" + [name settings]) + +(defn command + "Sends data to a port. + (Port/command port data)" + ([port data]) + ([port data opts])) + +(defn close + "Closes a port. + (Port/close port) ;=> true" + [port]) + +(defn connect + "Changes the owner of a port. + (Port/connect port new-owner-pid)" + [port pid]) + +(defn info + "Returns information about a port. + (Port/info port) ;=> [{:name ...} {:links [...]} ...] + (Port/info port :name) ;=> port name" + ([port]) + ([port item])) + +(defn list + "Returns all open ports. + (Port/list) ;=> [#Port<0.5> ...]" + []) + +(defn monitor + "Monitors a port. Returns reference." + [type port]) + +(defn demonitor + "Stops monitoring a port." + ([ref]) + ([ref opts])) diff --git a/stubs/Process.clj b/stubs/Process.clj new file mode 100644 index 0000000..d5033af --- /dev/null +++ b/stubs/Process.clj @@ -0,0 +1,166 @@ +(ns Process + "Elixir Process module — BEAM process management. + + In CljElixir: (Process/sleep 1000), (Process/send pid msg), etc. + Provides utilities for working with BEAM processes (lightweight actors).") + +(defn sleep + "Sleeps the current process for `timeout` milliseconds or :infinity. + (Process/sleep 1000) ;=> :ok (sleeps 1 second) + (Process/sleep :infinity) ;=> blocks forever" + [timeout]) + +(defn send + "Sends `msg` to `dest` (pid, port, or registered name). Returns `msg`. + (Process/send pid {:hello \"world\"}) + (Process/send pid msg [:noconnect]) ;=> with options" + ([dest msg]) + ([dest msg opts])) + +(defn spawn + "Spawns a new process. Returns the PID. + (Process/spawn (fn [] (IO/puts \"hi\")) []) + (Process/spawn MyModule :my-func [arg1 arg2] [:link])" + ([fun opts]) + ([module function args opts])) + +(defn spawn-link + "Spawns a linked process. If the child crashes, the parent crashes too. + (Process/spawn-link (fn [] (IO/puts \"linked\")) [])" + ([fun opts]) + ([module function args opts])) + +(defn spawn-monitor + "Spawns a monitored process. Returns {pid, ref}. + (Process/spawn-monitor (fn [] (do-work)))" + ([fun]) + ([module function args])) + +(defn alive? + "Returns true if the process identified by `pid` is alive. + (Process/alive? pid) ;=> true" + [pid]) + +(defn self + "Returns the PID of the calling process. + (Process/self) ;=> #PID<0.123.0>" + []) + +(defn whereis + "Returns the PID registered under `name`, or nil. + (Process/whereis :my-server) ;=> #PID<0.456.0>" + [name]) + +(defn register + "Registers a PID under the given atom `name`. + (Process/register pid :my-server) ;=> true" + [pid name]) + +(defn unregister + "Unregisters the given `name`. + (Process/unregister :my-server) ;=> true" + [name]) + +(defn registered + "Returns a list of all registered process names. + (Process/registered) ;=> [:logger :code-server ...]" + []) + +(defn link + "Creates a link between the calling process and `pid_or_port`. + (Process/link pid)" + [pid-or-port]) + +(defn unlink + "Removes a link between the calling process and `pid_or_port`. + (Process/unlink pid)" + [pid-or-port]) + +(defn monitor + "Starts monitoring `item`. Returns a reference. + (Process/monitor :process pid)" + ([type item]) + ([type item opts])) + +(defn demonitor + "Stops monitoring. The `ref` is from a previous `monitor` call. + (Process/demonitor ref) + (Process/demonitor ref [:flush])" + ([ref]) + ([ref opts])) + +(defn exit + "Sends an exit signal to `pid`. + (Process/exit pid :normal) ;=> normal shutdown + (Process/exit pid :kill) ;=> forceful kill (untrappable)" + [pid reason]) + +(defn flag + "Sets process flags. Returns the old value. + (Process/flag :trap-exit true) ;=> false (previous value) + (Process/flag :priority :high)" + ([flag value]) + ([pid flag value])) + +(defn info + "Returns info about a process. + (Process/info pid) ;=> keyword list of process info + (Process/info pid :message-queue-len) ;=> message queue length" + ([pid]) + ([pid item])) + +(defn list + "Returns a list of all running process PIDs. + (Process/list) ;=> [#PID<0.0.0> #PID<0.1.0> ...]" + []) + +(defn group-leader + "Returns or sets the group leader. + (Process/group-leader) ;=> #PID<0.64.0> + (Process/group-leader pid gl)" + ([]) + ([pid leader])) + +(defn hibernate + "Puts the process into hibernation (frees memory). Resumes in `fun`. + (Process/hibernate Module :function [args])" + [module function args]) + +(defn send-after + "Sends `msg` to `dest` after `time` milliseconds. Returns a timer ref. + (Process/send-after self :tick 1000)" + ([dest msg time]) + ([dest msg time opts])) + +(defn cancel-timer + "Cancels a timer created by send-after." + ([timer-ref]) + ([timer-ref opts])) + +(defn read-timer + "Returns the time remaining for a timer, or false." + ([timer-ref]) + ([timer-ref opts])) + +(defn put + "Puts a key-value pair in the process dictionary. Returns old value or nil. + (Process/put :my-key \"my value\") ;=> nil" + [key value]) + +(defn get + "Gets a value from the process dictionary. + (Process/get :my-key) ;=> \"my value\" + (Process/get :missing :default) ;=> :default" + ([key]) + ([key default])) + +(defn delete + "Deletes a key from the process dictionary. Returns old value. + (Process/delete :my-key) ;=> \"my value\"" + [key]) + +(defn get-keys + "Returns all keys in the process dictionary (or keys with given value). + (Process/get-keys) ;=> [:my-key ...]" + ([]) + ([value])) diff --git a/stubs/Range.clj b/stubs/Range.clj new file mode 100644 index 0000000..ed5a868 --- /dev/null +++ b/stubs/Range.clj @@ -0,0 +1,37 @@ +(ns Range + "Elixir Range module — integer ranges. + + In CljElixir: Ranges use first..last or first..last//step syntax. + (Range/new 1 10) ;=> 1..10") + +(defn new + "Creates a new range. + (Range/new 1 10) ;=> 1..10 + (Range/new 1 10 2) ;=> 1..10//2 (step of 2)" + ([first last]) + ([first last step])) + +(defn size + "Returns the number of elements in the range. + (Range/size (Range/new 1 10)) ;=> 10" + [range]) + +(defn disjoint? + "Returns true if two ranges don't overlap. + (Range/disjoint? (Range/new 1 5) (Range/new 6 10)) ;=> true" + [range1 range2]) + +(defn shift + "Shifts a range by `steps`. + (Range/shift (Range/new 1 5) 2) ;=> 3..7" + [range steps]) + +(defn split + "Splits a range at position `split`. + (Range/split (Range/new 1 5) 3) ;=> {1..3 4..5}" + [range split]) + +(defn to-list + "Converts range to a list. + (Range/to-list (Range/new 1 5)) ;=> [1 2 3 4 5]" + [range]) diff --git a/stubs/Regex.clj b/stubs/Regex.clj new file mode 100644 index 0000000..6f861e6 --- /dev/null +++ b/stubs/Regex.clj @@ -0,0 +1,85 @@ +(ns Regex + "Elixir Regex module — regular expression operations (wraps Erlang :re). + + In CljElixir: (Regex/match? ~r/pattern/ string), etc. + Regex literals use ~r/pattern/flags syntax.") + +(defn compile + "Compiles a regex pattern string. Returns {:ok regex} or {:error reason}. + (Regex/compile \"^hello\") ;=> {:ok ~r/^hello/} + (Regex/compile \"hello\" \"i\") ;=> case-insensitive" + ([source]) + ([source opts])) + +(defn compile! + "Compiles a regex. Raises on invalid pattern. + (Regex/compile! \"^hello\") ;=> ~r/^hello/" + ([source]) + ([source opts])) + +(defn match? + "Returns true if `string` matches `regex`. + (Regex/match? ~r/\\d+/ \"hello123\") ;=> true" + [regex string]) + +(defn run + "Runs the regex against `string`. Returns list of matches or nil. + (Regex/run ~r/(\\w+)@(\\w+)/ \"user@host\") ;=> [\"user@host\" \"user\" \"host\"] + (Regex/run ~r/\\d+/ \"hello\") ;=> nil" + ([regex string]) + ([regex string opts])) + +(defn scan + "Scans the string for all matches. + (Regex/scan ~r/\\d+/ \"a1b2c3\") ;=> [[\"1\"] [\"2\"] [\"3\"]]" + ([regex string]) + ([regex string opts])) + +(defn named-captures + "Returns a map of named captures. + (Regex/named-captures ~r/(?P\\d{4})-(?P\\d{2})/ \"2024-03\") + ;=> %{\"year\" => \"2024\", \"month\" => \"03\"}" + ([regex string]) + ([regex string opts])) + +(defn replace + "Replaces regex matches in `string` with `replacement`. + (Regex/replace ~r/\\d+/ \"a1b2\" \"X\") ;=> \"aXbX\" + (Regex/replace ~r/(\\w+)/ \"hello\" (fn [match] (String/upcase match)))" + ([regex string replacement]) + ([regex string replacement opts])) + +(defn split + "Splits `string` by `regex`. + (Regex/split ~r/\\s+/ \"hello world\") ;=> [\"hello\" \"world\"] + (Regex/split ~r/,/ \"a,b,c\" :parts 2) ;=> [\"a\" \"b,c\"]" + ([regex string]) + ([regex string opts])) + +(defn source + "Returns the source string of a compiled regex. + (Regex/source ~r/hello/) ;=> \"hello\"" + [regex]) + +(defn opts + "Returns the options string of a compiled regex. + (Regex/opts ~r/hello/i) ;=> \"i\"" + [regex]) + +(defn re-pattern + "Returns the underlying compiled pattern." + [regex]) + +(defn names + "Returns a list of named capture group names. + (Regex/names ~r/(?P\\w+)/) ;=> [\"name\"]" + [regex]) + +(defn escape + "Escapes a string for use in a regex. + (Regex/escape \"hello.world\") ;=> \"hello\\\\.world\"" + [string]) + +(defn regex? + "Returns true if `term` is a compiled regex." + [term]) diff --git a/stubs/Registry.clj b/stubs/Registry.clj new file mode 100644 index 0000000..5b5b6ab --- /dev/null +++ b/stubs/Registry.clj @@ -0,0 +1,86 @@ +(ns Registry + "Elixir Registry module — process registry for local name lookup. + + In CljElixir: (Registry/start-link :keys :unique :name :my-registry), etc. + Registries allow processes to be found by key (unique or duplicate).") + +(defn start-link + "Starts a registry. Returns {:ok pid}. + (Registry/start-link :keys :unique :name MyApp.Registry) + (Registry/start-link :keys :duplicate :name MyApp.PubSub)" + [opts]) + +(defn register + "Registers the current process under `key`. + (Registry/register MyApp.Registry :my-key \"value\") ;=> {:ok pid}" + [registry key value]) + +(defn unregister + "Unregisters the current process from `key`. + (Registry/unregister MyApp.Registry :my-key)" + [registry key]) + +(defn lookup + "Looks up processes registered under `key`. Returns [{pid, value} ...]. + (Registry/lookup MyApp.Registry :my-key) ;=> [{#PID<0.123.0> \"value\"}]" + [registry key]) + +(defn dispatch + "Dispatches to all registered processes for `key`. + (Registry/dispatch MyApp.PubSub :topic (fn [{pid val}] (send pid :msg)))" + ([registry key fun]) + ([registry key fun opts])) + +(defn keys + "Returns all keys registered by the current process. + (Registry/keys MyApp.Registry (Process/self)) ;=> [:key1 :key2]" + [registry pid]) + +(defn values + "Returns all {pid, value} pairs for `key`. + (Registry/values MyApp.Registry :my-key (Process/self))" + [registry key pid]) + +(defn count + "Returns the number of registered entries. + (Registry/count MyApp.Registry) ;=> 5" + [registry]) + +(defn count-match + "Returns count of entries matching `key` and `pattern`. + (Registry/count-match MyApp.Registry :my-key :_)" + ([registry key pattern]) + ([registry key pattern guards])) + +(defn match + "Matches entries by key and value pattern. + (Registry/match MyApp.Registry :my-key :_) ;=> all values for key" + ([registry key pattern]) + ([registry key pattern guards])) + +(defn select + "Selects entries using match specifications." + [registry spec]) + +(defn update-value + "Updates the value for the current process's registration. + (Registry/update-value MyApp.Registry :my-key (fn [old] (inc old)))" + [registry key callback]) + +(defn unregister-match + "Unregisters entries matching key and pattern." + ([registry key pattern]) + ([registry key pattern guards])) + +(defn meta + "Gets metadata for a registry key. + (Registry/meta MyApp.Registry :my-key)" + [registry key]) + +(defn put-meta + "Sets metadata for a registry key." + [registry key value]) + +(defn child-spec + "Returns a child spec for starting under a supervisor." + [opts]) diff --git a/stubs/Stream.clj b/stubs/Stream.clj new file mode 100644 index 0000000..679406d --- /dev/null +++ b/stubs/Stream.clj @@ -0,0 +1,168 @@ +(ns Stream + "Elixir Stream module — lazy, composable enumerables. + + In CljElixir: (Stream/map coll f), (Stream/filter coll f), etc. + Streams are lazy: operations are only executed when the stream is consumed + (e.g., by Enum/to-list, Enum/take, etc.).") + +(defn map + "Lazily maps `fun` over `enumerable`. + (-> [1 2 3] (Stream/map (fn [x] (* x 2))) (Enum/to-list)) ;=> [2 4 6]" + [enumerable fun]) + +(defn filter + "Lazily filters elements. + (-> [1 2 3 4] (Stream/filter (fn [x] (> x 2))) (Enum/to-list)) ;=> [3 4]" + [enumerable fun]) + +(defn reject + "Lazily rejects elements where `fun` returns truthy." + [enumerable fun]) + +(defn flat-map + "Lazily maps and flattens." + [enumerable fun]) + +(defn take + "Lazily takes `count` elements. + (-> (Stream/iterate 0 inc) (Stream/take 5) (Enum/to-list)) ;=> [0 1 2 3 4]" + [enumerable count]) + +(defn take-while + "Lazily takes while `fun` returns truthy." + [enumerable fun]) + +(defn take-every + "Lazily takes every `nth` element." + [enumerable nth]) + +(defn drop + "Lazily drops `count` elements." + [enumerable count]) + +(defn drop-while + "Lazily drops while `fun` returns truthy." + [enumerable fun]) + +(defn chunk-by + "Lazily chunks by result of `fun`." + [enumerable fun]) + +(defn chunk-every + "Lazily chunks into groups of `count`. + (-> [1 2 3 4 5] (Stream/chunk-every 2) (Enum/to-list)) ;=> [[1 2] [3 4] [5]]" + ([enumerable count]) + ([enumerable count step]) + ([enumerable count step leftover])) + +(defn chunk-while + "Lazily chunks with custom accumulator logic." + [enumerable acc chunk-fun after-fun]) + +(defn concat + "Lazily concatenates enumerables. + (-> (Stream/concat [1 2] [3 4]) Enum/to-list) ;=> [1 2 3 4]" + ([enumerables]) + ([first rest])) + +(defn dedup + "Lazily removes consecutive duplicates." + ([enumerable])) + +(defn dedup-by + "Lazily removes consecutive duplicates by `fun`." + [enumerable fun]) + +(defn each + "Lazily invokes `fun` for side effects. Elements pass through. + (-> [1 2 3] (Stream/each (fn [x] (IO/puts x))) Enum/to-list)" + [enumerable fun]) + +(defn scan + "Lazily emits successive reduced values. + (-> [1 2 3 4] (Stream/scan 0 +) Enum/to-list) ;=> [1 3 6 10]" + ([enumerable fun]) + ([enumerable acc fun])) + +(defn transform + "Lazily transforms with an accumulator." + [enumerable acc reducer after-fun]) + +(defn uniq + "Lazily removes duplicates." + [enumerable]) + +(defn uniq-by + "Lazily removes duplicates by `fun`." + [enumerable fun]) + +(defn with-index + "Lazily adds indices. + (-> [:a :b :c] Stream/with-index Enum/to-list) ;=> [{:a 0} {:b 1} {:c 2}]" + ([enumerable]) + ([enumerable offset])) + +(defn zip + "Lazily zips enumerables. + (-> (Stream/zip [1 2 3] [:a :b :c]) Enum/to-list) ;=> [{1 :a} {2 :b} {3 :c}]" + ([enumerables]) + ([enum1 enum2])) + +(defn zip-with + "Lazily zips with a merge function." + ([enumerables zip-fun]) + ([enum1 enum2 zip-fun])) + +;; --- Generators --- + +(defn iterate + "Generates an infinite stream by repeatedly applying `fun`. + (-> (Stream/iterate 0 inc) (Stream/take 5) Enum/to-list) ;=> [0 1 2 3 4]" + [start-value next-fun]) + +(defn repeatedly + "Generates a stream by calling `fun` repeatedly. + (-> (Stream/repeatedly (fn [] (rand 10))) (Stream/take 3) Enum/to-list)" + ([fun]) + ([count fun])) + +(defn unfold + "Generates a stream with an accumulator. `fun` returns {emit, next-acc} or nil. + (-> (Stream/unfold 5 (fn [n] (if (> n 0) {n (- n 1)} nil))) + Enum/to-list) ;=> [5 4 3 2 1]" + [acc fun]) + +(defn resource + "Creates a stream with setup/cleanup. Useful for external resources. + (Stream/resource + (fn [] (File/open! \"file.txt\")) + (fn [file] ...) + (fn [file] (File/close file)))" + [start-fun next-fun after-fun]) + +(defn cycle + "Repeats an enumerable infinitely. + (-> (Stream/cycle [1 2 3]) (Stream/take 7) Enum/to-list) ;=> [1 2 3 1 2 3 1]" + [enumerable]) + +(defn interval + "Emits incrementing integers at `interval` milliseconds. + (-> (Stream/interval 1000) (Stream/take 3) Enum/to-list) ;=> [0 1 2] (1s apart)" + [interval]) + +(defn timer + "Emits a single value of 0 after `delay` milliseconds. + (-> (Stream/timer 1000) Enum/to-list) ;=> [0]" + [delay]) + +(defn run + "Consumes the stream for side effects. Returns :ok." + [stream]) + +(defn intersperse + "Lazily intersperses `separator` between elements." + [enumerable separator]) + +(defn map-every + "Lazily maps `fun` over every `nth` element." + [enumerable nth fun]) diff --git a/stubs/String.clj b/stubs/String.clj new file mode 100644 index 0000000..fa912e0 --- /dev/null +++ b/stubs/String.clj @@ -0,0 +1,242 @@ +(ns String + "Elixir String module — UTF-8 string operations. + + In CljElixir: (String/split s \" \"), (String/trim s), etc. + Strings in Elixir are UTF-8 encoded binaries.") + +;; --- Searching --- + +(defn contains? + "Returns true if `string` contains `pattern`. + (String/contains? \"hello world\" \"world\") ;=> true + (String/contains? \"hello\" [\"x\" \"e\"]) ;=> true (any match)" + [string pattern]) + +(defn starts-with? + "Returns true if `string` starts with `prefix`. + (String/starts-with? \"hello\" \"he\") ;=> true" + [string prefix]) + +(defn ends-with? + "Returns true if `string` ends with `suffix`. + (String/ends-with? \"hello\" \"lo\") ;=> true" + [string suffix]) + +(defn match? + "Returns true if `string` matches the regex `pattern`. + (String/match? \"hello123\" ~r/\\d+/) ;=> true" + [string pattern]) + +;; --- Splitting & Joining --- + +(defn split + "Splits `string` by `pattern`. Without a pattern splits on whitespace. + (String/split \"a,b,c\" \",\") ;=> [\"a\" \"b\" \"c\"] + (String/split \"a,b,c\" \",\" 2) ;=> [\"a\" \"b,c\"]" + ([string]) + ([string pattern]) + ([string pattern parts])) + +(defn split-at + "Splits `string` at `position`. + (String/split-at \"hello\" 3) ;=> {\"hel\" \"lo\"}" + [string position]) + +;; --- Transformation --- + +(defn replace + "Replaces occurrences of `pattern` in `string` with `replacement`. + (String/replace \"hello world\" \"world\" \"elixir\") ;=> \"hello elixir\" + (String/replace \"aabba\" ~r/a/ \"x\") ;=> \"xxbbx\"" + [string pattern replacement]) + +(defn replace-prefix + "Replaces prefix if it matches. + (String/replace-prefix \"hello\" \"he\" \"HE\") ;=> \"HEllo\"" + [string match replacement]) + +(defn replace-suffix + "Replaces suffix if it matches. + (String/replace-suffix \"hello\" \"lo\" \"LO\") ;=> \"helLO\"" + [string match replacement]) + +(defn replace-leading + "Replaces all leading occurrences of `match` with `replacement`." + [string match replacement]) + +(defn replace-trailing + "Replaces all trailing occurrences of `match` with `replacement`." + [string match replacement]) + +(defn upcase + "Converts string to uppercase. + (String/upcase \"hello\") ;=> \"HELLO\"" + [string]) + +(defn downcase + "Converts string to lowercase. + (String/downcase \"HELLO\") ;=> \"hello\"" + [string]) + +(defn capitalize + "Capitalizes the first character, downcases the rest. + (String/capitalize \"hello world\") ;=> \"Hello world\"" + [string]) + +(defn reverse + "Reverses the string (grapheme-aware for Unicode). + (String/reverse \"hello\") ;=> \"olleh\"" + [string]) + +(defn duplicate + "Repeats `string` `n` times. + (String/duplicate \"ha\" 3) ;=> \"hahaha\"" + [string n]) + +(defn pad-leading + "Pads `string` on the left to `count` characters. + (String/pad-leading \"13\" 5 \"0\") ;=> \"00013\"" + ([string count]) + ([string count padding])) + +(defn pad-trailing + "Pads `string` on the right to `count` characters. + (String/pad-trailing \"hi\" 5) ;=> \"hi \"" + ([string count]) + ([string count padding])) + +;; --- Trimming --- + +(defn trim + "Removes leading and trailing whitespace (or specified characters). + (String/trim \" hello \") ;=> \"hello\"" + ([string]) + ([string to-trim])) + +(defn trim-leading + "Removes leading whitespace. + (String/trim-leading \" hello\") ;=> \"hello\"" + ([string]) + ([string to-trim])) + +(defn trim-trailing + "Removes trailing whitespace. + (String/trim-trailing \"hello \") ;=> \"hello\"" + ([string]) + ([string to-trim])) + +;; --- Slicing & Access --- + +(defn slice + "Returns a substring starting at `start` for `length` characters. + (String/slice \"hello\" 1 3) ;=> \"ell\" + (String/slice \"hello\" 1..3) ;=> \"ell\"" + ([string range]) + ([string start length])) + +(defn at + "Returns the grapheme at `position`. Negative indices count from end. + (String/at \"hello\" 1) ;=> \"e\" + (String/at \"hello\" -1) ;=> \"o\"" + [string position]) + +(defn first + "Returns the first grapheme. + (String/first \"hello\") ;=> \"h\"" + [string]) + +(defn last + "Returns the last grapheme. + (String/last \"hello\") ;=> \"o\"" + [string]) + +(defn length + "Returns the number of Unicode graphemes. + (String/length \"héllo\") ;=> 5" + [string]) + +(defn byte-size + "Returns the number of bytes in the string. + (String/byte-size \"héllo\") ;=> 6 (é is 2 bytes in UTF-8)" + [string]) + +(defn graphemes + "Returns a list of grapheme clusters. + (String/graphemes \"hello\") ;=> [\"h\" \"e\" \"l\" \"l\" \"o\"]" + [string]) + +(defn codepoints + "Returns a list of codepoints. + (String/codepoints \"hello\") ;=> [\"h\" \"e\" \"l\" \"l\" \"o\"]" + [string]) + +(defn next-grapheme + "Returns tuple {grapheme rest} or nil. + (String/next-grapheme \"abc\") ;=> {\"a\" \"bc\"}" + [string]) + +(defn next-codepoint + "Returns tuple {codepoint rest} or nil." + [string]) + +;; --- Conversion --- + +(defn to-integer + "Converts string to integer. + (String/to-integer \"123\") ;=> 123 + (String/to-integer \"FF\" 16) ;=> 255" + ([string]) + ([string base])) + +(defn to-float + "Converts string to float. + (String/to-float \"3.14\") ;=> 3.14" + [string]) + +(defn to-atom + "Converts string to an existing atom. + (String/to-atom \"hello\") ;=> :hello" + [string]) + +(defn to-existing-atom + "Converts string to an existing atom. Raises if atom doesn't exist. + (String/to-existing-atom \"hello\") ;=> :hello" + [string]) + +(defn to-charlist + "Converts string to a charlist. + (String/to-charlist \"hello\") ;=> 'hello'" + [string]) + +(defn myers-difference + "Returns a keyword list of edit steps to transform string1 into string2. + (String/myers-difference \"abc\" \"adc\") ;=> [[:eq \"a\"] [:del \"b\"] [:ins \"d\"] [:eq \"c\"]]" + [string1 string2]) + +(defn valid? + "Returns true if `string` is a valid UTF-8 string. + (String/valid? \"hello\") ;=> true" + [string]) + +(defn printable? + "Returns true if `string` consists only of printable characters. + (String/printable? \"hello\") ;=> true" + [string]) + +(defn equivalent? + "Returns true if two strings are equivalent ignoring Unicode normalization differences." + [string1 string2]) + +(defn bag-distance + "Returns the bag distance between two strings (simple edit distance metric)." + [string1 string2]) + +(defn jaro-distance + "Returns the Jaro distance between two strings (0.0 to 1.0). + (String/jaro-distance \"Dwayne\" \"Duane\") ;=> 0.822..." + [string1 string2]) + +(defn chunk + "Splits string into chunks by character type. + (String/chunk \"abc123def\" :valid) ;=> [\"abc123def\"]" + [string mode]) diff --git a/stubs/Supervisor.clj b/stubs/Supervisor.clj new file mode 100644 index 0000000..fc47239 --- /dev/null +++ b/stubs/Supervisor.clj @@ -0,0 +1,57 @@ +(ns Supervisor + "Elixir Supervisor module — OTP supervisor for fault-tolerant process trees. + + In CljElixir: (Supervisor/start-link children opts), etc. + Strategies: :one-for-one, :one-for-all, :rest-for-one.") + +(defn start-link + "Starts a supervisor with child specifications. + (Supervisor/start-link [{MyWorker [arg1]}] :strategy :one-for-one) + (Supervisor/start-link MyApp [init-arg])" + ([children-or-module opts-or-arg]) + ([module arg opts])) + +(defn start-child + "Dynamically starts a child under the supervisor. + (Supervisor/start-child sup child-spec)" + [supervisor child-spec]) + +(defn terminate-child + "Terminates a child process. + (Supervisor/terminate-child sup child-id)" + [supervisor child-id]) + +(defn restart-child + "Restarts a terminated child. + (Supervisor/restart-child sup child-id)" + [supervisor child-id]) + +(defn delete-child + "Deletes a terminated child specification. + (Supervisor/delete-child sup child-id)" + [supervisor child-id]) + +(defn which-children + "Returns a list of all children with their info. + (Supervisor/which-children sup) ;=> [{id, pid, type, modules} ...]" + [supervisor]) + +(defn count-children + "Returns a map with child counts by status. + (Supervisor/count-children sup) ;=> %{active: 2, specs: 2, supervisors: 0, workers: 2}" + [supervisor]) + +(defn stop + "Stops the supervisor. + (Supervisor/stop sup) ;=> :ok" + ([supervisor]) + ([supervisor reason]) + ([supervisor reason timeout])) + +(defn child-spec + "Builds a child specification map." + [module overrides]) + +(defn init + "Returns supervisor init spec. For use in module-based supervisors." + [children-and-opts]) diff --git a/stubs/System.clj b/stubs/System.clj new file mode 100644 index 0000000..430a627 --- /dev/null +++ b/stubs/System.clj @@ -0,0 +1,176 @@ +(ns System + "Elixir System module — system-level information and operations. + + In CljElixir: (System/argv), (System/halt 0), etc.") + +(defn argv + "Returns command line arguments as a list of strings. + (System/argv) ;=> [\"--flag\" \"value\"]" + []) + +(defn halt + "Halts the Erlang runtime. `status` is 0 (success) or positive integer. + (System/halt 0) ;=> exits with code 0 + (System/halt 1) ;=> exits with code 1 + (System/halt \"crash message\") ;=> prints message and exits" + ([]) + ([status])) + +(defn stop + "Gracefully stops the system. Runs all shutdown hooks. + (System/stop 0)" + ([status])) + +(defn cmd + "Runs an external command. Returns {output, exit_status}. + (System/cmd \"echo\" [\"hello\"]) ;=> {\"hello\\n\" 0} + (System/cmd \"ls\" [\"-la\"] :cd \"/tmp\") ;=> with options" + ([command args]) + ([command args opts])) + +(defn shell + "Runs a shell command. Returns {output, exit_status}. + (System/shell \"echo hello && echo world\") ;=> {\"hello\\nworld\\n\" 0}" + ([command]) + ([command opts])) + +(defn get-env + "Gets an environment variable. Returns nil if not set. + (System/get-env \"HOME\") ;=> \"/Users/ajet\" + (System/get-env \"MISSING\" \"default\") ;=> \"default\" + (System/get-env) ;=> all env vars as a map" + ([]) + ([varname]) + ([varname default])) + +(defn put-env + "Sets an environment variable. + (System/put-env \"MY_VAR\" \"value\") ;=> :ok + (System/put-env %{\"KEY1\" \"val1\" \"KEY2\" \"val2\"}) ;=> :ok" + ([varname value]) + ([env-map])) + +(defn delete-env + "Deletes an environment variable. + (System/delete-env \"MY_VAR\") ;=> :ok" + [varname]) + +(defn fetch-env + "Fetches an env var. Returns {:ok value} or :error. + (System/fetch-env \"HOME\") ;=> {:ok \"/Users/ajet\"}" + [varname]) + +(defn fetch-env! + "Fetches an env var. Raises if not set. + (System/fetch-env! \"HOME\") ;=> \"/Users/ajet\"" + [varname]) + +(defn cwd + "Returns the current working directory. + (System/cwd) ;=> \"/Users/ajet/repos/clje\"" + []) + +(defn cwd! + "Returns the current working directory. Raises on error." + []) + +(defn tmp-dir + "Returns the system temporary directory. + (System/tmp-dir) ;=> \"/tmp\"" + []) + +(defn tmp-dir! + "Returns the system temporary directory. Raises on error." + []) + +(defn user-home + "Returns the user's home directory. + (System/user-home) ;=> \"/Users/ajet\"" + []) + +(defn user-home! + "Returns the user's home directory. Raises on error." + []) + +(defn monotonic-time + "Returns monotonic time. Useful for measuring elapsed time. + (System/monotonic-time) ;=> nanoseconds + (System/monotonic-time :millisecond) ;=> milliseconds" + ([]) + ([unit])) + +(defn system-time + "Returns system time (wall clock, not monotonic). + (System/system-time) ;=> nanoseconds + (System/system-time :second) ;=> seconds since epoch" + ([]) + ([unit])) + +(defn os-time + "Returns OS time. + (System/os-time :second) ;=> seconds since epoch" + ([]) + ([unit])) + +(defn unique-integer + "Returns a unique integer. + (System/unique-integer) ;=> -576460752303423485 + (System/unique-integer [:positive :monotonic]) ;=> 1" + ([]) + ([modifiers])) + +(defn version + "Returns the Elixir version string. + (System/version) ;=> \"1.19.5\"" + []) + +(defn otp-release + "Returns the OTP release number as a string. + (System/otp-release) ;=> \"28\"" + []) + +(defn build-info + "Returns a map with build info." + []) + +(defn schedulers + "Returns the number of schedulers. + (System/schedulers) ;=> 8" + []) + +(defn schedulers-online + "Returns the number of online schedulers. + (System/schedulers-online) ;=> 8" + []) + +(defn pid + "Returns the PID of the OS process running the VM. + (System/pid) ;=> \"12345\"" + []) + +(defn at-exit + "Registers a function to run at VM exit. + (System/at-exit (fn [status] (IO/puts \"bye\")))" + [fun]) + +(defn stacktrace + "Returns the last exception stacktrace for the calling process." + []) + +(defn no-halt + "Configures whether the system halts on script completion." + ([status])) + +(defn trap-signal + "Traps an OS signal. Returns {:ok fun} or {:already-registered fun}. + (System/trap-signal :sigterm (fn [] (System/stop 0)))" + [signal fun]) + +(defn untrap-signal + "Removes a previously registered signal trap." + [signal id]) + +(defn compiled-endianness + "Returns the endianness (:big or :little). + (System/compiled-endianness) ;=> :little" + []) diff --git a/stubs/Task.clj b/stubs/Task.clj new file mode 100644 index 0000000..c218879 --- /dev/null +++ b/stubs/Task.clj @@ -0,0 +1,94 @@ +(ns Task + "Elixir Task module — convenient process abstraction for async computation. + + In CljElixir: (Task/async (fn [] (expensive-work))), etc. + Tasks are processes meant to run a single action and return a result.") + +(defn async + "Starts a task linked to the caller. Returns a Task struct. + (let [t (Task/async (fn [] (compute-result)))] + (Task/await t)) ;=> result" + ([fun]) + ([module function-name args])) + +(defn await + "Awaits a task reply. Raises on timeout (default 5000ms). + (Task/await task) ;=> result + (Task/await task 10000) ;=> with 10s timeout" + ([task]) + ([task timeout])) + +(defn await-many + "Awaits multiple tasks. Returns a list of results. + (Task/await-many [task1 task2 task3]) ;=> [result1 result2 result3] + (Task/await-many tasks 10000) ;=> with 10s timeout" + ([tasks]) + ([tasks timeout])) + +(defn yield + "Temporarily yields for a reply. Returns {:ok result}, {:exit reason}, or nil. + (Task/yield task 5000) ;=> {:ok result} or nil (if still running)" + ([task]) + ([task timeout])) + +(defn yield-many + "Yields on multiple tasks. Returns [{:ok result} | {:exit reason} | nil ...]. + (Task/yield-many [task1 task2] 5000)" + ([tasks]) + ([tasks opts-or-timeout])) + +(defn start + "Starts a task that is not linked. Useful for fire-and-forget. + (Task/start (fn [] (send-email)))" + ([fun]) + ([module function-name args])) + +(defn start-link + "Starts a linked task. Useful under a supervisor. + (Task/start-link (fn [] (do-work)))" + ([fun]) + ([module function-name args])) + +(defn shutdown + "Shuts down a task. Returns {:ok reply}, {:exit reason}, or nil. + (Task/shutdown task) ;=> {:ok result} + (Task/shutdown task :brutal-kill) ;=> force shutdown" + ([task]) + ([task timeout-or-brutal])) + +(defn ignore + "Ignores an existing task. The task continues but its result is discarded. + (Task/ignore task)" + [task]) + +(defn completed + "Creates an already-completed task with the given `result`. + (Task/completed {:ok \"cached\"}) ;=> a task that immediately resolves" + [result]) + +(defn async-stream + "Returns a stream that runs the given function concurrently on each element. + (Task/async-stream [1 2 3] (fn [x] (expensive x)) :max-concurrency 4)" + ([enumerable fun]) + ([enumerable fun opts]) + ([enumerable module function-name opts])) + +(defn Supervisor.start-link + "Starts a Task.Supervisor. Use with async_nolink for fault-tolerant tasks." + ([opts])) + +(defn Supervisor.async + "Starts an async task under a Task.Supervisor. + (Task/Supervisor.async supervisor (fn [] (do-work)))" + ([supervisor fun]) + ([supervisor fun opts]) + ([supervisor module fun args]) + ([supervisor module fun args opts])) + +(defn Supervisor.async-nolink + "Like Supervisor.async but doesn't link. Safe for untrusted work. + (Task/Supervisor.async-nolink supervisor (fn [] (risky-work)))" + ([supervisor fun]) + ([supervisor fun opts]) + ([supervisor module fun args]) + ([supervisor module fun args opts])) diff --git a/stubs/Time.clj b/stubs/Time.clj new file mode 100644 index 0000000..8f35509 --- /dev/null +++ b/stubs/Time.clj @@ -0,0 +1,111 @@ +(ns Time + "Elixir Time module — time-of-day operations. + + In CljElixir: (Time/utc-now), (Time/new 12 30 0), etc.") + +(defn utc-now + "Returns the current UTC time. + (Time/utc-now) ;=> ~T[12:30:00]" + ([]) + ([calendar])) + +(defn new + "Creates a new time. + (Time/new 12 30 0) ;=> {:ok ~T[12:30:00]}" + ([hour minute second]) + ([hour minute second microsecond]) + ([hour minute second microsecond calendar])) + +(defn new! + "Creates a new time. Raises on error. + (Time/new! 12 30 0) ;=> ~T[12:30:00]" + ([hour minute second]) + ([hour minute second microsecond]) + ([hour minute second microsecond calendar])) + +(defn from-iso8601 + "Parses ISO 8601 time. Returns {:ok time} or {:error reason}. + (Time/from-iso8601 \"12:30:00\") ;=> {:ok ~T[12:30:00]}" + ([string]) + ([string calendar])) + +(defn from-iso8601! + "Parses ISO 8601 time. Raises on error." + ([string]) + ([string calendar])) + +(defn to-iso8601 + "Converts to ISO 8601 string. + (Time/to-iso8601 time) ;=> \"12:30:00\"" + ([time]) + ([time format])) + +(defn to-string + "Converts to string. + (Time/to-string time) ;=> \"12:30:00\"" + [time]) + +(defn add + "Adds `amount` of time. + (Time/add time 3600) ;=> +1 hour + (Time/add time 30 :minute)" + ([time amount]) + ([time amount unit])) + +(defn diff + "Returns difference between two times. + (Time/diff t1 t2) ;=> seconds" + ([time1 time2]) + ([time1 time2 unit])) + +(defn truncate + "Truncates to given precision. + (Time/truncate time :second)" + [time precision]) + +(defn compare + "Compares two times. Returns :lt, :eq, or :gt." + [time1 time2]) + +(defn before? + "Returns true if `time1` is before `time2`." + [time1 time2]) + +(defn after? + "Returns true if `time1` is after `time2`." + [time1 time2]) + +(defn shift + "Shifts time by a duration. + (Time/shift time :hour 1 :minute -15)" + [time duration]) + +(defn from-erl + "Converts Erlang time tuple. + (Time/from-erl {12 30 0}) ;=> {:ok ~T[12:30:00]}" + ([tuple]) + ([tuple microsecond]) + ([tuple microsecond calendar])) + +(defn from-erl! + "Converts Erlang time tuple. Raises on error." + ([tuple]) + ([tuple microsecond]) + ([tuple microsecond calendar])) + +(defn to-erl + "Converts to Erlang time tuple. + (Time/to-erl time) ;=> {12 30 0}" + [time]) + +(defn from-seconds-after-midnight + "Creates time from seconds after midnight. + (Time/from-seconds-after-midnight 45000) ;=> {:ok ~T[12:30:00]}" + ([seconds]) + ([seconds microsecond]) + ([seconds microsecond calendar])) + +(defn to-seconds-after-midnight + "Returns seconds since midnight. + (Time/to-seconds-after-midnight time) ;=> 45000" + [time]) diff --git a/stubs/Tuple.clj b/stubs/Tuple.clj new file mode 100644 index 0000000..d2e8d61 --- /dev/null +++ b/stubs/Tuple.clj @@ -0,0 +1,39 @@ +(ns Tuple + "Elixir Tuple module — operations on tuples. + + In CljElixir: (Tuple/to-list tup), (Tuple/append tup elem), etc. + Tuples are fixed-size, contiguous containers on the BEAM. + Create tuples with #el[...] syntax in CljElixir.") + +(defn to-list + "Converts a tuple to a list. + (Tuple/to-list #el[1 2 3]) ;=> [1 2 3]" + [tuple]) + +(defn append + "Appends `value` to `tuple`. + (Tuple/append #el[1 2] 3) ;=> #el[1 2 3]" + [tuple value]) + +(defn insert-at + "Inserts `value` at `index` in `tuple`. + (Tuple/insert-at #el[1 2 3] 1 :a) ;=> #el[1 :a 2 3]" + [tuple index value]) + +(defn delete-at + "Deletes element at `index` from `tuple`. + (Tuple/delete-at #el[1 2 3] 1) ;=> #el[1 3]" + [tuple index]) + +(defn duplicate + "Creates a tuple with `data` repeated `size` times. + (Tuple/duplicate :ok 3) ;=> #el[:ok :ok :ok]" + [data size]) + +(defn product + "Returns the product of all numeric elements in the tuple." + [tuple]) + +(defn sum + "Returns the sum of all numeric elements in the tuple." + [tuple]) diff --git a/stubs/binary.clj b/stubs/binary.clj new file mode 100644 index 0000000..e6b9e9c --- /dev/null +++ b/stubs/binary.clj @@ -0,0 +1,101 @@ +(ns binary + "Erlang :binary module — binary data operations. + + In CljElixir: (binary/split data pattern), (binary/part data pos len), etc. + Efficient binary search, split, and manipulation.") + +(defn split + "Splits binary by pattern. Returns list of parts. + (binary/split \"a,b,c\" \",\") ;=> [\"a\" \"b,c\"] + (binary/split \"a,b,c\" \",\" [:global]) ;=> [\"a\" \"b\" \"c\"]" + ([subject pattern]) + ([subject pattern opts])) + +(defn part + "Extracts a part of a binary. + (binary/part \"hello\" 1 3) ;=> \"ell\" + (binary/part \"hello\" {1 3}) ;=> \"ell\"" + ([subject pos-len]) + ([subject pos len])) + +(defn match + "Finds the first/all occurrences of pattern in binary. + (binary/match \"hello world\" \"world\") ;=> {6 5} + (binary/match \"abcabc\" \"a\" [:global]) ;=> [... all positions ...]" + ([subject pattern]) + ([subject pattern opts])) + +(defn matches + "Returns all matches (like match with :global). + (binary/matches \"abcabc\" \"a\") ;=> [{0 1} {3 1}]" + ([subject pattern]) + ([subject pattern opts])) + +(defn replace + "Replaces pattern in binary. + (binary/replace \"hello\" \"l\" \"L\" [:global]) ;=> \"heLLo\"" + ([subject pattern replacement]) + ([subject pattern replacement opts])) + +(defn at + "Returns the byte at position. + (binary/at \"hello\" 0) ;=> 104" + [subject position]) + +(defn first + "Returns the first byte. + (binary/first \"hello\") ;=> 104" + [subject]) + +(defn last + "Returns the last byte. + (binary/last \"hello\") ;=> 111" + [subject]) + +(defn bin-to-list + "Converts binary to list of bytes. + (binary/bin-to-list \"hello\") ;=> [104 101 108 108 111]" + ([subject]) + ([subject pos-len]) + ([subject pos len])) + +(defn list-to-bin + "Converts byte list to binary. + (binary/list-to-bin [104 101 108 108 111]) ;=> \"hello\"" + [byte-list]) + +(defn copy + "Copies a binary, optionally repeating it. + (binary/copy \"ab\" 3) ;=> \"ababab\"" + ([subject]) + ([subject n])) + +(defn decode-unsigned + "Decodes an unsigned integer from binary. + (binary/decode-unsigned <<0 0 0 42>>) ;=> 42" + ([subject]) + ([subject endianness])) + +(defn encode-unsigned + "Encodes an unsigned integer to binary. + (binary/encode-unsigned 42) ;=> <<42>>" + ([value]) + ([value endianness])) + +(defn longest-common-prefix + "Returns the length of the longest common prefix of binaries. + (binary/longest-common-prefix [\"abc\" \"abd\"]) ;=> 2" + [binaries]) + +(defn longest-common-suffix + "Returns the length of the longest common suffix." + [binaries]) + +(defn compile-pattern + "Pre-compiles a search pattern for repeated use. + (let [pat (binary/compile-pattern \",\")] (binary/split data pat))" + [pattern]) + +(defn referenced-byte-size + "Returns the size of the referenced binary (before sub-binary optimization)." + [binary]) diff --git a/stubs/calendar.clj b/stubs/calendar.clj new file mode 100644 index 0000000..582bcb7 --- /dev/null +++ b/stubs/calendar.clj @@ -0,0 +1,123 @@ +(ns calendar + "Erlang :calendar module — date and time calculations. + + In CljElixir: (calendar/local-time), (calendar/universal-time), etc. + Works with Erlang date/time tuples: {Year Month Day} and {Hour Min Sec}.") + +(defn local-time + "Returns the current local datetime as {{Y M D} {H M S}}. + (calendar/local-time) ;=> {{2024 3 9} {12 30 0}}" + []) + +(defn universal-time + "Returns the current UTC datetime as {{Y M D} {H M S}}. + (calendar/universal-time) ;=> {{2024 3 9} {12 30 0}}" + []) + +(defn local-time-to-universal-time-dst + "Converts local time to UTC, handling DST. Returns list of possible results." + [datetime]) + +(defn universal-time-to-local-time + "Converts UTC to local time." + [datetime]) + +(defn now-to-datetime + "Converts erlang:now/0 tuple to datetime tuple." + [now]) + +(defn now-to-local-time + "Converts erlang:now/0 to local datetime." + [now]) + +(defn now-to-universal-time + "Converts erlang:now/0 to UTC datetime." + [now]) + +(defn datetime-to-gregorian-seconds + "Converts {{Y M D} {H M S}} to Gregorian seconds (since year 0). + (calendar/datetime-to-gregorian-seconds {{2024 1 1} {0 0 0}}) ;=> 63871..." + [datetime]) + +(defn gregorian-seconds-to-datetime + "Converts Gregorian seconds back to {{Y M D} {H M S}}. + (calendar/gregorian-seconds-to-datetime 63871...)" + [seconds]) + +(defn date-to-gregorian-days + "Converts {Year Month Day} to Gregorian day count. + (calendar/date-to-gregorian-days {2024 1 1})" + ([date]) + ([year month day])) + +(defn gregorian-days-to-date + "Converts Gregorian day count to {Year Month Day}." + [days]) + +(defn day-of-the-week + "Returns day of week (1=Monday, 7=Sunday). + (calendar/day-of-the-week {2024 3 9}) ;=> 6" + ([date]) + ([year month day])) + +(defn is-leap-year + "Returns true if year is a leap year. + (calendar/is-leap-year 2024) ;=> true" + [year]) + +(defn last-day-of-the-month + "Returns the last day of the month. + (calendar/last-day-of-the-month 2024 2) ;=> 29" + [year month]) + +(defn valid-date + "Returns true if the date is valid. + (calendar/valid-date {2024 2 29}) ;=> true" + ([date]) + ([year month day])) + +(defn iso-week-number + "Returns {year week} for a date. + (calendar/iso-week-number {2024 3 9}) ;=> {2024 10}" + ([date]) + ([year month day])) + +(defn time-difference + "Returns the time difference between two datetimes. + (calendar/time-difference dt1 dt2) ;=> {days {hours mins secs}}" + [datetime1 datetime2]) + +(defn seconds-to-daystime + "Converts seconds to {days {hours minutes seconds}}. + (calendar/seconds-to-daystime 90061) ;=> {1 {1 1 1}}" + [seconds]) + +(defn seconds-to-time + "Converts seconds to {hours minutes seconds}. + (calendar/seconds-to-time 3661) ;=> {1 1 1}" + [seconds]) + +(defn time-to-seconds + "Converts {hours minutes seconds} to seconds. + (calendar/time-to-seconds {1 1 1}) ;=> 3661" + [time]) + +(defn system-time-to-local-time + "Converts system time to local datetime." + [time unit]) + +(defn system-time-to-universal-time + "Converts system time to UTC datetime." + [time unit]) + +(defn rfc3339-to-system-time + "Parses RFC 3339 timestamp to system time. + (calendar/rfc3339-to-system-time \"2024-03-09T12:00:00Z\") ;=> integer" + ([string]) + ([string opts])) + +(defn system-time-to-rfc3339 + "Converts system time to RFC 3339 string. + (calendar/system-time-to-rfc3339 time) ;=> \"2024-03-09T12:00:00Z\"" + ([time]) + ([time opts])) diff --git a/stubs/clje/core.clj b/stubs/clje/core.clj new file mode 100644 index 0000000..3bc2a82 --- /dev/null +++ b/stubs/clje/core.clj @@ -0,0 +1,367 @@ +(ns clje.core + "CljElixir core — stubs for clj-kondo/clojure-lsp. + + These are builtin functions and special forms handled directly by the + CljElixir transformer. Standard Clojure forms (defn, let, fn, if, etc.) + are mapped via :lint-as in .clj-kondo/config.edn." + (:refer-clojure :exclude [case send pr pr-str prn print-str + vec vector subvec vector? + str println cons list + inc dec + map filter concat take drop + sort sort-by group-by frequencies distinct + mapcat partition + keys vals select-keys merge into + get assoc dissoc update get-in assoc-in update-in + count first rest seq + conj nth peek pop + reduce reduce-kv + contains? empty? nil? + not= with-open throw])) + +;; ===== Special Forms (hooks handle these) ===== +(defmacro receive + "BEAM message receive with pattern matching. + (receive + [:hello sender] (send sender :hi) + [:quit] (System/halt 0) + :after 5000 (println \"timeout\"))" + [& clauses]) +(defmacro defmodule + "Defines an Elixir module. + (defmodule MyModule + (defn my-func [x] (* x 2)))" + [name & body]) +(defmacro case + "Pattern matching (not constant matching like Clojure). + (case val + [a b] (+ a b) + {:key v} v + _ :default)" + [expr & clauses]) +(defmacro with + "Monadic binding — chains pattern matches, short-circuits on mismatch. + (with [{:ok val1} (step1) + {:ok val2} (step2 val1)] + (+ val1 val2))" + [bindings & body]) +(defmacro ns + "Declares the module namespace (primary module declaration). + (ns MyApp.Router + (require [Logger]) + (use [CljElixir.Core]))" + [name & body]) + +;; ===== BEAM Concurrency ===== +(defn spawn + "Spawns a new BEAM process. Returns PID. + (spawn (fn [] (IO/puts \"hello from new process\")))" + [f]) +(defn send + "Sends a message to a process. Returns the message. + (send pid {:hello \"world\"})" + [pid msg]) +(defn monitor + "Monitors a process. Returns a reference. + (monitor pid) + (monitor :process pid)" + ([pid]) ([type pid])) +(defn link + "Creates a bidirectional link between processes. + (link pid)" + [pid]) +(defn unlink + "Removes a link between processes. + (unlink pid)" + [pid]) +(defn alive? + "Returns true if the process is alive. + (alive? pid) ;=> true" + [pid]) + +;; ===== Arithmetic ===== +(defn inc + "Increments by 1. + (inc 5) ;=> 6" + [x]) +(defn dec + "Decrements by 1. + (dec 5) ;=> 4" + [x]) + +;; ===== String & Output ===== +(defn str + "Concatenates arguments into a string. + (str \"hello\" \" \" \"world\") ;=> \"hello world\"" + [& args]) +(defn println + "Prints arguments followed by newline. + (println \"hello\")" + [& args]) +(defn pr-str + "Returns a string representation (EDN-like). + (pr-str {:a 1}) ;=> \"{:a 1}\"" + [& args]) +(defn prn + "Prints EDN representation followed by newline." + [& args]) +(defn pr + "Prints EDN representation (no newline)." + [& args]) +(defn print-str + "Returns a human-readable string." + [& args]) + +;; ===== BEAM Types & Interop ===== +(defn tuple + "Creates a BEAM tuple from arguments. + (tuple :ok \"value\") ;=> #el[:ok \"value\"]" + [& args]) +(defn clojurify + "Converts Elixir types to Clojure equivalents (keyword lists → maps, etc.). + (clojurify elixir-val)" + [val]) +(defn elixirify + "Converts Clojure types to Elixir equivalents (maps → keyword lists, etc.). + (elixirify clj-val)" + [val]) +(defn hd + "Returns the head (first element) of a list. + (hd [1 2 3]) ;=> 1" + [coll]) +(defn tl + "Returns the tail (rest) of a list. + (tl [1 2 3]) ;=> [2 3]" + [coll]) +(defn cons + "Prepends an element to a list. + (cons 1 [2 3]) ;=> [1 2 3]" + [head tail]) +(defn list + "Creates a list from arguments. + (list 1 2 3) ;=> [1 2 3]" + [& args]) + +;; ===== BEAM Type Guards ===== +(defn is-binary + "Returns true if `x` is a binary (string). Allowed in guards. + (is-binary \"hello\") ;=> true" + [x]) +(defn is-integer + "Returns true if `x` is an integer. Allowed in guards." + [x]) +(defn is-float + "Returns true if `x` is a float. Allowed in guards." + [x]) +(defn is-number + "Returns true if `x` is a number. Allowed in guards." + [x]) +(defn is-atom + "Returns true if `x` is an atom. Allowed in guards." + [x]) +(defn is-list + "Returns true if `x` is a list. Allowed in guards." + [x]) +(defn is-map + "Returns true if `x` is a map. Allowed in guards." + [x]) +(defn is-tuple + "Returns true if `x` is a tuple. Allowed in guards." + [x]) +(defn is-pid + "Returns true if `x` is a PID. Allowed in guards." + [x]) +(defn is-boolean + "Returns true if `x` is a boolean. Allowed in guards." + [x]) +(defn is-nil + "Returns true if `x` is nil. Allowed in guards." + [x]) +(defn is-function + "Returns true if `x` is a function, optionally with given `arity`. + (is-function f) ;=> true + (is-function f 2) ;=> true if f takes 2 args" + ([x]) ([x arity])) + +;; ===== Vectors ===== +(defn vec + "Converts a collection to a PersistentVector. + (vec [1 2 3])" + [coll]) +(defn vector + "Creates a PersistentVector from arguments. + (vector 1 2 3)" + [& args]) +(defn subvec + "Returns a subvector from `start` to `end`. + (subvec v 1 3)" + ([v start]) ([v start end])) +(defn vector? + "Returns true if `x` is a PersistentVector. + (vector? (vector 1 2 3)) ;=> true" + [x]) + +;; ===== Core Data Operations ===== +(defn get + "Gets value at `key` from collection. Returns `default` if missing. + (get {:a 1} :a) ;=> 1 + (get {:a 1} :b :not-found) ;=> :not-found" + ([coll key]) ([coll key default])) +(defn assoc + "Associates `key` with `val` in collection. + (assoc {:a 1} :b 2) ;=> {:a 1 :b 2}" + [coll key val]) +(defn dissoc + "Dissociates `key` from collection. + (dissoc {:a 1 :b 2} :a) ;=> {:b 2}" + [coll key]) +(defn update + "Updates value at `key` by applying `f`. + (update {:a 1} :a inc) ;=> {:a 2}" + ([coll key f]) ([coll key f & args])) +(defn get-in + "Gets value at nested `path`. + (get-in {:a {:b 1}} [:a :b]) ;=> 1" + ([coll path]) ([coll path default])) +(defn assoc-in + "Associates value at nested `path`. + (assoc-in {} [:a :b] 1) ;=> {:a {:b 1}}" + [coll path val]) +(defn update-in + "Updates value at nested `path` by applying `f`. + (update-in {:a {:b 1}} [:a :b] inc) ;=> {:a {:b 2}}" + [coll path f]) +(defn contains? + "Returns true if `coll` contains `key`. + (contains? {:a 1} :a) ;=> true" + [coll key]) +(defn empty? + "Returns true if `coll` is empty. + (empty? []) ;=> true" + [coll]) +(defn nil? + "Returns true if `x` is nil. + (nil? nil) ;=> true" + [x]) +(defn count + "Returns the number of elements. + (count [1 2 3]) ;=> 3" + [coll]) +(defn first + "Returns the first element. + (first [1 2 3]) ;=> 1" + [coll]) +(defn rest + "Returns all but the first element. + (rest [1 2 3]) ;=> [2 3]" + [coll]) +(defn seq + "Returns a seq on the collection, or nil if empty. + (seq [1 2 3]) ;=> (1 2 3)" + [coll]) +(defn conj + "Adds element to collection (position depends on type). + (conj [1 2] 3) ;=> [1 2 3]" + [coll x]) +(defn nth + "Returns element at `index`. + (nth [10 20 30] 1) ;=> 20" + ([coll index]) ([coll index not-found])) +(defn peek + "Returns the most accessible element. + (peek [1 2 3]) ;=> 3" + [coll]) +(defn pop + "Returns collection without the most accessible element. + (pop [1 2 3]) ;=> [1 2]" + [coll]) + +;; ===== Reducing ===== +(defn reduce + "Reduces collection with `f`. + (reduce + [1 2 3]) ;=> 6 + (reduce + 0 [1 2 3]) ;=> 6" + ([f coll]) ([f init coll])) +(defn reduce-kv + "Reduces a map with `f` receiving (acc, key, value). + (reduce-kv (fn [acc k v] (+ acc v)) 0 {:a 1 :b 2}) ;=> 3" + [f init coll]) + +;; ===== Sequence Operations ===== +(defn map + "Applies `f` to each element, returns a list. + (map inc [1 2 3]) ;=> [2 3 4]" + [f coll]) +(defn filter + "Returns elements where `f` returns truthy. + (filter (fn [x] (> x 2)) [1 2 3 4]) ;=> [3 4]" + [f coll]) +(defn concat + "Concatenates collections. + (concat [1 2] [3 4]) ;=> [1 2 3 4]" + [& colls]) +(defn take + "Returns first `n` elements. + (take 2 [1 2 3 4]) ;=> [1 2]" + [n coll]) +(defn drop + "Drops first `n` elements. + (drop 2 [1 2 3 4]) ;=> [3 4]" + [n coll]) +(defn sort + "Sorts a collection. + (sort [3 1 2]) ;=> [1 2 3] + (sort > [3 1 2]) ;=> [3 2 1]" + ([coll]) ([comp coll])) +(defn sort-by + "Sorts by the result of `keyfn`. + (sort-by :name [{:name \"b\"} {:name \"a\"}])" + ([keyfn coll]) ([keyfn comp coll])) +(defn group-by + "Groups elements by the result of `f`. + (group-by even? [1 2 3 4]) ;=> {false [1 3] true [2 4]}" + [f coll]) +(defn frequencies + "Returns a map of element → count. + (frequencies [:a :b :a]) ;=> {:a 2 :b 1}" + [coll]) +(defn distinct + "Returns unique elements. + (distinct [1 2 1 3]) ;=> [1 2 3]" + [coll]) +(defn mapcat + "Maps then concatenates results. + (mapcat (fn [x] [x x]) [1 2 3]) ;=> [1 1 2 2 3 3]" + [f coll]) +(defn partition + "Partitions collection into groups of `n`. + (partition 2 [1 2 3 4]) ;=> [[1 2] [3 4]]" + ([n coll]) ([n step coll]) ([n step pad coll])) +(defn keys + "Returns keys of a map. + (keys {:a 1 :b 2}) ;=> [:a :b]" + [m]) +(defn vals + "Returns values of a map. + (vals {:a 1 :b 2}) ;=> [1 2]" + [m]) +(defn select-keys + "Selects only specified `ks` from map. + (select-keys {:a 1 :b 2 :c 3} [:a :c]) ;=> {:a 1 :c 3}" + [m ks]) +(defn merge + "Merges maps. Later maps take precedence. + (merge {:a 1} {:b 2}) ;=> {:a 1 :b 2}" + [& maps]) +(defn into + "Pours elements from `from` into `to`. + (into {} [[:a 1] [:b 2]]) ;=> {:a 1 :b 2}" + [to from]) +(defn not= + "Returns true if arguments are not equal. + (not= 1 2) ;=> true" + [& args]) +(defn throw + "Throws a value (caught by try/catch :throw). + (throw :some-error)" + [value]) diff --git a/stubs/crypto.clj b/stubs/crypto.clj new file mode 100644 index 0000000..e0ff6cf --- /dev/null +++ b/stubs/crypto.clj @@ -0,0 +1,120 @@ +(ns crypto + "Erlang :crypto module — cryptographic functions. + + In CljElixir: (crypto/hash :sha256 data), (crypto/strong-rand-bytes 16), etc. + Wraps OpenSSL for hashing, encryption, and random number generation.") + +(defn hash + "Computes a hash digest. + (crypto/hash :sha256 \"hello\") ;=> <> + Algorithms: :md5, :sha, :sha224, :sha256, :sha384, :sha512, :sha3-256, etc." + [type data]) + +(defn mac + "Computes a Message Authentication Code. + (crypto/mac :hmac :sha256 key data)" + ([type sub-type key data]) + ([type sub-type key data mac-length])) + +(defn hash-init + "Initializes incremental hashing. + (crypto/hash-init :sha256)" + [type]) + +(defn hash-update + "Updates incremental hash with more data. + (crypto/hash-update state data)" + [state data]) + +(defn hash-final + "Finalizes incremental hash. Returns the digest." + [state]) + +(defn strong-rand-bytes + "Generates `n` cryptographically strong random bytes. + (crypto/strong-rand-bytes 16) ;=> <<16 random bytes>>" + [n]) + +(defn crypto-one-time + "One-shot symmetric encryption/decryption. + (crypto/crypto-one-time :aes-256-ctr key iv data true) ;=> encrypted" + ([cipher key iv data encrypt-flag]) + ([cipher key data encrypt-flag])) + +(defn crypto-one-time-aead + "One-shot AEAD encryption/decryption (e.g., AES-GCM). + (crypto/crypto-one-time-aead :aes-256-gcm key iv data aad true)" + ([cipher key iv data aad encrypt-flag]) + ([cipher key iv data aad tag-length encrypt-flag])) + +(defn crypto-init + "Initializes streaming encryption/decryption. + 3-arity: (crypto/crypto-init cipher key encrypt-flag) + 4-arity: (crypto/crypto-init cipher key iv encrypt-flag-or-opts)" + ([cipher key encrypt-flag]) + ([cipher key iv encrypt-flag-or-opts])) + +(defn crypto-update + "Updates streaming encryption with more data." + [state data]) + +(defn crypto-final + "Finalizes streaming encryption." + [state]) + +(defn sign + "Creates a digital signature. + (crypto/sign :rsa :sha256 data private-key)" + ([algorithm digest-type data key]) + ([algorithm digest-type data key opts])) + +(defn verify + "Verifies a digital signature. + (crypto/verify :rsa :sha256 data signature public-key)" + ([algorithm digest-type data signature key]) + ([algorithm digest-type data signature key opts])) + +(defn generate-key + "Generates a key pair. + (crypto/generate-key :ecdh :secp256r1)" + ([type params]) + ([type params private-key])) + +(defn compute-key + "Computes shared secret from key exchange." + ([type others-public-key my-private-key params]) + ([type others-public-key my-private-key shared-info params])) + +(defn supports + "Returns lists of supported algorithms. + (crypto/supports) ;=> [{:ciphers [...]}, {:hashs [...]}, ...]" + ([]) + ([category])) + +(defn hash-info + "Returns information about a hash algorithm." + [type]) + +(defn cipher-info + "Returns information about a cipher." + [cipher]) + +(defn ec-curves + "Returns supported elliptic curves." + []) + +(defn rand-seed + "Seeds the random number generator. + (crypto/rand-seed seed)" + ([seed]) + ([alg-or-state seed])) + +(defn rand-uniform + "Returns a random integer in 1..n. + (crypto/rand-uniform 100) ;=> 42" + [n]) + +(defn exor + "XORs two equal-length binaries. + (crypto/exor bin1 bin2)" + [bin1 bin2]) diff --git a/stubs/erl_io.clj b/stubs/erl_io.clj new file mode 100644 index 0000000..cda4236 --- /dev/null +++ b/stubs/erl_io.clj @@ -0,0 +1,93 @@ +(ns io + "Erlang :io module — I/O protocol operations. + + In CljElixir: (io/format \"Hello ~s!~n\" [\"world\"]), etc. + Lower-level than Elixir's IO module. Uses charlists and format strings.") + +(defn format + "Formatted output (like C printf). + (io/format \"Hello ~s!~n\" [\"world\"]) ;=> prints 'Hello world!\\n' + (io/format device \"~p~n\" [term]) ;=> pretty-print to device + + Common format specs: + ~s string ~w write (Erlang term) + ~p pretty-print ~f float + ~e scientific ~b integer base 10 + ~.Xb integer base X ~n newline + ~c character ~i ignore" + ([format args]) + ([device format args])) + +(defn fwrite + "Like format but returns :ok or {:error reason}." + ([format args]) + ([device format args])) + +(defn get-line + "Reads a line from standard input. Returns charlist or :eof. + (io/get-line \"prompt> \")" + ([prompt]) + ([device prompt])) + +(defn put-chars + "Writes characters to the IO device. + (io/put-chars \"hello\")" + ([chars]) + ([device chars])) + +(defn nl + "Writes a newline. + (io/nl)" + ([]) + ([device])) + +(defn read + "Reads an Erlang term from input. Returns {:ok term} or {:error reason}. + (io/read \"enter term> \")" + ([prompt]) + ([device prompt])) + +(defn write + "Writes an Erlang term. + (io/write {:a 1}) ;=> prints '{a,1}'" + ([term]) + ([device term])) + +(defn scan-erl-form + "Scans an Erlang form from input." + ([prompt]) + ([device prompt]) + ([device prompt start-line])) + +(defn parse-erl-form + "Parses an Erlang form from input." + ([prompt]) + ([device prompt]) + ([device prompt start-line])) + +(defn setopts + "Sets IO device options. + (io/setopts [{:encoding :unicode}])" + ([opts]) + ([device opts])) + +(defn getopts + "Gets IO device options." + ([]) + ([device])) + +(defn columns + "Returns the terminal column count. + (io/columns) ;=> {:ok 120}" + ([]) + ([device])) + +(defn rows + "Returns the terminal row count. + (io/rows) ;=> {:ok 40}" + ([]) + ([device])) + +(defn printable-range + "Returns the printable character range (:unicode or :latin1)." + []) diff --git a/stubs/erlang.clj b/stubs/erlang.clj new file mode 100644 index 0000000..db60e41 --- /dev/null +++ b/stubs/erlang.clj @@ -0,0 +1,558 @@ +(ns erlang + "Erlang :erlang module — BEAM runtime BIFs (Built-In Functions). + + In CljElixir: (erlang/self), (erlang/band x y), etc. + These are the lowest-level BEAM operations, many are used internally.") + +;; --- Process --- + +(defn self + "Returns the PID of the calling process. + (erlang/self) ;=> #PID<0.123.0>" + []) + +(defn spawn + "Spawns a new process. Returns PID. + (erlang/spawn (fn [] (do-work))) + (erlang/spawn Module :fun [args])" + ([fun]) + ([module function args])) + +(defn spawn-link + "Spawns and links a process." + ([fun]) + ([module function args])) + +(defn spawn-monitor + "Spawns and monitors a process. Returns {pid ref}." + ([fun]) + ([module function args])) + +(defn send + "Sends a message. Same as `!` operator. + (erlang/send pid :hello)" + ([dest msg]) + ([dest msg opts])) + +(defn exit + "Sends an exit signal. + (erlang/exit :normal) + (erlang/exit pid :kill)" + ([reason]) + ([pid reason])) + +(defn link + "Creates a bidirectional link. + (erlang/link pid)" + [pid]) + +(defn unlink + "Removes a link. + (erlang/unlink pid)" + [pid]) + +(defn monitor + "Starts monitoring a process. + (erlang/monitor :process pid)" + [type item]) + +(defn demonitor + "Stops monitoring. + (erlang/demonitor ref)" + ([ref]) + ([ref opts])) + +(defn process-flag + "Sets process flags. + (erlang/process-flag :trap-exit true)" + ([flag value]) + ([pid flag value])) + +(defn process-info + "Returns info about a process. + (erlang/process-info pid) + (erlang/process-info pid :message-queue-len)" + ([pid]) + ([pid item])) + +(defn processes + "Returns a list of all process PIDs." + []) + +(defn is-process-alive + "Returns true if `pid` is alive." + [pid]) + +(defn register + "Registers a process under a name. + (erlang/register :my-server (erlang/self))" + [name pid]) + +(defn unregister + "Unregisters a name." + [name]) + +(defn whereis + "Returns PID for registered name, or :undefined." + [name]) + +(defn registered + "Returns list of all registered names." + []) + +(defn group-leader + "Gets or sets the group leader. + (erlang/group-leader) + (erlang/group-leader new-leader pid)" + ([]) + ([leader pid])) + +(defn hibernate + "Puts process in hibernate mode (frees heap)." + [module function args]) + +;; --- Bitwise Operations --- + +(defn band + "Bitwise AND. + (erlang/band 0xFF 0x0F) ;=> 15" + [int1 int2]) + +(defn bor + "Bitwise OR. + (erlang/bor 0x0F 0xF0) ;=> 255" + [int1 int2]) + +(defn bxor + "Bitwise XOR. + (erlang/bxor 0xFF 0x0F) ;=> 240" + [int1 int2]) + +(defn bnot + "Bitwise NOT. + (erlang/bnot 0) ;=> -1" + [int]) + +(defn bsl + "Bitwise shift left. + (erlang/bsl 1 5) ;=> 32" + [int shift]) + +(defn bsr + "Bitwise shift right. + (erlang/bsr 32 5) ;=> 1" + [int shift]) + +;; --- Arithmetic --- + +(defn abs + "Returns absolute value. + (erlang/abs -5) ;=> 5" + [number]) + +(defn div + "Integer division (truncated towards zero). + (erlang/div 10 3) ;=> 3" + [a b]) + +(defn rem + "Integer remainder. + (erlang/rem 10 3) ;=> 1" + [a b]) + +(defn float + "Converts to float. + (erlang/float 42) ;=> 42.0" + [number]) + +(defn round + "Rounds to nearest integer. + (erlang/round 3.5) ;=> 4" + [number]) + +(defn trunc + "Truncates to integer. + (erlang/trunc 3.9) ;=> 3" + [number]) + +(defn ceil + "Ceiling (smallest integer >= number). + (erlang/ceil 3.1) ;=> 4" + [number]) + +(defn floor + "Floor (largest integer <= number). + (erlang/floor 3.9) ;=> 3" + [number]) + +;; --- Tuple Operations --- + +(defn element + "Gets element at 1-based `index` from tuple. + (erlang/element 1 #el[:a :b :c]) ;=> :a" + [index tuple]) + +(defn setelement + "Sets element at 1-based `index` in tuple. + (erlang/setelement 1 #el[:a :b] :x) ;=> #el[:x :b]" + [index tuple value]) + +(defn tuple-size + "Returns the size of a tuple. + (erlang/tuple-size #el[1 2 3]) ;=> 3" + [tuple]) + +(defn make-tuple + "Creates a tuple of `arity` filled with `init-value`. + (erlang/make-tuple 3 0) ;=> #el[0 0 0]" + ([arity init-value]) + ([arity default-value init-list])) + +(defn append-element + "Appends `element` to `tuple`. + (erlang/append-element #el[1 2] 3) ;=> #el[1 2 3]" + [tuple element]) + +(defn tuple-to-list + "Converts a tuple to a list. + (erlang/tuple-to-list #el[1 2 3]) ;=> [1 2 3]" + [tuple]) + +(defn list-to-tuple + "Converts a list to a tuple. + (erlang/list-to-tuple [1 2 3]) ;=> #el[1 2 3]" + [list]) + +;; --- Type Conversion --- + +(defn atom-to-list + "Converts atom to charlist. + (erlang/atom-to-list :hello) ;=> 'hello'" + [atom]) + +(defn list-to-atom + "Converts charlist to atom." + [charlist]) + +(defn atom-to-binary + "Converts atom to binary string. + (erlang/atom-to-binary :hello) ;=> \"hello\"" + ([atom]) + ([atom encoding])) + +(defn binary-to-atom + "Converts binary to atom." + ([binary]) + ([binary encoding])) + +(defn binary-to-existing-atom + "Converts binary to existing atom." + ([binary]) + ([binary encoding])) + +(defn integer-to-list + "Converts integer to charlist. + (erlang/integer-to-list 123) ;=> '123' + (erlang/integer-to-list 255 16) ;=> 'FF'" + ([integer]) + ([integer base])) + +(defn list-to-integer + "Converts charlist to integer." + ([charlist]) + ([charlist base])) + +(defn integer-to-binary + "Converts integer to binary string. + (erlang/integer-to-binary 123) ;=> \"123\"" + ([integer]) + ([integer base])) + +(defn binary-to-integer + "Converts binary to integer." + ([binary]) + ([binary base])) + +(defn float-to-list + "Converts float to charlist." + ([float]) + ([float opts])) + +(defn float-to-binary + "Converts float to binary." + ([float]) + ([float opts])) + +(defn list-to-float + "Converts charlist to float." + [charlist]) + +(defn binary-to-float + "Converts binary to float." + [binary]) + +(defn binary-to-list + "Converts binary to list of bytes. + (erlang/binary-to-list \"hello\") ;=> [104 101 108 108 111]" + ([binary]) + ([binary start stop])) + +(defn list-to-binary + "Converts iolist to binary. + (erlang/list-to-binary [104 101 108 108 111]) ;=> \"hello\"" + [iolist]) + +(defn iolist-to-binary + "Converts iolist to binary. + (erlang/iolist-to-binary [\"hello\" \" \" \"world\"]) ;=> \"hello world\"" + [iolist]) + +(defn iolist-size + "Returns byte size of an iolist." + [iolist]) + +(defn term-to-binary + "Serializes a term to binary (External Term Format). + (erlang/term-to-binary {:key \"value\"}) ;=> <<131, ...>>" + ([term]) + ([term opts])) + +(defn binary-to-term + "Deserializes binary (ETF) to a term. + (erlang/binary-to-term bin) ;=> {:key \"value\"}" + ([binary]) + ([binary opts])) + +;; --- Type Checks --- + +(defn is-atom + "Returns true if term is an atom." + [term]) + +(defn is-binary + "Returns true if term is a binary." + [term]) + +(defn is-boolean + "Returns true if term is a boolean." + [term]) + +(defn is-float + "Returns true if term is a float." + [term]) + +(defn is-function + "Returns true if term is a function." + ([term]) + ([term arity])) + +(defn is-integer + "Returns true if term is an integer." + [term]) + +(defn is-list + "Returns true if term is a list." + [term]) + +(defn is-map + "Returns true if term is a map." + [term]) + +(defn is-map-key + "Returns true if key exists in map. Guard-safe." + [map key]) + +(defn is-number + "Returns true if term is a number." + [term]) + +(defn is-pid + "Returns true if term is a PID." + [term]) + +(defn is-port + "Returns true if term is a port." + [term]) + +(defn is-reference + "Returns true if term is a reference." + [term]) + +(defn is-tuple + "Returns true if term is a tuple." + [term]) + +;; --- List Operations --- + +(defn hd + "Returns the head of a list. + (erlang/hd [1 2 3]) ;=> 1" + [list]) + +(defn tl + "Returns the tail of a list. + (erlang/tl [1 2 3]) ;=> [2 3]" + [list]) + +(defn length + "Returns the length of a list. + (erlang/length [1 2 3]) ;=> 3" + [list]) + +;; --- Binary/Bitstring --- + +(defn byte-size + "Returns byte size of a binary. + (erlang/byte-size \"hello\") ;=> 5" + [binary]) + +(defn bit-size + "Returns bit size of a bitstring." + [bitstring]) + +(defn binary-part + "Extracts a part of a binary. Guard-safe. + (erlang/binary-part \"hello\" 1 3) ;=> \"ell\"" + [binary start length]) + +(defn split-binary + "Splits binary at position. + (erlang/split-binary \"hello\" 3) ;=> {\"hel\" \"lo\"}" + [binary pos]) + +;; --- Hash --- + +(defn phash2 + "Portable hash function. Returns integer in 0..(range-1). + (erlang/phash2 :my-term) ;=> 12345678 + (erlang/phash2 :my-term 100) ;=> 78" + ([term]) + ([term range])) + +;; --- Time --- + +(defn monotonic-time + "Returns monotonic time. + (erlang/monotonic-time) ;=> native time units + (erlang/monotonic-time :millisecond)" + ([]) + ([unit])) + +(defn system-time + "Returns system (wall-clock) time." + ([]) + ([unit])) + +(defn unique-integer + "Returns a unique integer. + (erlang/unique-integer [:positive :monotonic])" + ([]) + ([modifiers])) + +(defn timestamp + "Returns {megasecs secs microsecs} tuple." + []) + +(defn convert-time-unit + "Converts time between units. + (erlang/convert-time-unit time :native :millisecond)" + [time from-unit to-unit]) + +;; --- Node --- + +(defn node + "Returns the current node name. + (erlang/node) ;=> :nonode@nohost" + ([]) + ([arg])) + +(defn nodes + "Returns list of connected nodes. + (erlang/nodes) ;=> [:node1@host ...]" + ([]) + ([type])) + +(defn is-alive + "Returns true if the node is part of a distributed system." + []) + +;; --- System --- + +(defn apply + "Applies function. Like Kernel.apply. + (erlang/apply Enum :map [[1 2 3] inc]) + (erlang/apply fun args)" + ([fun args]) + ([module function args])) + +(defn error + "Raises an error. + (erlang/error :badarg) + (erlang/error {:my-error \"reason\"})" + ([reason]) + ([reason args])) + +(defn throw + "Throws a term (caught by try/catch :throw)." + [term]) + +(defn halt + "Halts the runtime. + (erlang/halt 0)" + ([]) + ([status]) + ([status opts])) + +(defn garbage-collect + "Forces garbage collection on current (or specified) process. + (erlang/garbage-collect)" + ([]) + ([pid])) + +(defn memory + "Returns memory usage info. + (erlang/memory) ;=> [{:total N} {:processes N} ...] + (erlang/memory :total) ;=> bytes" + ([]) + ([type])) + +(defn system-info + "Returns system information. + (erlang/system-info :process-count) + (erlang/system-info :otp-release)" + [item]) + +(defn statistics + "Returns runtime statistics. + (erlang/statistics :wall-clock)" + [type]) + +(defn make-ref + "Creates a unique reference." + []) + +(defn md5 + "Computes MD5 hash of binary. Returns 16-byte binary." + [data]) + +(defn crc32 + "Computes CRC32 checksum." + ([data]) + ([old-crc data])) + +(defn adler32 + "Computes Adler-32 checksum." + ([data]) + ([old-adler data])) + +;; --- Map Operations --- + +(defn map-get + "Gets a value from a map. Guard-safe. + (erlang/map-get :key my-map)" + [key map]) + +(defn map-size + "Returns the number of entries in a map. Guard-safe. + (erlang/map-size {:a 1 :b 2}) ;=> 2" + [map]) diff --git a/stubs/ets.clj b/stubs/ets.clj new file mode 100644 index 0000000..527e751 --- /dev/null +++ b/stubs/ets.clj @@ -0,0 +1,181 @@ +(ns ets + "Erlang :ets module — Erlang Term Storage (in-memory tables). + + In CljElixir: (ets/new :my-table [:set :public]), etc. + ETS tables are mutable, concurrent, in-memory key-value stores.") + +(defn new + "Creates a new ETS table. Returns table ID. + (ets/new :my-table [:set :public :named-table]) + Types: :set, :ordered-set, :bag, :duplicate-bag + Access: :public, :protected (default), :private" + [name opts]) + +(defn insert + "Inserts one or more tuples. Returns true. + (ets/insert table {:key \"value\"}) + (ets/insert table [{:a 1} {:b 2}])" + [table objects]) + +(defn insert-new + "Inserts only if key doesn't exist. Returns true/false." + [table objects]) + +(defn lookup + "Returns all objects matching `key`. Returns list. + (ets/lookup table :key) ;=> [{:key \"value\"}]" + [table key]) + +(defn lookup-element + "Returns specific element at `pos` for `key`. + (ets/lookup-element table :key 2) ;=> \"value\"" + ([table key pos]) + ([table key pos default])) + +(defn member + "Returns true if `key` exists. + (ets/member table :key) ;=> true" + [table key]) + +(defn delete + "Deletes a table or entries matching `key`. + (ets/delete table) ;=> deletes entire table + (ets/delete table :key) ;=> deletes entry" + ([table]) + ([table key])) + +(defn delete-object + "Deletes a specific object from the table." + [table object]) + +(defn delete-all-objects + "Deletes all objects from the table." + [table]) + +(defn update-counter + "Atomically updates a counter. Returns new value. + (ets/update-counter table :hits 1) ;=> increments by 1 + (ets/update-counter table :hits {2 1}) ;=> increments pos 2 by 1" + ([table key update-op]) + ([table key update-op default])) + +(defn update-element + "Atomically updates specific elements of a tuple." + ([table key element-spec]) + ([table key element-spec default])) + +(defn select + "Selects objects matching a match specification. + (ets/select table match-spec)" + ([table match-spec]) + ([table match-spec limit])) + +(defn match + "Pattern matches objects in the table. + (ets/match table {:_ :$1}) ;=> extracts matched values" + ([table pattern]) + ([table pattern limit])) + +(defn match-object + "Returns full objects matching the pattern. + (ets/match-object table {:_ :_}) ;=> all objects" + ([table pattern]) + ([table pattern limit])) + +(defn match-delete + "Deletes all objects matching the pattern. + (ets/match-delete table {:_ :_}) ;=> deletes all" + [table pattern]) + +(defn select-delete + "Deletes objects matching a match specification. Returns count." + [table match-spec]) + +(defn select-count + "Counts objects matching a match specification." + [table match-spec]) + +(defn select-replace + "Replaces objects matching a match specification." + [table match-spec]) + +(defn first + "Returns the first key in the table. + (ets/first table) ;=> :some-key or :'$end_of_table'" + [table]) + +(defn last + "Returns the last key (only for ordered-set)." + [table]) + +(defn next + "Returns the key after `key`. + (ets/next table :key) ;=> :next-key" + [table key]) + +(defn prev + "Returns the key before `key` (only for ordered-set)." + [table key]) + +(defn tab2list + "Converts entire table to a list. + (ets/tab2list table) ;=> [{:key1 \"val1\"} ...]" + [table]) + +(defn info + "Returns information about the table. + (ets/info table) ;=> keyword list + (ets/info table :size) ;=> number of objects" + ([table]) + ([table item])) + +(defn rename + "Renames a named table." + [table name]) + +(defn give-away + "Transfers table ownership to another process." + [table pid gift-data]) + +(defn i + "Prints brief info about all ETS tables." + ([]) + ([table])) + +(defn foldl + "Folds left over table entries." + [fun acc table]) + +(defn foldr + "Folds right over table entries." + [fun acc table]) + +(defn tab2file + "Dumps table to a file." + ([table filename]) + ([table filename opts])) + +(defn file2tab + "Loads table from a file." + ([filename]) + ([filename opts])) + +(defn whereis + "Returns the table ID for a named table." + [name]) + +(defn safe-fixtable + "Fixes/unfixes a table for safe traversal." + [table fix]) + +(defn fun2ms + "Converts a literal fun to a match specification." + [fun]) + +(defn test-ms + "Tests a match specification against a tuple." + [tuple match-spec]) + +(defn all + "Returns a list of all ETS table IDs." + []) diff --git a/stubs/filename.clj b/stubs/filename.clj new file mode 100644 index 0000000..9b6eec6 --- /dev/null +++ b/stubs/filename.clj @@ -0,0 +1,73 @@ +(ns filename + "Erlang :filename module — file name manipulation. + + In CljElixir: (filename/join [\"a\" \"b\"]), (filename/basename path), etc. + Works with charlists (unlike Elixir's Path which uses binaries).") + +(defn join + "Joins path components. + (filename/join [\"a\" \"b\" \"c\"]) ;=> 'a/b/c' + (filename/join \"a\" \"b\") ;=> 'a/b'" + ([components]) + ([name1 name2])) + +(defn split + "Splits a path into components. + (filename/split \"/a/b/c\") ;=> [\"/\" \"a\" \"b\" \"c\"]" + [name]) + +(defn basename + "Returns the last component of the path. + (filename/basename \"/a/b/c.txt\") ;=> 'c.txt' + (filename/basename \"/a/b/c.txt\" \".txt\") ;=> 'c'" + ([name]) + ([name ext])) + +(defn dirname + "Returns the directory component. + (filename/dirname \"/a/b/c\") ;=> '/a/b'" + [name]) + +(defn extension + "Returns the file extension including the dot. + (filename/extension \"file.ex\") ;=> '.ex'" + [name]) + +(defn rootname + "Returns path without extension. + (filename/rootname \"file.ex\") ;=> 'file'" + ([name]) + ([name ext])) + +(defn absname + "Converts to absolute path. + (filename/absname \"file.txt\")" + ([name]) + ([name dir])) + +(defn flatten + "Flattens a filename (resolves deep lists)." + [name]) + +(defn nativename + "Converts to native OS path format." + [name]) + +(defn pathtype + "Returns path type: :absolute, :relative, or :volumerelative. + (filename/pathtype \"/a/b\") ;=> :absolute" + [name]) + +(defn safe-relative-path + "Returns a safe relative path or :unsafe." + [name]) + +(defn find-src + "Finds source file from object file name." + ([beam]) + ([beam rules])) + +(defn find-file + "Finds a file in the given paths." + ([name paths]) + ([name paths extensions])) diff --git a/stubs/gen_server.clj b/stubs/gen_server.clj new file mode 100644 index 0000000..c39ec36 --- /dev/null +++ b/stubs/gen_server.clj @@ -0,0 +1,79 @@ +(ns gen_server + "Erlang :gen_server module — generic server (low-level OTP API). + + In CljElixir: prefer Elixir's GenServer module for most use cases. + Use this for direct Erlang OTP interop.") + +(defn start + "Starts a gen_server. Returns {:ok pid} or {:error reason}. + (gen_server/start module init-arg opts)" + ([module init-arg opts]) + ([server-name module init-arg opts])) + +(defn start-link + "Starts a linked gen_server." + ([module init-arg opts]) + ([server-name module init-arg opts])) + +(defn call + "Makes a synchronous call. + (gen_server/call pid request) ;=> reply + (gen_server/call pid request 5000)" + ([server-ref request]) + ([server-ref request timeout])) + +(defn cast + "Sends an async message. + (gen_server/cast pid message) ;=> :ok" + [server-ref request]) + +(defn reply + "Replies to a caller from within handle_call. + (gen_server/reply from reply-value)" + [client reply]) + +(defn stop + "Stops the server. + (gen_server/stop pid)" + ([server-ref]) + ([server-ref reason]) + ([server-ref reason timeout])) + +(defn multi-call + "Calls all registered servers on all/specified nodes." + ([name request]) + ([nodes name request]) + ([nodes name request timeout])) + +(defn abcast + "Casts to all registered servers on all/specified nodes." + ([name request]) + ([nodes name request])) + +(defn enter-loop + "Makes calling process enter the gen_server receive loop." + ([module opts state]) + ([module opts state server-name]) + ([module opts state server-name timeout])) + +(defn wait-response + "Waits for a gen_server response." + ([request-id timeout]) + ([request-id])) + +(defn send-request + "Sends an async request. Returns request-id. + (gen_server/send-request pid request)" + ([server-ref request]) + ([server-ref request label req-id-collection])) + +(defn receive-response + "Receives a response from send-request. + (gen_server/receive-response request-id timeout)" + ([request-id timeout]) + ([request-id])) + +(defn check-response + "Checks if a response has arrived (non-blocking). + (gen_server/check-response msg request-id-or-collection)" + [msg request-id-or-collection]) diff --git a/stubs/gen_tcp.clj b/stubs/gen_tcp.clj new file mode 100644 index 0000000..bf06374 --- /dev/null +++ b/stubs/gen_tcp.clj @@ -0,0 +1,52 @@ +(ns gen_tcp + "Erlang :gen_tcp module — TCP socket interface. + + In CljElixir: (gen_tcp/listen port opts), (gen_tcp/accept socket), etc. + Core networking module for TCP servers and clients on the BEAM.") + +(defn listen + "Starts listening on `port`. Returns {:ok listen-socket} or {:error reason}. + (gen_tcp/listen 4000 [:binary {:active false} {:reuseaddr true}])" + [port opts]) + +(defn accept + "Accepts an incoming connection. Blocks until a connection arrives. + (gen_tcp/accept listen-socket) ;=> {:ok socket} + (gen_tcp/accept listen-socket 5000) ;=> with 5s timeout" + ([listen-socket]) + ([listen-socket timeout])) + +(defn connect + "Connects to a TCP server. Returns {:ok socket} or {:error reason}. + (gen_tcp/connect \"localhost\" 4000 [:binary {:active false}]) + (gen_tcp/connect {127 0 0 1} 4000 opts 5000) ;=> with timeout" + ([address port opts]) + ([address port opts timeout])) + +(defn send + "Sends data over a TCP socket. Returns :ok or {:error reason}. + (gen_tcp/send socket \"hello\")" + [socket packet]) + +(defn recv + "Receives data from a socket. Blocks until data arrives. + (gen_tcp/recv socket 0) ;=> {:ok data} (0 = any amount) + (gen_tcp/recv socket 1024 5000) ;=> with timeout" + ([socket length]) + ([socket length timeout])) + +(defn close + "Closes a TCP socket. + (gen_tcp/close socket) ;=> :ok" + [socket]) + +(defn controlling-process + "Transfers socket ownership to another process. + (gen_tcp/controlling-process socket new-owner-pid) ;=> :ok" + [socket pid]) + +(defn shutdown + "Shuts down one or both directions of a socket. + (gen_tcp/shutdown socket :write) ;=> :ok + (gen_tcp/shutdown socket :read-write)" + [socket how]) diff --git a/stubs/gen_udp.clj b/stubs/gen_udp.clj new file mode 100644 index 0000000..5a7db86 --- /dev/null +++ b/stubs/gen_udp.clj @@ -0,0 +1,33 @@ +(ns gen_udp + "Erlang :gen_udp module — UDP socket interface. + + In CljElixir: (gen_udp/open port opts), (gen_udp/send socket addr port data), etc.") + +(defn open + "Opens a UDP socket. Returns {:ok socket} or {:error reason}. + (gen_udp/open 0 [:binary {:active false}]) ;=> {:ok socket}" + ([port]) + ([port opts])) + +(defn send + "Sends a UDP packet. + (gen_udp/send socket \"localhost\" 4000 \"hello\") + (gen_udp/send socket destination packet)" + ([socket destination packet]) + ([socket host port packet])) + +(defn recv + "Receives a UDP packet. Returns {:ok {address port data}} or {:error reason}. + (gen_udp/recv socket 0) ;=> {:ok {addr port data}} + (gen_udp/recv socket 1024 5000) ;=> with timeout" + ([socket length]) + ([socket length timeout])) + +(defn close + "Closes a UDP socket. + (gen_udp/close socket) ;=> :ok" + [socket]) + +(defn controlling-process + "Transfers socket ownership to another process." + [socket pid]) diff --git a/stubs/inet.clj b/stubs/inet.clj new file mode 100644 index 0000000..eb189af --- /dev/null +++ b/stubs/inet.clj @@ -0,0 +1,68 @@ +(ns inet + "Erlang :inet module — network interface configuration. + + In CljElixir: (inet/setopts socket opts), (inet/port socket), etc.") + +(defn setopts + "Sets socket options. + (inet/setopts socket [{:active true}]) + (inet/setopts socket [{:packet :line}])" + [socket opts]) + +(defn getopts + "Gets socket options. + (inet/getopts socket [:active :packet])" + [socket opts]) + +(defn port + "Returns the port number of a socket. + (inet/port socket) ;=> {:ok 4000}" + [socket]) + +(defn peername + "Returns {address port} of the remote end. + (inet/peername socket) ;=> {:ok {{127 0 0 1} 4000}}" + [socket]) + +(defn sockname + "Returns {address port} of the local end. + (inet/sockname socket) ;=> {:ok {{0 0 0 0} 4000}}" + [socket]) + +(defn getaddr + "Resolves hostname to IP address. + (inet/getaddr \"localhost\" :inet) ;=> {:ok {127 0 0 1}}" + [hostname family]) + +(defn gethostname + "Returns the hostname of the local machine. + (inet/gethostname) ;=> {:ok \"myhost\"}" + []) + +(defn getifaddrs + "Returns network interface addresses. + (inet/getifaddrs) ;=> {:ok [...]}" + []) + +(defn parse-address + "Parses an IP address string. + (inet/parse-address \"192.168.1.1\") ;=> {:ok {192 168 1 1}}" + [address]) + +(defn ntoa + "Converts IP tuple to string. + (inet/ntoa {192 168 1 1}) ;=> '192.168.1.1'" + [ip]) + +(defn close + "Closes a socket." + [socket]) + +(defn controlling-process + "Transfers socket control to another process." + [socket pid]) + +(defn i + "Prints information about all open sockets." + ([]) + ([proto])) diff --git a/stubs/lists.clj b/stubs/lists.clj new file mode 100644 index 0000000..75745bb --- /dev/null +++ b/stubs/lists.clj @@ -0,0 +1,249 @@ +(ns lists + "Erlang :lists module — list processing functions. + + In CljElixir: (lists/reverse lst), (lists/sort lst), etc. + Lower-level than Enum; operates directly on Erlang lists.") + +(defn reverse + "Reverses a list. + (lists/reverse [3 1 2]) ;=> [2 1 3]" + ([list]) + ([list tail])) + +(defn sort + "Sorts a list using Erlang term ordering. + (lists/sort [3 1 2]) ;=> [1 2 3] + (lists/sort (fn [a b] (> a b)) [3 1 2]) ;=> [3 2 1]" + ([list]) + ([fun list])) + +(defn usort + "Sorts and removes duplicates. + (lists/usort [3 1 2 1]) ;=> [1 2 3]" + ([list]) + ([fun list])) + +(defn keysort + "Sorts list of tuples by element at `n`-th position (1-based). + (lists/keysort 1 [{:b 2} {:a 1}]) ;=> [{:a 1} {:b 2}]" + [n tuple-list]) + +(defn keyfind + "Finds first tuple where element at `n` matches `key`. Returns tuple or false. + (lists/keyfind :a 1 [{:a 1} {:b 2}]) ;=> {:a 1}" + [key n tuple-list]) + +(defn keystore + "Replaces or appends tuple in list based on key at position `n`." + [key n tuple-list new-tuple]) + +(defn keydelete + "Deletes first tuple with matching key at position `n`." + [key n tuple-list]) + +(defn keymember + "Returns true if any tuple has `key` at position `n`." + [key n tuple-list]) + +(defn keyreplace + "Replaces first tuple with matching key at position `n`." + [key n tuple-list new-tuple]) + +(defn keytake + "Takes first tuple with matching key. Returns {value rest} or false." + [key n tuple-list]) + +(defn map + "Applies `fun` to each element, returns new list. + (lists/map (fn [x] (* x 2)) [1 2 3]) ;=> [2 4 6]" + [fun list]) + +(defn flatmap + "Maps and flattens one level. + (lists/flatmap (fn [x] [x x]) [1 2 3]) ;=> [1 1 2 2 3 3]" + [fun list]) + +(defn filter + "Returns elements for which `pred` returns true. + (lists/filter (fn [x] (> x 2)) [1 2 3 4]) ;=> [3 4]" + [pred list]) + +(defn filtermap + "Filters and maps in one pass. `fun` returns true/false/{true, value}. + (lists/filtermap (fn [x] (if (> x 2) {:true (* x 10)} false)) [1 2 3 4]) ;=> [30 40]" + [fun list]) + +(defn foldl + "Left fold. + (lists/foldl (fn [elem acc] (+ acc elem)) 0 [1 2 3]) ;=> 6" + [fun acc list]) + +(defn foldr + "Right fold. + (lists/foldr (fn [elem acc] (+ acc elem)) 0 [1 2 3]) ;=> 6" + [fun acc list]) + +(defn foreach + "Calls `fun` on each element for side effects. Returns :ok. + (lists/foreach (fn [x] (IO/puts x)) [1 2 3])" + [fun list]) + +(defn flatten + "Flattens nested lists. + (lists/flatten [[1 [2]] [3]]) ;=> [1 2 3]" + ([list]) + ([list tail])) + +(defn append + "Appends lists. + (lists/append [1 2] [3 4]) ;=> [1 2 3 4] + (lists/append [[1] [2] [3]]) ;=> [1 2 3]" + ([list-of-lists]) + ([list1 list2])) + +(defn concat + "Concatenates a list of things to a flat list." + [list]) + +(defn merge + "Merges sorted lists. + (lists/merge [1 3 5] [2 4 6]) ;=> [1 2 3 4 5 6]" + ([list1 list2]) + ([fun list1 list2]) + ([list-of-lists])) + +(defn zip + "Zips two lists into a list of tuples. + (lists/zip [1 2 3] [:a :b :c]) ;=> [{1 :a} {2 :b} {3 :c}]" + [list1 list2]) + +(defn unzip + "Unzips a list of tuples into two lists. + (lists/unzip [{1 :a} {2 :b}]) ;=> {[1 2] [:a :b]}" + [tuple-list]) + +(defn zipwith + "Zips with a combining function. + (lists/zipwith (fn [a b] (+ a b)) [1 2 3] [4 5 6]) ;=> [5 7 9]" + [fun list1 list2]) + +(defn member + "Returns true if `elem` is in `list`. + (lists/member 2 [1 2 3]) ;=> true" + [elem list]) + +(defn nth + "Returns the `n`-th element (1-based). + (lists/nth 2 [10 20 30]) ;=> 20" + [n list]) + +(defn nthtail + "Returns the tail starting at position `n` (1-based). + (lists/nthtail 2 [10 20 30]) ;=> [30]" + [n list]) + +(defn last + "Returns the last element. + (lists/last [1 2 3]) ;=> 3" + [list]) + +(defn delete + "Deletes first occurrence of `elem`. + (lists/delete 2 [1 2 3 2]) ;=> [1 3 2]" + [elem list]) + +(defn subtract + "Subtracts elements of `list2` from `list1`. + (lists/subtract [1 2 3 2] [2]) ;=> [1 3 2]" + [list1 list2]) + +(defn duplicate + "Creates a list with `elem` repeated `n` times. + (lists/duplicate 3 :ok) ;=> [:ok :ok :ok]" + [n elem]) + +(defn seq + "Generates a sequence from `from` to `to`. + (lists/seq 1 5) ;=> [1 2 3 4 5] + (lists/seq 1 10 2) ;=> [1 3 5 7 9]" + ([from to]) + ([from to step])) + +(defn sum + "Returns the sum of all elements." + [list]) + +(defn max + "Returns the maximum element." + [list]) + +(defn min + "Returns the minimum element." + [list]) + +(defn prefix + "Returns true if `list1` is a prefix of `list2`." + [list1 list2]) + +(defn suffix + "Returns true if `list1` is a suffix of `list2`." + [list1 list2]) + +(defn splitwith + "Splits list into two at the point where `pred` first returns false." + [pred list]) + +(defn partition + "Partitions list into two: elements satisfying `pred` and those that don't." + [pred list]) + +(defn takewhile + "Takes elements while `pred` returns true." + [pred list]) + +(defn dropwhile + "Drops elements while `pred` returns true." + [pred list]) + +(defn droplast + "Drops the last element of a list. + (lists/droplast [1 2 3]) ;=> [1 2]" + [list]) + +(defn sublist + "Returns a sublist. + (lists/sublist [1 2 3 4 5] 3) ;=> [1 2 3] + (lists/sublist [1 2 3 4 5] 2 3) ;=> [2 3 4]" + ([list len]) + ([list start len])) + +(defn search + "Searches for an element matching `pred`. Returns {:value elem} or false." + [pred list]) + +(defn enumerate + "Returns a list of {index, element} tuples. + (lists/enumerate [\"a\" \"b\" \"c\"]) ;=> [{1 \"a\"} {2 \"b\"} {3 \"c\"}]" + ([list]) + ([index list]) + ([index step list])) + +(defn all + "Returns true if `pred` returns true for all elements." + [pred list]) + +(defn any + "Returns true if `pred` returns true for any element." + [pred list]) + +(defn join + "Joins list elements into a string (Elixir extension)." + [list separator]) + +(defn map-foldl + "Combined map and foldl." + [fun acc list]) + +(defn map-foldr + "Combined map and foldr." + [fun acc list]) diff --git a/stubs/maps.clj b/stubs/maps.clj new file mode 100644 index 0000000..3eefa46 --- /dev/null +++ b/stubs/maps.clj @@ -0,0 +1,148 @@ +(ns maps + "Erlang :maps module — map operations. + + In CljElixir: (maps/get key map), (maps/put key value map), etc. + Lower-level than Elixir's Map module.") + +(defn get + "Gets value for `key`. Raises if missing (or returns `default`). + (maps/get :a {:a 1}) ;=> 1 + (maps/get :b {:a 1} :default) ;=> :default" + ([key map]) + ([key map default])) + +(defn find + "Returns {:ok value} or :error. + (maps/find :a {:a 1}) ;=> {:ok 1}" + [key map]) + +(defn is-key + "Returns true if `key` exists in `map`. + (maps/is-key :a {:a 1}) ;=> true" + [key map]) + +(defn keys + "Returns all keys. + (maps/keys {:a 1 :b 2}) ;=> [:a :b]" + [map]) + +(defn values + "Returns all values. + (maps/values {:a 1 :b 2}) ;=> [1 2]" + [map]) + +(defn size + "Returns the number of entries. + (maps/size {:a 1 :b 2}) ;=> 2" + [map]) + +(defn put + "Puts `key`/`value` into `map`. + (maps/put :c 3 {:a 1 :b 2}) ;=> {:a 1 :b 2 :c 3}" + [key value map]) + +(defn remove + "Removes `key` from `map`. + (maps/remove :a {:a 1 :b 2}) ;=> {:b 2}" + [key map]) + +(defn update + "Updates `key` by applying `fun` to current value. Raises if key missing. + (maps/update :a (fn [v] (+ v 1)) {:a 1}) ;=> {:a 2}" + [key fun map]) + +(defn update-with + "Updates `key` with `fun`, using `init` if key is missing. + (maps/update-with :a (fn [v] (+ v 1)) 0 {:a 1}) ;=> {:a 2} + (maps/update-with :b (fn [v] (+ v 1)) 0 {:a 1}) ;=> {:a 1 :b 0}" + ([key fun map]) + ([key fun init map])) + +(defn merge + "Merges two maps. `map2` values take precedence. + (maps/merge {:a 1} {:b 2}) ;=> {:a 1 :b 2}" + [map1 map2]) + +(defn merge-with + "Merges with a conflict resolver. + (maps/merge-with (fn [v1 v2] (+ v1 v2)) {:a 1} {:a 2}) ;=> {:a 3}" + [fun map1 map2]) + +(defn intersect + "Returns intersection of two maps." + ([map1 map2]) + ([combiner map1 map2])) + +(defn intersect-with + "Intersects with a combining function." + [combiner map1 map2]) + +(defn from-list + "Creates a map from a list of {key, value} tuples. + (maps/from-list [[:a 1] [:b 2]]) ;=> {:a 1 :b 2}" + [list]) + +(defn from-keys + "Creates a map from a list of keys, all with the same value. + (maps/from-keys [:a :b :c] 0) ;=> {:a 0 :b 0 :c 0}" + [keys value]) + +(defn to-list + "Converts map to list of {key, value} tuples. + (maps/to-list {:a 1 :b 2}) ;=> [[:a 1] [:b 2]]" + [map]) + +(defn new + "Creates an empty map. + (maps/new) ;=> {}" + []) + +(defn map + "Maps `fun` over map entries. `fun` receives (key, value). + (maps/map (fn [k v] (* v 2)) {:a 1 :b 2}) ;=> {:a 2 :b 4}" + [fun map]) + +(defn filter + "Filters entries where `pred` returns true. + (maps/filter (fn [k v] (> v 1)) {:a 1 :b 2 :c 3}) ;=> {:b 2 :c 3}" + [pred map]) + +(defn filtermap + "Filters and maps in one pass." + [fun map]) + +(defn fold + "Folds over map entries. + (maps/fold (fn [k v acc] (+ acc v)) 0 {:a 1 :b 2}) ;=> 3" + [fun acc map]) + +(defn foreach + "Calls `fun` on each entry for side effects. + (maps/foreach (fn [k v] (IO/puts (str k \": \" v))) my-map)" + [fun map]) + +(defn with + "Takes only the given `keys` from `map`. + (maps/with [:a :c] {:a 1 :b 2 :c 3}) ;=> {:a 1 :c 3}" + [keys map]) + +(defn without + "Drops the given `keys` from `map`. + (maps/without [:a] {:a 1 :b 2}) ;=> {:b 2}" + [keys map]) + +(defn groups-from-list + "Groups list elements by key function. + (maps/groups-from-list (fn [x] (rem x 2)) [1 2 3 4]) ;=> {1 [1 3], 0 [2 4]}" + ([key-fun list]) + ([key-fun value-fun list])) + +(defn iterator + "Returns a map iterator for lazy traversal. + (maps/iterator my-map)" + ([map]) + ([map order])) + +(defn next + "Advances a map iterator. Returns {key value iterator} or :none." + [iterator]) diff --git a/stubs/math.clj b/stubs/math.clj new file mode 100644 index 0000000..284f766 --- /dev/null +++ b/stubs/math.clj @@ -0,0 +1,141 @@ +(ns math + "Erlang :math module — mathematical functions. + + In CljElixir: (math/sqrt 2), (math/pow 2 10), etc. + All functions return floats.") + +(defn pi + "Returns the value of pi. + (math/pi) ;=> 3.141592653589793" + []) + +(defn e + "Returns Euler's number. + (math/e) ;=> 2.718281828459045" + []) + +(defn tau + "Returns tau (2*pi). + (math/tau) ;=> 6.283185307179586" + []) + +;; --- Powers & Roots --- + +(defn pow + "Returns `x` raised to `y`. + (math/pow 2 10) ;=> 1024.0" + [x y]) + +(defn sqrt + "Returns the square root. + (math/sqrt 4) ;=> 2.0" + [x]) + +(defn exp + "Returns e^x. + (math/exp 1) ;=> 2.718281828459045" + [x]) + +(defn log + "Returns the natural logarithm (base e). + (math/log 2.718281828459045) ;=> 1.0" + [x]) + +(defn log2 + "Returns the base-2 logarithm. + (math/log2 1024) ;=> 10.0" + [x]) + +(defn log10 + "Returns the base-10 logarithm. + (math/log10 1000) ;=> 3.0" + [x]) + +;; --- Trigonometry --- + +(defn sin + "Returns the sine (radians). + (math/sin (/ (math/pi) 2)) ;=> 1.0" + [x]) + +(defn cos + "Returns the cosine (radians). + (math/cos 0) ;=> 1.0" + [x]) + +(defn tan + "Returns the tangent (radians). + (math/tan 0) ;=> 0.0" + [x]) + +(defn asin + "Returns the arcsine (radians). + (math/asin 1) ;=> 1.5707963267948966" + [x]) + +(defn acos + "Returns the arccosine (radians). + (math/acos 1) ;=> 0.0" + [x]) + +(defn atan + "Returns the arctangent (radians). + (math/atan 1) ;=> 0.7853981633974483" + [x]) + +(defn atan2 + "Returns the two-argument arctangent (radians). + (math/atan2 1 1) ;=> 0.7853981633974483" + [y x]) + +;; --- Hyperbolic --- + +(defn sinh + "Returns the hyperbolic sine." + [x]) + +(defn cosh + "Returns the hyperbolic cosine." + [x]) + +(defn tanh + "Returns the hyperbolic tangent." + [x]) + +(defn asinh + "Returns the inverse hyperbolic sine." + [x]) + +(defn acosh + "Returns the inverse hyperbolic cosine." + [x]) + +(defn atanh + "Returns the inverse hyperbolic tangent." + [x]) + +;; --- Rounding --- + +(defn ceil + "Returns the smallest integer >= x (as float). + (math/ceil 3.2) ;=> 4.0" + [x]) + +(defn floor + "Returns the largest integer <= x (as float). + (math/floor 3.8) ;=> 3.0" + [x]) + +(defn fmod + "Returns the floating-point remainder of x/y. + (math/fmod 10.0 3.0) ;=> 1.0" + [x y]) + +(defn erf + "Returns the error function. + (math/erf 1) ;=> 0.8427007929497149" + [x]) + +(defn erfc + "Returns the complementary error function." + [x]) diff --git a/stubs/os.clj b/stubs/os.clj new file mode 100644 index 0000000..5f9b920 --- /dev/null +++ b/stubs/os.clj @@ -0,0 +1,72 @@ +(ns os + "Erlang :os module — operating system interface. + + In CljElixir: (os/cmd \"ls\"), (os/type), etc.") + +(defn cmd + "Executes an OS command and returns the output as a charlist. + (os/cmd \"ls -la\") ;=> 'total 42\\n...'" + ([command]) + ([command opts])) + +(defn type + "Returns the OS type. + (os/type) ;=> {:unix :darwin} or {:win32 :nt}" + []) + +(defn version + "Returns the OS version. + (os/version) ;=> {14 3 0}" + []) + +(defn getenv + "Gets an environment variable. Returns charlist or false. + (os/getenv \"HOME\") ;=> '/Users/ajet' + (os/getenv \"MISSING\") ;=> false" + ([name]) + ([name default])) + +(defn putenv + "Sets an environment variable. + (os/putenv \"MY_VAR\" \"value\") ;=> true" + [name value]) + +(defn unsetenv + "Removes an environment variable. + (os/unsetenv \"MY_VAR\") ;=> true" + [name]) + +(defn getenv + "Gets all environment variables as list of \"KEY=VALUE\" strings." + []) + +(defn timestamp + "Returns OS timestamp as {megasecs secs microsecs}. + (os/timestamp) ;=> {1709 123456 789012}" + []) + +(defn system-time + "Returns OS system time. + (os/system-time :millisecond)" + ([]) + ([unit])) + +(defn perf-counter + "Returns a performance counter value. + (os/perf-counter :nanosecond)" + ([]) + ([unit])) + +(defn find-executable + "Finds an executable in PATH. Returns path or false. + (os/find-executable \"elixir\") ;=> '/usr/local/bin/elixir'" + [name]) + +(defn getpid + "Returns the OS process ID as a string. + (os/getpid) ;=> \"12345\"" + []) + +(defn set-signal + "Sets OS signal handler." + [signal action]) diff --git a/stubs/timer.clj b/stubs/timer.clj new file mode 100644 index 0000000..7dfadfc --- /dev/null +++ b/stubs/timer.clj @@ -0,0 +1,73 @@ +(ns timer + "Erlang :timer module — timer utilities. + + In CljElixir: (timer/sleep 1000), (timer/send-after 5000 pid :msg), etc. + Note: for most uses, Process/send-after is preferred in Elixir.") + +(defn sleep + "Suspends the calling process for `time` milliseconds. + (timer/sleep 1000) ;=> :ok (sleeps 1 second) + (timer/sleep :infinity) ;=> blocks forever" + [time]) + +(defn send-after + "Sends `message` to `pid` after `time` milliseconds. Returns {:ok tref}. + (timer/send-after 5000 (Process/self) :timeout)" + ([time pid message]) + ([time message])) + +(defn send-interval + "Sends `message` to `pid` every `time` milliseconds. Returns {:ok tref}. + (timer/send-interval 1000 (Process/self) :tick)" + ([time pid message]) + ([time message])) + +(defn apply-after + "Applies `module:function(args)` after `time` milliseconds. + (timer/apply-after 5000 IO :puts [\"delayed\"])" + [time module function args]) + +(defn apply-interval + "Applies `module:function(args)` every `time` milliseconds." + [time module function args]) + +(defn apply-repeatedly + "Applies `module:function(args)` repeatedly with `time` delay after each." + [time module function args]) + +(defn cancel + "Cancels a timer. + (timer/cancel tref) ;=> {:ok :cancel}" + [tref]) + +(defn tc + "Times the execution of a function. Returns {time result} in microseconds. + (timer/tc (fn [] (fib 30))) ;=> {12345 832040} + (timer/tc Enum :sort [[3 1 2]]) ;=> {microsecs [1 2 3]}" + ([fun]) + ([fun args]) + ([module function args])) + +(defn now-diff + "Returns time difference in microseconds between two erlang:now() tuples." + [t2 t1]) + +(defn seconds + "Converts seconds to milliseconds. + (timer/seconds 5) ;=> 5000" + [secs]) + +(defn minutes + "Converts minutes to milliseconds. + (timer/minutes 5) ;=> 300000" + [mins]) + +(defn hours + "Converts hours to milliseconds. + (timer/hours 1) ;=> 3600000" + [hours]) + +(defn hms + "Converts hours, minutes, seconds to milliseconds. + (timer/hms 1 30 0) ;=> 5400000" + [hours minutes seconds]) diff --git a/test/clj_elixir/nrepl_test.exs b/test/clj_elixir/nrepl_test.exs index f487051..159cf27 100644 --- a/test/clj_elixir/nrepl_test.exs +++ b/test/clj_elixir/nrepl_test.exs @@ -245,6 +245,92 @@ defmodule CljElixir.NReplTest do [response] = Handler.handle(%{"op" => "bogus", "id" => "1"}, manager) assert "unknown-op" in response["status"] end + + test "eval returns actual ns after ns declaration", %{manager: manager} do + [clone_resp] = Handler.handle(%{"op" => "clone", "id" => "1"}, manager) + session = clone_resp["new-session"] + + # Default ns is "user" + responses = + Handler.handle( + %{"op" => "eval", "code" => "(+ 1 2)", "session" => session, "id" => "2"}, + manager + ) + + values = Enum.filter(responses, &Map.has_key?(&1, "value")) + assert hd(values)["ns"] == "user" + + # After ns declaration, ns should update + responses = + Handler.handle( + %{ + "op" => "eval", + "code" => "(ns NReplNsTest)\n(defn hi [] :hey)", + "session" => session, + "id" => "3" + }, + manager + ) + + values = Enum.filter(responses, &Map.has_key?(&1, "value")) + assert hd(values)["ns"] == "NReplNsTest" + end + + test "incremental def in ns via nrepl", %{manager: manager} do + [clone_resp] = Handler.handle(%{"op" => "clone", "id" => "1"}, manager) + session = clone_resp["new-session"] + + # Set up namespace + Handler.handle( + %{ + "op" => "eval", + "code" => "(ns NReplIncrTest)\n(defn foo [] :original)", + "session" => session, + "id" => "2" + }, + manager + ) + + # Add a new function without ns + Handler.handle( + %{ + "op" => "eval", + "code" => "(defn bar [] :added)", + "session" => session, + "id" => "3" + }, + manager + ) + + # Both should work + responses = + Handler.handle( + %{ + "op" => "eval", + "code" => "(NReplIncrTest/bar)", + "session" => session, + "id" => "4" + }, + manager + ) + + values = Enum.filter(responses, &Map.has_key?(&1, "value")) + assert hd(values)["value"] == ":added" + + responses = + Handler.handle( + %{ + "op" => "eval", + "code" => "(NReplIncrTest/foo)", + "session" => session, + "id" => "5" + }, + manager + ) + + values = Enum.filter(responses, &Map.has_key?(&1, "value")) + assert hd(values)["value"] == ":original" + end end # --- TCP Integration Test --- diff --git a/test/clj_elixir/repl_test.exs b/test/clj_elixir/repl_test.exs index 07ea568..9a821cb 100644 --- a/test/clj_elixir/repl_test.exs +++ b/test/clj_elixir/repl_test.exs @@ -86,6 +86,139 @@ defmodule CljElixir.REPLTest do end end + describe "ns tracking" do + test "ns sets current namespace" do + state = REPL.new() + assert REPL.current_ns(state) == "user" + + {:ok, _, state2} = REPL.eval("(ns ReplNsTest1)", state) + assert state2.current_ns == "ReplNsTest1" + assert REPL.current_ns(state2) == "ReplNsTest1" + end + + test "ns with defn creates module" do + state = REPL.new() + + {:ok, _, state2} = + REPL.eval(""" + (ns ReplNsTest2) + (defn greet [name] (str "hello " name)) + """, state) + + assert state2.current_ns == "ReplNsTest2" + {:ok, result, _} = REPL.eval("(ReplNsTest2/greet \"world\")", state2) + assert result == "\"hello world\"" + end + + test "bare defn in active ns updates module" do + state = REPL.new() + + # Set up initial module + {:ok, _, state2} = + REPL.eval(""" + (ns ReplNsTest3) + (defn hello [] :hi) + """, state) + + # Add a new function without ns + {:ok, result, state3} = REPL.eval("(defn world [] :earth)", state2) + assert result == "#'ReplNsTest3/world" + + # Both functions should work + {:ok, r1, _} = REPL.eval("(ReplNsTest3/hello)", state3) + assert r1 == ":hi" + {:ok, r2, _} = REPL.eval("(ReplNsTest3/world)", state3) + assert r2 == ":earth" + end + + test "redefine function in active ns" do + state = REPL.new() + + {:ok, _, state2} = + REPL.eval(""" + (ns ReplNsTest4) + (defn greet [] "v1") + """, state) + + {:ok, r1, _} = REPL.eval("(ReplNsTest4/greet)", state2) + assert r1 == "\"v1\"" + + # Redefine greet + {:ok, _, state3} = REPL.eval("(defn greet [] \"v2\")", state2) + + {:ok, r2, _} = REPL.eval("(ReplNsTest4/greet)", state3) + assert r2 == "\"v2\"" + end + + test "bare defn without ns evals plain" do + state = REPL.new() + # No namespace set — def outside module should fail + {:error, _, _} = REPL.eval("(defn orphan [] :lost)", state) + end + + test "expressions eval normally in ns context" do + state = REPL.new() + + {:ok, _, state2} = + REPL.eval(""" + (ns ReplNsTest5) + (defn add [a b] (+ a b)) + """, state) + + # Expression (no def) goes through plain eval + {:ok, result, _} = REPL.eval("(ReplNsTest5/add 3 4)", state2) + assert result == "7" + end + + test "mixed defs and exprs in ns context" do + state = REPL.new() + + {:ok, _, state2} = + REPL.eval(""" + (ns ReplNsTest6) + (defn double [x] (* x 2)) + """, state) + + # Eval def + expression together + {:ok, result, state3} = + REPL.eval(""" + (defn triple [x] (* x 3)) + (ReplNsTest6/triple 5) + """, state2) + + # Result should be the expression value + assert result == "15" + + # Both functions should work + {:ok, r1, _} = REPL.eval("(ReplNsTest6/double 4)", state3) + assert r1 == "8" + end + + test "switching namespace" do + state = REPL.new() + + {:ok, _, state2} = + REPL.eval(""" + (ns ReplNsTestA) + (defn a-fn [] :from-a) + """, state) + + {:ok, _, state3} = + REPL.eval(""" + (ns ReplNsTestB) + (defn b-fn [] :from-b) + """, state2) + + assert state3.current_ns == "ReplNsTestB" + + # Both modules still work + {:ok, r1, _} = REPL.eval("(ReplNsTestA/a-fn)", state3) + assert r1 == ":from-a" + {:ok, r2, _} = REPL.eval("(ReplNsTestB/b-fn)", state3) + assert r2 == ":from-b" + end + end + describe "REPL.balanced?" do test "balanced parens" do assert REPL.balanced?("(+ 1 2)")