Skip to content

Macro registry, template parsing, and the three commands - #266

Closed
kostub wants to merge 10 commits into
masterfrom
feature/modular-arithmetic-pr3
Closed

Macro registry, template parsing, and the three commands#266
kostub wants to merge 10 commits into
masterfrom
feature/modular-arithmetic-pr3

Conversation

@kostub

@kostub kostub commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Part 3 of 3 in the modular-arithmetic stack. This is the PR that makes the feature user-visible.

Plan: docs/plans/2026-07-25-modular-arithmetic.md
LLD: docs/lld/2026-07-13-modular-arithmetic.md

Goal

\pmod{n}, \mod{n}, \pod{n} parse from LaTeX into MTMacroAtoms, with amsmath's exact inline gaps, upright "mod", fail-loud missing-argument errors, and round-tripping serialization.

Commits

  1. [item 10] Add MTParseErrorMissingArgument and requiredArgumentWithError:
  2. [item 11] Parse #N placeholders in macro-template mode
  3. [item 12] Add MTMacroDefinition registry for pmod/mod/pod
  4. [item 13] Dispatch \pmod, \mod, \pod to MTMacroAtom
  5. [item 14] Test end-to-end equivalence and script behavior through LaTeX
  6. [item 15] Test macro serialization round-trip and parse errors
  7. [item 16] Test macro rendering geometry against the written-out expansion

Full suite green at 520 tests.

Deviations from the plan (all documented in commit messages)

  • \pmod{\frac} is not a parse error, contrary to the plan's literal parse-error table row. -requiredArgumentWithError: reads arguments with the same one-token buildInternal:YES reader \frac itself uses, so a \frac with no operands degrades to \frac{}{} exactly as a bare top-level \frac at EOF does, rather than failing. Verified directly and covered by a new testFracWithNoArgumentsIsNotAnErrorInsideMacroArgument. The table row was swapped to \pmod{{n} — a genuinely unbalanced brace, which does propagate MTParseErrorMismatchBraces — preserving the row's real intent per LLD §6 (an inner parse error must propagate).
  • \pod{n}.finalized serializes as \mkern8.0mu(n), not the plan's literal (n). The leading 8mu kern has no named LaTeX command in +spaceToCommands, so it correctly serializes through the \mkern%.1fmu fallback. Same root cause as the item-7 correction in PR 2.
  • Test-block placement (items 10-12). Several plan snippets imply a fresh @implementation/@end pair or nest an @interface inside @implementation, neither of which compiles. Test-only category declarations were placed at file scope and test methods inside the single existing @implementation MTModularArithmeticTest.

Known follow-ups raised in review of PR #265 (not addressed here)

  • kMTMathAtomMacro is unhandled in MTTypesetter.m's createDisplayAtoms: switch, which has no default: — so -Wswitch is its exhaustiveness guard. A macro atom escaping expansion would be skipped and drive getInterElementSpaceArrayIndexForType to return -1, an OOB index in Release (NS_BLOCK_ASSERTIONS=1).
  • -[MTMacroAtom expansion] substitutes only top-level #N; a #1 nested inside an MTMathGroup survives unsubstituted. Now reachable in principle, since this PR makes templates LaTeX strings where {#1} is natural — though none of the three built-in templates nest.
  • New files (MTMacroParameterAtom.h, MTModularArithmeticTest.m) are not in iosMath.xcodeproj, so the documented xcodebuild test iOS path skips this feature's tests. Originates in PR 1.

Stack

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw

Summary by CodeRabbit

  • New Features

    • Added modular-arithmetic notation support: \bmod, \pmod, \mod, and \pod.
    • Math expressions using these commands now render and serialize consistent with LaTeX-style notation.
    • Added an API to retrieve the names of supported macros.
  • Bug Fixes

    • Improved parse-time validation for macros missing required arguments, including clearer error reporting.
    • Fixed and stabilized parsing/expansion behavior for modular-arithmetic macros, including edge cases and malformed input.
  • Tests

    • Expanded coverage for modular-arithmetic parsing, expansion equivalence, and rendering/layout behavior.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 91fb1852-ddbd-4638-a4f9-0eae3b9c752c

📥 Commits

