Skip to content

Type-core: natural float params + results (Increments A/A+/B)#7

Merged
zemo-g merged 7 commits into
masterfrom
feat/type-layer
May 31, 2026
Merged

Type-core: natural float params + results (Increments A/A+/B)#7
zemo-g merged 7 commits into
masterfrom
feat/type-layer

Conversation

@zemo-g
Copy link
Copy Markdown
Owner

@zemo-g zemo-g commented May 31, 2026

Summary

Makes float scalars work as ordinary user-function parameters AND results, so natural ML code compiles without float_arr boxing or out-cell threading. This is the "checked type layer" enabling substrate for the verifiable-language pillars (auth-typed data, attested numerics, source-to-source AD).

Three compiler increments + a proof example + handoff/findings docs (all gated to the byte-identical self-hosting fixed point):

  • d653a72comparison float-params: a < b hits fcmp instead of _rail_lt deref-segfault
  • f6231c8Increment A (arithmetic): whole-call-site agreement (__argf_<fn>_<i>) marks a param __float_ iff every call site passes a provably-float arg
  • f042b6bIncrement A+ (int-param dual): true lub (all-float→float, all-int→int); fixes the 0.0 + int_param denormal
  • 4cf64deIncrement B (float-result-compose): a float user-fn RESULT bound to a let-local composes downstream, via a __vf_ marker threaded by argf_scan_d + read in infer_ty's V-case
  • ee25340 — proof artifact examples/mlp_natural.rail (segfaults on master, runs on this branch)
  • bec321f / dcf255f — Increment B handoff + the independent-verification findings

What it fixes (before → after, identical source)

Case master this branch
mul2 3.0 4.0 4.94e-324 12
addk 5 (0.0 + x) 5.43e-323 5
let y = mlp 1.0 2.0; mse y 1.0 5.3e+36 0.015625
5-float-param neuron (arity ≥4) segfault correct

Verification (independently reproduced from source, not trusted)

  • 146/146 tests
  • byte-identical self-hosting fixed point — the committed binary self-compiles to itself, SHA-256 b5e357706114f2106fece1e87a1b105528d9d0d5b4cf0f8cf0ac9bb751de3df1
  • diff-fuzzer --seed=42 --n=2020 agree / 0 divergence
  • examples/mlp_natural.rail1.125 / 0.375 / 0 / 2.75
  • t140 confirmed a real regression lock (parent bec321f5.3e+36 on identical source)
  • The two-pass returns_float/collect_float_ret_fns machinery is untouched — the fix is strictly additive, inert for int code (fuzzer grammar unaffected)

Deliberately NOT included

Recursive-float-return + show on an unboxed float (Phase 2). The joint-fixpoint + param V-tweak path remains documented for if/when it's needed.

Test plan

  • ./rail_native test → 146/146
  • ./rail_native self → byte-identical fixed point (cmp rail_native /tmp/rail_self)
  • ./rail_native run tools/fuzz/diff_fuzz.rail --seed=42 --n=20 → 20/20 agree
  • ./rail_native run examples/mlp_natural.rail → 1.125 / 0.375 / 0 / 2.75

🤖 Generated with Claude Code

zemo-g and others added 7 commits May 29, 2026 20:36
…er fns work

param_is_float now marks a param __float_ when it is an operand of a
comparison (< > <= >= == !=) whose other operand is unambiguously float
(an FL literal or an already-float operand). Previously such params
(e.g. minmod a b = if a < b then a else b) stayed unmarked, so `a < b`
fell through to the _rail_lt runtime helper, which dereferences an
even-LSB operand as a heap pointer -> data-dependent segfault (and
returned the wrong value for odd-LSB bit patterns).

