Skip to content

alignedat environment (argument-reading hook)#248

Open
kostub wants to merge 4 commits into
masterfrom
feature/subordinate-envs-pr3
Open

alignedat environment (argument-reading hook)#248
kostub wants to merge 4 commits into
masterfrom
feature/subordinate-envs-pr3

Conversation

@kostub

@kostub kostub commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Goal

Add \begin{alignedat}{n} — the only environment that reads a mandatory {n} argument. Introduces a generic argument-capture hook on the parser (MTEnvProperties.argument, +environmentsTakingArgument, -readEnvironmentArgument), threads it through a renamed -buildTable:argument:firstList:row:, validates numColumns == 2n in the parser, generalizes the aligned factory branch to 2n columns, and round-trips the {n} argument.

Stack

This PR is based on feature/subordinate-envs-pr2 and will auto-retarget to master once PR 1 and PR 2 merge.

Design docs

  • Plan: docs/plans/2026-06-30-smallmatrix-gathered-alignedat.md (PR 3 section)
  • LLD: docs/lld/2026-06-30-smallmatrix-gathered-alignedat.md

Commits

  • [item 7] Add \begin{env}{arg} reader and thread through buildTableMTEnvProperties.argument + +environmentsTakingArgument + -readEnvironmentArgument; renamed -buildTable:argument:firstList:row: with all 3 call sites updated in one commit. Missing-{n} error → MTParseErrorInvalidCommand.
  • [item 8] Add alignedat 2n validation and factory branch — parser validates n (positive int) and numColumns == 2n; factory branch with alternating R/L alignment + spacer before odd columns. Test testAlignedat + 3 error rows.
  • [item 9] Round-trip alignedat {n} argument and per-pair spacers — strip per-pair spacer for odd columns in -serializedCellAtRow:column:; emit {n} in -appendLaTeXToString:. Round-trip assertion in testAlignedat.
  • [item 10] Add alignedat layout test (4 columns, butted pairs)testAlignedatLayout.

Tests

Full suite green: 277 tests across MTTypesetterTest / MTMathListBuilderTest / MTMathListTest, 0 failures.

One test-only deviation from the plan (noted in item 9): the plan's round-trip expected literal asserted x + (with a space), but iosMath's serialization joins atom nuclei with no whitespace — consistent with the existing aligned round-trip test. The expected literal was corrected to x+; no implementation change.

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds support for the alignedat LaTeX environment in iosMath, including parsing the mandatory numeric argument, setting up alternating column alignments, injecting relation spacers, and adding corresponding unit tests. The review feedback suggests trimming whitespace from the parsed environment argument to ensure robust validation, and creating unique MTMathAtom spacer instances for each cell insertion to avoid potential side effects from sharing a single mutable instance.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

