Skip to content

Commit 286da95

Browse files
authored
Pipeline captures (#224)
1 parent f498b27 commit 286da95

23 files changed

Lines changed: 457 additions & 125 deletions

docs/src/app/docs/piping/page.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ Why would you use it?
1515

1616
Consider a chain of operations like this:
1717

18-
```apex
18+
```
1919
WHERE(WHERE([1, 2, 3, 4, 5, 6], $current > 2), $current < 5)
2020
```
2121

2222
This is slightly hard to read, and it's easy to get lost in the parenthesis. With piping, you can
2323
write it like this:
2424

25-
```apex
25+
```
2626
[1, 2, 3, 4, 5, 6]
2727
-> WHERE($current > 2)
2828
-> WHERE($current < 5)
@@ -35,8 +35,29 @@ Evaluator.run('MAP(WHERE(WHERE(ChildAccounts, NumberOfEmployees > 10), AnnualRev
3535
3636
Evaluator.run(
3737
'ChildAccounts ' +
38-
'-> WHERE(AnnualRevenue > 200) ' +
39-
'-> WHERE(NumberOfEmployees > 10) ' +
40-
'-> MAP(Name)',
38+
'-> WHERE(AnnualRevenue > 200) ' +
39+
'-> WHERE(NumberOfEmployees > 10) ' +
40+
'-> MAP(Name)',
4141
recordId);
4242
```
43+
44+
## Capturing Piped Values
45+
46+
By default, piped values are passed as the first argument to the next encountered function. However, you can explicitly
47+
"capture" the piped value, allowing you to use it in any position within the expression to the right-hand
48+
side of the pipe operator.
49+
50+
To capture the piped value, use the `_` keyword. For example:
51+
52+
```
53+
[1, 2, 3, 4, 5, 6]
54+
-> WHERE(_, $current > 2) # Here, _ represents the piped value
55+
```
56+
57+
Explicitly capturing the value also allows you to use the same value multiple times:
58+
59+
```
60+
RANGE(1, 10)
61+
-> WHERE(_, $current > 1)
62+
-> SUM(_) + SIZE(_)
63+
```
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3-
<apiVersion>60.0</apiVersion>
3+
<apiVersion>65.0</apiVersion>
44
<status>Active</status>
5-
</ApexClass>
5+
</ApexClass>

expression-src/main/src/interpreter/ContextResolver.cls

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,16 @@ public with sharing class ContextResolver implements Visitor {
437437
return null;
438438
}
439439

440+
public Object visit(Expr.PipelineFunction pipelineFunction) {
441+
resolve(pipelineFunction.left);
442+
resolve(pipelineFunction.body);
443+
return null;
444+
}
445+
446+
public Object visit(Expr.Capture capture) {
447+
return null;
448+
}
449+
440450
private final Map<SObjectType, Fields> cachedFieldsBySObjectType = new Map<SObjectType, Fields>();
441451
private Fields getFields() {
442452
if (cachedFieldsBySObjectType.containsKey(this.queryContext.objectType)) {

expression-src/main/src/interpreter/Exceptions.cls

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,18 @@ public with sharing class Exceptions {
5454
return 'Runtime';
5555
}
5656
}
57+
58+
public class UnknownException extends PositionAwareException {
59+
public UnknownException(SourcePosition position, String message) {
60+
super(position, message);
61+
}
62+
63+
public override String toString() {
64+
return super.toString();
65+
}
66+
67+
public override String getType() {
68+
return 'Unknown';
69+
}
70+
}
5771
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3-
<apiVersion>60.0</apiVersion>
3+
<apiVersion>65.0</apiVersion>
44
<status>Active</status>
55
</ApexClass>

expression-src/main/src/interpreter/Expr.cls

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,4 +545,69 @@ public abstract class Expr {
545545
return result;
546546
}
547547
}
548+
549+
public class PipelineFunction extends Expr {
550+
public final Token pipeToken;
551+
public final Expr left;
552+
public final Expr body;
553+
public Boolean hasExplicitCapture = false;
554+
555+
public PipelineFunction(Token pipeToken, Expr left, Expr body) {
556+
this.pipeToken = pipeToken;
557+
this.left = left;
558+
this.body = body;
559+
}
560+
561+
public override Object accept(Visitor v) {
562+
return v.visit(this);
563+
}
564+
565+
public PipelineFunction withInnerCapture(Boolean hasInnerCapture) {
566+
this.hasExplicitCapture = hasInnerCapture;
567+
return this;
568+
}
569+
570+
public Boolean equals(Object other) {
571+
if (other instanceof PipelineFunction) {
572+
PipelineFunction otherPipelineFunction = (PipelineFunction)other;
573+
return this.pipeToken == otherPipelineFunction.pipeToken
574+
&& this.left == otherPipelineFunction.left
575+
&& this.body == otherPipelineFunction.body
576+
&& this.hasExplicitCapture == otherPipelineFunction.hasExplicitCapture;
577+
}
578+
return false;
579+
}
580+
581+
public override Integer hashCode() {
582+
Integer result = System.hashCode(pipeToken);
583+
result = (31 * result) + System.hashCode(left);
584+
result = (31 * result) + System.hashCode(body);
585+
result = (31 * result) + System.hashCode(hasExplicitCapture);
586+
return result;
587+
}
588+
}
589+
590+
public class Capture extends Expr {
591+
public final Token captureToken;
592+
593+
public Capture(Token captureToken) {
594+
this.captureToken = captureToken;
595+
}
596+
597+
public override Object accept(Visitor v) {
598+
return v.visit(this);
599+
}
600+
601+
public Boolean equals(Object other) {
602+
if (other instanceof Capture) {
603+
Capture otherCapture = (Capture)other;
604+
return this.captureToken == otherCapture.captureToken;
605+
}
606+
return false;
607+
}
608+
609+
public override Integer hashCode() {
610+
return System.hashCode(captureToken);
611+
}
612+
}
548613
}

