Skip to content

The prompt rebinds the operators it says it rebinds (#1133) - #1134

Merged
Rafael-SOWNet merged 4 commits into
masterfrom
the-prompt-rebinds-what-it-says-it-rebinds
Sep 1, 2026
Merged

Rafael-SOWNet merged 4 commits into
masterfrom
the-prompt-rebinds-what-it-says-it-rebinds

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

Closes #1133.

The issue offered three options and did not pick one, because option 3 "changes what documented examples do and would want measuring against the wiki's samples first". That measurement inverts the concern.

The wiki already documents option 3

By default it applies aggressive operators which override the +, -, *, /, ** operators for any pair of object. This is not an F# console, but AngouriMath terminal, after all.

Five operators. The comparisons are not in that sentence and never were — and the Terminal's page has no code samples at all, so there is nothing documented to break. The code rebinds eleven; the documentation describes five. This makes the code match.

What the extra six were costing

Measured on one kernel, with and without AngouriMath loaded:

before now
if 1 < 2 then 1 else 0 typecheck error: expected bool, got Entity 1
let b: bool = 1 < 2 typecheck error True
[1;2;3] |> List.filter (fun x -> x > 1) typecheck error [2; 3]
seq { 1..100 } |> Seq.filter (fun x -> x % 7 = 0) |> Seq.length typecheck error 14
an active pattern with n % 2 = 0 typecheck error e

And the reading the Terminal exists for is untouched — x + 1, 3 / 2 → 3/2, x ** 2, solutions, derivative all unchanged.

AggressiveOperators is not touched

It is public surface, its comparison overloads are deliberate and covered by their own tests, and Jupyter users may well want x > 0 to be an inequality. All 18 of its tests still pass.

The old behaviour is one line at the prompt, and that is measured rather than asserted: x > 0 is an ordinary comparison until open AngouriMath.Interactive.AggressiveOperators, and an inequality after it. That is option 2 from the issue, obtained without adding any API.

What this does not fix

Rebinding arithmetic has the same shape of cost, and it is the cost the tool exists to pay. Once + and * build expressions, a function whose branches mix an Entity with an int does not typecheck, and List.sum over expressions has no + it can use:

let rec fib n = if n < 2 then n else fib (n-1) + fib (n-2)
  → still does not compile

fib was the issue's headline example and it still fails — on the arithmetic now rather than on the comparison. Rather than let that be discovered as an overclaim, it is pinned as a test whose remark says so, and which fails if it ever starts working so the remark gets rewritten.

Five of the issue's seven cases compile now. The two that do not are the trade the tool actually makes, and it is the documented one.

The wiki

The Terminal page's sentence becomes accurate with this change rather than needing an edit. Worth adding a line about open AngouriMath.Interactive.AggressiveOperators for whoever wants the comparisons, but that is the wiki repo rather than this one.

Failed: 0, Passed: 41 in TerminalUnitTests, and Failed: 0, Passed: 18 in InteractiveWrapperUnitTests.

🤖 Generated with Claude Code

https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura

The Terminal opened `AggressiveOperators`, which rebinds `+ - * / **` *and* `= <> < > <= >=`. The
wiki says it rebinds five:

  "By default it applies aggressive operators which override the `+`, `-`, `*`, `/`, `**` operators
   for any pair of object. This is not an F# console, but AngouriMath terminal, after all."

The comparisons are not in that sentence and never were. What they cost is that `if`, `while`,
`List.filter` and a `match` guard all want a `bool` and were getting an `Entity`, so a large part of
ordinary F# did not typecheck at a prompt whose purpose is to be typed at. Measured, with and
without AngouriMath loaded on the same kernel: `if 1 < 2 then 1 else 0`, `let b: bool = 1 < 2`,
`[1;2;3] |> List.filter (fun x -> x > 1)` and an active pattern all failed on the comparison.

So the Terminal opens `ArithmeticOperators`, which is the five the documentation promises.
`AggressiveOperators` is untouched: it is public surface, its comparison overloads are deliberate and
tested, and somebody who wants them opens it at the prompt in one line. That was measured too --
`x > 0` is an ordinary comparison until that line and an inequality after it.

**What this does not fix, which is the honest half.** Rebinding arithmetic has the same shape of
cost, and it is the cost the tool exists to pay: once `+` and `*` build expressions, a function whose
branches mix an `Entity` with an `int` does not typecheck, and `List.sum` over expressions has no
`+` it can use. `let rec fib n = if n < 2 then n else fib (n-1) + fib (n-2)` still does not compile.
It fails on the arithmetic now rather than on the comparison, and #1133 used it as the headline
example, so it is pinned as a test with a remark saying so rather than left to be discovered as an
overclaim.

Narrowing bought back the part that was costing something and buying nothing.

Five of the seven cases in the issue compile now; the two that do not are the trade the tool makes.
41 terminal tests pass, and `AggressiveOperators`' own 18 are unchanged.

Part of #1133.
…ants

The previous commit narrowed what the prompt rebinds and left `fib` failing, on the grounds that
rebinding arithmetic is the trade the tool makes. That is true and it is not the whole answer: the
trade can be made per session rather than once, and then `fib` is available.

    open Microsoft.FSharp.Core.Operators              // fib 20 is 6765, and 3 / 2 is 1
    open AngouriMath.Interactive.ArithmeticOperators  // back: x + 1 is an expression, 3 / 2 is 3/2

Both directions measured, in one session, either following the other. `operators ()` prints them, so
it is discoverable from the prompt rather than from a wiki page.

**Why not by default.** Rebinding arithmetic and writing ordinary F# are mutually exclusive, which
was measured rather than reasoned: with F#'s own operators `fib 20` is 6765 and `3 / 2` is 1; with
arithmetic rebound `3 / 2` is 3/2 and `x + 1` is an expression, and `fib` does not typecheck. One
operator cannot have both return types.

An overload that keeps `int + int` an int was tried before concluding that -- a witness-constrained
SRTP operator over a base class carrying the general case. It resolves the simple cases and resolves
them all at once: `1 + 1` is 2, `x + 1` is an expression, `1 + 4.5` is 11/2. It does not resolve
`let rec`, because the operator has to be picked while the function's own type is still being
inferred, so it falls to the general case and `fib` fails anyway. Annotating the parameter and the
return type does not help either, which was checked rather than assumed. That is a negative result
about this mechanism, not about every mechanism.

The test that recorded `fib` as broken is replaced by one that runs it, and it fails if `fib` ever
starts compiling by default -- which would mean this reasoning had stopped being true and the remark
should be rewritten.

Part of #1133.
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

fib compiles now

The PR above left fib failing and said so. That was honest but not finished — the trade can be made per session rather than once, and then fib is available:

open Microsoft.FSharp.Core.Operators              // fib 20 is 6765, and 3 / 2 is 1
open AngouriMath.Interactive.ArithmeticOperators  // back: x + 1 is an expression, 3 / 2 is 3/2

Measured in one session, either following the other, any number of times. operators () prints both lines, so it is discoverable from the prompt rather than from a wiki page.

Why not by default

Rebinding arithmetic and writing ordinary F# are mutually exclusive, measured rather than reasoned: with F#'s own operators fib 20 is 6765 and 3 / 2 is 1; with arithmetic rebound 3 / 2 is 3/2 and x + 1 is an expression, and fib does not typecheck. One operator cannot have both return types.

I tried to have both before concluding that. A witness-constrained SRTP operator over a base class carrying the general case does resolve the simple cases, and resolves them simultaneously:

1 + 1 2 — an int
x + 1 x + 1 — an expression
3 - 1 2
x * x x ^ 2
1 + 4.5 11/2

It does not resolve let rec: the operator must be picked while the function's own type is still being inferred, so it falls to the general case and fib fails anyway. Annotating the parameter and the return type does not help — checked, not assumed. That is a negative result about this mechanism rather than about every mechanism, and I would happily be shown a third one.

The test that recorded fib as broken now runs it, and fails if fib ever starts compiling by default — which would mean this reasoning had stopped being true and the remark wants rewriting.

Failed: 0, Passed: 41.

… case

Each case was calling createKernel, which starts an F# compiler service, and most of the new ones
then loaded AngouriMath into it as well: about thirty-four compiler services and sixteen full
AngouriMath loads for forty-one assertions.

Locally that was thirteen seconds and looked like nothing. On the macOS runner the
`Test Terminal Lib` step ran for over two hours without finishing, while the Linux and Windows legs
of the same run passed and the previous master run finished the same step in two minutes. So the
cost was real and this machine was hiding it.

xUnit runs the cases in a class one after another, so there was nothing to share a kernel with in
the first place. The cases that only ask the kernel to compile something now share one; the two
that change a session by `open`ing something still take a fresh one each, since that is the whole
of what they test.

Forty-one assertions, five seconds instead of thirteen.
…service

Every test in this assembly starts an F# compiler service, and xUnit runs test classes in parallel
by default. Two modules were fine; the third made three kernels start together, and on the macOS
runner the `Test Terminal Lib` step then ran for over an hour against the two minutes it takes on
master, while the Linux and Windows legs of the same run passed.

Sharing a kernel within each class was the previous commit and was not enough on its own: it cut
thirty-four kernels to four, and the four still started at the same time. Measured rather than
assumed -- that commit went to CI and the step hung again.

Serialising is right on the merits rather than as a workaround. There is nothing to gain from
running two compiler services side by side on a three-core runner, and the assembly takes seven
seconds either way here.

The config is copied to the output directory, which is checked rather than assumed: a
xunit.runner.json that stays in the source tree configures nothing.
@Rafael-SOWNet
Rafael-SOWNet merged commit 9923e4e into master Sep 1, 2026
27 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the the-prompt-rebinds-what-it-says-it-rebinds branch October 29, 2026 00:44
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.

The Terminal rebinds the comparison operators, so ordinary F# control flow does not typecheck in it

1 participant