Skip to content

Commit c82ebac

Browse files
authored
Added base64, image.png, wasm data: to image, (limited) support for namespaces (virtual map access). moved the image functions under image.* (#217)
* Add (limited) support for namespaces (virtual map access). move the image function under image.* * self review updates * adding missing empty ns map * revert the real namespace map, just use toplevel and hack lookup * further reduce the diff * Adding base64() and image.png() and looking for data: prefix in wasm for image producing * linter... * fixed #204 🎉 * format document and clear image to 1 pixel transparent gif when it goes back to empty
1 parent 462c40c commit c82ebac

File tree

10 files changed

+162
-47
lines changed

10 files changed

+162
-47
lines changed

Makefile

+8-4
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ TINYGO_STACKS:=-stack-size=40mb
3737

3838
wasm: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html wasm/grol_wasm.html
3939
# GOOS=wasip1 GOARCH=wasm go build -o grol.wasm -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" .
40-
GOOS=js GOARCH=wasm go build -o wasm/grol.wasm -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" ./wasm
40+
GOOS=js GOARCH=wasm $(WASM_GO) build -o wasm/grol.wasm -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" ./wasm
4141
# GOOS=wasip1 GOARCH=wasm tinygo build -target=wasi -no-debug -o grol_tiny.wasm -tags "$(GO_BUILD_TAGS)" .
4242
# Tiny go generates errors https://github.com/tinygo-org/tinygo/issues/1140
4343
# GOOS=js GOARCH=wasm tinygo build $(TINYGO_STACKS) -no-debug -o wasm/grol.wasm -tags "$(GO_BUILD_TAGS)" ./wasm
@@ -50,11 +50,15 @@ wasm: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html wasm/gro
5050
sleep 3
5151
open http://localhost:8080/
5252

53+
54+
#WASM_GO:=/opt/homebrew/Cellar/go/1.23.1/bin/go
55+
WASM_GO:=go
56+
5357
GIT_TAG=$(shell git describe --tags --always --dirty)
5458
# used to copy to site a release version
5559
wasm-release: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html
5660
@echo "Building wasm release GIT_TAG=$(GIT_TAG)"
57-
GOOS=js GOARCH=wasm go install -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" grol.io/grol/wasm@$(GIT_TAG)
61+
GOOS=js GOARCH=wasm $(WASM_GO) install -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" grol.io/grol/wasm@$(GIT_TAG)
5862
# No buildinfo and no tinygo install so we set version old style:
5963
# GOOS=js GOARCH=wasm tinygo build $(TINYGO_STACKS) -o wasm/grol.wasm -no-debug -ldflags="-X main.TinyGoVersion=$(GIT_TAG)" -tags "$(GO_BUILD_TAGS)" ./wasm
6064
mv "$(shell go env GOPATH)/bin/js_wasm/wasm" wasm/grol.wasm
@@ -67,10 +71,10 @@ install:
6771

6872
wasm/wasm_exec.js: Makefile
6973
# cp "$(shell tinygo env TINYGOROOT)/targets/wasm_exec.js" ./wasm/
70-
cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" ./wasm/
74+
cp "$(shell $(WASM_GO) env GOROOT)/misc/wasm/wasm_exec.js" ./wasm/
7175

7276
wasm/wasm_exec.html:
73-
cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.html" ./wasm/
77+
cp "$(shell $(WASM_GO) env GOROOT)/misc/wasm/wasm_exec.html" ./wasm/
7478

7579
test: grol
7680
CGO_ENABLED=0 go test -tags $(GO_BUILD_TAGS) ./...

eval/eval.go

+9
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,15 @@ func (s *State) evalInternal(node any) object.Object { //nolint:funlen,gocognit,
292292
case *ast.MapLiteral:
293293
return s.evalMapLiteral(node)
294294
case *ast.IndexExpression:
295+
if node.Value().Type() == token.DOT {
296+
// See commits in PR#217 for a version using double map lookup, trading off the string concat (alloc)
297+
// for a map lookup. code is a lot simpler without actual ns map though so we stick to this version
298+
// for now.
299+
extName := node.Left.Value().Literal() + "." + node.Index.Value().Literal()
300+
if ext, ok := s.Extensions[extName]; ok {
301+
return ext
302+
}
303+
}
295304
return s.evalIndexExpression(s.Eval(node.Left), node)
296305
case *ast.Comment:
297306
return object.NULL

eval/eval_api.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type State struct {
3333
env *object.Environment
3434
rootEnv *object.Environment // same as ancestor of env but used for reset in panic recovery.
3535
cache Cache
36-
Extensions map[string]object.Extension
36+
Extensions object.ExtensionMap
3737
NoLog bool // turn log() into println() (for EvalString)
3838
// Max depth / recursion level - default DefaultMaxDepth,
3939
// note that a simple function consumes at least 2 levels and typically at least 3 or 4.

examples/image.gr

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func ycbcr(angle) {
1818

1919
size = 1024
2020
imgName = "canvas"
21-
canvas = image(imgName, size, size)
21+
canvas = image.new(imgName, size, size)
2222
div = 6
2323

2424
t = 0
@@ -29,15 +29,15 @@ for t < 12*PI {
2929
y = cos(t) * (pow(E, cos(t)) - 2*cos(4*t) - pow(sin(t/12), 5))
3030
angle := int(t*180./PI) % 360 // so ycbr() get memoized with 360 values
3131
color = ycbcr(angle)
32-
image_set_ycbcr(canvas, int(size/2+(size/div)*x+0.5), int(size/2.5+(size/div)*y+0.5), color)
32+
image.set_ycbcr(canvas, int(size/2+(size/div)*x+0.5), int(size/2.5+(size/div)*y+0.5), color)
3333
// Or in HSL:
3434
// color[0] = t/(12*PI) // hue
3535
// image_set_hsl(canvas, int(size/2+(size/div)*x+0.5), int(size/2.5+(size/div)*y+0.5), color)
36-
t = t + 0.0005
36+
t = t + 0.0005 // 0.0001 for profiling.
3737
}
3838
elapsed = time() - now
3939
log("Time elapsed: ", elapsed, " seconds")
4040

41-
image_save(imgName)
41+
image.save(imgName)
4242

4343
println("Saved image to grol.png")

extensions/extension.go

+23
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package extensions
44

55
import (
66
"bytes"
7+
"encoding/base64"
78
"encoding/json"
89
"fmt"
910
"io"
@@ -373,6 +374,28 @@ func createMisc() {
373374
},
374375
}
375376
MustCreate(intFn)
377+
intFn.Name = "base64"
378+
intFn.Callback = func(st any, _ string, args []object.Object) object.Object {
379+
s := st.(*eval.State)
380+
o := args[0]
381+
var data []byte
382+
switch o.Type() {
383+
case object.REFERENCE:
384+
ref := o.(object.Reference)
385+
if ref.Value().Type() != object.STRING {
386+
return s.Errorf("cannot convert ref to %s to base64", ref.Value().Type())
387+
}
388+
data = []byte(ref.Value().(object.String).Value)
389+
case object.STRING:
390+
data = []byte(o.(object.String).Value)
391+
default:
392+
return s.Errorf("cannot convert %s to base64", o.Type())
393+
}
394+
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
395+
base64.StdEncoding.Encode(encoded, data)
396+
return object.String{Value: string(encoded)}
397+
}
398+
MustCreate(intFn)
376399
}
377400

378401
func createTimeFunctions() {

extensions/images.go

+29-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package extensions
22

33
import (
4+
"bytes"
45
"image"
56
"image/color"
67
"image/draw"
@@ -156,11 +157,11 @@ func ycbrArrayToRBGAColor(arr []object.Object) (color.RGBA, *object.Error) {
156157
return rgba, nil
157158
}
158159

159-
func createImageFunctions() {
160+
func createImageFunctions() { //nolint:funlen // this is a group of related functions.
160161
// All the functions consistently use args[0] as the image name/reference into the ClientData map.
161162
cdata := make(ImageMap)
162163
imgFn := object.Extension{
163-
Name: "image",
164+
Name: "image.new",
164165
MinArgs: 3,
165166
MaxArgs: 3,
166167
Help: "create a new RGBA image of the name and size, image starts entirely transparent",
@@ -184,7 +185,7 @@ func createImageFunctions() {
184185
},
185186
}
186187
MustCreate(imgFn)
187-
imgFn.Name = "image_set"
188+
imgFn.Name = "image.set"
188189
imgFn.Help = "img, x, y, color: set a pixel in the named image, color is an array of 3 or 4 elements 0-255"
189190
imgFn.MinArgs = 4
190191
imgFn.MaxArgs = 4
@@ -201,11 +202,11 @@ func createImageFunctions() {
201202
var color color.RGBA
202203
var oerr *object.Error
203204
switch name {
204-
case "image_set_ycbcr":
205+
case "image.set_ycbcr":
205206
color, oerr = ycbrArrayToRBGAColor(colorArray)
206-
case "image_set_hsl":
207+
case "image.set_hsl":
207208
color, oerr = hslArrayToRBGAColor(colorArray)
208-
case "image_set":
209+
case "image.set":
209210
color, oerr = rgbArrayToRBGAColor(colorArray)
210211
default:
211212
return object.Errorf("unknown image_set function %q", name)
@@ -217,13 +218,13 @@ func createImageFunctions() {
217218
return args[0]
218219
}
219220
MustCreate(imgFn)
220-
imgFn.Name = "image_set_ycbcr"
221+
imgFn.Name = "image.set_ycbcr"
221222
imgFn.Help = "img, x, y, color: set a pixel in the named image, color Y'CbCr in an array of 3 elements 0-255"
222223
MustCreate(imgFn)
223-
imgFn.Name = "image_set_hsl"
224+
imgFn.Name = "image.set_hsl"
224225
imgFn.Help = "img, x, y, color: set a pixel in the named image, color in an array [Hue (0-360), Sat (0-1), Light (0-1)]"
225226
MustCreate(imgFn)
226-
imgFn.Name = "image_save"
227+
imgFn.Name = "image.save"
227228
imgFn.Help = "save the named image grol.png"
228229
imgFn.MinArgs = 1
229230
imgFn.MaxArgs = 1
@@ -246,4 +247,23 @@ func createImageFunctions() {
246247
return args[0]
247248
}
248249
MustCreate(imgFn)
250+
imgFn.Name = "image.png"
251+
imgFn.Help = "returns the png data of the named image, suitable for base64"
252+
imgFn.MinArgs = 1
253+
imgFn.MaxArgs = 1
254+
imgFn.ArgTypes = []object.Type{object.STRING}
255+
imgFn.Callback = func(cdata any, _ string, args []object.Object) object.Object {
256+
images := cdata.(ImageMap)
257+
img, ok := images[args[0]]
258+
if !ok {
259+
return object.Errorf("image not found")
260+
}
261+
buf := bytes.Buffer{}
262+
err := png.Encode(&buf, img)
263+
if err != nil {
264+
return object.Errorf("error encoding image: %v", err)
265+
}
266+
return object.String{Value: buf.String()}
267+
}
268+
MustCreate(imgFn)
249269
}

object/interp.go

+39-4
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,40 @@ package object
22

33
import (
44
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"grol.io/grol/lexer"
59
)
610

11+
type ExtensionMap map[string]Extension
12+
713
var (
8-
extraFunctions map[string]Extension
14+
extraFunctions ExtensionMap
915
extraIdentifiers map[string]Object
1016
initDone bool
1117
)
1218

1319
// Init resets the table of extended functions to empty.
1420
// Optional, will be called on demand the first time through CreateFunction.
1521
func Init() {
16-
extraFunctions = make(map[string]Extension)
22+
extraFunctions = make(ExtensionMap)
1723
extraIdentifiers = make(map[string]Object)
1824
initDone = true
1925
}
2026

27+
func ValidIdentifier(name string) bool {
28+
if name == "" {
29+
return false
30+
}
31+
for _, b := range []byte(name) {
32+
if !lexer.IsAlphaNum(b) {
33+
return false
34+
}
35+
}
36+
return true
37+
}
38+
2139
// CreateFunction adds a new function to the table of extended functions.
2240
func CreateFunction(cmd Extension) error {
2341
if !initDone {
@@ -26,6 +44,19 @@ func CreateFunction(cmd Extension) error {
2644
if cmd.Name == "" {
2745
return errors.New("empty command name")
2846
}
47+
// Only support 1 level of namespace for now.
48+
dotSplit := strings.SplitN(cmd.Name, ".", 2)
49+
var ns string
50+
name := cmd.Name
51+
if len(dotSplit) == 2 {
52+
ns, name = dotSplit[0], dotSplit[1]
53+
if !ValidIdentifier(ns) {
54+
return fmt.Errorf("namespace %q not alphanumeric", ns)
55+
}
56+
}
57+
if !ValidIdentifier(name) {
58+
return errors.New(name + ": not alphanumeric")
59+
}
2960
if cmd.MaxArgs != -1 && cmd.MinArgs > cmd.MaxArgs {
3061
return errors.New(cmd.Name + ": min args > max args")
3162
}
@@ -36,13 +67,17 @@ func CreateFunction(cmd Extension) error {
3667
return errors.New(cmd.Name + ": already defined")
3768
}
3869
cmd.Variadic = (cmd.MaxArgs == -1) || (cmd.MaxArgs > cmd.MinArgs)
70+
// If namespaced, put both at top level (for sake of baseinfo and command completion) and
71+
// in namespace map (for access/ref by eval). We decided to not even have namespaces map
72+
// after all.
3973
extraFunctions[cmd.Name] = cmd
4074
return nil
4175
}
4276

4377
// Returns the table of extended functions to seed the state of an eval.
44-
func ExtraFunctions() map[string]Extension {
45-
return extraFunctions // no need to make a copy as each value need to be set to be changed (map of structs, not pointers).
78+
func ExtraFunctions() ExtensionMap {
79+
// no need to make a copy as each value need to be set to be changed (map of structs, not pointers).
80+
return extraFunctions
4681
}
4782

4883
func IsExtraFunction(name string) bool {

repl/repl.go

+1
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ func EvalOne(ctx context.Context, s *eval.State, what string, out io.Writer, opt
380380
}()
381381
}
382382
s.SetContext(ctx, options.MaxDuration)
383+
defer s.Cancel()
383384
continuation, errs, formatted = evalOne(s, what, out, options)
384385
return
385386
}

0 commit comments

Comments
 (0)