Skip to content

Claude/windows calculator optimization 3w9ttr - #2502

Closed
Eripto wants to merge 9 commits into
microsoft:mainfrom
Eripto:claude/windows-calculator-optimization-3w9ttr
Closed

Eripto wants to merge 9 commits into
microsoft:mainfrom
Eripto:claude/windows-calculator-optimization-3w9ttr

Conversation

@Eripto

@Eripto Eripto commented Sep 19, 2026

Copy link
Copy Markdown

Fixes #.

Description of the changes:

How changes were validated:

Eripto and others added 9 commits September 18, 2026 06:19
Adds src/CalculatorCompact: a native Win32 front end that links the
existing CalcManager engine as-is and builds to a single self-contained
Calculator.exe of 297 KB. Arithmetic, precision, operator precedence,
radix conversion, history and memory all come from the shipping engine,
so behaviour matches the packaged app; only the presentation layer
(XAML/WinUI/MSIX) is replaced, with GDI/GDI+ drawing.

Covers Standard, Scientific and Programmer modes, the memory strip and
multi-slot memory list, the history panel with command replay, light and
dark themes following the system setting, the system accent colour,
per-monitor DPI v2 and the documented keyboard shortcuts. The unit and
currency converters, graphing and date calculation are not included:
they live in the app layer rather than the engine.

CalcManager changes, all behaviour-preserving, made so the engine links
without dragging the C++ runtime's iostream, locale and regex machinery
into every consumer (that was ~620 KB of a 1292 KB starting binary):

- scidisp.cpp: replace std::wregex with a hand-written parser for the
  same grammar. The expression is deterministic -- the separator and 'e'
  are never digits, so the greedy \d* runs never need backtracking. A new
  differential test checks the two against each other over ~75,000
  generated inputs.
- Ratpack/support.cpp, ratpak.h: guard the constant-dump helpers and
  their <iostream> include behind GEN_CONST, which is what already gates
  their only callers.
- scicomm.cpp: format the rand command with swprintf rather than
  wstringstream (identical output in the C locale); seed mt19937 from the
  OS CSPRNG via rand_s on Windows, since libstdc++'s std::random_device
  reports failure with std::runtime_error and that one reference pulls in
  std::logic_error, narrow std::string and more; turn the operator-name
  table into a flat constexpr array of wstring_view, removing 78 heap
  allocations and 12.6 KB of start-up code.
- Ratpack/support.cpp: add the <cmath> include that MSVC's precompiled
  header was supplying for ceil/log2.
- Ratpack/conv.cpp: add <cwctype> for towupper, previously arriving
  transitively.
- pch.h: guard <ppltasks.h> with _MSC_VER so non-MSVC toolchains can use
  the header, matching the note already at the top of the file.

Engine tests build and run with the host compiler on any platform:
src/CalculatorCompact/tests/run_tests.sh (61 checks).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YLNYYTAuCvdsCs4jg7MCR
Extends the compact build to cover every mode this repository can actually
build, in 349 KB (budget raised to 350 KB, enforced by build.sh).

New in the front end:

- Converter: all 12 non-currency categories and their 158 units, driven by
  CalcManager's own UnitConverter. Unit tables are generated by
  tools/gen_converter_data.py from the shipping app's UnitConverterDataLoader,
  NavCategory manifest and en-US resources, so categories, ordering, default
  unit pairs, conversion factors, the whimsical units and the "About equal to"
  suggestions all match. Includes Temperature's explicit ratio/offset table.
- Date Calculation: difference between two dates and add/subtract
  years/months/days, with month/day/year pickers. Proleptic Gregorian
  arithmetic with the calendar's end-of-month clamping.
- Navigation pane listing both category groups, Always on top, and a
  Settings/About page.
- Scrollable dropdown flyouts, needed for 38-unit and century-long lists.

Graphing and Currency are deliberately absent: neither is functional in this
repository. src/GraphingImpl ships only MockGraphingImpl, whose MathSolver
returns an empty graph, and CurrencyHttpClient.cs states that the upstream rate
endpoints are dead and substitutes placeholder data. Both are noted in the
README rather than shipped as stubs.

CalcManager changes, all behaviour-preserving:

