Skip to content

Commit 453e8aa

Browse files
authored
enhance WAITFOR statement handling (#998)
* feat(formatter): enhance WAITFOR statement handling and add related tests * feat(formatter): refine WAITFOR EVENT operand handling, adjust parentheses logic, and add new tests * feat(formatter): expand test coverage and refine parentheses handling around `IN` operator for WAITFOR EVENT expressions * test(formatter): reorder struct fields in WAITFOR EVENT test case for consistency
1 parent 9986cde commit 453e8aa

17 files changed

Lines changed: 2325 additions & 1543 deletions

pkg/compiler/internal/wait_event.go

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -365,32 +365,20 @@ func (c *WaitCompiler) emitWaitEventCleanupIfReady(state waitEventCompileState,
365365

366366
// CompileWaitForEventName processes the event name expression in a WAITFOR statement.
367367
func (c *WaitCompiler) CompileWaitForEventName(ctx fql.IWaitForEventNameContext) bytecode.Operand {
368-
sl := ctx.StringLiteral()
369-
v := ctx.Variable()
370-
p := ctx.Param()
371-
me := ctx.MemberExpression()
372-
fce := ctx.FunctionCall()
373-
374-
return compileFirstOperand(
375-
newOperandBranch(sl != nil, func() bytecode.Operand { return c.literals.CompileStringLiteral(sl) }),
376-
newOperandBranch(v != nil, func() bytecode.Operand { return c.exprs.CompileVariable(v) }),
377-
newOperandBranch(p != nil, func() bytecode.Operand { return c.exprs.CompileParam(p) }),
378-
newOperandBranch(me != nil, func() bytecode.Operand { return c.exprs.CompileMemberExpression(me) }),
379-
newOperandBranch(fce != nil, func() bytecode.Operand { return c.exprs.CompileFunctionCall(fce, false) }),
380-
)
368+
if ctx == nil {
369+
return bytecode.NoopOperand
370+
}
371+
372+
return c.exprs.Compile(ctx.Expression())
381373
}
382374

383375
// CompileWaitForEventSource processes the event source expression in a WAITFOR statement.
384376
func (c *WaitCompiler) CompileWaitForEventSource(ctx fql.IWaitForEventSourceContext) bytecode.Operand {
385-
v := ctx.Variable()
386-
me := ctx.MemberExpression()
387-
fce := ctx.FunctionCallExpression()
388-
389-
return compileFirstOperand(
390-
newOperandBranch(v != nil, func() bytecode.Operand { return c.exprs.CompileVariable(v) }),
391-
newOperandBranch(me != nil, func() bytecode.Operand { return c.exprs.CompileMemberExpression(me) }),
392-
newOperandBranch(fce != nil, func() bytecode.Operand { return c.exprs.CompileFunctionCallExpression(fce) }),
393-
)
377+
if ctx == nil {
378+
return bytecode.NoopOperand
379+
}
380+
381+
return c.exprs.Compile(ctx.Expression())
394382
}
395383

396384
// CompileOptionsClause processes the options clause in a WAITFOR statement.

pkg/formatter/formatter_parentheses_test.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@ package formatter
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
67
"strings"
78
"testing"
89

10+
"github.com/antlr4-go/antlr/v4"
11+
912
"github.com/MontFerret/ferret/v2/pkg/compiler"
13+
"github.com/MontFerret/ferret/v2/pkg/parser"
14+
"github.com/MontFerret/ferret/v2/pkg/parser/fql"
15+
"github.com/MontFerret/ferret/v2/pkg/runtime"
1016
"github.com/MontFerret/ferret/v2/pkg/source"
17+
"github.com/MontFerret/ferret/v2/pkg/vm"
1118
"github.com/MontFerret/ferret/v2/test/spec"
19+
"github.com/MontFerret/ferret/v2/test/spec/mock"
1220
)
1321

1422
func TestFormatterSimplifiesOnlyRedundantParentheses(t *testing.T) {
@@ -71,6 +79,257 @@ func TestFormatterSimplifiesOnlyRedundantParentheses(t *testing.T) {
7179
}
7280
}
7381

82+
func TestFormatterWaitForEventOperands(t *testing.T) {
83+
tests := []struct {
84+
name string
85+
input string
86+
want string
87+
wantName string
88+
wantSource string
89+
}{
90+
{
91+
name: "composed operands",
92+
input: `RETURN WAITFOR EVENT (@kind ?? "message") IN (@source ?? fallback)`,
93+
want: `return waitfor event @kind ?? "message" in @source ?? fallback`,
94+
wantName: `@kind??"message"`,
95+
wantSource: `@source??fallback`,
96+
},
97+
{
98+
name: "membership name removes redundant grouping",
99+
input: `RETURN WAITFOR EVENT (@kind IN @names) IN @source`,
100+
want: `return waitfor event @kind in @names in @source`,
101+
wantName: `@kindin@names`,
102+
wantSource: `@source`,
103+
},
104+
{
105+
name: "membership source retains delimiter boundary",
106+
input: `RETURN WAITFOR EVENT "message" IN (@candidate IN @sources)`,
107+
want: `return waitfor event "message" in (@candidate in @sources)`,
108+
wantName: `"message"`,
109+
wantSource: `(@candidatein@sources)`,
110+
},
111+
{
112+
name: "negated membership source retains delimiter boundary",
113+
input: `RETURN WAITFOR EVENT "message" IN (@candidate NOT IN @sources)`,
114+
want: `return waitfor event "message" in (@candidate not in @sources)`,
115+
wantName: `"message"`,
116+
wantSource: `(@candidatenotin@sources)`,
117+
},
118+
{
119+
name: "function argument membership is bounded",
120+
input: `RETURN WAITFOR EVENT "message" IN (SOURCE(@candidate IN @sources))`,
121+
want: `return waitfor event "message" in SOURCE(@candidate in @sources)`,
122+
wantName: `"message"`,
123+
wantSource: `SOURCE(@candidatein@sources)`,
124+
},
125+
{
126+
name: "array entry membership is bounded",
127+
input: `RETURN WAITFOR EVENT "message" IN ([@candidate IN @sources])`,
128+
want: `return waitfor event "message" in [@candidate in @sources]`,
129+
wantName: `"message"`,
130+
wantSource: `[@candidatein@sources]`,
131+
},
132+
{
133+
name: "object property membership is bounded",
134+
input: `RETURN WAITFOR EVENT "message" IN ({ candidate: @candidate IN @sources })`,
135+
want: `return waitfor event "message" in { candidate: @candidate in @sources }`,
136+
wantName: `"message"`,
137+
wantSource: `{candidate:@candidatein@sources}`,
138+
},
139+
{
140+
name: "ternary true branch membership is bounded",
141+
input: `RETURN WAITFOR EVENT "message" IN (TRUE ? @candidate IN @sources : FALSE)`,
142+
want: `return waitfor event "message" in true ? @candidate in @sources : false`,
143+
wantName: `"message"`,
144+
wantSource: `true?@candidatein@sources:false`,
145+
},
146+
{
147+
name: "precedence grouping bounds nested membership",
148+
input: `RETURN WAITFOR EVENT "message" IN ((@candidate IN @sources) + 1)`,
149+
want: `return waitfor event "message" in (@candidate in @sources) + 1`,
150+
wantName: `"message"`,
151+
wantSource: `(@candidatein@sources)+1`,
152+
},
153+
{
154+
name: "removable inner grouping leaves membership exposed",
155+
input: `RETURN WAITFOR EVENT "message" IN (NOT (@candidate IN @sources))`,
156+
want: `return waitfor event "message" in (not @candidate in @sources)`,
157+
wantName: `"message"`,
158+
wantSource: `(not@candidatein@sources)`,
159+
},
160+
{
161+
name: "ternary condition membership remains exposed",
162+
input: `RETURN WAITFOR EVENT "message" IN (@candidate IN @sources ? TRUE : FALSE)`,
163+
want: `return waitfor event "message" in (@candidate in @sources ? true : false)`,
164+
wantName: `"message"`,
165+
wantSource: `(@candidatein@sources?true:false)`,
166+
},
167+
{
168+
name: "ternary false branch membership remains exposed",
169+
input: `RETURN WAITFOR EVENT "message" IN (TRUE ? FALSE : @candidate IN @sources)`,
170+
want: `return waitfor event "message" in (true ? false : @candidate in @sources)`,
171+
wantName: `"message"`,
172+
wantSource: `(true?false:@candidatein@sources)`,
173+
},
174+
{
175+
name: "membership operands use distinct grouping",
176+
input: `RETURN WAITFOR EVENT (@kind IN @names) IN (@candidate IN @sources)`,
177+
want: `return waitfor event @kind in @names in (@candidate in @sources)`,
178+
wantName: `@kindin@names`,
179+
wantSource: `(@candidatein@sources)`,
180+
},
181+
{
182+
name: "array membership source retains delimiter boundary",
183+
input: `RETURN WAITFOR EVENT "message" IN (@candidate ANY IN @sources)`,
184+
want: `return waitfor event "message" in (@candidate any in @sources)`,
185+
wantName: `"message"`,
186+
wantSource: `(@candidateanyin@sources)`,
187+
},
188+
{
189+
name: "query source",
190+
input: `RETURN WAITFOR EVENT GET_NAME(@id) IN QUERY ONE ".source" IN @registry`,
191+
want: `return waitfor event GET_NAME(@id) in query one ".source" in @registry`,
192+
wantName: `GET_NAME(@id)`,
193+
wantSource: `queryone".source"in@registry`,
194+
},
195+
}
196+
197+
for _, test := range tests {
198+
t.Run(test.name, func(t *testing.T) {
199+
got := formatParenthesesStable(t, test.input)
200+
if got != test.want {
201+
t.Fatalf("formatted output:\n%s\nwant:\n%s", got, test.want)
202+
}
203+
204+
assertWaitForEventOperandBoundaries(t, got, test.wantName, test.wantSource)
205+
})
206+
}
207+
}
208+
209+
func assertWaitForEventOperandBoundaries(t *testing.T, input, wantName, wantSource string) {
210+
t.Helper()
211+
212+
p := parser.New(input)
213+
program := p.Program()
214+
if !p.AtEOF() {
215+
t.Fatalf("formatted output did not parse completely:\n%s", input)
216+
}
217+
218+
event := findFirstWaitForEvent(program)
219+
if event == nil {
220+
t.Fatalf("formatted output has no WAITFOR EVENT expression:\n%s", input)
221+
}
222+
223+
name := event.WaitForEventName().(*fql.WaitForEventNameContext).Expression()
224+
if got := name.GetText(); got != wantName {
225+
t.Fatalf("reparsed event-name expression = %q, want %q", got, wantName)
226+
}
227+
228+
source := event.WaitForEventSource().(*fql.WaitForEventSourceContext).Expression()
229+
if got := source.GetText(); got != wantSource {
230+
t.Fatalf("reparsed source expression = %q, want %q", got, wantSource)
231+
}
232+
}
233+
234+
func findFirstWaitForEvent(tree antlr.Tree) *fql.WaitForEventExpressionContext {
235+
if tree == nil {
236+
return nil
237+
}
238+
239+
if event, ok := tree.(*fql.WaitForEventExpressionContext); ok {
240+
return event
241+
}
242+
243+
for i := 0; i < tree.GetChildCount(); i++ {
244+
if event := findFirstWaitForEvent(tree.GetChild(i)); event != nil {
245+
return event
246+
}
247+
}
248+
249+
return nil
250+
}
251+
252+
func TestFormatterWaitForEventOperandsPreserveExecution(t *testing.T) {
253+
tests := []struct {
254+
newEnvironment func() []vm.EnvironmentOption
255+
name string
256+
input string
257+
}{
258+
{
259+
name: "composed operands",
260+
input: `RETURN WAITFOR EVENT (@eventName ?? "message") IN (@source ?? @fallback)
261+
WHEN .type == "match"`,
262+
newEnvironment: func() []vm.EnvironmentOption {
263+
source := mock.NewObservable([]runtime.Value{
264+
mock.NewTestEventType("ignored"),
265+
mock.NewTestEventType("match"),
266+
})
267+
268+
return []vm.EnvironmentOption{
269+
vm.WithParams(map[string]runtime.Value{
270+
"eventName": runtime.None,
271+
"fallback": runtime.None,
272+
"source": source,
273+
}),
274+
}
275+
},
276+
},
277+
{
278+
name: "bounded membership source",
279+
input: `RETURN WAITFOR EVENT "message" IN (SOURCE(@candidate IN @sources))
280+
WHEN .type == "match"`,
281+
newEnvironment: func() []vm.EnvironmentOption {
282+
source := mock.NewObservable([]runtime.Value{
283+
mock.NewTestEventType("ignored"),
284+
mock.NewTestEventType("match"),
285+
})
286+
287+
return []vm.EnvironmentOption{
288+
vm.WithParams(map[string]runtime.Value{
289+
"candidate": runtime.NewString("candidate"),
290+
"sources": runtime.NewArrayWith(runtime.NewString("candidate")),
291+
}),
292+
vm.WithFunction("SOURCE", func(context.Context, ...runtime.Value) (runtime.Value, error) {
293+
return source, nil
294+
}),
295+
}
296+
},
297+
},
298+
}
299+
300+
for _, test := range tests {
301+
formatted := formatParenthesesStable(t, test.input)
302+
303+
for _, level := range []compiler.OptimizationLevel{compiler.O0, compiler.O1} {
304+
t.Run(test.name+"/"+optimizationNameForFormatter(level), func(t *testing.T) {
305+
originalProgram, err := spec.Compile(test.input, level)
306+
if err != nil {
307+
t.Fatalf("compile original: %v", err)
308+
}
309+
310+
formattedProgram, err := spec.Compile(formatted, level)
311+
if err != nil {
312+
t.Fatalf("compile formatted: %v\n%s", err, formatted)
313+
}
314+
315+
originalResult, err := spec.Run(originalProgram, test.newEnvironment()...)
316+
if err != nil {
317+
t.Fatalf("run original: %v", err)
318+
}
319+
320+
formattedResult, err := spec.Run(formattedProgram, test.newEnvironment()...)
321+
if err != nil {
322+
t.Fatalf("run formatted: %v", err)
323+
}
324+
325+
if !bytes.Equal(originalResult, formattedResult) {
326+
t.Fatalf("execution changed: original %s, formatted %s\n%s", originalResult, formattedResult, formatted)
327+
}
328+
})
329+
}
330+
}
331+
}
332+
74333
func TestFormatterSimplifiesGroupedForOnlyAtStatementBoundaries(t *testing.T) {
75334
tests := []struct {
76335
name string

pkg/formatter/internal/expression.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ func (f *expressionFormatter) formatExpression(ctx *fql.ExpressionContext) {
1616
f.formatExpressionOperand(ctx, expressionOperation{})
1717
}
1818

19+
// formatWaitForEventSource retains grouping around membership expressions so
20+
// their IN operator cannot become the WAITFOR EVENT source delimiter.
21+
func (f *expressionFormatter) formatWaitForEventSource(ctx *fql.ExpressionContext) {
22+
if waitForEventSourceNeedsParentheses(ctx) {
23+
f.formatExpressionOperand(ctx, expressionOperation{precedence: precedencePrimary})
24+
25+
return
26+
}
27+
28+
f.formatExpression(ctx)
29+
}
30+
1931
func (f *expressionFormatter) formatExpressionOperand(ctx *fql.ExpressionContext, outer expressionOperation) {
2032
if ctx == nil {
2133
return

0 commit comments

Comments
 (0)