This is safe where the arithmetic-sibling rule was not: `int < 0.0` is
ALREADY mishandled today (the FL literal forces the native-float path,
which reads the int's tag bits as a double), so marking here cannot
break a currently-correct mixed pattern. By contrast `0.0 +. int` IS
correct via runtime mixed-promotion, which is why arithmetic-sibling
marking stays omitted.

Real-world victim: stdlib/mhd_kernel.rail mk_minmod, which broke the
entire MUSCL second-order solver path -- the repo's own
tools/plasma/mhd_muscl_test.rail segfaulted. With this fix the
UNMODIFIED kernel runs: machine-precision conservation (dm 2e-13,
dE 6e-15), divB growth as documented, sharpness ratio 1.76 vs LF.

t136 float_user_fn_minmod locks the regression. 142/142, byte-identical
self-compile fixed point (gen2 == gen3).
…ia call-site agreement

A user-fn param is marked __float_ when EVERY call site passes a provably-float arg (infer_ty==2)
in that position -- new __argf_<fn>_<i> whole-program analysis (collect_argf/argf_scan) with a
COMPLETE AST traversal (incl. lambda bodies), partial-application demotion, and value-reference
demotion (a fn used as a value can't have its indirect calls enumerated -> demote). Hooked in
compile_func after mark_float_params. Monotone-safe: any int/unknown/under-applied/value-ref site
demotes the slot, so legit int-param code is never mismarked.

Fixes the long-standing bug: mul2 a b = a * b; mul2 3.0 4.0 was 4.94e-324 (tagged ints read as
doubles), now 12. Validated: 144/144 (new t137 arith_float_2arg + t138 argf_int_param_safe guard),
byte-identical 2-pass fixed point (gen2==gen3), diff-fuzzer 20/20 agree / 0 divergence.

Found en route (out of scope, NOT fixed): 0.0 + int_param is a SEPARATE pre-existing miscompile
(denormal via the use_float else-fmov path) -- the design doc's 'mixed-promotion correct for params'
claim is false. Candidate for a later increment (runtime tag-check on unknown float-context operands).
…es 0.0+int_param denormal)

Generalizes Increment A's call-site analysis to a true lub: a param's __argf_<fn>_<i> slot is 2 iff
EVERY call site is float, 1 iff every site is int, else 0 (mixed/unknown/value-ref/under-applied).
compile_func marks the param __float_ (slot 2) or __int_ (slot 1, never overriding an existing
__float_). An int param in a float context (0.0 + x) is then scvtf-promoted instead of read as
float bits.

Fixes the denormal found during Increment A: addk x = 0.0 + x; addk 5 was 5.43e-323, now 5.
Validated: 145/145 (new t139 argi_int_into_float), byte-identical fixed point (gen2==gen3),
diff-fuzzer 20/20 agree / 0 divergence. Monotone-safe by the lub (any disagreeing site -> unmarked).
…type-core fixes

A 2-layer MLP forward pass (5-float-param neurons, chained hidden activations,
ReLU) written with float scalars as ordinary params -- no float_arr boxing, no
out-cell workarounds. SEGFAULTS on the pre-type-layer compiler (master 548dcd7);
runs correctly since d653a72 + f6231c8 (A) + f042b6b (A+). Verified before/after:
master crashes, this prints mlp(1.0,2.0)=1.125 / mlp(1.0,0.0)=0.375.
…nt inference fixpoint)

Execution-ready handoff for a fresh dedicated session to tackle the fixed-point-risky
float-RESULT inference fixpoint (the mse/loss-composition gap diagnosed this session).
Includes the minimal repro, keystone line refs (returns_float:1989, infer_ty:2411,
get_arities two-phase), a Phase-0-diagnose-first plan, the bootstrap discipline + gates,
and the gotchas. Deliberately deferred from marathon-tail per the never-gamble-the-fixed-point rule.
…marker

A float user-fn RESULT bound to a let-local (let y = mlp 1.0 2.0) now composes
as a float at a later call site (mse y 1.0): was 5.3e+36 garbage, now 0.015625.
Unblocks natural ML code (autograd/transformer) where a forward pass is bound
to a local before loss/activation.

Mechanism: argf_scan's let case (new argf_scan_d) threads a __vf_<name> marker
into the rmap iff the bound value is provably float (infer_ty==2); infer_ty's
"V" case reads it so a bare local contributes FLOAT(2) instead of UNKNOWN(0).
__argf_mse_0 then sees y as float -> pred marked __float_ -> d*d takes the
float-mul path. Inert for int code (infer_ty never 2 there; TD tuple-lets are
HEAP-typed so the marker branch never fires) -- diff-fuzzer grammar unaffected.

Phase 0 empirically showed Increment A (call-site agreement) was already
landed, so the only remaining gap was let-local propagation: a 2-edit fix,
not the design's feared 2.8 joint fixpoint.

Locked by t140 float_result_compose; 146/146, byte-identical fixed point
(gen2==gen3), diff-fuzzer seed=42 n=20 (20/20 agree, 0 divergence).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ndently verified

Increment B (float-result-compose) is done and re-verified from source by a
separate session. Records the conclusion + findings on the handoff doc:

- What landed: a 2-edit additive fix (argf_scan_d __vf_ marker + infer_ty V-case),
  NOT the feared joint inference fixpoint. Two-pass machinery left untouched.
- Independent verification (throwaway worktree, canonical clone + beacon untouched):
  146/146; mse y 1.0 -> 0.015625; mlp_natural unchanged; diff-fuzzer 20/20;
  byte-identical fixed point (self-compile SHA-256 b5e35770...de3df1 == committed);
  t140 confirmed a real regression lock (parent bec321f, same source -> 5.3e+36).
- Verdict: correct and honest -- scope claimed == scope delivered.
- Finding: the handoff's required-reading ref (rail-compiler-phase-design.md §2.8)
  lives on feat/verifiable-language @ 337d66a, not this branch; noted inline.
- Open: recursive-return + show-on-unboxed-float (Phase 2) deliberately not done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@zemo-g zemo-g merged commit a55e4fa into master May 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant