Skip to content

Commit d4ab19c

Browse files
authored
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 🎉 * 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)
1 parent c4f454c commit d4ab19c

13 files changed

+270
-9
lines changed

.gitignore

+10-1
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,18 @@ go.work.sum
2424

2525
.golangci.yml
2626
gorepl
27-
grol
2827

2928
.DS_Store
3029
__*
3130
# *_string.go
3231
dist/
32+
wasm/wasm_exec.js
33+
wasm/wasm_exec.html
34+
wasm/grol.wasm
35+
wasm/test.wasm
36+
wasm/grol_tiny.wasm
37+
grol
38+
grol.tiny
39+
grol.wasm
40+
grol_tiny.wasm
41+
wasm/grol_tiny.wasm

Makefile

+35-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,39 @@ GEN:=object/type_string.go parser/priority_string.go token/type_string.go
99

1010
grol: Makefile *.go */*.go $(GEN)
1111
CGO_ENABLED=0 go build -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" .
12-
ls -l grol
12+
ls -lh grol
13+
14+
tinygo: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html
15+
CGO_ENABLED=0 tinygo build -o grol.tiny -tags "$(GO_BUILD_TAGS)" .
16+
strip grol.tiny
17+
ls -lh grol.tiny
18+
19+
wasm: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html
20+
# GOOS=wasip1 GOARCH=wasm go build -o grol.wasm -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" .
21+
GOOS=js GOARCH=wasm go build -o wasm/grol.wasm -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" ./wasm
22+
# GOOS=wasip1 GOARCH=wasm tinygo build -target=wasi -no-debug -o grol_tiny.wasm -tags "$(GO_BUILD_TAGS)" .
23+
# Tiny go generates errors https://github.com/tinygo-org/tinygo/issues/1140
24+
# GOOS=js GOARCH=wasm tinygo build -no-debug -o wasm/test.wasm -tags "$(GO_BUILD_TAGS)" ./wasm
25+
-ls -lh wasm/*.wasm
26+
-pkill wasm
27+
go run ./wasm ./wasm &
28+
sleep 3
29+
open http://localhost:8080/
30+
31+
GIT_TAG:=$(shell git describe --tags --abbrev=0)
32+
# used to copy to site a release version
33+
wasm-release: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html
34+
@echo "Building wasm release GIT_TAG=$(GIT_TAG)"
35+
GOOS=js GOARCH=wasm go install -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" grol.io/grol/wasm@$(GIT_TAG)
36+
mv "$(shell go env GOPATH)/bin/js_wasm/wasm" wasm/grol.wasm
37+
ls -lh wasm/*.wasm
38+
39+
wasm/wasm_exec.js: Makefile
40+
# cp "$(shell tinygo env TINYGOROOT)/targets/wasm_exec.js" ./wasm/
41+
cp "$(shell tinygo env GOROOT)/misc/wasm/wasm_exec.js" ./wasm/
42+
43+
wasm/wasm_exec.html:
44+
cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.html" ./wasm/
1345

1446
test: grol
1547
CGO_ENABLED=0 go test -tags $(GO_BUILD_TAGS) ./...
@@ -34,7 +66,7 @@ token/type_string.go: token/token.go
3466

3567

3668
clean:
37-
rm -f grol */*_string.go
69+
rm -f grol */*_string.go *.wasm wasm/*.wasm wasm/wasm_exec.html wasm/wasm_exec.js
3870

3971
build: grol
4072

@@ -44,4 +76,4 @@ lint: .golangci.yml
4476
.golangci.yml: Makefile
4577
curl -fsS -o .golangci.yml https://raw.githubusercontent.com/fortio/workflows/main/golangci.yml
4678

47-
.PHONY: all lint generate test clean run build
79+
.PHONY: all lint generate test clean run build wasm tinygo failing-tests wasm-release

apply.gr

+2
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ apply = func(f, a) {
1212
apply(func(x) {2*x}, a)
1313

1414
// ^^^ [2, 6, 10, 14]
15+
16+
log("should be reached, won't")

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ go 1.22.5
44

55
require (
66
fortio.org/cli v1.7.0
7-
fortio.org/log v1.14.0
7+
fortio.org/log v1.15.0
88
github.com/google/go-cmp v0.6.0 // only for tests
99
)
1010

1111
require (
1212
fortio.org/struct2env v0.4.1 // indirect
1313
fortio.org/version v1.0.4 // indirect
14+
github.com/kortschak/goroutine v1.1.2 // indirect
1415
golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 // indirect; not actually used with our build tags
1516
)

go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
fortio.org/cli v1.7.0 h1:w+uXZLGi4t3Vn/BvbeMuSw84Z1pvNPG9HqeGfpP68cc=
22
fortio.org/cli v1.7.0/go.mod h1:s4vxWz7P7T4cYOWdMF0NA693Nu1gK9OW4KoDj54/Do4=
3-
fortio.org/log v1.14.0 h1:ZkIc3Qqwfs9Dd931k07YzoC+bqCpJKEjVlZwxgXW3Nw=
4-
fortio.org/log v1.14.0/go.mod h1:1tnXMqd5rZAgvSeHJkD2xXpyXRBzdeXtKLZuzNLIwtA=
3+
fortio.org/log v1.15.0 h1:DRbZzgZH4av3ZPz6yIcvBwMy4NLH8a5iznRXXEegvJQ=
4+
fortio.org/log v1.15.0/go.mod h1:t58Spg9njjymvRioh5F6qKGSupEsnMjXLGWIS1i3khE=
55
fortio.org/struct2env v0.4.1 h1:rJludAMO5eBvpWplWEQNqoVDFZr4RWMQX7RUapgZyc0=
66
fortio.org/struct2env v0.4.1/go.mod h1:lENUe70UwA1zDUCX+8AsO663QCFqYaprk5lnPhjD410=
77
fortio.org/version v1.0.4 h1:FWUMpJ+hVTNc4RhvvOJzb0xesrlRmG/a+D6bjbQ4+5U=
88
fortio.org/version v1.0.4/go.mod h1:2JQp9Ax+tm6QKiGuzR5nJY63kFeANcgrZ0osoQFDVm0=
99
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
1010
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
11+
github.com/kortschak/goroutine v1.1.2 h1:lhllcCuERxMIK5cYr8yohZZScL1na+JM5JYPRclWjck=
12+
github.com/kortschak/goroutine v1.1.2/go.mod h1:zKpXs1FWN/6mXasDQzfl7g0LrGFIOiA6cLs9eXKyaMY=
1113
golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 h1:i7K6wQLN/0oxF7FT3tKkfMCstxoT4VGG36YIB9ZKLzI=
1214
golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=

main.go

+11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33

44
import (
55
"flag"
6+
"fmt"
67
"os"
78

89
"fortio.org/cli"
@@ -12,17 +13,27 @@ import (
1213
)
1314

1415
func main() {
16+
commandFlag := flag.String("c", "", "command/inline script to run instead of interactive mode")
1517
showParse := flag.Bool("parse", false, "show parse tree")
1618
showEval := flag.Bool("eval", true, "show eval results")
1719
sharedState := flag.Bool("shared-state", false, "All files share same interpreter state (default is new state for each)")
1820
cli.ArgsHelp = "*.gr files to interpret or no arg for stdin repl..."
1921
cli.MaxArgs = -1
2022
cli.Main()
23+
log.Printf("grol %s - welcome!", cli.LongVersion)
2124
options := repl.Options{
2225
ShowParse: *showParse,
2326
ShowEval: *showEval,
2427
}
2528
nArgs := len(flag.Args())
29+
if *commandFlag != "" {
30+
res, errs := repl.EvalString(*commandFlag)
31+
if len(errs) > 0 {
32+
log.Errf("Errors: %v", errs)
33+
}
34+
fmt.Println(res)
35+
return
36+
}
2637
if nArgs == 0 {
2738
repl.Interactive(os.Stdin, os.Stdout, options)
2839
return

pi.gr

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
f = func(n, fac, dfac, exp, N) {
2+
// log("Call n=", n, fac, dfac, exp)
3+
if (n>N) {
4+
[fac, dfac, exp]
5+
} else {
6+
dfac = 1.*dfac*(2*n - 1)
7+
exp = exp * 2
8+
fac = fac * n
9+
f(n+1, fac, dfac, exp, N)
10+
}
11+
}
12+
N = 100
13+
r = f(1,1.,1.,1.,N)
14+
// log("r", r)
15+
approx = r[0] * r[2] / r[1]
16+
approx * approx / N

pi2.gr

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Γ(x + 1/2) ~ Γ(x)x^(1/2) = (x-1)!√x
2+
// Γ(x + 1/2) = (2x - 1)!! * 2^-x * √π
3+
f=func(i,n, prod) {
4+
//log(i, prod)
5+
if (i==n+1) {
6+
return 1./(prod*prod*n)
7+
}
8+
f(i+1, n, prod * ( 1 - 1./(2*i)))
9+
}
10+
n=200000
11+
f(1,n,1)

repl/repl.go

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func Interactive(in io.Reader, out io.Writer, options Options) {
7373
return
7474
}
7575
l := prev + scanner.Text()
76+
// errors are already logged and this is the only case that can get contNeeded (EOL instead of EOF mode)
7677
contNeeded, _ := EvalOne(s, macroState, l, out, options)
7778
if contNeeded {
7879
prev = l + "\n"

tags.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Make go install fail if wrong tags are set
22

3-
//go:build cgo || !no_net || !no_json
4-
// +build cgo !no_net !no_json
3+
//go:build (cgo && !tinygo) || !no_net || !no_json
4+
// +build cgo,!tinygo !no_net !no_json
55

66
package main
77

wasm/dev_server.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//go:build !wasm
2+
// +build !wasm
3+
4+
// Not to be used for anything but localhost testing
5+
package main
6+
7+
import (
8+
"fmt"
9+
"log"
10+
"net/http"
11+
"os"
12+
)
13+
14+
func main() {
15+
port := ":8080"
16+
path := os.Args[1]
17+
fmt.Println("Serving", path, "on", port)
18+
fs := http.FileServer(http.Dir(path))
19+
log.Fatalf("%v", http.ListenAndServe(port, fs)) //nolint:gosec // just a test server
20+
}

wasm/index.html

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<!doctype html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Grol</title>
7+
<style>
8+
body {
9+
font-family: Arial, sans-serif;
10+
margin: 20px;
11+
}
12+
13+
.container {
14+
display: flex;
15+
flex-direction: column;
16+
gap: 10px;
17+
}
18+
19+
textarea, label, div {
20+
font-size: 14px;
21+
width: 100%;
22+
box-sizing: border-box;
23+
}
24+
25+
button {
26+
align-self: flex-start;
27+
}
28+
29+
label {
30+
margin-bottom: 5px;
31+
}
32+
33+
.error-textarea {
34+
color: red;
35+
}
36+
</style>
37+
</head>
38+
39+
<body>
40+
<script src="wasm_exec.js"></script>
41+
<script>
42+
if (!WebAssembly.instantiateStreaming) { // polyfill
43+
WebAssembly.instantiateStreaming = async (resp, importObject) => {
44+
const source = await (await resp).arrayBuffer();
45+
return await WebAssembly.instantiate(source, importObject);
46+
};
47+
}
48+
const go = new Go();
49+
let mod, inst;
50+
WebAssembly.instantiateStreaming(fetch("grol.wasm"), go.importObject).then((result) => {
51+
mod = result.module;
52+
inst = result.instance;
53+
document.getElementById("runButton").disabled = false;
54+
}).catch((err) => {
55+
console.error(err);
56+
});
57+
function resizeTextarea(textarea) {
58+
textarea.style.height = 'auto';
59+
textarea.style.height = (textarea.scrollHeight) + 'px';
60+
}
61+
62+
async function run() {
63+
try {
64+
// console.clear();
65+
console.log('In run')
66+
go.run(inst);
67+
var input = document.getElementById('input').value
68+
// Call the grol function with the input
69+
var output = grol(input);
70+
console.log('Eval done:');
71+
console.log(output);
72+
// Write the result to the output textarea
73+
document.getElementById('output').value = output.result;
74+
document.getElementById('errors').value = output.errors.join("\n");
75+
document.getElementById('version').innerHTML = "GROL " + grolVersion;
76+
resizeTextarea(document.getElementById('output'));
77+
resizeTextarea(document.getElementById('errors'));
78+
} catch (e) {
79+
console.error(e);
80+
} finally {
81+
inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
82+
}
83+
}
84+
document.addEventListener('DOMContentLoaded', (event) => {
85+
document.getElementById('input').addEventListener('keydown', function (e) {
86+
if (e.key === 'Enter') {
87+
run();
88+
}
89+
});
90+
});
91+
92+
</script>
93+
<div>
94+
<label for="input">Enter your GROL code here:</label>
95+
<textarea id="input" rows="12" cols="80"></textarea>
96+
</div>
97+
<div>
98+
Hit enter or click <button onClick="run();" id="runButton" disabled>Run</button>
99+
</div>
100+
<div>
101+
<label for="output">Result:</label>
102+
<textarea id="output" rows="2" cols="80"></textarea>
103+
</div>
104+
<div>
105+
<label for="errors">Errors:</label>
106+
<textarea id="errors" rows="1" cols="80" class="error-textarea"></textarea>
107+
</div>
108+
<div id="version">GROL</div>
109+
</body>
110+
111+
</html>

wasm/wasm_main.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//go:build wasm
2+
// +build wasm
3+
4+
/*
5+
Web assembly main for grol, exposing grol (repl.EvalString for now) to JS
6+
*/
7+
8+
package main
9+
10+
import (
11+
"syscall/js"
12+
13+
"fortio.org/cli"
14+
"fortio.org/log"
15+
"fortio.org/version"
16+
"grol.io/grol/repl"
17+
)
18+
19+
func jsEval(this js.Value, args []js.Value) interface{} {
20+
if len(args) != 1 {
21+
return "ERROR: number of arguments doesn't match"
22+
}
23+
input := args[0].String()
24+
res, errs := repl.EvalString(input)
25+
result := make(map[string]any)
26+
result["result"] = res
27+
// transfer errors to []any (!)
28+
anyErrs := make([]any, len(errs))
29+
for i, v := range errs {
30+
anyErrs[i] = v
31+
}
32+
result["errors"] = anyErrs
33+
return result
34+
}
35+
36+
func main() {
37+
cli.Main() // just to get version etc
38+
_, grolVersion, _ := version.FromBuildInfoPath("grol.io/grol")
39+
log.Infof("Grol wasm main %s", grolVersion)
40+
done := make(chan struct{}, 0)
41+
global := js.Global()
42+
global.Set("grol", js.FuncOf(jsEval))
43+
global.Set("grolVersion", js.ValueOf(grolVersion))
44+
<-done
45+
}

0 commit comments

Comments
 (0)