From c34ed9a1686e603945a9f6d1efe86e4960fb0cfe Mon Sep 17 00:00:00 2001 From: Jacek Olszak Date: Sat, 12 Aug 2023 15:20:27 +0200 Subject: [PATCH] Add REPL (scripting) to devtools This commit adds the ability to run Go code when the game is running. No need to compile, and restart the game. Just write the code in terminal, hit enter, and it will be immediately executed. Go code is interpreted by traefik/yaegi interpreter. It is a Go interpreter compatible with standard Go compiler. Lines are read from terminal by peterh/liner package. For now I'm using my fork of github.com/peterh/liner, because the original does not work well in Goland/VSCode on Win11 (see https://github.com/peterh/liner/issues/163). This is temporary, until peterh/liner is fixed properly. Limitations: * MidInt, MaxInt, MinInt - those functions can be executed in terminal but only accepts int's (not int64, byte etc.) * pi.Int cannot be used * The size of compiled game with devtools is increased by 3.7MB (+35% increase), the compilation takes 21% more time. But it is worth it. I plan to add a possibility to disable scripting in the future though. --- .vscode/launch.json | 16 + README.md | 2 +- devtools/control.go | 11 + devtools/devtools.go | 15 +- devtools/internal/help/help.go | 124 ++++++ devtools/internal/help/help_test.go | 106 +++++ devtools/internal/inspector/measure.go | 2 +- devtools/internal/inspector/update.go | 8 - devtools/internal/interpreter/error.go | 12 + devtools/internal/interpreter/interpreter.go | 294 +++++++++++++ .../internal/interpreter/interpreter_test.go | 400 ++++++++++++++++++ .../lib/github_com-elgopher-pi-font.go | 16 + .../lib/github_com-elgopher-pi-image.go | 20 + .../lib/github_com-elgopher-pi-key.go | 93 ++++ .../lib/github_com-elgopher-pi-snap.go | 16 + .../lib/github_com-elgopher-pi-state.go | 28 ++ .../internal/lib/github_com-elgopher-pi.go | 116 +++++ devtools/internal/lib/go1_20_bytes.go | 76 ++++ devtools/internal/lib/go1_20_fmt.go | 148 +++++++ devtools/internal/lib/go1_20_math.go | 113 +++++ devtools/internal/lib/go1_20_math_rand.go | 75 ++++ devtools/internal/lib/go1_20_os.go | 208 +++++++++ devtools/internal/lib/go1_20_sort.go | 59 +++ devtools/internal/lib/go1_20_strconv.go | 56 +++ devtools/internal/lib/go1_20_strings.go | 70 +++ devtools/internal/lib/lib.go | 42 ++ devtools/internal/lib/lib_test.go | 37 ++ devtools/internal/lib/maptypes.go | 30 ++ devtools/internal/lib/restricted.go | 20 + devtools/internal/lib/yaegi.go | 12 + devtools/internal/terminal/terminal.go | 72 ++++ devtools/internal/test/stdout_swapper.go | 43 ++ devtools/scripting.go | 82 ++++ devtools/update.go | 2 + docs/ROADMAP.md | 1 + examples/scripting/main.go | 95 +++++ examples/scripting/sprite-sheet.png | Bin 0 -> 268 bytes go.mod | 5 + go.sum | 6 + image/image.go | 11 + image/image_test.go | 22 + internal/sfmt/sfmt.go | 27 ++ internal/sfmt/sfmt_test.go | 55 +++ pixmap.go | 11 + pixmap_test.go | 14 + print.go | 6 + print_test.go | 26 ++ 47 files changed, 2692 insertions(+), 11 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 devtools/internal/help/help.go create mode 100644 devtools/internal/help/help_test.go create mode 100644 devtools/internal/interpreter/error.go create mode 100644 devtools/internal/interpreter/interpreter.go create mode 100644 devtools/internal/interpreter/interpreter_test.go create mode 100644 devtools/internal/lib/github_com-elgopher-pi-font.go create mode 100644 devtools/internal/lib/github_com-elgopher-pi-image.go create mode 100644 devtools/internal/lib/github_com-elgopher-pi-key.go create mode 100644 devtools/internal/lib/github_com-elgopher-pi-snap.go create mode 100644 devtools/internal/lib/github_com-elgopher-pi-state.go create mode 100644 devtools/internal/lib/github_com-elgopher-pi.go create mode 100644 devtools/internal/lib/go1_20_bytes.go create mode 100644 devtools/internal/lib/go1_20_fmt.go create mode 100644 devtools/internal/lib/go1_20_math.go create mode 100644 devtools/internal/lib/go1_20_math_rand.go create mode 100644 devtools/internal/lib/go1_20_os.go create mode 100644 devtools/internal/lib/go1_20_sort.go create mode 100644 devtools/internal/lib/go1_20_strconv.go create mode 100644 devtools/internal/lib/go1_20_strings.go create mode 100644 devtools/internal/lib/lib.go create mode 100644 devtools/internal/lib/lib_test.go create mode 100644 devtools/internal/lib/maptypes.go create mode 100644 devtools/internal/lib/restricted.go create mode 100644 devtools/internal/lib/yaegi.go create mode 100644 devtools/internal/terminal/terminal.go create mode 100644 devtools/internal/test/stdout_swapper.go create mode 100644 devtools/scripting.go create mode 100644 examples/scripting/main.go create mode 100644 examples/scripting/sprite-sheet.png create mode 100644 internal/sfmt/sfmt.go create mode 100644 internal/sfmt/sfmt_test.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e3b2791 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 87d1643..ce6761e 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Pi is under development. Only limited functionality is provided. API is not stab ## How to get started? 1. Install dependencies - * [Go 1.18+](https://go.dev/dl/) + * [Go 1.20+](https://go.dev/dl/) * If not on Windows, please install additional dependencies for [Linux](docs/install-linux.md) or [macOS](docs/install-macos.md). 2. Try examples from [examples](examples) directory. 3. Create a new game using provided [Github template](https://github.com/elgopher/pi-template). diff --git a/devtools/control.go b/devtools/control.go index d14f48a..0dc1329 100644 --- a/devtools/control.go +++ b/devtools/control.go @@ -4,6 +4,8 @@ package devtools import ( + "fmt" + "github.com/elgopher/pi" "github.com/elgopher/pi/devtools/internal/snapshot" ) @@ -13,7 +15,15 @@ var ( timeWhenPaused float64 ) +var helpShown bool + func pauseGame() { + fmt.Println("Game paused") + if !helpShown { + helpShown = true + fmt.Println("\nPress right mouse button in the game window to show the toolbar.") + fmt.Println("Press P in the game window to take screenshot.") + } gamePaused = true timeWhenPaused = pi.TimeSeconds snapshot.Take() @@ -23,4 +33,5 @@ func resumeGame() { gamePaused = false pi.TimeSeconds = timeWhenPaused snapshot.Draw() + fmt.Println("Game resumed") } diff --git a/devtools/devtools.go b/devtools/devtools.go index 7b2564f..a8a83df 100644 --- a/devtools/devtools.go +++ b/devtools/devtools.go @@ -8,6 +8,7 @@ import ( "github.com/elgopher/pi" "github.com/elgopher/pi/devtools/internal/inspector" + "github.com/elgopher/pi/devtools/internal/terminal" ) var ( @@ -33,7 +34,8 @@ func MustRun(runBackend func() error) { } inspector.BgColor, inspector.FgColor = BgColor, FgColor - fmt.Println("Press F12 to pause the game and show devtools.") + fmt.Println("Press F12 in the game window to pause the game and activate devtools inspector.") + fmt.Println("Terminal activated. Type help for help.") pi.Update = func() { updateDevTools() @@ -53,6 +55,17 @@ func MustRun(runBackend func() error) { } } + if err := interpreterInstance.SetUpdate(&update); err != nil { + panic(fmt.Sprintf("problem exporting Update function: %s", err)) + } + + if err := interpreterInstance.SetDraw(&draw); err != nil { + panic(fmt.Sprintf("problem exporting Draw function: %s", err)) + } + + terminal.StartReadingCommands() + defer terminal.StopReadingCommandsFromStdin() + if err := runBackend(); err != nil { panic(fmt.Sprintf("Something terrible happened! Pi cannot be run: %v\n", err)) } diff --git a/devtools/internal/help/help.go b/devtools/internal/help/help.go new file mode 100644 index 0000000..fed74f3 --- /dev/null +++ b/devtools/internal/help/help.go @@ -0,0 +1,124 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +package help + +import ( + "bufio" + "errors" + "fmt" + "os" + "os/exec" + "sort" + "strings" + + "github.com/elgopher/pi/devtools/internal/lib" +) + +var NotFound = fmt.Errorf("no help found") + +func PrintHelp(topic string) error { + switch topic { + case "": + fmt.Println("This is interactive terminal. " + + "You can write Go code here, which will run immediately. " + + "You can use all Pi packages: pi, key, state, snap, font, image and " + + "selection of standard packages: " + strings.Join(stdPackages(), ", ") + ". " + + "\n\n" + + "Type help topic for more information. For example: help pi or help pi.Spr" + + "\n\n" + + "Available commands: help [h], pause [p], resume [r], undo [u]", + ) + return nil + default: + return goDoc(topic) + } +} + +func stdPackages() []string { + var packages []string + for _, p := range lib.AllPackages() { + if p.IsStdPackage() { + packages = append(packages, p.Alias) + } + } + sort.Strings(packages) + return packages +} + +func goDoc(symbol string) error { + symbol = completeSymbol(symbol) + if symbolNotSupported(symbol) { + return NotFound + } + + fmt.Println("###############################################################################") + + var args []string + args = append(args, "doc") + if shouldShowDetailedDescriptionForSymbol(symbol) { + args = append(args, "-all") + } + args = append(args, symbol) + command := exec.Command("go", args...) + command.Stdout = bufio.NewWriter(os.Stdout) + + if err := command.Run(); err != nil { + var exitErr *exec.ExitError + if isExitErr := errors.As(err, &exitErr); isExitErr && exitErr.ExitCode() == 1 { + return NotFound + } + + return fmt.Errorf("problem getting help: %w", err) + } + + return nil +} + +func completeSymbol(symbol string) string { + packages := lib.AllPackages() + + for _, p := range packages { + if p.Alias == symbol { + return p.Path + } + } + + for _, p := range packages { + prefix := p.Alias + "." + if strings.HasPrefix(symbol, prefix) { + return p.Path + "." + symbol[len(prefix):] + } + } + + return symbol +} + +func symbolNotSupported(symbol string) bool { + packages := lib.AllPackages() + + for _, p := range packages { + prefix := p.Path + "." + if strings.HasPrefix(symbol, prefix) || symbol == p.Path { + return false + } + } + + return true +} + +var symbolsWithDetailedDescription = []string{ + "github.com/elgopher/pi.Button", + "github.com/elgopher/pi.MouseButton", + "github.com/elgopher/pi/key.Button", +} + +func shouldShowDetailedDescriptionForSymbol(symbol string) bool { + for _, s := range symbolsWithDetailedDescription { + if symbol == s { + return true + } + } + + return false +} diff --git a/devtools/internal/help/help_test.go b/devtools/internal/help/help_test.go new file mode 100644 index 0000000..3917770 --- /dev/null +++ b/devtools/internal/help/help_test.go @@ -0,0 +1,106 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +//go:build !js + +package help_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elgopher/pi/devtools/internal/help" + "github.com/elgopher/pi/devtools/internal/test" +) + +func TestPrintHelp(t *testing.T) { + t.Run("should return error when trying to print help for not imported packages", func(t *testing.T) { + topics := []string{ + "io", "io.Writer", + } + for _, topic := range topics { + t.Run(topic, func(t *testing.T) { + // when + err := help.PrintHelp(topic) + // then + assert.ErrorIs(t, err, help.NotFound) + }) + } + }) + + t.Run("should return error when trying to print help for non-existent symbol", func(t *testing.T) { + err := help.PrintHelp("pi.NonExistent") + assert.ErrorIs(t, err, help.NotFound) + }) + + t.Run("should print help for", func(t *testing.T) { + tests := map[string]struct { + topic string + expected string + }{ + "package": { + topic: "pi", + expected: `Package pi`, + }, + "function": { + topic: "pi.Spr", + expected: `func Spr(n, x, y int)`, + }, + "struct": { + topic: "pi.PixMap", + expected: `type PixMap struct {`, + }, + } + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + swapper := test.SwapStdout(t) + // when + err := help.PrintHelp(testCase.topic) + // then + swapper.BringStdoutBack() + assert.NoError(t, err) + output := swapper.ReadOutput(t) + assert.Contains(t, output, testCase.expected) + }) + } + }) + + t.Run("should show help for image.Image from github.com/elgopher/pi package, not from stdlib", func(t *testing.T) { + topics := []string{ + "image", "image.Image", + } + for _, topic := range topics { + t.Run(topic, func(t *testing.T) { + swapper := test.SwapStdout(t) + // when + err := help.PrintHelp("image.Image") + // then + swapper.BringStdoutBack() + assert.NoError(t, err) + output := swapper.ReadOutput(t) + assert.Contains(t, output, `// import "github.com/elgopher/pi/image"`) + }) + } + }) + + t.Run("should show detailed help for pi.Button", func(t *testing.T) { + tests := map[string]string{ + "pi.Button": "Keyboard mappings", + "pi.MouseButton": "MouseRight MouseButton = 2", + "key.Button": "func (b Button) String() string", + } + for topic, expected := range tests { + t.Run(topic, func(t *testing.T) { + swapper := test.SwapStdout(t) + // when + err := help.PrintHelp(topic) + // then + swapper.BringStdoutBack() + assert.NoError(t, err) + output := swapper.ReadOutput(t) + assert.Contains(t, output, expected) + }) + } + }) +} diff --git a/devtools/internal/inspector/measure.go b/devtools/internal/inspector/measure.go index 7820464..05db5ea 100644 --- a/devtools/internal/inspector/measure.go +++ b/devtools/internal/inspector/measure.go @@ -94,7 +94,7 @@ func (m *Measure) Update() { case pi.MouseBtnp(pi.MouseLeft) && !distance.measuring: distance.measuring = true distance.startX, distance.startY = x, y - fmt.Printf("Measuring started at (%d, %d)\n", x, y) + fmt.Printf("\nMeasuring started at (%d, %d)\n", x, y) case !pi.MouseBtn(pi.MouseLeft) && distance.measuring: distance.measuring = false dist, width, height := calcDistance() diff --git a/devtools/internal/inspector/update.go b/devtools/internal/inspector/update.go index f960cfb..5a6579f 100644 --- a/devtools/internal/inspector/update.go +++ b/devtools/internal/inspector/update.go @@ -4,7 +4,6 @@ package inspector import ( - "fmt" "math" "github.com/elgopher/pi" @@ -33,14 +32,7 @@ func calcDistance() (dist float64, width, height int) { return } -var helpShown bool - func Update() { - if !helpShown { - helpShown = true - fmt.Println("Press right mouse button to show toolbar.") - fmt.Println("Press P to take screenshot.") - } if !toolbar.visible { tool.Update() diff --git a/devtools/internal/interpreter/error.go b/devtools/internal/interpreter/error.go new file mode 100644 index 0000000..0c68027 --- /dev/null +++ b/devtools/internal/interpreter/error.go @@ -0,0 +1,12 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +package interpreter + +type ErrInvalidIdentifier struct { + message string +} + +func (e ErrInvalidIdentifier) Error() string { + return e.message +} diff --git a/devtools/internal/interpreter/interpreter.go b/devtools/internal/interpreter/interpreter.go new file mode 100644 index 0000000..712c98d --- /dev/null +++ b/devtools/internal/interpreter/interpreter.go @@ -0,0 +1,294 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +package interpreter + +import ( + "fmt" + "go/build" + "go/scanner" + "os" + "reflect" + "regexp" + "strings" + + "github.com/traefik/yaegi/interp" + + "github.com/elgopher/pi/devtools/internal/lib" +) + +type Instance struct { + yaegi *interp.Interpreter + alreadyImportedPackages map[string]struct{} + printHelp func(topic string) error +} + +func New(printHelp func(topic string) error) (Instance, error) { + yaegi := interp.New(interp.Options{ + GoPath: gopath(), // if GoPath is set then Yaegi does not complain about setting GOPATH. + }) + err := yaegi.Use(lib.Symbols) + if err != nil { + return Instance{}, fmt.Errorf("problem loading pi and stdlib symbols into Yaegi interpreter: %w", err) + } + + yaegi.ImportUsed() + + instance := Instance{ + yaegi: yaegi, + alreadyImportedPackages: map[string]struct{}{}, + printHelp: printHelp, + } + + err = ExportType[noResult](instance) + if err != nil { + return Instance{}, fmt.Errorf("problem exporting noResult type: %s", err) + } + + return instance, nil +} + +func gopath() string { + p := os.Getenv("GOPATH") + if p == "" { + p = build.Default.GOPATH + } + return p +} + +func (i Instance) SetUpdate(update *func()) error { + return i.usePointerToFunc("github.com/elgopher/pi/pi", "Update", update) +} + +func (i Instance) usePointerToFunc(packagePath string, variableName string, f *func()) error { + err := i.yaegi.Use(interp.Exports{ + packagePath: map[string]reflect.Value{ + variableName: reflect.ValueOf(f).Elem(), + }, + }) + + if err != nil { + return fmt.Errorf("problem loading %s symbol into Yaegi interpreter: %w", variableName, err) + } + + return nil +} + +func (i Instance) SetDraw(draw *func()) error { + return i.usePointerToFunc("github.com/elgopher/pi/pi", "Draw", draw) +} + +var goIdentifierRegex = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") + +func (i Instance) Export(name string, f any) error { + if !goIdentifierRegex.MatchString(name) { + return ErrInvalidIdentifier{ + message: fmt.Sprintf("'%s' is not a valid Go identifier", name), + } + } + value := reflect.ValueOf(f) + + if err := ensureNameDoesNotClashWithImportedPackages(name); err != nil { + return err + } + + if err := ensureNameDoesNotClashWithCommandNames(name); err != nil { + return err + } + + err := i.yaegi.Use(interp.Exports{ + "main/main": map[string]reflect.Value{ + name: value, + }, + }) + if err != nil { + return fmt.Errorf("problem loading %s symbol into Yaegi interpreter: %w", name, err) + } + + _, err = i.yaegi.Eval(`import . "main"`) + if err != nil { + return fmt.Errorf("problem re-importing main package: %w", err) + } + + return nil +} + +func ensureNameDoesNotClashWithImportedPackages(name string) error { + for _, pkg := range lib.AllPackages() { + if name == pkg.Alias { + return ErrInvalidIdentifier{ + message: fmt.Sprintf(`"%s" clashes with imported package name`, name), + } + } + } + + return nil +} + +var allCommandNames = []string{"h", "help", "p", "pause", "r", "resume", "u", "undo"} + +func ensureNameDoesNotClashWithCommandNames(name string) error { + for _, cmd := range allCommandNames { + if cmd == name { + return ErrInvalidIdentifier{ + message: fmt.Sprintf(`"%s" clashes with command name`, name), + } + } + } + + return nil +} + +func ExportType[T any](i Instance) error { + var nilValue *T + var t T + fullTypeName := fmt.Sprintf("%T", t) + + slice := strings.Split(fullTypeName, ".") + packageName := slice[0] + typeName := slice[1] + + if packageName == "main" { + if err := ensureNameDoesNotClashWithImportedPackages(typeName); err != nil { + return err + } + + if err := ensureNameDoesNotClashWithCommandNames(typeName); err != nil { + return err + } + } + + err := i.yaegi.Use(interp.Exports{ + packageName + "/" + packageName: map[string]reflect.Value{ + typeName: reflect.ValueOf(nilValue), + }, + }) + if err != nil { + return fmt.Errorf("problem loading %s symbol into Yaegi interpreter: %w", typeName, err) + } + + _, alreadyImported := i.alreadyImportedPackages[packageName] + if alreadyImported { + return nil + } + + _, err = i.yaegi.Eval(`import "` + packageName + `"`) + if err != nil { + return fmt.Errorf("problem re-importing main package: %w", err) + } + + i.alreadyImportedPackages[packageName] = struct{}{} + + return nil +} + +type EvalResult int + +const ( + HelpPrinted EvalResult = 0 + GoCodeExecuted EvalResult = 1 + Resumed EvalResult = 2 + Paused EvalResult = 3 + Undoed EvalResult = 4 + Continued EvalResult = 5 +) + +func (i Instance) Eval(cmd string) (EvalResult, error) { + trimmedCmd := strings.Trim(cmd, " ") + + if isHelpCommand(trimmedCmd) { + topic := strings.Trim(strings.TrimLeft(trimmedCmd, "help"), " ") + return HelpPrinted, i.printHelp(topic) + } else if trimmedCmd == "resume" || trimmedCmd == "r" { + return Resumed, nil + } else if trimmedCmd == "pause" || trimmedCmd == "p" { + return Paused, nil + } else if trimmedCmd == "undo" || trimmedCmd == "u" { + return Undoed, nil + } else { + return i.runGoCode(cmd) + } +} + +func isHelpCommand(trimmedCmd string) bool { + return strings.HasPrefix(trimmedCmd, "help ") || + strings.HasPrefix(trimmedCmd, "h ") || + trimmedCmd == "help" || + trimmedCmd == "h" +} + +func (i Instance) runGoCode(source string) (r EvalResult, e error) { + defer func() { + err := recover() + if err != nil { + r = GoCodeExecuted + e = fmt.Errorf("panic when running Yaegi: %s", err) + } + }() + + // Yaegi returns the last result computed by the interpreter which is unfortunate, because we don't know + // if source is a statement or an expression. If source is a statement and previously source was an expression + // then Yaegi returns the same result again. That's why following code overrides last computed result to the value + // used as a discriminator for no result. + _, err := i.yaegi.Eval("interpreter.noResult{}") + if err != nil { + return GoCodeExecuted, fmt.Errorf("yaegi Eval failed: %w", err) + } + + res, err := i.yaegi.Eval(source) + if err != nil { + if shouldContinueOnError(err, source) { + return Continued, nil + } + + return GoCodeExecuted, err + } + + printResult(res) + + return GoCodeExecuted, nil +} + +// shouldContinueOnError returns true if the error can be safely ignored +// to let the caller grab one more line before retrying to parse its input. +func shouldContinueOnError(err error, source string) bool { + errorsList, ok := err.(scanner.ErrorList) + if !ok || len(errorsList) < 1 { + return false + } + + e := errorsList[0] + + msg := e.Msg + if strings.HasSuffix(msg, "found 'EOF'") { + return true + } + + if msg == "raw string literal not terminated" { + return true + } + + if strings.HasPrefix(msg, "expected operand, found '}'") && !strings.HasSuffix(source, "}") { + return true + } + + return false +} + +func printResult(res reflect.Value) { + if res.IsValid() { + kind := res.Type().Kind() + + if kind == reflect.Struct { + _, noResultFound := res.Interface().(noResult) + if noResultFound { + return + } + } + + fmt.Printf("%+v: %+v\n", res.Type(), res) + } +} + +// noResult is a special struct which is used to check if code run by the interpreter returned something +type noResult struct{} diff --git a/devtools/internal/interpreter/interpreter_test.go b/devtools/internal/interpreter/interpreter_test.go new file mode 100644 index 0000000..42f947e --- /dev/null +++ b/devtools/internal/interpreter/interpreter_test.go @@ -0,0 +1,400 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +//go:build !js + +package interpreter_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elgopher/pi/devtools/internal/interpreter" + "github.com/elgopher/pi/devtools/internal/test" +) + +func TestEval(t *testing.T) { + t.Run("should evaluate command", func(t *testing.T) { + tests := map[string]struct { + code string + expectedOutput string + expectedResult interpreter.EvalResult + }{ + "simple expression": { + code: "1", + expectedOutput: "int: 1\n", + expectedResult: interpreter.GoCodeExecuted, + }, + "run statement": { + code: "pi.Spr(0,0,0)", + expectedResult: interpreter.GoCodeExecuted, + }, + "expression returning a struct": { + code: "struct{}{}", + expectedOutput: "struct {}: {}\n", + expectedResult: interpreter.GoCodeExecuted, + }, + "line with single curly bracket": { + code: "{", + expectedResult: interpreter.Continued, + }, + "two lines with curly bracket": { + code: "{\n{", + expectedResult: interpreter.Continued, + }, + "string literal not terminated": { + code: "` ", + expectedResult: interpreter.Continued, + }, + "undo": { + code: "undo", + expectedResult: interpreter.Undoed, + }, + "undo with space in the beginning": { + code: " undo", + expectedResult: interpreter.Undoed, + }, + "undo with space in the end": { + code: "undo ", + expectedResult: interpreter.Undoed, + }, + "pause": { + code: "pause", + expectedResult: interpreter.Paused, + }, + "pause with space in the beginning": { + code: " pause", + expectedResult: interpreter.Paused, + }, + "pause with space in the end": { + code: "pause ", + expectedResult: interpreter.Paused, + }, + "resume": { + code: "resume", + expectedResult: interpreter.Resumed, + }, + "resume with space in the beginning": { + code: " resume", + expectedResult: interpreter.Resumed, + }, + "resume with space in the end": { + code: "resume ", + expectedResult: interpreter.Resumed, + }, + } + for name, testCase := range tests { + t.Run(name, func(t *testing.T) { + swapper := test.SwapStdout(t) + // when + result, err := newInterpreterInstance(t).Eval(testCase.code) + // then + swapper.BringStdoutBack() + require.NoError(t, err) + assert.Equal(t, testCase.expectedResult, result) + assert.Equal(t, testCase.expectedOutput, swapper.ReadOutput(t)) + }) + } + }) + + t.Run("should return error on compilation error", func(t *testing.T) { + swapper := test.SwapStdout(t) + // when + result, err := newInterpreterInstance(t).Eval("1 === 1") + // then + swapper.BringStdoutBack() + assert.Error(t, err) + assert.Equal(t, interpreter.GoCodeExecuted, result) + assert.Equal(t, "", swapper.ReadOutput(t)) + }) + + t.Run("should return error when script panics", func(t *testing.T) { + swapper := test.SwapStdout(t) + // when + result, err := newInterpreterInstance(t).Eval(`panic("panic")`) + // then + swapper.BringStdoutBack() + assert.Error(t, err) + assert.Equal(t, interpreter.GoCodeExecuted, result) + assert.Equal(t, "", swapper.ReadOutput(t)) + }) + + t.Run("should return error when Yaegi panics", func(t *testing.T) { + swapper := test.SwapStdout(t) + // when + result, err := newInterpreterInstance(t).Eval(`fmt=1`) + // then + swapper.BringStdoutBack() + assert.Error(t, err) + assert.Equal(t, interpreter.GoCodeExecuted, result) + assert.Equal(t, "", swapper.ReadOutput(t)) + }) + + t.Run("script should read exported variable", func(t *testing.T) { + instance := newInterpreterInstance(t) + + err := instance.Export("variable", 10) + require.NoError(t, err) + + swapper := test.SwapStdout(t) + // when + _, err = instance.Eval("variable") + // then + swapper.BringStdoutBack() + require.NoError(t, err) + assert.Equal(t, "int: 10\n", swapper.ReadOutput(t)) + }) + + t.Run("script should update exported variable", func(t *testing.T) { + instance := newInterpreterInstance(t) + + var variable int + err := instance.Export("variable", &variable) + require.NoError(t, err) + + swapper := test.SwapStdout(t) + // when + _, err = instance.Eval("*variable=1") + // then + swapper.BringStdoutBack() + require.NoError(t, err) + assert.Equal(t, 1, variable) + }) + + t.Run("should run exported func", func(t *testing.T) { + instance := newInterpreterInstance(t) + + var functionExecuted bool + err := instance.Export("fun", func() { + functionExecuted = true + }) + require.NoError(t, err) + + swapper := test.SwapStdout(t) + // when + _, err = instance.Eval("fun()") + // then + swapper.BringStdoutBack() + require.NoError(t, err) + assert.True(t, functionExecuted) + }) + + t.Run("should update exported func", func(t *testing.T) { + instance := newInterpreterInstance(t) + + fun := func() int { return 0 } + + err := instance.Export("fun", &fun) + require.NoError(t, err) + + swapper := test.SwapStdout(t) + // when + _, err = instance.Eval(`*fun = func() int { return 1 }`) + // then + swapper.BringStdoutBack() + require.NoError(t, err) + assert.Equal(t, 1, fun()) + }) + + t.Run("should use exported type", func(t *testing.T) { + instance := newInterpreterInstance(t) + + type customType struct{} + err := interpreter.ExportType[customType](instance) + require.NoError(t, err) + + swapper := test.SwapStdout(t) + // when + _, err = instance.Eval(`interpreter_test.customType{}`) + // then + swapper.BringStdoutBack() + require.NoError(t, err) + assert.Equal(t, "interpreter_test.customType: {}\n", swapper.ReadOutput(t)) + }) + + t.Run("should use another exported type from the same package", func(t *testing.T) { + instance := newInterpreterInstance(t) + + type anotherType struct{} + type customType struct{} + err := interpreter.ExportType[anotherType](instance) + require.NoError(t, err) + err = interpreter.ExportType[customType](instance) + require.NoError(t, err) + + swapper := test.SwapStdout(t) + // when + _, err = instance.Eval(`interpreter_test.anotherType{}`) + // then + swapper.BringStdoutBack() + require.NoError(t, err) + assert.Equal(t, "interpreter_test.anotherType: {}\n", swapper.ReadOutput(t)) + }) +} + +func TestExport(t *testing.T) { + t.Run("should return error when name is not a Go identifier", func(t *testing.T) { + invalidIdentifiers := []string{ + "", "1", "1a", "a-", + } + for _, invalidIdentifier := range invalidIdentifiers { + t.Run(invalidIdentifier, func(t *testing.T) { + // when + err := newInterpreterInstance(t).Export(invalidIdentifier, "v") + // then + target := &interpreter.ErrInvalidIdentifier{} + assert.ErrorAs(t, err, target) + }) + } + }) + + t.Run("shout not return error when name is a Go identifier", func(t *testing.T) { + validIdentifiers := []string{ + "a", "A", "ab", "AB", "a1", "abc", "a_", + } + for _, validIdentifier := range validIdentifiers { + t.Run(validIdentifier, func(t *testing.T) { + // when + err := newInterpreterInstance(t).Export(validIdentifier, "v") + // then + assert.NoError(t, err) + }) + } + }) + + t.Run("should return error when name clashes with imported package name", func(t *testing.T) { + err := newInterpreterInstance(t).Export("fmt", "v") + target := &interpreter.ErrInvalidIdentifier{} + assert.ErrorAs(t, err, target) + fmt.Println(target.Error()) + }) + + t.Run("should return error when name clashes with command name", func(t *testing.T) { + var allCommandNames = []string{"h", "help", "p", "pause", "r", "resume", "u", "undo"} + + for _, cmd := range allCommandNames { + t.Run(cmd, func(t *testing.T) { + // when + err := newInterpreterInstance(t).Export(cmd, "v") + // then + target := &interpreter.ErrInvalidIdentifier{} + assert.ErrorAs(t, err, target) + }) + } + }) +} + +func TestInstance_SetUpdate(t *testing.T) { + t.Run("script should run pi.Update function", func(t *testing.T) { + instance := newInterpreterInstance(t) + + var updateExecuted bool + update := func() { + updateExecuted = true + } + // when + err := instance.SetUpdate(&update) + // then + require.NoError(t, err) + _, err = instance.Eval("pi.Update()") + require.NoError(t, err) + assert.True(t, updateExecuted) + }) + + t.Run("script should replace pi.Update function", func(t *testing.T) { + instance := newInterpreterInstance(t) + + var updateExecuted bool + update := func() { + updateExecuted = true + } + // when + err := instance.SetUpdate(&update) + // then + require.NoError(t, err) + _, err = instance.Eval("pi.Update = func() { }") + require.NoError(t, err) + assert.False(t, updateExecuted) + }) +} + +func TestInstance_SetDraw(t *testing.T) { + t.Run("script should run pi.Draw function", func(t *testing.T) { + instance := newInterpreterInstance(t) + + var drawExecuted bool + draw := func() { + drawExecuted = true + } + // when + err := instance.SetDraw(&draw) + // then + require.NoError(t, err) + _, err = instance.Eval("pi.Draw()") + require.NoError(t, err) + assert.True(t, drawExecuted) + }) + + t.Run("script should replace pi.Draw function", func(t *testing.T) { + instance := newInterpreterInstance(t) + + var drawExecuted bool + draw := func() { + drawExecuted = true + } + // when + err := instance.SetDraw(&draw) + // then + require.NoError(t, err) + _, err = instance.Eval("pi.Draw = func() { }") + require.NoError(t, err) + assert.False(t, drawExecuted) + }) + + t.Run("should print help", func(t *testing.T) { + tests := map[string]string{ + "h": "", + " h": "", + "h ": "", + "help": "", + " help": "", + "help ": "", + "help pi": "pi", + "help pi ": "pi", + "help pi ": "pi", + "help pi.Spr": "pi.Spr", + "help pi.Spr ": "pi.Spr", + "help pi.Spr ": "pi.Spr", + "h pi": "pi", + "h pi.Spr": "pi.Spr", + } + for cmd, expectedTopic := range tests { + var actualTopic string + printHelp := func(topic string) error { + actualTopic = topic + return nil + } + + instance, err := interpreter.New(printHelp) + require.NoError(t, err) + // when + result, err := instance.Eval(cmd) + // then + require.NoError(t, err) + assert.Equal(t, interpreter.HelpPrinted, result) + assert.Equal(t, expectedTopic, actualTopic) + } + }) +} + +func newInterpreterInstance(t *testing.T) interpreter.Instance { + instance, err := interpreter.New(noopPrintHelp) + require.NoError(t, err) + + return instance +} + +func noopPrintHelp(topic string) error { return nil } diff --git a/devtools/internal/lib/github_com-elgopher-pi-font.go b/devtools/internal/lib/github_com-elgopher-pi-font.go new file mode 100644 index 0000000..55b71ec --- /dev/null +++ b/devtools/internal/lib/github_com-elgopher-pi-font.go @@ -0,0 +1,16 @@ +// Code generated by 'yaegi extract github.com/elgopher/pi/font'. DO NOT EDIT. + +package lib + +import ( + "reflect" + + "github.com/elgopher/pi/font" +) + +func init() { + Symbols["github.com/elgopher/pi/font/font"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Load": reflect.ValueOf(font.Load), + } +} diff --git a/devtools/internal/lib/github_com-elgopher-pi-image.go b/devtools/internal/lib/github_com-elgopher-pi-image.go new file mode 100644 index 0000000..921021e --- /dev/null +++ b/devtools/internal/lib/github_com-elgopher-pi-image.go @@ -0,0 +1,20 @@ +// Code generated by 'yaegi extract github.com/elgopher/pi/image'. DO NOT EDIT. + +package lib + +import ( + "reflect" + + "github.com/elgopher/pi/image" +) + +func init() { + Symbols["github.com/elgopher/pi/image/image"] = map[string]reflect.Value{ + // function, constant and variable definitions + "DecodePNG": reflect.ValueOf(image.DecodePNG), + + // type definitions + "Image": reflect.ValueOf((*image.Image)(nil)), + "RGB": reflect.ValueOf((*image.RGB)(nil)), + } +} diff --git a/devtools/internal/lib/github_com-elgopher-pi-key.go b/devtools/internal/lib/github_com-elgopher-pi-key.go new file mode 100644 index 0000000..bbba15d --- /dev/null +++ b/devtools/internal/lib/github_com-elgopher-pi-key.go @@ -0,0 +1,93 @@ +// Code generated by 'yaegi extract github.com/elgopher/pi/key'. DO NOT EDIT. + +package lib + +import ( + "reflect" + + "github.com/elgopher/pi/key" +) + +func init() { + Symbols["github.com/elgopher/pi/key/key"] = map[string]reflect.Value{ + // function, constant and variable definitions + "A": reflect.ValueOf(key.A), + "Alt": reflect.ValueOf(key.Alt), + "Apostrophe": reflect.ValueOf(key.Apostrophe), + "B": reflect.ValueOf(key.B), + "Back": reflect.ValueOf(key.Back), + "Backquote": reflect.ValueOf(key.Backquote), + "Backslash": reflect.ValueOf(key.Backslash), + "BracketLeft": reflect.ValueOf(key.BracketLeft), + "BracketRight": reflect.ValueOf(key.BracketRight), + "Btn": reflect.ValueOf(key.Btn), + "Btnp": reflect.ValueOf(key.Btnp), + "C": reflect.ValueOf(key.C), + "Cap": reflect.ValueOf(key.Cap), + "Comma": reflect.ValueOf(key.Comma), + "Ctrl": reflect.ValueOf(key.Ctrl), + "D": reflect.ValueOf(key.D), + "Digit0": reflect.ValueOf(key.Digit0), + "Digit1": reflect.ValueOf(key.Digit1), + "Digit2": reflect.ValueOf(key.Digit2), + "Digit3": reflect.ValueOf(key.Digit3), + "Digit4": reflect.ValueOf(key.Digit4), + "Digit5": reflect.ValueOf(key.Digit5), + "Digit6": reflect.ValueOf(key.Digit6), + "Digit7": reflect.ValueOf(key.Digit7), + "Digit8": reflect.ValueOf(key.Digit8), + "Digit9": reflect.ValueOf(key.Digit9), + "Down": reflect.ValueOf(key.Down), + "Duration": reflect.ValueOf(&key.Duration).Elem(), + "E": reflect.ValueOf(key.E), + "Enter": reflect.ValueOf(key.Enter), + "Equal": reflect.ValueOf(key.Equal), + "Esc": reflect.ValueOf(key.Esc), + "F": reflect.ValueOf(key.F), + "F1": reflect.ValueOf(key.F1), + "F10": reflect.ValueOf(key.F10), + "F11": reflect.ValueOf(key.F11), + "F12": reflect.ValueOf(key.F12), + "F2": reflect.ValueOf(key.F2), + "F3": reflect.ValueOf(key.F3), + "F4": reflect.ValueOf(key.F4), + "F5": reflect.ValueOf(key.F5), + "F6": reflect.ValueOf(key.F6), + "F7": reflect.ValueOf(key.F7), + "F8": reflect.ValueOf(key.F8), + "F9": reflect.ValueOf(key.F9), + "G": reflect.ValueOf(key.G), + "H": reflect.ValueOf(key.H), + "I": reflect.ValueOf(key.I), + "J": reflect.ValueOf(key.J), + "K": reflect.ValueOf(key.K), + "L": reflect.ValueOf(key.L), + "Left": reflect.ValueOf(key.Left), + "M": reflect.ValueOf(key.M), + "Minus": reflect.ValueOf(key.Minus), + "N": reflect.ValueOf(key.N), + "O": reflect.ValueOf(key.O), + "P": reflect.ValueOf(key.P), + "Period": reflect.ValueOf(key.Period), + "Q": reflect.ValueOf(key.Q), + "R": reflect.ValueOf(key.R), + "Right": reflect.ValueOf(key.Right), + "S": reflect.ValueOf(key.S), + "Semicolon": reflect.ValueOf(key.Semicolon), + "Shift": reflect.ValueOf(key.Shift), + "Slash": reflect.ValueOf(key.Slash), + "Space": reflect.ValueOf(key.Space), + "T": reflect.ValueOf(key.T), + "Tab": reflect.ValueOf(key.Tab), + "U": reflect.ValueOf(key.U), + "Up": reflect.ValueOf(key.Up), + "V": reflect.ValueOf(key.V), + "W": reflect.ValueOf(key.W), + "X": reflect.ValueOf(key.X), + "Y": reflect.ValueOf(key.Y), + "Z": reflect.ValueOf(key.Z), + + // type definitions + "Button": reflect.ValueOf((*key.Button)(nil)), + } +} diff --git a/devtools/internal/lib/github_com-elgopher-pi-snap.go b/devtools/internal/lib/github_com-elgopher-pi-snap.go new file mode 100644 index 0000000..cf18b42 --- /dev/null +++ b/devtools/internal/lib/github_com-elgopher-pi-snap.go @@ -0,0 +1,16 @@ +// Code generated by 'yaegi extract github.com/elgopher/pi/snap'. DO NOT EDIT. + +package lib + +import ( + "reflect" + + "github.com/elgopher/pi/snap" +) + +func init() { + Symbols["github.com/elgopher/pi/snap/snap"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Take": reflect.ValueOf(snap.Take), + } +} diff --git a/devtools/internal/lib/github_com-elgopher-pi-state.go b/devtools/internal/lib/github_com-elgopher-pi-state.go new file mode 100644 index 0000000..6a41be7 --- /dev/null +++ b/devtools/internal/lib/github_com-elgopher-pi-state.go @@ -0,0 +1,28 @@ +// Code generated by 'yaegi extract github.com/elgopher/pi/state'. DO NOT EDIT. + +package lib + +import ( + "reflect" + + "github.com/elgopher/pi/state" +) + +func init() { + Symbols["github.com/elgopher/pi/state/state"] = map[string]reflect.Value{ + // function, constant and variable definitions + "All": reflect.ValueOf(state.All), + "Delete": reflect.ValueOf(state.Delete), + "ErrInvalidStateName": reflect.ValueOf(&state.ErrInvalidStateName).Elem(), + "ErrNilStateOutput": reflect.ValueOf(&state.ErrNilStateOutput).Elem(), + "ErrNotFound": reflect.ValueOf(&state.ErrNotFound).Elem(), + "ErrStateMarshalFailed": reflect.ValueOf(&state.ErrStateMarshalFailed).Elem(), + "ErrStateUnmarshalFailed": reflect.ValueOf(&state.ErrStateUnmarshalFailed).Elem(), + "Load": reflect.ValueOf(stateLoad), + "Save": reflect.ValueOf(state.Save), + } +} + +func stateLoad(name string, out any) { + state.Load(name, &out) +} diff --git a/devtools/internal/lib/github_com-elgopher-pi.go b/devtools/internal/lib/github_com-elgopher-pi.go new file mode 100644 index 0000000..507fad4 --- /dev/null +++ b/devtools/internal/lib/github_com-elgopher-pi.go @@ -0,0 +1,116 @@ +// Code generated by 'yaegi extract github.com/elgopher/pi'. DO NOT EDIT. + +package lib + +import ( + "go/constant" + "go/token" + "reflect" + + "github.com/elgopher/pi" +) + +func init() { + Symbols["github.com/elgopher/pi/pi"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Atan2": reflect.ValueOf(pi.Atan2), + "Audio": reflect.ValueOf(pi.Audio), + "Btn": reflect.ValueOf(pi.Btn), + "BtnBits": reflect.ValueOf(pi.BtnBits), + "BtnPlayer": reflect.ValueOf(pi.BtnPlayer), + "Btnp": reflect.ValueOf(pi.Btnp), + "BtnpBits": reflect.ValueOf(pi.BtnpBits), + "BtnpPlayer": reflect.ValueOf(pi.BtnpPlayer), + "Camera": reflect.ValueOf(pi.Camera), + "CameraReset": reflect.ValueOf(pi.CameraReset), + "Circ": reflect.ValueOf(pi.Circ), + "CircFill": reflect.ValueOf(pi.CircFill), + "Clip": reflect.ValueOf(pi.Clip), + "ClipReset": reflect.ValueOf(pi.ClipReset), + "Cls": reflect.ValueOf(pi.Cls), + "ClsCol": reflect.ValueOf(pi.ClsCol), + "ColorTransparency": reflect.ValueOf(&pi.ColorTransparency).Elem(), + "Controllers": reflect.ValueOf(&pi.Controllers).Elem(), + "Cos": reflect.ValueOf(pi.Cos), + "CustomFont": reflect.ValueOf(pi.CustomFont), + "DisplayPalette": reflect.ValueOf(&pi.DisplayPalette).Elem(), + "Down": reflect.ValueOf(pi.Down), + "Draw": reflect.ValueOf(&pi.Draw).Elem(), + "DrawPalette": reflect.ValueOf(&pi.DrawPalette).Elem(), + "GameLoopStopped": reflect.ValueOf(&pi.GameLoopStopped).Elem(), + "Left": reflect.ValueOf(pi.Left), + "Line": reflect.ValueOf(pi.Line), + "Load": reflect.ValueOf(pi.Load), + "MaxInt": reflect.ValueOf(pi.MaxInt[int]), // TODO Generic functions not supported by Yaegi yet + "Mid": reflect.ValueOf(pi.Mid), + "MidInt": reflect.ValueOf(pi.MidInt[int]), // TODO Generic functions not supported by Yaegi yet + "MinInt": reflect.ValueOf(pi.MinInt[int]), // TODO Generic functions not supported by Yaegi yet + "MouseBtn": reflect.ValueOf(pi.MouseBtn), + "MouseBtnDuration": reflect.ValueOf(&pi.MouseBtnDuration).Elem(), + "MouseBtnp": reflect.ValueOf(pi.MouseBtnp), + "MouseLeft": reflect.ValueOf(pi.MouseLeft), + "MouseMiddle": reflect.ValueOf(pi.MouseMiddle), + "MousePos": reflect.ValueOf(pi.MousePos), + "MousePosition": reflect.ValueOf(&pi.MousePosition).Elem(), + "MouseRight": reflect.ValueOf(pi.MouseRight), + "NewPixMap": reflect.ValueOf(pi.NewPixMap), + "NewPixMapWithPixels": reflect.ValueOf(pi.NewPixMapWithPixels), + "O": reflect.ValueOf(pi.O), + "Pal": reflect.ValueOf(pi.Pal), + "PalDisplay": reflect.ValueOf(pi.PalDisplay), + "PalReset": reflect.ValueOf(pi.PalReset), + "Palette": reflect.ValueOf(&pi.Palette).Elem(), + "Palt": reflect.ValueOf(pi.Palt), + "PaltReset": reflect.ValueOf(pi.PaltReset), + "Pget": reflect.ValueOf(pi.Pget), + "Print": reflect.ValueOf(pi.Print), + "Pset": reflect.ValueOf(pi.Pset), + "Rect": reflect.ValueOf(pi.Rect), + "RectFill": reflect.ValueOf(pi.RectFill), + "Reset": reflect.ValueOf(pi.Reset), + "Right": reflect.ValueOf(pi.Right), + "Scr": reflect.ValueOf(pi.Scr), + "ScreenCamera": reflect.ValueOf(&pi.ScreenCamera).Elem(), + "SetCustomFontHeight": reflect.ValueOf(pi.SetCustomFontHeight), + "SetCustomFontSpecialWidth": reflect.ValueOf(pi.SetCustomFontSpecialWidth), + "SetCustomFontWidth": reflect.ValueOf(pi.SetCustomFontWidth), + "SetScreenSize": reflect.ValueOf(pi.SetScreenSize), + "Sget": reflect.ValueOf(pi.Sget), + "Sin": reflect.ValueOf(pi.Sin), + "Spr": reflect.ValueOf(pi.Spr), + "SprSheet": reflect.ValueOf(pi.SprSheet), + "SprSize": reflect.ValueOf(pi.SprSize), + "SprSizeFlip": reflect.ValueOf(pi.SprSizeFlip), + "SpriteHeight": reflect.ValueOf(constant.MakeFromLiteral("8", token.INT, 0)), + "SpriteWidth": reflect.ValueOf(constant.MakeFromLiteral("8", token.INT, 0)), + "Sset": reflect.ValueOf(pi.Sset), + "Stop": reflect.ValueOf(pi.Stop), + "SystemFont": reflect.ValueOf(pi.SystemFont), + "Time": reflect.ValueOf(pi.Time), + "TimeSeconds": reflect.ValueOf(&pi.TimeSeconds).Elem(), + "Up": reflect.ValueOf(pi.Up), + "Update": reflect.ValueOf(&pi.Update).Elem(), + "UseEmptySpriteSheet": reflect.ValueOf(pi.UseEmptySpriteSheet), + "X": reflect.ValueOf(pi.X), + + // type definitions + "AudioSystem": reflect.ValueOf((*pi.AudioSystem)(nil)), + "Button": reflect.ValueOf((*pi.Button)(nil)), + "Controller": reflect.ValueOf((*pi.Controller)(nil)), + "Font": reflect.ValueOf((*pi.Font)(nil)), + //"Int": reflect.ValueOf((*pi.Int)(nil)), // TODO Generic constraints not supported by Yaegi yet + "MouseButton": reflect.ValueOf((*pi.MouseButton)(nil)), + "PixMap": reflect.ValueOf((*pi.PixMap)(nil)), + "Pointer": reflect.ValueOf((*pi.Pointer)(nil)), + "Position": reflect.ValueOf((*pi.Position)(nil)), + "Region": reflect.ValueOf((*pi.Region)(nil)), + + // interface wrapper definitions + "_Int": reflect.ValueOf((*_github_com_elgopher_pi_Int)(nil)), + } +} + +// _github_com_elgopher_pi_Int is an interface wrapper for Int type +type _github_com_elgopher_pi_Int struct { + IValue interface{} +} diff --git a/devtools/internal/lib/go1_20_bytes.go b/devtools/internal/lib/go1_20_bytes.go new file mode 100644 index 0000000..ef50481 --- /dev/null +++ b/devtools/internal/lib/go1_20_bytes.go @@ -0,0 +1,76 @@ +// Code generated by 'yaegi extract bytes'. DO NOT EDIT. + +package lib + +import ( + "bytes" + "go/constant" + "go/token" + "reflect" +) + +func init() { + Symbols["bytes/bytes"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Clone": reflect.ValueOf(bytes.Clone), + "Compare": reflect.ValueOf(bytes.Compare), + "Contains": reflect.ValueOf(bytes.Contains), + "ContainsAny": reflect.ValueOf(bytes.ContainsAny), + "ContainsRune": reflect.ValueOf(bytes.ContainsRune), + "Count": reflect.ValueOf(bytes.Count), + "Cut": reflect.ValueOf(bytes.Cut), + "CutPrefix": reflect.ValueOf(bytes.CutPrefix), + "CutSuffix": reflect.ValueOf(bytes.CutSuffix), + "Equal": reflect.ValueOf(bytes.Equal), + "EqualFold": reflect.ValueOf(bytes.EqualFold), + "ErrTooLarge": reflect.ValueOf(&bytes.ErrTooLarge).Elem(), + "Fields": reflect.ValueOf(bytes.Fields), + "FieldsFunc": reflect.ValueOf(bytes.FieldsFunc), + "HasPrefix": reflect.ValueOf(bytes.HasPrefix), + "HasSuffix": reflect.ValueOf(bytes.HasSuffix), + "Index": reflect.ValueOf(bytes.Index), + "IndexAny": reflect.ValueOf(bytes.IndexAny), + "IndexByte": reflect.ValueOf(bytes.IndexByte), + "IndexFunc": reflect.ValueOf(bytes.IndexFunc), + "IndexRune": reflect.ValueOf(bytes.IndexRune), + "Join": reflect.ValueOf(bytes.Join), + "LastIndex": reflect.ValueOf(bytes.LastIndex), + "LastIndexAny": reflect.ValueOf(bytes.LastIndexAny), + "LastIndexByte": reflect.ValueOf(bytes.LastIndexByte), + "LastIndexFunc": reflect.ValueOf(bytes.LastIndexFunc), + "Map": reflect.ValueOf(bytes.Map), + "MinRead": reflect.ValueOf(constant.MakeFromLiteral("512", token.INT, 0)), + "NewBuffer": reflect.ValueOf(bytes.NewBuffer), + "NewBufferString": reflect.ValueOf(bytes.NewBufferString), + "NewReader": reflect.ValueOf(bytes.NewReader), + "Repeat": reflect.ValueOf(bytes.Repeat), + "Replace": reflect.ValueOf(bytes.Replace), + "ReplaceAll": reflect.ValueOf(bytes.ReplaceAll), + "Runes": reflect.ValueOf(bytes.Runes), + "Split": reflect.ValueOf(bytes.Split), + "SplitAfter": reflect.ValueOf(bytes.SplitAfter), + "SplitAfterN": reflect.ValueOf(bytes.SplitAfterN), + "SplitN": reflect.ValueOf(bytes.SplitN), + "Title": reflect.ValueOf(bytes.Title), + "ToLower": reflect.ValueOf(bytes.ToLower), + "ToLowerSpecial": reflect.ValueOf(bytes.ToLowerSpecial), + "ToTitle": reflect.ValueOf(bytes.ToTitle), + "ToTitleSpecial": reflect.ValueOf(bytes.ToTitleSpecial), + "ToUpper": reflect.ValueOf(bytes.ToUpper), + "ToUpperSpecial": reflect.ValueOf(bytes.ToUpperSpecial), + "ToValidUTF8": reflect.ValueOf(bytes.ToValidUTF8), + "Trim": reflect.ValueOf(bytes.Trim), + "TrimFunc": reflect.ValueOf(bytes.TrimFunc), + "TrimLeft": reflect.ValueOf(bytes.TrimLeft), + "TrimLeftFunc": reflect.ValueOf(bytes.TrimLeftFunc), + "TrimPrefix": reflect.ValueOf(bytes.TrimPrefix), + "TrimRight": reflect.ValueOf(bytes.TrimRight), + "TrimRightFunc": reflect.ValueOf(bytes.TrimRightFunc), + "TrimSpace": reflect.ValueOf(bytes.TrimSpace), + "TrimSuffix": reflect.ValueOf(bytes.TrimSuffix), + + // type definitions + "Buffer": reflect.ValueOf((*bytes.Buffer)(nil)), + "Reader": reflect.ValueOf((*bytes.Reader)(nil)), + } +} diff --git a/devtools/internal/lib/go1_20_fmt.go b/devtools/internal/lib/go1_20_fmt.go new file mode 100644 index 0000000..323a308 --- /dev/null +++ b/devtools/internal/lib/go1_20_fmt.go @@ -0,0 +1,148 @@ +// Code generated by 'yaegi extract fmt'. DO NOT EDIT. + +package lib + +import ( + "fmt" + "reflect" +) + +func init() { + Symbols["fmt/fmt"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Append": reflect.ValueOf(fmt.Append), + "Appendf": reflect.ValueOf(fmt.Appendf), + "Appendln": reflect.ValueOf(fmt.Appendln), + "Errorf": reflect.ValueOf(fmt.Errorf), + "FormatString": reflect.ValueOf(fmt.FormatString), + "Fprint": reflect.ValueOf(fmt.Fprint), + "Fprintf": reflect.ValueOf(fmt.Fprintf), + "Fprintln": reflect.ValueOf(fmt.Fprintln), + "Fscan": reflect.ValueOf(fmt.Fscan), + "Fscanf": reflect.ValueOf(fmt.Fscanf), + "Fscanln": reflect.ValueOf(fmt.Fscanln), + "Print": reflect.ValueOf(fmt.Print), + "Printf": reflect.ValueOf(fmt.Printf), + "Println": reflect.ValueOf(fmt.Println), + "Scan": reflect.ValueOf(fmt.Scan), + "Scanf": reflect.ValueOf(fmt.Scanf), + "Scanln": reflect.ValueOf(fmt.Scanln), + "Sprint": reflect.ValueOf(fmt.Sprint), + "Sprintf": reflect.ValueOf(fmt.Sprintf), + "Sprintln": reflect.ValueOf(fmt.Sprintln), + "Sscan": reflect.ValueOf(fmt.Sscan), + "Sscanf": reflect.ValueOf(fmt.Sscanf), + "Sscanln": reflect.ValueOf(fmt.Sscanln), + + // type definitions + "Formatter": reflect.ValueOf((*fmt.Formatter)(nil)), + "GoStringer": reflect.ValueOf((*fmt.GoStringer)(nil)), + "ScanState": reflect.ValueOf((*fmt.ScanState)(nil)), + "Scanner": reflect.ValueOf((*fmt.Scanner)(nil)), + "State": reflect.ValueOf((*fmt.State)(nil)), + "Stringer": reflect.ValueOf((*fmt.Stringer)(nil)), + + // interface wrapper definitions + "_Formatter": reflect.ValueOf((*_fmt_Formatter)(nil)), + "_GoStringer": reflect.ValueOf((*_fmt_GoStringer)(nil)), + "_ScanState": reflect.ValueOf((*_fmt_ScanState)(nil)), + "_Scanner": reflect.ValueOf((*_fmt_Scanner)(nil)), + "_State": reflect.ValueOf((*_fmt_State)(nil)), + "_Stringer": reflect.ValueOf((*_fmt_Stringer)(nil)), + } +} + +// _fmt_Formatter is an interface wrapper for Formatter type +type _fmt_Formatter struct { + IValue interface{} + WFormat func(f fmt.State, verb rune) +} + +func (W _fmt_Formatter) Format(f fmt.State, verb rune) { + W.WFormat(f, verb) +} + +// _fmt_GoStringer is an interface wrapper for GoStringer type +type _fmt_GoStringer struct { + IValue interface{} + WGoString func() string +} + +func (W _fmt_GoStringer) GoString() string { + return W.WGoString() +} + +// _fmt_ScanState is an interface wrapper for ScanState type +type _fmt_ScanState struct { + IValue interface{} + WRead func(buf []byte) (n int, err error) + WReadRune func() (r rune, size int, err error) + WSkipSpace func() + WToken func(skipSpace bool, f func(rune) bool) (token []byte, err error) + WUnreadRune func() error + WWidth func() (wid int, ok bool) +} + +func (W _fmt_ScanState) Read(buf []byte) (n int, err error) { + return W.WRead(buf) +} +func (W _fmt_ScanState) ReadRune() (r rune, size int, err error) { + return W.WReadRune() +} +func (W _fmt_ScanState) SkipSpace() { + W.WSkipSpace() +} +func (W _fmt_ScanState) Token(skipSpace bool, f func(rune) bool) (token []byte, err error) { + return W.WToken(skipSpace, f) +} +func (W _fmt_ScanState) UnreadRune() error { + return W.WUnreadRune() +} +func (W _fmt_ScanState) Width() (wid int, ok bool) { + return W.WWidth() +} + +// _fmt_Scanner is an interface wrapper for Scanner type +type _fmt_Scanner struct { + IValue interface{} + WScan func(state fmt.ScanState, verb rune) error +} + +func (W _fmt_Scanner) Scan(state fmt.ScanState, verb rune) error { + return W.WScan(state, verb) +} + +// _fmt_State is an interface wrapper for State type +type _fmt_State struct { + IValue interface{} + WFlag func(c int) bool + WPrecision func() (prec int, ok bool) + WWidth func() (wid int, ok bool) + WWrite func(b []byte) (n int, err error) +} + +func (W _fmt_State) Flag(c int) bool { + return W.WFlag(c) +} +func (W _fmt_State) Precision() (prec int, ok bool) { + return W.WPrecision() +} +func (W _fmt_State) Width() (wid int, ok bool) { + return W.WWidth() +} +func (W _fmt_State) Write(b []byte) (n int, err error) { + return W.WWrite(b) +} + +// _fmt_Stringer is an interface wrapper for Stringer type +type _fmt_Stringer struct { + IValue interface{} + WString func() string +} + +func (W _fmt_Stringer) String() string { + if W.WString == nil { + return "" + } + return W.WString() +} diff --git a/devtools/internal/lib/go1_20_math.go b/devtools/internal/lib/go1_20_math.go new file mode 100644 index 0000000..5e55e6d --- /dev/null +++ b/devtools/internal/lib/go1_20_math.go @@ -0,0 +1,113 @@ +// Code generated by 'yaegi extract math'. DO NOT EDIT. + +package lib + +import ( + "go/constant" + "go/token" + "math" + "reflect" +) + +func init() { + Symbols["math/math"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Abs": reflect.ValueOf(math.Abs), + "Acos": reflect.ValueOf(math.Acos), + "Acosh": reflect.ValueOf(math.Acosh), + "Asin": reflect.ValueOf(math.Asin), + "Asinh": reflect.ValueOf(math.Asinh), + "Atan": reflect.ValueOf(math.Atan), + "Atan2": reflect.ValueOf(math.Atan2), + "Atanh": reflect.ValueOf(math.Atanh), + "Cbrt": reflect.ValueOf(math.Cbrt), + "Ceil": reflect.ValueOf(math.Ceil), + "Copysign": reflect.ValueOf(math.Copysign), + "Cos": reflect.ValueOf(math.Cos), + "Cosh": reflect.ValueOf(math.Cosh), + "Dim": reflect.ValueOf(math.Dim), + "E": reflect.ValueOf(constant.MakeFromLiteral("2.71828182845904523536028747135266249775724709369995957496696762566337824315673231520670375558666729784504486779277967997696994772644702281675346915668215131895555530285035761295375777990557253360748291015625", token.FLOAT, 0)), + "Erf": reflect.ValueOf(math.Erf), + "Erfc": reflect.ValueOf(math.Erfc), + "Erfcinv": reflect.ValueOf(math.Erfcinv), + "Erfinv": reflect.ValueOf(math.Erfinv), + "Exp": reflect.ValueOf(math.Exp), + "Exp2": reflect.ValueOf(math.Exp2), + "Expm1": reflect.ValueOf(math.Expm1), + "FMA": reflect.ValueOf(math.FMA), + "Float32bits": reflect.ValueOf(math.Float32bits), + "Float32frombits": reflect.ValueOf(math.Float32frombits), + "Float64bits": reflect.ValueOf(math.Float64bits), + "Float64frombits": reflect.ValueOf(math.Float64frombits), + "Floor": reflect.ValueOf(math.Floor), + "Frexp": reflect.ValueOf(math.Frexp), + "Gamma": reflect.ValueOf(math.Gamma), + "Hypot": reflect.ValueOf(math.Hypot), + "Ilogb": reflect.ValueOf(math.Ilogb), + "Inf": reflect.ValueOf(math.Inf), + "IsInf": reflect.ValueOf(math.IsInf), + "IsNaN": reflect.ValueOf(math.IsNaN), + "J0": reflect.ValueOf(math.J0), + "J1": reflect.ValueOf(math.J1), + "Jn": reflect.ValueOf(math.Jn), + "Ldexp": reflect.ValueOf(math.Ldexp), + "Lgamma": reflect.ValueOf(math.Lgamma), + "Ln10": reflect.ValueOf(constant.MakeFromLiteral("2.30258509299404568401799145468436420760110148862877297603332784146804725494827975466552490443295866962642372461496758838959542646932914211937012833592062802600362869664962772731087170541286468505859375", token.FLOAT, 0)), + "Ln2": reflect.ValueOf(constant.MakeFromLiteral("0.6931471805599453094172321214581765680755001343602552541206800092715999496201383079363438206637927920954189307729314303884387720696314608777673678644642390655170150035209453154294578780536539852619171142578125", token.FLOAT, 0)), + "Log": reflect.ValueOf(math.Log), + "Log10": reflect.ValueOf(math.Log10), + "Log10E": reflect.ValueOf(constant.MakeFromLiteral("0.43429448190325182765112891891660508229439700580366656611445378416636798190620320263064286300825210972160277489744884502676719847561509639618196799746596688688378591625127711495224502868950366973876953125", token.FLOAT, 0)), + "Log1p": reflect.ValueOf(math.Log1p), + "Log2": reflect.ValueOf(math.Log2), + "Log2E": reflect.ValueOf(constant.MakeFromLiteral("1.44269504088896340735992468100189213742664595415298593413544940772066427768997545329060870636212628972710992130324953463427359402479619301286929040235571747101382214539290471666532766903401352465152740478515625", token.FLOAT, 0)), + "Logb": reflect.ValueOf(math.Logb), + "Max": reflect.ValueOf(math.Max), + "MaxFloat32": reflect.ValueOf(constant.MakeFromLiteral("340282346638528859811704183484516925440", token.FLOAT, 0)), + "MaxFloat64": reflect.ValueOf(constant.MakeFromLiteral("179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368", token.FLOAT, 0)), + "MaxInt": reflect.ValueOf(constant.MakeFromLiteral("9223372036854775807", token.INT, 0)), + "MaxInt16": reflect.ValueOf(constant.MakeFromLiteral("32767", token.INT, 0)), + "MaxInt32": reflect.ValueOf(constant.MakeFromLiteral("2147483647", token.INT, 0)), + "MaxInt64": reflect.ValueOf(constant.MakeFromLiteral("9223372036854775807", token.INT, 0)), + "MaxInt8": reflect.ValueOf(constant.MakeFromLiteral("127", token.INT, 0)), + "MaxUint": reflect.ValueOf(constant.MakeFromLiteral("18446744073709551615", token.INT, 0)), + "MaxUint16": reflect.ValueOf(constant.MakeFromLiteral("65535", token.INT, 0)), + "MaxUint32": reflect.ValueOf(constant.MakeFromLiteral("4294967295", token.INT, 0)), + "MaxUint64": reflect.ValueOf(constant.MakeFromLiteral("18446744073709551615", token.INT, 0)), + "MaxUint8": reflect.ValueOf(constant.MakeFromLiteral("255", token.INT, 0)), + "Min": reflect.ValueOf(math.Min), + "MinInt": reflect.ValueOf(constant.MakeFromLiteral("-9223372036854775808", token.INT, 0)), + "MinInt16": reflect.ValueOf(constant.MakeFromLiteral("-32768", token.INT, 0)), + "MinInt32": reflect.ValueOf(constant.MakeFromLiteral("-2147483648", token.INT, 0)), + "MinInt64": reflect.ValueOf(constant.MakeFromLiteral("-9223372036854775808", token.INT, 0)), + "MinInt8": reflect.ValueOf(constant.MakeFromLiteral("-128", token.INT, 0)), + "Mod": reflect.ValueOf(math.Mod), + "Modf": reflect.ValueOf(math.Modf), + "NaN": reflect.ValueOf(math.NaN), + "Nextafter": reflect.ValueOf(math.Nextafter), + "Nextafter32": reflect.ValueOf(math.Nextafter32), + "Phi": reflect.ValueOf(constant.MakeFromLiteral("1.6180339887498948482045868343656381177203091798057628621354486119746080982153796619881086049305501566952211682590824739205931370737029882996587050475921915678674035433959321750307935872115194797515869140625", token.FLOAT, 0)), + "Pi": reflect.ValueOf(constant.MakeFromLiteral("3.141592653589793238462643383279502884197169399375105820974944594789982923695635954704435713335896673485663389728754819466702315787113662862838515639906529162340867271374644786874341662041842937469482421875", token.FLOAT, 0)), + "Pow": reflect.ValueOf(math.Pow), + "Pow10": reflect.ValueOf(math.Pow10), + "Remainder": reflect.ValueOf(math.Remainder), + "Round": reflect.ValueOf(math.Round), + "RoundToEven": reflect.ValueOf(math.RoundToEven), + "Signbit": reflect.ValueOf(math.Signbit), + "Sin": reflect.ValueOf(math.Sin), + "Sincos": reflect.ValueOf(math.Sincos), + "Sinh": reflect.ValueOf(math.Sinh), + "SmallestNonzeroFloat32": reflect.ValueOf(constant.MakeFromLiteral("1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158203125e-45", token.FLOAT, 0)), + "SmallestNonzeroFloat64": reflect.ValueOf(constant.MakeFromLiteral("4.940656458412465441765687928682213723650598026143247644255856825006755072702087518652998363616359923797965646954457177309266567103559397963987747960107818781263007131903114045278458171678489821036887186360569987307230500063874091535649843873124733972731696151400317153853980741262385655911710266585566867681870395603106249319452715914924553293054565444011274801297099995419319894090804165633245247571478690147267801593552386115501348035264934720193790268107107491703332226844753335720832431936092382893458368060106011506169809753078342277318329247904982524730776375927247874656084778203734469699533647017972677717585125660551199131504891101451037862738167250955837389733598993664809941164205702637090279242767544565229087538682506419718265533447265625e-324", token.FLOAT, 0)), + "Sqrt": reflect.ValueOf(math.Sqrt), + "Sqrt2": reflect.ValueOf(constant.MakeFromLiteral("1.414213562373095048801688724209698078569671875376948073176679739576083351575381440094441524123797447886801949755143139115339040409162552642832693297721230919563348109313505318596071447245776653289794921875", token.FLOAT, 0)), + "SqrtE": reflect.ValueOf(constant.MakeFromLiteral("1.64872127070012814684865078781416357165377610071014801157507931167328763229187870850146925823776361770041160388013884200789716007979526823569827080974091691342077871211546646890155898290686309337615966796875", token.FLOAT, 0)), + "SqrtPhi": reflect.ValueOf(constant.MakeFromLiteral("1.2720196495140689642524224617374914917156080418400962486166403754616080542166459302584536396369727769747312116100875915825863540562126478288118732191412003988041797518382391984914647764526307582855224609375", token.FLOAT, 0)), + "SqrtPi": reflect.ValueOf(constant.MakeFromLiteral("1.772453850905516027298167483341145182797549456122387128213807789740599698370237052541269446184448945647349951047154197675245574635259260134350885938555625028620527962319730619356050738133490085601806640625", token.FLOAT, 0)), + "Tan": reflect.ValueOf(math.Tan), + "Tanh": reflect.ValueOf(math.Tanh), + "Trunc": reflect.ValueOf(math.Trunc), + "Y0": reflect.ValueOf(math.Y0), + "Y1": reflect.ValueOf(math.Y1), + "Yn": reflect.ValueOf(math.Yn), + } +} diff --git a/devtools/internal/lib/go1_20_math_rand.go b/devtools/internal/lib/go1_20_math_rand.go new file mode 100644 index 0000000..9113d41 --- /dev/null +++ b/devtools/internal/lib/go1_20_math_rand.go @@ -0,0 +1,75 @@ +// Code generated by 'yaegi extract math/rand'. DO NOT EDIT. + +package lib + +import ( + "math/rand" + "reflect" +) + +func init() { + Symbols["math/rand/rand"] = map[string]reflect.Value{ + // function, constant and variable definitions + "ExpFloat64": reflect.ValueOf(rand.ExpFloat64), + "Float32": reflect.ValueOf(rand.Float32), + "Float64": reflect.ValueOf(rand.Float64), + "Int": reflect.ValueOf(rand.Int), + "Int31": reflect.ValueOf(rand.Int31), + "Int31n": reflect.ValueOf(rand.Int31n), + "Int63": reflect.ValueOf(rand.Int63), + "Int63n": reflect.ValueOf(rand.Int63n), + "Intn": reflect.ValueOf(rand.Intn), + "New": reflect.ValueOf(rand.New), + "NewSource": reflect.ValueOf(rand.NewSource), + "NewZipf": reflect.ValueOf(rand.NewZipf), + "NormFloat64": reflect.ValueOf(rand.NormFloat64), + "Perm": reflect.ValueOf(rand.Perm), + "Read": reflect.ValueOf(rand.Read), + "Seed": reflect.ValueOf(rand.Seed), + "Shuffle": reflect.ValueOf(rand.Shuffle), + "Uint32": reflect.ValueOf(rand.Uint32), + "Uint64": reflect.ValueOf(rand.Uint64), + + // type definitions + "Rand": reflect.ValueOf((*rand.Rand)(nil)), + "Source": reflect.ValueOf((*rand.Source)(nil)), + "Source64": reflect.ValueOf((*rand.Source64)(nil)), + "Zipf": reflect.ValueOf((*rand.Zipf)(nil)), + + // interface wrapper definitions + "_Source": reflect.ValueOf((*_math_rand_Source)(nil)), + "_Source64": reflect.ValueOf((*_math_rand_Source64)(nil)), + } +} + +// _math_rand_Source is an interface wrapper for Source type +type _math_rand_Source struct { + IValue interface{} + WInt63 func() int64 + WSeed func(seed int64) +} + +func (W _math_rand_Source) Int63() int64 { + return W.WInt63() +} +func (W _math_rand_Source) Seed(seed int64) { + W.WSeed(seed) +} + +// _math_rand_Source64 is an interface wrapper for Source64 type +type _math_rand_Source64 struct { + IValue interface{} + WInt63 func() int64 + WSeed func(seed int64) + WUint64 func() uint64 +} + +func (W _math_rand_Source64) Int63() int64 { + return W.WInt63() +} +func (W _math_rand_Source64) Seed(seed int64) { + W.WSeed(seed) +} +func (W _math_rand_Source64) Uint64() uint64 { + return W.WUint64() +} diff --git a/devtools/internal/lib/go1_20_os.go b/devtools/internal/lib/go1_20_os.go new file mode 100644 index 0000000..22b37b7 --- /dev/null +++ b/devtools/internal/lib/go1_20_os.go @@ -0,0 +1,208 @@ +// Code generated by 'yaegi extract os'. DO NOT EDIT. + +package lib + +import ( + "go/constant" + "go/token" + "io/fs" + "os" + "reflect" + "time" +) + +func init() { + Symbols["os/os"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Args": reflect.ValueOf(&os.Args).Elem(), + "Chdir": reflect.ValueOf(os.Chdir), + "Chmod": reflect.ValueOf(os.Chmod), + "Chown": reflect.ValueOf(os.Chown), + "Chtimes": reflect.ValueOf(os.Chtimes), + "Clearenv": reflect.ValueOf(os.Clearenv), + "Create": reflect.ValueOf(os.Create), + "CreateTemp": reflect.ValueOf(os.CreateTemp), + "DevNull": reflect.ValueOf(constant.MakeFromLiteral("\"/dev/null\"", token.STRING, 0)), + "DirFS": reflect.ValueOf(os.DirFS), + "Environ": reflect.ValueOf(os.Environ), + "ErrClosed": reflect.ValueOf(&os.ErrClosed).Elem(), + "ErrDeadlineExceeded": reflect.ValueOf(&os.ErrDeadlineExceeded).Elem(), + "ErrExist": reflect.ValueOf(&os.ErrExist).Elem(), + "ErrInvalid": reflect.ValueOf(&os.ErrInvalid).Elem(), + "ErrNoDeadline": reflect.ValueOf(&os.ErrNoDeadline).Elem(), + "ErrNotExist": reflect.ValueOf(&os.ErrNotExist).Elem(), + "ErrPermission": reflect.ValueOf(&os.ErrPermission).Elem(), + "ErrProcessDone": reflect.ValueOf(&os.ErrProcessDone).Elem(), + "Executable": reflect.ValueOf(os.Executable), + "Exit": reflect.ValueOf(osExit), + "Expand": reflect.ValueOf(os.Expand), + "ExpandEnv": reflect.ValueOf(os.ExpandEnv), + "FindProcess": reflect.ValueOf(osFindProcess), + "Getegid": reflect.ValueOf(os.Getegid), + "Getenv": reflect.ValueOf(os.Getenv), + "Geteuid": reflect.ValueOf(os.Geteuid), + "Getgid": reflect.ValueOf(os.Getgid), + "Getgroups": reflect.ValueOf(os.Getgroups), + "Getpagesize": reflect.ValueOf(os.Getpagesize), + "Getpid": reflect.ValueOf(os.Getpid), + "Getppid": reflect.ValueOf(os.Getppid), + "Getuid": reflect.ValueOf(os.Getuid), + "Getwd": reflect.ValueOf(os.Getwd), + "Hostname": reflect.ValueOf(os.Hostname), + "Interrupt": reflect.ValueOf(&os.Interrupt).Elem(), + "IsExist": reflect.ValueOf(os.IsExist), + "IsNotExist": reflect.ValueOf(os.IsNotExist), + "IsPathSeparator": reflect.ValueOf(os.IsPathSeparator), + "IsPermission": reflect.ValueOf(os.IsPermission), + "IsTimeout": reflect.ValueOf(os.IsTimeout), + "Kill": reflect.ValueOf(&os.Kill).Elem(), + "Lchown": reflect.ValueOf(os.Lchown), + "Link": reflect.ValueOf(os.Link), + "LookupEnv": reflect.ValueOf(os.LookupEnv), + "Lstat": reflect.ValueOf(os.Lstat), + "Mkdir": reflect.ValueOf(os.Mkdir), + "MkdirAll": reflect.ValueOf(os.MkdirAll), + "MkdirTemp": reflect.ValueOf(os.MkdirTemp), + "ModeAppend": reflect.ValueOf(os.ModeAppend), + "ModeCharDevice": reflect.ValueOf(os.ModeCharDevice), + "ModeDevice": reflect.ValueOf(os.ModeDevice), + "ModeDir": reflect.ValueOf(os.ModeDir), + "ModeExclusive": reflect.ValueOf(os.ModeExclusive), + "ModeIrregular": reflect.ValueOf(os.ModeIrregular), + "ModeNamedPipe": reflect.ValueOf(os.ModeNamedPipe), + "ModePerm": reflect.ValueOf(os.ModePerm), + "ModeSetgid": reflect.ValueOf(os.ModeSetgid), + "ModeSetuid": reflect.ValueOf(os.ModeSetuid), + "ModeSocket": reflect.ValueOf(os.ModeSocket), + "ModeSticky": reflect.ValueOf(os.ModeSticky), + "ModeSymlink": reflect.ValueOf(os.ModeSymlink), + "ModeTemporary": reflect.ValueOf(os.ModeTemporary), + "ModeType": reflect.ValueOf(os.ModeType), + "NewFile": reflect.ValueOf(os.NewFile), + "NewSyscallError": reflect.ValueOf(os.NewSyscallError), + "O_APPEND": reflect.ValueOf(os.O_APPEND), + "O_CREATE": reflect.ValueOf(os.O_CREATE), + "O_EXCL": reflect.ValueOf(os.O_EXCL), + "O_RDONLY": reflect.ValueOf(os.O_RDONLY), + "O_RDWR": reflect.ValueOf(os.O_RDWR), + "O_SYNC": reflect.ValueOf(os.O_SYNC), + "O_TRUNC": reflect.ValueOf(os.O_TRUNC), + "O_WRONLY": reflect.ValueOf(os.O_WRONLY), + "Open": reflect.ValueOf(os.Open), + "OpenFile": reflect.ValueOf(os.OpenFile), + "PathListSeparator": reflect.ValueOf(constant.MakeFromLiteral("58", token.INT, 0)), + "PathSeparator": reflect.ValueOf(constant.MakeFromLiteral("47", token.INT, 0)), + "Pipe": reflect.ValueOf(os.Pipe), + "ReadDir": reflect.ValueOf(os.ReadDir), + "ReadFile": reflect.ValueOf(os.ReadFile), + "Readlink": reflect.ValueOf(os.Readlink), + "Remove": reflect.ValueOf(os.Remove), + "RemoveAll": reflect.ValueOf(os.RemoveAll), + "Rename": reflect.ValueOf(os.Rename), + "SEEK_CUR": reflect.ValueOf(os.SEEK_CUR), + "SEEK_END": reflect.ValueOf(os.SEEK_END), + "SEEK_SET": reflect.ValueOf(os.SEEK_SET), + "SameFile": reflect.ValueOf(os.SameFile), + "Setenv": reflect.ValueOf(os.Setenv), + "StartProcess": reflect.ValueOf(os.StartProcess), + "Stat": reflect.ValueOf(os.Stat), + "Stderr": reflect.ValueOf(&os.Stderr).Elem(), + "Stdin": reflect.ValueOf(&os.Stdin).Elem(), + "Stdout": reflect.ValueOf(&os.Stdout).Elem(), + "Symlink": reflect.ValueOf(os.Symlink), + "TempDir": reflect.ValueOf(os.TempDir), + "Truncate": reflect.ValueOf(os.Truncate), + "Unsetenv": reflect.ValueOf(os.Unsetenv), + "UserCacheDir": reflect.ValueOf(os.UserCacheDir), + "UserConfigDir": reflect.ValueOf(os.UserConfigDir), + "UserHomeDir": reflect.ValueOf(os.UserHomeDir), + "WriteFile": reflect.ValueOf(os.WriteFile), + + // type definitions + "DirEntry": reflect.ValueOf((*os.DirEntry)(nil)), + "File": reflect.ValueOf((*os.File)(nil)), + "FileInfo": reflect.ValueOf((*os.FileInfo)(nil)), + "FileMode": reflect.ValueOf((*os.FileMode)(nil)), + "LinkError": reflect.ValueOf((*os.LinkError)(nil)), + "PathError": reflect.ValueOf((*os.PathError)(nil)), + "ProcAttr": reflect.ValueOf((*os.ProcAttr)(nil)), + "Process": reflect.ValueOf((*os.Process)(nil)), + "ProcessState": reflect.ValueOf((*os.ProcessState)(nil)), + "Signal": reflect.ValueOf((*os.Signal)(nil)), + "SyscallError": reflect.ValueOf((*os.SyscallError)(nil)), + + // interface wrapper definitions + "_DirEntry": reflect.ValueOf((*_os_DirEntry)(nil)), + "_FileInfo": reflect.ValueOf((*_os_FileInfo)(nil)), + "_Signal": reflect.ValueOf((*_os_Signal)(nil)), + } +} + +// _os_DirEntry is an interface wrapper for DirEntry type +type _os_DirEntry struct { + IValue interface{} + WInfo func() (fs.FileInfo, error) + WIsDir func() bool + WName func() string + WType func() fs.FileMode +} + +func (W _os_DirEntry) Info() (fs.FileInfo, error) { + return W.WInfo() +} +func (W _os_DirEntry) IsDir() bool { + return W.WIsDir() +} +func (W _os_DirEntry) Name() string { + return W.WName() +} +func (W _os_DirEntry) Type() fs.FileMode { + return W.WType() +} + +// _os_FileInfo is an interface wrapper for FileInfo type +type _os_FileInfo struct { + IValue interface{} + WIsDir func() bool + WModTime func() time.Time + WMode func() fs.FileMode + WName func() string + WSize func() int64 + WSys func() any +} + +func (W _os_FileInfo) IsDir() bool { + return W.WIsDir() +} +func (W _os_FileInfo) ModTime() time.Time { + return W.WModTime() +} +func (W _os_FileInfo) Mode() fs.FileMode { + return W.WMode() +} +func (W _os_FileInfo) Name() string { + return W.WName() +} +func (W _os_FileInfo) Size() int64 { + return W.WSize() +} +func (W _os_FileInfo) Sys() any { + return W.WSys() +} + +// _os_Signal is an interface wrapper for Signal type +type _os_Signal struct { + IValue interface{} + WSignal func() + WString func() string +} + +func (W _os_Signal) Signal() { + W.WSignal() +} +func (W _os_Signal) String() string { + if W.WString == nil { + return "" + } + return W.WString() +} diff --git a/devtools/internal/lib/go1_20_sort.go b/devtools/internal/lib/go1_20_sort.go new file mode 100644 index 0000000..8d17ce0 --- /dev/null +++ b/devtools/internal/lib/go1_20_sort.go @@ -0,0 +1,59 @@ +// Code generated by 'yaegi extract sort'. DO NOT EDIT. + +package lib + +import ( + "reflect" + "sort" +) + +func init() { + Symbols["sort/sort"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Find": reflect.ValueOf(sort.Find), + "Float64s": reflect.ValueOf(sort.Float64s), + "Float64sAreSorted": reflect.ValueOf(sort.Float64sAreSorted), + "Ints": reflect.ValueOf(sort.Ints), + "IntsAreSorted": reflect.ValueOf(sort.IntsAreSorted), + "IsSorted": reflect.ValueOf(sort.IsSorted), + "Reverse": reflect.ValueOf(sort.Reverse), + "Search": reflect.ValueOf(sort.Search), + "SearchFloat64s": reflect.ValueOf(sort.SearchFloat64s), + "SearchInts": reflect.ValueOf(sort.SearchInts), + "SearchStrings": reflect.ValueOf(sort.SearchStrings), + "Slice": reflect.ValueOf(sort.Slice), + "SliceIsSorted": reflect.ValueOf(sort.SliceIsSorted), + "SliceStable": reflect.ValueOf(sort.SliceStable), + "Sort": reflect.ValueOf(sort.Sort), + "Stable": reflect.ValueOf(sort.Stable), + "Strings": reflect.ValueOf(sort.Strings), + "StringsAreSorted": reflect.ValueOf(sort.StringsAreSorted), + + // type definitions + "Float64Slice": reflect.ValueOf((*sort.Float64Slice)(nil)), + "IntSlice": reflect.ValueOf((*sort.IntSlice)(nil)), + "Interface": reflect.ValueOf((*sort.Interface)(nil)), + "StringSlice": reflect.ValueOf((*sort.StringSlice)(nil)), + + // interface wrapper definitions + "_Interface": reflect.ValueOf((*_sort_Interface)(nil)), + } +} + +// _sort_Interface is an interface wrapper for Interface type +type _sort_Interface struct { + IValue interface{} + WLen func() int + WLess func(i int, j int) bool + WSwap func(i int, j int) +} + +func (W _sort_Interface) Len() int { + return W.WLen() +} +func (W _sort_Interface) Less(i int, j int) bool { + return W.WLess(i, j) +} +func (W _sort_Interface) Swap(i int, j int) { + W.WSwap(i, j) +} diff --git a/devtools/internal/lib/go1_20_strconv.go b/devtools/internal/lib/go1_20_strconv.go new file mode 100644 index 0000000..d8ab53a --- /dev/null +++ b/devtools/internal/lib/go1_20_strconv.go @@ -0,0 +1,56 @@ +// Code generated by 'yaegi extract strconv'. DO NOT EDIT. + +package lib + +import ( + "go/constant" + "go/token" + "reflect" + "strconv" +) + +func init() { + Symbols["strconv/strconv"] = map[string]reflect.Value{ + // function, constant and variable definitions + "AppendBool": reflect.ValueOf(strconv.AppendBool), + "AppendFloat": reflect.ValueOf(strconv.AppendFloat), + "AppendInt": reflect.ValueOf(strconv.AppendInt), + "AppendQuote": reflect.ValueOf(strconv.AppendQuote), + "AppendQuoteRune": reflect.ValueOf(strconv.AppendQuoteRune), + "AppendQuoteRuneToASCII": reflect.ValueOf(strconv.AppendQuoteRuneToASCII), + "AppendQuoteRuneToGraphic": reflect.ValueOf(strconv.AppendQuoteRuneToGraphic), + "AppendQuoteToASCII": reflect.ValueOf(strconv.AppendQuoteToASCII), + "AppendQuoteToGraphic": reflect.ValueOf(strconv.AppendQuoteToGraphic), + "AppendUint": reflect.ValueOf(strconv.AppendUint), + "Atoi": reflect.ValueOf(strconv.Atoi), + "CanBackquote": reflect.ValueOf(strconv.CanBackquote), + "ErrRange": reflect.ValueOf(&strconv.ErrRange).Elem(), + "ErrSyntax": reflect.ValueOf(&strconv.ErrSyntax).Elem(), + "FormatBool": reflect.ValueOf(strconv.FormatBool), + "FormatComplex": reflect.ValueOf(strconv.FormatComplex), + "FormatFloat": reflect.ValueOf(strconv.FormatFloat), + "FormatInt": reflect.ValueOf(strconv.FormatInt), + "FormatUint": reflect.ValueOf(strconv.FormatUint), + "IntSize": reflect.ValueOf(constant.MakeFromLiteral("64", token.INT, 0)), + "IsGraphic": reflect.ValueOf(strconv.IsGraphic), + "IsPrint": reflect.ValueOf(strconv.IsPrint), + "Itoa": reflect.ValueOf(strconv.Itoa), + "ParseBool": reflect.ValueOf(strconv.ParseBool), + "ParseComplex": reflect.ValueOf(strconv.ParseComplex), + "ParseFloat": reflect.ValueOf(strconv.ParseFloat), + "ParseInt": reflect.ValueOf(strconv.ParseInt), + "ParseUint": reflect.ValueOf(strconv.ParseUint), + "Quote": reflect.ValueOf(strconv.Quote), + "QuoteRune": reflect.ValueOf(strconv.QuoteRune), + "QuoteRuneToASCII": reflect.ValueOf(strconv.QuoteRuneToASCII), + "QuoteRuneToGraphic": reflect.ValueOf(strconv.QuoteRuneToGraphic), + "QuoteToASCII": reflect.ValueOf(strconv.QuoteToASCII), + "QuoteToGraphic": reflect.ValueOf(strconv.QuoteToGraphic), + "QuotedPrefix": reflect.ValueOf(strconv.QuotedPrefix), + "Unquote": reflect.ValueOf(strconv.Unquote), + "UnquoteChar": reflect.ValueOf(strconv.UnquoteChar), + + // type definitions + "NumError": reflect.ValueOf((*strconv.NumError)(nil)), + } +} diff --git a/devtools/internal/lib/go1_20_strings.go b/devtools/internal/lib/go1_20_strings.go new file mode 100644 index 0000000..d6dc91a --- /dev/null +++ b/devtools/internal/lib/go1_20_strings.go @@ -0,0 +1,70 @@ +// Code generated by 'yaegi extract strings'. DO NOT EDIT. + +package lib + +import ( + "reflect" + "strings" +) + +func init() { + Symbols["strings/strings"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Clone": reflect.ValueOf(strings.Clone), + "Compare": reflect.ValueOf(strings.Compare), + "Contains": reflect.ValueOf(strings.Contains), + "ContainsAny": reflect.ValueOf(strings.ContainsAny), + "ContainsRune": reflect.ValueOf(strings.ContainsRune), + "Count": reflect.ValueOf(strings.Count), + "Cut": reflect.ValueOf(strings.Cut), + "CutPrefix": reflect.ValueOf(strings.CutPrefix), + "CutSuffix": reflect.ValueOf(strings.CutSuffix), + "EqualFold": reflect.ValueOf(strings.EqualFold), + "Fields": reflect.ValueOf(strings.Fields), + "FieldsFunc": reflect.ValueOf(strings.FieldsFunc), + "HasPrefix": reflect.ValueOf(strings.HasPrefix), + "HasSuffix": reflect.ValueOf(strings.HasSuffix), + "Index": reflect.ValueOf(strings.Index), + "IndexAny": reflect.ValueOf(strings.IndexAny), + "IndexByte": reflect.ValueOf(strings.IndexByte), + "IndexFunc": reflect.ValueOf(strings.IndexFunc), + "IndexRune": reflect.ValueOf(strings.IndexRune), + "Join": reflect.ValueOf(strings.Join), + "LastIndex": reflect.ValueOf(strings.LastIndex), + "LastIndexAny": reflect.ValueOf(strings.LastIndexAny), + "LastIndexByte": reflect.ValueOf(strings.LastIndexByte), + "LastIndexFunc": reflect.ValueOf(strings.LastIndexFunc), + "Map": reflect.ValueOf(strings.Map), + "NewReader": reflect.ValueOf(strings.NewReader), + "NewReplacer": reflect.ValueOf(strings.NewReplacer), + "Repeat": reflect.ValueOf(strings.Repeat), + "Replace": reflect.ValueOf(strings.Replace), + "ReplaceAll": reflect.ValueOf(strings.ReplaceAll), + "Split": reflect.ValueOf(strings.Split), + "SplitAfter": reflect.ValueOf(strings.SplitAfter), + "SplitAfterN": reflect.ValueOf(strings.SplitAfterN), + "SplitN": reflect.ValueOf(strings.SplitN), + "Title": reflect.ValueOf(strings.Title), + "ToLower": reflect.ValueOf(strings.ToLower), + "ToLowerSpecial": reflect.ValueOf(strings.ToLowerSpecial), + "ToTitle": reflect.ValueOf(strings.ToTitle), + "ToTitleSpecial": reflect.ValueOf(strings.ToTitleSpecial), + "ToUpper": reflect.ValueOf(strings.ToUpper), + "ToUpperSpecial": reflect.ValueOf(strings.ToUpperSpecial), + "ToValidUTF8": reflect.ValueOf(strings.ToValidUTF8), + "Trim": reflect.ValueOf(strings.Trim), + "TrimFunc": reflect.ValueOf(strings.TrimFunc), + "TrimLeft": reflect.ValueOf(strings.TrimLeft), + "TrimLeftFunc": reflect.ValueOf(strings.TrimLeftFunc), + "TrimPrefix": reflect.ValueOf(strings.TrimPrefix), + "TrimRight": reflect.ValueOf(strings.TrimRight), + "TrimRightFunc": reflect.ValueOf(strings.TrimRightFunc), + "TrimSpace": reflect.ValueOf(strings.TrimSpace), + "TrimSuffix": reflect.ValueOf(strings.TrimSuffix), + + // type definitions + "Builder": reflect.ValueOf((*strings.Builder)(nil)), + "Reader": reflect.ValueOf((*strings.Reader)(nil)), + "Replacer": reflect.ValueOf((*strings.Replacer)(nil)), + } +} diff --git a/devtools/internal/lib/lib.go b/devtools/internal/lib/lib.go new file mode 100644 index 0000000..5fcad22 --- /dev/null +++ b/devtools/internal/lib/lib.go @@ -0,0 +1,42 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +// Package lib provides symbols to be used by Yaegi interpreter. +package lib + +import ( + "reflect" + "strings" +) + +// Symbols variable stores the map of stdlib symbols per package. +var Symbols = map[string]map[string]reflect.Value{} + +// MapTypes variable contains a map of functions which have an interface{} as parameter but +// do something special if the parameter implements a given interface. +var MapTypes = map[reflect.Value][]reflect.Type{} + +func AllPackages() []Package { + var packages []Package + for pkgWithAlias := range Symbols { + if strings.Contains(pkgWithAlias, "/") { + pkg := pkgWithAlias[:strings.LastIndex(pkgWithAlias, "/")] + alias := pkgWithAlias[strings.LastIndex(pkgWithAlias, "/")+1:] + packages = append(packages, Package{Path: pkg, Alias: alias}) + } + } + return packages +} + +type Package struct { + Path string // for example: github.com/elgopher/pi + Alias string // for example: pi +} + +func (p Package) IsPiPackage() bool { + return strings.HasPrefix(p.Path, "github.com/elgopher/pi") +} + +func (p Package) IsStdPackage() bool { + return !p.IsPiPackage() && !strings.HasPrefix(p.Path, "github.com/traefik/yaegi") +} diff --git a/devtools/internal/lib/lib_test.go b/devtools/internal/lib/lib_test.go new file mode 100644 index 0000000..3778914 --- /dev/null +++ b/devtools/internal/lib/lib_test.go @@ -0,0 +1,37 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +package lib_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elgopher/pi/devtools/internal/lib" +) + +var ( + piPackage = lib.Package{Path: "github.com/elgopher/pi", Alias: "pi"} + fmtPackage = lib.Package{Path: "fmt", Alias: "fmt"} +) + +func TestAllPackages(t *testing.T) { + t.Run("should return packages", func(t *testing.T) { + packages := lib.AllPackages() + assert.NotEmpty(t, packages) + + assert.Contains(t, packages, piPackage) + assert.Contains(t, packages, fmtPackage) + }) +} + +func TestPackage_IsStdPackage(t *testing.T) { + assert.True(t, fmtPackage.IsStdPackage()) + assert.False(t, piPackage.IsStdPackage()) +} + +func TestPackage_IsPiPackage(t *testing.T) { + assert.False(t, fmtPackage.IsPiPackage()) + assert.True(t, piPackage.IsPiPackage()) +} diff --git a/devtools/internal/lib/maptypes.go b/devtools/internal/lib/maptypes.go new file mode 100644 index 0000000..1b39a1b --- /dev/null +++ b/devtools/internal/lib/maptypes.go @@ -0,0 +1,30 @@ +package lib + +import ( + "fmt" + "reflect" +) + +func init() { + mt := []reflect.Type{ + reflect.TypeOf((*fmt.Formatter)(nil)).Elem(), + reflect.TypeOf((*fmt.Stringer)(nil)).Elem(), + } + + MapTypes[reflect.ValueOf(fmt.Errorf)] = mt + MapTypes[reflect.ValueOf(fmt.Fprint)] = mt + MapTypes[reflect.ValueOf(fmt.Fprintf)] = mt + MapTypes[reflect.ValueOf(fmt.Fprintln)] = mt + MapTypes[reflect.ValueOf(fmt.Print)] = mt + MapTypes[reflect.ValueOf(fmt.Printf)] = mt + MapTypes[reflect.ValueOf(fmt.Println)] = mt + MapTypes[reflect.ValueOf(fmt.Sprint)] = mt + MapTypes[reflect.ValueOf(fmt.Sprintf)] = mt + MapTypes[reflect.ValueOf(fmt.Sprintln)] = mt + + mt = []reflect.Type{reflect.TypeOf((*fmt.Scanner)(nil)).Elem()} + + MapTypes[reflect.ValueOf(fmt.Scan)] = mt + MapTypes[reflect.ValueOf(fmt.Scanf)] = mt + MapTypes[reflect.ValueOf(fmt.Scanln)] = mt +} diff --git a/devtools/internal/lib/restricted.go b/devtools/internal/lib/restricted.go new file mode 100644 index 0000000..9976c40 --- /dev/null +++ b/devtools/internal/lib/restricted.go @@ -0,0 +1,20 @@ +package lib + +import ( + "errors" + "os" + "strconv" +) + +var errRestricted = errors.New("restricted") + +// osExit invokes panic instead of exit. +func osExit(code int) { panic("os.Exit(" + strconv.Itoa(code) + ")") } + +// osFindProcess returns os.FindProcess, except for self process. +func osFindProcess(pid int) (*os.Process, error) { + if pid == os.Getpid() { + return nil, errRestricted + } + return os.FindProcess(pid) +} diff --git a/devtools/internal/lib/yaegi.go b/devtools/internal/lib/yaegi.go new file mode 100644 index 0000000..0169ac9 --- /dev/null +++ b/devtools/internal/lib/yaegi.go @@ -0,0 +1,12 @@ +package lib + +import "reflect" + +func init() { + Symbols["github.com/traefik/yaegi/stdlib/stdlib"] = map[string]reflect.Value{ + "Symbols": reflect.ValueOf(Symbols), + } + Symbols["."] = map[string]reflect.Value{ + "MapTypes": reflect.ValueOf(MapTypes), + } +} diff --git a/devtools/internal/terminal/terminal.go b/devtools/internal/terminal/terminal.go new file mode 100644 index 0000000..4b3d442 --- /dev/null +++ b/devtools/internal/terminal/terminal.go @@ -0,0 +1,72 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +package terminal + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/peterh/liner" +) + +var ( + linerState *liner.State + Commands = make(chan string, 1) + CommandsProcessed = make(chan ProcessingResult, 1) +) + +type ProcessingResult int + +const ( + Done ProcessingResult = 0 + MoreInputNeeded ProcessingResult = 1 +) + +func StartReadingCommands() { + linerState = liner.NewLiner() + linerState.SetCtrlCAborts(true) + linerState.SetBeep(false) + + var prompt = "> " + var cmd strings.Builder + + go func() { + for { + p, err := linerState.Prompt(prompt) + cmd.WriteString(p) + if err != nil { + if errors.Is(err, liner.ErrPromptAborted) { + fmt.Println("(^D to quit)") + continue + } + if err == io.EOF { + linerState.Close() + os.Exit(0) + } + linerState.Close() + panic(err) + } + + linerState.AppendHistory(p) + + Commands <- cmd.String() + result := <-CommandsProcessed + + if result == MoreInputNeeded { + cmd.WriteRune('\n') + prompt = " " + } else { + cmd.Reset() + prompt = "> " + } + } + }() +} + +func StopReadingCommandsFromStdin() { + linerState.Close() +} diff --git a/devtools/internal/test/stdout_swapper.go b/devtools/internal/test/stdout_swapper.go new file mode 100644 index 0000000..c9d9239 --- /dev/null +++ b/devtools/internal/test/stdout_swapper.go @@ -0,0 +1,43 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +//go:build !js + +package test + +import ( + "io" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// SwapStdout swaps stdout with fake one. +func SwapStdout(t *testing.T) StdoutSwapper { + reader, writer, err := os.Pipe() + require.NoError(t, err) + oldStdout := os.Stdout + os.Stdout = writer + + return StdoutSwapper{ + prevStdout: oldStdout, + reader: reader, + } +} + +type StdoutSwapper struct { + prevStdout *os.File + reader *os.File +} + +func (s *StdoutSwapper) BringStdoutBack() { + _ = os.Stdout.Close() + os.Stdout = s.prevStdout +} + +func (s *StdoutSwapper) ReadOutput(t *testing.T) string { + all, err := io.ReadAll(s.reader) + require.NoError(t, err) + return string(all) +} diff --git a/devtools/scripting.go b/devtools/scripting.go new file mode 100644 index 0000000..3a4492e --- /dev/null +++ b/devtools/scripting.go @@ -0,0 +1,82 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +package devtools + +import ( + "fmt" + + "github.com/elgopher/pi/devtools/internal/help" + "github.com/elgopher/pi/devtools/internal/interpreter" + "github.com/elgopher/pi/devtools/internal/snapshot" + "github.com/elgopher/pi/devtools/internal/terminal" +) + +var interpreterInstance interpreter.Instance + +func init() { + i, err := interpreter.New(help.PrintHelp) + if err != nil { + panic("problem creating interpreter instance: " + err.Error()) + } + + interpreterInstance = i +} + +// Export makes the v visible in terminal. v can be a variable or a function. +// It will be visible under the specified name. +// +// If you want to be able to update the value of v in the terminal, please pass a pointer to v. For example: +// +// v := 1 +// devtools.Export("v", &v) +// +// Then in the terminal write: +// +// v = 2 +func Export(name string, v any) { + if err := interpreterInstance.Export(name, v); err != nil { + panic("devtools.Export failed: " + err.Error()) + } +} + +// ExportType makes the type T visible in terminal. It will be visible under the name "package.Name". +// For example type "github.com/a/b/c/pkg.SomeType" will be visible as "pkg.SomeType". This function +// also automatically imports the package. +func ExportType[T any]() { + if err := interpreter.ExportType[T](interpreterInstance); err != nil { + panic("devtools.ExportType failed: " + err.Error()) + } +} + +func evaluateNextCommandFromTerminal() { + select { + case cmd := <-terminal.Commands: + result, err := interpreterInstance.Eval(cmd) + if err != nil { + fmt.Println(err) + terminal.CommandsProcessed <- terminal.Done + + return + } + + switch result { + case interpreter.GoCodeExecuted: + snapshot.Take() + case interpreter.Resumed: + resumeGame() + case interpreter.Paused: + pauseGame() + case interpreter.Undoed: + snapshot.Undo() + fmt.Println("Undoed last draw operation") + } + + if result == interpreter.Continued { + terminal.CommandsProcessed <- terminal.MoreInputNeeded + } else { + terminal.CommandsProcessed <- terminal.Done + } + default: + } +} diff --git a/devtools/update.go b/devtools/update.go index 311651f..98c3f24 100644 --- a/devtools/update.go +++ b/devtools/update.go @@ -20,4 +20,6 @@ func updateDevTools() { if gamePaused { inspector.Update() } + + evaluateNextCommandFromTerminal() } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c86f71d..51f4883 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -42,6 +42,7 @@ * [ ] drawing on the screen using Pi functions * [x] Pset, shapes * [ ] Spr, Print + * [x] **scripting/REPL** - write Go code **live** when the game is running * [ ] palette inspector * [ ] display, draw palette * [ ] sprite-sheet editor diff --git a/examples/scripting/main.go b/examples/scripting/main.go new file mode 100644 index 0000000..4d6d39f --- /dev/null +++ b/examples/scripting/main.go @@ -0,0 +1,95 @@ +// DevTools Terminal lets you write and execute Go code live when your game is running. +// You can run any Pi functions and access any Pi variables. You can also access +// your own variables and functions, but in order to do that you must first export +// them. This example shows how to do that. +// +// Disclaimer: even though you can write and execute Go code when the game is running, +// this does not mean that the Go code is run in parallel. All operations written to +// terminal are run sequentially in the next iteration of the main game loop. +package main + +import ( + "embed" + "fmt" + + "github.com/elgopher/pi" + "github.com/elgopher/pi/devtools" + "github.com/elgopher/pi/ebitengine" +) + +//go:embed sprite-sheet.png +var resources embed.FS + +func main() { + // export variable, so it can be used in terminal, for example: + // + // v := variable + devtools.Export("variable", 1) + + var another = 1 + + // export pointer to variable. You can then replace entire variable in terminal: + // + // *another = 2 + devtools.Export("another", &another) + + // export function, so it can be used in terminal, for example: + // + // drawCircle() + // + // Please note, that if you want to see the circle you must first pause the game. + devtools.Export("drawCircle", drawCircle) + + // export pointer to variable which contains function. You can swap this function with your own code in terminal: + // + // *drawCallback = func() { pi.Print("UPDATED", 10, 10, 7) } + devtools.Export("drawCallback", &drawCallback) + + // export type Struct. It is defined in main therefore you can use something like this: + // + // s := Struct{} + devtools.ExportType[Struct]() + + // export function accepting previously defined struct type: + // + // funcAcceptingStruct(Struct{}) + devtools.Export("funcAcceptingStruct", funcAcceptingStruct) + + pi.Load(resources) + pi.Draw = func() { + pi.Cls() + + // Animate hello world + for i := 0; i < 12; i++ { + x := 20 + i*8 + y := pi.Cos(pi.Time()+float64(i)/64) * 60 + pi.Spr(i, x, 60+int(y)) + } + + // run function, which can be replaced in terminal by writing: + // + // *drawCallback = func() { pi.Print("UPDATED", 10, 10, 7) } + drawCallback() + + // you can also replace entire pi.Draw or pi.Update callbacks by writing in terminal: + // + // pi.Draw = func() { pi.Cls(); pi.Print("DRAW UPDATED", 10, 10, 7) } + } + + // Run game with devtools (write pause or p in terminal to pause the game) + devtools.MustRun(ebitengine.Run) +} + +func drawCircle() { + pi.CircFill(64, 64, 11, 8) +} + +var drawCallback = func() {} + +type Struct struct { + Field string +} + +func funcAcceptingStruct(p Struct) { + fmt.Println("Struct received", p) +} diff --git a/examples/scripting/sprite-sheet.png b/examples/scripting/sprite-sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..946df326dc6f85621723f10e64c36c93934838d2 GIT binary patch literal 268 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&F%}28J29*~C-V}>F$nMpaRt({ z+QD_I!3^zztAotq!~G8(KK}pXi~kJ1{}(g-f1}Ru#7uMT|K_qe|I1hWKeOuHVyhaU zo-R)p$B>FSZ!ewZYcddEz0elA^w|IZF}dxWb`cSgJ1o^_3UF-S**fi0`PO4GyN>_e zb?o!CM^&=lzE4+uX7(lL?MI_S%Ja2-#80r^<33U&)*<=x)2#Vf0o!$RXKfcx+r;qsgkYlGth2iQ1g=S#2mJ}UmE``0||S&`njxgN@xNA D%KdHF literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod index 086ae4d..3bd7df1 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.20 require ( github.com/hajimehoshi/ebiten/v2 v2.5.6 + github.com/peterh/liner v1.2.2 github.com/stretchr/testify v1.8.4 + github.com/traefik/yaegi v0.15.1 ) require ( @@ -13,6 +15,7 @@ require ( github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect github.com/hajimehoshi/oto/v2 v2.4.1 // indirect github.com/jezek/xgb v1.1.0 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect golang.org/x/image v0.6.0 // indirect @@ -21,3 +24,5 @@ require ( golang.org/x/sys v0.7.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/peterh/liner => github.com/elgopher/liner v0.0.0-20230812143208-5760098a2c15 diff --git a/go.sum b/go.sum index b07958c..fde63a1 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.4.0 h1:RQVuMIxQPQ5iCGEJvjQ17YOK+1tMKjVau2FUMvXH4HE= github.com/ebitengine/purego v0.4.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/elgopher/liner v0.0.0-20230812143208-5760098a2c15 h1:u7QQZo3PMYrPKnPqGyA1eTxUHNdCIqW4biw8TNYUry0= +github.com/elgopher/liner v0.0.0-20230812143208-5760098a2c15/go.mod h1:/hQBcricMwKU2ip2HsFnf1TpjDXioOLCsPE0SxcRd8w= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/hajimehoshi/ebiten/v2 v2.5.6 h1:42Z8RUSE1e/CXl85mlbQs0OSM04st0Hhhc4DbAPpiz8= @@ -11,10 +13,14 @@ github.com/hajimehoshi/oto/v2 v2.4.1 h1:iTfZSulqdmQ5Hh4tVyVzNnK3aA4SgjbDapSM0YH3 github.com/hajimehoshi/oto/v2 v2.4.1/go.mod h1:guyF8uIgSrchrKewS1E6Xyx7joUbKOi4g9W7vpcYBSc= github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/traefik/yaegi v0.15.1 h1:YA5SbaL6HZA0Exh9T/oArRHqGN2HQ+zgmCY7dkoTXu4= +github.com/traefik/yaegi v0.15.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/image/image.go b/image/image.go index c0ea048..5be9ad6 100644 --- a/image/image.go +++ b/image/image.go @@ -6,6 +6,12 @@ // Package is used internally by Pi, but it can also be used when writing unit tests. package image +import ( + "fmt" + + "github.com/elgopher/pi/internal/sfmt" +) + // Image contains information about decoded image. type Image struct { Width, Height int @@ -18,6 +24,11 @@ type Image struct { Pixels []byte } +func (i Image) String() string { + return fmt.Sprintf("{width:%d, height:%d, palette: %+v, pixels:%s}", + i.Width, i.Height, sfmt.FormatBigSlice(i.Palette[:], 32), sfmt.FormatBigSlice(i.Pixels, 1000)) +} + // RGB represents color type RGB struct{ R, G, B byte } diff --git a/image/image_test.go b/image/image_test.go index 20f92fa..e3835bc 100644 --- a/image/image_test.go +++ b/image/image_test.go @@ -24,3 +24,25 @@ func TestRGB_String(t *testing.T) { assert.Equal(t, expected, rgb.String()) } } + +func TestImage_String(t *testing.T) { + t.Run("should convert small image to string", func(t *testing.T) { + img := image.Image{ + Width: 2, Height: 1, + Palette: [256]image.RGB{{1, 1, 1}, {2, 2, 2}}, + Pixels: make([]byte, 2), + } + + actual := img.String() + expected := "{width:2, height:1, palette: (256)[#010101 #020202 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 #000000 ...], pixels:[0 0]}" + assert.Equal(t, expected, actual) + }) + + t.Run("should convert big image to string", func(t *testing.T) { + img := image.Image{ + Pixels: make([]byte, 100*100), // 10K bytes + } + actual := img.String() + assert.True(t, len(actual) < 2500) + }) +} diff --git a/internal/sfmt/sfmt.go b/internal/sfmt/sfmt.go new file mode 100644 index 0000000..3021b1a --- /dev/null +++ b/internal/sfmt/sfmt.go @@ -0,0 +1,27 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +package sfmt + +import ( + "fmt" + "strings" +) + +func FormatBigSlice[T any](s []T, maxSize int) string { + var out string + l := len(s) + if l > maxSize { + var b strings.Builder + for i := 0; i < maxSize; i++ { + b.WriteString(fmt.Sprintf("%v", s[i])) + b.WriteString(" ") + } + + out = fmt.Sprintf("(%d)[%s...]", l, b.String()) + } else { + out = fmt.Sprintf("%v", s) + } + + return out +} diff --git a/internal/sfmt/sfmt_test.go b/internal/sfmt/sfmt_test.go new file mode 100644 index 0000000..2cf65a5 --- /dev/null +++ b/internal/sfmt/sfmt_test.go @@ -0,0 +1,55 @@ +// (c) 2023 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +package sfmt_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elgopher/pi/internal/sfmt" +) + +func TestFormatBigSlice(t *testing.T) { + const sliceLen = 10 + slice := make([]int, sliceLen) + for i := range slice { + slice[i] = i + } + + tests := []struct { + maxSize int + expected string + }{ + { + maxSize: -1, + expected: "(10)[...]", + }, + { + maxSize: 0, + expected: "(10)[...]", + }, + { + maxSize: 1, + expected: "(10)[0 ...]", + }, + { + maxSize: sliceLen, + expected: "[0 1 2 3 4 5 6 7 8 9]", + }, + { + maxSize: sliceLen + 1, + expected: "[0 1 2 3 4 5 6 7 8 9]", + }, + } + for _, testCase := range tests { + testName := fmt.Sprintf("%d", testCase.maxSize) + t.Run(testName, func(t *testing.T) { + // when + s := sfmt.FormatBigSlice(slice, testCase.maxSize) + assert.Equal(t, testCase.expected, s) + }) + } +} diff --git a/pixmap.go b/pixmap.go index 661da32..50909ad 100644 --- a/pixmap.go +++ b/pixmap.go @@ -3,6 +3,12 @@ package pi +import ( + "fmt" + + "github.com/elgopher/pi/internal/sfmt" +) + // PixMap is a generic data structure for manipulating any kind of pixel data - screen, sprite-sheet etc. // PixMap uses a single byte (8 bits) for storing single color/pixel. This means that max 256 colors // can be used. PixMap can also be used for maps which not necessary contain pixel colors, such as world map @@ -21,6 +27,11 @@ type PixMap struct { wholeLinePix []byte } +func (p PixMap) String() string { + return fmt.Sprintf("{width:%d, height:%d, clip:%+v, pix:%s}", + p.width, p.height, p.clip, sfmt.FormatBigSlice(p.pix, 1024)) +} + // NewPixMap creates new instance of PixMap with specified size. // Width and height cannot be negative. func NewPixMap(width, height int) PixMap { diff --git a/pixmap_test.go b/pixmap_test.go index 226d210..1ca9bf2 100644 --- a/pixmap_test.go +++ b/pixmap_test.go @@ -532,6 +532,20 @@ func TestPixMap_Merge(t *testing.T) { }) } +func TestPixMap_String(t *testing.T) { + t.Run("should convert small pixmap to string", func(t *testing.T) { + pixMap := pi.NewPixMap(1, 2) + actual := pixMap.String() + assert.Equal(t, "{width:1, height:2, clip:{X:0 Y:0 W:1 H:2}, pix:[0 0]}", actual) + }) + + t.Run("should convert big pixmap to string", func(t *testing.T) { + pixMap := pi.NewPixMap(100, 100) // 10K bytes + actual := pixMap.String() + assert.True(t, len(actual) < 2500) + }) +} + type pointerResult struct { pointer pi.Pointer ok bool diff --git a/print.go b/print.go index f2afb2f..c19bd9b 100644 --- a/print.go +++ b/print.go @@ -10,6 +10,7 @@ import ( "io/fs" "github.com/elgopher/pi/font" + "github.com/elgopher/pi/internal/sfmt" ) const fontDataSize = 8 * 256 @@ -61,6 +62,11 @@ type Font struct { Height int } +func (f Font) String() string { + return fmt.Sprintf("{width: %d, specialWidth: %d, height: %d, data: %s}", + f.Width, f.SpecialWidth, f.Height, sfmt.FormatBigSlice(f.Data, 512)) +} + // Print prints text on the screen at given coordinates. It takes into account // clipping region and camera position. // diff --git a/print_test.go b/print_test.go index 46ee5b6..e3b1de0 100644 --- a/print_test.go +++ b/print_test.go @@ -5,6 +5,7 @@ package pi_test import ( _ "embed" + "fmt" "strings" "testing" @@ -168,3 +169,28 @@ func TestPrint(t *testing.T) { } }) } + +func TestFont_String(t *testing.T) { + t.Run("should convert font with small data to string", func(t *testing.T) { + font := pi.Font{ + Data: make([]byte, 2), // invalid data, but still possible to initialize by hand + Width: 1, + SpecialWidth: 2, + Height: 3, + } + actual := font.String() + assert.Equal(t, "{width: 1, specialWidth: 2, height: 3, data: [0 0]}", actual) + }) + + t.Run("should convert font to string", func(t *testing.T) { + font := pi.Font{ + Data: make([]byte, 2048), + Width: 1, + SpecialWidth: 2, + Height: 3, + } + actual := font.String() + fmt.Println(len(actual)) + assert.True(t, len(actual) < 1500) + }) +}