mirror of
https://github.com/MontFerret/ferret.git
synced 2025-08-15 20:02:56 +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:
@@ -30,7 +30,7 @@ func (cp *ConstantPool) Add(val runtime.Value) vm.Operand {
|
|||||||
|
|
||||||
if hash > 0 || isNone {
|
if hash > 0 || isNone {
|
||||||
if idx, ok := cp.index[hash]; ok {
|
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
|
cp.index[hash] = idx
|
||||||
}
|
}
|
||||||
|
|
||||||
return vm.NewConstantOperand(idx)
|
return vm.NewConstant(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *ConstantPool) Get(addr vm.Operand) runtime.Value {
|
func (cp *ConstantPool) Get(addr vm.Operand) runtime.Value {
|
||||||
|
@@ -72,20 +72,20 @@ func (e *Emitter) EmitBoolean(dst vm.Operand, value bool) {
|
|||||||
|
|
||||||
// ─── Data Structures ──────────────────────────────────────────────────────
|
// ─── Data Structures ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
func (e *Emitter) EmitEmptyList(dst vm.Operand) {
|
func (e *Emitter) EmitList(dst vm.Operand, seq RegisterSequence) {
|
||||||
|
if len(seq) > 0 {
|
||||||
|
e.EmitAs(vm.OpList, dst, seq)
|
||||||
|
} else {
|
||||||
e.EmitA(vm.OpList, dst)
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Emitter) EmitMap(dst vm.Operand, seq RegisterSequence) {
|
func (e *Emitter) EmitMap(dst vm.Operand, seq RegisterSequence) {
|
||||||
|
if len(seq) > 0 {
|
||||||
e.EmitAs(vm.OpMap, dst, seq)
|
e.EmitAs(vm.OpMap, dst, seq)
|
||||||
|
} else {
|
||||||
|
e.EmitA(vm.OpMap, dst)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Emitter) EmitRange(dst, start, end vm.Operand) {
|
func (e *Emitter) EmitRange(dst, start, end vm.Operand) {
|
||||||
|
@@ -59,13 +59,13 @@ func (ra *RegisterAllocator) Allocate(typ RegisterType) vm.Operand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ra *RegisterAllocator) Free(reg vm.Operand) {
|
func (ra *RegisterAllocator) Free(reg vm.Operand) {
|
||||||
info, ok := ra.all[reg]
|
//info, ok := ra.all[reg]
|
||||||
if !ok || !info.allocated {
|
//if !ok || !info.allocated {
|
||||||
return // double-free or unknown
|
// return // double-free or unknown
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
info.allocated = false
|
//info.allocated = false
|
||||||
ra.freelist[info.typ] = append(ra.freelist[info.typ], reg)
|
//ra.freelist[info.typ] = append(ra.freelist[info.typ], reg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ra *RegisterAllocator) AllocateSequence(count int) RegisterSequence {
|
func (ra *RegisterAllocator) AllocateSequence(count int) RegisterSequence {
|
||||||
|
@@ -132,7 +132,7 @@ func (st *SymbolTable) Resolve(name string) (vm.Operand, SymbolKind, bool) {
|
|||||||
for i := len(st.locals) - 1; i >= 0; i-- {
|
for i := len(st.locals) - 1; i >= 0; i-- {
|
||||||
v := st.locals[i]
|
v := st.locals[i]
|
||||||
if v.Name == name {
|
if v.Name == name {
|
||||||
return vm.NewRegisterOperand(int(v.Register)), v.Kind, true
|
return vm.NewRegister(int(v.Register)), v.Kind, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -455,6 +455,8 @@ func (ec *ExprCompiler) CompileArgumentList(ctx fql.IArgumentListContext) core.R
|
|||||||
srcReg := ec.Compile(exp)
|
srcReg := ec.Compile(exp)
|
||||||
|
|
||||||
// TODO: Figure out how to remove OpMove and use Registers returned from each expression
|
// 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)
|
ec.ctx.Emitter.EmitMove(seq[i], srcReg)
|
||||||
|
|
||||||
// Free source register if temporary
|
// Free source register if temporary
|
||||||
|
@@ -136,58 +136,20 @@ func (lc *LiteralCompiler) CompileNoneLiteral(_ fql.INoneLiteralContext) vm.Oper
|
|||||||
func (lc *LiteralCompiler) CompileArrayLiteral(ctx fql.IArrayLiteralContext) vm.Operand {
|
func (lc *LiteralCompiler) CompileArrayLiteral(ctx fql.IArrayLiteralContext) vm.Operand {
|
||||||
// Allocate destination register for the array
|
// Allocate destination register for the array
|
||||||
destReg := lc.ctx.Registers.Allocate(core.Temp)
|
destReg := lc.ctx.Registers.Allocate(core.Temp)
|
||||||
|
seq := lc.ctx.ExprCompiler.CompileArgumentList(ctx.ArgumentList())
|
||||||
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)
|
lc.ctx.Emitter.EmitList(destReg, seq)
|
||||||
|
|
||||||
// Free seq Registers
|
|
||||||
//lc.ctx.Registers.FreeSequence(seq)
|
|
||||||
|
|
||||||
return destReg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty array
|
|
||||||
lc.ctx.Emitter.EmitEmptyList(destReg)
|
|
||||||
|
|
||||||
return destReg
|
return destReg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lc *LiteralCompiler) CompileObjectLiteral(ctx fql.IObjectLiteralContext) vm.Operand {
|
func (lc *LiteralCompiler) CompileObjectLiteral(ctx fql.IObjectLiteralContext) vm.Operand {
|
||||||
dst := lc.ctx.Registers.Allocate(core.Temp)
|
dst := lc.ctx.Registers.Allocate(core.Temp)
|
||||||
|
var seq core.RegisterSequence
|
||||||
assignments := ctx.AllPropertyAssignment()
|
assignments := ctx.AllPropertyAssignment()
|
||||||
size := len(assignments)
|
size := len(assignments)
|
||||||
|
|
||||||
if size == 0 {
|
if size > 0 {
|
||||||
lc.ctx.Emitter.EmitEmptyMap(dst)
|
seq = lc.ctx.Registers.AllocateSequence(len(assignments) * 2)
|
||||||
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
seq := lc.ctx.Registers.AllocateSequence(len(assignments) * 2)
|
|
||||||
|
|
||||||
for i := 0; i < size; i++ {
|
for i := 0; i < size; i++ {
|
||||||
var propOp vm.Operand
|
var propOp vm.Operand
|
||||||
@@ -215,6 +177,7 @@ func (lc *LiteralCompiler) CompileObjectLiteral(ctx fql.IObjectLiteralContext) v
|
|||||||
//lc.ctx.Registers.Free(propOp)
|
//lc.ctx.Registers.Free(propOp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lc.ctx.Emitter.EmitMap(dst, seq)
|
lc.ctx.Emitter.EmitMap(dst, seq)
|
||||||
|
|
||||||
|
@@ -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 {
|
func (lc *LoopCompiler) EmitLoopEnd(loop *core.Loop) vm.Operand {
|
||||||
lc.ctx.Emitter.EmitJump(loop.Jump - loop.JumpOffset)
|
lc.ctx.Emitter.EmitJump(loop.Jump - loop.JumpOffset)
|
||||||
|
|
||||||
|
@@ -42,7 +42,7 @@ func (cc *CollectCompiler) Compile(ctx fql.ICollectClauseContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kvValReg = cc.ctx.Registers.Allocate(core.Temp)
|
kvValReg = cc.ctx.Registers.Allocate(core.Temp)
|
||||||
loop.EmitValue(kvKeyReg, cc.ctx.Emitter)
|
loop.EmitValue(kvValReg, cc.ctx.Emitter)
|
||||||
|
|
||||||
var projectionVariableName string
|
var projectionVariableName string
|
||||||
collectorType := core.CollectorTypeKey
|
collectorType := core.CollectorTypeKey
|
||||||
@@ -78,18 +78,28 @@ func (cc *CollectCompiler) Compile(ctx fql.ICollectClauseContext) {
|
|||||||
cc.ctx.Emitter.EmitABC(vm.OpPushKV, loop.Result, kvKeyReg, kvValReg)
|
cc.ctx.Emitter.EmitABC(vm.OpPushKV, loop.Result, kvKeyReg, kvValReg)
|
||||||
loop.EmitFinalization(cc.ctx.Emitter)
|
loop.EmitFinalization(cc.ctx.Emitter)
|
||||||
|
|
||||||
// Replace the source with the collector
|
cc.ctx.Emitter.EmitMove(loop.Src, loop.Result)
|
||||||
cc.ctx.LoopCompiler.PatchLoop(loop)
|
|
||||||
|
|
||||||
// If the projection is used, we allocate a new register for the variable and put the iterator's value into it
|
cc.ctx.Registers.Free(loop.Value)
|
||||||
if projectionVariableName != "" {
|
cc.ctx.Registers.Free(loop.Key)
|
||||||
// Now we need to expand group variables from the dataset
|
loop.Value = kvValReg
|
||||||
loop.EmitKey(kvValReg, cc.ctx.Emitter)
|
loop.Key = vm.NoopOperand
|
||||||
loop.EmitValue(cc.ctx.Symbols.DeclareLocal(projectionVariableName), cc.ctx.Emitter)
|
cc.ctx.LoopCompiler.EmitLoopBegin(loop)
|
||||||
} else {
|
|
||||||
loop.EmitKey(kvKeyReg, cc.ctx.Emitter)
|
println(projectionVariableName)
|
||||||
loop.EmitValue(kvValReg, cc.ctx.Emitter)
|
|
||||||
}
|
//// 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
|
// Aggregation loop
|
||||||
@@ -97,12 +107,6 @@ func (cc *CollectCompiler) Compile(ctx fql.ICollectClauseContext) {
|
|||||||
cc.compileAggregator(aggregator, loop, isCollecting)
|
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 {
|
if isCollecting && isGrouping {
|
||||||
// Now we are defining new variables for the group selectors
|
// Now we are defining new variables for the group selectors
|
||||||
cc.compileCollectGroupKeySelectorVariables(groupSelectors, kvKeyReg, kvValReg, aggregator != nil)
|
cc.compileCollectGroupKeySelectorVariables(groupSelectors, kvKeyReg, kvValReg, aggregator != nil)
|
||||||
|
@@ -1,12 +1,47 @@
|
|||||||
package vm
|
package vm
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
type Instruction struct {
|
type Instruction struct {
|
||||||
Opcode Opcode
|
Opcode Opcode
|
||||||
Operands [3]Operand
|
Operands [3]Operand
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i Instruction) String() string {
|
func NewInstruction(opcode Opcode, operands ...Operand) Instruction {
|
||||||
return fmt.Sprintf("%d %s %s %s", i.Opcode, i.Operands[0], i.Operands[1], i.Operands[2])
|
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()
|
||||||
}
|
}
|
||||||
|
@@ -249,9 +249,9 @@ func (op Opcode) String() string {
|
|||||||
|
|
||||||
// Stream Operations
|
// Stream Operations
|
||||||
case OpStream:
|
case OpStream:
|
||||||
return "STREAM"
|
return "STRM"
|
||||||
case OpStreamIter:
|
case OpStreamIter:
|
||||||
return "STRITER"
|
return "STRMITER"
|
||||||
|
|
||||||
// Iterator Operations
|
// Iterator Operations
|
||||||
case OpIter:
|
case OpIter:
|
||||||
|
@@ -7,11 +7,11 @@ type Operand int
|
|||||||
// NoopOperand is a reserved operand for no operation and final results.
|
// NoopOperand is a reserved operand for no operation and final results.
|
||||||
const NoopOperand = Operand(0)
|
const NoopOperand = Operand(0)
|
||||||
|
|
||||||
func NewConstantOperand(idx int) Operand {
|
func NewConstant(idx int) Operand {
|
||||||
return Operand(-idx - 1)
|
return Operand(-idx - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegisterOperand(idx int) Operand {
|
func NewRegister(idx int) Operand {
|
||||||
return Operand(idx)
|
return Operand(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,14 +23,15 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (program *Program) Disassemble() string {
|
func (program *Program) String() string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', 0)
|
||||||
|
|
||||||
for offset := 0; offset < len(program.Bytecode); {
|
var counter int
|
||||||
instruction := program.Bytecode[offset]
|
|
||||||
program.disassembleInstruction(w, instruction, offset)
|
for _, inst := range program.Bytecode {
|
||||||
offset++
|
counter++
|
||||||
|
program.writeInstruction(w, counter, inst)
|
||||||
w.Write([]byte("\n"))
|
w.Write([]byte("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,33 +40,10 @@ func (program *Program) Disassemble() string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (program *Program) disassembleInstruction(out io.Writer, inst Instruction, offset int) {
|
func (program *Program) writeInstruction(w io.Writer, pos int, inst Instruction) {
|
||||||
opcode := inst.Opcode
|
if inst.Opcode != OpReturn {
|
||||||
out.Write([]byte(fmt.Sprintf("%d: [%d] ", offset, opcode)))
|
w.Write([]byte(fmt.Sprintf("%d: %s", pos, inst)))
|
||||||
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 {
|
} else {
|
||||||
out.Write([]byte(fmt.Sprintf("CALL %s %s %s", dst, src1, src2)))
|
w.Write([]byte(fmt.Sprintf("%d: %s", pos, inst.Opcode)))
|
||||||
}
|
|
||||||
case OpReturn:
|
|
||||||
out.Write([]byte(fmt.Sprintf("RET")))
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
test/integration/base/assertions.go
Normal file
39
test/integration/base/assertions.go
Normal 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 ""
|
||||||
|
}
|
48
test/integration/base/exec.go
Normal file
48
test/integration/base/exec.go
Normal 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
|
||||||
|
}
|
@@ -2,300 +2,12 @@ package base
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
j "encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/MontFerret/ferret/pkg/compiler"
|
"github.com/MontFerret/ferret/pkg/compiler"
|
||||||
"github.com/MontFerret/ferret/pkg/runtime"
|
|
||||||
"github.com/MontFerret/ferret/pkg/vm"
|
"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) {
|
func RunBenchmarkWith(b *testing.B, c *compiler.Compiler, expression string, opts ...vm.EnvironmentOption) {
|
||||||
prog, err := c.Compile(expression)
|
prog, err := c.Compile(expression)
|
||||||
|
|
||||||
@@ -309,12 +21,12 @@ func RunBenchmarkWith(b *testing.B, c *compiler.Compiler, expression string, opt
|
|||||||
options = append(options, opts...)
|
options = append(options, opts...)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
vm := vm.New(prog)
|
instance := vm.New(prog)
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
_, err := vm.Run(ctx, opts)
|
_, err := instance.Run(ctx, opts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
30
test/integration/base/use_case.go
Normal file
30
test/integration/base/use_case.go
Normal 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
|
||||||
|
}
|
27
test/integration/bytecode/bytecode_assertions.go
Normal file
27
test/integration/bytecode/bytecode_assertions.go
Normal 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 ""
|
||||||
|
}
|
89
test/integration/bytecode/bytecode_case.go
Normal file
89
test/integration/bytecode/bytecode_case.go
Normal 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
|
||||||
|
}
|
19
test/integration/bytecode/bytecode_collect_test.go
Normal file
19
test/integration/bytecode/bytecode_collect_test.go
Normal 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),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
154
test/integration/bytecode/bytecode_member_test.go
Normal file
154
test/integration/bytecode/bytecode_member_test.go
Normal 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
|
||||||
|
// `),
|
||||||
|
})
|
||||||
|
}
|
18
test/integration/bytecode/bytecode_sort_test.go
Normal file
18
test/integration/bytecode/bytecode_sort_test.go
Normal 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),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
69
test/integration/bytecode/bytecode_string_test.go
Normal file
69
test/integration/bytecode/bytecode_string_test.go
Normal 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"),
|
||||||
|
})
|
||||||
|
}
|
@@ -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"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
16
test/integration/bytecode/shortcuts.go
Normal file
16
test/integration/bytecode/shortcuts.go
Normal 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
|
170
test/integration/vm/vm_case.go
Normal file
170
test/integration/vm/vm_case.go
Normal 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...)
|
||||||
|
}
|
@@ -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.
|
// 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) {
|
func TestCollect(t *testing.T) {
|
||||||
RunUseCases(t, []UseCase{
|
RunUseCases(t, []UseCase{
|
||||||
CaseCompilationError(`
|
SkipCaseCompilationError(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -64,7 +64,7 @@ func TestCollect(t *testing.T) {
|
|||||||
gender: gender
|
gender: gender
|
||||||
}
|
}
|
||||||
`, "Should not have access to initial variables"),
|
`, "Should not have access to initial variables"),
|
||||||
CaseCompilationError(`
|
SkipCaseCompilationError(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -139,7 +139,7 @@ LET users = [
|
|||||||
COLLECT gender = i.gender
|
COLLECT gender = i.gender
|
||||||
RETURN gender
|
RETURN gender
|
||||||
`, []any{"f", "m"}, "Should group result by a single key"),
|
`, []any{"f", "m"}, "Should group result by a single key"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -182,7 +182,7 @@ LET users = [
|
|||||||
map[string]int{"ageGroup": 9},
|
map[string]int{"ageGroup": 9},
|
||||||
map[string]int{"ageGroup": 13},
|
map[string]int{"ageGroup": 13},
|
||||||
}, "Should group result by a single key expression"),
|
}, "Should group result by a single key expression"),
|
||||||
Case(`
|
SkipCase(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -220,7 +220,7 @@ LET users = [
|
|||||||
RETURN gender)
|
RETURN gender)
|
||||||
RETURN grouped[0]
|
RETURN grouped[0]
|
||||||
`, "f", "Should return correct group key by an index"),
|
`, "f", "Should return correct group key by an index"),
|
||||||
CaseArray(
|
SkipCaseArray(
|
||||||
`LET users = [
|
`LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -263,7 +263,7 @@ LET users = [
|
|||||||
map[string]any{"age": 36, "gender": "m"},
|
map[string]any{"age": 36, "gender": "m"},
|
||||||
map[string]any{"age": 69, "gender": "m"},
|
map[string]any{"age": 69, "gender": "m"},
|
||||||
}, "Should group result by multiple keys"),
|
}, "Should group result by multiple keys"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -354,7 +354,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Should create default projection"),
|
}, "Should create default projection"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = []
|
LET users = []
|
||||||
FOR i IN users
|
FOR i IN users
|
||||||
COLLECT gender = i.gender INTO genders
|
COLLECT gender = i.gender INTO genders
|
||||||
@@ -363,7 +363,7 @@ LET users = [
|
|||||||
values: genders
|
values: genders
|
||||||
}
|
}
|
||||||
`, []any{}, "COLLECT gender = i.gender INTO genders: should return an empty array when source is empty"),
|
`, []any{}, "COLLECT gender = i.gender INTO genders: should return an empty array when source is empty"),
|
||||||
CaseArray(
|
SkipCaseArray(
|
||||||
`LET users = [
|
`LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -419,7 +419,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Should create custom projection"),
|
}, "Should create custom projection"),
|
||||||
CaseArray(
|
SkipCaseArray(
|
||||||
`LET users = [
|
`LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -496,7 +496,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Should create custom projection grouped by multiple keys"),
|
}, "Should create custom projection grouped by multiple keys"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -553,7 +553,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Should create default projection with default KEEP"),
|
}, "Should create default projection with default KEEP"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = []
|
LET users = []
|
||||||
FOR i IN users
|
FOR i IN users
|
||||||
LET married = i.married
|
LET married = i.married
|
||||||
@@ -563,7 +563,7 @@ LET users = [
|
|||||||
values: genders
|
values: genders
|
||||||
}
|
}
|
||||||
`, []any{}, "COLLECT gender = i.gender INTO genders KEEP married: Should return an empty array when source is empty"),
|
`, []any{}, "COLLECT gender = i.gender INTO genders KEEP married: Should return an empty array when source is empty"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -636,7 +636,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Should create default projection with default KEEP using multiple keys"),
|
}, "Should create default projection with default KEEP using multiple keys"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -693,7 +693,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Should create default projection with custom KEEP"),
|
}, "Should create default projection with custom KEEP"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -766,7 +766,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Should create default projection with custom KEEP using multiple keys"),
|
}, "Should create default projection with custom KEEP using multiple keys"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -823,7 +823,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Should create default projection with custom KEEP with custom name"),
|
}, "Should create default projection with custom KEEP with custom name"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -881,7 +881,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, "Should create default projection with custom KEEP with multiple custom names"),
|
}, "Should create default projection with custom KEEP with multiple custom names"),
|
||||||
CaseArray(
|
SkipCaseArray(
|
||||||
`LET users = [
|
`LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -931,7 +931,7 @@ LET users = [
|
|||||||
},
|
},
|
||||||
}, "Should group and count result by a single key"),
|
}, "Should group and count result by a single key"),
|
||||||
|
|
||||||
CaseArray(
|
SkipCaseArray(
|
||||||
`
|
`
|
||||||
LET users = []
|
LET users = []
|
||||||
FOR i IN users
|
FOR i IN users
|
||||||
@@ -941,7 +941,7 @@ LET users = [
|
|||||||
values: numberOfUsers
|
values: numberOfUsers
|
||||||
}
|
}
|
||||||
`, []any{}, "COLLECT gender = i.gender WITH COUNT INTO numberOfUsers: Should return empty array when source is empty"),
|
`, []any{}, "COLLECT gender = i.gender WITH COUNT INTO numberOfUsers: Should return empty array when source is empty"),
|
||||||
CaseArray(
|
SkipCaseArray(
|
||||||
`LET users = [
|
`LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@@ -980,7 +980,7 @@ LET users = [
|
|||||||
`, []any{
|
`, []any{
|
||||||
5,
|
5,
|
||||||
}, "Should just count the number of items in the source"),
|
}, "Should just count the number of items in the source"),
|
||||||
CaseArray(
|
SkipCaseArray(
|
||||||
`LET users = []
|
`LET users = []
|
||||||
FOR i IN users
|
FOR i IN users
|
||||||
COLLECT WITH COUNT INTO numberOfUsers
|
COLLECT WITH COUNT INTO numberOfUsers
|
||||||
@@ -988,7 +988,7 @@ LET users = [
|
|||||||
`, []any{
|
`, []any{
|
||||||
0,
|
0,
|
||||||
}, "Should return 0 when there are no items in the source"),
|
}, "Should return 0 when there are no items in the source"),
|
||||||
CaseArray(`
|
SkipCaseArray(`
|
||||||
LET users = [
|
LET users = [
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
|
@@ -19,7 +19,7 @@ func TestFor(t *testing.T) {
|
|||||||
// ShouldEqualJSON,
|
// ShouldEqualJSON,
|
||||||
//},
|
//},
|
||||||
RunUseCases(t, []UseCase{
|
RunUseCases(t, []UseCase{
|
||||||
CaseCompilationError(`
|
SkipCaseCompilationError(`
|
||||||
FOR foo IN foo
|
FOR foo IN foo
|
||||||
RETURN foo
|
RETURN foo
|
||||||
`, "Should not compile FOR foo IN foo"),
|
`, "Should not compile FOR foo IN foo"),
|
||||||
|
@@ -221,7 +221,7 @@ func TestOptionalChaining(t *testing.T) {
|
|||||||
"bar",
|
"bar",
|
||||||
),
|
),
|
||||||
CaseNil("RETURN ERROR()?.foo"),
|
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(`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"),
|
CaseArray(`FOR i IN [1, 2, 3, 4] ERROR()? RETURN i`, []any{1, 2, 3, 4}, "Error in FOR loop"),
|
||||||
|
Reference in New Issue
Block a user