From a15e95c72a90fe83655598f3e76dc2d41901a2ea Mon Sep 17 00:00:00 2001 From: Tim Voronov Date: Wed, 23 Oct 2024 17:49:31 -0400 Subject: [PATCH] wip --- pkg/compiler/allocator.go | 166 +++ pkg/compiler/compiler_bench_test.go | 39 + pkg/compiler/compiler_eq_test.go | 56 - pkg/compiler/compiler_filter_test.go | 146 --- pkg/compiler/compiler_for_test.go | 178 --- pkg/compiler/compiler_for_while_test.go | 38 - pkg/compiler/compiler_func_test.go | 56 - pkg/compiler/compiler_like_test.go | 48 - pkg/compiler/compiler_limit_test.go | 20 - pkg/compiler/compiler_logical_test.go | 92 -- pkg/compiler/compiler_member_test.go | 499 -------- pkg/compiler/compiler_range_test.go | 39 - .../{setup_test.go => compiler_setup_test.go} | 0 pkg/compiler/compiler_ternary_test.go | 110 -- pkg/compiler/compiler_test.go | 1105 +++++++++++++++++ pkg/compiler/registers.go | 125 -- pkg/compiler/result.go | 26 - pkg/compiler/visitor.go | 211 ++-- pkg/runtime/instruction.go | 2 +- pkg/runtime/opcode.go | 16 +- pkg/runtime/program.go | 2 +- pkg/runtime/vm.go | 2 + 22 files changed, 1430 insertions(+), 1546 deletions(-) create mode 100644 pkg/compiler/allocator.go create mode 100644 pkg/compiler/compiler_bench_test.go delete mode 100644 pkg/compiler/compiler_eq_test.go delete mode 100644 pkg/compiler/compiler_filter_test.go delete mode 100644 pkg/compiler/compiler_for_test.go delete mode 100644 pkg/compiler/compiler_for_while_test.go delete mode 100644 pkg/compiler/compiler_func_test.go delete mode 100644 pkg/compiler/compiler_like_test.go delete mode 100644 pkg/compiler/compiler_limit_test.go delete mode 100644 pkg/compiler/compiler_logical_test.go delete mode 100644 pkg/compiler/compiler_member_test.go delete mode 100644 pkg/compiler/compiler_range_test.go rename pkg/compiler/{setup_test.go => compiler_setup_test.go} (100%) delete mode 100644 pkg/compiler/compiler_ternary_test.go delete mode 100644 pkg/compiler/registers.go delete mode 100644 pkg/compiler/result.go diff --git a/pkg/compiler/allocator.go b/pkg/compiler/allocator.go new file mode 100644 index 000000000..082b87d76 --- /dev/null +++ b/pkg/compiler/allocator.go @@ -0,0 +1,166 @@ +package compiler + +type ( + // RegisterStatus tracks register usage + RegisterStatus struct { + IsAllocated bool + LastUse int // Instruction number of last use + NextUse int // Instruction number of next use + VarName string // Associated variable name, if any + VarType VarType // Type of variable stored + Lifetime *RegisterLifetime // Lifetime information + } + + RegisterLifetime struct { + Start int // Instruction number where register becomes live + End int // Instruction number where register dies + } + + // RegisterAllocator manages register allocation + RegisterAllocator struct { + registers map[Register]*RegisterStatus + nextRegister Register + currentInstr int + lifetimes map[string]*RegisterLifetime + usageGraph map[Register]map[Register]bool + } +) + +func NewRegisterAllocator() *RegisterAllocator { + return &RegisterAllocator{ + registers: make(map[Register]*RegisterStatus), + nextRegister: 0, + lifetimes: make(map[string]*RegisterLifetime), + usageGraph: make(map[Register]map[Register]bool), + } +} + +func (ra *RegisterAllocator) AllocateLocalVarRegister(name string) Register { + // Allocate register + reg := ra.AllocateRegister(VarLocal) + + // Update register status + ra.registers[reg].VarName = name + + return reg +} + +// AllocateRegister assigns a register based on variable type +func (ra *RegisterAllocator) AllocateRegister(varType VarType) Register { + // Try to find a free register first + reg, found := ra.findFreeRegister() + + if found { + return reg + } + + // If no free registers, create a new one + newReg := ra.nextRegister + ra.nextRegister++ + + // Initialize register status + ra.registers[newReg] = &RegisterStatus{ + IsAllocated: true, + LastUse: ra.currentInstr, + NextUse: -1, + VarType: varType, + Lifetime: &RegisterLifetime{Start: ra.currentInstr}, + } + + return newReg +} + +// FreeRegister marks a register as available +func (ra *RegisterAllocator) FreeRegister(reg Register) { + if status, exists := ra.registers[reg]; exists { + status.IsAllocated = false + status.Lifetime.End = ra.currentInstr + + // Clean up interference graph + delete(ra.usageGraph, reg) + + for _, edges := range ra.usageGraph { + delete(edges, reg) + } + } +} + +// findFreeRegister looks for an unused register +func (ra *RegisterAllocator) findFreeRegister() (Register, bool) { + // First, try to find a completely free register + for reg, status := range ra.registers { + if !status.IsAllocated { + return reg, true + } + } + + // If no free registers, try to find one that's no longer needed + var candidate Register + var found bool + maxLastUse := -1 + + for reg, status := range ra.registers { + if status.NextUse == -1 && status.LastUse > maxLastUse { + maxLastUse = status.LastUse + candidate = reg + found = true + } + } + + if found { + // Free the candidate register + ra.FreeRegister(candidate) + + return candidate, true + } + + return 0, false +} + +// UpdateRegisterUse updates the usage information for a register +func (ra *RegisterAllocator) UpdateRegisterUse(reg Register) { + status := ra.registers[reg] + + if status == nil { + return + } + + status.LastUse = ra.currentInstr + + // Update interference graph for simultaneously live registers + for otherReg, otherStatus := range ra.registers { + if otherReg != reg && otherStatus.IsAllocated && + ra.registersInterfere(reg, otherReg) { + ra.addInterference(reg, otherReg) + } + } + + ra.currentInstr++ +} + +// registersInterfere checks if two registers have overlapping lifetimes +func (ra *RegisterAllocator) registersInterfere(reg1, reg2 Register) bool { + status1 := ra.registers[reg1] + status2 := ra.registers[reg2] + + if status1 == nil || status2 == nil { + return false + } + + // Registers interfere if their lifetimes overlap + return status1.Lifetime.Start <= status2.Lifetime.End && + status2.Lifetime.Start <= status1.Lifetime.End +} + +// addInterference records that two registers interfere +func (ra *RegisterAllocator) addInterference(reg1, reg2 Register) { + if ra.usageGraph[reg1] == nil { + ra.usageGraph[reg1] = make(map[Register]bool) + } + if ra.usageGraph[reg2] == nil { + ra.usageGraph[reg2] = make(map[Register]bool) + } + + ra.usageGraph[reg1][reg2] = true + ra.usageGraph[reg2][reg1] = true +} diff --git a/pkg/compiler/compiler_bench_test.go b/pkg/compiler/compiler_bench_test.go new file mode 100644 index 000000000..5c42519f0 --- /dev/null +++ b/pkg/compiler/compiler_bench_test.go @@ -0,0 +1,39 @@ +package compiler_test + +import "testing" + +func BenchmarkForEmpty(b *testing.B) { + RunBenchmark(b, ` + FOR i IN [] + RETURN i + `) +} + +func BenchmarkForStaticArray(b *testing.B) { + RunBenchmark(b, ` + FOR i IN [1,2,3,4,5,6,7,8,9,10] + RETURN i + `) +} + +func BenchmarkForRange(b *testing.B) { + RunBenchmark(b, ` + FOR i IN 1..10 + RETURN i + `) +} + +func BenchmarkForObject(b *testing.B) { + RunBenchmark(b, ` + FOR i IN {"1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9":9, "10":10} + RETURN i + `) +} + +func BenchmarkForNested(b *testing.B) { + RunBenchmark(b, ` + FOR prop IN ["a"] + FOR val IN [1, 2, 3] + RETURN {[prop]: val} + `) +} diff --git a/pkg/compiler/compiler_eq_test.go b/pkg/compiler/compiler_eq_test.go deleted file mode 100644 index 290386113..000000000 --- a/pkg/compiler/compiler_eq_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package compiler_test - -import ( - "context" - "testing" - - runtime2 "github.com/MontFerret/ferret/pkg/runtime" - - . "github.com/smartystreets/goconvey/convey" - - "github.com/MontFerret/ferret/pkg/compiler" -) - -func TestEqualityOperators(t *testing.T) { - Convey("Equality operators", t, func() { - run := func(p *runtime2.Program) (string, error) { - vm := runtime2.NewVM() - - out, err := vm.Run(context.Background(), p) - - return string(out), err - } - - type UseCase struct { - Operator string - Expected bool - } - - useCases := []UseCase{ - {">", true}, - {"==", false}, - {">=", true}, - {"<", false}, - {"!=", true}, - {"<=", false}, - } - - for _, useCase := range useCases { - Convey("Should compile RETURN 2 "+useCase.Operator+" 1", func() { - c := compiler.New() - - p, err := c.Compile(` - RETURN 2 ` + useCase.Operator + ` 1 - `) - - So(err, ShouldBeNil) - So(p, ShouldHaveSameTypeAs, &runtime2.Program{}) - - out, err := run(p) - - So(err, ShouldBeNil) - So(out == "true", ShouldEqual, useCase.Expected) - }) - } - }) -} diff --git a/pkg/compiler/compiler_filter_test.go b/pkg/compiler/compiler_filter_test.go deleted file mode 100644 index aef35ad7f..000000000 --- a/pkg/compiler/compiler_filter_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package compiler_test - -import ( - . "github.com/smartystreets/goconvey/convey" - "testing" -) - -func TestForFilter(t *testing.T) { - RunUseCases(t, []UseCase{ - { - ` - FOR i IN [ 1, 2, 3, 4, 1, 3 ] - FILTER i > 2 - RETURN i - `, - []any{3, 4, 3}, - ShouldEqualJSON, - }, - { - ` - FOR i IN [ 1, 2, 3, 4, 1, 3 ] - FILTER i > 1 AND i < 4 - RETURN i - `, - []any{2, 3, 3}, - ShouldEqualJSON, - }, - { - ` - LET users = [ - { - age: 31, - gender: "m", - name: "Josh" - }, - { - age: 29, - gender: "f", - name: "Mary" - }, - { - age: 36, - gender: "m", - name: "Peter" - } - ] - FOR u IN users - FILTER u.name =~ "r" - RETURN u - `, - []any{map[string]any{"age": 29, "gender": "f", "name": "Mary"}, map[string]any{"age": 36, "gender": "m", "name": "Peter"}}, - ShouldEqualJSON, - }, - { - ` - 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, - }, - }) -} diff --git a/pkg/compiler/compiler_for_test.go b/pkg/compiler/compiler_for_test.go deleted file mode 100644 index 26697b1d3..000000000 --- a/pkg/compiler/compiler_for_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package compiler_test - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestFor(t *testing.T) { - // Should not allocate memory if NONE is a return statement - //{ - // `FOR i IN 0..100 - // RETURN NONE`, - // []any{}, - // ShouldEqualJSON, - //}, - RunUseCases(t, []UseCase{ - { - "FOR i IN 1..5 RETURN i", - []any{1, 2, 3, 4, 5}, - ShouldEqualJSON, - }, - { - `FOR i IN 1..5 - LET x = i - PRINT(x) - RETURN i - `, - []any{1, 2, 3, 4, 5}, - ShouldEqualJSON, - }, - { - `FOR val, counter IN 1..5 - LET x = val - PRINT(counter) - LET y = counter - RETURN [x, y] - `, - []any{[]any{1, 0}, []any{2, 1}, []any{3, 2}, []any{4, 3}, []any{5, 4}}, - ShouldEqualJSON, - }, - { - `FOR i IN [] RETURN i - `, - []any{}, - ShouldEqualJSON, - }, - { - `FOR i IN [1, 2, 3] RETURN i - `, - []any{1, 2, 3}, - ShouldEqualJSON, - }, - - { - `FOR i, k IN [1, 2, 3] RETURN k`, - []any{0, 1, 2}, - ShouldEqualJSON, - }, - { - `FOR i IN ['foo', 'bar', 'qaz'] RETURN i`, - []any{"foo", "bar", "qaz"}, - ShouldEqualJSON, - }, - { - `FOR i IN {a: 'bar', b: 'foo', c: 'qaz'} RETURN i`, - []any{"foo", "bar", "qaz"}, - ShouldHaveSameItems, - }, - { - `FOR i, k IN {a: 'foo', b: 'bar', c: 'qaz'} RETURN k`, - []any{"a", "b", "c"}, - ShouldHaveSameItems, - }, - { - `FOR i IN [{name: 'foo'}, {name: 'bar'}, {name: 'qaz'}] RETURN i.name`, - []any{"foo", "bar", "qaz"}, - ShouldHaveSameItems, - }, - { - `FOR i IN { items: [{name: 'foo'}, {name: 'bar'}, {name: 'qaz'}] }.items RETURN i.name`, - []any{"foo", "bar", "qaz"}, - ShouldHaveSameItems, - }, - { - `FOR prop IN ["a"] - FOR val IN [1, 2, 3] - RETURN {[prop]: val}`, - []any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}}, - ShouldEqualJSON, - }, - { - `FOR val IN 1..3 - FOR prop IN ["a"] - RETURN {[prop]: val}`, - []any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}}, - ShouldEqualJSON, - }, - { - `FOR prop IN ["a"] - FOR val IN 1..3 - RETURN {[prop]: val}`, - []any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}}, - ShouldEqualJSON, - }, - { - `FOR prop IN ["a"] - FOR val IN [1, 2, 3] - FOR val2 IN [1, 2, 3] - RETURN { [prop]: [val, val2] }`, - []any{map[string]any{"a": []int{1, 1}}, map[string]any{"a": []int{1, 2}}, map[string]any{"a": []int{1, 3}}, map[string]any{"a": []int{2, 1}}, map[string]any{"a": []int{2, 2}}, map[string]any{"a": []int{2, 3}}, map[string]any{"a": []int{3, 1}}, map[string]any{"a": []int{3, 2}}, map[string]any{"a": []int{3, 3}}}, - ShouldEqualJSON, - }, - { - `FOR val IN [1, 2, 3] - RETURN ( - FOR prop IN ["a", "b", "c"] - RETURN { [prop]: val } - )`, - []any{[]any{map[string]any{"a": 1}, map[string]any{"b": 1}, map[string]any{"c": 1}}, []any{map[string]any{"a": 2}, map[string]any{"b": 2}, map[string]any{"c": 2}}, []any{map[string]any{"a": 3}, map[string]any{"b": 3}, map[string]any{"c": 3}}}, - ShouldEqualJSON, - }, - { - `FOR val IN [1, 2, 3] - LET sub = ( - FOR prop IN ["a", "b", "c"] - RETURN { [prop]: val } - ) - - RETURN sub`, - []any{[]any{map[string]any{"a": 1}, map[string]any{"b": 1}, map[string]any{"c": 1}}, []any{map[string]any{"a": 2}, map[string]any{"b": 2}, map[string]any{"c": 2}}, []any{map[string]any{"a": 3}, map[string]any{"b": 3}, map[string]any{"c": 3}}}, - ShouldEqualJSON, - }, - { - `FOR i IN [ 1, 2, 3, 4, 1, 3 ] - RETURN DISTINCT i - `, - []any{1, 2, 3, 4}, - ShouldEqualJSON, - }, - }) -} - -func BenchmarkForEmpty(b *testing.B) { - RunBenchmark(b, ` - FOR i IN [] - RETURN i - `) -} - -func BenchmarkForStaticArray(b *testing.B) { - RunBenchmark(b, ` - FOR i IN [1,2,3,4,5,6,7,8,9,10] - RETURN i - `) -} - -func BenchmarkForRange(b *testing.B) { - RunBenchmark(b, ` - FOR i IN 1..10 - RETURN i - `) -} - -func BenchmarkForObject(b *testing.B) { - RunBenchmark(b, ` - FOR i IN {"1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9":9, "10":10} - RETURN i - `) -} - -func BenchmarkForNested(b *testing.B) { - RunBenchmark(b, ` - FOR prop IN ["a"] - FOR val IN [1, 2, 3] - RETURN {[prop]: val} - `) -} diff --git a/pkg/compiler/compiler_for_while_test.go b/pkg/compiler/compiler_for_while_test.go deleted file mode 100644 index 41a865b02..000000000 --- a/pkg/compiler/compiler_for_while_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package compiler_test - -import ( - "context" - "testing" - - "github.com/MontFerret/ferret/pkg/runtime" - "github.com/MontFerret/ferret/pkg/runtime/core" - "github.com/MontFerret/ferret/pkg/runtime/values" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestForWhile(t *testing.T) { - var counter int64 - RunUseCases(t, []UseCase{ - //{ - // "FOR i WHILE false RETURN i", - // []any{}, - // ShouldEqualJSON, - //}, - { - "FOR i WHILE UNTIL(5) RETURN i", - []any{0, 1, 2, 3, 4}, - ShouldEqualJSON, - }, - }, runtime.WithFunctions(map[string]core.Function{ - "UNTIL": func(ctx context.Context, args ...core.Value) (core.Value, error) { - if counter < int64(values.ToInt(args[0])) { - counter++ - - return values.True, nil - } - - return values.False, nil - }, - })) -} diff --git a/pkg/compiler/compiler_func_test.go b/pkg/compiler/compiler_func_test.go deleted file mode 100644 index ba8621539..000000000 --- a/pkg/compiler/compiler_func_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package compiler_test - -import ( - . "github.com/smartystreets/goconvey/convey" - "testing" -) - -func TestFunctionCall(t *testing.T) { - RunUseCases(t, []UseCase{ - { - "RETURN TYPENAME(1)", - "int", - nil, - }, - { - "WAIT(10) RETURN 1", - 1, - nil, - }, - { - "LET duration = 10 WAIT(duration) RETURN 1", - 1, - nil, - }, - { - "RETURN (FALSE OR T::FAIL())?", - nil, - nil, - }, - { - "RETURN T::FAIL()?", - nil, - nil, - }, - { - `FOR i IN [1, 2, 3, 4] - LET duration = 10 - - WAIT(duration) - - RETURN i * 2`, - []int{2, 4, 6, 8}, - ShouldEqualJSON, - }, - { - `RETURN FIRST((FOR i IN 1..10 RETURN i * 2))`, - 2, - nil, - }, - { - `RETURN UNION((FOR i IN 0..5 RETURN i), (FOR i IN 6..10 RETURN i))`, - []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, - ShouldEqualJSON, - }, - }) -} diff --git a/pkg/compiler/compiler_like_test.go b/pkg/compiler/compiler_like_test.go deleted file mode 100644 index 901cfe25d..000000000 --- a/pkg/compiler/compiler_like_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package compiler_test - -import ( - "testing" -) - -func TestLikeOperator(t *testing.T) { - RunUseCases(t, []UseCase{ - {`RETURN "foo" LIKE "f*"`, true, nil}, - {`RETURN "foo" LIKE "b*"`, false, nil}, - {`RETURN "foo" NOT LIKE "f*"`, false, nil}, - {`RETURN "foo" NOT LIKE "b*"`, true, nil}, - {`LET res = "foo" LIKE "f*" - RETURN res`, true, nil}, - {`RETURN ("foo" LIKE "b*") ? "foo" : "bar"`, `bar`, nil}, - {`RETURN ("foo" NOT LIKE "b*") ? "foo" : "bar"`, `foo`, nil}, - {`RETURN true ? ("foo" NOT LIKE "b*") : false`, true, nil}, - {`RETURN true ? false : ("foo" NOT LIKE "b*")`, false, nil}, - {`RETURN false ? false : ("foo" NOT LIKE "b*")`, true, nil}, - }) - - //Convey("FOR IN LIKE", t, func() { - // c := compiler.New() - // - // out1, err := c.MustCompile(` - // FOR str IN ["foo", "bar", "qaz"] - // FILTER str LIKE "*a*" - // RETURN str - // `).Run(context.Background()) - // - // So(err, ShouldBeNil) - // So(string(out1), ShouldEqual, `["bar","qaz"]`) - //}) - // - //Convey("FOR IN LIKE 2", t, func() { - // c := compiler.New() - // - // out1, err := c.MustCompile(` - // FOR str IN ["foo", "bar", "qaz"] - // FILTER str LIKE "*a*" - // RETURN str - // `).Run(context.Background()) - // - // So(err, ShouldBeNil) - // So(string(out1), ShouldEqual, `["bar","qaz"]`) - //}) - -} diff --git a/pkg/compiler/compiler_limit_test.go b/pkg/compiler/compiler_limit_test.go deleted file mode 100644 index b3dd99d8f..000000000 --- a/pkg/compiler/compiler_limit_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package compiler_test - -import ( - . "github.com/smartystreets/goconvey/convey" - "testing" -) - -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, - }, - }) -} diff --git a/pkg/compiler/compiler_logical_test.go b/pkg/compiler/compiler_logical_test.go deleted file mode 100644 index ca79323c3..000000000 --- a/pkg/compiler/compiler_logical_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package compiler_test - -import ( - "testing" -) - -func TestLogicalOperators(t *testing.T) { - RunUseCases(t, []UseCase{ - {"RETURN 1 AND 0", 0, nil}, - {"RETURN 1 AND 1", 1, nil}, - {"RETURN 2 > 1 AND 1 > 0", true, nil}, - {"RETURN NONE && true", nil, nil}, - {"RETURN '' && true", "", nil}, - {"RETURN true && 23", 23, nil}, - {"RETURN 2 > 1 OR 1 < 0", true, nil}, - {"RETURN 1 || 7", 1, nil}, - {"RETURN 0 || 7", 7, nil}, - {"RETURN NONE || 'foo'", "foo", nil}, - }) - - // - //Convey("ERROR()? || 'boo' should return 'boo'", t, func() { - // c := compiler.New() - // c.RegisterFunction("ERROR", func(ctx context.visitor, args ...core.Value) (core.Value, error) { - // return nil, errors.New("test") - // }) - // - // p, err := c.Compile(` - // RETURN ERROR()? || 'boo' - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // So(string(out), ShouldEqual, `"boo"`) - //}) - // - //Convey("!ERROR()? && TRUE should return false", t, func() { - // c := compiler.New() - // c.RegisterFunction("ERROR", func(ctx context.visitor, args ...core.Value) (core.Value, error) { - // return nil, errors.New("test") - // }) - // - // p, err := c.Compile(` - // RETURN !ERROR()? && TRUE - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // So(string(out), ShouldEqual, `true`) - //}) - // - // - - // - //Convey("NOT TRUE should return false", t, func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // RETURN NOT TRUE - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // So(string(out), ShouldEqual, `false`) - //}) - // - //Convey("NOT u.valid should return true", t, func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // LET u = { valid: false } - // - // RETURN NOT u.valid - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // So(string(out), ShouldEqual, `true`) - //}) -} diff --git a/pkg/compiler/compiler_member_test.go b/pkg/compiler/compiler_member_test.go deleted file mode 100644 index 81d97da92..000000000 --- a/pkg/compiler/compiler_member_test.go +++ /dev/null @@ -1,499 +0,0 @@ -package compiler_test - -import ( - "fmt" - "regexp" - "strconv" - "strings" - "testing" - - "github.com/MontFerret/ferret/pkg/runtime" - - . "github.com/smartystreets/goconvey/convey" - - "github.com/MontFerret/ferret/pkg/compiler" - "github.com/MontFerret/ferret/pkg/parser" -) - -func TestMember(t *testing.T) { - RunUseCases(t, []UseCase{ - { - "LET arr = [1,2,3,4] RETURN arr[10]", - nil, - nil, - }, - { - "LET arr = [1,2,3,4] RETURN arr[1]", - 2, - nil, - }, - { - "LET arr = [1,2,3,4] LET idx = 1 RETURN arr[idx]", - 2, - nil, - }, - { - `LET obj = { foo: "bar", qaz: "wsx"} RETURN obj["qaz"]`, - "wsx", - nil, - }, - { - fmt.Sprintf(` - LET obj = { "foo": "bar", %s: "wsx"} - - RETURN obj["qaz"] - `, "`qaz`"), - "wsx", - nil, - }, - { - fmt.Sprintf(` - LET obj = { "foo": "bar", %s: "wsx"} - - RETURN obj["let"] - `, "`let`"), - "wsx", - nil, - }, - { - `LET obj = { foo: "bar", qaz: "wsx"} LET key = "qaz" RETURN obj[key]`, - "wsx", - nil, - }, - { - `RETURN { foo: "bar" }.foo`, - "bar", - nil, - }, - { - `LET inexp = 1 IN {'foo': [1]}.foo - LET ternaryexp = FALSE ? TRUE : {foo: TRUE}.foo - RETURN inexp && ternaryexp`, - true, - nil, - }, - { - `RETURN ["bar", "foo"][0]`, - "bar", - nil, - }, - { - `LET inexp = 1 IN [[1]][0] - LET ternaryexp = FALSE ? TRUE : [TRUE][0] - RETURN inexp && ternaryexp`, - true, - nil, - }, - { - `LET obj = { - first: { - second: { - third: { - fourth: { - fifth: { - bottom: true - } - } - } - } - } - } - - RETURN obj.first.second.third.fourth.fifth.bottom`, - true, - nil, - }, - { - `LET o1 = { - first: { - second: { - ["third"]: { - fourth: { - fifth: { - bottom: true - } - } - } - } - } - } - - LET o2 = { prop: "third" } - - RETURN o1["first"]["second"][o2.prop]["fourth"]["fifth"].bottom`, - - true, - nil, - }, - { - `LET o1 = { - first: { - second: { - third: { - fourth: { - fifth: { - bottom: true - } - } - } - } - } - } - - LET o2 = { prop: "third" } - - RETURN o1.first["second"][o2.prop].fourth["fifth"]["bottom"]`, - - true, - nil, - }, - { - `LET obj = { - attributes: { - 'data-index': 1 - } - } - - RETURN obj.attributes['data-index']`, - 1, - nil, - }, - { - `LET obj = NONE RETURN obj.foo`, - nil, - ShouldBeError, - }, - { - `LET obj = NONE RETURN obj?.foo`, - nil, - nil, - }, - }) - - // Convey("ObjectDecl by literal passed to func call", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // RETURN KEEP_KEYS({first: {second: "third"}}.first, "second") - // `) - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `{"second":"third"}`) - // }) - // - // Convey("ObjectDecl by literal as forSource", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // FOR v, k IN {f: {foo: "bar"}}.f - // RETURN [k, v] - // `) - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `[["foo","bar"]]`) - // }) - // - - // - // Convey("ArrayDecl by literal passed to func call", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // RETURN FIRST([[1, 2]][0]) - // `) - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `1`) - // }) - // - // Convey("ArrayDecl by literal as forSource", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // FOR i IN [[1, 2]][0] - // RETURN i - // `) - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `[1,2]`) - // }) - // - - // - // Convey("Prop after a func call", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // LET arr = [{ name: "Bob" }] - // - // RETURN FIRST(arr).name - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `"Bob"`) - // }) - // - // Convey("Computed prop after a func call", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // LET arr = [{ name: { first: "Bob" } }] - // - // RETURN FIRST(arr)['name'].first - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `"Bob"`) - // }) - // - - // - // Convey("Optional chaining", t, func() { - // Convey("Object", func() { - // Convey("When value does not exist", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // LET obj = { foo: None } - // - // RETURN obj.foo?.bar - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `null`) - // }) - // - // Convey("When value does exists", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // LET obj = { foo: { bar: "bar" } } - // - // RETURN obj.foo?.bar - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `"bar"`) - // }) - // }) - // - // Convey("Array", func() { - // Convey("When value does not exist", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // LET obj = { foo: None } - // - // RETURN obj.foo?.bar?.[0] - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `null`) - // }) - // - // Convey("When value does exists", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // LET obj = { foo: { bar: ["bar"] } } - // - // RETURN obj.foo?.bar?.[0] - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `"bar"`) - // }) - // }) - // - // Convey("Function", func() { - // Convey("When value does not exist", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // RETURN FIRST([])?.foo - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `null`) - // }) - // - // Convey("When value does exists", func() { - // c := compiler.New() - // - // p, err := c.Compile(` - // RETURN FIRST([{ foo: "bar" }])?.foo - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `"bar"`) - // }) - // - // Convey("When function returns error", func() { - // c := compiler.New() - // c.RegisterFunction("ERROR", func(ctx context.visitor, args ...core.Value) (core.Value, error) { - // return nil, core.ErrNotImplemented - // }) - // - // p, err := c.Compile(` - // RETURN ERROR()?.foo - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `null`) - // }) - // }) - // }) - // - Convey("Reserved words as property name", t, func() { - p := parser.New("RETURN TRUE") - - r := regexp.MustCompile(`\w+`) - - for idx, l := range p.GetLiteralNames() { - if r.MatchString(l) { - query := strings.Builder{} - query.WriteString("LET o = {\n") - query.WriteString(l[1 : len(l)-1]) - query.WriteString(":") - query.WriteString(strconv.Itoa(idx)) - query.WriteString(",\n") - query.WriteString("}\n") - query.WriteString("RETURN o") - - expected := strings.Builder{} - expected.WriteString("{") - expected.WriteString(strings.ReplaceAll(l, "'", "\"")) - expected.WriteString(":") - expected.WriteString(strconv.Itoa(idx)) - expected.WriteString("}") - - c := compiler.New() - prog, err := c.Compile(query.String()) - - So(err, ShouldBeNil) - - out, err := Exec(prog, true, runtime.WithFunctions(c.Functions().Unwrap())) - - So(err, ShouldBeNil) - So(out, ShouldEqual, expected.String()) - } - } - }) -} - -//func BenchmarkMemberArray(b *testing.B) { -// p := compiler.New().MustCompile(` -// LET arr = [[[[1]]]] -// -// RETURN arr[0][0][0][0] -// `) -// -// for n := 0; n < b.N; n++ { -// p.Run(context.Background()) -// } -//} -// -//func BenchmarkMemberObject(b *testing.B) { -// p := compiler.New().MustCompile(` -// LET obj = { -// first: { -// second: { -// third: { -// fourth: { -// fifth: { -// bottom: true -// } -// } -// } -// } -// } -// } -// -// RETURN obj.first.second.third.fourth.fifth.bottom -// `) -// -// for n := 0; n < b.N; n++ { -// p.Run(context.Background()) -// } -//} -// -//func BenchmarkMemberObjectComputed(b *testing.B) { -// p := compiler.New().MustCompile(` -// LET obj = { -// first: { -// second: { -// third: { -// fourth: { -// fifth: { -// bottom: true -// } -// } -// } -// } -// } -// } -// -// RETURN obj["first"]["second"]["third"]["fourth"]["fifth"]["bottom"] -// `) -// -// for n := 0; n < b.N; n++ { -// p.Run(context.Background()) -// } -//} diff --git a/pkg/compiler/compiler_range_test.go b/pkg/compiler/compiler_range_test.go deleted file mode 100644 index 16957a64d..000000000 --- a/pkg/compiler/compiler_range_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package compiler_test - -import ( - . "github.com/smartystreets/goconvey/convey" - "testing" -) - -func TestRange(t *testing.T) { - RunUseCases(t, []UseCase{ - { - "RETURN 1..10", - []any{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, - ShouldEqualJSON, - }, - { - "RETURN 10..1", - []any{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, - ShouldEqualJSON, - }, - { - ` - LET start = 1 - LET end = 10 - RETURN start..end - `, - []any{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, - ShouldEqualJSON, - }, - //{ - // ` - //LET start = @start - //LET end = @end - //RETURN start..end - //`, - // []any{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, - // ShouldEqualJSON, - //}, - }) -} diff --git a/pkg/compiler/setup_test.go b/pkg/compiler/compiler_setup_test.go similarity index 100% rename from pkg/compiler/setup_test.go rename to pkg/compiler/compiler_setup_test.go diff --git a/pkg/compiler/compiler_ternary_test.go b/pkg/compiler/compiler_ternary_test.go deleted file mode 100644 index 734824dc7..000000000 --- a/pkg/compiler/compiler_ternary_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package compiler_test - -import ( - "testing" -) - -func TestTernaryOperator(t *testing.T) { - RunUseCases(t, []UseCase{ - {"RETURN 1 < 2 ? 3 : 4", 3, nil}, - {"RETURN 1 > 2 ? 3 : 4", 4, nil}, - {"RETURN 2 ? : 4", 2, nil}, - {` -LET foo = TRUE -RETURN foo ? TRUE : FALSE -`, true, nil}, - {` -LET foo = FALSE -RETURN foo ? TRUE : FALSE -`, false, nil}, - }) - - //Convey("Should compile ternary operator", t, func() { - // c := compiler.New() - // p, err := c.Compile(` - // FOR i IN [1, 2, 3, 4, 5, 6] - // RETURN i < 3 ? i * 3 : i * 2 - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `[3,6,6,8,10,12]`) - //}) - // - //Convey("Should compile ternary operator with shortcut", t, func() { - // c := compiler.New() - // p, err := c.Compile(` - // FOR i IN [1, 2, 3, 4, 5, 6] - // RETURN i < 3 ? : i * 2 - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `[true,true,6,8,10,12]`) - //}) - // - //Convey("Should compile ternary operator with shortcut with nones", t, func() { - // c := compiler.New() - // p, err := c.Compile(` - // FOR i IN [NONE, 2, 3, 4, 5, 6] - // RETURN i ? : i - // `) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `[null,2,3,4,5,6]`) - //}) - // - //Convey("Should compile ternary operator with default values", t, func() { - // vals := []string{ - // "0", - // "0.0", - // "''", - // "NONE", - // "FALSE", - // } - // - // c := compiler.New() - // - // for _, val := range vals { - // p, err := c.Compile(fmt.Sprintf(` - // FOR i IN [%s, 1, 2, 3] - // RETURN i ? i * 2 : 'no value' - // `, val)) - // - // So(err, ShouldBeNil) - // - // out, err := p.Run(context.Background()) - // - // So(err, ShouldBeNil) - // - // So(string(out), ShouldEqual, `["no value",2,4,6]`) - // } - //}) - // - //Convey("Multi expression", t, func() { - // out := compiler.New().MustCompile(` - // RETURN 0 && true ? "1" : "some" - // `).MustRun(context.Background()) - // - // So(string(out), ShouldEqual, `"some"`) - // - // out = compiler.New().MustCompile(` - // RETURN length([]) > 0 && true ? "1" : "some" - // `).MustRun(context.Background()) - // - // So(string(out), ShouldEqual, `"some"`) - //}) -} diff --git a/pkg/compiler/compiler_test.go b/pkg/compiler/compiler_test.go index 01cf12a3e..d6fa631cb 100644 --- a/pkg/compiler/compiler_test.go +++ b/pkg/compiler/compiler_test.go @@ -1,7 +1,12 @@ package compiler_test import ( + "context" + "fmt" "github.com/MontFerret/ferret/pkg/compiler" + "github.com/MontFerret/ferret/pkg/runtime" + "github.com/MontFerret/ferret/pkg/runtime/core" + "github.com/MontFerret/ferret/pkg/runtime/values" "testing" . "github.com/smartystreets/goconvey/convey" @@ -311,3 +316,1103 @@ func TestUnaryOperators(t *testing.T) { //}) // } + +func TestEqualityOperators(t *testing.T) { + Convey("Equality operators", t, func() { + run := func(p *runtime.Program) (string, error) { + vm := runtime.NewVM() + + out, err := vm.Run(context.Background(), p) + + return string(out), err + } + + type UseCase struct { + Operator string + Expected bool + } + + useCases := []UseCase{ + {">", true}, + {"==", false}, + {">=", true}, + {"<", false}, + {"!=", true}, + {"<=", false}, + } + + for _, useCase := range useCases { + Convey("Should compile RETURN 2 "+useCase.Operator+" 1", func() { + c := compiler.New() + + p, err := c.Compile(` + RETURN 2 ` + useCase.Operator + ` 1 + `) + + So(err, ShouldBeNil) + So(p, ShouldHaveSameTypeAs, &runtime.Program{}) + + out, err := run(p) + + So(err, ShouldBeNil) + So(out == "true", ShouldEqual, useCase.Expected) + }) + } + }) +} + +func TestLogicalOperators(t *testing.T) { + RunUseCases(t, []UseCase{ + {"RETURN 1 AND 0", 0, nil}, + {"RETURN 1 AND 1", 1, nil}, + {"RETURN 2 > 1 AND 1 > 0", true, nil}, + {"RETURN NONE && true", nil, nil}, + {"RETURN '' && true", "", nil}, + {"RETURN true && 23", 23, nil}, + {"RETURN 2 > 1 OR 1 < 0", true, nil}, + {"RETURN 1 || 7", 1, nil}, + {"RETURN 0 || 7", 7, nil}, + {"RETURN NONE || 'foo'", "foo", nil}, + }) + + // + //Convey("ERROR()? || 'boo' should return 'boo'", t, func() { + // c := compiler.New() + // c.RegisterFunction("ERROR", func(ctx context.visitor, args ...core.Value) (core.Value, error) { + // return nil, errors.New("test") + // }) + // + // p, err := c.Compile(` + // RETURN ERROR()? || 'boo' + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // So(string(out), ShouldEqual, `"boo"`) + //}) + // + //Convey("!ERROR()? && TRUE should return false", t, func() { + // c := compiler.New() + // c.RegisterFunction("ERROR", func(ctx context.visitor, args ...core.Value) (core.Value, error) { + // return nil, errors.New("test") + // }) + // + // p, err := c.Compile(` + // RETURN !ERROR()? && TRUE + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // So(string(out), ShouldEqual, `true`) + //}) + // + // + + // + //Convey("NOT TRUE should return false", t, func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // RETURN NOT TRUE + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // So(string(out), ShouldEqual, `false`) + //}) + // + //Convey("NOT u.valid should return true", t, func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // LET u = { valid: false } + // + // RETURN NOT u.valid + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // So(string(out), ShouldEqual, `true`) + //}) +} + +func TestTernaryOperator(t *testing.T) { + RunUseCases(t, []UseCase{ + {"RETURN 1 < 2 ? 3 : 4", 3, nil}, + {"RETURN 1 > 2 ? 3 : 4", 4, nil}, + {"RETURN 2 ? : 4", 2, nil}, + {` +LET foo = TRUE +RETURN foo ? TRUE : FALSE +`, true, nil}, + {` +LET foo = FALSE +RETURN foo ? TRUE : FALSE +`, false, nil}, + }) + + //Convey("Should compile ternary operator", t, func() { + // c := compiler.New() + // p, err := c.Compile(` + // FOR i IN [1, 2, 3, 4, 5, 6] + // RETURN i < 3 ? i * 3 : i * 2 + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `[3,6,6,8,10,12]`) + //}) + // + //Convey("Should compile ternary operator with shortcut", t, func() { + // c := compiler.New() + // p, err := c.Compile(` + // FOR i IN [1, 2, 3, 4, 5, 6] + // RETURN i < 3 ? : i * 2 + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `[true,true,6,8,10,12]`) + //}) + // + //Convey("Should compile ternary operator with shortcut with nones", t, func() { + // c := compiler.New() + // p, err := c.Compile(` + // FOR i IN [NONE, 2, 3, 4, 5, 6] + // RETURN i ? : i + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `[null,2,3,4,5,6]`) + //}) + // + //Convey("Should compile ternary operator with default values", t, func() { + // vals := []string{ + // "0", + // "0.0", + // "''", + // "NONE", + // "FALSE", + // } + // + // c := compiler.New() + // + // for _, val := range vals { + // p, err := c.Compile(fmt.Sprintf(` + // FOR i IN [%s, 1, 2, 3] + // RETURN i ? i * 2 : 'no value' + // `, val)) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `["no value",2,4,6]`) + // } + //}) + // + //Convey("Multi expression", t, func() { + // out := compiler.New().MustCompile(` + // RETURN 0 && true ? "1" : "some" + // `).MustRun(context.Background()) + // + // So(string(out), ShouldEqual, `"some"`) + // + // out = compiler.New().MustCompile(` + // RETURN length([]) > 0 && true ? "1" : "some" + // `).MustRun(context.Background()) + // + // So(string(out), ShouldEqual, `"some"`) + //}) +} + +func TestLikeOperator(t *testing.T) { + RunUseCases(t, []UseCase{ + {`RETURN "foo" LIKE "f*"`, true, nil}, + {`RETURN "foo" LIKE "b*"`, false, nil}, + {`RETURN "foo" NOT LIKE "f*"`, false, nil}, + {`RETURN "foo" NOT LIKE "b*"`, true, nil}, + {`LET res = "foo" LIKE "f*" + RETURN res`, true, nil}, + {`RETURN ("foo" LIKE "b*") ? "foo" : "bar"`, `bar`, nil}, + {`RETURN ("foo" NOT LIKE "b*") ? "foo" : "bar"`, `foo`, nil}, + {`RETURN true ? ("foo" NOT LIKE "b*") : false`, true, nil}, + {`RETURN true ? false : ("foo" NOT LIKE "b*")`, false, nil}, + {`RETURN false ? false : ("foo" NOT LIKE "b*")`, true, nil}, + }) + + //Convey("FOR IN LIKE", t, func() { + // c := compiler.New() + // + // out1, err := c.MustCompile(` + // FOR str IN ["foo", "bar", "qaz"] + // FILTER str LIKE "*a*" + // RETURN str + // `).Run(context.Background()) + // + // So(err, ShouldBeNil) + // So(string(out1), ShouldEqual, `["bar","qaz"]`) + //}) + // + //Convey("FOR IN LIKE 2", t, func() { + // c := compiler.New() + // + // out1, err := c.MustCompile(` + // FOR str IN ["foo", "bar", "qaz"] + // FILTER str LIKE "*a*" + // RETURN str + // `).Run(context.Background()) + // + // So(err, ShouldBeNil) + // So(string(out1), ShouldEqual, `["bar","qaz"]`) + //}) + +} + +func TestRange(t *testing.T) { + RunUseCases(t, []UseCase{ + { + "RETURN 1..10", + []any{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + ShouldEqualJSON, + }, + { + "RETURN 10..1", + []any{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, + ShouldEqualJSON, + }, + { + ` + LET start = 1 + LET end = 10 + RETURN start..end + `, + []any{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + ShouldEqualJSON, + }, + //{ + // ` + //LET start = @start + //LET end = @end + //RETURN start..end + //`, + // []any{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + // ShouldEqualJSON, + //}, + }) +} + +func TestFunctionCall(t *testing.T) { + RunUseCases(t, []UseCase{ + { + "RETURN TYPENAME(1)", + "int", + nil, + }, + { + "WAIT(10) RETURN 1", + 1, + nil, + }, + { + "LET duration = 10 WAIT(duration) RETURN 1", + 1, + nil, + }, + { + "RETURN (FALSE OR T::FAIL())?", + nil, + nil, + }, + { + "RETURN T::FAIL()?", + nil, + nil, + }, + { + `FOR i IN [1, 2, 3, 4] + LET duration = 10 + + WAIT(duration) + + RETURN i * 2`, + []int{2, 4, 6, 8}, + ShouldEqualJSON, + }, + { + `RETURN FIRST((FOR i IN 1..10 RETURN i * 2))`, + 2, + nil, + }, + { + `RETURN UNION((FOR i IN 0..5 RETURN i), (FOR i IN 6..10 RETURN i))`, + []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + ShouldEqualJSON, + }, + }) +} + +func TestMember(t *testing.T) { + RunUseCases(t, []UseCase{ + { + "LET arr = [1,2,3,4] RETURN arr[10]", + nil, + nil, + }, + { + "LET arr = [1,2,3,4] RETURN arr[1]", + 2, + nil, + }, + { + "LET arr = [1,2,3,4] LET idx = 1 RETURN arr[idx]", + 2, + nil, + }, + { + `LET obj = { foo: "bar", qaz: "wsx"} RETURN obj["qaz"]`, + "wsx", + nil, + }, + { + fmt.Sprintf(` + LET obj = { "foo": "bar", %s: "wsx"} + + RETURN obj["qaz"] + `, "`qaz`"), + "wsx", + nil, + }, + { + fmt.Sprintf(` + LET obj = { "foo": "bar", %s: "wsx"} + + RETURN obj["let"] + `, "`let`"), + "wsx", + nil, + }, + { + `LET obj = { foo: "bar", qaz: "wsx"} LET key = "qaz" RETURN obj[key]`, + "wsx", + nil, + }, + { + `RETURN { foo: "bar" }.foo`, + "bar", + nil, + }, + { + `LET inexp = 1 IN {'foo': [1]}.foo + LET ternaryexp = FALSE ? TRUE : {foo: TRUE}.foo + RETURN inexp && ternaryexp`, + true, + nil, + }, + { + `RETURN ["bar", "foo"][0]`, + "bar", + nil, + }, + { + `LET inexp = 1 IN [[1]][0] + LET ternaryexp = FALSE ? TRUE : [TRUE][0] + RETURN inexp && ternaryexp`, + true, + nil, + }, + { + `LET obj = { + first: { + second: { + third: { + fourth: { + fifth: { + bottom: true + } + } + } + } + } + } + + RETURN obj.first.second.third.fourth.fifth.bottom`, + true, + nil, + }, + { + `LET o1 = { + first: { + second: { + ["third"]: { + fourth: { + fifth: { + bottom: true + } + } + } + } + } + } + + LET o2 = { prop: "third" } + + RETURN o1["first"]["second"][o2.prop]["fourth"]["fifth"].bottom`, + + true, + nil, + }, + { + `LET o1 = { + first: { + second: { + third: { + fourth: { + fifth: { + bottom: true + } + } + } + } + } + } + + LET o2 = { prop: "third" } + + RETURN o1.first["second"][o2.prop].fourth["fifth"]["bottom"]`, + + true, + nil, + }, + { + `LET obj = { + attributes: { + 'data-index': 1 + } + } + + RETURN obj.attributes['data-index']`, + 1, + nil, + }, + { + `LET obj = NONE RETURN obj.foo`, + nil, + ShouldBeError, + }, + { + `LET obj = NONE RETURN obj?.foo`, + nil, + nil, + }, + }) + + // Convey("ObjectDecl by literal passed to func call", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // RETURN KEEP_KEYS({first: {second: "third"}}.first, "second") + // `) + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `{"second":"third"}`) + // }) + // + // Convey("ObjectDecl by literal as forSource", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // FOR v, k IN {f: {foo: "bar"}}.f + // RETURN [k, v] + // `) + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `[["foo","bar"]]`) + // }) + // + + // + // Convey("ArrayDecl by literal passed to func call", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // RETURN FIRST([[1, 2]][0]) + // `) + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `1`) + // }) + // + // Convey("ArrayDecl by literal as forSource", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // FOR i IN [[1, 2]][0] + // RETURN i + // `) + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `[1,2]`) + // }) + // + + // + // Convey("Prop after a func call", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // LET arr = [{ name: "Bob" }] + // + // RETURN FIRST(arr).name + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `"Bob"`) + // }) + // + // Convey("Computed prop after a func call", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // LET arr = [{ name: { first: "Bob" } }] + // + // RETURN FIRST(arr)['name'].first + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `"Bob"`) + // }) + // + + // + // Convey("Optional chaining", t, func() { + // Convey("Object", func() { + // Convey("When value does not exist", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // LET obj = { foo: None } + // + // RETURN obj.foo?.bar + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `null`) + // }) + // + // Convey("When value does exists", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // LET obj = { foo: { bar: "bar" } } + // + // RETURN obj.foo?.bar + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `"bar"`) + // }) + // }) + // + // Convey("Array", func() { + // Convey("When value does not exist", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // LET obj = { foo: None } + // + // RETURN obj.foo?.bar?.[0] + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `null`) + // }) + // + // Convey("When value does exists", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // LET obj = { foo: { bar: ["bar"] } } + // + // RETURN obj.foo?.bar?.[0] + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `"bar"`) + // }) + // }) + // + // Convey("Function", func() { + // Convey("When value does not exist", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // RETURN FIRST([])?.foo + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `null`) + // }) + // + // Convey("When value does exists", func() { + // c := compiler.New() + // + // p, err := c.Compile(` + // RETURN FIRST([{ foo: "bar" }])?.foo + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `"bar"`) + // }) + // + // Convey("When function returns error", func() { + // c := compiler.New() + // c.RegisterFunction("ERROR", func(ctx context.visitor, args ...core.Value) (core.Value, error) { + // return nil, core.ErrNotImplemented + // }) + // + // p, err := c.Compile(` + // RETURN ERROR()?.foo + // `) + // + // So(err, ShouldBeNil) + // + // out, err := p.Run(context.Background()) + // + // So(err, ShouldBeNil) + // + // So(string(out), ShouldEqual, `null`) + // }) + // }) + // }) + // + //Convey("Reserved words as property name", t, func() { + // p := parser.New("RETURN TRUE") + // + // r := regexp.MustCompile(`\w+`) + // + // for idx, l := range p.GetLiteralNames() { + // if r.MatchString(l) { + // query := strings.Builder{} + // query.WriteString("LET o = {\n") + // query.WriteString(l[1 : len(l)-1]) + // query.WriteString(":") + // query.WriteString(strconv.Itoa(idx)) + // query.WriteString(",\n") + // query.WriteString("}\n") + // query.WriteString("RETURN o") + // + // expected := strings.Builder{} + // expected.WriteString("{") + // expected.WriteString(strings.ReplaceAll(l, "'", "\"")) + // expected.WriteString(":") + // expected.WriteString(strconv.Itoa(idx)) + // expected.WriteString("}") + // + // c := compiler.New() + // prog, err := c.Compile(query.String()) + // + // So(err, ShouldBeNil) + // + // out, err := Exec(prog, true, runtime.WithFunctions(c.Functions().Unwrap())) + // + // So(err, ShouldBeNil) + // So(out, ShouldEqual, expected.String()) + // } + // } + //}) +} + +func TestFor(t *testing.T) { + // Should not allocate memory if NONE is a return statement + //{ + // `FOR i IN 0..100 + // RETURN NONE`, + // []any{}, + // ShouldEqualJSON, + //}, + RunUseCases(t, []UseCase{ + { + "FOR i IN 1..5 RETURN i", + []any{1, 2, 3, 4, 5}, + ShouldEqualJSON, + }, + { + `FOR i IN 1..5 + LET x = i + PRINT(x) + RETURN i + `, + []any{1, 2, 3, 4, 5}, + ShouldEqualJSON, + }, + { + `FOR val, counter IN 1..5 + LET x = val + PRINT(counter) + LET y = counter + RETURN [x, y] + `, + []any{[]any{1, 0}, []any{2, 1}, []any{3, 2}, []any{4, 3}, []any{5, 4}}, + ShouldEqualJSON, + }, + { + `FOR i IN [] RETURN i + `, + []any{}, + ShouldEqualJSON, + }, + { + `FOR i IN [1, 2, 3] RETURN i + `, + []any{1, 2, 3}, + ShouldEqualJSON, + }, + + { + `FOR i, k IN [1, 2, 3] RETURN k`, + []any{0, 1, 2}, + ShouldEqualJSON, + }, + { + `FOR i IN ['foo', 'bar', 'qaz'] RETURN i`, + []any{"foo", "bar", "qaz"}, + ShouldEqualJSON, + }, + { + `FOR i IN {a: 'bar', b: 'foo', c: 'qaz'} RETURN i`, + []any{"foo", "bar", "qaz"}, + ShouldHaveSameItems, + }, + { + `FOR i, k IN {a: 'foo', b: 'bar', c: 'qaz'} RETURN k`, + []any{"a", "b", "c"}, + ShouldHaveSameItems, + }, + { + `FOR i IN [{name: 'foo'}, {name: 'bar'}, {name: 'qaz'}] RETURN i.name`, + []any{"foo", "bar", "qaz"}, + ShouldHaveSameItems, + }, + { + `FOR i IN { items: [{name: 'foo'}, {name: 'bar'}, {name: 'qaz'}] }.items RETURN i.name`, + []any{"foo", "bar", "qaz"}, + ShouldHaveSameItems, + }, + { + `FOR prop IN ["a"] + FOR val IN [1, 2, 3] + RETURN {[prop]: val}`, + []any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}}, + ShouldEqualJSON, + }, + { + `FOR val IN 1..3 + FOR prop IN ["a"] + RETURN {[prop]: val}`, + []any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}}, + ShouldEqualJSON, + }, + { + `FOR prop IN ["a"] + FOR val IN 1..3 + RETURN {[prop]: val}`, + []any{map[string]any{"a": 1}, map[string]any{"a": 2}, map[string]any{"a": 3}}, + ShouldEqualJSON, + }, + { + `FOR prop IN ["a"] + FOR val IN [1, 2, 3] + FOR val2 IN [1, 2, 3] + RETURN { [prop]: [val, val2] }`, + []any{map[string]any{"a": []int{1, 1}}, map[string]any{"a": []int{1, 2}}, map[string]any{"a": []int{1, 3}}, map[string]any{"a": []int{2, 1}}, map[string]any{"a": []int{2, 2}}, map[string]any{"a": []int{2, 3}}, map[string]any{"a": []int{3, 1}}, map[string]any{"a": []int{3, 2}}, map[string]any{"a": []int{3, 3}}}, + ShouldEqualJSON, + }, + { + `FOR val IN [1, 2, 3] + RETURN ( + FOR prop IN ["a", "b", "c"] + RETURN { [prop]: val } + )`, + []any{[]any{map[string]any{"a": 1}, map[string]any{"b": 1}, map[string]any{"c": 1}}, []any{map[string]any{"a": 2}, map[string]any{"b": 2}, map[string]any{"c": 2}}, []any{map[string]any{"a": 3}, map[string]any{"b": 3}, map[string]any{"c": 3}}}, + ShouldEqualJSON, + }, + { + `FOR val IN [1, 2, 3] + LET sub = ( + FOR prop IN ["a", "b", "c"] + RETURN { [prop]: val } + ) + + RETURN sub`, + []any{[]any{map[string]any{"a": 1}, map[string]any{"b": 1}, map[string]any{"c": 1}}, []any{map[string]any{"a": 2}, map[string]any{"b": 2}, map[string]any{"c": 2}}, []any{map[string]any{"a": 3}, map[string]any{"b": 3}, map[string]any{"c": 3}}}, + ShouldEqualJSON, + }, + { + `FOR i IN [ 1, 2, 3, 4, 1, 3 ] + RETURN DISTINCT i + `, + []any{1, 2, 3, 4}, + ShouldEqualJSON, + }, + }) +} + +func TestForWhile(t *testing.T) { + var counter int64 + RunUseCases(t, []UseCase{ + //{ + // "FOR i WHILE false RETURN i", + // []any{}, + // ShouldEqualJSON, + //}, + { + "FOR i WHILE UNTIL(5) RETURN i", + []any{0, 1, 2, 3, 4}, + ShouldEqualJSON, + }, + }, runtime.WithFunctions(map[string]core.Function{ + "UNTIL": func(ctx context.Context, args ...core.Value) (core.Value, error) { + if counter < int64(values.ToInt(args[0])) { + counter++ + + return values.True, nil + } + + return values.False, nil + }, + })) +} + +func TestForFilter(t *testing.T) { + RunUseCases(t, []UseCase{ + { + ` + FOR i IN [ 1, 2, 3, 4, 1, 3 ] + FILTER i > 2 + RETURN i + `, + []any{3, 4, 3}, + ShouldEqualJSON, + }, + { + ` + FOR i IN [ 1, 2, 3, 4, 1, 3 ] + FILTER i > 1 AND i < 4 + RETURN i + `, + []any{2, 3, 3}, + ShouldEqualJSON, + }, + { + ` + LET users = [ + { + age: 31, + gender: "m", + name: "Josh" + }, + { + age: 29, + gender: "f", + name: "Mary" + }, + { + age: 36, + gender: "m", + name: "Peter" + } + ] + FOR u IN users + FILTER u.name =~ "r" + RETURN u + `, + []any{map[string]any{"age": 29, "gender": "f", "name": "Mary"}, map[string]any{"age": 36, "gender": "m", "name": "Peter"}}, + ShouldEqualJSON, + }, + { + ` + 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, + }, + }) +} + +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, + }, + }) +} diff --git a/pkg/compiler/registers.go b/pkg/compiler/registers.go deleted file mode 100644 index f66dd2b7c..000000000 --- a/pkg/compiler/registers.go +++ /dev/null @@ -1,125 +0,0 @@ -package compiler - -import ( - "errors" -) - -// RegisterAllocator manages register allocation -type RegisterAllocator struct { - usedRegisters map[Register]bool - variables map[string]*Variable - currentInstr int - - // Register pools for different purposes - pools map[VarType][]Register -} - -func NewRegisterAllocator(initialSize int) *RegisterAllocator { - ra := &RegisterAllocator{ - usedRegisters: make(map[Register]bool), - variables: make(map[string]*Variable), - } - - // Initialize register pools - ra.initializePools(initialSize) - return ra -} - -func (ra *RegisterAllocator) initializePools(initialSize int) { - // Allocate registers to different pools - // Example distribution (adjustable based on query patterns): - // - 20% for iterators - // - 40% for temporaries - // - 30% for collect operations - // - 10% for results - - iteratorSize := initialSize * 2 / 10 - tempSize := initialSize * 4 / 10 - collectSize := initialSize * 3 / 10 - resultSize := initialSize - iteratorSize - tempSize - collectSize - - current := Register(0) - - // Iterator pool (R0-R19 in a 100-register machine) - for i := 0; i < iteratorSize; i++ { - ra.iteratorPool = append(ra.iteratorPool, current) - current++ - } - - // Temporary pool (R20-R59) - for i := 0; i < tempSize; i++ { - ra.tempPool = append(ra.tempPool, current) - current++ - } - - //// Collect pool (R60-R89) - //for i := 0; i < collectSize; i++ { - // ra.collectPool = append(ra.collectPool, current) - // current++ - //} - - // Result pool (R90-R99) - for i := 0; i < resultSize; i++ { - ra.resultPool = append(ra.resultPool, current) - current++ - } -} - -// AllocateRegister assigns a register based on variable type -func (ra *RegisterAllocator) AllocateRegister(varType VarType) Register { - var pool []Register - - switch varType { - case VarIterator: - pool = ra.iteratorPool - case VarTemporary: - pool = ra.tempPool - case VarResult: - pool = ra.resultPool - } - - // Find first free register in appropriate pool - for _, reg := range pool { - if !ra.usedRegisters[reg] { - ra.usedRegisters[reg] = true - return reg - } - } - - // If no registers available in the pool, expand the pool - nextPool := make([]Register, len(pool)*2) - copy(nextPool, pool) - - // TODO: Create a custom error type - return -1, errors.New("no registers available") -} - -// LivenessAnalysis performs liveness analysis on variables -func (ra *RegisterAllocator) LivenessAnalysis(instructions []Instruction) { - // Forward pass to record first use - for i, instr := range instructions { - for _, varName := range instr.VarRefs { - if v, exists := ra.variables[varName]; exists { - if v.FirstUse == -1 { - v.FirstUse = i - } - v.LastUse = i - } - } - } - - // Backward pass to optimize register allocation - for i := len(instructions) - 1; i >= 0; i-- { - for _, v := range ra.variables { - if v.FirstUse <= i && i <= v.LastUse { - v.IsLive = true - } else if i < v.FirstUse || i > v.LastUse { - v.IsLive = false - // Free register if variable is dead - if v.Register >= 0 { - ra.usedRegisters[v.Register] = false - } - } - } - } -} diff --git a/pkg/compiler/result.go b/pkg/compiler/result.go deleted file mode 100644 index 16928e16b..000000000 --- a/pkg/compiler/result.go +++ /dev/null @@ -1,26 +0,0 @@ -package compiler - -type visitorFn func() (interface{}, error) - -type result struct { - data interface{} - err error -} - -func newResultFrom(fn visitorFn) *result { - out, err := fn() - - return &result{out, err} -} - -func (res *result) Ok() bool { - return res.err == nil -} - -func (res *result) Data() interface{} { - return res.data -} - -func (res *result) Error() error { - return res.err -} diff --git a/pkg/compiler/visitor.go b/pkg/compiler/visitor.go index 5eb3fedd8..66a97cfd2 100644 --- a/pkg/compiler/visitor.go +++ b/pkg/compiler/visitor.go @@ -14,13 +14,17 @@ import ( const ( VarTemporary VarType = iota // Short-lived intermediate results + VarLocal // Local variables VarIterator // FOR loop iterators VarResult // Final result variables ) type ( + // Register represents a virtual register number Register int + Constant int + VarType int Variable struct { @@ -34,11 +38,10 @@ type ( } Instruction struct { - OpCode runtime.OpCode - Operands [3]Register - VarRefs []string // Referenced variable names - Immediate interface{} // Immediate value if any - Label string // For jumps and labels + OpCode runtime.Opcode + Operands [3]int + VarRefs []string // Referenced variable names + Label string // For jumps and labels SourceLine int } @@ -59,7 +62,7 @@ type ( constantsIndex map[uint64]int constants []core.Value //loops []*loopScope - globals map[string]Register + globals map[string]int locals []Variable catchTable [][2]int } @@ -78,7 +81,7 @@ func newVisitor(src string) *visitor { v := new(visitor) v.BaseFqlParserVisitor = new(fql.BaseFqlParserVisitor) v.src = src - v.registers = NewRegisterAllocator(1024) + v.registers = NewRegisterAllocator() //v.funcs = funcs v.constantsIndex = make(map[uint64]int) //v.locations = make([]core.Location, 0) @@ -86,7 +89,7 @@ func newVisitor(src string) *visitor { v.constants = make([]core.Value, 0) v.scope = 0 //v.loops = make([]*loopScope, 0) - v.globals = make(map[string]Register) + v.globals = make(map[string]int) v.locals = make([]Variable, 0) v.catchTable = make([][2]int, 0) @@ -168,9 +171,9 @@ func (v *visitor) VisitForExpression(ctx *fql.ForExpressionContext) interface{} // c.Accept(v) // } // - // v.emit(runtime.OpForLoopInitInput) + // v.emitABC(runtime.OpForLoopInitInput) // loopJump = len(v.instructions) - // v.emit(runtime.OpForLoopHasNext) + // v.emitABC(runtime.OpForLoopHasNext) // exitJump = v.emitJump(runtime.OpJumpIfFalse) // // pop the boolean value from the stack // v.emitPop() @@ -203,11 +206,11 @@ func (v *visitor) VisitForExpression(ctx *fql.ForExpressionContext) interface{} // // if hasValVar && hasCounterVar { // // we will calculate the index of the counter variable - // v.emit(runtime.OpForLoopNext) + // v.emitABC(runtime.OpForLoopNext) // } else if hasValVar { - // v.emit(runtime.OpForLoopNextValue) + // v.emitABC(runtime.OpForLoopNextValue) // } else if hasCounterVar { - // v.emit(runtime.OpForLoopNextCounter) + // v.emitABC(runtime.OpForLoopNextCounter) // } else { // panic(core.Error(ErrUnexpectedToken, ctx.GetText())) // } @@ -221,7 +224,7 @@ func (v *visitor) VisitForExpression(ctx *fql.ForExpressionContext) interface{} // } //} else { // // Create initial value for the loop counter - // v.emit(runtime.OpWhileLoopInitCounter) + // v.emitABC(runtime.OpWhileLoopInitCounter) // // loopJump = len(v.instructions) // @@ -238,7 +241,7 @@ func (v *visitor) VisitForExpression(ctx *fql.ForExpressionContext) interface{} // // declare counter variable // // and increment it by 1 // index := v.declareVariable(counterVar) - // v.emit(runtime.OpWhileLoopNext) + // v.emitABC(runtime.OpWhileLoopNext) // v.defineVariable(index) //} // @@ -383,39 +386,39 @@ func (v *visitor) VisitFunctionCallExpression(ctx *fql.FunctionCallExpressionCon //switch size { //case 0: // if isNonOptional { - // v.emit(runtime.OpCall, 0) + // v.emitABC(runtime.OpCall, 0) // } else { - // v.emit(runtime.OpCallSafe, 0) + // v.emitABC(runtime.OpCallSafe, 0) // } //case 1: // if isNonOptional { - // v.emit(runtime.OpCall1, 1) + // v.emitABC(runtime.OpCall1, 1) // } else { - // v.emit(runtime.OpCall1Safe, 1) + // v.emitABC(runtime.OpCall1Safe, 1) // } //case 2: // if isNonOptional { - // v.emit(runtime.OpCall2, 2) + // v.emitABC(runtime.OpCall2, 2) // } else { - // v.emit(runtime.OpCall2Safe, 2) + // v.emitABC(runtime.OpCall2Safe, 2) // } //case 3: // if isNonOptional { - // v.emit(runtime.OpCall3, 3) + // v.emitABC(runtime.OpCall3, 3) // } else { - // v.emit(runtime.OpCall3Safe, 3) + // v.emitABC(runtime.OpCall3Safe, 3) // } //case 4: // if isNonOptional { - // v.emit(runtime.OpCall4, 4) + // v.emitABC(runtime.OpCall4, 4) // } else { - // v.emit(runtime.OpCall4Safe, 4) + // v.emitABC(runtime.OpCall4Safe, 4) // } //default: // if isNonOptional { - // v.emit(runtime.OpCallN, size) + // v.emitABC(runtime.OpCallN, size) // } else { - // v.emit(runtime.OpCallNSafe, size) + // v.emitABC(runtime.OpCallNSafe, size) // } //} @@ -449,9 +452,9 @@ func (v *visitor) VisitMemberExpression(ctx *fql.MemberExpressionContext) interf // } // // if p.ErrorOperator() != nil { - // v.emit(runtime.OpLoadPropertyOptional) + // v.emitABC(runtime.OpLoadPropertyOptional) // } else { - // v.emit(runtime.OpLoadProperty) + // v.emitABC(runtime.OpLoadProperty) // } //} @@ -462,7 +465,7 @@ func (v *visitor) VisitRangeOperator(ctx *fql.RangeOperatorContext) interface{} //ctx.GetLeft().Accept(v) //ctx.GetRight().Accept(v) // - //v.emit(runtime.OpRange) + //v.emitABC(runtime.OpRange) return nil } @@ -490,11 +493,13 @@ func (v *visitor) VisitVariableDeclaration(ctx *fql.VariableDeclarationContext) name = reserved.GetText() } + reg := v.declareVariable(name) + ctx.Expression().Accept(v) if name != ignorePseudoVariable { // we do not have custom functions, thus this feature is not needed at this moment - index := v.declareVariable(name) + v.defineVariable(index) } @@ -515,7 +520,7 @@ func (v *visitor) VisitArrayLiteral(ctx *fql.ArrayLiteralContext) interface{} { // size = out.(int) //} // - //v.emit(runtime.OpArray, size) + //v.emitABC(runtime.OpArray, size) return nil } @@ -549,7 +554,7 @@ func (v *visitor) VisitObjectLiteral(ctx *fql.ObjectLiteralContext) interface{} // } //} // - //v.emit(runtime.OpObject, len(assignments)) + //v.emitABC(runtime.OpObject, len(assignments)) return nil } @@ -652,9 +657,9 @@ func (v *visitor) VisitFloatLiteral(ctx *fql.FloatLiteralContext) interface{} { func (v *visitor) VisitBooleanLiteral(ctx *fql.BooleanLiteralContext) interface{} { //switch strings.ToLower(ctx.GetText()) { //case "true": - // v.emit(runtime.OpTrue) + // v.emitABC(runtime.OpTrue) //case "false": - // v.emit(runtime.OpFalse) + // v.emitABC(runtime.OpFalse) //default: // panic(core.Error(ErrUnexpectedToken, ctx.GetText())) //} @@ -663,7 +668,7 @@ func (v *visitor) VisitBooleanLiteral(ctx *fql.BooleanLiteralContext) interface{ } func (v *visitor) VisitNoneLiteral(ctx *fql.NoneLiteralContext) interface{} { - //v.emit(runtime.OpLoadNone) + //v.emitABC(runtime.OpLoadNone) return nil } @@ -692,9 +697,9 @@ func (v *visitor) VisitReturnExpression(ctx *fql.ReturnExpressionContext) interf //ctx.Expression().Accept(v) // //if len(v.loops) == 0 { - // v.emit(runtime.OpReturn) + // v.emitABC(runtime.OpReturn) //} else { - // v.emit(runtime.OpLoopReturn, v.resolveLoopResultPosition()) + // v.emitABC(runtime.OpLoopReturn, v.resolveLoopResultPosition()) //} return nil @@ -707,11 +712,11 @@ func (v *visitor) VisitExpression(ctx *fql.ExpressionContext) interface{} { op := op.(*fql.UnaryOperatorContext) if op.Not() != nil { - //v.emit(runtime.OpNot) + //v.emitABC(runtime.OpNot) } else if op.Minus() != nil { - // v.emit(runtime.OpFlipNegative) + // v.emitABC(runtime.OpFlipNegative) } else if op.Plus() != nil { - //v.emit(runtime.OpFlipPositive) + //v.emitABC(runtime.OpFlipPositive) } else { panic(core.Error(ErrUnexpectedToken, op.GetText())) } @@ -753,17 +758,17 @@ func (v *visitor) VisitPredicate(ctx *fql.PredicateContext) interface{} { // // switch op.GetText() { // case "==": - // v.emit(runtime.OpEq) + // v.emitABC(runtime.OpEq) // case "!=": - // v.emit(runtime.OpNeq) + // v.emitABC(runtime.OpNeq) // case ">": - // v.emit(runtime.OpGt) + // v.emitABC(runtime.OpGt) // case ">=": - // v.emit(runtime.OpGte) + // v.emitABC(runtime.OpGte) // case "<": - // v.emit(runtime.OpLt) + // v.emitABC(runtime.OpLt) // case "<=": - // v.emit(runtime.OpLte) + // v.emitABC(runtime.OpLte) // default: // panic(core.Error(ErrUnexpectedToken, op.GetText())) // } @@ -773,15 +778,15 @@ func (v *visitor) VisitPredicate(ctx *fql.PredicateContext) interface{} { // ctx.Predicate(0).Accept(v) // ctx.Predicate(1).Accept(v) // - // v.emit(runtime.OpIn) + // v.emitABC(runtime.OpIn) //} else if op := ctx.LikeOperator(); op != nil { // ctx.Predicate(0).Accept(v) // ctx.Predicate(1).Accept(v) // // if op.(*fql.LikeOperatorContext).Not() != nil { - // v.emit(runtime.OpNotLike) + // v.emitABC(runtime.OpNotLike) // } else { - // v.emit(runtime.OpLike) + // v.emitABC(runtime.OpLike) // } //} else if c := ctx.ExpressionAtom(); c != nil { // startCatch := len(v.instructions) @@ -803,11 +808,11 @@ func (v *visitor) VisitExpressionAtom(ctx *fql.ExpressionAtomContext) interface{ switch op.GetText() { case "*": - v.emitTemp(runtime.OpMulti) + v.emit(runtime.OpMulti) case "/": - v.emitTemp(runtime.OpDiv) + v.emit(runtime.OpDiv) case "%": - v.emitTemp(runtime.OpMod) + v.emit(runtime.OpMod) } } else if op := ctx.AdditiveOperator(); op != nil { ctx.ExpressionAtom(0).Accept(v) @@ -815,9 +820,9 @@ func (v *visitor) VisitExpressionAtom(ctx *fql.ExpressionAtomContext) interface{ switch op.GetText() { case "+": - v.emitTemp(runtime.OpAdd) + v.emit(runtime.OpAdd) case "-": - v.emitTemp(runtime.OpSub) + v.emit(runtime.OpSub) } } else if op := ctx.RegexpOperator(); op != nil { ctx.ExpressionAtom(0).Accept(v) @@ -825,9 +830,9 @@ func (v *visitor) VisitExpressionAtom(ctx *fql.ExpressionAtomContext) interface{ switch op.GetText() { case "=~": - v.emitTemp(runtime.OpRegexpPositive) + v.emit(runtime.OpRegexpPositive) case "!~": - v.emitTemp(runtime.OpRegexpNegative) + v.emit(runtime.OpRegexpNegative) default: panic(core.Error(ErrUnexpectedToken, op.GetText())) } @@ -863,8 +868,8 @@ func (v *visitor) endScope() { // Pop all local variables from the stack within the closed scope. for len(v.locals) > 0 && v.locals[len(v.locals)-1].Depth > v.scope { - v.registers.LivenessAnalysis() - v.emit(runtime.OpPopLocal) + // TODO: Free registers + v.locals = v.locals[:len(v.locals)-1] } } @@ -895,7 +900,7 @@ func (v *visitor) beginLoopScope(passThrough, distinct bool) { // } // // resultPos = v.operandsStackTracker - // v.emit(runtime.OpLoopInitOutput, arg) + // v.emitABC(runtime.OpLoopInitOutput, arg) //} else { // resultPos = prevResult //} @@ -907,15 +912,17 @@ func (v *visitor) beginLoopScope(passThrough, distinct bool) { } func (v *visitor) patchLoopScope(jump int) { - v.loops[len(v.loops)-1].position = jump + //v.loops[len(v.loops)-1].position = jump } func (v *visitor) resolveLoopResultPosition() int { - return v.loops[len(v.loops)-1].result + //return v.loops[len(v.loops)-1].result + return 0 } func (v *visitor) resolveLoopPosition() int { - return v.loops[len(v.loops)-1].position + //return v.loops[len(v.loops)-1].position + return 0 } func (v *visitor) endLoopScope() { @@ -930,13 +937,13 @@ func (v *visitor) endLoopScope() { //} // //if unwrap { - // v.emit(runtime.OpLoopUnwrapOutput) + // v.emitABC(runtime.OpLoopUnwrapOutput) //} } func (v *visitor) resolveLocalVariable(name string) int { for i := len(v.locals) - 1; i >= 0; i-- { - if v.locals[i].name == name { + if v.locals[i].Name == name { return i } } @@ -949,14 +956,14 @@ func (v *visitor) readVariable(name string) { return } - //// Resolve the variable name to an index. - //arg := v.resolveLocalVariable(name) - // - //if arg > -1 { - // v.emit(runtime.OpLoadLocal, arg) - // - // return - //} + // Resolve the variable name to an index. + arg := v.resolveLocalVariable(name) + + if arg > -1 { + v.emitABC(runtime.OpLoadLocal, arg) + + return + } // //index, ok := v.globals[name] // @@ -964,10 +971,10 @@ func (v *visitor) readVariable(name string) { // panic(core.Error(ErrVariableNotFound, name)) //} // - //v.emit(runtime.OpLoadGlobal, index) + //v.emitABC(runtime.OpLoadGlobal, index) } -func (v *visitor) declareVariable(name string) int { +func (v *visitor) declareVariable(name string) Register { if name == ignorePseudoVariable { return -1 } @@ -980,7 +987,7 @@ func (v *visitor) declareVariable(name string) int { panic(core.Error(ErrVariableNotUnique, name)) } - index := v.addConstant(values.String(name)) + index := int(v.addConstant(values.String(name))) v.globals[name] = index return index @@ -990,16 +997,18 @@ func (v *visitor) declareVariable(name string) int { for i := len(v.locals) - 1; i >= 0; i-- { local := v.locals[i] - if local.depth > -1 && local.depth < v.scope { + if local.Depth > -1 && local.Depth < v.scope { break } - if local.name == name { + if local.Name == name { panic(core.Error(ErrVariableNotUnique, name)) } } - v.locals = append(v.locals, variable{name, undefinedVariable}) + register := v.registers.AllocateLocalVarRegister(name) + + v.locals = append(v.locals, Variable{Name: name, Register: register, Depth: v.scope}) return len(v.locals) - 1 } @@ -1007,18 +1016,18 @@ func (v *visitor) declareVariable(name string) int { // defineVariable defines a variable in the current scope. func (v *visitor) defineVariable(index int) { if v.scope == 0 { - v.emit(runtime.OpStoreGlobal, index) + v.emitAB(runtime.OpStoreGlobal, index) return } - v.emit(runtime.OpStoreLocal, index) + v.emitABC(runtime.OpStoreLocal, index) v.locals[index].depth = v.scope } // emitConstant emits an opcode with a constant argument. func (v *visitor) emitConstant(constant core.Value) { - //v.emit(runtime.OpPush, v.addConstant(constant)) + //v.emitABC(runtime.OpPush, v.addConstant(constant)) } // emitLoop emits a loop instruction. @@ -1029,8 +1038,8 @@ func (v *visitor) emitLoop(loopStart int) { } // emitJump emits an opcode with a jump result argument. -func (v *visitor) emitJump(op runtime.OpCode) int { - //v.emit(op, jumpPlaceholder) +func (v *visitor) emitJump(op runtime.Opcode) int { + //v.emitABC(op, jumpPlaceholder) // //return len(v.instructions) @@ -1048,39 +1057,35 @@ func (v *visitor) patchJumpWith(offset, jump int) { } func (v *visitor) emitPopAndClose() { - //v.emit(runtime.OpPopClose) + //v.emitABC(runtime.OpPopClose) } -func (v visitor) emitTemp(op runtime.OpCode) { - // Allocate result register - resultReg, err := v.registers.AllocateRegister(VarTemporary) - - if err != nil { - panic(err) - } - - v.emit(op, resultReg, 0, 0) -} +//func (v visitor) emit(op runtime.Opcode) { +// // Allocate result register +// resultReg := v.registers.AllocateRegister(VarTemporary) +// +// v.emitABC(op, resultReg, 0, 0) +//} -func (v *visitor) emitDS(op runtime.OpCode, dest, src1 Register) { - v.emit(op, dest, src1, 0) +func (v *visitor) emitA(op runtime.Opcode, dest int) { + v.emitABC(op, dest, 0, 0) } -func (v *visitor) emitD(op runtime.OpCode, dest Register) { - v.emit(op, dest, 0, 0) +func (v *visitor) emitAB(op runtime.Opcode, dest, src1 int) { + v.emitABC(op, dest, src1, 0) } -func (v *visitor) emit(op runtime.OpCode, dest, src1, src2 Register) { +func (v *visitor) emitABC(op runtime.Opcode, dest, src1, src2 int) { v.instructions = append(v.instructions, Instruction{ OpCode: op, - Operands: [3]Register{dest, src1, src2}, + Operands: [3]int{dest, src1, src2}, }) } // addConstant adds a constant to the constants pool and returns its index. // If the constant is a scalar, it will be deduplicated. // If the constant is not a scalar, it will be added to the pool without deduplication. -func (v *visitor) addConstant(constant core.Value) int { +func (v *visitor) addConstant(constant core.Value) Constant { var hash uint64 if values.IsScalar(constant) { @@ -1094,7 +1099,7 @@ func (v *visitor) addConstant(constant core.Value) int { } v.constants = append(v.constants, constant) - p := len(v.constants) - 1 + p := Constant(len(v.constants) - 1) if hash > 0 { v.constantsIndex[hash] = p diff --git a/pkg/runtime/instruction.go b/pkg/runtime/instruction.go index e3e473cef..24e9b315e 100644 --- a/pkg/runtime/instruction.go +++ b/pkg/runtime/instruction.go @@ -1,6 +1,6 @@ package runtime type Instruction struct { - Opcode OpCode + Opcode Opcode Operands [3]int } diff --git a/pkg/runtime/opcode.go b/pkg/runtime/opcode.go index 800327695..3c46c26f6 100644 --- a/pkg/runtime/opcode.go +++ b/pkg/runtime/opcode.go @@ -1,15 +1,15 @@ package runtime -type OpCode byte +type Opcode byte const ( - OpMove OpCode = iota - OpLoadConst // Load a constant to a register A - OpLoadNone // Load None to a register A - OpLoadTrue // Load True to a register A - OpLoadFalse // Load False to a register A - OpLoadGlobal // Load a global variable to a register A - OpStoreGlobal // Store a value from register A to a global variable + OpMove Opcode = iota + OpLoadConst // Load a constant to a register A + OpLoadNone // Load None to a register A + OpLoadTrue // Load True to a register A + OpLoadFalse // Load False to a register A + OpLoadGlobal // Load a global variable to a register A + OpStoreGlobal // Store a value from register A to a global variable OpAdd OpSub diff --git a/pkg/runtime/program.go b/pkg/runtime/program.go index dbfe52b5b..f1d98a671 100644 --- a/pkg/runtime/program.go +++ b/pkg/runtime/program.go @@ -29,7 +29,7 @@ func (program *Program) Disassemble() string { return buf.String() } -func (program *Program) disassembleInstruction(out io.Writer, opcode OpCode, offset int) { +func (program *Program) disassembleInstruction(out io.Writer, opcode Opcode, offset int) { switch opcode { default: return diff --git a/pkg/runtime/vm.go b/pkg/runtime/vm.go index 7c56356b9..2f9ee31df 100644 --- a/pkg/runtime/vm.go +++ b/pkg/runtime/vm.go @@ -22,6 +22,8 @@ func NewVM(opts ...EnvironmentOption) *VM { return vm } +// TODO: Move program to the constructor. No need to pass it as an argument since the VM is stateful. +// But the environment can be passed as an argument. func (vm *VM) Run(ctx context.Context, program *Program) ([]byte, error) { //tryCatch := func(pos int) bool { // for _, pair := range program.CatchTable {