fullofcaffeine/reflaxe.elixir
Elixir target for Haxe. Compile Haxe to (mostly idiomatic) Elixir.
Reflaxe.Elixir (aka Haxir)
Haxe -> Elixir compiler for the BEAM ecosystem, with first-class Phoenix/LiveView support.
Write application code in Haxe and compile to conventional Elixir shapes for pure Elixir/OTP services and Phoenix applications.
Warning
Stability: the project is currently pre-1.0 (v0.x) and actively evolving.
Some features remain experimental/opt-in (for example source mapping, migrations .exs emission, fast_boot).
See Known Limitations and Versioning & Stability.
Why Reflaxe.Elixir
Reflaxe.Elixir is for teams that want standard Elixir/OTP runtime behavior, while authoring with stronger compile-time feedback.
- Keep standard Elixir runtime semantics: generated code follows normal module/function/tuple/map conventions.
- Add a typed authoring layer: catch shape mismatches (assigns, params, tagged results) before runtime.
- Improve large refactors: typed Haxe APIs and compiler checks help keep changes coherent across modules.
- Build ergonomic abstractions: Haxe macros/typing can encode reusable authoring patterns without changing your BEAM deployment model.
- Use it with or without Phoenix: works for pure Elixir/OTP codebases and Phoenix apps.
Elixir's failure model is still the foundation (supervision, process isolation, let-it-crash where appropriate).
The typed layer helps you decide more deliberately what should crash, what should return data, and where boundaries should be explicit.
Build Higher-Level Abstractions (Haxe + Elixir)
These abstractions should earn their place. The question is not "can Haxe do this?", but "does this reduce drift, duplication, or unsafe boundaries compared to direct Haxe->Elixir authoring without this extra layer?"
| Haxe authoring surface | Direct Haxe->Elixir baseline | Edge over that baseline | Example |
|---|---|---|---|
Module-level final routes = [...] |
Hand-maintained route/controller wiring in direct modules | Typed route declarations reduce path/action drift during refactors | examples/09-phoenix-router |
@:schema + @:changeset |
Manually keeping schema/changeset field surfaces aligned | Typed field/params surfaces catch boundary mismatches earlier | examples/06-user-management |
TypedQueryLambda |
Ad-hoc query composition with repeated field assumptions | Typed query lambdas keep predicates aligned with source model shapes | examples/todo-app |
@:protocol / @:impl / @:behaviour |
Repeated contract maintenance across implementation modules | One typed contract surface, multiple implementations, less signature drift | examples/14-abstraction-lab |
Typed wrappers over Elixir externs (for example elixir.Kernel) |
Repeated low-level guard/send/type-check boilerplate | Centralized boundary helpers with explicit typed call surfaces | examples/14-abstraction-lab |
For a focused walkthrough of these patterns in one place, see examples/14-abstraction-lab.
Tradeoff: you add a compile step and should still read generated Elixir for hot paths and debugging. If an abstraction does not remove real duplication or drift, direct Haxe->Elixir modules are usually the simpler choice.
How this differs from Gleam (briefly)
Gleam is a strong typed BEAM language with its own language/runtime story.
Reflaxe.Elixir takes a different approach:
- You author in Haxe and compile to Elixir.
- You integrate directly with Phoenix/LiveView/Ecto through typed extern surfaces.
- You can reuse Haxe tooling/macros and keep cross-target options where they make sense.
Current Support (v0.x pre-1.0)
Stable (documented subset)
- Phoenix integration (LiveView/controllers/templates/routers) for documented paths
- HEEx-oriented template authoring modes (
tsx,balanced,metal) - Ecto schemas/changesets/typed query surfaces
- OTP patterns (GenServer/Supervisor/Registry)
- Mix integration (
mix compile.haxe, watcher workflows) and client hook builds with Genes
Experimental / opt-in
- Source mapping (
.ex.map,mix haxe.source_map) - Migration
.exsemission fast_boot
For exact boundaries, use:
Quick Start
Start here
If you're new to this stack, begin with:
Install with Lix (recommended)
npx lix scope create
# Install latest GitHub release tag
REFLAXE_ELIXIR_TAG="$(curl -fsSL https://api.github.com/repos/fullofcaffeine/reflaxe.elixir/releases/latest | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
npx lix install "github:fullofcaffeine/reflaxe.elixir#${REFLAXE_ELIXIR_TAG}"
# Download project-pinned Haxe deps
npx lix downloadMinimal build.hxml
-lib reflaxe.elixir
-cp src_haxe
-main my_app_hx.Main
-D reflaxe_runtime
-D no-utf16
-D elixir_output=lib/my_app_hx
-D app_name=MyApp
-dce fullImportant compiler flag note:
- Do not use
-D analyzer-optimizewhen targeting Elixir. - See Compiler Flags Guide.
New Phoenix app (greenfield)
Use the guided flow:
For gradual adoption in an existing Phoenix codebase:
Pure Elixir / OTP (no Phoenix)
Start from the Mix-based examples and author regular Elixir modules in Haxe:
Todo app smoke (repo)
npm run qa:sentinel
scripts/qa-logpeek.sh --run-id <RUN_ID> --until-done 120Example (LiveView)
Haxe:
import elixir.types.Term;
import phoenix.Phoenix.HandleEventResult;
import phoenix.Phoenix.MountResult;
import phoenix.Phoenix.Socket;
typedef CounterAssigns = { count: Int };
@:native("MyAppWeb.CounterLive")
@:liveview
class CounterLive {
public static function mount(params: Term, session: Term, socket: Socket<CounterAssigns>): MountResult<CounterAssigns> {
return Ok(socket.assign(_.count, 0));
}
}Notes:
socketin LiveView callbacks isSocket<TAssigns>(the Phoenix callback shape), and you can call assign helpers on it directly (socket.assign(...)).LiveSocket<TAssigns>is still available as an explicit wrapper when you prefer pipe-style chaining or helper signatures that useLiveSocket._.countcan look odd at first because_usually means “unused variable.” Here,_is a macro marker.- Why the API looks like this: Haxe has no built-in “field reference literal” for typedef fields, so
LiveSocket.assignuses_.fieldas a compact compile-time selector. - What you get from it: the compiler validates the field name, converts to Phoenix atom style, and emits
assign(socket, :count, value)._does not exist at runtime. - Phoenix-style bulk assigns are available as
assign({ ... }), which emitsassign(socket, %{...}). - Typed-key APIs (
assignKey/assignNewKey/updateKey) are optional advanced mode withvar keys = phoenix.AssignKeys.of(MyAssigns).
Use defaultassign(_.field, value)/assign({ ... })for shortest code; use typed keys when you want explicit key tokens in APIs.
Tiny comparison:
var keys = phoenix.AssignKeys.of(CounterAssigns);
return Ok(socket.assignKey(keys.count, 0)); - In Haxe, write callback args naturally (
params,session). If they are unused, generated Elixir is automatically normalized to_params,_session. - API deep dive:
docs/04-api-reference/LIVE_SOCKET_ASSIGN_API.md
Generated Elixir shape:
defmodule CounterLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
endMore examples:
Documentation
Start at docs/README.md.
Recommended links
- Installation
- Writing Idiomatic Haxe for Elixir
- Elixir Idioms & Hygiene
- Haxe->Elixir Mappings
- Interop With Existing Elixir
- Phoenix Integration
- API Index
- LiveSocket Assign API
- Mix Tasks
- Elixir Injection Guide
- Troubleshooting
Contributing
License
GPL-3.0 - see LICENSE.
