-
Notifications
You must be signed in to change notification settings - Fork 95
feat: add lambda expression support #710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
benbellick
merged 32 commits into
substrait-io:main
from
limameml:limame.malainine/add-lambda-support
Mar 18, 2026
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
5ca6ff8
feat: add lambda expression support
limameml ed61127
Merge branch 'main' into limame.malainine/add-lambda-support
limameml a90c609
adresse some of @benbellick's comments
limameml 4053357
tweak: encapsulate LambdaParameterStack logic in a class
limameml ed5de56
Merge branch 'main' into limame.malainine/add-lambda-support
limameml eb7b2b2
tweak: adding comments expailining LambdaParameterStack
limameml 60b66be
Merge branch 'main' into limame.malainine/add-lambda-support
vbarua 1d1b26e
build: ignore build related generated files
vbarua 2224c4b
feat: enable parsing of func types in extensions
vbarua 83b9e24
test: copy substrait-go lambda plans
vbarua d802c19
test: add LambdaRoundtripTests
vbarua 14738e3
feat(isthmus): add TRANSFORM SqlFunction to handle transform:list_func
vbarua 2b5436c
Merge pull request #1 from substrait-io/vbarua/lambda-testing
limameml 6fae010
tweak: add filter in the function mapping
limameml 1c1f8d2
adress @vbarua's comments
limameml bd30a21
feat(core): add LambdaBuilder for build-time validation of lambda par…
benbellick 50db699
refactor: unify lambda validation, add JSON-based roundtrip tests
benbellick f172704
docs: fix LambdaBuilder javadoc to use params/outer/inner naming
benbellick 67c5a8a
refactor: clarify Scope internals, extract stepsOut() method and docu…
benbellick eed9ea9
test: add test verifying stepsOut changes dynamically with nesting depth
benbellick c37d527
test: simplify arithmetic body test to single lambda (x -> x + x)
benbellick d557629
fix: remove unused local variables flagged by PMD
benbellick 0dd2114
Merge pull request #2 from substrait-io/benbellick/proposed-lambda-va…
limameml d158f07
Merge branch 'main' into limame.malainine/add-lambda-support
limameml 5c2c99c
fix: remove uri mentions left in substrait plans and add all_match an…
limameml 8b81dbb
adressing some of @benbellick's comments
limameml aa659c5
refactor: reorder newLambdaParameterReference parameters for readability
benbellick e3e9f48
docs: add javadoc to newLambdaParameterReference explaining validation
benbellick b35ad63
refactor: make newLambdaParameterReference package-private
benbellick 7dc36b9
refactor: simplify newLambdaParameterReference to take Type directly
benbellick c88459d
test: add lambdaWithFunctionCall test to LambdaBuilderTest
benbellick d34bb58
test: add invalid proto test for out-of-bounds param index
benbellick File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,3 +14,6 @@ out/** | |
| .metals | ||
| .bloop | ||
| .project | ||
| .classpath | ||
| .settings | ||
| bin/ | ||
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
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
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
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
167 changes: 167 additions & 0 deletions
167
core/src/main/java/io/substrait/expression/LambdaBuilder.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| package io.substrait.expression; | ||
|
|
||
| import io.substrait.type.Type; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.function.Function; | ||
|
|
||
| /** | ||
| * Builds lambda expressions with build-time validation of parameter references. | ||
| * | ||
| * <p>Maintains a stack of lambda parameter scopes. Each call to {@link #lambda} pushes parameters | ||
| * onto the stack, builds the body via a callback, and pops. Nested lambdas simply call {@code | ||
| * lambda()} again on the same builder. | ||
| * | ||
| * <p>The callback receives a {@link Scope} handle for creating validated parameter references. The | ||
| * correct {@code stepsOut} value is computed automatically from the stack. | ||
| * | ||
| * <pre>{@code | ||
| * LambdaBuilder lb = new LambdaBuilder(); | ||
| * | ||
| * // Simple: (x: i32) -> x | ||
| * Expression.Lambda simple = lb.lambda(List.of(R.I32), params -> params.ref(0)); | ||
| * | ||
| * // Nested: (x: i32) -> (y: i64) -> add(x, y) | ||
| * Expression.Lambda nested = lb.lambda(List.of(R.I32), outer -> | ||
| * lb.lambda(List.of(R.I64), inner -> | ||
| * add(outer.ref(0), inner.ref(0)) | ||
| * ) | ||
| * ); | ||
| * }</pre> | ||
| */ | ||
| public class LambdaBuilder { | ||
| private final List<Type.Struct> lambdaContext = new ArrayList<>(); | ||
|
|
||
| /** | ||
| * Builds a lambda expression. The body function receives a {@link Scope} for creating validated | ||
| * parameter references. Nested lambdas are built by calling this method again inside the | ||
| * callback. | ||
| * | ||
| * @param paramTypes the lambda's parameter types | ||
| * @param bodyFn function that builds the lambda body given a scope handle | ||
| * @return the constructed lambda expression | ||
| */ | ||
| public Expression.Lambda lambda(List<Type> paramTypes, Function<Scope, Expression> bodyFn) { | ||
| Type.Struct params = Type.Struct.builder().nullable(false).addAllFields(paramTypes).build(); | ||
| pushLambdaContext(params); | ||
| try { | ||
| Scope scope = new Scope(params); | ||
| Expression body = bodyFn.apply(scope); | ||
| return ImmutableExpression.Lambda.builder().parameters(params).body(body).build(); | ||
| } finally { | ||
| popLambdaContext(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Builds a lambda expression from a pre-built parameter struct. Used by internal converters that | ||
| * already have a Type.Struct (e.g., during protobuf deserialization). | ||
| * | ||
| * @param params the lambda's parameter struct | ||
| * @param bodyFn function that builds the lambda body | ||
| * @return the constructed lambda expression | ||
| */ | ||
| public Expression.Lambda lambdaFromStruct( | ||
| Type.Struct params, java.util.function.Supplier<Expression> bodyFn) { | ||
| pushLambdaContext(params); | ||
| try { | ||
| Expression body = bodyFn.get(); | ||
| return ImmutableExpression.Lambda.builder().parameters(params).body(body).build(); | ||
| } finally { | ||
| popLambdaContext(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the parameter struct for a lambda at the given stepsOut from the current innermost | ||
| * scope. Used by internal converters to validate lambda parameter references during | ||
| * deserialization. | ||
| * | ||
| * @param stepsOut number of lambda scopes to traverse outward (0 = current/innermost) | ||
| * @return the parameter struct at the target scope level | ||
| * @throws IllegalArgumentException if stepsOut exceeds the current nesting depth | ||
| */ | ||
| public Type.Struct resolveParams(int stepsOut) { | ||
| int targetDepth = lambdaContext.size() - stepsOut; | ||
| if (targetDepth <= 0 || targetDepth > lambdaContext.size()) { | ||
| throw new IllegalArgumentException( | ||
| String.format( | ||
| "Lambda parameter reference with stepsOut=%d is invalid (current depth: %d)", | ||
| stepsOut, lambdaContext.size())); | ||
| } | ||
| return lambdaContext.get(targetDepth - 1); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a validated field reference to a lambda parameter. Validates that stepsOut is valid for | ||
| * the current lambda nesting context. | ||
| * | ||
| * @param stepsOut number of lambda scopes to traverse outward (0 = current/innermost) | ||
| * @param paramIndex index of the parameter within the target lambda's parameter struct | ||
| * @return a field reference to the specified lambda parameter | ||
| * @throws IllegalArgumentException if stepsOut exceeds the current nesting depth | ||
| * @throws IndexOutOfBoundsException if paramIndex is out of bounds for the target lambda | ||
| */ | ||
| public FieldReference newParameterReference(int stepsOut, int paramIndex) { | ||
| Type.Struct params = resolveParams(stepsOut); | ||
| Type type = params.fields().get(paramIndex); | ||
| return FieldReference.newLambdaParameterReference(stepsOut, paramIndex, type); | ||
| } | ||
|
|
||
| /** | ||
| * Pushes a lambda's parameters onto the context stack. This makes the parameters available for | ||
| * validation when building the lambda's body, and allows nested lambda parameter references to | ||
| * correctly compute their stepsOut values. | ||
| */ | ||
| private void pushLambdaContext(Type.Struct params) { | ||
| lambdaContext.add(params); | ||
| } | ||
|
|
||
| /** | ||
| * Pops the most recently pushed lambda parameters from the context stack. Called after a lambda's | ||
| * body has been built, restoring the context to the enclosing lambda's scope. | ||
| */ | ||
| private void popLambdaContext() { | ||
| lambdaContext.remove(lambdaContext.size() - 1); | ||
| } | ||
|
|
||
| /** | ||
| * A handle to a particular lambda's parameter scope. Use {@link #ref} to create validated | ||
| * parameter references. | ||
| * | ||
| * <p>Each Scope captures the depth of the lambdaContext stack at the time it was created. When | ||
| * {@link #ref} is called, the Substrait {@code stepsOut} value is computed as the difference | ||
| * between the current stack depth and the captured depth. This means the same Scope produces | ||
| * different stepsOut values depending on the nesting level at the time of the call, which is what | ||
| * allows outer.ref(0) to produce stepsOut=1 when called inside a nested lambda. | ||
| */ | ||
| public class Scope { | ||
| private final Type.Struct params; | ||
| private final int depth; | ||
|
|
||
| private Scope(Type.Struct params) { | ||
| this.params = params; | ||
| this.depth = lambdaContext.size(); | ||
| } | ||
|
|
||
| /** | ||
| * Computes the number of lambda boundaries between this scope and the current innermost scope. | ||
| * This value changes dynamically as nested lambdas are built. | ||
| */ | ||
| private int stepsOut() { | ||
| return lambdaContext.size() - depth; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a validated reference to a parameter of this lambda. | ||
| * | ||
| * @param paramIndex index of the parameter within this lambda's parameter struct | ||
| * @return a {@link FieldReference} pointing to the specified parameter | ||
| * @throws IndexOutOfBoundsException if paramIndex is out of bounds | ||
| */ | ||
| public FieldReference ref(int paramIndex) throws IndexOutOfBoundsException { | ||
| Type type = params.fields().get(paramIndex); | ||
| return FieldReference.newLambdaParameterReference(stepsOut(), paramIndex, type); | ||
| } | ||
| } | ||
| } |
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.