- UnitConverter.h/.cpp: guard the PPL-based live-currency fetch behind
  CALC_SUPPORTS_CURRENCY_ASYNC (defined automatically for MSVC), so toolchains
  without ppltasks.h can build the converter. GetCurrencyConverterDataLoader
  avoids dynamic_pointer_cast when that path is compiled out, which also lets
  the engine build with -fno-rtti.
- New Header Files/NumericString.h: integer, double and parse conversions that
  do not route through std::to_wstring, std::stod or the iostream manipulators.
  Those pull a private copy of the C++ runtime's floating-point conversion into
  every binary linking CalcManager -- about 31 KB even for callers that only
  format integers. Includes an exact fixed-point path for values in [0, 1),
  used by the rand command so its digits match the previous stream output
  rather than stopping at a CRT's 17 significant digits.
- NumberFormattingUtils.cpp: use those helpers and drop pch.h, which was
  pulling <regex>, <sstream> and <iostream>.
- scicomm.cpp, ExpressionCommand.cpp, CalcEngine.h, UnitConverter.cpp: use the
  helpers in place of to_wstring/stod.
- scicomm.cpp, EngineStrings.h: store the operator-name table and g_sids as
  plain pointers rather than string views, halving their size and relocations.

Front-end fix: the navigation pane is a full-surface layout, not a flyout, so
only the dropdown menu captures the mouse. Previously every click while the pane
was open fell through to CloseMenus, which cleared the nav flag without
relaying out and swallowed the click.

Tests grow to 130 checks, now covering unit conversions in every category
(gallons, miles, all three temperature scales, data, angle, reverse-field
editing) and date arithmetic (leap years, end-of-month clamping, weekday
anchors, 14,286 day-number round trips, difference decomposition).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YLNYYTAuCvdsCs4jg7MCR
Brings the front end's motion in line with the shipping app. Timings and
curves follow the WinUI guidance it is built on: 167ms for a small state
change, 250ms for a surface entering, and a decelerating curve for
anything arriving on screen.

- Navigation pane and settings page slide in from the left over a scrim.
  Both became overlay surfaces above the content (their own button list,
  drawn after it) rather than replacing the layout, which is what lets
  them move over the keypad instead of cutting to it.
- Mode changes fade the new content in while it rises into place.
- Dropdown flyouts fade in and lift.
- The history and memory panel slides in: from the right when it docks
  beside the keypad, upward when it covers it.
- Keys cross-fade their hover highlight from one to the next, and
  pointer-down shrinks the key slightly while the pressed fill fades in,
  matching the Fluent pressed state.

Animated values are computed from the clock on demand rather than
stepped, so a dropped frame cannot strand an animation part-way, and the
frame timer only runs while something is moving. Transitions that need a
real fade render to an offscreen layer and AlphaBlend it, because GDI
text has no alpha of its own.

Two bugs found while verifying this:

- BuildLayout returned early for Converter and Date modes, so the
  navigation overlay was never built in them: the pane rendered empty and
  swallowed clicks. All exit paths now end at BuildOverlayLayout.
- The content entrance animation started at zero and was only ever
  advanced by a mode change, so the first frame after launch drew the
  content at zero alpha. It now starts already arrived.

Size is 357 KB against a budget raised to 425 KB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YLNYYTAuCvdsCs4jg7MCR
Compared each mode side by side with the shipping app and corrected the
places where the compact build diverged.

Keypad: leave 6px between keys and the same margin around the grid rather
than running the keys to the window edge, and drop the corner radius to 4px.

Standard and Scientific: set mathematical variables in italic the way the
shipping app does - x squared, one over x, the roots, x to the y, ten to the
x, n factorial, absolute value - while operators and named functions stay
upright.

Scientific: move DEG and F-E above the memory strip and the Trigonometry and
Function dropdowns below it. Remove the separate hyp toggle and fold the
hyperbolic functions into the Trigonometry flyout, which now carries all
twelve entries.

Programmer: left-align the HEX/DEC/OCT/BIN rows and mark the active radix
with an accent bar instead of filling the whole row, use the guillemets for
the shift keys, and set "Bit shift" in sentence case.

Date calculation: replace the three month/day/year spinners with one field
per date reading "September 18, 2026", backed by a calendar flyout that
reuses the existing flyout surface.

