diff --git a/vm/vm_test.go b/vm/vm_test.go index 3ed57d75b..d45ef2d6c 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -259,3 +259,935 @@ func TestRun_OpInvalid(t *testing.T) { _, err := vm.Run(program, nil) require.EqualError(t, err, "invalid opcode") } + +func TestVM_OpcodeOperations(t *testing.T) { + tests := []struct { + name string + expr string + env map[string]any + want any + expectError string + }{ + // Arithmetic Operations + { + name: "basic addition", + expr: "2 + 3", + want: 5, + }, + { + name: "mixed type arithmetic", + expr: "2.5 + 3", + want: 5.5, + }, + { + name: "chained arithmetic", + expr: "1 + 2 * 3 - 4 / 2", + want: 5.0, + }, + { + name: "modulo operation", + expr: "5 % 2", + want: 1, + }, + { + name: "exponent operation", + expr: "2 ^ 3", + want: 8.0, + }, + { + name: "negation", + expr: "-5", + want: -5, + }, + + // String Operations + { + name: "string concatenation", + expr: `"hello" + " " + "world"`, + want: "hello world", + }, + { + name: "string starts with", + expr: `"hello world" startsWith "hello"`, + want: true, + }, + { + name: "string ends with", + expr: `"hello world" endsWith "world"`, + want: true, + }, + { + name: "string contains", + expr: `"hello world" contains "lo wo"`, + want: true, + }, + { + name: "string matches regex", + expr: `"hello123" matches "^hello\\d+$"`, + want: true, + }, + + // Data Structure Operations + { + name: "array creation and access", + expr: "[1, 2, 3][1]", + want: 2, + }, + { + name: "map creation and access", + expr: `{"a": 1, "b": 2}.b`, + want: 2, + }, + { + name: "array length", + expr: "len([1, 2, 3])", + want: 3, + }, + { + name: "array slice", + expr: "[1, 2, 3, 4][1:3]", + want: []any{2, 3}, + }, + { + name: "array range", + expr: "1..5", + want: []int{1, 2, 3, 4, 5}, + }, + + // Error Cases + { + name: "invalid array index", + expr: "[1,2,3][5]", + expectError: "index out of range", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + program, err := expr.Compile(tt.expr, expr.Env(tt.env)) + require.NoError(t, err) + + testVM := &vm.VM{} + got, err := testVM.Run(program, tt.env) + + if tt.expectError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectError) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func TestVM_GroupAndSortOperations(t *testing.T) { + tests := []struct { + name string + expr string + env map[string]any + want any + expectError string + }{ + { + name: "group by single field", + expr: `groupBy([{"id": 1, "type": "a"}, {"id": 2, "type": "b"}, {"id": 3, "type": "a"}], #.type)`, + want: map[any][]any{ + "a": { + map[string]any{"id": 1, "type": "a"}, + map[string]any{"id": 3, "type": "a"}, + }, + "b": { + map[string]any{"id": 2, "type": "b"}, + }, + }, + }, + { + name: "sort by field ascending", + expr: `sortBy([{"id": 3}, {"id": 1}, {"id": 2}], #.id)`, + want: []any{ + map[string]any{"id": 1}, + map[string]any{"id": 2}, + map[string]any{"id": 3}, + }, + }, + { + name: "sort by field descending", + expr: `sortBy([{"id": 3}, {"id": 1}, {"id": 2}], #.id, "desc")`, + want: []any{ + map[string]any{"id": 3}, + map[string]any{"id": 2}, + map[string]any{"id": 1}, + }, + }, + { + name: "sort by computed value", + expr: `sortBy([1, 2, 3, 4], # % 2)`, + want: []any{2, 4, 1, 3}, + }, + { + name: "group by with complex key", + expr: `groupBy([1, 2, 3, 4, 5, 6], # % 2 == 0 ? "even" : "odd")`, + want: map[any][]any{ + "even": {2, 4, 6}, + "odd": {1, 3, 5}, + }, + }, + { + name: "invalid sort order", + expr: `sortBy([1, 2, 3], #, "invalid")`, + expectError: "unknown order", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + program, err := expr.Compile(tt.expr, expr.Env(tt.env)) + require.NoError(t, err) + + testVM := &vm.VM{} + got, err := testVM.Run(program, tt.env) + + if tt.expectError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectError) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +// TestVM_ProfileOperations tests the profiling opcodes +func TestVM_ProfileOperations(t *testing.T) { + program := &vm.Program{ + Bytecode: []vm.Opcode{ + vm.OpProfileStart, + vm.OpPush, + vm.OpProfileEnd, + }, + Arguments: []int{0, 0, 0}, + Constants: []any{ + &vm.Span{}, + }, + } + + testVM := &vm.VM{} + _, err := testVM.Run(program, nil) + require.NoError(t, err) + + span := program.Constants[0].(*vm.Span) + require.True(t, span.Duration > 0, "Profile duration should be greater than 0") +} + +// TestVM_IndexOperations tests the index manipulation opcodes +func TestVM_IndexOperations(t *testing.T) { + tests := []struct { + name string + expr string + want any + }{ + { + name: "decrement index in loop", + expr: "reduce([1,2,3], #acc + #, 0)", + want: 6, + }, + { + name: "set index in loop", + expr: "map([1,2,3], # * 2)", + want: []any{2, 4, 6}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + program, err := expr.Compile(tt.expr) + require.NoError(t, err) + + testVM := &vm.VM{} + got, err := testVM.Run(program, nil) + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +// TestVM_DirectCallOpcodes tests the specialized call opcodes directly +func TestVM_DirectCallOpcodes(t *testing.T) { + tests := []struct { + name string + bytecode []vm.Opcode + args []int + consts []any + funcs []vm.Function + want any + wantErr bool + }{ + { + name: "OpCall0", + bytecode: []vm.Opcode{vm.OpCall0}, + args: []int{0}, + funcs: []vm.Function{ + func(args ...any) (any, error) { + return 42, nil + }, + }, + want: 42, + }, + { + name: "OpCall1", + bytecode: []vm.Opcode{ + vm.OpPush, + vm.OpCall1, + }, + args: []int{0, 0}, + consts: []any{10}, + funcs: []vm.Function{ + func(args ...any) (any, error) { + return args[0].(int) * 2, nil + }, + }, + want: 20, + }, + { + name: "OpCall2", + bytecode: []vm.Opcode{ + vm.OpPush, + vm.OpPush, + vm.OpCall2, + }, + args: []int{0, 1, 0}, + consts: []any{10, 5}, + funcs: []vm.Function{ + func(args ...any) (any, error) { + return args[0].(int) + args[1].(int), nil + }, + }, + want: 15, + }, + { + name: "OpCall3", + bytecode: []vm.Opcode{ + vm.OpPush, + vm.OpPush, + vm.OpPush, + vm.OpCall3, + }, + args: []int{0, 1, 2, 0}, + consts: []any{10, 5, 2}, + funcs: []vm.Function{ + func(args ...any) (any, error) { + return args[0].(int) + args[1].(int) + args[2].(int), nil + }, + }, + want: 17, + }, + { + name: "OpCallN with error", + bytecode: []vm.Opcode{ + vm.OpLoadFunc, + vm.OpCallN, + }, + args: []int{0, 0}, // Function index, number of args (0) + funcs: []vm.Function{ + func(args ...any) (any, error) { + return nil, fmt.Errorf("test error") + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + program := vm.NewProgram( + nil, // source + nil, // node + nil, // locations + 0, // variables + tt.consts, + tt.bytecode, + tt.args, + tt.funcs, + nil, // debugInfo + nil, // span + ) + vm := &vm.VM{} + got, err := vm.Run(program, nil) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func TestVM_CallN(t *testing.T) { + input := `fn(1, 2, 3)` + + tree, err := parser.Parse(input) + require.NoError(t, err) + + env := map[string]any{ + "fn": func(args ...any) (any, error) { + sum := 0 + for _, arg := range args { + sum += arg.(int) + } + return sum, nil + }, + } + + config := conf.New(env) + program, err := compiler.Compile(tree, config) + require.NoError(t, err) + + out, err := vm.Run(program, env) + require.NoError(t, err) + require.Equal(t, 6, out) +} + +// TestVM_IndexAndCountOperations tests the index and count manipulation opcodes directly +func TestVM_IndexAndCountOperations(t *testing.T) { + tests := []struct { + name string + bytecode []vm.Opcode + args []int + consts []any + want any + wantErr bool + }{ + { + name: "GetIndex", + bytecode: []vm.Opcode{ + vm.OpPush, // Push array to stack + vm.OpBegin, // Start scope + vm.OpGetIndex, // Get current index + }, + args: []int{0, 0, 0}, + consts: []any{[]any{1, 2, 3}}, // Array for scope + want: 0, // Initial index is 0 + }, + { + name: "DecrementIndex", + bytecode: []vm.Opcode{ + vm.OpPush, // Push array to stack + vm.OpBegin, // Start scope + vm.OpDecrementIndex, // Decrement index + vm.OpGetIndex, // Get current index + }, + args: []int{0, 0, 0, 0}, + consts: []any{[]any{1, 2, 3}}, // Array for scope + want: -1, // After decrement + }, + { + name: "GetCount", + bytecode: []vm.Opcode{ + vm.OpPush, // Push array to stack + vm.OpBegin, // Start scope + vm.OpGetCount, // Get current count + }, + args: []int{0, 0, 0}, + consts: []any{[]any{1, 2, 3}}, // Array for scope + want: 0, // Initial count is 0 + }, + { + name: "IncrementCount", + bytecode: []vm.Opcode{ + vm.OpPush, // Push array to stack + vm.OpBegin, // Start scope + vm.OpIncrementCount, // Increment count + vm.OpGetCount, // Get current count + }, + args: []int{0, 0, 0, 0}, + consts: []any{[]any{1, 2, 3}}, // Array for scope + want: 1, // After increment + }, + { + name: "Multiple operations", + bytecode: []vm.Opcode{ + vm.OpPush, // Push array to stack + vm.OpBegin, // Start scope + vm.OpIncrementCount, // Count = 1 + vm.OpIncrementCount, // Count = 2 + vm.OpDecrementIndex, // Index = -1 + vm.OpDecrementIndex, // Index = -2 + vm.OpGetCount, // Push count (2) + vm.OpGetIndex, // Push index (-2) + vm.OpAdd, // Add them together + }, + args: []int{0, 0, 0, 0, 0, 0, 0, 0, 0}, + consts: []any{[]any{1, 2, 3}}, // Array for scope + want: 0, // 2 + (-2) = 0 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + program := vm.NewProgram( + nil, // source + nil, // node + nil, // locations + 0, // variables + tt.consts, + tt.bytecode, + tt.args, + nil, // functions + nil, // debugInfo + nil, // span + ) + vm := &vm.VM{} + got, err := vm.Run(program, nil) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +// TestVM_DirectBasicOpcodes tests basic opcodes directly +func TestVM_DirectBasicOpcodes(t *testing.T) { + tests := []struct { + name string + bytecode []vm.Opcode + args []int + consts []any + env any + want any + wantErr bool + }{ + { + name: "OpLoadEnv", + bytecode: []vm.Opcode{ + vm.OpLoadEnv, // Load entire environment + }, + args: []int{0}, + env: map[string]any{"key": "value"}, + want: map[string]any{"key": "value"}, + }, + { + name: "OpTrue", + bytecode: []vm.Opcode{ + vm.OpTrue, + }, + args: []int{0}, + want: true, + }, + { + name: "OpFalse", + bytecode: []vm.Opcode{ + vm.OpFalse, + }, + args: []int{0}, + want: false, + }, + { + name: "OpNil", + bytecode: []vm.Opcode{ + vm.OpNil, + }, + args: []int{0}, + want: nil, + }, + { + name: "OpNegate int", + bytecode: []vm.Opcode{ + vm.OpPush, // Push number + vm.OpNegate, // Negate it + }, + args: []int{0, 0}, + consts: []any{42}, + want: -42, + }, + { + name: "OpNegate float", + bytecode: []vm.Opcode{ + vm.OpPush, // Push number + vm.OpNegate, // Negate it + }, + args: []int{0, 0}, + consts: []any{42.5}, + want: -42.5, + }, + { + name: "OpNot true", + bytecode: []vm.Opcode{ + vm.OpTrue, // Push true + vm.OpNot, // Negate it + }, + args: []int{0, 0}, + want: false, + }, + { + name: "OpNot false", + bytecode: []vm.Opcode{ + vm.OpFalse, // Push false + vm.OpNot, // Negate it + }, + args: []int{0, 0}, + want: true, + }, + { + name: "OpNot error", + bytecode: []vm.Opcode{ + vm.OpPush, // Push non-bool + vm.OpNot, // Try to negate it + }, + args: []int{0, 0}, + consts: []any{"not a bool"}, + wantErr: true, + }, + { + name: "OpEqualString equal", + bytecode: []vm.Opcode{ + vm.OpPush, // Push first string + vm.OpPush, // Push second string + vm.OpEqualString, // Compare strings + }, + args: []int{0, 1, 0}, + consts: []any{"hello", "hello"}, + want: true, + }, + { + name: "OpEqualString not equal", + bytecode: []vm.Opcode{ + vm.OpPush, // Push first string + vm.OpPush, // Push second string + vm.OpEqualString, // Compare strings + }, + args: []int{0, 1, 0}, + consts: []any{"hello", "world"}, + want: false, + }, + { + name: "OpEqualString with empty strings", + bytecode: []vm.Opcode{ + vm.OpPush, // Push first string + vm.OpPush, // Push second string + vm.OpEqualString, // Compare strings + }, + args: []int{0, 1, 0}, + consts: []any{"", ""}, + want: true, + }, + { + name: "OpEqualString type error", + bytecode: []vm.Opcode{ + vm.OpPush, // Push non-string + vm.OpPush, // Push string + vm.OpEqualString, // Try to compare + }, + args: []int{0, 1, 0}, + consts: []any{42, "hello"}, + wantErr: true, + }, + { + name: "OpInt", + bytecode: []vm.Opcode{ + vm.OpInt, // Push int directly from args + }, + args: []int{42}, // The value 42 is passed directly in args + consts: []any{}, // No constants needed + want: 42, + }, + { + name: "OpInt negative", + bytecode: []vm.Opcode{ + vm.OpInt, // Push negative int directly from args + }, + args: []int{-42}, // The value -42 is passed directly in args + consts: []any{}, // No constants needed + want: -42, + }, + { + name: "OpInt zero", + bytecode: []vm.Opcode{ + vm.OpInt, // Push zero directly from args + }, + args: []int{0}, // The value 0 is passed directly in args + consts: []any{}, // No constants needed + want: 0, + }, + { + name: "OpIn array true", + bytecode: []vm.Opcode{ + vm.OpPush, // Push element + vm.OpPush, // Push array + vm.OpIn, // Check if element is in array + }, + args: []int{0, 1, 0}, + consts: []any{2, []any{1, 2, 3}}, + want: true, + }, + { + name: "OpIn array false", + bytecode: []vm.Opcode{ + vm.OpPush, // Push element + vm.OpPush, // Push array + vm.OpIn, // Check if element is in array + }, + args: []int{0, 1, 0}, + consts: []any{4, []any{1, 2, 3}}, + want: false, + }, + { + name: "OpIn map true", + bytecode: []vm.Opcode{ + vm.OpPush, // Push key + vm.OpPush, // Push map + vm.OpIn, // Check if key is in map + }, + args: []int{0, 1, 0}, + consts: []any{"b", map[string]any{"a": 1, "b": 2}}, + want: true, + }, + { + name: "OpIn map false", + bytecode: []vm.Opcode{ + vm.OpPush, // Push key + vm.OpPush, // Push map + vm.OpIn, // Check if key is in map + }, + args: []int{0, 1, 0}, + consts: []any{"c", map[string]any{"a": 1, "b": 2}}, + want: false, + }, + { + name: "OpExponent integers", + bytecode: []vm.Opcode{ + vm.OpPush, // Push base + vm.OpPush, // Push exponent + vm.OpExponent, // Calculate power + }, + args: []int{0, 1, 0}, + consts: []any{2, 3}, + want: 8.0, + }, + { + name: "OpExponent floats", + bytecode: []vm.Opcode{ + vm.OpPush, // Push base + vm.OpPush, // Push exponent + vm.OpExponent, // Calculate power + }, + args: []int{0, 1, 0}, + consts: []any{2.0, 3.0}, + want: 8.0, + }, + { + name: "OpExponent negative exponent", + bytecode: []vm.Opcode{ + vm.OpPush, // Push base + vm.OpPush, // Push exponent + vm.OpExponent, // Calculate power + }, + args: []int{0, 1, 0}, + consts: []any{2.0, -2.0}, + want: 0.25, + }, + { + name: "OpMatches valid regex", + bytecode: []vm.Opcode{ + vm.OpPush, // Push string + vm.OpPush, // Push pattern + vm.OpMatches, // Match string against pattern + }, + args: []int{0, 1, 0}, + consts: []any{"hello123", "^hello\\d+$"}, + want: true, + }, + { + name: "OpMatches non-matching regex", + bytecode: []vm.Opcode{ + vm.OpPush, // Push string + vm.OpPush, // Push pattern + vm.OpMatches, // Match string against pattern + }, + args: []int{0, 1, 0}, + consts: []any{"hello", "^\\d+$"}, + want: false, + }, + { + name: "OpMatches invalid regex", + bytecode: []vm.Opcode{ + vm.OpPush, // Push string + vm.OpPush, // Push pattern + vm.OpMatches, // Match string against pattern + }, + args: []int{0, 1, 0}, + consts: []any{"hello", "[invalid"}, + wantErr: true, + }, + { + name: "OpMatches type error", + bytecode: []vm.Opcode{ + vm.OpPush, // Push non-string + vm.OpPush, // Push pattern + vm.OpMatches, // Match against pattern + }, + args: []int{0, 1, 0}, + consts: []any{42, "^\\d+$"}, + wantErr: true, + }, + { + name: "OpCast int to float64", + bytecode: []vm.Opcode{ + vm.OpPush, // Push int + vm.OpCast, // Cast to float64 + }, + args: []int{0, 2}, + consts: []any{42}, + want: float64(42), + }, + { + name: "OpCast int32 to int64", + bytecode: []vm.Opcode{ + vm.OpPush, // Push int32 + vm.OpCast, // Cast to int64 + }, + args: []int{0, 1}, + consts: []any{int32(42)}, + want: int64(42), + }, + { + name: "OpCast invalid type", + bytecode: []vm.Opcode{ + vm.OpPush, // Push string + vm.OpCast, // Try to cast to float64 + }, + args: []int{0, 0}, + consts: []any{"not a number"}, + wantErr: true, + }, + { + name: "OpLen array", + bytecode: []vm.Opcode{ + vm.OpPush, // Push array + vm.OpLen, // Get length + }, + args: []int{0, 0}, + consts: []any{[]any{1, 2, 3}}, + want: 3, + }, + { + name: "OpLen empty array", + bytecode: []vm.Opcode{ + vm.OpPush, // Push empty array + vm.OpLen, // Get length + }, + args: []int{0, 0}, + consts: []any{[]any{}}, + want: 0, + }, + { + name: "OpLen string", + bytecode: []vm.Opcode{ + vm.OpPush, // Push string + vm.OpLen, // Get length + }, + args: []int{0, 0}, + consts: []any{"hello"}, + want: 5, + }, + { + name: "OpLen empty string", + bytecode: []vm.Opcode{ + vm.OpPush, // Push empty string + vm.OpLen, // Get length + }, + args: []int{0, 0}, + consts: []any{""}, + want: 0, + }, + { + name: "OpLen map", + bytecode: []vm.Opcode{ + vm.OpPush, // Push map + vm.OpLen, // Get length + }, + args: []int{0, 0}, + consts: []any{map[string]any{"a": 1, "b": 2, "c": 3}}, + want: 3, + }, + { + name: "OpLen empty map", + bytecode: []vm.Opcode{ + vm.OpPush, // Push empty map + vm.OpLen, // Get length + }, + args: []int{0, 0}, + consts: []any{map[string]any{}}, + want: 0, + }, + { + name: "OpLen invalid type", + bytecode: []vm.Opcode{ + vm.OpPush, // Push number + vm.OpLen, // Try to get length + }, + args: []int{0, 0}, + consts: []any{42}, + wantErr: true, + }, + { + name: "OpThrow with string", + bytecode: []vm.Opcode{ + vm.OpPush, // Push error message + vm.OpThrow, // Throw error + }, + args: []int{0, 0}, + consts: []any{"test error"}, + wantErr: true, + }, + { + name: "OpThrow with error", + bytecode: []vm.Opcode{ + vm.OpPush, // Push error + vm.OpThrow, // Throw error + }, + args: []int{0, 0}, + consts: []any{fmt.Errorf("test error")}, + wantErr: true, + }, + { + name: "OpDefault", + bytecode: []vm.Opcode{ + vm.OpEnd + 1, // OpEnd is always last, this is anunknown opcode + }, + args: []int{0, 0}, + consts: []any{fmt.Errorf("test error")}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + program := vm.NewProgram( + nil, // source + nil, // node + nil, // locations + 0, // variables + tt.consts, + tt.bytecode, + tt.args, + nil, // functions + nil, // debugInfo + nil, // span + ) + vm := &vm.VM{} + got, err := vm.Run(program, tt.env) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +}