Repository navigation
front-door/eshkol: admit exact rationals, IEEE doubles, and forward-mode AD - #1
Conversation
…ward-mode AD Why: docs/interop.md stated rational/float/AD were outside the front-door/eshkol gate. Add a second, independently admitted numeric dialect beside the existing exact-integer one: a grammar/type-check step where :int promotes freely into :rational or :float but the two never implicitly mix, a direct evaluator and an independently written kernel-VM oracle both lifted to dual numbers for derivative/gradient, and an Eshkol emitter/JIT/AOT gate that lowers to Eshkol's own derivative/gradient builtins. The exact-integer dialect in eshkol.lisp is untouched.
Why: exercise the new dialect the way eshkol-tests.lisp already exercises the integer one. Direct-vs-kernel agreement for rational exactness, a float sum checked under the stated ulp regime, a derivative checked against both the dual evaluator and an independent central-difference approximation, a gradient component, one negative case per addition (a literal zero exact denominator, a mixed exact/inexact operand pair, an out-of-range gradient component index), and emission sanity. The opt-in real-Eshkol rung runs the full JIT/AOT gate where ROSETTE_ISOLATED_WORKER's prlimit wrapper is available and falls back to a direct invocation of the real compiler where it is not, rather than silently skipping.
Why: interop.md said rational/float AD were outside the gate; state the admitted promotion lattice (:int into :rational or :float, never :rational into :float) and the explicit ulp comparison regime for :float readouts, and note the CHANGELOG entry in the repo's existing voice.
Eshkol displays (+ 0.1 0.2) as 0.3, and the worker's reader turns that into a SINGLE-FLOAT, so every real :float readout was refused at JIT. Reading it as a double would not be enough: 0.3d0 is one ulp from the computed value and passes the 2-ulp regime. Eshkol's INEXACT->EXACT cannot carry it either -- it converts over a fixed 2^52 denominator (0.1 -> 450359962737049/2^52). The emitted program now scales |x| by powers of two into an integer mantissa and prints (:dyadic m e); the gate decodes it, refuses decimal literals and non-finite values, and checks the receipt's :value-type against the type MAIN was admitted at. Real Eshkol JIT/AOT now match the direct evaluator bit-for-bit on 0.1+0.2, 1/3, a subnormal, and a negative value. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rting An ESHKOL-BACKEND-ERROR in one real-Eshkol case aborted the whole suite, so later checks never ran. Each real case now reports its stage/status and counts as one FAIL. Adds offline checks for the exact float readout, a float case that needs all 53 bits (1/3), and passes the container client policy to the numeric worker rung as the integer rung already does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tools/test-system front-door/tests printed 45 assertions and never loaded front-door/eshkol/tests, which is easy to read as full coverage. It now lists the suites it ran and names any suites of the same primary system it skipped. CONTRIBUTING documents the real-Eshkol opt-ins and the prlimit requirement. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RichardHoekstra
left a comment
There was a problem hiding this comment.
Thank you, and sorry it took us a couple of weeks to get to this. This is the first
outside PR on rosette-wire, and it's a really good one. It extends the gate exactly
the way we'd want: the numeric dialect sits beside the integer one rather than inside
it, the no-implicit-exact/inexact-mixing rule is the right call, and every addition
comes with a refusal test. It also closes an item docs/interop.md listed as out of
scope.
We ran it end-to-end on Linux (aarch64, eshkol:gb10-run container, Eshkol 1.3.3,
via ROSETTE_ESHKOL_CONTAINER):
| Run | Result |
|---|---|
tools/test-system front-door/eshkol/tests, no Eshkol |
37 passes, 0 failures (all 14 new checks pass) |
| Same, with real Eshkol | integer four-way gate ✅, rational through the worker ✅, float ❌ MALFORMED-RECEIPT at JIT |
The float failure was our infrastructure, not your code, so we've fixed it, and
you had no way to see it: the worker path needs prlimit, which macOS doesn't have.
Your CI run was also sitting on "approve workflow" for first-time contributors, and
we hadn't clicked it. Sorry about that.
What we pushed to this branch (3 commits on top of yours)
fix(front-door-eshkol): read :float receipts as the exact double. The
worker read Eshkol's0.3as aSINGLE-FLOAT, so every real float receipt was
refused. Reading it as a double wouldn't have been enough:displayrounds
0.1+0.2to0.3, which is 1 ulp off and would pass the 2-ulp regime. The
emitted program now scales the double by powers of two and prints(:dyadic m e)
with an integer mantissa. The gate decodes that, refuses decimal and non-finite
readouts, and checks:value-typeagainst the typemainwas admitted at.
Real JIT/AOT now match your direct evaluator bit-for-bit on 0.1+0.2, 1/3,
a subnormal, and a negative value. If you put the old decimal readout back, it's
refused.test(front-door-eshkol): count real-toolchain failures instead of aborting.
OneESHKOL-BACKEND-ERRORused to abort the whole suite; now it's one FAIL
with its stage. This commit also adds offline checks for the readout and a
1/3 case that needs all 53 bits.tools(test-system): report which test suites ran.tools/test-system front-door/testsquietly skipped the Eshkol suite. Now it prints what ran and
what it skipped. CONTRIBUTING documents the real-Eshkol opt-ins andprlimit.
With these, on Linux against real Eshkol (1.3.3, container):
tools/test-system front-door gives 45/0 plus 55/0 for the Eshkol suite,
and make contributor-check passes.
Something you may want on the Eshkol side
While building the readout we found that inexact->exact converts over a fixed
2^52 denominator in 1.3.3: (inexact->exact 0.1) gives
450359962737049/4503599627370496 (the exact value is 3602879701896397/2^55),
and (inexact->exact 1e-300) gives 0. R7RS asks for the exact equivalent. Also,
display prints doubles with fewer than round-trip digits (0.1+0.2 gives 0.3,
1/3. gives 0.333333). Happy to open an issue with the repro if that's useful,
or maybe it's already different in 1.3.5?
Small things on the PR itself
None of these block merging. If you'd rather we fold them in, just say so.
- The direct-invocation fallback parses the whole receipt line (inline comment on the fallback).
numeric-program-evaldropsfunswhen it re-parses (inline comment onnumeric-program-eval).- A derivative test through
/would give the quotient rule an independent check (inline comment on the kernel oracle).
Nits (take or leave)
- Only a literal
0denominator is refused.(/ 1 (- 1 1))still reaches a raw CL
division-by-zero. Fine as documented behaviour if you prefer. +default-ulp-bound+is adefparameter. Maybedefconstant?%format-double's docstring mentions.scratch/, which isn't in the tree.prlimit-available-paccepts any exit code.zeropis probably what's meant.
🤖 Generated with Claude Code
| (value-line (car (last (remove "" (uiop:split-string output :separator '(#\Newline)) | ||
| :test #'string=))))) | ||
| (ignore-errors (delete-file path)) | ||
| (format t "~& INFO ~A => ~A~%" (getf case :name) (string-trim '(#\Space) (or value-line ""))) | ||
| (check-anon (format nil "real-eshkol-numeric-direct-~A" (getf case :name)) | ||
| (let ((*read-default-float-format* 'double-float)) | ||
| (handler-case | ||
| (= (read-from-string (string-trim '(#\Space) value-line)) |
There was a problem hiding this comment.
value-line is the last non-empty stdout line, which is the whole receipt:
ROSETTE-FRONT-DOOR-ESHKOL-NUMERIC-RESULT (:schema … :value …). So
read-from-string returns the marker symbol and the check fails. Taking the text
after +numeric-receipt-marker+ and decoding :value from the plist should fix it.
There's now esh::%numeric-accept-result, which handles the new (:dyadic m e)
float encoding, so the fallback can reuse it. We couldn't run this branch on Linux,
so this comes from the receipt shape we observed, not from an actual run.
| (t (%numeric-refuse :eval term "outside the numeric dialect"))))) | ||
| (t (%numeric-refuse :eval term "not a numeric-dialect term")))) | ||
|
|
||
| (defun numeric-program-eval (program) |
There was a problem hiding this comment.
This re-parses only (main-numeric …), so a main that calls
derivative/gradient is refused with "call to undefined function". Skipping the
re-parse (the program is already admitted) or passing the defun-numeric forms
along should fix it.
| (eval-numeric-term (numeric-program-main program) '() (numeric-program-funs program))) | ||
|
|
||
| ;;; ------------------------------------------------------------------------- | ||
| ;;; Coincidence 2: the kernel-VM oracle, written independently. |
There was a problem hiding this comment.
The kernel walk and dual*/dual/ share the same product and quotient rules, so the
central difference is the independent derivative check, and it only covers cube.
A case like x/(x+1) against the central difference would cover the quotient rule.
Minor: the header mentions (:INT n)/(:RAT …)/(:FLT d) tags, but only duals are
tagged. Maybe adjust the comment?
|
Thank you for the careful review, and for running it end to end on Linux — the dyadic readout is a better design than what I had, and catching the On the Eshkol side, both are fixed in 1.3.5, which is about to ship: I'll push fixes for the three inline points (the fallback receipt parse, |
…computed zero denominators numeric-program-eval re-checked only MAIN-NUMERIC, so a MAIN that calls DERIVATIVE or GRADIENT on a DEFUN-NUMERIC was refused as an undefined function. It now re-checks the whole program. A zero denominator the term computes, as in (/ 1 (- 1 1)), reached CL's DIVISION-BY-ZERO. The direct evaluator and the kernel walk now refuse it the same way the literal case is refused at type-check. Also: +default-ulp-bound+ is a defconstant, the kernel walk's header describes the values it actually walks, %format-double's docstring no longer cites an untracked directory, and the readout comments say which Eshkol releases print and convert doubles inexactly.
…the quotient rule and computed zeros The macOS direct-invocation fallback read the whole receipt line with READ-FROM-STRING and so compared the marker symbol. It now reads the plist after the marker and decodes :VALUE with %decode-numeric-readout, comparing floats under the ulp regime. Adds a derivative of x/(x+1) checked against a central difference, which is independent of the quotient rule the two evaluators share; a check that numeric-program-eval keeps DEFUN-NUMERIC helpers; and refusal checks for computed exact and inexact zero denominators in both evaluators. prlimit-available-p now requires a zero exit status.
|
Pushed two commits on top of yours:
Checked on macOS against eshkol-run 1.3.5: the new checks pass, including the fallback path on a float sum, a rational, a derivative, and the quotient-rule derivative (all four decode through the dyadic readout). One thing I couldn't do from macOS: the full |
|
Update: ran the full suite on Linux (x86_64, with
Without Eshkol the Eshkol suite is 50/0 on both x86_64 and aarch64. |
|
Thanks! You fixed every point, nits included, and the Eshkol-side fixes are great to hear about. Refusing a computed zero denominator is better than what I suggested. It also means the direct evaluator, the kernel walk and Eshkol can't disagree about a non-finite result, because none of them is allowed to produce one. I read through CI on
Merging now. Thanks again for the PR, and for the Eshkol fixes that came out of it. |
Extends the Eshkol adapter's admitted dialect beyond exact integers, as docs/interop.md listed as out of scope.
:intpromotes into:rational; Eshkol's numeric tower keeps them exact, and the direct evaluator and kernel VM agree.derivativeandgradient: the direct evaluator gains a dual-number evaluator, the kernel VM an independently written dual-number oracle, and Eshkol JIT/AOT are the systems under test.The existing integer dialect is untouched; rational and float never mix implicitly. Tests cover each addition positively (rational exactness, a float sum at the stated bound, a derivative against both the dual evaluator and a central difference, a gradient component) and negatively (zero exact denominator, mixed exact/inexact operands, out-of-range gradient index).
tools/test-system front-door/testsreports 45 assertions, 0 failures before and after. The four positive cases were also checked by hand against eshkol-run 1.3.5 (JIT and AOT).One note: the shared harness wraps Eshkol launches in
prlimit, which is Linux-only; on macOS the new tests fall back to direct invocation rather than skipping. That gap predates this change.