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
-}