Day-to-day development practices for building applications and contributing to the compiler.
# 1. Make changes to Haxe source files
vim src_haxe/MyModule.hx
# 2. Compile and test
npm run test:quick # Broad compiler-shape checkpoint
npm test # Broad local compiler/runtime aggregate
# 3. Test specific functionality
haxe build.hxml # Basic compilation
haxe build.hxml -D source-map # With debugging support-D analyzer-optimize).
# Start automatic recompilation (todo-app example)
cd examples/todo-app
mix haxe.watch
# Make changes to .hx files → automatic recompilation
# Sub-second compilation times with hot reload
# Perfect for iterative developmentRun this once per clone:
npm run hooks:install- Enables repo-managed pre-commit checks (path leaks, secrets scan, staged
.hxauto-format, naming guards). - The repo hook exports Beads issues to
.beads/issues.jsonlwhen a local Beads database is bootstrapped, then runs the staged guards on the final content. - If you previously used an older Beads chained hook, re-running
npm run hooks:installreplaces wrappers that call the removedbd sync --flush-onlycommand.
- The pre-commit hook blocks staged absolute local paths (for example
/Users/...,/home/...,/var/folders/...,C:\Users\...). - Dot-relative path references (
./...,../...) are allowed when they resolve inside the repo root. - Dot-relative references that escape repo root are blocked by default.
- To bypass relative-path blocking for an intentional case:
ALLOW_RELATIVE_PATH_REFERENCES=1 git commit ...
- This guard is enforced from
scripts/hooks/pre-commitviascripts/lint/local_path_guard_staged.sh.
Reflaxe.Elixir coordinates two development ecosystems for practical end-to-end validation:
Purpose: Build and test the Haxe→Elixir compiler itself
- Package Manager:
lix(modern Haxe package manager) - Runtime:
npmscripts orchestrate the workflow - Dependencies: Reflaxe framework, tink_unittest
- Output: Generated Elixir source code files
npm install # Installs lix package manager locally
npx lix download # Downloads Haxe dependencies (project-specific versions)
haxe TestMain.hxml # Compile using your Haxe toolchain
# If `haxe` is not on your PATH, use the project-local wrapper:
# ./node_modules/.bin/haxe TestMain.hxmlWhy lix?
- ✅ Locked Haxe library versions (avoids "works on my machine")
- ✅ Exact GitHub commits/releases and pinned Haxelib sources (reviewed, reproducible inputs)
- ✅ Locked dependency versions (zero software erosion)
- ✅ Scoped installs (keeps Haxe libs out of global state)
Reflaxe.Elixir itself is installed from a versioned GitHub Release package through Lix. Haxelib package compatibility remains tested, but a global Haxelib-registry install is not the recommended application workflow. See Installation And Setup.
Purpose: Test and run the generated Elixir code
- Package Manager:
mix(native Elixir build system) - Dependencies: Phoenix, Ecto, and LiveView; OTP and GenServer are part of the Elixir runtime
- Output: Running BEAM applications
mix deps.get # Installs Phoenix, Ecto, etc.
mix test # Tests generated Elixir code and Mix tasks
mix ecto.migrate # Runs database migrations Why mix?
- ✅ Native Elixir tooling (industry standard)
- ✅ Phoenix integration (LiveView, router, etc.)
- ✅ BEAM ecosystem (the exact tested OTP boundary is documented in the OTP Support Contract)
Use the smallest test that owns the behavior while editing, then widen validation to match the claim. A compiler snapshot gives fast code-generation feedback; a runtime claim also needs generated Elixir acceptance and BEAM execution.
# One focused snapshot
make -C test test-core__<case>
# Broader compiler-shape checkpoint
npm run test:quick
# Portable runtime
npm run test:haxe-exunit-stdlib
# Mixed portable + OTP/Mix runtime aggregates
npm run test:runtime-smoke
npm run test:mix-fast
# Broad local compiler/runtime aggregate for cross-cutting changes
npm test
# Agent-safe todo-app build, boot, probe, and teardown
npm run qa:sentinelnpm run test:changed is an advisory local heuristic, not completion evidence:
it does not yet have a reviewed semantic-ownership manifest or selector-miss
audit.
The canonical Testing Strategy maps change types to required evidence, separates portable Haxe semantics from Elixir/OTP/Phoenix product behavior, and defines the R0–R5 feedback rings used by people and agents.
For details on non-blocking Phoenix validation (async runs, bounded log viewing, Playwright integration), see Phoenix E2E & QA Sentinel.
- Focused ownership: small snapshot, negative, macro, and ExUnit tests localize failures during implementation.
- Target acceptance: strict Elixir parsing, formatting, and warnings-as- errors catch invalid or poor generated source.
- Runtime behavior: Haxe-authored ExUnit and bounded smokes execute the emitted program on BEAM.
- Product integration: examples, OTP/Phoenix/Ecto tests, and thin Playwright flows preserve framework and browser contracts.
- Consumer/release evidence: isolated package installation and exact-head CI keep repository-only classpaths from masquerading as user success.
Some errors are expected during testing and appear as warnings:
# Expected in test environment - shows as warning
[warning] Haxe compilation failed (expected in test): Library reflaxe.elixir is not installedWhy this happens:
- Tests run in isolated environments without full library installation
- Test framework validates compilation behavior, not successful execution
- These warnings indicate the test is working correctly
Actual compilation problems show with error symbols:
# Real error - shows with ❌ symbol
[error] ❌ Haxe compilation failed: src_haxe/Main.hx:5: Type not found : MyClassHaxe Source Code (.hx files)
↓ (npm/lix tools)
Reflaxe.Elixir Compiler
↓ (generates)
Elixir Source Code (.ex files)
↓ (mix tools)
Running BEAM Application
Reflaxe.Elixir uses two compilation profiles that affect macros and AST transformers:
-
fast_boot– opt-in “fast profile” for large codebases- Minimal macro work:
- Macros like
RouterBuildMacro,HXX, andModuleMacrostill run, but avoid expensiveContext.getType/ project‑wide scans where possible. - Template processing uses memoization and cheap shape checks when enabled.
- Macros like
- Core semantic transforms only:
- Phoenix/Ecto/OTP shape‑driven transforms remain active.
- Correctness passes that preserve Haxe state across Elixir closures remain active. For example,
a lowered
Enum.reduce_whilemust bind its returned accumulator back to the surrounding local. - Ultra‑late cosmetic hygiene passes (naming, underscore promotion, unused assignment cleanup) are skipped when
fast_bootanddisable_hygiene_finalare defined.
- Goal: keep cold todo‑app builds bounded and responsive during day‑to‑day work.
- Implementation:
- Haxe macros read it via
Context.defined("fast_boot")and avoid project‑wide scans. - The AST pass registry gates selected expensive passes with
#if fast_boot. - It should never be required for correctness; it exists to trade “perfect hygiene” for speed.
- Haxe macros read it via
- Minimal macro work:
-
full_prepasses/ full hygiene – used for compiler/snapshot/CI runs- All macros and transforms are enabled.
- Hygiene and final sweep passes run to enforce the strictest shape and naming invariants.
- Intended for full validation, not for tight dev loops.
fast_boot is enabled by passing -D fast_boot to Haxe. For Mix builds, this repo treats it as opt-in via:
HAXE_FAST_BOOT=1 mix compileSee lib/haxe_compiler.ex for the injection point. Legacy todo-app perf/debug HXML experiments are kept in git history and are not required for normal development.
Use the compilation server for a long-lived edit loop, not for isolated builds:
# Recommended for an application-side Haxe → Elixir build
mix haxe.watch --hxml build-server.hxmlThe watcher owns one native haxe --wait process and sends later rebuilds to it.
This lets Haxe reuse safely cached parsing and typing work while it still checks
changed files and their dependents. Reflaxe receives the resulting typed program,
regenerates what is required, and leaves byte-identical output files untouched.
Ordinary one-shot commands, CI, and production builds compile directly by
default. Use HAXE_NO_SERVER=1 when automation must explicitly prohibit a
background server. See the Watcher Workflow
for Phoenix integration, plain HXML commands, lifecycle details, and troubleshooting.
Fast, TypeScript-class edit feedback is a project objective, not a claim derived from tiny isolated operation timings. Compiler measurements distinguish clean builds, fresh-process rebuilds, persistent server rebuilds, generated-file output, and downstream Mix compilation. See the Performance Guide for the current benchmark vocabulary and reproducible commands.
package.json: npm scripts, lix dependency.haxerc: Project-specific Haxe version (4.3.7)haxe_libraries/: lix-managed dependencies (tink_unittest, reflaxe)mix.exs: Elixir dependencies (Phoenix, Ecto)
test/Test.hxml: Snapshot test runner configurationtest/snapshot/<category>/<case>/: Individual compiler cases with expected outputsexamples/todo-app/: Integration test as real Phoenix application
src/reflaxe/elixir/ElixirCompiler.hx: Main compilersrc/reflaxe/elixir/helpers/: Feature-specific compilers (Changeset, OTP, LiveView, etc.)std/: Phoenix/Elixir type definitions and externs
Reflaxe.Elixir has a source mapping design (to map generated .ex back to .hx), but it is
currently experimental and not fully wired end‑to‑end in the AST pipeline.
See docs/04-api-reference/SOURCE_MAPPING.md for the current status and next steps.
Reflaxe.Elixir supports full-stack development with a single language:
- Server-side: Haxe → Elixir (Phoenix LiveView, Ecto, OTP)
- Client-side: Haxe → JavaScript with native async/await support
- Shared Types: Define data structures in
shared/directory - Dual Compilation: Build both targets with type-safe contracts
- Live Reload: Hot reload for both Elixir and JavaScript changes
- Type Safety: Compile-time checks across shared server/client contracts
See: Quick Start Patterns for copy‑paste, end‑to‑end patterns.
- Check existing implementations first - Search for similar patterns before starting
- Plan with documentation - Update roadmap with your feature
- Create helper compiler in
src/reflaxe/elixir/helpers/ - Add annotation support to main
ElixirCompiler.hx - Write the focused regression under
test/snapshot/<category>/<case>/ - Document thoroughly - Update guides and examples
- Run the focused owner, then every affected layer from the
Testing Strategy;
cross-cutting changes also run
npm test - Mark task complete - Verify implementation meets requirements
// Create test/snapshot/<category>/new_feature/Main.hx
class TestNewFeature {
public static function main() {
trace("Testing new feature");
// Your feature test code here
var result = MyNewFeature.doSomething();
trace('Result: $result');
}
}# Check the version this repo expects
cat .haxerc
# Check your installed Haxe
haxe --version
# Reset lix scope
npx lix scope create
npx lix download# Reset npm dependencies
rm -rf node_modules && npm install
# Re-download the toolchain + libraries (lix cache)
npx lix download
# If your lix cache is corrupted, remove it and retry:
# rm -rf ~/haxe && npx lix download
# Reset Elixir dependencies
mix deps.clean --all && mix deps.get# Run individual test components
npm run test:quick # Snapshot suite + Elixir validation (fast)
npm run test:mix # Elixir/Mix tests only
# Narrow scope
npm run test:failed
npm run test:changed # advisory only; widen using the canonical change map
# Update test snapshots when output improves
npm run test:update# Clean compiler-owned generated files through the repository manifest
npm run clean:generated
haxe build.hxml # Regenerate from Haxe
mix compile --force # Verify Elixir compilation- Prefer
haxefrom a proper Haxe install; if it’s not on your PATH, use the repo shim:./node_modules/.bin/haxe ...(provided bylix+.haxerc). - Run the focused semantic owner while editing, then every affected layer from the canonical testing strategy. Cross-cutting compiler/runtime/runner changes require the broad local aggregate; the complete backstop is the exact-head CI graph.
- Use source maps for debugging (
-D source-map) - Test todo-app integration when compiler changes can affect generated application/runtime behavior; use the canonical change map for other changes
- Update documentation when adding features
- Follow existing patterns in the codebase
- Write thorough tests for all new functionality
- Document architectural decisions in appropriate guides
- Maintain performance targets (see Performance Guide; large modules may require
fast_bootduring iteration)
- Commit frequently with descriptive messages
- Test before pushing to ensure CI/CD success
- Update changelogs for user-facing changes
- Reference issues in commit messages
✅ Modern Haxe tooling (lix + tink_unittest)
✅ Native Elixir integration (mix + Phoenix ecosystem)
✅ End-to-end validation (compiler + generated code)
✅ One broad local compiler/runtime aggregate (npm test) with the complete
repository evidence graph owned by CI
✅ Zero global state (project-specific everything)
✅ Fast compilation for typical modules (see docs/06-guides/PERFORMANCE_GUIDE.md)
- Phoenix Integration - Build Phoenix applications
- LiveView Architecture - Real-time UI patterns
- ExUnit Testing - Application testing strategies
- Compiler Architecture - How the compiler works
- AST Pipeline - TypedExpr → ElixirAST → transforms → print
- Testing Infrastructure - Snapshot testing system
- Troubleshooting Guide - Comprehensive problem solving
- Performance Guide - Compilation performance
Ready to build? Check out Phoenix Integration to start building applications.
Reflaxe.Elixir can use the Haxe compilation server (haxe --wait) to speed up
repeated builds by retaining Haxe compiler state between requests. Ordinary
one-shot commands such as mix compile compile directly by default, because a
background server can outlive a canceled Mix VM. The long-running
mix haxe.watch workflow starts and owns one server explicitly.
The server still receives a complete HXML compilation request. Reusing its process and frontend cache does not automatically mean Reflaxe skipped every unaffected target module, so project performance results distinguish server reuse from demonstrated module-level skipping.
Behavior:
- Direct compile (default): one-shot dev, test, CI, and production commands do not auto-start
haxe --wait. - Watcher ownership: long-running
mix haxe.watchstarts and reuses a server for the lifetime of that watcher.mix haxe.watch --oncecompiles directly and exits without starting one. - Explicit auto-start: set
HAXE_SERVER_AUTOSTART=devoralwaysonly when the calling process is expected to own a long-lived server. - Reuse (owned): If Mix started the server in this VM, it is reused automatically.
- Attach (opt‑in): If
HAXE_SERVER_ALLOW_ATTACH=1and the configured port is already bound by a compatible server, Mix attaches and uses it (including a prior server recorded in the cookie). - Relocate: If the configured port is already bound and attach is not enabled (or not compatible), Mix relocates to a free port and starts its own server.
- Cookie: Mix records the last working server info per project/toolchain in
.reflaxe_elixir/haxe_server.jsonto reduce port churn across restarts and enable stale-server cleanup. - Native owner: a packaged host helper ties the exact
haxe --waitprocess tree to the Mix VM's port. If VM shutdown skips Elixir termination callbacks, closing that port still reaps the compiler child instead of leaving it under PID 1. - Fallback: If the server cannot be reached, Mix compiles directly (no server).
Defaults and environment variables:
- Default server port:
6116(aligned with the QA sentinel). HAXE_NO_SERVER=1— disable the server for the current run (use direct Haxe).HAXE_SERVER_PORT=<port>— force a specific port (e.g.,6116).HAXE_SERVER_ALLOW_ATTACH=1— allow attaching to an externally-started compatible server on the configured port.HAXE_SERVER_AUTOSTART=dev|always|never— opt a caller into automatic startup (default:never).
Notes:
- No flag is needed for normal direct compilation or for
mix haxe.watch. HAXE_NO_SERVER=1remains a hard override for automation that must never start a server.
Server caching and Mix freshness are separate. Before a no-op, Mix fingerprints the effective Haxe
build by content: recursive HXML, direct classpaths such as src_shared, resources, resolved Haxe
libraries and their configuration, the selected toolchain and standard library, relevant environment,
and output-affecting Mix options. A same-timestamp edit rebuilds; a timestamp-only touch does not.
If a macro reads a non-Haxe file that is not declared as an HXML resource, declare it explicitly in
mix.exs (even when that file happens to live below a classpath or library root):
haxe: [
hxml_file: "build-server.hxml",
source_dir: "src_haxe",
target_dir: "lib",
extra_inputs: ["config/haxe/**/*.json"]
]The default watcher covers the discovered project classpaths, HXML locations, and these extra-input
roots. A normal mix compile also detects external library or toolchain changes that a project-local
filesystem watcher cannot observe directly.
If you repeatedly see messages like:
Haxe server port 6116 is in use; relocating to ...
it usually means a previous Mix VM crashed and left behind stale haxe --wait processes.
Clean them up (bounded, repo-local) and retry. The cleanup command checks both
launcher paths and process working directories, so it also finds a native Haxe
child whose Node/Lix launcher has already exited. Its process classifier checks
the executable and argument shape, so a shell or diagnostic command that merely
mentions haxe --wait is never selected:
scripts/haxe-server-cleanup.shThe focused lifecycle checks avoid the full generated-code test bootstrap:
npm run test:haxe-server-policy
npm run test:haxe-server-cleanup
npm run test:haxe-server-owner-exit