init commit

This commit is contained in:
2026-03-09 23:09:46 -04:00
parent 5cbc493cc5
commit 5da77e3360
73 changed files with 9935 additions and 103 deletions
+72
View File
@@ -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/<app>/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 <file.clje> [...] [-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
+45
View File
@@ -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 '<expression>' [...]")
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
+40 -13
View File
@@ -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()
<<c>> 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
+103
View File
@@ -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] <file.clje>")
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