Skip to content

Commit

Permalink
wasm support (#37)
Browse files Browse the repository at this point in the history
* 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 🎉

* 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)
  • Loading branch information
ldemailly authored Jul 19, 2024
1 parent c4f454c commit d4ab19c
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 9 deletions.
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
38 changes: 35 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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) ./...
Expand All @@ -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

Expand All @@ -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
2 changes: 2 additions & 0 deletions apply.gr
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ apply = func(f, a) {
apply(func(x) {2*x}, a)

// ^^^ [2, 6, 10, 14]

log("should be reached, won't")
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
11 changes: 11 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main

import (
"flag"
"fmt"
"os"

"fortio.org/cli"
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions pi.gr
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions pi2.gr
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions tags.go
Original file line number Diff line number Diff line change
@@ -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

Expand Down
20 changes: 20 additions & 0 deletions wasm/dev_server.go
Original file line number Diff line number Diff line change
@@ -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
}
111 changes: 111 additions & 0 deletions wasm/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<!doctype html>
<html>

<head>
<meta charset="utf-8">
<title>Grol</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}

.container {
display: flex;
flex-direction: column;
gap: 10px;
}

textarea, label, div {
font-size: 14px;
width: 100%;
box-sizing: border-box;
}

button {
align-self: flex-start;
}

label {
margin-bottom: 5px;
}

.error-textarea {
color: red;
}
</style>
</head>

<body>
<script src="wasm_exec.js"></script>
<script>
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("grol.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
document.getElementById("runButton").disabled = false;
}).catch((err) => {
console.error(err);
});
function resizeTextarea(textarea) {
textarea.style.height = 'auto';
textarea.style.height = (textarea.scrollHeight) + 'px';
}

async function run() {
try {
// console.clear();
console.log('In run')
go.run(inst);
var input = document.getElementById('input').value
// Call the grol function with the input
var output = grol(input);
console.log('Eval done:');
console.log(output);
// Write the result to the output textarea
document.getElementById('output').value = output.result;
document.getElementById('errors').value = output.errors.join("\n");
document.getElementById('version').innerHTML = "GROL " + grolVersion;
resizeTextarea(document.getElementById('output'));
resizeTextarea(document.getElementById('errors'));
} catch (e) {
console.error(e);
} finally {
inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
}
}
document.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('input').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
run();
}
});
});

</script>
<div>
<label for="input">Enter your GROL code here:</label>
<textarea id="input" rows="12" cols="80"></textarea>
</div>
<div>
Hit enter or click <button onClick="run();" id="runButton" disabled>Run</button>
</div>
<div>
<label for="output">Result:</label>
<textarea id="output" rows="2" cols="80"></textarea>
</div>
<div>
<label for="errors">Errors:</label>
<textarea id="errors" rows="1" cols="80" class="error-textarea"></textarea>
</div>
<div id="version">GROL</div>
</body>

</html>
45 changes: 45 additions & 0 deletions wasm/wasm_main.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit d4ab19c

Please sign in to comment.