Skip to content

Add modular-arithmetic macros: \pmod, \mod, \pod - #268

Open
kostub wants to merge 3 commits into
masterfrom
feature/modular-arithmetic
Open

Add modular-arithmetic macros: \pmod, \mod, \pod#268
kostub wants to merge 3 commits into
masterfrom
feature/modular-arithmetic

Conversation

@kostub

@kostub kostub commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Rebuild of #265 + #266 as one PR. Those two were reverted in #267 — the design that emerged from their review had accumulated a #N template engine, a placeholder atom type, and an expansion-depth budget that the three macros never needed. This is the same architecture the LLD specifies, built once, without that.

\bmod already shipped in #264 as a symbol-table entry and is untouched here.

What it does

\pmod{n}, \mod{n}, \pod{n} — one argument each, expanding to amsmath's exact inline form:

command expansion
\pmod{n} \mkern8mu(\mathrm{mod}\mkern6mu n)
\mod{n} \mkern12mu\mathrm{mod}\mkern6mu n
\pod{n} \mkern8mu(n)

a \equiv b \pmod{n} now renders as it does in LaTeX.

How

An invocation parses to a single MTMacroAtom — command name, parsed arguments, and the fixed prefix/suffix LaTeX bracketing them. -[MTMathList finalized] expands every macro atom before the reclassifying pass, so:

  • the macro never reaches the typesetter (asserted in MTTypesetter.m);
  • Bin/Unary boundary demotion and number fusion see the flat stream the macro stands for, which is what makes \pmod{a+} and friends agree with the written-out expansion — tested by comparing both against each other;
  • \pmod{n} serializes back to \pmod{n}, not to its expansion.

Scripts on the invocation transfer to the last script-capable atom of the expansion, so \pmod{n}^2 scripts the closing paren. On collision (\mod{n^2}^3) or with no target, both scripts land on a fresh empty Ordinary — the same fallback the builder already uses for x^2^3.

Deliberate deviation from the LLD

LLD §3.1/§3.3 specify a #N template engine. This ships prefix/suffix halves instead: two ordinary LaTeX strings parsed by the ordinary builder, no template mode, no #N parsing, no placeholder atom type. For an expansion with one substitution region — all three of these — it is exactly equivalent, and it removes ~230 production lines along with both template-invariant asserts. LLD §4.1 already records why: the engine's stated payoff was \newcommand reuse, but \newcommand{\abs}[1]{\left|#1\right|} needs substitution that descends into sub-lists, which a top-level template engine cannot do either way.

The cost: an expansion interleaving fixed text between several arguments isn't expressible. None of the three needs that, and it's noted in the header doc.

Error handling

MTParseErrorMissingArgument — a command that requires an argument and is given none. Covers end of input, a }/^/_/& in argument position, and stop commands (\right, \\, \cr, \end, \over, …), which would otherwise terminate the enclosing list and be handed back as the "argument" — silently losing a matrix row for \begin{matrix}a\pmod\\b\end{matrix}. Only macros route through the new check; \sqrt keeps its long-standing permissive behavior.

Known gap

amsmath widens the leading gap to 18mu in display style (\if@display). iosMath can't: a macro expands at parse time, before the render style is known, so it always emits the inline 8mu/12mu. Out of scope per PRD §3.1/§9.1.

Testing

swift test — 526 tests, 0 failures (87 in MTModularArithmeticTest). swift build -c release clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw

Summary by CodeRabbit

  • New Features

    • Added support for modular-arithmetic notation: \bmod, \pmod, \mod, and \pod.
    • Added macro parsing, expansion, serialization, nesting, and script handling.
    • Added clear errors for missing or malformed macro arguments.
    • Added a public list of supported macro names.
  • Documentation

    • Documented the new notation and release details for v2.6.0.
  • Tests

    • Added comprehensive coverage for parsing, rendering, serialization, layout, and error handling.

\bmod shipped in #264 as a plain symbol-table entry. The three remaining
commands are macros: each takes one argument and expands to amsmath's exact
inline form -- \pmod{n} to \mkern8mu(\mathrm{mod}\mkern6mu n).

Rather than expanding at parse time, an invocation parses to a single
MTMacroAtom holding the command name, its parsed arguments, and the fixed
prefix/suffix LaTeX that brackets them. -[MTMathList finalized] expands every
macro atom before the reclassifying pass runs, so the macro never reaches the
typesetter, and the Bin/Unary boundary rules see the flat atom stream the macro
stands for. Keeping the atom means \pmod{n} serializes back to \pmod{n} rather
than to its expansion.

Scripts written on the invocation transfer to the last script-capable atom of
the expansion, so \pmod{n}^2 puts the 2 on the closing paren. On collision, or
when there is no target, both scripts go on a fresh empty Ordinary -- the same
fallback the builder already uses for x^2^3.

The expansion halves are plain LaTeX parsed by the ordinary builder; there is
no template syntax and no placeholder atom. That expresses one substitution
region, which is what these three macros need. LLD 3.1/3.3 specified a #N
template engine instead; 4.1 records why it was dropped.

