Repository navigation
feat: generate the arithmetic operators a plain value object declares - #188
Merged
Merged
Conversation
A plain value object that declares IAdditionOperators, ISubtractionOperators, IMultiplyOperators, IDivisionOperators, IModulusOperators, IUnaryNegationOperators or IUnaryPlusOperators, with itself as the first type argument, gets those operators and their checked forms, where the wrapped type implements the same interface with the value object replaced by itself. `Money * 3m` and `Money / Money` as a decimal ratio work the same way: an operand or result of another type passes through. Each operator calls the wrapped type's through GeneratedArithmetic, a new [EditorBrowsable(Never)] helper in the base package: a call through a type parameter constrained to the interface, unchecked or checked as stated. Written as `left.Value + right.Value`, two bytes were added as ints and char's explicit operators were out of reach. No reflection, so Native AOT needs nothing; the smoke test's aot consumer uses one directly, through generic code and in a checked context. Never by default and never on a validated value object: the interface then stays unimplemented and fails with CS0535, as before. A hand-written operator for the same operands, or an explicit implementation of the interface, its own or a base record's, is kept with its checked form (decision 121, refining 28), so 2.3 code builds unchanged and nothing new is an Error. A hand-written checked operator alone keeps failing with CS9025. Input class: the declaration's shape (GTS), over the generated-type axes. The differential oracle ArithmeticParityTests runs 22 declarations (sbyte to BigInteger, char, Half, float, double, decimal, a record class over int and double, a nested and an internal one) against the wrapped type's own operators, unchecked and checked, on bounds, zero, one, minus one and the floating-point specials. Seeded: no checked forms (20 tests fail), a checked + beside a hand-written + (1), an explicit implementation not kept (2), operators on a validated value object (1), a lone hand-written checked + completed (2), % not generated (CS0535). EF Core translates an operator between two value objects as the wrapped type's arithmetic (ef_add on SQLite); one with another operand throws (#186), and a hand-written one would run as the database's arithmetic (#187). Closes #184 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 30, 2026
Closed
… is kept An interface can implement a generic-math operator explicitly, and every type that declares it reaches that implementation until it declares a public operator of its own. The arithmetic aspect looked for one on the value object and its base records only, so it generated a public `+` beside `IAddsLeft<TSelf>`'s, which returns the left operand: generic addition read 3 where the interface's answered 1, and code that compiled on 2.3 changed behaviour on 2.4 (found in an outside review of #188). The preservation check now reads the interfaces the value object implements too, its base records' included, and keeps that operator with its checked form, as it keeps the type's own. Input class: where an existing implementation of the operator comes from. ArithmeticTests now covers a generic interface over TSelf, one two interfaces down, an interface of a base record, and an interface for a single value object, beside which `-` is still generated. Reverting the check fails the test with 3 for 1. Measured on the way, outside this change: the generated IMinMaxValue and IFormattable members replace an interface's explicit implementation the same way; that predates 2.4 and is filed on its own. Defect-Class: GTS Found-By: OR Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 30, 2026
2.3.1 takes 0121 for #191's record (epic #198), since 2.3.1 stops at 0120 and DesignRecordTests wants the numbers without a gap. The arithmetic record becomes 0122: its file and heading, its index row, design/aspects.md's link and the aspect's doc comment. Until 2.3.1's 0121 reaches release/2.4.0, The_records_are_numbered_without_a_gap fails on this branch, the only conventions test that does. Refs #184 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CaffeinatedCoder
added a commit
that referenced
this pull request
Oct 30, 2026
2.3.1 takes decision 0121 (#198), and the 2.4.0 records move up one: the arithmetic record is 0122 (#188), the hand-written-operator record 0123 (#189), and this one 0124. Its heading and index row move, its citations of 121 and 122 become 122 and 123, the arithmetic record's index row names it as refining, and design/ef-core.md links it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g 2.3.1 release/2.4.0 took main after v2.3.1 (#264). Four conflicts, each two additions at one place: - design/adr/README.md: 0121 (2.3.1) before 0122 (arithmetic). - Generators README, "Write your own members": 2.3.1's paragraph on seams in a base record follows the seams it names, then the arithmetic one. - ValueObjectDeclaration: ImplementsExplicitly beside 2.3.1's Elsewhere, IsPublic, PublicName and InheritedSeams; none is in the merge base. - GeneratedSurface.verified.txt: regenerated by GeneratedSurfaceTests; it is the merge base plus both sides' changes, entry for entry (7623). Generators.Tests 1448/1448, Conventions.Tests 2034/2034. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #184
Problem
A plain value object over a number couldn't be added, subtracted or scaled without unwrapping it:
Money.From(a.Value + b.Value). You could declareIAdditionOperators<Money, Money, Money>, but then you had to write the operator yourself. The generators already write the equality, comparison andIMinMaxValuemembers next to it. EricksonLopez.DomainPrimitives generates arithmetic on every numeric primitive (KB-055).Change
IAdditionOperators,ISubtractionOperators,IMultiplyOperators,IDivisionOperators,IModulusOperators,IUnaryNegationOperatorsorIUnaryPlusOperators(with itself as the first type argument, directly or through another interface) gets those operators and theircheckedforms. The wrapped type must implement the same interface with the value object replaced by itself. Another operand or result type passes through:IMultiplyOperators<Money, decimal, Money>scales, andIDivisionOperators<Money, Money, decimal>is a ratio.TSelf * TSelfis allowed when declared.GeneratedArithmetic(new,[EditorBrowsable(Never)], in the base package). It is a call through a type parameter constrained to the interface, in a statedunchecked/checkedcontext. Results, overflow and the checked context are therefore the wrapped type's. This includesbyte(a plainleft.Value + right.Valuewould add asint) andchar(explicit operators). No reflection, so it is Native AOT safe.DateTime,TimeSpan,DateOnlyandGuidimplement none of the operator interfaces. The interface then fails with CS0535, as before.checkedform, so 2.3 code builds unchanged. A lone hand-writtencheckedoperator keeps failing (CS9025) rather than being completed. Of two interfaces asking for one operator, the second fails with CS0738.ValueObjectArithmeticAspect(+ templates),AspectOrder,ValueObjectAspect, andValueObjectDeclaration.ImplementsExplicitly.docs/value-objects.mdgains "Add, subtract and scale" (compiled sample with checked outputs), the Generators README gets a table row and the seam paragraph, plus the changelog. Decision record 0122 refines 28.design/aspects.mdanddesign/packages.mdare updated.C28-*),ArithmeticParityTestsregistered as a differential oracle with the seedarithmetic-checked-forms-not-generated, and anaxes.mdrow.Tests
ArithmeticParityTestsis a differential oracle over 22 declarations: every numeric wrapped type fromsbytetoBigInteger,char,Half,float,doubleanddecimal, a record class overintanddouble, a nested one and an internal one. They are declared through an interface of the tests' own. Each runs against the wrapped type's own operators, unchecked and checked, on bounds, zero, one, minus one and the floating-point specials.ArithmeticTestscovers the concrete-type calls, the other operand forms, the checked context, thebytewidth and a record class'snulloperand. It also covers a hand-written+(kept, with its checked form,-still generated), an explicit implementation, a base record's explicit implementation, and a validated value object with its own+.ArithmeticBuildTestscovers CS0535 (validated,string,DateTime), CS0738 (two interfaces for one operator) and CS9025 (a lonechecked +), with no LAMA error.ArithmeticQueryTests(EF Core, SQLite): an operator between two value objects translates (ef_add) and keeps the rows memory keeps. One with another operand throws, pinned and filed as EF Core: translate an arithmetic operator whose other operand or result is not the value object #186. A hand-written operator would run as the database's arithmetic: EF Core: a hand-written arithmetic operator in a query runs as the database's arithmetic #187.arithmetic=7.0 42 OverflowExceptionwith no trim or AOT warning (whole smoke test passed).GeneratedSurface.verified.txt: additions only.checked +generated beside a hand-written+:A_hand_written_operator_is_kept_with_its_checked_formfails.ArithmeticBuildTestsfails.checked +completed: 2 tests fail.%not generated: the build fails with CS0535 naming the operator.Weaving review
AspectOrderand to the outbound list. Missing from either, no operator is generated and the declared interface fails with CS0535, which is loud, not silent. Covered sites: directly declared, through another interface, nested, internal, record class, across an assembly (ProbeMoney), and transitively (the smoke test's layered consumers).GeneratedArithmetic's own members.CodoMetis.TypeKitstays Metalama-free (GeneratedArithmeticusesSystem.Numericsonly). Native AOT is measured by the smoke test.Review fix (5b0723a)
An outside review found that an interface's explicit implementation of an operator was replaced:
IAddsLeft<TSelf>'s+, which returns the left operand, gave way to a generated public one, and generic addition read 3 where the interface's answered 1. The preservation check now reads the interfaces the value object implements too, its base records' included.ArithmeticTests.An_interfaces_explicit_implementation_is_keptcovers a generic interface, one two interfaces down, a base record's interface and a single value object's (beside which-is still generated); reverting the check fails it with 3 for 1. Generators 1145/1145, Conventions 2027/2027. The same gap in members generated before 2.4 (IMinMaxValue,IFormattable) is #191.🤖 Generated with Claude Code