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 0000000..946df32 Binary files /dev/null and b/examples/scripting/sprite-sheet.png differ 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) + }) +}