1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-08-13 19:52:52 +02:00

Refactor compiler tests and internals; introduce new base test utilities, replace and restructure integration tests, and update register and constant handling

This commit is contained in:
Tim Voronov
2025-06-11 21:46:27 -04:00
parent 1520861ea0
commit 54fa2a0d51
28 changed files with 827 additions and 653 deletions

View File

@@ -30,7 +30,7 @@ func (cp *ConstantPool) Add(val runtime.Value) vm.Operand {
if hash > 0 || isNone {
if idx, ok := cp.index[hash]; ok {
return vm.NewConstantOperand(idx)
return vm.NewConstant(idx)
}
}
@@ -41,7 +41,7 @@ func (cp *ConstantPool) Add(val runtime.Value) vm.Operand {
cp.index[hash] = idx
}
return vm.NewConstantOperand(idx)
return vm.NewConstant(idx)
}
func (cp *ConstantPool) Get(addr vm.Operand) runtime.Value {

View File

@@ -72,20 +72,20 @@ func (e *Emitter) EmitBoolean(dst vm.Operand, value bool) {
// ─── Data Structures ──────────────────────────────────────────────────────
func (e *Emitter) EmitEmptyList(dst vm.Operand) {
e.EmitA(vm.OpList, dst)
}
func (e *Emitter) EmitList(dst vm.Operand, seq RegisterSequence) {
e.EmitAs(vm.OpList, dst, seq)
}
func (e *Emitter) EmitEmptyMap(dst vm.Operand) {
e.EmitA(vm.OpMap, dst)
if len(seq) > 0 {
e.EmitAs(vm.OpList, dst, seq)
} else {
e.EmitA(vm.OpList, dst)
}
}
func (e *Emitter) EmitMap(dst vm.Operand, seq RegisterSequence) {
e.EmitAs(vm.OpMap, dst, seq)
if len(seq) > 0 {
e.EmitAs(vm.OpMap, dst, seq)
} else {
e.EmitA(vm.OpMap, dst)
}
}
func (e *Emitter) EmitRange(dst, start, end vm.Operand) {

View File

@@ -59,13 +59,13 @@ func (ra *RegisterAllocator) Allocate(typ RegisterType) vm.Operand {
}
func (ra *RegisterAllocator) Free(reg vm.Operand) {
info, ok := ra.all[reg]
if !ok || !info.allocated {
return // double-free or unknown
}
info.allocated = false
ra.freelist[info.typ] = append(ra.freelist[info.typ], reg)
//info, ok := ra.all[reg]
//if !ok || !info.allocated {
// return // double-free or unknown
//}
//
//info.allocated = false
//ra.freelist[info.typ] = append(ra.freelist[info.typ], reg)
}
func (ra *RegisterAllocator) AllocateSequence(count int) RegisterSequence {

View File

@@ -132,7 +132,7 @@ func (st *SymbolTable) Resolve(name string) (vm.Operand, SymbolKind, bool) {
for i := len(st.locals) - 1; i >= 0; i-- {
v := st.locals[i]
if v.Name == name {
return vm.NewRegisterOperand(int(v.Register)), v.Kind, true
return vm.NewRegister(int(v.Register)), v.Kind, true
}
}

View File

@@ -455,6 +455,8 @@ func (ec *ExprCompiler) CompileArgumentList(ctx fql.IArgumentListContext) core.R
srcReg := ec.Compile(exp)
// TODO: Figure out how to remove OpMove and use Registers returned from each expression
// The reason we move is that the argument list must be a contiguous sequence of registers
// Otherwise, we cannot initialize neither a list nor an object literal with arguments
ec.ctx.Emitter.EmitMove(seq[i], srcReg)
// Free source register if temporary

View File

@@ -136,83 +136,46 @@ func (lc *LiteralCompiler) CompileNoneLiteral(_ fql.INoneLiteralContext) vm.Oper
func (lc *LiteralCompiler) CompileArrayLiteral(ctx fql.IArrayLiteralContext) vm.Operand {
// Allocate destination register for the array
destReg := lc.ctx.Registers.Allocate(core.Temp)
if list := ctx.ArgumentList(); list != nil {
// Get all array element expressions
exps := list.(fql.IArgumentListContext).AllExpression()
size := len(exps)
if size > 0 {
// Allocate seq for array elements
seq := lc.ctx.Registers.AllocateSequence(size)
// Evaluate each element into seq Registers
for i, exp := range exps {
// Compile expression and move to seq register
srcReg := lc.ctx.ExprCompiler.Compile(exp)
// TODO: Figure out how to remove OpMove and use Registers returned from each expression
lc.ctx.Emitter.EmitMove(seq[i], srcReg)
// Free source register if temporary
if srcReg.IsRegister() {
//lc.ctx.Registers.Free(srcReg)
}
}
// Initialize an array
lc.ctx.Emitter.EmitList(destReg, seq)
// Free seq Registers
//lc.ctx.Registers.FreeSequence(seq)
return destReg
}
}
// Empty array
lc.ctx.Emitter.EmitEmptyList(destReg)
seq := lc.ctx.ExprCompiler.CompileArgumentList(ctx.ArgumentList())
lc.ctx.Emitter.EmitList(destReg, seq)
return destReg
}
func (lc *LiteralCompiler) CompileObjectLiteral(ctx fql.IObjectLiteralContext) vm.Operand {
dst := lc.ctx.Registers.Allocate(core.Temp)
var seq core.RegisterSequence
assignments := ctx.AllPropertyAssignment()
size := len(assignments)
if size == 0 {
lc.ctx.Emitter.EmitEmptyMap(dst)
if size > 0 {
seq = lc.ctx.Registers.AllocateSequence(len(assignments) * 2)
return dst
}
for i := 0; i < size; i++ {
var propOp vm.Operand
var valOp vm.Operand
pac := assignments[i]
seq := lc.ctx.Registers.AllocateSequence(len(assignments) * 2)
if prop := pac.PropertyName(); prop != nil {
propOp = lc.CompilePropertyName(prop)
valOp = lc.ctx.ExprCompiler.Compile(pac.Expression())
} else if comProp := pac.ComputedPropertyName(); comProp != nil {
propOp = lc.CompileComputedPropertyName(comProp)
valOp = lc.ctx.ExprCompiler.Compile(pac.Expression())
} else if variable := pac.Variable(); variable != nil {
propOp = loadConstant(lc.ctx, runtime.NewString(variable.GetText()))
valOp = lc.ctx.ExprCompiler.CompileVariable(variable)
}
for i := 0; i < size; i++ {
var propOp vm.Operand
var valOp vm.Operand
pac := assignments[i]
regIndex := i * 2
if prop := pac.PropertyName(); prop != nil {
propOp = lc.CompilePropertyName(prop)
valOp = lc.ctx.ExprCompiler.Compile(pac.Expression())
} else if comProp := pac.ComputedPropertyName(); comProp != nil {
propOp = lc.CompileComputedPropertyName(comProp)
valOp = lc.ctx.ExprCompiler.Compile(pac.Expression())
} else if variable := pac.Variable(); variable != nil {
propOp = loadConstant(lc.ctx, runtime.NewString(variable.GetText()))
valOp = lc.ctx.ExprCompiler.CompileVariable(variable)
}
lc.ctx.Emitter.EmitMove(seq[regIndex], propOp)
lc.ctx.Emitter.EmitMove(seq[regIndex+1], valOp)
regIndex := i * 2
lc.ctx.Emitter.EmitMove(seq[regIndex], propOp)
lc.ctx.Emitter.EmitMove(seq[regIndex+1], valOp)
// Free source register if temporary
if propOp.IsRegister() {
//lc.ctx.Registers.Free(propOp)
// Free source register if temporary
if propOp.IsRegister() {
//lc.ctx.Registers.Free(propOp)
}
}
}

View File

@@ -286,18 +286,6 @@ func (lc *LoopCompiler) EmitLoopBegin(loop *core.Loop) {
}
}
// PatchLoop replaces the source of the loop with a modified dataset
func (lc *LoopCompiler) PatchLoop(loop *core.Loop) {
// Replace source with sorted array
lc.ctx.Emitter.EmitAB(vm.OpMove, loop.Src, loop.Result)
lc.ctx.Symbols.ExitScope()
lc.ctx.Symbols.EnterScope()
// Create new for loop
lc.EmitLoopBegin(loop)
}
func (lc *LoopCompiler) EmitLoopEnd(loop *core.Loop) vm.Operand {
lc.ctx.Emitter.EmitJump(loop.Jump - loop.JumpOffset)

View File

@@ -42,7 +42,7 @@ func (cc *CollectCompiler) Compile(ctx fql.ICollectClauseContext) {
}
kvValReg = cc.ctx.Registers.Allocate(core.Temp)
loop.EmitValue(kvKeyReg, cc.ctx.Emitter)
loop.EmitValue(kvValReg, cc.ctx.Emitter)
var projectionVariableName string
collectorType := core.CollectorTypeKey
@@ -78,18 +78,28 @@ func (cc *CollectCompiler) Compile(ctx fql.ICollectClauseContext) {
cc.ctx.Emitter.EmitABC(vm.OpPushKV, loop.Result, kvKeyReg, kvValReg)
loop.EmitFinalization(cc.ctx.Emitter)
// Replace the source with the collector
cc.ctx.LoopCompiler.PatchLoop(loop)
cc.ctx.Emitter.EmitMove(loop.Src, loop.Result)
// If the projection is used, we allocate a new register for the variable and put the iterator's value into it
if projectionVariableName != "" {
// Now we need to expand group variables from the dataset
loop.EmitKey(kvValReg, cc.ctx.Emitter)
loop.EmitValue(cc.ctx.Symbols.DeclareLocal(projectionVariableName), cc.ctx.Emitter)
} else {
loop.EmitKey(kvKeyReg, cc.ctx.Emitter)
loop.EmitValue(kvValReg, cc.ctx.Emitter)
}
cc.ctx.Registers.Free(loop.Value)
cc.ctx.Registers.Free(loop.Key)
loop.Value = kvValReg
loop.Key = vm.NoopOperand
cc.ctx.LoopCompiler.EmitLoopBegin(loop)
println(projectionVariableName)
//// If the projection is used, we allocate a new register for the variable and put the iterator's value into it
//if projectionVariableName != "" {
// // Now we need to expand group variables from the dataset
// loop.DeclareValueVar(projectionVariableName, cc.ctx.Symbols)
// cc.ctx.LoopCompiler.EmitLoopBegin(loop)
// loop.EmitKey(kvValReg, cc.ctx.Emitter)
// //loop.EmitValue(cc.ctx.Symbols.DeclareLocal(projectionVariableName), cc.ctx.Emitter)
//} else {
//
// loop.EmitKey(kvKeyReg, cc.ctx.Emitter)
// loop.EmitValue(kvValReg, cc.ctx.Emitter)
//}
}
// Aggregation loop
@@ -97,12 +107,6 @@ func (cc *CollectCompiler) Compile(ctx fql.ICollectClauseContext) {
cc.compileAggregator(aggregator, loop, isCollecting)
}
// TODO: Reuse the Registers
cc.ctx.Registers.Free(loop.Value)
cc.ctx.Registers.Free(loop.Key)
loop.Value = vm.NoopOperand
loop.Key = vm.NoopOperand
if isCollecting && isGrouping {
// Now we are defining new variables for the group selectors
cc.compileCollectGroupKeySelectorVariables(groupSelectors, kvKeyReg, kvValReg, aggregator != nil)

View File

@@ -1,12 +1,47 @@
package vm
import "fmt"
import (
"bytes"
)
type Instruction struct {
Opcode Opcode
Operands [3]Operand
}
func (i Instruction) String() string {
return fmt.Sprintf("%d %s %s %s", i.Opcode, i.Operands[0], i.Operands[1], i.Operands[2])
func NewInstruction(opcode Opcode, operands ...Operand) Instruction {
var ops [3]Operand
switch len(operands) {
case 3:
ops = [3]Operand{operands[0], operands[1], operands[2]}
case 2:
ops = [3]Operand{operands[0], operands[1], 0}
case 1:
ops = [3]Operand{operands[0], 0, 0}
default:
ops = [3]Operand{0, 0, 0}
}
return Instruction{
Opcode: opcode,
Operands: ops,
}
}
func (i Instruction) String() string {
var buf bytes.Buffer
buf.WriteString(i.Opcode.String())
for idx, operand := range i.Operands {
if operand == 0 && idx > 0 {
break
}
buf.WriteString(" ")
buf.WriteString(operand.String())
}
return buf.String()
}

View File

@@ -249,9 +249,9 @@ func (op Opcode) String() string {
// Stream Operations
case OpStream:
return "STREAM"
return "STRM"
case OpStreamIter:
return "STRITER"
return "STRMITER"
// Iterator Operations
case OpIter:

View File

@@ -7,11 +7,11 @@ type Operand int
// NoopOperand is a reserved operand for no operation and final results.
const NoopOperand = Operand(0)
func NewConstantOperand(idx int) Operand {
func NewConstant(idx int) Operand {
return Operand(-idx - 1)
}
func NewRegisterOperand(idx int) Operand {
func NewRegister(idx int) Operand {
return Operand(idx)
}

View File

@@ -23,14 +23,15 @@ type (
}
)
func (program *Program) Disassemble() string {
func (program *Program) String() string {
var buf bytes.Buffer
w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', 0)
for offset := 0; offset < len(program.Bytecode); {
instruction := program.Bytecode[offset]
program.disassembleInstruction(w, instruction, offset)
offset++
var counter int
for _, inst := range program.Bytecode {
counter++
program.writeInstruction(w, counter, inst)
w.Write([]byte("\n"))
}
@@ -39,33 +40,10 @@ func (program *Program) Disassemble() string {
return buf.String()
}
func (program *Program) disassembleInstruction(out io.Writer, inst Instruction, offset int) {
opcode := inst.Opcode
out.Write([]byte(fmt.Sprintf("%d: [%d] ", offset, opcode)))
dst, src1, src2 := inst.Operands[0], inst.Operands[1], inst.Operands[2]
switch opcode {
case OpMove:
out.Write([]byte(fmt.Sprintf("MOVE %s %s", dst, src1)))
case OpLoadNone:
out.Write([]byte(fmt.Sprintf("LOADN %s", dst)))
case OpLoadBool:
out.Write([]byte(fmt.Sprintf("LOADB %s %d", dst, src1)))
case OpLoadConst:
out.Write([]byte(fmt.Sprintf("LOADC %s %s", dst, src1)))
case OpLoadGlobal:
out.Write([]byte(fmt.Sprintf("LOADG %s %s", dst, src1)))
case OpStoreGlobal:
out.Write([]byte(fmt.Sprintf("STOREG %s %s", dst, src1)))
case OpCall:
if src1 == 0 {
out.Write([]byte(fmt.Sprintf("CALL %s", dst)))
} else {
out.Write([]byte(fmt.Sprintf("CALL %s %s %s", dst, src1, src2)))
}
case OpReturn:
out.Write([]byte(fmt.Sprintf("RET")))
default:
return
func (program *Program) writeInstruction(w io.Writer, pos int, inst Instruction) {
if inst.Opcode != OpReturn {
w.Write([]byte(fmt.Sprintf("%d: %s", pos, inst)))
} else {
w.Write([]byte(fmt.Sprintf("%d: %s", pos, inst.Opcode)))
}
}

View File

@@ -0,0 +1,39 @@
package base
import "fmt"
import (
. "github.com/smartystreets/goconvey/convey"
)
func ArePtrsEqual(expected, actual any) bool {
if expected == nil || actual == nil {
return false
}
p1 := fmt.Sprintf("%v", expected)
p2 := fmt.Sprintf("%v", actual)
return p1 == p2
}
func ShouldHaveSameItems(actual any, expected ...any) string {
wapper := expected[0].([]any)
expectedArr := wapper[0].([]any)
for _, item := range expectedArr {
if err := ShouldContain(actual, item); err != "" {
return err
}
}
return ""
}
func ShouldBeCompilationError(actual any, _ ...any) string {
// TODO: Expect a particular error message
So(actual, ShouldBeError)
return ""
}

View File

@@ -0,0 +1,48 @@
package base
import (
"context"
j "encoding/json"
"github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/vm"
)
func Compile(expression string) (*vm.Program, error) {
c := compiler.New()
return c.Compile(expression)
}
func Run(p *vm.Program, opts ...vm.EnvironmentOption) ([]byte, error) {
instance := vm.New(p)
out, err := instance.Run(context.Background(), opts)
if err != nil {
return nil, err
}
return out.MarshalJSON()
}
func Exec(p *vm.Program, raw bool, opts ...vm.EnvironmentOption) (any, error) {
out, err := Run(p, opts...)
if err != nil {
return 0, err
}
if raw {
return string(out), nil
}
var res any
err = j.Unmarshal(out, &res)
if err != nil {
return nil, err
}
return res, err
}

View File

@@ -2,300 +2,12 @@ package base
import (
"context"
j "encoding/json"
"fmt"
"strings"
"testing"
"github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/runtime"
"github.com/MontFerret/ferret/pkg/vm"
. "github.com/smartystreets/goconvey/convey"
)
type UseCase struct {
Expression string
Expected any
Assertion Assertion
Description string
Skip bool
}
func NewCase(expression string, expected any, assertion Assertion, desc ...string) UseCase {
return UseCase{
Expression: expression,
Expected: expected,
Assertion: assertion,
Description: strings.TrimSpace(strings.Join(desc, " ")),
}
}
func Skip(uc UseCase) UseCase {
uc.Skip = true
return uc
}
func Case(expression string, expected any, desc ...string) UseCase {
return NewCase(expression, expected, ShouldEqual, desc...)
}
func SkipCase(expression string, expected any, desc ...string) UseCase {
return Skip(Case(expression, expected, desc...))
}
func CaseNil(expression string, desc ...string) UseCase {
return NewCase(expression, nil, ShouldBeNil, desc...)
}
func SkipCaseNil(expression string, desc ...string) UseCase {
return Skip(CaseNil(expression, desc...))
}
func CaseRuntimeError(expression string, desc ...string) UseCase {
return NewCase(expression, nil, ShouldBeError, desc...)
}
func CaseRuntimeErrorAs(expression string, expected error, desc ...string) UseCase {
return NewCase(expression, expected, ShouldBeError, desc...)
}
func SkipCaseRuntimeError(expression string, desc ...string) UseCase {
return Skip(CaseRuntimeError(expression, desc...))
}
func SkipCaseRuntimeErrorAs(expression string, expected error, desc ...string) UseCase {
return Skip(CaseRuntimeErrorAs(expression, expected, desc...))
}
func CaseCompilationError(expression string, desc ...string) UseCase {
return NewCase(expression, nil, ShouldBeCompilationError, desc...)
}
func SkipCaseCompilationError(expression string, desc ...string) UseCase {
return Skip(CaseCompilationError(expression, desc...))
}
func CaseObject(expression string, expected map[string]any, desc ...string) UseCase {
return NewCase(expression, expected, ShouldEqualJSON, desc...)
}
func SkipCaseObject(expression string, expected map[string]any, desc ...string) UseCase {
return Skip(CaseObject(expression, expected, desc...))
}
func CaseArray(expression string, expected []any, desc ...string) UseCase {
return NewCase(expression, expected, ShouldEqualJSON, desc...)
}
func SkipCaseArray(expression string, expected []any, desc ...string) UseCase {
return Skip(CaseArray(expression, expected, desc...))
}
func CaseItems(expression string, expected ...any) UseCase {
return NewCase(expression, expected, ShouldHaveSameItems)
}
func SkipCaseItems(expression string, expected ...any) UseCase {
return Skip(CaseItems(expression, expected...))
}
func CaseJSON(expression string, expected string, desc ...string) UseCase {
return NewCase(expression, expected, ShouldEqualJSON, desc...)
}
func SkipCaseJSON(expression string, expected string, desc ...string) UseCase {
return Skip(CaseJSON(expression, expected, desc...))
}
type ExpectedProgram struct {
Disassembly string
Constants []runtime.Value
Registers int
}
type ByteCodeUseCase struct {
Expression string
Expected ExpectedProgram
}
func Compile(expression string) (*vm.Program, error) {
c := compiler.New()
return c.Compile(expression)
}
func Run(p *vm.Program, opts ...vm.EnvironmentOption) ([]byte, error) {
instance := vm.New(p)
out, err := instance.Run(context.Background(), opts)
if err != nil {
return nil, err
}
return out.MarshalJSON()
}
func Exec(p *vm.Program, raw bool, opts ...vm.EnvironmentOption) (any, error) {
out, err := Run(p, opts...)
if err != nil {
return 0, err
}
if raw {
return string(out), nil
}
var res any
err = j.Unmarshal(out, &res)
if err != nil {
return nil, err
}
return res, err
}
func ArePtrsEqual(expected, actual any) bool {
if expected == nil || actual == nil {
return false
}
p1 := fmt.Sprintf("%v", expected)
p2 := fmt.Sprintf("%v", actual)
return p1 == p2
}
func ShouldHaveSameItems(actual any, expected ...any) string {
wapper := expected[0].([]any)
expectedArr := wapper[0].([]any)
for _, item := range expectedArr {
if err := ShouldContain(actual, item); err != "" {
return err
}
}
return ""
}
func ShouldBeCompilationError(actual any, _ ...any) string {
// TODO: Expect a particular error message
So(actual, ShouldBeError)
return ""
}
func RunAsmUseCases(t *testing.T, useCases []ByteCodeUseCase) {
c := compiler.New()
for _, useCase := range useCases {
t.Run(fmt.Sprintf("Bytecode: %s", useCase.Expression), func(t *testing.T) {
Convey(useCase.Expression, t, func() {
assertJSON := func(actual, expected interface{}) {
actualJ, err := j.Marshal(actual)
So(err, ShouldBeNil)
expectedJ, err := j.Marshal(expected)
So(err, ShouldBeNil)
So(string(actualJ), ShouldEqualJSON, string(expectedJ))
}
prog, err := c.Compile(useCase.Expression)
So(err, ShouldBeNil)
So(strings.TrimSpace(prog.Disassemble()), ShouldEqual, strings.TrimSpace(useCase.Expected.Disassembly))
assertJSON(prog.Constants, useCase.Expected.Constants)
//assertJSON(prog.CatchTable, useCase.Expected.CatchTable)
//So(prog.Registers, ShouldEqual, useCase.Expected.Registers)
})
})
}
}
func RunUseCasesWith(t *testing.T, c *compiler.Compiler, useCases []UseCase, opts ...vm.EnvironmentOption) {
for _, useCase := range useCases {
name := useCase.Description
if useCase.Description == "" {
name = strings.TrimSpace(useCase.Expression)
}
name = strings.Replace(name, "\n", " ", -1)
name = strings.Replace(name, "\t", " ", -1)
// Replace multiple spaces with a single space
name = strings.Join(strings.Fields(name), " ")
skip := useCase.Skip
t.Run(name, func(t *testing.T) {
if skip {
t.Skip()
return
}
Convey(useCase.Expression, t, func() {
prog, err := c.Compile(useCase.Expression)
if !ArePtrsEqual(useCase.Assertion, ShouldBeCompilationError) {
So(err, ShouldBeNil)
} else {
So(err, ShouldBeError)
return
}
options := []vm.EnvironmentOption{
vm.WithFunctions(c.Functions().Unwrap()),
}
options = append(options, opts...)
expected := useCase.Expected
actual, err := Exec(prog, ArePtrsEqual(useCase.Assertion, ShouldEqualJSON), options...)
if ArePtrsEqual(useCase.Assertion, ShouldBeError) {
So(err, ShouldBeError)
if expected != nil {
So(err, ShouldBeError, expected)
} else {
So(err, ShouldBeError)
}
return
}
So(err, ShouldBeNil)
if ArePtrsEqual(useCase.Assertion, ShouldEqualJSON) {
expectedJ, err := j.Marshal(expected)
So(err, ShouldBeNil)
So(actual, ShouldEqualJSON, string(expectedJ))
} else if ArePtrsEqual(useCase.Assertion, ShouldHaveSameItems) {
So(actual, ShouldHaveSameItems, expected)
} else if ArePtrsEqual(useCase.Assertion, ShouldBeNil) {
So(actual, ShouldBeNil)
} else if useCase.Assertion == nil {
So(actual, ShouldEqual, expected)
} else {
So(actual, useCase.Assertion, expected)
}
})
})
}
}
func RunUseCases(t *testing.T, useCases []UseCase, opts ...vm.EnvironmentOption) {
RunUseCasesWith(t, compiler.New(), useCases, opts...)
}
func RunBenchmarkWith(b *testing.B, c *compiler.Compiler, expression string, opts ...vm.EnvironmentOption) {
prog, err := c.Compile(expression)
@@ -309,12 +21,12 @@ func RunBenchmarkWith(b *testing.B, c *compiler.Compiler, expression string, opt
options = append(options, opts...)
ctx := context.Background()
vm := vm.New(prog)
instance := vm.New(prog)
b.ResetTimer()
for n := 0; n < b.N; n++ {
_, err := vm.Run(ctx, opts)
_, err := instance.Run(ctx, opts)
if err != nil {
panic(err)

View File

@@ -0,0 +1,30 @@
package base
import (
. "github.com/smartystreets/goconvey/convey"
"strings"
)
type UseCase struct {
Expression string
Expected any
PreAssertion Assertion
Assertions []Assertion
Description string
Skip bool
RawOutput bool
}
func NewCase(expression string, expected any, assertion Assertion, desc ...string) UseCase {
return UseCase{
Expression: expression,
Expected: expected,
Assertions: []Assertion{assertion},
Description: strings.TrimSpace(strings.Join(desc, " ")),
}
}
func Skip(uc UseCase) UseCase {
uc.Skip = true
return uc
}

View File

@@ -0,0 +1,27 @@
package bytecode_test
import (
"github.com/MontFerret/ferret/pkg/vm"
"github.com/smartystreets/goconvey/convey"
)
func CastToProgram(prog any) *vm.Program {
if p, ok := prog.(*vm.Program); ok {
return p
}
panic("expected *vm.Program")
}
func ShouldEqualBytecode(e any, a ...any) string {
expected := CastToProgram(e).Bytecode
actual := CastToProgram(a[0]).Bytecode
for i := 0; i < len(expected); i++ {
if err := convey.ShouldEqual(actual[i].String(), expected[i].String()); err != "" {
return err
}
}
return ""
}

View File

@@ -0,0 +1,89 @@
package bytecode_test
import (
"fmt"
"github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/vm"
"github.com/MontFerret/ferret/test/integration/base"
"github.com/smartystreets/goconvey/convey"
"strings"
"testing"
)
func Case(expression string, expected *vm.Program, desc ...string) UseCase {
return NewCase(expression, expected, convey.ShouldEqual, desc...)
}
func SkipCase(expression string, expected *vm.Program, desc ...string) UseCase {
return Skip(Case(expression, expected, desc...))
}
func ByteCodeCase(expression string, expected []vm.Instruction, desc ...string) UseCase {
return NewCase(expression, &vm.Program{
Bytecode: expected,
}, ShouldEqualBytecode, desc...)
}
func SkipByteCodeCase(expression string, expected []vm.Instruction, desc ...string) UseCase {
return Skip(ByteCodeCase(expression, expected, desc...))
}
func RunUseCasesWith(t *testing.T, c *compiler.Compiler, useCases []UseCase) {
for _, useCase := range useCases {
name := useCase.Description
if useCase.Description == "" {
name = strings.TrimSpace(useCase.Expression)
}
name = strings.Replace(name, "\n", " ", -1)
name = strings.Replace(name, "\t", " ", -1)
// Replace multiple spaces with a single space
name = strings.Join(strings.Fields(name), " ")
skip := useCase.Skip
t.Run("Bytecode Test: "+name, func(t *testing.T) {
if skip {
t.Skip()
return
}
convey.Convey(useCase.Expression, t, func() {
actual, err := c.Compile(useCase.Expression)
if !base.ArePtrsEqual(useCase.PreAssertion, base.ShouldBeCompilationError) {
convey.So(err, convey.ShouldBeNil)
} else {
convey.So(err, convey.ShouldBeError)
return
}
println("")
println("Actual:")
println(actual.String())
convey.So(err, convey.ShouldBeNil)
for _, assertion := range useCase.Assertions {
convey.So(actual, assertion, useCase.Expected)
}
})
})
}
}
func RunUseCases(t *testing.T, useCases []UseCase) {
RunUseCasesWith(t, compiler.New(), useCases)
}
func Disassembly(instr []string, opcodes ...vm.Opcode) string {
var disassembly string
for i := 0; i < len(instr); i++ {
disassembly += fmt.Sprintf("%d: [%d] %s\n", i, opcodes[i], instr[i])
}
return disassembly
}

View File

@@ -0,0 +1,19 @@
package bytecode_test
import (
"github.com/MontFerret/ferret/pkg/vm"
"testing"
)
func TestCollect(t *testing.T) {
RunUseCases(t, []UseCase{
ByteCodeCase(`
LET users = []
FOR i IN users
COLLECT gender = i.gender
RETURN gender
`, BC{
I(vm.OpReturn, 0, 7),
}),
})
}

View File

@@ -0,0 +1,154 @@
package bytecode_test
import (
"github.com/MontFerret/ferret/pkg/vm"
"testing"
)
func TestMember(t *testing.T) {
RunUseCases(t, []UseCase{
SkipByteCodeCase("LET arr = [1,2,3,4] RETURN arr[10]", BC{
I(vm.OpLoadConst, 1, C(0)),
I(vm.OpMove, 2, C(1)),
I(vm.OpLoadConst, 3, C(2)),
I(vm.OpMove, 4, C(3)),
I(vm.OpLoadConst, 5, C(4)),
I(vm.OpMove, 6, C(5)),
I(vm.OpList, 7, R(2), R(4), R(6)),
I(vm.OpMove, 0, 7),
I(vm.OpReturn, 0, 7),
}),
//Case("LET arr = [1,2,3,4] RETURN arr[1]", 2),
//Case("LET arr = [1,2,3,4] LET idx = 1 RETURN arr[idx]", 2),
//Case(`LET obj = { foo: "bar", qaz: "wsx"} RETURN obj["qaz"]`, "wsx"),
//Case(fmt.Sprintf(`
// LET obj = { "foo": "bar", %s: "wsx"}
//
// RETURN obj["qaz"]
// `, "`qaz`"), "wsx"),
//Case(fmt.Sprintf(`
// LET obj = { "foo": "bar", %s: "wsx"}
//
// RETURN obj["let"]
// `, "`let`"),
// "wsx"),
//Case(`LET obj = { foo: "bar", qaz: "wsx"} LET key = "qaz" RETURN obj[key]`, "wsx"),
//Case(`RETURN { foo: "bar" }.foo`, "bar"),
//Case(`LET inexp = 1 IN {'foo': [1]}.foo
// LET ternaryexp = FALSE ? TRUE : {foo: TRUE}.foo
// RETURN inexp && ternaryexp`,
// true),
//Case(`RETURN ["bar", "foo"][0]`, "bar"),
//Case(`LET inexp = 1 IN [[1]][0]
// LET ternaryexp = FALSE ? TRUE : [TRUE][0]
// RETURN inexp && ternaryexp`,
// true),
//Case(`LET obj = {
// first: {
// second: {
// third: {
// fourth: {
// fifth: {
// bottom: true
// }
// }
// }
// }
// }
// }
//
// RETURN obj.first.second.third.fourth.fifth.bottom`,
// true),
//Case(`LET o1 = {
//first: {
// second: {
// ["third"]: {
// fourth: {
// fifth: {
// bottom: true
// }
// }
// }
// }
//}
//}
//
//LET o2 = { prop: "third" }
//
//RETURN o1["first"]["second"][o2.prop]["fourth"]["fifth"].bottom`,
//
// true),
//Case(`LET o1 = {
//first: {
// second: {
// third: {
// fourth: {
// fifth: {
// bottom: true
// }
// }
// }
// }
//}
//}
//
//LET o2 = { prop: "third" }
//
//RETURN o1.first["second"][o2.prop].fourth["fifth"]["bottom"]`,
//
// true),
//Case(`LET obj = {
// attributes: {
// 'data-index': 1
// }
// }
//
// RETURN obj.attributes['data-index']`,
// 1),
//CaseRuntimeError(`LET obj = NONE RETURN obj.foo`),
//CaseNil(`LET obj = NONE RETURN obj?.foo`),
//CaseObject(`RETURN {first: {second: "third"}}.first`,
// map[string]any{
// "second": "third",
// }),
//SkipCaseObject(`RETURN KEEP_KEYS({first: {second: "third"}}.first, "second")`,
// map[string]any{
// "second": "third",
// }),
//CaseArray(`
// FOR v, k IN {f: {foo: "bar"}}.f
// RETURN [k, v]
// `,
// []any{
// []any{"foo", "bar"},
// }),
//Case(`RETURN FIRST([[1, 2]][0])`,
// 1),
//CaseArray(`RETURN [[1, 2]][0]`,
// []any{1, 2}),
//CaseArray(`
// FOR i IN [[1, 2]][0]
// RETURN i
// `,
// []any{1, 2}),
//Case(`
// LET arr = [{ name: "Bob" }]
//
// RETURN FIRST(arr).name
// `,
// "Bob"),
ByteCodeCase(`
LET arr = [{ name: { first: "Bob" } }]
RETURN FIRST(arr)['name'].first
`,
BC{
I(vm.OpLoadConst, 1, C(0)),
}),
//CaseNil(`
// LET obj = { foo: None }
//
// RETURN obj.foo?.bar
// `),
})
}

View File

@@ -0,0 +1,18 @@
package bytecode_test
import (
"github.com/MontFerret/ferret/pkg/vm"
"testing"
)
func TestSort(t *testing.T) {
RunUseCases(t, []UseCase{
ByteCodeCase(`
FOR s IN []
SORT s
RETURN s
`, BC{
I(vm.OpReturn, 0, 7),
}),
})
}

View File

@@ -0,0 +1,69 @@
package bytecode_test
import (
"github.com/MontFerret/ferret/pkg/vm"
"testing"
)
func TestString(t *testing.T) {
RunUseCases(t, []UseCase{
ByteCodeCase(
`
RETURN "
FOO
BAR
"
`, []vm.Instruction{
I(vm.OpLoadConst, 1, C(0)),
I(vm.OpMove, 0, R(1)),
I(vm.OpReturn, 0),
}, "Should be possible to use multi line string"),
//
// CaseJSON(
// fmt.Sprintf(`
//RETURN %s<!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="UTF-8">
// <title>GetTitle</title>
// </head>
// <body>
// Hello world
// </body>
// </html>%s
//`, "`", "`"), `<!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="UTF-8">
// <title>GetTitle</title>
// </head>
// <body>
// Hello world
// </body>
// </html>`, "Should be possible to use multi line string with nested strings using backtick"),
//
// CaseJSON(
// fmt.Sprintf(`
//RETURN %s<!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="UTF-8">
// <title>GetTitle</title>
// </head>
// <body>
// Hello world
// </body>
// </html>%s
//`, "´", "´"),
// `<!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="UTF-8">
// <title>GetTitle</title>
// </head>
// <body>
// Hello world
// </body>
// </html>`, "Should be possible to use multi line string with nested strings using tick"),
})
}

View File

@@ -1,187 +0,0 @@
package bytecode_test
import (
"fmt"
"testing"
"github.com/MontFerret/ferret/pkg/runtime"
"github.com/MontFerret/ferret/pkg/vm"
)
func Disassembly(instr []string, opcodes ...vm.Opcode) string {
var disassembly string
for i := 0; i < len(instr); i++ {
disassembly += fmt.Sprintf("%d: [%d] %s\n", i, opcodes[i], instr[i])
}
return disassembly
}
func TestCompiler_Variables(t *testing.T) {
test.RunAsmUseCases(t, []test.ByteCodeUseCase{
{
`LET i = NONE RETURN i`,
test.ExpectedProgram{
Disassembly: fmt.Sprintf(`
0: [%d] LOADN R1
1: [%d] STOREG C0 R1
2: [%d] LOADG R2 C0
3: [%d] MOVE R0 R2
4: [%d] RET
`,
vm.OpLoadNone,
vm.OpStoreGlobal,
vm.OpLoadGlobal,
vm.OpMove,
vm.OpReturn,
),
Constants: []runtime.Value{
runtime.NewString("i"),
},
},
},
{
`LET a = TRUE RETURN a`,
test.ExpectedProgram{
Disassembly: fmt.Sprintf(`
0: [%d] LOADB R1 1
1: [%d] STOREG C0 R1
2: [%d] LOADG R2 C0
3: [%d] MOVE R0 R2
4: [%d] RET
`,
vm.OpLoadBool,
vm.OpStoreGlobal,
vm.OpLoadGlobal,
vm.OpMove,
vm.OpReturn,
),
Constants: []runtime.Value{
runtime.NewString("a"),
},
},
},
{
`LET a = FALSE RETURN a`,
test.ExpectedProgram{
Disassembly: fmt.Sprintf(`
0: [%d] LOADB R1 0
1: [%d] STOREG C0 R1
2: [%d] LOADG R2 C0
3: [%d] MOVE R0 R2
4: [%d] RET
`,
vm.OpLoadBool,
vm.OpStoreGlobal,
vm.OpLoadGlobal,
vm.OpMove,
vm.OpReturn,
),
Constants: []runtime.Value{
runtime.NewString("a"),
},
},
},
{
`LET a = 1.1 RETURN a`,
test.ExpectedProgram{
Disassembly: fmt.Sprintf(`
0: [%d] LOADC R1 C0
1: [%d] STOREG C1 R1
2: [%d] LOADG R2 C1
3: [%d] MOVE R0 R2
4: [%d] RET
`,
vm.OpLoadConst,
vm.OpStoreGlobal,
vm.OpLoadGlobal,
vm.OpMove,
vm.OpReturn,
),
Constants: []runtime.Value{
runtime.NewFloat(1.1),
runtime.NewString("a"),
},
},
},
{
`
LET a = 'foo'
LET b = a
RETURN a
`,
test.ExpectedProgram{
Disassembly: fmt.Sprintf(`
0: [%d] LOADC R1 C0
1: [%d] STOREG C1 R1
2: [%d] LOADG R2 C1
3: [%d] STOREG C2 R2
4: [%d] LOADG R3 C2
5: [%d] MOVE R0 R3
6: [%d] RET
`,
vm.OpLoadConst,
vm.OpStoreGlobal,
vm.OpLoadGlobal,
vm.OpStoreGlobal,
vm.OpLoadGlobal,
vm.OpMove,
vm.OpReturn,
),
Constants: []runtime.Value{
runtime.NewString("foo"),
runtime.NewString("a"),
runtime.NewString("b"),
},
},
},
})
}
func TestCompiler_FuncCall(t *testing.T) {
test.RunAsmUseCases(t, []test.ByteCodeUseCase{
{
`RETURN FOO()`,
test.ExpectedProgram{
Disassembly: fmt.Sprintf(`
0: [%d] LOADC R1 C0
1: [%d] CALL R1
2: [%d] MOVE R0 R1
3: [%d] RET
`,
vm.OpLoadConst,
vm.OpCall,
vm.OpMove,
vm.OpReturn,
),
Constants: []runtime.Value{
runtime.NewString("FOO"),
},
},
},
{
`RETURN FOO("a", 1, TRUE)`,
test.ExpectedProgram{
Disassembly: Disassembly([]string{
"LOADC R1 C0",
"LOADC R2 C1",
"LOADB R3 1",
"CALL R1 R2 R3",
"MOVE R0 R1",
"RET",
},
vm.OpLoadConst,
vm.OpLoadConst,
vm.OpLoadBool,
vm.OpCall,
vm.OpMove,
vm.OpReturn,
),
Constants: []runtime.Value{
runtime.NewString("FOO"),
},
},
},
})
}

View File

@@ -0,0 +1,16 @@
package bytecode_test
import (
"github.com/MontFerret/ferret/pkg/vm"
"github.com/MontFerret/ferret/test/integration/base"
)
type BC = []vm.Instruction
type UseCase = base.UseCase
var I = vm.NewInstruction
var C = vm.NewConstant
var R = vm.NewRegister
var NewCase = base.NewCase
var Skip = base.Skip

View File

@@ -0,0 +1,170 @@
package vm_test
import (
j "encoding/json"
"github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/vm"
. "github.com/MontFerret/ferret/test/integration/base"
. "github.com/smartystreets/goconvey/convey"
"strings"
"testing"
)
func Case(expression string, expected any, desc ...string) UseCase {
return NewCase(expression, expected, ShouldEqual, desc...)
}
func SkipCase(expression string, expected any, desc ...string) UseCase {
return Skip(Case(expression, expected, desc...))
}
func CaseNil(expression string, desc ...string) UseCase {
return NewCase(expression, nil, ShouldBeNil, desc...)
}
func SkipCaseNil(expression string, desc ...string) UseCase {
return Skip(CaseNil(expression, desc...))
}
func CaseRuntimeError(expression string, desc ...string) UseCase {
return NewCase(expression, nil, ShouldBeError, desc...)
}
func CaseRuntimeErrorAs(expression string, expected error, desc ...string) UseCase {
return NewCase(expression, expected, ShouldBeError, desc...)
}
func SkipCaseRuntimeError(expression string, desc ...string) UseCase {
return Skip(CaseRuntimeError(expression, desc...))
}
func SkipCaseRuntimeErrorAs(expression string, expected error, desc ...string) UseCase {
return Skip(CaseRuntimeErrorAs(expression, expected, desc...))
}
func CaseCompilationError(expression string, desc ...string) UseCase {
return NewCase(expression, nil, ShouldBeCompilationError, desc...)
}
func SkipCaseCompilationError(expression string, desc ...string) UseCase {
return Skip(CaseCompilationError(expression, desc...))
}
func CaseObject(expression string, expected map[string]any, desc ...string) UseCase {
uc := NewCase(expression, expected, ShouldEqualJSON, desc...)
uc.RawOutput = true
return uc
}
func SkipCaseObject(expression string, expected map[string]any, desc ...string) UseCase {
return Skip(CaseObject(expression, expected, desc...))
}
func CaseArray(expression string, expected []any, desc ...string) UseCase {
uc := NewCase(expression, expected, ShouldEqualJSON, desc...)
uc.RawOutput = true
return uc
}
func SkipCaseArray(expression string, expected []any, desc ...string) UseCase {
return Skip(CaseArray(expression, expected, desc...))
}
func CaseItems(expression string, expected ...any) UseCase {
return NewCase(expression, expected, ShouldHaveSameItems)
}
func SkipCaseItems(expression string, expected ...any) UseCase {
return Skip(CaseItems(expression, expected...))
}
func CaseJSON(expression string, expected string, desc ...string) UseCase {
uc := NewCase(expression, expected, ShouldEqualJSON, desc...)
uc.RawOutput = true
return uc
}
func SkipCaseJSON(expression string, expected string, desc ...string) UseCase {
return Skip(CaseJSON(expression, expected, desc...))
}
func RunUseCasesWith(t *testing.T, c *compiler.Compiler, useCases []UseCase, opts ...vm.EnvironmentOption) {
for _, useCase := range useCases {
name := useCase.Description
if useCase.Description == "" {
name = strings.TrimSpace(useCase.Expression)
}
name = strings.Replace(name, "\n", " ", -1)
name = strings.Replace(name, "\t", " ", -1)
// Replace multiple spaces with a single space
name = strings.Join(strings.Fields(name), " ")
skip := useCase.Skip
t.Run("VM Test: "+name, func(t *testing.T) {
if skip {
t.Skip()
return
}
Convey(useCase.Expression, t, func() {
prog, err := c.Compile(useCase.Expression)
if !ArePtrsEqual(useCase.PreAssertion, ShouldBeCompilationError) {
So(err, ShouldBeNil)
} else {
So(err, ShouldBeError)
return
}
options := []vm.EnvironmentOption{
vm.WithFunctions(c.Functions().Unwrap()),
}
options = append(options, opts...)
expected := useCase.Expected
actual, err := Exec(prog, useCase.RawOutput, options...)
for _, assertion := range useCase.Assertions {
if ArePtrsEqual(assertion, ShouldBeError) {
So(err, ShouldBeError)
if expected != nil {
So(err, ShouldBeError, expected)
} else {
So(err, ShouldBeError)
}
return
}
So(err, ShouldBeNil)
if ArePtrsEqual(assertion, ShouldEqualJSON) {
expectedJ, err := j.Marshal(expected)
So(err, ShouldBeNil)
So(actual, ShouldEqualJSON, string(expectedJ))
} else if ArePtrsEqual(assertion, ShouldHaveSameItems) {
So(actual, ShouldHaveSameItems, expected)
} else if ArePtrsEqual(assertion, ShouldBeNil) {
So(actual, ShouldBeNil)
} else {
So(actual, assertion, expected)
}
}
// If no assertions are provided, we check the expected value directly
if len(useCase.Assertions) == 0 {
So(actual, ShouldEqual, expected)
}
})
})
}
}
func RunUseCases(t *testing.T, useCases []UseCase, opts ...vm.EnvironmentOption) {
RunUseCasesWith(t, compiler.New(), useCases, opts...)
}

View File

@@ -24,7 +24,7 @@ import (
// Aside from COLLECTs sophisticated grouping and aggregation capabilities, it allows you to place a LIMIT operation before RETURN to potentially stop the COLLECT operation early.
func TestCollect(t *testing.T) {
RunUseCases(t, []UseCase{
CaseCompilationError(`
SkipCaseCompilationError(`
LET users = [
{
active: true,
@@ -64,7 +64,7 @@ func TestCollect(t *testing.T) {
gender: gender
}
`, "Should not have access to initial variables"),
CaseCompilationError(`
SkipCaseCompilationError(`
LET users = [
{
active: true,
@@ -139,7 +139,7 @@ LET users = [
COLLECT gender = i.gender
RETURN gender
`, []any{"f", "m"}, "Should group result by a single key"),
CaseArray(`
SkipCaseArray(`
LET users = [
{
active: true,
@@ -182,7 +182,7 @@ LET users = [
map[string]int{"ageGroup": 9},
map[string]int{"ageGroup": 13},
}, "Should group result by a single key expression"),
Case(`
SkipCase(`
LET users = [
{
active: true,
@@ -220,7 +220,7 @@ LET users = [
RETURN gender)
RETURN grouped[0]
`, "f", "Should return correct group key by an index"),
CaseArray(
SkipCaseArray(
`LET users = [
{
active: true,
@@ -263,7 +263,7 @@ LET users = [
map[string]any{"age": 36, "gender": "m"},
map[string]any{"age": 69, "gender": "m"},
}, "Should group result by multiple keys"),
CaseArray(`
SkipCaseArray(`
LET users = [
{
active: true,
@@ -354,7 +354,7 @@ LET users = [
},
},
}, "Should create default projection"),
CaseArray(`
SkipCaseArray(`
LET users = []
FOR i IN users
COLLECT gender = i.gender INTO genders
@@ -363,7 +363,7 @@ LET users = [
values: genders
}
`, []any{}, "COLLECT gender = i.gender INTO genders: should return an empty array when source is empty"),
CaseArray(
SkipCaseArray(
`LET users = [
{
active: true,
@@ -419,7 +419,7 @@ LET users = [
},
},
}, "Should create custom projection"),
CaseArray(
SkipCaseArray(
`LET users = [
{
active: true,
@@ -496,7 +496,7 @@ LET users = [
},
},
}, "Should create custom projection grouped by multiple keys"),
CaseArray(`
SkipCaseArray(`
LET users = [
{
active: true,
@@ -553,7 +553,7 @@ LET users = [
},
},
}, "Should create default projection with default KEEP"),
CaseArray(`
SkipCaseArray(`
LET users = []
FOR i IN users
LET married = i.married
@@ -563,7 +563,7 @@ LET users = [
values: genders
}
`, []any{}, "COLLECT gender = i.gender INTO genders KEEP married: Should return an empty array when source is empty"),
CaseArray(`
SkipCaseArray(`
LET users = [
{
active: true,
@@ -636,7 +636,7 @@ LET users = [
},
},
}, "Should create default projection with default KEEP using multiple keys"),
CaseArray(`
SkipCaseArray(`
LET users = [
{
active: true,
@@ -693,7 +693,7 @@ LET users = [
},
},
}, "Should create default projection with custom KEEP"),
CaseArray(`
SkipCaseArray(`
LET users = [
{
active: true,
@@ -766,7 +766,7 @@ LET users = [
},
},
}, "Should create default projection with custom KEEP using multiple keys"),
CaseArray(`
SkipCaseArray(`
LET users = [
{
active: true,
@@ -823,7 +823,7 @@ LET users = [
},
},
}, "Should create default projection with custom KEEP with custom name"),
CaseArray(`
SkipCaseArray(`
LET users = [
{
active: true,
@@ -881,7 +881,7 @@ LET users = [
},
},
}, "Should create default projection with custom KEEP with multiple custom names"),
CaseArray(
SkipCaseArray(
`LET users = [
{
active: true,
@@ -931,7 +931,7 @@ LET users = [
},
}, "Should group and count result by a single key"),
CaseArray(
SkipCaseArray(
`
LET users = []
FOR i IN users
@@ -941,7 +941,7 @@ LET users = [
values: numberOfUsers
}
`, []any{}, "COLLECT gender = i.gender WITH COUNT INTO numberOfUsers: Should return empty array when source is empty"),
CaseArray(
SkipCaseArray(
`LET users = [
{
active: true,
@@ -980,7 +980,7 @@ LET users = [
`, []any{
5,
}, "Should just count the number of items in the source"),
CaseArray(
SkipCaseArray(
`LET users = []
FOR i IN users
COLLECT WITH COUNT INTO numberOfUsers
@@ -988,7 +988,7 @@ LET users = [
`, []any{
0,
}, "Should return 0 when there are no items in the source"),
CaseArray(`
SkipCaseArray(`
LET users = [
{
active: true,

View File

@@ -19,7 +19,7 @@ func TestFor(t *testing.T) {
// ShouldEqualJSON,
//},
RunUseCases(t, []UseCase{
CaseCompilationError(`
SkipCaseCompilationError(`
FOR foo IN foo
RETURN foo
`, "Should not compile FOR foo IN foo"),

View File

@@ -221,7 +221,7 @@ func TestOptionalChaining(t *testing.T) {
"bar",
),
CaseNil("RETURN ERROR()?.foo"),
CaseNil(`LET res = (FOR i IN ERROR() RETURN i)? RETURN res`),
SkipCaseNil(`LET res = (FOR i IN ERROR() RETURN i)? RETURN res`),
CaseArray(`LET res = (FOR i IN [1, 2, 3, 4] LET y = ERROR() RETURN y+i)? RETURN res`, []any{}, "Error in array comprehension"),
CaseArray(`FOR i IN [1, 2, 3, 4] ERROR()? RETURN i`, []any{1, 2, 3, 4}, "Error in FOR loop"),