feat(arxjit): lower comparisons and boolean operators to astx - #112
Conversation
Adds the comparison and logical half of the expression layer, plus the type inference both need. A comparison lowers to an astx.BinaryOp carrying the comparison's op code, not to astx.CompareOp: IRx's visitor for CompareOp is _not_implemented, so a CompareOp would pass this stage and fail codegen, while the six comparison op codes are exactly the ones IRx resolves to Boolean. A chain becomes the conjunction Python defines it as, and an n-ary and/or folds left into astx's binary node. Operands cannot be lowered at the expected type the way an arithmetic operand is: at a condition the expected type is the bool the comparison yields, so "a < 3" would ask for 3 as a bool. Inference over a scope of the function's parameters supplies an operand type instead, widening to the type that can represent both sides. Also, from the arxlang#109 review: - A negated bool literal is now refused. bool is a subclass of int and negating one in Python yields an int, so -True folded to Int64 -1, putting an integer literal where the user wrote a bool. - Every binary node is built through one guard that refuses an operand astx cannot combine. astx requires a DataType on both operands and gives its UnaryOp the generic ExprType, so "not a" in either position raised a bare Exception out of astx with no location on it. The arithmetic form of this, "a + (not b)", failed the same way before this change; both are now located diagnostics. The real fix belongs upstream in astx. Two unreachable branches found while covering this are removed rather than left in: the boolean operator lookup has no failing case, since Python defines exactly two and both are mapped, and a rank lookup cannot fail because a signature naming an unmapped type is refused while the prototype is built. 285 tests, lowering.py at 100% line and branch coverage, verified on CPython 3.10, 3.11 and 3.14.
|
Thanks for the detailed PR. Lowering simple comparisons through IRx-supported Blocking issue 1:
|
|
@Jaskirat-s7 any estimate to finish this pr? I am planning to implement new stuff in the compiler ... so it will be nice to have this merged as soon as possible |
|
@Jaskirat-s7 I will merge this just to start to work here ... otherwise a lot of conflicts will pop up ... thanks for working on that |
Lowers the comparison and logical half of the expression layer, plus the type inference both need. Continues wiki Issue 5 after #109.
What lowers now
==!=<<=>>=, andand/or, including chained comparisons and n-ary boolean expressions.Design points for review
1. A comparison lowers to
astx.BinaryOp, notastx.CompareOp.CompareOpis the node that looks right, but IRx's visitor for it is_not_implemented(irx/base/visitors/base.py), and there is no CompareOp codegen in the builder or handlers. It would pass this stage and fail in codegen — the failure mode the unary-minus comment in this file already warns about. The six comparison op codes are exactly whatirx.analysis.typing.binary_result_typeresolves toBoolean, so that is what is emitted.test_the_comparison_tables_agree_with_irxreads IRx directly so the tables cannot drift.2. Operands cannot be lowered at the expected type.
Everywhere else an operand takes the width its context declares. That breaks down at a comparison: the expected type there is the bool the comparison yields, so
a < 3would try to build3as a bool and refuse a correct program. So this PR addsinfer, a dispatch family mirroring theexpressionoverloads, over ascopeseeded with the function's parameters. Operands are lowered at the widest type among them.The rank is
bool < i32 < i64 < f32 < f64. Float outranks every integer because mixing the two compares as float, which is what Python and C both do. Worth a second opinion: that meansi64 < f32compares in single precision.3. Chained comparisons repeat an operand.
a < b < cfolds to(a < b) && (b < c), which evaluatesbtwice. That is only safe because the v1 subset admits no expression with an effect — no calls, no walrus — so a second evaluation returns what the first did. Stated explicitly because it stops being true the moment the subset grows.4.
and/orare not Python'sand/or.Python's evaluate to one of their operands; lowered they are logical operators. Same answer only where the operands are already bools. IRx enforces this:
binary_result_type("&&", Int64, Int64)returnsNone, soa and bover two integers is a type error there, not here. This is the same question as yournot-in-a-numeric-context note on #109 — flagging them together since the answer is one decision.From the #109 review
-Trueis fixed. It folded toInt64 -1:boolsubclassesint, and negating one in Python produces an int, so the value reached_literal_valueas an int and the bool-before-int check could not see it. Now refused._UNARY_OPS' generic lookup — no change needed.test_a_unary_operator_astx_lacks_is_rejectedalready exercises it: it lowers~athrough the test helper that bypasses validation, soast.Invertdoes reach theop_code is Nonebranch.An upstream astx bug this surfaces
astx.UnaryOp.type_is the genericExprType, not aDataType, andastx.BinaryOprequires aDataTypeon both operands. Sonot acannot be an operand of any binary operator, and astx raises a bareExceptionwith no source location.This is not new —
a + (not b)fails the same way onmaintoday. Every binary node is now built through one guard that reports it as a located diagnostic instead. The real fix belongs in astx: givingUnaryOpthe type of its operand would make these compose. Happy to open that upstream if you agree.Two unreachable branches removed
Found while getting coverage back to 100%, and deleted rather than left in:
test_every_sig_type_is_rankedpins the tables together instead.Verification
285 tests,
lowering.pyat 100% line and branch coverage, arxjit total 99.69%. Run on CPython 3.10, 3.11 and 3.14. ruff, mypy strict, bandit, vulture and douki all clean; douki idempotent.Comparison tests assert through
analyze()rather than only on node shape, so each emitted form is proven to be one IRx accepts — including the narrow-type cases where ani32orf32parameter is compared against a wider literal.Next
Local assignment (locals join the scope, rebinding rules), then control flow.