Skip to content

Commit 301dad8

Browse files
authored
Merge pull request #57 from hasithaa/fix_8840
Enhance await functionality with tuple-binding and union LHS support
2 parents d547bb5 + 7d5b462 commit 301dad8

10 files changed

Lines changed: 582 additions & 14 deletions

File tree

ballerina/context.bal

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,25 +108,32 @@ public client class Context {
108108
return getWorkflowTypeNative(self.nativeContext);
109109
}
110110

111-
# Waits for at least `minCount` data futures to complete. Results are positional tuples
111+
# Waits for at least `minCount` data futures to complete. Results are a positional tuple
112112
# aligned to input order. Use nullable types (`T?`) for partial waits.
113113
#
114+
# The result can be captured in several ways:
115+
#
114116
# ```ballerina
115-
# // Wait for all
116-
# [Approval, Payment] result = check ctx->await([events.approval, events.payment]);
117-
# // Wait for any (1 of 2)
117+
# // Wait for all (tuple binding pattern)
118+
# [Approval, Payment] [approval, payment] = check ctx->await([events.approval, events.payment]);
119+
# // Capture the whole result, including a possible timeout, without `check`
120+
# [Approval, Payment]|error result = ctx->await([events.approval, events.payment]);
121+
# if result is error { /* handle timeout */ }
122+
# // Handle each position independently (a slot is a value or an error)
123+
# [Approval|error, Payment|error] [a, p] = check ctx->await([events.approval, events.payment]);
124+
# // Wait for any (1 of 2) — use nilable members for partial waits
118125
# [Approval?, Payment?] result = check ctx->await([events.approval, events.payment], minCount = 1);
119126
# ```
120127
#
121128
# + futures - Data futures from the workflow's events record
122129
# + minCount - Minimum completions required (default: all)
123-
# + timeout - Maximum wait duration; returns error on timeout
124-
# + T - Expected return type (inferred from context)
125-
# + return - Positional tuple of values (or nil for incomplete), or an error
130+
# + timeout - Maximum wait duration; returns an error on timeout
131+
# + T - Expected return type, inferred from how the result is assigned
132+
# + return - Positional tuple of values (`nil` for an incomplete position), or an error
126133
remote isolated function await(future<anydata>[] futures,
127134
int:Unsigned32 minCount = <int:Unsigned32>futures.length(),
128135
time:Duration? timeout = (),
129-
typedesc<anydata> T = <>) returns T|error = @java:Method {
136+
typedesc<anydata|error|(anydata|error)[]> T = <>) returns T = @java:Method {
130137
'class: "io.ballerina.lib.workflow.runtime.nativeimpl.WaitUtils",
131138
name: "awaitFutures"
132139
} external;

