Skip to content

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

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

Add modular-arithmetic macros: \pmod, \mod, \pod#268
kostub wants to merge 9 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, and the rebuild first shipped a simpler prefix/suffix expansion in place of the LLD's #N template engine. That call was reversed on review: the #N engine and its placeholder atom are back, because arity-N table entries written in TeX macro syntax are worth the machinery. What did not come back is the expansion-depth budget and the recursive nested-placeholder walker — see "LLD fidelity" below.

\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

A registry entry is {argumentCount, templateString}, where the template is ordinary LaTeX plus #N argument references. An invocation parses to a single MTMacroAtom — command name, parsed arguments, and the parsed argument-free template — and expands by splicing a deep copy of each argument into its #N placeholders. -[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.

LLD fidelity

This PR now ships the #N template engine LLD §3.1/§3.3 specify (restored after review): a registry entry is {argumentCount, templateString} and the template is written in TeX macro syntax, so table entries read exactly like the amsmath definitions they reproduce and extend to any argument count. Substitution reaches only the top level of a template — #N inside \frac{}, {…}, or a script is not expressible. That, and the expansion-depth budget, remain \newcommand-era work (LLD §8.2).

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 — 482 tests, 0 failures (43 in MTModularArithmeticTest, trimmed to the set that catches a real regression). swift build 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

📝 Walkthrough

Walkthrough

Adds deferred macro atoms and built-in support for \pmod, \mod, and \pod. The change includes argument validation, template expansion, serialization, script handling, rendering tests, build integration, design documentation, and release notes.

Changes

Modular arithmetic macros

Layer / File(s) Summary
Macro atom model and expansion
iosMath/lib/MTMathList.h, iosMath/lib/MTMathList.m, iosMath/lib/internal/MTMacroParameterAtom.h, iosMath/render/internal/MTTypesetter.m
Adds macro atom types and template placeholders. Expands nested macros during finalization, transfers scripts, serializes raw commands, 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 required arguments and template placeholders. Reports missing-argument and template parsing errors.
Expansion and rendering validation
iosMathTests/MTModularArithmeticTest.m
Tests copying, mutation, nested expansion, finalization, structural equivalence, serialization, script placement, styling, rendering, and layout metrics.
Parser validation and build integration
iosMathTests/MTMathListBuilderTest.m, iosMath.xcodeproj/project.pbxproj, Package.swift, CHANGELOG.md
Adds malformed-input coverage, registers the modular-arithmetic test and internal header paths, and documents the v2.6.0 release.
Modular arithmetic design specification
docs/lld/2026-07-13-modular-arithmetic.md
Documents the parser registry, macro model, two-phase expansion, serialization, spacing, errors, tests, and future macro extensions.

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

Sequence Diagram(s)

sequenceDiagram
  participant Input
  participant MTMathListBuilder
  participant MTMacroAtom
  participant MTMathList
  participant MTTypesetter
  Input->>MTMathListBuilder: parse modular-arithmetic command
  MTMathListBuilder->>MTMacroAtom: store command, arguments, and template
  MTMathList->>MTMacroAtom: expand macro during finalization
  MTMacroAtom-->>MTMathList: return finalized atom sequence
  MTMathList->>MTTypesetter: typeset finalized sequence
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding the modular-arithmetic macros \pmod, \mod, and \pod.
✨ 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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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.

kostub and others added 7 commits July 28, 2026 02:49
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
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
iosMath.xcodeproj/project.pbxproj (1)

642-647: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add $(SRCROOT)/iosMath/lib/internal to both HEADER_SEARCH_PATHS arrays.

MTMathList.m, MTMathListBuilder.m, and MTModularArithmeticTest.m use bare imports for MTMacroParameterAtom.h. With USE_HEADERMAP = NO, the inherited Xcode search paths cannot resolve this header. Package.swift already includes the required path.

