diff --git a/pkg/compiler/compiler_bench_test.go b/pkg/compiler/compiler_bench_test.go index 7d39e654..7043ffcd 100644 --- a/pkg/compiler/compiler_bench_test.go +++ b/pkg/compiler/compiler_bench_test.go @@ -1,6 +1,17 @@ package compiler_test -import "testing" +import ( + "testing" +) + +func BenchmarkStringLiteral(b *testing.B) { + RunBenchmark(b, ` + RETURN " +FOO +BAR +" + `) +} func BenchmarkEmptyArray(b *testing.B) { RunBenchmark(b, `RETURN []`) @@ -14,6 +25,44 @@ func BenchmarkEmptyObject(b *testing.B) { RunBenchmark(b, `RETURN {}`) } +func BenchmarkUnaryOperatorExcl(b *testing.B) { + RunBenchmark(b, `RETURN !TRUE`) +} + +func BenchmarkUnaryOperatorQ(b *testing.B) { + RunBenchmark(b, ` + LET foo = TRUE + RETURN !foo ? TRUE : FALSE + `) +} + +func BenchmarkUnaryOperatorN(b *testing.B) { + RunBenchmark(b, ` + LET v = 1 + RETURN -v + `) +} + +func BenchmarkTernaryOperator(b *testing.B) { + RunBenchmark(b, ` + LET a = "a" + LET b = "b" + LET c = FALSE + RETURN c ? a : b; + + `) +} + +func BenchmarkTernaryOperatorDef(b *testing.B) { + RunBenchmark(b, ` + LET a = "a" + LET b = "b" + LET c = FALSE + RETURN c ? : a; + + `) +} + func BenchmarkForEmpty(b *testing.B) { RunBenchmark(b, ` FOR i IN [] diff --git a/pkg/compiler/compiler_exec_test.go b/pkg/compiler/compiler_exec_test.go index 4f7d269b..9ac870f1 100644 --- a/pkg/compiler/compiler_exec_test.go +++ b/pkg/compiler/compiler_exec_test.go @@ -17,6 +17,65 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +func TestString(t *testing.T) { + RunUseCases(t, []UseCase{ + Case( + ` + RETURN " +FOO +BAR +" + `, "\nFOO\nBAR\n", "Should be possible to use multi line string"), + + CaseJSON( + fmt.Sprintf(` +RETURN %s + + + + GetTitle + + + Hello world + + %s +`, "`", "`"), ` + + + + GetTitle + + + Hello world + + `, "Should be possible to use multi line string with nested strings using backtick"), + + CaseJSON( + fmt.Sprintf(` +RETURN %s + + + + GetTitle + + + Hello world + + %s +`, "´", "´"), + ` + + + + GetTitle + + + Hello world + + `, "Should be possible to use multi line string with nested strings using tick"), + }) +} + func TestVariables(t *testing.T) { RunUseCases(t, []UseCase{ CaseNil(`LET i = NONE RETURN i`), @@ -401,6 +460,12 @@ func TestRegexpOperator(t *testing.T) { }) } +func TestAllArrayOperator(t *testing.T) { + RunUseCases(t, []UseCase{ + Case("RETURN [1,2,3] ALL IN [1,2,3]", true, "All elements are in"), + }) +} + func TestRange(t *testing.T) { RunUseCases(t, []UseCase{ CaseArray("RETURN 1..10", []any{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}), @@ -456,6 +521,13 @@ func TestFunctionCall(t *testing.T) { }) } +func TestBuiltinFunctions(t *testing.T) { + RunUseCases(t, []UseCase{ + Case("RETURN LENGTH([1,2,3])", 3), + Case("RETURN TYPENAME([1,2,3])", "array"), + }) +} + func TestMember(t *testing.T) { RunUseCases(t, []UseCase{ CaseNil("LET arr = [1,2,3,4] RETURN arr[10]"), @@ -844,110 +916,146 @@ func TestForFilter(t *testing.T) { `, []any{map[string]any{"age": 29, "gender": "f", "name": "Mary"}, map[string]any{"age": 36, "gender": "m", "name": "Peter"}}, ), - //{ - // ` - // LET users = [ - // { - // active: true, - // age: 31, - // gender: "m" - // }, - // { - // active: true, - // age: 29, - // gender: "f" - // }, - // { - // active: true, - // age: 36, - // gender: "m" - // } - // ] - // FOR u IN users - // FILTER u.active == true - // FILTER u.age < 35 - // RETURN u - // `, - // []any{map[string]any{"active": true, "gender": "m", "age": 31}, map[string]any{"active": true, "gender": "f", "age": 29}}, - // ShouldEqualJSON, - //}, - //{ - // ` - // LET users = [ - // { - // active: true, - // age: 31, - // gender: "m" - // }, - // { - // active: true, - // age: 29, - // gender: "f" - // }, - // { - // active: true, - // age: 36, - // gender: "m" - // }, - // { - // active: false, - // age: 69, - // gender: "m" - // } - // ] - // FOR u IN users - // FILTER u.active - // RETURN u - // `, - // []any{map[string]any{"active": true, "gender": "m", "age": 31}, map[string]any{"active": true, "gender": "f", "age": 29}, map[string]any{"active": true, "gender": "m", "age": 36}}, - // ShouldEqualJSON, - //}, - //{ - // ` - // LET users = [ - // { - // active: true, - // age: 31, - // gender: "m" - // }, - // { - // active: true, - // age: 29, - // gender: "f" - // }, - // { - // active: true, - // age: 36, - // gender: "m" - // }, - // { - // active: false, - // age: 69, - // gender: "m" - // } - // ] - // FOR u IN users - // FILTER u.active == true - // LIMIT 2 - // FILTER u.gender == "m" - // RETURN u - //`, - // []any{map[string]any{"active": true, "gender": "m", "age": 31}}, - // ShouldEqualJSON, - //}, + CaseArray( + ` + LET users = [ + { + active: true, + age: 31, + gender: "m" + }, + { + active: true, + age: 29, + gender: "f" + }, + { + active: true, + age: 36, + gender: "m" + } + ] + FOR u IN users + FILTER u.active == true + FILTER u.age < 35 + RETURN u + `, + []any{map[string]any{"active": true, "gender": "m", "age": 31}, map[string]any{"active": true, "gender": "f", "age": 29}}, + ), + CaseArray( + ` + LET users = [ + { + active: true, + age: 31, + gender: "m" + }, + { + active: true, + age: 29, + gender: "f" + }, + { + active: true, + age: 36, + gender: "m" + }, + { + active: false, + age: 69, + gender: "m" + } + ] + FOR u IN users + FILTER u.active + RETURN u + `, + []any{map[string]any{"active": true, "gender": "m", "age": 31}, map[string]any{"active": true, "gender": "f", "age": 29}, map[string]any{"active": true, "gender": "m", "age": 36}}, + ), + CaseArray( + ` + LET users = [ + { + active: true, + age: 31, + gender: "m" + }, + { + active: true, + age: 29, + gender: "f" + }, + { + active: true, + age: 36, + gender: "m" + }, + { + active: false, + age: 69, + gender: "m" + } + ] + FOR u IN users + FILTER u.active == true + LIMIT 2 + FILTER u.gender == "m" + RETURN u + `, + []any{map[string]any{"active": true, "gender": "m", "age": 31}}, + ), }) } func TestForLimit(t *testing.T) { RunUseCases(t, []UseCase{ - //{ - // ` - // FOR i IN [ 1, 2, 3, 4, 1, 3 ] - // LIMIT 2 - // RETURN i - //`, - // []any{1, 2}, - // ShouldEqualJSON, - //}, - }) + CaseArray( + ` + FOR i IN [ 1, 2, 3, 4, 1, 3 ] + LIMIT 2 + RETURN i + `, + []any{1, 2}), + CaseArray(` + FOR i IN [ 1,2,3,4,5,6,7,8 ] + LIMIT 4, 2 + RETURN i + `, []any{5, 6}), + CaseArray(` + FOR i IN [ 1,2,3,4,5,6,7,8 ] + LET x = i + LIMIT 2 + RETURN i*x + `, []any{1, 4}, + "Should be able to reuse values from a source"), + CaseArray(` + FOR i IN [ 1,2,3,4,5,6,7,8 ] + LET x = "foo" + TYPENAME(x) + LIMIT 2 + RETURN i + `, []any{1, 2}, "Should define variables and call functions"), + CaseArray(` + FOR i IN [ 1,2,3,4,5,6,7,8 ] + LIMIT LIMIT_VALUE() + RETURN i + `, []any{1, 2}, "Should be able to use function call"), + CaseArray(` + LET o = { + limit: 2 + } + FOR i IN [ 1,2,3,4,5,6,7,8 ] + LIMIT o.limit + RETURN i + `, []any{1, 2}, "Should be able to use object property"), + CaseArray(` + LET o = [1,2] + + FOR i IN [ 1,2,3,4,5,6,7,8 ] + LIMIT o[1] + RETURN i + `, []any{1, 2}, "Should be able to use array element"), + }, runtime.WithFunction("LIMIT_VALUE", func(ctx context.Context, args ...core.Value) (core.Value, error) { + return values.NewInt(2), nil + })) } diff --git a/pkg/compiler/compiler_setup_test.go b/pkg/compiler/compiler_setup_test.go index cdc3e432..858466f9 100644 --- a/pkg/compiler/compiler_setup_test.go +++ b/pkg/compiler/compiler_setup_test.go @@ -85,6 +85,14 @@ 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 []core.Value diff --git a/pkg/compiler/visitor.go b/pkg/compiler/visitor.go index d98cbcfd..fc756dce 100644 --- a/pkg/compiler/visitor.go +++ b/pkg/compiler/visitor.go @@ -306,6 +306,65 @@ func (v *visitor) VisitFilterClause(ctx *fql.FilterClauseContext) interface{} { return nil } +func (v *visitor) VisitLimitClause(ctx *fql.LimitClauseContext) interface{} { + clauses := ctx.AllLimitClauseValue() + + if len(clauses) == 1 { + return v.visitLimit(clauses[0].Accept(v).(runtime.Operand)) + } else { + v.visitOffset(clauses[0].Accept(v).(runtime.Operand)) + v.visitLimit(clauses[1].Accept(v).(runtime.Operand)) + } + + return nil +} + +func (v *visitor) visitOffset(src1 runtime.Operand) interface{} { + state := v.registers.Allocate(State) + v.emitter.EmitA(runtime.OpIncr, state) + + comp := v.registers.Allocate(Temp) + v.emitter.EmitABC(runtime.OpGt, comp, state, src1) + v.emitter.EmitJumpc(runtime.OpJumpIfFalse, v.loops.Loop().Next, comp) + + return state +} + +func (v *visitor) visitLimit(src1 runtime.Operand) interface{} { + state := v.registers.Allocate(State) + v.emitter.EmitA(runtime.OpIncr, state) + + comp := v.registers.Allocate(Temp) + v.emitter.EmitABC(runtime.OpGt, comp, state, src1) + v.emitter.EmitJumpc(runtime.OpJumpIfTrue, v.loops.Loop().Next, comp) + + return state +} + +func (v *visitor) VisitLimitClauseValue(ctx *fql.LimitClauseValueContext) interface{} { + if c := ctx.IntegerLiteral(); c != nil { + return c.Accept(v) + } + + if c := ctx.Param(); c != nil { + return c.Accept(v) + } + + if c := ctx.Variable(); c != nil { + return c.Accept(v) + } + + if c := ctx.FunctionCallExpression(); c != nil { + return c.Accept(v) + } + + if c := ctx.MemberExpression(); c != nil { + return c.Accept(v) + } + + panic(core.Error(ErrUnexpectedToken, ctx.GetText())) +} + func (v *visitor) VisitForExpressionStatement(ctx *fql.ForExpressionStatementContext) interface{} { if c := ctx.VariableDeclaration(); c != nil { return c.Accept(v) @@ -976,12 +1035,22 @@ func (v *visitor) visitFunctionCall(ctx *fql.FunctionCallContext, protected bool case "LENGTH": dst := v.registers.Allocate(Temp) - if seq == nil || len(seq.Registers) > 1 { + if seq == nil || len(seq.Registers) != 1 { panic(core.Error(core.ErrInvalidArgument, "LENGTH: expected 1 argument")) } v.emitter.EmitAB(runtime.OpLength, dst, seq.Registers[0]) + return dst + case "TYPENAME": + dst := v.registers.Allocate(Temp) + + if seq == nil || len(seq.Registers) != 1 { + panic(core.Error(core.ErrInvalidArgument, "TYPENAME: expected 1 argument")) + } + + v.emitter.EmitAB(runtime.OpType, dst, seq.Registers[0]) + return dst default: nameAndDest := v.loadConstant(v.functionName(ctx)) diff --git a/pkg/runtime/env.go b/pkg/runtime/env.go index 0a3117fb..989e754d 100644 --- a/pkg/runtime/env.go +++ b/pkg/runtime/env.go @@ -13,7 +13,16 @@ type ( } ) +var noopEnv = &Environment{ + functions: make(map[string]core.Function), + params: make([]core.Value, 0), +} + func newEnvironment(opts []EnvironmentOption) *Environment { + if len(opts) == 0 { + return noopEnv + } + env := &Environment{ functions: make(map[string]core.Function), params: make([]core.Value, 0), diff --git a/pkg/runtime/opcode.go b/pkg/runtime/opcode.go index a6e78993..8d054278 100644 --- a/pkg/runtime/opcode.go +++ b/pkg/runtime/opcode.go @@ -49,6 +49,7 @@ const ( OpLoadPropertyOptional OpLength + OpType OpCall OpProtectedCall diff --git a/pkg/runtime/vm.go b/pkg/runtime/vm.go index 3b8f9d6e..7a15f263 100644 --- a/pkg/runtime/vm.go +++ b/pkg/runtime/vm.go @@ -41,7 +41,6 @@ func (vm *VM) Run(ctx context.Context, opts []EnvironmentOption) (core.Value, er vm.pc = 0 program := vm.program - // TODO: Add panic handling and snapshot the last instruction and frame that caused it loop: for vm.pc < len(program.Bytecode) { inst := program.Bytecode[vm.pc] @@ -287,6 +286,8 @@ loop: types.Measurable, ) } + case OpType: + reg[dst] = values.String(core.Reflect(reg[src1]).Name()) case OpRange: res, err := operators.Range(reg[src1], reg[src2]) diff --git a/pkg/stdlib/types/lib.go b/pkg/stdlib/types/lib.go index e2bdbec5..0b0c57e2 100644 --- a/pkg/stdlib/types/lib.go +++ b/pkg/stdlib/types/lib.go @@ -27,7 +27,6 @@ func RegisterLib(ns core.Namespace) error { "IS_HTML_DOCUMENT": IsHTMLDocument, "IS_BINARY": IsBinary, "IS_NAN": IsNaN, - "TYPENAME": TypeName, })) } diff --git a/pkg/stdlib/types/type_name.go b/pkg/stdlib/types/type_name.go deleted file mode 100644 index 99bcba2a..00000000 --- a/pkg/stdlib/types/type_name.go +++ /dev/null @@ -1,19 +0,0 @@ -package types - -import ( - "context" - - "github.com/MontFerret/ferret/pkg/runtime/core" - "github.com/MontFerret/ferret/pkg/runtime/values" -) - -// TYPENAME returns the data type name of value. -// @param {Any} value - Input value of arbitrary type. -// @return {Boolean} - Returns string representation of a type. -func TypeName(_ context.Context, args ...core.Value) (core.Value, error) { - if err := core.ValidateArgs(args, 1, 1); err != nil { - return values.None, err - } - - return values.NewString(core.Reflect(args[0]).Name()), nil -}