changelog.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- [#8840](https://github.com/ballerina-platform/ballerina-library/issues/8840) -
12+
Widened the `ctx->await()` dependent type parameter to
13+
`typedesc<anydata|error|(anydata|error)[]>` (returning `T`). The result can now be
14+
destructured directly with a tuple-binding pattern
15+
(`[Approval, Payment] [a, p] = check ctx->await(...)`), captured as `[T1, T2, ...]|error`
16+
without a forced `check`, and use per-position error types (`[T1|error, T2|error]`). The
17+
compiler plugin validates that each tuple position matches the corresponding future's type.
18+
19+
### Fixed
20+
21+
- [Fix#8820](https://github.com/ballerina-platform/ballerina-library/issues/8820) -
22+
`workflow:sendData()` now supports all persistable `anydata` payloads — primitive types
23+
(`boolean`, `int`, `float`, `decimal`, `string`), `json`, `xml`, and `table` — not only
24+
records. Previously a non-record payload was delivered as an empty `map<anydata>`, causing a
25+
`{ballerina}ConversionError`. Added the `WORKFLOW_129` compiler diagnostic to enforce that
26+
each `future<T>` field in a workflow's events record has a `T` that is a subtype of `anydata`.
27+
928
## [0.5.0] - 2026-06-18
1029

1130
### Added

compiler-plugin-tests/src/test/java/io/ballerina/lib/workflow/compiler/WorkflowCompilerPluginTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,31 @@ public void testValidAwaitTyped() {
291291
+ getDiagnosticMessages(diagnosticResult));
292292
}
293293

294+
@Test(groups = "valid")
295+
public void testValidAwaitUnionAndBinding() {
296+
// ctx->await uses `typedesc<anydata|error|(anydata|error)[]> T = <>` returning `T`, so the result
297+
// can be captured as `[..]|error` without a forced `check`, destructured via a tuple-binding
298+
// pattern, and use per-position error members (`[A|error, B|error]`).
299+
String packagePath = "valid_await_union_and_binding";
300+
DiagnosticResult diagnosticResult = getValidationDiagnosticResult(packagePath);
301+
Assert.assertEquals(diagnosticResult.errorCount(), 0,
302+
"Expected no errors for ctx->await union-LHS, tuple-binding and per-position-error patterns. "
303+
+ "Errors: " + getDiagnosticMessages(diagnosticResult));
304+
}
305+
306+
@Test(groups = "invalid")
307+
public void testInvalidAwaitPerPositionErrorMismatch() {
308+
// The widened typedesc constraint admits per-position error tuples, so the compiler plugin must
309+
// still validate that each position matches its future's inner type (WORKFLOW_117).
310+
String packagePath = "invalid_await_per_position_error_mismatch";
311+
DiagnosticResult diagnosticResult = getValidationDiagnosticResult(packagePath);
312+
// The fixture swaps both tuple members, so both positions must be validated.
313+
List<Diagnostic> diags = getDiagnosticsWithCode(diagnosticResult, "WORKFLOW_117");
314+
Assert.assertEquals(diags.size(), 2, "Expected 2 WORKFLOW_117 errors (both positions swapped)");
315+
assertMessageContains(diags.get(0), "position 0");
316+
assertMessageContains(diags.get(1), "position 1");
317+
}
318+
294319
@Test(groups = "valid")
295320
public void testValidAwaitWithTimeout() {
296321
String packagePath = "valid_await_with_timeout";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
org = "test_samples"
3+
name = "invalid_await_per_position_error_mismatch"
4+
version = "0.1.0"
5+
distribution = "2201.13.0"
6+
7+
[build-options]
8+
observabilityIncluded = true
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved.
2+
//
3+
// WSO2 LLC. licenses this file to you under the Apache License,
4+
// Version 2.0 (the "License"); you may not use this file except
5+
// in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing,
11+
// software distributed under the License is distributed on an
12+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13+
// KIND, either express or implied. See the License for the
14+
// specific language governing permissions and limitations
15+
// under the License.
16+
17+
import ballerina/workflow;
18+
19+
type Input record {|
20+
string id;
21+
|};
22+
23+
type ApprovalDecision record {|
24+
boolean approved;
25+
string approverId;
26+
|};
27+
28+
type ComplianceDecision record {|
29+
boolean compliant;
30+
string reviewerId;
31+
|};
32+
33+
// WORKFLOW_117: per-position error tuple with swapped types. The widened typedesc constraint
34+
// (anydata|error|(anydata|error)[]) admits the shape, so the compiler plugin must catch that
35+
// position 0 is declared 'ComplianceDecision|error' but the future carries 'ApprovalDecision'.
36+
@workflow:Workflow
37+
function swappedPerPositionErrorWorkflow(
38+
workflow:Context ctx,
39+
Input input,
40+
record {|
41+
future<ApprovalDecision> approval;
42+
future<ComplianceDecision> compliance;
43+
|} events
44+
) returns string|error {
45+
[ComplianceDecision|error, ApprovalDecision|error] [comp, app] =
46+
check ctx->await([events.approval, events.compliance]);
47+
return "done";
48+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
org = "test_samples"
3+
name = "valid_await_union_and_binding"
4+
version = "0.1.0"
5+
distribution = "2201.13.0"
6+
7+
[build-options]
8+
observabilityIncluded = true
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved.
2+
//
3+
// WSO2 LLC. licenses this file to you under the Apache License,
4+
// Version 2.0 (the "License"); you may not use this file except
5+
// in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing,
11+
// software distributed under the License is distributed on an
12+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13+
// KIND, either express or implied. See the License for the
14+
// specific language governing permissions and limitations
15+
// under the License.
16+
17+
import ballerina/workflow;
18+
19+
type Input record {| string id; |};
20+
21+
type ApprovalDecision record {| boolean approved; string approverId; |};
22+
23+
type ComplianceDecision record {| boolean compliant; string reviewerId; |};
24+
25+
// Valid: full wait captured WITHOUT `check` as a `T|error` union (no forced check).
26+
// Enabled by the dependent type parameter `typedesc<anydata|error>`.
27+
@workflow:Workflow
28+
function awaitUnionNoCheck(
29+
workflow:Context ctx,
30+
Input input,
31+
record {|
32+
future<ApprovalDecision> approval;
33+
future<ComplianceDecision> compliance;
34+
|} events
35+
) returns string|error {
36+
[ApprovalDecision, ComplianceDecision]|error result = ctx->await([events.approval, events.compliance]);
37+
if result is error {
38+
return result;
39+
}
40+
[ApprovalDecision, ComplianceDecision] [appr, comp] = result;
41+
return appr.approved && comp.compliant ? "DONE" : "REJECTED";
42+
}
43+
44+
// Valid: full wait with a tuple-binding pattern + check (direct destructuring).
45+
@workflow:Workflow
46+
function awaitBindingPattern(
47+
workflow:Context ctx,
48+
Input input,
49+
record {|
50+
future<ApprovalDecision> approval;
51+
future<ComplianceDecision> compliance;
52+
|} events
53+
) returns string|error {
54+
[ApprovalDecision, ComplianceDecision] [appr, comp] =
55+
check ctx->await([events.approval, events.compliance]);
56+
return appr.approverId + ":" + comp.reviewerId;
57+
}
58+
59+
// Valid: per-position error tuple — each slot is a value or an error. Enabled by the
60+
// widened constraint `typedesc<anydata|error|(anydata|error)[]>` and validated per position
61+
// by the compiler plugin.
62+
@workflow:Workflow
63+
function awaitPerPositionError(
64+
workflow:Context ctx,
65+
Input input,
66+
record {|
67+
future<ApprovalDecision> approval;
68+
future<ComplianceDecision> compliance;
69+
|} events
70+
) returns string|error {
71+
[ApprovalDecision|error, ComplianceDecision|error] [appr, comp] =
72+
check ctx->await([events.approval, events.compliance]);
73+
if appr is error {
74+
return appr;
75+
}
76+
if comp is error {
77+
return comp;
78+
}
79+
return appr.approved && comp.compliant ? "DONE" : "REJECTED";
80+
}
81+
82+
// Valid: single wait.
83+
@workflow:Workflow
84+
function awaitSingle(
85+
workflow:Context ctx,
86+
Input input,
87+
record {|
88+
future<ApprovalDecision> approval;
89+
|} events
90+
) returns string|error {
91+
[ApprovalDecision] [appr] = check ctx->await([events.approval]);
92+
return appr.approverId;
93+
}
94+
95+
// Valid: partial (alternate) wait, nilable members, binding pattern.
96+
@workflow:Workflow
97+
function awaitPartial(
98+
workflow:Context ctx,
99+
Input input,
100+
record {|
101+
future<ApprovalDecision> approverA;
102+
future<ApprovalDecision> approverB;
103+
|} events
104+
) returns string|error {
105+
[ApprovalDecision?, ApprovalDecision?] [a, b] =
106+
check ctx->await([events.approverA, events.approverB], 1);
107+
if a is ApprovalDecision {
108+
return a.approverId;
109+
}
110+
if b is ApprovalDecision {
111+
return b.approverId;
112+
}
113+
return "NONE";
114+
}
115+
116+
// Valid: partial wait captured as a union without check.
117+
@workflow:Workflow
118+
function awaitPartialUnionNoCheck(
119+
workflow:Context ctx,
120+
Input input,
121+
record {|
122+
future<ApprovalDecision> approverA;
123+
future<ApprovalDecision> approverB;
124+
|} events
125+
) returns string|error {
126+
[ApprovalDecision?, ApprovalDecision?]|error result =
127+
ctx->await([events.approverA, events.approverB], 1, timeout = {seconds: 5});
128+
if result is error {
129+
return "TIMED_OUT";
130+
}
131+
return "OK";
132+
}

0 commit comments

Comments
 (0)