🤖 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.xcodeproj/project.pbxproj` around lines 642 - 647, Add
$(SRCROOT)/iosMath/lib/internal to both HEADER_SEARCH_PATHS arrays in the Xcode
project, alongside the existing iosMath/lib and render paths, so bare
MTMacroParameterAtom.h imports resolve when USE_HEADERMAP is disabled.
🤖 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 `@docs/lld/2026-07-13-modular-arithmetic.md`:
- Around line 157-176: Add the appropriate language identifier, such as text, to
the fenced data-flow block in the documentation while preserving its contents
unchanged.

In `@iosMath/lib/MTMathList.m`:
- Around line 193-198: Update the exception reason in the kMTMathAtomMacro case
to reference the valid MTMacroAtom initializer
-initWithCommand:arguments:templateExpression: instead of the nonexistent
selector, leaving the exception behavior unchanged.
- Around line 1889-1890: Add a kMTMaxMacroExpansionDepth guard to the recursive
expandMacros/expansion flow, tracking nested macro expansion depth and stopping
or failing safely once the bound is reached. Preserve normal expansion behavior
for inputs within the limit and ensure nested arguments cannot recurse
indefinitely.

---

Outside diff comments:
In `@iosMath.xcodeproj/project.pbxproj`:
- Around line 642-647: Add $(SRCROOT)/iosMath/lib/internal to both
HEADER_SEARCH_PATHS arrays in the Xcode project, alongside the existing
iosMath/lib and render paths, so bare MTMacroParameterAtom.h imports resolve
when USE_HEADERMAP is disabled.
🪄 Autofix

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: be56345b-656a-4319-9a3f-407314381c10

📥 Commits

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

📒 Files selected for processing (9)
  • Package.swift
  • docs/lld/2026-07-13-modular-arithmetic.md
  • iosMath.xcodeproj/project.pbxproj
  • iosMath/lib/MTMathList.h
  • iosMath/lib/MTMathList.m
  • iosMath/lib/MTMathListBuilder.h
  • iosMath/lib/MTMathListBuilder.m
  • iosMath/lib/internal/MTMacroParameterAtom.h
  • iosMathTests/MTModularArithmeticTest.m
💤 Files with no reviewable changes (1)
  • iosMath/lib/MTMathListBuilder.h

Comment on lines +157 to +176
```
LaTeX "\pmod{n}"
│ parse — read 1 arg via existing reader; parse template string
│ "\mkern8mu(\mathrm{mod}\mkern6mu#1)" with a FRESH builder → golden raw list
MTMacroAtom{ command:"pmod",
arguments:[ MTMathList("n") ],
template:[Space8, Open"(", Ord"mod"(rom), Space6, «#1», Close")"] }
│ │
│ +mathListToString: │ MTMathList.finalized (two-phase, model layer)
▼ ▼
"\pmod{n}" phase 1: expand ALL macros → RAW flat list
(command-faithful) deep-copy template, splice arguments into «#N»:
[Space8, Open"(", Ord"mod"(rom), Space6, <n copy>, Close")"]
phase 2: existing finalization, run ONCE → macro-free list
│ MTTypesetter (unchanged)
glyphs
```

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

Add a language identifier to the fenced data-flow block.

The fence at Line [157] has no language tag. Use text or another accurate identifier so markdownlint MD040 passes.

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 157-157: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@docs/lld/2026-07-13-modular-arithmetic.md` around lines 157 - 176, Add the
appropriate language identifier, such as text, to the fenced data-flow block in
the documentation while preserving its contents unchanged.

Source: Linters/SAST tools

Comment thread iosMath/lib/MTMathList.m
Comment on lines +193 to +198
case kMTMathAtomMacro:
// The default would mint a plain MTMathAtom carrying type 22 — an atom
// that claims to be a macro but cannot expand.
@throw [NSException exceptionWithName:@"InvalidMethod"
reason:@"A macro atom cannot be created by type. Use -[MTMacroAtom initWithCommand:argument:prefix:suffix:] instead."
userInfo:nil];

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

Fix the stale selector in the exception message.

The message names -[MTMacroAtom initWithCommand:argument:prefix:suffix:]. That selector does not exist. The designated initializer is -initWithCommand:arguments:templateExpression:, as used at line 1816.

🐛 Proposed fix for the message text
             `@throw` [NSException exceptionWithName:@"InvalidMethod"
-                                           reason:@"A macro atom cannot be created by type. Use -[MTMacroAtom initWithCommand:argument:prefix:suffix:] instead."
+                                           reason:@"A macro atom cannot be created by type. Use -[MTMacroAtom initWithCommand:arguments:templateExpression:] instead."
                                          userInfo:nil];
📝 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
case kMTMathAtomMacro:
// The default would mint a plain MTMathAtom carrying type 22 — an atom
// that claims to be a macro but cannot expand.
@throw [NSException exceptionWithName:@"InvalidMethod"
reason:@"A macro atom cannot be created by type. Use -[MTMacroAtom initWithCommand:argument:prefix:suffix:] instead."
userInfo:nil];
case kMTMathAtomMacro:
// The default would mint a plain MTMathAtom carrying type 22 — an atom
// that claims to be a macro but cannot expand.
@throw [NSException exceptionWithName:@"InvalidMethod"
reason:@"A macro atom cannot be created by type. Use -[MTMacroAtom initWithCommand:arguments:templateExpression:] instead."
userInfo:nil];
🤖 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 193 - 198, Update the exception reason
in the kMTMathAtomMacro case to reference the valid MTMacroAtom initializer
-initWithCommand:arguments:templateExpression: instead of the nonexistent
selector, leaving the exception behavior unchanged.

Comment thread iosMath/lib/MTMathList.m
Comment on lines +1889 to +1890
// An argument may itself contain a macro. Re-scan so the result is macro-free
// at its top level, and so script transfer targets a real atom.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect macro expansion recursion and any depth bound.
rg -n 'MaxMacroExpansionDepth|expandMacros|expansionAtDepth|- \(MTMathList \*\)expansion' iosMath/lib/MTMathList.m
sed -n '1755,1925p' iosMath/lib/MTMathList.m

Repository: kostub/iosMath

Length of output: 6957


🏁 Script executed:

#!/bin/bash
# Inspect all macro-expansion declarations, call sites, and macro-definition paths
rg -n --glob '*.{h,m}' 'expandMacros|expansionAtDepth|MaxMacroExpansionDepth|MTMacroAtom|defineMacro|macro' iosMath
printf '\n--- relevant declarations ---\n'
sed -n '60,115p' iosMath/lib/MTMathList.m
printf '\n--- expansion call sites ---\n'
sed -n '1685,1785p' iosMath/lib/MTMathList.m

Repository: kostub/iosMath

Length of output: 10012


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- macro definitions and template construction ---'
sed -n '1150,1210p' iosMath/lib/MTMathListBuilder.m
sed -n '1780,1825p' iosMath/lib/MTMathListBuilder.m
sed -n '1850,1890p' iosMath/lib/MTMathListBuilder.m

printf '%s\n' '--- static expansion-cycle verifier ---'
python3 - <<'PY'
from pathlib import Path
p = Path("iosMath/lib/MTMathList.m").read_text()

def body_after(signature, end_marker):
    start = p.index(signature)
    end = p.index(end_marker, start)
    return p[start:end]

expand = body_after("- (MTMathList *)expandMacros", "`#pragma` mark NSCopying")
expansion = body_after("- (MTMathList *)expansion", "- (void)transferScriptsToExpansion")

checks = {
    "expandMacros calls expansion": " expansion]" in expand or " expansion]]" in expand,
    "expansion calls expandMacros": "[out expandMacros]" in expansion,
    "expandMacros has no depth parameter": "AtDepth" not in expand and "depth" not in expand,
    "expansion has no depth parameter": "AtDepth" not in expansion and "depth" not in expansion,
    "no declared max depth symbol": "MaxMacroExpansionDepth" not in p,
}
for name, ok in checks.items():
    print(f"{name}: {'yes' if ok else 'no'}")
if not all(checks.values()):
    raise SystemExit("static expansion-cycle invariant failed")
PY

Repository: kostub/iosMath

Length of output: 6976


Add a recursion bound to nested macro expansion.

-expandMacros calls -expansion, which calls -expandMacros again for nested arguments. No kMTMaxMacroExpansionDepth bound exists. Deeply nested input can exhaust the call stack.

🤖 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 1889 - 1890, Add a
kMTMaxMacroExpansionDepth guard to the recursive expandMacros/expansion flow,
tracking nested macro expansion depth and stopping or failing safely once the
bound is reached. Preserve normal expansion behavior for inputs within the limit
and ensure nested arguments cannot recurse indefinitely.

Source: Learnings

@kostub
kostub force-pushed the feature/modular-arithmetic branch from 1b52511 to 3078e54 Compare August 12, 2026 19:32

@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.

Reviewed the seven [item 1][item 7] commits (b33eb3c..1b52511) that restore the #N template engine. Read-only review — no build, no test run. CI (Build & Test) is still IN_PROGRESS on 1b52511, so pass/fail is its answer, not mine.

What's solid: the swap is faithful to the plan and self-consistent — MTMacroParameterAtom's -copyWithZone: correctly reconstructs _argumentIndex after MTMathAtom's -initWithType:value: round-trip; -expansion deep-copies per placeholder so repeated #N and repeated -finalized both stay correct; _templateMode keeps # an invalid character in user input, and testBuildTemplateParsesParameterAtoms pins that. Dropping the finalized-loop assert (item 2) is safe — MTTypesetter.m:619-621 still catches the forged-type case. The test trim is defensible: the deleted unit tests each have a surviving end-to-end counterpart, the missing-argument rows live in MTMathListBuilderTest.m:1713-1719, and the equivalence tests still pin the three template constants against hand-written LaTeX, so a wrong template string still fails.


1. Stale selector in an exception message — iosMath/lib/MTMathList.m:197

reason:@"A macro atom cannot be created by type. Use -[MTMacroAtom initWithCommand:argument:prefix:suffix:] instead."

Item 5 renamed that initializer to initWithCommand:arguments:templateExpression: and updated the sibling message in -[MTMacroAtom initWithType:value:], but missed this one. Trigger: any caller doing [MTMathAtom atomWithType:kMTMathAtomMacro value:@""] gets an exception pointing at a selector that no longer exists.

2. The LLD document is being committed into the repo — docs/lld/2026-07-13-modular-arithmetic.md

git ls-tree -r 1b52511 -- docs/ returns exactly this one file, and master tracks no docs/ directory at all — the rest of docs/ (plans, research, reqs, the other LLDs) is untracked working material. Item 7 adds 601 lines of internal design doc to the shipped repository. If that wasn't the intent, drop it from the branch; item 7's goal ("record the restore in the LLD") is satisfied by editing the untracked copy.

3. PR description contradicts itself

The opening paragraph still reads:

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.

The "LLD fidelity" section directly below now says the #N template engine and the placeholder atom are exactly what this PR ships. The plan only called for replacing the "Deliberate deviation" section; the intro paragraph needs the same update.

4. MTMacroDefinition.argumentCount is a second source of truth for something the template already states

MTMathListBuilder.m — all three registry entries declare argumentCount:1 and reference #1. The arity is derivable from the template (max #N), and the only justification given is a future non-goal:

// Arity is declared rather than inferred from the template because a
// future \newcommand declares [argc] and its body may ignore arguments.

\newcommand is an explicit non-goal in the LLD, so nothing today needs the two to be independent — and keeping them independent is what makes them able to disagree, which then has to be defended in two more places: the arity assert plus its release-mode fallback in -[MTMacroAtom expansion] (8 lines), and the arity half of testEveryRegisteredMacroParses.

Deriving it instead: the registry goes back to a plain NSDictionary<NSString*, NSString*>, the 22-line MTMacroDefinition class disappears (along with its redeclaration in the test file), and -expansion's bounds check becomes structurally unreachable rather than asserted. In -macroAtomForCommand:, parse the template first (it consumes no input, so the order swap is free):

NSString* templateString = [MTMathListBuilder builtinMacros][command];
if (!templateString) {
    return nil;
}
MTMathList* templateExpression = [MTMathListBuilder buildTemplate:templateString];
// ...existing assert + internal-error guard...
NSUInteger argumentCount = 0;
for (MTMathAtom* atom in templateExpression.atoms) {
    if ([atom isKindOfClass:[MTMacroParameterAtom class]]) {
        argumentCount = MAX(argumentCount, [(MTMacroParameterAtom*)atom argumentIndex]);
    }
}

then the existing argument-reading loop, unchanged. Net: roughly −30 lines and one fewer way for the table to be wrong.

Separately, and only once because I gather this was settled on 2026-08-13: for three one-argument macros whose expansions are all prefix #1 suffix, the template engine (placeholder atom class + internal header + three build-system edits + _templateMode + the # parse branch) buys registry entries that read like the amsmath source, at the cost of two new failure modes the halves form couldn't have — arity/template disagreement, and #N nested in a sub-list silently not substituting. That's a real but small gain for the requirement as scoped. Your call; I'm not asking for a change.

5. iosMath.xcodeproj never gets lib/internal on its header search path

Package.swift added lib/internal to all three targets, but iosMath.xcodeproj/project.pbxproj:642-647 and :694-699 still list only render/internal — even though MTMathList.m and MTMathListBuilder.m now #import "MTMacroParameterAtom.h", which is not a sibling of either. Whether that compiles depends on the project headermap covering a header that sits in no build phase, which I can't establish by reading; CI's xcodebuild step will. If it's green, the point is only that this now diverges from how render/internal is wired — worth adding "$(SRCROOT)/iosMath/lib/internal" to both configurations for consistency.

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