Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/addition instruction #10

Merged
merged 13 commits into from
Oct 8, 2024
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ jobs:
go-version: '1.22'

- name: Install golines
run: go install github.com/segmentio/golines@v0.12.2
run: |
go install github.com/segmentio/golines@v0.12.2
go install golang.org/x/tools/cmd/goimports@v0.25.0

- name: Check format
run: |
if [-n "$(golines -l .)" ]; then
if [-n "$(golines --base-formatter goimports -l .)" ]; then
echo "The following files are not formatted:"
golines -l .
exit 1
Expand Down
152 changes: 152 additions & 0 deletions pkg/code/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Package code contains the instruction set, the set of operations for the virtual machine.
package code

// The fetch-decode-execute cycle, aka instruction cycle, is the clock speed for a CPU.
// A computer's memory is segmented into "words" - smallest unit of memory - which is usually 32/64 bits.

import (
"bytes"
"encoding/binary"
"fmt"
"strings"
)

// Instructions represents instructions for the virtual machine.
// An instruction is a small, basic command that tells the simulated processor what to do.
// It is made up of an Opcode and an operator.
type Instructions []byte

func (ins Instructions) String() string {
var out bytes.Buffer

// i represents the index of the instruction in the slice of instructions
i := 0
for i < len(ins) {
definition, err := Lookup(ins[i])
if err != nil {
fmt.Fprintf(&out, "ERROR: %s\n", err)
continue

Check warning on line 28 in pkg/code/code.go

View check run for this annotation

Codecov / codecov/patch

pkg/code/code.go#L27-L28

Added lines #L27 - L28 were not covered by tests
}

// +1 because the ith position is the opcode
operands, offset := ReadOperands(definition, ins[i+1:])
fmt.Fprintf(&out, "%04d %s\n", i, ins.fmtInstruction(definition, operands))

i += 1 + offset
}

return strings.TrimRight(out.String(), "\n")
}

// TODO: Rename method
func (ins Instructions) fmtInstruction(definition *Definition, operands []int) string {
if len(operands) != len(definition.OperandWidths) {
return fmt.Sprintf(
"ERROR: operand len %d does not match defined %d\n",
len(operands),
len(definition.OperandWidths),
)

Check warning on line 48 in pkg/code/code.go

View check run for this annotation

Codecov / codecov/patch

pkg/code/code.go#L44-L48

Added lines #L44 - L48 were not covered by tests
}

// No newline is needed because it is include in ins.String()
switch len(definition.OperandWidths) {
case 0:
return definition.Name
case 1:
return fmt.Sprintf("%s %d", definition.Name, operands[0])
}

return fmt.Sprintf("ERROR: unhandled operand count for %s\n", definition.Name)

Check warning on line 59 in pkg/code/code.go

View check run for this annotation

Codecov / codecov/patch

pkg/code/code.go#L59

Added line #L59 was not covered by tests
}

// Opcode represents the "operator" part of an instruction.
type Opcode byte

// We let iota generate the byte values because the actual values do not matter.
const (
OpConstant Opcode = iota // OpConstant retrives the constant using the operand as an index and pushes it onto the stack.
OpAdd // OpAdd pops two objects off the stack, adds them together, and adds the result on the stack.
OpPop // OpPop pops the top most element off the stack
OpSub // OpSub pops two objects off the stack, subtracts them, and pushes the result onto the stack.
OpDiv // OpDiv pops two objects off the stack, divdes them, and pushes the result onto the stack.
OpMul // OpMul pops two objects off the stack, multiples them, and pushes the result onto the stack.
)

// Definition represents the definition for an Opcode.
type Definition struct {
Name string // Name represents the name of the Opcode.
OperandWidths []int // OperandWidths represents the number of bytes each operand, the argument / parameter to an operator, uses.
}

var definitions = map[Opcode]*Definition{
OpConstant: {"OpConstant", []int{2}},
OpAdd: {"OpAdd", make([]int, 0)},
OpPop: {"OpPop", make([]int, 0)},
OpSub: {"OpSub", make([]int, 0)},
OpDiv: {"OpDiv", make([]int, 0)},
OpMul: {"OpMul", make([]int, 0)},
}

// Lookup gets the Opcode definition for a given byte.
func Lookup(op byte) (*Definition, error) {
def, ok := definitions[Opcode(op)]
if !ok {
return nil, fmt.Errorf("opcode %d is not defined", op)

Check warning on line 94 in pkg/code/code.go

View check run for this annotation

Codecov / codecov/patch

pkg/code/code.go#L94

Added line #L94 was not covered by tests
}

return def, nil
}

