Repository navigation
expr: Give the binary lazy blanket impl a concrete iterator type - #39061
Merged
antiguru merged 1 commit intoOct 14, 2026
Merged
Conversation
antiguru
added this pull request to stack #38942
September 23, 2026 13:12
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
September 23, 2026 14:23
bf4a86a to
dc73d40
Compare
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
September 23, 2026 14:33
dc73d40 to
f96d625
Compare
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
September 23, 2026 17:54
f96d625 to
1405739
Compare
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
September 23, 2026 18:09
1405739 to
6388d87
Compare
mgree
approved these changes
Sep 23, 2026
mgree
left a comment
Contributor
There was a problem hiding this comment.
Neat!
Claude had some thoughts around the assertions and checks.
Details
Strong suggestion
- Keep assert_none!(datums.next(), ...) instead of assert_eq!(exprs.len(), 2) (binary.rs:176-180). The new assert is weaker, and the four-line comment defending it relies on a rule nothing enforces. The macro guarantees one datum per component, but a hand-written EagerBinaryFunc doesn't have to. For example, one with Input = (A,) would silently drop the second argument, and the old check would have caught that. The iterator's type is now always array::IntoIter<_, 2>, so keeping the direct check adds no per-function code. With it, the comment and the invariant it has to justify both go away.
Nits
- Arity is checked in two places. Too few arguments is caught by the let-else before evaluation, and too many by an assert after evaluation. If exprs.len() == 2 stays, put it next to the let-else, or match let [a, b] = exprs else { ... }, so all arity handling happens before any eval. (Matching [a, b] would turn three arguments from a panic into an Internal error. Either is fine since the state can't be reached.)
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
September 24, 2026 10:14
6388d87 to
99836fa
Compare
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
September 24, 2026 14:29
99836fa to
4d9c433
Compare
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
September 25, 2026 16:00
4d9c433 to
88841f6
Compare
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
October 14, 2026 09:11
88841f6 to
f79eb4a
Compare
antiguru
commented
Oct 14, 2026
Comment on lines
+150
to
+152
| let [expr_a, expr_b, ..] = exprs else { | ||
| return Err(EvalError::Internal("unexpectedly missing parameter".into())); | ||
| }; |
Member
Author
There was a problem hiding this comment.
Can we turn this into an exact match, and return an error if the arity doesn't match, instead of the assert further down.
Member
Author
There was a problem hiding this comment.
Done in 83b4099. let [expr_a, expr_b] = exprs now matches exactly and returns an internal error for any other count before evaluating. The post-evaluation assert is replaced by a datums.next() check that returns an internal error, which also catches an Input that leaves a datum unread, the case from the earlier review.
Posted by Claude Code.
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
October 14, 2026 15:18
f79eb4a to
e136d2c
Compare
antiguru
removed this pull request from stack #38942
October 14, 2026 15:45
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
October 14, 2026 15:46
e136d2c to
83b4099
Compare
antiguru
added this pull request to stack #39823
October 14, 2026 15:46
mgree
approved these changes
Oct 14, 2026
| .map(|expr| expr.eval(datums, temp_storage)); | ||
| let [expr_a, expr_b] = exprs else { | ||
| return Err(EvalError::Internal( | ||
| format!("binary function called with {} arguments", exprs.len()).into(), |
`InputDatumType::try_from_iter` is generic over the iterator it reads from, and the blanket impl passed it a `map` closure over `exprs`. That closure is a fresh anonymous type for every function and every `Eval` implementor, so the tuple's unwrapping was instantiated once per combination rather than once per input type. Evaluating the two arguments into an array gives every call site the same iterator type, `core::array::iter::IntoIter<Result<Datum, EvalError>, 2>`, so the instantiations are keyed by the `Input` tuple and the types its unwrapping recurses into. Doing it in the blanket impl rather than at each call site means hand-written `EagerBinaryFunc` implementations get it too. Measured with `cargo llvm-lines -p mz-expr` on this tree, not reusing the figures from the superseded #36705. The `try_from_iter` instantiations reaching binary functions drop from 108,569 lines across 1,118 entries to 33,539 across 164. The crate total drops from 1,786,549 lines to 1,703,578, 4.6% fewer. Evaluation is unchanged for two arguments, which is the only count planning produces: both were already evaluated before any failure was inspected, so the array changes when they are evaluated relative to each other, not how often. All nine crossings of valid, NULL, evaluation error and wrong type return what they did before. Unreachable states change from panics to internal errors. An argument count other than two returns an internal error before evaluating any argument, where three or more arguments used to panic after evaluating the third. A single argument that is a non-NULL datum of the wrong type now reports this arity error rather than `invalid input type`. An `Input` that leaves one of the two datums unread returns an internal error, where it used to panic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
antiguru
force-pushed
the
sqlfunc-binary-direct-unwrap
branch
from
October 14, 2026 19:12
83b4099 to
413f441
Compare
antiguru
added a commit
that referenced
this pull request
Oct 14, 2026
…ro (#39062) Converts the remaining hand-written scalar functions to `#[sqlfunc]`, one commit per source file or function: the `Cast*ToNumeric` casts (`int16`, `int32`, `int64`, `uint16`, `uint32`, `uint64`, `float32`, `float64`, `jsonb`), `PadChar`, `AdjustNumericScale`, `CastArrayToString`, `CastRangeToString`, and all of `date.rs`, `time.rs`, `map.rs`, `record.rs`, `list.rs`, `timestamp.rs` and `string.rs`. Each commit message accounts for every method the deleted impl overrode. No declared property changes. The registry reports only the macro provenance fields moving. Where a hand-written impl declared a property more conservatively than the body needs, the conversion pins the declared value: `could_error` on `record_get`, and `introduces_nulls`, `could_error` and a nullable output on `map_build_from_record_list`. #39814 corrects both. Supersedes #39079 to #39097, which held one conversion each. Based on #39061. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
antiguru
added a commit
that referenced
this pull request
Oct 19, 2026
…9903) #39061 made the blanket `LazyBinaryFunc` impl evaluate both arguments into an array of results. That made `case_literal/if_chain/*` 11-13% slower, which nightly cargo-bench flags. A named iterator that evaluates each argument on demand restores most of the previous speed and keeps #39061's code-size reduction (`cargo llvm-lines -p mz-expr --lib`: 1,933,557 lines vs. 1,934,453 on main). Local A/B against ae3cc46, #39061's parent (median of 3, pinned core): `if_chain/64_arms/last` 4741 ns before #39061, 5319 ns on main, 4816 ns with this PR. Most of the family lands within 2.3% of the parent. `4_arms/last` is the exception (306 / 340 / 333 ns), and it reproduced in two runs. Every binary function's input is a 2-tuple whose `try_from_iter` pulls both arguments before it checks for errors and NULLs, so evaluation semantics are unchanged. A new case in `error_semantics.slt` pins that an eval error wins over a NULL in either argument position. Closes: [CPU-315](https://linear.app/materializeinc/issue/CPU-315) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The blanket
LazyBinaryFuncimpl forEagerBinaryFuncfedInputDatumType::try_from_iteran iterator built from a closure, which is a new type for every function and everyEvalimplementor. Evaluating both arguments into an array gives every call site one iterator type, so argument unwrapping is instantiated once per input type. Hand-writtenEagerBinaryFuncimpls get this too.cargo llvm-lines -p mz-exprdrops from 1,786,549 to 1,703,578 lines, 4.6% fewer. Results don't change for two arguments, the only count planning produces, and the macro snapshots are untouched.Based on #38941.
🤖 Generated with Claude Code