Skip to content

Latest commit

 

History

History
509 lines (392 loc) · 21 KB

File metadata and controls

509 lines (392 loc) · 21 KB

Development Workflow Guide

Day-to-day development practices for building applications and contributing to the compiler.

Quick Development Loop

Standard Workflow

# 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

⚠️ Important: See Compiler Flags Guide for critical information about which optimization flags to avoid (particularly -D analyzer-optimize).

File Watching (Recommended)

# 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 development

Install Git Hooks (Recommended)

Run this once per clone:

npm run hooks:install
  • Enables repo-managed pre-commit checks (path leaks, secrets scan, staged .hx auto-format, naming guards).
  • The repo hook exports Beads issues to .beads/issues.jsonl when 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:install replaces wrappers that call the removed bd sync --flush-only command.

Pre-commit Local Path Guard

  • 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-commit via scripts/lint/local_path_guard_staged.sh.

Dual-Ecosystem Architecture

Reflaxe.Elixir coordinates two development ecosystems for practical end-to-end validation:

🔧 Haxe Development Side (npm + lix)

Purpose: Build and test the Haxe→Elixir compiler itself

  • Package Manager: lix (modern Haxe package manager)
  • Runtime: npm scripts 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.hxml

Why 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.

⚡ Elixir Runtime Side (mix)

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)

Testing Strategy

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:sentinel

npm 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.

Testing Infrastructure Benefits

  1. Focused ownership: small snapshot, negative, macro, and ExUnit tests localize failures during implementation.
  2. Target acceptance: strict Elixir parsing, formatting, and warnings-as- errors catch invalid or poor generated source.
  3. Runtime behavior: Haxe-authored ExUnit and bounded smokes execute the emitted program on BEAM.
  4. Product integration: examples, OTP/Phoenix/Ecto tests, and thin Playwright flows preserve framework and browser contracts.
  5. Consumer/release evidence: isolated package installation and exact-head CI keep repository-only classpaths from masquerading as user success.

Test Error Interpretation

Expected Test Warnings ⚠️

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 installed

Why 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

Real Errors ❌

Actual compilation problems show with error symbols:

# Real error - shows with ❌ symbol
[error] ❌ Haxe compilation failed: src_haxe/Main.hx:5: Type not found : MyClass

Integration Flow

Haxe Source Code (.hx files)
     ↓ (npm/lix tools)
Reflaxe.Elixir Compiler  
     ↓ (generates)
Elixir Source Code (.ex files)
     ↓ (mix tools)  
Running BEAM Application

fast_boot vs full_prepasses Profiles

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, and ModuleMacro still run, but avoid expensive Context.getType / project‑wide scans where possible.
      • Template processing uses memoization and cheap shape checks when enabled.
    • 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_while must bind its returned accumulator back to the surrounding local.
      • Ultra‑late cosmetic hygiene passes (naming, underscore promotion, unused assignment cleanup) are skipped when fast_boot and disable_hygiene_final are 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.
  • 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 compile

See 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.

Haxe Compilation Server Policy

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.hxml

The 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.

Performance Objectives

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.

Key Development Files

Configuration Files

  • 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)

Testing Files

  • test/Test.hxml: Snapshot test runner configuration
  • test/snapshot/<category>/<case>/: Individual compiler cases with expected outputs
  • examples/todo-app/: Integration test as real Phoenix application

Source Code Files

  • src/reflaxe/elixir/ElixirCompiler.hx: Main compiler
  • src/reflaxe/elixir/helpers/: Feature-specific compilers (Changeset, OTP, LiveView, etc.)
  • std/: Phoenix/Elixir type definitions and externs

Source Mapping (Experimental)

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.

Full-Stack Development

Dual-Target Compilation

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

Development Workflow

  1. Shared Types: Define data structures in shared/ directory
  2. Dual Compilation: Build both targets with type-safe contracts
  3. Live Reload: Hot reload for both Elixir and JavaScript changes
  4. Type Safety: Compile-time checks across shared server/client contracts

See: Quick Start Patterns for copy‑paste, end‑to‑end patterns.

Contributing to the Compiler

Adding New Features

  1. Check existing implementations first - Search for similar patterns before starting
  2. Plan with documentation - Update roadmap with your feature
  3. Create helper compiler in src/reflaxe/elixir/helpers/
  4. Add annotation support to main ElixirCompiler.hx
  5. Write the focused regression under test/snapshot/<category>/<case>/
  6. Document thoroughly - Update guides and examples
  7. Run the focused owner, then every affected layer from the Testing Strategy; cross-cutting changes also run npm test
  8. Mark task complete - Verify implementation meets requirements

Adding Tests

// 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');
    }
}

Troubleshooting Development Issues

Haxe Version Issues

# Check the version this repo expects
cat .haxerc

# Check your installed Haxe
haxe --version

# Reset lix scope
npx lix scope create
npx lix download

Dependency Issues

# 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

Test Failures

# 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

Compilation Issues

# 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

Best Practices

Development Workflow

  • Prefer haxe from a proper Haxe install; if it’s not on your PATH, use the repo shim: ./node_modules/.bin/haxe ... (provided by lix + .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

Code Quality

  • 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_boot during iteration)

Git Workflow

  • Commit frequently with descriptive messages
  • Test before pushing to ensure CI/CD success
  • Update changelogs for user-facing changes
  • Reference issues in commit messages

Architecture Benefits

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)

Next Steps

For Application Development

For Compiler Development

For Troubleshooting


Ready to build? Check out Phoenix Integration to start building applications.

Haxe Compile Server (Explicit Long-Lived Workflow)

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.watch starts and reuses a server for the lifetime of that watcher. mix haxe.watch --once compiles directly and exits without starting one.
  • Explicit auto-start: set HAXE_SERVER_AUTOSTART=dev or always only 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=1 and 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.json to reduce port churn across restarts and enable stale-server cleanup.
  • Native owner: a packaged host helper ties the exact haxe --wait process 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=1 remains a hard override for automation that must never start a server.

Server Reuse and Build Freshness

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.

Cleanup (if ports keep relocating)

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.sh

The 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