Skip to content

Add complex-number (phasor) arithmetic support - #9

Closed
Mearman wants to merge 3 commits into
mainfrom
feat/2-complex-number-arithmetic
Closed

Mearman wants to merge 3 commits into
mainfrom
feat/2-complex-number-arithmetic

Conversation

@Mearman

@Mearman Mearman commented Sep 1, 2026

Copy link
Copy Markdown
Member

Fixes #2

Adds a complex computed value / complexLiteral expression node, with arithmetic (add/subtract/multiply/divide/power), comparison (eq/neq), negation, and membership support in the evaluator.

Schema design: a structural union, not a tag field

A complex number can be authored either rectangular (re/im) or polar (magnitude/phase, in radians). The natural first instinct is a single object type with both sets of fields optional plus a runtime check that exactly one set is present -- but that throws away the type safety this package relies on everywhere else (no exhaustive narrowing, and the shipped JSON Schema would have to fall back to a loose anyOf-with-manual-validation shape instead of describing the real constraint).

The alternative -- a discriminant tag (form: "rectangular" | "polar") alongside a single kind: "complex" -- doesn't work either, for a reason specific to this schema's construction: PredicateNodeSchema/ExpressionNodeSchema and ComputedValueSchema are z.discriminatedUnion("kind", [...]), and Zod's discriminated union requires every member to have a distinct literal value for the discriminant key. Two members both carrying kind: "complex" is rejected (verified empirically -- it doesn't even get to a validation error, it throws lazily the first time something is parsed against it).

So complex/complexLiteral is a genuine two-branch union (ComplexRectangularSchema | ComplexPolarSchema, each a z.strictObject), discriminated structurally by which fields are present (re/im vs magnitude/phase) rather than by an extra tag. This meant ComputedValueSchema and ExpressionNodeSchema themselves could no longer stay flat z.discriminatedUnions -- each is now a plain z.union of (the existing discriminated union of every other node kind) alongside (the nested complex union). PredicateNodeSchema is untouched; predicates don't carry computed values directly.

This has one real consequence worth flagging for review: ExpressionNodeSchema's top-level JSON Schema conversion changes from a flat oneOf (one entry per kind) to a nested anyOf (two entries, one of which is itself a oneOf/anyOf). The two test helpers that walk the discriminant-kind set (one against the runtime .options, one against the generated JSON Schema) now recurse into nested unions instead of assuming every branch is a flat leaf object -- see the first commit. Everything downstream (the shipped schemas/trilean.schema.json, the smoke tests against the built dist/ output) still round-trips correctly; verified end to end.

toRectangular normalises either form to { re, im } before any arithmetic runs, mirroring the existing precedent of normalising every duration to milliseconds before combining two of different units.

Scope

  • add/subtract/multiply/divide support two complex operands or a mixed complex/number pair (a plain number widens to complex with a zero imaginary part).
  • power against a complex base is defined only for a real, dimensionless, integer exponent (computed by repeated multiplication); a non-integer or complex exponent is wrong-type.
  • modulo is not defined for complex values.
  • compare's ordering operators (gt/gte/lt/lte) are wrong-type against complex -- there's no natural ordering -- leaving eq/neq, and memberOf extends the same equality to complex candidates.
  • Dividing by a zero-magnitude complex number is domain-error, same category as ordinary division by zero.

README gets a new "Complex values" section, and the "Complex-number or phasor arithmetic" bullet is retired from "Out of scope".

Verification

pnpm lint && pnpm typecheck && pnpm test && pnpm test:integration && pnpm test:workers && pnpm build -- all green.

Issue #1 (native boolean kind) had not yet merged to main when this branch was cut, so ComputedValueSchema/ExpressionNodeSchema don't include a boolean member here -- this branch is based on current main as-is. Worth checking merge order against #1 before merging either.

…odes

