Reflaxe.Elixir supports two complementary “stdlib” layers:
- Haxe standard library compatibility (e.g.
Array,StringTools,haxe.io.*,sys.*) - Typed Elixir externs (e.g.
elixir.File,elixir.DateTime,elixir.IO, plusphoenix.*,ecto.*)
You can use either layer (or both) in the same codebase. The choice is about portability vs BEAM-first ergonomics.
See also:
docs/04-api-reference/STDLIB_SUPPORT_MATRIX.mddocs/02-user-guide/AUTHORING_STYLES_PORTABLE_VS_ELIXIR_FIRST.mddocs/02-user-guide/IMPERATIVE_TO_FUNCTIONAL_LOWERING.mddocs/02-user-guide/WRITING_IDIOMATIC_HAXE_FOR_ELIXIR.mddocs/02-user-guide/REFLAXE_RUNTIME_EXPLAINED.mddocs/06-guides/KNOWN_LIMITATIONS.md
reflaxe_runtime is a Haxe compilation define, not an Elixir/Mix project.
You will see it in .hxml files and docs because it gates code that should type-check during compilation
(#if (macro || reflaxe_runtime) / #if (elixir || reflaxe_runtime)) even though it does not exist at runtime
in other targets (and should not leak into the Haxe macro/interp contexts). See
docs/02-user-guide/REFLAXE_RUNTIME_EXPLAINED.md.
Runtime helpers that must exist as emitted Elixir (for example, the exception/throw bridge used by the Elixir lowering pipeline) live as Haxe sources under:
std/reflaxe/elixir/runtime/**(native modules likeReflaxe.Elixir.HaxeThrow)
They are forcibly included/kept at macro-time by src/reflaxe/elixir/CompilerInit.hx, so they are emitted
into your app’s generated .ex output even under -dce full.
For many haxe.* modules, the upstream Haxe stdlib already compiles correctly for the Elixir target.
We only override/replace modules when one (or more) is true:
- upstream implementation uses inline patterns that produce invalid Elixir after lowering
- upstream implementation compiles, but produces systematically non-idiomatic Elixir (hard to read/maintain)
- we can map to a strong BEAM primitive (binary, iodata, pattern matching) and get both correctness + readability
- we need a BEAM mapping of
sys.*(filesystem/process/network/thread) rather than “pretend-JS” behavior
This repo ships a small set of Elixir-target overrides in std/:
std/*.cross.hx: “cross” overrides for core modules (Array,String,Std, etc.)std/haxe/**: selected Haxe std modules implemented/adjusted for Elixirstd/sys/**: BEAM-backedsys.*surfacesstd/_std/**: Elixir-only shims injected only for Elixir builds (to prevent__elixir__()leaking into macro/other targets)
Some stdlib modules are resolved very early during compilation, and Haxe runs macros using the eval interpreter.
That combination means a few modules must satisfy two requirements at once:
- Macro/eval phase (host-side): constructors must exist and be runnable (eval can instantiate classes).
- Elixir output phase (target-side): we must avoid emitting the canonical Haxe stdlib implementation when it is
non-idiomatic for BEAM or produces Elixir warnings that fail CI under
--warnings-as-errors(WAE).
For those specific modules, we use a dual-mode override under src/:
#if macro: small in-memory implementation (keeps macro/eval happy).#else:@:nativeGen externsurface (prevents canonical stdlib code from being emitted into generated.ex).
Examples:
src/haxe/ds/BalancedTree.hxsrc/haxe/ds/EnumValueMap.hx
Why src/?
- For haxelib installs,
src/is the only path guaranteed to be on the initial classpath for-lib reflaxe.elixir. - Macro-time classpath injection can be too late because Haxe may cache some stdlib modules before bootstrap macros run.
What does this “replace”?
- Only the specific modules we place under
src/haxe/**are shadowed early. - Everything else still comes from the upstream Haxe stdlib unless we explicitly override it via:
std/*.cross.hx(cross-platform override selection), orstd/haxe/**,std/sys/**,std/_std/**(Elixir-target additions/shims).
Why the path looks like the Haxe stdlib (src/haxe/ds/...)?
- This is intentional: Haxe module resolution is path-based. Putting a file at
haxe/ds/BalancedTree.hxon the classpath shadows the upstreamhaxe.ds.BalancedTreemodule for this compilation. - We keep it surgical: only add these early overrides when we have a concrete macro/eval + WAE reason.
Is this a Reflaxe convention?
- It’s a common pattern across target compilers (including Reflaxe-based ones): when a module must be resolved before bootstrap/injection can run, it needs to live on the library’s initial classpath.
- The “dual-mode” approach (
#if macroimplementation,#elseextern) is specific to our constraints: Haxe eval must be able to instantiate the type, but we don’t want to emit the upstream implementation into Elixir output when it is non-idiomatic or breaks--warnings-as-errors.
The injection point is macro-time, in:
src/reflaxe/elixir/CompilerBootstrap.hx:1(early injection, invoked fromextraParams.hxml)src/reflaxe/elixir/CompilerInit.hx:1(compiler registration + early injection)
See also:
docs/01-getting-started/cross-hx.mddocs/03-compiler-development/CROSS_FILES_STAGING_MECHANISM.md
This keeps:
- macro context and other targets using the official Haxe stdlib
- Elixir builds using the Elixir-specific overrides (and only those)
Best for:
- pure business logic you want to reuse across targets (JS/Node, Elixir, etc.)
- algorithms/data transforms that don’t need Phoenix/Ecto/OTP primitives
- libraries you intend to ship as “multi-target Haxe code”
Typical examples:
Array,StringTools,haxe.ds.Option,haxe.format.JsonPrinterhaxe.io.Bytesfor binary data manipulation (portable API, BEAM-optimized implementation)
Best for:
- Phoenix/LiveView/Ecto integration
- BEAM primitives (processes, iodata/binaries, filesystem ops) where the Elixir API is the “native shape”
- eliminating impedance mismatch (structs, atoms, tagged tuples) in app code
Typical examples:
elixir.DateTime/elixir.File/elixir.Path/elixir.IOphoenix.*/ecto.*integrations fromstd/phoenixandstd/ecto
The ideal architecture for most Phoenix apps:
- Haxe stdlib in the “domain layer” (pure logic, transformations, validation logic, parsing)
- typed Elixir/Phoenix externs in the “integration layer” (LiveView, Ecto, OTP callbacks)
- explicit boundaries:
- decode
Terminputs to typed structures at the edges - keep assigns typed (
Socket<Assigns>)
- decode
Rule of thumb:
- if you’re writing code that “looks like Phoenix”, prefer Phoenix/Elixir externs
- if you’re writing code that “looks like a reusable library”, prefer Haxe stdlib
When you need to fix stdlib behavior for the Elixir target:
- Prefer adding/adjusting Haxe sources in:
std/*.cross.hx,std/haxe/**,std/sys/**, orstd/_std/**
- Add a snapshot test under:
test/snapshot/stdlib/**
- Do not patch generated
.exas a behavior change (generated outputs are not the source of truth).
If a change touches std/_std, keep it Elixir-target-only (it is injected conditionally).
“Support the whole stdlib” doesn’t mean “every sys.* module is identical to native OS targets”.
For sys.*:
- implement what maps cleanly to BEAM/Elixir
- document differences when semantics diverge
- fail fast with actionable errors for things that cannot be supported safely
Track the current stdlib parity work in bd:
haxe.elixir-hm47(stdlib parity roadmap)