Skip to content

Commit c6ed511

Browse files
authored
feat: enhance WAITFOR predicate handling and add tests for value presence (#967)
1 parent fc61380 commit c6ed511

7 files changed

Lines changed: 101 additions & 20 deletions

File tree

pkg/compiler/internal/wait_helpers.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ func literalFromExpression(ctx fql.IExpressionContext) fql.ILiteralContext {
137137
return atom.Literal()
138138
}
139139

140+
func literalPresentFromExpression(ctx fql.IExpressionContext) (bool, bool) {
141+
lit := literalFromExpression(ctx)
142+
if lit == nil {
143+
return false, false
144+
}
145+
146+
return lit.NoneLiteral() == nil, true
147+
}
148+
140149
func literalExistsFromExpression(ctx fql.IExpressionContext) (bool, bool) {
141150
lit := literalFromExpression(ctx)
142151
if lit == nil {

pkg/compiler/internal/wait_polling.go

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ func (c *WaitCompiler) tryCompileWaitPredicateFastPath(config waitPredicateCompi
3838
return c.emitImmediateWaitBool(false), true
3939
}
4040

41+
return bytecode.NoopOperand, false
42+
case waitForPredicateModeValue:
43+
present, ok := literalPresentFromExpression(config.predExpr)
44+
if !ok {
45+
return bytecode.NoopOperand, false
46+
}
47+
48+
if present {
49+
return c.exprs.Compile(config.predExpr), true
50+
}
51+
52+
if config.timeoutReg != bytecode.NoopOperand {
53+
c.ctx.Program.Emitter.EmitA(bytecode.OpSleep, config.timeoutReg)
54+
return c.emitImmediateWaitNone(), true
55+
}
56+
4157
return bytecode.NoopOperand, false
4258
default:
4359
exists, ok := literalExistsFromExpression(config.predExpr)
@@ -51,19 +67,11 @@ func (c *WaitCompiler) tryCompileWaitPredicateFastPath(config waitPredicateCompi
5167
}
5268

5369
if cond {
54-
if config.mode == waitForPredicateModeValue {
55-
return c.exprs.Compile(config.predExpr), true
56-
}
57-
5870
return c.emitImmediateWaitBool(true), true
5971
}
6072

6173
if config.timeoutReg != bytecode.NoopOperand {
6274
c.ctx.Program.Emitter.EmitA(bytecode.OpSleep, config.timeoutReg)
63-
if config.mode == waitForPredicateModeValue {
64-
return c.emitImmediateWaitNone(), true
65-
}
66-
6775
return c.emitImmediateWaitBool(false), true
6876
}
6977

@@ -160,16 +168,24 @@ func (c *WaitCompiler) emitWaitPredicatePollIteration(
160168
startLabel, successLabel, timeoutLabel core.Label,
161169
) bytecode.Operand {
162170
valueReg := c.exprs.Compile(config.predExpr)
163-
condReg := c.emitWaitPredicateCondition(config.mode, valueReg)
164-
165-
if len(config.whenExprs) == 0 {
166-
c.ctx.Program.Emitter.EmitJumpIfTrue(condReg, successLabel)
167-
} else {
171+
if config.mode == waitForPredicateModeValue {
168172
retryLabel := c.ctx.Program.Emitter.NewLabel()
169-
c.ctx.Program.Emitter.EmitJumpIfFalse(condReg, retryLabel)
173+
c.ctx.Program.Emitter.EmitJumpIfNone(valueReg, retryLabel)
170174
c.emitWaitPredicateWhenConditions(config, valueReg, retryLabel)
171175
c.ctx.Program.Emitter.EmitJump(successLabel)
172176
c.ctx.Program.Emitter.MarkLabel(retryLabel)
177+
} else {
178+
condReg := c.emitWaitPredicateCondition(config.mode, valueReg)
179+
180+
if len(config.whenExprs) == 0 {
181+
c.ctx.Program.Emitter.EmitJumpIfTrue(condReg, successLabel)
182+
} else {
183+
retryLabel := c.ctx.Program.Emitter.NewLabel()
184+
c.ctx.Program.Emitter.EmitJumpIfFalse(condReg, retryLabel)
185+
c.emitWaitPredicateWhenConditions(config, valueReg, retryLabel)
186+
c.ctx.Program.Emitter.EmitJump(successLabel)
187+
c.ctx.Program.Emitter.MarkLabel(retryLabel)
188+
}
173189
}
174190

175191
elapsedReg := c.emitWaitPredicateTimeoutCheck(config.timeoutReg, state.startReg, state.unitReg, timeoutLabel)
@@ -210,7 +226,7 @@ func (c *WaitCompiler) emitWaitPredicateWhenConditions(
210226

211227
func (c *WaitCompiler) emitWaitPredicateCondition(mode waitForPredicateMode, valueReg bytecode.Operand) bytecode.Operand {
212228
switch mode {
213-
case waitForPredicateModeValue, waitForPredicateModeExists:
229+
case waitForPredicateModeExists:
214230
return c.emitExistsCheck(valueReg)
215231
case waitForPredicateModeNotExists:
216232
existsReg := c.emitExistsCheck(valueReg)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package benchmarks_test
2+
3+
import "testing"
4+
5+
const waitForValuePresentQuery = `
6+
RETURN WAITFOR VALUE @candidate`
7+
8+
func BenchmarkWaitForValuePresent_O0(b *testing.B) {
9+
RunBenchmarkO0(b, waitForValuePresentQuery, WithParam("candidate", []any{1}))
10+
}
11+
12+
func BenchmarkWaitForValuePresent_O1(b *testing.B) {
13+
RunBenchmarkO1(b, waitForValuePresentQuery, WithParam("candidate", []any{1}))
14+
}

test/integration/compiler/compiler_dispatch_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func TestDispatchGroupedTargetsCompile(t *testing.T) {
116116
DISPATCH "click" IN (QUERY ONE "#search-form button[type='submit']" IN page USING css)
117117
118118
LET result = WAITFOR VALUE (QUERY ONE "#form-result" IN page USING css)
119-
WHEN .attributes.disabled == false
119+
WHEN .textContent != ""
120120
TIMEOUT 10s
121121
122122
RETURN result.textContent

test/integration/compiler/compiler_waitfor_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/MontFerret/ferret/v2/pkg/bytecode"
8+
"github.com/MontFerret/ferret/v2/pkg/compiler"
89
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
910
"github.com/MontFerret/ferret/v2/test/spec"
1011
. "github.com/MontFerret/ferret/v2/test/spec/compile"
@@ -160,6 +161,19 @@ func TestWaitforPredicateWhenCompiles(t *testing.T) {
160161
})
161162
}
162163

164+
func TestWaitforValuePresenceLowering(t *testing.T) {
165+
RunSpecsLevels(t, []spec.Spec{
166+
Opcode(`RETURN WAITFOR VALUE @candidate TIMEOUT 1ms`, OpcodeExistence{
167+
Exists: []bytecode.Opcode{bytecode.OpJumpIfNone},
168+
NotExists: []bytecode.Opcode{bytecode.OpExists},
169+
}, "WAITFOR VALUE should use NONE presence without EXISTS semantics"),
170+
Opcode(`RETURN WAITFOR EXISTS @candidate TIMEOUT 1ms`, OpcodeExistence{
171+
Exists: []bytecode.Opcode{bytecode.OpExists},
172+
NotExists: []bytecode.Opcode{bytecode.OpJumpIfNone},
173+
}, "WAITFOR EXISTS should preserve EXISTS semantics"),
174+
}, compiler.O0, compiler.O1)
175+
}
176+
163177
func noCompilerError(*bytecode.Program) error {
164178
return nil
165179
}

test/integration/optimization/waitfor_fastpath_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ func TestWaitforFastPath(t *testing.T) {
2626
NotExists: []bytecode.Opcode{bytecode.OpJump, bytecode.OpJumpIfTrue, bytecode.OpJumpIfFalse},
2727
}, nil, "should include sleep for none value"),
2828

29+
Opcode(`RETURN WAITFOR VALUE [] TIMEOUT 10ms`, compile.OpcodeExistence{
30+
NotExists: []bytecode.Opcode{bytecode.OpSleep},
31+
}, []any{}, "should skip sleep for empty array value"),
32+
33+
Opcode(`RETURN WAITFOR VALUE {} TIMEOUT 10ms`, compile.OpcodeExistence{
34+
NotExists: []bytecode.Opcode{bytecode.OpSleep},
35+
}, map[string]any{}, "should skip sleep for empty object value"),
36+
37+
Opcode(`RETURN WAITFOR VALUE "" TIMEOUT 10ms`, compile.OpcodeExistence{
38+
NotExists: []bytecode.Opcode{bytecode.OpSleep},
39+
}, "", "should skip sleep for empty string value"),
40+
2941
Opcode(`RETURN WAITFOR EXISTS [] TIMEOUT 10ms`, compile.OpcodeExistence{
3042
Exists: []bytecode.Opcode{bytecode.OpSleep},
3143
NotExists: []bytecode.Opcode{bytecode.OpJump, bytecode.OpJumpIfTrue, bytecode.OpJumpIfFalse},

test/integration/vm/vm_waitfor_predicate_test.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,33 @@ func TestWaitforPredicate(t *testing.T) {
4747
LET token = WAITFOR VALUE (DATE_DIFF(start, NOW(), "f") > 20 ? "ok" : NONE) TIMEOUT 0.5s EVERY 10ms
4848
RETURN token
4949
`, "ok", "Should return value once it exists"),
50+
S(`
51+
RETURN WAITFOR VALUE "" TIMEOUT 20ms
52+
`, "", "Should return an empty string value immediately"),
53+
Object(`
54+
RETURN WAITFOR VALUE {} TIMEOUT 20ms
55+
`, map[string]any{}, "Should return an empty object value immediately"),
56+
Array(`
57+
RETURN WAITFOR VALUE [] TIMEOUT 20ms
58+
`, []any{}, "Should return an empty array value immediately"),
59+
Array(`
60+
RETURN WAITFOR VALUE [] WHEN LENGTH(.) == 0 TIMEOUT 20ms
61+
`, []any{}, "Should evaluate WHEN for an empty non-NONE value"),
5062
S(`
5163
LET start = NOW()
5264
LET ok = WAITFOR EXISTS (DATE_DIFF(start, NOW(), "f") > 20 ? { foo: 1 } : {}) TIMEOUT 0.5s EVERY 10ms
5365
RETURN ok
5466
`, true, "Should wait for non-empty object with EXISTS"),
5567
Object(`
5668
LET start = NOW()
57-
LET obj = WAITFOR VALUE (DATE_DIFF(start, NOW(), "f") > 20 ? { foo: 1 } : {}) TIMEOUT 0.5s EVERY 10ms
69+
LET obj = WAITFOR VALUE (DATE_DIFF(start, NOW(), "f") > 20 ? { foo: 1 } : {}) WHEN LENGTH(.) > 0 TIMEOUT 0.5s EVERY 10ms
5870
RETURN obj
59-
`, map[string]any{"foo": 1}, "Should return object once it exists"),
71+
`, map[string]any{"foo": 1}, "Should return object once WHEN accepts the candidate"),
6072
Array(`
6173
LET start = NOW()
62-
LET arr = WAITFOR VALUE (DATE_DIFF(start, NOW(), "f") > 20 ? [1, 2] : []) TIMEOUT 0.5s EVERY 10ms
74+
LET arr = WAITFOR VALUE (DATE_DIFF(start, NOW(), "f") > 20 ? [1, 2] : []) WHEN LENGTH(.) > 0 TIMEOUT 0.5s EVERY 10ms
6375
RETURN arr
64-
`, []any{1, 2}, "Should return array once it exists"),
76+
`, []any{1, 2}, "Should return array once WHEN accepts the candidate"),
6577
S(`
6678
LET start = NOW()
6779
LET ok = WAITFOR EXISTS (DATE_DIFF(start, NOW(), "f") > 20 ? "ok" : "") TIMEOUT 0.5s EVERY 10ms
@@ -181,6 +193,10 @@ func TestWaitforPredicateWhenSkipsPredicateUntilBasePasses(t *testing.T) {
181193
LET ok = WAITFOR EXISTS NONE WHEN PREDICATE(.) TIMEOUT 20ms EVERY 1ms ON TIMEOUT RETURN false
182194
RETURN ok
183195
`, false, "WAITFOR EXISTS WHEN should not evaluate the predicate before existence passes"),
196+
Nil(`
197+
LET value = WAITFOR VALUE NONE WHEN PREDICATE(.) TIMEOUT 20ms EVERY 1ms ON TIMEOUT RETURN NONE
198+
RETURN value
199+
`, "WAITFOR VALUE WHEN should not evaluate the predicate before presence passes"),
184200
},
185201
vm.WithFunction("PREDICATE", func(ctx context.Context, args ...runtime.Value) (runtime.Value, error) {
186202
predicateCalls++

0 commit comments

Comments
 (0)