A complex value is authored as either rectangular (re/im) or polar
(magnitude/phase, in radians), discriminated structurally by which fields
are present rather than by a form tag -- z.discriminatedUnion cannot host
two members sharing one literal discriminant value ("complex" twice; it
throws lazily at parse time), so ComputedValueSchema and
ExpressionNodeSchema each become a plain z.union of their existing core
discriminated union alongside a further nested union of the two complex
shapes.

Restructuring ExpressionNodeSchema this way changes its top-level JSON
Schema conversion from a flat oneOf to a nested anyOf, so the two test
helpers that walk its discriminant kinds (one against the runtime
.options, one against the generated JSON Schema branches) now recurse
into a nested union instead of assuming every branch is a flat leaf.
Adds a complex case throughout the evaluator: compare/memberOf equality
(eq/neq only -- complex numbers have no natural ordering), negate (via
rectangular normalisation), and the full arithmetic dispatch --
add/subtract/multiply/divide over two complex operands or a mixed
complex/number pair (a plain number widens to complex with a zero
imaginary part), power restricted to a real dimensionless integer
exponent computed by repeated multiplication, and modulo left undefined.
Division by a zero-magnitude complex number is domain-error, the same
category as ordinary division by zero.
…r out-of-scope bullet

Adds a "Complex values" section covering the rectangular/polar dual-input
design and why it needs a structural union rather than a form tag, the
toRectangular normalisation (mirroring duration's own millisecond
normalisation), the number-widens-to-complex mixed-arithmetic rule, and
power/modulo/ordering-operator scope limits. Extends the ComputedValue
and ExpressionNode type blocks, the compare section's operand-kinds
sentence, and the indeterminacy reference table's compare row. Complex
support moves this out of "Out of scope", leaving symbolic algebra as the
only remaining delegation precedent there.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-01T14:11:07.276186Z 3c2766c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman

Mearman commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Duplicate of #8 -- two independent implementations of the same issue (#2) landed in parallel. Both are complete and CI-green; closing this one to avoid two competing PRs against the same issue. #8 carries the surviving implementation.

@Mearman Mearman closed this Sep 1, 2026
@Mearman

Mearman commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Pushed 4 commits addressing the adversarial review findings on this branch:

Fixed (real defects):

  • applyComplexPower only checked the exponent's own unit for dimensionlessness, so a unit-ed complex base (e.g. (2+3i)[m] ^ 2) silently produced a unit-ed result, diverging from applyArithmeticOnNumbers's power case, which requires both operands dimensionless. Now requires the base to be dimensionless too, matching the real-number path exactly.
  • modulo against a complex operand was classified wrong-type; the accepted issue (issue Add complex-number (phasor) arithmetic support #2) specifies domain-error ("the same treatment already given to any operator outside its mathematical domain"). Fixed to match.
  • Added test coverage for four previously-untested indeterminate branches: memberOf's complex kind-mismatch and unit-mismatch cases, a unit-ed base/exponent to power against a complex base, and applyArithmetic's complex-vs-non-numeric fallback (e.g. complex + text).
  • README's indeterminacy reference table (arithmetic row) and the "Complex values" section now document all of the above, and the misleading "no requirement to re-serialise as rectangular" sentence is corrected -- every arithmetic operation always produces a rectangular result; only an untouched complexLiteral leaf can stay polar.
  • index.ts was missing re-exports for ComplexRectangularLiteralNodeSchema/ComplexPolarLiteralNodeSchema (defined in tree.ts), unlike their computed-value.ts counterparts, which already were. Added.

All of lint, typecheck, pnpm test (365), pnpm test:integration (52), pnpm test:smoke (55), and pnpm test:workers (9) are green on the updated branch.

Note for whoever picks this up: this PR is closed as a duplicate of #8, which already implements the dimensionless-base-and-exponent power check and the modulo-as-domain-error classification correctly -- these fixes match what #8 already does independently. #8's index.ts doesn't appear to export the rectangular/polar literal node schemas either, so that gap may be worth checking there too.

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.

Add complex-number (phasor) arithmetic support

1 participant