// Make creates an instruction from a given opcode and associated operands.
func Make(op Opcode, operands ...int) Instructions {
definition, ok := definitions[op]
if !ok {
// BUG: nil or empty slice?
return []byte{}

Check warning on line 105 in pkg/code/code.go

View check run for this annotation

Codecov / codecov/patch

pkg/code/code.go#L105

Added line #L105 was not covered by tests
}

var sumOfWidths int
for _, width := range definition.OperandWidths {
sumOfWidths += width
}

// +1 because we need to account for the length of the instruction
offset := 1
instruction := make([]byte, sumOfWidths+offset)
instruction[0] = byte(op)

// Iterate over the defined OperandWidths, take the matching element from the given operands and put it into the instruction.
for i, operand := range operands {
switch definition.OperandWidths[i] {
case 2:
binary.BigEndian.PutUint16(instruction[offset:], uint16(operand))
}
offset += definition.OperandWidths[i]
}

return instruction
}

// ReadOperands is the opposite of Make - converts a definition and instruction to respective opcode and operands.
// Returns the operands for the instruction and the offset which represents the index of the last operand in the instruction.
func ReadOperands(definition *Definition, instruction Instructions) ([]int, int) {
operands := make([]int, len(definition.OperandWidths))
offset := 0

for i, width := range definition.OperandWidths {
switch width {
case 2:
operands[i] = int(ReadUint16(instruction[offset:]))
}

offset += width
}

return operands, offset
}

// TODO: Figure out what is going on
// ReadUint16 reads an instruction which is stored in memory using big endian.
func ReadUint16(instruction Instructions) uint16 {
return binary.BigEndian.Uint16(instruction)
}
110 changes: 110 additions & 0 deletions pkg/code/code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package code

import (
"testing"
)

func TestMake(t *testing.T) {
tests := []struct {
op Opcode
operands []int
expect []byte
}{
// OpConstant's operand is two bytes wide meaning 65535 is the highest value that can be represented.
{OpConstant, []int{65534}, []byte{byte(OpConstant), 255, 254}},
{OpAdd, []int{}, []byte{byte(OpAdd)}},
{OpPop, []int{}, []byte{byte(OpPop)}},
{OpSub, []int{}, []byte{byte(OpSub)}},
{OpDiv, []int{}, []byte{byte(OpDiv)}},
{OpMul, []int{}, []byte{byte(OpMul)}},
}

for _, test := range tests {
instruction := Make(test.op, test.operands...)

if len(instruction) != len(test.expect) {
t.Errorf(
"instruction has wrong length. expected=%d, got=%d",
len(instruction),
len(test.expect),
)
}

for i, b := range test.expect {
if instruction[i] != b {
t.Errorf("wrong byte at position %d. expected=%d, got=%d", i, b, instruction[i])
}
}
}

}

func TestInstructionsString(t *testing.T) {
tests := []struct {
instructions []Instructions
expected string
}{
{
[]Instructions{
Make(OpConstant, 1),
Make(OpConstant, 2),
Make(OpConstant, 65534),
}, "0000 OpConstant 1\n0003 OpConstant 2\n0006 OpConstant 65534"},
{
[]Instructions{
Make(OpAdd),
Make(OpConstant, 2),
Make(OpConstant, 65534),
}, "0000 OpAdd\n0001 OpConstant 2\n0004 OpConstant 65534"},
}

for _, test := range tests {
concatted := Instructions{}
for _, instruction := range test.instructions {
concatted = append(concatted, instruction...)
}

if concatted.String() != test.expected {
t.Fatalf(
"instructions incorrectly formatted. expected=%s, got=%s",
test.expected,
concatted,
)
}
}
}

func TestReadOperands(t *testing.T) {
tests := []struct {
op Opcode
operands []int
bytesRead int
}{
{OpConstant, []int{65535}, 2},
{OpAdd, []int{}, 0},
{OpPop, []int{}, 0},
{OpMul, []int{}, 0},
{OpDiv, []int{}, 0},
{OpSub, []int{}, 0},
}

for _, test := range tests {
instruction := Make(test.op, test.operands...)

def, err := Lookup(byte(test.op))
if err != nil {
t.Fatalf("definition not found: %q\n", err)
}

operandsRead, n := ReadOperands(def, instruction[1:])
if n != test.bytesRead {
t.Fatalf("n wrong. expected=%d, got=%d", test.bytesRead, n)
}

for i, expected := range test.operands {
if operandsRead[i] != expected {
t.Errorf("operand wrong. expected=%d, got=%d", expected, operandsRead[i])
}
}
}
}
108 changes: 108 additions & 0 deletions pkg/compiler/compiler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Package compiler contains all of the logic for compiling an AST into bytecode.
package compiler