Settings: rebuild as cards under "Appearance" and "About" section headers,
with the theme choice and the always-on-top state as card subtitles.

Combo boxes: the unit pickers and the date-mode picker now anchor their text
to the leading edge and park the chevron at the trailing one, rather than
centring the pair.

Glyphs: M- uses a real minus sign.

363 KB, tests 130/130.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YLNYYTAuCvdsCs4jg7MCR
Screenshots against the shipping app showed four things wrong.

The title-bar icons came out in colour. They were font glyphs, and when the
icon font is missing a codepoint the fallback characters can be substituted
by a colour emoji font, which puts blue and orange into a title bar that
should be monochrome. The hamburger, back arrow, history, keep-on-top,
backspace, chevrons, calendar, gear, radio buttons and the settings header
icons are now GDI+ paths on the same 16x16 grid the icon fonts use, so they
render identically everywhere and cannot be recoloured. The navigation
pane's category icons still come from the icon font; their codepoints
already match NavCategory.cs exactly.

The title-bar layout did not match MainPage.xaml: the keep-on-top button
belongs immediately after the mode name, not at the window edge, the
hamburger is a 48px square, and the mode name is SubtitleTextBlockStyle,
20px semibold rather than 15.

The navigation pane covered the whole window. A NavigationView overlay pane
is SplitViewOpenPaneLength wide, 256px, so the keypad stays visible beside
it under the scrim. It also gains the WinUI selection indicator, a 3x16
accent bar on the leading edge of the selected item, and a scrollbar.
"Date calculation" now matches DateCalculationModeText.

Settings cycled the theme on every click of the App theme card. Per
Settings.xaml it is a SettingsExpander: the card expands to Light / Dark /
Use system setting radio buttons. About expands to the licence links, and
the page closes with the "Send feedback" link and the contribute paragraph.
The always-on-top card is gone, matching the shipping page, and the title
bar button still toggles it.

Animation frames are now paced against the compositor with DwmFlush from
the message loop. WM_TIMER's 15.6ms granularity, low priority and
coalescing are what made the motion stutter. The loop blocks on GetMessage
again once everything settles, so an idle window still costs nothing.

373 KB, tests 130/130.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YLNYYTAuCvdsCs4jg7MCR
The window's off-screen bitmaps dominate this app's footprint and were being
rebuilt constantly. Every WM_PAINT created and destroyed a full-window
bitmap, and a frame compositing a transition or a flyout fade created a
second and sometimes a third. At 360x620x32bpp each one is 892KB.

They are now held in a small Surface wrapper: recreated only when the client
area changes size, shared between the two transitions that composite through
AlphaBlend (they never overlap inside a frame -- the content layer is blended
and finished with before the overlays are drawn), and released two seconds
after everything settles, when the window is minimised, and on shutdown. An
idle process holds no full-window bitmaps at all.

Counted inside the app over an identical scripted session of three
Standard/Scientific/Programmer cycles: 779 allocations totalling 663 MB
before, 19 totalling 16 MB after.

Also starts GDI+ with SuppressBackgroundThread and pumps its notification
hook by hand, dropping a thread and its stack, and uses the settle timer to
trim the working set and decommit the heap's free blocks, since laying out a
mode churns a lot of small allocations the heap otherwise holds pages for.

Nothing about what is drawn changes. Five screens captured before and after
are pixel-identical, zero differing pixels across all of them.

Note the requested target of 100KB is not reachable for a Win32 window: one
back buffer for this window is 892KB by itself, and GDI+ maps several MB when
it initialises.

374 KB, tests 130/130.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YLNYYTAuCvdsCs4jg7MCR
There is nothing to port for the maths: src/GraphingImpl contains only
MockGraphingImpl, whose MathSolver returns an empty graph, and the real
solver and renderer are a closed-source component shipped separately. So the
parsing, evaluation and plotting are written here. What is taken from the
repository is the shape of the feature -- the function set in
GraphingNumPad.xaml, the fourteen equation colours in App.xaml in both theme
variants, and the NavCategory.cs placement, glyph and shortcut.

Expressions compile to a flat postfix program rather than a node tree, which
avoids per-node allocation and is a good deal less code, and evaluate over
doubles on a fixed stack. The grammar covers implicit multiplication, a
right-associative power operator, absolute-value bars, unparenthesised
function arguments, the superscript forms the keypads produce, and pi and e.
A leading "y=" is stripped, and anything that does not parse leaves the curve
undrawn rather than guessing.

