Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added base64, image.png, wasm data: to image, (limited) support for namespaces (virtual map access). moved the image functions under image.* #217

Merged
merged 9 commits into from
Sep 7, 2024
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
}
}
Comment on lines +31 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually it's better to loop on runes from a string

Unicode characters such as accent, and emoji are badly checked otherwise

Then Unicode package can be used to check the type

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's on purpose (for now at least) only bytes (ascii) is allowed outside of strings

unicode does work fine inside strings (as demoed in many examples)

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)
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
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
6 changes: 6 additions & 0 deletions wasm/grol_wasm.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@
document.getElementById('output').value = output.result;
document.getElementById('input').value = output.formatted;
document.getElementById('errors').value = output.errors.join("\n");
if (output.image !== undefined) {
document.getElementById('image').src = output.image;
}
} else {
document.getElementById('errors').value = "Unexpected runtime error, see JS console";
}
Expand Down Expand Up @@ -141,6 +144,9 @@
</div>
<div>
<label for="output">Result:</label>
<br />
<img id="image" src="" alt="Image output (if any)">
<br />
<textarea id="output" rows="2" cols="80"></textarea>
</div>
<div>
Expand Down
5 changes: 5 additions & 0 deletions wasm/wasm_main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ func jsEval(this js.Value, args []js.Value) interface{} {
opts.MaxDuration = WasmMaxDuration
res, errs, formatted := repl.EvalStringWithOption(context.Background(), opts, input)
result := make(map[string]any)
if strings.HasPrefix(res, "data:") {
// special case for data: urls, we don't want to return the data
result["image"] = res
res = ""
}
result["result"] = strings.TrimSuffix(res, "\n")
// transfer errors to []any (!)
anyErrs := make([]any, len(errs))
Expand Down