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:
@@ -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 {
|
||||
|
@@ -72,20 +72,20 @@ func (e *Emitter) EmitBoolean(dst vm.Operand, value bool) {
|
||||
|
||||
// ─── 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -136,58 +136,20 @@ 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
|
||||
seq := lc.ctx.ExprCompiler.CompileArgumentList(ctx.ArgumentList())
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
seq := lc.ctx.Registers.AllocateSequence(len(assignments) * 2)
|
||||
if size > 0 {
|
||||
seq = lc.ctx.Registers.AllocateSequence(len(assignments) * 2)
|
||||
|
||||
for i := 0; i < size; i++ {
|
||||
var propOp vm.Operand
|
||||
@@ -215,6 +177,7 @@ func (lc *LiteralCompiler) CompileObjectLiteral(ctx fql.IObjectLiteralContext) v
|
||||
//lc.ctx.Registers.Free(propOp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
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)
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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)))
|
||||
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 {
|
||||
out.Write([]byte(fmt.Sprintf("CALL %s %s %s", dst, src1, src2)))
|
||||
}
|
||||
case OpReturn:
|
||||
out.Write([]byte(fmt.Sprintf("RET")))
|
||||
default:
|
||||
return
|
||||
w.Write([]byte(fmt.Sprintf("%d: %s", pos, inst.Opcode)))
|
||||
}
|
||||
}
|
||||
|
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 (
|
||||
"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)
|
||||
|
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.
|
||||
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,
|
||||
|
@@ -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"),
|
||||
|
@@ -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"),
|
||||
|
Reference in New Issue
Block a user