import (
"fmt"

"github.com/grantwforsythe/monkeylang/pkg/ast"
"github.com/grantwforsythe/monkeylang/pkg/code"
"github.com/grantwforsythe/monkeylang/pkg/object"
)

type Compiler struct {
instructions code.Instructions
constants []object.Object
}

// ByteCode represents a domain-specific language for a domain-specific virtual machine.
// It is called bytecode because the opcode in each instruction is one byte long.
type ByteCode struct {
Instructions code.Instructions // Instructions represent the instructions generated by the compiler.
Constants []object.Object // Constants represent the constants generated by the compiler.
}

// New initializes a new compiler.
func New() *Compiler {
return &Compiler{
instructions: code.Instructions{},
constants: []object.Object{}, // constants is a global pool for all constants.
}
}

// Compile traverses the nodes in the AST, converting it into bytecode.
func (c *Compiler) Compile(node ast.Node) error {
switch node := node.(type) {
case *ast.Program:
for _, stmt := range node.Statements {
err := c.Compile(stmt)
if err != nil {
return err

Check warning on line 39 in pkg/compiler/compiler.go

View check run for this annotation

Codecov / codecov/patch

pkg/compiler/compiler.go#L39

Added line #L39 was not covered by tests
}
}

case *ast.ExpressionStatement:
err := c.Compile(node.Expression)
if err != nil {
return err

Check warning on line 46 in pkg/compiler/compiler.go

View check run for this annotation

Codecov / codecov/patch

pkg/compiler/compiler.go#L46

Added line #L46 was not covered by tests
}
// Expression statements emit a value but don't store it like an assignment statement
c.emit(code.OpPop)

case *ast.InfixExpression:
err := c.Compile(node.Left)
if err != nil {
return err

Check warning on line 54 in pkg/compiler/compiler.go

View check run for this annotation

Codecov / codecov/patch

pkg/compiler/compiler.go#L54

Added line #L54 was not covered by tests
}

err = c.Compile(node.Right)
if err != nil {
return err

Check warning on line 59 in pkg/compiler/compiler.go

View check run for this annotation

Codecov / codecov/patch

pkg/compiler/compiler.go#L59

Added line #L59 was not covered by tests
}

switch node.Operator {
case "+":
c.emit(code.OpAdd)
case "-":
c.emit(code.OpSub)
case "*":
c.emit(code.OpMul)
case "/":
c.emit(code.OpDiv)
default:
return fmt.Errorf("unknown operator: %s", node.Operator)

Check warning on line 72 in pkg/compiler/compiler.go

View check run for this annotation

Codecov / codecov/patch

pkg/compiler/compiler.go#L71-L72

Added lines #L71 - L72 were not covered by tests
}

case *ast.IntegerLiteral:
integer := &object.Integer{Value: node.Value}
// The index of the newly added constant is used as an operand in the emitted instruction.
c.emit(code.OpConstant, c.addConstant(integer))
}
// Iterate over the instructions in memory, repeating the fetch-decode-execute cycle like in an actual machine.
return nil
}

// addConstant adds a constant to the constants pool.
// Returns the index of the newly added constant.
func (c *Compiler) addConstant(obj object.Object) int {
// PERF: Unperformant way to add elements to a slice because the cap is 0 by default and will always be x2 the len by default
c.constants = append(c.constants, obj)
return len(c.constants) - 1
}

// emit generates an instruction and add it to the results.
// Returns the position of the newly added instruction.
func (c *Compiler) emit(op code.Opcode, operands ...int) int {
instruction := code.Make(op, operands...)
// Starting position of the newly added instruction.
position := len(c.instructions)
// PERF: Unperformant way to add elements to a slice because the cap is 0 by default and will always be x2 the len by default
c.instructions = append(c.instructions, instruction...)
return position
}

func (c *Compiler) ByteCode() *ByteCode {
return &ByteCode{
Instructions: c.instructions,
Constants: c.constants,
}
}
Loading
Loading