Undefined points come back as NaN and break the curve, so sqrt(x) stops at
the origin; a jump larger than four times the view height breaks it too,
which keeps the two branches of 1/x from being joined through the asymptote.
Curves are sampled once per pixel column. Dragging pans, the wheel and three
corner buttons zoom, and the Trigonometry and Function flyouts shared with
the scientific keypad write their function into the equation instead of
sending a command to the engine.

Not covered, because it needs the solver: the key graph features panel,
implicit relations and inequalities. This plots explicit functions of x.

Separately, Execute()'s ranged-action chain was not in descending order --
ACT_DATE_VALUE_BASE (10000) was tested before every higher base, so it
swallowed the calendar's actions. Picking a day out of the date picker did
nothing before this. The chain now runs highest first, and the comment on
the bases says why the order matters.

Alt+1..5 now number the modes as NavCategory.cs does, so Alt+3 is Graphing.

399 KB, 25 KB of it the graphing mode, against a 75 KB budget. Tests 175/175.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YLNYYTAuCvdsCs4jg7MCR
Adding the graphing mode pushed it to 399 KB. A linker map found the way
back, and none of it touches behaviour.

Link-time optimisation is worth about 52 KB. It ICEd on the first attempt
during the original size work (binds_to_current_def_p, GCC 13 mingw), which
is why the build has been without it; -flto-partition=none avoids the ICE.

Another 22 KB comes from a force-included prelude that sets
_GLIBCXX_EXTERN_TEMPLATE to -1. libstdc++ ships std::wstring as an explicit
instantiation and declares it extern template, so any use of it links the
whole of wstring-inst.o, most of it members this app never calls. c++config.h
defines the macro unconditionally, so it cannot be set from the command line;
including that header first and redefining it works because of its own
include guard. The members actually used are then emitted locally, where
-ffunction-sections and --gc-sections can drop the rest.

The rest is CRT shims that had cheaper equivalents already linked:
GetLocalTime in place of time plus localtime_s, and RtlGenRandom -- which is
what rand_s calls -- in place of rand_s, which also drops the secure
parameter handler behind them. About 10 KB of linked code, though PE section
alignment absorbs most of it in the file. Also --disable-runtime-pseudo-reloc,
since nothing here relies on auto-import.

409,088 bytes to 327,680, and the build now fails over 350 KB rather than
425. Every mode was captured before and after, including the arithmetic and
divide-by-zero paths, and compared channel by channel: zero differing pixels
across all eight screens. Tests 175/175.

Two things were measured and then rejected rather than kept. Replacing libm's
pow, cbrt and inverse hyperbolics with identities built from log, exp and
sqrt saves about 13 KB, but it moves results by a few ulp, and LTO alone made
the target, so precision stayed exactly as it was. And -fno-exceptions on the
UI translation units is not available at all, since main.cpp catches what the
engine throws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YLNYYTAuCvdsCs4jg7MCR
The display's auto-fit had two faults that only showed on the longest values
the app can produce.

It never measured its own floor: the loop condition was `while (dip > 16)`,
so on reaching 16 it exited with the font from the previous iteration still
selected and drew at 18, one step larger than intended and never checked.

And when the text still did not fit, it was drawn right-aligned and clipped,
which takes the characters off the *front* of a number. A scientific result
read ".3333333333333333333333333333333e-1" with its leading digit gone, and
a 64-bit binary word showed eight nibbles of sixteen with the first one cut
through the middle. Both are worse than wrong: they are plausible.

The floor now goes to 8 and is itself measured, which is low enough for the
two genuine worst cases -- 64 binary digits with 15 nibble separators, and 32
significant digits plus an exponent. Small type is better typography than a
number missing its leading digit.

Values that already fitted are untouched, since the loop still stops at the
first size that fits: the 12 + 34 = 46 screen is pixel-identical before and
after.

Found by driving the app: Programmer, HEX, sixteen Fs, switch to BIN; and
Scientific, 1 / 3 =, F-E.

Tests 175/175.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YLNYYTAuCvdsCs4jg7MCR
@Eripto Eripto closed this Sep 19, 2026
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