Typed at the source. At home on the BEAM.
Add Haxe's type system to Phoenix, Ecto, and OTP—then ship ordinary Elixir through Mix.
Why Reflaxe.Elixir? · Writing styles · Typed Ecto · Try it · PhoenixHx · Gradual adoption · Examples · Docs
Reflaxe.Elixir compiles typed Haxe into .ex source for the normal Mix and BEAM pipeline.
PhoenixHx is the typed Haxe authoring and integration layer for Phoenix, LiveView, Ecto, and OTP.
It generates ordinary Elixir that runs on the real frameworks and the BEAM runtime.
For Elixir developers: add static types, closed domain states, typed Phoenix boundaries, and compile-time DSL checks one module or feature at a time. Generated and hand-written Elixir coexist through ordinary module/function contracts.
For Haxe developers: use Haxe as the primary language for BEAM services and Phoenix applications while keeping Mix, Hex packages, Elixir interop, OTP operations, and standard deployment. Functional, Elixir-flavored Haxe maps closest to direct target code; covered portable and imperative forms keep their behavior through explicit lowering. Haxe is a build dependency, not a second production VM.
Important
Pre-1.0: suitable for controlled pilots inside documented, pinned paths, not a general stability promise. Major 1 will remain blocked until every applicable public Haxe standard-library API is supported and tested. See Production Readiness, the independent 1.0 review, standard-library and package roadmap, exact OTP support boundary, Known Limitations, and Versioning & Stability.
- Type the risky boundaries. Check assigns, params, routes, results, schemas, changesets, child specs, and existing Elixir calls before generated code reaches Mix.
- Adopt in either direction. Add one typed island to an Elixir app, or author a larger bounded context in Haxe while Phoenix and the BEAM remain the platform.
- Generate beside handwritten Elixir safely. Manifest-owned, content-hashed output rejects collisions and manual edits before publication; stale cleanup and interrupted builds recover without scanning or deleting unowned Phoenix files.
- Keep the output recognizable. Prefer normal modules, functions, maps, tuples, pattern matches,
Enum, Phoenix, Ecto, and OTP calls whenever Haxe semantics can be preserved. - Build typed project DSLs. Use Haxe macros, metadata, algebraic enums, and structural types to remove duplicated strings and invalid framework combinations.
- Share selected behavior. Compile a deliberately portable domain layer to Elixir and JavaScript; target-specific Phoenix and browser code stays at the edges.
- Keep normal operations. Format, test, inspect, profile, and deploy the resulting Elixir with ordinary Mix and BEAM tools.
Haxe / HXX -> Reflaxe.Elixir + PhoenixHx -> ordinary .ex / ~H -> Mix -> BEAM
Reflaxe.Elixir supports two source styles through one compiler within its documented pre-1.0 surface. A project can use either style in each module. The styles do not select different backends or change the meaning of Haxe code.
| Starting point | Source style | What you write | When it fits |
|---|---|---|---|
| Haxe application or shared library | Portable stdlib-first | Haxe types, standard-library APIs, loops, and ordinary Haxe control flow | You want familiar Haxe code or selected logic that can also compile to JavaScript or another target. |
| Elixir, Phoenix, or BEAM application | Typed Elixir-first | Typed elixir.*, phoenix.*, ecto.*, and OTP APIs, with explicit data transformations |
You want Haxe type checks while the source and generated code stay close to Elixir concepts. |
| Most Phoenix applications | A deliberate mix | Portable domain rules with Elixir-first framework and process boundaries | You want reuse where it helps, without hiding Phoenix, Ecto, or OTP behind portable abstractions. |
The mixed approach is the recommended default for Phoenix applications. Keep reusable domain rules portable when another target needs them. Write LiveView, Ecto, supervision, and process state with the typed BEAM APIs that own those concepts.
Portable code can use supported Haxe APIs such as String, Array, Map, Lambda, and
haxe.functional.Result. The compiler uses native BEAM operations when they preserve the Haxe
contract. It adds compatibility code when Haxe and Elixir define different behavior.
Elixir-first code can call typed surfaces for common Elixir modules such as Enum, String,
MapSet, Process, and Task. It can also use typed atoms, tuples, keyword lists, process IDs,
and opaque terms. These Haxe declarations map to normal Elixir calls and values. For example,
haxe.functional.Result<T, E> becomes {:ok, value} or {:error, reason}.
These declarations present common Elixir APIs through Haxe types. They do not copy or replace the
Elixir standard library. For example, elixir.Enum.map accepts typed Haxe values at the source and
emits a normal Enum.map call.
The Haxe standard library is not forbidden in Elixir-first code. Use it when its contract is the contract that you want. Use a typed Elixir surface when you want the exact BEAM API and behavior. See Standard Library Handling and Interop With Existing Elixir.
For new Elixir-first code, prefer functions that receive data and return new data. Use closed Haxe
enums, exhaustive switch, Result, explicit collection operations, and typed framework APIs.
Keep long-lived state in LiveView assigns, GenServer state, or ETS instead of static mutable fields.
This style is the closest Reflaxe.Elixir offers to a Gleam-like development experience. It combines typed functional source with BEAM libraries, but it still generates ordinary Elixir for Mix. Haxe also permits classes, macros, exceptions, reassignment, and imperative control flow. The Gleam comparison explains the different goals and current maturity.
Imperative Haxe remains supported within the documented surface. The compiler converts local
reassignment and loops into immutable Elixir value flow, often with Enum operations or reducers.
This helps Haxe teams migrate existing code, but complex control flow can produce more generated
code and intermediate values. Effects and exceptions still occur, so immutable output is not
automatically pure.
Shared mutable aliases need special care. The current compiler does not preserve every case where
two Haxe variables refer to the same mutable object or collection. This is a correctness limitation,
not a style preference. The compiler now rejects one proven Array case: a fresh local Array, one
direct alias, push, and a later length read through the other name in the same straight-line block.
The push call can be a statement, a direct variable initializer, or a direct assignment.
This check prevents known-wrong output, but it is not general alias analysis. No error means only
that this narrow check did not match. Read Imperative to Functional Lowering
and Known Limitations before you port mutation-heavy code.
The complete Authoring Styles guide
shows portable, Elixir-first, and mixed project examples.
These are checked excerpts from executable examples. The links contain the complete imports, types, build files, and canonical generated output.
This real Todos context
queries a typed Todo schema.
The lambda is ordinary Haxe, so the compiler knows which schema it is querying and which fields the
predicate may use:
import ecto.TypedQuery;
import elixir.Enum;
import phoenix_hx_todo_hx.data.Todo;
import phoenix_hx_todo_hx.infrastructure.Repo;
using reflaxe.elixir.macros.TypedQueryLambda;
class Todos {
static function getForUser(userId:Int, id:Int):Null<Todo> {
var query = TypedQuery.from(Todo)
.where(todo -> todo.userId == userId && todo.id == id);
var todos:Array<Todo> = Repo.all(query);
return Enum.at(todos, 0);
}
}It becomes an ordinary Ecto query with normal pins and a normal Repo call (line-wrapped here):
defp get_for_user(user_id, id) do
query =
(require Ecto.Query;
Ecto.Query.where(
Ecto.Query.from(t in PhoenixHxTodo.Todo, []),
[t],
(t.user_id == ^user_id) and (t.id == ^id)
))
todos = PhoenixHxTodo.Repo.all(query)
Enum.at(todos, 0)
endThere is no second query engine in production: Ecto executes the generated query. But mistakes stop earlier. The checked negative fixture deliberately writes:
var query = TypedQuery.from(User);
var value = 123;
var q2 = query.where(user -> user.noSuchField == value);and Haxe rejects it with Field "noSuchField" does not exist in User. Typed field and association
selectors extend the same idea to changesets, preloads, and joins, while generating normal atoms such
as :email and :posts. See the negative fixture,
the exact generated context, and the
Ecto integration guide.
SearchDomain.hx uses typed
Elixir APIs and Haxe's typed Result:
var normalized = ElixirString.trim(query);
var needle = ElixirString.downcase(normalized);
var visible = Enum.filter(catalog,
item -> item != null &&
ElixirString.contains(ElixirString.downcase(item), needle));
return Ok({query: normalized, visible: visible, result_count: visible.length});normalized = String.trim(query)
needle = String.downcase(normalized)
visible =
Enum.filter(catalog, fn item ->
not Kernel.is_nil(item) and String.contains?(String.downcase(item), needle)
end)
{:ok, %{query: normalized, visible: visible, result_count: length(visible)}}The externs become direct String and Enum calls, Result becomes {:ok, value} / {:error, reason},
and the structural record becomes a map. See the
reviewed output.
This portable Transcript.render
uses a normal Haxe loop and Array.push:
var lines = [];
for (message in history) {
lines.push(MessageRules.format(message));
}
return lines;The compiler proves that it is a fresh, ordered, one-value projection and emits:
Enum.map(history, fn message ->
PortableChatDomain.MessageRules.format(message)
end)More complex mutation currently uses explicit immutable rebinding or reducers.
That preserves many local flows, but it does not make another alias observe an
ordinary Haxe object/collection mutation; shared-reference semantics remain a
known pre-1.0 gap. See
Imperative to Functional Lowering and the
reviewed output.
The same target-neutral MessageRules
is executed through Haxe-authored ExUnit on the BEAM and through the generated JavaScript in Node, so
validation behavior is shared without pulling Phoenix or JavaScript APIs into the domain layer.
PhoenixHx combines typed bindings, Haxe authoring tools, and compiler integration. The bindings describe existing Phoenix APIs to Haxe. The authoring tools cover routes, LiveViews, components, templates, Ecto, and OTP application code. The compiler then generates ordinary Elixir and HEEx. PhoenixHx does not copy or replace Phoenix.
For example, LiveViews use typed Socket<TAssigns> and callback result types. Haxe parses inline HXX
and type-checks assigns and embedded expressions before emitting Phoenix ~H. Strict options also
check registered components, slots, hooks, and events. This excerpt is from the checked
SearchLive:
return <p data-testid="result-count">
${assigns.result_count} result(s)
</p>;~H"""
<p data-testid="result-count">
{@result_count} result(s)
</p>
"""Phoenix still compiles the resulting HEEx, and there is no separate template runtime. The same
example's typed final routes DSL
emits normal Phoenix.Router pipelines, scopes, and live_session declarations. See the tracked
Haxe-first router source and
generated output,
Phoenix Integration, and the complete
SearchLive output.
Repeated client/server events can be one checked contract instead of three matching strings. This
real shared declaration generates the event name companion, requires the LiveView binding, and owns
the id decoder:
@:liveEventProtocol
enum TodoEvent {
@:templateEvent
ToggleTodo(id:Int);
}
@:liveEvents(TodoEvent)
class AppLive {}The inline template uses phx-click=${TodoEvents.ToggleTodoEvent} and the emitted HEEx uses
phx-click={"toggle_todo"}. Phoenix still receives a normal handle_event/3 boundary; PhoenixHx
generates the string-to-Int decoding and rejects missing handlers or incompatible template payloads
at Haxe compile time. See the complete event contract,
LiveView source, and
generated LiveView.
| Check in Haxe | What ships to the target |
|---|---|
final routes with typed plugs, sessions, LiveViews, and params |
Normal Phoenix.Router pipelines, scopes, and routes in router.ex |
@:application + typed child specs |
An ordinary Application module and Supervisor.start_link/2 child list in application.ex; the OTP contract explains the tested boot boundary and restart/failure exclusions |
@:exunit, ConnTest, and LiveViewTest |
Normal ExUnit integration tests in todo_persistence_test.exs |
| Closed domain enums and portable rules | The same selected behavior compiled and executed as Elixir and JavaScript |
The benefit is not new runtime machinery. It is one typed authoring vocabulary across the risky boundaries, followed by framework code that Elixir teams can still format, inspect, test, and operate.
The compiler tries, in order: proven native lowering such as Int operators and Enum.map; direct
typed Elixir/Phoenix/Ecto/OTP APIs; Haxe stdlib overrides backed by String, Map, :crypto, and
other BEAM primitives; then explicit compatibility lowering or small helpers when semantics differ.
Special floats, unresolved numeric values, and arbitrary Haxe throw values are examples of the last case.
Correctness wins when source and target semantics differ. Today the two core helper modules are kept
in generated builds even when a particular application has no call site for one of them; generated
calls remain selective, but module inclusion is not yet fully demand-driven. This is tracked footprint
work, not an application mode. Normal applications do not use -D reflaxe_runtime; see
reflaxe_runtime And Generated Helpers.
| Goal | Start here |
|---|---|
| Compile the smallest Haxe modules | 01-simple-modules |
| Bring a Haxe service or library to the BEAM | 02-mix-project |
| Add one feature to an existing Phoenix app | Gradual Adoption Tutorial |
| Author typed LiveViews or call hand-written Elixir | 13-elixir-first-liveview |
| See a complete typed Phoenix/Ecto vertical slice | 17-railshx-to-phoenixhx-todo |
| Share selected browser/server domain logic | 16-portable-chat-domain |
| Explore the full reference app | todo-app |
| Install a verified release package | Installation |
No all-at-once rewrite is required.
Reflaxe.Elixir fits best when generated Elixir, Phoenix integration, gradual adoption, Haxe macros, or existing Haxe/JavaScript sharing matter. Gleam favors a smaller immutable language and mature 1.x ecosystem. Read the honest comparison.
git clone https://github.com/fullofcaffeine/reflaxe.elixir.git
cd reflaxe.elixir
npm install
npm run test:quickFor application use, install and checksum a pinned release ZIP rather than depending on a source checkout. Continue with Installation and Start Here.
Direct haxelib git and Lix github: installs clone the repository's development layout; they do
not build the flattened .cross.hx package needed by normal consumers. Use the release ZIP for an
application, or follow the documented source-checkout setup when deliberately testing unreleased
compiler code.
CI covers full codegen snapshots and negative cases, Haxe-authored ExUnit semantics, selected upstream stdlib fixtures, strict generated-Elixir compilation, runtime examples, source/package parity, reproducible release artifacts, and Phoenix browser smoke.
Those tests prove the features listed as supported; they do not guarantee that every Haxe program
works yet. The loop and nested-comprehension bugs found during the 1.0 review are fixed. Mix now
rebuilds when any tracked Haxe input changes, and the compiler refuses to overwrite or delete a file
unless it can verify that it generated the file. The tested OTP feature set is deliberately small
and clearly listed. Callback functions written directly inside Result branches also have source
and runtime tests. Before the project can promise 1.0 stability, it still needs an exact list of
supported APIs and versions, a qualified licensing decision, and one unchanged proposed release
tested in independent projects for a defined period.
| Topic | Guide |
|---|---|
| Product thesis and tradeoffs | Why Reflaxe.Elixir? |
| Setup and first application | Start Here |
| Fast local rebuilds | Haxe Compilation Server and Watcher |
| Elixir-friendly Haxe | Writing Idiomatic Haxe |
| Portable vs Elixir-first source design | Authoring Styles |
| Native lowering and compatibility helpers | reflaxe_runtime And Generated Helpers |
| Runtime-tested OTP operations and exclusions | OTP Support Contract |
| Phoenix, LiveView, Ecto, and API references | Documentation Index |
| Supported versions and sharp edges | Support Matrix |
| Contributor architecture and tests | Contributing |
npm testCompiler changes must preserve source/package behavior, runtime semantics, generated-output quality, examples, and browser QA. See Testing Infrastructure.
GPL-3.0. Generated applications can include support code from this repository; review Licensing & Distribution before commercial distribution.
