From 57399c164b3ae56a61bfc29ece12c72ff67d7571 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 5 Sep 2024 18:49:58 -0700 Subject: [PATCH] Add begining image function and demo and performance optimizations (10x improvements related to logging level skips) (#211) * Add begining image function and demo * Fix extension type apply * review comments * Only deref go functions if they don't take ANY in, that way type() still works * Fix bug that using PI for instance would prevent caching * Add image_set_ycbcr, allow color to be array of float (still 0-255 but rounded to nearest if float), add not yet used HSL * remove remaining direct object.Error{} creation. add stack for error returned by extensions with non eval state cdata * Ran with -cpu-profile and found expensive logging needing if guard. dropped images.gr from .9 to .3s * Make linters happy, make setup-go use latest * go mod tidy mystery * cross ref the other butterfly implementation * Add prime number as iterator demo * another 10x speed improvements by skipping log evaluations --- eval/eval.go | 141 +++++++++------ eval/stack.go | 4 + examples/image.gr | 43 +++++ examples/prime_sequence_generator.gr | 43 +++++ extensions/extension.go | 6 +- extensions/images.go | 249 +++++++++++++++++++++++++++ go.mod | 2 +- object/object.go | 15 +- object/state.go | 7 +- 9 files changed, 455 insertions(+), 55 deletions(-) create mode 100644 examples/image.gr create mode 100644 examples/prime_sequence_generator.gr create mode 100644 extensions/images.go diff --git a/eval/eval.go b/eval/eval.go index 4cabdfcb..72537490 100644 --- a/eval/eval.go +++ b/eval/eval.go @@ -2,7 +2,6 @@ package eval import ( "bytes" - "fmt" "math" "strings" @@ -81,8 +80,8 @@ func (s *State) evalIndexAssigment(which ast.Node, index, value object.Object) o } return value default: - return object.Error{Value: fmt.Sprintf("index assignment to %s of unexpected type %s", - id.Literal(), val.Type().String())} + return s.Errorf("index assignment to %s of unexpected type %s", + id.Literal(), val.Type().String()) } } @@ -102,7 +101,9 @@ func argCheck[T any](s *State, msg string, n int, vararg bool, args []T) *object } func (s *State) evalPrefixIncrDecr(operator token.Type, node ast.Node) object.Object { - log.LogVf("eval prefix %s", ast.DebugString(node)) + if log.LogVerbose() { + log.LogVf("eval prefix %s", ast.DebugString(node)) + } nv := node.Value() if nv.Type() != token.IDENT { return s.NewError("can't prefix increment/decrement " + nv.DebugString()) @@ -128,7 +129,9 @@ func (s *State) evalPrefixIncrDecr(operator token.Type, node ast.Node) object.Ob } func (s *State) evalPostfixExpression(node *ast.PostfixExpression) object.Object { - log.LogVf("eval postfix %s", node.DebugString()) + if log.LogVerbose() { + log.LogVf("eval postfix %s", node.DebugString()) + } id := node.Prev.Literal() val, ok := s.env.Get(id) if !ok { @@ -162,7 +165,7 @@ func (s *State) evalPostfixExpression(node *ast.PostfixExpression) object.Object // Doesn't unwrap return - return bubbles up. // Initially this was the one to use internally recursively, except for when evaluating a function // but now it's less clear because of the need to unwrap references too. TODO: fix/clarify. -func (s *State) evalInternal(node any) object.Object { //nolint:funlen,gocyclo // quite a lot of cases. +func (s *State) evalInternal(node any) object.Object { //nolint:funlen,gocognit,gocyclo // quite a lot of cases. if s.Context != nil && s.Context.Err() != nil { return s.Error(s.Context.Err()) } @@ -182,7 +185,9 @@ func (s *State) evalInternal(node any) object.Object { //nolint:funlen,gocyclo / case *ast.Identifier: return s.evalIdentifier(node) case *ast.PrefixExpression: - log.LogVf("eval prefix %s", node.DebugString()) + if log.LogVerbose() { + log.LogVf("eval prefix %s", node.DebugString()) + } switch node.Type() { case token.INCR, token.DECR: return s.evalPrefixIncrDecr(node.Type(), node.Right) @@ -196,7 +201,9 @@ func (s *State) evalInternal(node any) object.Object { //nolint:funlen,gocyclo / case *ast.PostfixExpression: return s.evalPostfixExpression(node) case *ast.InfixExpression: - log.LogVf("eval infix %s", node.DebugString()) + if log.LogVerbose() { // DebugString() is expensive/shows up in profiles significantly otherwise (before the ifs). + log.LogVf("eval infix %s", node.DebugString()) + } // Eval and not evalInternal because we need to unwrap "return". if node.Token.Type() == token.ASSIGN || node.Token.Type() == token.DEFINE { return s.evalAssignment(s.Eval(node.Right), node) @@ -422,10 +429,14 @@ func (s *State) evalIndexRangeExpression(left object.Object, leftIdx, rightIdx a nilRight := (rightIdx == nil) var rightIndex object.Object if nilRight { - log.Debugf("eval index %s[%s:]", left.Inspect(), leftIndex.Inspect()) + if log.LogDebug() { + log.Debugf("eval index %s[%s:]", left.Inspect(), leftIndex.Inspect()) + } } else { rightIndex = s.Eval(rightIdx) - log.Debugf("eval index %s[%s:%s]", left.Inspect(), leftIndex.Inspect(), rightIndex.Inspect()) + if log.LogDebug() { + log.Debugf("eval index %s[%s:%s]", left.Inspect(), leftIndex.Inspect(), rightIndex.Inspect()) + } } if leftIndex.Type() != object.INTEGER || (!nilRight && rightIndex.Type() != object.INTEGER) { return s.NewError("range index not integer") @@ -514,8 +525,11 @@ func evalArrayIndexExpression(array, index object.Object) object.Object { } func (s *State) applyExtension(fn object.Extension, args []object.Object) object.Object { + // TODO: consider memoizing the extension functions too? or maybe based on flags on the extension. l := len(args) - log.Debugf("apply extension %s variadic %t : %d args %v", fn.Inspect(), fn.Variadic, l, args) + if log.LogDebug() { + log.Debugf("apply extension %s variadic %t : %d args %v", fn.Inspect(), fn.Variadic, l, args) + } if fn.MaxArgs == -1 { // Only do this for true variadic functions (maxargs == -1) if l > 0 && args[l-1].Type() == object.ARRAY { @@ -525,12 +539,12 @@ func (s *State) applyExtension(fn object.Extension, args []object.Object) object } } if l < fn.MinArgs { - return object.Error{Value: fmt.Sprintf("wrong number of arguments got=%d, want %s", - l, fn.Inspect())} // shows usage + return s.Errorf("wrong number of arguments got=%d, want %s", + l, fn.Inspect()) // shows usage } if fn.MaxArgs != -1 && l > fn.MaxArgs { - return object.Error{Value: fmt.Sprintf("wrong number of arguments got=%d, want %s", - l, fn.Inspect())} // shows usage + return s.Errorf("wrong number of arguments got=%d, want %s", + l, fn.Inspect()) // shows usage } for i, arg := range args { if i >= len(fn.ArgTypes) { @@ -539,18 +553,26 @@ func (s *State) applyExtension(fn object.Extension, args []object.Object) object if fn.ArgTypes[i] == object.ANY { continue } + // deref but only if type isn't ANY - so type() gets the REFERENCES but math functions don't/get values. + arg = object.Value(arg) + args[i] = arg // Auto promote integer to float if needed. if fn.ArgTypes[i] == object.FLOAT && arg.Type() == object.INTEGER { args[i] = object.Float{Value: float64(arg.(object.Integer).Value)} continue } if fn.ArgTypes[i] != arg.Type() { - return object.Error{Value: fmt.Sprintf("wrong type of argument got=%s, want %s", - arg.Type(), fn.Inspect())} + return s.Errorf("wrong type of argument got=%s, want %s", + arg.Type(), fn.Inspect()) } } if fn.ClientData != nil { - return fn.Callback(fn.ClientData, fn.Name, args) + res := fn.Callback(fn.ClientData, fn.Name, args) + if res.Type() == object.ERROR { + // Add the stack trace to the error. + return s.ErrorAddStack(res.(object.Error)) + } + return res } return fn.Callback(s, fn.Name, args) } @@ -562,13 +584,15 @@ func (s *State) applyFunction(name string, fn object.Object, args []object.Objec } if v, output, ok := s.cache.Get(function.CacheKey, args); ok { log.Debugf("Cache hit for %s %v", function.CacheKey, args) - _, err := s.Out.Write(output) - if err != nil { - log.Warnf("output: %v", err) + if len(output) > 0 { + _, err := s.Out.Write(output) + if err != nil { + log.Warnf("output: %v", err) + } } return v } - nenv, oerr := extendFunctionEnv(s.env, name, function, args) + nenv, oerr := s.extendFunctionEnv(s.env, name, function, args) if oerr != nil { return *oerr } @@ -585,10 +609,13 @@ func (s *State) applyFunction(name string, fn object.Object, args []object.Objec // restore the previous env/state. s.env = curState s.Out = oldOut - output := buf.Bytes() - _, err := s.Out.Write(output) - if err != nil { - log.Warnf("output: %v", err) + var output []byte + if buf.Len() > 0 { + output = buf.Bytes() + _, err := s.Out.Write(output) + if err != nil { + log.Warnf("output: %v", err) + } } if after != before { log.Debugf("Cache miss for %s %v, %d get misses", function.CacheKey, args, after-before) @@ -606,7 +633,7 @@ func (s *State) applyFunction(name string, fn object.Object, args []object.Objec return res } -func extendFunctionEnv( +func (s *State) extendFunctionEnv( currrentEnv *object.Environment, name string, fn object.Function, args []object.Object, @@ -636,13 +663,16 @@ func extendFunctionEnv( } n := len(params) if len(args) != n { - return nil, &object.Error{Value: fmt.Sprintf("wrong number of arguments for %s. got=%d, want%s=%d", - name, len(args), atLeast, n)} + oerr := s.Errorf("wrong number of arguments for %s. got=%d, want%s=%d", + name, len(args), atLeast, n) + return nil, &oerr } for paramIdx, param := range params { // By definition function parameters are local copies, deref argument values: oerr := env.CreateOrSet(param.Value().Literal(), object.Value(args[paramIdx]), true) - log.LogVf("set %s to %s - %s", param.Value().Literal(), args[paramIdx].Inspect(), oerr.Inspect()) + if log.LogVerbose() { + log.LogVf("set %s to %s - %s", param.Value().Literal(), args[paramIdx].Inspect(), oerr.Inspect()) + } if oerr.Type() == object.ERROR { oe, _ := oerr.(object.Error) return nil, &oe @@ -695,10 +725,14 @@ func (s *State) evalIfExpression(ie *ast.IfExpression) object.Object { condition := s.evalInternal(ie.Condition) switch condition { case object.TRUE: - log.LogVf("if %s is object.TRUE, picking true branch", ie.Condition.Value().DebugString()) + if log.LogVerbose() { + log.LogVf("if %s is object.TRUE, picking true branch", ie.Condition.Value().DebugString()) + } return s.evalInternal(ie.Consequence) case object.FALSE: - log.LogVf("if %s is object.FALSE, picking else branch", ie.Condition.Value().DebugString()) + if log.LogVerbose() { + log.LogVf("if %s is object.FALSE, picking else branch", ie.Condition.Value().DebugString()) + } return s.evalInternal(ie.Alternative) default: return s.NewError("condition is not a boolean: " + condition.Inspect()) @@ -828,13 +862,17 @@ func (s *State) evalForExpression(fe *ast.ForExpression) object.Object { condition := s.evalInternal(fe.Condition) switch condition { case object.TRUE: - log.LogVf("for %s is object.TRUE, running body", fe.Condition.Value().DebugString()) + if log.LogVerbose() { + log.LogVf("for %s is object.TRUE, running body", fe.Condition.Value().DebugString()) + } lastEval = s.evalInternal(fe.Body) if rt := lastEval.Type(); rt == object.RETURN || rt == object.ERROR { return lastEval } case object.FALSE, object.NULL: - log.LogVf("for %s is object.FALSE, done", fe.Condition.Value().DebugString()) + if log.LogVerbose() { + log.LogVf("for %s is object.FALSE, done", fe.Condition.Value().DebugString()) + } return lastEval default: switch condition.Type() { @@ -858,7 +896,9 @@ func (s *State) evalStatements(stmts []ast.Node) object.Object { var result object.Object result = object.NULL // no crash when empty program. for _, statement := range stmts { - log.LogVf("eval statement %T %s", statement, statement.Value().DebugString()) + if log.LogVerbose() { + log.LogVf("eval statement %T %s", statement, statement.Value().DebugString()) + } if isComment(statement) { log.Debugf("skipping comment") continue @@ -947,7 +987,7 @@ func (s *State) evalInfixExpression(operator token.Type, left, right object.Obje case left.Type() == object.ARRAY: return s.evalArrayInfixExpression(operator, left, right) case left.Type() == object.MAP && right.Type() == object.MAP: - return evalMapInfixExpression(operator, left, right) + return s.evalMapInfixExpression(operator, left, right) default: return s.NewError("no " + operator.String() + " on left=" + left.Inspect() + " right=" + right.Inspect()) } @@ -968,8 +1008,8 @@ func (s *State) evalStringInfixExpression(operator token.Type, left, right objec object.MustBeOk(n / object.ObjectSize) return object.String{Value: strings.Repeat(leftVal, int(rightVal))} default: - return object.Error{Value: fmt.Sprintf("unknown operator: %s %s %s", - left.Type(), operator, right.Type())} + return s.Errorf("unknown operator: %s %s %s", + left.Type(), operator, right.Type()) } } @@ -998,20 +1038,20 @@ func (s *State) evalArrayInfixExpression(operator token.Type, left, right object object.MustBeOk(len(leftVal) + len(rightArr)) return object.NewArray(append(leftVal, rightArr...)) default: - return object.Error{Value: fmt.Sprintf("unknown operator: %s %s %s", - left.Type(), operator, right.Type())} + return s.Errorf("unknown operator: %s %s %s", + left.Type(), operator, right.Type()) } } -func evalMapInfixExpression(operator token.Type, left, right object.Object) object.Object { +func (s *State) evalMapInfixExpression(operator token.Type, left, right object.Object) object.Object { leftMap := left.(object.Map) rightMap := right.(object.Map) switch operator { case token.PLUS: // concat / append return leftMap.Append(rightMap) default: - return object.Error{Value: fmt.Sprintf("unknown operator: %s %s %s", - left.Type(), operator, right.Type())} + return s.Errorf("unknown operator: %s %s %s", + left.Type(), operator, right.Type()) } } @@ -1059,27 +1099,30 @@ func (s *State) evalIntegerInfixExpression(operator token.Type, left, right obje } } -func (s *State) getFloatValue(o object.Object) (float64, *object.Error) { +func GetFloatValue(o object.Object) (float64, *object.Error) { switch o.Type() { case object.INTEGER: return float64(o.(object.Integer).Value), nil case object.FLOAT: return o.(object.Float).Value, nil default: - e := s.NewError("not converting to float: " + o.Type().String()) + // Not using state.NewError here because we want this to be reusable by extensions that do not have a state. + // they will get the stack trace added by the eval extension code. for here we + // will add the stack with s.ErrorAddStack(). + e := object.Error{Value: "not converting to float: " + o.Type().String()} return math.NaN(), &e } } // So we copy-pasta instead :-(. func (s *State) evalFloatInfixExpression(operator token.Type, left, right object.Object) object.Object { - leftVal, oerr := s.getFloatValue(left) + leftVal, oerr := GetFloatValue(left) if oerr != nil { - return *oerr + return s.ErrorAddStack(*oerr) } - rightVal, oerr := s.getFloatValue(right) + rightVal, oerr := GetFloatValue(right) if oerr != nil { - return *oerr + return s.ErrorAddStack(*oerr) } switch operator { case token.PLUS: diff --git a/eval/stack.go b/eval/stack.go index 4cc9b78b..54953bab 100644 --- a/eval/stack.go +++ b/eval/stack.go @@ -46,6 +46,10 @@ func (s *State) NewError(msg string) object.Error { return object.Error{Value: msg, Stack: s.Stack()} } +func (s *State) ErrorAddStack(e object.Error) object.Error { + return object.Error{Value: e.Value, Stack: s.Stack()} +} + // Errorf formats and create an object.Error using given format and args. func (s *State) Errorf(format string, args ...interface{}) object.Error { return s.NewError(fmt.Sprintf(format, args...)) diff --git a/examples/image.gr b/examples/image.gr new file mode 100644 index 00000000..afe69591 --- /dev/null +++ b/examples/image.gr @@ -0,0 +1,43 @@ +/* + * Create an image - very useful to use a smaller increment and run with profiling. + */ + +// inspiration @shokhie +// See https://github.com/grol-io/grol-discord-bot/blob/main/discord.gr for a version +// with X Y input etc... integrated with discord bot. + +// With angle as an int modulo 360 input, this gets memoized. +func ycbcr(angle) { + angle = PI * angle / 180. + // Y Cb Cr + [190, 128 + 120*sin(angle), 128 + 120*cos(angle)] +} + +// saturation = 1 +// lightness = .6 + +size = 1024 +imgName = "canvas" +canvas = image(imgName, size, size) +div = 6 + +t = 0 +now = time() +// color = [0, saturation, lightness] +for t < 12*PI { + x = sin(t) * (pow(E, cos(t)) - 2*cos(4*t) - pow(sin(t/12), 5)) + 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) + // 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 +} +elapsed = time() - now +log("Time elapsed: ", elapsed, " seconds") + +image_save(imgName) + +println("Saved image to grol.png") diff --git a/examples/prime_sequence_generator.gr b/examples/prime_sequence_generator.gr new file mode 100644 index 00000000..1cffc975 --- /dev/null +++ b/examples/prime_sequence_generator.gr @@ -0,0 +1,43 @@ +/* Demonstrate how to have lambdas as iterators */ + +func primesGen(maxSieve) { + sieve = [true] * maxSieve + num = 2 + currentPrime = 2 + nextPrime = () => { + for currentPrime < maxSieve { + if sieve[currentPrime] { + prime = currentPrime + currentPrime++ + mult = prime * prime + for max(maxSieve - mult, 0) { + if mult % prime == 0 { + sieve[mult] = false + } + mult++ + } + return prime + } else { + currentPrime++ + } + } + return nil + } +} + + +maxS = 1000 // use 100_000 for profiling. +primeIter = primesGen(maxS) +p = primeIter() +printf("| %4d", p) +n = 1 +for p <= maxS { + p = primeIter() + if p != nil { + printf(" %4d", p) + if ++n % 15 == 0 { + print("\n|") + } + } +} +println() diff --git a/extensions/extension.go b/extensions/extension.go index f024a9ca..5741faa9 100644 --- a/extensions/extension.go +++ b/extensions/extension.go @@ -158,13 +158,14 @@ func initInternal(c *Config) error { MaxArgs: 1, ArgTypes: []object.Type{object.INTEGER}, Callback: func(env any, _ string, args []object.Object) object.Object { - eval.TriggerNoCache(env) + s := env.(*eval.State) + eval.TriggerNoCache(s) if len(args) == 0 { return object.Float{Value: rand.Float64()} //nolint:gosec // no need for crypto/rand here. } n := args[0].(object.Integer).Value if n <= 0 { - return object.Error{Value: "argument to rand() if given must be > 0, >=2 for something useful"} + return s.NewError("argument to rand() if given must be > 0, >=2 for something useful") } return object.Integer{Value: rand.Int64N(n)} //nolint:gosec // no need for crypto/rand here. }, @@ -173,6 +174,7 @@ func initInternal(c *Config) error { createStrFunctions() createMisc() createTimeFunctions() + createImageFunctions() return nil } diff --git a/extensions/images.go b/extensions/images.go new file mode 100644 index 00000000..000e699a --- /dev/null +++ b/extensions/images.go @@ -0,0 +1,249 @@ +package extensions + +import ( + "image" + "image/color" + "image/draw" + "image/png" + "math" + "os" + + "grol.io/grol/eval" + "grol.io/grol/object" +) + +type ImageMap map[object.Object]*image.RGBA + +// TODO: make this configurable and use the slice check as well as some sort of LRU. +const MaxImageDimension = 1024 // in pixels. + +// HSLToRGB converts HSL values to RGB. h, s and l in [0,1]. +func HSLToRGB(h, s, l float64) color.RGBA { + var r, g, b float64 + + // h = math.Mod(h, 360.) / 360. + + if s == 0 { + r, g, b = l, l, l + } else { + var q float64 + if l < 0.5 { + q = l * (1. + s) + } else { + q = l + s - l*s + } + p := 2*l - q + r = hueToRGB(p, q, h+1/3.) + g = hueToRGB(p, q, h) + b = hueToRGB(p, q, h-1/3.) + } + + return color.RGBA{ + R: uint8(math.Round(r * 255)), + G: uint8(math.Round(g * 255)), + B: uint8(math.Round(b * 255)), + A: 255, + } +} + +func hueToRGB(p, q, t float64) float64 { + if t < 0 { + t += 1. + } + if t > 1 { + t -= 1. + } + if t < 1/6. { + return p + (q-p)*6*t + } + if t < 0.5 { + return q + } + if t < 2/3. { + return p + (q-p)*(2/3.-t)*6 + } + return p +} + +func hslArrayToRBGAColor(arr []object.Object) (color.RGBA, *object.Error) { + rgba := color.RGBA{} + if len(arr) != 3 { + return rgba, object.Errorfp("color array must be [Hue,Saturation,Lightness]") + } + var oerr *object.Error + h, oerr := eval.GetFloatValue(arr[0]) + if oerr != nil { + return rgba, oerr + } + s, oerr := eval.GetFloatValue(arr[1]) + if oerr != nil { + return rgba, oerr + } + l, oerr := eval.GetFloatValue(arr[2]) + if oerr != nil { + return rgba, oerr + } + return HSLToRGB(h, s, l), nil +} + +func elem2ColorComponent(o object.Object) (uint8, *object.Error) { + var i int + switch o.Type() { + case object.FLOAT: + i = int(math.Round(o.(object.Float).Value)) + case object.INTEGER: + i = int(o.(object.Integer).Value) + default: + return 0, object.Errorfp("color component not an integer: %s", o.Inspect()) + } + if i < 0 || i > 255 { + return 0, object.Errorfp("color component out of range (should be 0-255): %s", o.Inspect()) + } + return uint8(i), nil //nolint:gosec // gosec not smart enough to see the range check just above +} + +func rgbArrayToRBGAColor(arr []object.Object) (color.RGBA, *object.Error) { + rgba := color.RGBA{} + if len(arr) < 3 || len(arr) > 4 { + return rgba, object.Errorfp("color array must be [R,G,B] or [R,G,B,A]") + } + var oerr *object.Error + rgba.R, oerr = elem2ColorComponent(arr[0]) + if oerr != nil { + return rgba, oerr + } + rgba.G, oerr = elem2ColorComponent(arr[1]) + if oerr != nil { + return rgba, oerr + } + rgba.B, oerr = elem2ColorComponent(arr[2]) + if oerr != nil { + return rgba, oerr + } + if len(arr) == 4 { + rgba.A, oerr = elem2ColorComponent(arr[3]) + if oerr != nil { + return rgba, oerr + } + } else { + rgba.A = 255 + } + return rgba, nil +} + +func ycbrArrayToRBGAColor(arr []object.Object) (color.RGBA, *object.Error) { + rgba := color.RGBA{} + ycbcr := color.YCbCr{} + if len(arr) != 3 { + return rgba, object.Errorfp("color array must be [Y',Cb,Cr]") + } + var oerr *object.Error + ycbcr.Y, oerr = elem2ColorComponent(arr[0]) + if oerr != nil { + return rgba, oerr + } + ycbcr.Cb, oerr = elem2ColorComponent(arr[1]) + if oerr != nil { + return rgba, oerr + } + ycbcr.Cr, oerr = elem2ColorComponent(arr[2]) + if oerr != nil { + return rgba, oerr + } + rgba.A = 255 + rgba.R, rgba.G, rgba.B = color.YCbCrToRGB(ycbcr.Y, ycbcr.Cb, ycbcr.Cr) + // return color.YCbCrModel.Convert(ycbcr).(color.RGBA), nil + return rgba, nil +} + +func createImageFunctions() { + // All the functions consistently use args[0] as the image name/reference into the ClientData map. + cdata := make(ImageMap) + imgFn := object.Extension{ + Name: "image", + MinArgs: 3, + MaxArgs: 3, + Help: "create a new RGBA image of the name and size, image starts entirely transparent", + ArgTypes: []object.Type{object.STRING, object.INTEGER, object.INTEGER}, + ClientData: cdata, + Callback: func(cdata any, _ string, args []object.Object) object.Object { + images := cdata.(ImageMap) + x := int(args[1].(object.Integer).Value) + y := int(args[2].(object.Integer).Value) + if x > MaxImageDimension || y > MaxImageDimension { + return object.Errorf("image size too large") + } + if x < 0 || y < 0 { + return object.Errorf("image sizes must be positive") + } + img := image.NewRGBA(image.Rect(0, 0, x, y)) + transparent := color.RGBA{0, 0, 0, 0} + draw.Draw(img, img.Bounds(), &image.Uniform{transparent}, image.Point{}, draw.Src) + images[args[0]] = img + return args[0] + }, + } + MustCreate(imgFn) + 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 + imgFn.ArgTypes = []object.Type{object.STRING, object.INTEGER, object.INTEGER, object.ARRAY} + imgFn.Callback = func(cdata any, name string, args []object.Object) object.Object { + images := cdata.(ImageMap) + x := int(args[1].(object.Integer).Value) + y := int(args[2].(object.Integer).Value) + img, ok := images[args[0]] + if !ok { + return object.Errorf("image not found") + } + colorArray := object.Elements(args[3]) + var color color.RGBA + var oerr *object.Error + switch name { + case "image_set_ycbcr": + color, oerr = ycbrArrayToRBGAColor(colorArray) + case "image_set_hsl": + color, oerr = hslArrayToRBGAColor(colorArray) + case "image_set": + color, oerr = rgbArrayToRBGAColor(colorArray) + default: + return object.Errorf("unknown image_set function %q", name) + } + if oerr != nil { + return oerr + } + img.SetRGBA(x, y, color) + return args[0] + } + MustCreate(imgFn) + 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.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.Help = "save the named image grol.png" + 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") + } + outputFile, err := os.Create("grol.png") + if err != nil { + return object.Errorf("error opening image file: %v", err) + } + defer outputFile.Close() + err = png.Encode(outputFile, img) + if err != nil { + return object.Errorf("error encoding image: %v", err) + } + return args[0] + } + MustCreate(imgFn) +} diff --git a/go.mod b/go.mod index 11cd96b6..382248fb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module grol.io/grol -go 1.22.6 +go 1.22.7 require ( fortio.org/cli v1.9.0 diff --git a/object/object.go b/object/object.go index 16ffb92d..5d41716c 100644 --- a/object/object.go +++ b/object/object.go @@ -498,6 +498,17 @@ type Error struct { Stack []string } +// Use eval's Errorf() instead whenever possible, to get the stack. +// This one should only be used by extensions that do not take the state as clientdata. +func Errorf(format string, args ...interface{}) Error { + return Error{Value: fmt.Sprintf(format, args...)} +} + +// Pointer version of Errorf. used in code conditionally returning an error (oerr pointer). +func Errorfp(format string, args ...interface{}) *Error { + return &Error{Value: fmt.Sprintf(format, args...)} +} + func (e Error) JSON(w io.Writer) error { _, err := fmt.Fprintf(w, `{"err":%q}`, e.Value) return err @@ -1045,7 +1056,9 @@ type Reference struct { } func (r Reference) Value() Object { - log.Debugf("Reference Value() %s -> %s", r.Name, r.RefEnv.store[r.Name].Inspect()) + if log.LogDebug() { + log.Debugf("Reference Value() %s -> %s", r.Name, r.RefEnv.store[r.Name].Inspect()) + } v := r.RefEnv.store[r.Name] if v == r { panic("Self reference") diff --git a/object/state.go b/object/state.go index 1f57dfd3..9c117c61 100644 --- a/object/state.go +++ b/object/state.go @@ -189,7 +189,9 @@ func (e *Environment) makeRef(name string) (*Reference, bool) { ref = r // set and return the original ref instead of ref of ref. } orig.store[name] = ref - orig.getMiss++ // creating a ref is a miss. + if !Constant(name) { + orig.getMiss++ // creating a ref to a non constant is a miss. + } return &ref, true } return nil, false @@ -201,7 +203,8 @@ func (e *Environment) Get(name string) (Object, bool) { } obj, ok := e.store[name] if ok { - if _, ok = obj.(Reference); ok { // using references implies uncacheable. + // using references to non constant (extensions are constants) implies uncacheable. + if r, ok := obj.(Reference); ok && !Constant(r.Name) { e.getMiss++ } return obj, true