expression-src/main/src/interpreter/Interpreter.cls

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,28 @@ public virtual with sharing class Interpreter implements Visitor {
357357
}
358358

359359
public virtual Object visit(Expr.FunctionCall function) {
360+
attemptCapture(function);
360361
return FunctionCaller.exec(this, function);
361362
}
362363

364+
protected void attemptCapture(Expr.FunctionCall function) {
365+
if (pendingCapture == null || pendingCapture.isExplicit == true) {
366+
return;
367+
}
368+
369+
// If there is a pipeline capture pending, then the first function
370+
// call we encounter should use that as its first argument.
371+
if (function.arguments.isEmpty()) {
372+
function.arguments.add(pendingCapture.expression);
373+
} else {
374+
function.arguments.add(0, pendingCapture.expression);
375+
}
376+
377+
// Once captured, we clear the pending capture to avoid any additional
378+
// functions to the right-hand side of the pipeline to try and use it.
379+
pendingCapture = null;
380+
}
381+
363382
public Object visit(Expr.ListLiteral listLiteral) {
364383
inListLiteralCounter++;
365384
List<Object> resultList = new List<Object>();
@@ -567,4 +586,66 @@ public virtual with sharing class Interpreter implements Visitor {
567586

568587
return new ShouldNotAdd();
569588
}
589+
590+
private PendingCapture pendingCapture;
591+
public Object visit(Expr.PipelineFunction pipelineFunction) {
592+
if (pipelineFunction.hasExplicitCapture) {
593+
// When dealing with an explicit capture, we immediately evaluate
594+
// the left-hand side of the pipeline, and store it for it to be used
595+
// whenever a capture ("_") expression is encountered within the right-hand side (body).
596+
this.pendingCapture = explicit(evaluate(pipelineFunction.left));
597+
} else {
598+
// If there is no capture on the right-hand side of the pipeline,
599+
// then it acts as syntactic sugar for passing the left-hand side
600+
// as the first argument to the function call on the right-hand side.
601+
// Thus, it does not get evaluated, but rather stored as an expression
602+
// for it to be evaluated during function call argument resolution.
603+
this.pendingCapture = implicit(pipelineFunction.left);
604+
}
605+
606+
Object toReturn = evaluate(pipelineFunction.body);
607+
return toReturn;
608+
}
609+
610+
public Object visit(Expr.Capture capture) {
611+
if (this.pendingCapture == null) {
612+
throw new Exceptions.RuntimeException(
613+
capture.captureToken,
614+
'Capture expressions can only be used inside pipeline functions.'
615+
);
616+
}
617+
618+
if (pendingCapture.isExplicit == false) {
619+
// This should be unreachable, since the PipelineFunction visitor
620+
// should have populated the pending capture as explicit.
621+
throw new Exceptions.UnknownException(
622+
capture.captureToken.position,
623+
'Internal error: expected an explicit pending capture.'
624+
);
625+
}
626+
627+
return this.pendingCapture.result;
628+
}
629+
630+
private class PendingCapture {
631+
private Boolean isExplicit;
632+
// Populated when isExplicit is false
633+
private Expr expression;
634+
// Populated when isExplicit is true
635+
private Object result;
636+
}
637+
638+
private static PendingCapture explicit(Object result) {
639+
PendingCapture capture = new PendingCapture();
640+
capture.isExplicit = true;
641+
capture.result = result;
642+
return capture;
643+
}
644+
645+
private static PendingCapture implicit(Expr expression) {
646+
PendingCapture capture = new PendingCapture();
647+
capture.isExplicit = false;
648+
capture.expression = expression;
649+
return capture;
650+
}
570651
}

