Add Variable expressions - #9701
Conversation
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
Merging this PR will regress 2 benchmarks
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | WallTime | arrow_checked_add_u32_neon[16384] |
13.4 µs | 20.3 µs | -34.26% |
| ❌ | WallTime | mul_u32_nonnull_avx512 |
5.6 µs | 6.3 µs | -10.98% |
| ⚡ | WallTime | subtract_shapes_neon[(128, PerRowPerRow)] |
3.3 µs | 1.9 µs | +73.42% |
| ⚡ | WallTime | add_shapes_neon[(128, PerRowPerRow)] |
3.2 µs | 1.9 µs | +70.86% |
| ⚡ | WallTime | multiply_shapes_neon[(128, PerRowPerRow)] |
3.2 µs | 2 µs | +64.94% |
| ⚡ | WallTime | arrow_checked_add_u32_avx512[16384] |
21.4 µs | 17.6 µs | +21.48% |
| ⚡ | WallTime | add_u32_nonnull_neon |
7.7 µs | 6.5 µs | +18.3% |
| ⚡ | WallTime | add_shapes_neon[(16384, PerRowPerRow)] |
11.1 µs | 9.7 µs | +14.82% |
| ⚡ | WallTime | subtract_shapes_neon[(16384, PerRowPerRow)] |
11 µs | 9.7 µs | +13.8% |
| ⚡ | WallTime | words_gather_scalar_avx2[65536] |
9.4 µs | 8.3 µs | +13.3% |
| ⚡ | WallTime | mul_i8_nonnull_neon |
12.3 µs | 11 µs | +12.46% |
| ⚡ | WallTime | add_i64_nullable_neon |
12.6 µs | 11.3 µs | +11.07% |
| ⚡ | WallTime | add_i32_nonnull_neon |
8.7 µs | 7.9 µs | +11.04% |
| ⚡ | WallTime | mul_i16_nonnull_neon |
11.4 µs | 10.3 µs | +10.93% |
| ⚡ | WallTime | add_constant_shapes_neon[(16384, ConstantPerRow)] |
10.3 µs | 9.3 µs | +10.79% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing mk/variable-expressions (91f7aa1) with develop (47fd3e8)
Footnotes
-
206 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
-
4 benchmarks were run, but are now archived. If they were deleted in another branch, consider rebasing to remove them from the report. Instead if they were added back, click here to restore them. ↩
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
Summary
This PR adds variables as a first-class
ExpressionandBoundExpressionnode, and introduces machinery to resolve them against a lexical scope during binding. It is the variable-only foundation for lambdas and higher-order functions.Variables
A
Variableconsists only of a name. It participates explicitly in expression traversal, optimization, validity, and analysis as a leaf, similarly toroot.Scope and name resolution
Scopepreviously represented only the dtype ofroot(). It now consists of:Frames.A frame contains an ordered set of
(Variable, DType)bindings introduced by one binder. Duplicate names in the same frame are rejected, while an inner frame may shadow a name from an outer frame.Scope::push_framereturns an extended scope. Resolution searches from the innermost frame outward and returns both the declared dtype and a stableVariableRef { frame, slot }. Frame indices are counted from the outermost frame and slots follow declaration order, so pushing a new inner frame does not invalidate references to captured variables in outer frames.For example, a future higher-order function could bind a lambda parameter by extending its surrounding scope:
Binding
Expression::bind_scopenow handles all three expression node kinds:Rootbecomes a bound root carryingscope.root().Variableis resolved through the scope and becomes aBoundVariablecarrying its source name, declared dtype, and stableVariableRef.Scalarrecursively binds its children and derives its result dtype from their bound dtypes.Binding fails if a variable has no enclosing binder.
Expression::bind(&root_dtype)remains useful for expressions that only depend on the root, but a variable-containing expression must usebind_scopewith the appropriate frames.Every node in the resulting
BoundExpressionhas a known dtype. In particular,BoundExpression::Variable::dtype()comes from its resolved scope binding rather than from the expression node itself.Intentional restrictions
Expression::return_dtype(root_dtype)returns an error for variables. A root dtype alone cannot determine the dtype of a lexical variable; callers must bind against a fullScope.ArrayRef::applyrejects unbound variables.ArrayRef::apply_boundalso rejects bound variables because the current evaluator only receives the root array; it has no runtime environment containing values forVariableRefs yet.