Commands that require an argument now fail loud when given none:
MTParseErrorMissingArgument covers end-of-input, a }/^/_/& in argument
position, and a stop command (\right, \\, \cr, \end, ...) which would otherwise
end the enclosing list and be returned as the argument. Only macros route
through the new check; \sqrt keeps its existing permissive behavior.

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

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

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kostub, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c68aa06a-fd2c-42a6-b360-efa51ccab77f

📥 Commits

Reviewing files that changed from the base of the PR and between 1259952 and b33eb3c.

📒 Files selected for processing (5)
  • iosMath/lib/MTMathList.h
  • iosMath/lib/MTMathList.m
  • iosMath/lib/MTMathListBuilder.h
  • iosMath/lib/MTMathListBuilder.m
  • iosMathTests/MTModularArithmeticTest.m
📝 Walkthrough

Walkthrough

Adds deferred macro atoms and built-in support for \pmod, \mod, and \pod, including argument validation, expansion, serialization, rendering behavior, tests, and release documentation.

Changes

Modular arithmetic macros

Layer / File(s) Summary
Macro atom model and expansion
iosMath/lib/MTMathList.h, iosMath/lib/MTMathList.m, iosMath/render/internal/MTTypesetter.m
Adds kMTMathAtomMacro and MTMacroAtom, expands nested macros during finalization, transfers scripts to expansion targets, and prevents unexpanded macros from reaching typesetting.
Macro registry and argument parsing
iosMath/lib/MTMathListBuilder.h, iosMath/lib/MTMathListBuilder.m
Registers pmod, mod, and pod, parses their arguments and prefix/suffix definitions, exposes supported names, and reports missing arguments.
Expansion and script behavior tests
iosMathTests/MTModularArithmeticTest.m
Tests construction, copying, serialization, nested expansion, finalization stability, and script-transfer behavior.
Parsing, rendering, and project validation
iosMathTests/MTMathListBuilderTest.m, iosMathTests/MTModularArithmeticTest.m, iosMath.xcodeproj/project.pbxproj, CHANGELOG.md
Validates parse errors, structural equivalence, layout and rendering metrics, adds the test file to the Xcode target, and documents the v2.6.0 notation support.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Input
  participant MTMathListBuilder
  participant MTMacroAtom
  participant MTMathList
  participant MTTypesetter
  Input->>MTMathListBuilder: parse modular-arithmetic command
  MTMathListBuilder->>MTMacroAtom: construct macro with arguments and prefix/suffix
  MTMathList->>MTMacroAtom: expand during finalization
  MTMacroAtom-->>MTMathList: return expanded atom sequence
  MTMathList->>MTTypesetter: typeset finalized sequence
Loading

Possibly related PRs

  • kostub/iosMath#265: Provides macro infrastructure extended here for modular-arithmetic expansion.
  • kostub/iosMath#266: Adds the related macro registry and argument-parsing flow used here.
  • kostub/iosMath#267: Reverts macro-atom and expansion changes that this PR reintroduces.
🚥 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 clearly and concisely summarizes the main change: adding the modular-arithmetic macros.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/modular-arithmetic

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

@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

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

111-116: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

The "recursion is bounded by the parser" invariant does not hold for programmatically built atoms.

arguments exposes mutable MTMathLists, so [macro.arguments[0] addAtom:macro] after init creates a cycle and -expansion recurses until the stack overflows (the tests already rely on post-init argument mutation, e.g. testMacroAtomSerializationTracksArgumentMutation). A small depth budget threaded through -expansion/-expandMacros (or an identity set of macros currently expanding) would make the invariant enforced rather than assumed.

Also applies to: 1905-1921

🤖 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/MTMathList.m` around lines 111 - 116, Update MTMathList expansion
around expansion and expandMacros to guard against cycles introduced through
mutable macro arguments after initialization. Thread a bounded recursion/depth
budget or track macros currently being expanded, and stop expansion safely when
the limit or cycle is detected while preserving normal nested-macro expansion
and argument mutation behavior.
🤖 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`:
- Around line 3-4: Update the v2.6.0 entry in the changelog to remain unreleased
rather than using the future date, and revise its feature description to remove
\bmod as a newly added item while retaining the newly introduced \pmod, \mod,
and \pod macro changes.

In `@iosMath/lib/MTMathListBuilder.h`:
- Around line 110-112: Update the MTParseErrorMissingArgument documentation to
state that it applies specifically to missing macro arguments, while preserving
the listed end-of-input and delimiter cases and avoiding claims about all
commands such as \sqrt.
- Around line 54-60: Revise the documentation for the supported one-argument
macro command list in MTMathListBuilder so it describes only the symbol and
built-in-macro registries. Remove any wording that implies the two lists contain
every command accepted by the parser, while retaining the distinction that
symbol names and macros are maintained separately.

---