Reviewing files that changed from the base of the PR and between 92be6b6 and 6700318.

📒 Files selected for processing (6)
  • iosMath.xcodeproj/project.pbxproj
  • iosMath/lib/MTMathList.h
  • iosMath/lib/MTMathList.m
  • iosMath/lib/MTMathListBuilder.m
  • iosMath/lib/internal/MTMacroParameterAtom.h
  • iosMathTests/MTModularArithmeticTest.m
💤 Files with no reviewable changes (2)
  • iosMath/lib/internal/MTMacroParameterAtom.h
  • iosMath.xcodeproj/project.pbxproj

📝 Walkthrough

Walkthrough

The parser adds built-in \pmod, \mod, and \pod macros with strict required-argument validation, while refactoring the macro representation from templateExpression with #N placeholders to a simpler prefix/suffix model. Macro expansion no longer uses explicit depth APIs, instead assembling arguments between prefix and suffix halves and recursively expanding the result. Macro parameter placeholders are removed entirely. Comprehensive tests validate parsing, equivalence, scripts, serialization, and rendering.

Changes

Built-in modular arithmetic macros

Layer / File(s) Summary
Public macro discovery and error handling
iosMath/lib/MTMathListBuilder.h
Declares supportedMacroNames() method and MTParseErrorMissingArgument error code.
Macro argument parser infrastructure
iosMath/lib/MTMathListBuilder.m
Adds requiredArgumentWithError:, peekCommand lookahead, and stopCommands registry to strictly parse mandatory macro arguments and prevent consumption of list terminators.
Built-in macro registry and invocation
iosMath/lib/MTMathListBuilder.m, iosMathTests/MTMathListBuilderTest.m
Defines MTMacroDefinition model, registers pmod/mod/pod in builtinMacros, implements macroAtomForCommand: lookup and expansion, integrates into command dispatch, and tests parse-error cases.
Macro atom refactoring: template → prefix/suffix
iosMath/lib/MTMathList.h, iosMath/lib/MTMathList.m, iosMath.xcodeproj/project.pbxproj
Changes MTMacroAtom from templateExpression API to prefix/suffix properties, removes MTMacroParameterAtom placeholder class, updates copy and initialization semantics, and removes deleted header from project.
Macro expansion without depth tracking
iosMath/lib/MTMathList.m
Refactors expansion to assemble prefix, arguments, and suffix, re-expands the result to eliminate nested macros, and removes explicit depth parameters and placeholder assertions.
Comprehensive test validation
iosMathTests/MTModularArithmeticTest.m
Tests macro atom construction, argument parsing, stop-command interaction, registry invariants, parsing shape, end-to-end equivalence, script attachment, serialization round-trips, and rendering/layout geometry.
Release notes
CHANGELOG.md
Documents v2.6.0 modular-arithmetic support and macro serialization behavior.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Parser as Parser<br/>(buildInternal)
  participant MacroLookup as macroAtomForCommand
  participant ArgParser as requiredArgumentWithError
  participant PrefixParser as buildFromString (fresh instance)
  participant MTMacroAtom
  Parser->>MacroLookup: resolve pmod, mod, or pod
  MacroLookup->>ArgParser: parse required argument
  ArgParser-->>MacroLookup: argument parsed
  MacroLookup->>PrefixParser: parse prefix string
  PrefixParser-->>MacroLookup: prefix nodes
  MacroLookup->>PrefixParser: parse suffix string
  PrefixParser-->>MacroLookup: suffix nodes
  MacroLookup->>MTMacroAtom: construct with command, arguments, prefix, suffix
  MTMacroAtom-->>Parser: macro atom ready
Loading

Possibly related PRs

  • kostub/iosMath#265: Introduces the macro atom and macro parameter design that this PR refactors from template-expression to prefix/suffix representation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the PR’s main focus: macro registry/parser changes and support for the three modular-arithmetic commands.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/modular-arithmetic-pr3

Comment @coderabbitai help to get the list of available commands.

@kostub kostub left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — PR 3 (diff vs feature/modular-arithmetic-pr2 only)

Verified locally: worktree at b8fdc60, swift test520 tests, 0 failures, clean tree.