expression-src/main/src/interpreter/Parser.cls

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,21 @@ public with sharing class Parser {
7070
consume(TokenType.SEMICOLON, 'Expect \';\' after function body.');
7171
return new Expr.FunctionDeclaration(name, parameters, body, skipCache);
7272
}
73-
return orCheck();
73+
return pipe();
74+
}
75+
76+
private Expr pipe() {
77+
Expr expr = orCheck();
78+
79+
while (match(new List<TokenType> {
80+
TokenType.ARROW
81+
})) {
82+
Token operator = previous();
83+
Expr right = orCheck();
84+
expr = new Expr.Binary(expr, operator, right);
85+
}
86+
87+
return expr;
7488
}
7589

7690
private Expr orCheck() {
@@ -205,7 +219,7 @@ public with sharing class Parser {
205219
}
206220

207221
private Expr getExpr() {
208-
Expr expr = pipe();
222+
Expr expr = primary();
209223

210224
while (true) {
211225
if (match(TokenType.DOT)) {
@@ -224,20 +238,6 @@ public with sharing class Parser {
224238
return expr;
225239
}
226240

227-
private Expr pipe() {
228-
Expr expr = primary();
229-
230-
while (match(new List<TokenType> {
231-
TokenType.ARROW
232-
})) {
233-
Token operator = previous();
234-
Expr right = primary();
235-
expr = new Expr.Binary(expr, operator, right);
236-
}
237-
238-
return expr;
239-
}
240-
241241
private Expr primary() {
242242
if (match(TokenType.FALSE_KEYWORD)) {
243243
return new Expr.Literal(false);
@@ -255,6 +255,10 @@ public with sharing class Parser {
255255
return parseQuery();
256256
}
257257

258+
if (match(TokenType.CAPTURE)) {
259+
return new Expr.Capture(previous());
260+
}
261+
258262
if (match(TokenType.STRING_LITERAL_START)) {
259263
// Grab all characters until we reach the STRING_LITERAL_END
260264
List<Object> strings = new List<Object>();
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3-
<apiVersion>60.0</apiVersion>
3+
<apiVersion>65.0</apiVersion>
44
<status>Active</status>
55
</ApexClass>

0 commit comments

Comments
 (0)