From 7a92eeddacebdf21e1b9c0bb80a14fecf39ae9f7 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 11 Dec 2024 22:30:14 +1100 Subject: [PATCH] Added custom printers --- cmd/cmsh/fancy.go | 57 +++++++++++++++++++ cmd/cmsh/main.go | 5 ++ repl/evaldisplay.go | 108 ++++++++++++++++++++++++++++++++--- repl/repl.go | 18 ++++-- repl/typeprinter.go | 20 +++++++ ucl/builtins.go | 133 ++++++++++++++++++++++++++++---------------- ucl/inst.go | 9 +++ 7 files changed, 287 insertions(+), 63 deletions(-) create mode 100644 cmd/cmsh/fancy.go create mode 100644 repl/typeprinter.go diff --git a/cmd/cmsh/fancy.go b/cmd/cmsh/fancy.go new file mode 100644 index 0000000..e570954 --- /dev/null +++ b/cmd/cmsh/fancy.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + "io" + "ucl.lmika.dev/repl" + "ucl.lmika.dev/ucl" +) + +type FancyType struct { + Foo string + Bar string +} + +var newFancyDoc = repl.Doc{ + Brief: "returns something fancy", + Detailed: ` + This will create and return something fancy. + `, +} + +func newFancy(ctx context.Context, args ucl.CallArgs) (any, error) { + return FancyType{Foo: "is foo", Bar: "is bar"}, nil +} + +func manyFancies(ctx context.Context, args ucl.CallArgs) (any, error) { + return []FancyType{ + {Foo: "foo 1", Bar: "bar 1"}, + {Foo: "foo 2", Bar: "bar 2"}, + {Foo: "foo 3", Bar: "bar 3"}, + }, nil +} + +func displayFancy(w io.Writer, f FancyType, concise bool) error { + if concise { + _, err := fmt.Fprintf(w, "%s:%s", f.Foo, f.Bar) + return err + } + + fmt.Fprintf(w, "Foo.. %s\n", f.Foo) + fmt.Fprintf(w, "Bar.. %s\n", f.Bar) + return nil +} + +func displayFancies(w io.Writer, fs []FancyType, concise bool) error { + if concise { + _, err := fmt.Fprintf(w, "%d fancies", len(fs)) + return err + } + + fmt.Fprintln(w, "FOO\tBAR") + for _, f := range fs { + fmt.Fprintf(w, "%v\t%v\n", f.Foo, f.Bar) + } + return nil +} diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index 11bd913..8793777 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -33,6 +33,11 @@ func main() { It then terminates. `, }) + instRepl.SetCommand("new-fancy", newFancy, newFancyDoc) + instRepl.SetCommand("many-fancies", manyFancies) + + repl.AddTypePrinter(instRepl, displayFancy) + repl.AddTypePrinter(instRepl, displayFancies) for { line, err := rl.Readline() diff --git a/repl/evaldisplay.go b/repl/evaldisplay.go index 8b1abb5..d062778 100644 --- a/repl/evaldisplay.go +++ b/repl/evaldisplay.go @@ -3,7 +3,10 @@ package repl import ( "context" "fmt" + "io" "os" + "reflect" + "strings" "ucl.lmika.dev/ucl" ) @@ -15,31 +18,118 @@ func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error { return err } - return displayResult(ctx, r.inst, res) + return r.displayResult(ctx, os.Stdout, res, false) } -func displayResult(ctx context.Context, inst *ucl.Inst, res any) (err error) { +func (r *REPL) echoPrinter(ctx context.Context, w io.Writer, args []any) (err error) { + if len(args) == 0 { + _, err := fmt.Fprintln(w) + return err + } + + var line strings.Builder + for _, arg := range args { + if err := r.displayResult(ctx, &line, arg, len(args) > 1); err != nil { + return err + } + } + + res := line.String() + if strings.HasSuffix(res, "\n") { + res = res[:len(res)-1] + } + + _, err = fmt.Fprintln(w, res) + return nil +} + +func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise bool) (err error) { + // Check type printers + tp, ok := r.typePrinters[reflect.TypeOf(res)] + if ok { + return tp(w, res, concise) + } + switch v := res.(type) { - case NoResults: - return nil case nil: if _, err = fmt.Fprintln(os.Stdout, "(nil)"); err != nil { return err } case ucl.Listable: - for i := 0; i < v.Len(); i++ { - if err = displayResult(ctx, inst, v.Index(i)); err != nil { - return err + if concise { + fmt.Fprintf(w, "[") + for i := 0; i < v.Len(); i++ { + if i > 0 { + fmt.Fprintf(w, " ") + } + if err = r.displayResult(ctx, w, v.Index(i), true); err != nil { + return err + } + } + fmt.Fprintf(w, "]") + } else { + for i := 0; i < v.Len(); i++ { + if err = r.displayResult(ctx, w, v.Index(i), true); err != nil { + return err + } + fmt.Fprintf(w, "\n") + } + } + case []interface{}: + if concise { + fmt.Fprintf(w, "[") + for i := 0; i < len(v); i++ { + if i > 0 { + fmt.Fprintf(w, " ") + } + if err = r.displayResult(ctx, w, v[i], true); err != nil { + return err + } + } + fmt.Fprintf(w, "]") + } else { + // In the off-chance that this is actually a slice of printables + vt := reflect.SliceOf(reflect.TypeOf(v[0])) + if tp, ok := r.typePrinters[vt]; ok { + canDisplay := true + + typeSlice := reflect.MakeSlice(vt, len(v), len(v)) + for i := 0; i < len(v); i++ { + vv := reflect.ValueOf(v[i]) + if vv.CanConvert(vt.Elem()) { + typeSlice.Index(i).Set(vv) + } else { + canDisplay = false + break + } + } + + if canDisplay { + return tp(w, typeSlice.Interface(), concise) + } + } + + for i := 0; i < len(v); i++ { + if err = r.displayResult(ctx, w, v[i], true); err != nil { + return err + } + fmt.Fprintf(w, "\n") } } case ucl.Object: - if _, err = fmt.Fprintln(os.Stdout, v.String()); err != nil { + if _, err = fmt.Fprint(w, v.String()); err != nil { return err } + if !concise { + fmt.Fprintln(w) + } default: - if _, err = fmt.Fprintln(os.Stdout, v); err != nil { + if _, err = fmt.Fprint(w, v); err != nil { return err } + if !concise { + fmt.Fprintln(w) + } } return nil } diff --git a/repl/repl.go b/repl/repl.go index b9cf19e..58997d4 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -1,6 +1,9 @@ package repl import ( + "io" + "reflect" + "slices" "ucl.lmika.dev/ucl" ) @@ -11,16 +14,19 @@ type CommandOpt interface { type REPL struct { inst *ucl.Inst - commandDocs map[string]Doc + commandDocs map[string]Doc + typePrinters map[reflect.Type]func(w io.Writer, v any, brief bool) error } func New(opts ...ucl.InstOption) *REPL { - inst := ucl.New(opts...) - r := &REPL{ - inst: inst, - commandDocs: make(map[string]Doc), + commandDocs: make(map[string]Doc), + typePrinters: make(map[reflect.Type]func(w io.Writer, v any, brief bool) error), } + + instOpts := append(slices.Clone(opts), ucl.WithCustomEchoPrinter(r.echoPrinter)) + r.inst = ucl.New(instOpts...) + r.SetCommand("help", r.helpBuiltin, Doc{ Brief: "displays help about a command", Usage: "[command]", @@ -36,6 +42,8 @@ func New(opts ...ucl.InstOption) *REPL { `, }) + AddTypePrinter(r, func(w io.Writer, t NoResults, concise bool) error { return nil }) + return r } diff --git a/repl/typeprinter.go b/repl/typeprinter.go new file mode 100644 index 0000000..d373e72 --- /dev/null +++ b/repl/typeprinter.go @@ -0,0 +1,20 @@ +package repl + +import ( + "fmt" + "io" + "reflect" +) + +func AddTypePrinter[T any](r *REPL, p func(w io.Writer, t T, concise bool) error) { + var t T + + tt := reflect.TypeOf(t) + r.typePrinters[tt] = func(w io.Writer, v any, concise bool) error { + vt, ok := v.(T) + if !ok { + return fmt.Errorf("cannot convert %v to T", v) + } + return p(w, vt, concise) + } +} diff --git a/ucl/builtins.go b/ucl/builtins.go index 89a26ac..d9cce72 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -4,10 +4,24 @@ import ( "context" "errors" "fmt" + "strconv" "strings" ) -func echoBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func echoBuiltin(ctx context.Context, args invocationArgs) (Object, error) { + echoPrinter := args.inst.echoPrinter + if echoPrinter != nil { + convertedArgs := make([]interface{}, len(args.args)) + for i, arg := range args.args { + if convArg, ok := toGoValue(arg); ok { + convertedArgs[i] = convArg + } else { + convertedArgs[i] = arg + } + } + return nil, echoPrinter(ctx, args.inst.out, convertedArgs) + } + if len(args.args) == 0 { if _, err := fmt.Fprintln(args.inst.Out()); err != nil { return nil, err @@ -28,7 +42,7 @@ func echoBuiltin(ctx context.Context, args invocationArgs) (object, error) { return nil, nil } -func addBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func addBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) == 0 { return intObject(0), nil } @@ -52,7 +66,7 @@ func addBuiltin(ctx context.Context, args invocationArgs) (object, error) { return intObject(n), nil } -func subBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func subBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) == 0 { return intObject(0), nil } @@ -82,7 +96,7 @@ func subBuiltin(ctx context.Context, args invocationArgs) (object, error) { return intObject(n), nil } -func mupBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func mupBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) == 0 { return intObject(1), nil } @@ -106,7 +120,7 @@ func mupBuiltin(ctx context.Context, args invocationArgs) (object, error) { return intObject(n), nil } -func divBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func divBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) == 0 { return intObject(1), nil } @@ -136,7 +150,7 @@ func divBuiltin(ctx context.Context, args invocationArgs) (object, error) { return intObject(n), nil } -func modBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func modBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) == 0 { return intObject(0), nil } @@ -166,7 +180,7 @@ func modBuiltin(ctx context.Context, args invocationArgs) (object, error) { return intObject(n), nil } -func setBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func setBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -182,7 +196,7 @@ func setBuiltin(ctx context.Context, args invocationArgs) (object, error) { return newVal, nil } -func toUpperBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func toUpperBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err } @@ -193,7 +207,7 @@ func toUpperBuiltin(ctx context.Context, args invocationArgs) (object, error) { return strObject(strings.ToUpper(sarg)), nil } -func eqBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func eqBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -204,7 +218,7 @@ func eqBuiltin(ctx context.Context, args invocationArgs) (object, error) { return boolObject(objectsEqual(l, r)), nil } -func neBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -215,7 +229,7 @@ func neBuiltin(ctx context.Context, args invocationArgs) (object, error) { return boolObject(!objectsEqual(l, r)), nil } -func ltBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func ltBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -227,7 +241,7 @@ func ltBuiltin(ctx context.Context, args invocationArgs) (object, error) { return boolObject(isLess), nil } -func leBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func leBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -239,7 +253,7 @@ func leBuiltin(ctx context.Context, args invocationArgs) (object, error) { return boolObject(isLess || objectsEqual(args.args[0], args.args[1])), nil } -func gtBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func gtBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -251,7 +265,7 @@ func gtBuiltin(ctx context.Context, args invocationArgs) (object, error) { return boolObject(isGreater), nil } -func geBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func geBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -263,7 +277,7 @@ func geBuiltin(ctx context.Context, args invocationArgs) (object, error) { return boolObject(isGreater || objectsEqual(args.args[0], args.args[1])), nil } -func andBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func andBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -276,7 +290,7 @@ func andBuiltin(ctx context.Context, args invocationArgs) (object, error) { return args.args[len(args.args)-1], nil } -func orBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func orBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -289,7 +303,7 @@ func orBuiltin(ctx context.Context, args invocationArgs) (object, error) { return boolObject(false), nil } -func notBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func notBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err } @@ -299,7 +313,7 @@ func notBuiltin(ctx context.Context, args invocationArgs) (object, error) { var errObjectsNotEqual = errors.New("objects not equal") -func objectsEqual(l, r object) bool { +func objectsEqual(l, r Object) bool { if l == nil || r == nil { return l == nil && r == nil } @@ -317,8 +331,8 @@ func objectsEqual(l, r object) bool { if rv, ok := r.(boolObject); ok { return lv == rv } - case listable: - rv, ok := r.(listable) + case Listable: + rv, ok := r.(Listable) if !ok { return false } @@ -341,7 +355,7 @@ func objectsEqual(l, r object) bool { if lv.Len() != rv.Len() { return false } - if err := lv.Each(func(k string, lkv object) error { + if err := lv.Each(func(k string, lkv Object) error { rkv := rv.Value(k) if rkv == nil { return errObjectsNotEqual @@ -357,7 +371,7 @@ func objectsEqual(l, r object) bool { return false } -func objectsLessThan(l, r object) (bool, error) { +func objectsLessThan(l, r Object) (bool, error) { switch lv := l.(type) { case strObject: if rv, ok := r.(strObject); ok { @@ -371,7 +385,7 @@ func objectsLessThan(l, r object) (bool, error) { return false, errors.New("objects are not comparable") } -func strBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func strBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err } @@ -383,7 +397,7 @@ func strBuiltin(ctx context.Context, args invocationArgs) (object, error) { return strObject(args.args[0].String()), nil } -func intBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func intBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err } @@ -411,7 +425,7 @@ func intBuiltin(ctx context.Context, args invocationArgs) (object, error) { return nil, errors.New("cannot convert to int") } -func concatBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func concatBuiltin(ctx context.Context, args invocationArgs) (Object, error) { var sb strings.Builder for _, a := range args.args { @@ -424,7 +438,7 @@ func concatBuiltin(ctx context.Context, args invocationArgs) (object, error) { return strObject(sb.String()), nil } -func callBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func callBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err } @@ -437,7 +451,7 @@ func callBuiltin(ctx context.Context, args invocationArgs) (object, error) { return inv.invoke(ctx, args.shift(1)) } -func lenBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func lenBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err } @@ -445,7 +459,7 @@ func lenBuiltin(ctx context.Context, args invocationArgs) (object, error) { switch v := args.args[0].(type) { case strObject: return intObject(len(string(v))), nil - case listable: + case Listable: return intObject(v.Len()), nil case hashable: return intObject(v.Len()), nil @@ -454,9 +468,9 @@ func lenBuiltin(ctx context.Context, args invocationArgs) (object, error) { return intObject(0), nil } -func indexLookup(ctx context.Context, obj, elem object) (object, error) { +func indexLookup(ctx context.Context, obj, elem Object) (Object, error) { switch v := obj.(type) { - case listable: + case Listable: intIdx, ok := elem.(intObject) if !ok { return nil, nil @@ -475,7 +489,7 @@ func indexLookup(ctx context.Context, obj, elem object) (object, error) { return nil, nil } -func indexBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err } @@ -492,7 +506,28 @@ func indexBuiltin(ctx context.Context, args invocationArgs) (object, error) { return val, nil } -func mapBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func keysBuiltin(ctx context.Context, args invocationArgs) (Object, error) { + if err := args.expectArgn(1); err != nil { + return nil, err + } + + val := args.args[0] + switch v := val.(type) { + case hashable: + keys := make(listObject, 0, v.Len()) + if err := v.Each(func(k string, _ Object) error { + keys = append(keys, strObject(k)) + return nil + }); err != nil { + return nil, err + } + return keys, nil + } + + return nil, nil +} + +func mapBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err } @@ -503,12 +538,12 @@ func mapBuiltin(ctx context.Context, args invocationArgs) (object, error) { } switch t := args.args[0].(type) { - case listable: + case Listable: l := t.Len() newList := listObject{} for i := 0; i < l; i++ { v := t.Index(i) - m, err := inv.invoke(ctx, args.fork([]object{v})) + m, err := inv.invoke(ctx, args.fork([]Object{v})) if err != nil { return nil, err } @@ -525,7 +560,7 @@ func firstBuiltin(ctx context.Context, args invocationArgs) (object, error) { } switch t := args.args[0].(type) { - case listable: + case Listable: if t.Len() == 0 { return nil, nil } @@ -561,7 +596,7 @@ func (s seqObject) Len() int { return l } -func (s seqObject) Index(i int) object { +func (s seqObject) Index(i int) Object { l := s.Len() if i < 0 || i > l { return nil @@ -572,7 +607,7 @@ func (s seqObject) Index(i int) object { return intObject(s.from + i) } -func seqBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func seqBuiltin(ctx context.Context, args invocationArgs) (Object, error) { inclusive := false if inc, ok := args.kwargs["inc"]; ok { inclusive = (inc.Len() == 0) || inc.Truthy() @@ -601,7 +636,7 @@ func seqBuiltin(ctx context.Context, args invocationArgs) (object, error) { } } -func ifBuiltin(ctx context.Context, args macroArgs) (object, error) { +func ifBuiltin(ctx context.Context, args macroArgs) (Object, error) { if args.nargs() < 2 { return nil, errors.New("need at least 2 arguments") } @@ -641,7 +676,7 @@ func ifBuiltin(ctx context.Context, args macroArgs) (object, error) { func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { var ( - items object + items Object blockIdx int err error ) @@ -664,16 +699,16 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { } var ( - last object + last Object breakErr errBreak ) switch t := items.(type) { - case listable: + case Listable: l := t.Len() for i := 0; i < l; i++ { v := t.Index(i) - last, err = args.evalBlock(ctx, blockIdx, []object{v}, true) // TO INCLUDE: the index + last, err = args.evalBlock(ctx, blockIdx, []Object{v}, true) // TO INCLUDE: the index if err != nil { if errors.As(err, &breakErr) { if !breakErr.isCont { @@ -685,8 +720,8 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { } } case hashable: - err := t.Each(func(k string, v object) error { - last, err = args.evalBlock(ctx, blockIdx, []object{strObject(k), v}, true) + err := t.Each(func(k string, v Object) error { + last, err = args.evalBlock(ctx, blockIdx, []Object{strObject(k), v}, true) return err }) if errors.As(err, &breakErr) { @@ -701,25 +736,25 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (object, error) { return last, nil } -func breakBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func breakBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) < 1 { return nil, errBreak{} } return nil, errBreak{ret: args.args[0]} } -func continueBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func continueBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return nil, errBreak{isCont: true} } -func returnBuiltin(ctx context.Context, args invocationArgs) (object, error) { +func returnBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) < 1 { return nil, errReturn{} } return nil, errReturn{ret: args.args[0]} } -func procBuiltin(ctx context.Context, args macroArgs) (object, error) { +func procBuiltin(ctx context.Context, args macroArgs) (Object, error) { if args.nargs() < 1 { return nil, errors.New("need at least one arguments") } @@ -763,7 +798,7 @@ func (b procObject) Truthy() bool { return true } -func (b procObject) invoke(ctx context.Context, args invocationArgs) (object, error) { +func (b procObject) invoke(ctx context.Context, args invocationArgs) (Object, error) { newEc := b.ec.fork() for i, name := range b.block.Names { diff --git a/ucl/inst.go b/ucl/inst.go index bf770ce..f5cbc9f 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -11,6 +11,7 @@ import ( type Inst struct { out io.Writer missingBuiltinHandler MissingBuiltinHandler + echoPrinter EchoPrinter rootEC *evalCtx } @@ -37,6 +38,14 @@ func WithModule(module Module) InstOption { } } +type EchoPrinter func(ctx context.Context, w io.Writer, args []any) error + +func WithCustomEchoPrinter(echoPrinter EchoPrinter) InstOption { + return func(i *Inst) { + i.echoPrinter = echoPrinter + } +} + type Module struct { Name string Builtins map[string]BuiltinHandler