From 0b1ed43ae506014d49a8a98f8a9a1935c2741e07 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 18 Mar 2024 20:00:14 -0400 Subject: [PATCH] Demo changes --- Makefile | 96 ++++++----------------------- code/code.go | 8 +++ examples/demo.monkey | 2 +- examples/fact.monkey | 8 +++ examples/factt.monkey | 8 +++ examples/hello.monkey | 1 + testdata/popbug1.monkey | 1 + testdata/popbug2.monkey | 1 + testdata/popbug3.monkey | 1 + vm/frame.go | 4 ++ vm/vm.go | 19 +++++- vm/vm_test.go | 133 ++++++++++++++++++++++++++++++---------- 12 files changed, 171 insertions(+), 111 deletions(-) create mode 100644 examples/fact.monkey create mode 100644 examples/factt.monkey create mode 100644 examples/hello.monkey create mode 100644 testdata/popbug1.monkey create mode 100644 testdata/popbug2.monkey create mode 100644 testdata/popbug3.monkey diff --git a/Makefile b/Makefile index 4187880..12d27b4 100644 --- a/Makefile +++ b/Makefile @@ -1,93 +1,33 @@ -.PHONY: dev build cli server install image release profile compare bench test clean +.PHONY: dev build install image profile bench test clean CGO_ENABLED=0 +COMMIT=$(shell git rev-parse --short HEAD) -VERSION ?= $(shell git describe 2>/dev/null || echo "") +all: dev -DESTDIR ?= $(GOBIN) +dev: build + @./monkey-lang -d -ifeq ($(LOCAL), 1) -IMAGE := r.mills.io/prologic/monkey -TAG := dev -else -ifeq ($(BRANCH), master) -IMAGE := prologic/monkey -TAG := latest -else -IMAGE := prologic/monkey -TAG := dev -endif -endif - -all: help - -help: ## Show this help message - @echo "monkey - Monkey Lang" - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) - -dev: cli ## Build monkey and run the REPL - @./monkey - -build: clean cli server ## Build monkey - -cli: ## Build monkey CLI +build: clean @go build \ -tags "netgo static_build" -installsuffix netgo \ - -ldflags "-w -X go.mills.io/monkey/v2.MonkeyVersion=$(VERSION)" \ - ./cmd/monkey/... + -ldflags "-w -X $(shell go list)/version/.GitCommit=$(COMMIT)" \ + . -server: ## Build Monkey server - @go build \ - -tags "netgo static_build" -installsuffix netgo \ - -ldflags "-w -X go.mills.io/monkey/v2.MonkeyVersion=$(VERSION)" \ - ./cmd/monkey-server/... +install: build + @go install -install: cli server ## Install monkey to $DESTDIR - @install -D -m 755 monkey $(DESTDIR)/monkey - @install -D -m 755 monkey-server $(DESTDIR)monkey-server +image: + @docker build -t prologic/monkey-lang . -ifeq ($(PUBLISH), 1) -image: ## Build and Publish the Docker image - @docker buildx build \ - --build-arg VERSION="$(VERSION)" \ - --build-arg COMMIT="$(COMMIT)" \ - --build-arg BUILD="$(BUILD)" \ - --platform linux/amd64,linux/arm64 --push -t $(IMAGE):$(TAG) . -else -image: ## Build the Docker image - @docker build \ - --build-arg VERSION="$(VERSION)" -t $(IMAGE):$(TAG) . -endif - -release: ## Release monkey - @./tools/release.sh - -profile: ## Run tests with profiling enabled +profile: @go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench ./... -compare: ## Run benchmarks comparing Monkey with other languages - @hyperfine -w 5 -p 'make build; gcc -o examples/fib examples/fib.c' \ - -n c -n go -n tengo -n python -n tauc -n taugo -n monkey \ - --sort mean-time --export-markdown Benchmark.md \ - './examples/fib' \ - 'go run examples/fib.go' \ - 'tengo examples/fib.tengo' \ - 'python3 examples/fib.py' \ - 'tauc examples/fib.tau' \ - 'taugo examples/fib.tau' \ - './monkey examples/fib.m' - -bench: # Run test benchmarks +bench: @go test -v -benchmem -bench=. ./... -test: ## Run unit tests - @go test -v \ - -cover \ - -coverprofile coverage.out \ - -covermode atomic \ - -coverpkg ./... \ - -race \ - ./... +test: + @go test -v -cover -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... -race ./... -clean: ## Cleanup untrakced files - @git clean -f -d -X 2> /dev/null || true \ No newline at end of file +clean: + @git clean -f -d -X \ No newline at end of file diff --git a/code/code.go b/code/code.go index a7d59cb..3dbcd5f 100644 --- a/code/code.go +++ b/code/code.go @@ -10,6 +10,14 @@ type Instructions []byte type Opcode byte +func (o Opcode) String() string { + def, err := Lookup(byte(o)) + if err != nil { + return "" + } + return def.Name +} + const ( OpConstant Opcode = iota OpAdd diff --git a/examples/demo.monkey b/examples/demo.monkey index 4a06e8d..8ebe304 100644 --- a/examples/demo.monkey +++ b/examples/demo.monkey @@ -40,4 +40,4 @@ let map = fn(arr, f) { }; let numbers = [1, 1 + 1, 4 - 1, 2 * 2, 2 + 3, 12 / 2]; -map(numbers, fibonacci); \ No newline at end of file +//map(numbers, fibonacci); \ No newline at end of file diff --git a/examples/fact.monkey b/examples/fact.monkey new file mode 100644 index 0000000..ba5153c --- /dev/null +++ b/examples/fact.monkey @@ -0,0 +1,8 @@ +let fact = fn(n) { + if (n == 0) { + return 1 + } + return n * fact(n - 1) +} + +print(fact(5) \ No newline at end of file diff --git a/examples/factt.monkey b/examples/factt.monkey new file mode 100644 index 0000000..5939894 --- /dev/null +++ b/examples/factt.monkey @@ -0,0 +1,8 @@ + let fact = fn(n, a) { + if (n == 0) { + return a + } + return fact(n - 1, a * n) +} + +print(fact(5, 1)) \ No newline at end of file diff --git a/examples/hello.monkey b/examples/hello.monkey new file mode 100644 index 0000000..1dc45ac --- /dev/null +++ b/examples/hello.monkey @@ -0,0 +1 @@ +print("Hello World!") \ No newline at end of file diff --git a/testdata/popbug1.monkey b/testdata/popbug1.monkey new file mode 100644 index 0000000..4431c96 --- /dev/null +++ b/testdata/popbug1.monkey @@ -0,0 +1 @@ +let n = 2; let x = 0; while (n > 0) { if (n > 1) { x = x + 1 }; n = n - 1; }; x; \ No newline at end of file diff --git a/testdata/popbug2.monkey b/testdata/popbug2.monkey new file mode 100644 index 0000000..64d053c --- /dev/null +++ b/testdata/popbug2.monkey @@ -0,0 +1 @@ +let n = 2; let x = 0; while (n > 0) { if (n > 1) { x = x + 1 }; let n = n - 1; }; x; \ No newline at end of file diff --git a/testdata/popbug3.monkey b/testdata/popbug3.monkey new file mode 100644 index 0000000..ca3968b --- /dev/null +++ b/testdata/popbug3.monkey @@ -0,0 +1 @@ +let x = 1; if (x == 1) { let x = 2 } \ No newline at end of file diff --git a/vm/frame.go b/vm/frame.go index 6a44dbd..9f6a8ca 100644 --- a/vm/frame.go +++ b/vm/frame.go @@ -21,3 +21,7 @@ func NewFrame(cl *object.Closure, basePointer int) *Frame { func (f *Frame) Instructions() code.Instructions { return f.cl.Fn.Instructions } + +func (f *Frame) NextOp() code.Opcode { + return code.Opcode(f.Instructions()[f.ip+1]) +} diff --git a/vm/vm.go b/vm/vm.go index b89f595..d82d627 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -119,7 +119,11 @@ func (vm *VM) Run() error { } case code.OpPop: - vm.pop() + // This makes things like this work: + // >> let x = 1; if (x == 1) { x = 2 } + if vm.sp > 0 { + vm.pop() + } case code.OpTrue: err := vm.push(True) @@ -563,6 +567,19 @@ func (vm *VM) callClosure(cl *object.Closure, numArgs int) error { return fmt.Errorf("wrong number of arguments: want=%d, got=%d", cl.Fn.NumParameters, numArgs) } + // Optimize tail calls and avoid a new frame + if cl.Fn == vm.currentFrame().cl.Fn { + nextOP := vm.currentFrame().NextOp() + if nextOP == code.OpReturn { + for p := 0; p < numArgs; p++ { + vm.stack[vm.currentFrame().basePointer+p] = vm.stack[vm.sp-numArgs+p] + } + vm.sp -= numArgs + 1 + vm.currentFrame().ip = -1 // reset IP to the beginning of the frame + return nil + } + } + frame := NewFrame(cl, vm.sp-numArgs) vm.pushFrame(frame) vm.sp = frame.basePointer + cl.Fn.NumLocals diff --git a/vm/vm_test.go b/vm/vm_test.go index c858cd4..778d2cd 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -833,6 +833,40 @@ func TestAssignmentStatements(t *testing.T) { runVmTests(t, tests) } +func TestTailCalls(t *testing.T) { + tests := []vmTestCase{ + { + input: ` + let fact = fn(n, a) { + if (n == 0) { + return a + } + return fact(n - 1, a * n) + } + + fact(5, 1) + `, + expected: 120, + }, + + // without tail recursion optimization this will cause a stack overflow + { + input: ` + let iter = fn(n, max) { + if (n == max) { + return n + } + return iter(n + 1, max) + } + iter(0, 9999) + `, + expected: 9999, + }, + } + + runVmTests(t, tests) +} + func TestIntegration(t *testing.T) { matches, err := filepath.Glob("../testdata/*.monkey") if err != nil { @@ -874,42 +908,79 @@ func TestIntegration(t *testing.T) { } } -func TestExamples(t *testing.T) { - matches, err := filepath.Glob("../examples/*.monkey") - if err != nil { - t.Error(err) +func BenchmarkFibonacci(b *testing.B) { + tests := map[string]string{ + "iterative": ` + let fib = fn(n) { + if (n < 3) { + return 1 + } + let a = 1 + let b = 1 + let c = 0 + let i = 0 + while (i < n - 2) { + c = a + b + b = a + a = c + i = i + 1 + } + return a + } + + fib(35) + `, + "recursive": ` + let fib = fn(x) { + if (x == 0) { + return 0 + } + if (x == 1) { + return 1 + } + return fib(x-1) + fib(x-2) + } + + fib(35) + `, + "tail-recursive": ` + let fib = fn(n, a, b) { + if (n == 0) { + return a + } + if (n == 1) { + return b + } + return fib(n - 1, b, a + b) + } + + fib(35, 0, 1) + `, } - for _, match := range matches { - basename := path.Base(match) - name := strings.TrimSuffix(basename, filepath.Ext(basename)) + for name, input := range tests { + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + program := parse(input) - t.Run(name, func(t *testing.T) { - b, err := os.ReadFile(match) - if err != nil { - t.Error(err) - } + c := compiler.New() + err := c.Compile(program) + if err != nil { + b.Log(input) + b.Fatalf("compiler error: %s", err) + } - input := string(b) - program := parse(input) + vm := New(c.Bytecode()) - c := compiler.New() - err = c.Compile(program) - if err != nil { - t.Log(input) - t.Fatalf("compiler error: %s", err) - } - - vm := New(c.Bytecode()) - - err = vm.Run() - if err != nil { - t.Log(input) - t.Fatalf("vm error: %s", err) - } - if vm.sp != 0 { - t.Log(input) - t.Fatal("vm stack pointer non-zero") + err = vm.Run() + if err != nil { + b.Log(input) + b.Fatalf("vm error: %s", err) + } + if vm.sp != 0 { + b.Log(input) + b.Fatal("vm stack pointer non-zero") + } } }) }