From d10011646ab2b76d0bd06910e64ffd1e74b616cf Mon Sep 17 00:00:00 2001 From: Ryan Beltran Date: Fri, 3 Jan 2025 23:39:40 -0800 Subject: [PATCH] vm: Implement COMPARE instruction and remove IS_EQUAL --- vm/instruction_compare.go | 114 ++++++++++++++++++++++++++++++++ vm/instruction_compare_test.go | 91 +++++++++++++++++++++++++ vm/instruction_is_equal.go | 31 --------- vm/instruction_is_equal_test.go | 54 --------------- vm/instructions.go | 12 ++-- 5 files changed, 211 insertions(+), 91 deletions(-) create mode 100644 vm/instruction_compare.go create mode 100644 vm/instruction_compare_test.go delete mode 100644 vm/instruction_is_equal.go delete mode 100644 vm/instruction_is_equal_test.go diff --git a/vm/instruction_compare.go b/vm/instruction_compare.go new file mode 100644 index 0000000..24cfdc4 --- /dev/null +++ b/vm/instruction_compare.go @@ -0,0 +1,114 @@ +package vm + +import ( + "fmt" + "yolk/types" +) + +const ( + comparison_equal uint8 = iota + comparison_unequal uint8 = iota + comparison_less uint8 = iota + comparison_lte uint8 = iota + comparison_greater uint8 = iota + comparison_gte uint8 = iota +) + +type Instruction_COMPARE struct { + mode uint8 +} + +func (instruction *Instruction_COMPARE) Parse(args *string) error { + switch *args { + case "equal": + instruction.mode = comparison_equal + case "unequal": + instruction.mode = comparison_unequal + case "less": + instruction.mode = comparison_less + case "lte": + instruction.mode = comparison_lte + case "greater": + instruction.mode = comparison_greater + case "gte": + instruction.mode = comparison_gte + default: + if len(*args) == 0 { + return fmt.Errorf("COMPARE instruction needs a test mode, none provided") + } + return fmt.Errorf("COMPARE instruction specifies unexpected test mode %q", *args) + } + return nil +} + +func (instruction *Instruction_COMPARE) String() string { + switch instruction.mode { + case comparison_equal: + return "COMPARE equal" + case comparison_unequal: + return "COMPARE unequal" + case comparison_less: + return "COMPARE less" + case comparison_lte: + return "COMPARE lte" + case comparison_greater: + return "COMPARE greater" + case comparison_gte: + return "COMPARE gte" + default: + panic(fmt.Sprintf("Unimplemented COMPARE serialization for test mode %d", instruction.mode)) + } +} + +func (instruction *Instruction_COMPARE) Perform(vm *VirtualMachine) error { + left, err := vm.stack.Pop() + if err != nil { + return fmt.Errorf("popping lhs for COMPARE: %v", err) + } + + right, err := vm.stack.Pop() + if err != nil { + return fmt.Errorf("popping rhs for COMPARE: %v", err) + } + + switch instruction.mode { + case comparison_equal: + vm.stack.Push(types.MakeBool(left.Equal(right))) + case comparison_unequal: + vm.stack.Push(types.MakeBool(!left.Equal(right))) + case comparison_less: + if lt, err := left.LessThan(right); err != nil { + return fmt.Errorf("computing \"less than\": %v", err) + } else { + vm.stack.Push(types.MakeBool(lt)) + } + case comparison_lte: + if left.Equal(right) { + vm.stack.Push(types.MakeBool(true)) + } else if lt, err := left.LessThan(right); err != nil { + return fmt.Errorf("computing \"less than or equal to\": %v", err) + } else { + vm.stack.Push(types.MakeBool(lt)) + } + case comparison_greater: + if left.Equal(right) { + vm.stack.Push(types.MakeBool(false)) + } else if lt, err := left.LessThan(right); err != nil { + return fmt.Errorf("computing \"greater than\": %v", err) + } else { + vm.stack.Push(types.MakeBool(!lt)) + } + case comparison_gte: + if left.Equal(right) { + vm.stack.Push(types.MakeBool(true)) + } else if lt, err := left.LessThan(right); err != nil { + return fmt.Errorf("computing \"greater than\": %v", err) + } else { + vm.stack.Push(types.MakeBool(!lt)) + } + default: + panic(fmt.Sprintf("Unimplemented COMPARE for test mode %d", instruction.mode)) + } + + return nil +} diff --git a/vm/instruction_compare_test.go b/vm/instruction_compare_test.go new file mode 100644 index 0000000..38f6d8e --- /dev/null +++ b/vm/instruction_compare_test.go @@ -0,0 +1,91 @@ +package vm + +import ( + "fmt" + "testing" + "yolk/types" +) + +func TestIsEqualParsing(t *testing.T) { + expected_type := "*vm.Instruction_COMPARE" + + ExpectParse(t, "COMPARE equal", expected_type, "COMPARE equal") + ExpectParse(t, "COMPARE unequal", expected_type, "COMPARE unequal") + ExpectParse(t, "COMPARE less", expected_type, "COMPARE less") + ExpectParse(t, "COMPARE lte", expected_type, "COMPARE lte") + ExpectParse(t, "COMPARE greater", expected_type, "COMPARE greater") + ExpectParse(t, "COMPARE gte", expected_type, "COMPARE gte") + ExpectParseFailure(t, "COMPARE", "needs a test mode") + ExpectParseFailure(t, "COMPARE 1", "unexpected test mode") + ExpectParseFailure(t, "COMPARE foo", "unexpected test mode") + ExpectParseFailure(t, "COMPARE true", "unexpected test mode") +} + +type CompareTestCase struct { + left types.Primitive + right types.Primitive + comparison string + expected bool +} + +func TestComparePerform(t *testing.T) { + tests := []CompareTestCase{ + {types.MakeString("foo"), types.MakeString("bar"), "equal", false}, + {types.MakeString("foo"), types.MakeString("foo"), "equal", true}, + {types.MakeString("foo"), types.MakeString("bar"), "unequal", true}, + {types.MakeString("foo"), types.MakeString("foo"), "unequal", false}, + {types.MakeString("a"), types.MakeString("b"), "less", true}, + {types.MakeString("a"), types.MakeString("a"), "less", false}, + {types.MakeString("b"), types.MakeString("a"), "less", false}, + {types.MakeString("a"), types.MakeString("b"), "lte", true}, + {types.MakeString("a"), types.MakeString("a"), "lte", true}, + {types.MakeString("b"), types.MakeString("a"), "lte", false}, + {types.MakeString("a"), types.MakeString("b"), "greater", false}, + {types.MakeString("a"), types.MakeString("a"), "greater", false}, + {types.MakeString("b"), types.MakeString("a"), "greater", true}, + {types.MakeString("a"), types.MakeString("b"), "gte", false}, + {types.MakeString("a"), types.MakeString("a"), "gte", true}, + {types.MakeString("b"), types.MakeString("a"), "gte", true}, + } + + for _, test := range tests { + vm := NewVM() + + vm.stack.Push(test.right) + vm.stack.Push(test.left) + + test_instruction := fmt.Sprintf("COMPARE %s", test.comparison) + + if instruction, err := ParseInstruction(test_instruction); err != nil { + t.Fatalf("Error parsing instruction %q: %v", test_instruction, err) + } else if err := instruction.Perform(&vm); err != nil { + t.Fatalf("Unexpected error executing %q: %v", test_instruction, err) + } else if stack_size := vm.stack.Size(); stack_size != 1 { + t.Fatalf("Unexpected stack to have 1 element after %q, had: %d", test_instruction, stack_size) + } else if top, err := vm.stack.Pop(); err != nil { + t.Fatalf("Unexpected error popping stack after %q: %v", test_instruction, err) + } else if top_bool, err := top.RequireBool(); err != nil { + t.Fatalf("Unexpected error interpretting result of %q as a bool: %v", test_instruction, err) + } else if top_bool.Truthy() != test.expected { + t.Fatalf("Expected %q to push %t, got %t", test_instruction, test.expected, !test.expected) + } + } +} + +func TestCompareArgFailures(t *testing.T) { + vm := NewVM() + + if instruction, err := ParseInstruction("COMPARE equal"); err != nil { + t.Fatalf("Error parsing instruction %q: %v", instruction, err) + } else if err := instruction.Perform(&vm); err == nil { + t.Fatalf("Expected error executing 'COMPARE equal', got success") + } + + vm.stack.Push(types.MakeString("foo")) + + if instruction, err := ParseInstruction("COMPARE equal"); err != nil { + t.Fatalf("Error parsing instruction %q: %v", instruction, err) + } else if err := instruction.Perform(&vm); err == nil { + t.Fatalf("Expected error executing 'COMPARE equal', got success") + } +} diff --git a/vm/instruction_is_equal.go b/vm/instruction_is_equal.go deleted file mode 100644 index a74aec3..0000000 --- a/vm/instruction_is_equal.go +++ /dev/null @@ -1,31 +0,0 @@ -package vm - -import ( - "fmt" - "yolk/types" -) - -type Instruction_IS_EQUAL struct{} - -func (instruction *Instruction_IS_EQUAL) Parse(args *string) error { - if len(*args) != 0 { - return fmt.Errorf("IS_EQUAL instruction expected no arguments, received %q", *args) - } - return nil -} - -func (instruction *Instruction_IS_EQUAL) String() string { - return "IS_EQUAL" -} - -func (instruction *Instruction_IS_EQUAL) Perform(vm *VirtualMachine) error { - if left, err := vm.stack.Pop(); err != nil { - return fmt.Errorf("popping lhs for IS_EQUAL: %v", err) - } else if right, err := vm.stack.Pop(); err != nil { - return fmt.Errorf("popping rhs for IS_EQUAL: %v", err) - } else { - equal := types.MakeBool(left.Equal(right)) - vm.stack.Push(equal) - } - return nil -} diff --git a/vm/instruction_is_equal_test.go b/vm/instruction_is_equal_test.go deleted file mode 100644 index 50acfa7..0000000 --- a/vm/instruction_is_equal_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package vm - -import ( - "testing" - "yolk/types" -) - -func TestIsEqualParsing(t *testing.T) { - expected_type := "*vm.Instruction_IS_EQUAL" - - ExpectParse(t, "IS_EQUAL", expected_type, "IS_EQUAL") - ExpectParseFailure(t, "IS_EQUAL 1", "expected no arguments") - ExpectParseFailure(t, "IS_EQUAL foo", "expected no arguments") - ExpectParseFailure(t, "IS_EQUAL true", "expected no arguments") -} - -func TestIsEqualPerform(t *testing.T) { - vm := NewVM() - - vm.stack.Push(types.MakeString("foo")) - vm.stack.Push(types.MakeString("bar")) - - if instruction, err := ParseInstruction("IS_EQUAL"); err != nil { - t.Fatalf("Error parsing instruction %q: %v", instruction, err) - } else if err := instruction.Perform(&vm); err != nil { - t.Fatalf("Unexpected error executing IS_EQUAL: %v", err) - } else if stack_size := vm.stack.Size(); stack_size != 1 { - t.Fatalf("Unexpected stack to have 1 element after IS_EQUAL, had: %d", stack_size) - } else if top, err := vm.stack.Pop(); err != nil { - t.Fatalf("Unexpected error popping stack after IS_EQUAL: %v", err) - } else if top_bool, err := top.RequireBool(); err != nil { - t.Fatalf("Unexpected error interpretting result of IS_EQUAL as a bool: %v", err) - } else if top_bool.Truthy() { - t.Fatal("Expected IS_EQUAL to push false, got true") - } -} - -func TestIsEqualFailure(t *testing.T) { - vm := NewVM() - - if instruction, err := ParseInstruction("IS_EQUAL"); err != nil { - t.Fatalf("Error parsing instruction %q: %v", instruction, err) - } else if err := instruction.Perform(&vm); err == nil { - t.Fatalf("Expected error executing IS_EQUAL, got success") - } - - vm.stack.Push(types.MakeString("foo")) - - if instruction, err := ParseInstruction("IS_EQUAL"); err != nil { - t.Fatalf("Error parsing instruction %q: %v", instruction, err) - } else if err := instruction.Perform(&vm); err == nil { - t.Fatalf("Expected error executing IS_EQUAL, got success") - } -} diff --git a/vm/instructions.go b/vm/instructions.go index 267e0b1..df90e65 100644 --- a/vm/instructions.go +++ b/vm/instructions.go @@ -34,20 +34,20 @@ func ParseInstruction(yolk_line string) (Instruction, error) { return nil, err } return &instruction, nil - case "DECLARE_NAME": - var instruction Instruction_DECLARE_NAME + case "COMPARE": + var instruction Instruction_COMPARE if err := instruction.Parse(&args); err != nil { return nil, err } return &instruction, nil - case "EXEC": - var instruction Instruction_EXEC + case "DECLARE_NAME": + var instruction Instruction_DECLARE_NAME if err := instruction.Parse(&args); err != nil { return nil, err } return &instruction, nil - case "IS_EQUAL": - var instruction Instruction_IS_EQUAL + case "EXEC": + var instruction Instruction_EXEC if err := instruction.Parse(&args); err != nil { return nil, err }