fix(vars): compare array vars element-wise with Python semantics

AN
2d ago
+213-4

Fixes #7654

Problem

Array ordering comparisons (<, <=, >, >=) on array vars compile to a direct JS comparison between the two arrays. JS does not compare arrays element-wise; it coerces both sides to comma-joined strings, so [1, 10, 0] < [1, 9, 0] evaluates "1,10,0" < "1,9,0" and returns true, where Python compares element-wise and returns False.

Fix

The four array ordering operations now compile through a new compareArrays frontend helper ($/utils/helpers/array.js, same pattern as compareDatetime) that mirrors Python list comparison:

  • pairwise element comparison; the first unequal pair decides the result
  • when every shared element is equal, the shorter array sorts first
  • nested arrays compare recursively
  • booleans compare as numbers, matching Python where bool subclasses int ([True] == [1], [True] < [2])
  • a NaN operand pair yields NaN, matching Python where every ordering comparison against float("nan") is false
  • an incomparable element pair (mismatched or unsupported types, where Python raises TypeError) also yields NaN, so every ordering result is false instead of a silent string comparison

Equality (== / !=) is unchanged and keeps its existing comparison path.

Scope and remaining divergences

This covers scalar (number/string/bool) and nested-array elements, which is what serialized array vars contain in practice. Remaining known divergences from Python, unchanged by this PR:

  • string comparison is by UTF-16 code units, so astral-plane characters can order differently than Python's code-point ordering
  • elements serialized to strings before comparison (e.g. dates inside arrays) compare as their serialized form, not their original type
  • plain-object elements are treated as incomparable (ordering is false); Python compares dicts for equality and raises on ordering

Tests

  • tests/units/test_var.py: JS emission for all four operators plus the compareArrays import wiring
  • tests/units/vars/array_helpers.test.mjs: runtime suites against the real template module (numeric ordering, prefix rule, strings/booleans, nested arrays, NaN operands, bool/number mixing, incomparable types)
  • tests/units/vars/test_array_helpers_template.py: pytest wrapper that executes the runtime suites (skips when Node.js is unavailable)
  • Verified in a compiled app in a real browser: the unpatched build renders the string-coerced result, the patched build renders the Python-consistent result

Test limits: ruff check, ruff format, codespell, biome, pyright on the touched files, the touched pytest modules, and node --test were run directly and pass; the uv-managed pre-commit env hooks and full CI have not run on this branch.

@masenf - following the compareDatetime precedent here; would appreciate a look when you have a moment.