diff --git a/pkg/compiler/internal/core/constants.go b/pkg/compiler/internal/core/constants.go index 246a462d..98f2121d 100644 --- a/pkg/compiler/internal/core/constants.go +++ b/pkg/compiler/internal/core/constants.go @@ -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 { diff --git a/pkg/compiler/internal/core/emitter_helpers.go b/pkg/compiler/internal/core/emitter_helpers.go index cf4c4622..8bb5eb57 100644 --- a/pkg/compiler/internal/core/emitter_helpers.go +++ b/pkg/compiler/internal/core/emitter_helpers.go @@ -72,20 +72,20 @@ func (e *Emitter) EmitBoolean(dst vm.Operand, value bool) { // ─── Data Structures ────────────────────────────────────────────────────── -func (e *Emitter) EmitEmptyList(dst vm.Operand) { - e.EmitA(vm.OpList, dst) -} - func (e *Emitter) EmitList(dst vm.Operand, seq RegisterSequence) { - e.EmitAs(vm.OpList, dst, seq) -} - -func (e *Emitter) EmitEmptyMap(dst vm.Operand) { - e.EmitA(vm.OpMap, dst) + if len(seq) > 0 { + e.EmitAs(vm.OpList, dst, seq) + } else { + e.EmitA(vm.OpList, dst) + } } func (e *Emitter) EmitMap(dst vm.Operand, seq RegisterSequence) { - e.EmitAs(vm.OpMap, dst, seq) + if len(seq) > 0 { + e.EmitAs(vm.OpMap, dst, seq) + } else { + e.EmitA(vm.OpMap, dst) + } } func (e *Emitter) EmitRange(dst, start, end vm.Operand) { diff --git a/pkg/compiler/internal/core/registers.go b/pkg/compiler/internal/core/registers.go index 48a51f50..a43a625e 100644 --- a/pkg/compiler/internal/core/registers.go +++ b/pkg/compiler/internal/core/registers.go @@ -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 { diff --git a/pkg/compiler/internal/core/symbols.go b/pkg/compiler/internal/core/symbols.go index 6114c3ef..9be869a9 100644 --- a/pkg/compiler/internal/core/symbols.go +++ b/pkg/compiler/internal/core/symbols.go @@ -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 } } diff --git a/pkg/compiler/internal/expr.go b/pkg/compiler/internal/expr.go index 9bccf2bd..d352ae22 100644 --- a/pkg/compiler/internal/expr.go +++ b/pkg/compiler/internal/expr.go @@ -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 diff --git a/pkg/compiler/internal/literal.go b/pkg/compiler/internal/literal.go index f6ffe2e6..32adf51f 100644 --- a/pkg/compiler/internal/literal.go +++ b/pkg/compiler/internal/literal.go @@ -136,83 +136,46 @@ func (lc *LiteralCompiler) CompileNoneLiteral(_ fql.INoneLiteralContext) vm.Oper func (lc *LiteralCompiler) CompileArrayLiteral(ctx fql.IArrayLiteralContext) vm.Operand { // Allocate destination register for the array destReg := lc.ctx.Registers.Allocate(core.Temp) - - if list := ctx.ArgumentList(); list != nil { - // Get all array element expressions - exps := list.(fql.IArgumentListContext).AllExpression() - size := len(exps) - - if size > 0 { - // Allocate seq for array elements - seq := lc.ctx.Registers.AllocateSequence(size) - - // Evaluate each element into seq Registers - for i, exp := range exps { - // Compile expression and move to seq register - srcReg := lc.ctx.ExprCompiler.Compile(exp) - - // TODO: Figure out how to remove OpMove and use Registers returned from each expression - lc.ctx.Emitter.EmitMove(seq[i], srcReg) - - // Free source register if temporary - if srcReg.IsRegister() { - //lc.ctx.Registers.Free(srcReg) - } - } - - // Initialize an array - lc.ctx.Emitter.EmitList(destReg, seq) - - // Free seq Registers - //lc.ctx.Registers.FreeSequence(seq) - - return destReg - } - } - - // Empty array - lc.ctx.Emitter.EmitEmptyList(destReg) + seq := lc.ctx.ExprCompiler.CompileArgumentList(ctx.ArgumentList()) + lc.ctx.Emitter.EmitList(destReg, seq) return destReg } func (lc *LiteralCompiler) CompileObjectLiteral(ctx fql.IObjectLiteralContext) vm.Operand { dst := lc.ctx.Registers.Allocate(core.Temp) + var seq core.RegisterSequence assignments := ctx.AllPropertyAssignment() size := len(assignments) - if size == 0 { - lc.ctx.Emitter.EmitEmptyMap(dst) + if size > 0 { + seq = lc.ctx.Registers.AllocateSequence(len(assignments) * 2) - return dst - } + for i := 0; i < size; i++ { + var propOp vm.Operand + var valOp vm.Operand + pac := assignments[i] - seq := lc.ctx.Registers.AllocateSequence(len(assignments) * 2) + if prop := pac.PropertyName(); prop != nil { + propOp = lc.CompilePropertyName(prop) + valOp = lc.ctx.ExprCompiler.Compile(pac.Expression()) + } else if comProp := pac.ComputedPropertyName(); comProp != nil { + propOp = lc.CompileComputedPropertyName(comProp) + valOp = lc.ctx.ExprCompiler.Compile(pac.Expression()) + } else if variable := pac.Variable(); variable != nil { + propOp = loadConstant(lc.ctx, runtime.NewString(variable.GetText())) + valOp = lc.ctx.ExprCompiler.CompileVariable(variable) + } - for i := 0; i < size; i++ { - var propOp vm.Operand - var valOp vm.Operand - pac := assignments[i] + regIndex := i * 2 - if prop := pac.PropertyName(); prop != nil { - propOp = lc.CompilePropertyName(prop) - valOp = lc.ctx.ExprCompiler.Compile(pac.Expression()) - } else if comProp := pac.ComputedPropertyName(); comProp != nil { - propOp = lc.CompileComputedPropertyName(comProp) - valOp = lc.ctx.ExprCompiler.Compile(pac.Expression()) - } else if variable := pac.Variable(); variable != nil { - propOp = loadConstant(lc.ctx, runtime.NewString(variable.GetText())) - valOp = lc.ctx.ExprCompiler.CompileVariable(variable) - } + lc.ctx.Emitter.EmitMove(seq[regIndex], propOp) + lc.ctx.Emitter.EmitMove(seq[regIndex+1], valOp) - regIndex := i * 2 - - lc.ctx.Emitter.EmitMove(seq[regIndex], propOp) - lc.ctx.Emitter.EmitMove(seq[regIndex+1], valOp) - - // Free source register if temporary - if propOp.IsRegister() { - //lc.ctx.Registers.Free(propOp) + // Free source register if temporary + if propOp.IsRegister() { + //lc.ctx.Registers.Free(propOp) + } } } diff --git a/pkg/compiler/internal/loop.go b/pkg/compiler/internal/loop.go index 898f2fb2..fc6836e1 100644 --- a/pkg/compiler/internal/loop.go +++ b/pkg/compiler/internal/loop.go @@ -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) diff --git a/pkg/compiler/internal/loop_collect.go b/pkg/compiler/internal/loop_collect.go index d6bf718c..e9ec3a27 100644 --- a/pkg/compiler/internal/loop_collect.go +++ b/pkg/compiler/internal/loop_collect.go @@ -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) diff --git a/pkg/vm/instruction.go b/pkg/vm/instruction.go index ae5f7424..0c05b9a0 100644 --- a/pkg/vm/instruction.go +++ b/pkg/vm/instruction.go @@ -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() } diff --git a/pkg/vm/opcode.go b/pkg/vm/opcode.go index 23deb64d..c270b373 100644 --- a/pkg/vm/opcode.go +++ b/pkg/vm/opcode.go @@ -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: diff --git a/pkg/vm/operand.go b/pkg/vm/operand.go index 824a20a5..20a5386e 100644 --- a/pkg/vm/operand.go +++ b/pkg/vm/operand.go @@ -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) } diff --git a/pkg/vm/program.go b/pkg/vm/program.go index fc4499b6..fab14b20 100644 --- a/pkg/vm/program.go +++ b/pkg/vm/program.go @@ -23,14 +23,15 @@ type ( } ) -func (program *Program) Disassemble() string { +func (program *Program) String() string { var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', 0) - for offset := 0; offset < len(program.Bytecode); { - instruction := program.Bytecode[offset] - program.disassembleInstruction(w, instruction, offset) - offset++ + var counter int + + for _, inst := range program.Bytecode { + counter++ + program.writeInstruction(w, counter, inst) w.Write([]byte("\n")) } @@ -39,33 +40,10 @@ func (program *Program) Disassemble() string { return buf.String() } -func (program *Program) disassembleInstruction(out io.Writer, inst Instruction, offset int) { - opcode := inst.Opcode - out.Write([]byte(fmt.Sprintf("%d: [%d] ", offset, opcode))) - dst, src1, src2 := inst.Operands[0], inst.Operands[1], inst.Operands[2] - - switch opcode { - case OpMove: - out.Write([]byte(fmt.Sprintf("MOVE %s %s", dst, src1))) - case OpLoadNone: - out.Write([]byte(fmt.Sprintf("LOADN %s", dst))) - case OpLoadBool: - out.Write([]byte(fmt.Sprintf("LOADB %s %d", dst, src1))) - case OpLoadConst: - out.Write([]byte(fmt.Sprintf("LOADC %s %s", dst, src1))) - case OpLoadGlobal: - out.Write([]byte(fmt.Sprintf("LOADG %s %s", dst, src1))) - case OpStoreGlobal: - out.Write([]byte(fmt.Sprintf("STOREG %s %s", dst, src1))) - case OpCall: - if src1 == 0 { - out.Write([]byte(fmt.Sprintf("CALL %s", dst))) - } else { - out.Write([]byte(fmt.Sprintf("CALL %s %s %s", dst, src1, src2))) - } - case OpReturn: - out.Write([]byte(fmt.Sprintf("RET"))) - default: - return +func (program *Program) writeInstruction(w io.Writer, pos int, inst Instruction) { + if inst.Opcode != OpReturn { + w.Write([]byte(fmt.Sprintf("%d: %s", pos, inst))) + } else { + w.Write([]byte(fmt.Sprintf("%d: %s", pos, inst.Opcode))) } } diff --git a/test/integration/base/assertions.go b/test/integration/base/assertions.go new file mode 100644 index 00000000..f05c4c6d --- /dev/null +++ b/test/integration/base/assertions.go @@ -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 "" +} diff --git a/test/integration/base/exec.go b/test/integration/base/exec.go new file mode 100644 index 00000000..83d7b21d --- /dev/null +++ b/test/integration/base/exec.go @@ -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 +} diff --git a/test/integration/base/setup.go b/test/integration/base/setup.go index 401c630f..1cb7bcda 100644 --- a/test/integration/base/setup.go +++ b/test/integration/base/setup.go @@ -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) diff --git a/test/integration/base/use_case.go b/test/integration/base/use_case.go new file mode 100644 index 00000000..4e747cfe --- /dev/null +++ b/test/integration/base/use_case.go @@ -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 +} diff --git a/test/integration/bytecode/bytecode_assertions.go b/test/integration/bytecode/bytecode_assertions.go new file mode 100644 index 00000000..f9aba1df --- /dev/null +++ b/test/integration/bytecode/bytecode_assertions.go @@ -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 "" +} diff --git a/test/integration/bytecode/bytecode_case.go b/test/integration/bytecode/bytecode_case.go new file mode 100644 index 00000000..ac1c03f7 --- /dev/null +++ b/test/integration/bytecode/bytecode_case.go @@ -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 +} diff --git a/test/integration/bytecode/bytecode_collect_test.go b/test/integration/bytecode/bytecode_collect_test.go new file mode 100644 index 00000000..51aff5e9 --- /dev/null +++ b/test/integration/bytecode/bytecode_collect_test.go @@ -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), + }), + }) +} diff --git a/test/integration/bytecode/bytecode_member_test.go b/test/integration/bytecode/bytecode_member_test.go new file mode 100644 index 00000000..76384c1b --- /dev/null +++ b/test/integration/bytecode/bytecode_member_test.go @@ -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 + // `), + }) +} diff --git a/test/integration/bytecode/bytecode_sort_test.go b/test/integration/bytecode/bytecode_sort_test.go new file mode 100644 index 00000000..b0511803 --- /dev/null +++ b/test/integration/bytecode/bytecode_sort_test.go @@ -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), + }), + }) +} diff --git a/test/integration/bytecode/bytecode_string_test.go b/test/integration/bytecode/bytecode_string_test.go new file mode 100644 index 00000000..501b9408 --- /dev/null +++ b/test/integration/bytecode/bytecode_string_test.go @@ -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 + // + //
+ // + //