From d4ab19c2e6d1774ab0a8728299675511171c17c0 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Fri, 19 Jul 2024 14:54:47 -0700 Subject: [PATCH] wasm support (#37) * add comment about EvalOne's in interactive mode * start of wasm support, logger to stdout==web browser console * log now autoswitches to stdout if stderr is invalid * New tinygo target! 245K instead of 1.8M. Adding -c for equiv of bash -c. * build 2 variants of wasm * GOOS=wasip1 doesn't work withou -target, -no-debug makes smaller binaries * release version of fortio.org/log v1.15.0 * finally working :tada: * proper error handling, auto eval on return, auto resize of output/err * start smaller for input * added grolVersion, some fix to font size, new wasm-release target * working wasm-release target (embbeded version) --- .gitignore | 11 ++++- Makefile | 38 ++++++++++++++-- apply.gr | 2 + go.mod | 3 +- go.sum | 6 ++- main.go | 11 +++++ pi.gr | 16 +++++++ pi2.gr | 11 +++++ repl/repl.go | 1 + tags.go | 4 +- wasm/dev_server.go | 20 ++++++++ wasm/index.html | 111 +++++++++++++++++++++++++++++++++++++++++++++ wasm/wasm_main.go | 45 ++++++++++++++++++ 13 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 pi.gr create mode 100644 pi2.gr create mode 100644 wasm/dev_server.go create mode 100644 wasm/index.html create mode 100644 wasm/wasm_main.go diff --git a/.gitignore b/.gitignore index d02fd796..c88045cb 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,18 @@ go.work.sum .golangci.yml gorepl -grol .DS_Store __* # *_string.go dist/ +wasm/wasm_exec.js +wasm/wasm_exec.html +wasm/grol.wasm +wasm/test.wasm +wasm/grol_tiny.wasm +grol +grol.tiny +grol.wasm +grol_tiny.wasm +wasm/grol_tiny.wasm diff --git a/Makefile b/Makefile index 01854c5c..2a5711be 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,39 @@ GEN:=object/type_string.go parser/priority_string.go token/type_string.go grol: Makefile *.go */*.go $(GEN) CGO_ENABLED=0 go build -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" . - ls -l grol + ls -lh grol + +tinygo: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html + CGO_ENABLED=0 tinygo build -o grol.tiny -tags "$(GO_BUILD_TAGS)" . + strip grol.tiny + ls -lh grol.tiny + +wasm: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html +# GOOS=wasip1 GOARCH=wasm go build -o grol.wasm -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" . + GOOS=js GOARCH=wasm go build -o wasm/grol.wasm -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" ./wasm +# GOOS=wasip1 GOARCH=wasm tinygo build -target=wasi -no-debug -o grol_tiny.wasm -tags "$(GO_BUILD_TAGS)" . +# Tiny go generates errors https://github.com/tinygo-org/tinygo/issues/1140 +# GOOS=js GOARCH=wasm tinygo build -no-debug -o wasm/test.wasm -tags "$(GO_BUILD_TAGS)" ./wasm + -ls -lh wasm/*.wasm + -pkill wasm + go run ./wasm ./wasm & + sleep 3 + open http://localhost:8080/ + +GIT_TAG:=$(shell git describe --tags --abbrev=0) +# used to copy to site a release version +wasm-release: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html + @echo "Building wasm release GIT_TAG=$(GIT_TAG)" + GOOS=js GOARCH=wasm go install -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" grol.io/grol/wasm@$(GIT_TAG) + mv "$(shell go env GOPATH)/bin/js_wasm/wasm" wasm/grol.wasm + ls -lh wasm/*.wasm + +wasm/wasm_exec.js: Makefile +# cp "$(shell tinygo env TINYGOROOT)/targets/wasm_exec.js" ./wasm/ + cp "$(shell tinygo env GOROOT)/misc/wasm/wasm_exec.js" ./wasm/ + +wasm/wasm_exec.html: + cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.html" ./wasm/ test: grol CGO_ENABLED=0 go test -tags $(GO_BUILD_TAGS) ./... @@ -34,7 +66,7 @@ token/type_string.go: token/token.go clean: - rm -f grol */*_string.go + rm -f grol */*_string.go *.wasm wasm/*.wasm wasm/wasm_exec.html wasm/wasm_exec.js build: grol @@ -44,4 +76,4 @@ lint: .golangci.yml .golangci.yml: Makefile curl -fsS -o .golangci.yml https://raw.githubusercontent.com/fortio/workflows/main/golangci.yml -.PHONY: all lint generate test clean run build +.PHONY: all lint generate test clean run build wasm tinygo failing-tests wasm-release diff --git a/apply.gr b/apply.gr index 2eb43635..66135179 100644 --- a/apply.gr +++ b/apply.gr @@ -12,3 +12,5 @@ apply = func(f, a) { apply(func(x) {2*x}, a) // ^^^ [2, 6, 10, 14] + +log("should be reached, won't") diff --git a/go.mod b/go.mod index f4ff4a09..57d76790 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,13 @@ go 1.22.5 require ( fortio.org/cli v1.7.0 - fortio.org/log v1.14.0 + fortio.org/log v1.15.0 github.com/google/go-cmp v0.6.0 // only for tests ) require ( fortio.org/struct2env v0.4.1 // indirect fortio.org/version v1.0.4 // indirect + github.com/kortschak/goroutine v1.1.2 // indirect golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 // indirect; not actually used with our build tags ) diff --git a/go.sum b/go.sum index 07df51f6..1d4bf9c6 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,14 @@ fortio.org/cli v1.7.0 h1:w+uXZLGi4t3Vn/BvbeMuSw84Z1pvNPG9HqeGfpP68cc= fortio.org/cli v1.7.0/go.mod h1:s4vxWz7P7T4cYOWdMF0NA693Nu1gK9OW4KoDj54/Do4= -fortio.org/log v1.14.0 h1:ZkIc3Qqwfs9Dd931k07YzoC+bqCpJKEjVlZwxgXW3Nw= -fortio.org/log v1.14.0/go.mod h1:1tnXMqd5rZAgvSeHJkD2xXpyXRBzdeXtKLZuzNLIwtA= +fortio.org/log v1.15.0 h1:DRbZzgZH4av3ZPz6yIcvBwMy4NLH8a5iznRXXEegvJQ= +fortio.org/log v1.15.0/go.mod h1:t58Spg9njjymvRioh5F6qKGSupEsnMjXLGWIS1i3khE= fortio.org/struct2env v0.4.1 h1:rJludAMO5eBvpWplWEQNqoVDFZr4RWMQX7RUapgZyc0= fortio.org/struct2env v0.4.1/go.mod h1:lENUe70UwA1zDUCX+8AsO663QCFqYaprk5lnPhjD410= fortio.org/version v1.0.4 h1:FWUMpJ+hVTNc4RhvvOJzb0xesrlRmG/a+D6bjbQ4+5U= fortio.org/version v1.0.4/go.mod h1:2JQp9Ax+tm6QKiGuzR5nJY63kFeANcgrZ0osoQFDVm0= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kortschak/goroutine v1.1.2 h1:lhllcCuERxMIK5cYr8yohZZScL1na+JM5JYPRclWjck= +github.com/kortschak/goroutine v1.1.2/go.mod h1:zKpXs1FWN/6mXasDQzfl7g0LrGFIOiA6cLs9eXKyaMY= golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 h1:i7K6wQLN/0oxF7FT3tKkfMCstxoT4VGG36YIB9ZKLzI= golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= diff --git a/main.go b/main.go index 17d025b5..06363fd3 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "flag" + "fmt" "os" "fortio.org/cli" @@ -12,17 +13,27 @@ import ( ) func main() { + commandFlag := flag.String("c", "", "command/inline script to run instead of interactive mode") showParse := flag.Bool("parse", false, "show parse tree") showEval := flag.Bool("eval", true, "show eval results") sharedState := flag.Bool("shared-state", false, "All files share same interpreter state (default is new state for each)") cli.ArgsHelp = "*.gr files to interpret or no arg for stdin repl..." cli.MaxArgs = -1 cli.Main() + log.Printf("grol %s - welcome!", cli.LongVersion) options := repl.Options{ ShowParse: *showParse, ShowEval: *showEval, } nArgs := len(flag.Args()) + if *commandFlag != "" { + res, errs := repl.EvalString(*commandFlag) + if len(errs) > 0 { + log.Errf("Errors: %v", errs) + } + fmt.Println(res) + return + } if nArgs == 0 { repl.Interactive(os.Stdin, os.Stdout, options) return diff --git a/pi.gr b/pi.gr new file mode 100644 index 00000000..2982d9ca --- /dev/null +++ b/pi.gr @@ -0,0 +1,16 @@ +f = func(n, fac, dfac, exp, N) { + // log("Call n=", n, fac, dfac, exp) + if (n>N) { + [fac, dfac, exp] + } else { + dfac = 1.*dfac*(2*n - 1) + exp = exp * 2 + fac = fac * n + f(n+1, fac, dfac, exp, N) + } +} +N = 100 +r = f(1,1.,1.,1.,N) +// log("r", r) +approx = r[0] * r[2] / r[1] +approx * approx / N diff --git a/pi2.gr b/pi2.gr new file mode 100644 index 00000000..2590c660 --- /dev/null +++ b/pi2.gr @@ -0,0 +1,11 @@ +// Γ(x + 1/2) ~ Γ(x)x^(1/2) = (x-1)!√x +// Γ(x + 1/2) = (2x - 1)!! * 2^-x * √π +f=func(i,n, prod) { + //log(i, prod) + if (i==n+1) { + return 1./(prod*prod*n) + } + f(i+1, n, prod * ( 1 - 1./(2*i))) +} +n=200000 +f(1,n,1) diff --git a/repl/repl.go b/repl/repl.go index 9fc1240f..e123fc29 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -73,6 +73,7 @@ func Interactive(in io.Reader, out io.Writer, options Options) { return } l := prev + scanner.Text() + // errors are already logged and this is the only case that can get contNeeded (EOL instead of EOF mode) contNeeded, _ := EvalOne(s, macroState, l, out, options) if contNeeded { prev = l + "\n" diff --git a/tags.go b/tags.go index 83a49a2c..6afdbdcd 100644 --- a/tags.go +++ b/tags.go @@ -1,7 +1,7 @@ // Make go install fail if wrong tags are set -//go:build cgo || !no_net || !no_json -// +build cgo !no_net !no_json +//go:build (cgo && !tinygo) || !no_net || !no_json +// +build cgo,!tinygo !no_net !no_json package main diff --git a/wasm/dev_server.go b/wasm/dev_server.go new file mode 100644 index 00000000..728f1fd4 --- /dev/null +++ b/wasm/dev_server.go @@ -0,0 +1,20 @@ +//go:build !wasm +// +build !wasm + +// Not to be used for anything but localhost testing +package main + +import ( + "fmt" + "log" + "net/http" + "os" +) + +func main() { + port := ":8080" + path := os.Args[1] + fmt.Println("Serving", path, "on", port) + fs := http.FileServer(http.Dir(path)) + log.Fatalf("%v", http.ListenAndServe(port, fs)) //nolint:gosec // just a test server +} diff --git a/wasm/index.html b/wasm/index.html new file mode 100644 index 00000000..5a766bac --- /dev/null +++ b/wasm/index.html @@ -0,0 +1,111 @@ + + + + + + Grol + + + + + + +
+ + +
+
+ Hit enter or click +
+
+ + +
+
+ + +
+
GROL
+ + + diff --git a/wasm/wasm_main.go b/wasm/wasm_main.go new file mode 100644 index 00000000..ab56135e --- /dev/null +++ b/wasm/wasm_main.go @@ -0,0 +1,45 @@ +//go:build wasm +// +build wasm + +/* +Web assembly main for grol, exposing grol (repl.EvalString for now) to JS +*/ + +package main + +import ( + "syscall/js" + + "fortio.org/cli" + "fortio.org/log" + "fortio.org/version" + "grol.io/grol/repl" +) + +func jsEval(this js.Value, args []js.Value) interface{} { + if len(args) != 1 { + return "ERROR: number of arguments doesn't match" + } + input := args[0].String() + res, errs := repl.EvalString(input) + result := make(map[string]any) + result["result"] = res + // transfer errors to []any (!) + anyErrs := make([]any, len(errs)) + for i, v := range errs { + anyErrs[i] = v + } + result["errors"] = anyErrs + return result +} + +func main() { + cli.Main() // just to get version etc + _, grolVersion, _ := version.FromBuildInfoPath("grol.io/grol") + log.Infof("Grol wasm main %s", grolVersion) + done := make(chan struct{}, 0) + global := js.Global() + global.Set("grol", js.FuncOf(jsEval)) + global.Set("grolVersion", js.ValueOf(grolVersion)) + <-done +}