Skip to content

Commit

Permalink
Added base64, image.png, wasm data: to image, (limited) support for n…
Browse files Browse the repository at this point in the history
…amespaces (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
  • Loading branch information
ldemailly authored Sep 7, 2024
1 parent 462c40c commit c82ebac
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 47 deletions.
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ TINYGO_STACKS:=-stack-size=40mb

wasm: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html wasm/grol_wasm.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=js GOARCH=wasm $(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 $(TINYGO_STACKS) -no-debug -o wasm/grol.wasm -tags "$(GO_BUILD_TAGS)" ./wasm
Expand All @@ -50,11 +50,15 @@ wasm: Makefile *.go */*.go $(GEN) wasm/wasm_exec.js wasm/wasm_exec.html wasm/gro
sleep 3
open http://localhost:8080/


#WASM_GO:=/opt/homebrew/Cellar/go/1.23.1/bin/go
WASM_GO:=go

GIT_TAG=$(shell git describe --tags --always --dirty)
# 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)
GOOS=js GOARCH=wasm $(WASM_GO) install -trimpath -ldflags="-w -s" -tags "$(GO_BUILD_TAGS)" grol.io/grol/wasm@$(GIT_TAG)
# No buildinfo and no tinygo install so we set version old style:
# 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
mv "$(shell go env GOPATH)/bin/js_wasm/wasm" wasm/grol.wasm
Expand All @@ -67,10 +71,10 @@ install:

wasm/wasm_exec.js: Makefile
# cp "$(shell tinygo env TINYGOROOT)/targets/wasm_exec.js" ./wasm/
cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" ./wasm/
cp "$(shell $(WASM_GO) env GOROOT)/misc/wasm/wasm_exec.js" ./wasm/

wasm/wasm_exec.html:
cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.html" ./wasm/
cp "$(shell $(WASM_GO) env GOROOT)/misc/wasm/wasm_exec.html" ./wasm/

test: grol
CGO_ENABLED=0 go test -tags $(GO_BUILD_TAGS) ./...
Expand Down
9 changes: 9 additions & 0 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,15 @@ func (s *State) evalInternal(node any) object.Object { //nolint:funlen,gocognit,
case *ast.MapLiteral:
return s.evalMapLiteral(node)
case *ast.IndexExpression:
if node.Value().Type() == token.DOT {
// See commits in PR#217 for a version using double map lookup, trading off the string concat (alloc)
// for a map lookup. code is a lot simpler without actual ns map though so we stick to this version
// for now.
extName := node.Left.Value().Literal() + "." + node.Index.Value().Literal()
if ext, ok := s.Extensions[extName]; ok {
return ext
}
}
return s.evalIndexExpression(s.Eval(node.Left), node)
case *ast.Comment:
return object.NULL
Expand Down
2 changes: 1 addition & 1 deletion eval/eval_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type State struct {
env *object.Environment
rootEnv *object.Environment // same as ancestor of env but used for reset in panic recovery.
cache Cache
Extensions map[string]object.Extension
Extensions object.ExtensionMap
NoLog bool // turn log() into println() (for EvalString)
// Max depth / recursion level - default DefaultMaxDepth,
// note that a simple function consumes at least 2 levels and typically at least 3 or 4.
Expand Down
8 changes: 4 additions & 4 deletions examples/image.gr
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func ycbcr(angle) {

size = 1024
imgName = "canvas"
canvas = image(imgName, size, size)
canvas = image.new(imgName, size, size)
div = 6

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

image_save(imgName)
image.save(imgName)

println("Saved image to grol.png")
23 changes: 23 additions & 0 deletions extensions/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package extensions

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -373,6 +374,28 @@ func createMisc() {
},
}
MustCreate(intFn)
intFn.Name = "base64"
intFn.Callback = func(st any, _ string, args []object.Object) object.Object {
s := st.(*eval.State)
o := args[0]
var data []byte
switch o.Type() {
case object.REFERENCE:
ref := o.(object.Reference)
if ref.Value().Type() != object.STRING {
return s.Errorf("cannot convert ref to %s to base64", ref.Value().Type())
}
data = []byte(ref.Value().(object.String).Value)
case object.STRING:
data = []byte(o.(object.String).Value)
default:
return s.Errorf("cannot convert %s to base64", o.Type())
}
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(encoded, data)
return object.String{Value: string(encoded)}
}
MustCreate(intFn)
}

func createTimeFunctions() {
Expand Down
38 changes: 29 additions & 9 deletions extensions/images.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package extensions

import (
"bytes"
"image"
"image/color"
"image/draw"
Expand Down Expand Up @@ -156,11 +157,11 @@ func ycbrArrayToRBGAColor(arr []object.Object) (color.RGBA, *object.Error) {
return rgba, nil
}

func createImageFunctions() {
func createImageFunctions() { //nolint:funlen // this is a group of related functions.
// All the functions consistently use args[0] as the image name/reference into the ClientData map.
cdata := make(ImageMap)
imgFn := object.Extension{
Name: "image",
Name: "image.new",
MinArgs: 3,
MaxArgs: 3,
Help: "create a new RGBA image of the name and size, image starts entirely transparent",
Expand All @@ -184,7 +185,7 @@ func createImageFunctions() {
},
}
MustCreate(imgFn)
imgFn.Name = "image_set"
imgFn.Name = "image.set"
imgFn.Help = "img, x, y, color: set a pixel in the named image, color is an array of 3 or 4 elements 0-255"
imgFn.MinArgs = 4
imgFn.MaxArgs = 4
Expand All @@ -201,11 +202,11 @@ func createImageFunctions() {
var color color.RGBA
var oerr *object.Error
switch name {
case "image_set_ycbcr":
case "image.set_ycbcr":
color, oerr = ycbrArrayToRBGAColor(colorArray)
case "image_set_hsl":
case "image.set_hsl":
color, oerr = hslArrayToRBGAColor(colorArray)
case "image_set":
case "image.set":
color, oerr = rgbArrayToRBGAColor(colorArray)
default:
return object.Errorf("unknown image_set function %q", name)
Expand All @@ -217,13 +218,13 @@ func createImageFunctions() {
return args[0]
}
MustCreate(imgFn)
imgFn.Name = "image_set_ycbcr"
imgFn.Name = "image.set_ycbcr"
imgFn.Help = "img, x, y, color: set a pixel in the named image, color Y'CbCr in an array of 3 elements 0-255"
MustCreate(imgFn)
imgFn.Name = "image_set_hsl"
imgFn.Name = "image.set_hsl"
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)]"
MustCreate(imgFn)
imgFn.Name = "image_save"
imgFn.Name = "image.save"
imgFn.Help = "save the named image grol.png"
imgFn.MinArgs = 1
imgFn.MaxArgs = 1
Expand All @@ -246,4 +247,23 @@ func createImageFunctions() {
return args[0]
}
MustCreate(imgFn)
imgFn.Name = "image.png"
imgFn.Help = "returns the png data of the named image, suitable for base64"
imgFn.MinArgs = 1
imgFn.MaxArgs = 1
imgFn.ArgTypes = []object.Type{object.STRING}
imgFn.Callback = func(cdata any, _ string, args []object.Object) object.Object {
images := cdata.(ImageMap)
img, ok := images[args[0]]
if !ok {
return object.Errorf("image not found")
}
buf := bytes.Buffer{}
err := png.Encode(&buf, img)
if err != nil {
return object.Errorf("error encoding image: %v", err)
}
return object.String{Value: buf.String()}
}
MustCreate(imgFn)
}
43 changes: 39 additions & 4 deletions object/interp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,40 @@ package object

import (
"errors"
"fmt"
"strings"

"grol.io/grol/lexer"
)

type ExtensionMap map[string]Extension

var (
extraFunctions map[string]Extension
extraFunctions ExtensionMap
extraIdentifiers map[string]Object
initDone bool
)

// Init resets the table of extended functions to empty.
// Optional, will be called on demand the first time through CreateFunction.
func Init() {
extraFunctions = make(map[string]Extension)
extraFunctions = make(ExtensionMap)
extraIdentifiers = make(map[string]Object)
initDone = true
}

func ValidIdentifier(name string) bool {
if name == "" {
return false
}
for _, b := range []byte(name) {
if !lexer.IsAlphaNum(b) {
return false
}
}
return true
}

// CreateFunction adds a new function to the table of extended functions.
func CreateFunction(cmd Extension) error {
if !initDone {
Expand All @@ -26,6 +44,19 @@ func CreateFunction(cmd Extension) error {
if cmd.Name == "" {
return errors.New("empty command name")
}
// Only support 1 level of namespace for now.
dotSplit := strings.SplitN(cmd.Name, ".", 2)
var ns string
name := cmd.Name
if len(dotSplit) == 2 {
ns, name = dotSplit[0], dotSplit[1]
if !ValidIdentifier(ns) {
return fmt.Errorf("namespace %q not alphanumeric", ns)
}
}
if !ValidIdentifier(name) {
return errors.New(name + ": not alphanumeric")
}
if cmd.MaxArgs != -1 && cmd.MinArgs > cmd.MaxArgs {
return errors.New(cmd.Name + ": min args > max args")
}
Expand All @@ -36,13 +67,17 @@ func CreateFunction(cmd Extension) error {
return errors.New(cmd.Name + ": already defined")
}
cmd.Variadic = (cmd.MaxArgs == -1) || (cmd.MaxArgs > cmd.MinArgs)
// If namespaced, put both at top level (for sake of baseinfo and command completion) and
// in namespace map (for access/ref by eval). We decided to not even have namespaces map
// after all.
extraFunctions[cmd.Name] = cmd
return nil
}

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

func IsExtraFunction(name string) bool {
Expand Down
1 change: 1 addition & 0 deletions repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ func EvalOne(ctx context.Context, s *eval.State, what string, out io.Writer, opt
}()
}
s.SetContext(ctx, options.MaxDuration)
defer s.Cancel()
continuation, errs, formatted = evalOne(s, what, out, options)
return
}
Expand Down
Loading

0 comments on commit c82ebac

Please sign in to comment.