Skip to content

Commit 8d722fd

Browse files
committed
feat: replace $WAIT_DURATION_DIFF with OpElapsed opcode for VM runtime clock handling
1 parent 5d8ac04 commit 8d722fd

21 files changed

Lines changed: 314 additions & 86 deletions
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package ferret
2+
3+
import (
4+
"context"
5+
"errors"
6+
"strings"
7+
"testing"
8+
9+
"github.com/MontFerret/ferret/v2/pkg/source"
10+
)
11+
12+
func TestTimedWaitForRunsWithoutStdlib(t *testing.T) {
13+
engine := mustNewEngine(t, WithoutStdlib())
14+
t.Cleanup(func() { _ = engine.Close() })
15+
16+
tests := []struct {
17+
name string
18+
query string
19+
want string
20+
}{
21+
{
22+
name: "duration timeout",
23+
query: `LET ready = false RETURN WAITFOR ready TIMEOUT 1ms EVERY 0ms`,
24+
want: "false",
25+
},
26+
{
27+
name: "computed timeout",
28+
query: `LET ready = false LET timeout = 0.5ms RETURN WAITFOR ready TIMEOUT timeout * 2 EVERY 0ms`,
29+
want: "false",
30+
},
31+
{
32+
name: "timeout fallback",
33+
query: `RETURN WAITFOR VALUE "ready" WHEN false TIMEOUT 1ms EVERY 0ms ON TIMEOUT RETURN "timeout"`,
34+
want: `"timeout"`,
35+
},
36+
}
37+
38+
for _, tc := range tests {
39+
t.Run(tc.name, func(t *testing.T) {
40+
output, err := engine.Run(context.Background(), source.NewAnonymous(tc.query))
41+
if err != nil {
42+
t.Fatalf("run failed: %v", err)
43+
}
44+
if got := string(output.Content); got != tc.want {
45+
t.Fatalf("unexpected output: got %q, want %q", got, tc.want)
46+
}
47+
})
48+
}
49+
}
50+
51+
func TestTimedWaitForWithoutStdlibHonorsCancellation(t *testing.T) {
52+
engine := mustNewEngine(t, WithoutStdlib())
53+
t.Cleanup(func() { _ = engine.Close() })
54+
55+
ctx, cancel := context.WithCancel(context.Background())
56+
cancel()
57+
58+
_, err := engine.Run(
59+
ctx,
60+
source.NewAnonymous(`LET ready = false RETURN WAITFOR ready TIMEOUT 10s EVERY 10s`),
61+
)
62+
if !errors.Is(err, context.Canceled) {
63+
t.Fatalf("expected cancellation, got %v", err)
64+
}
65+
}
66+
67+
func TestTimedWaitForWithoutStdlibSupportsRepeatedSessionRuns(t *testing.T) {
68+
engine := mustNewEngine(t, WithoutStdlib())
69+
t.Cleanup(func() { _ = engine.Close() })
70+
71+
plan := mustCompilePlan(t, engine, `LET ready = false RETURN WAITFOR ready TIMEOUT 0.5ms EVERY 0ms`)
72+
t.Cleanup(func() { _ = plan.Close() })
73+
74+
session := mustNewSession(t, plan)
75+
t.Cleanup(func() { _ = session.Close() })
76+
77+
for run := 0; run < 2; run++ {
78+
output, err := session.Run(context.Background())
79+
if err != nil {
80+
t.Fatalf("run %d failed: %v", run+1, err)
81+
}
82+
if got := string(output.Content); got != "false" {
83+
t.Fatalf("run %d returned %q, want false", run+1, got)
84+
}
85+
}
86+
}
87+
88+
func TestExplicitNowStillRequiresStdlib(t *testing.T) {
89+
engine := mustNewEngine(t, WithoutStdlib())
90+
t.Cleanup(func() { _ = engine.Close() })
91+
92+
_, err := engine.Run(context.Background(), source.NewAnonymous(`RETURN NOW()`))
93+
if err == nil || !strings.Contains(err.Error(), "unresolved function") {
94+
t.Fatalf("expected unresolved NOW function, got %v", err)
95+
}
96+
}