message:@"alignedat requires a numeric argument, e.g. \\begin{alignedat}{2}"];
return nil;
}
return arg;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The captured environment argument may contain leading or trailing whitespace if the user includes spaces inside the braces (e.g., \begin{alignedat}{ 2 }). This causes the subsequent validation in buildTable:argument:firstList:row: to fail because it strictly checks for digit characters. Trimming whitespace from the argument string before returning it ensures robust parsing.

    return [arg stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

Comment on lines +525 to +533
MTMathAtom* spacer = [MTMathAtom atomWithType:kMTMathAtomOrdinary value:@""];
for (int i = 0; i < table.cells.count; i++) {
NSArray<MTMathList*>* row = table.cells[i];
for (int j = 0; j < row.count; j++) {
if (j % 2 == 1) {
[row[j] insertAtom:spacer atIndex:0];
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Sharing a single mutable MTMathAtom instance (spacer) across multiple cells in the table is a bad practice and can lead to unexpected side effects if any of the cells or their atoms are modified or inspected. Instead, a new MTMathAtom instance should be created for each insertion.

        for (int i = 0; i < table.cells.count; i++) {
            NSArray<MTMathList*>* row = table.cells[i];
            for (int j = 0; j < row.count; j++) {
                if (j % 2 == 1) {
                    MTMathAtom* spacer = [MTMathAtom atomWithType:kMTMathAtomOrdinary value:@""];
                    [row[j] insertAtom:spacer atIndex:0];
                }
            }
        }

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

Code Review — alignedat environment (items 7-10)

Reviewed the diff against the base branch feature/subordinate-envs-pr2 (commits 30e67ee0dda5ed), checked out against the plan (PR 3, items 7-10) and LLD §3.2/§3.3/§3.4/§6/§7. Static review only — no build/test runs.

Strengths

  • Faithful to the LLD/plan. Every planned artifact is present and in the right place: MTEnvProperties.argument property + nil init (MTMathListBuilder.m:20,33), +environmentsTakingArgument gate (:851), -readEnvironmentArgument reader (:865), the buildTable:argument:firstList:row: rename with all 3 call sites updated (:309, 1075, 1240), the 2n validation block (:1342-1367), the factory branch (MTMathAtomFactory.m:519-540), the spacer-strip (MTMathList.m:1104-1109), the {n} emit (:1226-1228), and all 4 error rows + structure test + round-trip + layout test.
  • Correct argument-reader edge cases. Traced {2} (normal), {} (empty → rejected downstream by the length check), {2 (EOF → expectCharacter:'}' fails), and missing { entirely — all four produce the right MTParseErrorInvalidCommand. The unlookCharacter/expectCharacter dance mirrors readColor exactly, and the NSAssert(_currentChar > 0) in unlookCharacter is safe because { was always consumed first.
  • Argument flow is correct. _currentEnv.argument = argument is set immediately after _currentEnv creation (:1305), and the validation reads _currentEnv.argument (:1344) after the parse loop but before the factory call — exactly as the LLD §3.4 logic flow specifies. Anonymous-table call sites pass argument:nil and never enter the alignedat validation block (gated on envName).
  • Justified deviation from the plan: kMTMathAtomNumber not kMTMathAtomVariable. The plan (line 691) asserted even-column cells start with a Variable atom, but the input 10/3/13 parse to Number atoms. The implementer corrected this (MTMathListBuilderTest.m:1541). Good catch.
  • Justified deviation: round-trip string. The plan (line 799) expected 10&x +&3&y (preserving spaces), but the serializer concatenates atoms without inter-atom whitespace, so x + correctly round-trips to x+ (:1548). The implementer's expected string is the actually-correct one; the plan's was wrong.
  • Consistent with the established atom-sharing pattern. The alignedat factory creates one spacer instance and inserts it into every odd column of every row (MTMathAtomFactory.m:525-532) — this is exactly what matrix does with its style atom (:406,410) and aligned does with its spacer (:442,446). No new sharing-safety concern is introduced.
  • Validation lives in the parser, factory stays spec-less. This matches the LLD §5 explicit resolution; the factory derives everything from numColumns and the parser owns the {n} interpretation. Clean separation.

Issues

Critical (Must Fix)

None.

Important (Should Fix)

None. The implementation is correct and complete against the acceptance criteria. The items below are quality/fidelity refinements, not blockers.

Minor (Nice to Have)

  1. readEnvironmentArgument does not strip whitespace inside {…} (MTMathListBuilder.m:872-880). The cited precedent -readColor calls skipSpaces after the opening brace (:587); this reader does not. So { 2 } or { 2} is rejected as non-numeric (space fails the digit loop at :1348), diverging from TeX's argument scanner which strips surrounding spaces. Low impact since users rarely write spaces inside {n}, but it's a fidelity gap and an inconsistency with the reader it was modeled on. Fix: either skipSpaces after { and strip trailing spaces before }, or have the digit-validation loop skip whitespace.

  2. testAlignedatLayout's "butted pairs" assertion is too weak (MTTypesetterTest.m:3213-3218). XCTAssertGreaterThanOrEqual(col.position.x, prevX) only checks left-to-right ordering, which holds for any non-overlapping table — including one with interColumnSpacing=18. A regression that accidentally set a nonzero spacing would still pass. The real interColumnSpacing==0 guarantee is already locked structurally in testAlignedat (:1520), so the layout test is effectively a smoke test. Consider asserting the x-gap between col 1 and col 2 is ~0 (the pair boundary), or at least weaken the comment to not overstate what it validates.

  3. Test coverage gaps vs. LLD §6/§7. No test for: (a) n=1 (the 2-column minimum / degenerate single pair), (b) an empty odd-column cell like x&&z&w with n=2 (exercises spacer-insert-into-empty-list + strip-on-empty-after-strip round-trip), (c) alignedat wrapped in delimiters (\bigl(\begin{alignedat}{1}...\end{alignedat}\bigr), the MTInner path the LLD §6 explicitly mentions), (d) {-1} (the LLD §6 negative case; {x} covers the non-numeric path transitively but not the leading-- specifically), (e) {} (empty braces). None are bugs — the code handles them — but they're natural edge cases left untested.

Recommendations

  • When the array environment lands, converge environmentsTakingArgument/readEnvironmentArgument with environmentsRequiringColumnSpec/columnSpec as the LLD §5 note prescribes. The reader currently hardcodes "alignedat" in its error message (:869,883) — fine for one env, but the message should become parameterized (or env-name-derived) when a second gated env arrives, otherwise users of the future env get a confusing "alignedat requires…" error.
  • The aligned factory branch validates numColumns == 2 itself (MTMathAtomFactory.m:434), but alignedat trusts the parser's 2n check and does no factory-side validation. This asymmetry is by design (LLD §5), but if the factory is ever called directly with an odd column count, it will silently produce a lopsided R/L/R layout with no error. A defensive numColumns % 2 == 0 assertion in the factory branch would be cheap insurance.

Assessment

Ready to merge? Yes.

Reasoning: The implementation is correct, complete against the plan/LLD, and consistent with the surrounding aligned/matrix patterns. I found no bugs — the argument reader handles EOF/empty/malformed inputs correctly, the validation flow is sound, and the two deviations from the plan (atom-type and round-trip string) are justified corrections. The remaining findings are minor fidelity/coverage refinements that can be addressed in a follow-up.


Review posted via the superpowers:requesting-code-review skill. This is review feedback only — not an approval.

@kostub kostub force-pushed the feature/subordinate-envs-pr2 branch from d6b62ce to 8a82ff6 Compare July 1, 2026 20:27
@kostub kostub force-pushed the feature/subordinate-envs-pr3 branch from 0dda5ed to 9d20f83 Compare July 1, 2026 20:27
@kostub kostub force-pushed the feature/subordinate-envs-pr2 branch from 8a82ff6 to 5702b0f Compare July 1, 2026 21:02
@kostub kostub force-pushed the feature/subordinate-envs-pr3 branch from 9d20f83 to 2feefb0 Compare July 1, 2026 21:03
@kostub kostub force-pushed the feature/subordinate-envs-pr2 branch from 5702b0f to 6c49503 Compare July 1, 2026 21:10
@kostub kostub force-pushed the feature/subordinate-envs-pr3 branch from 2feefb0 to b0986b5 Compare July 1, 2026 21:14
@kostub kostub force-pushed the feature/subordinate-envs-pr3 branch from b0986b5 to 38a877f Compare July 1, 2026 21:15
@kostub kostub changed the base branch from feature/subordinate-envs-pr2 to master July 1, 2026 21:15
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