[codex] add logical OR support#57
Conversation
Add || parsing and typing for statement conditions and value-position conditional expressions. In value position, logical OR now preserves Pluto's conditional value semantics by returning the first admitted value and supporting plain fallback values when the left condition fails. Covers parser behavior, bitwise OR non-regression, scalar/function-argument fallback, ranged array fallback, and ranged condition gates in E2E tests.
Rename the OR fallback lowering path around its onTrue/onFalse contract, share scalar comparison extraction, and gate base conditions once before entering OR fallback selection. Add E2E coverage for heap-string fallback, tuple fallback, and nested value-position OR expressions.
Lower boolean-position || through explicit branching instead of the eager scalar operator table. Tighten the conditional LHS binding invariant, move the i1 predicate alongside type helpers, and add condition-gate regression coverage for RHS skipping.
Add parser and E2E coverage for expressions where || appears in both the statement condition and value expression. Remove defensive internal panics that were not needed for normal logical OR lowering.
Document why value-position || accepts a conditional left operand with a fallback right operand, and why the language does not add a matching && form in this PR.
Rename ExprInfo.HasCondOr to HasFallbackOr so boolean logical OR dispatch reads in terms of fallback-OR semantics rather than the internal CondOr lowering mode.
Remove redundant arity panics from boolean logical OR lowering; the solver already rejects multi-output operands before compilation.
Remove a defensive nil check in fallback OR detection; compiler lowering already assumes typed expressions are present in ExprCache.
Add ast.IsLogicalOr and use it at AST-expression logical OR checks so compiler conditional lowering does not import token just to inspect operator spelling.
Use InfixExpression.IsLogicalOr when callers already have an infix node, while keeping ast.IsLogicalOr for arbitrary expression checks.
Collapse the WithFailure variant into compileCondExprBranch by making the false continuation explicit at each call site.
Rename extractCondExprSelf to extractInfixCondExpr to distinguish recursive expression-tree extraction from extracting one infix comparison node.
Rename conditional OR token/parser labels and shorten the fallback-yield lowering helpers so the control flow is easier to follow. Keep comments focused on ownership and value-position behavior rather than restating helper names.
thiremani
left a comment
There was a problem hiding this comment.
Review: logical OR support
Overview
This PR adds || as a logical OR operator distinct from bitwise |, in three roles:
- Statement conditions — short-circuit scalar
i1OR (x = a > 2 || b < 5 99) - Value-position fallback — left-biased conditional value selection (
x = b > 2 || d < 10) - Plain fallback / default values — (
x = b > 2 || -1), giving Pluto a default-value idiom
Are the changes useful/needed? Yes.
- Before this PR Pluto had no OR at all for conditions — comma gives AND, but expressing "either condition" required restructuring into multiple statements. This is a genuine expressiveness gap being filled.
- The value-position fallback semantics are a natural extension of Pluto's existing conditional-value model (CondScalar LHS extraction), and the "default value" form (
|| -1) is a particularly useful idiom — see theLogicalOrArrayDefaulttest. - Short-circuiting is real, not just documented:
LogicalOrStmtShortCircuitproves the RHS (an out-of-bounds index) is never evaluated when LHS is true. That makes||usable as a guard, which bitwise|could never be. - The deliberate omission of
&&is well-reasoned in the code comment (comma conditions already provide statement-position AND; chained comparisons refine values). Worth documenting in user-facing docs, since C-family users will look for it.
Verified during review
- Lexer: no lexer change needed —
readOperatorconsumes maximal operator runs, so||lexes as one token. Confirmed6 | 3regression test guards bitwise OR. - Precedence:
COND_ORsits betweenCOMMAandBITWISE_OR, matching conventional C precedence (||lowest of the operators). Correct placement. - Arity safety:
typeLogicalOrExpressionindexesright[i]bylen(left), but the pre-existing length-mismatch check inTypeInfixExpressionruns first, so no panic path. Multi-value fallback (Pair(...) > Pair(...) || Pair(7, 8)) is covered by tests. - CI: both test jobs green.
Minor findings (none blocking)
-
compileLogicalOrCondition(compiler/compiler.go): asymmetricfreeTemporary.freeTemporary(expr.Left, left)is called only on the true path; on the RHS path the left operand's temporaries are never freed. This is harmless today because condition operands are type-constrained toi1(never heap) — but by the same logic the true-path free is dead code. Suggest either removing it or freeing on both paths with a comment, so a future reader doesn't infer the wrong invariant. -
Code-size growth from continuation duplication.
compileFallbackOr/compileYieldcompile theonTruecontinuation once per success arm rather than merging arms with a phi before invoking it once. A chaina>1 || b>2 || cduplicates the consumer linearly (fine), but ORs in separate operands multiply:(b>2 || d<10) + (a>10 || c>2)compiles the add 4 times, and m OR-bearing operands give 2^m copies of the consumer. Realistic code won't hit this, but a phi-merge per OR (likecompileLogicalOrConditionalready does for conditions) would bound it if value-position ORs proliferate. Fine as a follow-up. -
Test coverage gap: no negative/type-error tests. All 26 new
.sptcases are success paths. The new diagnostics are untested: non-i1condition operands (1 || 2in condition position), mismatched value types (a > 1 || "str"), and "logical OR in value position requires a conditional left operand" (5 || 10). Worth adding if there's an error-fixture mechanism. -
Nit:
ast.IsLogicalOr(free function) and(*InfixExpression).IsLogicalOr(method) are near-duplicates; harmless, but the free function alone would suffice.
Code quality
The refactors are tasteful: compileCondExprBranch → branchCond gaining an onFalse hook is the minimal generalization needed, extractCondComparison is a clean extraction, and CondOr slots into the existing CondMode machinery rather than inventing a parallel path. The panic("internal: value-position logical OR must be lowered...") guard in compileInfixBasic is good defensive practice. Parser boundary handling (first || binds to the statement condition, second to the value) is subtle and well-tested.
Verdict: the feature is well-motivated and the implementation is solid. Recommend merging after considering the minor notes above (especially #3).
Add solver coverage for logical OR error paths and simplify boolean OR lowering by removing branch-local cleanup for i1 operands.
Split the token symbol grouping so conditional OR is not described as part of the bitwise operators.
Document how conditions behave by position: a statement gate keeps the previous value on failure, while a value-position conditional is a temporary that resolves to the zero value (or a || fallback) and composes like a named temporary, so storing and inlining never differ. Records the planned migration of nested value-position comparisons from expression-wide propagation to local zero resolution, and delineates the implemented surface from the designed (parenthesized explicit-value form, per-cell array conditionals). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a worked contrast between `a > 3 b < 5 || 10` (gate plus value-position fallback) and `(a > 3 b < 5) || 10` (one bracketed conditional), and note that spacing splits array cells so brackets also control cell count. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the condition-position i1 validation into named bothResolved and bothConditions locals and use the negated-conjunction form, so the check reads as "both types known and not both conditions". Add a comment noting the unresolved guard defers validation across solver passes. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The global/heap string flavor is an internal specialization detail. Make StrG.String() and StrH.String() return the user-facing "Str" so type mismatch and other diagnostics no longer leak StrG/StrH; the flavor is still carried by Mangle() for specialization identity and by the IsStrG/IsStrH predicates. Update the logical OR diagnostic test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`y > 2 3 || 7` is not "3 when y > 2, else 7" — at statement level `y > 2` is the gate, leaving `3 || 7` as a value-position OR whose left operand `3` cannot fail (a compile error). Replace with valid forms (`a > 2 || 7`, the chain) and show that an explicit-value fallback needs parentheses, `(a > 2 3) || 7`, with a note explaining the gate-vs-value distinction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
||as a logical OR operator distinct from bitwise|.||in statement conditions with scalarI1OR lowering.||as left-biased conditional fallback, including plain fallback values such asa < 5 || 10.Validation
python3 test.py tests