pkg/asm/disassembler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ func disasmLine(ip int, instr bytecode.Instruction, p *bytecode.Program, labels
269269

270270
// Op R
271271
case bytecode.OpLoadNone, bytecode.OpLoadZero,
272-
bytecode.OpClose, bytecode.OpSleep, bytecode.OpRand, bytecode.OpIncr, bytecode.OpDecr, bytecode.OpReturn, bytecode.OpCounterInc:
272+
bytecode.OpClose, bytecode.OpSleep, bytecode.OpRand, bytecode.OpElapsed, bytecode.OpIncr, bytecode.OpDecr, bytecode.OpReturn, bytecode.OpCounterInc:
273273
out = fmt.Sprintf("%d: %s %s", ip, opcode, formatOperand(ops[0]))
274274

275275
// Op R Arg

pkg/asm/disassembler_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,3 +586,23 @@ func TestDisassemble_SourcePointUsesImmediateID(t *testing.T) {
586586
t.Fatalf("expected source point immediate in output:\n%s", out)
587587
}
588588
}
589+
590+
func TestDisassemble_ElapsedUsesDestinationRegister(t *testing.T) {
591+
prog := &bytecode.Program{
592+
ISAVersion: bytecode.Version,
593+
Registers: 1,
594+
Bytecode: []bytecode.Instruction{
595+
bytecode.NewInstruction(bytecode.OpElapsed, bytecode.NewRegister(0)),
596+
bytecode.NewInstruction(bytecode.OpReturn, bytecode.NewRegister(0)),
597+
},
598+
}
599+
600+
out, err := Disassemble(prog)
601+
if err != nil {
602+
t.Fatalf("Disassemble() error: %v", err)
603+
}
604+
605+
if !strings.Contains(out, "0: ELAPSED R0") {
606+
t.Fatalf("expected elapsed destination in output:\n%s", out)
607+
}
608+
}

pkg/bytecode/artifact/artifact_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,45 @@ func TestMarshalAllowsDistinctOpcode(t *testing.T) {
106106
}
107107
}
108108

