Skip to content

cypher-codegen: fix scalar math & numeric arithmetic type inference#95

Merged
jbmusso merged 5 commits into
mainfrom
fix/cypher-codegen-math-inference
Jun 12, 2026
Merged

cypher-codegen: fix scalar math & numeric arithmetic type inference#95
jbmusso merged 5 commits into
mainfrom
fix/cypher-codegen-math-inference

Conversation

@jbmusso

@jbmusso jbmusso commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes two numeric type-inference gaps in the cypher-codegen type checker (src/frontend/InferType.ts):

  1. Scalar math functions were unrecognized. log, log10, exp, sqrt, the trigonometric
    functions, ceil, floor, round, e(), pi(), rand() and friends were absent from the
    function registry and not special-cased, so the checker threw
    CypherTypeError: Unrecognized function '<name>' — a hard codegen failure for any query
    projecting a bare math call (e.g. RETURN log(x) AS weight).
  2. Arithmetic typed only the first operand. a * b / a ^ b only inspected the first operand,
    and numeric a + b returned the first operand's type. So Long * Double (or priceLong * 1.5)
    inferred Long, emitting Neo4jInt and risking a runtime decode failure when Neo4j returns a
    float. Compounded by all numeric literals typing as Long (the lexer's DIGIT token doesn't
    distinguish ints from floats).

Changes

  • Math functions: added Neo4j scalar math functions to AGGREGATE_RETURN_TYPESlog/log10/
    exp/sqrt/ceil/floor/round/sin/cos/tan/cot/asin/acos/atan/atan2/degrees/
    radians/haversine/e/pi/randDouble, signLong. abs is special-cased to
    preserve its argument's numeric type (abs(Long)=Long, abs(Double)=Double).
  • Arithmetic: refactored the addSub/multDiv/power/unary levels of inferExpressionType into
    recursive helpers (inferAddSubType/inferMultDivType/inferPowerType/inferUnaryType) that
    infer every operand and unify them with a numericJoin (Long ⊔ Double = Double). A Double
    anywhere widens the result; ^ yields Double. Non-numeric (date/duration), String-concatenation,
    and list-concatenation cases are preserved.
  • Numeric literals: numLit now types literals by their text — 1.5, 1e3, and float-suffixed
    literals infer Double; plain integers infer Long. So priceLong * 1.5 correctly infers
    Double.
  • Docs: updated src/frontend/CLAUDE.md (function table + a numeric-arithmetic typing rule).

Tests

Reproduce-then-fix per gap (RED → GREEN → RED → GREEN):

  • Added a scalar math functions describe block (log/trig → Double, signLong, abs
    input-preserving) — failed with "Unrecognized function" before the fix.
  • Added an arithmetic operand typing describe block (Long ⊔ Double = Double, ^Double,
    float literals → Double, with String/list concatenation as regression guards) — failed inferring
    Long before the fix.

Full package unit suite passes (215 tests), including CypherCodegen.typecheck which compiles
the generated output under Effect v4 (confirms the DoubleSchema.Number codegen path is
unaffected). eslint and tsc are clean.

Note: the Neo4j *.integration.test.ts suite was not run in this environment (Docker
unavailable); changes are confined to type inference and are covered by the unit suite.

A changeset (patch for @evryg/effect-cypher-codegen) is included.

jbmusso and others added 5 commits June 13, 2026 01:00
Adds unit coverage for log/log10/exp/sqrt/trig/ceil/floor/round/e/pi/rand
(→ Double), sign (→ Long), and abs (input-preserving). These currently throw
"Unrecognized function" — the type checker has no entries for scalar math.

Generated with AI

Co-Authored-By: AI <ai@example.com>
Adds Neo4j scalar math functions to the type checker. log/log10/exp/sqrt,
trig, ceil/floor/round, e/pi/rand and friends return Double; sign returns
Long; abs is special-cased to preserve its argument's numeric type. Previously
any of these threw "Unrecognized function", failing codegen for queries that
project bare math calls.

Generated with AI

Co-Authored-By: AI <ai@example.com>
Adds unit coverage for numeric arithmetic unification (Long ⊔ Double = Double
across operands and operator levels, power → Double) and float-literal typing
(1.5 → Double). These currently infer Long because arithmetic types only the
first operand and all numeric literals are typed Long. String and list
concatenation cases included as regression guards.

Generated with AI

Co-Authored-By: AI <ai@example.com>
Arithmetic previously typed only the first operand, so Long * Double inferred
Long (codegenning Neo4jInt and risking a runtime decode failure on floats).
Refactors the addSub/multDiv/power/unary levels into recursive helpers that
infer every operand and unify them by the numeric join (Long ⊔ Double = Double);
^ yields Double. Also types numeric literals by their text (1.5/1e3 → Double),
so priceLong * 1.5 now infers Double. Non-numeric, String, and list operands
are unchanged.

Generated with AI

Co-Authored-By: AI <ai@example.com>
Generated with AI

Co-Authored-By: AI <ai@example.com>
@jbmusso
jbmusso merged commit 2c84a48 into main Jun 12, 2026
8 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 12, 2026
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