Demo changes
Some checks failed
Build / build (push) Successful in 2m1s
Test / build (push) Failing after 11m48s

This commit is contained in:
Chuck Smith
2024-03-18 20:00:14 -04:00
parent ca4eed10b8
commit 0b1ed43ae5
12 changed files with 171 additions and 111 deletions

View File

@@ -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 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) build: clean
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
@go build \ @go build \
-tags "netgo static_build" -installsuffix netgo \ -tags "netgo static_build" -installsuffix netgo \
-ldflags "-w -X go.mills.io/monkey/v2.MonkeyVersion=$(VERSION)" \ -ldflags "-w -X $(shell go list)/version/.GitCommit=$(COMMIT)" \
./cmd/monkey/... .
server: ## Build Monkey server install: build
@go build \ @go install
-tags "netgo static_build" -installsuffix netgo \
-ldflags "-w -X go.mills.io/monkey/v2.MonkeyVersion=$(VERSION)" \
./cmd/monkey-server/...
install: cli server ## Install monkey to $DESTDIR image:
@install -D -m 755 monkey $(DESTDIR)/monkey @docker build -t prologic/monkey-lang .
@install -D -m 755 monkey-server $(DESTDIR)monkey-server
ifeq ($(PUBLISH), 1) profile:
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
@go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench ./... @go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench ./...
compare: ## Run benchmarks comparing Monkey with other languages bench:
@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
@go test -v -benchmem -bench=. ./... @go test -v -benchmem -bench=. ./...
test: ## Run unit tests test:
@go test -v \ @go test -v -cover -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... -race ./...
-cover \
-coverprofile coverage.out \
-covermode atomic \
-coverpkg ./... \
-race \
./...
clean: ## Cleanup untrakced files clean:
@git clean -f -d -X 2> /dev/null || true @git clean -f -d -X

View File

@@ -10,6 +10,14 @@ type Instructions []byte
type Opcode byte type Opcode byte
func (o Opcode) String() string {
def, err := Lookup(byte(o))
if err != nil {
return ""
}
return def.Name
}
const ( const (
OpConstant Opcode = iota OpConstant Opcode = iota
OpAdd OpAdd

View File

@@ -40,4 +40,4 @@ let map = fn(arr, f) {
}; };
let numbers = [1, 1 + 1, 4 - 1, 2 * 2, 2 + 3, 12 / 2]; let numbers = [1, 1 + 1, 4 - 1, 2 * 2, 2 + 3, 12 / 2];
map(numbers, fibonacci); //map(numbers, fibonacci);

8
examples/fact.monkey Normal file
View File

@@ -0,0 +1,8 @@
let fact = fn(n) {
if (n == 0) {
return 1
}
return n * fact(n - 1)
}
print(fact(5)

8
examples/factt.monkey Normal file
View File

@@ -0,0 +1,8 @@
let fact = fn(n, a) {
if (n == 0) {
return a
}
return fact(n - 1, a * n)
}
print(fact(5, 1))

1
examples/hello.monkey Normal file
View File

@@ -0,0 +1 @@
print("Hello World!")

1
testdata/popbug1.monkey vendored Normal file
View File

@@ -0,0 +1 @@
let n = 2; let x = 0; while (n > 0) { if (n > 1) { x = x + 1 }; n = n - 1; }; x;

1
testdata/popbug2.monkey vendored Normal file
View File

@@ -0,0 +1 @@
let n = 2; let x = 0; while (n > 0) { if (n > 1) { x = x + 1 }; let n = n - 1; }; x;

1
testdata/popbug3.monkey vendored Normal file
View File

@@ -0,0 +1 @@
let x = 1; if (x == 1) { let x = 2 }

View File

@@ -21,3 +21,7 @@ func NewFrame(cl *object.Closure, basePointer int) *Frame {
func (f *Frame) Instructions() code.Instructions { func (f *Frame) Instructions() code.Instructions {
return f.cl.Fn.Instructions return f.cl.Fn.Instructions
} }
func (f *Frame) NextOp() code.Opcode {
return code.Opcode(f.Instructions()[f.ip+1])
}

View File

@@ -119,7 +119,11 @@ func (vm *VM) Run() error {
} }
case code.OpPop: case code.OpPop:
// This makes things like this work:
// >> let x = 1; if (x == 1) { x = 2 }
if vm.sp > 0 {
vm.pop() vm.pop()
}
case code.OpTrue: case code.OpTrue:
err := vm.push(True) 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) 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) frame := NewFrame(cl, vm.sp-numArgs)
vm.pushFrame(frame) vm.pushFrame(frame)
vm.sp = frame.basePointer + cl.Fn.NumLocals vm.sp = frame.basePointer + cl.Fn.NumLocals

View File

@@ -833,6 +833,40 @@ func TestAssignmentStatements(t *testing.T) {
runVmTests(t, tests) 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) { func TestIntegration(t *testing.T) {
matches, err := filepath.Glob("../testdata/*.monkey") matches, err := filepath.Glob("../testdata/*.monkey")
if err != nil { if err != nil {
@@ -874,42 +908,79 @@ func TestIntegration(t *testing.T) {
} }
} }
func TestExamples(t *testing.T) { func BenchmarkFibonacci(b *testing.B) {
matches, err := filepath.Glob("../examples/*.monkey") tests := map[string]string{
if err != nil { "iterative": `
t.Error(err) 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
} }
for _, match := range matches { fib(35)
basename := path.Base(match) `,
name := strings.TrimSuffix(basename, filepath.Ext(basename)) "recursive": `
let fib = fn(x) {
t.Run(name, func(t *testing.T) { if (x == 0) {
b, err := os.ReadFile(match) return 0
if err != nil { }
t.Error(err) if (x == 1) {
return 1
}
return fib(x-1) + fib(x-2)
} }
input := string(b) 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 name, input := range tests {
b.Run(name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
program := parse(input) program := parse(input)
c := compiler.New() c := compiler.New()
err = c.Compile(program) err := c.Compile(program)
if err != nil { if err != nil {
t.Log(input) b.Log(input)
t.Fatalf("compiler error: %s", err) b.Fatalf("compiler error: %s", err)
} }
vm := New(c.Bytecode()) vm := New(c.Bytecode())
err = vm.Run() err = vm.Run()
if err != nil { if err != nil {
t.Log(input) b.Log(input)
t.Fatalf("vm error: %s", err) b.Fatalf("vm error: %s", err)
} }
if vm.sp != 0 { if vm.sp != 0 {
t.Log(input) b.Log(input)
t.Fatal("vm stack pointer non-zero") b.Fatal("vm stack pointer non-zero")
}
} }
}) })
} }