109+
func TestMarshalPreservesElapsedOpcode(t *testing.T) {
110+
tests := []struct {
111+
name string
112+
opts []Option
113+
}{
114+
{name: "message_pack"},
115+
{name: "json", opts: []Option{WithFormat(FormatJSON)}},
116+
}
117+
118+
for _, tc := range tests {
119+
t.Run(tc.name, func(t *testing.T) {
120+
program := newArtifactTestProgram()
121+
program.Registers = 1
122+
program.Bytecode = []bytecode.Instruction{
123+
bytecode.NewInstruction(bytecode.OpElapsed, bytecode.NewRegister(0)),
124+
bytecode.NewInstruction(bytecode.OpReturn, bytecode.NewRegister(0)),
125+
}
126+
program.Metadata.Labels = nil
127+
program.Metadata.AggregateSelectorSlots = nil
128+
program.Metadata.MatchFailTargets = nil
129+
program.Metadata.DebugSpans = nil
130+
131+
data, err := Marshal(program, tc.opts...)
132+
if err != nil {
133+
t.Fatalf("Marshal() error = %v", err)
134+
}
135+
136+
decoded, err := Unmarshal(data)
137+
if err != nil {
138+
t.Fatalf("Unmarshal() error = %v", err)
139+
}
140+
141+
if got := decoded.Bytecode[0].Opcode; got != bytecode.OpElapsed {
142+
t.Fatalf("expected OpElapsed after round trip, got %s", got)
143+
}
144+
})
145+
}
146+
}
147+
109148
func TestMarshalPreservesSourcePoint(t *testing.T) {
110149
program := newArtifactTestProgram()
111150
program.Bytecode = []bytecode.Instruction{

pkg/bytecode/opcode.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ const (
190190

191191
// Source Observation Operations
192192
OpSourcePoint
193+
194+
// Runtime Clock Operations
195+
OpElapsed
193196
)
194197

195198
func (op Opcode) String() string {
@@ -497,6 +500,8 @@ func (op Opcode) String() string {
497500
return "DISTINCT"
498501
case OpSourcePoint:
499502
return "SOURCEPOINT"
503+
case OpElapsed:
504+
return "ELAPSED"
500505

501506
default:
502507
return "UNKNOWN"

pkg/bytecode/opcode_meta.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func opcodeClass(op Opcode) OpcodeClass {
140140
OpNoneEq, OpNoneNe, OpNoneGt, OpNoneGte, OpNoneLt, OpNoneLte, OpNoneIn,
141141
OpAllEq, OpAllNe, OpAllGt, OpAllGte, OpAllLt, OpAllLte, OpAllIn:
142142
return OpcodeClassArray
143-
case OpLength, OpClose, OpSleep, OpExists, OpRand, OpDispatch, OpFail, OpFailTimeout, OpRethrow, OpStoreCell, OpSourcePoint:
143+
case OpLength, OpClose, OpSleep, OpExists, OpRand, OpElapsed, OpDispatch, OpFail, OpFailTimeout, OpRethrow, OpStoreCell, OpSourcePoint:
144144
return OpcodeClassUtility
145145
case OpHCall, OpProtectedHCall, OpCall, OpProtectedCall, OpTailCall:
146146
return OpcodeClassCall

pkg/bytecode/opcode_meta_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package bytecode
33
import "testing"
44

55
func TestOpcodeInfoCompleteness(t *testing.T) {
6-
for op := Opcode(0); op <= OpSourcePoint; op++ {
6+
for op := Opcode(0); op <= OpElapsed; op++ {
77
info := OpcodeInfoOf(op)
88

99
if info.Class == OpcodeClassUnknown {
@@ -12,6 +12,17 @@ func TestOpcodeInfoCompleteness(t *testing.T) {
1212
}
1313
}
1414

15+
func TestOpcodeInfoElapsedMetadata(t *testing.T) {
16+
info := OpcodeInfoOf(OpElapsed)
17+
18+
if info.Class != OpcodeClassUtility {
19+
t.Fatalf("expected elapsed utility class, got %d", info.Class)
20+
}
21+
if info.ControlFlow != ControlFlowNone {
22+
t.Fatalf("expected elapsed to have no control-flow role, got %d", info.ControlFlow)
23+
}
24+
}
25+
1526
func TestOpcodeInfoCallMetadata(t *testing.T) {
1627
type tc struct {
1728
op Opcode

pkg/bytecode/validation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ func validateInstructions(program *Program) error {
329329
if err := validateRegisterOperand(src1, registers, pc, "src1"); err != nil {
330330
return err
331331
}
332-
case OpLoadNone, OpLoadZero, OpIncr, OpDecr, OpClose, OpSleep, OpRand:
332+
case OpLoadNone, OpLoadZero, OpIncr, OpDecr, OpClose, OpSleep, OpRand, OpElapsed:
333333
if err := validateRegisterOperand(dst, registers, pc, "dst"); err != nil {
334334
return err
335335
}

pkg/bytecode/validation_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ func TestValidateProgram(t *testing.T) {
2424
program: withProgramMutation(func(program *Program) { program.Bytecode[0] = NewInstruction(OpReturn, NewRegister(3)) }),
2525
target: ErrInvalidInstruction,
2626
},
27+
{
28+
name: "elapsed_register_out_of_range",
29+
program: withProgramMutation(func(program *Program) { program.Bytecode[0] = NewInstruction(OpElapsed, NewRegister(3)) }),
30+
target: ErrInvalidInstruction,
31+
},
2732
{
2833
name: "constant_out_of_range",
2934
program: withProgramMutation(func(program *Program) {
@@ -186,6 +191,23 @@ func TestValidateProgramAllowsDistinctOpcode(t *testing.T) {
186191
}
187192
}
188193

194+
func TestValidateProgramAllowsElapsedOpcode(t *testing.T) {
195+
program := withProgramMutation(func(program *Program) {
196+
program.Bytecode = []Instruction{
197+
NewInstruction(OpElapsed, NewRegister(0)),
198+
NewInstruction(OpReturn, NewRegister(0)),
199+
}
200+
program.Metadata.Labels = nil
201+
program.Metadata.AggregateSelectorSlots = nil
202+
program.Metadata.MatchFailTargets = nil
203+
program.Metadata.DebugSpans = nil
204+
})
205+
206+
if err := ValidateProgram(program); err != nil {
207+
t.Fatalf("expected elapsed opcode to be valid, got %v", err)
208+
}
209+
}
210+
189211
func TestValidateProgramAllowsSourcePoints(t *testing.T) {
190212
if err := ValidateProgram(validSourcePointProgram()); err != nil {
191213
t.Fatalf("expected source point program to be valid, got %v", err)

pkg/compiler/internal/optimization/coalescing.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ func operandIsRegister(op bytecode.Opcode, idx int) bool {
301301
return idx == 0 || idx == 1
302302
case bytecode.OpAddConst:
303303
return idx == 0 || idx == 1
304-
case bytecode.OpLoadNone, bytecode.OpLoadZero, bytecode.OpLoadBool, bytecode.OpLoadConst, bytecode.OpLoadParam, bytecode.OpRand:
304+
case bytecode.OpLoadNone, bytecode.OpLoadZero, bytecode.OpLoadBool, bytecode.OpLoadConst, bytecode.OpLoadParam, bytecode.OpRand, bytecode.OpElapsed:
305305
return idx == 0
306306
case bytecode.OpLoadArray, bytecode.OpLoadObject:
307307
return idx == 0

0 commit comments

Comments
 (0)