Overall this is a tight, well-scoped landing. The registry is genuinely parser-owned and file-private, buildTemplate: really does use a fresh builder (no state swap/restore), macroAtomForCommand: falls through without setting an error for non-macros, and the two-phase finalized seam from PR 2 is not disturbed. Test coverage is unusually good for a parser change — the "macro vs. hand-written expansion" geometry test in particular is the right acceptance proof that render/ is untouched.

Things I checked and can confirm are correct

amsmath fidelity — the gap values are exactly right. Checked against TeX Live 2025 amsmath.sty (lines 907-912):

\DeclareRobustCommand{\pod}[1]{\allowbreak
  \if@display\mkern18mu\else\mkern8mu\fi(#1)}
\DeclareRobustCommand{\pmod}[1]{\pod{{\operator@font mod}\mkern6mu#1}}
\DeclareRobustCommand{\mod}[1]{\allowbreak\if@display\mkern18mu
  \else\mkern12mu\fi{\operator@font mod}\,\,#1}
  • \pod → inline \mkern8mu(#1) — matches the template exactly.
  • \pmod → inlining \pod gives \mkern8mu({\operator@font mod}\mkern6mu#1) — matches, modulo the {…} group (which is an Ord noad in TeX and therefore spacing-neutral here; flattening it is fine, and it is why the template's #1 is at top level — see the nested-#N note below).
  • \mod\mkern12mu{\operator@font mod}\,\,#1. \, is \mskip\thinmuskip = 3mu, so \,\, = 6mu, matching the template's \mkern6mu. (The kern-vs-glue distinction is only observable under line-breaking/stretch, which iosMath doesn't do.)
  • \operator@font ≡ upright roman ≡ \mathrm. ✔
  • Dropped \allowbreak / \penalty900 are line-break hints — no-ops here.
  • \if@display 18mu is a documented non-goal (PRD §3.1 / LLD §4.2).

Script-style behavior is correct and matches TeX. Neither \pmod/\mod/\pod uses \nonscript, so in real TeX the mu kerns survive into script styles but shrink, because mu is 1/18 of the current style's quad. iosMath matches: MTTypesetter.m:634 uses _styleFont.mathTable.muUnit and MTFontMathTable.m:71 is _fontSize/18 on the style font. So x^{\pmod{n}} gets a proportionally smaller gap, as amsmath does. (Nit: testLeadingGapWidths only exercises display style — a one-line script-style case would lock this in.)

Deviation 1 (\pmod{\frac} is not a parse error) — sound. Confirmed the mechanism directly. requiredArgumentWithError: ends in [self buildInternal:YES], the same one-token reader \frac uses for its own operands, so \frac's numerator/denominator reads see the } immediately and each come back empty, not missing — identical to bare top-level \frac at EOF. The substituted \pmod{{n} row does exercise the original intent: the MTParseErrorMismatchBraces comes from the inner recursion hitting EOF with an unclosed group (MTMathListBuilder.m:548), not from -build's trailing leftover-characters check, so it is genuine inner-error propagation per LLD §6. testRequiredArgumentPropagatesInnerError ({\notacommand}MTParseErrorInvalidCommand) covers the same invariant from a second angle. Good call keeping the observed behavior and characterizing it rather than forcing the plan's literal string.

Deviation 2 (\mkern8.0mu(n)) — sound. 8/12/6 mu have no named commands in +spaceToCommands (which covers 3/4/5/-3/18/36mu), so the \mkern%.1fmu fallback is the only correct output, and it re-parses.

Deviation 3 (test-block placement) — fine, purely mechanical.

+supportedMacroNames vs. file-private MTMacroDefinition — coherent. The method's return type leaks no private type, and returning .allKeys matches the existing convention of +[MTMathAtomFactory supportedLatexSymbolNames] (also .allKeys, also unordered), so there's no new API-shape inconsistency. testSupportedMacroNames even asserts the two surfaces stay disjoint, which is the right guard given macroAtomForCommand: runs before atomForCommand: and would silently shadow a symbol of the same name.


Findings

Important — requiredArgumentWithError: is not fail-loud against stop commands, and silently swallows a \\ row break

MTMathListBuilder.m:212-227 blacklists }, ^, _, & as "argument position is empty", but a following stop command (\\, \cr, \right, \end) is not in the blacklist, so control reaches [self buildInternal:YES], which returns an empty list with no error — the exact silent-permissiveness the method's own header comment says it exists to close.

Measured on this branch (all parse successfully, no error):

input raw serialization
\left(\pmod\right) \left( \pmod{}\right) — argument silently empty
x \pmod \\ y x\pmod{\\ y} — the \\ builds a table nested inside \pmod's parens
\begin{matrix}a\pmod\\b\end{matrix} \begin{matrix}a\pmod{}b\end{matrix}the row break is consumed and the matrix loses a row

The last one is the one that bothers me: a \pmod \\ b inside a matrix silently renders as a single row. That's wrong output rather than an error, which is the failure mode this repo explicitly tries to avoid, and it's new behavior — before this PR \pmod was simply an unknown command and errored.

Yes, \sqrt has the identical hole (x\sqrt{\\ y} — I checked), and migrating \sqrt is correctly out of scope. But requiredArgumentWithError: is new code whose whole stated purpose is to be stricter than buildInternal:YES, and it's cheap to close: after the }^_& check, peek for \ and, if readCommand would yield one of the stop commands, raise MTParseErrorMissingArgument (restoring the read position first). \over/\atop/\choose already error correctly via stopCommand:'s oneChar guard, and \right/\end outside their opener already error for unrelated reasons — so in practice this is about \\ and \cr, plus \right/\end when the opener is present.

If you'd rather not widen the guard in this PR, please at least add the three rows above as characterization tests so the behavior is recorded rather than accidental.

Minor — nested #N is not reachable today, but this PR moves it one registry edit away

Answering the question raised in the PR body: no, this PR does not make the nested-#N follow-up reachable in practice. The registry is a file-private compile-time constant and none of the three templates put #1 inside braces, so no user input can trigger it. I confirmed the failure mode empirically with a hand-built template \mkern8mu({\mathrm{mod}\mkern6mu#1}): debug asserts loudly ("Macro parameter placeholder #1 escaped a template" from finalizedAssumingNoMacros), Release renders a literal #1.

What's worth flagging is that the natural transcription of amsmath's own \pmod\pod{{\operator@font mod}\mkern6mu#1} — nests exactly this way. The template here is correct only because it was deliberately flattened, and nothing in the code records that constraint. A cheap guard in buildTemplate: (or in +builtinMacros) that walks the parsed template and rejects an MTMacroParameterAtom below top level would make the invariant self-enforcing rather than depending on whoever edits the dictionary next.

Minor — the registry's declared argumentCount is never validated against its template

argumentCount is declared per entry (MTMathListBuilder.m:1788) and independently the template contains #N; nothing checks they agree. Confirmed the consequence: with argumentCount:1 and template (#1,#2), -[MTMacroAtom expansion] asserts in debug but takes the continue branch in Release — the #2 is silently dropped from the output. Same one-line fix location as the note above: while walking the parsed template, assert max(#N) <= argumentCount. Both checks together turn two latent silent-wrong-output paths into a startup-time programming error, which is what they are.

(Related: +builtinMacros's templates are re-parsed from scratch on every macro invocation — a fresh MTMathListBuilder, a malloc, and a full parse per \pmod in the input. MTMacroAtom deep-copies the template anyway, so caching the parsed golden list on MTMacroDefinition would be safe and would be the natural place to hang the validation above. Not a correctness issue; 8 atoms is cheap.)

Minor — no CHANGELOG.md entry

This is the PR that makes the feature user-visible, and CHANGELOG.md follows a per-feature-per-release convention (v2.5.0 lists array, \cancel, etc.). Nothing across the whole 3-PR stack touches it, so \bmod, \pmod, \mod, \pod would ship undocumented. Worth a line here covering all three PRs.

Nit — ## in a template

## is rejected (MTParseErrorInvalidCharacter) where TeX would read it as an escaped literal #. Irrelevant for a fixed internal registry, and arguably the safer default; noting only for completeness. #12 correctly reads as #1 followed by 2, matching TeX's one-digit rule. #, #0, #x are all correctly rejected and covered by testBuildTemplateRejectsMalformedPlaceholder.

Nit — buildTemplate: recursion is not depth-limited across builder instances

Each buildTemplate: call gets a fresh builder, so _recursionDepth resets to 0. A self-referential or mutually-recursive template would stack-overflow rather than hitting MTParseErrorNestingTooDeep. Not reachable with a hardcoded, non-recursive registry; would matter if LLD §8.2's user-defined registry ever lands.


Per the review request I've deliberately not re-reported the three known PR #265 follow-ups (unhandled kMTMathAtomMacro in MTTypesetter.m's switch, top-level-only #N, missing iosMath.xcodeproj entries), and I've treated the plan as authoritative over LLD §6/§7.3 for \mod{\;}^2.

Not approving — the stop-command hole in requiredArgumentWithError: is worth a decision before merge (fix, or characterize with tests).

@kostub
kostub force-pushed the feature/modular-arithmetic-pr2 branch from 34dd3f7 to 36d72d2 Compare July 26, 2026 13:27
kostub and others added 7 commits July 27, 2026 01:29
Adds a fail-loud required-argument reader for future macro invocations.
-buildInternal:YES alone is silently permissive at EOF and before a
following }/^/_/&; \sqrt and friends keep that behavior (out of scope
here), but macros must error instead.
Adds an opt-in _templateMode ivar and a +buildTemplate: entry point that
parses a fresh builder instance with placeholder recognition enabled.
'#' in ordinary user input is untouched and still raises
MTParseErrorInvalidCharacter.
Parser-owned, file-private registry mapping each of the three macro
names to its argument count and amsmath-exact LaTeX template string.
+supportedMacroNames exposes discovery without overlapping the symbol
table's +supportedLatexSymbolNames (\bmod stays symbol-only).
Add -macroAtomForCommand: which looks up the built-in macro registry,
reads arguments with the existing -requiredArgumentWithError: reader,
and parses the golden template with a fresh builder instance via
+buildTemplate:. Wire it into -buildInternal:oneCharOnly:stopChar: ahead
of -atomForCommand: so a macro command yields a single MTMacroAtom that
flows through the existing shared script-attach + append + oneCharOnly
tail with no new logic.

Non-macro commands are unaffected: macroAtomForCommand: returns nil
without setting an error when the command isn't in the registry, so
dispatch falls through to atomForCommand: exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
Test-only additions covering \pmod, \mod, and \pod parsed from LaTeX
end-to-end: parsing, macro expansion, and superscript/subscript
attachment behavior.

Notably documents that the `^2` in `\mod{\;}^2` lands on the `d` of
"mod" rather than the trailing space/argument — a deliberate,
documented divergence from LLD §6/§7.3, per the plan's preamble.
Adds \pmod/\mod/\pod parse-error rows to getTestDataParseErrors() and
serialization round-trip + raw-vs-finalized tests to
MTModularArithmeticTest.m, per plan Task 15.

Two deviations from the plan's literal expectations, verified against
actual behavior rather than reverted:

- `\pmod{\frac}` is NOT a parse error: requiredArgumentWithError: reads
  arguments with the same one-token buildInternal:YES reader that
  \frac itself uses for its numerator/denominator, so \frac with no
  operands degrades to \frac{}{} (same as bare top-level \frac at
  EOF) instead of failing. Swapped in \pmod{{n} (an actually
  unbalanced brace), which does propagate MTParseErrorMismatchBraces,
  and added testFracWithNoArgumentsIsNotAnErrorInsideMacroArgument to
  characterize the \frac case directly.
- \pod{n}.finalized serializes to "\mkern8.0mu(n)", not "(n)": the
  leading 8mu kern has no named LaTeX command, so it serializes via
  the documented \mkern%.1fmu fallback rather than disappearing.

Full suite: 516 tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
…sion

Adds the rendering acceptance tests from plan Task 16: upright (non-italic)
"mod" text, byte-identical layout geometry (width/ascent/descent/subdisplay
positions) between a macro and its hand-written-out expansion, exact 8mu/12mu
leading-gap widths, and a no-throw smoke test across representative macro
inputs. Imports MTFontMathTable.h for muUnit.

No production changes: iosMath/render/ is untouched by this feature (LLD §1),
and all four tests pass immediately, which is the acceptance proof that the
renderer really is unchanged and the macro expansion really does lay out
identically to the equivalent hand-typed LaTeX.

Full suite: 520 tests passing. This is the final item of the final PR in the
modular-arithmetic plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
@kostub
kostub force-pushed the feature/modular-arithmetic-pr3 branch from b8fdc60 to 9b21b2d Compare July 26, 2026 20:03
@kostub
kostub changed the base branch from feature/modular-arithmetic-pr2 to master July 26, 2026 20:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
iosMath/lib/MTMathListBuilder.m (1)

1805-1808: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

supportedMacroNames order is non-deterministic.

allKeys on an NSDictionary has unspecified ordering. If any caller or test asserts an exact ordered array (e.g. @[@"pmod", @"mod", @"pod"]), this can flake across runs/OS versions even though the underlying registry never changed.

♻️ Proposed fix: return a deterministic order
 + (NSArray<NSString *> *) supportedMacroNames
 {
-    return [MTMathListBuilder builtinMacros].allKeys;
+    return [[MTMathListBuilder builtinMacros].allKeys sortedArrayUsingSelector:`@selector`(compare:)];
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@iosMath/lib/MTMathListBuilder.m` around lines 1805 - 1808, Update
supportedMacroNames to return macro names in a deterministic order instead of
directly exposing builtinMacros.allKeys. Sort the returned names using the
project’s standard string ordering while preserving the existing macro registry
contents.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@iosMath/lib/MTMathListBuilder.m`:
- Around line 1805-1808: Update supportedMacroNames to return macro names in a
deterministic order instead of directly exposing builtinMacros.allKeys. Sort the
returned names using the project’s standard string ordering while preserving the
existing macro registry contents.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c2c7e41-6517-466f-9de2-7b7c8e054759

📥 Commits

Reviewing files that changed from the base of the PR and between 8d43971 and 9b21b2d.

📒 Files selected for processing (4)
  • iosMath/lib/MTMathListBuilder.h
  • iosMath/lib/MTMathListBuilder.m
  • iosMathTests/MTMathListBuilderTest.m
  • iosMathTests/MTModularArithmeticTest.m

Fail loud on a stop command in a macro argument slot.
-requiredArgumentWithError: blacklisted }/^/_/& but not a following stop
command, so control reached -buildInternal:YES, which handed it to
-stopCommand:. For \\ and \cr that ended the row and returned it as the
"argument" — `\begin{matrix}a\pmod\\b\end{matrix}` silently lost a row —
and for \right/\end it returned an empty list with NO error. Wrong output
rather than a diagnostic, which is what this method exists to prevent.

Guard the whole set -stopCommand: recognizes (\right, \over, \atop,
\choose, \brack, \brace, \\, \cr, \end) rather than only the two that
leaked: none can begin an argument, so one uniform rule beats four special
cases. This changes `x\pmod\over y` from MTParseErrorInvalidCommand (via
-stopCommand:'s oneChar guard) to MTParseErrorMissingArgument, which names
the actual problem. Only the argument POSITION is guarded — a stop command
after a complete argument keeps its normal meaning.

Adds -peekCommand (read a command, restore the position) and a
+stopCommands set beside -stopCommand: so the two stay coupled.

Validate a macro template against its declared arity.
buildTemplate: gains an argumentCount: parameter and checks the three
invariants -[MTMacroAtom expansion] relies on but cannot enforce:

- No #N above the declared argumentCount. Release carried the placeholder
  through to render as a literal "#2".
- No #N below top level. -expansion substitutes only top-level
  placeholders. amsmath's own \pmod nests exactly this way
  (\pod{{\operator@font mod}\mkern6mu#1}), so the flattened built-in
  template was correct only by deliberate choice and nothing recorded it.
- No script on a top-level #N. -expansion replaces the placeholder with the
  argument list wholesale, dropping any script the placeholder carried.
  Found while writing the check above; same silent-wrong-output family.

All three are programming errors in +builtinMacros — no LaTeX input can
reach them — so they NSAssert and reject. Rejecting also closes the Release
path, where the assert is compiled out but the template is still refused.

MTContainsMacroParameter (MTMathList.m) is un-staticked and declared in
MTMacroParameterAtom.h to do the nested walk; it already visits every
container an atom can hold.

The parsed template is deliberately NOT cached on MTMacroDefinition:
parsing one reaches -macroAtomForCommand:, which calls +builtinMacros, so
caching inside its dispatch_once would re-enter it on the same thread and
deadlock. Reasoning recorded at the registry.

Also: sort +supportedMacroNames (NSDictionary key order is unspecified, so
an ordered assertion by a caller could flake); add a script-style case to
the leading-gap test, locking in that the mu kerns shrink with the style as
amsmath's do; CHANGELOG entry covering all three PRs in the stack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 3: Update the v2.6.0 changelog heading to “Unreleased” instead of the
future date, and retain the existing version and entries until the release is
officially published.

In `@iosMath/lib/internal/MTMacroParameterAtom.h`:
- Around line 33-39: Add MTMacroParameterAtom.h and its implementation file to
the Xcode project and the relevant target membership, ensuring the MTMathList.m
reference to MTContainsMacroParameter links successfully.

In `@iosMath/lib/MTMathListBuilder.m`:
- Around line 1217-1219: Update MTTypesetter to explicitly handle MTMacroAtom
before returning or rendering parsed macros, ensuring \pmod, \mod, and \pod are
expanded or otherwise processed into layoutable content. Preserve existing
handling for non-macro atoms and use the established macro expansion path where
available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 32e4db45-3e10-4f5e-b34c-76dde0d5c5c0

📥 Commits

Reviewing files that changed from the base of the PR and between 9b21b2d and fc63471.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • iosMath/lib/MTMathList.m
  • iosMath/lib/MTMathListBuilder.m
  • iosMath/lib/internal/MTMacroParameterAtom.h
  • iosMathTests/MTModularArithmeticTest.m

Comment thread CHANGELOG.md
@@ -1,5 +1,8 @@
## Changelog

### v2.6.0 (2026-07-27)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not publish a future-dated release entry.

v2.6.0 is dated July 27, 2026, but today is July 26, 2026. Mark this section “Unreleased” until publication, or update the date when the release is cut.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 3, Update the v2.6.0 changelog heading to “Unreleased”
instead of the future date, and retain the existing version and entries until
the release is officially published.

Comment thread iosMath/lib/internal/MTMacroParameterAtom.h Outdated
Comment thread iosMath/lib/MTMathListBuilder.m Outdated
Comment thread CHANGELOG.md
@@ -1,5 +1,8 @@
## Changelog

### v2.6.0 (2026-07-27)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing is released. Why did CHANGELOG get updated? Please revert.

Comment thread iosMath/lib/MTMathList.m Outdated

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is weird and difficult to maintain. What is this function trying to do and why is it necessary. can we remove it?

}

// The built-in macro registry. Each entry is amsmath's exact INLINE expansion
// (LLD §3.3): the 8mu/12mu leading and 6mu inner gaps are amsmath's literal \mkern

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't refer LLD doc.

kostub and others added 2 commits July 27, 2026 12:08
Two review rounds accreted guards the LLD does not specify, and they duplicate
checks that already existed. Diffing the branch against docs/lld/2026-07-13:

- kMTMaxMacroExpansionDepth / -expansionAtDepth: / RunawayMacroExpansion.
  LLD 8.2 scopes a depth guard as roadmap-only: "the three built-ins are
  non-recursive and terminate by construction, so nothing here blocks this PR."
  It also fired on legal input — the parser accepts nesting up to
  kMTMaxRecursionDepth (150), so a 33-deep chain parsed fine and then threw an
  uncaught exception on MTMathUILabel's render path. Removed; expansion
  recursion is already bounded by the nesting -buildInternal: accepted.

- +validateTemplate:argumentCount:. Not in the LLD, and two of its three checks
  were already covered: the nested-#N check duplicates the assert in
  -[MTMacroAtom initWithCommand:arguments:templateExpression:], and the arity
  check duplicates the one in -expansion. Removed, and buildTemplate: goes back
  to the LLD's single-argument form.

  Its third check (a script on a top-level placeholder) was genuinely new, so it
  moves into the initializer loop that already walks the template, beside the
  nested-placeholder assert it belongs with — two lines rather than a new method
  in the builder.

- MTContainsMacroParameter goes back to static; only the deleted duplicate
  needed it exported from MTMacroParameterAtom.h.

The stop-command guard in -requiredArgumentWithError: stays. That one is a real
gap the LLD missed — 3.3 lists only "} ^ _ &", and without it
\begin{matrix}a\pmod\\b\end{matrix} silently lost a row.

The runaway-depth test is replaced by its inverse: deep-but-legal nesting (8,
32, 33, 64 programmatic; 40 through the parser) must expand without throwing.

No user-facing LaTeX behavior changes. 535 tests pass; release build clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
Overrules LLD §4.1, which rejected code-built expansions because "\newcommand
would need this template engine built from scratch anyway." That premise does not
survive the implementation: substitution only reaches the top level of a template,
so \newcommand{\abs}[1]{\left|#1\right|} — the example §8.2 cites as the payoff —
is rejected by MTMacroAtom's own nested-placeholder assert. MTMathList.h said so
outright ("a future user-facing \newcommand would need substitution to descend
into sub-lists first"). The engine was paying for a reuse it cannot deliver, and
\newcommand appears nowhere in the PRD.

MTMacroDefinition now holds a prefix and a suffix LaTeX string; \pmod{n} expands
to prefix + arguments + suffix:

  pmod  \mkern8mu(\mathrm{mod}\mkern6mu  …  )
  mod   \mkern12mu\mathrm{mod}\mkern6mu  …  (none)
  pod   \mkern8mu(                       …  )

Both halves are ordinary LaTeX parsed by the ordinary builder, so this deletes
template mode, #N parsing, +buildTemplate:, MTMacroParameterAtom (60 lines), the
recursive MTContainsMacroParameter walker (51 lines), and both template-invariant
asserts — the placeholder cannot be misplaced when there is no placeholder.

MTMacroAtom keeps command + arguments, which is the requirement that justified it
existing: \pmod{n} still serializes back as \pmod{n}, and ^/_ still attach to one
atom via the builder's shared tail.

Cost: an expansion that interleaves fixed text between several arguments is no
longer expressible. None of the three built-ins needs that, and \newcommand would
need the descending substitution above regardless.

No user-facing LaTeX behavior changes — parsing, expansion, spacing, serialization
and round-trip tests are unchanged and still pass. 527 tests, 0 failures; release
build clean. LLD §4 annotated with the supersession.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
kostub added a commit that referenced this pull request Jul 27, 2026
This reverts commit 8d43971.

PR #265 added MTMacroAtom, the kMTMathAtomMacro type, a two-phase -finalized
(expandMacros, then the existing reclassifying pass), and
-transferScriptsToExpansion:, so that \pmod{n} would serialize back to
\pmod{n} rather than to its expansion. Nothing else uses any of it, and
\pmod/\mod/\pod themselves had not landed yet — so on master today this is
~460 lines of model-layer machinery with no caller.

Preserving the command name through serialization is not an acceptance
criterion for the feature (PRD §10 lists eight; none concern serialization).
It appears only as an open question for the LLD (§9.5), and PRD §11's own
reference table already describes \pmod's atom head as "Space + Open + Ord …"
— an atom sequence, i.e. the expansion. iosMath already declines the same
fidelity elsewhere: \implies serializes as \Longrightarrow.

\pmod, \mod and \pod land in #266 instead as a parse-time expansion, the way
TeX defines them. That also turns out to be the more faithful of the two: with
no macro atom to carry them, \pmod{n}^2 attaches ^2 to the ")" exactly as
LaTeX does, rather than transferring it onto the last scriptable atom of the
expansion.

MTMathList.h and MTMathList.m return to their state at e278be3. The \bmod
tests from #264 are unaffected. 446 tests, 0 failures.


Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@kostub

kostub commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #268, which rebuilds this and #265 as a single PR off the reverted master. Same architecture (MTMacroAtom expanded in -finalized), without the #N template engine, placeholder atom, and expansion-depth budget this branch accumulated. The one genuine finding from this review — stop commands in argument position being silently swallowed — carries over as part of MTParseErrorMissingArgument.

@kostub kostub closed this Jul 27, 2026
@kostub
kostub deleted the feature/modular-arithmetic-pr3 branch July 27, 2026 20:39
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