Nitpick comments:
In `@iosMath/lib/MTMathList.m`:
- Around line 111-116: Update MTMathList expansion around expansion and
expandMacros to guard against cycles introduced through mutable macro arguments
after initialization. Thread a bounded recursion/depth budget or track macros
currently being expanded, and stop expansion safely when the limit or cycle is
detected while preserving normal nested-macro expansion and argument mutation
behavior.
🪄 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: e9a0fd71-b55b-464f-8181-61bbeb3d536b

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0cb6e and 1259952.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • iosMath.xcodeproj/project.pbxproj
  • iosMath/lib/MTMathList.h
  • iosMath/lib/MTMathList.m
  • iosMath/lib/MTMathListBuilder.h
  • iosMath/lib/MTMathListBuilder.m
  • iosMath/render/internal/MTTypesetter.m
  • iosMathTests/MTMathListBuilderTest.m
  • iosMathTests/MTModularArithmeticTest.m

Comment thread CHANGELOG.md
Comment on lines +3 to +4
### v2.6.0 (2026-07-28)
* Add **modular-arithmetic notation**: `\bmod` as a binary operator, and the `\pmod`, `\mod`, and `\pod` macros with amsmath's exact inline gaps and upright "mod" (#264, #268). `a \equiv b \pmod{n}` now renders as it does in LaTeX. The macros expand through a new internal macro atom, so a macro invocation serializes back to the command the author wrote rather than to its expansion. amsmath's wider display-style gap (18mu instead of 8/12mu) is not reproduced: a macro expands at parse time, before the render style is known.

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

Correct the release metadata and \bmod scope.

July 28, 2026 is future-dated relative to July 27, 2026; keep this section unreleased until the tag is cut. Also, \bmod is unchanged in this PR, so do not present it as newly added.

Proposed fix
-### v2.6.0 (2026-07-28)
-* Add **modular-arithmetic notation**: `\bmod` as a binary operator, and the `\pmod`, `\mod`, and `\pod` macros ...
+### Unreleased
+* Add the `\pmod`, `\mod`, and `\pod` modular-arithmetic macros ... Existing `\bmod` remains a binary operator.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### v2.6.0 (2026-07-28)
* Add **modular-arithmetic notation**: `\bmod` as a binary operator, and the `\pmod`, `\mod`, and `\pod` macros with amsmath's exact inline gaps and upright "mod" (#264, #268). `a \equiv b \pmod{n}` now renders as it does in LaTeX. The macros expand through a new internal macro atom, so a macro invocation serializes back to the command the author wrote rather than to its expansion. amsmath's wider display-style gap (18mu instead of 8/12mu) is not reproduced: a macro expands at parse time, before the render style is known.
### Unreleased
* Add the `\pmod`, `\mod`, and `\pod` modular-arithmetic macros with amsmath's exact inline gaps and upright "mod" (`#264`, `#268`). Existing `\bmod` remains a binary operator. `a \equiv b \pmod{n}` now renders as it does in LaTeX. The macros expand through a new internal macro atom, so a macro invocation serializes back to the command the author wrote rather than to its expansion. amsmath's wider display-style gap (18mu instead of 8/12mu) is not reproduced: a macro expands at parse time, before the render style is known.
🤖 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` around lines 3 - 4, Update the v2.6.0 entry in the changelog to
remain unreleased rather than using the future date, and revise its feature
description to remove \bmod as a newly added item while retaining the newly
introduced \pmod, \mod, and \pod macro changes.

Comment thread iosMath/lib/MTMathListBuilder.h Outdated
Comment on lines +110 to +112
/// A command that requires an argument was given none — end of input, or a
/// `}`/`^`/`_`/`&` where the argument should be.
MTParseErrorMissingArgument,

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

Scope this error to macro arguments.

\sqrt still accepts omitted arguments by design, so “a command that requires an argument” overstates what currently emits this error.

Proposed fix
- /// A command that requires an argument was given none — end of input, or a
+ /// A built-in macro requiring an argument was given none — end of input, or a
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// A command that requires an argument was given none — end of input, or a
/// `}`/`^`/`_`/`&` where the argument should be.
MTParseErrorMissingArgument,
/// A built-in macro requiring an argument was given none — end of input, or a
/// `}`/`^`/`_`/`&` where the argument should be.
MTParseErrorMissingArgument,
🤖 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.h` around lines 110 - 112, Update the
MTParseErrorMissingArgument documentation to state that it applies specifically
to missing macro arguments, while preserving the listed end-of-input and
delimiter cases and avoiding claims about all commands such as \sqrt.

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

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

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.

Do not add anything tot he changelog. There is no release.

MTMacroAtom held an NSArray of arguments, but every macro takes exactly one
and nothing needed more. The array was the only reason MTMacroDefinition
existed -- its sole non-string field was argumentCount -- so fixing arity at
one collapses the registry to a plain command -> @[prefix, suffix] dictionary
and removes the class, the deep-copy-array helper, the zero-argument
serialization branch, and four loops.

Also drops +supportedMacroNames, which had no caller outside its own tests.

Comment density on the new code was 25-36% against a 3-14% baseline in these
files. Cut roughly in half by removing the archaeology: notes arguing against
the template engine that was never in this PR, LLD/PRD section citations, and
line-number references that go stale.

No behavior change. 524 tests, 0 failures.

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