MTMacroAtom and two-phase finalized - #265
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
📝 WalkthroughWalkthroughAdds public and internal macro atom representations, expands macros during ChangesMacro expansion
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant MTMathList
participant MTMacroAtom
participant MTTypesetter
MTMathList->>MTMacroAtom: expand invocation and substitute arguments
MTMacroAtom-->>MTMathList: return expanded atoms
MTMathList->>MTMathList: finalize and reclassify
MTMathList-->>MTTypesetter: pass macro-free atoms
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
kostub
left a comment
There was a problem hiding this comment.
Code review — PR 2 (MTMacroAtom and two-phase finalized)
Reviewed the diff against feature/modular-arithmetic-pr1 only (gh pr diff 265, 7 commits, items 3–9), plus the plan's ## PR 2 section and LLD §3.1/§3.3/§3.4/§3.5/§6/§7/§8. Verified in a throwaway worktree at 34dd3f7: swift build, swift build -c release, swift test (480 tests, 0 failures), plus targeted probe tests for the deep-copy, nesting, and script-transfer questions.
Strengths
- The two-phase split is correct and the phase-2 loop provably runs exactly once per list.
-finalizedis the only caller of-finalizedAssumingNoMacros; every container's-finalized(MTFraction.mline 421,MTInner637,MTMathGroup1193,MTMathTable1282,MTMathStack1482, …) recurses through the public-finalized, so each sub-list gets exactly one expand + one reclassify.-[MTMathAtom finalized](line 322) also routessuperScript/subScriptthrough the public-finalized, so a macro buried in a script list is expanded too — that path isn't tested but it works. - Deep-copy correctness holds under repeated finalize, including nested macros. I probed a
\pod{\pod{n}^7}^2built by hand and finalized it three times: stable output((n)^{7})^{2}every call, with the raw list still serializing as\pod{\pod{n}^{7}}^{2}. The chain that makes this work is worth calling out because it's easy to break:-expansioncopies each template atom ([templateAtom copy], deep perMTFraction/MTInner/etc. overrides) and each argument ([self.arguments[i] copy], deep via-[MTMathList copyWithZone:]'scopyItems:YES), so-transferScriptsToExpansion:only ever mutates freshly-minted atoms. - Recursion for nested macros is right.
-expansionre-scans via[out mathListByExpandingMacros]before transferring scripts, so the outer script never lands on an unexpandedMTMacroAtom. Inner-then-outer ordering falls out correctly. - The script-transfer target rule matches TeX. I checked the case the tests don't cover —
\mod{n+1}^2, where the template ends in#1— and it finalizes to\mathrm{mod}n+1^{2}, i.e.^2binds to the1. That is exactly what amsmath's\newcommand{\mod}[1]{…\mkern18mu#1}does, since TeX strips the argument braces on substitution. Good. - All three documented deviations are sound, and I verified the third empirically. (a) Dropping
NS_UNAVAILABLEis the only resolution consistent withtestMacroAtomRejectsGenericInitializer;MTMathColorboxis the right precedent. (b) The declaration moves are forced —MTMacroAtomnow sits afterMTMathGroup(line 708), which is the plan's intent minus the impossible "beforeMTMathSpace" clause. (c)\mkern8.0mu(n)is genuinely correct:8is not in+spaceToCommands, soMTMathSpaceuses the\mkern%.1fmufallback. Fixing the assertion rather than the code was the right call. MTMacroParameterAtomas anisKindOfClass:-detected sentinel rather than a new public enum value is the right trade, and-copyWithZone:correctly restores_argumentIndexafter[super copyWithZone:]round-trips through-initWithType:value:.
Issues
Important
1. Three new compiler warnings on a previously warning-clean build — one of them load-bearing.
feature/modular-arithmetic-pr1 builds with zero warnings (swift build). This PR adds three, in both debug and release:
MTTypesetter.m:615:17: warning: enumeration value 'kMTMathAtomMacro' not handled in switch [-Wswitch]
MTMathList.m:1849:1: warning: convenience initializer missing a 'self' call to another initializer [-Wobjc-designated-initializers]
MTMathList.m:1967:17: warning: method override for the designated initializer of the superclass '-initWithType:value:' not found [-Wobjc-designated-initializers]
The -Wswitch one matters beyond tidiness. -[MTTypesetter createDisplayAtoms:] has no default: clause — -Wswitch is the project's mechanism for guaranteeing every atom type is handled. Leaving kMTMathAtomMacro unhandled means a leaked macro atom is silently skipped by the display loop, then becomes prevNode, and the next atom's addInterElementSpace: calls getInterElementSpaceArrayIndexForType(22, …) (MTTypesetter.m:49), which falls to default: → NSCAssert(false) + return -1. With NS_BLOCK_ASSERTIONS=1 (set in iosMath.xcodeproj/project.pbxproj:694 for Release) that's a NSUInteger(-1) array index — an out-of-bounds read rather than a diagnosable failure.
Fix: add case kMTMathAtomMacro: alongside the existing kMTMathAtomNumber/kMTMathAtomVariable/kMTMathAtomUnaryOperator "should never show here" group at MTTypesetter.m:616-621. That silences the warning and documents the invariant at the only place it can be violated.
For the other two: MTMacroAtom's throwing -initWithType:value: needs NS_DESIGNATED_INITIALIZER on the header declaration (a class may have several), or the MTMathColorbox shape of calling [self init] first. MTMacroParameterAtom's warning goes away by dropping NS_DESIGNATED_INITIALIZER from -initWithArgumentIndex: — which is arguably more honest anyway, since its -copyWithZone: deliberately depends on MTMathAtom's -initWithType:value: still being reachable.
2. -[MTMacroAtom expansion] substitutes only top-level #N; a placeholder inside a container atom is silently carried into the output.
MTMathList.m -expansion walks self.templateList.atoms flat and never descends into innerList/numerator/script lists. I confirmed the behavior with a template ( {#1} ) where the #1 sits inside an MTMathGroup: phase 1 returns it unsubstituted, and the raw expansion serializes as ({#1}). The argument is dropped entirely.
Nothing in this PR reaches it, and PR 3's three registry templates (\mkern8mu(\mathrm{mod}\mkern6mu#1), \mkern12mu\mathrm{mod}\mkern6mu#1, \mkern8mu(#1)) are all flat — \mathrm{…} sets fontStyle rather than producing a container. But PR 3 is where templates become LaTeX strings, and {#1} or \frac{#1}{2} is exactly how a contributor would naturally write one. The failure mode is bad: in debug, the NSAssert in -finalizedAssumingNoMacros fires late and from a confusing place (the group's inner list, one recursion level down); in release, it renders a literal #1.
Cheapest durable fix: validate in -initWithCommand:arguments:templateList: that no MTMacroParameterAtom occurs below the top level of templateList, and @throw if one does (fail loud, at the point where the mistake was made). Alternatively make substitution recurse into sub-lists — but that's real scope, and rejecting the template is enough to make PR 3 safe.
3. iosMath.xcodeproj never sees the new files, so none of these tests run under the documented iOS test command.
MTMacroParameterAtom.h has 0 references in iosMath.xcodeproj/project.pbxproj, while its peer internal header MTUnicode.h has 2. More significantly, MTModularArithmeticTest.m also has 0, while MTTypesetterTest.m and MTMathListBuilderTest.m have 4 each. So xcodebuild test -project iosMath.xcodeproj -scheme iosMath … — the command CLAUDE.md documents for iOS — compiles and runs none of the 40+ tests this PR adds (or PR 1's). Only swift test exercises them.
The MTModularArithmeticTest.m omission originates in PR 1, not here, so fix it wherever is least disruptive to the stack — but it should be fixed before PR 3 lands, or the iOS target silently loses coverage for the whole feature.
Minor
4. +[MTMathAtom atomWithType:kMTMathAtomMacro value:] mints a malformed atom. atomWithType: (MTMathList.m:162) has a case for every other structured type (Fraction, Radical, Inner, Stack, Text, Box, OrdGroup, Space, Color, Colorbox…) but not Macro, so it falls to default: and returns a plain MTMathAtom with type == 22. -mathListByExpandingMacros dispatches on atom.type == kMTMathAtomMacro and then casts unconditionally, so [list finalized] on such an atom dies with -[MTMathAtom expansion]: unrecognized selector (verified). It's loud, but opaque for a library consumer. Two options, either is fine: add a case kMTMathAtomMacro: to the factory that throws the same clear "use -initWithCommand:arguments:templateList:" message; and/or have phase 1 dispatch on isKindOfClass:[MTMacroAtom class] (which is already the idiom used for MTMacroParameterAtom two lines away) instead of on the mutable public type property.
5. The out-of-range argument index degrades silently in shipping builds. In -expansion:
NSAssert(index >= 1 && index <= self.arguments.count, …);
if (index < 1 || index > self.arguments.count) {
continue;
}With NS_BLOCK_ASSERTIONS=1 the assert vanishes and the continue silently drops the placeholder — the argument disappears from the rendered output with no signal. Same applies to the two NSAsserts guarding the reclassifying loop. Given the repo's fail-loud stance, @throw (as -initWithType:value: already does) would be more consistent than NSAssert + fallback for the arity mismatch specifically, since it is unambiguously a programming error.
6. testFinalizedUnchangedForMacroFreeLists doesn't test what its comment claims. Its comment promises "same Bin/Unary reclassification, same number fusion, same index ranges," but the body only asserts (a) finalized != list and (b) that two successive mathListToString: calls agree. It would pass with phase 2 completely broken, as long as it were deterministic. Worse, it compares via serialization — the exact lossiness the file's own AtomSignature comment calls out ("\bmod and a demoted \bmod stringify identically"), so the Bin/Unary distinction it names is invisible to it. ListSignature() already exists further down the file; asserting literal expected signatures for x+, -x, 1\times 23 would make it a real regression test. (The suite's other 480 tests are the actual safety net here, so this is a strength-of-test issue, not a correctness one.)
7. -[MTMacroAtom copyWithZone:] hardcodes the class. [[MTMacroAtom allocWithZone:zone] initWithCommand:…] should be [[[self class] allocWithZone:zone] …] — every other -copyWithZone: in the file gets this via [super copyWithZone:]'s [self class]. A future MTMacroAtom subclass would silently copy to the wrong class.
8. Undocumented aliasing in phase 1's output. When the list does contain a macro, -mathListByExpandingMacros copies nothing for the non-macro atoms — [expanded addAtom:atom] shares the receiver's atom objects (verified). That's safe as used (phase 2 copies via [atom finalized]), but the header comment only mentions the no-macro identity case. Worth one sentence, since the tests call this private method directly and a future caller could reasonably assume the result is detached.
9. Zero-argument macros don't round-trip. -appendLaTeXToString: emits \command with no terminator when arguments is empty, so a macro followed by a variable serializes as \noargsx (verified). Unreachable with PR 3's arity-1 registry, but if \newcommand (LLD §8.2) ever lands it will need the trailing-space handling the rest of the serializer does.
Notes (no action needed)
- Unbounded expansion recursion.
-expansion↔-mathListByExpandingMacrosrecurse with no depth budget, so a self-referential template would blow the stack. LLD §8 explicitly defers this to the\newcommandroadmap and states the three built-ins terminate by construction — correctly scoped out. Flagging only so it isn't forgotten when the registry becomes writable. indexRangedivergence between the raw and expanded lists is a non-issue per LLD §3.5 (assigned by finalization, consumed only by the editor's cursor) — confirmed against-finalizedAssumingNoMacros's zero-range branch.- Publicly re-declaring
-initWithType:value:onMTMacroAtomwidens the public surface slightly (MTMathAtomkeeps it in a class extension). It's what makes the test compile and it's documented, so this is fine — just noting it's a deliberate API-surface choice, not an accident.
Assessment
Ready to merge: with fixes.
Reasoning: The core design — two-phase finalized, deep-copy-on-expand, re-scan-then-transfer-scripts — is correct, and I could not break the properties that matter (idempotent repeated finalize, pristine templates/arguments, nested macros, exactly-one reclassify pass) under direct probing. The blocking work is hygiene rather than logic: restore the warning-clean build (issue 1, where the -Wswitch gap removes the project's only guard against an unhandled atom type reaching the typesetter), close the nested-placeholder hole before PR 3 turns templates into LaTeX strings (issue 2), and get the new files into iosMath.xcodeproj so the iOS scheme actually runs this coverage (issue 3).
Reviewed against 34dd3f7.
Deviation from LLD/plan: the header does NOT mark -[MTMacroAtom initWithType:value:] as NS_UNAVAILABLE, unlike the plan's literal text. NS_UNAVAILABLE makes a direct call to that selector a compile-time error, which conflicts with testMacroAtomRejectsGenericInitializer calling it directly and expecting a runtime XCTAssertThrows. MTMathColorbox (the idiom this task cites) does not mark it unavailable either -- it just overrides the method to throw -- so this brings MTMacroAtom in line with that existing, working pattern.
Renames the existing -finalized body to -finalizedAssumingNoMacros (verbatim) and adds assertions guarding against a macro atom or placeholder reaching the reclassifying loop. The new public -finalized composes phase 1 (-mathListByExpandingMacros, identity for now) with phase 2 (-finalizedAssumingNoMacros). Item 7 fills in real expansion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
Implements -[MTMathList mathListByExpandingMacros] and -[MTMacroAtom expansion]: phase 1 of -finalized now replaces every top-level MTMacroAtom with a deep-copied, argument-spliced RAW atom stream before phase 2's reclassifying pass runs, recursing into macros nested inside arguments so the result is macro-free at the top level. Deviation: PodMacroWithArgument's testFinalizedTracksArgumentMutation, copied verbatim from the plan, asserted "(n)"/"(m)" for the finalized serialization. The \pod template's leading 8mu space is not one of the named keywords in +[MTMathListBuilder spaceToCommands] (3/4/5/18/36/-3), so MTMathSpace correctly serializes it as "\mkern8.0mu" rather than being dropped. Corrected the expected strings to "\mkern8.0mu(n)"/"\mkern8.0mu(m)" to match actual, correct behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
34dd3f7 to
36d72d2
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
iosMath/lib/MTMathList.m (1)
1727-1754: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winPhase 1 dispatches on
typebut hard-casts toMTMacroAtom.
kMTMathAtomMacrois a public enum value, so[MTMathAtom atomWithType:kMTMathAtomMacro value:@""]yields a plainMTMathAtomthat passes this check and then gets-expansionsent to it — unrecognized selector. A class check makes the cast sound.♻️ Class-based dispatch
- BOOL hasMacro = NO; - for (MTMathAtom* atom in self.atoms) { - if (atom.type == kMTMathAtomMacro) { + BOOL hasMacro = NO; + for (MTMathAtom* atom in self.atoms) { + if ([atom isKindOfClass:[MTMacroAtom class]]) { hasMacro = YES; break; } } @@ - for (MTMathAtom* atom in self.atoms) { - if (atom.type != kMTMathAtomMacro) { + for (MTMathAtom* atom in self.atoms) { + if (![atom isKindOfClass:[MTMacroAtom class]]) {🤖 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 1727 - 1754, Update mathListByExpandingMacros to dispatch only when an atom is both typed kMTMathAtomMacro and an instance of MTMacroAtom before casting and calling expansion. Leave non-MTMacroAtom instances in the carried-through path unchanged.iosMathTests/MTModularArithmeticTest.m (1)
634-661: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSignature ignores container sublists.
AtomSignaturedescends into scripts only, so a divergence inside a fraction/radical/inner list would compare equal. Recursing on the known container lists would keep these equivalence tests honest as arguments get richer.🤖 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 `@iosMathTests/MTModularArithmeticTest.m` around lines 634 - 661, Extend AtomSignature to recurse into every supported container sublist, including fraction numerator/denominator, radical radicand, and inner-list contents, in addition to superScript and subScript. Use the existing ListSignature helper and the atom-type-specific container properties so divergences within nested structures produce different signatures.
🤖 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 `@iosMath/lib/MTMathList.m`:
- Around line 1901-1927: Add a bounded expansion-depth guard to the MTMacroAtom
expansion flow centered on expansion and mathListByExpandingMacros, so
self-referential arguments cannot recurse indefinitely. Track depth across
nested macro expansions, assert or abort once a fixed maximum is exceeded, and
preserve normal expansion behavior below that limit.
---
Nitpick comments:
In `@iosMath/lib/MTMathList.m`:
- Around line 1727-1754: Update mathListByExpandingMacros to dispatch only when
an atom is both typed kMTMathAtomMacro and an instance of MTMacroAtom before
casting and calling expansion. Leave non-MTMacroAtom instances in the
carried-through path unchanged.
In `@iosMathTests/MTModularArithmeticTest.m`:
- Around line 634-661: Extend AtomSignature to recurse into every supported
container sublist, including fraction numerator/denominator, radical radicand,
and inner-list contents, in addition to superScript and subScript. Use the
existing ListSignature helper and the atom-type-specific container properties so
divergences within nested structures produce different signatures.
🪄 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: 143dcd46-bfaf-49cb-835e-961df4a70382
📒 Files selected for processing (4)
iosMath/lib/MTMacroParameterAtom.hiosMath/lib/MTMathList.hiosMath/lib/MTMathList.miosMathTests/MTModularArithmeticTest.m
| claiming a new `MTMathAtomType`, because the public enum should not grow a value | ||
| that can never legally reach a finalized list. Detect it with `isKindOfClass:`. | ||
|
|
||
| Design: docs/lld/2026-07-13-modular-arithmetic.md §3.3. |
There was a problem hiding this comment.
Remove these kind of comments which refer documents that aren't checked in.
There was a problem hiding this comment.
Removed. Dropped the Design: docs/lld/... lines here and in MTMathList.h / MTMathList.m.
| // MTMacroParameterAtom.h | ||
| // iosMath | ||
| // | ||
| // INTERNAL HEADER — deliberately not listed in iosMath/module.modulemap, so it |
There was a problem hiding this comment.
should this go into iosMath/lib/internal? like the render/internal?
There was a problem hiding this comment.
Yes. Created iosMath/lib/internal/ and moved both MTMacroParameterAtom.h and MTUnicode.h into it — MTUnicode.h was sitting in the public lib/ dir for the same reason.
Wiring: .headerSearchPath("lib/internal") on the iosMath target and ../iosMath/lib/internal on both test targets in Package.swift; $(SRCROOT)/iosMath/lib/internal added to HEADER_SEARCH_PATHS (Debug + Release) plus a matching lib/internal group in iosMath.xcodeproj. Neither header is in module.modulemap, so the Swift surface is unchanged.
|
|
||
| /** The golden expansion template: a raw (non-finalized) list whose `#N` references | ||
| are placeholder atoms. Argument-free. */ | ||
| @property (nonatomic, strong, readonly) MTMathList* templateList; |
There was a problem hiding this comment.
why is it called templateList? it is not a list/array. should be template or templateExpression
There was a problem hiding this comment.
Renamed to templateExpression. Not plain template — this header is includable from ObjC++, where template is a keyword.
|
|
||
| - (instancetype)initWithCommand:(NSString*)command | ||
| arguments:(NSArray<MTMathList*>*)arguments | ||
| templateList:(MTMathList*)templateList NS_DESIGNATED_INITIALIZER; |
There was a problem hiding this comment.
same comment as above. Not templateList
|
|
||
| /// Overridden to fail loud: see -[MTMacroAtom initWithType:value:] in MTMathList.m | ||
| /// (same guard idiom as MTMathColorbox, which likewise does not mark this | ||
| /// NS_UNAVAILABLE — doing so would make the guard a compile-time error instead of |
There was a problem hiding this comment.
mark it NS_UNAVAILABLE instead.
There was a problem hiding this comment.
Done — initWithType:value: is now NS_UNAVAILABLE on MTMacroAtom.
Kept the runtime @throw alongside it: NS_UNAVAILABLE is compile-time only, so an id-typed caller walks straight past it. testMacroAtomRejectsGenericInitializer covers that path. Same reasoning for +atomWithType:kMTMathAtomMacro, which now throws instead of falling through to default: and minting a plain MTMathAtom carrying type 22.
|
|
||
| @interface MTMathList () | ||
|
|
||
| /** Phase 1 of -finalized: replaces every top-level MTMacroAtom with its RAW |
There was a problem hiding this comment.
Don't call it phase 1 of "finalised". It is just expand macros. I think just rename the method to "expandMacros"
There was a problem hiding this comment.
Renamed to expandMacros, and the phase wording is gone from the docs.
|
|
||
| /** Phase 2 of -finalized: the reclassifying left-to-right pass. Asserts that phase | ||
| 1 has already run. */ | ||
| - (MTMathList *)finalizedAssumingNoMacros; |
There was a problem hiding this comment.
Keep the name of the method as "finalized". document that macros should already be expanded. The first line for finalized should be to expand macros. Separate method is not needed.
There was a problem hiding this comment.
Done. finalized keeps its name, finalizedAssumingNoMacros is gone, and expansion is its first line:
- (MTMathList *)finalized
{
MTMathList* expanded = [self expandMacros];
...Its doc comment now states why expansion has to come first: finalization is irreversible and context-dependent (a Bin demoted to Unary at one boundary cannot be restored), so the reclassifying pass must see the flat raw stream a macro stands for rather than the macro atom.
|
|
||
| - (MTMathList *)finalized | ||
| { | ||
| // Two-phase (LLD §3.3, blocking issue #1). Finalization is irreversible and |
|
|
||
| - (MTMathList *)mathListByExpandingMacros | ||
| { | ||
| BOOL hasMacro = NO; |
There was a problem hiding this comment.
Why is this pass needed? Second pass handles this just fine.
There was a problem hiding this comment.
Removed — the main loop handles it. expandMacros is now a single pass.
- Move MTMacroParameterAtom.h and MTUnicode.h into iosMath/lib/internal/, wire the new search path into Package.swift and iosMath.xcodeproj. - Add MTModularArithmeticTest.m to the iOS test target (it was SPM-only). - Restore the -Wswitch exhaustiveness guard: handle kMTMathAtomMacro in MTTypesetter's createDisplayAtoms: switch. - Collapse finalizedAssumingNoMacros back into -finalized, which now expands macros as its first step; rename mathListByExpandingMacros -> expandMacros and templateList -> templateExpression. - Drop the pre-scan pass in expansion; the main pass already handles it. - Reject a #N placeholder nested inside a template sub-list at init, reject kMTMathAtomMacro in +atomWithType:, and throw (not assert) on an out-of-range argument index, since NS_BLOCK_ASSERTIONS is set in Release. - Bound expansion depth at 32 to turn deep macro-in-argument nesting into a diagnosable exception instead of a stack overflow. - Dispatch on isKindOfClass: rather than atom.type, since type is settable. - Emit a space after a zero-argument macro so \noargs x does not re-serialize as \noargsx; fix -copyWithZone: to use [self class]. - Strengthen tests to assert literal list signatures, and remove references to design docs that are not checked in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
|
Review round addressed in 0736def. Inline threads have per-item replies; the two nitpicks from the review summary (not repliable in-thread) are covered here: Class-based dispatch instead of
Verification on this commit: |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
iosMathTests/MTModularArithmeticTest.m (1)
323-335: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd a boundary test at the exact depth limit.
Only far-under (8, no throw) and far-over (64, throw) cases are tested.
depth >= kMTMaxMacroExpansionDepthis exactly the kind of comparison prone to off-by-one errors; a test at 32 vs 33 nested macros would directly pin down the boundary.✅ Suggested boundary test
// Boundary: exactly at the limit should still succeed; one more should throw. MTMathList* atLimit = [MTMathList new]; [atLimit addAtom:NestedPodChain(32)]; XCTAssertNoThrow([atLimit finalized]); MTMathList* overLimit = [MTMathList new]; [overLimit addAtom:NestedPodChain(33)]; XCTAssertThrows([overLimit finalized]);🤖 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 `@iosMathTests/MTModularArithmeticTest.m` around lines 323 - 335, Add exact-boundary coverage to testRunawayExpansionDepthThrows: verify NestedPodChain(32) finalizes without throwing and NestedPodChain(33) throws, preserving the existing shallow and runaway cases.
🤖 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 `@iosMathTests/MTModularArithmeticTest.m`:
- Around line 323-335: Add exact-boundary coverage to
testRunawayExpansionDepthThrows: verify NestedPodChain(32) finalizes without
throwing and NestedPodChain(33) throws, preserving the existing shallow and
runaway cases.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 08fe2a1c-feb7-40b5-868c-d8169f3abc3c
📒 Files selected for processing (10)
Package.swiftiosMath.xcodeproj/project.pbxprojiosMath/lib/MTMathList.hiosMath/lib/MTMathList.miosMath/lib/MTUnicode.miosMath/lib/internal/MTMacroParameterAtom.hiosMath/lib/internal/MTUnicode.hiosMath/module.modulemapiosMath/render/internal/MTTypesetter.miosMathTests/MTModularArithmeticTest.m
🚧 Files skipped from review as they are similar to previous changes (2)
- iosMath/lib/MTMathList.h
- iosMath/lib/MTMathList.m
Code review —
|
Correction to the review above — assertion policyThe review's central recommendation ("make the two invariant guards throw rather than assert") applies the wrong rule for this project. Correcting it here so the record isn't misleading. The policy: Re-scoring each guard
What changes
And one thing the review got backwardsIt listed under Strengths: "Converting the arity guard from Where the assert-only path would otherwise drop content silently in Release, the fix is to keep the assert and make the Release fallback visibly wrong (render the literal Open judgment call
UnaffectedIssues 3 (flat-template constraint), 4 ( Revised assessment: still "ready to merge with fixes" — but the fix list shrinks to issue 2 (restore the deleted guard as an assert), the two undocumented deviations, and the minors. 🤖 Generated with Claude Code |
Second review round on #265. The main change is which guards throw and which assert. The rule this project uses is who can violate the invariant, not whether the check survives NS_BLOCK_ASSERTIONS: NSAssert is for programmer bugs caught in testing, exceptions for errors the person writing the LaTeX can cause and that must still signal in Release. Re-scored against that rule, two guards added in 0736def were escalated too far: - InvalidTemplate (a #N nested in a sub-list) and InvalidMacroArgumentIndex (an arity mismatch) are both reachable only from templates authored inside this library, so both become NSAssert. Where the assert-only path would otherwise lose content in Release, the fallback is made visibly wrong rather than lossy: an out-of-range placeholder is carried into the output and renders as a literal "#2" instead of dropping the argument. - The depth guard stays a throw. \pmod{\pmod{...}} nests straight from user LaTeX. Restores the phase-2 macro guard that 0736def dropped when expansion switched to isKindOfClass: dispatch. The two changes were orthogonal -- -type is a settable public property, so a plain MTMathAtom relabelled kMTMathAtomMacro still walks through expansion and reaches the typesetter, which drops it silently. Also from the round: - Boundary coverage at exactly 32 and 33 nesting levels, not just 8 and 64. - Re-finalize idempotency on a list that contained a macro. - MTContainsMacroParameter caches its selectors; it ran NSSelectorFromString 13 times per visited object. - Bare "MTUnicode.h" / "MTMacroParameterAtom.h" imports, matching the MTFont+Internal.h idiom now that lib/internal is on the search path everywhere. - Header notes: -finalized on a lone macro atom does not expand, and templates must keep #N at the top level (so \frac{#1}{#2} is not expressible). - Stale reference to finalizedAssumingNoMacros, removed in 0736def. The hasMacro fast path stays out, per review direction; the ~6% on -finalized is not worth a second scan. swift test 488/488, iOS 447/447, macOS builds, release build warning-clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
Review round 2 —
|
| Guard | Who can trigger it | Now |
|---|---|---|
InvalidTemplate — #N nested in a sub-list |
library-authored templates only | NSAssert |
InvalidMacroArgumentIndex — arity mismatch |
library-authored templates only | NSAssert |
RunawayMacroExpansion — depth 32 |
user LaTeX (\pmod{\pmod{…}}) |
@throw, unchanged |
placeholder escape, NSParameterAsserts |
internal only | NSAssert, unchanged |
Where the assert-only path would otherwise lose content in Release, the fallback is made visibly wrong rather than lossy: an out-of-range placeholder is now carried into the output and renders as a literal #2, instead of being dropped so the argument silently vanishes. That satisfies fail-loud without escalating a bug-guard to an exception.
Restored the phase-2 macro guard
0736def deleted NSAssert(atom.type != kMTMathAtomMacro, …) while switching expansion to isKindOfClass: dispatch. The two changes were orthogonal and the guard still earns its keep in Debug: -type is a settable public property, so a plain MTMathAtom relabelled kMTMathAtomMacro is not an MTMacroAtom, walks through expansion untouched, and reaches MTTypesetter, where case kMTMathAtomMacro: drops it. testFinalizedRejectsNonMacroAtomTypedAsMacro covers it.
Also in this commit
testRefinalizingExpandedListIsIdempotent—list.finalized.finalizedon a list that contained a macro, which is the invariant the typesetter relies on and was previously only tested for macro-free lists.MTContainsMacroParametercaches its selectors. It ranNSSelectorFromString13× per visited object, on everyMTMacroAtomconstruction.- Import form unified to bare
#import "MTUnicode.h"/"MTMacroParameterAtom.h", matching the existingMTFont+Internal.hidiom now thatlib/internalis on the header search path in all three build systems. - Header notes on
MTMacroAtom:-finalizedon a lone macro atom does not expand (only-[MTMathList finalized]does), and templates must keep#Nat the top level — so\frac{#1}{#2}is not expressible, which matters for the\newcommandfollow-on but not for any built-in template. - Removed a stale comment referencing
finalizedAssumingNoMacros, which0736defdeleted.
Deliberately not done
The hasMacro fast path stays out. Per review direction on the round-1 thread. It costs ~6% on a microbenchmark of 2000 -finalized calls over a mixed expression (0.070s vs 0.066s) — not worth a second scan and a self-vs-copy return asymmetry. Now recorded as a deviation in the PR description, since the plan specifies it.
Flagged, not fixed
PR 3 (#266) will not compile against this head. Its branch is stacked on the pre-0736def tip and still uses templateList in 4 files. It needs a rebase plus the rename before it can build; the plan text carries the old name in PR 3's chunks too. Left as separate work rather than churning #266 while this PR is still moving.
Verification on c2bd777
swift build and swift build -c release clean (0 warnings) · swift test 488/488 · xcodebuild test -project iosMath.xcodeproj -scheme iosMath -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16' 447/447 · xcodebuild build -project MacOSMath.xcodeproj -scheme MacOSMath -sdk macosx succeeds.
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>
Part 2 of 3 in the modular-arithmetic stack.
Plan:
docs/plans/2026-07-25-modular-arithmetic.mdLLD:
docs/lld/2026-07-13-modular-arithmetic.mdGoal
The model layer gains a macro atom and the ability to expand it.
MTMathList.finalizedbecomes phase 1 (raw-expand every top-level macro) followed by phase 2 (the existing reclassifying loop, run exactly once). No parser changes — every test in this PR buildsMTMacroAtoms by hand, which is precisely what isolates the model-layer contract from the parser.Commits
[item 3] Add MTMacroParameterAtom template placeholder[item 4] Add MTMacroAtom and kMTMathAtomMacro[item 5] Serialize MTMacroAtom as \<command>{arg}[item 6] Split -[MTMathList finalized] into expand + reclassify phases[item 7] Expand macros to raw atoms in finalize phase 1[item 8] Transfer macro scripts onto the last scriptable expansion atom[item 9] Test one-pass expansion equivalence at the model layerDeviations from the plan (all documented in commit messages)
NS_UNAVAILABLEkept, plus a runtime@throw(item 4). The plan's Task 4 header snippet marksinitWithType:value:NS_UNAVAILABLE, but its owntestMacroAtomRejectsGenericInitializercalls that selector directly and expects a runtime throw.NS_UNAVAILABLEis compile-time only, so anid-typed caller walks straight past it — both guards are needed and both are present. (This entry previously saidNS_UNAVAILABLEwas dropped; that was never true of the shipped code.)templateListrenamed totemplateExpression(item 4, review round 1). The LLD names ittemplate, which is a C++ keyword and so unusable in a public header; the plan'stemplateListwas rejected in review because the property is not a list/array.templateExpressionis the third name and the one in the code. PR 3 (Macro registry, template parsing, and the three commands #266) is stacked on the pre-rename tip of this branch and still usestemplateListin 4 files — it needs a rebase and rename before it will compile. The plan text atdocs/plans/2026-07-25-modular-arithmetic.mdalso still saystemplateListin PR 3's chunks.hasMacrofast path removed (item 7, review round 1). The plan hasexpandMacrosreturnselfuntouched when the list holds no macro. Removed on review direction — a second scan to save a pass is not worth it — so every-finalizednow allocates and re-adds. Measured at ~6% on a microbenchmark of 2000finalizedcalls; accepted deliberately. The plan'sXCTAssertEqual(expanded, list)is inverted to match (testExpandingMacrosCopiesListWithoutMacros).MTMacroAtomafterMTMathGroup/beforeMTMathSpace(unsatisfiable —MTMathSpacecomes first in the header); an@interface...@endcategory nested inside@implementation...@endin the test file; and theMTMacroAtom ()extension after its own use site inMTMathList.m. Each moved to the nearest valid location, no behavioral change.testFinalizedTracksArgumentMutationexpects"(n)"; actual correct output is"\mkern8.0mu(n)", because the template's leading 8mu space has no named command in+spaceToCommands(only 3/4/5/18/36/-3 do) and so serializes via the\mkern%.1fmufallback. Fixed the assertion, not the code.Review rounds
0736def) — 11 inline comments, each answered in its thread.c2bd777) — assertion policy.InvalidTemplateandInvalidMacroArgumentIndexdemoted from@throwtoNSAssert(only library-authored templates can violate them); the depth guard stays a throw because\pmod{\pmod{…}}nests from user LaTeX. Restored the phase-2kMTMathAtomMacroguard that round 1 dropped. Plus boundary/idempotency tests, selector caching, import-form consistency, and header notes.Stack
\bmodas a binary operator (\bmod as a binary operator #264)MTMacroAtomand two-phasefinalized🤖 Generated with Claude Code
https://claude.ai/code/session_01Ts3f5UtUywaqimvE4U1rkw
Summary by CodeRabbit
New Features
Bug Fixes
Tests