Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -820,4 +820,5 @@
{"_type":"issue","id":"haxe.rust-t8e","title":"Emit profile contract report artifacts for portable|metal builds","description":"Add opt-in profile contract artifacts (json+md) that record effective profile policy switches and contract diagnostics for deterministic CI review.","notes":"Source spike: haxe.rust-8z8. Keep deterministic ordering and typed serializers.","status":"closed","priority":4,"issue_type":"task","owner":"boss@fullofcaffeine.com","created_at":"2026-02-24T17:48:34Z","created_by":"Marcelo Serpa","updated_at":"2026-02-24T18:09:26Z","closed_at":"2026-02-24T12:09:26Z","close_reason":"Closed","labels":["architecture","family","profiles"],"dependencies":[{"issue_id":"haxe.rust-t8e","depends_on_id":"haxe.rust-8z8","type":"discovered-from","created_at":"2026-02-24T11:48:34Z","created_by":"Marcelo Serpa","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"haxe.rust-8z8","title":"Spike: align reflaxe.rust to family portable|metal contract after haxe.go canonicalization","description":"## Why\nCross-compiler investigation indicates the family should standardize on `portable|metal` as profile contracts, with profiles as presets over orthogonal axes (strictness, runtime policy, interop policy), while preserving target-specific internals.\n\n## Inputs\n- Canonical direction from `../haxe.go/docs/portable-canonical-contract.md`\n- Go phase roadmap: `../haxe.go/docs/phase2-roadmap.md`\n- Go selective runtime plan: `../haxe.go/docs/hxrt-selective-runtime.md`\n- Rust implementation references:\n - `src/reflaxe/rust/ProfileResolver.hx`\n - `src/reflaxe/rust/analyze/HxrtFeatureAnalyzer.hx`\n - `src/reflaxe/rust/emit/ProjectEmitter.hx`\n - `src/reflaxe/rust/passes/NoHxrtPass.hx`\n - `docs/profiles.md`, `docs/metal-profile.md`\n\n## Spike Scope\n1. Validate where Rust already matches the family model and where terminology/contracts drift.\n2. Define a family-aligned artifact contract for Rust:\n - profile contract report\n - runtime plan report (features selected + reasons)\n3. Propose minimal changes needed for explicit family parity without regressing Rust-specific strengths (feature slicing, no-hxrt, fallback).\n\n## Deliverables\n- `docs/spikes/family-portable-metal-alignment.md` (or equivalent) with:\n - current state\n - gap matrix\n - migration tasks\n - risk notes\n- Beads follow-up tasks created from that matrix (all low priority unless blocking).\n\n## Future Follow-up Trigger\nAfter `haxe.go` lands selective runtime slicing + metal fallback + runtime-plan reporting, run a new GPT-5.2 Pro cross-repo spike comparing latest Go/Rust/Elixir/hxhx and refresh family guidance.\n","status":"closed","priority":4,"issue_type":"task","owner":"boss@fullofcaffeine.com","created_at":"2026-02-24T17:12:05Z","created_by":"Marcelo Serpa","updated_at":"2026-07-19T21:15:36Z","closed_at":"2026-02-24T11:49:16Z","close_reason":"Closed","labels":["architecture","family","metal","portable","spike"],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"haxe.rust-oo3.11","title":"Milestone 10 — Performance + idiomatic mode optimizations","description":"Deliverables:\n- Optional `-D rust_native` / `-D rust_idiomatic` mode\n- Arrays → `Vec\u003cT\u003e` when semantics allow\n- Reduce clones in hot paths\n- Better loop lowering to iterators\n- Optional `rustfmt` integration (`-D rustfmt`)\n- Compilation output determinism guarantees\n","acceptance_criteria":"- Benchmarks show measurable improvements on representative workloads\n- Output stays readable and stable across runs\n","status":"closed","priority":4,"issue_type":"epic","owner":"boss@fullofcaffeine.com","created_at":"2026-01-23T18:08:53Z","created_by":"Marcelo Serpa","updated_at":"2026-01-24T02:17:36Z","closed_at":"2026-01-23T20:17:36Z","close_reason":"Closed","labels":["idiomatic","m10","milestone","perf"],"dependencies":[{"issue_id":"haxe.rust-oo3.11","depends_on_id":"haxe.rust-oo3","type":"parent-child","created_at":"2026-01-23T12:08:53Z","created_by":"Marcelo Serpa","metadata":"{}"},{"issue_id":"haxe.rust-oo3.11","depends_on_id":"haxe.rust-oo3.10","type":"blocks","created_at":"2026-01-23T12:08:54Z","created_by":"Marcelo Serpa","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"haxe_rust-4tc8","title":"Remove dead null throw after nullable scalar switch narrowing","description":"A Haxe switch over Null\u003cInt\u003e can lower to an outer null guard and a non-null branch that still treats the bound value as Option\u003ci32\u003e. The generated Rust rematches that Option and includes a dead hxrt Null Access throw. codex-hxrust exposed the problem while it narrowed sys.io.Process.exitCode(true) before storing an Int in a typed report. Keep the fix generic and compiler-owned. Do not add Codex or process-specific recognition.","acceptance_criteria":"Add a product-neutral Haxe fixture for a Null\u003cInt\u003e switch with null and bound-value arms. Before the fix, the fixture must show the redundant Option rematch and dead Null Access throw. After the fix, generated Rust must bind or unwrap the non-null value without that dead throw. The fixture must build and run in the applicable profiles. Adjacent nullable-scalar and switch gates, repository hooks, and the unchanged codex-hxrust consumer gate must pass. Inspect the generated Rust and record deferred nullable shapes, if any.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"repository-maintainer","owner":"boss@fullofcaffeine.com","created_at":"2026-09-01T01:09:28Z","created_by":"repository-maintainer","updated_at":"2026-09-01T01:09:28Z","labels":["codegen","consumer-pressure","nullability","output-quality","thinking:high"],"dependencies":[{"issue_id":"haxe_rust-4tc8","depends_on_id":"haxe.rust-luv2","type":"discovered-from","created_at":"2026-09-01T01:09:28Z","created_by":"repository-maintainer","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"haxe_rust-igkk","title":"Reduce inherited-shim duplication in generated Rust","description":"Subclass lowering copies inherited method bodies and emits broad forwarding trait implementations for each child. A small compatibility subclass of a 658-line typed session generated roughly 2,000 Rust lines, while the base session generated roughly 2,300 lines after concrete and interface adapters. Preserve Haxe inheritance and interface dispatch while reducing duplicate generated methods and review noise generically.","design":"Start from a small framework-neutral base, child, and interface fixture. Measure generated functions and lines before changing lowering. Reuse base implementations or emit only required child adapters without changing virtual dispatch, private-helper access, self-to-interface coercion, or public compatibility.","acceptance_criteria":"A generic inheritance fixture preserves interpreter and generated-Rust behavior while materially reducing duplicate child method bodies and forwarding adapters. Existing inheritance, generic-interface specialization, and inherited-interface-self fixtures remain green. Generated Rust is warning-clean and rustfmt-clean, and a consumer output review confirms that a test-only compatibility subclass no longer duplicates the complete base implementation.","status":"open","priority":1,"issue_type":"task","owner":"boss@fullofcaffeine.com","created_at":"2026-08-30T19:31:35Z","created_by":"repository-maintainer","updated_at":"2026-08-30T19:31:35Z","labels":["compiler","inheritance","output-quality","thinking:high"],"dependencies":[{"issue_id":"haxe_rust-igkk","depends_on_id":"haxe_rust-uwml","type":"discovered-from","created_at":"2026-08-30T19:31:35Z","created_by":"repository-maintainer","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
2 changes: 1 addition & 1 deletion docs/rust-raw-authority-inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
},
{
"path": "src/reflaxe/rust/RustCompiler.hx",
"line": 17386,
"line": 17491,
"enclosingFunction": "addSpec",
"factory": "traitImplementationAt",
"authorityId": "metadata-owned",
Expand Down
2 changes: 1 addition & 1 deletion docs/semantic-confidence-summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"portableCandidateMissingFromTier2": 0,
"portableSemanticDiffCases": 37,
"laneSemanticDiffCases": 2,
"snapshotCases": 141,
"snapshotCases": 142,
"bucketsByClass": {
"compile_inventory": 2,
"targeted_semantic_parity": 8,
Expand Down
2 changes: 1 addition & 1 deletion docs/semantic-confidence-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This summary rolls up the current evidence buckets without pretending that Tier2
- Portable candidate missing from Tier2: `0`
- Portable semantic-diff cases: `37`
- Lane semantic-diff cases: `2`
- Snapshot cases: `141`
- Snapshot cases: `142`
- Compile/inventory buckets: `2`
- Targeted semantic/runtime buckets: `8`
- Snapshot/smoke-only buckets: `7`
Expand Down
6 changes: 3 additions & 3 deletions scripts/lint/dynamic_allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ src/reflaxe/rust/RustCompiler.hx:2803
src/reflaxe/rust/RustCompiler.hx:2806
src/reflaxe/rust/RustCompiler.hx:2814
src/reflaxe/rust/RustCompiler.hx:10839
src/reflaxe/rust/RustCompiler.hx:13636
src/reflaxe/rust/RustCompiler.hx:13655
src/reflaxe/rust/RustCompiler.hx:19875
src/reflaxe/rust/RustCompiler.hx:13641
src/reflaxe/rust/RustCompiler.hx:13660
src/reflaxe/rust/RustCompiler.hx:19980
src/reflaxe/rust/RustSourceMap.hx:419
src/reflaxe/rust/analyze/RepresentationAnalysisSnapshot.hx:77
src/reflaxe/rust/analyze/RepresentationAnalysisSnapshot.hx:81
Expand Down
105 changes: 105 additions & 0 deletions src/reflaxe/rust/RustCompiler.hx
Original file line number Diff line number Diff line change
Expand Up @@ -11128,6 +11128,11 @@ class RustCompiler extends GenericCompiler<RustFile, RustFile, RustExpr, RustFil
compileUnop(op, postFix, expr, e);

case TIf(cond, eThen, eElse):
if (eElse != null) {
var narrowed = compileNullableScalarSwitchNarrowing(cond, eThen, eElse, e.t);
if (narrowed != null)
return narrowed;
}
var condExpr = coerceExprToExpected(compileExpr(cond), cond, Context.getType("Bool"));
if (eElse == null) {
// `if (...) expr;` in Haxe is statement-shaped; ensure the Rust `if` branches yield `()`.
Expand Down Expand Up @@ -14184,6 +14189,106 @@ class RustCompiler extends GenericCompiler<RustFile, RustFile, RustExpr, RustFil
}
}

/**
Lowers Haxe's proven non-null scalar switch arm without a second null check.

Why
- Haxe rewrites `switch (value:Null<Int>)` into a null test and a non-null branch.
- The non-null branch contains `var bound = value; bound`, but both local types remain
`Null<Int>` in the typed tree.
- Normal nullable coercion must keep a catchable `Null Access` branch. In this exact branch,
the preceding condition already proves that the value is present.

What
- Recognizes only the two-expression alias block produced for a nullable scalar switch.
- Emits one native `Option` match that binds the present payload directly.

How
- Requires the null comparison, alias initializer, and alias tail to name the same Haxe local.
- Peels only metadata, parentheses, and transparent singleton blocks around the alias tail.
- Requires a Copy inner type and the exact non-null Rust result type, then compiles the source once.
- Returns `null` for every other conditional so ordinary Haxe null behavior stays unchanged.
**/
function compileNullableScalarSwitchNarrowing(cond:TypedExpr, thenExpr:TypedExpr, elseExpr:TypedExpr,
expected:Type):Null<RustExpr> {
var condition = unwrapMetaParen(cond);
var comparedLocal:Null<TVar> = null;
var nullWhenTrue = false;
switch (condition.expr) {
case TBinop(op, left, right) if (op == OpEq || op == OpNotEq):
var leftValue = unwrapMetaParen(left);
var rightValue = unwrapMetaParen(right);
if (isNullConstExpr(leftValue)) {
switch (rightValue.expr) {
case TLocal(variable): comparedLocal = variable;
case _:
}
} else if (isNullConstExpr(rightValue)) {
switch (leftValue.expr) {
case TLocal(variable): comparedLocal = variable;
case _:
}
}
nullWhenTrue = op == OpEq;
case _:
}
if (comparedLocal == null)
return null;

var presentBranch = nullWhenTrue ? elseExpr : thenExpr;
var nullBranch = nullWhenTrue ? thenExpr : elseExpr;
var present = unwrapMetaParen(presentBranch);
var expressions = switch (present.expr) {
case TBlock(items) if (items.length == 2): items;
case _: null;
};
if (expressions == null)
return null;

var alias:Null<TVar> = null;
var initializer:Null<TypedExpr> = null;
switch (unwrapMetaParen(expressions[0]).expr) {
case TVar(variable, value) if (value != null):
alias = variable;
initializer = unwrapMetaParen(value);
case _:
}
if (alias == null || initializer == null)
return null;

var sourceMatches = switch (initializer.expr) {
case TLocal(variable): variable.id == comparedLocal.id;
case _: false;
};
function unwrapAliasTail(expression:TypedExpr):TypedExpr {
var current = unwrapMetaParen(expression);
return switch (current.expr) {
case TBlock(items) if (items.length == 1): unwrapAliasTail(items[0]);
case _: current;
};
}
var tailMatches = switch (unwrapAliasTail(expressions[1]).expr) {
case TLocal(variable): variable.id == alias.id;
case _: false;
};
if (!sourceMatches || !tailMatches)
return null;

var inner = nullOptionInnerType(alias.t, present.pos);
if (inner == null || !isCopyType(inner))
return null;
if (!rustTypesEqual(toRustType(inner, present.pos), toRustType(expected, present.pos)))
return null;

// The emitted match replaces the typed null comparison, so consume that skipped local read.
consumeLocalRead(comparedLocal);
var nullRust = coerceExprToExpected(compileBranchExpr(nullBranch), nullBranch, expected);
return EMatch(compileExpr(initializer), [
{pat: PPath(RustPath.single("None")), expr: nullRust},
{pat: PTupleStruct(RustPath.single("Some"), [PBind("__hx_value")]), expr: rustSingleExpr("__hx_value")}
]);
}

/**
Checks whether one function type parameter disappears from every lowered Rust argument type.

Expand Down
44 changes: 44 additions & 0 deletions test/snapshot/nullable_scalar_switch_narrowing/Main.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class Main {
static function readExit(value:Null<Int>):Null<Int> {
return value;
}

static function closeSource():Void {}

static function narrowRead(value:Null<Int>):Int {
try {
final exit = readExit(value);
closeSource();
final concreteExit:Int = switch (exit) {
case null: -1;
case concrete: concrete;
}
return concreteExit;
} catch (_:haxe.Exception) {
return -2;
}
}

static function narrow(value:Null<Int>):Int {
return switch (value) {
case null: -1;
case concrete: concrete;
};
}

static function narrowFloat(value:Null<Float>):Float {
return switch (value) {
case null: -1.5;
case concrete: concrete;
};
}

static function main():Void {
if (narrowRead(9) != 9) throw "read narrowing failed";
if (narrowRead(null) != -1) throw "null read narrowing failed";
if (narrow(7) != 7) throw "non-null narrowing failed";
if (narrow(null) != -1) throw "null narrowing failed";
if (narrowFloat(2.5) != 2.5) throw "non-null Float narrowing failed";
if (narrowFloat(null) != -1.5) throw "null Float narrowing failed";
}
}
8 changes: 8 additions & 0 deletions test/snapshot/nullable_scalar_switch_narrowing/compile.hxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-cp .
-lib reflaxe.rust
-D reflaxe_rust_strict_examples
-D rust_output=out
-D rust_cargo_subcommand=run
-D rust_cargo_quiet
-D reflaxe.dont_output_metadata_id
-main Main
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-cp .
-lib reflaxe.rust
-D reflaxe_rust_profile=metal
-D rust_output=out_metal
-D rust_cargo_subcommand=run
-D rust_cargo_quiet
-D reflaxe.dont_output_metadata_id
-main Main
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
**/*.rs.bk
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "hx_app"
version = "0.0.1"
edition = "2021"
rust-version = "1.96.0"
resolver = "3"

[dependencies]
hxrt = { path = "./hxrt", default-features = false, features = ["core"] }
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
[package]
name = "hxrt"
version = "0.0.1"
edition = "2021"
rust-version = "1.96.0"
resolver = "3"

[lib]
path = "src/lib.rs"

[features]
default = ["core", "io", "fs", "process", "thread", "date", "json", "db", "ssl", "net", "async"]
core = ["io", "dep:serde"]
io = []
fs = []
process = ["fs"]
thread = []
date = ["dep:chrono"]
json = ["dep:serde_json"]
db = []
ssl = [
"io",
"dep:rustls",
"dep:rustls-native-certs",
"dep:rustls-pemfile",
"dep:webpki-roots",
"dep:ring",
"dep:pem-rfc7468",
"dep:md-5",
"dep:ripemd",
"dep:sha1",
"dep:sha2",
"dep:x509-parser",
"dep:oid-registry",
]
net = ["io", "ssl", "dep:mio", "dep:socket2", "dep:hostname", "dep:dns-lookup"]
async = ["dep:pollster", "dep:futures-timer", "dep:futures"]
async_tokio = ["async", "dep:tokio"]

[dependencies]
chrono = { version = "0.4", default-features = true, optional = true }
mio = { version = "0.8", features = ["os-poll", "net"], optional = true }
socket2 = { version = "0.5", optional = true }
hostname = { version = "0.4", optional = true }
dns-lookup = { version = "2", optional = true }
parking_lot = "0.12"
rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "ring"], optional = true }
rustls-native-certs = { version = "0.7", optional = true }
rustls-pemfile = { version = "2", optional = true }
webpki-roots = { version = "0.26", optional = true }
ring = { version = "0.17", optional = true }
pem-rfc7468 = { version = "0.7", features = ["std"], optional = true }

# `haxe.Json`
serde = { version = "1", optional = true }
serde_json = { version = "1", optional = true }

# async preview runtime bridge
pollster = { version = "0.4", optional = true }
futures-timer = { version = "3", optional = true }
futures = { version = "0.3", default-features = false, features = ["std"], optional = true }
tokio = { version = "1", default-features = false, features = ["rt", "time", "macros"], optional = true }

# `sys.ssl.Digest`
md-5 = { version = "0.10", optional = true }
ripemd = { version = "0.1", optional = true }
sha1 = { version = "0.10", optional = true }
sha2 = { version = "0.10", optional = true }

# `sys.ssl.Certificate` parsing helpers.
x509-parser = { version = "0.16", optional = true }
oid-registry = { version = "0.7", optional = true }

[dev-dependencies]
rcgen = "0.13"
Loading
Loading